forked from josch/img2pdf
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
194
src/img2pdf_test.py
Normal file → Executable file
194
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()
|
(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(
|
subprocess.check_call(
|
||||||
["pdftocairo", "-r", "96", "-png", str(pdf), str(tmpdir / "poppler")]
|
["pdftocairo", "-r", "96", "-png", str(pdf), str(tmpdir / "poppler")]
|
||||||
)
|
)
|
||||||
if exact:
|
if exact:
|
||||||
|
if icc:
|
||||||
|
raise Exception("not exact with icc")
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
[
|
[
|
||||||
"compare",
|
"compare",
|
||||||
|
@ -367,6 +369,26 @@ def compare_poppler(tmpdir, img, pdf, exact=True):
|
||||||
"null:",
|
"null:",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
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:
|
else:
|
||||||
psnr = subprocess.run(
|
psnr = subprocess.run(
|
||||||
[
|
[
|
||||||
|
@ -445,9 +467,11 @@ def compare_pdfimages_tiff(tmpdir, img, pdf):
|
||||||
(tmpdir / "images-000.tif").unlink()
|
(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")])
|
subprocess.check_call(["pdfimages", "-png", str(pdf), str(tmpdir / "images")])
|
||||||
if exact:
|
if exact:
|
||||||
|
if icc:
|
||||||
|
raise Exception("not exact with icc")
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
[
|
[
|
||||||
"compare",
|
"compare",
|
||||||
|
@ -458,6 +482,26 @@ def compare_pdfimages_png(tmpdir, img, pdf, exact=True):
|
||||||
"null:",
|
"null:",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
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:
|
else:
|
||||||
psnr = subprocess.run(
|
psnr = subprocess.run(
|
||||||
[
|
[
|
||||||
|
@ -504,13 +548,7 @@ def tiff_header_for_ccitt(width, height, img_size, ccitt_group=4):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
###############################################################################
|
def alpha_value():
|
||||||
# INPUT FIXTURES #
|
|
||||||
###############################################################################
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def alpha():
|
|
||||||
# gaussian kernel with sigma=3
|
# gaussian kernel with sigma=3
|
||||||
kernel = numpy.array(
|
kernel = numpy.array(
|
||||||
[
|
[
|
||||||
|
@ -548,6 +586,103 @@ def alpha():
|
||||||
return 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")
|
@pytest.fixture(scope="session")
|
||||||
def tmp_alpha_png(tmp_path_factory, alpha):
|
def tmp_alpha_png(tmp_path_factory, alpha):
|
||||||
tmp_alpha_png = tmp_path_factory.mktemp("alpha_png") / "alpha.png"
|
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")
|
@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]
|
normal16 = alpha[:, :, 0:3]
|
||||||
tmp_icc_png = tmp_path_factory.mktemp("icc_png") / "icc.png"
|
tmp_icc_png = tmp_path_factory.mktemp("icc_png") / "icc.png"
|
||||||
write_png(
|
write_png(
|
||||||
0xFF - normal16 / 0xFFFF * 0xFF,
|
normal16 / 0xFFFF * 0xFF,
|
||||||
str(tmp_icc_png),
|
str(tmp_icc_png),
|
||||||
8,
|
8,
|
||||||
2,
|
2,
|
||||||
iccp="/usr/share/color/icc/sRGB.icc",
|
iccp=str(tmp_icc_profile),
|
||||||
)
|
)
|
||||||
assert (
|
assert (
|
||||||
hashlib.md5(tmp_icc_png.read_bytes()).hexdigest()
|
hashlib.md5(tmp_icc_png.read_bytes()).hexdigest()
|
||||||
== "d09865464626a87b4e7f398e1f914cca"
|
== "3058ba4703212fe8c18560e6d1cb61b1"
|
||||||
)
|
)
|
||||||
yield tmp_icc_png
|
yield tmp_icc_png
|
||||||
tmp_icc_png.unlink()
|
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"])
|
@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"
|
out_pdf = tmp_path_factory.mktemp("png_icc_pdf") / "out.pdf"
|
||||||
subprocess.check_call(
|
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].Alternate == "/DeviceRGB"
|
||||||
assert (
|
assert (
|
||||||
p.pages[0].Resources.XObject.Im0.ColorSpace[1].read_bytes()
|
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.BitsPerComponent == 8
|
||||||
assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
|
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):
|
def test_png_icc(tmp_path_factory, png_icc_img, png_icc_pdf):
|
||||||
tmpdir = tmp_path_factory.mktemp("png_icc")
|
tmpdir = tmp_path_factory.mktemp("png_icc")
|
||||||
compare_ghostscript(tmpdir, png_icc_img, png_icc_pdf, icc=True)
|
compare_ghostscript(tmpdir, png_icc_img, png_icc_pdf, icc=True)
|
||||||
# compare_poppler(tmpdir, png_icc_img, png_icc_pdf)
|
compare_poppler(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True)
|
||||||
# compare_mupdf(tmpdir, png_icc_img, png_icc_pdf)
|
# mupdf ignores the ICC profile
|
||||||
# compare_pdfimages_png(tmpdir, png_icc_img, png_icc_pdf)
|
compare_pdfimages_png(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
|
@ -6500,3 +6642,19 @@ def test_general(general_input, engine):
|
||||||
orig_img.close()
|
orig_img.close()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
pass
|
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