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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Cargo.lock +33 -3
- data/Cargo.toml +1 -1
- data/README.md +41 -1
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +15 -4
- data/ext/imprimatur_native/core/README.md +27 -4
- data/ext/imprimatur_native/core/src/barcode/code128.rs +162 -0
- data/ext/imprimatur_native/core/src/barcode/draw.rs +124 -0
- data/ext/imprimatur_native/core/src/barcode/ean.rs +91 -0
- data/ext/imprimatur_native/core/src/barcode/gs1.rs +188 -0
- data/ext/imprimatur_native/core/src/barcode/matrix.rs +130 -0
- data/ext/imprimatur_native/core/src/barcode.rs +314 -0
- data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc.rs +11 -1
- data/ext/imprimatur_native/core/src/docx.rs +22 -10
- data/ext/imprimatur_native/core/src/error.rs +6 -0
- data/ext/imprimatur_native/core/src/font.rs +3 -1
- data/ext/imprimatur_native/core/src/lib.rs +3 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +48 -14
- data/ext/imprimatur_native/core/src/pdf/truetype/descriptor.rs +129 -0
- data/ext/imprimatur_native/core/src/pdf/truetype/program.rs +70 -0
- data/ext/imprimatur_native/core/src/pdf/truetype.rs +149 -0
- data/ext/imprimatur_native/core/src/pdf.rs +9 -1
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- data/ext/imprimatur_native/core/src/read.rs +3 -2
- data/ext/imprimatur_native/core/src/style.rs +11 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +1 -1
- data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +29 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +5 -3
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +17 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +4 -0
- metadata +13 -2
- 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.
|
|
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::
|
|
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.
|
|
@@ -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
|
data/lib/imprimatur/document.rb
CHANGED
|
@@ -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]
|
data/lib/imprimatur/version.rb
CHANGED
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
|
#
|