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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/Cargo.lock +33 -3
- data/Cargo.toml +1 -1
- data/README.md +48 -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/reader.rb +48 -11
- data/lib/imprimatur/spreadsheet.rb +1 -1
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur.rb +6 -1
- metadata +13 -2
- 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
|
|
133
|
-
let (
|
|
134
|
-
|
|
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.
|
|
145
|
-
|
|
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.
|
|
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
|
+
}
|