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
@@ -132,7 +132,7 @@ impl Format {
132
132
  if length == 0 || length > MAX_FONT_NAME || name.chars().any(char::is_control) {
133
133
  return Err(Error::InvalidFontName(name));
134
134
  }
135
- self.font.name = (name != DEFAULT_FONT).then_some(name);
135
+ self.font.name = Some(name);
136
136
  Ok(self)
137
137
  }
138
138
 
@@ -147,7 +147,7 @@ impl Format {
147
147
  #[expect(clippy::cast_possible_truncation, reason = "a whole number from 2 to 818")]
148
148
  #[expect(clippy::cast_sign_loss, reason = "a whole number from 2 to 818")]
149
149
  let half_points = half_points as u16;
150
- self.font.half_points = (half_points != DEFAULT_HALF_POINTS).then_some(half_points);
150
+ self.font.half_points = Some(half_points);
151
151
  Ok(self)
152
152
  }
153
153
 
@@ -213,13 +213,29 @@ pub struct Font {
213
213
  }
214
214
 
215
215
  impl Font {
216
- pub(crate) fn name(&self) -> &str {
217
- self.name.as_deref().unwrap_or(DEFAULT_FONT)
216
+ /// No name or size of its own: the workbook's, Calibri 11 by default.
217
+ pub(crate) const BASE: Self = Self {
218
+ bold: false,
219
+ italic: false,
220
+ underline: false,
221
+ color: None,
222
+ name: None,
223
+ half_points: None,
224
+ };
225
+
226
+ /// Its name, or else the workbook's `base` one.
227
+ pub(crate) fn name<'a>(&'a self, base: &'a Self) -> &'a str {
228
+ self.name.as_deref().or(base.name.as_deref()).unwrap_or(DEFAULT_FONT)
229
+ }
230
+
231
+ /// Its size in half points, or else the workbook's `base` one.
232
+ pub(crate) fn half_points(&self, base: &Self) -> u16 {
233
+ self.half_points.or(base.half_points).unwrap_or(DEFAULT_HALF_POINTS)
218
234
  }
219
235
 
220
236
  /// The size in points as `<sz>` writes it: `11`, `10.5`.
221
- pub(crate) fn size(&self) -> String {
222
- let half_points = self.half_points.unwrap_or(DEFAULT_HALF_POINTS);
237
+ pub(crate) fn size(&self, base: &Self) -> String {
238
+ let half_points = self.half_points(base);
223
239
  let whole = half_points / 2;
224
240
  if half_points.is_multiple_of(2) { whole.to_string() } else { format!("{whole}.5") }
225
241
  }
@@ -1,10 +1,13 @@
1
1
  //! The parts of an `.xlsx` package and the relationships between them.
2
2
 
3
+ use std::collections::BTreeMap;
3
4
  use std::fmt::Write as _;
4
5
  use std::io::Write;
5
6
 
7
+ use super::autofit;
6
8
  use super::cell::{Cell, cell_reference, push_cell_reference};
7
9
  use super::comment;
10
+ use super::format::Font;
8
11
  use super::styles::Stylesheet;
9
12
  use super::text::{SharedStrings, escape, needs_preserve};
10
13
  use super::validation;
@@ -28,7 +31,9 @@ const DRAWING_ID: &str = "rId2";
28
31
  /// compressed and dropped before the next is built, so only one sheet's XML
29
32
  /// is in memory at a time; the styles and the shared string table, complete
30
33
  /// only after every sheet, follow them.
31
- pub fn write<W: Write>(sheets: &[Worksheet], out: W) -> Result<W> {
34
+ /// `font` is the workbook's: unformatted cells', and the formats' with no
35
+ /// name or size.
36
+ pub fn write<W: Write>(sheets: &[Worksheet], font: &Font, out: W) -> Result<W> {
32
37
  if sheets.is_empty() {
33
38
  return Err(Error::EmptyWorkbook);
34
39
  }
@@ -57,7 +62,7 @@ pub fn write<W: Write>(sheets: &[Worksheet], out: W) -> Result<W> {
57
62
  for (number, sheet) in (1..).zip(sheets) {
58
63
  let remap = styles.add_sheet(sheet)?;
59
64
  set(&mut name, format_args!("xl/worksheets/sheet{number}.xml"));
60
- zip.add(&name, worksheet(sheet, &remap, &mut strings).as_bytes())?;
65
+ zip.add(&name, worksheet(sheet, &remap, &mut strings, font).as_bytes())?;
61
66
  let comments = sheet.comments();
62
67
  if comments.is_empty() {
63
68
  continue;
@@ -74,7 +79,7 @@ pub fn write<W: Write>(sheets: &[Worksheet], out: W) -> Result<W> {
74
79
  next_block += comment::blocks_for(comments.len());
75
80
  }
76
81
  debug_assert_eq!(has_strings, !strings.is_empty(), "the text scan matches the table");
77
- zip.add("xl/styles.xml", styles.to_xml().as_bytes())?;
82
+ zip.add("xl/styles.xml", styles.to_xml(font).as_bytes())?;
78
83
  if has_strings {
79
84
  zip.add("xl/sharedStrings.xml", strings.to_xml().as_bytes())?;
80
85
  }
@@ -137,7 +142,12 @@ fn workbook(sheets: &[Worksheet]) -> String {
137
142
 
138
143
  /// `remap` turns the sheet's own style ids into the workbook's. The cell
139
144
  /// loop formats into three scratch buffers instead of allocating per cell.
140
- fn worksheet<'a>(sheet: &'a Worksheet, remap: &[u32], strings: &mut SharedStrings<'a>) -> String {
145
+ fn worksheet<'a>(
146
+ sheet: &'a Worksheet,
147
+ remap: &[u32],
148
+ strings: &mut SharedStrings<'a>,
149
+ font: &Font,
150
+ ) -> String {
141
151
  let mut xml = XmlWriter::new();
142
152
  xml.open("worksheet", &[("xmlns", MAIN_NS), ("xmlns:r", opc::RELATIONSHIPS)]);
143
153
  xml.empty("dimension", &[("ref", &dimension(sheet))]);
@@ -146,7 +156,7 @@ fn worksheet<'a>(sheet: &'a Worksheet, remap: &[u32], strings: &mut SharedString
146
156
  xml.empty("sheetView", &[("rightToLeft", "1"), ("workbookViewId", "0")]);
147
157
  xml.close("sheetViews");
148
158
  }
149
- write_columns(&mut xml, sheet);
159
+ write_columns(&mut xml, sheet, font);
150
160
  xml.open("sheetData", &[]);
151
161
  let (mut reference, mut style, mut value) = (String::new(), String::new(), String::new());
152
162
  for (row, cells) in sheet.rows() {
@@ -216,13 +226,17 @@ fn set(buffer: &mut String, value: impl std::fmt::Display) {
216
226
  }
217
227
 
218
228
  /// `<cols>`: one `<col>` per run of adjacent columns with the same width.
219
- fn write_columns(xml: &mut XmlWriter, sheet: &Worksheet) {
220
- let widths = sheet.column_widths();
229
+ /// The columns' widths: those set, and for an autofit sheet those its cells
230
+ /// need.
231
+ fn write_columns(xml: &mut XmlWriter, sheet: &Worksheet, font: &Font) {
232
+ let mut widths =
233
+ if sheet.is_autofit() { autofit::widths(sheet, font) } else { BTreeMap::new() };
234
+ widths.extend(sheet.column_widths());
221
235
  if widths.is_empty() {
222
236
  return;
223
237
  }
224
238
  let mut runs: Vec<(u16, u16, f64)> = Vec::new();
225
- for (&column, &width) in widths {
239
+ for (column, width) in widths {
226
240
  match runs.last_mut() {
227
241
  Some((_, last, run_width))
228
242
  if *last + 1 == column && run_width.to_bits() == width.to_bits() =>
@@ -70,7 +70,8 @@ impl Stylesheet {
70
70
  Ok(remap)
71
71
  }
72
72
 
73
- pub fn to_xml(&self) -> String {
73
+ /// The stylesheet, fonts without a name or size of their own in `base`.
74
+ pub fn to_xml(&self, base: &Font) -> String {
74
75
  let mut number_formats = Table::<&str>::new(FIRST_CUSTOM_NUMBER_FORMAT);
75
76
  let mut fonts = Table::<&Font>::new(1);
76
77
  let mut fills = Table::<Color>::new(2);
@@ -101,7 +102,7 @@ impl Stylesheet {
101
102
  }
102
103
  xml.close("numFmts");
103
104
  }
104
- write_fonts(&mut xml, &fonts.order);
105
+ write_fonts(&mut xml, &fonts.order, base);
105
106
  write_fills(&mut xml, &fills.order);
106
107
  write_borders(&mut xml, &borders.order);
107
108
  xml.open("cellStyleXfs", &[("count", "1")]);
@@ -131,24 +132,24 @@ impl Stylesheet {
131
132
  }
132
133
  }
133
134
 
134
- fn write_fonts(xml: &mut XmlWriter, fonts: &[&Font]) {
135
+ /// Font 0, the one of unformatted cells, is `base`.
136
+ fn write_fonts(xml: &mut XmlWriter, fonts: &[&Font], base: &Font) {
135
137
  let count = (fonts.len() + 1).to_string();
136
138
  xml.open("fonts", &[("count", &count)]);
137
- let default = Font::default();
138
- for font in std::iter::once(&default).chain(fonts.iter().copied()) {
139
+ for font in std::iter::once(base).chain(fonts.iter().copied()) {
139
140
  xml.open("font", &[]);
140
141
  for (on, tag) in [(font.bold, "b"), (font.italic, "i"), (font.underline, "u")] {
141
142
  if on {
142
143
  xml.empty(tag, &[]);
143
144
  }
144
145
  }
145
- xml.empty("sz", &[("val", &font.size())]);
146
+ xml.empty("sz", &[("val", &font.size(base))]);
146
147
  if let Some(color) = font.color {
147
148
  xml.empty("color", &[("rgb", &color.argb())]);
148
149
  }
149
- xml.empty("name", &[("val", font.name())]);
150
+ xml.empty("name", &[("val", font.name(base))]);
150
151
  // The family is a fallback hint; only Calibri's is known (2, swiss).
151
- if font.name() == DEFAULT_FONT {
152
+ if font.name(base) == DEFAULT_FONT {
152
153
  xml.empty("family", &[("val", "2")]);
153
154
  }
154
155
  xml.close("font");
@@ -6,6 +6,7 @@ use std::sync::Arc;
6
6
 
7
7
  use super::text::check_length;
8
8
  use super::{Cell, CellRange, Comment, Format, Validation};
9
+ use crate::script::Counts;
9
10
  use crate::{Error, Result};
10
11
 
11
12
  /// Rows in a sheet (Excel 2007 and later).
@@ -40,7 +41,9 @@ pub struct Worksheet {
40
41
  /// The style of an unformatted date, then of an unformatted date and time.
41
42
  date_styles: [Option<u32>; 2],
42
43
  widths: BTreeMap<u16, f64>,
43
- right_to_left: bool,
44
+ autofit: bool,
45
+ /// As set; `None` to follow the sheet's text.
46
+ right_to_left: Option<bool>,
44
47
  validations: Vec<(CellRange, Validation)>,
45
48
  comments: BTreeMap<(u32, u16), Comment>,
46
49
  }
@@ -55,7 +58,8 @@ impl Worksheet {
55
58
  format_ids: HashMap::new(),
56
59
  date_styles: [None; 2],
57
60
  widths: BTreeMap::new(),
58
- right_to_left: false,
61
+ autofit: false,
62
+ right_to_left: None,
59
63
  validations: Vec::new(),
60
64
  comments: BTreeMap::new(),
61
65
  }
@@ -174,16 +178,40 @@ impl Worksheet {
174
178
  Ok(self)
175
179
  }
176
180
 
177
- /// Lays the sheet out right to left: column `A` on the right, as Excel
178
- /// shows sheets for Arabic and Hebrew.
181
+ /// Sizes the columns without a width of their own to their widest cell,
182
+ /// estimated from its characters and font as
183
+ /// `(characters + 3) × points / 10`, bold one and a half times wider.
184
+ pub const fn set_autofit(&mut self, autofit: bool) -> &mut Self {
185
+ self.autofit = autofit;
186
+ self
187
+ }
188
+
189
+ #[must_use]
190
+ pub const fn is_autofit(&self) -> bool {
191
+ self.autofit
192
+ }
193
+
194
+ /// Lays the sheet out right to left, column `A` on the right, as Excel
195
+ /// shows sheets for Arabic and Hebrew; or left to right. Without a
196
+ /// call, a sheet is right to left when most of the letters of its name
197
+ /// and text cells are.
179
198
  pub const fn set_right_to_left(&mut self, right_to_left: bool) -> &mut Self {
180
- self.right_to_left = right_to_left;
199
+ self.right_to_left = Some(right_to_left);
181
200
  self
182
201
  }
183
202
 
184
203
  #[must_use]
185
- pub const fn is_right_to_left(&self) -> bool {
186
- self.right_to_left
204
+ pub fn is_right_to_left(&self) -> bool {
205
+ self.right_to_left.unwrap_or_else(|| {
206
+ let mut counts = Counts::default();
207
+ counts.add(&self.name);
208
+ for entry in self.rows.values().flat_map(BTreeMap::values) {
209
+ if let Cell::Text(text) = &entry.cell {
210
+ counts.add(text);
211
+ }
212
+ }
213
+ counts.right_to_left()
214
+ })
187
215
  }
188
216
 
189
217
  /// Applies a rule to the cells of `range`, whether or not they hold values.
@@ -1,5 +1,6 @@
1
1
  //! `SpreadsheetML` (`.xlsx`) workbooks, per ECMA-376 Part 1.
2
2
 
3
+ mod autofit;
3
4
  mod cell;
4
5
  mod comment;
5
6
  mod date;
@@ -22,6 +23,9 @@ pub(crate) use cell::push_cell_reference;
22
23
  pub use cell::{Cell, cell_reference, column_name};
23
24
  pub use comment::Comment;
24
25
  pub use date::DateTime;
26
+ #[cfg(feature = "archive")]
27
+ pub(crate) use date::civil_from_days;
28
+ use format::Font;
25
29
  pub use format::{Border, Format, VerticalAlign};
26
30
  pub use range::{CellRange, parse_cell_reference};
27
31
  pub use validation::{Alert, Validation};
@@ -36,12 +40,44 @@ const MAX_SHEET_NAME: usize = 31;
36
40
  #[derive(Debug, Default)]
37
41
  pub struct Workbook {
38
42
  sheets: Vec<Worksheet>,
43
+ /// The font of unformatted cells, and of formats with no name or size.
44
+ font: Font,
45
+ /// Whether new sheets size their columns to their cells.
46
+ autofit: bool,
39
47
  }
40
48
 
41
49
  impl Workbook {
42
50
  #[must_use]
43
51
  pub const fn new() -> Self {
44
- Self { sheets: Vec::new() }
52
+ Self { sheets: Vec::new(), font: Font::BASE, autofit: false }
53
+ }
54
+
55
+ /// The font of cells without a format, and of formats that name no font:
56
+ /// Calibri unless set.
57
+ ///
58
+ /// # Errors
59
+ /// [`Error::InvalidFontName`] for an empty name, one longer than 31
60
+ /// characters or with a control character.
61
+ pub fn set_font_name(&mut self, name: impl Into<String>) -> Result<&mut Self> {
62
+ self.font.name = Format::new().font_name(name)?.font.name;
63
+ Ok(self)
64
+ }
65
+
66
+ /// The size of cells without a format, and of formats that set none: 11
67
+ /// points unless set.
68
+ ///
69
+ /// # Errors
70
+ /// [`Error::InvalidFontSize`] outside 1 to 409 points in steps of 0.5.
71
+ pub fn set_font_size(&mut self, points: f64) -> Result<&mut Self> {
72
+ self.font.half_points = Format::new().font_size(points)?.font.half_points;
73
+ Ok(self)
74
+ }
75
+
76
+ /// Whether the sheets added from now on size their columns to their
77
+ /// cells, as [`Worksheet::set_autofit`] does.
78
+ pub const fn set_autofit(&mut self, autofit: bool) -> &mut Self {
79
+ self.autofit = autofit;
80
+ self
45
81
  }
46
82
 
47
83
  /// Adds a sheet after the existing ones and returns it for writing.
@@ -56,7 +92,8 @@ impl Workbook {
56
92
  if self.sheets.iter().any(|sheet| sheet.name().to_lowercase() == folded) {
57
93
  return Err(Error::DuplicateSheetName(name));
58
94
  }
59
- Ok(self.sheets.push_mut(Worksheet::new(name)))
95
+ let autofit = self.autofit;
96
+ Ok(self.sheets.push_mut(Worksheet::new(name)).set_autofit(autofit))
60
97
  }
61
98
 
62
99
  #[must_use]
@@ -73,7 +110,7 @@ impl Workbook {
73
110
  /// # Errors
74
111
  /// [`Error::EmptyWorkbook`] without sheets, [`Error::Io`] when `out` fails.
75
112
  pub fn write_to<W: Write>(&self, out: W) -> Result<W> {
76
- package::write(&self.sheets, out)
113
+ package::write(&self.sheets, &self.font, out)
77
114
  }
78
115
 
79
116
  /// The `.xlsx` package as bytes.
@@ -57,6 +57,16 @@ impl<'a> Tag<'a> {
57
57
  }
58
58
  }
59
59
 
60
+ /// The attribute parsed: `None` when it is missing or does not parse.
61
+ pub fn parse<T: std::str::FromStr>(&self, local: &str) -> Option<T> {
62
+ self.attr(local)?.parse().ok()
63
+ }
64
+
65
+ /// Whether the attribute is there and on: `1`, `true` or `on`.
66
+ pub fn on(&self, local: &str) -> bool {
67
+ self.attr(local).is_some() && self.flag(local)
68
+ }
69
+
60
70
  /// `w:val`-style booleans: present without a value, `1`, `true` or
61
71
  /// `on` are true; Word writes `w:val="0"` to switch a property off.
62
72
  pub fn flag(&self, local: &str) -> bool {
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "imprimatur_native"
3
- version = "0.2.0"
3
+ version = "0.4.0"
4
4
  description = "Ruby bindings for the imprimatur crate"
5
5
  publish = false
6
6
  edition.workspace = true
@@ -15,7 +15,8 @@ crate-type = ["cdylib"]
15
15
  workspace = true
16
16
 
17
17
  [dependencies]
18
- imprimatur = { workspace = true, features = ["archive", "json"] }
18
+ # Gems ship precompiled, so they carry every feature of the crate.
19
+ imprimatur = { workspace = true, features = ["archive", "fonts", "json"] }
19
20
  serde_json.workspace = true
20
21
  magnus.workspace = true
21
22
  rb-sys.workspace = true
@@ -45,9 +45,7 @@ impl ZipWriter {
45
45
  data: RString,
46
46
  comment: Option<String>,
47
47
  ) -> Result<(), Error> {
48
- // SAFETY: the bytes are copied out at once, while the GVL is held.
49
- #[allow(unsafe_code)]
50
- let data = unsafe { data.as_slice() }.to_vec();
48
+ let data = crate::bytes(data);
51
49
  let mut slot = borrow(ruby, &rb_self.0, "zip writer")?;
52
50
  let writer = slot.as_mut().ok_or_else(|| finished(ruby))?;
53
51
  without_gvl(ruby, || writer.add(&name, &data, comment.as_deref()))?.or_raise(ruby)
@@ -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, 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.
@@ -40,10 +41,80 @@ impl NativeDocument {
40
41
  &size,
41
42
  &[("a4", PageSize::A4), ("letter", PageSize::Letter)],
42
43
  )?;
44
+ Self::page(ruby, rb_self, size, page_numbers)
45
+ }
46
+
47
+ /// A page `width` by `height` points.
48
+ fn set_page_points(
49
+ ruby: &Ruby,
50
+ rb_self: &Self,
51
+ width: f64,
52
+ height: f64,
53
+ page_numbers: bool,
54
+ ) -> Result<(), Error> {
55
+ let size = PageSize::custom(width, height).or_raise(ruby)?;
56
+ Self::page(ruby, rb_self, size, page_numbers)
57
+ }
58
+
59
+ fn page(ruby: &Ruby, rb_self: &Self, size: PageSize, page_numbers: bool) -> Result<(), Error> {
43
60
  borrow(ruby, &rb_self.0, "document")?.set_page_size(size).set_page_numbers(page_numbers);
44
61
  Ok(())
45
62
  }
46
63
 
64
+ fn set_margin(ruby: &Ruby, rb_self: &Self, points: f64) -> Result<(), Error> {
65
+ borrow(ruby, &rb_self.0, "document")?.set_margin(points).or_raise(ruby)?;
66
+ Ok(())
67
+ }
68
+
69
+ /// A PNG or JPEG from its bytes, `width` and `height` in points.
70
+ fn picture(
71
+ ruby: &Ruby,
72
+ rb_self: &Self,
73
+ data: RString,
74
+ width: Option<f64>,
75
+ height: Option<f64>,
76
+ align: Option<String>,
77
+ ) -> Result<(), Error> {
78
+ let mut picture = Picture::new(crate::bytes(data)).or_raise(ruby)?;
79
+ if let Some(width) = width {
80
+ picture = picture.width(width).or_raise(ruby)?;
81
+ }
82
+ if let Some(height) = height {
83
+ picture = picture.height(height).or_raise(ruby)?;
84
+ }
85
+ if let Some(align) = align {
86
+ picture = picture.align(keyword(ruby, "align", &align, &ALIGNS)?);
87
+ }
88
+ borrow(ruby, &rb_self.0, "document")?.picture(picture);
89
+ Ok(())
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
+
47
118
  fn heading(ruby: &Ruby, rb_self: &Self, level: u8, text: String) -> Result<(), Error> {
48
119
  borrow(ruby, &rb_self.0, "document")?.heading(level, text).or_raise(ruby)?;
49
120
  Ok(())
@@ -128,6 +199,10 @@ pub fn define(ruby: &Ruby, native: RModule) -> Result<(), Error> {
128
199
  let class = native.define_class("Document", ruby.class_object())?;
129
200
  class.define_singleton_method("new", function!(NativeDocument::new, 0))?;
130
201
  class.define_method("set_page", method!(NativeDocument::set_page, 2))?;
202
+ class.define_method("set_page_points", method!(NativeDocument::set_page_points, 3))?;
203
+ class.define_method("set_margin", method!(NativeDocument::set_margin, 1))?;
204
+ class.define_method("picture", method!(NativeDocument::picture, 4))?;
205
+ class.define_method("barcode", method!(NativeDocument::barcode, 4))?;
131
206
  class.define_method("heading", method!(NativeDocument::heading, 2))?;
132
207
  class.define_method("paragraph", method!(NativeDocument::paragraph, 3))?;
133
208
  class.define_method("table", method!(NativeDocument::table, 2))?;