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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/Cargo.lock +153 -3
- data/Cargo.toml +1 -1
- data/README.md +139 -21
- data/ext/imprimatur_native/cli/Cargo.toml +3 -1
- data/ext/imprimatur_native/core/Cargo.toml +20 -4
- data/ext/imprimatur_native/core/README.md +36 -4
- data/ext/imprimatur_native/core/src/archive.rs +3 -16
- 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/paragraph.rs +10 -3
- data/ext/imprimatur_native/core/src/doc/picture.rs +192 -0
- data/ext/imprimatur_native/core/src/doc/symbol.rs +136 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +21 -0
- data/ext/imprimatur_native/core/src/doc.rs +96 -2
- data/ext/imprimatur_native/core/src/docx/picture.rs +72 -0
- data/ext/imprimatur_native/core/src/docx.rs +103 -28
- data/ext/imprimatur_native/core/src/error.rs +18 -0
- data/ext/imprimatur_native/core/src/font.rs +231 -0
- data/ext/imprimatur_native/core/src/lib.rs +6 -1
- data/ext/imprimatur_native/core/src/pdf/layout.rs +72 -15
- data/ext/imprimatur_native/core/src/pdf/objects.rs +29 -5
- data/ext/imprimatur_native/core/src/pdf/picture.rs +197 -0
- 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 +40 -4
- data/ext/imprimatur_native/core/src/read/content.rs +75 -2
- data/ext/imprimatur_native/core/src/read/image.rs +17 -0
- data/ext/imprimatur_native/core/src/read/json.rs +94 -7
- data/ext/imprimatur_native/core/src/read/numbers.rs +65 -42
- data/ext/imprimatur_native/core/src/read/ods.rs +1 -2
- data/ext/imprimatur_native/core/src/read/pdf/layout/bidi.rs +212 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +25 -119
- data/ext/imprimatur_native/core/src/read/xls.rs +1 -1
- data/ext/imprimatur_native/core/src/read/xlsx/formats.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +104 -19
- data/ext/imprimatur_native/core/src/read.rs +5 -1
- data/ext/imprimatur_native/core/src/script.rs +116 -0
- data/ext/imprimatur_native/core/src/style.rs +32 -0
- data/ext/imprimatur_native/core/src/xlsx/autofit.rs +60 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +7 -1
- data/ext/imprimatur_native/core/src/xlsx/format.rs +22 -6
- data/ext/imprimatur_native/core/src/xlsx/package.rs +22 -8
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +9 -8
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +35 -7
- data/ext/imprimatur_native/core/src/xlsx.rs +40 -3
- data/ext/imprimatur_native/core/src/xml/reader.rs +10 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +3 -2
- data/ext/imprimatur_native/ffi/src/archive.rs +1 -3
- data/ext/imprimatur_native/ffi/src/barcode.rs +147 -0
- data/ext/imprimatur_native/ffi/src/document.rs +76 -1
- data/ext/imprimatur_native/ffi/src/lib.rs +40 -1
- data/ext/imprimatur_native/ffi/src/read.rs +8 -11
- data/ext/imprimatur_native/ffi/src/sheets.rs +114 -0
- data/lib/imprimatur/barcode.rb +78 -0
- data/lib/imprimatur/document.rb +46 -6
- data/lib/imprimatur/reader.rb +2 -2
- data/lib/imprimatur/spreadsheet.rb +151 -0
- data/lib/imprimatur/version.rb +1 -1
- data/lib/imprimatur/xlsx.rb +25 -4
- data/lib/imprimatur.rb +19 -3
- metadata +23 -2
- data/ext/imprimatur_native/core/src/test_support.rs +0 -250
|
@@ -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,14 +1,22 @@
|
|
|
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;
|
|
7
8
|
#[rustfmt::skip]
|
|
8
9
|
pub mod metrics;
|
|
9
10
|
mod objects;
|
|
11
|
+
mod picture;
|
|
10
12
|
#[cfg(test)]
|
|
11
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;
|
|
12
20
|
|
|
13
21
|
use std::fmt;
|
|
14
22
|
use std::io::Write;
|
|
@@ -26,12 +34,18 @@ const FIRST_FONT: usize = 3;
|
|
|
26
34
|
const INFO: usize = FIRST_FONT + Face::ALL.len();
|
|
27
35
|
|
|
28
36
|
pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
29
|
-
let mut layout = Layout::new(document
|
|
37
|
+
let mut layout = Layout::new(document)?;
|
|
38
|
+
let mut pictures = Vec::new();
|
|
30
39
|
for block in document.blocks() {
|
|
31
40
|
match block {
|
|
32
41
|
Block::Heading { level, text } => layout.heading(*level, text)?,
|
|
33
42
|
Block::Paragraph(paragraph) => layout.paragraph(paragraph)?,
|
|
34
43
|
Block::Table(table) => layout.table(table)?,
|
|
44
|
+
Block::Picture(picture) => {
|
|
45
|
+
layout.picture(picture, pictures.len());
|
|
46
|
+
pictures.push(picture);
|
|
47
|
+
}
|
|
48
|
+
Block::Symbol(symbol) => layout.symbol(symbol),
|
|
35
49
|
Block::PageBreak => layout.page_break(),
|
|
36
50
|
}
|
|
37
51
|
}
|
|
@@ -67,6 +81,23 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
|
67
81
|
}
|
|
68
82
|
Ok(())
|
|
69
83
|
});
|
|
84
|
+
// Pictures follow the pages: an image, then its soft mask if it has one.
|
|
85
|
+
let mut ids = Vec::with_capacity(pictures.len());
|
|
86
|
+
let mut next = page_id(pages.len());
|
|
87
|
+
for picture in &pictures {
|
|
88
|
+
ids.push(next);
|
|
89
|
+
next += 1 + usize::from(picture::has_mask(picture));
|
|
90
|
+
}
|
|
91
|
+
let images = fmt::from_fn(|f| {
|
|
92
|
+
if !ids.is_empty() {
|
|
93
|
+
f.write_str("/XObject << ")?;
|
|
94
|
+
for (index, id) in ids.iter().enumerate() {
|
|
95
|
+
write!(f, "/Im{index} {id} 0 R ")?;
|
|
96
|
+
}
|
|
97
|
+
f.write_str(">> ")?;
|
|
98
|
+
}
|
|
99
|
+
Ok(())
|
|
100
|
+
});
|
|
70
101
|
let (width, height) = (Num(width), Num(height));
|
|
71
102
|
for (index, content) in pages.iter().enumerate() {
|
|
72
103
|
let id = page_id(index);
|
|
@@ -74,11 +105,16 @@ pub fn write<W: Write>(document: &Document, out: W) -> Result<W> {
|
|
|
74
105
|
id,
|
|
75
106
|
format_args!(
|
|
76
107
|
"<< /Type /Page /Parent {PAGES} 0 R /MediaBox [0 0 {width} {height}] \
|
|
77
|
-
/Resources << /Font << {fonts}>> >> /Contents {} 0 R >>",
|
|
108
|
+
/Resources << /Font << {fonts}>> {images}>> /Contents {} 0 R >>",
|
|
78
109
|
id + 1
|
|
79
110
|
),
|
|
80
111
|
)?;
|
|
81
|
-
pdf.stream(id + 1, content)?;
|
|
112
|
+
pdf.stream(id + 1, content, &[])?;
|
|
113
|
+
}
|
|
114
|
+
for (picture, id) in pictures.into_iter().zip(ids) {
|
|
115
|
+
for (offset, stream) in (0..).zip(picture::streams(picture, id + 1)?) {
|
|
116
|
+
pdf.stored(id + offset, format_args!("{}", stream.dictionary), &stream.data)?;
|
|
117
|
+
}
|
|
82
118
|
}
|
|
83
119
|
pdf.finish(CATALOG, INFO)
|
|
84
120
|
}
|
|
@@ -144,13 +144,86 @@ pub struct Mark {
|
|
|
144
144
|
pub link: Option<String>,
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
/// A workbook sheet: its cells,
|
|
148
|
-
|
|
147
|
+
/// A workbook sheet: its cells and comments and, from an XLSX workbook,
|
|
148
|
+
/// its column widths, data validations and cell formats.
|
|
149
|
+
#[derive(Debug, Clone, Default, PartialEq)]
|
|
149
150
|
pub struct Sheet {
|
|
150
151
|
pub name: String,
|
|
151
152
|
pub hidden: bool,
|
|
152
153
|
pub cells: Grid,
|
|
153
154
|
pub comments: Vec<Note>,
|
|
155
|
+
pub columns: Vec<Columns>,
|
|
156
|
+
pub validations: Vec<Validation>,
|
|
157
|
+
/// The cells with a format of their own, empty ones included.
|
|
158
|
+
pub styles: Vec<Styled>,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// A run of columns, from 0, and their width in characters.
|
|
162
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
163
|
+
pub struct Columns {
|
|
164
|
+
pub first: u32,
|
|
165
|
+
pub last: u32,
|
|
166
|
+
pub width: Option<f64>,
|
|
167
|
+
pub hidden: bool,
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/// A data validation rule, as the workbook stores it.
|
|
171
|
+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
|
172
|
+
pub struct Validation {
|
|
173
|
+
/// The cells it applies to: `B2:B100 D2`.
|
|
174
|
+
pub range: String,
|
|
175
|
+
/// `list`, `whole`, `decimal`, `date`, `time`, `textLength`, `custom`,
|
|
176
|
+
/// or `none` for an input message alone.
|
|
177
|
+
pub kind: String,
|
|
178
|
+
pub operator: Option<String>,
|
|
179
|
+
pub formula1: Option<String>,
|
|
180
|
+
pub formula2: Option<String>,
|
|
181
|
+
pub allow_blank: bool,
|
|
182
|
+
/// Whether a list shows its in-cell drop-down arrow.
|
|
183
|
+
pub dropdown: bool,
|
|
184
|
+
/// `stop`, `warning` or `information`, when a wrong value is refused.
|
|
185
|
+
pub alert: Option<String>,
|
|
186
|
+
pub error_title: Option<String>,
|
|
187
|
+
pub error: Option<String>,
|
|
188
|
+
pub prompt_title: Option<String>,
|
|
189
|
+
pub prompt: Option<String>,
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// A cell with a format of its own, from 0.
|
|
193
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
194
|
+
pub struct Styled {
|
|
195
|
+
pub row: u32,
|
|
196
|
+
pub column: u32,
|
|
197
|
+
pub look: Arc<Look>,
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
impl Styled {
|
|
201
|
+
/// The cell's A1 reference: `B2`.
|
|
202
|
+
#[must_use]
|
|
203
|
+
pub fn reference(&self) -> String {
|
|
204
|
+
reference(self.row, self.column)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/// How a cell format shows a cell. Colors are ARGB (`FFFF0000`), and only
|
|
209
|
+
/// those given as such: theme and indexed colors are left out.
|
|
210
|
+
#[derive(Debug, Clone, Default, PartialEq)]
|
|
211
|
+
#[expect(clippy::struct_excessive_bools, reason = "a format's switches, as Excel keeps them")]
|
|
212
|
+
pub struct Look {
|
|
213
|
+
pub font: String,
|
|
214
|
+
/// In points.
|
|
215
|
+
pub size: f64,
|
|
216
|
+
pub bold: bool,
|
|
217
|
+
pub italic: bool,
|
|
218
|
+
pub underline: bool,
|
|
219
|
+
pub color: Option<String>,
|
|
220
|
+
pub fill: Option<String>,
|
|
221
|
+
pub align: Option<String>,
|
|
222
|
+
pub valign: Option<String>,
|
|
223
|
+
pub wrap: bool,
|
|
224
|
+
/// The left border's style: `thin`, `medium`, `thick`, `dashed`...
|
|
225
|
+
pub border: Option<String>,
|
|
226
|
+
pub number_format: String,
|
|
154
227
|
}
|
|
155
228
|
|
|
156
229
|
/// A comment on a cell.
|
|
@@ -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
|
|
@@ -9,17 +9,34 @@ mod tests;
|
|
|
9
9
|
|
|
10
10
|
use serde_json::{Map, Value as Json, json};
|
|
11
11
|
|
|
12
|
-
use super::{
|
|
12
|
+
use super::{
|
|
13
|
+
Block, Bounds, Cell, Columns, Content, Grid, Image, ImageLocation, Look, Mark, Page, Sheet,
|
|
14
|
+
Styled, Validation, Value,
|
|
15
|
+
};
|
|
16
|
+
use crate::xlsx::column_name;
|
|
17
|
+
|
|
18
|
+
/// What [`content_with`] adds to the content.
|
|
19
|
+
#[derive(Debug, Clone, Copy, Default)]
|
|
20
|
+
pub struct Options {
|
|
21
|
+
/// Each formatted cell of a sheet, with its look.
|
|
22
|
+
pub styles: bool,
|
|
23
|
+
}
|
|
13
24
|
|
|
14
25
|
/// The document's content, by format: `pages`, `sheets` or `blocks`.
|
|
15
26
|
#[must_use]
|
|
16
27
|
pub fn content(content: &Content) -> Map<String, Json> {
|
|
28
|
+
content_with(content, Options::default())
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/// The document's content, with what `options` add.
|
|
32
|
+
#[must_use]
|
|
33
|
+
pub fn content_with(content: &Content, options: Options) -> Map<String, Json> {
|
|
17
34
|
let mut object = Map::new();
|
|
18
35
|
object.insert("format".into(), content.format().as_str().into());
|
|
19
36
|
match content {
|
|
20
37
|
Content::Pdf(pages) => object.insert("pages".into(), pages.iter().map(page).collect()),
|
|
21
38
|
Content::Xlsx(sheets) | Content::Ods(sheets) | Content::Xls(sheets) => {
|
|
22
|
-
object.insert("sheets".into(), sheets.iter().map(sheet).collect())
|
|
39
|
+
object.insert("sheets".into(), sheets.iter().map(|it| sheet(it, options)).collect())
|
|
23
40
|
}
|
|
24
41
|
Content::Docx(blocks) => object.insert("blocks".into(), blocks.iter().map(block).collect()),
|
|
25
42
|
};
|
|
@@ -116,7 +133,19 @@ fn with_marks(mut object: Json, marks: &[Mark]) -> Json {
|
|
|
116
133
|
object
|
|
117
134
|
}
|
|
118
135
|
|
|
119
|
-
fn sheet(sheet: &Sheet) -> Json {
|
|
136
|
+
fn sheet(sheet: &Sheet, options: Options) -> Json {
|
|
137
|
+
let mut object = Map::new();
|
|
138
|
+
object.insert("name".into(), sheet.name.clone().into());
|
|
139
|
+
object.insert("hidden".into(), sheet.hidden.into());
|
|
140
|
+
object.insert("rows".into(), rows(&sheet.cells, true));
|
|
141
|
+
object.extend(sheet_details(sheet, options));
|
|
142
|
+
Json::Object(object)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// What a sheet has besides its values: `comments`, `columns`,
|
|
146
|
+
/// `validations` and, with `options.styles`, `styles`.
|
|
147
|
+
#[must_use]
|
|
148
|
+
pub fn sheet_details(sheet: &Sheet, options: Options) -> Map<String, Json> {
|
|
120
149
|
let comments: Vec<Json> = sheet
|
|
121
150
|
.comments
|
|
122
151
|
.iter()
|
|
@@ -125,11 +154,69 @@ fn sheet(sheet: &Sheet) -> Json {
|
|
|
125
154
|
json!({ "cell": note.reference(), "author": author, "text": note.text })
|
|
126
155
|
})
|
|
127
156
|
.collect();
|
|
157
|
+
let mut details = Map::new();
|
|
158
|
+
details.insert("comments".into(), comments.into());
|
|
159
|
+
details.insert("columns".into(), sheet.columns.iter().map(columns).collect());
|
|
160
|
+
details.insert("validations".into(), sheet.validations.iter().map(validation).collect());
|
|
161
|
+
if options.styles {
|
|
162
|
+
details.insert("styles".into(), sheet.styles.iter().map(styled).collect());
|
|
163
|
+
}
|
|
164
|
+
details
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/// Columns by letter: `A`, `XFD`.
|
|
168
|
+
fn columns(run: &Columns) -> Json {
|
|
169
|
+
let name = |column: u32| column_name(u16::try_from(column).unwrap_or(u16::MAX));
|
|
170
|
+
let (first, last) = (name(run.first), name(run.last));
|
|
171
|
+
json!({ "first": first, "last": last, "width": run.width, "hidden": run.hidden })
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
fn validation(rule: &Validation) -> Json {
|
|
175
|
+
json!({
|
|
176
|
+
"range": rule.range,
|
|
177
|
+
"type": rule.kind,
|
|
178
|
+
"operator": rule.operator,
|
|
179
|
+
"formula1": rule.formula1,
|
|
180
|
+
"formula2": rule.formula2,
|
|
181
|
+
"allow_blank": rule.allow_blank,
|
|
182
|
+
"dropdown": rule.dropdown,
|
|
183
|
+
"alert": rule.alert,
|
|
184
|
+
"error_title": rule.error_title,
|
|
185
|
+
"error": rule.error,
|
|
186
|
+
"prompt_title": rule.prompt_title,
|
|
187
|
+
"prompt": rule.prompt,
|
|
188
|
+
})
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
fn styled(cell: &Styled) -> Json {
|
|
192
|
+
let Look {
|
|
193
|
+
font,
|
|
194
|
+
size,
|
|
195
|
+
bold,
|
|
196
|
+
italic,
|
|
197
|
+
underline,
|
|
198
|
+
color,
|
|
199
|
+
fill,
|
|
200
|
+
align,
|
|
201
|
+
valign,
|
|
202
|
+
wrap,
|
|
203
|
+
border,
|
|
204
|
+
number_format,
|
|
205
|
+
} = cell.look.as_ref();
|
|
128
206
|
json!({
|
|
129
|
-
"
|
|
130
|
-
"
|
|
131
|
-
"
|
|
132
|
-
"
|
|
207
|
+
"cell": cell.reference(),
|
|
208
|
+
"font": font,
|
|
209
|
+
"size": size,
|
|
210
|
+
"bold": bold,
|
|
211
|
+
"italic": italic,
|
|
212
|
+
"underline": underline,
|
|
213
|
+
"color": color,
|
|
214
|
+
"fill": fill,
|
|
215
|
+
"align": align,
|
|
216
|
+
"valign": valign,
|
|
217
|
+
"wrap": wrap,
|
|
218
|
+
"border": border,
|
|
219
|
+
"number_format": number_format,
|
|
133
220
|
})
|
|
134
221
|
}
|
|
135
222
|
|