initial commit

This commit is contained in:
2026-09-21 13:41:40 +09:00
commit 855c7328df
411 changed files with 85352 additions and 0 deletions
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Generate self-authored ICC intent/state/cache probes without renderer goldens."""
import argparse
import hashlib
import itertools
import json
from pathlib import Path
import struct
from pypdf import PdfWriter
from pypdf.generic import (ArrayObject, ByteStringObject, DecodedStreamObject, DictionaryObject,
FloatObject, NameObject, NumberObject)
ROOT = Path(__file__).resolve().parents[3]
BASE = ROOT / 'tests/fixtures/pdf/cmyk-lut'
INTENTS = ('Perceptual', 'RelativeColorimetric', 'Saturation', 'AbsoluteColorimetric')
SAMPLE_BYTES = (51, 128, 204, 77)
SAMPLE = tuple(x/255 for x in SAMPLE_BYTES)
WIDTH, HEIGHT = 820, 800
CASES = ('default-relative', 'ri-vector', 'extgstate-ri', 'nested-q-inner', 'nested-Q-restore',
'form-inherit-shared', 'form-inherit-shared-gs', 'form-ri-override', 'form-gs-override', 'form-return-restore',
'image-inherit-shared-forward', 'image-inherit-shared-reverse', 'image-intent-override',
'image-inherit-shared-gs', 'image-override-return', 'indexed-vector', 'indexed-image', 'stroke', 'text', 'axial-shading',
'shading-pattern-inherit', 'shading-pattern-RI-override',
'type3-inherit-rectangle', 'type3-inherit-image', 'type3-ri-override')
def sha(data): return hashlib.sha256(data).hexdigest()
def array(values): return ArrayObject([FloatObject(x) for x in values])
def name(value): return NameObject('/'+value)
def dictionary(**values): return DictionaryObject({name(k):v for k,v in values.items()})
def fixed(values): return struct.pack('>'+'i'*len(values), *(round(v*65536) for v in values))
def operands(values): return ' '.join(f'{v:.10f}' for v in values)
def derived_model(tag, values):
c,m,y,k = values
if tag == 'A2B0': return (100-12*c-15*m-10*y-35*k,18*m-15*c,20*y-12*c)
if tag == 'A2B1': return (100-20*c-20*m-20*y-40*k,40*m-20*c-20*k,40*y-20*c-20*k)
if tag == 'A2B2': return (100-25*c-25*m-20*y-30*k,-40*m+20*c+20*k,35*y-20*m-15*k)
raise ValueError(tag)
def derived_profile(base):
# Reuse the existing input profile's v2 header, CLUT layout and attribution;
# add explicitly distinct self-authored intent tables and a non-D50 medium.
tags = {}
for i in range(struct.unpack_from('>I',base,128)[0]):
signature,offset,size=struct.unpack_from('>4sII',base,132+12*i)
tags[signature]=base[offset:offset+size]
tags[b'wtpt']=b'XYZ '+bytes(4)+fixed((.82,.86,.72))
for signature in ('A2B0','A2B1','A2B2'):
lut=bytearray(tags[b'A2B0'])
for i,values in enumerate(itertools.product((0,1),repeat=4)):
lightness,a,b=derived_model(signature,values)
struct.pack_into('>3H',lut,68+6*i,round(lightness/100*65280),round((a+128)*256),round((b+128)*256))
tags[signature.encode()]=bytes(lut)
description=b'DocView synthetic distinct rendering intent tables\0'
tags[b'desc']=b'desc'+bytes(4)+struct.pack('>I',len(description))+description+bytes(78)
header=bytearray(base[:128]);entries=bytearray();body=bytearray();offset=132+12*len(tags)
for signature,data in sorted(tags.items()):
entries+=signature+struct.pack('>II',offset+len(body),len(data))
body+=data+bytes((-len(data))%4)
result=header+struct.pack('>I',len(tags))+entries+body
result[:4]=struct.pack('>I',len(result))
return bytes(result)
def generate(output):
output.mkdir(parents=True,exist_ok=False)
base_spec=json.loads((BASE/'manifest.json').read_text())
base=(BASE/base_spec['profileFile']).read_bytes()
assert sha(base)==base_spec['profileSha256']
profiles={'original':base,'distinct':derived_profile(base)}
writer=PdfWriter();writer.pdf_header='%PDF-1.7'
writer.add_metadata({'/Title':'Rendering intent: explicit state, inheritance and cache',
'/Author':'DocView synthetic regression corpus'})
rows=[];profile_records={}
for profile_id,profile in profiles.items():
filename=profile_id+'-cmyk-lab.icc';(output/filename).write_bytes(profile)
profile_records[profile_id]={'file':filename,'sha256':sha(profile),'size':len(profile),
'origin':'byte-identical existing synthetic fixture' if profile_id=='original' else 'self-authored extension of the existing CLUT',
'mediaWhitePoint':[.9642,1,.8249] if profile_id=='original' else [.82,.86,.72],
'tables':['A2B0'] if profile_id=='original' else ['A2B0','A2B1','A2B2']}
stream=DecodedStreamObject();stream.set_data(profile)
stream.update(dictionary(N=NumberObject(4),Alternate=name('DeviceCMYK')))
space=ArrayObject([name('ICCBased'),writer._add_object(stream)])
indexed=ArrayObject([name('Indexed'),space,NumberObject(0),ByteStringObject(bytes(SAMPLE_BYTES))])
font=writer._add_object(dictionary(Type=name('Font'),Subtype=name('Type1'),BaseFont=name('Helvetica-Bold')))
gs=dictionary(**{'I'+str(i):dictionary(Type=name('ExtGState'),RI=name(intent)) for i,intent in enumerate(INTENTS)})
local_resources=dictionary(ColorSpace=dictionary(CS=space,IX=indexed),ExtGState=gs,Font=dictionary(F=font))
image=DecodedStreamObject();image.set_data(bytes(SAMPLE_BYTES))
image.update(dictionary(Type=name('XObject'),Subtype=name('Image'),Width=NumberObject(1),Height=NumberObject(1),
BitsPerComponent=NumberObject(8),ColorSpace=space))
shared_image=writer._add_object(image)
fonts=dictionary(F=font)
glyph_resources=dictionary(ColorSpace=dictionary(CS=space),XObject=dictionary(Shared=shared_image))
def type3_font(prefix='',image=False):
glyph=DecodedStreamObject()
painting='q 1000 0 0 1000 0 0 cm /Shared Do Q' if image else '/CS cs '+operands(SAMPLE)+' scn 0 0 1000 1000 re f'
glyph.set_data(('1000 0 d0\n'+prefix+'\n'+painting+'\n').encode())
encoding=dictionary(Type=name('Encoding'),Differences=ArrayObject([NumberObject(65),name('A')]))
return writer._add_object(dictionary(Type=name('Font'),Subtype=name('Type3'),FontBBox=array((0,0,1000,1000)),
FontMatrix=array((.001,0,0,.001,0,0)),CharProcs=dictionary(A=writer._add_object(glyph)),
Encoding=encoding,FirstChar=NumberObject(65),LastChar=NumberObject(65),Widths=array((1000,)),Resources=glyph_resources))
fonts[name('T3Rect')]=type3_font()
fonts[name('T3Image')]=type3_font(image=True)
for i,intent in enumerate(INTENTS):fonts[name('T3Override'+str(i))]=type3_font('/'+intent+' ri')
iximage=DecodedStreamObject();iximage.set_data(b'\0')
iximage.update(dictionary(Type=name('XObject'),Subtype=name('Image'),Width=NumberObject(1),Height=NumberObject(1),
BitsPerComponent=NumberObject(8),ColorSpace=indexed))
xobjects=dictionary(Shared=shared_image,Indexed=writer._add_object(iximage))
def form(prefix):
stream=DecodedStreamObject()
stream.set_data((prefix+'\n/CS cs '+operands(SAMPLE)+' scn 0 0 90 44 re f\n').encode())
stream.update(dictionary(Type=name('XObject'),Subtype=name('Form'),BBox=array((0,0,90,44)),Resources=local_resources))
return writer._add_object(stream)
xobjects[name('Inherited')]=form('')
for i,intent in enumerate(INTENTS):
copy=DecodedStreamObject();copy.set_data(bytes(SAMPLE_BYTES));copy.update(image)
copy[name('Intent')]=name(intent)
xobjects[name('Image'+str(i))]=writer._add_object(copy)
xobjects[name('Form'+str(i))]=form('/'+intent+' ri')
xobjects[name('FormGs'+str(i))]=form('/I'+str(i)+' gs')
shading_function=dictionary(FunctionType=NumberObject(2),Domain=array((0,1)),
C0=array((.1,.3,.6,.2)),C1=array((.5,.7,.9,.4)),N=NumberObject(1))
shading=dictionary(ShadingType=NumberObject(2),ColorSpace=space,Coords=array((0,0,90,0)),
Function=writer._add_object(shading_function))
from pypdf.generic import BooleanObject
shading[name('Extend')]=ArrayObject([BooleanObject(False),BooleanObject(False)])
wide_shading=DictionaryObject(shading);wide_shading[name('Coords')]=array((0,0,WIDTH,0))
wide_ref=writer._add_object(wide_shading)
patterns=dictionary(Inherited=dictionary(Type=name('Pattern'),PatternType=NumberObject(2),Shading=wide_ref))
for i,intent in enumerate(INTENTS):
patterns[name('P'+str(i))]=dictionary(Type=name('Pattern'),PatternType=NumberObject(2),Shading=wide_ref,
ExtGState=dictionary(RI=name(intent)))
resources=dictionary(ColorSpace=dictionary(CS=space,IX=indexed),ExtGState=gs,Font=fonts,
XObject=xobjects,Shading=dictionary(A=writer._add_object(shading)),Pattern=patterns)
for group in range((len(CASES)+7)//8):
page=writer.add_blank_page(WIDTH,HEIGHT);page[name('Resources')]=resources
commands=['0 g BT /F 17 Tf 28 762 Td (ICC rendering intent - '+profile_id+f' - {group+1}/{(len(CASES)+7)//8}) Tj ET']
commands+=['0 g BT /F 8 Tf 28 742 Td (Synthetic input profile; reference colors are computed independently by CMM, not copied from a renderer.) Tj ET']
for column,intent in enumerate(INTENTS):
commands.append(f'0 g BT /F 10 Tf {310+column*125} 716 Td ({intent}) Tj ET')
for local_row,case in enumerate(CASES[group*8:group*8+8]):
y=625-local_row*80
commands.append(f'0 g BT /F 10 Tf 28 {y+22} Td ({case}) Tj ET')
for column in range(4):
intent_index=3-column if case=='image-inherit-shared-reverse' else column
if case=='image-inherit-shared-gs': intent_index=(0,1,0,3)[column]
intent=INTENTS[intent_index];other=(intent_index+1)%4
x=310+column*125
start=['q'];body=[];expected=intent_index;point=(x+45,y+22);components=SAMPLE
if case=='default-relative':
# Start each cell at the page's untouched default. Other
# commands are isolated by q/Q, including all labels.
expected=1
elif case in ('extgstate-ri','image-inherit-shared-gs','form-inherit-shared-gs'): start+=['/I'+str(intent_index)+' gs']
else: start+=['/'+intent+' ri']
paint=f'/CS cs {operands(SAMPLE)} scn {x} {y} 90 44 re f'
if case in ('default-relative','ri-vector','extgstate-ri'): body=[paint]
elif case=='nested-q-inner': body=['q /'+INTENTS[other]+' ri',paint,'Q'];expected=other
elif case=='nested-Q-restore': body=['q /'+INTENTS[other]+' ri Q',paint]
elif case.startswith('form-'):
form_name='Inherited'
if case=='form-ri-override': form_name='Form'+str(other);expected=other
if case=='form-gs-override': form_name='FormGs'+str(other);expected=other
if case=='form-return-restore':
# Invoke the override at the origin, then
# paint with the caller's restored graphics state.
body=[f'/Form{other} Do',paint]
else: body=[f'q 1 0 0 1 {x} {y} cm /{form_name} Do Q']
elif case.startswith('image-') or case=='indexed-image':
image_name='Indexed' if case=='indexed-image' else 'Shared'
if case=='image-intent-override': image_name='Image'+str(other);expected=other
if case=='image-override-return': body=[f'/Image{other} Do',paint]
else: body=[f'q 90 0 0 44 {x} {y} cm /{image_name} Do Q']
elif case=='indexed-vector': body=[f'/IX cs 0 scn {x} {y} 90 44 re f']
elif case=='stroke': body=[f'/CS CS {operands(SAMPLE)} SCN 24 w {x} {y+22} m {x+90} {y+22} l S']
elif case=='text':
body=[f'/CS cs {operands(SAMPLE)} scn BT /F 64 Tf 1 0 0 1 {x} {y} Tm (H) Tj ET']
point=(x+8,y+29)
elif case=='axial-shading':
body=[f'q {x} {y} 90 44 re W n 1 0 0 1 {x} {y} cm /A sh Q']
components=(.3,.5,.75,.3)
elif case.startswith('shading-pattern-'):
pattern='Inherited'
if case=='shading-pattern-RI-override':pattern='P'+str(other);expected=other
body=[f'/Pattern cs /{pattern} scn {x} {y} 90 44 re f']
t=(x+45)/WIDTH;components=tuple(a+(b-a)*t for a,b in zip((.1,.3,.6,.2),(.5,.7,.9,.4)))
elif case.startswith('type3-'):
glyph_font='T3Image' if case=='type3-inherit-image' else 'T3Rect'
if case=='type3-ri-override':glyph_font='T3Override'+str(other);expected=other
body=[f'BT /{glyph_font} 44 Tf 1 0 0 1 {x} {y} Tm (A) Tj ET']
point=(x+22,y+22)
commands+=start+body+['Q']
rows.append({'page':len(writer.pages),'profile':profile_id,'case':case,'column':column,
'graphicsStateIntent':INTENTS[intent_index] if case!='default-relative' else 'default',
'expectedIntent':INTENTS[expected],'expectedIntentIndex':expected,'pointPt':list(point),
'components':list(components),'commandSequence':start+body+['Q'],
**({'shading':{'xStart':x if case=='axial-shading' else 0,'xEnd':x+90 if case=='axial-shading' else WIDTH,
'c0':[.1,.3,.6,.2],'c1':[.5,.7,.9,.4]}}
if case=='axial-shading' or case.startswith('shading-pattern-') else {})})
content=DecodedStreamObject();content.set_data(('\n'.join(commands)+'\n').encode())
page[name('Contents')]=writer._add_object(content)
pdf=output/'rendering-intents.pdf'
with pdf.open('wb') as target:writer.write(target)
manifest={'schemaVersion':1,'file':pdf.name,'sha256':sha(pdf.read_bytes()),'pageSizePt':[WIDTH,HEIGHT],
'pageCount':len(writer.pages),'profiles':profile_records,'baseProfileSha256':sha(base),'sampleBytes':list(SAMPLE_BYTES),
'intentOrder':list(INTENTS),'cases':list(CASES),'probes':rows,'tolerance8Bit':4,
'generatorSha256':sha(Path(__file__).read_bytes()),
'derivedProfileModels':{'A2B0':['100-12*C-15*M-10*Y-35*K','18*M-15*C','20*Y-12*C'],
'A2B1':['100-20*C-20*M-20*Y-40*K','40*M-20*C-20*K','40*Y-20*C-20*K'],
'A2B2':['100-25*C-25*M-20*Y-30*K','-40*M+20*C+20*K','35*Y-20*M-15*K']},
'references':['https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf',
'https://archive.color.org/files/icc32.pdf','https://www.littlecms.com/'],
'limits':['No measured press profile, human-approved golden, display calibration or broad ICC conformance claim.',
'Independent direct-CMM oracle can share LittleCMS code with PDF renderers; not a separate CMM implementation.',
'Text uses an interior point of the standard Helvetica-Bold H; axial samples account for raster pixel centers.']}
(output/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
print(json.dumps({'fileSha256':manifest['sha256'],'pages':len(writer.pages),'probes':len(rows)}))
if __name__=='__main__':
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--output',required=True,type=Path)
generate(parser.parse_args().output)