diff --git a/harfrust_capi/README.md b/harfrust_capi/README.md index 5d1554bd..5de9cef4 100644 --- a/harfrust_capi/README.md +++ b/harfrust_capi/README.md @@ -105,8 +105,12 @@ along with the tags, directions, scripts, languages, features and variations they need. OpenType `BASE` baseline queries are available through `hr_ot_layout_get_baseline` and its related functions. OpenType `MATH` queries are available through `hr_ot_math_*` functions. Layout lookup queries support -glyph participation checks. CPAL palette queries are exposed through -`hr_ot_color_has_palettes` and `hr_ot_color_palette_get_count/flags/colors`. +glyph participation checks. Script selection and language-specific feature lookup +are available with `hr_ot_layout_table_select_script` and +`hr_ot_layout_language_find_feature`; `hr_ot_layout_table_get_feature_tags` +enumerates all feature records, including duplicate tags. CPAL palette queries +are exposed through `hr_ot_color_has_palettes` and +`hr_ot_color_palette_get_count/flags/colors`. Variation-axis metadata is exposed by `hr_ot_var_get_axis_count/infos`; `hr_ot_name_get_utf16` retrieves localized names with HarfBuzz's encoding preferences, platform language IDs, and UTF-16 buffer sizing. Unicode-platform @@ -114,8 +118,8 @@ names can use Apple's `ltag` table; Mac Roman names follow HarfBuzz's ASCII interpretation. AAT feat-table metadata is exposed by `hr_aat_layout_get_feature_types` and `hr_aat_layout_feature_type_get_selector_infos`, including exclusive defaults -and non-exclusive enable/disable pairs. The complete HarfBuzz set and map APIs are available -as `hr_set_*` and `hr_map_*`, including inverted sets, ranges, set algebra, and +and non-exclusive enable/disable pairs. The complete HarfBuzz set and map APIs +are available as `hr_set_*` and `hr_map_*`, including inverted sets, ranges, set algebra, and iteration. OpenType script and language tags can be converted with `hr_ot_tag_to_language` and `hr_ot_tags_from_script_and_language`. Set `hr_font_set_ppem` when `BASE` or `MATH` device adjustments should apply; it is diff --git a/harfrust_capi/include/hr-hb-ot.h b/harfrust_capi/include/hr-hb-ot.h index 7ecfea75..d51fd42a 100644 --- a/harfrust_capi/include/hr-hb-ot.h +++ b/harfrust_capi/include/hr-hb-ot.h @@ -19,8 +19,11 @@ #define hb_ot_layout_get_horizontal_baseline_tag_for_script hr_ot_layout_get_horizontal_baseline_tag_for_script #define hb_ot_layout_has_positioning hr_ot_layout_has_positioning #define hb_ot_layout_has_substitution hr_ot_layout_has_substitution +#define hb_ot_layout_language_find_feature hr_ot_layout_language_find_feature #define hb_ot_layout_lookup_collect_glyphs hr_ot_layout_lookup_collect_glyphs +#define hb_ot_layout_table_get_feature_tags hr_ot_layout_table_get_feature_tags #define hb_ot_layout_table_get_lookup_count hr_ot_layout_table_get_lookup_count +#define hb_ot_layout_table_select_script hr_ot_layout_table_select_script #define hb_ot_math_constant_t hr_ot_math_constant_t #define hb_ot_math_get_constant hr_ot_math_get_constant #define hb_ot_math_get_glyph_assembly hr_ot_math_get_glyph_assembly @@ -61,6 +64,9 @@ #define HB_OT_LAYOUT_BASELINE_TAG_IDEO_FACE_TOP_OR_RIGHT HR_OT_LAYOUT_BASELINE_TAG_IDEO_FACE_TOP_OR_RIGHT #define HB_OT_LAYOUT_BASELINE_TAG_MATH HR_OT_LAYOUT_BASELINE_TAG_MATH #define HB_OT_LAYOUT_BASELINE_TAG_ROMAN HR_OT_LAYOUT_BASELINE_TAG_ROMAN +#define HB_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX +#define HB_OT_LAYOUT_NO_FEATURE_INDEX HR_OT_LAYOUT_NO_FEATURE_INDEX +#define HB_OT_LAYOUT_NO_SCRIPT_INDEX HR_OT_LAYOUT_NO_SCRIPT_INDEX #define HB_OT_MATH_CONSTANT_ACCENT_BASE_HEIGHT HR_OT_MATH_CONSTANT_ACCENT_BASE_HEIGHT #define HB_OT_MATH_CONSTANT_AXIS_HEIGHT HR_OT_MATH_CONSTANT_AXIS_HEIGHT #define HB_OT_MATH_CONSTANT_DELIMITED_SUB_FORMULA_MIN_HEIGHT HR_OT_MATH_CONSTANT_DELIMITED_SUB_FORMULA_MIN_HEIGHT @@ -148,6 +154,10 @@ #define HB_OT_NAME_ID_VERSION_STRING HR_OT_NAME_ID_VERSION_STRING #define HB_OT_NAME_ID_WWS_FAMILY HR_OT_NAME_ID_WWS_FAMILY #define HB_OT_NAME_ID_WWS_SUBFAMILY HR_OT_NAME_ID_WWS_SUBFAMILY +#define HB_OT_TAG_DEFAULT_LANGUAGE HR_OT_TAG_DEFAULT_LANGUAGE +#define HB_OT_TAG_DEFAULT_SCRIPT HR_OT_TAG_DEFAULT_SCRIPT +#define HB_OT_TAG_GPOS HR_OT_TAG_GPOS +#define HB_OT_TAG_GSUB HR_OT_TAG_GSUB #define HB_OT_TAG_MATH HR_OT_TAG_MATH #define HB_OT_TAG_MATH_SCRIPT HR_OT_TAG_MATH_SCRIPT #define HB_OT_VAR_AXIS_FLAG_HIDDEN HR_OT_VAR_AXIS_FLAG_HIDDEN diff --git a/harfrust_capi/include/hr-hb.h b/harfrust_capi/include/hr-hb.h index 5e77433c..c2ec466a 100644 --- a/harfrust_capi/include/hr-hb.h +++ b/harfrust_capi/include/hr-hb.h @@ -16,8 +16,9 @@ * macros below would rewrite HarfBuzz's own declarations. Include one or the * other. * - * Shaping, BASE baseline, MATH, CPAL palettes, axis/name and AAT metadata, layout lookup, set, map, and OpenType tag - * conversion APIs are covered. + * Shaping, BASE baseline, MATH, CPAL palettes, axis/name and AAT metadata, + * layout script/feature/lookup, set, map, and OpenType tag conversion APIs + * are covered. * Anything else HarfBuzz offers -- drawing and painting callbacks, * subsetting, other layout table introspection, custom Unicode * callbacks -- has no mapping here and will fail to compile, which is the @@ -384,6 +385,7 @@ #define HB_GLYPH_FLAG_UNSAFE_TO_CONCAT HR_GLYPH_FLAG_UNSAFE_TO_CONCAT #define HB_LANGUAGE_INVALID HR_LANGUAGE_INVALID #define HB_MAP_VALUE_INVALID HR_MAP_VALUE_INVALID +#define HB_MATH_GLYPH_PART_FLAG_EXTENDER HR_MATH_GLYPH_PART_FLAG_EXTENDER #define HB_MEMORY_MODE_DUPLICATE HR_MEMORY_MODE_DUPLICATE #define HB_MEMORY_MODE_READONLY HR_MEMORY_MODE_READONLY #define HB_MEMORY_MODE_READONLY_MAY_MAKE_WRITABLE HR_MEMORY_MODE_READONLY_MAY_MAKE_WRITABLE diff --git a/harfrust_capi/include/hr.h b/harfrust_capi/include/hr.h index ea758e47..998b6790 100644 --- a/harfrust_capi/include/hr.h +++ b/harfrust_capi/include/hr.h @@ -41,6 +41,21 @@ */ #define HR_FEATURE_GLOBAL_END 4294967295u +/** + * No script was selected. + */ +#define HR_OT_LAYOUT_NO_SCRIPT_INDEX 65535u + +/** + * No feature was found. + */ +#define HR_OT_LAYOUT_NO_FEATURE_INDEX 65535u + +/** + * Selects a script's default language system. + */ +#define HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX 65535u + /** * The major version of this library. * @@ -2330,6 +2345,26 @@ typedef unsigned int hr_ot_name_id_predefined_t; */ #define HR_OT_COLOR_PALETTE_FLAG_USABLE_WITH_DARK_BACKGROUND 2 +/** + * The OpenType substitution table tag. + */ +#define HR_OT_TAG_GSUB 1196643650u + +/** + * The OpenType positioning table tag. + */ +#define HR_OT_TAG_GPOS 1196445523u + +/** + * The default OpenType script tag, `DFLT`. + */ +#define HR_OT_TAG_DEFAULT_SCRIPT 1145457748u + +/** + * The default OpenType language tag, `dflt`. + */ +#define HR_OT_TAG_DEFAULT_LANGUAGE 1684434036u + #define HR_OT_LAYOUT_BASELINE_TAG_ROMAN 1919905134u #define HR_OT_LAYOUT_BASELINE_TAG_HANGING 1751215719u @@ -2470,6 +2505,11 @@ typedef unsigned int hr_ot_name_id_predefined_t; #define HR_OT_MATH_GLYPH_PART_FLAG_EXTENDER 1u +/** + * Deprecated HarfBuzz spelling of `HR_OT_MATH_GLYPH_PART_FLAG_EXTENDER`. + */ +#define HR_MATH_GLYPH_PART_FLAG_EXTENDER HR_OT_MATH_GLYPH_PART_FLAG_EXTENDER + #define HR_OT_TAG_MATH 1296127048u #define HR_OT_TAG_MATH_SCRIPT 1835103336u @@ -4729,6 +4769,58 @@ unsigned int hr_ot_color_palette_get_colors(struct hr_face_t *face, unsigned int *color_count, hr_color_t *colors); +/** + * Selects the first available requested script in GSUB or GPOS. + * + * If none matches, tries `DFLT`, `dflt`, then `latn`. Returns true only for + * a requested script, even when a fallback was selected. With no match, + * writes `HR_OT_LAYOUT_NO_SCRIPT_INDEX` and a zero tag to the outputs. + * + * # Safety + * `face` must be null or live; `script_tags` must hold `script_count` tags + * when non-null. Both output pointers must be null or writable. + */ +hr_bool_t hr_ot_layout_table_select_script(struct hr_face_t *face, + hr_tag_t table_tag, + unsigned int script_count, + const hr_tag_t *script_tags, + unsigned int *script_index, + hr_tag_t *chosen_script); + +/** + * Returns the total number of feature records in GSUB or GPOS. + * + * Copies tags starting at `start_offset`, including duplicates. When both + * array and count are non-null, the count gives capacity on entry and the + * number written on return. A null array leaves the count unchanged. + * + * # Safety + * `face` must be null or live; `feature_count` must be null or writable; + * `feature_tags` must hold the input capacity when non-null. + */ +unsigned int hr_ot_layout_table_get_feature_tags(struct hr_face_t *face, + hr_tag_t table_tag, + unsigned int start_offset, + unsigned int *feature_count, + hr_tag_t *feature_tags); + +/** + * Finds an optional feature in a script's language system in GSUB or GPOS. + * + * Use `HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX` for the default language system. + * A required feature is not included in this search. Writes the feature's + * table-wide index, or `HR_OT_LAYOUT_NO_FEATURE_INDEX` on failure. + * + * # Safety + * `face` must be null or live; `feature_index` must be null or writable. + */ +hr_bool_t hr_ot_layout_language_find_feature(struct hr_face_t *face, + hr_tag_t table_tag, + unsigned int script_index, + unsigned int language_index, + hr_tag_t feature_tag, + unsigned int *feature_index); + /** * Returns whether the face has a readable GSUB table. * diff --git a/harfrust_capi/src/lib.rs b/harfrust_capi/src/lib.rs index fb7a8b8e..1508a540 100644 --- a/harfrust_capi/src/lib.rs +++ b/harfrust_capi/src/lib.rs @@ -10,7 +10,7 @@ as HarfBuzz itself without collisions. # Scope This covers shaping: blobs, faces, fonts, buffers and `hr_shape`, plus OpenType -`BASE` baseline and `MATH` queries. It also provides layout lookup queries, +`BASE` baseline and `MATH` queries. It also provides layout script/feature and lookup queries, OpenType script and language tag conversion, and the complete set and map container APIs. Drawing and painting callbacks and subsetting are outside this API. @@ -99,6 +99,10 @@ mod skia_version_tests; #[path = "../tests/tables.rs"] mod table_tests; +#[cfg(test)] +#[path = "../tests/ot_queries.rs"] +mod ot_query_tests; + #[cfg(test)] #[path = "../tests/ot_color.rs"] mod ot_color_tests; diff --git a/harfrust_capi/src/ot_layout.rs b/harfrust_capi/src/ot_layout.rs index b1b7bc2c..2565a8bd 100644 --- a/harfrust_capi/src/ot_layout.rs +++ b/harfrust_capi/src/ot_layout.rs @@ -1,4 +1,4 @@ -//! OpenType BASE baseline queries. +//! OpenType layout metadata, lookup glyph collection, and BASE baseline queries. use core::ffi::c_uint; @@ -7,7 +7,8 @@ use read_fonts::tables::base::{BaseAxis, BaseInstance}; use read_fonts::tables::gpos::{PairPos, PositionSubtables}; use read_fonts::tables::gsub::{SingleSubst, SubstitutionLookupList, SubstitutionSubtables}; use read_fonts::tables::layout::{ - ChainedSequenceContext, ClassDef, CoverageTable, SequenceContext, SequenceLookupRecord, + ChainedSequenceContext, ClassDef, CoverageTable, FeatureList, ScriptList, SequenceContext, + SequenceLookupRecord, }; use read_fonts::types::F48Dot16; use read_fonts::{ReadError, TableProvider}; @@ -22,8 +23,183 @@ use crate::font::{hr_font_get_glyph_extents, hr_font_get_nominal_glyph, hr_font_ use crate::object; use crate::set::hr_set_t; -const GSUB_TAG: hr_tag_t = 0x4753_5542; -const GPOS_TAG: hr_tag_t = 0x4750_4F53; +/// The OpenType substitution table tag. +pub const HR_OT_TAG_GSUB: hr_tag_t = 0x4753_5542u32; +/// The OpenType positioning table tag. +pub const HR_OT_TAG_GPOS: hr_tag_t = 0x4750_4F53u32; +/// The default OpenType script tag, `DFLT`. +pub const HR_OT_TAG_DEFAULT_SCRIPT: hr_tag_t = 0x4446_4C54u32; +/// The default OpenType language tag, `dflt`. +pub const HR_OT_TAG_DEFAULT_LANGUAGE: hr_tag_t = 0x6466_6C74u32; +/// No script was selected. +pub const HR_OT_LAYOUT_NO_SCRIPT_INDEX: c_uint = 0xFFFFu32; +/// No feature was found. +pub const HR_OT_LAYOUT_NO_FEATURE_INDEX: c_uint = 0xFFFFu32; +/// Selects a script's default language system. +pub const HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX: c_uint = 0xFFFFu32; + +const GSUB_TAG: hr_tag_t = HR_OT_TAG_GSUB; +const GPOS_TAG: hr_tag_t = HR_OT_TAG_GPOS; + +fn script_list(face: &hr_face_t, table_tag: hr_tag_t) -> Option> { + let font = face.font()?; + match table_tag { + GSUB_TAG => font.tables().gsub().ok()?.script_list().ok(), + GPOS_TAG => font.tables().gpos().ok()?.script_list().ok(), + _ => None, + } +} + +fn feature_list(face: &hr_face_t, table_tag: hr_tag_t) -> Option> { + let font = face.font()?; + match table_tag { + GSUB_TAG => font.tables().gsub().ok()?.feature_list().ok(), + GPOS_TAG => font.tables().gpos().ok()?.feature_list().ok(), + _ => None, + } +} + +/// Selects the first available requested script in GSUB or GPOS. +/// +/// If none matches, tries `DFLT`, `dflt`, then `latn`. Returns true only for +/// a requested script, even when a fallback was selected. With no match, +/// writes `HR_OT_LAYOUT_NO_SCRIPT_INDEX` and a zero tag to the outputs. +/// +/// # Safety +/// `face` must be null or live; `script_tags` must hold `script_count` tags +/// when non-null. Both output pointers must be null or writable. +#[no_mangle] +pub unsafe extern "C" fn hr_ot_layout_table_select_script( + face: *mut hr_face_t, + table_tag: hr_tag_t, + script_count: c_uint, + script_tags: *const hr_tag_t, + script_index: *mut c_uint, + chosen_script: *mut hr_tag_t, +) -> hr_bool_t { + let face = unsafe { object::or_empty(face.cast_const()) }; + let list = script_list(face, table_tag); + let records = list.as_ref().map_or(&[][..], |list| list.script_records()); + let requested = if script_tags.is_null() { + &[][..] + } else { + unsafe { core::slice::from_raw_parts(script_tags, script_count as usize) } + }; + let fallbacks = [ + HR_OT_TAG_DEFAULT_SCRIPT, + HR_OT_TAG_DEFAULT_LANGUAGE, + u32::from_be_bytes(*b"latn"), + ]; + let selected = requested + .iter() + .map(|tag| (*tag, true)) + .chain(fallbacks.into_iter().map(|tag| (tag, false))) + .find_map(|(tag, requested)| { + records + .binary_search_by_key(&tag_to_rust(tag), |record| record.script_tag()) + .ok() + .map(|index| (index as c_uint, tag, requested)) + }); + let (index, tag, requested) = selected.unwrap_or((HR_OT_LAYOUT_NO_SCRIPT_INDEX, 0, false)); + if let Some(output) = unsafe { script_index.as_mut() } { + *output = index; + } + if let Some(output) = unsafe { chosen_script.as_mut() } { + *output = tag; + } + requested.into() +} + +/// Returns the total number of feature records in GSUB or GPOS. +/// +/// Copies tags starting at `start_offset`, including duplicates. When both +/// array and count are non-null, the count gives capacity on entry and the +/// number written on return. A null array leaves the count unchanged. +/// +/// # Safety +/// `face` must be null or live; `feature_count` must be null or writable; +/// `feature_tags` must hold the input capacity when non-null. +#[no_mangle] +pub unsafe extern "C" fn hr_ot_layout_table_get_feature_tags( + face: *mut hr_face_t, + table_tag: hr_tag_t, + start_offset: c_uint, + feature_count: *mut c_uint, + feature_tags: *mut hr_tag_t, +) -> c_uint { + let face = unsafe { object::or_empty(face.cast_const()) }; + let list = feature_list(face, table_tag); + let records = list.as_ref().map_or(&[][..], |list| list.feature_records()); + if !feature_tags.is_null() { + if let Some(count) = unsafe { feature_count.as_mut() } { + let mut written = 0; + for record in records + .iter() + .skip(start_offset as usize) + .take(*count as usize) + { + unsafe { + feature_tags + .add(written) + .write(tag_from_rust(record.feature_tag())); + }; + written += 1; + } + *count = written as c_uint; + } + } + records.len() as c_uint +} + +/// Finds an optional feature in a script's language system in GSUB or GPOS. +/// +/// Use `HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX` for the default language system. +/// A required feature is not included in this search. Writes the feature's +/// table-wide index, or `HR_OT_LAYOUT_NO_FEATURE_INDEX` on failure. +/// +/// # Safety +/// `face` must be null or live; `feature_index` must be null or writable. +#[no_mangle] +pub unsafe extern "C" fn hr_ot_layout_language_find_feature( + face: *mut hr_face_t, + table_tag: hr_tag_t, + script_index: c_uint, + language_index: c_uint, + feature_tag: hr_tag_t, + feature_index: *mut c_uint, +) -> hr_bool_t { + let face = unsafe { object::or_empty(face.cast_const()) }; + let found = (|| { + let scripts = script_list(face, table_tag)?; + let script = scripts + .script_records() + .get(script_index as usize)? + .script(scripts.offset_data()) + .ok()?; + let language = if language_index == HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX { + script.default_lang_sys()?.ok()? + } else { + script + .lang_sys_records() + .get(language_index as usize)? + .lang_sys(script.offset_data()) + .ok()? + }; + let features = feature_list(face, table_tag)?; + language.feature_indices().iter().find_map(|index| { + let index = index.get() as usize; + let tag = features + .feature_records() + .get(index) + .map_or(0, |record| tag_from_rust(record.feature_tag())); + (tag == feature_tag).then_some(index as c_uint) + }) + })(); + if let Some(output) = unsafe { feature_index.as_mut() } { + *output = found.unwrap_or(HR_OT_LAYOUT_NO_FEATURE_INDEX); + } + found.is_some().into() +} /// Returns whether the face has a readable GSUB table. /// diff --git a/harfrust_capi/src/ot_math.rs b/harfrust_capi/src/ot_math.rs index f10336e6..f5604fc7 100644 --- a/harfrust_capi/src/ot_math.rs +++ b/harfrust_capi/src/ot_math.rs @@ -82,6 +82,10 @@ pub const HR_OT_MATH_KERN_TOP_LEFT: hr_ot_math_kern_t = 1u32; pub const HR_OT_MATH_KERN_BOTTOM_RIGHT: hr_ot_math_kern_t = 2u32; pub const HR_OT_MATH_KERN_BOTTOM_LEFT: hr_ot_math_kern_t = 3u32; pub const HR_OT_MATH_GLYPH_PART_FLAG_EXTENDER: hr_ot_math_glyph_part_flags_t = 1u32; + +/// Deprecated HarfBuzz spelling of `HR_OT_MATH_GLYPH_PART_FLAG_EXTENDER`. +pub const HR_MATH_GLYPH_PART_FLAG_EXTENDER: hr_ot_math_glyph_part_flags_t = + HR_OT_MATH_GLYPH_PART_FLAG_EXTENDER; pub const HR_OT_TAG_MATH: hr_tag_t = 0x4D41_5448u32; pub const HR_OT_TAG_MATH_SCRIPT: hr_tag_t = 0x6D61_7468u32; diff --git a/harfrust_capi/tests/ot_queries.rs b/harfrust_capi/tests/ot_queries.rs new file mode 100644 index 00000000..b23b1e3e --- /dev/null +++ b/harfrust_capi/tests/ot_queries.rs @@ -0,0 +1,308 @@ +//! Script selection, language feature lookup, and feature-list pagination. + +use crate::{table_tests::TableFace, *}; +use std::ptr; + +fn tag(bytes: &[u8; 4]) -> u32 { + u32::from_be_bytes(*bytes) +} + +fn words(values: &[u16]) -> Vec { + values + .iter() + .flat_map(|value| value.to_be_bytes()) + .collect() +} + +fn layout(scripts: &[&[u8; 4]]) -> Vec { + // Each script has a default language with required rlig and optional smcp + // and the second liga record. FRA has only the first liga record. + let mut script_list = words(&[scripts.len() as u16]); + for (i, name) in scripts.iter().enumerate() { + script_list.extend_from_slice(*name); + script_list.extend(words(&[(2 + 6 * scripts.len() + 28 * i) as u16])); + } + for _ in scripts { + script_list.extend(words(&[10, 1])); + script_list.extend_from_slice(b"FRA "); + script_list.extend(words(&[20, 0, 3, 2, 4, 2, 0, 0xFFFF, 1, 1])); + } + let mut features = words(&[5]); + for (i, name) in [b"kern", b"liga", b"liga", b"rlig", b"smcp"] + .iter() + .enumerate() + { + features.extend_from_slice(*name); + features.extend(words(&[(32 + 4 * i) as u16])); + } + features.extend([0; 20]); + let mut table = words(&[ + 1, + 0, + 10, + (10 + script_list.len()) as u16, + (10 + script_list.len() + features.len()) as u16, + ]); + table.extend(script_list); + table.extend(features); + table.extend(words(&[0])); + table +} + +#[test] +fn requested_scripts_take_priority_and_fallbacks_return_false() { + unsafe { + for table in [HR_OT_TAG_GSUB, HR_OT_TAG_GPOS] { + let face = TableFace::new(vec![(table, layout(&[b"DFLT", b"arab", b"dflt", b"latn"]))]); + let mut index = 99; + let mut chosen = 99; + let requested = [tag(b"thai"), tag(b"latn"), tag(b"arab")]; + assert_eq!( + hr_ot_layout_table_select_script( + face.0, + table, + 3, + requested.as_ptr(), + &raw mut index, + &raw mut chosen + ), + 1 + ); + assert_eq!((index, chosen), (3, tag(b"latn"))); + assert_eq!( + hr_ot_layout_table_select_script( + face.0, + table, + 1, + requested.as_ptr(), + &raw mut index, + &raw mut chosen + ), + 0 + ); + assert_eq!((index, chosen), (0, tag(b"DFLT"))); + assert_eq!( + hr_ot_layout_table_select_script( + face.0, + table, + 0, + ptr::null(), + ptr::null_mut(), + ptr::null_mut() + ), + 0 + ); + } + for (scripts, expected) in [ + (vec![b"arab", b"dflt", b"latn"], tag(b"dflt")), + (vec![b"arab", b"latn"], tag(b"latn")), + (vec![b"arab"], 0), + ] { + let face = TableFace::new(vec![(HR_OT_TAG_GSUB, layout(&scripts))]); + let mut index = 99; + let mut chosen = 99; + assert_eq!( + hr_ot_layout_table_select_script( + face.0, + HR_OT_TAG_GSUB, + 0, + ptr::null(), + &raw mut index, + &raw mut chosen + ), + 0 + ); + assert_eq!(chosen, expected); + assert_eq!( + index, + if expected == 0 { + HR_OT_LAYOUT_NO_SCRIPT_INDEX + } else { + 1 + } + ); + } + } +} + +#[test] +fn language_features_keep_global_indices_and_exclude_required_features() { + unsafe { + for table in [HR_OT_TAG_GSUB, HR_OT_TAG_GPOS] { + let face = TableFace::new(vec![(table, layout(&[b"latn"]))]); + for (language, name, expected) in [ + (HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, b"smcp", Some(4)), + (HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, b"liga", Some(2)), + (0, b"liga", Some(1)), + (0, b"smcp", None), + (HR_OT_LAYOUT_DEFAULT_LANGUAGE_INDEX, b"rlig", None), + (1, b"liga", None), + (u32::MAX, b"liga", None), + ] { + let mut index = 99; + assert_eq!( + hr_ot_layout_language_find_feature( + face.0, + table, + 0, + language, + tag(name), + &raw mut index + ), + i32::from(expected.is_some()) + ); + assert_eq!(index, expected.unwrap_or(HR_OT_LAYOUT_NO_FEATURE_INDEX)); + } + assert_eq!( + hr_ot_layout_language_find_feature( + face.0, + table, + 0, + 0, + tag(b"liga"), + ptr::null_mut() + ), + 1 + ); + let mut index = 99; + assert_eq!( + hr_ot_layout_language_find_feature( + face.0, + table, + 1, + 0, + tag(b"liga"), + &raw mut index + ), + 0 + ); + assert_eq!(index, HR_OT_LAYOUT_NO_FEATURE_INDEX); + } + } +} + +#[test] +fn feature_pagination_preserves_duplicates_and_null_query_counts() { + unsafe { + let face = TableFace::new(vec![(HR_OT_TAG_GSUB, layout(&[b"latn"]))]); + let mut output = [99; 6]; + let mut count = 6; + assert_eq!( + hr_ot_layout_table_get_feature_tags( + face.0, + HR_OT_TAG_GSUB, + 0, + &raw mut count, + output.as_mut_ptr() + ), + 5 + ); + assert_eq!(count, 5); + assert_eq!( + output, + [ + tag(b"kern"), + tag(b"liga"), + tag(b"liga"), + tag(b"rlig"), + tag(b"smcp"), + 99 + ] + ); + count = 2; + assert_eq!( + hr_ot_layout_table_get_feature_tags( + face.0, + HR_OT_TAG_GSUB, + 4, + &raw mut count, + output.as_mut_ptr() + ), + 5 + ); + assert_eq!((count, output[0]), (1, tag(b"smcp"))); + count = 2; + assert_eq!( + hr_ot_layout_table_get_feature_tags( + face.0, + HR_OT_TAG_GSUB, + u32::MAX, + &raw mut count, + output.as_mut_ptr() + ), + 5 + ); + assert_eq!(count, 0); + count = 99; + assert_eq!( + hr_ot_layout_table_get_feature_tags( + face.0, + HR_OT_TAG_GSUB, + 0, + &raw mut count, + ptr::null_mut() + ), + 5 + ); + assert_eq!(count, 99); + assert_eq!( + hr_ot_layout_table_get_feature_tags( + face.0, + HR_OT_TAG_GSUB, + 0, + ptr::null_mut(), + output.as_mut_ptr() + ), + 5 + ); + } +} + +#[test] +fn absent_and_truncated_tables_return_empty_results() { + unsafe { + let broken = TableFace::new(vec![(HR_OT_TAG_GSUB, vec![0; 11])]); + for face in [ptr::null_mut(), hr_face_get_empty(), broken.0] { + for table in [HR_OT_TAG_GSUB, HR_OT_TAG_GPOS, 0] { + let mut index = 99; + let mut chosen = 99; + assert_eq!( + hr_ot_layout_table_select_script( + face, + table, + 0, + ptr::null(), + &raw mut index, + &raw mut chosen + ), + 0 + ); + assert_eq!((index, chosen), (HR_OT_LAYOUT_NO_SCRIPT_INDEX, 0)); + assert_eq!( + hr_ot_layout_language_find_feature( + face, + table, + 0, + 0, + tag(b"smcp"), + &raw mut index + ), + 0 + ); + assert_eq!(index, HR_OT_LAYOUT_NO_FEATURE_INDEX); + let mut count = 1; + assert_eq!( + hr_ot_layout_table_get_feature_tags( + face, + table, + 0, + &raw mut count, + &raw mut chosen + ), + 0 + ); + assert_eq!(count, 0); + } + } + } +} diff --git a/scripts/gen-hb-compat-header.py b/scripts/gen-hb-compat-header.py index f2350c3b..52666c1e 100644 --- a/scripts/gen-hb-compat-header.py +++ b/scripts/gen-hb-compat-header.py @@ -73,8 +73,9 @@ * macros below would rewrite HarfBuzz's own declarations. Include one or the * other. * - * Shaping, BASE baseline, MATH, CPAL palettes, axis/name and AAT metadata, layout lookup, set, map, and OpenType tag - * conversion APIs are covered. + * Shaping, BASE baseline, MATH, CPAL palettes, axis/name and AAT metadata, + * layout script/feature/lookup, set, map, and OpenType tag conversion APIs + * are covered. * Anything else HarfBuzz offers -- drawing and painting callbacks, * subsetting, other layout table introspection, custom Unicode * callbacks -- has no mapping here and will fail to compile, which is the diff --git a/scripts/test-capi-ot-queries.py b/scripts/test-capi-ot-queries.py new file mode 100644 index 00000000..996556e7 --- /dev/null +++ b/scripts/test-capi-ot-queries.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Compare the C API's layout metadata queries with a HarfBuzz shared library. + +Uses only Python's standard library. Build harfrust_capi first, then run: + python3 scripts/test-capi-ot-queries.py --library target/debug/libharfrust_c.so \ + --harfbuzz /path/to/libharfbuzz.so --fonts harfrust/tests/fonts +""" +import argparse +import ctypes as C +from pathlib import Path + +U = C.c_uint +P = C.c_void_p +UP = C.POINTER(U) + + +def bind(library, prefix, name, result, args): + function = getattr(library, prefix + name) + function.restype = result + function.argtypes = args + return function + + +class Api: + def __init__(self, library, prefix): + self.blob_create = bind(library, prefix, "blob_create", P, + [P, U, C.c_int, P, P]) + self.blob_destroy = bind(library, prefix, "blob_destroy", None, [P]) + self.face_create = bind(library, prefix, "face_create", P, [P, U]) + self.face_count = bind(library, prefix, "face_count", U, [P]) + self.face_destroy = bind(library, prefix, "face_destroy", None, [P]) + self.select = bind(library, prefix, "ot_layout_table_select_script", C.c_int, + [P, U, U, UP, UP, UP]) + self.features = bind(library, prefix, "ot_layout_table_get_feature_tags", U, + [P, U, U, UP, UP]) + self.find = bind(library, prefix, "ot_layout_language_find_feature", C.c_int, + [P, U, U, U, U, UP]) + + def face(self, data, index): + blob = self.blob_create(data, len(data), 0, None, None) + face = self.face_create(blob, index) + self.blob_destroy(blob) + return face + + +def tag(text): + return int.from_bytes(text.encode("ascii"), "big") + + +def select(api, face, table, scripts): + index, chosen = U(123), U(123) + result = api.select(face, table, len(scripts), (U * len(scripts))(*scripts), + C.byref(index), C.byref(chosen)) + return result, index.value, chosen.value + + +def page(api, face, table, start, capacity, array=True, count=True): + size = U(capacity) + output = (U * (capacity + 1))(*([0xDEADBEEF] * (capacity + 1))) + total = api.features(face, table, start, C.byref(size) if count else None, + output if array else None) + return total, size.value, list(output) + + +def find(api, face, table, script, language, feature): + index = U(123) + result = api.find(face, table, script, language, feature, C.byref(index)) + return result, index.value + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--library", required=True) + parser.add_argument("--harfbuzz", required=True, help="HarfBuzz 14.5.1 or newer shared library") + parser.add_argument("--fonts", type=Path, required=True) + args = parser.parse_args() + hr = Api(C.CDLL(args.library), "hr_") + hb_lib = C.CDLL(args.harfbuzz) + hb = Api(hb_lib, "hb_") + version = bind(hb_lib, "hb_", "version_atleast", C.c_int, [U, U, U]) + if not version(14, 5, 1): + parser.error("use HarfBuzz >= 14.5.1 for nullable pagination tests") + has_sub = bind(hb_lib, "hb_", "ot_layout_has_substitution", C.c_int, [P]) + has_pos = bind(hb_lib, "hb_", "ot_layout_has_positioning", C.c_int, [P]) + scripts = bind(hb_lib, "hb_", "ot_layout_table_get_script_tags", U, + [P, U, U, UP, UP]) + languages = bind(hb_lib, "hb_", "ot_layout_script_get_language_tags", U, + [P, U, U, U, UP, UP]) + checks = faces = skipped = 0 + + def equal(label, actual, expected): + nonlocal checks + checks += 1 + assert actual == expected, (label, actual, expected) + + for path in sorted(args.fonts.rglob("*")): + if path.suffix.lower() not in {".ttf", ".otf", ".ttc"}: + continue + data = path.read_bytes() + blob = hb.blob_create(data, len(data), 0, None, None) + count = hb.face_count(blob) + hb.blob_destroy(blob) + for index in range(count): + hrf, hbf = hr.face(data, index), hb.face(data, index) + faces += 1 + try: + for table in [tag("GSUB"), tag("GPOS"), 0]: + # Fontations queries readable metadata directly; HarfBuzz + # sanitizes the entire layout table, including its lookups. + # The shaping corpus contains deliberately broken tables. + if (table == tag("GSUB") and not has_sub(hbf)) or ( + table == tag("GPOS") and not has_pos(hbf)): + skipped += 1 + continue + total = scripts(hbf, table, 0, None, None) + values = (U * total)() + n = U(total) + scripts(hbf, table, 0, C.byref(n), values) + for requested in [[], [tag("zzzz")], [tag("latn")], + [tag("arab"), tag("latn")], list(values), + list(reversed(values))]: + actual = select(hr, hrf, table, requested) + expected = select(hb, hbf, table, requested) + # Duplicate script records are outside the specification; + # either matching index is acceptable for that tag. + if list(values).count(expected[2]) > 1: + equal((str(path), "select duplicate", table, requested), + (actual[0], actual[2]), (expected[0], expected[2])) + else: + equal((str(path), "select", table, requested), actual, expected) + feature_total = hb.features(hbf, table, 0, None, None) + for start, capacity, array, count in [ + (0, feature_total, True, True), (0, 1, True, True), + (1, 3, True, True), (feature_total, 2, True, True), + (0xFFFFFFFF, 2, True, True), (0, 99, False, True), + (0, 99, True, False), (0, 0, True, True), + ]: + equal((str(path), "page", table, start, capacity, array, count), + page(hr, hrf, table, start, capacity, array, count), + page(hb, hbf, table, start, capacity, array, count)) + feature_tags = page(hb, hbf, table, 0, feature_total)[2][:-1] + for script in [*range(total), 0xFFFF, 0xFFFFFFFF]: + language_count = languages(hbf, table, script, 0, None, None) + for language in [*range(language_count), 0xFFFF, 0xFFFFFFFF]: + for feature in {*feature_tags, tag("zzzz"), 0}: + equal((str(path), "find", table, script, language, feature), + find(hr, hrf, table, script, language, feature), + find(hb, hbf, table, script, language, feature)) + finally: + hr.face_destroy(hrf) + hb.face_destroy(hbf) + print(f"Matched HarfBuzz in {checks} query comparisons across {faces} font faces " + f"({skipped} absent or HarfBuzz-rejected layout tables skipped).") + + +if __name__ == "__main__": + main()