imprimatur 0.1.0
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +8 -0
- data/Cargo.lock +835 -0
- data/Cargo.toml +44 -0
- data/LICENSE.txt +29 -0
- data/README.md +314 -0
- data/ext/imprimatur_native/cli/Cargo.toml +30 -0
- data/ext/imprimatur_native/cli/src/app.rs +183 -0
- data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
- data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
- data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
- data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
- data/ext/imprimatur_native/cli/src/cli.rs +71 -0
- data/ext/imprimatur_native/cli/src/main.rs +15 -0
- data/ext/imprimatur_native/core/Cargo.toml +33 -0
- data/ext/imprimatur_native/core/LICENSE +29 -0
- data/ext/imprimatur_native/core/README.md +105 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
- data/ext/imprimatur_native/core/src/doc.rs +210 -0
- data/ext/imprimatur_native/core/src/docx.rs +343 -0
- data/ext/imprimatur_native/core/src/error.rs +113 -0
- data/ext/imprimatur_native/core/src/lib.rs +38 -0
- data/ext/imprimatur_native/core/src/opc.rs +51 -0
- data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
- data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
- data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
- data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
- data/ext/imprimatur_native/core/src/pdf.rs +84 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
- data/ext/imprimatur_native/core/src/read/content.rs +266 -0
- data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
- data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
- data/ext/imprimatur_native/core/src/read/image.rs +233 -0
- data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
- data/ext/imprimatur_native/core/src/read/json.rs +172 -0
- data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
- data/ext/imprimatur_native/core/src/read/package.rs +191 -0
- data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
- data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
- data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
- data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
- data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
- data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
- data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
- data/ext/imprimatur_native/core/src/read/span.rs +22 -0
- data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
- data/ext/imprimatur_native/core/src/read.rs +145 -0
- data/ext/imprimatur_native/core/src/style.rs +82 -0
- data/ext/imprimatur_native/core/src/test_support.rs +163 -0
- data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
- data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
- data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
- data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
- data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
- data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
- data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
- data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
- data/ext/imprimatur_native/core/src/xml.rs +95 -0
- data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
- data/ext/imprimatur_native/core/src/zip.rs +161 -0
- data/ext/imprimatur_native/extconf.rb +45 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
- data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
- data/ext/imprimatur_native/ffi/src/document.rs +138 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
- data/ext/imprimatur_native/ffi/src/read.rs +182 -0
- data/ext/imprimatur_native/ffi/src/style.rs +85 -0
- data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
- data/lib/imprimatur/document.rb +127 -0
- data/lib/imprimatur/error.rb +22 -0
- data/lib/imprimatur/formula.rb +13 -0
- data/lib/imprimatur/native.rb +17 -0
- data/lib/imprimatur/reader.rb +59 -0
- data/lib/imprimatur/style.rb +31 -0
- data/lib/imprimatur/validation.rb +35 -0
- data/lib/imprimatur/version.rb +5 -0
- data/lib/imprimatur/xlsx.rb +183 -0
- data/lib/imprimatur.rb +111 -0
- metadata +157 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
//! A PDF's images as files (ISO 32000-1, 8.9). JPEG and JPEG 2000 streams
|
|
2
|
+
//! are kept as they are, fax-compressed scans are wrapped in a TIFF, and
|
|
3
|
+
//! raw pixels become PNG: gray, RGB, CMYK (converted), indexed, with their
|
|
4
|
+
//! soft mask as alpha when it matches. Each image is extracted once, from
|
|
5
|
+
//! the first page that draws it.
|
|
6
|
+
|
|
7
|
+
#[cfg(test)]
|
|
8
|
+
mod tests;
|
|
9
|
+
|
|
10
|
+
use std::collections::HashSet;
|
|
11
|
+
|
|
12
|
+
use super::content::{Drawing, page_images};
|
|
13
|
+
use super::document::{Document, decode_with};
|
|
14
|
+
use super::object::{Dict, Object};
|
|
15
|
+
use super::xref::is_limit;
|
|
16
|
+
use crate::Result;
|
|
17
|
+
use crate::read::Budget;
|
|
18
|
+
use crate::read::image::{Fax, Image, ImageFormat, ImageLocation, PngColor, png, tiff};
|
|
19
|
+
|
|
20
|
+
/// Calls `each` with every image the document's pages draw.
|
|
21
|
+
pub fn images(
|
|
22
|
+
data: &[u8],
|
|
23
|
+
budget: &Budget<'_>,
|
|
24
|
+
each: &mut dyn FnMut(Image) -> Result<()>,
|
|
25
|
+
) -> Result<()> {
|
|
26
|
+
let document = Document::open(data, budget)?;
|
|
27
|
+
let pages = document.pages()?;
|
|
28
|
+
let mut seen = HashSet::new();
|
|
29
|
+
let mut drawn = Vec::new();
|
|
30
|
+
for (number, page) in (1..).zip(&pages) {
|
|
31
|
+
budget.check()?;
|
|
32
|
+
drawn.clear();
|
|
33
|
+
let frame = match page_images(&document, page, &mut drawn) {
|
|
34
|
+
Ok(frame) => frame,
|
|
35
|
+
Err(error) if is_limit(&error) => return Err(error),
|
|
36
|
+
// A damaged page loses its images, not the document's.
|
|
37
|
+
Err(_) => continue,
|
|
38
|
+
};
|
|
39
|
+
for item in &drawn {
|
|
40
|
+
let encoded = match &item.image {
|
|
41
|
+
Drawing::Object { number: Some(object), .. } if !seen.insert(*object) => continue,
|
|
42
|
+
Drawing::Object { number, object } => {
|
|
43
|
+
let Object::Stream(stream) = &**object else { continue };
|
|
44
|
+
document
|
|
45
|
+
.raw(stream, *number)
|
|
46
|
+
.and_then(|raw| encode(&document, &stream.dict, &raw))
|
|
47
|
+
}
|
|
48
|
+
Drawing::Inline { dict, data } => encode(&document, dict, data),
|
|
49
|
+
};
|
|
50
|
+
let encoded = match encoded {
|
|
51
|
+
Ok(Some(encoded)) => encoded,
|
|
52
|
+
Err(error) if is_limit(&error) => return Err(error),
|
|
53
|
+
Ok(None) | Err(_) => continue,
|
|
54
|
+
};
|
|
55
|
+
let bounds = Some(frame.place(&item.ctm));
|
|
56
|
+
each(Image {
|
|
57
|
+
data: encoded.data,
|
|
58
|
+
format: encoded.format,
|
|
59
|
+
width: Some(encoded.width),
|
|
60
|
+
height: Some(encoded.height),
|
|
61
|
+
location: ImageLocation::Page { number, bounds },
|
|
62
|
+
})?;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
Ok(())
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
struct Encoded {
|
|
69
|
+
data: Vec<u8>,
|
|
70
|
+
format: ImageFormat,
|
|
71
|
+
width: u32,
|
|
72
|
+
height: u32,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// An image stream's data (decrypted) as a file; `None` for what cannot
|
|
76
|
+
/// be written as one (Lab colour, unknown spaces, broken dimensions).
|
|
77
|
+
fn encode(document: &Document<'_>, dict: &Dict, raw: &[u8]) -> Result<Option<Encoded>> {
|
|
78
|
+
let number = |key: &[u8]| -> Result<Option<f64>> {
|
|
79
|
+
Ok(document.entry(dict, key)?.and_then(|value| value.as_number()))
|
|
80
|
+
};
|
|
81
|
+
#[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "1 to 1e6")]
|
|
82
|
+
let dimension = |key: &[u8]| -> Result<Option<u32>> {
|
|
83
|
+
Ok(number(key)?.filter(|value| (1.0..=1e6).contains(value)).map(|value| value as u32))
|
|
84
|
+
};
|
|
85
|
+
let (Some(width), Some(height)) = (dimension(b"Width")?, dimension(b"Height")?) else {
|
|
86
|
+
return Ok(None);
|
|
87
|
+
};
|
|
88
|
+
let mut filters = document.filters(dict)?;
|
|
89
|
+
let codec = filters.last().and_then(|(name, _)| match &**name {
|
|
90
|
+
b"DCTDecode" | b"DCT" => Some(ImageFormat::Jpeg),
|
|
91
|
+
b"JPXDecode" => Some(ImageFormat::Jpeg2000),
|
|
92
|
+
b"JBIG2Decode" => Some(ImageFormat::Jbig2),
|
|
93
|
+
b"CCITTFaxDecode" | b"CCF" => Some(ImageFormat::Tiff),
|
|
94
|
+
_ => None,
|
|
95
|
+
});
|
|
96
|
+
let params = if codec.is_some() { filters.pop().and_then(|(_, params)| params) } else { None };
|
|
97
|
+
let data = decode_with(raw, &filters, document.budget())?;
|
|
98
|
+
let params = params.as_deref().and_then(Object::as_dict);
|
|
99
|
+
let encoded = |data, format| Ok(Some(Encoded { data, format, width, height }));
|
|
100
|
+
match codec {
|
|
101
|
+
Some(format @ (ImageFormat::Jpeg | ImageFormat::Jpeg2000)) => encoded(data, format),
|
|
102
|
+
Some(ImageFormat::Jbig2) => {
|
|
103
|
+
let globals = match params.and_then(|params| params.get(b"JBIG2Globals")) {
|
|
104
|
+
Some(object) => document.stream_data(object)?.unwrap_or_default(),
|
|
105
|
+
None => Vec::new(),
|
|
106
|
+
};
|
|
107
|
+
encoded([globals, data].concat(), ImageFormat::Jbig2)
|
|
108
|
+
}
|
|
109
|
+
Some(_) => {
|
|
110
|
+
let value = |key: &[u8]| params.and_then(|params| params.get(key));
|
|
111
|
+
let k = value(b"K").and_then(Object::as_integer).unwrap_or(0);
|
|
112
|
+
let fax = match k {
|
|
113
|
+
..0 => Fax::Group4,
|
|
114
|
+
0 => Fax::Group3OneDimensional,
|
|
115
|
+
_ => Fax::Group3TwoDimensional,
|
|
116
|
+
};
|
|
117
|
+
let black_is_one = value(b"BlackIs1") == Some(&Object::Bool(true));
|
|
118
|
+
let aligned = value(b"EncodedByteAlign") == Some(&Object::Bool(true));
|
|
119
|
+
let flipped = inverted(document, dict)?;
|
|
120
|
+
let data = tiff(width, height, fax, black_is_one != flipped, aligned, &data);
|
|
121
|
+
encoded(data, ImageFormat::Tiff)
|
|
122
|
+
}
|
|
123
|
+
None => pixels(document, dict, &data, width, height),
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Whether `Decode` is `[1 0]`: a one-component image drawn inverted.
|
|
128
|
+
fn inverted(document: &Document<'_>, dict: &Dict) -> Result<bool> {
|
|
129
|
+
let decode = document.entry(dict, b"Decode")?;
|
|
130
|
+
let decode = decode.as_deref().and_then(Object::as_array).unwrap_or_default();
|
|
131
|
+
let bound = |object: &Object| object.as_number();
|
|
132
|
+
Ok(matches!(decode, [low, high] if bound(low) == Some(1.0) && bound(high) == Some(0.0)))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// What colour space a raw image's samples are in, as far as writing it
|
|
136
|
+
/// out goes.
|
|
137
|
+
enum Space {
|
|
138
|
+
Gray,
|
|
139
|
+
/// One ink: 1 is full ink, so dark.
|
|
140
|
+
Ink,
|
|
141
|
+
Rgb,
|
|
142
|
+
Cmyk,
|
|
143
|
+
/// Indices into a palette of RGB triples.
|
|
144
|
+
Indexed(Vec<u8>),
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
impl Space {
|
|
148
|
+
const fn components(&self) -> usize {
|
|
149
|
+
match self {
|
|
150
|
+
Self::Gray | Self::Ink | Self::Indexed(_) => 1,
|
|
151
|
+
Self::Rgb => 3,
|
|
152
|
+
Self::Cmyk => 4,
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
fn space(document: &Document<'_>, object: &Object, depth: usize) -> Result<Option<Space>> {
|
|
158
|
+
let object = document.resolve(object)?;
|
|
159
|
+
let name = match &*object {
|
|
160
|
+
Object::Name(name) => Some(&**name),
|
|
161
|
+
Object::Array(items) => items.first().and_then(Object::as_name),
|
|
162
|
+
_ => None,
|
|
163
|
+
};
|
|
164
|
+
let items = object.as_array().unwrap_or_default();
|
|
165
|
+
Ok(match name {
|
|
166
|
+
Some(b"DeviceGray" | b"CalGray" | b"G") => Some(Space::Gray),
|
|
167
|
+
Some(b"DeviceRGB" | b"CalRGB" | b"RGB") => Some(Space::Rgb),
|
|
168
|
+
Some(b"DeviceCMYK" | b"CMYK") => Some(Space::Cmyk),
|
|
169
|
+
Some(b"Separation") => Some(Space::Ink),
|
|
170
|
+
Some(b"DeviceN") => {
|
|
171
|
+
let inks = items.get(1).map(|names| document.resolve(names)).transpose()?;
|
|
172
|
+
(inks.as_deref().and_then(Object::as_array).map(<[Object]>::len) == Some(1))
|
|
173
|
+
.then_some(Space::Ink)
|
|
174
|
+
}
|
|
175
|
+
Some(b"ICCBased") => {
|
|
176
|
+
let profile = items.get(1).map(|profile| document.resolve(profile)).transpose()?;
|
|
177
|
+
let dict = profile.as_deref().and_then(Object::as_dict);
|
|
178
|
+
match dict.and_then(|dict| dict.get(b"N")).and_then(Object::as_integer) {
|
|
179
|
+
Some(1) => Some(Space::Gray),
|
|
180
|
+
Some(3) => Some(Space::Rgb),
|
|
181
|
+
Some(4) => Some(Space::Cmyk),
|
|
182
|
+
_ => match dict.and_then(|dict| dict.get(b"Alternate")) {
|
|
183
|
+
Some(alternate) if depth < 4 => space(document, alternate, depth + 1)?,
|
|
184
|
+
_ => None,
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
Some(b"Indexed" | b"I") if depth < 4 && items.len() >= 4 => {
|
|
189
|
+
let Some(base) = space(document, &items[1], depth + 1)? else { return Ok(None) };
|
|
190
|
+
let entries =
|
|
191
|
+
usize::try_from(items[2].as_integer().unwrap_or(0)).unwrap_or(0).min(255) + 1;
|
|
192
|
+
let lookup = match document.resolve(&items[3])?.as_bytes() {
|
|
193
|
+
Some(bytes) => bytes.to_vec(),
|
|
194
|
+
None => document.stream_data(&items[3])?.unwrap_or_default(),
|
|
195
|
+
};
|
|
196
|
+
let mut palette = Vec::with_capacity(entries * 3);
|
|
197
|
+
for entry in lookup.chunks(base.components()).take(entries) {
|
|
198
|
+
push_rgb(&mut palette, &base, entry);
|
|
199
|
+
}
|
|
200
|
+
palette.resize(entries * 3, 0);
|
|
201
|
+
Some(Space::Indexed(palette))
|
|
202
|
+
}
|
|
203
|
+
_ => None,
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// One sample set as RGB.
|
|
208
|
+
fn push_rgb(out: &mut Vec<u8>, space: &Space, samples: &[u8]) {
|
|
209
|
+
let sample = |index: usize| samples.get(index).copied().unwrap_or(0);
|
|
210
|
+
match space {
|
|
211
|
+
Space::Gray => out.extend_from_slice(&[sample(0); 3]),
|
|
212
|
+
Space::Ink => out.extend_from_slice(&[!sample(0); 3]),
|
|
213
|
+
Space::Rgb => out.extend_from_slice(&[sample(0), sample(1), sample(2)]),
|
|
214
|
+
Space::Cmyk => {
|
|
215
|
+
let ink = |value: u8| {
|
|
216
|
+
u8::try_from(u16::from(255 - value) * u16::from(255 - sample(3)) / 255).unwrap_or(0)
|
|
217
|
+
};
|
|
218
|
+
out.extend_from_slice(&[ink(sample(0)), ink(sample(1)), ink(sample(2))]);
|
|
219
|
+
}
|
|
220
|
+
Space::Indexed(palette) => {
|
|
221
|
+
let at = usize::from(sample(0)) * 3;
|
|
222
|
+
out.extend_from_slice(palette.get(at..at + 3).unwrap_or(&[0, 0, 0]));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Raw samples as a PNG.
|
|
228
|
+
fn pixels(
|
|
229
|
+
document: &Document<'_>,
|
|
230
|
+
dict: &Dict,
|
|
231
|
+
data: &[u8],
|
|
232
|
+
width: u32,
|
|
233
|
+
height: u32,
|
|
234
|
+
) -> Result<Option<Encoded>> {
|
|
235
|
+
let mask =
|
|
236
|
+
document.entry(dict, b"ImageMask")?.is_some_and(|value| *value == Object::Bool(true));
|
|
237
|
+
let bits = if mask {
|
|
238
|
+
1
|
|
239
|
+
} else {
|
|
240
|
+
let bits = document.entry(dict, b"BitsPerComponent")?.and_then(|value| value.as_integer());
|
|
241
|
+
match bits.unwrap_or(8) {
|
|
242
|
+
bits @ (1 | 2 | 4 | 8 | 16) => u8::try_from(bits).unwrap_or(8),
|
|
243
|
+
_ => return Ok(None),
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
let space = if mask {
|
|
247
|
+
Space::Gray
|
|
248
|
+
} else {
|
|
249
|
+
match dict.get(b"ColorSpace") {
|
|
250
|
+
Some(object) => match space(document, object, 0)? {
|
|
251
|
+
Some(space) => space,
|
|
252
|
+
None => return Ok(None),
|
|
253
|
+
},
|
|
254
|
+
None => return Ok(None),
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
let (pixels, rows) = (width as usize * height as usize, height as usize);
|
|
258
|
+
let stride = (width as usize * space.components() * usize::from(bits)).div_ceil(8);
|
|
259
|
+
// What is decoded beyond the stream: rows as stored, or 4 bytes a pixel.
|
|
260
|
+
document.budget().inflate(stride.saturating_mul(rows).max(pixels.saturating_mul(4)))?;
|
|
261
|
+
let alpha = soft_mask(document, dict, width, height)?;
|
|
262
|
+
// An ink's 1 is dark; `Decode [1 0]` turns it around again.
|
|
263
|
+
let flip = inverted(document, dict)? != matches!(space, Space::Ink);
|
|
264
|
+
let one = |png| Ok(Some(Encoded { data: png, format: ImageFormat::Png, width, height }));
|
|
265
|
+
// As stored, when PNG can say it the same way.
|
|
266
|
+
let stored = data.get(..stride * rows).unwrap_or(data);
|
|
267
|
+
match (&space, &alpha) {
|
|
268
|
+
(Space::Gray | Space::Ink, None) => {
|
|
269
|
+
let flipped: Vec<u8>;
|
|
270
|
+
let samples = if flip {
|
|
271
|
+
flipped = stored.iter().map(|byte| !byte).collect();
|
|
272
|
+
&flipped
|
|
273
|
+
} else {
|
|
274
|
+
stored
|
|
275
|
+
};
|
|
276
|
+
return one(png(width, height, PngColor::Gray, bits, None, samples));
|
|
277
|
+
}
|
|
278
|
+
(Space::Rgb, None) if bits >= 8 => {
|
|
279
|
+
return one(png(width, height, PngColor::Rgb, bits, None, stored));
|
|
280
|
+
}
|
|
281
|
+
(Space::Indexed(palette), None) if bits <= 8 => {
|
|
282
|
+
return one(png(width, height, PngColor::Palette, bits, Some(palette), stored));
|
|
283
|
+
}
|
|
284
|
+
_ => {}
|
|
285
|
+
}
|
|
286
|
+
// Otherwise, sample by sample: 8 bits, RGB or gray, alpha if any.
|
|
287
|
+
let samples =
|
|
288
|
+
unpack(data, width, height, space.components(), bits, !matches!(space, Space::Indexed(_)));
|
|
289
|
+
let gray = matches!(space, Space::Gray | Space::Ink);
|
|
290
|
+
let mut out = Vec::with_capacity(pixels * if gray { 2 } else { 4 });
|
|
291
|
+
for (index, set) in samples.chunks(space.components()).take(pixels).enumerate() {
|
|
292
|
+
if gray {
|
|
293
|
+
out.push(if flip { !set[0] } else { set[0] });
|
|
294
|
+
} else {
|
|
295
|
+
push_rgb(&mut out, &space, set);
|
|
296
|
+
}
|
|
297
|
+
if let Some(alpha) = &alpha {
|
|
298
|
+
out.push(alpha.get(index).copied().unwrap_or(255));
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
let color = match (gray, alpha.is_some()) {
|
|
302
|
+
(true, false) => PngColor::Gray,
|
|
303
|
+
(true, true) => PngColor::GrayAlpha,
|
|
304
|
+
(false, false) => PngColor::Rgb,
|
|
305
|
+
(false, true) => PngColor::Rgba,
|
|
306
|
+
};
|
|
307
|
+
one(png(width, height, color, 8, None, &out))
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/// A soft mask that matches the image, 8 bits a pixel, as alpha.
|
|
311
|
+
fn soft_mask(
|
|
312
|
+
document: &Document<'_>,
|
|
313
|
+
dict: &Dict,
|
|
314
|
+
width: u32,
|
|
315
|
+
height: u32,
|
|
316
|
+
) -> Result<Option<Vec<u8>>> {
|
|
317
|
+
let Some(reference) = dict.get(b"SMask") else { return Ok(None) };
|
|
318
|
+
let resolved = document.resolve(reference)?;
|
|
319
|
+
let Object::Stream(stream) = &*resolved else { return Ok(None) };
|
|
320
|
+
let size = |key: &[u8]| stream.dict.get(key).and_then(Object::as_integer);
|
|
321
|
+
let matches = size(b"Width") == Some(i64::from(width))
|
|
322
|
+
&& size(b"Height") == Some(i64::from(height))
|
|
323
|
+
&& size(b"BitsPerComponent").unwrap_or(8) == 8;
|
|
324
|
+
if !matches {
|
|
325
|
+
return Ok(None);
|
|
326
|
+
}
|
|
327
|
+
let number = reference.as_reference().map(|(number, _)| number);
|
|
328
|
+
let Ok(alpha) = document.decode(stream, number) else { return Ok(None) };
|
|
329
|
+
Ok((alpha.len() >= width as usize * height as usize).then_some(alpha))
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/// Packed samples as one byte each; `scale` stretches fewer than 8 bits to
|
|
333
|
+
/// 0..=255 (indices keep their value), 16 bits keep their high byte.
|
|
334
|
+
fn unpack(
|
|
335
|
+
data: &[u8],
|
|
336
|
+
width: u32,
|
|
337
|
+
height: u32,
|
|
338
|
+
components: usize,
|
|
339
|
+
bits: u8,
|
|
340
|
+
scale: bool,
|
|
341
|
+
) -> Vec<u8> {
|
|
342
|
+
let per_row = width as usize * components;
|
|
343
|
+
let stride = (per_row * usize::from(bits)).div_ceil(8);
|
|
344
|
+
let mut out = Vec::with_capacity(per_row * height as usize);
|
|
345
|
+
let max = (1_u16 << bits.min(8)) - 1;
|
|
346
|
+
for row in 0..height as usize {
|
|
347
|
+
let line = data.get(row * stride..(row + 1) * stride).unwrap_or_default();
|
|
348
|
+
for index in 0..per_row {
|
|
349
|
+
let value = match bits {
|
|
350
|
+
16 => line.get(index * 2).copied().unwrap_or(0),
|
|
351
|
+
8 => line.get(index).copied().unwrap_or(0),
|
|
352
|
+
_ => {
|
|
353
|
+
let bit = index * usize::from(bits);
|
|
354
|
+
let byte = line.get(bit / 8).copied().unwrap_or(0);
|
|
355
|
+
let shift = 8 - usize::from(bits) - bit % 8;
|
|
356
|
+
let value = u16::from(byte >> shift) & max;
|
|
357
|
+
let value = if scale { value * 255 / max } else { value };
|
|
358
|
+
u8::try_from(value).unwrap_or(u8::MAX)
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
out.push(value);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
out
|
|
365
|
+
}
|