From bf51768fb4e4fc092027ec1312d1d4db6cad6d50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 12 Jun 2021 22:03:56 +0200 Subject: [PATCH 1/7] Convert 8-bit PNG alpha channels to /SMasks in PDF --- src/img2pdf.py | 123 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 84 insertions(+), 39 deletions(-) diff --git a/src/img2pdf.py b/src/img2pdf.py index 8d5f2cf..27a1943 100755 --- a/src/img2pdf.py +++ b/src/img2pdf.py @@ -742,6 +742,7 @@ class pdfdoc(object): imgheightpx, imgformat, imgdata, + smaskdata, imgwidthpdf, imgheightpdf, imgxpdf, @@ -759,6 +760,8 @@ class pdfdoc(object): artborder=None, iccp=None, ): + assert color != Colorspace.RGBA or (imgformat == ImageFormat.PNG and smaskdata is not None) + if self.engine == Engine.pikepdf: PdfArray = pikepdf.Array PdfDict = pikepdf.Dictionary @@ -779,7 +782,7 @@ class pdfdoc(object): if color == Colorspace["1"] or color == Colorspace.L: colorspace = PdfName.DeviceGray - elif color == Colorspace.RGB: + elif color == Colorspace.RGB or color == Colorspace.RGBA: colorspace = PdfName.DeviceRGB elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]: colorspace = PdfName.DeviceCMYK @@ -818,7 +821,7 @@ class pdfdoc(object): iccpdict[PdfName.Alternate] = colorspace if color == Colorspace["1"] or color == Colorspace.L: iccpdict[PdfName.N] = 1 - elif color == Colorspace.RGB: + elif color == Colorspace.RGB or color == Colorspace.RGBA: iccpdict[PdfName.N] = 3 elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]: iccpdict[PdfName.N] = 4 @@ -869,15 +872,34 @@ class pdfdoc(object): decodeparms[PdfName.Rows] = imgheightpx image[PdfName.DecodeParms] = [decodeparms] elif imgformat is ImageFormat.PNG: - decodeparms = PdfDict() - decodeparms[PdfName.Predictor] = 15 - if color in [Colorspace.P, Colorspace["1"], Colorspace.L]: - decodeparms[PdfName.Colors] = 1 + 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[PdfName.Colors] = 3 - decodeparms[PdfName.Columns] = imgwidthpx - decodeparms[PdfName.BitsPerComponent] = depth - image[PdfName.DecodeParms] = decodeparms + decodeparms = PdfDict() + decodeparms[PdfName.Predictor] = 15 + if color in [Colorspace.P, Colorspace["1"], Colorspace.L]: + decodeparms[PdfName.Colors] = 1 + else: + decodeparms[PdfName.Colors] = 3 + decodeparms[PdfName.Columns] = imgwidthpx + decodeparms[PdfName.BitsPerComponent] = depth + image[PdfName.DecodeParms] = decodeparms text = ( "q\n%0.4f 0 0 %0.4f %0.4f %0.4f cm\n/Im0 Do\nQ" @@ -954,6 +976,8 @@ class pdfdoc(object): if self.engine == Engine.internal: self.writer.addobj(content) self.writer.addobj(image) + if smask is not None: + self.writer.addobj(smask) if iccp is not None: self.writer.addobj(iccpdict) @@ -1183,8 +1207,10 @@ def get_imgmetadata( # Search online for the 72.009 dpi problem for more info. ndpi = (int(round(ndpi[0])), int(round(ndpi[1]))) 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("img2pdf will not perform a lossy operation.") logger.warning("You can remove the alpha channel using imagemagick:") @@ -1427,6 +1453,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, imgformat, rawdata, + None, imgwidthpx, imgheightpx, [], @@ -1483,6 +1510,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, ImageFormat.JPEG, rawdata[offset : offset + mpent["Size"]], + None, imgwidthpx, imgheightpx, [], @@ -1495,7 +1523,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): img_page_count += 1 cleanup() return result - + # We can directly embed the IDAT chunk of PNG images if the PNG is not # interlaced # @@ -1503,35 +1531,46 @@ 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 # IHDR chunk. We know where to find that in the file because the IHDR 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( imgdata, imgformat, default_dpi, colorspace, rawdata, rot ) - pngidat, palette = parse_png(rawdata) - # PIL does not provide the information about the original bits per - # 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 - # IHDR chunk must be the first chunk - depth = rawdata[24] - if depth not in [1, 2, 4, 8, 16]: - raise ValueError("invalid bit depth: %d" % depth) - logger.debug("read_images() embeds a PNG") - cleanup() - return [ - ( - color, - ndpi, - imgformat, - pngidat, - imgwidthpx, - imgheightpx, - palette, - False, - depth, - rotation, - iccp, - ) - ] + + 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 + # 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 + # IHDR chunk must be the first chunk + depth = rawdata[24] + if depth not in [1, 2, 4, 8, 16]: + raise ValueError("invalid bit depth: %d" % depth) + logger.debug("read_images() embeds a PNG") + cleanup() + return [ + ( + color, + ndpi, + imgformat, + pngdata, + smaskdata, + imgwidthpx, + imgheightpx, + palette, + False, + depth, + rotation, + iccp, + ) + ] # If our input is not JPEG or PNG, then we might have a format that # supports multiple frames (like TIFF or GIF), so we need a loop to @@ -1615,6 +1654,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, ImageFormat.CCITTGroup4, rawdata, + None, imgwidthpx, imgheightpx, [], @@ -1644,6 +1684,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, ImageFormat.CCITTGroup4, ccittdata, + None, imgwidthpx, imgheightpx, [], @@ -1682,6 +1723,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, imgformat, imggz, + None, imgwidthpx, imgheightpx, [], @@ -1713,6 +1755,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, ImageFormat.PNG, pngidat, + None, imgwidthpx, imgheightpx, palette, @@ -2118,6 +2161,7 @@ def convert(*images, **kwargs): ndpi, imgformat, imgdata, + smaskdata, imgwidthpx, imgheightpx, palette, @@ -2171,6 +2215,7 @@ def convert(*images, **kwargs): imgheightpx, imgformat, imgdata, + smaskdata, imgwidthpdf, imgheightpdf, imgxpdf, -- 2.39.5 From 219dbd285647a316fc58718d5672b1dea47f661e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 19 Jun 2021 01:08:18 +0200 Subject: [PATCH 2/7] Added transparency support for GIFs, palette-based PNGs and grayscale PNGs --- src/img2pdf.py | 124 ++++++++++++++++++++++++++++++------------------- 1 file changed, 76 insertions(+), 48 deletions(-) diff --git a/src/img2pdf.py b/src/img2pdf.py index 27a1943..ee7797a 100755 --- a/src/img2pdf.py +++ b/src/img2pdf.py @@ -85,9 +85,9 @@ FitMode = Enum("FitMode", "into fill exact shrink enlarge") PageOrientation = Enum("PageOrientation", "portrait landscape") -Colorspace = Enum("Colorspace", "RGB L 1 CMYK CMYK;I RGBA P other") +Colorspace = Enum("Colorspace", "RGB RGBA L LA 1 CMYK CMYK;I P other") -ImageFormat = Enum("ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG TIFF MPO other") +ImageFormat = Enum("ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG GIF TIFF MPO other") PageMode = Enum("PageMode", "none outlines thumbs") @@ -760,7 +760,7 @@ class pdfdoc(object): artborder=None, iccp=None, ): - assert color != Colorspace.RGBA or (imgformat == ImageFormat.PNG and smaskdata is not None) + assert (color != Colorspace.RGBA and color != Colorspace.LA) or (imgformat == ImageFormat.PNG and smaskdata is not None) if self.engine == Engine.pikepdf: PdfArray = pikepdf.Array @@ -780,7 +780,7 @@ class pdfdoc(object): TrueObject = True if self.engine == Engine.pikepdf else PdfObject("true") FalseObject = False if self.engine == Engine.pikepdf else PdfObject("false") - if color == Colorspace["1"] or color == Colorspace.L: + if color == Colorspace["1"] or color == Colorspace.L or color == Colorspace.LA: colorspace = PdfName.DeviceGray elif color == Colorspace.RGB or color == Colorspace.RGBA: colorspace = PdfName.DeviceRGB @@ -819,7 +819,7 @@ class pdfdoc(object): else: iccpdict = PdfDict(stream=convert_load(iccp)) iccpdict[PdfName.Alternate] = colorspace - if color == Colorspace["1"] or color == Colorspace.L: + if color == Colorspace["1"] or color == Colorspace.L or color == Colorspace.LA: iccpdict[PdfName.N] = 1 elif color == Colorspace.RGB or color == Colorspace.RGBA: iccpdict[PdfName.N] = 3 @@ -872,7 +872,7 @@ class pdfdoc(object): decodeparms[PdfName.Rows] = imgheightpx image[PdfName.DecodeParms] = [decodeparms] elif imgformat is ImageFormat.PNG: - if color == Colorspace.RGBA: + if smaskdata is not None: if self.engine == Engine.pikepdf: smask = self.writer.make_stream(smaskdata) else: @@ -890,16 +890,16 @@ class pdfdoc(object): # /SMask requires PDF 1.4 if self.output_version < "1.4": self.output_version = "1.4" + + decodeparms = PdfDict() + decodeparms[PdfName.Predictor] = 15 + if color in [Colorspace.P, Colorspace["1"], Colorspace.L, Colorspace.LA]: + decodeparms[PdfName.Colors] = 1 else: - decodeparms = PdfDict() - decodeparms[PdfName.Predictor] = 15 - if color in [Colorspace.P, Colorspace["1"], Colorspace.L]: - decodeparms[PdfName.Colors] = 1 - else: - decodeparms[PdfName.Colors] = 3 - decodeparms[PdfName.Columns] = imgwidthpx - decodeparms[PdfName.BitsPerComponent] = depth - image[PdfName.DecodeParms] = decodeparms + decodeparms[PdfName.Colors] = 3 + decodeparms[PdfName.Columns] = imgwidthpx + decodeparms[PdfName.BitsPerComponent] = depth + image[PdfName.DecodeParms] = decodeparms text = ( "q\n%0.4f 0 0 %0.4f %0.4f %0.4f cm\n/Im0 Do\nQ" @@ -1208,8 +1208,19 @@ def get_imgmetadata( ndpi = (int(round(ndpi[0])), int(round(ndpi[1]))) ics = imgdata.mode - 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.") + # GIF and PNG files with transparency are supported + if ( + (imgformat == ImageFormat.PNG or imgformat == ImageFormat.GIF) + and (ics in ["RGBA", "LA"] or "transparency" in imgdata.info) + ): + # Must check the IHDR chunk for the bit depth, because PIL would lossily + # convert 16-bit RGBA/LA images to 8-bit. + if imgformat == ImageFormat.PNG and rawdata != None: + depth = rawdata[24] + if depth > 8: + logger.warning("Image with transparency and a bit depth of %d." % depth) + logger.warning("This is unsupported due to PIL limitations.") + raise AlphaChannelError("Refusing to work with multiple >8bit channels.") elif (ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info): logger.warning("Image contains transparency which cannot be retained in PDF.") logger.warning("img2pdf will not perform a lossy operation.") @@ -1523,7 +1534,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): img_page_count += 1 cleanup() return result - + # We can directly embed the IDAT chunk of PNG images if the PNG is not # interlaced # @@ -1531,21 +1542,12 @@ 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 # IHDR chunk. We know where to find that in the file because the IHDR chunk # must be the first chunk. - if imgformat == ImageFormat.PNG: + if imgformat == ImageFormat.PNG and rawdata[28] == 0: color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata( imgdata, imgformat, default_dpi, colorspace, rawdata, rot ) - - 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 - + if color != Colorspace.RGBA and color != Colorspace.LA and "transparency" not in imgdata.info: + pngidat, palette = parse_png(rawdata) # PIL does not provide the information about the original bits per # 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 @@ -1560,8 +1562,8 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): color, ndpi, imgformat, - pngdata, - smaskdata, + pngidat, + None, imgwidthpx, imgheightpx, palette, @@ -1703,7 +1705,9 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): color = Colorspace.L elif color in [ Colorspace.RGB, + Colorspace.RGBA, Colorspace.L, + Colorspace.LA, Colorspace.CMYK, Colorspace["CMYK;I"], Colorspace.P, @@ -1734,28 +1738,35 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ) ) else: - # cheapo version to retrieve a PNG encoding of the payload is to - # just save it with PIL. In the future this could be replaced by - # dedicated function applying the Paeth PNG filter to the raw pixel - pngbuffer = BytesIO() - newimg.save(pngbuffer, format="png") - pngidat, palette = parse_png(pngbuffer.getvalue()) - # PIL does not provide the information about the original bits per - # 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 - # IHDR chunk must be the first chunk - pngbuffer.seek(24) - depth = ord(pngbuffer.read(1)) - if depth not in [1, 2, 4, 8, 16]: - raise ValueError("invalid bit depth: %d" % depth) + if color == Colorspace.RGBA or color == Colorspace.LA or "transparency" in newimg.info: + if color == Colorspace.RGBA: + newcolor = color + r, g, b, a = newimg.split() + newimg = Image.merge("RGB", (r, g, b)) + elif color == Colorspace.LA: + newcolor = color + l, a = newimg.split() + newimg = l + else: + newcolor = Colorspace.RGBA + r, g, b, a = newimg.convert(mode="RGBA").split() + newimg = Image.merge("RGB", (r, g, b)) + + smaskdata = zlib.compress(a.tobytes()) + logger.warning("Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.") + else: + newcolor = color + smaskdata = None + + pngidat, palette, depth = to_png_data(newimg) logger.debug("read_images() encoded an image as PNG") result.append( ( - color, + newcolor, ndpi, ImageFormat.PNG, pngidat, - None, + smaskdata, imgwidthpx, imgheightpx, palette, @@ -1769,6 +1780,23 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): cleanup() return result +def to_png_data(img): + # cheapo version to retrieve a PNG encoding of the payload is to + # just save it with PIL. In the future this could be replaced by + # dedicated function applying the Paeth PNG filter to the raw pixel + pngbuffer = BytesIO() + img.save(pngbuffer, format="png") + + pngidat, palette = parse_png(pngbuffer.getvalue()) + # PIL does not provide the information about the original bits per + # 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 + # IHDR chunk must be the first chunk + pngbuffer.seek(24) + depth = ord(pngbuffer.read(1)) + if depth not in [1, 2, 4, 8, 16]: + raise ValueError("invalid bit depth: %d" % depth) + return pngidat, palette, depth # converts a length in pixels to a length in PDF units (1/72 of an inch) def px_to_pt(length, dpi): -- 2.39.5 From e6613d3244087313fda042eef3e6e2174c7710b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 19 Jun 2021 01:19:06 +0200 Subject: [PATCH 3/7] Use PNG predictor for /SMask too --- src/img2pdf.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/img2pdf.py b/src/img2pdf.py index ee7797a..e57dbaa 100755 --- a/src/img2pdf.py +++ b/src/img2pdf.py @@ -885,6 +885,13 @@ class pdfdoc(object): smask[PdfName.ColorSpace] = PdfName.DeviceGray smask[PdfName.BitsPerComponent] = depth + decodeparms = PdfDict() + decodeparms[PdfName.Predictor] = 15 + decodeparms[PdfName.Colors] = 1 + decodeparms[PdfName.Columns] = imgwidthpx + decodeparms[PdfName.BitsPerComponent] = depth + smask[PdfName.DecodeParms] = decodeparms + image[PdfName.SMask] = smask # /SMask requires PDF 1.4 @@ -1752,11 +1759,11 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): r, g, b, a = newimg.convert(mode="RGBA").split() newimg = Image.merge("RGB", (r, g, b)) - smaskdata = zlib.compress(a.tobytes()) + smaskidat, _, _ = to_png_data(a) logger.warning("Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.") else: newcolor = color - smaskdata = None + smaskidat = None pngidat, palette, depth = to_png_data(newimg) logger.debug("read_images() encoded an image as PNG") @@ -1766,7 +1773,7 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ndpi, ImageFormat.PNG, pngidat, - smaskdata, + smaskidat, imgwidthpx, imgheightpx, palette, -- 2.39.5 From ff03d9c1cd8e4ebb6ce03cab379376a8a4acb16a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 19 Jun 2021 01:28:57 +0200 Subject: [PATCH 4/7] Formatting --- src/img2pdf.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/img2pdf.py b/src/img2pdf.py index e57dbaa..00be692 100755 --- a/src/img2pdf.py +++ b/src/img2pdf.py @@ -760,7 +760,10 @@ class pdfdoc(object): artborder=None, iccp=None, ): - assert (color != Colorspace.RGBA and color != Colorspace.LA) or (imgformat == ImageFormat.PNG and smaskdata is not None) + assert ( + (color != Colorspace.RGBA and color != Colorspace.LA) + or (imgformat == ImageFormat.PNG and smaskdata is not None) + ) if self.engine == Engine.pikepdf: PdfArray = pikepdf.Array @@ -1222,7 +1225,7 @@ def get_imgmetadata( ): # Must check the IHDR chunk for the bit depth, because PIL would lossily # convert 16-bit RGBA/LA images to 8-bit. - if imgformat == ImageFormat.PNG and rawdata != None: + if imgformat == ImageFormat.PNG and rawdata is not None: depth = rawdata[24] if depth > 8: logger.warning("Image with transparency and a bit depth of %d." % depth) @@ -1553,7 +1556,11 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata( imgdata, imgformat, default_dpi, colorspace, rawdata, rot ) - if color != Colorspace.RGBA and color != Colorspace.LA and "transparency" not in imgdata.info: + if ( + color != Colorspace.RGBA + and color != Colorspace.LA + and "transparency" not in imgdata.info + ): pngidat, palette = parse_png(rawdata) # PIL does not provide the information about the original bits per # sample. Thus, we retrieve that info manually by looking at byte 9 in @@ -1745,7 +1752,11 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): ) ) else: - if color == Colorspace.RGBA or color == Colorspace.LA or "transparency" in newimg.info: + if ( + color == Colorspace.RGBA + or color == Colorspace.LA + or "transparency" in newimg.info + ): if color == Colorspace.RGBA: newcolor = color r, g, b, a = newimg.split() @@ -1760,7 +1771,10 @@ def read_images(rawdata, colorspace, first_frame_only=False, rot=None): newimg = Image.merge("RGB", (r, g, b)) smaskidat, _, _ = to_png_data(a) - logger.warning("Image contains an alpha channel which will be stored as a separate soft mask (/SMask) image in PDF.") + logger.warning( + "Image contains an alpha channel which will be stored " + "as a separate soft mask (/SMask) image in PDF." + ) else: newcolor = color smaskidat = None -- 2.39.5 From cfbb40b0f635c44becce49260bc1680d6b45a67e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 19 Jun 2021 18:49:21 +0200 Subject: [PATCH 5/7] Always initialize `smask` --- src/img2pdf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/img2pdf.py b/src/img2pdf.py index 00be692..1dc01c4 100755 --- a/src/img2pdf.py +++ b/src/img2pdf.py @@ -858,6 +858,8 @@ class pdfdoc(object): image[PdfName.ColorSpace] = colorspace image[PdfName.BitsPerComponent] = depth + smask = None + if color == Colorspace["CMYK;I"]: # Inverts all four channels image[PdfName.Decode] = [1, 0, 1, 0, 1, 0, 1, 0] -- 2.39.5 From 968fc0c27ac2983613ee147ad51d82d0c9237eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 19 Jun 2021 18:53:45 +0200 Subject: [PATCH 6/7] Test support on macOS --- src/img2pdf_test.py | 290 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 226 insertions(+), 64 deletions(-) diff --git a/src/img2pdf_test.py b/src/img2pdf_test.py index bdb8c7d..265fab4 100755 --- a/src/img2pdf_test.py +++ b/src/img2pdf_test.py @@ -2287,7 +2287,15 @@ def tiff_float_img(tmp_path_factory, tmp_normal_png): def tiff_cmyk8_img(tmp_path_factory, tmp_normal_png): in_img = tmp_path_factory.mktemp("tiff_cmyk8") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_normal_png), "-colorspace", "cmyk", str(in_img)] + [ + "convert", + str(tmp_normal_png), + "-colorspace", + "cmyk", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 @@ -2344,6 +2352,8 @@ def tiff_cmyk16_img(tmp_path_factory, tmp_normal_png): "16", "-colorspace", "cmyk", + "-compress", + "Zip", str(in_img), ] ) @@ -2394,7 +2404,15 @@ def tiff_cmyk16_img(tmp_path_factory, tmp_normal_png): @pytest.fixture(scope="session") def tiff_rgb8_img(tmp_path_factory, tmp_normal_png): in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff" - subprocess.check_call(["convert", str(tmp_normal_png), str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_normal_png), + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -2442,7 +2460,15 @@ def tiff_rgb8_img(tmp_path_factory, tmp_normal_png): def tiff_rgb12_img(tmp_path_factory, tmp_normal16_png): in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_normal16_png), "-depth", "12", str(in_img)] + [ + "convert", + str(tmp_normal16_png), + "-depth", + "12", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 @@ -2495,7 +2521,15 @@ def tiff_rgb12_img(tmp_path_factory, tmp_normal16_png): def tiff_rgb14_img(tmp_path_factory, tmp_normal16_png): in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_normal16_png), "-depth", "14", str(in_img)] + [ + "convert", + str(tmp_normal16_png), + "-depth", + "14", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 @@ -2548,7 +2582,15 @@ def tiff_rgb14_img(tmp_path_factory, tmp_normal16_png): def tiff_rgb16_img(tmp_path_factory, tmp_normal16_png): in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_normal16_png), "-depth", "16", str(in_img)] + [ + "convert", + str(tmp_normal16_png), + "-depth", + "16", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 @@ -2597,7 +2639,16 @@ def tiff_rgb16_img(tmp_path_factory, tmp_normal16_png): def tiff_rgba8_img(tmp_path_factory, tmp_alpha_png): in_img = tmp_path_factory.mktemp("tiff_rgba8") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_alpha_png), "-depth", "8", "-strip", str(in_img)] + [ + "convert", + str(tmp_alpha_png), + "-depth", + "8", + "-strip", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 @@ -2646,7 +2697,16 @@ def tiff_rgba8_img(tmp_path_factory, tmp_alpha_png): def tiff_rgba16_img(tmp_path_factory, tmp_alpha_png): in_img = tmp_path_factory.mktemp("tiff_rgba16") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_alpha_png), "-depth", "16", "-strip", str(in_img)] + [ + "convert", + str(tmp_alpha_png), + "-depth", + "16", + "-strip", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 @@ -2694,7 +2754,17 @@ def tiff_rgba16_img(tmp_path_factory, tmp_alpha_png): @pytest.fixture(scope="session") def tiff_gray1_img(tmp_path_factory, tmp_gray1_png): in_img = tmp_path_factory.mktemp("tiff_gray1") / "in.tiff" - subprocess.check_call(["convert", str(tmp_gray1_png), "-depth", "1", str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_gray1_png), + "-depth", + "1", + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -2742,7 +2812,17 @@ def tiff_gray1_img(tmp_path_factory, tmp_gray1_png): @pytest.fixture(scope="session") def tiff_gray2_img(tmp_path_factory, tmp_gray2_png): in_img = tmp_path_factory.mktemp("tiff_gray2") / "in.tiff" - subprocess.check_call(["convert", str(tmp_gray2_png), "-depth", "2", str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_gray2_png), + "-depth", + "2", + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -2790,7 +2870,17 @@ def tiff_gray2_img(tmp_path_factory, tmp_gray2_png): @pytest.fixture(scope="session") def tiff_gray4_img(tmp_path_factory, tmp_gray4_png): in_img = tmp_path_factory.mktemp("tiff_gray4") / "in.tiff" - subprocess.check_call(["convert", str(tmp_gray4_png), "-depth", "4", str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_gray4_png), + "-depth", + "4", + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -2838,7 +2928,17 @@ def tiff_gray4_img(tmp_path_factory, tmp_gray4_png): @pytest.fixture(scope="session") def tiff_gray8_img(tmp_path_factory, tmp_gray8_png): in_img = tmp_path_factory.mktemp("tiff_gray8") / "in.tiff" - subprocess.check_call(["convert", str(tmp_gray8_png), "-depth", "8", str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_gray8_png), + "-depth", + "8", + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -2886,7 +2986,17 @@ def tiff_gray8_img(tmp_path_factory, tmp_gray8_png): @pytest.fixture(scope="session") def tiff_gray16_img(tmp_path_factory, tmp_gray16_png): in_img = tmp_path_factory.mktemp("tiff_gray16") / "in.tiff" - subprocess.check_call(["convert", str(tmp_gray16_png), "-depth", "16", str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_gray16_png), + "-depth", + "16", + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -2935,7 +3045,15 @@ def tiff_gray16_img(tmp_path_factory, tmp_gray16_png): def tiff_multipage_img(tmp_path_factory, tmp_normal_png, tmp_inverse_png): in_img = tmp_path_factory.mktemp("tiff_multipage_img") / "in.tiff" subprocess.check_call( - ["convert", str(tmp_normal_png), str(tmp_inverse_png), "-strip", str(in_img)] + [ + "convert", + str(tmp_normal_png), + str(tmp_inverse_png), + "-strip", + "-compress", + "Zip", + str(in_img) + ] ) identify = json.loads( subprocess.check_output(["convert", str(in_img) + "[0]", "json:"]) @@ -3027,7 +3145,15 @@ def tiff_multipage_img(tmp_path_factory, tmp_normal_png, tmp_inverse_png): @pytest.fixture(scope="session") def tiff_palette1_img(tmp_path_factory, tmp_palette1_png): in_img = tmp_path_factory.mktemp("tiff_palette1_img") / "in.tiff" - subprocess.check_call(["convert", str(tmp_palette1_png), str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_palette1_png), + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -3076,7 +3202,15 @@ def tiff_palette1_img(tmp_path_factory, tmp_palette1_png): @pytest.fixture(scope="session") def tiff_palette2_img(tmp_path_factory, tmp_palette2_png): in_img = tmp_path_factory.mktemp("tiff_palette2_img") / "in.tiff" - subprocess.check_call(["convert", str(tmp_palette2_png), str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_palette2_png), + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -3125,7 +3259,15 @@ def tiff_palette2_img(tmp_path_factory, tmp_palette2_png): @pytest.fixture(scope="session") def tiff_palette4_img(tmp_path_factory, tmp_palette4_png): in_img = tmp_path_factory.mktemp("tiff_palette4_img") / "in.tiff" - subprocess.check_call(["convert", str(tmp_palette4_png), str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_palette4_png), + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -3174,7 +3316,15 @@ def tiff_palette4_img(tmp_path_factory, tmp_palette4_png): @pytest.fixture(scope="session") def tiff_palette8_img(tmp_path_factory, tmp_palette8_png): in_img = tmp_path_factory.mktemp("tiff_palette8_img") / "in.tiff" - subprocess.check_call(["convert", str(tmp_palette8_png), str(in_img)]) + subprocess.check_call( + [ + "convert", + str(tmp_palette8_png), + "-compress", + "Zip", + str(in_img) + ] + ) identify = json.loads(subprocess.check_output(["convert", str(in_img), "json:"])) assert len(identify) == 1 # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was @@ -3234,6 +3384,8 @@ def tiff_ccitt_lsb_m2l_white_img(tmp_path_factory, tmp_gray1_png): "tiff:fill-order=msb", "-define", "quantum:polarity=min-is-white", + "-compress", + "Group4", str(in_img), ] ) @@ -3313,6 +3465,8 @@ def tiff_ccitt_msb_m2l_white_img(tmp_path_factory, tmp_gray1_png): "tiff:fill-order=msb", "-define", "quantum:polarity=min-is-white", + "-compress", + "Group4", str(in_img), ] ) @@ -3393,6 +3547,8 @@ def tiff_ccitt_msb_l2m_white_img(tmp_path_factory, tmp_gray1_png): "tiff:fill-order=lsb", "-define", "quantum:polarity=min-is-white", + "-compress", + "Group4", str(in_img), ] ) @@ -3478,6 +3634,8 @@ def tiff_ccitt_lsb_m2l_black_img(tmp_path_factory, tmp_gray1_png): "tiff:fill-order=msb", "-define", "quantum:polarity=min-is-black", + "-compress", + "Group4", str(in_img), ] ) @@ -3557,6 +3715,8 @@ def tiff_ccitt_nometa1_img(tmp_path_factory, tmp_gray1_png): "tiff:fill-order=msb", "-define", "quantum:polarity=min-is-white", + "-compress", + "Group4", str(in_img), ] ) @@ -3645,6 +3805,8 @@ def tiff_ccitt_nometa2_img(tmp_path_factory, tmp_gray1_png): "tiff:fill-order=msb", "-define", "quantum:polarity=min-is-white", + "-compress", + "Group4", str(in_img), ] ) @@ -5048,7 +5210,7 @@ def test_jpg_cmyk(tmp_path_factory, jpg_cmyk_img, jpg_cmyk_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.skipif( @@ -5063,7 +5225,7 @@ def test_jpg_2000(tmp_path_factory, jpg_2000_img, jpg_2000_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_rgb8(tmp_path_factory, png_rgb8_img, png_rgb8_pdf): @@ -5075,7 +5237,7 @@ def test_png_rgb8(tmp_path_factory, png_rgb8_img, png_rgb8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_rgb16(tmp_path_factory, png_rgb16_img, png_rgb16_pdf): @@ -5087,7 +5249,7 @@ def test_png_rgb16(tmp_path_factory, png_rgb16_img, png_rgb16_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5110,7 +5272,7 @@ def test_png_rgba8(tmp_path_factory, png_rgba8_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5133,7 +5295,7 @@ def test_png_rgba16(tmp_path_factory, png_rgba16_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5156,7 +5318,7 @@ def test_png_gray8a(tmp_path_factory, png_gray8a_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5179,7 +5341,7 @@ def test_png_gray16a(tmp_path_factory, png_gray16a_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_interlaced(tmp_path_factory, png_interlaced_img, png_interlaced_pdf): @@ -5191,7 +5353,7 @@ def test_png_interlaced(tmp_path_factory, png_interlaced_img, png_interlaced_pdf @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_gray1(tmp_path_factory, png_gray1_img, png_gray1_pdf): @@ -5203,7 +5365,7 @@ def test_png_gray1(tmp_path_factory, png_gray1_img, png_gray1_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_gray2(tmp_path_factory, png_gray2_img, png_gray2_pdf): @@ -5215,7 +5377,7 @@ def test_png_gray2(tmp_path_factory, png_gray2_img, png_gray2_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_gray4(tmp_path_factory, png_gray4_img, png_gray4_pdf): @@ -5227,7 +5389,7 @@ def test_png_gray4(tmp_path_factory, png_gray4_img, png_gray4_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_gray8(tmp_path_factory, png_gray8_img, png_gray8_pdf): @@ -5239,7 +5401,7 @@ def test_png_gray8(tmp_path_factory, png_gray8_img, png_gray8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_gray16(tmp_path_factory, png_gray16_img, png_gray16_pdf): @@ -5255,7 +5417,7 @@ def test_png_gray16(tmp_path_factory, png_gray16_img, png_gray16_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_palette1(tmp_path_factory, png_palette1_img, png_palette1_pdf): @@ -5267,7 +5429,7 @@ def test_png_palette1(tmp_path_factory, png_palette1_img, png_palette1_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_palette2(tmp_path_factory, png_palette2_img, png_palette2_pdf): @@ -5279,7 +5441,7 @@ def test_png_palette2(tmp_path_factory, png_palette2_img, png_palette2_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_palette4(tmp_path_factory, png_palette4_img, png_palette4_pdf): @@ -5291,7 +5453,7 @@ def test_png_palette4(tmp_path_factory, png_palette4_img, png_palette4_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_png_palette8(tmp_path_factory, png_palette8_img, png_palette8_pdf): @@ -5315,7 +5477,7 @@ def test_png_icc(tmp_path_factory, png_icc_img, png_icc_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5338,7 +5500,7 @@ def test_gif_transparent(tmp_path_factory, gif_transparent_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_gif_palette1(tmp_path_factory, gif_palette1_img, gif_palette1_pdf): @@ -5350,7 +5512,7 @@ def test_gif_palette1(tmp_path_factory, gif_palette1_img, gif_palette1_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_gif_palette2(tmp_path_factory, gif_palette2_img, gif_palette2_pdf): @@ -5362,7 +5524,7 @@ def test_gif_palette2(tmp_path_factory, gif_palette2_img, gif_palette2_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_gif_palette4(tmp_path_factory, gif_palette4_img, gif_palette4_pdf): @@ -5374,7 +5536,7 @@ def test_gif_palette4(tmp_path_factory, gif_palette4_img, gif_palette4_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_gif_palette8(tmp_path_factory, gif_palette8_img, gif_palette8_pdf): @@ -5386,7 +5548,7 @@ def test_gif_palette8(tmp_path_factory, gif_palette8_img, gif_palette8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_gif_animation(tmp_path_factory, gif_animation_img, gif_animation_pdf): @@ -5433,7 +5595,7 @@ def test_tiff_float(tmp_path_factory, tiff_float_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_cmyk8(tmp_path_factory, tiff_cmyk8_img, tiff_cmyk8_pdf): @@ -5447,7 +5609,7 @@ def test_tiff_cmyk8(tmp_path_factory, tiff_cmyk8_img, tiff_cmyk8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5471,7 +5633,7 @@ def test_tiff_cmyk16(tmp_path_factory, tiff_cmyk16_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_rgb8(tmp_path_factory, tiff_rgb8_img, tiff_rgb8_pdf): @@ -5483,7 +5645,7 @@ def test_tiff_rgb8(tmp_path_factory, tiff_rgb8_img, tiff_rgb8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5507,7 +5669,7 @@ def test_tiff_rgb12(tmp_path_factory, tiff_rgb12_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5531,7 +5693,7 @@ def test_tiff_rgb14(tmp_path_factory, tiff_rgb14_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5555,7 +5717,7 @@ def test_tiff_rgb16(tmp_path_factory, tiff_rgb16_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5578,7 +5740,7 @@ def test_tiff_rgba8(tmp_path_factory, tiff_rgba8_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5601,7 +5763,7 @@ def test_tiff_rgba16(tmp_path_factory, tiff_rgba16_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_gray1(tmp_path_factory, tiff_gray1_img, tiff_gray1_pdf): @@ -5613,7 +5775,7 @@ def test_tiff_gray1(tmp_path_factory, tiff_gray1_img, tiff_gray1_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_gray2(tmp_path_factory, tiff_gray2_img, tiff_gray2_pdf): @@ -5625,7 +5787,7 @@ def test_tiff_gray2(tmp_path_factory, tiff_gray2_img, tiff_gray2_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_gray4(tmp_path_factory, tiff_gray4_img, tiff_gray4_pdf): @@ -5637,7 +5799,7 @@ def test_tiff_gray4(tmp_path_factory, tiff_gray4_img, tiff_gray4_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_gray8(tmp_path_factory, tiff_gray8_img, tiff_gray8_pdf): @@ -5649,7 +5811,7 @@ def test_tiff_gray8(tmp_path_factory, tiff_gray8_img, tiff_gray8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) @pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) @@ -5672,7 +5834,7 @@ def test_tiff_gray16(tmp_path_factory, tiff_gray16_img, engine): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_multipage(tmp_path_factory, tiff_multipage_img, tiff_multipage_pdf): @@ -5702,7 +5864,7 @@ def test_tiff_multipage(tmp_path_factory, tiff_multipage_img, tiff_multipage_pdf reason="requires imagemagick with support for keeping the palette depth", ) @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_palette1(tmp_path_factory, tiff_palette1_img, tiff_palette1_pdf): @@ -5718,7 +5880,7 @@ def test_tiff_palette1(tmp_path_factory, tiff_palette1_img, tiff_palette1_pdf): reason="requires imagemagick with support for keeping the palette depth", ) @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_palette2(tmp_path_factory, tiff_palette2_img, tiff_palette2_pdf): @@ -5734,7 +5896,7 @@ def test_tiff_palette2(tmp_path_factory, tiff_palette2_img, tiff_palette2_pdf): reason="requires imagemagick with support for keeping the palette depth", ) @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_palette4(tmp_path_factory, tiff_palette4_img, tiff_palette4_pdf): @@ -5746,7 +5908,7 @@ def test_tiff_palette4(tmp_path_factory, tiff_palette4_img, tiff_palette4_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_palette8(tmp_path_factory, tiff_palette8_img, tiff_palette8_pdf): @@ -5758,7 +5920,7 @@ def test_tiff_palette8(tmp_path_factory, tiff_palette8_img, tiff_palette8_pdf): @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_ccitt_lsb_m2l_white( @@ -5779,7 +5941,7 @@ def test_tiff_ccitt_lsb_m2l_white( @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_ccitt_msb_m2l_white( @@ -5800,7 +5962,7 @@ def test_tiff_ccitt_msb_m2l_white( @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_ccitt_msb_l2m_white( @@ -5825,7 +5987,7 @@ def test_tiff_ccitt_msb_l2m_white( reason="requires imagemagick with support for min-is-black", ) @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_ccitt_lsb_m2l_black( @@ -5846,7 +6008,7 @@ def test_tiff_ccitt_lsb_m2l_black( @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_ccitt_nometa1( @@ -5862,7 +6024,7 @@ def test_tiff_ccitt_nometa1( @pytest.mark.skipif( - sys.platform in ["darwin", "win32"], + sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) def test_tiff_ccitt_nometa2( -- 2.39.5 From 8cbe03d486f1816229a13255119182331fdd2cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tam=C3=A1s=20Zahola?= Date: Sat, 19 Jun 2021 18:54:32 +0200 Subject: [PATCH 7/7] Test cases for transparency --- src/img2pdf_test.py | 198 ++++++++++++++++++++++++++++++++------------ 1 file changed, 146 insertions(+), 52 deletions(-) diff --git a/src/img2pdf_test.py b/src/img2pdf_test.py index 265fab4..3890b16 100755 --- a/src/img2pdf_test.py +++ b/src/img2pdf_test.py @@ -406,6 +406,23 @@ def compare_pdfimages_tiff(tmpdir, img, pdf): def compare_pdfimages_png(tmpdir, img, pdf, exact=True, icc=False): subprocess.check_call(["pdfimages", "-png", str(pdf), str(tmpdir / "images")]) + # images-001.png is the grayscale SMask image (the original alpha channel) + if os.path.isfile(tmpdir / "images-001.png"): + subprocess.check_call( + [ + "convert", + str(tmpdir / "images-000.png"), + str(tmpdir / "images-001.png"), + "-compose", + "copy-opacity", + "-composite", + str(tmpdir / "composite.png") + ] + ) + (tmpdir / "images-000.png").unlink() + (tmpdir / "images-001.png").unlink() + os.rename(tmpdir / "composite.png", tmpdir / "images-000.png") + if exact: if icc: raise Exception("not exact with icc") @@ -4066,6 +4083,86 @@ def png_rgb8_pdf(tmp_path_factory, png_rgb8_img, request): out_pdf.unlink() +@pytest.fixture(scope="session", params=["internal", "pikepdf", "pdfrw"]) +def png_rgba8_pdf(tmp_path_factory, png_rgba8_img, request): + out_pdf = tmp_path_factory.mktemp("png_rgba8_pdf") / "out.pdf" + subprocess.check_call( + [ + "src/img2pdf.py", + "--producer=", + "--nodate", + "--engine=" + request.param, + "--output=" + str(out_pdf), + str(png_rgba8_img), + ] + ) + with pikepdf.open(str(out_pdf)) as p: + assert ( + p.pages[0].Contents.read_bytes() + == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ" + ) + assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB" + assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3 + assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15 + assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode" + assert p.pages[0].Resources.XObject.Im0.Height == 60 + assert p.pages[0].Resources.XObject.Im0.Width == 60 + assert p.pages[0].Resources.XObject.Im0.SMask is not None + + assert p.pages[0].Resources.XObject.Im0.SMask.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.SMask.ColorSpace == "/DeviceGray" + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Colors == 1 + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Predictor == 15 + assert p.pages[0].Resources.XObject.Im0.SMask.Filter == "/FlateDecode" + assert p.pages[0].Resources.XObject.Im0.SMask.Height == 60 + assert p.pages[0].Resources.XObject.Im0.SMask.Width == 60 + yield out_pdf + out_pdf.unlink() + + +@pytest.fixture(scope="session", params=["internal", "pikepdf", "pdfrw"]) +def gif_transparent_pdf(tmp_path_factory, gif_transparent_img, request): + out_pdf = tmp_path_factory.mktemp("gif_transparent_pdf") / "out.pdf" + subprocess.check_call( + [ + "src/img2pdf.py", + "--producer=", + "--nodate", + "--engine=" + request.param, + "--output=" + str(out_pdf), + str(gif_transparent_img), + ] + ) + with pikepdf.open(str(out_pdf)) as p: + assert ( + p.pages[0].Contents.read_bytes() + == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ" + ) + assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB" + assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3 + assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15 + assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode" + assert p.pages[0].Resources.XObject.Im0.Height == 60 + assert p.pages[0].Resources.XObject.Im0.Width == 60 + assert p.pages[0].Resources.XObject.Im0.SMask is not None + + assert p.pages[0].Resources.XObject.Im0.SMask.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.SMask.ColorSpace == "/DeviceGray" + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Colors == 1 + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Predictor == 15 + assert p.pages[0].Resources.XObject.Im0.SMask.Filter == "/FlateDecode" + assert p.pages[0].Resources.XObject.Im0.SMask.Height == 60 + assert p.pages[0].Resources.XObject.Im0.SMask.Width == 60 + yield out_pdf + out_pdf.unlink() + + @pytest.fixture(scope="session", params=["internal", "pikepdf", "pdfrw"]) def png_rgb16_pdf(tmp_path_factory, png_rgb16_img, request): out_pdf = tmp_path_factory.mktemp("png_rgb16_pdf") / "out.pdf" @@ -4246,6 +4343,46 @@ def png_gray8_pdf(tmp_path_factory, tmp_gray8_png, request): out_pdf.unlink() +@pytest.fixture(scope="session", params=["internal", "pikepdf", "pdfrw"]) +def png_gray8a_pdf(tmp_path_factory, png_gray8a_img, request): + out_pdf = tmp_path_factory.mktemp("png_gray8a_pdf") / "out.pdf" + subprocess.check_call( + [ + "src/img2pdf.py", + "--producer=", + "--nodate", + "--engine=" + request.param, + "--output=" + str(out_pdf), + str(png_gray8a_img), + ] + ) + with pikepdf.open(str(out_pdf)) as p: + assert ( + p.pages[0].Contents.read_bytes() + == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ" + ) + assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray" + assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1 + assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15 + assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode" + assert p.pages[0].Resources.XObject.Im0.Height == 60 + assert p.pages[0].Resources.XObject.Im0.Width == 60 + assert p.pages[0].Resources.XObject.Im0.SMask is not None + + assert p.pages[0].Resources.XObject.Im0.SMask.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.SMask.ColorSpace == "/DeviceGray" + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.BitsPerComponent == 8 + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Colors == 1 + assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Predictor == 15 + assert p.pages[0].Resources.XObject.Im0.SMask.Filter == "/FlateDecode" + assert p.pages[0].Resources.XObject.Im0.SMask.Height == 60 + assert p.pages[0].Resources.XObject.Im0.SMask.Width == 60 + yield out_pdf + out_pdf.unlink() + + @pytest.fixture(scope="session", params=["internal", "pikepdf", "pdfrw"]) def png_gray16_pdf(tmp_path_factory, tmp_gray16_png, request): out_pdf = tmp_path_factory.mktemp("png_gray16_pdf") / "out.pdf" @@ -5252,24 +5389,9 @@ def test_png_rgb16(tmp_path_factory, png_rgb16_img, png_rgb16_pdf): sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) -@pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) -def test_png_rgba8(tmp_path_factory, png_rgba8_img, engine): - out_pdf = tmp_path_factory.mktemp("png_rgba8") / "out.pdf" - assert ( - 0 - != subprocess.run( - [ - "src/img2pdf.py", - "--producer=", - "--nodate", - "--engine=" + engine, - "--output=" + str(out_pdf), - str(png_rgba8_img), - ] - ).returncode - ) - out_pdf.unlink() - +def test_png_rgba8(tmp_path_factory, png_rgba8_img, png_rgba8_pdf): + tmpdir = tmp_path_factory.mktemp("png_rgba8") + compare_pdfimages_png(tmpdir, png_rgba8_img, png_rgba8_pdf) @pytest.mark.skipif( sys.platform in ["win32"], @@ -5298,23 +5420,9 @@ def test_png_rgba16(tmp_path_factory, png_rgba16_img, engine): sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) -@pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) -def test_png_gray8a(tmp_path_factory, png_gray8a_img, engine): - out_pdf = tmp_path_factory.mktemp("png_gray8a") / "out.pdf" - assert ( - 0 - != subprocess.run( - [ - "src/img2pdf.py", - "--producer=", - "--nodate", - "--engine=" + engine, - "--output=" + str(out_pdf), - str(png_gray8a_img), - ] - ).returncode - ) - out_pdf.unlink() +def test_png_gray8a(tmp_path_factory, png_gray8a_img, png_gray8a_pdf): + tmpdir = tmp_path_factory.mktemp("png_gray8a") + compare_pdfimages_png(tmpdir, png_gray8a_img, png_gray8a_pdf) @pytest.mark.skipif( @@ -5480,23 +5588,9 @@ def test_png_icc(tmp_path_factory, png_icc_img, png_icc_pdf): sys.platform in ["win32"], reason="test utilities not available on Windows and MacOS", ) -@pytest.mark.parametrize("engine", ["internal", "pikepdf", "pdfrw"]) -def test_gif_transparent(tmp_path_factory, gif_transparent_img, engine): - out_pdf = tmp_path_factory.mktemp("gif_transparent") / "out.pdf" - assert ( - 0 - != subprocess.run( - [ - "src/img2pdf.py", - "--producer=", - "--nodate", - "--engine=" + engine, - "--output=" + str(out_pdf), - str(gif_transparent_img), - ] - ).returncode - ) - out_pdf.unlink() +def test_gif_transparent(tmp_path_factory, gif_transparent_img, gif_transparent_pdf): + tmpdir = tmp_path_factory.mktemp("gif_transparent") + compare_pdfimages_png(tmpdir, gif_transparent_img, gif_transparent_pdf) @pytest.mark.skipif( -- 2.39.5