From 11907242a5b5cebb8c267e8f8d4b945d7357baf0 Mon Sep 17 00:00:00 2001 From: Johannes 'josch' Schauer Date: Sun, 9 Aug 2020 22:03:47 +0200 Subject: [PATCH] src/img2pdf_test.py: we create our own channel-switching ICC profile --- src/img2pdf_test.py | 242 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 200 insertions(+), 42 deletions(-) mode change 100644 => 100755 src/img2pdf_test.py diff --git a/src/img2pdf_test.py b/src/img2pdf_test.py old mode 100644 new mode 100755 index 8877d25..4c99350 --- a/src/img2pdf_test.py +++ b/src/img2pdf_test.py @@ -352,11 +352,13 @@ def compare_ghostscript(tmpdir, img, pdf, gsdevice="png16m", exact=True, icc=Fal (tmpdir / ("gs-1." + ext)).unlink() -def compare_poppler(tmpdir, img, pdf, exact=True): +def compare_poppler(tmpdir, img, pdf, exact=True, icc=False): subprocess.check_call( ["pdftocairo", "-r", "96", "-png", str(pdf), str(tmpdir / "poppler")] ) if exact: + if icc: + raise Exception("not exact with icc") subprocess.check_call( [ "compare", @@ -368,18 +370,38 @@ def compare_poppler(tmpdir, img, pdf, exact=True): ] ) else: - psnr = subprocess.run( - [ - "compare", - "-metric", - "PSNR", - str(img), - str(tmpdir / "poppler-1.png"), - "null:", - ], - check=False, - stderr=subprocess.PIPE, - ).stderr + if icc: + psnr = subprocess.run( + [ + "compare", + "-metric", + "PSNR", + "(", + "-profile", + "/usr/share/color/icc/ghostscript/srgb.icc", + "-depth", + "8", + str(img), + ")", + str(tmpdir / "poppler-1.png"), + "null:", + ], + check=False, + stderr=subprocess.PIPE, + ).stderr + else: + psnr = subprocess.run( + [ + "compare", + "-metric", + "PSNR", + str(img), + str(tmpdir / "poppler-1.png"), + "null:", + ], + check=False, + stderr=subprocess.PIPE, + ).stderr assert psnr != b"0" psnr = float(psnr.strip(b"0")) assert psnr != 0 # or otherwise we would use the exact variant @@ -445,9 +467,11 @@ def compare_pdfimages_tiff(tmpdir, img, pdf): (tmpdir / "images-000.tif").unlink() -def compare_pdfimages_png(tmpdir, img, pdf, exact=True): +def compare_pdfimages_png(tmpdir, img, pdf, exact=True, icc=False): subprocess.check_call(["pdfimages", "-png", str(pdf), str(tmpdir / "images")]) if exact: + if icc: + raise Exception("not exact with icc") subprocess.check_call( [ "compare", @@ -459,18 +483,38 @@ def compare_pdfimages_png(tmpdir, img, pdf, exact=True): ] ) else: - psnr = subprocess.run( - [ - "compare", - "-metric", - "PSNR", - str(img), - str(tmpdir / "images-000.png"), - "null:", - ], - check=False, - stderr=subprocess.PIPE, - ).stderr + if icc: + psnr = subprocess.run( + [ + "compare", + "-metric", + "PSNR", + "(", + "-profile", + "/usr/share/color/icc/ghostscript/srgb.icc", + "-depth", + "8", + str(img), + ")", + str(tmpdir / "images-000.png"), + "null:", + ], + check=False, + stderr=subprocess.PIPE, + ).stderr + else: + psnr = subprocess.run( + [ + "compare", + "-metric", + "PSNR", + str(img), + str(tmpdir / "images-000.png"), + "null:", + ], + check=False, + stderr=subprocess.PIPE, + ).stderr assert psnr != b"0" psnr = float(psnr.strip(b"0")) assert psnr != 0 # or otherwise we would use the exact variant @@ -504,13 +548,7 @@ def tiff_header_for_ccitt(width, height, img_size, ccitt_group=4): ) -############################################################################### -# INPUT FIXTURES # -############################################################################### - - -@pytest.fixture(scope="session") -def alpha(): +def alpha_value(): # gaussian kernel with sigma=3 kernel = numpy.array( [ @@ -548,6 +586,103 @@ def alpha(): return alpha +def icc_profile(): + PCS = (0.96420288, 1.0, 0.82490540) # D50 illuminant constants + # approximate X,Y,Z values for white, red, green and blue + white = (0.95, 1.0, 1.09) + red = (0.44, 0.22, 0.014) + green = (0.39, 0.72, 0.1) + blue = (0.14, 0.06, 0.71) + + getxyz = lambda v: (round(65536 * v[0]), round(65536 * v[1]), round(65536 * v[2])) + + header = ( + # header + +4 * b"\0" # cmmsignatures + + 4 * b"\0" # version + + b"mntr" # device class + + b"RGB " # color space + + b"XYZ " # PCS + + 12 * b"\0" # datetime + + b"\x61\x63\x73\x70" # static signature + + 4 * b"\0" # platform + + 4 * b"\0" # flags + + 4 * b"\0" # device manufacturer + + 4 * b"\0" # device model + + 8 * b"\0" # device attributes + + 4 * b"\0" # rendering intents + + struct.pack(">III", *getxyz(PCS)) + + 4 * b"\0" # creator + + 16 * b"\0" # identifier + + 28 * b"\0" # reserved + ) + + def pad4(s): + if len(s) % 4 == 0: + return s + else: + return s + b"\x00" * (4 - len(s) % 4) + + tagdata = [ + b"desc\x00\x00\x00\x00" + struct.pack(">I", 5) + b"fake" + 79 * b"\x00", + b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(white)), + # by mixing up red, green and blue, we create a test profile + b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(blue)), # red + b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(red)), # green + b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(green)), # blue + # by only supplying two values, we create the most trivial "curve", + # where the remaining values will be linearly interpolated between them + b"curv\x00\x00\x00\x00" + struct.pack(">IHH", 2, 0, 65535), + b"text\x00\x00\x00\x00" + b"no copyright, use freely" + 1 * b"\x00", + ] + + table = [ + (b"desc", 0), + (b"wtpt", 1), + (b"rXYZ", 2), + (b"gXYZ", 3), + (b"bXYZ", 4), + # we use the same curve for all three channels, so the same offset is referenced + (b"rTRC", 5), + (b"gTRC", 5), + (b"bTRC", 5), + (b"cprt", 6), + ] + + offset = ( + lambda n: 4 # total size + + len(header) # header length + + 4 # number table entries + + len(table) * 12 # table length + + sum([len(pad4(s)) for s in tagdata[:n]]) + ) + + table = struct.pack(">I", len(table)) + b"".join( + [t + struct.pack(">II", offset(o), len(tagdata[o])) for t, o in table] + ) + + data = b"".join([pad4(s) for s in tagdata]) + + data = ( + struct.pack(">I", 4 + len(header) + len(table) + len(data)) + + header + + table + + data + ) + + return data + + +############################################################################### +# INPUT FIXTURES # +############################################################################### + + +@pytest.fixture(scope="session") +def alpha(): + return alpha_value() + + @pytest.fixture(scope="session") def tmp_alpha_png(tmp_path_factory, alpha): tmp_alpha_png = tmp_path_factory.mktemp("alpha_png") / "alpha.png" @@ -659,19 +794,26 @@ def tmp_inverse_png(tmp_path_factory, alpha): @pytest.fixture(scope="session") -def tmp_icc_png(tmp_path_factory, alpha): +def tmp_icc_profile(tmp_path_factory): + tmp_icc_profile = tmp_path_factory.mktemp("icc_profile") / "fake.icc" + tmp_icc_profile.write_bytes(icc_profile()) + yield tmp_icc_profile + tmp_icc_profile.unlink() + +@pytest.fixture(scope="session") +def tmp_icc_png(tmp_path_factory, alpha, tmp_icc_profile): normal16 = alpha[:, :, 0:3] tmp_icc_png = tmp_path_factory.mktemp("icc_png") / "icc.png" write_png( - 0xFF - normal16 / 0xFFFF * 0xFF, + normal16 / 0xFFFF * 0xFF, str(tmp_icc_png), 8, 2, - iccp="/usr/share/color/icc/sRGB.icc", + iccp=str(tmp_icc_profile), ) assert ( hashlib.md5(tmp_icc_png.read_bytes()).hexdigest() - == "d09865464626a87b4e7f398e1f914cca" + == "3058ba4703212fe8c18560e6d1cb61b1" ) yield tmp_icc_png tmp_icc_png.unlink() @@ -4249,7 +4391,7 @@ def png_palette8_pdf(tmp_path_factory, tmp_palette8_png, request): @pytest.fixture(scope="session", params=["internal", "pikepdf", "pdfrw"]) -def png_icc_pdf(tmp_path_factory, tmp_icc_png, request): +def png_icc_pdf(tmp_path_factory, tmp_icc_png, tmp_icc_profile, request): out_pdf = tmp_path_factory.mktemp("png_icc_pdf") / "out.pdf" subprocess.check_call( [ @@ -4272,7 +4414,7 @@ def png_icc_pdf(tmp_path_factory, tmp_icc_png, request): assert p.pages[0].Resources.XObject.Im0.ColorSpace[1].Alternate == "/DeviceRGB" assert ( p.pages[0].Resources.XObject.Im0.ColorSpace[1].read_bytes() - == pathlib.Path("/usr/share/color/icc/sRGB.icc").read_bytes() + == tmp_icc_profile.read_bytes() ) assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8 assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3 @@ -5319,9 +5461,9 @@ def test_png_palette8(tmp_path_factory, png_palette8_img, png_palette8_pdf): def test_png_icc(tmp_path_factory, png_icc_img, png_icc_pdf): tmpdir = tmp_path_factory.mktemp("png_icc") compare_ghostscript(tmpdir, png_icc_img, png_icc_pdf, icc=True) - # compare_poppler(tmpdir, png_icc_img, png_icc_pdf) - # compare_mupdf(tmpdir, png_icc_img, png_icc_pdf) - # compare_pdfimages_png(tmpdir, png_icc_img, png_icc_pdf) + compare_poppler(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True) + # mupdf ignores the ICC profile + compare_pdfimages_png(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True) @pytest.mark.skipif( @@ -6500,3 +6642,19 @@ def test_general(general_input, engine): orig_img.close() except AttributeError: pass + + +def main(): + normal16 = alpha_value()[:, :, 0:3] + pathlib.Path('test.icc').write_bytes(icc_profile()) + write_png( + normal16 / 0xFFFF * 0xFF, + "icc.png", + 8, + 2, + iccp="test.icc", + ) + + +if __name__ == "__main__": + main()