imprimatur 0.3.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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -0
  3. data/Cargo.lock +33 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +41 -1
  6. data/ext/imprimatur_native/cli/Cargo.toml +3 -1
  7. data/ext/imprimatur_native/core/Cargo.toml +15 -4
  8. data/ext/imprimatur_native/core/README.md +27 -4
  9. data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
  10. data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
  11. data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
  12. data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
  13. data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
  14. data/ext/imprimatur_native/core/src/barcode.rs +314 -0
  15. data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
  16. data/ext/imprimatur_native/core/src/doc.rs +11 -1
  17. data/ext/imprimatur_native/core/src/docx.rs +22 -10
  18. data/ext/imprimatur_native/core/src/error.rs +6 -0
  19. data/ext/imprimatur_native/core/src/font.rs +3 -1
  20. data/ext/imprimatur_native/core/src/lib.rs +3 -1
  21. data/ext/imprimatur_native/core/src/pdf/layout.rs +48 -14
  22. data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
  23. data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
  24. data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
  25. data/ext/imprimatur_native/core/src/pdf.rs +9 -1
  26. data/ext/imprimatur_native/core/src/read/image.rs +17 -0
  27. data/ext/imprimatur_native/core/src/read.rs +3 -2
  28. data/ext/imprimatur_native/core/src/style.rs +11 -0
  29. data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
  30. data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
  31. data/ext/imprimatur_native/ffi/src/document.rs +29 -1
  32. data/ext/imprimatur_native/ffi/src/lib.rs +5 -3
  33. data/lib/imprimatur/barcode.rb +78 -0
  34. data/lib/imprimatur/document.rb +17 -1
  35. data/lib/imprimatur/version.rb +1 -1
  36. data/lib/imprimatur.rb +4 -0
  37. metadata +13 -2
  38. data/ext/imprimatur_native/core/src/test_support.rs +0 -251
