imprimatur 0.3.0 → 0.5.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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +16 -0
  3. data/Cargo.lock +33 -3
  4. data/Cargo.toml +1 -1
  5. data/README.md +48 -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/reader.rb +48 -11
  36. data/lib/imprimatur/spreadsheet.rb +1 -1
  37. data/lib/imprimatur/version.rb +1 -1
  38. data/lib/imprimatur.rb +6 -1
  39. metadata +13 -2
  40. data/ext/imprimatur_native/core/src/test_support.rs +0 -251
@@ -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
+ }
@@ -0,0 +1,149 @@
1
+ //! A complete TrueType font as a horizontal Type 0 / `CIDFontType2` resource
2
+ //! (ISO 32000-1, 9.7 and 9.10). Document font selection and layout are separate.
3
+
4
+ mod descriptor;
5
+ mod program;
6
+ #[cfg(test)]
7
+ mod tests;
8
+
9
+ use std::collections::BTreeMap;
10
+ use std::fmt::{self, Write as _};
11
+ use std::io::Write;
12
+
13
+ use super::objects::{Num, PdfWriter};
14
+ use crate::font::TrueTypeFont;
15
+ use crate::{Error, Result};
16
+ use descriptor::Descriptor;
17
+
18
+ /// Objects written in order: Type 0 font, descendant, descriptor, font file,
19
+ /// CID-to-glyph map, `ToUnicode` character map.
20
+ pub const OBJECTS: usize = 6;
21
+
22
+ /// Collects glyph uses before writing the font resource once for all its pages.
23
+ /// CIDs are allocated per (glyph, original text), not per glyph alone: distinct
24
+ /// characters can share an outline, and a ligature can represent several.
25
+ pub struct EmbeddedFont<'a> {
26
+ font: TrueTypeFont<'a>,
27
+ descriptor: Descriptor,
28
+ program: Vec<u8>,
29
+ /// CID 1 onwards, with unscaled advances; CID 0 is reserved for .notdef.
30
+ glyphs: Vec<(u16, u16)>,
31
+ codes: BTreeMap<(u16, String), u16>,
32
+ }
33
+
34
+ impl<'a> EmbeddedFont<'a> {
35
+ /// Reads the metadata needed for a PDF descriptor and checks embedding rights.
36
+ pub fn new(font: TrueTypeFont<'a>) -> Result<Self> {
37
+ let descriptor = Descriptor::read(&font)?;
38
+ let program = program::for_pdf(&font)?;
39
+ Ok(Self { font, descriptor, program, glyphs: Vec::new(), codes: BTreeMap::new() })
40
+ }
41
+
42
+ /// The two big-endian bytes to put in a PDF text string for this glyph use.
43
+ /// `text` is its original Unicode sequence, in logical order. Empty text
44
+ /// omits its `ToUnicode` entry, for glyphs with no text of their own.
45
+ /// This does not shape text or infer glyphs from characters.
46
+ pub fn encode_glyph(&mut self, glyph: u16, text: &str) -> Result<[u8; 2]> {
47
+ // A ToUnicode destination string may contain at most 512 bytes.
48
+ (text.encode_utf16().take(257).count() <= 256)
49
+ .ok_or_else(|| invalid("Unicode mapping exceeds 256 UTF-16 code units"))?;
50
+ let width = self
51
+ .font
52
+ .advance_width(glyph)
53
+ .filter(|_| glyph != 0)
54
+ .ok_or_else(|| invalid("glyph is missing or outside the font's glyph range"))?;
55
+ let key = (glyph, text.to_owned());
56
+ if let Some(code) = self.codes.get(&key) {
57
+ return Ok(code.to_be_bytes());
58
+ }
59
+ let code = u16::try_from(self.glyphs.len() + 1)
60
+ .map_err(|_| invalid("more than 65535 distinct glyph mappings"))?;
61
+ self.glyphs.push((glyph, width));
62
+ self.codes.insert(key, code);
63
+ Ok(code.to_be_bytes())
64
+ }
65
+
66
+ /// Writes six consecutive objects, starting at `first`, which is the font
67
+ /// reference for a page's resources. Collect every glyph before calling this.
68
+ /// Every glyph is embedded with its original ID and outline. The font's cmap
69
+ /// is replaced by PDF mappings; its table directory and checksums are rebuilt.
70
+ pub fn write<W: Write>(&self, pdf: &mut PdfWriter<W>, first: usize) -> Result<()> {
71
+ let (descendant, descriptor, file, mapping, unicode) =
72
+ (first + 1, first + 2, first + 3, first + 4, first + 5);
73
+ let name = &self.descriptor.name;
74
+ pdf.object(
75
+ first,
76
+ format_args!(
77
+ "<< /Type /Font /Subtype /Type0 /BaseFont /{name} /Encoding /Identity-H \
78
+ /DescendantFonts [{descendant} 0 R] /ToUnicode {unicode} 0 R >>"
79
+ ),
80
+ )?;
81
+ let scale = 1000.0 / f64::from(self.font.metrics().units_per_em);
82
+ let default_width = Num(f64::from(self.font.advance_width(0).unwrap_or(0)) * scale);
83
+ let widths = fmt::from_fn(|f| {
84
+ // Keep both the outer W array and each inner width array below the
85
+ // PDF 1.7 implementation limit of 8191 elements (Annex C).
86
+ for (index, glyphs) in self.glyphs.chunks(256).enumerate() {
87
+ if index > 0 {
88
+ f.write_str(" ")?;
89
+ }
90
+ write!(f, "{} [", 1 + index * 256)?;
91
+ for &(_, width) in glyphs {
92
+ write!(f, "{} ", Num(f64::from(width) * scale))?;
93
+ }
94
+ f.write_str("]")?;
95
+ }
96
+ Ok(())
97
+ });
98
+ pdf.object(
99
+ descendant,
100
+ format_args!(
101
+ "<< /Type /Font /Subtype /CIDFontType2 /BaseFont /{name} \
102
+ /CIDSystemInfo << /Registry (Adobe) /Ordering (Identity) /Supplement 0 >> \
103
+ /FontDescriptor {descriptor} 0 R /DW {default_width} /W [{widths}] \
104
+ /CIDToGIDMap {mapping} 0 R >>"
105
+ ),
106
+ )?;
107
+ self.descriptor.write(pdf, descriptor, file)?;
108
+ pdf.stream(file, &self.program, &[("Length1", &self.program.len())])?;
109
+ let mut gids = Vec::with_capacity(2 * (self.glyphs.len() + 1));
110
+ gids.extend_from_slice(&[0, 0]);
111
+ for &(glyph, _) in &self.glyphs {
112
+ gids.extend_from_slice(&glyph.to_be_bytes());
113
+ }
114
+ pdf.stream(mapping, &gids, &[])?;
115
+ pdf.stream(unicode, self.to_unicode().as_bytes(), &[])
116
+ }
117
+
118
+ fn to_unicode(&self) -> String {
119
+ let mut cmap = String::from(
120
+ "/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
121
+ /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
122
+ /CMapName /Imprimatur-UCS def\n/CMapType 2 def\n\
123
+ 1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
124
+ );
125
+ let mappings: Vec<_> =
126
+ self.codes.iter().filter(|((_, text), _)| !text.is_empty()).collect();
127
+ // CMap mapping operators accept at most 100 entries per block.
128
+ for chunk in mappings.chunks(100) {
129
+ // Writing to a String cannot fail.
130
+ let _ = writeln!(cmap, "{} beginbfchar", chunk.len());
131
+ for ((_, text), code) in chunk {
132
+ // Writing to a String cannot fail.
133
+ let _ = write!(cmap, "<{code:04X}> <");
134
+ for unit in text.encode_utf16() {
135
+ // Writing to a String cannot fail.
136
+ let _ = write!(cmap, "{unit:04X}");
137
+ }
138
+ cmap.push_str(">\n");
139
+ }
140
+ cmap.push_str("endbfchar\n");
141
+ }
142
+ cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
143
+ cmap
144
+ }
145
+ }
146
+
147
+ fn invalid(reason: &str) -> Error {
148
+ Error::malformed("PDF font", reason)
149
+ }
@@ -1,6 +1,7 @@
1
1
  //! PDF output: the document laid out with the standard Helvetica fonts
