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,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.4.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
+ }
@@ -2,10 +2,11 @@
2
2
 
3
3
  use std::cell::RefCell;
4
4
 
5
- use imprimatur::doc::{Document, PageSize, Paragraph, Picture, Run, Table};
5
+ use imprimatur::doc::{Document, PageSize, Paragraph, Picture, Run, Symbol, Table};
6
6
  use imprimatur::style::Color;
7
7
  use magnus::{Error, RModule, RString, Ruby, function, method, prelude::*};
8
8
 
9
+ use crate::barcode::Barcode;
9
10
  use crate::{ALIGNS, DIRECTIONS, OrRaise, RubyBytes, borrow, keyword};
10
11
 
11
12
  /// `[text, bold, italic, underline, color]`, as `Imprimatur::Paragraph` collects it.
@@ -88,6 +89,32 @@ impl NativeDocument {
88
89
  Ok(())
89
90
  }
90
91
 
92
+ /// A barcode, `width` and `height` in points, in a `color` "#RRGGBB".
93
+ fn barcode(
94
+ ruby: &Ruby,
95
+ rb_self: &Self,
96
+ barcode: &Barcode,
97
+ (width, height): (Option<f64>, Option<f64>),
98
+ align: Option<String>,
99
+ color: Option<String>,
100
+ ) -> Result<(), Error> {
101
+ let mut symbol = Symbol::new(barcode.0.clone());
102
+ if let Some(width) = width {
103
+ symbol = symbol.width(width).or_raise(ruby)?;
104
+ }
105
+ if let Some(height) = height {
106
+ symbol = symbol.height(height).or_raise(ruby)?;
107
+ }
108
+ if let Some(align) = align {
109
+ symbol = symbol.align(keyword(ruby, "align", &align, &ALIGNS)?);
110
+ }
111
+ if let Some(color) = color {
112
+ symbol = symbol.color(Color::from_hex(&color).or_raise(ruby)?);
113
+ }
114
+ borrow(ruby, &rb_self.0, "document")?.barcode(symbol);
115
+ Ok(())
116
+ }
117
+
91
118
  fn heading(ruby: &Ruby, rb_self: &Self, level: u8, text: String) -> Result<(), Error> {
92
119
  borrow(ruby, &rb_self.0, "document")?.heading(level, text).or_raise(ruby)?;
93
120
  Ok(())
@@ -175,6 +202,7 @@ pub fn define(ruby: &Ruby, native: RModule) -> Result<(), Error> {
175
202
  class.define_method("set_page_points", method!(NativeDocument::set_page_points, 3))?;
176
203
  class.define_method("set_margin", method!(NativeDocument::set_margin, 1))?;
177
204
  class.define_method("picture", method!(NativeDocument::picture, 4))?;
205
+ class.define_method("barcode", method!(NativeDocument::barcode, 4))?;
178
206
  class.define_method("heading", method!(NativeDocument::heading, 2))?;
179
207
  class.define_method("paragraph", method!(NativeDocument::paragraph, 3))?;
180
208
  class.define_method("table", method!(NativeDocument::table, 2))?;
@@ -4,6 +4,7 @@
4
4
  //! drives them; sheets cross as indices.
5
5
 
6
6
  mod archive;
7
+ mod barcode;
7
8
  mod blocking;
8
9
  mod convert;
9
10
  mod document;
@@ -202,16 +203,16 @@ impl std::io::Write for RubyBytes {
202
203
  }
203
204
  }
204
205
 
205
- /// Mutable access to a wrapped value; Ruby code that calls back into the
206
- /// same object mid-call gets a `RuntimeError`, not a panic.
207
206
  /// A Ruby String's bytes, copied out.
208
207
  fn bytes(string: RString) -> Vec<u8> {
208
+ #[allow(unsafe_code)]
209
209
  // SAFETY: the bytes are copied out at once, while the GVL is held and
210
210
  // before any Ruby code can run and move or free the string.
211
- #[allow(unsafe_code)]
212
211
  unsafe { string.as_slice() }.to_vec()
213
212
  }
214
213
 
214
+ /// Mutable access to a wrapped value; Ruby code that calls back into the
215
+ /// same object mid-call gets a `RuntimeError`, not a panic.
215
216
  fn borrow<'a, T>(ruby: &Ruby, cell: &'a RefCell<T>, what: &str) -> Result<RefMut<'a, T>, Error> {
