|
| 1 | +#![allow(non_upper_case_globals)] |
| 2 | +#![deny(clippy::unwrap_used, clippy::expect_used)] |
| 3 | +use fontspector_checkapi::{prelude::*, StatusCode}; |
| 4 | +use pyo3::prelude::*; |
| 5 | +mod checks; |
| 6 | +struct FontbakeryBridge; |
| 7 | + |
| 8 | +// We isolate the Python part to avoid type/result madness. |
| 9 | +fn call_python(module: &str, function: &str, testable: &Testable) -> PyResult<CheckFnResult> { |
| 10 | + let filename = testable.filename.to_string_lossy(); |
| 11 | + Python::with_gil(|py| { |
| 12 | + let module = PyModule::import_bound(py, module)?; |
| 13 | + let check = module.getattr(function)?; |
| 14 | + |
| 15 | + // Let's check this check's mandatory arguments |
| 16 | + let args = check.getattr("mandatoryArgs")?.extract::<Vec<String>>()?; |
| 17 | + if args.len() != 1 { |
| 18 | + return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>( |
| 19 | + "Expected exactly one mandatory argument".to_string(), |
| 20 | + )); |
| 21 | + } |
| 22 | + let arg = if args[0] == "font" { |
| 23 | + // Convert the Testable to a Python Font object |
| 24 | + let testable = PyModule::import_bound(py, "fontbakery.testable")?; |
| 25 | + let font = testable.getattr("Font")?; |
| 26 | + font.call1((filename,))? |
| 27 | + } else if args[0] == "ttFont" { |
| 28 | + // Convert the Testable to a Python TTFont object |
| 29 | + let ttlib = PyModule::import_bound(py, "fontTools.ttLib")?; |
| 30 | + let ttfont = ttlib.getattr("TTFont")?; |
| 31 | + ttfont.call1((filename,))? |
| 32 | + } else { |
| 33 | + return Err(PyErr::new::<pyo3::exceptions::PyValueError, _>( |
| 34 | + "Unknown mandatory argument".to_string(), |
| 35 | + )); |
| 36 | + }; |
| 37 | + |
| 38 | + let checkresult = check.call1((arg,))?; |
| 39 | + let mut messages: Vec<Status> = vec![]; |
| 40 | + |
| 41 | + // Now convert the Fontbakery status to our StatusList |
| 42 | + while let Ok(value) = checkresult.getattr("__next__")?.call0() { |
| 43 | + // Value is a tuple of status and message |
| 44 | + let status_str = value.get_item(0)?.getattr("name")?.extract::<String>()?; |
| 45 | + let status = StatusCode::from_string(&status_str).ok_or_else(|| { |
| 46 | + PyErr::new::<pyo3::exceptions::PyValueError, _>( |
| 47 | + "Fontbakery returned unknown status code".to_string(), |
| 48 | + ) |
| 49 | + })?; |
| 50 | + let code = value.get_item(1)?.getattr("code")?.extract::<String>()?; |
| 51 | + let message = value.get_item(1)?.getattr("message")?.extract::<String>()?; |
| 52 | + messages.push(Status { |
| 53 | + message: Some(message), |
| 54 | + severity: status, |
| 55 | + code: Some(code), |
| 56 | + }); |
| 57 | + } |
| 58 | + Ok(return_result(messages)) |
| 59 | + }) |
| 60 | +} |
| 61 | + |
| 62 | +// This wrapper will work for any fontbakery check that takes a single |
| 63 | +// Font or ttFont object as an argument. |
| 64 | +fn run_a_python_test(c: &Testable, context: &Context) -> CheckFnResult { |
| 65 | + let module = context |
| 66 | + .check_metadata |
| 67 | + .get("module") |
| 68 | + .ok_or_else(|| CheckError::Error("No module specified".to_string()))? |
| 69 | + .as_str() |
| 70 | + .ok_or_else(|| CheckError::Error("module in metadata was not a string!".to_string()))?; |
| 71 | + let function = context |
| 72 | + .check_metadata |
| 73 | + .get("function") |
| 74 | + .ok_or_else(|| CheckError::Error("No function specified".to_string()))? |
| 75 | + .as_str() |
| 76 | + .ok_or_else(|| CheckError::Error("function in metadata was not a string!".to_string()))?; |
| 77 | + call_python(module, function, c) |
| 78 | + .unwrap_or_else(|e| Err(CheckError::Error(format!("Python error: {}", e)))) |
| 79 | +} |
| 80 | + |
| 81 | +impl fontspector_checkapi::Plugin for FontbakeryBridge { |
| 82 | + fn register(&self, cr: &mut Registry) -> Result<(), String> { |
| 83 | + cr.register_check(checks::hinting_impact); |
| 84 | + cr.register_check(checks::opentype_name_empty_records); |
| 85 | + cr.register_check(checks::monospace); |
| 86 | + pyo3::prepare_freethreaded_python(); |
| 87 | + cr.register_profile( |
| 88 | + "fontbakery", |
| 89 | + Profile::from_toml( |
| 90 | + r#" |
| 91 | +[sections] |
| 92 | +"Test profile" = [ |
| 93 | + "hinting_impact", |
| 94 | + "opentype/name/empty_records", |
| 95 | + "opentype/monospace", |
| 96 | +] |
| 97 | +"#, |
| 98 | + ) |
| 99 | + .map_err(|_| "Couldn't parse profile")?, |
| 100 | + ) |
| 101 | + } |
| 102 | +} |
| 103 | + |
| 104 | +#[cfg(not(target_family = "wasm"))] |
| 105 | +pluginator::plugin_implementation!(fontspector_checkapi::Plugin, FontbakeryBridge); |
0 commit comments