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.
Files changed (98) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +8 -0
  3. data/Cargo.lock +835 -0
  4. data/Cargo.toml +44 -0
  5. data/LICENSE.txt +29 -0
  6. data/README.md +314 -0
  7. data/ext/imprimatur_native/cli/Cargo.toml +30 -0
  8. data/ext/imprimatur_native/cli/src/app.rs +183 -0
  9. data/ext/imprimatur_native/cli/src/cli/completion.rs +26 -0
  10. data/ext/imprimatur_native/cli/src/cli/grep.rs +359 -0
  11. data/ext/imprimatur_native/cli/src/cli/images.rs +137 -0
  12. data/ext/imprimatur_native/cli/src/cli/limits.rs +49 -0
  13. data/ext/imprimatur_native/cli/src/cli/markdown.rs +128 -0
  14. data/ext/imprimatur_native/cli/src/cli.rs +71 -0
  15. data/ext/imprimatur_native/cli/src/main.rs +15 -0
  16. data/ext/imprimatur_native/core/Cargo.toml +33 -0
  17. data/ext/imprimatur_native/core/LICENSE +29 -0
  18. data/ext/imprimatur_native/core/README.md +105 -0
  19. data/ext/imprimatur_native/core/src/doc/paragraph.rs +112 -0
  20. data/ext/imprimatur_native/core/src/doc/table.rs +41 -0
  21. data/ext/imprimatur_native/core/src/doc.rs +210 -0
  22. data/ext/imprimatur_native/core/src/docx.rs +343 -0
  23. data/ext/imprimatur_native/core/src/error.rs +113 -0
  24. data/ext/imprimatur_native/core/src/lib.rs +38 -0
  25. data/ext/imprimatur_native/core/src/opc.rs +51 -0
  26. data/ext/imprimatur_native/core/src/pdf/font.rs +96 -0
  27. data/ext/imprimatur_native/core/src/pdf/layout/wrap.rs +161 -0
  28. data/ext/imprimatur_native/core/src/pdf/layout.rs +280 -0
  29. data/ext/imprimatur_native/core/src/pdf/metrics.rs +139 -0
  30. data/ext/imprimatur_native/core/src/pdf/objects.rs +141 -0
  31. data/ext/imprimatur_native/core/src/pdf.rs +84 -0
  32. data/ext/imprimatur_native/core/src/read/budget.rs +140 -0
  33. data/ext/imprimatur_native/core/src/read/content.rs +266 -0
  34. data/ext/imprimatur_native/core/src/read/docx/styles.rs +118 -0
  35. data/ext/imprimatur_native/core/src/read/docx.rs +336 -0
  36. data/ext/imprimatur_native/core/src/read/image/dimensions.rs +215 -0
  37. data/ext/imprimatur_native/core/src/read/image.rs +233 -0
  38. data/ext/imprimatur_native/core/src/read/inflate.rs +44 -0
  39. data/ext/imprimatur_native/core/src/read/json.rs +172 -0
  40. data/ext/imprimatur_native/core/src/read/markdown.rs +254 -0
  41. data/ext/imprimatur_native/core/src/read/package.rs +191 -0
  42. data/ext/imprimatur_native/core/src/read/pdf/cmap.rs +216 -0
  43. data/ext/imprimatur_native/core/src/read/pdf/content/geometry.rs +79 -0
  44. data/ext/imprimatur_native/core/src/read/pdf/content/operands.rs +156 -0
  45. data/ext/imprimatur_native/core/src/read/pdf/content.rs +439 -0
  46. data/ext/imprimatur_native/core/src/read/pdf/crypt.rs +256 -0
  47. data/ext/imprimatur_native/core/src/read/pdf/document.rs +420 -0
  48. data/ext/imprimatur_native/core/src/read/pdf/filter.rs +258 -0
  49. data/ext/imprimatur_native/core/src/read/pdf/font.rs +315 -0
  50. data/ext/imprimatur_native/core/src/read/pdf/glyphs.rs +179 -0
  51. data/ext/imprimatur_native/core/src/read/pdf/glyphs.txt +6298 -0
  52. data/ext/imprimatur_native/core/src/read/pdf/images.rs +365 -0
  53. data/ext/imprimatur_native/core/src/read/pdf/layout.rs +451 -0
  54. data/ext/imprimatur_native/core/src/read/pdf/lexer.rs +255 -0
  55. data/ext/imprimatur_native/core/src/read/pdf/object/name.rs +44 -0
  56. data/ext/imprimatur_native/core/src/read/pdf/object.rs +261 -0
  57. data/ext/imprimatur_native/core/src/read/pdf/xref.rs +346 -0
  58. data/ext/imprimatur_native/core/src/read/pdf.rs +53 -0
  59. data/ext/imprimatur_native/core/src/read/span.rs +22 -0
  60. data/ext/imprimatur_native/core/src/read/xlsx/numbers.rs +155 -0
  61. data/ext/imprimatur_native/core/src/read/xlsx.rs +340 -0
  62. data/ext/imprimatur_native/core/src/read.rs +145 -0
  63. data/ext/imprimatur_native/core/src/style.rs +82 -0
  64. data/ext/imprimatur_native/core/src/test_support.rs +163 -0
  65. data/ext/imprimatur_native/core/src/xlsx/cell.rs +112 -0
  66. data/ext/imprimatur_native/core/src/xlsx/comment.rs +256 -0
  67. data/ext/imprimatur_native/core/src/xlsx/date.rs +173 -0
  68. data/ext/imprimatur_native/core/src/xlsx/format.rs +226 -0
  69. data/ext/imprimatur_native/core/src/xlsx/package.rs +268 -0
  70. data/ext/imprimatur_native/core/src/xlsx/range.rs +124 -0
  71. data/ext/imprimatur_native/core/src/xlsx/styles.rs +240 -0
  72. data/ext/imprimatur_native/core/src/xlsx/text.rs +122 -0
  73. data/ext/imprimatur_native/core/src/xlsx/validation.rs +258 -0
  74. data/ext/imprimatur_native/core/src/xlsx/worksheet.rs +350 -0
  75. data/ext/imprimatur_native/core/src/xlsx.rs +120 -0
  76. data/ext/imprimatur_native/core/src/xml/reader.rs +238 -0
  77. data/ext/imprimatur_native/core/src/xml.rs +95 -0
  78. data/ext/imprimatur_native/core/src/zip/reader.rs +144 -0
  79. data/ext/imprimatur_native/core/src/zip.rs +161 -0
  80. data/ext/imprimatur_native/extconf.rb +45 -0
  81. data/ext/imprimatur_native/ffi/Cargo.toml +21 -0
  82. data/ext/imprimatur_native/ffi/src/convert.rs +132 -0
  83. data/ext/imprimatur_native/ffi/src/document.rs +138 -0
  84. data/ext/imprimatur_native/ffi/src/lib.rs +301 -0
  85. data/ext/imprimatur_native/ffi/src/read.rs +182 -0
  86. data/ext/imprimatur_native/ffi/src/style.rs +85 -0
  87. data/ext/imprimatur_native/ffi/src/validation.rs +56 -0
  88. data/lib/imprimatur/document.rb +127 -0
  89. data/lib/imprimatur/error.rb +22 -0
  90. data/lib/imprimatur/formula.rb +13 -0
  91. data/lib/imprimatur/native.rb +17 -0
  92. data/lib/imprimatur/reader.rb +59 -0
  93. data/lib/imprimatur/style.rb +31 -0
  94. data/lib/imprimatur/validation.rb +35 -0
  95. data/lib/imprimatur/version.rb +5 -0
  96. data/lib/imprimatur/xlsx.rb +183 -0
  97. data/lib/imprimatur.rb +111 -0
  98. metadata +157 -0