216
217
  cell.try_borrow_mut().map_err(|_| {
217
218
  Error::new(
@@ -315,6 +316,7 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
315
316
  style.define_singleton_method("__build", function!(Style::build, 13))?;
316
317
  let validation = imprimatur.define_class("Validation", ruby.class_object())?;
317
318
  validation.define_singleton_method("__build", function!(Validation::build, 8))?;
319
+ barcode::define(ruby, imprimatur)?;
318
320
 
319
321
  let native = imprimatur.define_module("Native")?;
320
322
  native.define_singleton_method("markdown", function!(read::markdown_native, 4))?;
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Imprimatur
4
+ # A barcode: QR Code, Data Matrix, EAN-13, EAN-8, UPC-A or Code 128, and the
5
+ # GS1 forms of QR Code, Data Matrix and Code 128 (GS1-128). Built with
6
+ # {Imprimatur.barcode}; draw it with {#svg} or {#png}, or place it in a
7
+ # {Document} with {Document#barcode}. The class itself is native.
8
+ #
9
+ # qr = Imprimatur.barcode(:qr, 'https://id.gs1.org/01/09506000134352')
10
+ # File.write('qr.svg', qr.svg(module_size: 4))
11
+ #
12
+ # label = Imprimatur.barcode(:code128, '(00)106141412345678908', gs1: true)
13
+ # File.binwrite('label.png', label.png(module_size: 3))
14
+ #
15
+ # Imprimatur.barcode(:ean13, '950600013435').text # => "9506000134352"
16
+ class Barcode
17
+ SYMBOLOGIES = %i[qr datamatrix ean13 ean8 upca code128].freeze
18
+
19
+ # @param symbology [:qr, :datamatrix, :ean13, :ean8, :upca, :code128]
20
+ # @param data [String] text; a GTIN's digits for EAN and UPC, the check
21
+ # digit added when left out; with +gs1: true+, a GS1 element string
22
+ # such as "(01)09506000134352(17)251231(10)AB12"
23
+ # @param gs1 [Boolean] a GS1 QR Code, GS1 DataMatrix or GS1-128
24
+ # @param level [:l, :m, :q, :h, nil] a QR Code's error correction, :m unless set
25
+ # @return [Barcode]
26
+ # @raise [Imprimatur::Error] data the symbology cannot hold, an invalid
27
+ # element string, or a wrong check digit
28
+ # @raise [ArgumentError] an unknown symbology or level, or one that does not apply
29
+ def self.encode(symbology, data, gs1: false, level: nil)
30
+ __encode(symbology.to_s, String(data), gs1 ? true : false, level&.to_s)
31
+ end
32
+
33
+ private_class_method :__encode
34
+
35
+ # @param module_size [Numeric] user units (pixels) a module, 0.01 to 1000
36
+ # @param color [String] the bars, "#RRGGBB"
37
+ # @param background [String, nil] "#RRGGBB", or nil for none
38
+ # @param quiet_zone [Boolean] keep the blank margin scanners need
39
+ # @param bar_height [Integer, nil] a linear code's bar height in modules;
40
+ # the symbology's nominal height unless set
41
+ # @return [String] SVG markup
42
+ # @raise [Imprimatur::Error] a module size or color out of range
43
+ def svg(module_size: 4, color: '#000000', background: '#FFFFFF', quiet_zone: true, bar_height: nil)
44
+ __svg(Float(module_size), look(color, background, quiet_zone, bar_height))
45
+ end
46
+
47
+ # @param module_size [Integer] pixels a module
48
+ # @param (see #svg)
49
+ # @return [String] the PNG bytes (binary): one bit a pixel, the background
50
+ # transparent when nil
51
+ # @raise [Imprimatur::Error] a size of 0, an image above 65 536 pixels a side,
52
+ # or an invalid color
53
+ def png(module_size: 4, color: '#000000', background: '#FFFFFF', quiet_zone: true, bar_height: nil)
54
+ __png(Integer(module_size), look(color, background, quiet_zone, bar_height))
55
+ end
56
+
57
+ # @!method symbology
58
+ # @return [Symbol] one of {SYMBOLOGIES}
59
+ # @!method text
60
+ # @return [String] what it encodes: the data given, or a GTIN with its check digit
61
+ # @!method gs1?
62
+ # @return [Boolean] whether it carries a GS1 element string
63
+ # @!method width
64
+ # @return [Integer] modules across, without the quiet zone
65
+ # @!method height
66
+ # @return [Integer] modules down; 1 for a linear code
67
+ # @!method modules
68
+ # @return [Array<Array<Boolean>>] rows of modules, true for dark
69
+
70
+ def inspect = "#<#{self.class} #{symbology}#{' gs1' if gs1?} #{text.inspect}>"
71
+
72
+ private
73
+
74
+ def look(color, background, quiet_zone, bar_height)
75
+ [color.to_s, background&.to_s, quiet_zone ? true : false, bar_height && Integer(bar_height)]
76
+ end
77
+ end
78
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imprimatur
4
- # A flowing document (headings, paragraphs, tables, pictures, page breaks)
4
+ # A flowing document (headings, paragraphs, tables, pictures, barcodes, page breaks)
5
5
  # that renders to PDF and DOCX from the same content.
6
6
  #
7
7
  # doc = Imprimatur::Document.new(page_numbers: true)
@@ -49,6 +49,22 @@ module Imprimatur
49
49
  self
50
50
  end
51
51
 
52
+ # A barcode in the flow: vector shapes in a PDF, a 300 dpi picture in DOCX.
53
+ # Drawn at +width+ or +height+ points (a QR Code or Data Matrix keeps
54
+ # square modules; a linear code's height is its bars'), or else at its
55
+ # symbology's usual module size; never larger than the page leaves room for.
56
+ # Its quiet zone is part of its size.
57
+ #
58
+ # @param barcode [Barcode] from {Imprimatur.barcode}
59
+ # @param align [:left, :center, :right, nil]
60
+ # @param color [String, nil] the bars, "#RRGGBB"; black unless set
61
+ # @return [self]
62
+ # @raise [Imprimatur::Error] a size out of range, or an invalid color
63
+ def barcode(barcode, width: nil, height: nil, align: nil, color: nil)
64
+ @native.barcode(barcode, [width && Float(width), height && Float(height)], align&.to_s, color&.to_s)
65
+ self
66
+ end
67
+
52
68
  # @param text [String]
53
69
  # @param level [Integer] 1 (the title) to 3
54
70
  # @return [self]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imprimatur
4
- VERSION = '0.3.0'
4
+ VERSION = '0.4.0'
5
5
  end
data/lib/imprimatur.rb CHANGED
@@ -6,6 +6,7 @@ require_relative 'imprimatur/formula'
6
6
  require_relative 'imprimatur/native'
7
7
  require_relative 'imprimatur/style'
8
8
  require_relative 'imprimatur/validation'
9
+ require_relative 'imprimatur/barcode'
9
10
  require_relative 'imprimatur/xlsx'
10
11
  require_relative 'imprimatur/document'
11
12
  require_relative 'imprimatur/reader'
@@ -38,6 +39,9 @@ module Imprimatur
38
39
  # @see Validation.build
39
40
  def validation(**) = Validation.build(**)
40
41
 
42
+ # @see Barcode.encode
43
+ def barcode(symbology, data, **) = Barcode.encode(symbology, data, **)
44
+
41
45
  # Builds a {Document} and renders it as PDF. With a path, saves it there
42
46
  # and returns the path; without, returns the bytes.
43
47
  #