initial commit
This commit is contained in:
+214
@@ -0,0 +1,214 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Self-authored RI probes for luminosity masks and pattern initial state."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from pypdf import PdfWriter
|
||||
from pypdf.generic import (ArrayObject, BooleanObject, DecodedStreamObject,
|
||||
DictionaryObject, FloatObject, NameObject, NumberObject)
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
SOURCE = ROOT / 'tests/fixtures/pdf/rendering-intents/distinct-cmyk-lab.icc'
|
||||
PROFILE_SHA = 'b9a40bfc18e8280eefe0fedc4e607467e1cc27adeee3beb77f4a2b83fc7e9315'
|
||||
INTENTS = ('Perceptual', 'RelativeColorimetric', 'Saturation', 'AbsoluteColorimetric')
|
||||
SAMPLE = (0, .75, .25, 0)
|
||||
WIDTH, HEIGHT = 880, 850
|
||||
CASES = (
|
||||
'direct-icc-control', 'mask-caller-ri', 'mask-internal-ri', 'mask-internal-gs',
|
||||
'mask-group-qQ', 'mask-q-inner-none', 'mask-Q-restores', 'mask-none',
|
||||
'mask-BC-only', 'mask-opaque-over-BC',
|
||||
'tiling-page-initial', 'tiling-colored-ri', 'tiling-colored-gs', 'tiling-colored-qQ',
|
||||
'tiling-form-initial', 'tiling-form-initial-reuse', 'tiling-form-ri',
|
||||
'tiling-uncolored-fill', 'tiling-uncolored-fill-gs',
|
||||
'tiling-uncolored-stroke-small', 'tiling-uncolored-stroke-large',
|
||||
'shading-page-initial', 'shading-pattern-ri', 'shading-form-initial',
|
||||
'shading-form-initial-reuse', 'shading-form-ri', 'direct-shading-control',
|
||||
)
|
||||
|
||||
|
||||
def digest(data): return hashlib.sha256(data).hexdigest()
|
||||
def n(value): return NameObject('/' + value)
|
||||
def a(values): return ArrayObject([FloatObject(v) for v in values])
|
||||
def d(**values): return DictionaryObject({n(k): v for k, v in values.items()})
|
||||
def numbers(values): return ' '.join(format(v, '.10g') for v in values)
|
||||
|
||||
|
||||
def generate(output):
|
||||
output.mkdir(parents=True, exist_ok=False)
|
||||
profile = SOURCE.read_bytes()
|
||||
assert digest(profile) == PROFILE_SHA
|
||||
(output / SOURCE.name).write_bytes(profile)
|
||||
writer = PdfWriter(); writer.pdf_header = '%PDF-1.7'
|
||||
writer.add_metadata({'/Title': 'Rendering intent: soft masks and pattern initial state',
|
||||
'/Author': 'DocView synthetic regression corpus'})
|
||||
|
||||
def stream(data, **entries):
|
||||
obj = DecodedStreamObject(); obj.set_data(data.encode() if isinstance(data, str) else data)
|
||||
obj.update(d(**entries)); return writer._add_object(obj)
|
||||
|
||||
cs = ArrayObject([n('ICCBased'), stream(profile, N=NumberObject(4), Alternate=n('DeviceCMYK'))])
|
||||
colors = d(CS=cs, PCS=ArrayObject([n('Pattern'), cs]))
|
||||
gs = d(**{'I'+str(i): d(Type=n('ExtGState'), RI=n(intent)) for i, intent in enumerate(INTENTS)})
|
||||
gs[n('NoMask')] = d(Type=n('ExtGState'), SMask=n('None'))
|
||||
font = writer._add_object(d(Type=n('Font'), Subtype=n('Type1'), BaseFont=n('Helvetica')))
|
||||
base = d(ColorSpace=colors, ExtGState=gs)
|
||||
paint = '/CS cs ' + numbers(SAMPLE) + ' scn '
|
||||
group_attributes = d(S=n('Transparency'), CS=n('DeviceRGB'), I=BooleanObject(True), K=BooleanObject(False))
|
||||
|
||||
def group(prefix='', empty=False):
|
||||
return stream(prefix + '\n' + ('' if empty else paint + f'0 0 {WIDTH} {HEIGHT} re f\n'),
|
||||
Type=n('XObject'), Subtype=n('Form'), BBox=a((0,0,WIDTH,HEIGHT)),
|
||||
Group=group_attributes, Resources=base)
|
||||
|
||||
shared_group = group()
|
||||
empty_group = group(empty=True)
|
||||
def mask(name, group_ref, bc=(0,0,0)):
|
||||
gs[n(name)] = d(Type=n('ExtGState'), SMask=d(S=n('Luminosity'), G=group_ref,
|
||||
BC=a(bc), TR=n('Identity')))
|
||||
mask('Mask', shared_group)
|
||||
mask('OpaqueBC', shared_group, (0,.5,1))
|
||||
backgrounds = ((1,0,0), (0,1,0), (0,0,1), (0,.5,1))
|
||||
for i, intent in enumerate(INTENTS):
|
||||
mask('MaskRi'+str(i), group('/'+intent+' ri'))
|
||||
mask('MaskGs'+str(i), group('/I'+str(i)+' gs'))
|
||||
mask('MaskQ'+str(i), group('q /'+intent+' ri Q'))
|
||||
mask('BC'+str(i), empty_group, backgrounds[i])
|
||||
|
||||
patterns = d()
|
||||
def tiling(prefix='', colored=True, size=16):
|
||||
content = prefix+'\n'+(paint if colored else '')+f'0 0 {size} {size} re f\n'
|
||||
return stream(content, Type=n('Pattern'), PatternType=NumberObject(1), PaintType=NumberObject(1 if colored else 2),
|
||||
TilingType=NumberObject(2), BBox=a((0,0,size,size)), XStep=NumberObject(size), YStep=NumberObject(size),
|
||||
Resources=base if colored else d())
|
||||
patterns[n('Tile')] = tiling()
|
||||
patterns[n('U')] = tiling(colored=False, size=8)
|
||||
patterns[n('ULarge')] = tiling(colored=False, size=1024)
|
||||
for i, intent in enumerate(INTENTS):
|
||||
patterns[n('TileRi'+str(i))] = tiling('/'+intent+' ri')
|
||||
patterns[n('TileGs'+str(i))] = tiling('/I'+str(i)+' gs')
|
||||
patterns[n('TileQ'+str(i))] = tiling('q /'+intent+' ri Q')
|
||||
# Constant ICC axial function eliminates interpolation-position uncertainty.
|
||||
function = writer._add_object(d(FunctionType=NumberObject(2), Domain=a((0,1)), C0=a(SAMPLE), C1=a(SAMPLE), N=NumberObject(1)))
|
||||
shading = writer._add_object(d(ShadingType=NumberObject(2), ColorSpace=cs, Coords=a((0,0,WIDTH,0)),
|
||||
Function=function, Extend=ArrayObject([BooleanObject(True),BooleanObject(True)])))
|
||||
patterns[n('Shade')] = d(Type=n('Pattern'), PatternType=NumberObject(2), Shading=shading)
|
||||
for i, intent in enumerate(INTENTS):
|
||||
patterns[n('ShadeRi'+str(i))] = d(Type=n('Pattern'), PatternType=NumberObject(2), Shading=shading,
|
||||
ExtGState=d(RI=n(intent)))
|
||||
# A shared Form is invoked under distinct initial RI values. Its painting RI
|
||||
# is always Absolute, which must not replace the pattern's stream-start RI.
|
||||
form_resources = d(ColorSpace=colors, ExtGState=gs, Pattern=patterns)
|
||||
xobjects = d()
|
||||
for label, pattern_name in (('Tile','Tile'),('Shade','Shade')):
|
||||
xobjects[n(label+'Form')] = stream('/AbsoluteColorimetric ri\n/Pattern cs /'+pattern_name+' scn 0 0 90 42 re f\n',
|
||||
Type=n('XObject'), Subtype=n('Form'), BBox=a((0,0,90,42)), Resources=form_resources)
|
||||
for i in range(4):
|
||||
xobjects[n(label+'FormRi'+str(i))] = stream('/AbsoluteColorimetric ri\n/Pattern cs /'+pattern_name+'Ri'+str(i)+' scn 0 0 90 42 re f\n',
|
||||
Type=n('XObject'), Subtype=n('Form'), BBox=a((0,0,90,42)), Resources=form_resources)
|
||||
resources = d(ColorSpace=colors, ExtGState=gs, Pattern=patterns, XObject=xobjects,
|
||||
Shading=d(A=shading), Font=d(F=font))
|
||||
probes=[]
|
||||
for group_index in range((len(CASES)+6)//7):
|
||||
page=writer.add_blank_page(WIDTH,HEIGHT);page[n('Resources')]=resources
|
||||
commands=[f'1 g 0 0 {WIDTH} {HEIGHT} re f',
|
||||
f'0 g BT /F 18 Tf 24 818 Td (Soft masks and pattern state - {group_index+1}) Tj ET',
|
||||
'0 g BT /F 9 Tf 24 799 Td (Synthetic ICC; expected intent comes from PDF state rules, colors from a direct CMM.) Tj ET']
|
||||
for i,intent in enumerate(INTENTS):
|
||||
commands.append(f'0 g BT /F 9 Tf {320+i*132} 771 Td ({intent}) Tj ET')
|
||||
for row,case in enumerate(CASES[group_index*7:group_index*7+7]):
|
||||
y=684-row*96
|
||||
commands.append(f'0 g BT /F 10 Tf 24 {y+22} Td ({case}) Tj ET')
|
||||
for column in range(4):
|
||||
current=(2,0,2,1)[column] if case.endswith('-reuse') else column
|
||||
other=(current+1)%4;expected=current;kind='icc';bc=None
|
||||
x=320+column*132;rect=f'{x} {y} 90 42 re f'
|
||||
start=['q','/'+INTENTS[current]+' ri'];body=[]
|
||||
if case=='direct-icc-control': body=[paint+rect]
|
||||
elif case.startswith('mask-'):
|
||||
kind='luminosity-icc';mask_name='Mask'
|
||||
if case=='mask-internal-ri':mask_name='MaskRi'+str(other);expected=other
|
||||
elif case=='mask-internal-gs':mask_name='MaskGs'+str(other);expected=other
|
||||
elif case=='mask-group-qQ':mask_name='MaskQ'+str(other)
|
||||
elif case=='mask-BC-only':mask_name='BC'+str(column);bc=backgrounds[column];kind='luminosity-bc'
|
||||
elif case=='mask-opaque-over-BC':mask_name='OpaqueBC'
|
||||
body=['/'+mask_name+' gs']
|
||||
if case=='mask-q-inner-none':body+=['q /NoMask gs','0 g '+rect,'Q'];kind='black'
|
||||
elif case=='mask-Q-restores':body+=['q /NoMask gs Q','0 g '+rect]
|
||||
elif case=='mask-none':body+=['/NoMask gs','0 g '+rect];kind='black'
|
||||
else:body+=['0 g '+rect]
|
||||
elif case.startswith('tiling-uncolored'):
|
||||
if case.endswith('-gs'):start=['q','/I'+str(current)+' gs']
|
||||
if 'stroke' in case:
|
||||
pattern='ULarge' if case.endswith('-large') else 'U'
|
||||
body=['1 0 0 rg','/PCS CS '+numbers(SAMPLE)+' /'+pattern+' SCN',
|
||||
f'30 w {x} {y+21} m {x+90} {y+21} l S']
|
||||
else:body=['/PCS cs '+numbers(SAMPLE)+' /U scn '+rect]
|
||||
elif case.startswith('tiling-') or case.startswith('shading-'):
|
||||
label='Tile' if case.startswith('tiling-') else 'Shade';pattern=label
|
||||
if '-form-' in case:
|
||||
form_name=label+'Form'
|
||||
if case.endswith('-ri'):form_name+='Ri'+str(other);expected=other
|
||||
body=[f'1 0 0 1 {x} {y} cm /'+form_name+' Do']
|
||||
else:
|
||||
expected=1
|
||||
if case.endswith('-ri'):pattern+='Ri'+str(other);expected=other
|
||||
elif case.endswith('-gs'):pattern+='Gs'+str(other);expected=other
|
||||
elif case.endswith('-qQ'):pattern+='Q'+str(other)
|
||||
body=['/Pattern cs /'+pattern+' scn '+rect]
|
||||
elif case=='direct-shading-control':body=[f'{x} {y} 90 42 re W n','/A sh']
|
||||
else:raise AssertionError(case)
|
||||
sequence=start+body+['Q'];commands+=sequence
|
||||
probes.append({'id':case+'-'+str(column),'case':case,'column':column,'page':group_index+1,
|
||||
'callerIntentIndex':current,'callerIntent':INTENTS[current],
|
||||
'expectedIntentIndex':expected,'expectedIntent':INTENTS[expected],
|
||||
'oracle':kind,'components':list(SAMPLE),'backgroundRgb':list(bc) if bc else None,
|
||||
'pointPt':[x+45,y+21],'commandSequence':sequence})
|
||||
page[n('Contents')]=stream('\n'.join(commands)+'\n')
|
||||
target=output/'intent-transparency.pdf';writer.write(target)
|
||||
manifest={'schemaVersion':1,'file':target.name,'sha256':digest(target.read_bytes()),
|
||||
'generatorSha256':digest(Path(__file__).read_bytes()),'pageCount':len(writer.pages),'pageSizePt':[WIDTH,HEIGHT],
|
||||
'profile':{'file':SOURCE.name,'sha256':PROFILE_SHA,'size':len(profile),'origin':str(SOURCE.relative_to(ROOT))},
|
||||
'sample':list(SAMPLE),'tolerance8Bit':4,'cases':list(CASES),'probes':probes,
|
||||
'specification':{'url':'https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf',
|
||||
'printedPages':{'uncoloredForbiddenOperators':218,'tilingInitialState':223,'shadingInitialState':231,
|
||||
'deviceRgbGray':377,'luminosityMask':440,'softMaskDictionary':446,'transparentPatterns':453}},
|
||||
'luminosityOracle':{'weights':[.30,.59,.11],'foregroundRgb':[0,0,0],'pageBackgroundRgb':[255,255,255],
|
||||
'formula':'round(255 - dot(weights, directCmmRgb8))',
|
||||
'qualification':'DeviceRGB gray conversion is device-dependent; these are the PDF recommended weights. Intent dispatch is tested separately from that numerical convention.'}}
|
||||
(output/'manifest.json').write_text(json.dumps(manifest,indent=2)+'\n')
|
||||
(output/'README.md').write_text('''# Soft-mask and pattern rendering-intent probes
|
||||
|
||||
This self-authored 4-page PDF contains 108 declared pixel probes. The existing
|
||||
984-byte distinct CMYK/Lab ICC profile is copied without alteration. Its four
|
||||
intent outputs for CMYK (0, .75, .25, 0) have separated luminances; no PDF renderer
|
||||
supplies the expected colors.
|
||||
|
||||
[Adobe PDF Reference 1.4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.4.pdf)
|
||||
pp. 218, 223, 231 and 453 define pattern state. Colored tiling and shading patterns
|
||||
start with their resource-owning page/Form stream's initial state. A Form is
|
||||
reused under different initial intents, then changes its painting intent to
|
||||
Absolute. Explicit pattern overrides and q/Q are separate controls. Uncolored
|
||||
stencils contain no color operators or `ri`; their ICC base color is supplied by
|
||||
the caller. Small/large uncolored strokes distinguish stroke from fill color.
|
||||
|
||||
Pages 440–446 define luminosity soft masks and their backdrop. The mask's RGB
|
||||
group contains one opaque ICC rectangle, or no objects for the BC-only controls.
|
||||
The direct CMM RGB result is reduced with the recommended .30/.59/.11 weights;
|
||||
black over white has value 255 minus that luminosity. This device-dependent
|
||||
formula is a declared test convention, not universal device calibration.
|
||||
|
||||
Run `tests/cmyk_lut/transparency.py --self-check --output NEW_DIRECTORY`, or pass
|
||||
`--build-dir build --pdfium-library PATH/libpdfium.so --output NEW_DIRECTORY`.
|
||||
The runner records all failures, validates the sandbox helper and library hash,
|
||||
and judges PDFium and Poppler separately against the direct CMM. LittleCMS may
|
||||
be shared with the renderers; this is not an independent CMM implementation or
|
||||
general ICC conformance acceptance. The older 8-page intent fixture is unchanged.
|
||||
''')
|
||||
print(json.dumps({'file':str(target),'sha256':manifest['sha256'],'pages':len(writer.pages),'probes':len(probes)}))
|
||||
|
||||
|
||||
if __name__=='__main__':
|
||||
parser=argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output',type=Path,default=Path(__file__).parent/'intent-transparency')
|
||||
generate(parser.parse_args().output)
|
||||
Reference in New Issue
Block a user