spotpdf 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spotpdf/__init__.py +54 -0
- spotpdf/__main__.py +3 -0
- spotpdf/alternate.py +146 -0
- spotpdf/alternate_plan.py +286 -0
- spotpdf/alternate_validation.py +272 -0
- spotpdf/budget_content.py +123 -0
- spotpdf/budget_graph.py +83 -0
- spotpdf/budget_preflight.py +60 -0
- spotpdf/cli.py +128 -0
- spotpdf/cli_dry_run.py +31 -0
- spotpdf/cli_limits.py +90 -0
- spotpdf/cli_output.py +477 -0
- spotpdf/cli_parser.py +254 -0
- spotpdf/cmyk.py +144 -0
- spotpdf/colors.py +148 -0
- spotpdf/content.py +401 -0
- spotpdf/content_support.py +105 -0
- spotpdf/convert.py +143 -0
- spotpdf/convert_aliases.py +303 -0
- spotpdf/convert_content.py +583 -0
- spotpdf/convert_operators.py +67 -0
- spotpdf/convert_plan.py +128 -0
- spotpdf/convert_resource_contexts.py +307 -0
- spotpdf/convert_resources.py +121 -0
- spotpdf/convert_state.py +64 -0
- spotpdf/convert_stream_owners.py +243 -0
- spotpdf/convert_streams.py +426 -0
- spotpdf/document.py +543 -0
- spotpdf/inspection.py +44 -0
- spotpdf/inventory.py +527 -0
- spotpdf/inventory_content.py +600 -0
- spotpdf/inventory_graph.py +159 -0
- spotpdf/inventory_hazards.py +359 -0
- spotpdf/inventory_prepress.py +168 -0
- spotpdf/inventory_usage.py +65 -0
- spotpdf/inventory_values.py +128 -0
- spotpdf/limits.py +103 -0
- spotpdf/metadata_fingerprint.py +165 -0
- spotpdf/model.py +264 -0
- spotpdf/mutation_verification.py +139 -0
- spotpdf/objects.py +47 -0
- spotpdf/publication.py +206 -0
- spotpdf/py.typed +0 -0
- spotpdf/removal_resources.py +228 -0
- spotpdf/rename.py +236 -0
- spotpdf/rename_hazards.py +261 -0
- spotpdf/rename_plan.py +595 -0
- spotpdf/rename_request.py +74 -0
- spotpdf/rename_slots.py +412 -0
- spotpdf/rename_structures.py +320 -0
- spotpdf/resource_owners.py +62 -0
- spotpdf/scan.py +324 -0
- spotpdf/separation_targets.py +233 -0
- spotpdf/trailer_semantics.py +30 -0
- spotpdf-0.7.0.dist-info/METADATA +525 -0
- spotpdf-0.7.0.dist-info/RECORD +60 -0
- spotpdf-0.7.0.dist-info/WHEEL +5 -0
- spotpdf-0.7.0.dist-info/entry_points.txt +2 -0
- spotpdf-0.7.0.dist-info/licenses/LICENSE +21 -0
- spotpdf-0.7.0.dist-info/top_level.txt +1 -0
spotpdf/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Inspect and safely mutate named spot colors in PDF content."""
|
|
2
|
+
|
|
3
|
+
from .alternate import set_alternate_cmyk
|
|
4
|
+
from .convert import convert_spot_to_cmyk
|
|
5
|
+
from .document import check_spot, inspect_pdf, remove_all_spots, remove_spot
|
|
6
|
+
from .limits import (
|
|
7
|
+
DEFAULT_PROCESSING_LIMITS,
|
|
8
|
+
ProcessingBudgetExceeded,
|
|
9
|
+
ProcessingLimits,
|
|
10
|
+
)
|
|
11
|
+
from .model import (
|
|
12
|
+
AlternateResult,
|
|
13
|
+
BatchRemovalResult,
|
|
14
|
+
ColorantRole,
|
|
15
|
+
ConversionResult,
|
|
16
|
+
InspectionReport,
|
|
17
|
+
InvalidPdfError,
|
|
18
|
+
NestingLimitExceededError,
|
|
19
|
+
RemovalStats,
|
|
20
|
+
RenameResult,
|
|
21
|
+
SpotKind,
|
|
22
|
+
SpotPdfError,
|
|
23
|
+
SpotSummary,
|
|
24
|
+
UnsupportedSpotUseError,
|
|
25
|
+
__version__,
|
|
26
|
+
)
|
|
27
|
+
from .rename import rename_spot
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"DEFAULT_PROCESSING_LIMITS",
|
|
31
|
+
"AlternateResult",
|
|
32
|
+
"BatchRemovalResult",
|
|
33
|
+
"ColorantRole",
|
|
34
|
+
"ConversionResult",
|
|
35
|
+
"InspectionReport",
|
|
36
|
+
"InvalidPdfError",
|
|
37
|
+
"NestingLimitExceededError",
|
|
38
|
+
"ProcessingBudgetExceeded",
|
|
39
|
+
"ProcessingLimits",
|
|
40
|
+
"RemovalStats",
|
|
41
|
+
"RenameResult",
|
|
42
|
+
"SpotKind",
|
|
43
|
+
"SpotPdfError",
|
|
44
|
+
"SpotSummary",
|
|
45
|
+
"UnsupportedSpotUseError",
|
|
46
|
+
"__version__",
|
|
47
|
+
"check_spot",
|
|
48
|
+
"convert_spot_to_cmyk",
|
|
49
|
+
"inspect_pdf",
|
|
50
|
+
"remove_all_spots",
|
|
51
|
+
"remove_spot",
|
|
52
|
+
"rename_spot",
|
|
53
|
+
"set_alternate_cmyk",
|
|
54
|
+
]
|
spotpdf/__main__.py
ADDED
spotpdf/alternate.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Atomic orchestration for changing Separation alternate CMYK previews."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from os import PathLike
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pikepdf
|
|
11
|
+
|
|
12
|
+
from .alternate_plan import (
|
|
13
|
+
AlternatePlan,
|
|
14
|
+
NormalizedCmyk,
|
|
15
|
+
build_alternate_plan,
|
|
16
|
+
)
|
|
17
|
+
from .cmyk import (
|
|
18
|
+
PercentageCmyk,
|
|
19
|
+
normalized_cmyk,
|
|
20
|
+
parse_cmyk_percentages,
|
|
21
|
+
validate_cmyk_percentages,
|
|
22
|
+
)
|
|
23
|
+
from .inventory import discover_spot_declarations
|
|
24
|
+
from .limits import DEFAULT_PROCESSING_LIMITS, ProcessingLimits, require_processing_limits
|
|
25
|
+
from .model import AlternateResult, SpotPdfError
|
|
26
|
+
from .mutation_verification import (
|
|
27
|
+
ContentFingerprint,
|
|
28
|
+
InventoryFingerprint,
|
|
29
|
+
content_fingerprint,
|
|
30
|
+
inventory_fingerprint,
|
|
31
|
+
parse_content_streams,
|
|
32
|
+
)
|
|
33
|
+
from .publication import atomic_pdf_output, open_strict, save_pdf
|
|
34
|
+
from .rename_slots import semantic_pdf_fingerprint
|
|
35
|
+
from .scan import validate_document_for_mutation
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def set_alternate_cmyk(
|
|
39
|
+
input_path: str | PathLike[str],
|
|
40
|
+
output_path: str | PathLike[str],
|
|
41
|
+
spot: str,
|
|
42
|
+
cmyk: Sequence[object],
|
|
43
|
+
*,
|
|
44
|
+
force: bool = False,
|
|
45
|
+
limits: ProcessingLimits = DEFAULT_PROCESSING_LIMITS,
|
|
46
|
+
) -> AlternateResult:
|
|
47
|
+
"""Replace every matching Separation preview with one linear CMYK fallback."""
|
|
48
|
+
|
|
49
|
+
input_path = Path(input_path)
|
|
50
|
+
output_path = Path(output_path)
|
|
51
|
+
limits = require_processing_limits(limits)
|
|
52
|
+
percentages = validate_cmyk_percentages(cmyk)
|
|
53
|
+
normalized = normalized_cmyk(percentages)
|
|
54
|
+
stored_percentages: PercentageCmyk = (
|
|
55
|
+
normalized[0] * 100,
|
|
56
|
+
normalized[1] * 100,
|
|
57
|
+
normalized[2] * 100,
|
|
58
|
+
normalized[3] * 100,
|
|
59
|
+
)
|
|
60
|
+
result: AlternateResult | None = None
|
|
61
|
+
with atomic_pdf_output(input_path, output_path, force=force, limits=limits) as output:
|
|
62
|
+
with open_strict(output.input_path, limits=limits) as pdf:
|
|
63
|
+
validate_document_for_mutation(pdf)
|
|
64
|
+
before = discover_spot_declarations(pdf)
|
|
65
|
+
plan = build_alternate_plan(pdf, before, spot, normalized)
|
|
66
|
+
expected_inventory = inventory_fingerprint(before)
|
|
67
|
+
expected_content = content_fingerprint(pdf)
|
|
68
|
+
expected_masked_document = plan.normalized_document_fingerprint()
|
|
69
|
+
|
|
70
|
+
plan.apply()
|
|
71
|
+
_verify_in_memory(
|
|
72
|
+
pdf,
|
|
73
|
+
plan,
|
|
74
|
+
expected_inventory,
|
|
75
|
+
expected_content,
|
|
76
|
+
expected_masked_document,
|
|
77
|
+
)
|
|
78
|
+
expected_saved_document = semantic_pdf_fingerprint(pdf)
|
|
79
|
+
save_pdf(pdf, output.temp_path)
|
|
80
|
+
result = AlternateResult(
|
|
81
|
+
spot=spot,
|
|
82
|
+
cmyk_percentages=stored_percentages,
|
|
83
|
+
definitions_changed=plan.definitions_changed,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
_verify_saved_pdf(
|
|
87
|
+
output.temp_path,
|
|
88
|
+
spot,
|
|
89
|
+
normalized,
|
|
90
|
+
expected_inventory,
|
|
91
|
+
expected_content,
|
|
92
|
+
expected_saved_document,
|
|
93
|
+
result.definitions_changed,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
if result is None: # pragma: no cover - guarded by the transaction above
|
|
97
|
+
raise SpotPdfError("alternate preview change did not produce a result")
|
|
98
|
+
return result
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _verify_in_memory(
|
|
102
|
+
pdf: pikepdf.Pdf,
|
|
103
|
+
plan: AlternatePlan,
|
|
104
|
+
expected_inventory: InventoryFingerprint,
|
|
105
|
+
expected_content: ContentFingerprint,
|
|
106
|
+
expected_masked_document: tuple[Any, ...],
|
|
107
|
+
) -> None:
|
|
108
|
+
plan.verify_requested()
|
|
109
|
+
if inventory_fingerprint(discover_spot_declarations(pdf)) != expected_inventory:
|
|
110
|
+
raise SpotPdfError("spot inventory changed while setting the alternate preview")
|
|
111
|
+
if content_fingerprint(pdf) != expected_content:
|
|
112
|
+
raise SpotPdfError("content streams changed while setting the alternate preview")
|
|
113
|
+
if plan.normalized_document_fingerprint() != expected_masked_document:
|
|
114
|
+
raise SpotPdfError("PDF semantics changed beyond the planned alternate preview slots")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _verify_saved_pdf(
|
|
118
|
+
path: Path,
|
|
119
|
+
spot: str,
|
|
120
|
+
cmyk: NormalizedCmyk,
|
|
121
|
+
expected_inventory: InventoryFingerprint,
|
|
122
|
+
expected_content: ContentFingerprint,
|
|
123
|
+
expected_document: tuple[Any, ...],
|
|
124
|
+
expected_definitions: int,
|
|
125
|
+
) -> None:
|
|
126
|
+
with open_strict(path, limits=None) as pdf:
|
|
127
|
+
report = discover_spot_declarations(pdf)
|
|
128
|
+
if inventory_fingerprint(report) != expected_inventory:
|
|
129
|
+
raise SpotPdfError("saved PDF spot inventory changed unexpectedly")
|
|
130
|
+
plan = build_alternate_plan(pdf, report, spot, cmyk)
|
|
131
|
+
if plan.definitions_changed != expected_definitions:
|
|
132
|
+
raise SpotPdfError("saved PDF has a different number of target Separations")
|
|
133
|
+
plan.verify_requested()
|
|
134
|
+
if content_fingerprint(pdf) != expected_content:
|
|
135
|
+
raise SpotPdfError("saved PDF content streams changed unexpectedly")
|
|
136
|
+
parse_content_streams(pdf)
|
|
137
|
+
if semantic_pdf_fingerprint(pdf) != expected_document:
|
|
138
|
+
raise SpotPdfError("saved PDF object semantics changed during rewrite")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
__all__ = [
|
|
142
|
+
"PercentageCmyk",
|
|
143
|
+
"parse_cmyk_percentages",
|
|
144
|
+
"set_alternate_cmyk",
|
|
145
|
+
"validate_cmyk_percentages",
|
|
146
|
+
]
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""Validated mutation plans for Separation alternate CMYK previews."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pikepdf
|
|
11
|
+
|
|
12
|
+
from .alternate_validation import reject_inline_target_definitions, validate_existing_preview
|
|
13
|
+
from .cmyk import NormalizedCmyk, canonical_pdf_number, canonicalize_normalized_cmyk
|
|
14
|
+
from .colors import PROCESS_COLORANTS, SPECIAL_COLORANTS
|
|
15
|
+
from .inventory_graph import walk_reachable
|
|
16
|
+
from .inventory_values import name_or_string, name_value
|
|
17
|
+
from .model import (
|
|
18
|
+
ColorantRole,
|
|
19
|
+
InspectionReport,
|
|
20
|
+
InvalidPdfError,
|
|
21
|
+
SpotKind,
|
|
22
|
+
SpotPdfError,
|
|
23
|
+
UnsupportedSpotUseError,
|
|
24
|
+
)
|
|
25
|
+
from .rename_hazards import devicen_target_mentions, name_field_mentions
|
|
26
|
+
from .rename_slots import semantic_object_fingerprint, semantic_pdf_fingerprint
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class AlternateSlot:
|
|
31
|
+
"""One complete Separation definition whose preview may be replaced."""
|
|
32
|
+
|
|
33
|
+
separation: pikepdf.Array
|
|
34
|
+
spot: str
|
|
35
|
+
original_alternate: tuple[Any, ...]
|
|
36
|
+
original_tint: tuple[Any, ...]
|
|
37
|
+
requested_tint: tuple[Any, ...]
|
|
38
|
+
locations: set[str] = field(default_factory=set)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def label(self) -> str:
|
|
42
|
+
return min(self.locations)
|
|
43
|
+
|
|
44
|
+
def verify_original(self) -> None:
|
|
45
|
+
self._verify_identity()
|
|
46
|
+
if semantic_object_fingerprint(self.separation[2]) != self.original_alternate:
|
|
47
|
+
raise SpotPdfError(f"alternate color space changed before apply at {self.label}")
|
|
48
|
+
if semantic_object_fingerprint(self.separation[3]) != self.original_tint:
|
|
49
|
+
raise SpotPdfError(f"tint transform changed before apply at {self.label}")
|
|
50
|
+
|
|
51
|
+
def verify_requested(self) -> None:
|
|
52
|
+
self._verify_identity()
|
|
53
|
+
if self.separation[2] != pikepdf.Name.DeviceCMYK:
|
|
54
|
+
raise SpotPdfError(f"requested /DeviceCMYK alternate is absent at {self.label}")
|
|
55
|
+
if semantic_object_fingerprint(self.separation[3]) != self.requested_tint:
|
|
56
|
+
raise SpotPdfError(f"requested linear CMYK tint transform is absent at {self.label}")
|
|
57
|
+
|
|
58
|
+
def _verify_identity(self) -> None:
|
|
59
|
+
if (
|
|
60
|
+
len(self.separation) != 4
|
|
61
|
+
or name_value(self.separation[0]) != "Separation"
|
|
62
|
+
or name_value(self.separation[1]) != self.spot
|
|
63
|
+
):
|
|
64
|
+
raise SpotPdfError(f"Separation identity changed unexpectedly at {self.label}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class AlternatePlan:
|
|
69
|
+
"""A complete set of preview-only mutations for one spot plate."""
|
|
70
|
+
|
|
71
|
+
pdf: pikepdf.Pdf
|
|
72
|
+
spot: str
|
|
73
|
+
cmyk: NormalizedCmyk
|
|
74
|
+
slots: tuple[AlternateSlot, ...]
|
|
75
|
+
_applied: bool = False
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def definitions_changed(self) -> int:
|
|
79
|
+
return len(self.slots)
|
|
80
|
+
|
|
81
|
+
def apply(self) -> None:
|
|
82
|
+
"""Replace only alternate-space and tint-transform array members."""
|
|
83
|
+
|
|
84
|
+
if self._applied:
|
|
85
|
+
raise SpotPdfError("alternate preview plan has already been applied")
|
|
86
|
+
for slot in self.slots:
|
|
87
|
+
slot.verify_original()
|
|
88
|
+
tint_transform = self.pdf.make_indirect(_linear_cmyk_function(self.cmyk))
|
|
89
|
+
for slot in self.slots:
|
|
90
|
+
slot.separation[2] = pikepdf.Name.DeviceCMYK
|
|
91
|
+
slot.separation[3] = tint_transform
|
|
92
|
+
self._applied = True
|
|
93
|
+
self.verify_requested()
|
|
94
|
+
|
|
95
|
+
def verify_requested(self) -> None:
|
|
96
|
+
"""Require every planned definition to contain the exact requested preview."""
|
|
97
|
+
|
|
98
|
+
for slot in self.slots:
|
|
99
|
+
slot.verify_requested()
|
|
100
|
+
|
|
101
|
+
def normalized_document_fingerprint(self) -> tuple[Any, ...]:
|
|
102
|
+
"""Fingerprint the document while masking only the two preview slots."""
|
|
103
|
+
|
|
104
|
+
with self._masked_preview_slots():
|
|
105
|
+
return semantic_pdf_fingerprint(self.pdf)
|
|
106
|
+
|
|
107
|
+
@contextmanager
|
|
108
|
+
def _masked_preview_slots(self) -> Iterator[None]:
|
|
109
|
+
originals: list[tuple[AlternateSlot, Any, Any]] = []
|
|
110
|
+
try:
|
|
111
|
+
for slot in self.slots:
|
|
112
|
+
slot.verify_requested() if self._applied else slot.verify_original()
|
|
113
|
+
originals.append((slot, slot.separation[2], slot.separation[3]))
|
|
114
|
+
slot.separation[2] = pikepdf.Name("/__spotpdf_alternate_space_slot__")
|
|
115
|
+
slot.separation[3] = pikepdf.Name("/__spotpdf_tint_transform_slot__")
|
|
116
|
+
yield
|
|
117
|
+
finally:
|
|
118
|
+
for slot, alternate, tint in reversed(originals):
|
|
119
|
+
slot.separation[2] = alternate
|
|
120
|
+
slot.separation[3] = tint
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class _PlanBuilder:
|
|
124
|
+
"""Find every matching Separation and reject DeviceN or malformed uses."""
|
|
125
|
+
|
|
126
|
+
def __init__(
|
|
127
|
+
self,
|
|
128
|
+
pdf: pikepdf.Pdf,
|
|
129
|
+
report: InspectionReport,
|
|
130
|
+
spot: str,
|
|
131
|
+
cmyk: NormalizedCmyk,
|
|
132
|
+
) -> None:
|
|
133
|
+
self.pdf = pdf
|
|
134
|
+
self.report = report
|
|
135
|
+
self.spot = spot
|
|
136
|
+
self.cmyk = cmyk
|
|
137
|
+
self.requested_tint = semantic_object_fingerprint(_linear_cmyk_function(cmyk))
|
|
138
|
+
self.slots: dict[str, AlternateSlot] = {}
|
|
139
|
+
|
|
140
|
+
def build(self) -> AlternatePlan:
|
|
141
|
+
self._validate_request()
|
|
142
|
+
for visit in walk_reachable(self.pdf):
|
|
143
|
+
value = visit.value
|
|
144
|
+
if not isinstance(value, pikepdf.Array) or not value:
|
|
145
|
+
continue
|
|
146
|
+
family = name_or_string(value[0])
|
|
147
|
+
if family == "Separation":
|
|
148
|
+
self._inspect_separation(value, visit.locations)
|
|
149
|
+
elif family == "DeviceN" and devicen_target_mentions(value, frozenset({self.spot})):
|
|
150
|
+
raise UnsupportedSpotUseError(
|
|
151
|
+
f"{min(visit.locations)}: DeviceN use of {self.spot!r} is not supported"
|
|
152
|
+
)
|
|
153
|
+
reject_inline_target_definitions(self.pdf, self.spot)
|
|
154
|
+
self._validate_coverage()
|
|
155
|
+
return AlternatePlan(
|
|
156
|
+
pdf=self.pdf,
|
|
157
|
+
spot=self.spot,
|
|
158
|
+
cmyk=self.cmyk,
|
|
159
|
+
slots=tuple(sorted(self.slots.values(), key=lambda slot: slot.label)),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def _validate_request(self) -> None:
|
|
163
|
+
if self.spot in SPECIAL_COLORANTS:
|
|
164
|
+
raise InvalidPdfError("reserved /All and /None separation previews cannot be changed")
|
|
165
|
+
if self.spot in PROCESS_COLORANTS:
|
|
166
|
+
raise InvalidPdfError("canonical process-color previews cannot be changed")
|
|
167
|
+
summary = self.report.colorants.get(self.spot)
|
|
168
|
+
if summary is None:
|
|
169
|
+
raise InvalidPdfError(f"spot color is absent: {self.spot!r}")
|
|
170
|
+
if summary.roles != {ColorantRole.SPOT}:
|
|
171
|
+
roles = ", ".join(sorted(role.value for role in summary.roles)) or "unknown"
|
|
172
|
+
raise InvalidPdfError(
|
|
173
|
+
f"colorant {self.spot!r} is not an unambiguous spot (roles: {roles})"
|
|
174
|
+
)
|
|
175
|
+
if SpotKind.DEVICEN in summary.kinds:
|
|
176
|
+
raise UnsupportedSpotUseError(
|
|
177
|
+
f"DeviceN use of {self.spot!r} is not supported by set-alternate"
|
|
178
|
+
)
|
|
179
|
+
if not self._expected_definition_ids():
|
|
180
|
+
raise InvalidPdfError(
|
|
181
|
+
f"spot color {self.spot!r} has no reachable Separation definition"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
def _inspect_separation(
|
|
185
|
+
self,
|
|
186
|
+
value: pikepdf.Array,
|
|
187
|
+
locations: tuple[str, ...],
|
|
188
|
+
) -> None:
|
|
189
|
+
raw_name = value[1] if len(value) >= 2 else None
|
|
190
|
+
if name_or_string(raw_name) != self.spot:
|
|
191
|
+
if name_field_mentions(raw_name, frozenset({self.spot})):
|
|
192
|
+
raise UnsupportedSpotUseError(
|
|
193
|
+
f"{min(locations)}: malformed Separation name field mentions {self.spot!r}"
|
|
194
|
+
)
|
|
195
|
+
return
|
|
196
|
+
location = min(locations)
|
|
197
|
+
if (
|
|
198
|
+
not isinstance(value[0], pikepdf.Name)
|
|
199
|
+
or not isinstance(raw_name, pikepdf.Name)
|
|
200
|
+
or len(value) != 4
|
|
201
|
+
or not isinstance(value[2], (pikepdf.Name, pikepdf.Array))
|
|
202
|
+
or not isinstance(value[3], (pikepdf.Dictionary, pikepdf.Stream))
|
|
203
|
+
):
|
|
204
|
+
raise UnsupportedSpotUseError(
|
|
205
|
+
f"{location}: malformed Separation array cannot be changed safely"
|
|
206
|
+
)
|
|
207
|
+
validate_existing_preview(value[2], value[3], location)
|
|
208
|
+
definition_id = self._definition_id(locations)
|
|
209
|
+
proposed = AlternateSlot(
|
|
210
|
+
separation=value,
|
|
211
|
+
spot=self.spot,
|
|
212
|
+
original_alternate=semantic_object_fingerprint(value[2]),
|
|
213
|
+
original_tint=semantic_object_fingerprint(value[3]),
|
|
214
|
+
requested_tint=self.requested_tint,
|
|
215
|
+
)
|
|
216
|
+
slot = self.slots.setdefault(definition_id, proposed)
|
|
217
|
+
if slot.separation is not value and (
|
|
218
|
+
semantic_object_fingerprint(slot.separation) != semantic_object_fingerprint(value)
|
|
219
|
+
):
|
|
220
|
+
raise UnsupportedSpotUseError(
|
|
221
|
+
f"{location}: inventory identity maps to conflicting Separation arrays"
|
|
222
|
+
)
|
|
223
|
+
slot.locations.update(locations)
|
|
224
|
+
|
|
225
|
+
def _definition_id(self, locations: tuple[str, ...]) -> str:
|
|
226
|
+
location_set = set(locations)
|
|
227
|
+
matches = [
|
|
228
|
+
definition.object_id
|
|
229
|
+
for definition in self.report.definitions.values()
|
|
230
|
+
if definition.kind is SpotKind.SEPARATION
|
|
231
|
+
and any(component.name == self.spot for component in definition.components)
|
|
232
|
+
and location_set.intersection(definition.locations)
|
|
233
|
+
]
|
|
234
|
+
if len(set(matches)) != 1:
|
|
235
|
+
raise UnsupportedSpotUseError(
|
|
236
|
+
f"{min(locations)}: Separation has no unique inventory identity"
|
|
237
|
+
)
|
|
238
|
+
return matches[0]
|
|
239
|
+
|
|
240
|
+
def _expected_definition_ids(self) -> set[str]:
|
|
241
|
+
return {
|
|
242
|
+
definition.object_id
|
|
243
|
+
for definition in self.report.definitions.values()
|
|
244
|
+
if definition.kind is SpotKind.SEPARATION
|
|
245
|
+
and any(component.name == self.spot for component in definition.components)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
def _validate_coverage(self) -> None:
|
|
249
|
+
expected = self._expected_definition_ids()
|
|
250
|
+
actual = set(self.slots)
|
|
251
|
+
if actual != expected:
|
|
252
|
+
raise UnsupportedSpotUseError(
|
|
253
|
+
"inventory Separation definitions could not be mapped one-to-one "
|
|
254
|
+
f"(expected {len(expected)}, planned {len(actual)})"
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _linear_cmyk_function(cmyk: NormalizedCmyk) -> pikepdf.Dictionary:
|
|
259
|
+
full_tone = tuple(canonical_pdf_number(value) for value in cmyk)
|
|
260
|
+
return pikepdf.Dictionary(
|
|
261
|
+
FunctionType=2,
|
|
262
|
+
Domain=pikepdf.Array([0, 1]),
|
|
263
|
+
Range=pikepdf.Array([0, 1, 0, 1, 0, 1, 0, 1]),
|
|
264
|
+
C0=pikepdf.Array([0, 0, 0, 0]),
|
|
265
|
+
C1=pikepdf.Array(full_tone),
|
|
266
|
+
N=1,
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def build_alternate_plan(
|
|
271
|
+
pdf: pikepdf.Pdf,
|
|
272
|
+
report: InspectionReport,
|
|
273
|
+
spot: str,
|
|
274
|
+
cmyk: NormalizedCmyk,
|
|
275
|
+
) -> AlternatePlan:
|
|
276
|
+
"""Build a complete preview mutation plan without changing the PDF."""
|
|
277
|
+
|
|
278
|
+
return _PlanBuilder(pdf, report, spot, canonicalize_normalized_cmyk(cmyk)).build()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
__all__ = [
|
|
282
|
+
"AlternatePlan",
|
|
283
|
+
"NormalizedCmyk",
|
|
284
|
+
"build_alternate_plan",
|
|
285
|
+
"canonicalize_normalized_cmyk",
|
|
286
|
+
]
|