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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ invocations. A Form's own resources take precedence over inherited page
resources. If reuse under different inherited resources produces different
estimates, the definition's estimate becomes unknown with reason
`context_dependent_color_space`; invocation counting still includes every use.
Version 0.2.1 treats a Form's direct or indirect `/Resources null` like an omitted
entry, preserving the tool's existing legacy resource fallback. An explicitly
empty dictionary remains empty. PDF 32000-1 sections 7.3.7 and 7.3.9 specify
null-entry equivalence; section 7.8.3 describes legacy Form resource inheritance
as obsolete. This compatibility handling is not a modern PDF conformance claim.
Cycles, excessive nesting/invocations, page limits and image limits
produce explicit partial results. Uninvoked resource entries are not scanned.
Top-level content is keyed per page; a content stream shared between pages is
Expand Down Expand Up @@ -178,6 +183,7 @@ python -m unittest discover -s tests -v
Synthetic PDF fixtures test parser-backed payload measurements, filter arrays,
long-form keys, false-positive text strings, bit-packed rows, image masks, repeated
Forms, recursion, indirect named color spaces, Form resource precedence,
null-versus-empty Form resources,
context-dependent reuse, cyclic aliases, unknown color spaces, corruption,
encryption, scan limits,
timeouts and the Windows-compatible spawn-based CLI.
Expand Down
6 changes: 4 additions & 2 deletions pdf_inline_audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@

import pypdf
from pypdf import PdfReader
from pypdf.generic import ContentStream, DictionaryObject
from pypdf.generic import ContentStream, DictionaryObject, NullObject

VERSION = "0.2.0"
VERSION = "0.2.1"


@dataclass(frozen=True)
Expand Down Expand Up @@ -168,6 +168,8 @@ def walk(stream: Any, resources: Any, label: str, page: int,
nested_resources = form.get("/Resources", resources)
if hasattr(nested_resources, "get_object"):
nested_resources = nested_resources.get_object()
if isinstance(nested_resources, NullObject):
nested_resources = resources
walk(form, nested_resources, form_label(form), page, depth + 1, active)

try:
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.0"
version = "0.2.1"
description = "Reproducible corpus statistics for PDF inline image payloads"
readme = "README.md"
requires-python = ">=3.10"
Expand Down
32 changes: 31 additions & 1 deletion tests/test_audit.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from pathlib import Path
from pypdf import PdfWriter
from pypdf.generic import ArrayObject, BooleanObject, ByteStringObject, DecodedStreamObject, DictionaryObject, NameObject, NumberObject
from pypdf.generic import ArrayObject, BooleanObject, ByteStringObject, DecodedStreamObject, DictionaryObject, NameObject, NullObject, NumberObject
from pdf_inline_audit import Limits, distribution, isolated_scan, main, sample_estimate, scan_pdf

def make_pdf(path, streams, form_data=None, recursive=False, color_spaces=None, form_color_spaces=None):
Expand Down Expand Up @@ -171,6 +171,36 @@ def test_form_colorspace_overrides_page_resource_name(self):
assert result['status'] == 'ok'
assert result['images'][0]['estimated_packed_sample_bytes'] == 12

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'),
color_spaces=[{'/Custom': NameObject('/DeviceGray')}])
cases = [
('absent', None, False, 1),
('direct-null', NullObject(), False, 1),
('indirect-null', NullObject(), True, 1),
('empty-dictionary', DictionaryObject(), False, None),
]
for name, resources, indirect, expected in cases:
with self.subTest(resources=name):
path = self.tmp_path / f'{name}.pdf'
writer = PdfWriter(clone_from=str(source))
form = writer.pages[0]['/Resources']['/XObject']['/Fm'].get_object()
if resources is not None:
form[NameObject('/Resources')] = (
writer._add_object(resources) if indirect else resources)
writer.write(path)
writer.close()
result = scan_pdf(path)
assert result['status'] == 'ok'
assert result['errors'] == []
assert len(result['images']) == 1
image = result['images'][0]
assert image['invocations'] == 2
assert image['estimated_packed_sample_bytes'] == expected
reason = 'unresolved_or_unsupported_color_space' if expected is None else None
assert image['estimate_unknown_reason'] == reason

def test_reused_form_with_conflicting_inherited_colorspaces_is_unknown(self):
path = make_pdf(self.tmp_path / 'context-dependent.pdf', [b'/Fm Do', b'/Fm Do'],
inline(b'000102>', b'/W 1 /H 1 /BPC 8 /CS /Custom /F /AHx'),
Expand Down
Loading