Convert 8-bit PNG alpha channels to /SMasks in PDF
This commit is contained in:
parent
b4c8aa1a5f
commit
1821befff4
1 changed files with 84 additions and 39 deletions
|
@ -742,6 +742,7 @@ class pdfdoc(object):
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
imgformat,
|
imgformat,
|
||||||
imgdata,
|
imgdata,
|
||||||
|
smaskdata,
|
||||||
imgwidthpdf,
|
imgwidthpdf,
|
||||||
imgheightpdf,
|
imgheightpdf,
|
||||||
imgxpdf,
|
imgxpdf,
|
||||||
|
@ -759,6 +760,8 @@ class pdfdoc(object):
|
||||||
artborder=None,
|
artborder=None,
|
||||||
iccp=None,
|
iccp=None,
|
||||||
):
|
):
|
||||||
|
assert color != Colorspace.RGBA or (imgformat == ImageFormat.PNG and smaskdata is not None)
|
||||||
|
|
||||||
if self.engine == Engine.pikepdf:
|
if self.engine == Engine.pikepdf:
|
||||||
PdfArray = pikepdf.Array
|
PdfArray = pikepdf.Array
|
||||||
PdfDict = pikepdf.Dictionary
|
PdfDict = pikepdf.Dictionary
|
||||||
|
@ -779,7 +782,7 @@ class pdfdoc(object):
|
||||||
|
|
||||||
if color == Colorspace["1"] or color == Colorspace.L:
|
if color == Colorspace["1"] or color == Colorspace.L:
|
||||||
colorspace = PdfName.DeviceGray
|
colorspace = PdfName.DeviceGray
|
||||||
elif color == Colorspace.RGB:
|
elif color == Colorspace.RGB or color == Colorspace.RGBA:
|
||||||
colorspace = PdfName.DeviceRGB
|
colorspace = PdfName.DeviceRGB
|
||||||
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
|
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
|
||||||
colorspace = PdfName.DeviceCMYK
|
colorspace = PdfName.DeviceCMYK
|
||||||
|
@ -818,7 +821,7 @@ class pdfdoc(object):
|
||||||
iccpdict[PdfName.Alternate] = colorspace
|
iccpdict[PdfName.Alternate] = colorspace
|
||||||
if color == Colorspace["1"] or color == Colorspace.L:
|
if color == Colorspace["1"] or color == Colorspace.L:
|
||||||
iccpdict[PdfName.N] = 1
|
iccpdict[PdfName.N] = 1
|
||||||
elif color == Colorspace.RGB:
|
elif color == Colorspace.RGB or color == Colorspace.RGBA:
|
||||||
iccpdict[PdfName.N] = 3
|
iccpdict[PdfName.N] = 3
|
||||||
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
|
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
|
||||||
iccpdict[PdfName.N] = 4
|
iccpdict[PdfName.N] = 4
|
||||||
|
@ -867,6 +870,25 @@ class pdfdoc(object):
|
||||||
decodeparms[PdfName.Rows] = imgheightpx
|
decodeparms[PdfName.Rows] = imgheightpx
|
||||||
image[PdfName.DecodeParms] = [decodeparms]
|
image[PdfName.DecodeParms] = [decodeparms]
|
||||||
elif imgformat is ImageFormat.PNG:
|
elif imgformat is ImageFormat.PNG:
|
||||||
|
if color == Colorspace.RGBA:
|
||||||
|
if self.engine == Engine.pikepdf:
|
||||||
|
smask = self.writer.make_stream(smaskdata)
|
||||||
|
else:
|
||||||
|
smask = PdfDict(stream=convert_load(smaskdata))
|
||||||
|
smask[PdfName.Type] = PdfName.XObject
|
||||||
|
smask[PdfName.Subtype] = PdfName.Image
|
||||||
|
smask[PdfName.Filter] = PdfName.FlateDecode
|
||||||
|
smask[PdfName.Width] = imgwidthpx
|
||||||
|
smask[PdfName.Height] = imgheightpx
|
||||||
|
smask[PdfName.ColorSpace] = PdfName.DeviceGray
|
||||||
|
smask[PdfName.BitsPerComponent] = depth
|
||||||
|
|
||||||
|
image[PdfName.SMask] = smask
|
||||||
|
|
||||||
|
# /SMask requires PDF 1.4
|
||||||
|
if self.output_version < "1.4":
|
||||||
|
self.output_version = "1.4"
|
||||||
|
else:
|
||||||
decodeparms = PdfDict()
|
decodeparms = PdfDict()
|
||||||
decodeparms[PdfName.Predictor] = 15
|
decodeparms[PdfName.Predictor] = 15
|
||||||
if color in [Colorspace.P, Colorspace["1"], Colorspace.L]:
|
if color in [Colorspace.P, Colorspace["1"], Colorspace.L]:
|
||||||
|
@ -952,6 +974,8 @@ class pdfdoc(object):
|
||||||
if self.engine == Engine.internal:
|
if self.engine == Engine.internal:
|
||||||
self.writer.addobj(content)
|
self.writer.addobj(content)
|
||||||
self.writer.addobj(image)
|
self.writer.addobj(image)
|
||||||
|
if smask is not None:
|
||||||
|
self.writer.addobj(smask)
|
||||||
if iccp is not None:
|
if iccp is not None:
|
||||||
self.writer.addobj(iccpdict)
|
self.writer.addobj(iccpdict)
|
||||||
|
|
||||||
|
@ -1182,7 +1206,9 @@ def get_imgmetadata(
|
||||||
ndpi = (int(round(ndpi[0])), int(round(ndpi[1])))
|
ndpi = (int(round(ndpi[0])), int(round(ndpi[1])))
|
||||||
ics = imgdata.mode
|
ics = imgdata.mode
|
||||||
|
|
||||||
if ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info:
|
if imgformat == ImageFormat.PNG and ics == "RGBA":
|
||||||
|
logger.warning("Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.")
|
||||||
|
elif (ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info):
|
||||||
logger.warning("Image contains transparency which cannot be retained in PDF.")
|
logger.warning("Image contains transparency which cannot be retained in PDF.")
|
||||||
logger.warning("img2pdf will not perform a lossy operation.")
|
logger.warning("img2pdf will not perform a lossy operation.")
|
||||||
logger.warning("You can remove the alpha channel using imagemagick:")
|
logger.warning("You can remove the alpha channel using imagemagick:")
|
||||||
|
@ -1425,6 +1451,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
ndpi,
|
ndpi,
|
||||||
imgformat,
|
imgformat,
|
||||||
rawdata,
|
rawdata,
|
||||||
|
None,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
[],
|
[],
|
||||||
|
@ -1481,6 +1508,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
ndpi,
|
ndpi,
|
||||||
ImageFormat.JPEG,
|
ImageFormat.JPEG,
|
||||||
rawdata[offset : offset + mpent["Size"]],
|
rawdata[offset : offset + mpent["Size"]],
|
||||||
|
None,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
[],
|
[],
|
||||||
|
@ -1501,11 +1529,21 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
# or not. Thus, we retrieve that info manually by looking at byte 13 in the
|
# or not. Thus, we retrieve that info manually by looking at byte 13 in the
|
||||||
# IHDR chunk. We know where to find that in the file because the IHDR chunk
|
# IHDR chunk. We know where to find that in the file because the IHDR chunk
|
||||||
# must be the first chunk.
|
# must be the first chunk.
|
||||||
if imgformat == ImageFormat.PNG and rawdata[28] == 0:
|
if imgformat == ImageFormat.PNG:
|
||||||
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
|
||||||
imgdata, imgformat, default_dpi, colorspace, rawdata, rot
|
imgdata, imgformat, default_dpi, colorspace, rawdata, rot
|
||||||
)
|
)
|
||||||
pngidat, palette = parse_png(rawdata)
|
|
||||||
|
if color == Colorspace.RGBA or rawdata[28] == 0:
|
||||||
|
if color == Colorspace.RGBA:
|
||||||
|
r, g, b, a = imgdata.split()
|
||||||
|
pngdata = zlib.compress(Image.merge("RGB", (r, g, b)).tobytes())
|
||||||
|
smaskdata = zlib.compress(a.tobytes())
|
||||||
|
palette = None
|
||||||
|
else:
|
||||||
|
pngdata, palette = parse_png(rawdata)
|
||||||
|
smaskdata = None
|
||||||
|
|
||||||
# PIL does not provide the information about the original bits per
|
# PIL does not provide the information about the original bits per
|
||||||
# sample. Thus, we retrieve that info manually by looking at byte 9 in
|
# sample. Thus, we retrieve that info manually by looking at byte 9 in
|
||||||
# the IHDR chunk. We know where to find that in the file because the
|
# the IHDR chunk. We know where to find that in the file because the
|
||||||
|
@ -1520,7 +1558,8 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
color,
|
color,
|
||||||
ndpi,
|
ndpi,
|
||||||
imgformat,
|
imgformat,
|
||||||
pngidat,
|
pngdata,
|
||||||
|
smaskdata,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
palette,
|
palette,
|
||||||
|
@ -1613,6 +1652,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
ndpi,
|
ndpi,
|
||||||
ImageFormat.CCITTGroup4,
|
ImageFormat.CCITTGroup4,
|
||||||
rawdata,
|
rawdata,
|
||||||
|
None,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
[],
|
[],
|
||||||
|
@ -1642,6 +1682,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
ndpi,
|
ndpi,
|
||||||
ImageFormat.CCITTGroup4,
|
ImageFormat.CCITTGroup4,
|
||||||
ccittdata,
|
ccittdata,
|
||||||
|
None,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
[],
|
[],
|
||||||
|
@ -1680,6 +1721,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
ndpi,
|
ndpi,
|
||||||
imgformat,
|
imgformat,
|
||||||
imggz,
|
imggz,
|
||||||
|
None,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
[],
|
[],
|
||||||
|
@ -1711,6 +1753,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None):
|
||||||
ndpi,
|
ndpi,
|
||||||
ImageFormat.PNG,
|
ImageFormat.PNG,
|
||||||
pngidat,
|
pngidat,
|
||||||
|
None,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
palette,
|
palette,
|
||||||
|
@ -2116,6 +2159,7 @@ def convert(*images, **kwargs):
|
||||||
ndpi,
|
ndpi,
|
||||||
imgformat,
|
imgformat,
|
||||||
imgdata,
|
imgdata,
|
||||||
|
smaskdata,
|
||||||
imgwidthpx,
|
imgwidthpx,
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
palette,
|
palette,
|
||||||
|
@ -2169,6 +2213,7 @@ def convert(*images, **kwargs):
|
||||||
imgheightpx,
|
imgheightpx,
|
||||||
imgformat,
|
imgformat,
|
||||||
imgdata,
|
imgdata,
|
||||||
|
smaskdata,
|
||||||
imgwidthpdf,
|
imgwidthpdf,
|
||||||
imgheightpdf,
|
imgheightpdf,
|
||||||
imgxpdf,
|
imgxpdf,
|
||||||
|
|
Loading…
Reference in a new issue