Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ helpers may discard or retain some delimiter whitespace. The second field is
**not measured decoded pixels**, not an extracted PNG/JPEG file size, and not
allocated image memory. Device and Indexed color spaces can be resolved directly
or by name in the current page/Form `/Resources` dictionary. Indexed images use
one component per sample, regardless of the palette's base space. Other custom
spaces (including ICCBased, Separation and DeviceN), missing names, and cyclic
one component per sample, regardless of the palette's base space. Version 0.3.0
also resolves named CalGray (one component), CalRGB and Lab (three components)
resources, including indirect parameter dictionaries. Only the family and
dictionary shape are inspected; colour conversion parameters are not validated.
Other custom spaces (including ICCBased, Separation and DeviceN), missing names, and cyclic
aliases remain explicitly unknown. Lookup is bounded to 32 steps and never
reads image pixels or decodes profiles. See PDF 32000-1, sections 7.8.3 and 8.9.7
reads image pixels or decodes profiles. See PDF 32000-1, sections 7.8.3,
8.6.5.2–8.6.5.4 and 8.9.7
([Adobe reference](https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf)).
DCT payloads are never treated as decoded rasters. PDF 32000-1 sections 7.4.9
and 8.9.7 prohibit JPXDecode and JBIG2Decode for inline images; JPX image XObjects
Expand Down
15 changes: 12 additions & 3 deletions pdf_inline_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from pypdf import PdfReader
from pypdf.generic import ContentStream, DictionaryObject, NullObject

VERSION = "0.2.1"
VERSION = "0.3.0"


@dataclass(frozen=True)
Expand All @@ -34,14 +34,23 @@ class ScanLimit(Exception):


def color_components(color: Any, resources: dict | None = None) -> int | None:
"""Resolve known device/Indexed spaces, without decoding profiles or pixels."""
"""Resolve known component counts without decoding profiles or pixels."""
spaces = None
seen = set()
for _ in range(32):
if hasattr(color, "get_object"):
color = color.get_object()
if isinstance(color, list):
return 1 if color and str(color[0]) in ("/I", "/Indexed") else None
if color and str(color[0]) in ("/I", "/Indexed"):
return 1
if len(color) != 2:
return None
parameters = color[1]
if hasattr(parameters, "get_object"):
parameters = parameters.get_object()
if not isinstance(parameters, dict):
return None
return {"/CalGray": 1, "/CalRGB": 3, "/Lab": 3}.get(str(color[0]))
if not isinstance(color, str):
return None
channels = {
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "pdf-inline-audit"
version = "0.2.1"
version = "0.3.0"
description = "Reproducible corpus statistics for PDF inline image payloads"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
34 changes: 34 additions & 0 deletions tests/test_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,40 @@ def test_form_colorspace_overrides_page_resource_name(self):
assert result['status'] == 'ok'
assert result['images'][0]['estimated_packed_sample_bytes'] == 12

def test_named_cie_spaces_resolve_indirect_resources_and_parameters(self):
for family, expected in [('/CalGray', 6), ('/CalRGB', 18), ('/Lab', 18)]:
with self.subTest(family=family):
params = DictionaryObject({NameObject('/WhitePoint'): ArrayObject([
NumberObject(1), NumberObject(1), NumberObject(1)])})
space = ArrayObject([NameObject(family), params])
payload = b'00' * expected + b'>'
path = make_pdf(self.tmp_path / f'{family[1:]}.pdf',
[inline(payload, b'/W 2 /H 3 /BPC 8 /CS /Alias /F /AHx')],
color_spaces=[{'/Alias': NameObject('/CIE'), '/CIE': space}])
writer = PdfWriter(clone_from=str(path))
color = writer.pages[0]['/Resources']['/ColorSpace']['/CIE'].get_object()
color[1] = writer._add_object(color[1])
writer.write(path)
writer.close()
result = scan_pdf(path)
assert result['status'] == 'ok'
image = result['images'][0]
assert image['estimated_packed_sample_bytes'] == expected
assert image['estimate_unknown_reason'] is None
assert image['parsed_encoded_payload_bytes'] == len(payload)

def test_cie_row_padding_and_unsupported_descriptions(self):
for family, expected in [('/CalGray', 4), ('/CalRGB', 10), ('/Lab', 10)]:
with self.subTest(family=family):
assert sample_estimate({'/W': 3, '/H': 2, '/BPC': 4,
'/CS': '/CIE'},
{'/ColorSpace': {'/CIE': [family, {}]}}) == (expected, None)
for color in [[], ['/CalGray'], ['/CalRGB', None], ['/Lab', {}, {}],
['/ICCBased', {}], ['/Separation', {}], '/CalRGB']:
with self.subTest(color=color):
assert sample_estimate({'/W': 3, '/H': 2, '/BPC': 4, '/CS': color}) == (
None, 'unresolved_or_unsupported_color_space')

def test_null_form_resources_match_absence_but_empty_dictionary_does_not(self):
source = make_pdf(self.tmp_path / 'source.pdf', [b'/Fm Do /Fm Do'],
inline(b'00>', b'/W 1 /H 1 /BPC 8 /CS /Custom /F /AHx'),
Expand Down
Loading