imprimatur 0.1.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 +7 -0
- data/CHANGELOG.md +8 -0
- data/Cargo.lock +835 -0
- data/Cargo.toml +44 -0
- data/LICENSE.txt +29 -0
- data/README.md +314 -0
- data/ext/imprimatur_native/cli/Cargo.toml +30 -0
- data/ext/imprimatur_native/cli/src/app.rs +183 -0
- data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
- data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
- data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
- data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
- data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
- data/ext/imprimatur_native/cli/src/cli.rs +71 -0
- data/ext/imprimatur_native/cli/src/main.rs +15 -0
- data/ext/imprimatur_native/core/Cargo.toml +33 -0
- data/ext/imprimatur_native/core/LICENSE +29 -0
- data/ext/imprimatur_native/core/README.md +105 -0
- data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
- data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
- data/ext/imprimatur_native/core/src/doc.rs +210 -0
- data/ext/imprimatur_native/core/src/docx.rs +343 -0
- data/ext/imprimatur_native/core/src/error.rs +113 -0
- data/ext/imprimatur_native/core/src/lib.rs +38 -0
- data/ext/imprimatur_native/core/src/opc.rs +51 -0
- data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
- data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
- data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
- data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
- data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
- data/ext/imprimatur_native/core/src/pdf.rs +84 -0
- data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
- data/ext/imprimatur_native/core/src/read/content.rs +266 -0
- data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
- data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
- data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
- data/ext/imprimatur_native/core/src/read/image.rs +233 -0
- data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
- data/ext/imprimatur_native/core/src/read/json.rs +172 -0
- data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
- data/ext/imprimatur_native/core/src/read/package.rs +191 -0
- data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
- data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
- data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
- data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
- data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
- data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
- data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
- data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
- data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
- data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
- data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
- data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
- data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
- data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
- data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
- data/ext/imprimatur_native/core/src/read/span.rs +22 -0
- data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
- data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
- data/ext/imprimatur_native/core/src/read.rs +145 -0
- data/ext/imprimatur_native/core/src/style.rs +82 -0
- data/ext/imprimatur_native/core/src/test_support.rs +163 -0
- data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
- data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
- data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
- data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
- data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
- data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
- data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
- data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
- data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
- data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
- data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
- data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
- data/ext/imprimatur_native/core/src/xml.rs +95 -0
- data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
- data/ext/imprimatur_native/core/src/zip.rs +161 -0
- data/ext/imprimatur_native/extconf.rb +45 -0
- data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
- data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
- data/ext/imprimatur_native/ffi/src/document.rs +138 -0
- data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
- data/ext/imprimatur_native/ffi/src/read.rs +182 -0
- data/ext/imprimatur_native/ffi/src/style.rs +85 -0
- data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
- data/lib/imprimatur/document.rb +127 -0
- data/lib/imprimatur/error.rb +22 -0
- data/lib/imprimatur/formula.rb +13 -0
- data/lib/imprimatur/native.rb +17 -0
- data/lib/imprimatur/reader.rb +59 -0
- data/lib/imprimatur/style.rb +31 -0
- data/lib/imprimatur/validation.rb +35 -0
- data/lib/imprimatur/version.rb +5 -0
- data/lib/imprimatur/xlsx.rb +183 -0
- data/lib/imprimatur.rb +111 -0
- metadata +157 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
//! A write-only zip archive: enough for the Open Packaging Conventions
|
|
2
|
+
//! container that XLSX and DOCX are. Entries are deflated (`miniz_oxide`), or
|
|
3
|
+
//! stored when deflate would not make them smaller.
|
|
4
|
+
//!
|
|
5
|
+
//! Output is deterministic: every entry carries the zip epoch (1980-01-01
|
|
6
|
+
//! 00:00) as its timestamp, and deflate runs at a fixed level, so the same
|
|
7
|
+
//! document always gives the same bytes.
|
|
8
|
+
|
|
9
|
+
pub mod reader;
|
|
10
|
+
#[cfg(test)]
|
|
11
|
+
mod tests;
|
|
12
|
+
|
|
13
|
+
use std::io::Write;
|
|
14
|
+
|
|
15
|
+
use crate::{Error, Result};
|
|
16
|
+
|
|
17
|
+
const LOCAL_HEADER: u32 = 0x0403_4b50;
|
|
18
|
+
const CENTRAL_HEADER: u32 = 0x0201_4b50;
|
|
19
|
+
const END_OF_CENTRAL_DIRECTORY: u32 = 0x0605_4b50;
|
|
20
|
+
/// 2.0: the version that introduced folders, which every reader handles.
|
|
21
|
+
const VERSION: u16 = 20;
|
|
22
|
+
const METHOD_STORED: u16 = 0;
|
|
23
|
+
const METHOD_DEFLATED: u16 = 8;
|
|
24
|
+
/// zlib's default trade-off between size and speed.
|
|
25
|
+
const DEFLATE_LEVEL: u8 = 6;
|
|
26
|
+
/// 00:00:00 in MS-DOS time.
|
|
27
|
+
const DOS_TIME: u16 = 0;
|
|
28
|
+
/// 1980-01-01 in MS-DOS date: day 1, month 1, year 0 (1980).
|
|
29
|
+
const DOS_DATE: u16 = (1 << 5) | 1;
|
|
30
|
+
|
|
31
|
+
struct Entry {
|
|
32
|
+
name: String,
|
|
33
|
+
name_len: u16,
|
|
34
|
+
method: u16,
|
|
35
|
+
crc: u32,
|
|
36
|
+
compressed_size: u32,
|
|
37
|
+
size: u32,
|
|
38
|
+
offset: u32,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/// Streams entries into `out`, then the central directory on [`finish`](Self::finish).
|
|
42
|
+
pub struct ZipWriter<W: Write> {
|
|
43
|
+
out: W,
|
|
44
|
+
entries: Vec<Entry>,
|
|
45
|
+
written: u64,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
impl<W: Write> ZipWriter<W> {
|
|
49
|
+
pub const fn new(out: W) -> Self {
|
|
50
|
+
Self { out, entries: Vec::new(), written: 0 }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Writes one entry. Names are ASCII part names such as
|
|
54
|
+
/// `xl/workbook.xml`, so the UTF-8 name flag is never needed.
|
|
55
|
+
pub fn add(&mut self, name: &str, data: &[u8]) -> Result<()> {
|
|
56
|
+
debug_assert!(name.is_ascii(), "OPC part names are ASCII: {name:?}");
|
|
57
|
+
let offset = self.offset()?;
|
|
58
|
+
let too_large = |_| Error::ArchiveTooLarge("entry over 4 GiB");
|
|
59
|
+
let size = u32::try_from(data.len()).map_err(too_large)?;
|
|
60
|
+
let name_len =
|
|
61
|
+
u16::try_from(name.len()).map_err(|_| Error::ArchiveTooLarge("entry name"))?;
|
|
62
|
+
let crc = crc32fast::hash(data);
|
|
63
|
+
|
|
64
|
+
let deflated = miniz_oxide::deflate::compress_to_vec(data, DEFLATE_LEVEL);
|
|
65
|
+
let (method, body) = if deflated.len() < data.len() {
|
|
66
|
+
(METHOD_DEFLATED, deflated.as_slice())
|
|
67
|
+
} else {
|
|
68
|
+
(METHOD_STORED, data)
|
|
69
|
+
};
|
|
70
|
+
let compressed_size = u32::try_from(body.len()).map_err(too_large)?;
|
|
71
|
+
|
|
72
|
+
let mut header = Vec::with_capacity(30 + name.len());
|
|
73
|
+
put32(&mut header, LOCAL_HEADER);
|
|
74
|
+
put16(&mut header, VERSION);
|
|
75
|
+
put16(&mut header, 0); // flags
|
|
76
|
+
put16(&mut header, method);
|
|
77
|
+
put16(&mut header, DOS_TIME);
|
|
78
|
+
put16(&mut header, DOS_DATE);
|
|
79
|
+
put32(&mut header, crc);
|
|
80
|
+
put32(&mut header, compressed_size);
|
|
81
|
+
put32(&mut header, size);
|
|
82
|
+
put16(&mut header, name_len);
|
|
83
|
+
put16(&mut header, 0); // extra field
|
|
84
|
+
header.extend_from_slice(name.as_bytes());
|
|
85
|
+
|
|
86
|
+
self.write(&header)?;
|
|
87
|
+
self.write(body)?;
|
|
88
|
+
self.entries.push(Entry {
|
|
89
|
+
name: name.to_owned(),
|
|
90
|
+
name_len,
|
|
91
|
+
method,
|
|
92
|
+
crc,
|
|
93
|
+
compressed_size,
|
|
94
|
+
size,
|
|
95
|
+
offset,
|
|
96
|
+
});
|
|
97
|
+
Ok(())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Writes the central directory and returns the underlying writer.
|
|
101
|
+
pub fn finish(mut self) -> Result<W> {
|
|
102
|
+
let directory_offset = self.offset()?;
|
|
103
|
+
let count = u16::try_from(self.entries.len())
|
|
104
|
+
.map_err(|_| Error::ArchiveTooLarge("more than 65535 entries"))?;
|
|
105
|
+
|
|
106
|
+
let mut directory = Vec::new();
|
|
107
|
+
for entry in &self.entries {
|
|
108
|
+
put32(&mut directory, CENTRAL_HEADER);
|
|
109
|
+
put16(&mut directory, VERSION); // made by: MS-DOS, 2.0
|
|
110
|
+
put16(&mut directory, VERSION); // needed to extract
|
|
111
|
+
put16(&mut directory, 0); // flags
|
|
112
|
+
put16(&mut directory, entry.method);
|
|
113
|
+
put16(&mut directory, DOS_TIME);
|
|
114
|
+
put16(&mut directory, DOS_DATE);
|
|
115
|
+
put32(&mut directory, entry.crc);
|
|
116
|
+
put32(&mut directory, entry.compressed_size);
|
|
117
|
+
put32(&mut directory, entry.size);
|
|
118
|
+
put16(&mut directory, entry.name_len);
|
|
119
|
+
put16(&mut directory, 0); // extra field
|
|
120
|
+
put16(&mut directory, 0); // comment
|
|
121
|
+
put16(&mut directory, 0); // disk number
|
|
122
|
+
put16(&mut directory, 0); // internal attributes
|
|
123
|
+
put32(&mut directory, 0); // external attributes
|
|
124
|
+
put32(&mut directory, entry.offset);
|
|
125
|
+
directory.extend_from_slice(entry.name.as_bytes());
|
|
126
|
+
}
|
|
127
|
+
let directory_size = u32::try_from(directory.len())
|
|
128
|
+
.map_err(|_| Error::ArchiveTooLarge("central directory over 4 GiB"))?;
|
|
129
|
+
|
|
130
|
+
put32(&mut directory, END_OF_CENTRAL_DIRECTORY);
|
|
131
|
+
put16(&mut directory, 0); // this disk
|
|
132
|
+
put16(&mut directory, 0); // disk with the directory
|
|
133
|
+
put16(&mut directory, count); // entries on this disk
|
|
134
|
+
put16(&mut directory, count); // entries in total
|
|
135
|
+
put32(&mut directory, directory_size);
|
|
136
|
+
put32(&mut directory, directory_offset);
|
|
137
|
+
put16(&mut directory, 0); // comment
|
|
138
|
+
|
|
139
|
+
self.write(&directory)?;
|
|
140
|
+
self.out.flush()?;
|
|
141
|
+
Ok(self.out)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
fn offset(&self) -> Result<u32> {
|
|
145
|
+
u32::try_from(self.written).map_err(|_| Error::ArchiveTooLarge("archive over 4 GiB"))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
fn write(&mut self, bytes: &[u8]) -> Result<()> {
|
|
149
|
+
self.out.write_all(bytes)?;
|
|
150
|
+
self.written += bytes.len() as u64;
|
|
151
|
+
Ok(())
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
fn put16(buf: &mut Vec<u8>, value: u16) {
|
|
156
|
+
buf.extend_from_slice(&value.to_le_bytes());
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
fn put32(buf: &mut Vec<u8>, value: u32) {
|
|
160
|
+
buf.extend_from_slice(&value.to_le_bytes());
|
|
161
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# imprimatur is native only: without the extension there is no gem, so every
|
|
4
|
+
# missing prerequisite stops the install with what to do about it.
|
|
5
|
+
def fail_build(reason)
|
|
6
|
+
abort "imprimatur: #{reason}"
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def command?(command) = system(command, out: File::NULL, err: File::NULL)
|
|
10
|
+
|
|
11
|
+
# rb_sys writes a GNU Makefile; RubyGems runs the system make (BSD make here).
|
|
12
|
+
BSD = RUBY_PLATFORM.include?('bsd')
|
|
13
|
+
|
|
14
|
+
fail_build("#{RUBY_ENGINE} is not supported; imprimatur needs CRuby") unless RUBY_ENGINE == 'ruby'
|
|
15
|
+
fail_build('Cargo not found; install Rust (https://rustup.rs) or use a platform gem') unless command?('cargo --version')
|
|
16
|
+
fail_build('GNU make not found (pkg install gmake)') if BSD && !command?('gmake --version')
|
|
17
|
+
|
|
18
|
+
require 'mkmf'
|
|
19
|
+
require 'rb_sys/mkmf'
|
|
20
|
+
|
|
21
|
+
create_rust_makefile('imprimatur/imprimatur_native') do |r|
|
|
22
|
+
r.ext_dir = 'ffi'
|
|
23
|
+
r.profile = ENV.fetch('RB_SYS_CARGO_PROFILE', :release).to_sym
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# BSD make reads BSDmakefile first (GNU make ignores it): delegate to gmake.
|
|
27
|
+
if BSD
|
|
28
|
+
File.write('BSDmakefile', <<~MAKE)
|
|
29
|
+
GMAKE_VARS =
|
|
30
|
+
.for var in DESTDIR sitearchdir sitelibdir
|
|
31
|
+
.if defined(${var})
|
|
32
|
+
GMAKE_VARS += ${var}="${${var}}"
|
|
33
|
+
.endif
|
|
34
|
+
.endfor
|
|
35
|
+
|
|
36
|
+
# BSD make exports its own job flags (-J) in MAKEFLAGS; gmake rejects them.
|
|
37
|
+
GMAKE = env -u MAKEFLAGS -u MFLAGS gmake -f Makefile
|
|
38
|
+
|
|
39
|
+
all:
|
|
40
|
+
\t${GMAKE} ${GMAKE_VARS}
|
|
41
|
+
|
|
42
|
+
.DEFAULT:
|
|
43
|
+
\t${GMAKE} ${GMAKE_VARS} $@
|
|
44
|
+
MAKE
|
|
45
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "imprimatur_native"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Ruby bindings for the imprimatur crate"
|
|
5
|
+
publish = false
|
|
6
|
+
edition.workspace = true
|
|
7
|
+
rust-version.workspace = true
|
|
8
|
+
authors.workspace = true
|
|
9
|
+
license.workspace = true
|
|
10
|
+
|
|
11
|
+
[lib]
|
|
12
|
+
crate-type = ["cdylib"]
|
|
13
|
+
|
|
14
|
+
[lints]
|
|
15
|
+
workspace = true
|
|
16
|
+
|
|
17
|
+
[dependencies]
|
|
18
|
+
imprimatur = { workspace = true, features = ["json"] }
|
|
19
|
+
serde_json.workspace = true
|
|
20
|
+
magnus.workspace = true
|
|
21
|
+
rb-sys.workspace = true
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
//! Ruby values to cells.
|
|
2
|
+
|
|
3
|
+
use imprimatur::xlsx::{Cell, DateTime};
|
|
4
|
+
use magnus::prelude::*;
|
|
5
|
+
use magnus::{Error, Float, Integer, RClass, RString, Ruby, Value};
|
|
6
|
+
|
|
7
|
+
use crate::{OrRaise, domain_error};
|
|
8
|
+
|
|
9
|
+
/// The largest integer every `f64` neighbour of which is also an integer:
|
|
10
|
+
/// beyond it, Excel would silently store a different number.
|
|
11
|
+
const MAX_EXACT_INTEGER: u64 = 1 << 53;
|
|
12
|
+
|
|
13
|
+
/// Ruby classes looked up once per call, not once per cell.
|
|
14
|
+
pub struct Classes {
|
|
15
|
+
formula: RClass,
|
|
16
|
+
date: Option<RClass>,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
impl Classes {
|
|
20
|
+
pub fn load(ruby: &Ruby) -> Result<Self, Error> {
|
|
21
|
+
let imprimatur: magnus::RModule = ruby.class_object().const_get("Imprimatur")?;
|
|
22
|
+
// Date is only there once something has required 'date'.
|
|
23
|
+
let date_defined: bool = ruby.class_object().funcall("const_defined?", ("Date",))?;
|
|
24
|
+
let date = if date_defined { Some(ruby.class_object().const_get("Date")?) } else { None };
|
|
25
|
+
Ok(Self { formula: imprimatur.const_get("Formula")?, date })
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// `nil` is no cell.
|
|
30
|
+
pub fn to_cell(ruby: &Ruby, value: Value, classes: &Classes) -> Result<Option<Cell>, Error> {
|
|
31
|
+
if value.is_nil() {
|
|
32
|
+
return Ok(None);
|
|
33
|
+
}
|
|
34
|
+
if let Some(text) = RString::from_value(value) {
|
|
35
|
+
return Ok(Some(Cell::Text(text.to_string()?)));
|
|
36
|
+
}
|
|
37
|
+
if let Some(float) = Float::from_value(value) {
|
|
38
|
+
return Ok(Some(Cell::Number(float.to_f64())));
|
|
39
|
+
}
|
|
40
|
+
if let Some(integer) = Integer::from_value(value) {
|
|
41
|
+
return integer_cell(ruby, integer).map(Some);
|
|
42
|
+
}
|
|
43
|
+
if value.is_kind_of(ruby.class_true_class()) {
|
|
44
|
+
return Ok(Some(Cell::Bool(true)));
|
|
45
|
+
}
|
|
46
|
+
if value.is_kind_of(ruby.class_false_class()) {
|
|
47
|
+
return Ok(Some(Cell::Bool(false)));
|
|
48
|
+
}
|
|
49
|
+
if value.is_kind_of(classes.formula) {
|
|
50
|
+
let expression: String = value.funcall("expression", ())?;
|
|
51
|
+
return Ok(Some(Cell::formula(expression)));
|
|
52
|
+
}
|
|
53
|
+
if value.is_kind_of(ruby.class_time()) {
|
|
54
|
+
return time_cell(ruby, value).map(Some);
|
|
55
|
+
}
|
|
56
|
+
if let Some(date) = classes.date
|
|
57
|
+
&& value.is_kind_of(date)
|
|
58
|
+
{
|
|
59
|
+
// DateTime is a Date with a time of day; a plain Date has none.
|
|
60
|
+
if value.respond_to("hour", false)? {
|
|
61
|
+
let fraction: Value = value.funcall("sec_fraction", ())?;
|
|
62
|
+
let nanosecond: u32 =
|
|
63
|
+
fraction.funcall::<_, _, Value>("*", (1_000_000_000,))?.funcall("to_i", ())?;
|
|
64
|
+
return date_cell(ruby, value, ["hour", "minute", "second"], nanosecond).map(Some);
|
|
65
|
+
}
|
|
66
|
+
return date_cell(ruby, value, [], 0).map(Some);
|
|
67
|
+
}
|
|
68
|
+
// BigDecimal, Rational: Excel keeps every number as a double anyway.
|
|
69
|
+
if value.is_kind_of(ruby.class_numeric()) {
|
|
70
|
+
let number: f64 = value.funcall("to_f", ())?;
|
|
71
|
+
return Ok(Some(Cell::Number(number)));
|
|
72
|
+
}
|
|
73
|
+
// ActiveSupport::TimeWithZone is a Time only to Ruby's own is_a?; its
|
|
74
|
+
// fields are the wall clock of its zone.
|
|
75
|
+
if value.respond_to("is_a?", false)?
|
|
76
|
+
&& value.funcall::<_, _, bool>("is_a?", (ruby.class_time(),))?
|
|
77
|
+
{
|
|
78
|
+
return time_cell(ruby, value).map(Some);
|
|
79
|
+
}
|
|
80
|
+
Err(Error::new(
|
|
81
|
+
ruby.exception_type_error(),
|
|
82
|
+
format!("unsupported cell value {} ({})", value.inspect(), value.class().inspect()),
|
|
83
|
+
))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn time_cell(ruby: &Ruby, value: Value) -> Result<Cell, Error> {
|
|
87
|
+
let nanosecond: u32 = value.funcall("nsec", ())?;
|
|
88
|
+
date_cell(ruby, value, ["hour", "min", "sec"], nanosecond)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// The wall-clock fields of a `Time`, `Date` or `DateTime`, named by `time`.
|
|
92
|
+
fn date_cell<const N: usize>(
|
|
93
|
+
ruby: &Ruby,
|
|
94
|
+
value: Value,
|
|
95
|
+
time: [&str; N],
|
|
96
|
+
nanosecond: u32,
|
|
97
|
+
) -> Result<Cell, Error> {
|
|
98
|
+
let year: i64 = value.funcall("year", ())?;
|
|
99
|
+
let month: u8 = value.funcall("month", ())?;
|
|
100
|
+
let day: u8 = value.funcall("day", ())?;
|
|
101
|
+
let mut clock = [0_u8; 3];
|
|
102
|
+
for (field, method) in clock.iter_mut().zip(time) {
|
|
103
|
+
*field = value.funcall(method, ())?;
|
|
104
|
+
}
|
|
105
|
+
let out_of_range = || {
|
|
106
|
+
let error = imprimatur::Error::DateOutOfRange(value.inspect());
|
|
107
|
+
domain_error(ruby, &error)
|
|
108
|
+
};
|
|
109
|
+
let year = u16::try_from(year).map_err(|_| out_of_range())?;
|
|
110
|
+
let date = DateTime::date(year, month, day).or_raise(ruby)?;
|
|
111
|
+
let [hour, minute, second] = clock;
|
|
112
|
+
let date = date.at(hour, minute, second, nanosecond).or_raise(ruby)?;
|
|
113
|
+
Ok(Cell::DateTime(date))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Integers become numbers only when the `f64` holds them exactly.
|
|
117
|
+
fn integer_cell(ruby: &Ruby, integer: Integer) -> Result<Cell, Error> {
|
|
118
|
+
let out_of_range = || {
|
|
119
|
+
Error::new(
|
|
120
|
+
ruby.exception_range_error(),
|
|
121
|
+
format!(
|
|
122
|
+
"{integer} cannot be stored exactly; Excel keeps numbers as doubles (up to 2**53)"
|
|
123
|
+
),
|
|
124
|
+
)
|
|
125
|
+
};
|
|
126
|
+
let value = integer.to_i64().map_err(|_| out_of_range())?;
|
|
127
|
+
if value.unsigned_abs() > MAX_EXACT_INTEGER {
|
|
128
|
+
return Err(out_of_range());
|
|
129
|
+
}
|
|
130
|
+
#[expect(clippy::cast_precision_loss, reason = "|value| <= 2**53, so the cast is exact")]
|
|
131
|
+
Ok(Cell::Number(value as f64))
|
|
132
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
//! `Imprimatur::Native::Document`: the document model, rendered to PDF or DOCX.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
|
|
5
|
+
use imprimatur::doc::{Document, PageSize, Paragraph, Run, Table};
|
|
6
|
+
use imprimatur::style::Color;
|
|
7
|
+
use magnus::{Error, RModule, RString, Ruby, function, method, prelude::*};
|
|
8
|
+
|
|
9
|
+
use crate::{ALIGNS, DIRECTIONS, OrRaise, RubyBytes, borrow, keyword};
|
|
10
|
+
|
|
11
|
+
/// `[text, bold, italic, underline, color]`, as `Imprimatur::Paragraph` collects it.
|
|
12
|
+
type RunSpec = (String, bool, bool, bool, Option<String>);
|
|
13
|
+
|
|
14
|
+
#[magnus::wrap(class = "Imprimatur::Native::Document", free_immediately, size)]
|
|
15
|
+
struct NativeDocument(RefCell<Document>);
|
|
16
|
+
|
|
17
|
+
#[derive(Clone, Copy)]
|
|
18
|
+
enum Output {
|
|
19
|
+
Pdf,
|
|
20
|
+
Docx,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const OUTPUTS: [(&str, Output); 2] = [("pdf", Output::Pdf), ("docx", Output::Docx)];
|
|
24
|
+
|
|
25
|
+
#[expect(clippy::needless_pass_by_value, reason = "magnus passes Ruby arguments as owned values")]
|
|
26
|
+
impl NativeDocument {
|
|
27
|
+
fn new() -> Self {
|
|
28
|
+
Self(RefCell::new(Document::new()))
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fn set_page(
|
|
32
|
+
ruby: &Ruby,
|
|
33
|
+
rb_self: &Self,
|
|
34
|
+
size: String,
|
|
35
|
+
page_numbers: bool,
|
|
36
|
+
) -> Result<(), Error> {
|
|
37
|
+
let size = keyword(
|
|
38
|
+
ruby,
|
|
39
|
+
"page_size",
|
|
40
|
+
&size,
|
|
41
|
+
&[("a4", PageSize::A4), ("letter", PageSize::Letter)],
|
|
42
|
+
)?;
|
|
43
|
+
borrow(ruby, &rb_self.0, "document")?.set_page_size(size).set_page_numbers(page_numbers);
|
|
44
|
+
Ok(())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
fn heading(ruby: &Ruby, rb_self: &Self, level: u8, text: String) -> Result<(), Error> {
|
|
48
|
+
borrow(ruby, &rb_self.0, "document")?.heading(level, text).or_raise(ruby)?;
|
|
49
|
+
Ok(())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
fn paragraph(
|
|
53
|
+
ruby: &Ruby,
|
|
54
|
+
rb_self: &Self,
|
|
55
|
+
runs: Vec<RunSpec>,
|
|
56
|
+
align: Option<String>,
|
|
57
|
+
direction: Option<String>,
|
|
58
|
+
) -> Result<(), Error> {
|
|
59
|
+
let mut paragraph = Paragraph::new();
|
|
60
|
+
for (text, bold, italic, underline, color) in runs {
|
|
61
|
+
let mut run = Run::new(text);
|
|
62
|
+
if bold {
|
|
63
|
+
run = run.bold();
|
|
64
|
+
}
|
|
65
|
+
if italic {
|
|
66
|
+
run = run.italic();
|
|
67
|
+
}
|
|
68
|
+
if underline {
|
|
69
|
+
run = run.underline();
|
|
70
|
+
}
|
|
71
|
+
if let Some(color) = color {
|
|
72
|
+
run = run.color(Color::from_hex(&color).or_raise(ruby)?);
|
|
73
|
+
}
|
|
74
|
+
paragraph = paragraph.run(run);
|
|
75
|
+
}
|
|
76
|
+
if let Some(align) = align {
|
|
77
|
+
paragraph = paragraph.align(keyword(ruby, "align", &align, &ALIGNS)?);
|
|
78
|
+
}
|
|
79
|
+
if let Some(direction) = direction {
|
|
80
|
+
paragraph = paragraph.direction(keyword(ruby, "direction", &direction, &DIRECTIONS)?);
|
|
81
|
+
}
|
|
82
|
+
borrow(ruby, &rb_self.0, "document")?.push_paragraph(paragraph).or_raise(ruby)?;
|
|
83
|
+
Ok(())
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
fn table(
|
|
87
|
+
ruby: &Ruby,
|
|
88
|
+
rb_self: &Self,
|
|
89
|
+
rows: Vec<Vec<String>>,
|
|
90
|
+
header: bool,
|
|
91
|
+
) -> Result<(), Error> {
|
|
92
|
+
let mut table = Table::new(rows);
|
|
93
|
+
if header {
|
|
94
|
+
table = table.header();
|
|
95
|
+
}
|
|
96
|
+
borrow(ruby, &rb_self.0, "document")?.table(table).or_raise(ruby)?;
|
|
97
|
+
Ok(())
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
fn page_break(ruby: &Ruby, rb_self: &Self) -> Result<(), Error> {
|
|
101
|
+
borrow(ruby, &rb_self.0, "document")?.page_break();
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// `:pdf` or `:docx` as a binary String.
|
|
106
|
+
fn render(ruby: &Ruby, rb_self: &Self, format: String) -> Result<RString, Error> {
|
|
107
|
+
let document = borrow(ruby, &rb_self.0, "document")?;
|
|
108
|
+
let out = RubyBytes(ruby.str_buf_new(0));
|
|
109
|
+
let written = match keyword(ruby, "format", &format, &OUTPUTS)? {
|
|
110
|
+
Output::Pdf => document.write_pdf(out),
|
|
111
|
+
Output::Docx => document.write_docx(out),
|
|
112
|
+
};
|
|
113
|
+
Ok(written.or_raise(ruby)?.0)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Writes `:pdf` or `:docx` straight to `path`.
|
|
117
|
+
fn save(ruby: &Ruby, rb_self: &Self, format: String, path: String) -> Result<(), Error> {
|
|
118
|
+
let document = borrow(ruby, &rb_self.0, "document")?;
|
|
119
|
+
let saved = match keyword(ruby, "format", &format, &OUTPUTS)? {
|
|
120
|
+
Output::Pdf => document.save_pdf(path),
|
|
121
|
+
Output::Docx => document.save_docx(path),
|
|
122
|
+
};
|
|
123
|
+
saved.or_raise(ruby)
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
pub fn define(ruby: &Ruby, native: RModule) -> Result<(), Error> {
|
|
128
|
+
let class = native.define_class("Document", ruby.class_object())?;
|
|
129
|
+
class.define_singleton_method("new", function!(NativeDocument::new, 0))?;
|
|
130
|
+
class.define_method("set_page", method!(NativeDocument::set_page, 2))?;
|
|
131
|
+
class.define_method("heading", method!(NativeDocument::heading, 2))?;
|
|
132
|
+
class.define_method("paragraph", method!(NativeDocument::paragraph, 3))?;
|
|
133
|
+
class.define_method("table", method!(NativeDocument::table, 2))?;
|
|
134
|
+
class.define_method("page_break", method!(NativeDocument::page_break, 0))?;
|
|
135
|
+
class.define_method("render", method!(NativeDocument::render, 1))?;
|
|
136
|
+
class.define_method("save", method!(NativeDocument::save, 2))?;
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|