imprimatur 0.2.0 → 0.4.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/Cargo.lock +153 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +139 -21
  6. data/ext/imprimatur_native/cli/Cargo.toml +3 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +20 -4
  8. data/ext/imprimatur_native/core/README.md +36 -4
  9. data/ext/imprimatur_native/core/src/archive.rs +3 -16
  10. data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
  11. data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
  12. data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
  13. data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
  14. data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
  15. data/ext/imprimatur_native/core/src/barcode.rs +314 -0
  16. data/ext/imprimatur_native/core/src/doc/paragraph.rs +10 -3
  17. data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
  18. data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
  19. data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
  20. data/ext/imprimatur_native/core/src/doc.rs +96 -2
  21. data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +103 -28
  23. data/ext/imprimatur_native/core/src/error.rs +18 -0
  24. data/ext/imprimatur_native/core/src/font.rs +231 -0
  25. data/ext/imprimatur_native/core/src/lib.rs +6 -1
  26. data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -15
  27. data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
  28. data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
  29. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
  30. data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
  31. data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
  32. data/ext/imprimatur_native/core/src/pdf.rs +40 -4
  33. data/ext/imprimatur_native/core/src/read/content.rs +75 -2
  34. data/ext/imprimatur_native/core/src/read/image.rs +17 -0
  35. data/ext/imprimatur_native/core/src/read/json.rs +94 -7
  36. data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
  37. data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
  38. data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
  39. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
  40. data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
  41. data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
  42. data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
  43. data/ext/imprimatur_native/core/src/read.rs +5 -1
  44. data/ext/imprimatur_native/core/src/script.rs +116 -0
  45. data/ext/imprimatur_native/core/src/style.rs +32 -0
  46. data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
  47. data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
  48. data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
  49. data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
  50. data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
  51. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
  52. data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
  53. data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
  54. data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
  55. data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
  56. data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
  57. data/ext/imprimatur_native/ffi/src/document.rs +76 -1
  58. data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
  59. data/ext/imprimatur_native/ffi/src/read.rs +8 -11
  60. data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
  61. data/lib/imprimatur/barcode.rb +78 -0
  62. data/lib/imprimatur/document.rb +46 -6
  63. data/lib/imprimatur/reader.rb +2 -2
  64. data/lib/imprimatur/spreadsheet.rb +151 -0
  65. data/lib/imprimatur/version.rb +1 -1
  66. data/lib/imprimatur/xlsx.rb +25 -4
  67. data/lib/imprimatur.rb +19 -3
  68. metadata +23 -2
  69. data/ext/imprimatur_native/core/src/test_support.rs +0 -250
