Files
docview/tests/cmyk_lut/diagnose.py
T
2026-09-21 13:41:40 +09:00

82 lines
5.0 KiB
Python

#!/usr/bin/env python3
"""Explain the retained failing CMYK comparison without relaxing its criterion."""
from io import BytesIO
import hashlib
import json
from pathlib import Path
from PIL import Image, ImageCms
from pypdf import PdfReader
ROOT = Path(__file__).resolve().parents[2]
RESULTS = ROOT / 'tests/results/cmyk-lut'
def sha(path):
with path.open('rb') as stream:
return hashlib.file_digest(stream, 'sha256').hexdigest()
def main():
corpus=ROOT/'tests/fixtures/pdf/cmyk-lut'
spec=json.loads((corpus/'manifest.json').read_text())
pdf=PdfReader(corpus/spec['file'])
assert b'/RelativeColorimetric ri' in pdf.pages[0].get_contents().get_data()
assert pdf.pages[0]['/Resources']['/XObject']['/Sample']['/Intent']=='/RelativeColorimetric'
source=json.loads((RESULTS/'pdfium-source/record.json').read_text())
for row in source['files']:
assert sha(RESULTS/'pdfium-source'/row['path'])==row['sha256']
transform_source=(RESULTS/'pdfium-source/core/fxcodec/icc/icc_transform.cpp').read_text()
assert 'TYPE_BGR_8, INTENT_PERCEPTUAL, /*dwFlags=*/0' in transform_source
profile=ImageCms.ImageCmsProfile(BytesIO((corpus/spec['profileFile']).read_bytes()))
transformations={intent:ImageCms.buildTransformFromOpenProfiles(profile,ImageCms.createProfile('sRGB'),
'CMYK','RGB',renderingIntent=value) for intent,value in [('perceptual',0),('relative',1)]}
colors={}
for probe in spec['probes']:
if probe['kind']!='icc-image': continue
pixel=Image.new('CMYK',(1,1),tuple(round(c*255) for c in probe['components']))
colors[probe['sample']]={name:list(ImageCms.applyTransform(pixel,t).getpixel((0,0))) for name,t in transformations.items()}
rows=[]
for os_name in ('arch','ubuntu'):
report=json.loads((RESULTS/os_name/'report.json').read_text())
assert not report['success'] and report['fixture']['sha256']==spec['sha256']
assert report['originalUnchanged'] and report['binariesUnchanged']
assert report['runnerSha256']==sha(ROOT/'tests/cmyk_lut/run.py')
for name,digest in report['evidenceSha256'].items():
assert sha(RESULTS/os_name/name)==digest
failed=0; matches=[]
for scale in report['scales']:
failed+=sum(not p['success'] for p in scale['probes'])
for probe in scale['probes']:
if probe['kind']!='icc-image': continue
pair=colors[probe['sample']]
errors={name:max(abs(a-b) for a,b in zip(probe['actualRgb'][name],pair[intent]))
for name,intent in [('pdfium','perceptual'),('poppler','relative')]}
matches.append({'scale':scale['scale'],'sample':probe['sample'],'actualRgb':probe['actualRgb'],
'cmmIntentColors':pair,'errorsAgainstRespectiveIntent':errors})
failed_by_renderer={name:sum(p['maxChannelError'].get(name,0)>spec['tolerance8Bit']
for scale in report['scales'] for p in scale['probes'])
for name in ('pdfium','poppler')}
maximum=max(p['maxChannelError'].get('pdfium',0) for scale in report['scales'] for p in scale['probes'])
assert failed==failed_by_renderer['pdfium']==30 and failed_by_renderer['poppler']==0 and maximum==74
assert all(max(m['errorsAgainstRespectiveIntent'].values())<=1 for m in matches)
rows.append({'os':os_name,'comparisonReportSha256':sha(RESULTS/os_name/'report.json'),
'analyticProbes':48,'failedPdfiumAnalyticProbes':failed,'failedPopplerAnalyticProbes':failed_by_renderer['poppler'],
'maximumPdfiumAnalyticChannelDifference':maximum,'intentMatchedImageProbes':matches})
result={'diagnosisVerified':True,'renderingAcceptance':False,'relativeIntentPreserved':False,
'toleranceUnchanged':spec['tolerance8Bit'],'profileSha256':spec['profileSha256'],
'sourceRevision':source['revision'],'sourceEvidenceSha256':sha(RESULTS/'pdfium-source/record.json'),
'diagnoserSha256':sha(Path(__file__)),'cmmVersion':ImageCms.core.littlecms_version,'results':rows,
'finding':'At this fixed PDFium revision, ICC conversion selects INTENT_PERCEPTUAL regardless of the '
'relative-colorimetric intent in the test PDF. On both OSs, all eight image swatches at all '
'three scales match LittleCMS perceptual output within one channel level. Poppler matches '
'relative-colorimetric output. This explains, but does not accept or repair, the difference.',
'remaining':'G-RENDER requires review and resolution/explicit compatibility judgment. No renderer '
'patch, engine substitution or user-visible detection was implemented in this comparison.'}
(RESULTS/'diagnosis.json').write_text(json.dumps(result,indent=2)+'\n')
print(json.dumps({'diagnosisVerified':True,'renderingAcceptance':False,'failedAnalyticProbes':60,
'intentMatchedImageProbes':48}))
if __name__=='__main__': main()