@@ -1317,6 +1317,57 @@ def _save_all(im: Image.Image, fp: IO[bytes], filename: str | bytes) -> None:
13171317 _save (im , fp , filename , save_all = True )
13181318
13191319
1320+ def _pa_to_p (im : Image .Image ) -> Image .Image :
1321+ """
1322+ Try to fold a `PA` image's alpha band into an RGBA palette.
1323+
1324+ :param im: A `PA` mode image.
1325+ :return: A `P` mode image with an RGBA palette.
1326+ """
1327+
1328+ # PNG has no palette+alpha mode like Pillow's PA mode.
1329+ # However, palette entries may carry alpha via the tRNS chunk:
1330+ # > For color type 3 (indexed-color), the tRNS chunk contains
1331+ # > a series of one-byte alpha values,
1332+ # > corresponding to entries in the PLTE chunk.
1333+ # > Each entry indicates that pixels of the corresponding
1334+ # palette index shall be treated as having the specified alpha value.
1335+ # - https://www.w3.org/TR/png-3/#11tRNS
1336+ # This means that in some lucky cases of PA images,
1337+ # if every palette index is used with exactly a single alpha value,
1338+ # we can fold the alpha band into an RGBA palette, and save the image
1339+ # as P, so the RGBA palette gets split back to PLTE + tRNS.
1340+
1341+ assert im .mode == "PA"
1342+ indexes , alpha = im .split ()
1343+ index_alpha : dict [int , int ] = {}
1344+ for index , alpha_value in set (zip (indexes .tobytes (), alpha .tobytes ())):
1345+ if index_alpha .setdefault (index , alpha_value ) != alpha_value :
1346+ msg = (
1347+ "cannot write mode PA as PNG: "
1348+ "a palette entry is used with multiple alpha values"
1349+ )
1350+ raise OSError (msg )
1351+
1352+ # If the palette happened to be RGBA, we deliberately drop its alpha here,
1353+ # because the alpha we care about will be in the `alpha` band.
1354+ palette = im .getpalette ("RGB" ) or []
1355+ colors = max (len (palette ) // 3 , max (index_alpha , default = - 1 ) + 1 , 1 )
1356+ rgba_palette = bytearray ()
1357+ for i in range (colors ):
1358+ # The raster could (unfortunately) be using palette indexes
1359+ # that don't have entries, hence the fallback to black.
1360+ rgba_palette += bytes (palette [i * 3 : i * 3 + 3 ] or (0 , 0 , 0 ))
1361+ # Palette entries not referenced by any pixel stay opaque.
1362+ rgba_palette .append (index_alpha .get (i , 255 ))
1363+
1364+ indexes .putpalette (rgba_palette , "RGBA" )
1365+ indexes .load () # sync the palette to the core image for the tRNS check
1366+ indexes .encoderinfo = im .encoderinfo
1367+ indexes .info = {k : v for k , v in im .info .items () if k != "transparency" }
1368+ return indexes
1369+
1370+
13201371def _save (
13211372 im : Image .Image ,
13221373 fp : IO [bytes ],
@@ -1326,6 +1377,9 @@ def _save(
13261377) -> None :
13271378 # save an image to disk (called by the save method)
13281379
1380+ if im .mode == "PA" and not save_all :
1381+ im = _pa_to_p (im )
1382+
13291383 if save_all :
13301384 default_image = im .encoderinfo .get (
13311385 "default_image" , im .info .get ("default_image" )
0 commit comments