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