pdfrum_edit/error.rs
1//! What can go wrong while writing a document back out.
2
3use std::io;
4
5use pdfrum_common::PageIndex;
6
7/// A failure that stops the editor producing output.
8///
9/// Damage in the *input* is not an error here: a broken object silently
10/// vanishes from the output the way the C++ writer drops it, and every such
11/// recovery is recorded as a [`pdfrum_common::Diagnostic`]. `Err` is reserved
12/// for "cannot continue".
13#[derive(Debug, thiserror::Error)]
14#[non_exhaustive]
15pub enum Error {
16 /// The sink refused the bytes.
17 #[error("write failed: {0}")]
18 Io(#[from] io::Error),
19
20 /// The document declares `/Encrypt`, this reader derived no key for it
21 /// (an `/Identity` crypt filter, or a handler opened as
22 /// [`pdfrum_crypt::SecurityHandler::Identity`]), and `remove_security`
23 /// was not set. Re-declaring a cipher over plaintext would produce a file
24 /// nothing could open.
25 #[error(
26 "cannot save an encrypted document without its key; \
27 set `SaveOptions::remove_security` to save it decrypted"
28 )]
29 EncryptedSaveUnsupported,
30 /// A password given for a new encryption is not valid UTF-8, or does
31 /// not survive `SASLprep` (ISO 32000-2 §7.6.4.3.3).
32 #[error("a password for encryption must be text")]
33 PasswordNotText,
34 /// The operating system's cryptographic generator is unavailable, so the
35 /// file key and AES vectors a new encryption needs cannot be drawn.
36 /// Only an encrypting save can raise this; every other save needs no
37 /// randomness it cannot derive.
38 #[error("the operating system's random generator is unavailable")]
39 NoEntropy,
40
41 /// A document with no usable catalog cannot be the destination of an
42 /// import: there is nowhere to attach the pages.
43 #[error("the destination document has no catalog to import into")]
44 NoDestinationCatalog,
45
46 /// A page index named by an import is outside the source document.
47 #[error("page index {0} is outside the source document")]
48 PageIndexOutOfRange(PageIndex),
49
50 /// A page-range string the grammar of ISO 32000 viewers accepts could not
51 /// be parsed; the C++ treats one bad entry as discarding everything.
52 #[error("malformed page range")]
53 BadPageRange,
54
55 /// N-up was asked for a grid or sheet size with a zero dimension.
56 #[error("N-up needs a non-zero grid and sheet size")]
57 BadNupParams,
58
59 /// An SVG would not resolve. Only produced with the `svg-import`
60 /// feature, whose ingestion parses it.
61 #[cfg(feature = "svg-import")]
62 #[error("the SVG would not resolve: {0}")]
63 Svg(#[source] usvg::Error),
64
65 /// The font program could not be subset.
66 #[error("font subsetting failed: {0}")]
67 Subset(String),
68
69 /// The bytes are not a TrueType, OpenType, or Type 1 font program.
70 #[error("unrecognised font program")]
71 UnrecognisedFontProgram,
72
73 /// The program parsed but declares no glyphs.
74 #[error("font program has no glyphs")]
75 EmptyFontProgram,
76
77 /// A caller-supplied `/ToUnicode` CMap was empty.
78 ///
79 /// The font it would write would carry no statement of what its codes
80 /// mean. A caller who wanted a *generated* `/ToUnicode` should ask for
81 /// [`FontEncoding::Composite`](crate::FontEncoding::Composite) instead.
82 #[error("the /ToUnicode CMap is empty")]
83 EmptyToUnicodeCMap,
84
85 /// A caller-supplied `/CIDToGIDMap` was empty, or was not a whole number
86 /// of big-endian `u16` entries.
87 ///
88 /// `[oracle-bug]` ISO 32000-1 §9.7.4.2 defines `/CIDToGIDMap` as a stream
89 /// of two-byte glyph indices, so a half-entry is not a map. Both the
90 /// empty and the odd-length case are rejected here, before any of it is
91 /// written.
92 //
93 // [oracle-bug] `FPDFText_LoadCidType2Font` rejects the empty case
94 // (`fpdfsdk/fpdf_edittext.cpp:488-491`) but not the odd-length one:
95 // `LoadCustomCompositeFont` walks `i += 2` to
96 // `cid_to_gid_map_span.size()` and takes
97 // `cid_to_gid_map_span.subspan(i).first<2u>()` (`:308-313`), so a final
98 // one-byte remainder asks a 1-element span for its first 2 elements —
99 // a bounds `CHECK` in `pdfium::span`, i.e. an abort rather than a
100 // rejection. An error is the same answer without the crash. pdf.js is the
101 // reader that depends on the map: `readCidToGidMap` pairs the bytes as
102 // `(glyphsData[j++] << 8) | glyphsData[j]` over the map's whole length
103 // (`src/core/evaluator.js:4103-4116`), so a trailing half-entry reads
104 // `undefined` as its low byte and yields a glyph index 256 times too
105 // large for the last CID.
106 #[error("the /CIDToGIDMap is {0} bytes; it must be a non-empty whole number of 2-byte entries")]
107 BadCidToGidMap(usize),
108
109 /// The bytes are not a JPEG or JPEG 2000 codestream a PDF may hold.
110 ///
111 /// Raised when no JPEG header can be read, when the component count is
112 /// not 1, 3 or 4, or when the sample precision is not 1, 2, 4, 8 or 16.
113 /// The oracle reaches the same outcome by producing no stream at all;
114 /// this is the same refusal with the reason attached.
115 #[error("not a JPEG or JPEG 2000 image")]
116 UnrecognisedImageData,
117
118 /// An image was asked for with a zero width or height.
119 #[error("an image needs a non-zero width and height")]
120 EmptyImage,
121
122 /// The sample buffer is not the length the dimensions and pixel format
123 /// require.
124 ///
125 /// The oracle cannot reach this: its API takes a bitmap object that
126 /// already knows its own pitch, so a short buffer is not expressible.
127 /// Loose bytes are, and reading past them is not an option.
128 #[error("image data is {found} bytes; {expected} are needed")]
129 ImageDataLength {
130 /// Bytes the dimensions and format require.
131 expected: usize,
132 /// Bytes the caller supplied.
133 found: usize,
134 },
135
136 /// A drawing refused a character the font has no glyph for.
137 ///
138 /// Raised by [`EmbeddedFont::encode_checked`](crate::EmbeddedFont::encode_checked)
139 /// and by the facade's canvas, which encodes through it. A blank where a
140 /// glyph should be is a worse answer than a refusal, because nothing
141 /// downstream can tell the two apart.
142 #[error("cannot draw the text: {0}")]
143 MissingGlyph(#[from] crate::MissingGlyph),
144
145 /// A page written inline in its parent's `/Kids` has no object of its own
146 /// for an appended content stream to reach.
147 #[error("page {0} has no object of its own to draw on")]
148 InlinePage(PageIndex),
149
150 /// A highlight or underline was asked for with no `/QuadPoints`.
151 ///
152 /// ISO 32000-1 §12.5.6.10 requires the array; an empty one is not a
153 /// markup of anything.
154 #[error("a highlight or underline needs at least one quadrilateral")]
155 EmptyQuadPoints,
156
157 /// `update_annotation` / `delete_annotation` named an object that is not
158 /// listed in that page's `/Annots`.
159 #[error("annotation {}:{} is not on page {}", .0.num, .0.generation, .1)]
160 AnnotNotOnPage(pdfrum_object::ObjRef, PageIndex),
161
162 /// `update_annotation_at` / `delete_annotation_at` named an index outside
163 /// the page's `/Annots` array (or the page has no `/Annots`).
164 #[error("annotation index {0} is out of range on page {1}")]
165 AnnotIndexOutOfRange(usize, PageIndex),
166
167 /// `add_form_field` was given a name the document already uses.
168 ///
169 /// A field's fully qualified name is its identity, not a label: the reader
170 /// merges two fields sharing one into a single field with two controls,
171 /// and every lookup, action and script then addresses both at once. So a
172 /// collision is refused rather than written.
173 #[error("the document already has a form field named {0:?}")]
174 DuplicateFieldName(String),
175
176 /// `add_form_field` was given an empty name.
177 ///
178 /// The reader drops a field with no name anywhere in its ancestry, so such
179 /// a field would be written and then silently lost on the next load.
180 #[error("a form field needs a name")]
181 EmptyFieldName,
182
183 /// `add_form_field` was given a radio group with no buttons.
184 #[error("a radio group needs at least one button")]
185 EmptyRadioGroup,
186 /// `set_attachment_name` was given a name another attachment already has.
187 ///
188 /// The embedded-files tree is keyed by name, so writing a duplicate would
189 /// leave one of the two unreachable by the only lookup there is.
190 #[error("another attachment is already named {0:?}")]
191 DuplicateAttachmentName(String),
192
193 /// An object the writer needed could not be fetched or made sense of.
194 #[error("object model: {0}")]
195 Object(#[from] pdfrum_object::Error),
196}