@@ -0,0 +1,231 @@
1
+ //! Static TrueType font data, Unicode glyph lookup and unscaled metrics.
2
+ //!
3
+ //! This does not select or embed fonts in documents, shape text, or perform
4
+ //! bidirectional layout. A character's nominal glyph is not necessarily the
5
+ //! glyph used when shaping a script such as Arabic.
6
+
7
+ #[cfg(test)]
8
+ mod tests;
9
+
10
+ use read_fonts::{
11
+ FontRead, FontRef, MinByteRange, TableProvider,
12
+ tables::{
13
+ cmap::{Cmap, CmapSubtable, PlatformId},
14
+ hmtx::Hmtx,
15
+ },
16
+ types::{GlyphId, Tag, Version16Dot16},
17
+ };
18
+
19
+ use crate::{Error, Result};
20
+
21
+ /// Horizontal line metrics in font design units, taken from `head` and `hhea`.
22
+ ///
23
+ /// Multiply a metric by the point size and divide by `units_per_em` to convert
24
+ /// it to points. These are the raw `hhea` metrics, not an OS/2 line-height policy.
25
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
26
+ pub struct FontMetrics {
27
+ pub units_per_em: u16,
28
+ pub ascender: i16,
29
+ /// Usually negative: the position below the baseline.
30
+ pub descender: i16,
31
+ pub line_gap: i16,
32
+ }
33
+
34
+ /// A borrowed, standalone static TrueType font with a Unicode character map.
35
+ ///
36
+ /// Loading checks table bounds and the data needed for glyph lookup and metrics,
37
+ /// including the `loca` offsets into `glyf`. It is not a full font sanitizer:
38
+ /// checksums, outline programs and shaping tables are not validated.
39
+ #[derive(Clone)]
40
+ pub struct TrueTypeFont<'a> {
41
+ data: &'a [u8],
42
+ cmap: CmapSubtable<'a>,
43
+ hmtx: Hmtx<'a>,
44
+ metrics: FontMetrics,
45
+ glyph_count: u16,
46
+ }
47
+
48
+ impl<'a> TrueTypeFont<'a> {
49
+ /// Load font data without copying it or accessing system fonts.
50
+ ///
51
+ /// # Errors
52
+ /// [`Error::UnsupportedFont`] for collections, WOFF, CFF, variable fonts or
53
+ /// fonts without a supported Unicode cmap. [`Error::Malformed`] for invalid
54
+ /// headers, missing required tables or inconsistent metrics/outline offsets.
55
+ pub fn from_bytes(data: &'a [u8]) -> Result<Self> {
56
+ match data.get(..4) {
57
+ Some(b"\0\x01\0\0" | b"true") => {}
58
+ Some(b"ttcf") => return Err(Error::UnsupportedFont("font collections")),
59
+ Some(b"wOFF" | b"wOF2") => return Err(Error::UnsupportedFont("WOFF/WOFF2")),
60
+ Some(b"OTTO") => return Err(Error::UnsupportedFont("CFF outlines")),
61
+ _ => return Err(invalid("invalid or truncated sfnt header")),
62
+ }
63
+ let font = FontRef::new(data).map_err(invalid)?;
64
+ for record in font.table_directory().table_records() {
65
+ if u64::from(record.offset()) + u64::from(record.length()) > data.len() as u64 {
66
+ return Err(invalid(format!(
67
+ "{} table extends beyond the font data",
68
+ record.tag()
69
+ )));
70
+ }
71
+ match record.tag() {
72
+ tag if tag == Tag::new(b"fvar") || tag == Tag::new(b"gvar") => {
73
+ return Err(Error::UnsupportedFont("variable fonts"));
74
+ }
75
+ tag if tag == Tag::new(b"CFF ") || tag == Tag::new(b"CFF2") => {
76
+ return Err(Error::UnsupportedFont("CFF outlines"));
77
+ }
78
+ _ => {}
79
+ }
80
+ }
81
+
82
+ let head = font.head().map_err(invalid)?;
83
+ if head.magic_number() != 0x5F0F_3CF5 || !(16..=16384).contains(&head.units_per_em()) {
84
+ return Err(invalid("invalid head magic number or units per em"));
85
+ }
86
+ let maxp = font.maxp().map_err(invalid)?;
87
+ let glyph_count = maxp.num_glyphs();
88
+ if maxp.version() != Version16Dot16::VERSION_1_0
89
+ || maxp.max_component_depth().is_none()
90
+ || glyph_count == 0
91
+ {
92
+ return Err(invalid("expected a complete TrueType maxp table with at least one glyph"));
93
+ }
94
+ let hhea = font.hhea().map_err(invalid)?;
95
+ let count = hhea.number_of_h_metrics();
96
+ if count == 0 || count > glyph_count {
97
+ return Err(invalid("hhea metric count is outside the glyph range"));
98
+ }
99
+ let hmtx = font.hmtx().map_err(invalid)?;
100
+ if hmtx.h_metrics().len() != usize::from(count)
101
+ || hmtx.left_side_bearings().len() < usize::from(glyph_count - count)
102
+ {
103
+ return Err(invalid("truncated hmtx table"));
104
+ }
105
+ validate_outlines(&font, head.index_to_loc_format(), glyph_count)?;
106
+ let cmap = font.cmap().map_err(invalid)?;
107
+ if cmap.version() != 0 || cmap.encoding_records().len() != usize::from(cmap.num_tables()) {
108
+ return Err(invalid("invalid or truncated cmap header"));
109
+ }
110
+ Ok(Self {
111
+ data,
112
+ cmap: unicode_cmap(&cmap)?,
113
+ hmtx,
114
+ metrics: FontMetrics {
115
+ units_per_em: head.units_per_em(),
116
+ ascender: hhea.ascender().into(),
117
+ descender: hhea.descender().into(),
118
+ line_gap: hhea.line_gap().into(),
119
+ },
120
+ glyph_count,
121
+ })
122
+ }
123
+
124
+ /// The original font bytes.
125
+ #[must_use]
126
+ pub const fn as_bytes(&self) -> &'a [u8] {
127
+ self.data
128
+ }
129
+
130
+ /// Unscaled horizontal line metrics.
131
+ #[must_use]
132
+ pub const fn metrics(&self) -> FontMetrics {
133
+ self.metrics
134
+ }
135
+
136
+ /// The number of glyphs, including glyph zero (`.notdef`).
137
+ #[must_use]
138
+ pub const fn glyph_count(&self) -> u16 {
139
+ self.glyph_count
140
+ }
141
+
142
+ /// The nominal glyph for a Unicode character, without shaping or fallback.
143
+ ///
144
+ /// Returns `None` for missing characters, mappings to `.notdef`, or mappings
145
+ /// outside the font's glyph range. Symbol and legacy cmaps are never used as
146
+ /// Unicode mappings; full-repertoire Unicode cmaps take priority over BMP ones.
147
+ #[must_use]
148
+ pub fn glyph_id(&self, character: char) -> Option<u16> {
149
+ let glyph = self.cmap.map_codepoint(character)?.to_u32();
150
+ u16::try_from(glyph).ok().filter(|&id| id != 0 && id < self.glyph_count)
151
+ }
152
+
153
+ /// The horizontal advance in font design units, without kerning or shaping.
154
+ ///
155
+ /// Glyph zero is allowed; an out-of-range glyph returns `None`. A valid
156
+ /// zero-width glyph returns `Some(0)`.
157
+ #[must_use]
158
+ pub fn advance_width(&self, glyph_id: u16) -> Option<u16> {
159
+ if glyph_id >= self.glyph_count {
160
+ return None;
161
+ }
162
+ self.hmtx.advance(GlyphId::new(u32::from(glyph_id)))
163
+ }
164
+ }
165
+
166
+ fn validate_outlines(font: &FontRef<'_>, loca_format: i16, glyph_count: u16) -> Result<()> {
167
+ if !matches!(loca_format, 0 | 1) {
168
+ return Err(invalid("invalid head indexToLocFormat"));
169
+ }
170
+ let loca = font.loca(loca_format == 1).map_err(invalid)?;
171
+ let glyf = font.glyf().map_err(invalid)?;
172
+ if loca.len() != usize::from(glyph_count)
173
+ || !loca.all_offsets_are_ascending()
174
+ || loca
175
+ .get_raw(usize::from(glyph_count))
176
+ .is_none_or(|end| u64::from(end) > glyf.offset_data().len() as u64)
177
+ {
178
+ return Err(invalid("invalid loca offsets or truncated glyf table"));
179
+ }
180
+ Ok(())
181
+ }
182
+
183
+ fn unicode_cmap<'a>(cmap: &Cmap<'a>) -> Result<CmapSubtable<'a>> {
184
+ // Fontations' best_subtable also accepts symbol and MacRoman maps. Only
185
+ // Unicode encodings belong here; prefer full repertoire over BMP mappings.
186
+ let encodings = [
187
+ (PlatformId::Windows, 10),
188
+ (PlatformId::Unicode, 6),
189
+ (PlatformId::Unicode, 4),
190
+ (PlatformId::Windows, 1),
191
+ (PlatformId::Unicode, 3),
192
+ (PlatformId::Unicode, 2),
193
+ (PlatformId::Unicode, 1),
194
+ (PlatformId::Unicode, 0),
195
+ ];
196
+ for (platform, encoding) in encodings {
197
+ for record in cmap.encoding_records() {
198
+ if record.platform_id() != platform || record.encoding_id() != encoding {
199
+ continue;
200
+ }
201
+ let subtable = record.subtable(cmap.offset_data()).map_err(invalid)?;
202
+ let length = match &subtable {
203
+ CmapSubtable::Format0(table) => u32::from(table.length()),
204
+ CmapSubtable::Format4(table) => u32::from(table.length()),
205
+ CmapSubtable::Format6(table) => u32::from(table.length()),
206
+ CmapSubtable::Format10(table) => table.length(),
207
+ CmapSubtable::Format12(table) => table.length(),
208
+ CmapSubtable::Format13(table) => table.length(),
209
+ _ => continue,
210
+ };
211
+ let length = usize::try_from(length).map_err(|_| invalid("cmap subtable too large"))?;
212
+ let data = subtable
213
+ .offset_data()
214
+ .slice(0..length)
215
+ .ok_or_else(|| invalid("truncated cmap subtable"))?;
216
+ // Constrain lazy array reads to this subtable's declared extent.
217
+ let subtable = CmapSubtable::read(data).map_err(invalid)?;
218
+ if subtable.min_byte_range().end > length {
219
+ return Err(invalid("truncated cmap arrays"));
220
+ }
221
+ return Ok(subtable);
222
+ }
223
+ }
224
+ Err(Error::UnsupportedFont("no supported Unicode cmap"))
225
+ }
226
+
227
+ /// `Malformed`, categorized as TrueType font data: parsing, embedding and PDF
228
+ /// resource errors ([`crate::pdf::truetype`]) all point at the same category.
229
+ pub(crate) fn invalid(reason: impl std::fmt::Display) -> Error {
230
+ Error::malformed("TrueType font", reason.to_string())
231
+ }
@@ -3,7 +3,8 @@
3
3
  //! (deflate) and `crc32fast`.
