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