src/img2pdf_test.py: we create our own channel-switching ICC profile
This commit is contained in:
parent
692b54ac67
commit
11907242a5
1 changed files with 200 additions and 42 deletions
242
src/img2pdf_test.py
Normal file → Executable file
242
src/img2pdf_test.py
Normal file → Executable file
|
@ -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()
|
||||
|
|
Loading…
Reference in a new issue