4
4
  //!
5
5
  //! [`xlsx`] writes workbooks; [`doc`] is one document model that renders to
6
- //! PDF and DOCX.
6
+ //! PDF and DOCX; [`barcode`] draws QR Code, Data Matrix, EAN, UPC and Code
7
+ //! 128 symbols, GS1 ones included.
7
8
  //!
8
9
  //! ```
9
10
  //! use imprimatur::xlsx::{Cell, Workbook};
@@ -20,12 +21,16 @@
20
21
 
21
22
  #[cfg(feature = "archive")]
22
23
  pub mod archive;
24
+ pub mod barcode;
23
25
  pub mod doc;
24
26
  mod docx;
25
27
  mod error;
28
+ #[cfg(feature = "fonts")]
29
+ pub mod font;
26
30
  mod opc;
27
31
  mod pdf;
28
32
  pub mod read;
33
+ mod script;
29
34
  pub mod style;
30
35
  #[cfg(test)]
31
36
  mod test_support;
@@ -8,13 +8,11 @@ use std::fmt::{self, Arguments, Write as _};
8
8
 
9
9
  use super::font::Face;
10
10
  use super::objects::{Num, push_string};
11
- use crate::doc::{PageSize, Paragraph, Table};
11
+ use crate::doc::{Document, Paragraph, Picture, Symbol, Table};
12
12
  use crate::style::{Align, Color};