@@ -0,0 +1,301 @@
1
+ //! Ruby bindings for the imprimatur crate: `Imprimatur::Native::Workbook`
2
+ //! wraps an [`imprimatur::xlsx::Workbook`], `Imprimatur::Style` a
3
+ //! [`imprimatur::xlsx::Format`]. The Ruby DSL in `lib/imprimatur/xlsx.rb`
4
+ //! drives them; sheets cross as indices.
5
+
6
+ mod convert;
7
+ mod document;
8
+ mod read;
9
+ mod style;
10
+ mod validation;
11
+
12
+ use std::cell::{RefCell, RefMut};
13
+
14
+ use imprimatur::style::{Align, TextDirection};
15
+ use imprimatur::xlsx::{CellRange, Comment, Workbook, Worksheet, parse_cell_reference};
16
+ use magnus::{
17
+ Error, ExceptionClass, RArray, RModule, RString, Ruby, TryConvert, Value, function, method,
18
+ prelude::*,
19
+ };
20
+
21
+ use convert::{Classes, to_cell};
22
+ use style::Style;
23
+ use validation::Validation;
24
+
25
+ #[magnus::wrap(class = "Imprimatur::Native::Workbook", free_immediately, size)]
26
+ struct NativeWorkbook(RefCell<Workbook>);
27
+
28
+ impl NativeWorkbook {
29
+ const fn new() -> Self {
30
+ Self(RefCell::new(Workbook::new()))
31
+ }
32
+
33
+ /// Adds a sheet; returns its index for the other methods.
34
+ fn add_sheet(ruby: &Ruby, rb_self: &Self, name: String) -> Result<usize, Error> {
35
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
36
+ book.add_sheet(name).or_raise(ruby)?;
37
+ Ok(book.sheets().len() - 1)
38
+ }
39
+
40
+ /// Appends a row; `nil` leaves its column empty. `style` is `nil`, one
41
+ /// Style for every cell, or an Array of them (or `nil`) by column.
42
+ fn append_row(
43
+ ruby: &Ruby,
44
+ rb_self: &Self,
45
+ sheet: usize,
46
+ cells: RArray,
47
+ style: Value,
48
+ ) -> Result<(), Error> {
49
+ // Convert before borrowing: a Formula's #expression or a Date's
50
+ // #year is Ruby code that could call back into this workbook.
51
+ let classes = Classes::load(ruby)?;
52
+ let mut row = Vec::with_capacity(cells.len());
53
+ for value in cells {
54
+ row.push(to_cell(ruby, value, &classes)?);
55
+ }
56
+ let styles = row_styles(ruby, style, row.len())?;
57
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
58
+ let formats = styles.iter().map(|style| style.map(|style| &style.0));
59
+ let cells = row.into_iter().zip(formats.chain(std::iter::repeat(None)));
60
+ sheet_mut(ruby, &mut book, sheet)?.append_styled_row(cells).or_raise(ruby)?;
61
+ Ok(())
62
+ }
63
+
64
+ /// Sets one cell at a zero-based row and column.
65
+ fn write(
66
+ ruby: &Ruby,
67
+ rb_self: &Self,
68
+ position: (usize, u32, u16),
69
+ value: Value,
70
+ style: Option<&Style>,
71
+ ) -> Result<(), Error> {
72
+ let (sheet, row, column) = position;
73
+ let cell = to_cell(ruby, value, &Classes::load(ruby)?)?
74
+ .ok_or_else(|| argument_error(ruby, "a cell cannot be set to nil"))?;
75
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
76
+ let sheet = sheet_mut(ruby, &mut book, sheet)?;
77
+ let written = match style {
78
+ Some(style) => sheet.write_with_format(row, column, cell, &style.0),
79
+ None => sheet.write(row, column, cell),
80
+ };
81
+ written.or_raise(ruby)?;
82
+ Ok(())
83
+ }
84
+
85
+ fn set_column_width(
86
+ ruby: &Ruby,
87
+ rb_self: &Self,
88
+ sheet: usize,
89
+ column: u16,
90
+ width: f64,
91
+ ) -> Result<(), Error> {
92
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
93
+ sheet_mut(ruby, &mut book, sheet)?.set_column_width(column, width).or_raise(ruby)?;
94
+ Ok(())
95
+ }
96
+
97
+ /// Applies a validation to an A1 range such as `B2:B100`.
98
+ fn add_validation(
99
+ ruby: &Ruby,
100
+ rb_self: &Self,
101
+ sheet: usize,
102
+ range: RString,
103
+ validation: &Validation,
104
+ ) -> Result<(), Error> {
105
+ let range: CellRange = range.to_string()?.parse().or_raise(ruby)?;
106
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
107
+ sheet_mut(ruby, &mut book, sheet)?
108
+ .add_validation(range, validation.0.clone())
109
+ .or_raise(ruby)?;
110
+ Ok(())
111
+ }
112
+
113
+ /// Attaches a comment to the cell at an A1 reference such as `C1`.
114
+ fn set_comment(
115
+ ruby: &Ruby,
116
+ rb_self: &Self,
117
+ (sheet, reference): (usize, String),
118
+ text: String,
119
+ author: Option<String>,
120
+ visible: bool,
121
+ ) -> Result<(), Error> {
122
+ let (row, column) = parse_cell_reference(&reference).or_raise(ruby)?;
123
+ let mut comment = Comment::new(text).or_raise(ruby)?.visible(visible);
124
+ if let Some(author) = author {
125
+ comment = comment.author(author);
126
+ }
127
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
128
+ sheet_mut(ruby, &mut book, sheet)?.set_comment(row, column, comment).or_raise(ruby)?;
129
+ Ok(())
130
+ }
131
+
132
+ fn set_right_to_left(
133
+ ruby: &Ruby,
134
+ rb_self: &Self,
135
+ sheet: usize,
136
+ right_to_left: bool,
137
+ ) -> Result<(), Error> {
138
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
139
+ sheet_mut(ruby, &mut book, sheet)?.set_right_to_left(right_to_left);
140
+ Ok(())
141
+ }
142
+
143
+ /// The zero-based row the next append writes.
144
+ fn next_row(ruby: &Ruby, rb_self: &Self, sheet: usize) -> Result<u32, Error> {
145
+ let mut book = borrow(ruby, &rb_self.0, "workbook")?;
146
+ Ok(sheet_mut(ruby, &mut book, sheet)?.next_row())
147
+ }
148
+
149
+ /// The `.xlsx` package as a binary String, written straight into it.
150
+ fn render(ruby: &Ruby, rb_self: &Self) -> Result<RString, Error> {
151
+ let out = RubyBytes(ruby.str_buf_new(0));
152
+ let book = borrow(ruby, &rb_self.0, "workbook")?;
153
+ Ok(book.write_to(out).or_raise(ruby)?.0)
154
+ }
155
+
156
+ /// Writes the package straight to `path`, without a Ruby String copy.
157
+ fn save(ruby: &Ruby, rb_self: &Self, path: String) -> Result<(), Error> {
158
+ borrow(ruby, &rb_self.0, "workbook")?.save(path).or_raise(ruby)
159
+ }
160
+ }
161
+
162
+ /// `io::Write` into a binary Ruby String: rendered files land in Ruby memory
163
+ /// directly, not in an intermediate buffer that is then copied.
164
+ struct RubyBytes(RString);
165
+
166
+ impl std::io::Write for RubyBytes {
167
+ fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
168
+ self.0.cat(bytes);
169
+ Ok(bytes.len())
170
+ }
171
+
172
+ fn flush(&mut self) -> std::io::Result<()> {
173
+ Ok(())
174
+ }
175
+ }
176
+
177
+ /// Mutable access to a wrapped value; Ruby code that calls back into the
178
+ /// same object mid-call gets a `RuntimeError`, not a panic.
179
+ fn borrow<'a, T>(ruby: &Ruby, cell: &'a RefCell<T>, what: &str) -> Result<RefMut<'a, T>, Error> {
180
+ cell.try_borrow_mut().map_err(|_| {
181
+ Error::new(
182
+ ruby.exception_runtime_error(),
183
+ format!("{what} is already in use (re-entrant call)"),
184
+ )
185
+ })
186
+ }
187
+
188
+ /// A row's styles by column: one Style for all, an Array, or none.
189
+ fn row_styles<'a>(
190
+ ruby: &Ruby,
191
+ style: Value,
192
+ cells: usize,
193
+ ) -> Result<Vec<Option<&'a Style>>, Error> {
194
+ if style.is_nil() {
195
+ return Ok(Vec::new());
196
+ }
197
+ let Some(styles) = RArray::from_value(style) else {
198
+ return Ok(vec![Some(<&Style>::try_convert(style)?); cells]);
199
+ };
200
+ if styles.len() > cells {
201
+ let message = format!("{} styles for a row of {cells} cells", styles.len());
202
+ return Err(argument_error(ruby, message));
203
+ }
204
+ styles.into_iter().map(Option::<&Style>::try_convert).collect()
205
+ }
206
+
207
+ /// The symbols `align:` takes.
208
+ const ALIGNS: [(&str, Align); 3] =
209
+ [("left", Align::Left), ("center", Align::Center), ("right", Align::Right)];
210
+ /// The symbols `direction:` takes.
211
+ const DIRECTIONS: [(&str, TextDirection); 2] =
212
+ [("ltr", TextDirection::LeftToRight), ("rtl", TextDirection::RightToLeft)];
213
+
214
+ fn sheet_mut<'a>(
215
+ ruby: &Ruby,
216
+ book: &'a mut Workbook,
217
+ index: usize,
218
+ ) -> Result<&'a mut Worksheet, Error> {
219
+ book.sheet_mut(index).ok_or_else(|| {
220
+ Error::new(ruby.exception_index_error(), format!("no sheet at index {index}"))
221
+ })
222
+ }
223
+
224
+ /// Imprimatur's errors as the Ruby exceptions they raise.
225
+ trait OrRaise<T> {
226
+ fn or_raise(self, ruby: &Ruby) -> Result<T, Error>;
227
+ }
228
+
229
+ impl<T> OrRaise<T> for imprimatur::Result<T> {
230
+ fn or_raise(self, ruby: &Ruby) -> Result<T, Error> {
231
+ self.map_err(|error| domain_error(ruby, &error))
232
+ }
233
+ }
234
+
235
+ /// An `ArgumentError` with this message.
236
+ fn argument_error(ruby: &Ruby, message: impl Into<std::borrow::Cow<'static, str>>) -> Error {
237
+ Error::new(ruby.exception_arg_error(), message)
238
+ }
239
+
240
+ /// Raises `Imprimatur::Error`, or the subclass of it the error has, all
241
+ /// defined in `lib/imprimatur/error.rb`.
242
+ fn domain_error(ruby: &Ruby, error: &imprimatur::Error) -> Error {
243
+ let name = match error {
244
+ imprimatur::Error::NoTextLayer => "NoTextLayer",
245
+ imprimatur::Error::PasswordRequired => "PasswordRequired",
246
+ imprimatur::Error::UnsupportedDocument(_) => "UnsupportedDocument",
247
+ imprimatur::Error::LimitExceeded(_) | imprimatur::Error::Cancelled => "LimitExceeded",
248
+ _ => "Error",
249
+ };
250
+ let class = ruby
251
+ .class_object()
252
+ .const_get::<_, RModule>("Imprimatur")
253
+ .and_then(|module| module.const_get::<_, ExceptionClass>(name));
254
+ match class {
255
+ Ok(class) => Error::new(class, error.to_string()),
256
+ Err(lookup) => lookup,
257
+ }
258
+ }
259
+
260
+ /// The value for a symbol among `choices`, or an `ArgumentError` naming them.
261
+ fn keyword<T: Copy>(
262
+ ruby: &Ruby,
263
+ name: &str,
264
+ given: &str,
265
+ choices: &[(&str, T)],
266
+ ) -> Result<T, Error> {
267
+ if let Some(&(_, value)) = choices.iter().find(|(symbol, _)| *symbol == given) {
268
+ return Ok(value);
269
+ }
270
+ let expected: Vec<String> = choices.iter().map(|(symbol, _)| format!(":{symbol}")).collect();
271
+ let message = format!("{name} must be one of {}, not :{given}", expected.join(", "));
272
+ Err(argument_error(ruby, message))
273
+ }
274
+
275
+ #[magnus::init]
276
+ fn init(ruby: &Ruby) -> Result<(), Error> {
277
+ let imprimatur = ruby.define_module("Imprimatur")?;
278
+ let style = imprimatur.define_class("Style", ruby.class_object())?;
279
+ style.define_singleton_method("__build", function!(Style::build, 13))?;
280
+ let validation = imprimatur.define_class("Validation", ruby.class_object())?;
281
+ validation.define_singleton_method("__build", function!(Validation::build, 8))?;
282
+
283
+ let native = imprimatur.define_module("Native")?;
284
+ native.define_singleton_method("markdown", function!(read::markdown_native, 4))?;
285
+ native.define_singleton_method("content", function!(read::content_native, 4))?;
286
+ native.define_singleton_method("images", function!(read::images_native, 4))?;
287
+ document::define(ruby, native)?;
288
+ let workbook = native.define_class("Workbook", ruby.class_object())?;
289
+ workbook.define_singleton_method("new", function!(NativeWorkbook::new, 0))?;
290
+ workbook.define_method("add_sheet", method!(NativeWorkbook::add_sheet, 1))?;
291
+ workbook.define_method("append_row", method!(NativeWorkbook::append_row, 3))?;
292
+ workbook.define_method("write", method!(NativeWorkbook::write, 3))?;
293
+ workbook.define_method("set_column_width", method!(NativeWorkbook::set_column_width, 3))?;
294
+ workbook.define_method("add_validation", method!(NativeWorkbook::add_validation, 3))?;
295
+ workbook.define_method("set_comment", method!(NativeWorkbook::set_comment, 4))?;
296
+ workbook.define_method("set_right_to_left", method!(NativeWorkbook::set_right_to_left, 2))?;
297
+ workbook.define_method("next_row", method!(NativeWorkbook::next_row, 1))?;
298
+ workbook.define_method("render", method!(NativeWorkbook::render, 0))?;
299
+ workbook.define_method("save", method!(NativeWorkbook::save, 1))?;
300
+ Ok(())
301
+ }
@@ -0,0 +1,182 @@
1
+ //! `Imprimatur::Native.markdown`, `.content` and `.images`: reading runs
2
+ //! without the GVL, so other Ruby threads (and the fibers of a reactor)
3
+ //! keep running meanwhile, and a `Thread#raise`, `Timeout` or Ctrl-C
4
+ //! cancels it.
5
+
6
+ use std::ffi::c_void;
7
+ use std::panic::{AssertUnwindSafe, catch_unwind};
8
+ use std::sync::Arc;
9
+ use std::sync::atomic::{AtomicBool, Ordering};
10
+ use std::time::Duration;
11
+
12
+ use imprimatur::read::{Limits, content, images, json, markdown};
13
+ use magnus::{Error, IntoValue, RArray, RString, Ruby};
14
+ use serde_json::Value as Json;
15
+
16
+ use crate::{argument_error, domain_error};
17
+
18
+ /// Reads interrupted without anything to raise are retried this many times.
19
+ const MAX_ATTEMPTS: usize = 64;
20
+
21
+ /// A read of a document's bytes within limits, run without the GVL.
22
+ type Job<T> = fn(&[u8], &Limits) -> imprimatur::Result<T>;
23
+
24
+ /// What crosses into the thread without the GVL, and comes back.
25
+ struct Read<T> {
26
+ document: Vec<u8>,
27
+ limits: Limits,
28
+ job: Job<T>,
29
+ result: Option<imprimatur::Result<T>>,
30
+ }
31
+
32
+ /// Called by `Imprimatur.markdown`, which owns the keyword interface.
33
+ /// `timeout` is in seconds; `nil` limits keep their defaults.
34
+ pub fn markdown_native(
35
+ ruby: &Ruby,
36
+ document: RString,
37
+ max_input: Option<usize>,
38
+ max_inflated: Option<u64>,
39
+ timeout: Option<f64>,
40
+ ) -> Result<RString, Error> {
41
+ let text = without_gvl(ruby, document, (max_input, max_inflated, timeout), markdown)?;
42
+ Ok(ruby.str_new(&text))
43
+ }
44
+
45
+ /// Called by `Imprimatur.content`: the structure as JSON, the schema the
46
+ /// command line prints, serialized before the GVL is taken back.
47
+ pub fn content_native(
48
+ ruby: &Ruby,
49
+ document: RString,
50
+ max_input: Option<usize>,
51
+ max_inflated: Option<u64>,
52
+ timeout: Option<f64>,
53
+ ) -> Result<RString, Error> {
54
+ let text = without_gvl(ruby, document, (max_input, max_inflated, timeout), |bytes, limits| {
55
+ content(bytes, limits).map(|content| Json::Object(json::content(&content)).to_string())
56
+ })?;
57
+ Ok(ruby.str_new(&text))
58
+ }
59
+
60
+ /// Called by `Imprimatur.images`: `[data, description]` pairs, the data
61
+ /// binary and the description JSON.
62
+ pub fn images_native(
63
+ ruby: &Ruby,
64
+ document: RString,
65
+ max_input: Option<usize>,
66
+ max_inflated: Option<u64>,
67
+ timeout: Option<f64>,
68
+ ) -> Result<RArray, Error> {
69
+ let found =
70
+ without_gvl(ruby, document, (max_input, max_inflated, timeout), |bytes, limits| {
71
+ let mut found = Vec::new();
72
+ images(bytes, limits, |image| {
73
+ let description = Json::Object(json::image(&image)).to_string();
74
+ found.push((image.data, description));
75
+ Ok(())
76
+ })?;
77
+ Ok(found)
78
+ })?;
79
+ let pairs = ruby.ary_new_capa(found.len());
80
+ for (data, description) in found {
81
+ let pair = [
82
+ ruby.str_from_slice(&data).into_value_with(ruby),
83
+ ruby.str_new(&description).into_value_with(ruby),
84
+ ];
85
+ pairs.push(ruby.ary_new_from_values(&pair))?;
86
+ }
87
+ Ok(pairs)
88
+ }
89
+
90
+ /// The limits `Imprimatur.*`'s keywords set, stopped by `cancel`.
91
+ fn limits(
92
+ ruby: &Ruby,
93
+ cancel: &Arc<AtomicBool>,
94
+ (max_input, max_inflated, timeout): (Option<usize>, Option<u64>, Option<f64>),
95
+ ) -> Result<Limits, Error> {
96
+ let mut limits = Limits::default().cancel_on(Arc::clone(cancel));
97
+ if let Some(bytes) = max_input {
98
+ limits = limits.max_input(bytes);
99
+ }
100
+ if let Some(bytes) = max_inflated {
101
+ limits = limits.max_inflated(bytes);
102
+ }
103
+ if let Some(seconds) = timeout {
104
+ let timeout = Duration::try_from_secs_f64(seconds)
105
+ .map_err(|_| argument_error(ruby, "timeout must be a positive number of seconds"))?;
106
+ limits = limits.timeout(Some(timeout));
107
+ }
108
+ Ok(limits)
109
+ }
110
+
111
+ /// Runs `job` on the document's bytes on this thread, the GVL released.
112
+ fn without_gvl<T>(
113
+ ruby: &Ruby,
114
+ document: RString,
115
+ keywords: (Option<usize>, Option<u64>, Option<f64>),
116
+ job: Job<T>,
117
+ ) -> Result<T, Error> {
118
+ // SAFETY: the bytes are copied out at once, while the GVL is held and
119
+ // before any Ruby code can run and move or free the string.
120
+ #[allow(unsafe_code)]
121
+ let document = unsafe { document.as_slice() }.to_vec();
122
+ let cancel = Arc::new(AtomicBool::new(false));
123
+ let limits = limits(ruby, &cancel, keywords)?;
124
+ let mut read = Read { document, limits, job, result: None };
125
+ let read_pointer: *mut Read<T> = &raw mut read;
126
+ let flag_pointer = Arc::as_ptr(&cancel).cast_mut();
127
+ for _ in 0..MAX_ATTEMPTS {
128
+ // SAFETY: `read` and `cancel` outlive the call, which returns only
129
+ // once `run` has finished (or never started); `run` touches no Ruby
130
+ // object, and `stop` only an atomic. The `2` variant raises no
131
+ // pending interrupt across these Rust frames.
132
+ #[allow(unsafe_code)]
133
+ unsafe {
134
+ rb_sys::rb_thread_call_without_gvl2(
135
+ Some(run::<T>),
136
+ read_pointer.cast(),
137
+ Some(stop),
138
+ flag_pointer.cast(),
139
+ );
140
+ }
141
+ match read.result.take() {
142
+ Some(Ok(value)) => return Ok(value),
143
+ // Cut short by Ruby: raise what Ruby has to raise (Thread#raise,
144
+ // Timeout, Ctrl-C) through the protected check; with nothing to
145
+ // raise, the wakeup was spurious, and the read starts again.
146
+ Some(Err(imprimatur::Error::Cancelled)) if cancel.swap(false, Ordering::Relaxed) => {
147
+ ruby.thread_check_ints()?;
148
+ }
149
+ Some(Err(error)) => return Err(domain_error(ruby, &error)),
150
+ // An interrupt was pending, so the read never started (thread
151
+ // switching sets one too): let Ruby handle it, then start.
152
+ None => ruby.thread_check_ints()?,
153
+ }
154
+ }
155
+ Err(domain_error(ruby, &imprimatur::Error::Cancelled))
156
+ }
157
+
158
+ /// The read itself, on the thread without the GVL. A panic would abort
159
+ /// the process at this C boundary; it becomes an error instead.
160
+ #[allow(unsafe_code)]
161
+ unsafe extern "C" fn run<T>(data: *mut c_void) -> *mut c_void {
162
+ // SAFETY: `data` is the `Read<T>` without_gvl passed, alive and not
163
+ // otherwise borrowed until this returns.
164
+ let read = unsafe { &mut *data.cast::<Read<T>>() };
165
+ let result = catch_unwind(AssertUnwindSafe(|| (read.job)(&read.document, &read.limits)));
166
+ read.result = Some(result.unwrap_or_else(|_| {
167
+ Err(imprimatur::Error::Malformed {
168
+ format: "document",
169
+ reason: "the reader failed on it".to_owned(),
170
+ })
171
+ }));
172
+ std::ptr::null_mut()
173
+ }
174
+
175
+ /// Ruby's unblocking function: asks the read to stop.
176
+ #[allow(unsafe_code)]
177
+ unsafe extern "C" fn stop(data: *mut c_void) {
178
+ // SAFETY: `data` is the cancel flag, kept alive by without_gvl's Arc for
179
+ // the whole blocking region.
180
+ let flag = unsafe { &*data.cast::<AtomicBool>() };
181
+ flag.store(true, Ordering::Relaxed);
182
+ }
@@ -0,0 +1,85 @@
1
+ //! `Imprimatur::Style`: an immutable, validated cell format, built once and
2
+ //! shared by every cell that uses it.
3
+
4
+ use imprimatur::xlsx::{Border, Color, Format, VerticalAlign};
5
+ use magnus::{Error, Ruby};
6
+
7
+ use crate::{ALIGNS, DIRECTIONS, OrRaise, keyword};
8
+
9
+ #[magnus::wrap(class = "Imprimatur::Style", free_immediately, size)]
10
+ pub struct Style(pub Format);
11
+
12
+ impl Style {
13
+ /// Called by `Imprimatur.style`, which owns the keyword interface.
14
+ #[expect(clippy::too_many_arguments, reason = "one positional argument per Ruby keyword")]
15
+ #[expect(clippy::fn_params_excessive_bools, reason = "they arrive as Ruby keywords")]
16
+ pub fn build(
17
+ ruby: &Ruby,
18
+ number_format: Option<String>,
19
+ bold: bool,
20
+ italic: bool,
21
+ underline: bool,
22
+ color: Option<String>,
23
+ fill: Option<String>,
24
+ align: Option<String>,
25
+ direction: Option<String>,
26
+ wrap: bool,
27
+ border: Option<String>,
28
+ font: Option<String>,
29
+ size: Option<f64>,
30
+ valign: Option<String>,
31
+ ) -> Result<Self, Error> {
32
+ let mut format = Format::new();
33
+ if let Some(code) = number_format {
34
+ format = format.number_format(code).or_raise(ruby)?;
35
+ }
36
+ if let Some(font) = font {
37
+ format = format.font_name(font).or_raise(ruby)?;
38
+ }
39
+ if let Some(size) = size {
40
+ format = format.font_size(size).or_raise(ruby)?;
41
+ }
42
+ if let Some(valign) = valign {
43
+ let choices = [
44
+ ("top", VerticalAlign::Top),
45
+ ("center", VerticalAlign::Center),
46
+ ("bottom", VerticalAlign::Bottom),
47
+ ];
48
+ format = format.valign(keyword(ruby, "valign", &valign, &choices)?);
49
+ }
50
+ if bold {
51
+ format = format.bold();
52
+ }
53
+ if italic {
54
+ format = format.italic();
55
+ }
56
+ if underline {
57
+ format = format.underline();
58
+ }
59
+ if wrap {
60
+ format = format.wrap();
61
+ }
62
+ if let Some(color) = color {
63
+ format = format.font_color(parse_color(ruby, &color)?);
64
+ }
65
+ if let Some(fill) = fill {
66
+ format = format.fill(parse_color(ruby, &fill)?);
67
+ }
68
+ if let Some(align) = align {
69
+ format = format.align(keyword(ruby, "align", &align, &ALIGNS)?);
70
+ }
71
+ if let Some(direction) = direction {
72
+ format = format.direction(keyword(ruby, "direction", &direction, &DIRECTIONS)?);
73
+ }
74
+ if let Some(border) = border {
75
+ let choices =
76
+ [("thin", Border::Thin), ("medium", Border::Medium), ("thick", Border::Thick)];
77
+ format = format.border(keyword(ruby, "border", &border, &choices)?);
78
+ }
79
+ Ok(Self(format))
80
+ }
81
+ }
82
+
83
+ fn parse_color(ruby: &Ruby, hex: &str) -> Result<Color, Error> {
84
+ Color::from_hex(hex).or_raise(ruby)
85
+ }
@@ -0,0 +1,56 @@
1
+ //! `Imprimatur::Validation`: an immutable data validation rule, built and
2
+ //! checked once, then applied to any number of ranges.
3
+
4
+ use imprimatur::xlsx::{Alert, Validation as Rule};
5
+ use magnus::{Error, Ruby};
6
+
7
+ use crate::{OrRaise, argument_error, keyword};
8
+
9
+ /// The symbols `alert:` takes.
10
+ const ALERTS: [(&str, Alert); 4] = [
11
+ ("stop", Alert::Stop),
12
+ ("warning", Alert::Warning),
13
+ ("information", Alert::Information),
14
+ ("off", Alert::Off),
15
+ ];
16
+
17
+ #[magnus::wrap(class = "Imprimatur::Validation", free_immediately, size)]
18
+ pub struct Validation(pub Rule);
19
+
20
+ impl Validation {
21
+ /// Called by `Imprimatur.validation`, which owns the keyword interface
22
+ /// and passes exactly one of `values`, `source` and `formula`.
23
+ #[expect(clippy::too_many_arguments, reason = "one positional argument per Ruby keyword")]
24
+ pub fn build(
25
+ ruby: &Ruby,
26
+ values: Option<Vec<String>>,
27
+ source: Option<String>,
28
+ formula: Option<String>,
29
+ allow_blank: bool,
30
+ dropdown: bool,
31
+ alert: Option<String>,
32
+ (prompt_title, prompt): (String, String),
33
+ (error_title, error): (String, String),
34
+ ) -> Result<Self, Error> {
35
+ let rule = match (values, source, formula) {
36
+ (Some(values), None, None) => Rule::list(values),
37
+ (None, Some(source), None) => Rule::list_from(source),
38
+ (None, None, Some(formula)) => Rule::custom(formula),
39
+ _ => {
40
+ let message = "a validation takes one of list: or formula:";
41
+ return Err(argument_error(ruby, message));
42
+ }
43
+ }
44
+ .or_raise(ruby)?;
45
+ let mut rule = rule
46
+ .allow_blank(allow_blank)
47
+ .dropdown(dropdown)
48
+ .prompt(prompt_title, prompt)
49
+ .and_then(|rule| rule.error(error_title, error))
50
+ .or_raise(ruby)?;
51
+ if let Some(alert) = alert {
52
+ rule = rule.alert(keyword(ruby, "alert", &alert, &ALERTS)?);
53
+ }
54
+ Ok(Self(rule))
55
+ }
56
+ }