#!/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')