13
13
  use crate::{Error, Result};
14
14
  use wrap::{Line, Look, wrap};
15
15
 
16
- /// One inch on every side.
17
- const MARGIN: f64 = 72.0;
18
16
  const BODY_SIZE: f64 = 11.0;
19
17
  const TABLE_SIZE: f64 = 10.0;
20
18
  /// Line height as a multiple of the font size.
@@ -32,6 +30,7 @@ const HEADINGS: [(f64, f64, Option<Color>); 3] =
32
30
  pub struct Layout {
33
31
  width: f64,
34
32
  height: f64,
33
+ margin: f64,
35
34
  pages: Vec<Vec<u8>>,
36
35
  /// The top of the free space on the current page, from the bottom edge.
37
36
  y: f64,
@@ -39,9 +38,15 @@ pub struct Layout {
39
38
  }
40
39
 
41
40
  impl Layout {
42
- pub fn new(size: PageSize) -> Self {
43
- let (width, height) = size.points();
44
- Self { width, height, pages: vec![Vec::new()], y: height - MARGIN, blank: true }
41
+ /// The document's pages, between its margins.
42
+ ///
43
+ /// # Errors
44
+ /// [`Error::InvalidMargin`] when the margins leave no room.
45
+ pub fn new(document: &Document) -> Result<Self> {
46
+ document.room()?;
47
+ let (width, height) = document.page_size().points();
48
+ let margin = document.margin();
49
+ Ok(Self { width, height, margin, pages: vec![Vec::new()], y: height - margin, blank: true })
45
50
  }
46
51
 
47
52
  pub const fn page_size(&self) -> (f64, f64) {
@@ -49,7 +54,7 @@ impl Layout {
49
54
  }
50
55
 
51
56
  const fn content_width(&self) -> f64 {
52
- (-2.0f64).mul_add(MARGIN, self.width)
57
+ (-2.0f64).mul_add(self.margin, self.width)
53
58
  }
54
59
 
55
60
  pub fn heading(&mut self, level: u8, text: &str) -> Result<()> {
@@ -105,7 +110,7 @@ impl Layout {
105
110
  }
106
111
 
107
112
  let header_height = rows.first().filter(|row| row.2).map_or(0.0, |row| row.1);
108
- let room = (-2.0f64).mul_add(MARGIN, self.height) - header_height;
113
+ let room = (-2.0f64).mul_add(self.margin, self.height) - header_height;
109
114
  for (index, (cells, height, header)) in rows.iter().enumerate() {
110
115
  (*height <= room || *header).ok_or(Error::RowTooTall(*height))?;
111
116
  // A header needs the first body row beside it.
@@ -121,6 +126,35 @@ impl Layout {
121
126
  Ok(())
122
127
  }
123
128
 
129
+ /// Draws picture `index` (`/Im{index}`), on a new page when it does not
130
+ /// fit, as its alignment says.
131
+ pub fn picture(&mut self, picture: &Picture, index: usize) {
132
+ let (width, height) = picture.size(self.room());
133
+ let (x, y) = self.place((width, height), picture.align);
134
+ let (width, height, x, y) = (Num(width), Num(height), Num(x), Num(y));
135
+ emit(self.current(), format_args!("q {width} 0 0 {height} {x} {y} cm /Im{index} Do Q\n"));
136
+ self.end_figure();
137
+ }
138
+
139
+ /// Draws a barcode as filled rectangles, one module a unit, the
140
+ /// coordinates flipped to run down from its top left corner.
141
+ pub fn symbol(&mut self, symbol: &Symbol) {
142
+ let (shapes, (width, height)) = symbol.placed(self.room());
143
+ let (x, y) = self.place((width, height), symbol.align);
144
+ let (columns, rows) = shapes.extent();
145
+ let (across, down) = (width / columns, height / rows);
146
+ let (fill, top, x) = (Fill(symbol.color), Num(y + height), Num(x));
147
+ let page = self.current();
148
+ // Four decimals: a module's size in hundredths of a point would be
149
+ // off by up to half a percent across the symbol.
150
+ emit(page, format_args!("q {fill} {across:.4} 0 0 {:.4} {x} {top} cm\n", -down));
151
+ for [x, y, run, rows] in &shapes.rects {
152
+ emit(page, format_args!("{x} {y} {run} {rows} re\n"));
153
+ }
154
+ page.extend_from_slice(b"f Q\n");
155
+ self.end_figure();
156
+ }
157
+
124
158
  pub fn page_break(&mut self) {
125
159
  self.new_page();
126
160
  }
@@ -129,24 +163,48 @@ impl Layout {
129
163
  pub fn finish(mut self, page_numbers: bool) -> Vec<Vec<u8>> {
130
164
  if page_numbers {
131
165
  let total = self.pages.len();
132
- let center = self.width / 2.0;
166
+ let (center, margin) = (self.width / 2.0, self.margin);
133
167
  for (index, page) in self.pages.iter_mut().enumerate() {
134
168
  let bytes = format!("{} / {total}", index + 1).into_bytes();
135
169
  let width = Face::Regular.width(&bytes, FOOTER_SIZE);
136
170
  let look =
137
171
  Look { face: Face::Regular, color: Some(FOOTER_COLOR), underline: false };
138
172
  let label = wrap::Fragment { bytes, look, width };
139
- draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0, MARGIN / 2.0);
173
+ draw_fragment(page, &label, FOOTER_SIZE, center - width / 2.0, margin / 2.0);
140
174
  }
141
175
  }
142
176
  self.pages
143
177
  }
144
178
 
179
+ /// The width and height between the margins.
180
+ const fn room(&self) -> (f64, f64) {
181
+ (self.content_width(), (-2.0f64).mul_add(self.margin, self.height))
182
+ }
183
+
184
+ /// Makes room for a figure of `size`, on a new page when it does not
185
+ /// fit, and returns its bottom left corner as `align` places it.
186
+ fn place(&mut self, (width, height): (f64, f64), align: Option<Align>) -> (f64, f64) {
187
+ self.ensure_room(height);
188
+ let x = self.margin
189
+ + match align.unwrap_or(Align::Left) {
190
+ Align::Left => 0.0,
191
+ Align::Center => (self.content_width() - width) / 2.0,
192
+ Align::Right => self.content_width() - width,
193
+ };
194
+ self.y -= height;
195
+ (x, self.y)
196
+ }
197
+
198
+ fn end_figure(&mut self) {
199
+ self.y -= PARAGRAPH_GAP;
200
+ self.blank = false;
201
+ }
202
+
145
203
  /// Starts a new page unless `needed` points fit above the bottom margin
146
204
  /// (or the page is still blank, where nothing would fit better).
147
205
  /// Returns whether it did.
148
206
  fn ensure_room(&mut self, needed: f64) -> bool {
149
- let full = !self.blank && self.y - needed < MARGIN;
207
+ let full = !self.blank && self.y - needed < self.margin;
150
208
  if full {
151
209
  self.new_page();
152
210
  }
@@ -155,7 +213,7 @@ impl Layout {
155
213
 
156
214
  fn new_page(&mut self) {
157
215
  self.pages.push(Vec::new());
158
- self.y = self.height - MARGIN;
216
+ self.y = self.height - self.margin;
159
217
  self.blank = true;
160
218
  }
161
219
 
@@ -174,7 +232,7 @@ impl Layout {
174
232
  self.ensure_room(leading);
175
233
  let baseline = self.y - baseline_offset(size);
176
234
  let slack = self.content_width() - line.width;
177
- let x = MARGIN
235
+ let x = self.margin
178
236
  + match align {
179
237
  Align::Left => 0.0,
180
238
  Align::Center => slack / 2.0,
@@ -188,9 +246,8 @@ impl Layout {
188
246
 
189
247
  /// One table row from the cursor down: fill, borders, then text.
190
248
  fn row(&mut self, cells: &[Vec<Line>], height: f64, column_width: f64, header: bool) {
191
- let top = self.y;
249
+ let (top, mut x) = (self.y, self.margin);
192
250
  let page = self.current();
193
- let mut x = MARGIN;
194
251
  for lines in cells {
195
252
  let rect = format_args!(
196
253
  "{} {} {} {} re",
@@ -1,6 +1,9 @@
1
1
  //! PDF file syntax: numbered objects, compressed streams, the
2
2
  //! cross-reference table and the trailer (ISO 32000-1, section 7).
3
3
 
4
+ #[cfg(test)]
5
+ mod tests;
6
+
4
7
  use std::fmt;
5
8
  use std::io::Write;
6
9
 
@@ -34,13 +37,34 @@ impl<W: Write> PdfWriter<W> {
34
37
  self.write_fmt(format_args!("{dictionary}\nendobj\n"))
35
38
  }
36
39
 
37
- /// Writes a Flate-compressed stream object.
38
- pub fn stream(&mut self, id: usize, data: &[u8]) -> Result<()> {
40
+ /// Writes a Flate-compressed stream object with optional dictionary entries.
41
+ /// Names omit the leading `/`; values are already PDF syntax. For example,
42
+ /// embedded TrueType fonts need `Length1` to hold the decoded byte count.
43
+ /// The writer supplies `Length` and `Filter`; entries must not repeat them.
44
+ pub fn stream(
45
+ &mut self,
46
+ id: usize,
47
+ data: &[u8],
48
+ entries: &[(&str, &dyn fmt::Display)],
49
+ ) -> Result<()> {
39
50
  let compressed = miniz_oxide::deflate::compress_to_vec_zlib(data, LEVEL);
51
+ let dictionary = fmt::from_fn(|f| {
52
+ f.write_str("/Filter /FlateDecode")?;
53
+ for (name, value) in entries {
54
+ write!(f, " /{name} {value}")?;
55
+ }
56
+ Ok(())
57
+ });
58
+ self.stored(id, format_args!("{dictionary}"), &compressed)
59
+ }
60
+
61
+ /// Writes a stream object as it is stored, `dictionary` following its
62
+ /// `/Length`.
63
+ pub fn stored(&mut self, id: usize, dictionary: fmt::Arguments<'_>, data: &[u8]) -> Result<()> {
40
64
  self.begin(id)?;
41
- let length = compressed.len();
42
- self.write_fmt(format_args!("<< /Length {length} /Filter /FlateDecode >>\nstream\n"))?;
43
- self.write(&compressed)?;
65
+ let length = data.len();
66
+ self.write_fmt(format_args!("<< /Length {length} {dictionary} >>\nstream\n"))?;
67
+ self.write(data)?;
44
68
  self.write(b"\nendstream\nendobj\n")
45
69
  }
46
70
 
@@ -0,0 +1,197 @@
1
+ //! Pictures as image `XObject`s: a JPEG as it is; a PNG's compressed pixels
2
+ //! as they are, PDF reading them with the PNG predictor, unless an alpha
3
+ //! channel needs them decoded into a soft mask.
4
+
5
+ use std::borrow::Cow;
6
+ use std::fmt::Write as _;
7
+
8
+ use crate::doc::Picture;
9
+ use crate::doc::picture::{Kind, Png};
10
+ use crate::{Error, Result};
11
+
12
+ /// The most pixels a PNG with alpha may have: it is decoded in memory.
13
+ const MAX_DECODED: u64 = 1 << 26;
14
+
15
+ /// An image stream: its dictionary after `/Length`, and its data as stored.
16
+ pub struct Stream<'a> {
17
+ pub dictionary: String,
18
+ pub data: Cow<'a, [u8]>,
19
+ }
20
+
21
+ /// Whether the picture has a soft mask, written right after it.
22
+ pub const fn has_mask(picture: &Picture) -> bool {
23
+ match &picture.kind {
24
+ Kind::Png(png) => alpha(png),
25
+ Kind::Jpeg { .. } => false,
26
+ }
27
+ }
28
+
29
+ /// The image, then its soft mask when it has one; `mask` is the mask's
30
+ /// object number.
31
+ pub fn streams(picture: &Picture, mask: usize) -> Result<Vec<Stream<'_>>> {
32
+ let (width, height) = picture.pixels;
33
+ let head = format!("/Type /XObject /Subtype /Image /Width {width} /Height {height}");
34
+ match &picture.kind {
35
+ Kind::Jpeg { components, inverted } => {
36
+ let space = ["DeviceGray", "", "DeviceRGB", "DeviceCMYK"][usize::from(*components) - 1];
37
+ let decode = if *inverted { " /Decode [1 0 1 0 1 0 1 0]" } else { "" };
38
+ let dictionary = format!(
39
+ "{head} /ColorSpace /{space} /BitsPerComponent 8 /Filter /DCTDecode{decode}"
40
+ );
41
+ Ok(vec![Stream { dictionary, data: Cow::Borrowed(&picture.data) }])
42
+ }
43
+ Kind::Png(png) if alpha(png) => decoded(png, (width, height), &head, mask),
44
+ Kind::Png(png) => Ok(vec![predicted(png, width, &head)]),
45
+ }
46
+ }
47
+
48
+ /// Alpha that only a soft mask carries: an alpha channel, or a palette's.
49
+ const fn alpha(png: &Png) -> bool {
50
+ matches!(png.color, 4 | 6) || (png.color == 3 && !png.transparency.is_empty())
51
+ }
52
+
53
+ /// The compressed pixels as stored, read through the PNG predictor; a
54
+ /// transparent color becomes a color key mask.
55
+ fn predicted<'a>(png: &'a Png, width: u32, head: &str) -> Stream<'a> {
56
+ let depth = png.depth;
57
+ let (colors, space) = match png.color {
58
+ 0 => (1, "/DeviceGray".to_owned()),
59
+ 2 => (3, "/DeviceRGB".to_owned()),
60
+ _ => (1, indexed(&png.palette)),
61
+ };
62
+ let mut dictionary = format!(
63
+ "{head} /ColorSpace {space} /BitsPerComponent {depth} /Filter /FlateDecode \
64
+ /DecodeParms << /Predictor 15 /Colors {colors} /BitsPerComponent {depth} \
65
+ /Columns {width} >>"
66
+ );
67
+ if !png.transparency.is_empty() {
68
+ dictionary.push_str(" /Mask [");
69
+ for key in png.transparency.as_chunks::<2>().0 {
70
+ let value = u16::from_be_bytes(*key);
71
+ // Writing to a String cannot fail.
72
+ let _ = write!(dictionary, "{value} {value} ");
73
+ }
74
+ dictionary.push(']');
75
+ }
76
+ Stream { dictionary, data: Cow::Borrowed(&png.compressed) }
77
+ }
78
+
79
+ /// `[/Indexed /DeviceRGB n <palette>]`.
80
+ fn indexed(palette: &[u8]) -> String {
81
+ let mut space = format!("[/Indexed /DeviceRGB {} <", palette.len() / 3 - 1);
82
+ for byte in palette {
83
+ // Writing to a String cannot fail.
84
+ let _ = write!(space, "{byte:02X}");
85
+ }
86
+ space.push_str(">]");
87
+ space
88
+ }
89
+
90
+ /// The pixels decoded into 8-bit color and alpha, both compressed again.
91
+ fn decoded<'a>(
92
+ png: &Png,
93
+ (width, height): (u32, u32),
94
+ head: &str,
95
+ mask: usize,
96
+ ) -> Result<Vec<Stream<'a>>> {
97
+ let damaged = || Error::InvalidImage("a damaged PNG");
98
+ (u64::from(width) * u64::from(height) <= MAX_DECODED)
99
+ .ok_or(Error::InvalidImage("a PNG with alpha of at most 67 million pixels"))?;
100
+ let channels = match png.color {
101
+ 4 => 2,
102
+ 6 => 4,
103
+ _ => 1,
104
+ };
105
+ let bits = channels * usize::from(png.depth);
106
+ let (width, height) = (width as usize, height as usize);
107
+ let stride = (width * bits).div_ceil(8);
108
+ let filtered = miniz_oxide::inflate::decompress_to_vec_zlib_with_limit(
109
+ &png.compressed,
110
+ height * (stride + 1),
111
+ )
112
+ .map_err(|_| damaged())?;
113
+ let pixels = unfilter(&filtered, stride, bits.div_ceil(8), height).ok_or_else(damaged)?;
114
+ let gray = png.color == 4;
115
+ let (mut color, mut alpha) =
116
+ (Vec::with_capacity(width * height * 3), Vec::with_capacity(width * height));
117
+ for row in pixels.chunks_exact(stride) {
118
+ for x in 0..width {
119
+ if png.color == 3 {
120
+ let index = usize::from(sample(row, x, png.depth));
121
+ let rgb = png.palette.get(index * 3..index * 3 + 3).ok_or_else(damaged)?;
122
+ color.extend_from_slice(rgb);
123
+ alpha.push(png.transparency.get(index).copied().unwrap_or(u8::MAX));
124
+ } else {
125
+ // 16-bit samples keep their high byte.
126
+ let step = usize::from(png.depth / 8);
127
+ let at = |channel: usize| row[(x * channels + channel) * step];
128
+ color.extend((0..channels - 1).map(at));
129
+ alpha.push(at(channels - 1));
130
+ }
131
+ }
132
+ }
133
+ let space = if gray { "DeviceGray" } else { "DeviceRGB" };
134
+ let compress = |data: &[u8]| Cow::Owned(miniz_oxide::deflate::compress_to_vec_zlib(data, 6));
135
+ Ok(vec![
136
+ Stream {
137
+ dictionary: format!(
138
+ "{head} /ColorSpace /{space} /BitsPerComponent 8 /Filter /FlateDecode \
139
+ /SMask {mask} 0 R"
140
+ ),
141
+ data: compress(&color),
142
+ },
143
+ Stream {
144
+ dictionary: format!(
145
+ "{head} /ColorSpace /DeviceGray /BitsPerComponent 8 /Filter /FlateDecode"
146
+ ),
147
+ data: compress(&alpha),
148
+ },
149
+ ])
150
+ }
151
+
152
+ /// The `x`th sample of a row of `depth`-bit samples.
153
+ fn sample(row: &[u8], x: usize, depth: u8) -> u8 {
154
+ let depth = usize::from(depth);
155
+ let (byte, shift) = (x * depth / 8, 8 - depth - x * depth % 8);
156
+ (row[byte] >> shift) & (u8::MAX >> (8 - depth))
157
+ }
158
+
159
+ /// PNG's filters undone (ISO/IEC 15948, 9): rows of `stride` bytes, `bpp`
160
+ /// bytes a pixel.
161
+ fn unfilter(data: &[u8], stride: usize, bpp: usize, rows: usize) -> Option<Vec<u8>> {
162
+ let mut out = vec![0_u8; stride * rows];
163
+ for row in 0..rows {
164
+ let line = data.get(row * (stride + 1)..(row + 1) * (stride + 1))?;
165
+ let (done, rest) = out.split_at_mut(row * stride);
166
+ let previous = row.checked_sub(1).map(|_| &done[done.len() - stride..]);
167
+ let current = &mut rest[..stride];
168
+ for at in 0..stride {
169
+ let left = at.checked_sub(bpp).map_or(0, |at| current[at]);
170
+ let up = previous.map_or(0, |previous| previous[at]);
171
+ let up_left = at.checked_sub(bpp).and_then(|at| previous.map(|previous| previous[at]));
172
+ let predicted = match line[0] {
173
+ 0 => 0,
174
+ 1 => left,
175
+ 2 => up,
176
+ 3 => left.midpoint(up),
177
+ 4 => paeth(left, up, up_left.unwrap_or(0)),
178
+ _ => return None,
179
+ };
180
+ current[at] = line[at + 1].wrapping_add(predicted);
181
+ }
182
+ }
183
+ Some(out)
184
+ }
185
+
186
+ /// The neighbor nearest `left + up - up_left`.
187
+ fn paeth(left: u8, up: u8, up_left: u8) -> u8 {
188
+ let estimate = i16::from(left) + i16::from(up) - i16::from(up_left);
189
+ let distance = |byte: u8| (estimate - i16::from(byte)).abs();
190
+ if distance(left) <= distance(up) && distance(left) <= distance(up_left) {
191
+ left
192
+ } else if distance(up) <= distance(up_left) {
193
+ up
194
+ } else {
195
+ up_left
196
+ }
197
+ }