@@ -0,0 +1,136 @@
1
+ //! Barcodes in the flow: shapes in a PDF, a picture in DOCX.
2
+
3
+ use crate::Result;
4
+ use crate::barcode::{Barcode, Look, Shapes};
5
+ use crate::doc::Picture;
6
+ use crate::style::{Align, Color};
7
+
8
+ /// The largest side, in points, a symbol may be set to: PDF's page limit.
9
+ const MAX_POINTS: f64 = 14_400.0;
10
+ /// Pixels an inch a DOCX picture of a symbol has at least.
11
+ const DOCX_DPI: f64 = 300.0;
12
+
13
+ /// A [`Barcode`] drawn in a document's flow.
14
+ ///
15
+ /// It is drawn at the width or height set (in points; a QR Code or Data
16
+ /// Matrix keeps square modules, a linear code's height is its bars'), or
17
+ /// else at its symbology's usual module size, never larger than the page
18
+ /// leaves room for. Its quiet zone is part of its size.
19
+ ///
20
+ /// ```
21
+ /// use imprimatur::barcode::{Barcode, Level};
22
+ /// use imprimatur::doc::{Document, Symbol};
23
+ /// use imprimatur::style::Align;
24
+ ///
25
+ /// let mut doc = Document::new();
26
+ /// let qr = Barcode::qr("https://example.com", Level::Medium)?;
27
+ /// doc.barcode(Symbol::new(qr).width(144.0)?.align(Align::Center));
28
+ /// doc.barcode(Barcode::ean13("950600013435")?);
29
+ /// assert!(doc.to_pdf()?.starts_with(b"%PDF-"));
30
+ /// # Ok::<(), imprimatur::Error>(())
31
+ /// ```
32
+ #[derive(Debug, Clone, PartialEq)]
33
+ pub struct Symbol {
34
+ pub(crate) barcode: Barcode,
35
+ width: Option<f64>,
36
+ height: Option<f64>,
37
+ pub(crate) align: Option<Align>,
38
+ pub(crate) color: Color,
39
+ }
40
+
41
+ impl Symbol {
42
+ #[must_use]
43
+ pub const fn new(barcode: Barcode) -> Self {
44
+ Self { barcode, width: None, height: None, align: None, color: Color::BLACK }
45
+ }
46
+
47
+ /// Its width in points, quiet zone included.
48
+ ///
49
+ /// # Errors
50
+ /// [`Error::InvalidBarcode`](crate::Error::InvalidBarcode) outside 1 to
51
+ /// 14 400 points.
52
+ pub fn width(mut self, points: f64) -> Result<Self> {
53
+ self.width = Some(self.points_in_range(points)?);
54
+ Ok(self)
55
+ }
56
+
57
+ /// Its height in points: a linear code's bars, a QR Code or Data
58
+ /// Matrix's whole (quiet zone included).
59
+ ///
60
+ /// # Errors
61
+ /// [`Error::InvalidBarcode`](crate::Error::InvalidBarcode) outside 1 to
62
+ /// 14 400 points.
63
+ pub fn height(mut self, points: f64) -> Result<Self> {
64
+ self.height = Some(self.points_in_range(points)?);
65
+ Ok(self)
66
+ }
67
+
68
+ #[must_use]
69
+ pub const fn align(mut self, align: Align) -> Self {
70
+ self.align = Some(align);
71
+ self
72
+ }
73
+
74
+ /// The bars' color; black unless set.
75
+ #[must_use]
76
+ pub const fn color(mut self, color: Color) -> Self {
77
+ self.color = color;
78
+ self
79
+ }
80
+
81
+ /// Its rectangles and the size they are drawn at, in points, fitted
82
+ /// into `room` (the width and height between the margins).
83
+ pub(crate) fn placed(&self, room: (f64, f64)) -> (Shapes, (f64, f64)) {
84
+ let shapes = self.barcode.shapes(true, None);
85
+ let (columns, rows) = shapes.extent();
86
+ let module = self.barcode.default_module();
87
+ let (width, height) = if self.barcode.symbology().is_linear() {
88
+ let width = self.width.unwrap_or(columns * module);
89
+ (width, self.height.unwrap_or(rows * width / columns))
90
+ } else {
91
+ let module = match (self.width, self.height) {
92
+ (Some(width), Some(height)) => (width / columns).min(height / rows),
93
+ (Some(width), None) => width / columns,
94
+ (None, Some(height)) => height / rows,
95
+ (None, None) => module,
96
+ };
97
+ (columns * module, rows * module)
98
+ };
99
+ let scale = (room.0 / width).min(room.1 / height).min(1.0);
100
+ (shapes, (width * scale, height * scale))
101
+ }
102
+
103
+ /// A PNG of it at 300 pixels an inch or more, for formats that take
104
+ /// pictures only.
105
+ pub(crate) fn picture(&self, room: (f64, f64)) -> Result<Picture> {
106
+ let (shapes, (width, height)) = self.placed(room);
107
+ let module = width / shapes.extent().0;
108
+ #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "1 to 64")]
109
+ let scale = (module * DOCX_DPI / 72.0).ceil().clamp(1.0, 64.0) as u32;
110
+ #[expect(clippy::cast_possible_truncation, clippy::cast_sign_loss, reason = "bars, small")]
111
+ let bar_height = (height / module).round().max(1.0) as u32;
112
+ let look = Look {
113
+ dark: self.color,
114
+ light: Some(Color::WHITE),
115
+ quiet_zone: true,
116
+ bar_height: Some(bar_height),
117
+ };
118
+ let png = self.barcode.png(scale, &look)?;
119
+ let picture = Picture::new(png)?.width(width)?.height(height)?;
120
+ Ok(match self.align {
121
+ Some(align) => picture.align(align),
122
+ None => picture,
123
+ })
124
+ }
125
+
126
+ fn points_in_range(&self, points: f64) -> Result<f64> {
127
+ let invalid = || self.barcode.symbology().invalid("a size of 1 to 14 400 points");
128
+ (1.0..=MAX_POINTS).contains(&points).then_some(points).ok_or_else(invalid)
129
+ }
130
+ }
131
+
132
+ impl From<Barcode> for Symbol {
133
+ fn from(barcode: Barcode) -> Self {
134
+ Self::new(barcode)
135
+ }
136
+ }
@@ -23,6 +23,7 @@
23
23
 
