I have a Surface BaseSurf
with an indexed colour palette, and I want to blit
another indexed-colour Surface to it (let's call it NewSurf
). BaseSurf's palette is different from NewSurf's, though, and pygame does not automatically add the missing colours to BaseSurf's palette.
I therefore need some way to append NewSurf's colours to BaseSurf's palette without mangling either (overwriting palette indices which are actually used by the Surface's pixels).
When I run this code:
screen = pygame.display.set_mode((222,222))
screen.fill((255,255,255))
BaseSurf = pygame.load.image("4samps.png").convert(8) # indexed with four colours; larger dimensions
NewSurf = pygame.load.image("another4samps.png").convert(8) # indexed with four more colours
screen.blit(BaseSurf, (10,10))
screen.blit(NewSurf, (160,43))
BaseSurf.blit(NewSurf, (33,33))
screen.blit(BaseSurf, (50,122))
...this is what I see...
--EDIT: Notice that the combined graphic approximates the colours in NewSurf to whatever BaseSurf is using, even if the sequence is off. The bottom right blob actually takes a null value (0,0,0,255) to be the closest matching colour!
Obviously, NewSurf (on the right) has had its indices transferred to BaseSurf, which is not incorrect exactly, but is also not what I want. How can I blit NewSurf to BaseSurf, preserving its exact colour data in the revised image (but retaining an indexed colour image)?
I actually figured out an answer in the course of asking it.
Using
pygame.Surface.set_palette_at()
, I was able to extract the palette data from NewSurf (usingNewSurf.get_palette(..)
and an iterator to clear out that palette's blanks) and paste it onto the 'end' (that is, after the last non-blank RGBA index value) of BaseSurf's palette usingBaseSurf.set_palette_at(first_null_RGBA_index, new_RGBA_value)
.You can find
first_null_RGBA_value
in a number of ways; I've usednext()
to find the first null value (defined byblankRGBA
, which is usually (0,0,0,255) in my experience) in the 'train' of null (unused) values that follows the in-use palette inget_palette()
. You could also useget_palette().index(blankRGBA)
to get the index of the first blank value (which is super-risky if you actually have blank-value pixels in the graphic!). I'll describe the methods and their issues (as I see them) in a moment.As it happens, I do not seem to have to reindex NewSurf that it's indices align to their new locations in BaseSurf's palette. I do not know what consequence this will have if you remap a duplicated RGB value! I imagine that pygame approximates RGB values from the blit'd image to match the closest ones in the receiving one-- an exact match, if you purposefully paste the whole palette from NewSurf into BaseSurf beforehand.
Here's how I did that.
I could probably have also cleaned the blank RGBA values from NewSurf's palette with something like
copypal = list(RGBA for RGBA in NewSurf.get_palette() if RGBA != blankRGBA)
instead of thewhile True; ... copypal.pop()
loop. I also could have found the first blank in BaseSurf's palette usingdestpal.index(blankRGBA)
instead of the more complicatednext()
instruction. The reason why I did neither of these is because there's a chance that the palettes used theblankRGBA
value for at least one pixel in the images, and that these pixels were intended to be blank-- it's not so unlikely that (0,0,0,255) would be used somewhere in the image.Presumably, if that were the case, the RGBA indices would be at the start of the palette instead of at the end. If they were anywhere but the last index, they'll be safe. Otherwise, there could be a problem.
For situations where you can control the image data very closely, these condensed versions may also work.
Note however that they are very probably less Pythonic as they're harder to read, and that they are more vulnerable to certain problems (accidentally replacing a null-looking index value that's actually in use) and unhandled exceptions (running out of palette space in BaseSurf).
Use at your own risk!
This is a little slower, since
next()
will iterate through the palette for each value in copypal, but hey, it's fewer lines, and that's agoodthing.You could even just use this horrible and extremely unPythonic singleton:
Note that I do not recommend either short version. I've only included them for academic consideration.