Skip to main content

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}