24
24
  mod paragraph;
25
25
  pub(crate) mod picture;
26
+ mod symbol;
26
27
  mod table;
27
28
  #[cfg(test)]
28
29
  mod tests;
@@ -33,6 +34,7 @@ use std::path::Path;
33
34
 
34
35
  pub use paragraph::{Paragraph, Run};
35
36
  pub use picture::Picture;
37
+ pub use symbol::Symbol;
36
38
  pub use table::Table;
37
39
 
38
40
  use crate::script::Counts;
@@ -96,6 +98,7 @@ pub enum Block {
96
98
  Paragraph(Paragraph),
97
99
  Table(Table),
98
100
  Picture(Picture),
101
+ Symbol(Symbol),
99
102
  PageBreak,
100
103
  }
101
104
 
@@ -158,6 +161,13 @@ impl Document {
158
161
  self
159
162
  }
160
163
 
164
+ /// A barcode, in the flow of the text: a [`Barcode`](crate::barcode::Barcode)
165
+ /// at its usual size, or a [`Symbol`] sized and aligned.
166
+ pub fn barcode(&mut self, symbol: impl Into<Symbol>) -> &mut Self {
167
+ self.blocks.push(Block::Symbol(symbol.into()));
168
+ self
169
+ }
170
+
161
171
  /// Numbers pages `1 / 3` at the bottom of each one.
162
172
  pub const fn set_page_numbers(&mut self, enabled: bool) -> &mut Self {
163
173
  self.page_numbers = enabled;
@@ -222,7 +232,7 @@ impl Document {
222
232
  paragraph.runs.iter().for_each(|run| counts.add(&run.text));
223
233
  }
224
234
  Block::Table(table) => table.count(&mut counts),
225
- Block::Picture(_) | Block::PageBreak => {}
235
+ Block::Picture(_) | Block::Symbol(_) | Block::PageBreak => {}
226
236
  }
227
237
  }
228
238
  counts.right_to_left()
@@ -6,6 +6,7 @@ mod picture;
6
6
  #[cfg(test)]
7
7
  mod tests;
8
8
 
9
+ use std::borrow::Cow;
9
10
  use std::io::Write;
10
11
 
11
12
  use crate::Result;
@@ -29,11 +30,17 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
29
30
  }
30
31
  // Pictures' files follow, their relationships numbered from here.
31
32
  let first_media = parts.len() + 1;
32
- let pictures: Vec<&Picture> = document
33
+ // Barcodes go in as pictures.
34
+ let rendered = document
33
35
  .blocks()
34
36
  .iter()
35
- .filter_map(|block| if let Block::Picture(picture) = block { Some(picture) } else { None })
36
- .collect();
37
+ .filter_map(|block| match block {
38
+ Block::Picture(picture) => Some(Ok(Cow::Borrowed(picture))),
39
+ Block::Symbol(symbol) => Some(symbol.picture(room).map(Cow::Owned)),
40
+ _ => None,
41
+ })
42
+ .collect::<Result<Vec<Cow<'_, Picture>>>>()?;
43
+ let pictures: Vec<&Picture> = rendered.iter().map(|picture| &**picture).collect();
37
44
  for (number, picture) in (1..).zip(&pictures) {
38
45
  parts.push(("image", picture::file_name(picture, number)));
39
46
  }
@@ -41,7 +48,7 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
41
48
  zip.add("[Content_Types].xml", content_types(footer, &pictures).as_bytes())?;
