initial commit
This commit is contained in:
Vendored
+117
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Self-authored linear-sRGB ICCBased vector/image fixture; no borrowed ICC data.
|
||||
|
||||
Uses public LittleCMS APIs to encode D65 + sRGB primaries + gamma 1. The
|
||||
color expectation is the analytic sRGB transfer function, independent of PDF
|
||||
renderer output. Only the ICC creation timestamp is normalized for determinism.
|
||||
"""
|
||||
from pathlib import Path
|
||||
import ctypes as C
|
||||
import ctypes.util
|
||||
import hashlib
|
||||
import json
|
||||
import shutil
|
||||
import struct
|
||||
|
||||
|
||||
class xyY(C.Structure):
|
||||
_fields_ = [("x", C.c_double), ("y", C.c_double), ("Y", C.c_double)]
|
||||
|
||||
|
||||
class Primaries(C.Structure):
|
||||
_fields_ = [("Red", xyY), ("Green", xyY), ("Blue", xyY)]
|
||||
|
||||
|
||||
def linear_profile():
|
||||
cms = C.CDLL(ctypes.util.find_library("lcms2") or "liblcms2.so")
|
||||
cms.cmsGetEncodedCMMversion.restype = C.c_uint32
|
||||
cms.cmsBuildGamma.argtypes = [C.c_void_p, C.c_double]; cms.cmsBuildGamma.restype = C.c_void_p
|
||||
cms.cmsFreeToneCurve.argtypes = [C.c_void_p]
|
||||
cms.cmsCreateRGBProfile.argtypes = [C.POINTER(xyY), C.POINTER(Primaries), C.POINTER(C.c_void_p)]
|
||||
cms.cmsCreateRGBProfile.restype = C.c_void_p
|
||||
cms.cmsSaveProfileToMem.argtypes = [C.c_void_p, C.c_void_p, C.POINTER(C.c_uint32)]
|
||||
cms.cmsSaveProfileToMem.restype = C.c_int
|
||||
cms.cmsCloseProfile.argtypes = [C.c_void_p]
|
||||
curve = cms.cmsBuildGamma(None, 1.0)
|
||||
if not curve: raise RuntimeError("LittleCMS gamma creation failed")
|
||||
white = xyY(.3127, .3290, 1.)
|
||||
primaries = Primaries(xyY(.64, .33, 1.), xyY(.30, .60, 1.), xyY(.15, .06, 1.))
|
||||
curves = (C.c_void_p * 3)(curve, curve, curve)
|
||||
profile = cms.cmsCreateRGBProfile(C.byref(white), C.byref(primaries), curves)
|
||||
cms.cmsFreeToneCurve(curve)
|
||||
if not profile: raise RuntimeError("LittleCMS profile creation failed")
|
||||
try:
|
||||
length = C.c_uint32()
|
||||
if not cms.cmsSaveProfileToMem(profile, None, C.byref(length)): raise RuntimeError("ICC size failed")
|
||||
buffer = C.create_string_buffer(length.value)
|
||||
if not cms.cmsSaveProfileToMem(profile, buffer, C.byref(length)): raise RuntimeError("ICC save failed")
|
||||
data = bytearray(buffer.raw[:length.value])
|
||||
finally: cms.cmsCloseProfile(profile)
|
||||
data[24:36] = struct.pack(">6H", 2026, 9, 19, 0, 0, 0)
|
||||
data[84:100] = bytes(16) # Zero profile ID is permitted; no stale timestamp-derived ID.
|
||||
assert data[36:40] == b"acsp" and data[16:20] == b"RGB "
|
||||
return bytes(data), cms.cmsGetEncodedCMMversion()
|
||||
|
||||
|
||||
def srgb(value):
|
||||
encoded = 12.92 * value if value <= .0031308 else 1.055 * value ** (1 / 2.4) - .055
|
||||
return round(255 * encoded)
|
||||
|
||||
|
||||
def pdf_stream(data, extra=b""):
|
||||
return b"<< /Length " + str(len(data)).encode() + b" " + extra + b" >>\nstream\n" + data + b"\nendstream"
|
||||
|
||||
|
||||
def generate(out):
|
||||
out = Path(out); out.mkdir(parents=True, exist_ok=True)
|
||||
profile, version = linear_profile()
|
||||
(out / "linear-srgb.icc").write_bytes(profile)
|
||||
license_path = Path("/usr/share/licenses/lcms2/LICENSE")
|
||||
if license_path.is_file(): shutil.copyfile(license_path, out / "LCMS2-LICENSE.txt")
|
||||
values = [(.0625, .0625, .0625), (.25, .25, .25), (.5, .5, .5), (.75, .75, .75), (.5, .125, .0625), (.0625, .25, .5)]
|
||||
content = ["0 0 0 rg BT /F1 16 Tf 30 325 Td (EMBEDDED ICC: LINEAR RGB TO sRGB) Tj ET",
|
||||
"BT /F1 10 Tf 30 298 Td (Top: ICCBased vector / Middle: DeviceRGB control / Bottom: ICCBased image) Tj ET",
|
||||
"/RelativeColorimetric ri"]
|
||||
probes = []
|
||||
for i, value in enumerate(values):
|
||||
x = 30 + i * 88
|
||||
operands = " ".join(str(v) for v in value)
|
||||
content += [f"/Linear cs {operands} scn {x} 216 76 64 re f", f"{operands} rg {x} 132 76 64 re f"]
|
||||
probes.append({"kind": "icc-vector", "point_pt": [x+38, 248], "components": value, "expected_rgb": [srgb(v) for v in value]})
|
||||
probes.append({"kind": "device-control", "point_pt": [x+38, 164], "components": value, "expected_rgb": [round(v*255) for v in value]})
|
||||
quantized = [round(v*255) for v in value]
|
||||
probes.append({"kind": "icc-image", "point_pt": [30+i*88+44, 68], "components": quantized, "expected_rgb": [srgb(v/255) for v in quantized]})
|
||||
content += ["q 528 0 0 48 30 44 cm /Sample Do Q", "0 0 0 rg BT /F1 10 Tf 30 22 Td (ICC gamma 1 / D65 / sRGB primaries / original data and coordinates) Tj ET"]
|
||||
image = bytes(round(v*255) for rgb in values for v in rgb)
|
||||
objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Count 1 /Kids [3 0 R] >>",
|
||||
b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 600 350] /Resources << /Font << /F1 6 0 R >> /ColorSpace << /Linear [/ICCBased 5 0 R] >> /XObject << /Sample 7 0 R >> >> /Contents 4 0 R >>",
|
||||
pdf_stream("\n".join(content).encode()), pdf_stream(profile,b"/N 3 /Alternate /DeviceRGB /Range [0 1 0 1 0 1]"),
|
||||
b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>",
|
||||
pdf_stream(image,b"/Type /XObject /Subtype /Image /Width 6 /Height 1 /BitsPerComponent 8 /Interpolate false /ColorSpace [/ICCBased 5 0 R] /Intent /RelativeColorimetric")]
|
||||
data = bytearray(b"%PDF-1.7\n%\xe2\xe3\xcf\xd3\n"); offsets=[0]
|
||||
for i, obj in enumerate(objects,1):
|
||||
offsets.append(len(data)); data += f"{i} 0 obj\n".encode()+obj+b"\nendobj\n"
|
||||
xref=len(data); data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode()
|
||||
for offset in offsets[1:]: data += f"{offset:010d} 00000 n \n".encode()
|
||||
data += f"trailer << /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
|
||||
name="icc-linear-rgb.pdf"; (out/name).write_bytes(data)
|
||||
record={"file":name,"sha256":hashlib.sha256(data).hexdigest(),"page_count":1,
|
||||
"expected":{"page_sizes_pt":[[600,350]],"icc_probes":probes,"probe_tolerance_rgb":3,
|
||||
"profile_effect":"ICCBased 0.5 linear gray becomes approximately 188, distinct from DeviceRGB approximately 128."},
|
||||
"limits":["One RGB matrix/TRC profile, not arbitrary ICC LUT/CMYK/printing conformance.","Analytic sRGB color expectation; independent renderer comparison still requires visual review."]}
|
||||
origin={"file":"linear-srgb.icc","sha256":hashlib.sha256(profile).hexdigest(),"bytes":len(profile),
|
||||
"source":"Self-created with LittleCMS cmsCreateRGBProfile using the stated numeric white point, primaries and gamma; no third-party ICC profile is copied.",
|
||||
"lcms_encoded_version":version,"lcms_license":"MIT; LCMS2-LICENSE.txt",
|
||||
"upstream":"https://github.com/mm2/Little-CMS","profile_copyright_tag":"No copyright, use freely",
|
||||
"white_xy":[.3127,.3290],"primaries_xy":[[.64,.33],[.30,.60],[.15,.06]],"gamma":1,
|
||||
"timestamp_normalization":"Header creation time is fixed to 2026-09-19; optional profile ID is zero."}
|
||||
return record,origin
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
out=Path(__file__).resolve().parent/"extended"; record,origin=generate(out)
|
||||
manifest_path=out/"manifest.json"; manifest=json.loads(manifest_path.read_text())
|
||||
manifest["documents"]=[d for d in manifest["documents"] if d["file"]!=record["file"]]+[record]
|
||||
manifest["color_profile"]=origin
|
||||
manifest_path.write_text(json.dumps(manifest,ensure_ascii=False,indent=2)+"\n")
|
||||
print(json.dumps({"fixture":record["sha256"],"profile":origin["sha256"]}))
|
||||
Reference in New Issue
Block a user