96 lines
5.6 KiB
Python
96 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Deterministic self-created links; no third-party document or font content."""
|
|
from pathlib import Path
|
|
import hashlib
|
|
import json
|
|
|
|
ROOT = Path(__file__).parent / 'link-borders'
|
|
ROOT.mkdir(exist_ok=True)
|
|
|
|
def stream(content, extra=''):
|
|
content = content.encode('ascii')
|
|
return f'<< /Length {len(content)} {extra} >>\nstream\n'.encode() + content + b'\nendstream'
|
|
|
|
def write(name, objects):
|
|
data = bytearray(b'%PDF-1.7\n%\xe2\xe3\xcf\xd3\n')
|
|
offsets = [0]
|
|
for i, value in enumerate(objects, 1):
|
|
offsets.append(len(data))
|
|
if isinstance(value, str): value = value.encode('ascii')
|
|
data += f'{i} 0 obj\n'.encode() + value + 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\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n'.encode()
|
|
(ROOT / name).write_bytes(data)
|
|
return {'file': name, 'bytes': len(data), 'sha256': hashlib.sha256(data).hexdigest()}
|
|
|
|
CASES = [
|
|
('default', ''),
|
|
('solid-rgb', '/BS << /W 3 /S /S >> /C [0.8 0.1 0.2]'),
|
|
('dashed-border', '/Border [0 0 2 [6 3]] /C [0 0.2 0.8]'),
|
|
('dashed-bs-odd', '/BS << /W 2 /S /D /D [7 3 2] >> /C [0.1 0.5 0.2]'),
|
|
('dashed-default', '/BS << /W 2 /S /D >>'),
|
|
('underline', '/BS << /W 3 /S /U >> /C [0.6 0.1 0.8]'),
|
|
('bs-overrides-border', '/Border [9 9 12 [1 1]] /BS << /W 2 /S /S >> /C [0 0.3 0.7]'),
|
|
('zero-width', '/BS << /W 0 >>'),
|
|
('transparent', '/BS << /W 4 >> /C []'),
|
|
('hidden', '/BS << /W 4 >> /F 2'),
|
|
('no-view', '/BS << /W 4 >> /F 32'),
|
|
('print-flag-visible', '/BS << /W 2 >> /F 4'),
|
|
('gray', '/BS << /W 3 >> /C [0.5]'),
|
|
('cmyk', '/BS << /W 3 >> /C [1 0 0 0]'),
|
|
('rounded', '/Border [8 6 2] /C [0.6 0.3 0.1]'),
|
|
('beveled', '/BS << /W 3 /S /B >> /C [0.2 0.6 0.8]'),
|
|
('inset', '/BS << /W 3 /S /I >> /C [0.2 0.6 0.8]'),
|
|
('appearance-preserved', '/BS << /W 8 /S /D >> /C [1 0 0] /AP << /N 7 0 R >>'),
|
|
('empty-appearance-preserved', '/BS << /W 8 >> /AP << /N 8 0 R >>'),
|
|
('alpha', '/BS << /W 3 >> /C [0 0 1] /CA 0.5'),
|
|
]
|
|
|
|
# All pages share the same annotations, but their CropBox and Rotate differ.
|
|
# Rects straddle 512px tile boundaries at the evidence renderer's scale 2.
|
|
objects = [
|
|
'<< /Type /Catalog /Pages 2 0 R >>',
|
|
'<< /Type /Pages /Count 4 /Kids [3 0 R 4 0 R 5 0 R 6 0 R] >>',
|
|
'', '', '', '',
|
|
stream('0 0.8 0 rg 0 0 100 30 re f', '/Type /XObject /Subtype /Form /BBox [0 0 100 30] /Resources << >>'),
|
|
stream('', '/Type /XObject /Subtype /Form /BBox [0 0 100 30] /Resources << >>'),
|
|
]
|
|
expected = []
|
|
for index, (label, extra) in enumerate(CASES):
|
|
x = 40 + (index % 4) * 120
|
|
y = 50 + (index // 4) * 75
|
|
rect = [x, y, x + 100, y + 30]
|
|
objects.append(f'<< /Type /Annot /Subtype /Link /Rect [{" ".join(map(str, rect))}] '
|
|
f'/A << /S /URI /URI (https://example.org/{label}) >> {extra} >>')
|
|
expected.append({'name': label, 'rect': rect, 'has_border': label not in {'zero-width', 'transparent', 'hidden', 'no-view', 'empty-appearance-preserved'}})
|
|
annots = ' '.join(f'{i + 9} 0 R' for i in range(len(CASES)))
|
|
for page, rotation in enumerate([0, 90, 180, 270]):
|
|
objects[page + 2] = f'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 550 470] /CropBox [20 30 530 450] /Rotate {rotation} /Resources << >> /Annots [{annots}] >>'
|
|
records = [write('styles.pdf', objects)]
|
|
records[0]['pages'] = 4
|
|
records[0]['annotations'] = expected
|
|
records[0]['reference_limitations'] = ['Poppler ignores gray/CMYK border colors, rounded corners, bevel/inset, and alpha in its no-AP link fallback; assert these independently.', 'Saved normal AP must be preserved without adding a second border, even if Poppler adds one.']
|
|
|
|
for name, extra in [('unknown-style', '/BS << /S /Unknown >>'), ('no-zoom', '/F 8'), ('no-rotate', '/F 16'), ('invalid-dash', '/BS << /S /D /D [0 0] >>'), ('oversized-dash', '/BS << /S /D /D [' + '1 ' * 65 + '] >>'), ('invalid-color', '/C [0 1]'), ('invalid-ap', '/AP << /N 42 >>')]:
|
|
data = ['<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Count 1 /Kids [3 0 R] >>',
|
|
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << >> /Annots [4 0 R] >>',
|
|
f'<< /Type /Annot /Subtype /Link /Rect [20 20 80 80] {extra} >>']
|
|
record = write(name + '.pdf', data)
|
|
if name in {'no-zoom', 'no-rotate'}:
|
|
record['expected'] = 'Valid annotation flag; rendering succeeds. Geometry coverage is in annotation-flags/flags.pdf.'
|
|
else:
|
|
record['render_error'] = 'E_PDF_ANNOT_UNSUPPORTED' if name == 'unknown-style' else 'E_LIMIT_EXCEEDED' if name == 'oversized-dash' else 'E_PDF_CORRUPT'
|
|
records.append(record)
|
|
retry = ['<< /Type /Catalog /Pages 2 0 R >>', '<< /Type /Pages /Count 2 /Kids [3 0 R 4 0 R] >>', '',
|
|
'<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << >> /Annots [5 0 R] >>']
|
|
for index in range(512):
|
|
retry.append('<< /Type /Annot /Subtype /Link /Rect [20 20 80 80] /BS << /S /D /D [' + '1 ' * 64 + '] >> >>')
|
|
retry.append('<< /Type /Annot /Subtype /Link /Rect [20 20 80 80] /BS << /S /Unknown >> >>')
|
|
retry[2] = '<< /Type /Page /Parent 2 0 R /MediaBox [0 0 100 100] /Resources << >> /Annots [' + ' '.join(f'{i} 0 R' for i in range(5, len(retry) + 1)) + '] >>'
|
|
record = write('retry-budget.pdf', retry)
|
|
record['expected'] = 'Forty failed page-0 decodes must not charge discarded appearances or prevent valid page-1 rendering.'
|
|
records.append(record)
|
|
(ROOT / 'manifest.json').write_text(json.dumps({'generator': 'generate_link_borders.py', 'license': 'Self-created test content; same license as DocView.', 'fixtures': records}, indent=2) + '\n')
|