42
49
  let package = opc::relationships(&[("officeDocument", "word/document.xml")]);
43
50
  zip.add("_rels/.rels", package.as_bytes())?;
44
- zip.add("word/document.xml", body(document, room, first_media).as_bytes())?;
51
+ zip.add("word/document.xml", body(document, room, first_media, &pictures).as_bytes())?;
45
52
  zip.add("word/_rels/document.xml.rels", opc::relationships(&parts).as_bytes())?;
46
53
  zip.add("word/styles.xml", styles().as_bytes())?;
47
54
  if footer {
@@ -102,7 +109,12 @@ fn page_number_footer() -> String {
102
109
  xml.finish()
103
110
  }
104
111
 
105
- fn body(document: &Document, room: (f64, f64), first_media: usize) -> String {
112
+ fn body(
113
+ document: &Document,
114
+ room: (f64, f64),
115
+ first_media: usize,
116
+ pictures: &[&Picture],
117
+ ) -> String {
106
118
  // What reads in no direction of its own (figures, punctuation) takes the
107
119
  // document's.
108
120
  let rtl = document.reads_right_to_left();
@@ -116,7 +128,7 @@ fn body(document: &Document, room: (f64, f64), first_media: usize) -> String {
116
128
  ];
117
129
  xml.open("w:document", &namespaces);
118
130
  xml.open("w:body", &[]);
119
- let mut pictures = 0;
131
+ let mut drawn = 0;
120
132
  for block in document.blocks() {
121
133
  match block {
122
134
  Block::Heading { level, text } => {
@@ -125,10 +137,10 @@ fn body(document: &Document, room: (f64, f64), first_media: usize) -> String {
125
137
  }
126
138
  Block::Paragraph(content) => paragraph(&mut xml, content, rtl),
127
139
  Block::Table(content) => table(&mut xml, content, document, rtl),
128
- Block::Picture(content) => {
129
- pictures += 1;
130
- let relationship = format!("rId{}", first_media + pictures - 1);
131
- picture::paragraph(&mut xml, content, room, pictures, &relationship);
140
+ Block::Picture(_) | Block::Symbol(_) => {
141
+ drawn += 1;
142
+ let relationship = format!("rId{}", first_media + drawn - 1);
143
+ picture::paragraph(&mut xml, pictures[drawn - 1], room, drawn, &relationship);
132
144
  }
133
145
  Block::PageBreak => {
134
146
  xml.open("w:p", &[]);
@@ -88,6 +88,12 @@ pub enum Error {
88
88
  #[error("a table row needs {0:.0}pt, more than a whole page holds")]
89
89
  RowTooTall(f64),
90
90
 
91
+ #[error("invalid {symbology} barcode: {reason}")]
92
+ InvalidBarcode { symbology: &'static str, reason: String },
93
+
94
+ #[error("invalid GS1 element string: {0}")]
95
+ InvalidElementString(String),
96
+
91
97
  #[error("a workbook needs at least one sheet")]
92
98
  EmptyWorkbook,
93
99
 
@@ -224,6 +224,8 @@ fn unicode_cmap<'a>(cmap: &Cmap<'a>) -> Result<CmapSubtable<'a>> {
224
224
  Err(Error::UnsupportedFont("no supported Unicode cmap"))
225
225
  }
226
226
 
227
- fn invalid(reason: impl std::fmt::Display) -> Error {
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 {
228
230
  Error::malformed("TrueType font", reason.to_string())
229
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,6 +21,7 @@
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;
@@ -8,7 +8,7 @@ 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::{Document, Paragraph, Picture, 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};
@@ -129,20 +129,30 @@ impl Layout {
129
129
  /// Draws picture `index` (`/Im{index}`), on a new page when it does not
130
130
  /// fit, as its alignment says.
131
131
  pub fn picture(&mut self, picture: &Picture, index: usize) {
132
- let room = (self.content_width(), (-2.0f64).mul_add(self.margin, self.height));
133
- let (width, height) = picture.size(room);
134
- self.ensure_room(height);
135
- let x = self.margin
136
- + match picture.align.unwrap_or(Align::Left) {
137
- Align::Left => 0.0,
138
- Align::Center => (room.0 - width) / 2.0,
139
- Align::Right => room.0 - width,
140
- };
141
- self.y -= height;
142
- let (width, height, x, y) = (Num(width), Num(height), Num(x), Num(self.y));
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));
143
135
  emit(self.current(), format_args!("q {width} 0 0 {height} {x} {y} cm /Im{index} Do Q\n"));
144
- self.y -= PARAGRAPH_GAP;
145
- self.blank = false;
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();
146
156
  }
147
157
 
148
158
  pub fn page_break(&mut self) {
@@ -166,6 +176,30 @@ impl Layout {
166
176
  self.pages
167
177
  }
168
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
+
169
203
  /// Starts a new page unless `needed` points fit above the bottom margin
170
204
  /// (or the page is still blank, where nothing would fit better).
171
205
  /// Returns whether it did.
@@ -0,0 +1,129 @@
1
+ //! TrueType metadata needed by a PDF `FontDescriptor`, in 1000-unit glyph space.
2
+
3
+ #[cfg(test)]
4
+ mod tests;
5
+
6
+ use std::io::Write;
7
+
8
+ use read_fonts::{
9
+ FontRead, FontRef, TableProvider,
10
+ tables::{head::MacStyle, os2::Os2, post::Post},
11
+ types::{NameId, Tag},
12
+ };
13
+
14
+ use crate::font::{TrueTypeFont, invalid};
15
+ use crate::pdf::objects::{Num, PdfWriter};
16
+ use crate::{Error, Result};
17
+
18
+ pub struct Descriptor {
19
+ /// PostScript name, escaped as a PDF name (without its leading slash).
20
+ pub name: String,
21
+ bounds: [Num; 4],
22
+ ascent: Num,
23
+ descent: Num,
24
+ cap_height: Num,
25
+ italic_angle: Num,
26
+ flags: u32,
27
+ stem: Num,
28
+ }
29
+
30
+ impl Descriptor {
31
+ pub fn read(font: &TrueTypeFont<'_>) -> Result<Self> {
32
+ let tables = FontRef::new(font.as_bytes()).map_err(invalid)?;
33
+ let head = tables.head().map_err(invalid)?;
34
+ let post =
35
+ tables.table_data(Tag::new(b"post")).map(Post::read).transpose().map_err(invalid)?;
36
+ let os2 =
37
+ tables.table_data(Tag::new(b"OS/2")).map(Os2::read).transpose().map_err(invalid)?;
38
+ if let Some(os2) = &os2 {
39
+ let rights = os2.fs_type();
40
+ // Older fonts can set several rights bits; the least restrictive wins.
41
+ (rights & 0x0002 == 0 || rights & 0x000C != 0)
42
+ .ok_or(Error::UnsupportedFont("restricted font embedding"))?;
43
+ (rights & 0x0200 == 0).ok_or(Error::UnsupportedFont("bitmap-only font embedding"))?;
44
+ // The no-subsetting bit is compatible with embedding the complete font.
45
+ }
46
+ (head.x_min() <= head.x_max() && head.y_min() <= head.y_max())
47
+ .ok_or_else(|| invalid("inverted font bounding box"))?;
48
+ let metrics = font.metrics();
49
+ let scale = 1000.0 / f64::from(metrics.units_per_em);
50
+ let cap_height = os2
51
+ .as_ref()
52
+ .and_then(Os2::s_cap_height)
53
+ .filter(|height| *height > 0)
54
+ .unwrap_or(metrics.ascender);
55
+ let angle = post.as_ref().map_or(0.0, |post| post.italic_angle().to_f64());
56
+ // Symbolic: Identity-H is a CID encoding, not the standard Latin encoding.
57
+ let mut flags = 4;
58
+ if post.as_ref().is_some_and(|post| post.is_fixed_pitch() != 0) {
59
+ flags |= 1;
60
+ }
61
+ if angle != 0.0 || head.mac_style().contains(MacStyle::ITALIC) {
62
+ flags |= 64;
63
+ }
64
+ let family = os2.as_ref().map_or(0, |os2| os2.s_family_class() >> 8);
65
+ if (1..=7).contains(&family) {
66
+ flags |= 2;
67
+ } else if family == 10 {
68
+ flags |= 8;
69
+ }
70
+ let weight = os2.as_ref().map_or(400, |os2| os2.us_weight_class().clamp(1, 1000));
71
+ let stem_weight = f64::from(weight) / 65.0;
72
+ Ok(Self {
73
+ name: postscript_name(&tables)?,
74
+ bounds: [head.x_min(), head.y_min(), head.x_max(), head.y_max()]
75
+ .map(|value| Num(f64::from(value) * scale)),
76
+ ascent: Num(f64::from(metrics.ascender) * scale),
77
+ descent: Num(f64::from(metrics.descender) * scale),
78
+ // Older OS/2 tables lack cap height; the ascender is an approximation.
79
+ cap_height: Num(f64::from(cap_height) * scale),
80
+ italic_angle: Num(angle),
81
+ flags,
82
+ // TrueType has no stem-width field: estimate it from the weight class.
83
+ stem: Num(stem_weight.mul_add(stem_weight, 50.0)),
84
+ })
85
+ }
86
+
87
+ pub fn write<W: Write>(&self, pdf: &mut PdfWriter<W>, id: usize, file: usize) -> Result<()> {
88
+ let Self { name, bounds, ascent, descent, cap_height, italic_angle, flags, stem } = self;
89
+ let [left, bottom, right, top] = bounds;
90
+ pdf.object(
91
+ id,
92
+ format_args!(
93
+ "<< /Type /FontDescriptor /FontName /{name} /Flags {flags} \
94
+ /FontBBox [{left} {bottom} {right} {top}] /ItalicAngle {italic_angle} \
95
+ /Ascent {ascent} /Descent {descent} /CapHeight {cap_height} \
96
+ /StemV {stem} /FontFile2 {file} 0 R >>"
97
+ ),
98
+ )
99
+ }
100
+ }
101
+
102
+ fn postscript_name(font: &FontRef<'_>) -> Result<String> {
103
+ let names = font.name().map_err(invalid)?;
104
+ for platform in [3, 0, 1] {
105
+ for record in names.name_record() {
106
+ if record.name_id() != NameId::POSTSCRIPT_NAME || record.platform_id() != platform {
107
+ continue;
108
+ }
109
+ if !(record.is_unicode() || platform == 1 && record.encoding_id() == 0) {
110
+ continue;
111
+ }
112
+ if record.is_unicode() && !record.length().is_multiple_of(2) {
113
+ return Err(invalid("odd UTF-16 PostScript name length"));
114
+ }
115
+ let name = record.string(names.string_data()).map_err(invalid)?.to_string();
116
+ // OpenType name ID 6: 1..63 printable ASCII characters, excluding
117
+ // PostScript delimiters. '#' is legal there but needs escaping in PDF.
118
+ if !name.is_empty()
119
+ && name.len() <= 63
120
+ && name
121
+ .bytes()
122
+ .all(|byte| (33..=126).contains(&byte) && !b"[](){}<>/%".contains(&byte))
123
+ {
124
+ return Ok(name.replace('#', "#23"));
125
+ }
126
+ }
127
+ }
128
+ Err(Error::UnsupportedFont("no usable PostScript font name"))
129
+ }
@@ -0,0 +1,70 @@
1
+ //! A `CIDFont`'s TrueType program retains every glyph but omits the cmap table
2
+ //! (ISO 32000-1, 9.9). Rebuild the sfnt directory, padding and checksums; glyph
3
+ //! outlines, IDs, metrics and hinting tables are unchanged.
4
+
5
+ #[cfg(test)]
6
+ mod tests;
7
+
8
+ use read_fonts::{FontRef, TableRecord, types::Tag};
9
+
10
+ use crate::Result;
11
+ use crate::font::{TrueTypeFont, invalid};
12
+
13
+ pub fn for_pdf(font: &TrueTypeFont<'_>) -> Result<Vec<u8>> {
14
+ let source = FontRef::new(font.as_bytes()).map_err(invalid)?;
15
+ let mut tags: Vec<_> = source
16
+ .table_directory()
17
+ .table_records()
18
+ .iter()
19
+ .map(TableRecord::tag)
20
+ // Rewriting the directory also invalidates any digital signature.
21
+ .filter(|tag| *tag != Tag::new(b"cmap") && *tag != Tag::new(b"DSIG"))
22
+ .collect();
23
+ tags.sort_unstable();
24
+ (!tags.windows(2).any(|pair| pair[0] == pair[1]))
25
+ .ok_or_else(|| invalid("duplicate sfnt tables"))?;
26
+ let count = u16::try_from(tags.len()).map_err(invalid)?;
27
+ // searchRange and rangeShift are byte counts in 16-bit fields.
28
+ (count > 0 && count <= 4095).ok_or_else(|| invalid("sfnt table count out of range"))?;
29
+ let selector = u16::try_from(count.ilog2()).map_err(invalid)?;
30
+ let range = (1_u16 << selector) * 16;
31
+ let mut data = Vec::with_capacity(font.as_bytes().len());
32
+ data.extend_from_slice(&font.as_bytes()[..4]);
33
+ for value in [count, range, selector, count * 16 - range] {
34
+ data.extend_from_slice(&value.to_be_bytes());
35
+ }
36
+ data.resize(12 + 16 * tags.len(), 0);
37
+ let mut head = None;
38
+ for (index, tag) in tags.into_iter().enumerate() {
39
+ let table = source.table_data(tag).ok_or_else(|| invalid("missing sfnt table"))?;
40
+ let start = data.len();
41
+ let offset = u32::try_from(start).map_err(invalid)?;
42
+ let length = u32::try_from(table.len()).map_err(invalid)?;
43
+ data.extend_from_slice(table.as_bytes());
44
+ if tag == Tag::new(b"head") {
45
+ // head's own checksum is calculated with checkSumAdjustment zero.
46
+ data[start + 8..start + 12].fill(0);
47
+ head = Some(start);
48
+ }
49
+ let sum = checksum(&data[start..]);
50
+ data.resize(data.len().next_multiple_of(4), 0);
51
+ let record = 12 + 16 * index;
52
+ data[record..record + 4].copy_from_slice(&tag.to_be_bytes());
53
+ for (field, value) in [sum, offset, length].into_iter().enumerate() {
54
+ let field = record + 4 * (field + 1);
55
+ data[field..field + 4].copy_from_slice(&value.to_be_bytes());
56
+ }
57
+ }
58
+ let head = head.ok_or_else(|| invalid("missing head table"))?;
59
+ let adjustment = 0xB1B0_AFBA_u32.wrapping_sub(checksum(&data));
60
+ data[head + 8..head + 12].copy_from_slice(&adjustment.to_be_bytes());
61
+ Ok(data)
62
+ }
63
+
64
+ fn checksum(data: &[u8]) -> u32 {
65
+ data.chunks(4).fold(0_u32, |sum, bytes| {
66
+ let mut word = [0; 4];
67
+ word[..bytes.len()].copy_from_slice(bytes);
68
+ sum.wrapping_add(u32::from_be_bytes(word))
69
+ })
70
+ }