fontisan 0.4.46 → 0.4.48
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/BENCHMARKS.md +56 -0
- data/CHANGELOG.md +12 -0
- data/Cargo.lock +550 -0
- data/Cargo.toml +12 -0
- data/Gemfile +10 -0
- data/README.adoc +52 -0
- data/Rakefile +5 -1
- data/benchmark/baseline.json +5 -0
- data/benchmark/ci_gate.rb +217 -0
- data/benchmark/corpus/harness.rb +175 -0
- data/benchmark/corpus/ops.rb +150 -0
- data/benchmark/corpus/run.rb +182 -0
- data/benchmark/corpus/scan_child.rb +8 -0
- data/benchmark/corpus/tiers.rb +176 -0
- data/benchmark/tier_baseline.json +12 -0
- data/docs/.vitepress/config.ts +2 -1
- data/docs/guide/engines.md +117 -0
- data/docs/reference-facade.md +130 -0
- data/ext/fontisan_cdylib/Cargo.lock +313 -0
- data/ext/fontisan_cdylib/Cargo.toml +18 -0
- data/ext/fontisan_cdylib/build.rs +22 -0
- data/ext/fontisan_cdylib/extconf.rb +36 -0
- data/ext/fontisan_cdylib/src/lib.rs +13 -0
- data/ext/fontisan_native/Cargo.toml +21 -0
- data/ext/fontisan_native/build.rs +22 -0
- data/ext/fontisan_native/clippy.toml +10 -0
- data/ext/fontisan_native/extconf.rb +59 -0
- data/ext/fontisan_native/src/lib.rs +463 -0
- data/lib/fontisan/audit/checks/cmap_check.rb +7 -0
- data/lib/fontisan/audit/checks/hinting_check.rb +1 -1
- data/lib/fontisan/audit/checks/maxp_check.rb +1 -1
- data/lib/fontisan/audit/checks/opentype_conformance_check.rb +5 -2
- data/lib/fontisan/base_collection.rb +30 -0
- data/lib/fontisan/byte_ops.rb +219 -0
- data/lib/fontisan/cli.rb +5 -5
- data/lib/fontisan/commands/audit_command.rb +38 -43
- data/lib/fontisan/commands/convert_command.rb +53 -0
- data/lib/fontisan/commands/instance_command.rb +75 -0
- data/lib/fontisan/commands/subset_command.rb +43 -0
- data/lib/fontisan/commands/unpack_command.rb +56 -0
- data/lib/fontisan/converters/cff_table_builder.rb +1 -1
- data/lib/fontisan/converters/collection_converter.rb +13 -5
- data/lib/fontisan/converters/format_converter.rb +1 -1
- data/lib/fontisan/converters/woff2_encoder.rb +8 -6
- data/lib/fontisan/converters/woff_writer.rb +6 -5
- data/lib/fontisan/engine.rb +350 -0
- data/lib/fontisan/ffi/client.rb +121 -0
- data/lib/fontisan/ffi.rb +343 -0
- data/lib/fontisan/ffi_platform.rb +24 -0
- data/lib/fontisan/font_loader.rb +5 -1
- data/lib/fontisan/font_writer.rb +13 -1
- data/lib/fontisan/native.rb +231 -0
- data/lib/fontisan/pipeline/transformation_pipeline.rb +10 -8
- data/lib/fontisan/pipeline.rb +1 -1
- data/lib/fontisan/reference/table_digests.rb +251 -0
- data/lib/fontisan/reference.rb +619 -0
- data/lib/fontisan/scan.rb +372 -0
- data/lib/fontisan/sfnt_directory.rb +41 -0
- data/lib/fontisan/sfnt_font.rb +16 -6
- data/lib/fontisan/subset/builder.rb +2 -1
- data/lib/fontisan/tables/cff/charstring.rb +16 -0
- data/lib/fontisan/tables/cff.rb +15 -5
- data/lib/fontisan/tables/cmap.rb +11 -1
- data/lib/fontisan/tables/glyf.rb +4 -6
- data/lib/fontisan/tables/gpos.rb +7 -0
- data/lib/fontisan/tables/gsub.rb +7 -0
- data/lib/fontisan/tables/layout_common.rb +172 -0
- data/lib/fontisan/tables/name.rb +10 -4
- data/lib/fontisan/utilities/brotli_wrapper.rb +39 -4
- data/lib/fontisan/validators/opentype_validator.rb +15 -3
- data/lib/fontisan/variable/instancer.rb +23 -192
- data/lib/fontisan/variable/static_font_builder.rb +4 -1
- data/lib/fontisan/variable/table_updater.rb +4 -2
- data/lib/fontisan/variation/instance_generator.rb +3 -149
- data/lib/fontisan/variation/instancing/axis_space.rb +136 -0
- data/lib/fontisan/variation/instancing/glyph_outline.rb +204 -0
- data/lib/fontisan/variation/instancing/gvar_reader.rb +146 -0
- data/lib/fontisan/variation/instancing/item_variation_store.rb +92 -0
- data/lib/fontisan/variation/instancing/name_rewriter.rb +90 -0
- data/lib/fontisan/variation/instancing/true_type_instancer.rb +326 -0
- data/lib/fontisan/variation/instancing.rb +20 -0
- data/lib/fontisan/variation/variable_svg_generator.rb +19 -1
- data/lib/fontisan/variation.rb +1 -0
- data/lib/fontisan/version.rb +1 -1
- data/lib/fontisan/woff2/collection_decoder.rb +15 -1
- data/lib/fontisan/woff2/collection_encoder.rb +29 -6
- data/lib/fontisan/woff2/encoder_rules.rb +12 -4
- data/lib/fontisan/woff2/glyf_loca_reconstruct.rb +1 -1
- data/lib/fontisan/woff2_collection.rb +7 -5
- data/lib/fontisan/woff2_font.rb +12 -3
- data/lib/fontisan/woff_container.rb +83 -0
- data/lib/fontisan/woff_font.rb +24 -20
- data/lib/fontisan.rb +24 -1
- data/rakelib/bench.rake +9 -0
- data/rakelib/cdylib.rake +39 -0
- data/rakelib/corpus.rake +272 -0
- data/rakelib/corpus_convert.rake +43 -0
- data/rakelib/corpus_goldens.rake +22 -0
- data/rakelib/corpus_malformed.rake +20 -0
- data/rakelib/corpus_scan.rake +24 -0
- data/rakelib/java.rake +61 -0
- data/rakelib/native.rake +43 -0
- data/scripts/build-woff2.sh +57 -0
- data/scripts/corpus/generate_convert_goldens.rb +478 -0
- data/scripts/corpus/generate_goldens.rb +282 -0
- data/scripts/corpus/generate_malformed_goldens.rb +254 -0
- data/scripts/corpus/generate_scan_goldens.rb +98 -0
- data/scripts/tier_fallback.rb +219 -0
- data/scripts/tier_smoke.rb +64 -0
- metadata +75 -3
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
//! The `:native` tier of the fontisan gem: the fontisan-rs engine through magnus.
|
|
2
|
+
//!
|
|
3
|
+
//! Division of labour (decided here, see `lib/fontisan/native.rb`):
|
|
4
|
+
//! * Rust walks the engine's `Json` documents straight into Ruby Hash / Array /
|
|
5
|
+
//! String / Integer objects. No JSON text is rendered or parsed on either side,
|
|
6
|
+
//! so each batch row is decoded exactly once. Hash keys are inserted bytewise
|
|
7
|
+
//! sorted, like `Reference.normalize`.
|
|
8
|
+
//! * The GVL is released around every engine call. Nothing inside the released
|
|
9
|
+
//! section touches Ruby: inputs are copied into owned Rust values first and
|
|
10
|
+
//! the result is plain Rust data until the GVL is back.
|
|
11
|
+
//! * A failure is raised as `Fontisan::Native::Failure` carrying the engine's
|
|
12
|
+
//! `[class, message, [[cause class, cause message], ...]]`; the Ruby side
|
|
13
|
+
//! rebuilds the Ruby tier's exception class and cause chain from it. A Rust
|
|
14
|
+
//! panic is caught inside the released section and raised as
|
|
15
|
+
//! `Fontisan::Native::Panic`.
|
|
16
|
+
use fontisan::audit;
|
|
17
|
+
use fontisan::convert::{self, Target};
|
|
18
|
+
use fontisan::json::Json;
|
|
19
|
+
use fontisan::read;
|
|
20
|
+
use fontisan::scan::{self, RowStatus};
|
|
21
|
+
use fontisan::sfnt::{self, Format, LoadingMode, Options};
|
|
22
|
+
use fontisan::{instance, subset};
|
|
23
|
+
use magnus::value::ReprValue;
|
|
24
|
+
use magnus::{function, prelude::*, Error, IntoValue, RArray, RModule, Ruby, Value};
|
|
25
|
+
use std::ffi::c_void;
|
|
26
|
+
use std::panic::{catch_unwind, AssertUnwindSafe};
|
|
27
|
+
use std::path::Path;
|
|
28
|
+
|
|
29
|
+
/// Bumped on any change to the functions below. `lib/fontisan/native.rb` refuses a
|
|
30
|
+
/// mismatch (then the tier is simply unavailable).
|
|
31
|
+
const ABI_VERSION: u32 = 1;
|
|
32
|
+
|
|
33
|
+
/// What the engine reported, before it becomes a Ruby exception.
|
|
34
|
+
enum Fail {
|
|
35
|
+
Engine {
|
|
36
|
+
class: String,
|
|
37
|
+
message: String,
|
|
38
|
+
causes: Vec<(String, String)>,
|
|
39
|
+
},
|
|
40
|
+
Panic(String),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
impl Fail {
|
|
44
|
+
fn simple(class: &str, message: impl Into<String>) -> Self {
|
|
45
|
+
Self::Engine {
|
|
46
|
+
class: class.to_string(),
|
|
47
|
+
message: message.into(),
|
|
48
|
+
causes: Vec::new(),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// From an engine `{class, message, causes}` document (`read::error_json`,
|
|
53
|
+
/// `audit::command_error_json`).
|
|
54
|
+
fn from_doc(doc: &Json) -> Self {
|
|
55
|
+
let Json::Obj(members) = doc else {
|
|
56
|
+
return Self::simple("Fontisan::Error", "unreadable engine error");
|
|
57
|
+
};
|
|
58
|
+
let text = |key: &str| match members.iter().find(|(k, _)| *k == key) {
|
|
59
|
+
Some((_, Json::Str(s))) => s.clone(),
|
|
60
|
+
_ => String::new(),
|
|
61
|
+
};
|
|
62
|
+
let causes = match members.iter().find(|(k, _)| *k == "causes") {
|
|
63
|
+
Some((_, Json::Arr(items))) => items
|
|
64
|
+
.iter()
|
|
65
|
+
.filter_map(|item| match item {
|
|
66
|
+
Json::Obj(m) => {
|
|
67
|
+
let get = |key: &str| match m.iter().find(|(k, _)| *k == key) {
|
|
68
|
+
Some((_, Json::Str(s))) => s.clone(),
|
|
69
|
+
_ => String::new(),
|
|
70
|
+
};
|
|
71
|
+
Some((get("class"), get("message")))
|
|
72
|
+
}
|
|
73
|
+
_ => None,
|
|
74
|
+
})
|
|
75
|
+
.collect(),
|
|
76
|
+
_ => Vec::new(),
|
|
77
|
+
};
|
|
78
|
+
Self::Engine {
|
|
79
|
+
class: text("class"),
|
|
80
|
+
message: text("message"),
|
|
81
|
+
causes,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
type Outcome<T> = Result<T, Fail>;
|
|
87
|
+
|
|
88
|
+
fn panic_text(payload: &(dyn std::any::Any + Send)) -> String {
|
|
89
|
+
if let Some(s) = payload.downcast_ref::<&str>() {
|
|
90
|
+
(*s).to_string()
|
|
91
|
+
} else if let Some(s) = payload.downcast_ref::<String>() {
|
|
92
|
+
s.clone()
|
|
93
|
+
} else {
|
|
94
|
+
"non-string panic payload".to_string()
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// Run `work` with the GVL released. `work` must not touch Ruby. A panic in it
|
|
99
|
+
/// is caught here, inside the released section, and returned as [`Fail::Panic`].
|
|
100
|
+
fn nogvl<T, F>(work: F) -> Outcome<T>
|
|
101
|
+
where
|
|
102
|
+
F: FnOnce() -> Outcome<T>,
|
|
103
|
+
{
|
|
104
|
+
struct Slot<F, T> {
|
|
105
|
+
work: Option<F>,
|
|
106
|
+
out: Option<Outcome<T>>,
|
|
107
|
+
}
|
|
108
|
+
unsafe extern "C" fn run<T, F: FnOnce() -> Outcome<T>>(data: *mut c_void) -> *mut c_void {
|
|
109
|
+
// SAFETY: `data` is the `Slot` on the caller's stack below; the call
|
|
110
|
+
// blocks until this function returns, so it outlives the call.
|
|
111
|
+
let slot = unsafe { &mut *data.cast::<Slot<F, T>>() };
|
|
112
|
+
let work = slot.work.take().expect("work runs once");
|
|
113
|
+
slot.out = Some(
|
|
114
|
+
catch_unwind(AssertUnwindSafe(work))
|
|
115
|
+
.unwrap_or_else(|p| Err(Fail::Panic(panic_text(&*p)))),
|
|
116
|
+
);
|
|
117
|
+
std::ptr::null_mut()
|
|
118
|
+
}
|
|
119
|
+
let mut slot = Slot {
|
|
120
|
+
work: Some(work),
|
|
121
|
+
out: None,
|
|
122
|
+
};
|
|
123
|
+
// SAFETY: `run::<T, F>` matches the callback type and only touches `slot`.
|
|
124
|
+
unsafe {
|
|
125
|
+
rb_sys::rb_thread_call_without_gvl(
|
|
126
|
+
Some(run::<T, F>),
|
|
127
|
+
std::ptr::addr_of_mut!(slot).cast(),
|
|
128
|
+
None,
|
|
129
|
+
std::ptr::null_mut(),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
slot.out.expect("the callback always stores an outcome")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
fn module(ruby: &Ruby) -> Result<RModule, Error> {
|
|
136
|
+
let fontisan = ruby.define_module("Fontisan")?;
|
|
137
|
+
fontisan.define_module("Native")
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// Raise `Fontisan::Native::Failure` / `Panic` for a [`Fail`].
|
|
141
|
+
fn raise(ruby: &Ruby, fail: Fail) -> Error {
|
|
142
|
+
let result = (|| -> Result<Error, Error> {
|
|
143
|
+
let native = module(ruby)?;
|
|
144
|
+
match fail {
|
|
145
|
+
Fail::Panic(text) => {
|
|
146
|
+
let class: magnus::ExceptionClass = native.const_get("Panic")?;
|
|
147
|
+
Ok(Error::new(class, format!("engine panic: {text}")))
|
|
148
|
+
}
|
|
149
|
+
Fail::Engine {
|
|
150
|
+
class,
|
|
151
|
+
message,
|
|
152
|
+
causes,
|
|
153
|
+
} => {
|
|
154
|
+
let klass: magnus::ExceptionClass = native.const_get("Failure")?;
|
|
155
|
+
let exc: Value = klass.funcall("new", (message.as_str(),))?;
|
|
156
|
+
let causes_rb = ruby.ary_new_capa(causes.len());
|
|
157
|
+
for (c, m) in &causes {
|
|
158
|
+
let pair = ruby.ary_new_capa(2);
|
|
159
|
+
pair.push(ruby.str_new(c))?;
|
|
160
|
+
pair.push(ruby.str_new(m))?;
|
|
161
|
+
causes_rb.push(pair)?;
|
|
162
|
+
}
|
|
163
|
+
let details = ruby.ary_new_capa(3);
|
|
164
|
+
details.push(ruby.str_new(&class))?;
|
|
165
|
+
details.push(ruby.str_new(&message))?;
|
|
166
|
+
details.push(causes_rb)?;
|
|
167
|
+
exc.funcall::<_, _, Value>("instance_variable_set", ("@details", details))?;
|
|
168
|
+
let exception = magnus::Exception::from_value(exc)
|
|
169
|
+
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "bad failure"))?;
|
|
170
|
+
Ok(Error::from(exception))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
})();
|
|
174
|
+
result.unwrap_or_else(|e| e)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
fn finish<T>(ruby: &Ruby, outcome: Outcome<T>) -> Result<T, Error> {
|
|
178
|
+
outcome.map_err(|fail| raise(ruby, fail))
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Engine document -> Ruby objects, keys bytewise sorted.
|
|
182
|
+
fn to_value(ruby: &Ruby, json: &Json) -> Result<Value, Error> {
|
|
183
|
+
Ok(match json {
|
|
184
|
+
Json::Null => ruby.qnil().as_value(),
|
|
185
|
+
Json::Bool(true) => ruby.qtrue().as_value(),
|
|
186
|
+
Json::Bool(false) => ruby.qfalse().as_value(),
|
|
187
|
+
Json::Int(i) => i.into_value_with(ruby),
|
|
188
|
+
Json::Str(s) => ruby.str_new(s).as_value(),
|
|
189
|
+
Json::Arr(items) => {
|
|
190
|
+
let array = ruby.ary_new_capa(items.len());
|
|
191
|
+
for item in items {
|
|
192
|
+
array.push(to_value(ruby, item)?)?;
|
|
193
|
+
}
|
|
194
|
+
array.as_value()
|
|
195
|
+
}
|
|
196
|
+
Json::Obj(members) => {
|
|
197
|
+
hash(ruby, members.iter().map(|(k, v)| (*k, v)).collect())?
|
|
198
|
+
}
|
|
199
|
+
Json::Map(members) => {
|
|
200
|
+
hash(ruby, members.iter().map(|(k, v)| (k.as_str(), v)).collect())?
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
fn hash(ruby: &Ruby, mut members: Vec<(&str, &Json)>) -> Result<Value, Error> {
|
|
206
|
+
members.sort_by(|a, b| a.0.as_bytes().cmp(b.0.as_bytes()));
|
|
207
|
+
let out = ruby.hash_new();
|
|
208
|
+
for (key, value) in members {
|
|
209
|
+
out.aset(ruby.str_new(key), to_value(ruby, value)?)?;
|
|
210
|
+
}
|
|
211
|
+
Ok(out.as_value())
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fn rows(ruby: &Ruby, docs: &[Json]) -> Result<RArray, Error> {
|
|
215
|
+
let array = ruby.ary_new_capa(docs.len());
|
|
216
|
+
for doc in docs {
|
|
217
|
+
array.push(to_value(ruby, doc)?)?;
|
|
218
|
+
}
|
|
219
|
+
Ok(array)
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn read_file(path: &str) -> Outcome<Vec<u8>> {
|
|
223
|
+
std::fs::read(path).map_err(|e| {
|
|
224
|
+
let class = if e.kind() == std::io::ErrorKind::NotFound {
|
|
225
|
+
"Errno::ENOENT"
|
|
226
|
+
} else {
|
|
227
|
+
"IOError"
|
|
228
|
+
};
|
|
229
|
+
Fail::simple(class, format!("{e} - {path}"))
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
fn bytes_value(ruby: &Ruby, bytes: &[u8]) -> Value {
|
|
234
|
+
let s = ruby.str_from_slice(bytes);
|
|
235
|
+
// A binary String, like the Ruby tier's `FontWriter` output.
|
|
236
|
+
let _ = s.funcall::<_, _, Value>("force_encoding", ("BINARY",));
|
|
237
|
+
s.as_value()
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// --- read-type ops -------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
fn native_read(ruby: &Ruby, path: String, metadata: bool, lazy: bool) -> Result<RArray, Error> {
|
|
243
|
+
let options = Options {
|
|
244
|
+
mode: if metadata {
|
|
245
|
+
LoadingMode::Metadata
|
|
246
|
+
} else {
|
|
247
|
+
LoadingMode::Full
|
|
248
|
+
},
|
|
249
|
+
lazy,
|
|
250
|
+
};
|
|
251
|
+
let docs = finish(
|
|
252
|
+
ruby,
|
|
253
|
+
nogvl(move || {
|
|
254
|
+
let bytes = read_file(&path)?;
|
|
255
|
+
read::read(&bytes, options).map_err(|e| Fail::from_doc(&read::error_json(&e)))
|
|
256
|
+
}),
|
|
257
|
+
)?;
|
|
258
|
+
rows(ruby, &docs)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
fn native_audit(ruby: &Ruby, path: String, validate: bool) -> Result<RArray, Error> {
|
|
262
|
+
let docs = finish(
|
|
263
|
+
ruby,
|
|
264
|
+
nogvl(move || {
|
|
265
|
+
let bytes = read_file(&path)?;
|
|
266
|
+
let name = Path::new(&path)
|
|
267
|
+
.file_name()
|
|
268
|
+
.map_or_else(|| path.clone(), |n| n.to_string_lossy().into_owned());
|
|
269
|
+
let reports = audit::audit_staged(&bytes, &name, validate)
|
|
270
|
+
.map_err(|(e, stage)| Fail::from_doc(&audit::command_error_json(&e, stage)))?;
|
|
271
|
+
Ok(reports.iter().map(audit::Report::to_json).collect::<Vec<_>>())
|
|
272
|
+
}),
|
|
273
|
+
)?;
|
|
274
|
+
rows(ruby, &docs)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/// Scan rows of one file (no "status" key on supported rows, like the Ruby tier).
|
|
278
|
+
fn native_scan(ruby: &Ruby, path: String) -> Result<RArray, Error> {
|
|
279
|
+
let docs = finish(
|
|
280
|
+
ruby,
|
|
281
|
+
nogvl(move || {
|
|
282
|
+
Ok(scan::scan_path(Path::new(&path))
|
|
283
|
+
.iter()
|
|
284
|
+
.map(|row| match (row.status, row.to_json()) {
|
|
285
|
+
(RowStatus::Ok, Json::Obj(members)) => {
|
|
286
|
+
Json::Obj(members.into_iter().filter(|(k, _)| *k != "status").collect())
|
|
287
|
+
}
|
|
288
|
+
(_, doc) => doc,
|
|
289
|
+
})
|
|
290
|
+
.collect::<Vec<_>>())
|
|
291
|
+
}),
|
|
292
|
+
)?;
|
|
293
|
+
rows(ruby, &docs)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// One engine row per font file below `dir` (see `audit::dir_row`).
|
|
297
|
+
fn native_audit_dir_rows(ruby: &Ruby, dir: String, validate: bool) -> Result<RArray, Error> {
|
|
298
|
+
let docs = finish(
|
|
299
|
+
ruby,
|
|
300
|
+
nogvl(move || {
|
|
301
|
+
let files = scan::font_files(Path::new(&dir))
|
|
302
|
+
.map_err(|e| Fail::simple("IOError", format!("cannot read {dir}: {e}")))?;
|
|
303
|
+
Ok(files
|
|
304
|
+
.iter()
|
|
305
|
+
.map(|file| audit::dir_row(file, validate))
|
|
306
|
+
.collect::<Vec<_>>())
|
|
307
|
+
}),
|
|
308
|
+
)?;
|
|
309
|
+
rows(ruby, &docs)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/// Whether the engine reads the file itself: false for a file that is gone, a
|
|
313
|
+
/// directory, dfont and Type 1 (the Ruby tier serves those).
|
|
314
|
+
fn native_supports_file(ruby: &Ruby, path: String) -> Result<bool, Error> {
|
|
315
|
+
finish(
|
|
316
|
+
ruby,
|
|
317
|
+
nogvl(move || {
|
|
318
|
+
use std::io::Read;
|
|
319
|
+
let Ok(file) = std::fs::File::open(&path) else {
|
|
320
|
+
return Ok(false);
|
|
321
|
+
};
|
|
322
|
+
let mut head = Vec::new();
|
|
323
|
+
if file.take(256).read_to_end(&mut head).is_err() {
|
|
324
|
+
return Ok(false);
|
|
325
|
+
}
|
|
326
|
+
Ok(!matches!(
|
|
327
|
+
sfnt::detect_format(&head),
|
|
328
|
+
Some(Format::Dfont | Format::Pfa | Format::Pfb)
|
|
329
|
+
))
|
|
330
|
+
}),
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// --- byte ops (path based; the Ruby gates for these are card 2f) ----------
|
|
335
|
+
|
|
336
|
+
fn convert_failed(e: &convert::ConvertError) -> Fail {
|
|
337
|
+
Fail::simple(e.class, e.message.clone())
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
fn native_convert(ruby: &Ruby, path: String, target: u32, timestamp: i64) -> Result<Value, Error> {
|
|
341
|
+
let bytes = finish(
|
|
342
|
+
ruby,
|
|
343
|
+
nogvl(move || {
|
|
344
|
+
let target = Target::from_code(target)
|
|
345
|
+
.ok_or_else(|| Fail::simple("ArgumentError", format!("unknown target {target}")))?;
|
|
346
|
+
let input = read_file(&path)?;
|
|
347
|
+
convert::convert(&input, target, timestamp).map_err(|e| convert_failed(&e))
|
|
348
|
+
}),
|
|
349
|
+
)?;
|
|
350
|
+
Ok(bytes_value(ruby, &bytes))
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/// `[basename, bytes]` of one collection member.
|
|
354
|
+
fn native_extract(ruby: &Ruby, path: String, index: usize) -> Result<RArray, Error> {
|
|
355
|
+
let (name, data) = finish(
|
|
356
|
+
ruby,
|
|
357
|
+
nogvl(move || {
|
|
358
|
+
let input = read_file(&path)?;
|
|
359
|
+
let x = convert::extract(&input, index).map_err(|e| convert_failed(&e))?;
|
|
360
|
+
Ok((x.basename, x.data))
|
|
361
|
+
}),
|
|
362
|
+
)?;
|
|
363
|
+
let out = ruby.ary_new_capa(2);
|
|
364
|
+
out.push(ruby.str_new(&name))?;
|
|
365
|
+
out.push(bytes_value(ruby, &data))?;
|
|
366
|
+
Ok(out)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/// `coords`: `[[tag, value], ...]` with a 4-byte tag.
|
|
370
|
+
fn native_instance(ruby: &Ruby, path: String, coords: Vec<(String, f64)>) -> Result<Value, Error> {
|
|
371
|
+
let bytes = finish(
|
|
372
|
+
ruby,
|
|
373
|
+
nogvl(move || {
|
|
374
|
+
let mut axes = Vec::with_capacity(coords.len());
|
|
375
|
+
for (tag, value) in &coords {
|
|
376
|
+
let tag: [u8; 4] = tag
|
|
377
|
+
.as_bytes()
|
|
378
|
+
.try_into()
|
|
379
|
+
.map_err(|_| Fail::simple("ArgumentError", format!("bad axis tag {tag:?}")))?;
|
|
380
|
+
axes.push((tag, *value));
|
|
381
|
+
}
|
|
382
|
+
let input = read_file(&path)?;
|
|
383
|
+
instance::instance(&input, &axes).map_err(|e| convert_failed(&e))
|
|
384
|
+
}),
|
|
385
|
+
)?;
|
|
386
|
+
Ok(bytes_value(ruby, &bytes))
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
fn native_subset(ruby: &Ruby, path: String, gids: Vec<u32>, profile: String) -> Result<Value, Error> {
|
|
390
|
+
let bytes = finish(
|
|
391
|
+
ruby,
|
|
392
|
+
nogvl(move || {
|
|
393
|
+
let input = read_file(&path)?;
|
|
394
|
+
subset::subset(&input, &gids, &profile)
|
|
395
|
+
.map_err(|e| Fail::simple(e.class, e.message.clone()))
|
|
396
|
+
}),
|
|
397
|
+
)?;
|
|
398
|
+
Ok(bytes_value(ruby, &bytes))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/// Glyph ids kept for `codepoints` (the facade's `gids_for`); `name` is the
|
|
402
|
+
/// file's basename, used in the "None of the requested characters" error.
|
|
403
|
+
fn native_glyph_ids(ruby: &Ruby, path: String, codepoints: Vec<u32>, name: String) -> Result<Vec<u32>, Error> {
|
|
404
|
+
finish(
|
|
405
|
+
ruby,
|
|
406
|
+
nogvl(move || {
|
|
407
|
+
let input = read_file(&path)?;
|
|
408
|
+
subset::glyph_ids_for(&input, &codepoints, &name)
|
|
409
|
+
.map_err(|e| Fail::simple(e.class, e.message.clone()))
|
|
410
|
+
}),
|
|
411
|
+
)
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// --- probes --------------------------------------------------------------
|
|
415
|
+
|
|
416
|
+
/// Seeded failure for the specs: panics inside the released section.
|
|
417
|
+
fn native_selftest_panic(ruby: &Ruby) -> Result<(), Error> {
|
|
418
|
+
finish(
|
|
419
|
+
ruby,
|
|
420
|
+
nogvl(|| -> Outcome<()> { panic!("seeded panic (selftest)") }),
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/// Holds the engine call open for `millis` with the GVL released, so a spec can
|
|
425
|
+
/// prove another Ruby thread keeps running (the sleep stands in for a long scan).
|
|
426
|
+
fn native_selftest_block(ruby: &Ruby, millis: u64) -> Result<(), Error> {
|
|
427
|
+
finish(
|
|
428
|
+
ruby,
|
|
429
|
+
nogvl(move || {
|
|
430
|
+
std::thread::sleep(std::time::Duration::from_millis(millis));
|
|
431
|
+
Ok(())
|
|
432
|
+
}),
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
fn extension_abi_version() -> u32 {
|
|
437
|
+
ABI_VERSION
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/// The engine rev this build was compiled against (see `build.rs`).
|
|
441
|
+
fn extension_engine_rev() -> &'static str {
|
|
442
|
+
env!("FONTISAN_ENGINE_REV")
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[magnus::init]
|
|
446
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
447
|
+
let native = module(ruby)?;
|
|
448
|
+
native.define_singleton_method("extension_abi_version", function!(extension_abi_version, 0))?;
|
|
449
|
+
native.define_singleton_method("extension_engine_rev", function!(extension_engine_rev, 0))?;
|
|
450
|
+
native.define_singleton_method("native_read", function!(native_read, 3))?;
|
|
451
|
+
native.define_singleton_method("native_audit", function!(native_audit, 2))?;
|
|
452
|
+
native.define_singleton_method("native_scan", function!(native_scan, 1))?;
|
|
453
|
+
native.define_singleton_method("native_audit_dir_rows", function!(native_audit_dir_rows, 2))?;
|
|
454
|
+
native.define_singleton_method("native_supports_file", function!(native_supports_file, 1))?;
|
|
455
|
+
native.define_singleton_method("native_convert", function!(native_convert, 3))?;
|
|
456
|
+
native.define_singleton_method("native_extract", function!(native_extract, 2))?;
|
|
457
|
+
native.define_singleton_method("native_instance", function!(native_instance, 2))?;
|
|
458
|
+
native.define_singleton_method("native_subset", function!(native_subset, 3))?;
|
|
459
|
+
native.define_singleton_method("native_glyph_ids", function!(native_glyph_ids, 3))?;
|
|
460
|
+
native.define_singleton_method("native_selftest_panic", function!(native_selftest_panic, 0))?;
|
|
461
|
+
native.define_singleton_method("native_selftest_block", function!(native_selftest_block, 1))?;
|
|
462
|
+
Ok(())
|
|
463
|
+
}
|
|
@@ -46,6 +46,13 @@ module Fontisan
|
|
|
46
46
|
|
|
47
47
|
def self.validate_header(raw)
|
|
48
48
|
issues = []
|
|
49
|
+
if raw.bytesize < 4
|
|
50
|
+
return [issue(severity: :error,
|
|
51
|
+
message: "cmap table is #{raw.bytesize} bytes, " \
|
|
52
|
+
"shorter than its 4-byte header",
|
|
53
|
+
location: "cmap.header")]
|
|
54
|
+
end
|
|
55
|
+
|
|
49
56
|
version, num_tables = raw.unpack("nn")
|
|
50
57
|
if version != 0
|
|
51
58
|
issues << issue(severity: :error,
|
|
@@ -43,7 +43,7 @@ module Fontisan
|
|
|
43
43
|
issues = []
|
|
44
44
|
has_fpgm = font.has_table?("fpgm")
|
|
45
45
|
has_prep = font.has_table?("prep")
|
|
46
|
-
has_cvt = font.has_table?("cvt")
|
|
46
|
+
has_cvt = font.has_table?("cvt ")
|
|
47
47
|
has_glyf_data = table_has_data?(font, "glyf")
|
|
48
48
|
|
|
49
49
|
if has_fpgm && has_glyf_data
|
|
@@ -26,7 +26,7 @@ module Fontisan
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
def self.validate_version(maxp, font)
|
|
29
|
-
version_raw = maxp.version_raw
|
|
29
|
+
version_raw = maxp.version_raw.to_i
|
|
30
30
|
is_truetype = font.has_table?("glyf")
|
|
31
31
|
tt_version = 0x00010000 # 1.0 as Fixed 16.16
|
|
32
32
|
cff_version = 0x00005000 # 0.5 as Fixed 16.16
|
|
@@ -65,7 +65,8 @@ module Fontisan
|
|
|
65
65
|
|
|
66
66
|
head = font.table("head")
|
|
67
67
|
loca = font.table("loca")
|
|
68
|
-
|
|
68
|
+
loca_entry = font.tables.find { |entry| entry.tag == "loca" }
|
|
69
|
+
return [] unless head && loca && loca_entry
|
|
69
70
|
|
|
70
71
|
format = head.index_to_loc_format
|
|
71
72
|
maxp = font.table("maxp")
|
|
@@ -73,7 +74,9 @@ module Fontisan
|
|
|
73
74
|
return [] if num_glyphs.zero?
|
|
74
75
|
|
|
75
76
|
expected_size = format.zero? ? (num_glyphs + 1) * 2 : (num_glyphs + 1) * 4
|
|
76
|
-
|
|
77
|
+
# The loca record is parsed lazily and serialises to 0 bytes, so the
|
|
78
|
+
# table directory length is the real size.
|
|
79
|
+
actual_size = loca_entry.table_length
|
|
77
80
|
return [] if actual_size == expected_size
|
|
78
81
|
|
|
79
82
|
[issue(severity: :warning,
|
|
@@ -38,6 +38,36 @@ module Fontisan
|
|
|
38
38
|
uint32 :num_fonts
|
|
39
39
|
array :font_offsets, type: :uint32, initial_length: :num_fonts
|
|
40
40
|
|
|
41
|
+
HEADER_BYTES = 12
|
|
42
|
+
OFFSET_BYTES = 4
|
|
43
|
+
|
|
44
|
+
# Parse the header, refusing a +num_fonts+ whose offset table cannot fit in
|
|
45
|
+
# the remaining bytes. BinData reads the table eagerly, so an unchecked
|
|
46
|
+
# count of 0xFFFFFFFF would spin through 4 GiB of uint32s instead of
|
|
47
|
+
# failing on the first missing offset.
|
|
48
|
+
#
|
|
49
|
+
# @raise [BinData::ValidityError] if the offset table exceeds the input
|
|
50
|
+
def self.read(io, *, &)
|
|
51
|
+
verify_offset_table_fits!(io)
|
|
52
|
+
super
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.verify_offset_table_fits!(io)
|
|
56
|
+
return unless io.respond_to?(:pos) && io.respond_to?(:size)
|
|
57
|
+
|
|
58
|
+
start = io.pos
|
|
59
|
+
head = io.read(HEADER_BYTES)
|
|
60
|
+
io.seek(start)
|
|
61
|
+
return unless head && head.bytesize == HEADER_BYTES
|
|
62
|
+
|
|
63
|
+
count = head.byteslice(8, 4).unpack1("N")
|
|
64
|
+
return if HEADER_BYTES + (count * OFFSET_BYTES) <= io.size - start
|
|
65
|
+
|
|
66
|
+
raise BinData::ValidityError,
|
|
67
|
+
"#{count} fonts declared but the file is too short for their offsets"
|
|
68
|
+
end
|
|
69
|
+
private_class_method :verify_offset_table_fits!
|
|
70
|
+
|
|
41
71
|
# Abstract method: Get the font class for this collection type
|
|
42
72
|
#
|
|
43
73
|
# Subclasses must override this to return their specific font class
|