2
2
  //! (`WinAnsiEncoding`, so Windows-1252 text), written with our own object
3
- //! writer. Embedded TrueType fonts, and with them every other script, come next.
3
+ //! writer. `truetype` serializes embedded font resources; connecting these to
4
+ //! document layout, shaping and bidirectional text comes next.
4
5
 
5
6
  mod font;
6
7
  mod layout;
@@ -10,6 +11,12 @@ mod objects;
10
11
  mod picture;
11
12
  #[cfg(test)]
12
13
  mod tests;
14
+ #[cfg(feature = "fonts")]
15
+ #[cfg_attr(
16
+ not(test),
17
+ expect(dead_code, reason = "font resources are connected to document layout in a follow-up")
18
+ )]
19
+ mod truetype;
13
20
 
14
21
  use std::fmt;
15
22
  use std::io::Write;
@@ -38,6 +45,7 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
38
45
  layout.picture(picture, pictures.len());
39
46
  pictures.push(picture);
40
47
  }
48
+ Block::Symbol(symbol) => layout.symbol(symbol),
41
49
  Block::PageBreak => layout.page_break(),
42
50
  }
43
51
  }
@@ -128,6 +128,20 @@ pub fn png(
128
128
  bits: u8,
129
129
  palette: Option<&[u8]>,
130
130
  rows: &[u8],
131
+ ) -> Vec<u8> {
132
+ png_with_transparency(width, height, color, bits, palette, &[], rows)
133
+ }
134
+
135
+ /// As [`png`], with a `tRNS` chunk unless `transparency` is empty: a
136
+ /// palette's alphas by index, or the gray or RGB color that is transparent.
137
+ pub fn png_with_transparency(
138
+ width: u32,
139
+ height: u32,
140
+ color: PngColor,
141
+ bits: u8,
142
+ palette: Option<&[u8]>,
143
+ transparency: &[u8],
144
+ rows: &[u8],
131
145
  ) -> Vec<u8> {
132
146
  let channels: usize = match color {
133
147
  PngColor::Gray | PngColor::Palette => 1,
@@ -157,6 +171,9 @@ pub fn png(
157
171
  if let Some(palette) = palette {
158
172
  chunk(&mut out, *b"PLTE", palette);
159
173
  }
174
+ if !transparency.is_empty() {
175
+ chunk(&mut out, *b"tRNS", transparency);
176
+ }
160
177
  chunk(&mut out, *b"IDAT", &compressed);
161
178
  chunk(&mut out, *b"IEND", &[]);
162
179
  out
@@ -38,9 +38,10 @@ pub use content::{
38
38
  Block, Bounds, Cell, Columns, Content, Grid, Look, Mark, Note, Page, Placed, Sheet, Styled,
39
39
  Text, Validation, Value,
40
40
  };
41
- pub use image::{Image, ImageFormat, ImageLocation};
42
41
  #[cfg(test)]
43
- pub(crate) use image::{PngColor, png};
42
+ pub(crate) use image::png;
43
+ pub use image::{Image, ImageFormat, ImageLocation};
44
+ pub(crate) use image::{PngColor, png_with_transparency};
44
45
 
45
46
  use crate::zip::reader::ZipReader;
46
47
  use crate::{Error, Result};
@@ -41,6 +41,17 @@ impl Color {
41
41
  pub(crate) fn argb(self) -> String {
42
42
  format!("FF{:06X}", self.0)
43
43
  }
44
+
45
+ /// `#RRGGBB`, as SVG takes it.
46
+ pub(crate) fn hex(self) -> String {
47
+ format!("#{:06X}", self.0)
48
+ }
49
+
50
+ /// Red, green and blue bytes.
51
+ pub(crate) const fn bytes(self) -> [u8; 3] {
52
+ let [_, r, g, b] = self.0.to_be_bytes();
53
+ [r, g, b]
54
+ }
44
55
  }
45
56
 
46
57
  /// Horizontal alignment within the cell.
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "imprimatur_native"
3
- version = "0.3.0"
3
+ version = "0.5.0"
4
4
  description = "Ruby bindings for the imprimatur crate"
5
5
  publish = false
6
6
  edition.workspace = true
@@ -0,0 +1,147 @@
1
+ //! `Imprimatur::Barcode`: a barcode symbol, drawn as SVG or PNG or placed in
2
+ //! a document. `lib/imprimatur/barcode.rb` owns the keyword interface.
3
+
4
+ use imprimatur::barcode::{self, Level, Look, Symbology};
5
+ use imprimatur::style::Color;
6
+ use magnus::{Error, RString, Ruby, Symbol};
7
+
8
+ use crate::{OrRaise, argument_error, keyword};
9
+
10
+ #[magnus::wrap(class = "Imprimatur::Barcode", free_immediately, size)]
11
+ pub struct Barcode(pub barcode::Barcode);
12
+
13
+ #[derive(Clone, Copy, PartialEq, Eq)]
14
+ enum Kind {
15
+ Qr,
16
+ DataMatrix,
17
+ Ean13,
18
+ Ean8,
19
+ UpcA,
20
+ Code128,
21
+ }
22
+
23
+ const KINDS: [(&str, Kind); 6] = [
24
+ ("qr", Kind::Qr),
25
+ ("datamatrix", Kind::DataMatrix),
26
+ ("ean13", Kind::Ean13),
27
+ ("ean8", Kind::Ean8),
28
+ ("upca", Kind::UpcA),
29
+ ("code128", Kind::Code128),
30
+ ];
31
+ const LEVELS: [(&str, Level); 4] =
32
+ [("l", Level::Low), ("m", Level::Medium), ("q", Level::Quartile), ("h", Level::High)];
33
+
34
+ #[expect(clippy::needless_pass_by_value, reason = "magnus passes Ruby arguments as owned values")]
35
+ impl Barcode {
36
+ /// Called by `Imprimatur::Barcode.encode`.
37
+ pub fn encode(
38
+ ruby: &Ruby,
39
+ symbology: String,
40
+ data: String,
41
+ gs1: bool,
42
+ level: Option<String>,
43
+ ) -> Result<Self, Error> {
44
+ let kind = keyword(ruby, "symbology", &symbology, &KINDS)?;
45
+ let level = level.map(|level| keyword(ruby, "level", &level, &LEVELS)).transpose()?;
46
+ if level.is_some() && kind != Kind::Qr {
47
+ return Err(argument_error(ruby, "level: applies to :qr only"));
48
+ }
49
+ if gs1 && !matches!(kind, Kind::Qr | Kind::DataMatrix | Kind::Code128) {
50
+ return Err(argument_error(ruby, "gs1: applies to :qr, :datamatrix and :code128"));
51
+ }
52
+ let level = level.unwrap_or_default();
53
+ let code = match (kind, gs1) {
54
+ (Kind::Qr, false) => barcode::Barcode::qr(&data, level),
55
+ (Kind::Qr, true) => barcode::Barcode::gs1_qr(&data, level),
56
+ (Kind::DataMatrix, false) => barcode::Barcode::data_matrix(&data),
57
+ (Kind::DataMatrix, true) => barcode::Barcode::gs1_data_matrix(&data),
58
+ (Kind::Ean13, _) => barcode::Barcode::ean13(&data),
59
+ (Kind::Ean8, _) => barcode::Barcode::ean8(&data),
60
+ (Kind::UpcA, _) => barcode::Barcode::upc_a(&data),
61
+ (Kind::Code128, false) => barcode::Barcode::code128(&data),
62
+ (Kind::Code128, true) => barcode::Barcode::gs1_128(&data),
63
+ };
64
+ Ok(Self(code.or_raise(ruby)?))
65
+ }
66
+
67
+ fn svg(
68
+ ruby: &Ruby,
69
+ rb_self: &Self,
70
+ module: f64,
71
+ look: (String, Option<String>, bool, Option<u32>),
72
+ ) -> Result<String, Error> {
73
+ rb_self.0.svg(module, &self::look(ruby, look)?).or_raise(ruby)
74
+ }
75
+
76
+ /// The PNG as a binary String.
77
+ fn png(
78
+ ruby: &Ruby,
79
+ rb_self: &Self,
80
+ scale: u32,
81
+ look: (String, Option<String>, bool, Option<u32>),
82
+ ) -> Result<RString, Error> {
83
+ let png = rb_self.0.png(scale, &self::look(ruby, look)?).or_raise(ruby)?;
84
+ Ok(ruby.str_from_slice(&png))
85
+ }
86
+
87
+ fn symbology(ruby: &Ruby, rb_self: &Self) -> Symbol {
88
+ let name = match rb_self.0.symbology() {
89
+ Symbology::QrCode => "qr",
90
+ Symbology::DataMatrix => "datamatrix",
91
+ Symbology::Ean13 => "ean13",
92
+ Symbology::Ean8 => "ean8",
93
+ Symbology::UpcA => "upca",
94
+ _ => "code128",
95
+ };
96
+ ruby.to_symbol(name)
97
+ }
98
+
99
+ fn text(&self) -> String {
100
+ self.0.text().to_owned()
101
+ }
102
+
103
+ const fn is_gs1(&self) -> bool {
104
+ self.0.is_gs1()
105
+ }
106
+
107
+ const fn width(&self) -> usize {
108
+ self.0.size().0
109
+ }
110
+
111
+ const fn height(&self) -> usize {
112
+ self.0.size().1
113
+ }
114
+
115
+ /// Rows of modules, true for dark ones.
116
+ fn modules(&self) -> Vec<Vec<bool>> {
117
+ let (columns, rows) = self.0.size();
118
+ (0..rows).map(|y| (0..columns).map(|x| self.0.is_dark(x, y)).collect()).collect()
119
+ }
120
+ }
121
+
122
+ /// `[color, background, quiet_zone, bar_height]`, as `Imprimatur::Barcode`
123
+ /// passes them.
124
+ fn look(
125
+ ruby: &Ruby,
126
+ (dark, light, quiet_zone, bar_height): (String, Option<String>, bool, Option<u32>),
127
+ ) -> Result<Look, Error> {
128
+ let dark = Color::from_hex(&dark).or_raise(ruby)?;
129
+ let light = light.map(|light| Color::from_hex(&light)).transpose().or_raise(ruby)?;
130
+ Ok(Look { dark, light, quiet_zone, bar_height })
131
+ }
132
+
133
+ pub fn define(ruby: &Ruby, imprimatur: magnus::RModule) -> Result<(), Error> {
134
+ use magnus::{function, method, prelude::*};
135
+
136
+ let class = imprimatur.define_class("Barcode", ruby.class_object())?;
137
+ class.define_singleton_method("__encode", function!(Barcode::encode, 4))?;
138
+ class.define_method("__svg", method!(Barcode::svg, 2))?;
139
+ class.define_method("__png", method!(Barcode::png, 2))?;
140
+ class.define_method("symbology", method!(Barcode::symbology, 0))?;
141
+ class.define_method("text", method!(Barcode::text, 0))?;
142
+ class.define_method("gs1?", method!(Barcode::is_gs1, 0))?;
143
+ class.define_method("width", method!(Barcode::width, 0))?;
144
+ class.define_method("height", method!(Barcode::height, 0))?;
145
+ class.define_method("modules", method!(Barcode::modules, 0))?;
146
+ Ok(())
147
+ }