Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

1
vendor/skrifa/.cargo-checksum.json vendored Normal file

File diff suppressed because one or more lines are too long

195
vendor/skrifa/Cargo.lock generated vendored Normal file
View File

@@ -0,0 +1,195 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "arrayvec"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96d30a06541fbafbc7f82ed10c06164cfbd2c401138f6addd8404629c4b16711"
[[package]]
name = "bytemuck"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78834c15cb5d5efe3452d58b1e8ba890dd62d21907f867f383358198e56ebca5"
dependencies = [
"bytemuck_derive",
]
[[package]]
name = "bytemuck_derive"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4da9a32f3fed317401fa3c862968128267c3106685286e15d5aaa3d7389c2f60"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "core_maths"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3b02505ccb8c50b0aa21ace0fc08c3e53adebd4e58caa18a36152803c7709a3"
dependencies = [
"libm",
]
[[package]]
name = "diff"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "font-types"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02a596f5713680923a2080d86de50fe472fb290693cf0f701187a1c8b36996b7"
dependencies = [
"bytemuck",
"serde",
]
[[package]]
name = "itoa"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c"
[[package]]
name = "kurbo"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e5aa9f0f96a938266bdb12928a67169e8d22c6a786fda8ed984b85e6ba93c3c"
dependencies = [
"arrayvec",
"smallvec",
]
[[package]]
name = "libm"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058"
[[package]]
name = "pretty_assertions"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66"
dependencies = [
"diff",
"yansi",
]
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
dependencies = [
"proc-macro2",
]
[[package]]
name = "read-fonts"
version = "0.29.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f96bfbb7df43d34a2b7b8582fcbcb676ba02a763265cb90bc8aabfd62b57d64"
dependencies = [
"bytemuck",
"core_maths",
"font-types",
"serde",
]
[[package]]
name = "ryu"
version = "1.0.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f98d2aa92eebf49b69786be48e4477826b256916e84a57ff2a4f21923b48eb4c"
[[package]]
name = "serde"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63261df402c67811e9ac6def069e4786148c4563f4b50fd4bf30aa370d626b02"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.195"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46fe8f8603d81ba86327b23a2e9cdf49e1255fb94a4c5f297f6ee0547178ea2c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "skrifa"
version = "0.31.3"
dependencies = [
"bytemuck",
"core_maths",
"kurbo",
"pretty_assertions",
"read-fonts",
"serde",
"serde_json",
]
[[package]]
name = "smallvec"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7"
[[package]]
name = "syn"
version = "2.0.48"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "yansi"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"

86
vendor/skrifa/Cargo.toml vendored Normal file
View File

@@ -0,0 +1,86 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
rust-version = "1.75"
name = "skrifa"
version = "0.31.3"
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "Metadata reader and glyph scaler for OpenType fonts."
readme = "README.md"
categories = [
"text-processing",
"parsing",
"graphics",
]
license = "MIT OR Apache-2.0"
repository = "https://github.com/googlefonts/fontations"
[package.metadata.docs.rs]
all-features = true
[features]
autohint_shaping = []
default = [
"autohint_shaping",
"traversal",
]
libm = [
"dep:core_maths",
"read-fonts/libm",
]
spec_next = ["read-fonts/spec_next"]
std = ["read-fonts/std"]
traversal = [
"std",
"read-fonts/experimental_traverse",
]
[lib]
name = "skrifa"
path = "src/lib.rs"
[dependencies.bytemuck]
version = "1.13.1"
[dependencies.core_maths]
version = "0.1"
optional = true
[dependencies.read-fonts]
version = "0.29.2"
default-features = false
[dev-dependencies.kurbo]
version = "0.11.0"
[dev-dependencies.pretty_assertions]
version = "1.3.0"
[dev-dependencies.read-fonts]
version = "0.29.2"
features = [
"scaler_test",
"serde",
]
default-features = false
[dev-dependencies.serde]
version = "1.0"
[dev-dependencies.serde_json]
version = "1.0"

67
vendor/skrifa/LICENSE-APACHE vendored Normal file
View File

@@ -0,0 +1,67 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
You must give any other recipients of the Work or Derivative Works a copy of this License; and
You must cause any modified files to carry prominent notices stating that You changed the files; and
You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2019 Colin Rothfels
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

25
vendor/skrifa/LICENSE-MIT vendored Normal file
View File

@@ -0,0 +1,25 @@
Copyright (c) 2019 Colin Rothfels
Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated
documentation files (the "Software"), to deal in the
Software without restriction, including without
limitation the rights to use, copy, modify, merge,
publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software
is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice
shall be included in all copies or substantial portions
of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.

62
vendor/skrifa/README.md vendored Normal file
View File

@@ -0,0 +1,62 @@
# skrifa
[![Crates.io](https://img.shields.io/crates/v/skrifa.svg)](https://crates.io/crates/skrifa)
[![Docs](https://docs.rs/skrifa/badge.svg)](https://docs.rs/skrifa)
[![MIT/Apache 2.0](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](#license)
This crate aims to be a robust, ergonomic, high performance library for reading
OpenType fonts. It is built on top of the
[read-fonts](https://github.com/googlefonts/fontations/tree/main/read-fonts)
low level parsing library and is also part of the
[oxidize](https://github.com/googlefonts/oxidize) project.
## Features
### Metadata
The following information is currently exposed:
* Global font metrics with variation support (units per em, ascender,
descender, etc)
* Glyph metrics with variation support (advance width, left side-bearing, etc)
* Codepoint to nominal glyph identifier mapping
* Unicode variation sequences
* Localized strings
* Attributes (stretch, style and weight)
* Variation axes and named instances
* Conversion from user coordinates to normalized design coordinates
Future goals include:
* Color palettes
* Embedded bitmap strikes
### Glyph formats
| Source | Decoding | Variations | Hinting |
|--------|----------|------------|---------|
| glyf | ✔️ | ✔️ | ✔️ |
| CFF | ✔️ | - | ✔️ |
| CFF2 | ✔️ | ✔️ | ✔️ |
| COLRv0 | ✔️ | - | - |
| COLRv1 | ✔️ | ✔️ | - |
| EBDT | ✔️* | - | - |
| CBDT | ✔️* | - | - |
| sbix | ✔️* | - | - |
\* Raw support available through the `read-fonts` crate.
## Panicking
This library should not panic regardless of API misuse or use of
corrupted/malicious font files. Please file an issue if this occurs.
## The name?
Following along with our theme, *skrifa* is Old Norse for "write" or "it is
written." And so it is named.
## Safety
Unsafe code is forbidden by a `#![forbid(unsafe_code)]` attribute in the root
of the library.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

277
vendor/skrifa/src/attribute.rs vendored Normal file
View File

@@ -0,0 +1,277 @@
//! Primary attributes typically used for font classification and selection.
use read_fonts::{
tables::{
head::{Head, MacStyle},
os2::{Os2, SelectionFlags},
post::Post,
},
FontRef, TableProvider,
};
/// Stretch, style and weight attributes of a font.
///
/// Variable fonts may contain axes that modify these attributes. The
/// [new](Self::new) method on this type returns values for the default
/// instance.
///
/// These are derived from values in the
/// [OS/2](https://learn.microsoft.com/en-us/typography/opentype/spec/os2) if
/// available. Otherwise, they are retrieved from the
/// [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head)
/// table.
#[derive(Copy, Clone, PartialEq, Debug, Default)]
pub struct Attributes {
pub stretch: Stretch,
pub style: Style,
pub weight: Weight,
}
impl Attributes {
/// Extracts the stretch, style and weight attributes for the default
/// instance of the given font.
pub fn new(font: &FontRef) -> Self {
if let Ok(os2) = font.os2() {
// Prefer values from the OS/2 table if it exists. We also use
// the post table to extract the angle for oblique styles.
Self::from_os2_post(os2, font.post().ok())
} else if let Ok(head) = font.head() {
// Otherwise, fall back to the macStyle field of the head table.
Self::from_head(head)
} else {
Self::default()
}
}
fn from_os2_post(os2: Os2, post: Option<Post>) -> Self {
let stretch = Stretch::from_width_class(os2.us_width_class());
// Bits 1 and 9 of the fsSelection field signify italic and
// oblique, respectively.
// See: <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#fsselection>
let fs_selection = os2.fs_selection();
let style = if fs_selection.contains(SelectionFlags::ITALIC) {
Style::Italic
} else if fs_selection.contains(SelectionFlags::OBLIQUE) {
let angle = post.map(|post| post.italic_angle().to_f64() as f32);
Style::Oblique(angle)
} else {
Style::Normal
};
// The usWeightClass field is specified with a 1-1000 range, but
// we don't clamp here because variable fonts could potentially
// have a value outside of that range.
// See <https://learn.microsoft.com/en-us/typography/opentype/spec/os2#usweightclass>
let weight = Weight(os2.us_weight_class() as f32);
Self {
stretch,
style,
weight,
}
}
fn from_head(head: Head) -> Self {
let mac_style = head.mac_style();
let style = mac_style
.contains(MacStyle::ITALIC)
.then_some(Style::Italic)
.unwrap_or_default();
let weight = mac_style
.contains(MacStyle::BOLD)
.then_some(Weight::BOLD)
.unwrap_or_default();
Self {
stretch: Stretch::default(),
style,
weight,
}
}
}
/// Visual width of a font-- a relative change from the normal aspect
/// ratio, typically in the range 0.5 to 2.0.
///
/// In variable fonts, this can be controlled with the `wdth` axis.
///
/// See <https://fonts.google.com/knowledge/glossary/width>
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct Stretch(f32);
impl Stretch {
/// Width that is 50% of normal.
pub const ULTRA_CONDENSED: Self = Self(0.5);
/// Width that is 62.5% of normal.
pub const EXTRA_CONDENSED: Self = Self(0.625);
/// Width that is 75% of normal.
pub const CONDENSED: Self = Self(0.75);
/// Width that is 87.5% of normal.
pub const SEMI_CONDENSED: Self = Self(0.875);
/// Width that is 100% of normal.
pub const NORMAL: Self = Self(1.0);
/// Width that is 112.5% of normal.
pub const SEMI_EXPANDED: Self = Self(1.125);
/// Width that is 125% of normal.
pub const EXPANDED: Self = Self(1.25);
/// Width that is 150% of normal.
pub const EXTRA_EXPANDED: Self = Self(1.5);
/// Width that is 200% of normal.
pub const ULTRA_EXPANDED: Self = Self(2.0);
}
impl Stretch {
/// Creates a new stretch attribute with the given ratio.
pub const fn new(ratio: f32) -> Self {
Self(ratio)
}
/// Creates a new stretch attribute from the
/// [usWidthClass](<https://learn.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass>)
/// field of the OS/2 table.
fn from_width_class(width_class: u16) -> Self {
// The specified range is 1-9 and Skia simply clamps out of range
// values. We follow.
// See <https://skia.googlesource.com/skia/+/21b7538fe0757d8cda31598bc9e5a6d0b4b54629/include/core/SkFontStyle.h#52>
match width_class {
0..=1 => Stretch::ULTRA_CONDENSED,
2 => Stretch::EXTRA_CONDENSED,
3 => Stretch::CONDENSED,
4 => Stretch::SEMI_CONDENSED,
5 => Stretch::NORMAL,
6 => Stretch::SEMI_EXPANDED,
7 => Stretch::EXPANDED,
8 => Stretch::EXTRA_EXPANDED,
_ => Stretch::ULTRA_EXPANDED,
}
}
/// Returns the stretch attribute as a ratio.
///
/// This is a linear scaling factor with 1.0 being "normal" width.
pub const fn ratio(self) -> f32 {
self.0
}
/// Returns the stretch attribute as a percentage value.
///
/// This is generally the value associated with the `wdth` axis.
pub fn percentage(self) -> f32 {
self.0 * 100.0
}
}
impl Default for Stretch {
fn default() -> Self {
Self::NORMAL
}
}
/// Visual style or 'slope' of a font.
///
/// In variable fonts, this can be controlled with the `ital`
/// and `slnt` axes for italic and oblique styles, respectively.
///
/// See <https://fonts.google.com/knowledge/glossary/style>
#[derive(Copy, Clone, PartialEq, Default, Debug)]
pub enum Style {
/// An upright or "roman" style.
#[default]
Normal,
/// Generally a slanted style, originally based on semi-cursive forms.
/// This often has a different structure from the normal style.
Italic,
/// Oblique (or slanted) style with an optional angle in degrees,
/// counter-clockwise from the vertical.
Oblique(Option<f32>),
}
/// Visual weight class of a font, typically on a scale from 1.0 to 1000.0.
///
/// In variable fonts, this can be controlled with the `wght` axis.
///
/// See <https://fonts.google.com/knowledge/glossary/weight>
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct Weight(f32);
impl Weight {
/// Weight value of 100.
pub const THIN: Self = Self(100.0);
/// Weight value of 200.
pub const EXTRA_LIGHT: Self = Self(200.0);
/// Weight value of 300.
pub const LIGHT: Self = Self(300.0);
/// Weight value of 350.
pub const SEMI_LIGHT: Self = Self(350.0);
/// Weight value of 400.
pub const NORMAL: Self = Self(400.0);
/// Weight value of 500.
pub const MEDIUM: Self = Self(500.0);
/// Weight value of 600.
pub const SEMI_BOLD: Self = Self(600.0);
/// Weight value of 700.
pub const BOLD: Self = Self(700.0);
/// Weight value of 800.
pub const EXTRA_BOLD: Self = Self(800.0);
/// Weight value of 900.
pub const BLACK: Self = Self(900.0);
/// Weight value of 950.
pub const EXTRA_BLACK: Self = Self(950.0);
}
impl Weight {
/// Creates a new weight attribute with the given value.
pub const fn new(weight: f32) -> Self {
Self(weight)
}
/// Returns the underlying weight value.
pub const fn value(self) -> f32 {
self.0
}
}
impl Default for Weight {
fn default() -> Self {
Self::NORMAL
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::prelude::*;
#[test]
fn missing_os2() {
let font = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
let attrs = font.attributes();
assert_eq!(attrs.stretch, Stretch::NORMAL);
assert_eq!(attrs.style, Style::Italic);
assert_eq!(attrs.weight, Weight::BOLD);
}
#[test]
fn so_stylish() {
let font = FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
let attrs = font.attributes();
assert_eq!(attrs.stretch, Stretch::SEMI_CONDENSED);
assert_eq!(attrs.style, Style::Oblique(Some(-14.0)));
assert_eq!(attrs.weight, Weight::EXTRA_BOLD);
}
}

482
vendor/skrifa/src/bitmap.rs vendored Normal file
View File

@@ -0,0 +1,482 @@
//! Bitmap strikes and glyphs.
use super::{instance::Size, metrics::GlyphMetrics, MetadataProvider};
use crate::prelude::LocationRef;
use raw::{
tables::{bitmap, cbdt, cblc, ebdt, eblc, sbix},
types::{GlyphId, Tag},
FontData, FontRef, TableProvider,
};
/// Set of strikes, each containing embedded bitmaps of a single size.
#[derive(Clone)]
pub struct BitmapStrikes<'a>(StrikesKind<'a>);
impl<'a> BitmapStrikes<'a> {
/// Creates a new `BitmapStrikes` for the given font.
///
/// This will prefer `sbix`, `CBDT`, and `CBLC` formats in that order.
///
/// To select a specific format, use [`with_format`](Self::with_format).
pub fn new(font: &FontRef<'a>) -> Self {
for format in [BitmapFormat::Sbix, BitmapFormat::Cbdt, BitmapFormat::Ebdt] {
if let Some(strikes) = Self::with_format(font, format) {
return strikes;
}
}
Self(StrikesKind::None)
}
/// Creates a new `BitmapStrikes` for the given font and format.
///
/// Returns `None` if the requested format is not available.
pub fn with_format(font: &FontRef<'a>, format: BitmapFormat) -> Option<Self> {
let kind = match format {
BitmapFormat::Sbix => StrikesKind::Sbix(
font.sbix().ok()?,
font.glyph_metrics(Size::unscaled(), LocationRef::default()),
),
BitmapFormat::Cbdt => {
StrikesKind::Cbdt(CbdtTables::new(font.cblc().ok()?, font.cbdt().ok()?))
}
BitmapFormat::Ebdt => {
StrikesKind::Ebdt(EbdtTables::new(font.eblc().ok()?, font.ebdt().ok()?))
}
};
Some(Self(kind))
}
/// Returns the format representing the underlying table for this set of
/// strikes.
pub fn format(&self) -> Option<BitmapFormat> {
match &self.0 {
StrikesKind::None => None,
StrikesKind::Sbix(..) => Some(BitmapFormat::Sbix),
StrikesKind::Cbdt(..) => Some(BitmapFormat::Cbdt),
StrikesKind::Ebdt(..) => Some(BitmapFormat::Ebdt),
}
}
/// Returns the number of available strikes.
pub fn len(&self) -> usize {
match &self.0 {
StrikesKind::None => 0,
StrikesKind::Sbix(sbix, _) => sbix.strikes().len(),
StrikesKind::Cbdt(cbdt) => cbdt.location.bitmap_sizes().len(),
StrikesKind::Ebdt(ebdt) => ebdt.location.bitmap_sizes().len(),
}
}
/// Returns true if there are no available strikes.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the strike at the given index.
pub fn get(&self, index: usize) -> Option<BitmapStrike<'a>> {
let kind = match &self.0 {
StrikesKind::None => return None,
StrikesKind::Sbix(sbix, metrics) => {
StrikeKind::Sbix(sbix.strikes().get(index).ok()?, metrics.clone())
}
StrikesKind::Cbdt(tables) => StrikeKind::Cbdt(
tables.location.bitmap_sizes().get(index).copied()?,
tables.clone(),
),
StrikesKind::Ebdt(tables) => StrikeKind::Ebdt(
tables.location.bitmap_sizes().get(index).copied()?,
tables.clone(),
),
};
Some(BitmapStrike(kind))
}
/// Returns the best matching glyph for the given size and glyph
/// identifier.
///
/// In this case, "best" means a glyph of the exact size, nearest larger
/// size, or nearest smaller size, in that order.
pub fn glyph_for_size(&self, size: Size, glyph_id: GlyphId) -> Option<BitmapGlyph<'a>> {
// Return the largest size for an unscaled request
let size = size.ppem().unwrap_or(f32::MAX);
self.iter()
.fold(None, |best: Option<BitmapGlyph<'a>>, entry| {
let entry_size = entry.ppem();
if let Some(best) = best {
let best_size = best.ppem_y;
if (entry_size >= size && entry_size < best_size)
|| (best_size < size && entry_size > best_size)
{
entry.get(glyph_id).or(Some(best))
} else {
Some(best)
}
} else {
entry.get(glyph_id)
}
})
}
/// Returns an iterator over all available strikes.
pub fn iter(&self) -> impl Iterator<Item = BitmapStrike<'a>> + 'a + Clone {
let this = self.clone();
(0..this.len()).filter_map(move |ix| this.get(ix))
}
}
#[derive(Clone)]
enum StrikesKind<'a> {
None,
Sbix(sbix::Sbix<'a>, GlyphMetrics<'a>),
Cbdt(CbdtTables<'a>),
Ebdt(EbdtTables<'a>),
}
/// Set of embedded bitmap glyphs of a specific size.
#[derive(Clone)]
pub struct BitmapStrike<'a>(StrikeKind<'a>);
impl<'a> BitmapStrike<'a> {
/// Returns the pixels-per-em (size) of this strike.
pub fn ppem(&self) -> f32 {
match &self.0 {
StrikeKind::Sbix(sbix, _) => sbix.ppem() as f32,
// Original implementation also considers `ppem_y` here:
// https://github.com/google/skia/blob/02cd0561f4f756bf4f7b16641d8fc4c61577c765/src/ports/fontations/src/bitmap.rs#L48
StrikeKind::Cbdt(size, _) => size.ppem_y() as f32,
StrikeKind::Ebdt(size, _) => size.ppem_y() as f32,
}
}
/// Returns a bitmap glyph for the given identifier, if available.
pub fn get(&self, glyph_id: GlyphId) -> Option<BitmapGlyph<'a>> {
match &self.0 {
StrikeKind::Sbix(sbix, metrics) => {
let glyph = sbix.glyph_data(glyph_id).ok()??;
if glyph.graphic_type() != Tag::new(b"png ") {
return None;
}
// Note that this calculation does not entirely correspond to the description in
// the specification, but it's implemented this way in Skia (https://github.com/google/skia/blob/02cd0561f4f756bf4f7b16641d8fc4c61577c765/src/ports/fontations/src/bitmap.rs#L161-L178),
// the implementation of which has been tested against behavior in CoreText.
let glyf_bb = metrics.bounds(glyph_id).unwrap_or_default();
let lsb = metrics.left_side_bearing(glyph_id).unwrap_or_default();
let ppem = sbix.ppem() as f32;
let png_data = glyph.data();
// PNG format:
// 8 byte header, IHDR chunk (4 byte length, 4 byte chunk type), width, height
let reader = FontData::new(png_data);
let width = reader.read_at::<u32>(16).ok()?;
let height = reader.read_at::<u32>(20).ok()?;
Some(BitmapGlyph {
data: BitmapData::Png(glyph.data()),
bearing_x: lsb,
bearing_y: glyf_bb.y_min,
inner_bearing_x: glyph.origin_offset_x() as f32,
inner_bearing_y: glyph.origin_offset_y() as f32,
ppem_x: ppem,
ppem_y: ppem,
width,
height,
advance: None,
placement_origin: Origin::BottomLeft,
})
}
StrikeKind::Cbdt(size, tables) => {
let location = size
.location(tables.location.offset_data(), glyph_id)
.ok()?;
let data = tables.data.data(&location).ok()?;
BitmapGlyph::from_bdt(size, &data)
}
StrikeKind::Ebdt(size, tables) => {
let location = size
.location(tables.location.offset_data(), glyph_id)
.ok()?;
let data = tables.data.data(&location).ok()?;
BitmapGlyph::from_bdt(size, &data)
}
}
}
}
#[derive(Clone)]
enum StrikeKind<'a> {
Sbix(sbix::Strike<'a>, GlyphMetrics<'a>),
Cbdt(bitmap::BitmapSize, CbdtTables<'a>),
Ebdt(bitmap::BitmapSize, EbdtTables<'a>),
}
#[derive(Clone)]
struct BdtTables<L, D> {
location: L,
data: D,
}
impl<L, D> BdtTables<L, D> {
fn new(location: L, data: D) -> Self {
Self { location, data }
}
}
type CbdtTables<'a> = BdtTables<cblc::Cblc<'a>, cbdt::Cbdt<'a>>;
type EbdtTables<'a> = BdtTables<eblc::Eblc<'a>, ebdt::Ebdt<'a>>;
/// An embedded bitmap glyph.
#[derive(Clone)]
pub struct BitmapGlyph<'a> {
/// The underlying data of the bitmap glyph.
pub data: BitmapData<'a>,
/// Outer glyph bearings in the x direction, given in font units.
pub bearing_x: f32,
/// Outer glyph bearings in the y direction, given in font units.
pub bearing_y: f32,
/// Inner glyph bearings in the x direction, given in pixels. This value should be scaled
/// by `ppem_*` and be applied as an offset when placing the image within the bounds rectangle.
pub inner_bearing_x: f32,
/// Inner glyph bearings in the y direction, given in pixels. This value should be scaled
/// by `ppem_*` and be applied as an offset when placing the image within the bounds rectangle.
pub inner_bearing_y: f32,
/// The assumed pixels-per-em in the x direction.
pub ppem_x: f32,
/// The assumed pixels-per-em in the y direction.
pub ppem_y: f32,
/// The horizontal advance width of the bitmap glyph in pixels, if given.
pub advance: Option<f32>,
/// The number of columns in the bitmap.
pub width: u32,
/// The number of rows in the bitmap.
pub height: u32,
/// The placement origin of the bitmap.
pub placement_origin: Origin,
}
impl<'a> BitmapGlyph<'a> {
fn from_bdt(
bitmap_size: &bitmap::BitmapSize,
bitmap_data: &bitmap::BitmapData<'a>,
) -> Option<Self> {
let metrics = BdtMetrics::new(bitmap_data);
let (ppem_x, ppem_y) = (bitmap_size.ppem_x() as f32, bitmap_size.ppem_y() as f32);
let bpp = bitmap_size.bit_depth();
let data = match bpp {
32 => {
match &bitmap_data.content {
bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::Png, bytes) => {
BitmapData::Png(bytes)
}
// 32-bit formats are always byte aligned
bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::ByteAligned, bytes) => {
BitmapData::Bgra(bytes)
}
_ => return None,
}
}
1 | 2 | 4 | 8 => {
let (data, is_packed) = match &bitmap_data.content {
bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::ByteAligned, bytes) => {
(bytes, false)
}
bitmap::BitmapContent::Data(bitmap::BitmapDataFormat::BitAligned, bytes) => {
(bytes, true)
}
_ => return None,
};
BitmapData::Mask(MaskData {
bpp,
is_packed,
data,
})
}
// All other bit depth values are invalid
_ => return None,
};
Some(Self {
data,
bearing_x: 0.0,
bearing_y: 0.0,
inner_bearing_x: metrics.inner_bearing_x,
inner_bearing_y: metrics.inner_bearing_y,
ppem_x,
ppem_y,
width: metrics.width,
height: metrics.height,
advance: Some(metrics.advance),
placement_origin: Origin::TopLeft,
})
}
}
struct BdtMetrics {
inner_bearing_x: f32,
inner_bearing_y: f32,
advance: f32,
width: u32,
height: u32,
}
impl BdtMetrics {
fn new(data: &bitmap::BitmapData) -> Self {
match data.metrics {
bitmap::BitmapMetrics::Small(metrics) => Self {
inner_bearing_x: metrics.bearing_x() as f32,
inner_bearing_y: metrics.bearing_y() as f32,
advance: metrics.advance() as f32,
width: metrics.width() as u32,
height: metrics.height() as u32,
},
bitmap::BitmapMetrics::Big(metrics) => Self {
inner_bearing_x: metrics.hori_bearing_x() as f32,
inner_bearing_y: metrics.hori_bearing_y() as f32,
advance: metrics.hori_advance() as f32,
width: metrics.width() as u32,
height: metrics.height() as u32,
},
}
}
}
///The origin point for drawing a bitmap glyph.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Origin {
/// The origin is in the top-left.
TopLeft,
/// The origin is in the bottom-left.
BottomLeft,
}
/// Data content of a bitmap.
#[derive(Clone)]
pub enum BitmapData<'a> {
/// Uncompressed 32-bit color bitmap data, pre-multiplied in BGRA order
/// and encoded in the sRGB color space.
Bgra(&'a [u8]),
/// Compressed PNG bitmap data.
Png(&'a [u8]),
/// Data representing a single channel alpha mask.
Mask(MaskData<'a>),
}
/// A single channel alpha mask.
#[derive(Clone)]
pub struct MaskData<'a> {
/// Number of bits-per-pixel. Always 1, 2, 4 or 8.
pub bpp: u8,
/// True if each row of the data is bit-aligned. Otherwise, each row
/// is padded to the next byte.
pub is_packed: bool,
/// Raw bitmap data.
pub data: &'a [u8],
}
/// The format (or table) containing the data backing a set of bitmap strikes.
#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
pub enum BitmapFormat {
Sbix,
Cbdt,
Ebdt,
}
#[cfg(test)]
mod tests {
use crate::bitmap::{BitmapData, StrikesKind};
use crate::prelude::Size;
use crate::{GlyphId, MetadataProvider};
use raw::FontRef;
#[test]
fn cbdt_metadata() {
let font = FontRef::new(font_test_data::CBDT).unwrap();
let strikes = font.bitmap_strikes();
assert!(matches!(strikes.0, StrikesKind::Cbdt(_)));
assert!(matches!(strikes.len(), 3));
// Note that this is only `ppem_y`.
assert!(matches!(strikes.get(0).unwrap().ppem(), 16.0));
assert!(matches!(strikes.get(1).unwrap().ppem(), 64.0));
assert!(matches!(strikes.get(2).unwrap().ppem(), 128.0));
}
#[test]
fn cbdt_glyph_metrics() {
let font = FontRef::new(font_test_data::CBDT).unwrap();
let strike_0 = font.bitmap_strikes().get(0).unwrap();
let zero = strike_0.get(GlyphId::new(0)).unwrap();
assert_eq!(zero.width, 11);
assert_eq!(zero.height, 13);
assert_eq!(zero.bearing_x, 0.0);
assert_eq!(zero.bearing_y, 0.0);
assert_eq!(zero.inner_bearing_x, 1.0);
assert_eq!(zero.inner_bearing_y, 13.0);
assert_eq!(zero.advance, Some(12.0));
let strike_1 = font.bitmap_strikes().get(1).unwrap();
let zero = strike_1.get(GlyphId::new(2)).unwrap();
assert_eq!(zero.width, 39);
assert_eq!(zero.height, 52);
assert_eq!(zero.bearing_x, 0.0);
assert_eq!(zero.bearing_y, 0.0);
assert_eq!(zero.inner_bearing_x, 6.0);
assert_eq!(zero.inner_bearing_y, 52.0);
assert_eq!(zero.advance, Some(51.0));
}
#[test]
fn cbdt_glyph_selection() {
let font = FontRef::new(font_test_data::CBDT).unwrap();
let strikes = font.bitmap_strikes();
let g1 = strikes
.glyph_for_size(Size::new(12.0), GlyphId::new(2))
.unwrap();
assert_eq!(g1.ppem_x, 16.0);
let g2 = strikes
.glyph_for_size(Size::new(17.0), GlyphId::new(2))
.unwrap();
assert_eq!(g2.ppem_x, 64.0);
let g3 = strikes
.glyph_for_size(Size::new(60.0), GlyphId::new(2))
.unwrap();
assert_eq!(g3.ppem_x, 64.0);
let g4 = strikes
.glyph_for_size(Size::unscaled(), GlyphId::new(2))
.unwrap();
assert_eq!(g4.ppem_x, 128.0);
}
#[test]
fn sbix_metadata() {
let font = FontRef::new(font_test_data::NOTO_HANDWRITING_SBIX).unwrap();
let strikes = font.bitmap_strikes();
assert!(matches!(strikes.0, StrikesKind::Sbix(_, _)));
assert!(matches!(strikes.len(), 1));
assert!(matches!(strikes.get(0).unwrap().ppem(), 109.0));
}
#[test]
fn sbix_glyph_metrics() {
let font = FontRef::new(font_test_data::NOTO_HANDWRITING_SBIX).unwrap();
let strike_0 = font.bitmap_strikes().get(0).unwrap();
let g0 = strike_0.get(GlyphId::new(7)).unwrap();
// `bearing_x` is always the lsb, which is 0 for this glyph.
assert_eq!(g0.bearing_x, 0.0);
// The glyph doesn't have an associated outline, so `bbox.min_y` is 0, and thus bearing_y
// should also be 0.
assert_eq!(g0.bearing_y, 0.0);
// Origin offsets are 4.0 and -27.0 respectively.
assert_eq!(g0.inner_bearing_x, 4.0);
assert_eq!(g0.inner_bearing_y, -27.0);
assert!(matches!(g0.data, BitmapData::Png(_)))
}
}

531
vendor/skrifa/src/charmap.rs vendored Normal file
View File

@@ -0,0 +1,531 @@
//! Mapping of characters (codepoints, not graphemes) to nominal glyph identifiers.
//!
//! If you have never run into character to glyph mapping before
//! [Glyph IDs and the 'cmap' table](https://rsheeter.github.io/font101/#glyph-ids-and-the-cmap-table)
//! might be informative.
//!
//! The functionality in this module provides a 1-to-1 mapping from Unicode
//! characters (or [Unicode variation sequences](http://unicode.org/faq/vs.html)) to
//! nominal or "default" internal glyph identifiers for a given font.
//! This is a necessary first step, but generally insufficient for proper layout of
//! [complex text](https://en.wikipedia.org/wiki/Complex_text_layout) or even
//! simple text containing diacritics and ligatures.
//!
//! Comprehensive mapping of characters to positioned glyphs requires a process called
//! shaping. For more detail, see: [Why do I need a shaping engine?](https://harfbuzz.github.io/why-do-i-need-a-shaping-engine.html)
use read_fonts::{
tables::cmap::{
self, Cmap, Cmap12, Cmap12Iter, Cmap14, Cmap14Iter, Cmap4, Cmap4Iter, CmapIterLimits,
CmapSubtable, EncodingRecord, PlatformId,
},
types::GlyphId,
FontData, FontRef, TableProvider,
};
pub use read_fonts::tables::cmap::MapVariant;
/// Mapping of characters to nominal glyph identifiers.
///
/// The mappings are derived from the [cmap](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap)
/// table. Depending on the font, the returned mapping may have entries that point to virtual/phantom glyph
/// ids beyond the `num_glyphs` entry of the `maxp` table, which are only used during the shaping process,
/// for example.
///
/// ## Obtaining a Charmap
///
/// Typically a Charmap is acquired by calling [charmap](crate::MetadataProvider::charmap) on a [FontRef].
///
/// ## Selection strategy
///
/// Fonts may contain multiple subtables in various formats supporting different encodings. The selection
/// strategy implemented here is designed to choose mappings that capture the broadest available Unicode
/// coverage:
///
/// * Unicode characters: a symbol mapping subtable is selected if available. Otherwise, subtables supporting
/// the Unicode full repertoire or Basic Multilingual Plane (BMP) are preferred, in that order. Formats
/// [4](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-4-segment-mapping-to-delta-values)
/// and [12](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-12-segmented-coverage) are
/// supported.
///
/// * Unicode variation sequences: these are provided by a format
/// [14](https://learn.microsoft.com/en-us/typography/opentype/spec/cmap#format-14-unicode-variation-sequences)
/// subtable.
///
#[derive(Clone, Default)]
pub struct Charmap<'a> {
codepoint_subtable: Option<CodepointSubtable<'a>>,
variant_subtable: Option<Cmap14<'a>>,
cmap12_limits: CmapIterLimits,
}
impl<'a> Charmap<'a> {
/// Creates a new character map from the given font.
pub fn new(font: &FontRef<'a>) -> Self {
let Ok(cmap) = font.cmap() else {
return Default::default();
};
let selection = MappingSelection::new(font, &cmap);
Self {
codepoint_subtable: selection
.codepoint_subtable
.map(|subtable| CodepointSubtable {
subtable,
is_symbol: selection.mapping_index.codepoint_subtable_is_symbol,
}),
variant_subtable: selection.variant_subtable,
cmap12_limits: selection.mapping_index.cmap12_limits,
}
}
/// Returns true if a suitable Unicode character mapping is available.
pub fn has_map(&self) -> bool {
self.codepoint_subtable.is_some()
}
/// Returns true if a symbol mapping was selected.
pub fn is_symbol(&self) -> bool {
self.codepoint_subtable
.as_ref()
.map(|x| x.is_symbol)
.unwrap_or(false)
}
/// Returns true if a Unicode variation sequence mapping is available.
pub fn has_variant_map(&self) -> bool {
self.variant_subtable.is_some()
}
/// Maps a character to a nominal glyph identifier.
///
/// Returns `None` if a mapping does not exist.
pub fn map(&self, ch: impl Into<u32>) -> Option<GlyphId> {
self.codepoint_subtable.as_ref()?.map(ch.into())
}
/// Returns an iterator over all mappings of codepoint to nominal glyph
/// identifiers in the character map.
pub fn mappings(&self) -> Mappings<'a> {
self.codepoint_subtable
.as_ref()
.map(|subtable| {
Mappings(match &subtable.subtable {
SupportedSubtable::Format4(cmap4) => MappingsInner::Format4(cmap4.iter()),
SupportedSubtable::Format12(cmap12) => {
MappingsInner::Format12(cmap12.iter_with_limits(self.cmap12_limits))
}
})
})
.unwrap_or(Mappings(MappingsInner::None))
}
/// Maps a character and variation selector to a nominal glyph identifier.
///
/// Returns `None` if a mapping does not exist.
pub fn map_variant(&self, ch: impl Into<u32>, selector: impl Into<u32>) -> Option<MapVariant> {
self.variant_subtable.as_ref()?.map_variant(ch, selector)
}
/// Returns an iterator over all mappings of character and variation
/// selector to nominal glyph identifier in the character map.
pub fn variant_mappings(&self) -> VariantMappings<'a> {
VariantMappings(self.variant_subtable.clone().map(|cmap14| cmap14.iter()))
}
}
/// Cacheable indices of selected mapping tables for materializing a character
/// map.
///
/// Since [`Charmap`] carries a lifetime, it is difficult to store in a cache.
/// This type serves as an acceleration structure that allows for construction
/// of a character map while skipping the search for the most suitable Unicode
/// mappings.
#[derive(Copy, Clone, Default, Debug)]
pub struct MappingIndex {
/// Index of Unicode or symbol mapping subtable.
codepoint_subtable: Option<u16>,
/// True if the above is a symbol mapping.
codepoint_subtable_is_symbol: bool,
/// Index of Unicode variation selector subtable.
variant_subtable: Option<u16>,
/// Limits for iterating a cmap format 12 subtable.
cmap12_limits: CmapIterLimits,
}
impl MappingIndex {
/// Finds the indices of the most suitable Unicode mapping tables in the
/// given font.
pub fn new(font: &FontRef) -> Self {
let Ok(cmap) = font.cmap() else {
return Default::default();
};
MappingSelection::new(font, &cmap).mapping_index
}
/// Creates a new character map for the given font using the tables referenced by
/// the precomputed indices.
///
/// The font should be the same as the one used to construct this object.
pub fn charmap<'a>(&self, font: &FontRef<'a>) -> Charmap<'a> {
let Ok(cmap) = font.cmap() else {
return Default::default();
};
let records = cmap.encoding_records();
let data = cmap.offset_data();
Charmap {
codepoint_subtable: self
.codepoint_subtable
.and_then(|index| get_subtable(data, records, index))
.and_then(SupportedSubtable::new)
.map(|subtable| CodepointSubtable {
subtable,
is_symbol: self.codepoint_subtable_is_symbol,
}),
variant_subtable: self
.variant_subtable
.and_then(|index| get_subtable(data, records, index))
.and_then(|subtable| match subtable {
CmapSubtable::Format14(cmap14) => Some(cmap14),
_ => None,
}),
cmap12_limits: CmapIterLimits::default_for_font(font),
}
}
}
/// Iterator over all mappings of character to nominal glyph identifier
/// in a character map.
///
/// This is created with the [`Charmap::mappings`] method.
#[derive(Clone)]
pub struct Mappings<'a>(MappingsInner<'a>);
impl Iterator for Mappings<'_> {
type Item = (u32, GlyphId);
fn next(&mut self) -> Option<Self::Item> {
loop {
let item = match &mut self.0 {
MappingsInner::None => None,
MappingsInner::Format4(iter) => iter.next(),
MappingsInner::Format12(iter) => iter.next(),
}?;
if item.1 != GlyphId::NOTDEF {
return Some(item);
}
}
}
}
#[derive(Clone)]
enum MappingsInner<'a> {
None,
Format4(Cmap4Iter<'a>),
Format12(Cmap12Iter<'a>),
}
/// Iterator over all mappings of character and variation selector to
/// nominal glyph identifier in a character map.
///
/// This is created with the [`Charmap::variant_mappings`] method.
#[derive(Clone)]
pub struct VariantMappings<'a>(Option<Cmap14Iter<'a>>);
impl Iterator for VariantMappings<'_> {
type Item = (u32, u32, MapVariant);
fn next(&mut self) -> Option<Self::Item> {
self.0.as_mut()?.next()
}
}
fn get_subtable<'a>(
data: FontData<'a>,
records: &[EncodingRecord],
index: u16,
) -> Option<CmapSubtable<'a>> {
records
.get(index as usize)
.and_then(|record| record.subtable(data).ok())
}
#[derive(Clone)]
struct CodepointSubtable<'a> {
subtable: SupportedSubtable<'a>,
/// True if the subtable is a symbol mapping.
is_symbol: bool,
}
impl CodepointSubtable<'_> {
fn map(&self, codepoint: u32) -> Option<GlyphId> {
self.map_impl(codepoint).or_else(|| {
if self.is_symbol && codepoint <= 0x00FF {
// From HarfBuzz:
// For symbol-encoded OpenType fonts, we duplicate the
// U+F000..F0FF range at U+0000..U+00FF. That's what
// Windows seems to do, and that's hinted about at:
// https://docs.microsoft.com/en-us/typography/opentype/spec/recom
// under "Non-Standard (Symbol) Fonts".
// See <https://github.com/harfbuzz/harfbuzz/blob/453ded05392af38bba9f89587edce465e86ffa6b/src/hb-ot-cmap-table.hh#L1595>
self.map_impl(codepoint + 0xF000)
} else {
None
}
})
}
fn map_impl(&self, codepoint: u32) -> Option<GlyphId> {
let gid = match &self.subtable {
SupportedSubtable::Format4(subtable) => subtable.map_codepoint(codepoint),
SupportedSubtable::Format12(subtable) => subtable.map_codepoint(codepoint),
}?;
(gid != GlyphId::NOTDEF).then_some(gid)
}
}
#[derive(Clone)]
enum SupportedSubtable<'a> {
Format4(Cmap4<'a>),
Format12(Cmap12<'a>),
}
impl<'a> SupportedSubtable<'a> {
fn new(subtable: CmapSubtable<'a>) -> Option<Self> {
Some(match subtable {
CmapSubtable::Format4(cmap4) => Self::Format4(cmap4),
CmapSubtable::Format12(cmap12) => Self::Format12(cmap12),
_ => return None,
})
}
fn from_cmap_record(cmap: &Cmap<'a>, record: &cmap::EncodingRecord) -> Option<Self> {
Self::new(record.subtable(cmap.offset_data()).ok()?)
}
}
/// The mapping kind of a cmap subtable.
///
/// The ordering is significant and determines the priority of subtable
/// selection (greater is better).
#[derive(Copy, Clone, PartialEq, PartialOrd)]
enum MappingKind {
None = 0,
UnicodeBmp = 1,
UnicodeFull = 2,
Symbol = 3,
}
/// The result of searching the cmap table for the "best" available
/// subtables.
///
/// For `codepoint_subtable`, best means either symbol (which is preferred)
/// or a Unicode subtable with the greatest coverage.
///
/// For `variant_subtable`, best means a format 14 subtable.
struct MappingSelection<'a> {
/// The mapping index accelerator that holds indices of the following
/// subtables.
mapping_index: MappingIndex,
/// Either a symbol subtable or the Unicode subtable with the
/// greatest coverage.
codepoint_subtable: Option<SupportedSubtable<'a>>,
/// Subtable that supports mapping Unicode variation sequences.
variant_subtable: Option<Cmap14<'a>>,
}
impl<'a> MappingSelection<'a> {
fn new(font: &FontRef<'a>, cmap: &Cmap<'a>) -> Self {
const ENCODING_MS_SYMBOL: u16 = 0;
const ENCODING_MS_UNICODE_CS: u16 = 1;
const ENCODING_APPLE_ID_UNICODE_32: u16 = 4;
const ENCODING_APPLE_ID_VARIANT_SELECTOR: u16 = 5;
const ENCODING_MS_ID_UCS_4: u16 = 10;
let mut mapping_index = MappingIndex::default();
let mut mapping_kind = MappingKind::None;
let mut codepoint_subtable = None;
let mut variant_subtable = None;
let mut maybe_choose_subtable = |kind, index, subtable| {
if kind > mapping_kind {
mapping_kind = kind;
mapping_index.codepoint_subtable_is_symbol = kind == MappingKind::Symbol;
mapping_index.codepoint_subtable = Some(index as u16);
codepoint_subtable = Some(subtable);
}
};
// This generally follows the same strategy as FreeType, searching the encoding
// records in reverse and prioritizing UCS-4 subtables over UCS-2.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/ac5babe87629107c43f627e2cd17c6cf4f2ecd43/src/base/ftobjs.c#L1370>
// The exception is that we prefer a symbol subtable over all others which matches the behavior
// of HarfBuzz.
// See <https://github.com/harfbuzz/harfbuzz/blob/453ded05392af38bba9f89587edce465e86ffa6b/src/hb-ot-cmap-table.hh#L1818>
for (i, record) in cmap.encoding_records().iter().enumerate().rev() {
match (record.platform_id(), record.encoding_id()) {
(PlatformId::Unicode, ENCODING_APPLE_ID_VARIANT_SELECTOR) => {
// Unicode variation sequences
if let Ok(CmapSubtable::Format14(subtable)) =
record.subtable(cmap.offset_data())
{
if variant_subtable.is_none() {
mapping_index.variant_subtable = Some(i as u16);
variant_subtable = Some(subtable);
}
}
}
(PlatformId::Windows, ENCODING_MS_SYMBOL) => {
// Symbol
if let Some(subtable) = SupportedSubtable::from_cmap_record(cmap, record) {
maybe_choose_subtable(MappingKind::Symbol, i, subtable);
}
}
(PlatformId::Windows, ENCODING_MS_ID_UCS_4)
| (PlatformId::Unicode, ENCODING_APPLE_ID_UNICODE_32) => {
// Unicode full repertoire
if let Some(subtable) = SupportedSubtable::from_cmap_record(cmap, record) {
maybe_choose_subtable(MappingKind::UnicodeFull, i, subtable);
}
}
(PlatformId::ISO, _)
| (PlatformId::Unicode, _)
| (PlatformId::Windows, ENCODING_MS_UNICODE_CS) => {
// Unicode BMP only
if let Some(subtable) = SupportedSubtable::from_cmap_record(cmap, record) {
maybe_choose_subtable(MappingKind::UnicodeBmp, i, subtable);
}
}
_ => {}
}
}
mapping_index.cmap12_limits = CmapIterLimits::default_for_font(font);
Self {
mapping_index,
codepoint_subtable,
variant_subtable,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MetadataProvider;
use read_fonts::FontRef;
#[test]
fn choose_format_12_over_4() {
let font = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
let charmap = font.charmap();
assert!(matches!(
charmap.codepoint_subtable.unwrap().subtable,
SupportedSubtable::Format12(..)
));
}
#[test]
fn choose_format_4() {
let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
let charmap = font.charmap();
assert!(matches!(
charmap.codepoint_subtable.unwrap().subtable,
SupportedSubtable::Format4(..)
));
}
#[test]
fn choose_symbol() {
let font = FontRef::new(font_test_data::CMAP4_SYMBOL_PUA).unwrap();
let charmap = font.charmap();
assert!(charmap.is_symbol());
assert!(matches!(
charmap.codepoint_subtable.unwrap().subtable,
SupportedSubtable::Format4(..)
));
}
#[test]
fn map_format_4() {
let font = FontRef::new(font_test_data::VAZIRMATN_VAR).unwrap();
let charmap = font.charmap();
assert_eq!(charmap.map('A'), Some(GlyphId::new(1)));
assert_eq!(charmap.map('À'), Some(GlyphId::new(2)));
assert_eq!(charmap.map('`'), Some(GlyphId::new(3)));
assert_eq!(charmap.map('B'), None);
}
#[test]
fn map_format_12() {
let font = FontRef::new(font_test_data::CMAP12_FONT1).unwrap();
let charmap = font.charmap();
assert_eq!(charmap.map(' '), None);
assert_eq!(charmap.map(0x101723_u32), Some(GlyphId::new(1)));
assert_eq!(charmap.map(0x101725_u32), Some(GlyphId::new(3)));
assert_eq!(charmap.map(0x102523_u32), Some(GlyphId::new(6)));
assert_eq!(charmap.map(0x102526_u32), Some(GlyphId::new(9)));
assert_eq!(charmap.map(0x102527_u32), Some(GlyphId::new(10)));
}
#[test]
fn map_symbol_pua() {
let font = FontRef::new(font_test_data::CMAP4_SYMBOL_PUA).unwrap();
let charmap = font.charmap();
assert!(charmap.codepoint_subtable.as_ref().unwrap().is_symbol);
assert_eq!(charmap.map(0xF001_u32), Some(GlyphId::new(1)));
assert_eq!(charmap.map(0xF002_u32), Some(GlyphId::new(2)));
assert_eq!(charmap.map(0xF003_u32), Some(GlyphId::new(3)));
assert_eq!(charmap.map(0xF0FE_u32), Some(GlyphId::new(4)));
// The following don't exist in the cmap table and are remapped into the U+F000..F0FF range
// due to the selection of a symbol mapping subtable.
assert_eq!(charmap.map(0x1_u32), Some(GlyphId::new(1)));
assert_eq!(charmap.map(0x2_u32), Some(GlyphId::new(2)));
assert_eq!(charmap.map(0x3_u32), Some(GlyphId::new(3)));
assert_eq!(charmap.map(0xFE_u32), Some(GlyphId::new(4)));
}
#[test]
fn map_variants() {
use super::MapVariant::*;
let font = FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
let charmap = font.charmap();
let selector = '\u{e0100}';
assert_eq!(charmap.map_variant('a', selector), None);
assert_eq!(charmap.map_variant('\u{4e00}', selector), Some(UseDefault));
assert_eq!(charmap.map_variant('\u{4e06}', selector), Some(UseDefault));
assert_eq!(
charmap.map_variant('\u{4e08}', selector),
Some(Variant(GlyphId::new(25)))
);
assert_eq!(
charmap.map_variant('\u{4e09}', selector),
Some(Variant(GlyphId::new(26)))
);
}
#[test]
fn mappings() {
for font_data in [
font_test_data::VAZIRMATN_VAR,
font_test_data::CMAP12_FONT1,
font_test_data::SIMPLE_GLYF,
font_test_data::CMAP4_SYMBOL_PUA,
] {
let font = FontRef::new(font_data).unwrap();
let charmap = font.charmap();
for (codepoint, glyph_id) in charmap.mappings() {
assert_ne!(
glyph_id,
GlyphId::NOTDEF,
"we should never encounter notdef glyphs"
);
assert_eq!(charmap.map(codepoint), Some(glyph_id));
}
}
}
#[test]
fn variant_mappings() {
let font = FontRef::new(font_test_data::CMAP14_FONT1).unwrap();
let charmap = font.charmap();
for (codepoint, selector, variant) in charmap.variant_mappings() {
assert_eq!(charmap.map_variant(codepoint, selector), Some(variant));
}
}
}

352
vendor/skrifa/src/collections.rs vendored Normal file
View File

@@ -0,0 +1,352 @@
//! Internal "small" style collection types.
use alloc::vec::Vec;
use core::hash::{Hash, Hasher};
/// A growable vector type with inline storage optimization.
///
/// Note that unlike the real `SmallVec`, this only works with types that
/// are `Copy + Default` to simplify our implementation.
#[derive(Clone)]
pub(crate) struct SmallVec<T, const N: usize>(Storage<T, N>);
impl<T, const N: usize> SmallVec<T, N>
where
T: Copy + Default,
{
/// Creates a new, empty `SmallVec<T>`.
pub fn new() -> Self {
Self(Storage::Inline([T::default(); N], 0))
}
/// Creates a new `SmallVec<T>` of the given length with each element
/// containing a copy of `value`.
pub fn with_len(len: usize, value: T) -> Self {
if len <= N {
Self(Storage::Inline([value; N], len))
} else {
let mut vec = Vec::new();
vec.resize(len, value);
Self(Storage::Heap(vec))
}
}
/// Clears the vector, removing all values.
pub fn clear(&mut self) {
match &mut self.0 {
Storage::Inline(_buf, len) => *len = 0,
Storage::Heap(vec) => vec.clear(),
}
}
/// Tries to reserve capacity for at least `additional` more elements.
pub fn try_reserve(&mut self, additional: usize) -> bool {
match &mut self.0 {
Storage::Inline(buf, len) => {
let new_cap = *len + additional;
if new_cap > N {
let mut vec = Vec::new();
if vec.try_reserve(new_cap).is_err() {
return false;
}
vec.extend_from_slice(&buf[..*len]);
self.0 = Storage::Heap(vec);
}
}
Storage::Heap(vec) => {
if vec.try_reserve(additional).is_err() {
return false;
}
}
}
true
}
/// Appends an element to the back of the collection.
pub fn push(&mut self, value: T) {
match &mut self.0 {
Storage::Inline(buf, len) => {
if *len + 1 > N {
let mut vec = Vec::with_capacity(*len + 1);
vec.extend_from_slice(&buf[..*len]);
vec.push(value);
self.0 = Storage::Heap(vec);
} else {
buf[*len] = value;
*len += 1;
}
}
Storage::Heap(vec) => vec.push(value),
}
}
/// Removes and returns the value at the back of the collection.
pub fn pop(&mut self) -> Option<T> {
match &mut self.0 {
Storage::Inline(buf, len) => {
if *len > 0 {
*len -= 1;
Some(buf[*len])
} else {
None
}
}
Storage::Heap(vec) => vec.pop(),
}
}
/// Shortens the vector, keeping the first `len` elements.
pub fn truncate(&mut self, len: usize) {
match &mut self.0 {
Storage::Inline(_buf, inline_len) => {
*inline_len = len.min(*inline_len);
}
Storage::Heap(vec) => vec.truncate(len),
}
}
}
impl<T, const N: usize> SmallVec<T, N> {
/// Extracts a slice containing the entire vector.
pub fn as_slice(&self) -> &[T] {
match &self.0 {
Storage::Inline(buf, len) => &buf[..*len],
Storage::Heap(vec) => vec.as_slice(),
}
}
/// Extracts a mutable slice containing the entire vector.
pub fn as_mut_slice(&mut self) -> &mut [T] {
match &mut self.0 {
Storage::Inline(buf, len) => &mut buf[..*len],
Storage::Heap(vec) => vec.as_mut_slice(),
}
}
}
impl<T, const N: usize> Default for SmallVec<T, N>
where
T: Copy + Default,
{
fn default() -> Self {
Self::new()
}
}
impl<T, const N: usize> core::ops::Deref for SmallVec<T, N> {
type Target = [T];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl<T, const N: usize> core::ops::DerefMut for SmallVec<T, N> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.as_mut_slice()
}
}
impl<T, const N: usize> Hash for SmallVec<T, N>
where
T: Hash,
{
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_slice().hash(state);
}
}
impl<T, const N: usize> PartialEq for SmallVec<T, N>
where
T: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.as_slice() == other.as_slice()
}
}
impl<T, const N: usize> PartialEq<[T]> for SmallVec<T, N>
where
T: PartialEq,
{
fn eq(&self, other: &[T]) -> bool {
self.as_slice() == other
}
}
impl<T, const N: usize> Eq for SmallVec<T, N> where T: Eq {}
impl<T, const N: usize> core::fmt::Debug for SmallVec<T, N>
where
T: core::fmt::Debug,
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_list().entries(self.as_slice().iter()).finish()
}
}
impl<'a, T, const N: usize> IntoIterator for &'a SmallVec<T, N> {
type IntoIter = core::slice::Iter<'a, T>;
type Item = &'a T;
fn into_iter(self) -> Self::IntoIter {
self.as_slice().iter()
}
}
impl<'a, T, const N: usize> IntoIterator for &'a mut SmallVec<T, N> {
type IntoIter = core::slice::IterMut<'a, T>;
type Item = &'a mut T;
fn into_iter(self) -> Self::IntoIter {
self.as_mut_slice().iter_mut()
}
}
impl<T, const N: usize> IntoIterator for SmallVec<T, N>
where
T: Copy,
{
type IntoIter = IntoIter<T, N>;
type Item = T;
fn into_iter(self) -> Self::IntoIter {
IntoIter { vec: self, pos: 0 }
}
}
#[derive(Clone)]
pub(crate) struct IntoIter<T, const N: usize> {
vec: SmallVec<T, N>,
pos: usize,
}
impl<T, const N: usize> Iterator for IntoIter<T, N>
where
T: Copy,
{
type Item = T;
fn next(&mut self) -> Option<Self::Item> {
let value = self.vec.get(self.pos)?;
self.pos += 1;
Some(*value)
}
}
#[derive(Clone)]
enum Storage<T, const N: usize> {
Inline([T; N], usize),
Heap(Vec<T>),
}
#[cfg(test)]
mod test {
use super::{SmallVec, Storage};
#[test]
fn choose_inline() {
let vec = SmallVec::<_, 4>::with_len(4, 0);
assert!(matches!(vec.0, Storage::Inline(..)));
assert_eq!(vec.len(), 4);
}
#[test]
fn choose_heap() {
let vec = SmallVec::<_, 4>::with_len(5, 0);
assert!(matches!(vec.0, Storage::Heap(..)));
assert_eq!(vec.len(), 5);
}
#[test]
fn store_and_read_inline() {
let mut vec = SmallVec::<_, 8>::with_len(8, 0);
for (i, value) in vec.iter_mut().enumerate() {
*value = i * 2;
}
let expected = [0, 2, 4, 6, 8, 10, 12, 14];
assert_eq!(vec.as_slice(), &expected);
assert_eq!(format!("{vec:?}"), format!("{expected:?}"));
}
#[test]
fn store_and_read_heap() {
let mut vec = SmallVec::<_, 4>::with_len(8, 0);
for (i, value) in vec.iter_mut().enumerate() {
*value = i * 2;
}
let expected = [0, 2, 4, 6, 8, 10, 12, 14];
assert_eq!(vec.as_slice(), &expected);
assert_eq!(format!("{vec:?}"), format!("{expected:?}"));
}
#[test]
fn spill_to_heap() {
let mut vec = SmallVec::<_, 4>::new();
for i in 0..4 {
vec.push(i);
}
assert!(matches!(vec.0, Storage::Inline(..)));
vec.push(4);
assert!(matches!(vec.0, Storage::Heap(..)));
let expected = [0, 1, 2, 3, 4];
assert_eq!(vec.as_slice(), &expected);
}
#[test]
fn clear_inline() {
let mut vec = SmallVec::<_, 4>::new();
for i in 0..4 {
vec.push(i);
}
assert!(matches!(vec.0, Storage::Inline(..)));
assert_eq!(vec.len(), 4);
vec.clear();
assert_eq!(vec.len(), 0);
}
#[test]
fn clear_heap() {
let mut vec = SmallVec::<_, 3>::new();
for i in 0..4 {
vec.push(i);
}
assert!(matches!(vec.0, Storage::Heap(..)));
assert_eq!(vec.len(), 4);
vec.clear();
assert_eq!(vec.len(), 0);
}
#[test]
fn reserve() {
let mut vec = SmallVec::<_, 3>::new();
for i in 0..2 {
vec.push(i);
}
assert!(matches!(vec.0, Storage::Inline(..)));
assert!(vec.try_reserve(1));
// still inline after reserving 1
assert!(matches!(vec.0, Storage::Inline(..)));
assert!(vec.try_reserve(2));
// reserving 2 spills to heap
assert!(matches!(vec.0, Storage::Heap(..)));
}
#[test]
fn iter() {
let mut vec = SmallVec::<_, 3>::new();
for i in 0..3 {
vec.push(i);
}
assert!(&[0, 1, 2].iter().eq(vec.iter()));
}
#[test]
fn into_iter() {
let mut vec = SmallVec::<_, 3>::new();
for i in 0..3 {
vec.push(i);
}
assert!([0, 1, 2].into_iter().eq(vec.into_iter()));
}
}

597
vendor/skrifa/src/color/instance.rs vendored Normal file
View File

@@ -0,0 +1,597 @@
//! COLR table instance.
use read_fonts::{
tables::{
colr::*,
variations::{
DeltaSetIndex, DeltaSetIndexMap, FloatItemDelta, FloatItemDeltaTarget,
ItemVariationStore,
},
},
types::{BoundingBox, F2Dot14, GlyphId16, Point},
ReadError,
};
use core::ops::{Deref, Range};
/// Unique paint identifier used for detecting cycles in the paint graph.
pub type PaintId = usize;
/// Combination of a `COLR` table and a location in variation space for
/// resolving paints.
///
/// See [`resolve_paint`], [`ColorStops::resolve`] and [`resolve_clip_box`].
#[derive(Clone)]
pub struct ColrInstance<'a> {
colr: Colr<'a>,
index_map: Option<DeltaSetIndexMap<'a>>,
var_store: Option<ItemVariationStore<'a>>,
coords: &'a [F2Dot14],
}
impl<'a> ColrInstance<'a> {
/// Creates a new instance for the given `COLR` table and normalized variation
/// coordinates.
pub fn new(colr: Colr<'a>, coords: &'a [F2Dot14]) -> Self {
let index_map = colr.var_index_map().and_then(|res| res.ok());
let var_store = colr.item_variation_store().and_then(|res| res.ok());
Self {
colr,
coords,
index_map,
var_store,
}
}
/// Computes a sequence of N variation deltas starting at the given
/// `var_base` index.
fn var_deltas<const N: usize>(&self, var_index_base: u32) -> [FloatItemDelta; N] {
// Magic value that indicates deltas should not be applied.
const NO_VARIATION_DELTAS: u32 = 0xFFFFFFFF;
// Note: FreeType never returns an error for these lookups, so
// we do the same and just `unwrap_or_default` on var store
// errors.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/fc01e7dd/src/sfnt/ttcolr.c#L574>
let mut deltas = [FloatItemDelta::ZERO; N];
if self.coords.is_empty()
|| self.var_store.is_none()
|| var_index_base == NO_VARIATION_DELTAS
{
return deltas;
}
let var_store = self.var_store.as_ref().unwrap();
if let Some(index_map) = self.index_map.as_ref() {
for (i, delta) in deltas.iter_mut().enumerate() {
let var_index = var_index_base + i as u32;
if let Ok(delta_ix) = index_map.get(var_index) {
*delta = var_store
.compute_float_delta(delta_ix, self.coords)
.unwrap_or_default();
}
}
} else {
for (i, delta) in deltas.iter_mut().enumerate() {
let var_index = var_index_base + i as u32;
// If we don't have a var index map, use our index as the inner
// component and set the outer to 0.
let delta_ix = DeltaSetIndex {
outer: 0,
inner: var_index as u16,
};
*delta = var_store
.compute_float_delta(delta_ix, self.coords)
.unwrap_or_default();
}
}
deltas
}
}
impl<'a> Deref for ColrInstance<'a> {
type Target = Colr<'a>;
fn deref(&self) -> &Self::Target {
&self.colr
}
}
/// Resolves a clip box, applying variation deltas using the given
/// instance.
pub fn resolve_clip_box(instance: &ColrInstance, clip_box: &ClipBox) -> BoundingBox<f32> {
match clip_box {
ClipBox::Format1(cbox) => BoundingBox {
x_min: cbox.x_min().to_i16() as f32,
y_min: cbox.y_min().to_i16() as f32,
x_max: cbox.x_max().to_i16() as f32,
y_max: cbox.y_max().to_i16() as f32,
},
ClipBox::Format2(cbox) => {
let deltas = instance.var_deltas::<4>(cbox.var_index_base());
BoundingBox {
x_min: cbox.x_min().apply_float_delta(deltas[0]),
y_min: cbox.y_min().apply_float_delta(deltas[1]),
x_max: cbox.x_max().apply_float_delta(deltas[2]),
y_max: cbox.y_max().apply_float_delta(deltas[3]),
}
}
}
}
/// Simplified version of a [`ColorStop`] or [`VarColorStop`] with applied
/// variation deltas.
#[derive(Clone, Debug)]
pub struct ResolvedColorStop {
pub offset: f32,
pub palette_index: u16,
pub alpha: f32,
}
/// Collection of [`ColorStop`] or [`VarColorStop`].
// Note: only one of these fields is used at any given time, but this structure
// was chosen over the obvious enum approach for simplicity in generating a
// single concrete type for the `impl Iterator` return type of the `resolve`
// method.
#[derive(Clone)]
pub struct ColorStops<'a> {
stops: &'a [ColorStop],
var_stops: &'a [VarColorStop],
}
impl ColorStops<'_> {
pub fn len(&self) -> usize {
self.stops.len() + self.var_stops.len()
}
pub fn is_empty(&self) -> bool {
self.stops.is_empty() && self.var_stops.is_empty()
}
}
impl<'a> From<ColorLine<'a>> for ColorStops<'a> {
fn from(value: ColorLine<'a>) -> Self {
Self {
stops: value.color_stops(),
var_stops: &[],
}
}
}
impl<'a> From<VarColorLine<'a>> for ColorStops<'a> {
fn from(value: VarColorLine<'a>) -> Self {
Self {
stops: &[],
var_stops: value.color_stops(),
}
}
}
impl<'a> ColorStops<'a> {
/// Returns an iterator yielding resolved color stops with variation deltas
/// applied.
pub fn resolve(
&self,
instance: &'a ColrInstance<'a>,
) -> impl Iterator<Item = ResolvedColorStop> + 'a {
self.stops
.iter()
.map(|stop| ResolvedColorStop {
offset: stop.stop_offset().to_f32(),
palette_index: stop.palette_index(),
alpha: stop.alpha().to_f32(),
})
.chain(self.var_stops.iter().map(|stop| {
let deltas = instance.var_deltas::<2>(stop.var_index_base());
ResolvedColorStop {
offset: stop.stop_offset().apply_float_delta(deltas[0]),
palette_index: stop.palette_index(),
alpha: stop.alpha().apply_float_delta(deltas[1]),
}
}))
}
}
/// Simplified version of `Paint` with applied variation deltas.
///
/// These are constructed with the [`resolve_paint`] function.
///
/// This is roughly equivalent to FreeType's
/// [`FT_COLR_Paint`](https://freetype.org/freetype2/docs/reference/ft2-layer_management.html#ft_colr_paint)
/// type.
pub enum ResolvedPaint<'a> {
ColrLayers {
range: Range<usize>,
},
Solid {
palette_index: u16,
alpha: f32,
},
LinearGradient {
x0: f32,
y0: f32,
x1: f32,
y1: f32,
x2: f32,
y2: f32,
color_stops: ColorStops<'a>,
extend: Extend,
},
RadialGradient {
x0: f32,
y0: f32,
radius0: f32,
x1: f32,
y1: f32,
radius1: f32,
color_stops: ColorStops<'a>,
extend: Extend,
},
SweepGradient {
center_x: f32,
center_y: f32,
start_angle: f32,
end_angle: f32,
color_stops: ColorStops<'a>,
extend: Extend,
},
Glyph {
glyph_id: GlyphId16,
paint: Paint<'a>,
},
ColrGlyph {
glyph_id: GlyphId16,
},
Transform {
xx: f32,
yx: f32,
xy: f32,
yy: f32,
dx: f32,
dy: f32,
paint: Paint<'a>,
},
Translate {
dx: f32,
dy: f32,
paint: Paint<'a>,
},
Scale {
scale_x: f32,
scale_y: f32,
around_center: Option<Point<f32>>,
paint: Paint<'a>,
},
Rotate {
angle: f32,
around_center: Option<Point<f32>>,
paint: Paint<'a>,
},
Skew {
x_skew_angle: f32,
y_skew_angle: f32,
around_center: Option<Point<f32>>,
paint: Paint<'a>,
},
Composite {
source_paint: Paint<'a>,
mode: CompositeMode,
backdrop_paint: Paint<'a>,
},
}
/// Resolves this paint with the given instance.
///
/// Resolving means that all numeric values are converted to 32-bit floating
/// point, variation deltas are applied (also computed fully in floating
/// point), and the various transform paints are collapsed into a single value
/// for their category (transform, translate, scale, rotate and skew).
///
/// This provides a simpler type for consumers that are more interested
/// in extracting the semantics of the graph rather than working with the
/// raw encoded structures.
pub fn resolve_paint<'a>(
instance: &ColrInstance<'a>,
paint: &Paint<'a>,
) -> Result<ResolvedPaint<'a>, ReadError> {
Ok(match paint {
Paint::ColrLayers(layers) => {
let start = layers.first_layer_index() as usize;
ResolvedPaint::ColrLayers {
range: start..start + layers.num_layers() as usize,
}
}
Paint::Solid(solid) => ResolvedPaint::Solid {
palette_index: solid.palette_index(),
alpha: solid.alpha().to_f32(),
},
Paint::VarSolid(solid) => {
let deltas = instance.var_deltas::<1>(solid.var_index_base());
ResolvedPaint::Solid {
palette_index: solid.palette_index(),
alpha: solid.alpha().apply_float_delta(deltas[0]),
}
}
Paint::LinearGradient(gradient) => {
let color_line = gradient.color_line()?;
let extend = color_line.extend();
ResolvedPaint::LinearGradient {
x0: gradient.x0().to_i16() as f32,
y0: gradient.y0().to_i16() as f32,
x1: gradient.x1().to_i16() as f32,
y1: gradient.y1().to_i16() as f32,
x2: gradient.x2().to_i16() as f32,
y2: gradient.y2().to_i16() as f32,
color_stops: color_line.into(),
extend,
}
}
Paint::VarLinearGradient(gradient) => {
let color_line = gradient.color_line()?;
let extend = color_line.extend();
let deltas = instance.var_deltas::<6>(gradient.var_index_base());
ResolvedPaint::LinearGradient {
x0: gradient.x0().apply_float_delta(deltas[0]),
y0: gradient.y0().apply_float_delta(deltas[1]),
x1: gradient.x1().apply_float_delta(deltas[2]),
y1: gradient.y1().apply_float_delta(deltas[3]),
x2: gradient.x2().apply_float_delta(deltas[4]),
y2: gradient.y2().apply_float_delta(deltas[5]),
color_stops: color_line.into(),
extend,
}
}
Paint::RadialGradient(gradient) => {
let color_line = gradient.color_line()?;
let extend = color_line.extend();
ResolvedPaint::RadialGradient {
x0: gradient.x0().to_i16() as f32,
y0: gradient.y0().to_i16() as f32,
radius0: gradient.radius0().to_u16() as f32,
x1: gradient.x1().to_i16() as f32,
y1: gradient.y1().to_i16() as f32,
radius1: gradient.radius1().to_u16() as f32,
color_stops: color_line.into(),
extend,
}
}
Paint::VarRadialGradient(gradient) => {
let color_line = gradient.color_line()?;
let extend = color_line.extend();
let deltas = instance.var_deltas::<6>(gradient.var_index_base());
ResolvedPaint::RadialGradient {
x0: gradient.x0().apply_float_delta(deltas[0]),
y0: gradient.y0().apply_float_delta(deltas[1]),
radius0: gradient.radius0().apply_float_delta(deltas[2]),
x1: gradient.x1().apply_float_delta(deltas[3]),
y1: gradient.y1().apply_float_delta(deltas[4]),
radius1: gradient.radius1().apply_float_delta(deltas[5]),
color_stops: color_line.into(),
extend,
}
}
Paint::SweepGradient(gradient) => {
let color_line = gradient.color_line()?;
let extend = color_line.extend();
ResolvedPaint::SweepGradient {
center_x: gradient.center_x().to_i16() as f32,
center_y: gradient.center_y().to_i16() as f32,
start_angle: gradient.start_angle().to_f32(),
end_angle: gradient.end_angle().to_f32(),
color_stops: color_line.into(),
extend,
}
}
Paint::VarSweepGradient(gradient) => {
let color_line = gradient.color_line()?;
let extend = color_line.extend();
let deltas = instance.var_deltas::<4>(gradient.var_index_base());
ResolvedPaint::SweepGradient {
center_x: gradient.center_x().apply_float_delta(deltas[0]),
center_y: gradient.center_y().apply_float_delta(deltas[1]),
start_angle: gradient.start_angle().apply_float_delta(deltas[2]),
end_angle: gradient.end_angle().apply_float_delta(deltas[3]),
color_stops: color_line.into(),
extend,
}
}
Paint::Glyph(glyph) => ResolvedPaint::Glyph {
glyph_id: glyph.glyph_id(),
paint: glyph.paint()?,
},
Paint::ColrGlyph(glyph) => ResolvedPaint::ColrGlyph {
glyph_id: glyph.glyph_id(),
},
Paint::Transform(transform) => {
let affine = transform.transform()?;
let paint = transform.paint()?;
ResolvedPaint::Transform {
xx: affine.xx().to_f32(),
yx: affine.yx().to_f32(),
xy: affine.xy().to_f32(),
yy: affine.yy().to_f32(),
dx: affine.dx().to_f32(),
dy: affine.dy().to_f32(),
paint,
}
}
Paint::VarTransform(transform) => {
let affine = transform.transform()?;
let paint = transform.paint()?;
let deltas = instance.var_deltas::<6>(affine.var_index_base());
ResolvedPaint::Transform {
xx: affine.xx().apply_float_delta(deltas[0]),
yx: affine.yx().apply_float_delta(deltas[1]),
xy: affine.xy().apply_float_delta(deltas[2]),
yy: affine.yy().apply_float_delta(deltas[3]),
dx: affine.dx().apply_float_delta(deltas[4]),
dy: affine.dy().apply_float_delta(deltas[5]),
paint,
}
}
Paint::Translate(transform) => ResolvedPaint::Translate {
dx: transform.dx().to_i16() as f32,
dy: transform.dy().to_i16() as f32,
paint: transform.paint()?,
},
Paint::VarTranslate(transform) => {
let deltas = instance.var_deltas::<2>(transform.var_index_base());
ResolvedPaint::Translate {
dx: transform.dx().apply_float_delta(deltas[0]),
dy: transform.dy().apply_float_delta(deltas[1]),
paint: transform.paint()?,
}
}
Paint::Scale(transform) => ResolvedPaint::Scale {
scale_x: transform.scale_x().to_f32(),
scale_y: transform.scale_y().to_f32(),
around_center: None,
paint: transform.paint()?,
},
Paint::VarScale(transform) => {
let deltas = instance.var_deltas::<2>(transform.var_index_base());
ResolvedPaint::Scale {
scale_x: transform.scale_x().apply_float_delta(deltas[0]),
scale_y: transform.scale_y().apply_float_delta(deltas[1]),
around_center: None,
paint: transform.paint()?,
}
}
Paint::ScaleAroundCenter(transform) => ResolvedPaint::Scale {
scale_x: transform.scale_x().to_f32(),
scale_y: transform.scale_y().to_f32(),
around_center: Some(Point::new(
transform.center_x().to_i16() as f32,
transform.center_y().to_i16() as f32,
)),
paint: transform.paint()?,
},
Paint::VarScaleAroundCenter(transform) => {
let deltas = instance.var_deltas::<4>(transform.var_index_base());
ResolvedPaint::Scale {
scale_x: transform.scale_x().apply_float_delta(deltas[0]),
scale_y: transform.scale_y().apply_float_delta(deltas[1]),
around_center: Some(Point::new(
transform.center_x().apply_float_delta(deltas[2]),
transform.center_y().apply_float_delta(deltas[3]),
)),
paint: transform.paint()?,
}
}
Paint::ScaleUniform(transform) => {
let scale = transform.scale().to_f32();
ResolvedPaint::Scale {
scale_x: scale,
scale_y: scale,
around_center: None,
paint: transform.paint()?,
}
}
Paint::VarScaleUniform(transform) => {
let deltas = instance.var_deltas::<1>(transform.var_index_base());
let scale = transform.scale().apply_float_delta(deltas[0]);
ResolvedPaint::Scale {
scale_x: scale,
scale_y: scale,
around_center: None,
paint: transform.paint()?,
}
}
Paint::ScaleUniformAroundCenter(transform) => {
let scale = transform.scale().to_f32();
ResolvedPaint::Scale {
scale_x: scale,
scale_y: scale,
around_center: Some(Point::new(
transform.center_x().to_i16() as f32,
transform.center_y().to_i16() as f32,
)),
paint: transform.paint()?,
}
}
Paint::VarScaleUniformAroundCenter(transform) => {
let deltas = instance.var_deltas::<3>(transform.var_index_base());
let scale = transform.scale().apply_float_delta(deltas[0]);
ResolvedPaint::Scale {
scale_x: scale,
scale_y: scale,
around_center: Some(Point::new(
transform.center_x().apply_float_delta(deltas[1]),
transform.center_y().apply_float_delta(deltas[2]),
)),
paint: transform.paint()?,
}
}
Paint::Rotate(transform) => ResolvedPaint::Rotate {
angle: transform.angle().to_f32(),
around_center: None,
paint: transform.paint()?,
},
Paint::VarRotate(transform) => {
let deltas = instance.var_deltas::<1>(transform.var_index_base());
ResolvedPaint::Rotate {
angle: transform.angle().apply_float_delta(deltas[0]),
around_center: None,
paint: transform.paint()?,
}
}
Paint::RotateAroundCenter(transform) => ResolvedPaint::Rotate {
angle: transform.angle().to_f32(),
around_center: Some(Point::new(
transform.center_x().to_i16() as f32,
transform.center_y().to_i16() as f32,
)),
paint: transform.paint()?,
},
Paint::VarRotateAroundCenter(transform) => {
let deltas = instance.var_deltas::<3>(transform.var_index_base());
ResolvedPaint::Rotate {
angle: transform.angle().apply_float_delta(deltas[0]),
around_center: Some(Point::new(
transform.center_x().apply_float_delta(deltas[1]),
transform.center_y().apply_float_delta(deltas[2]),
)),
paint: transform.paint()?,
}
}
Paint::Skew(transform) => ResolvedPaint::Skew {
x_skew_angle: transform.x_skew_angle().to_f32(),
y_skew_angle: transform.y_skew_angle().to_f32(),
around_center: None,
paint: transform.paint()?,
},
Paint::VarSkew(transform) => {
let deltas = instance.var_deltas::<2>(transform.var_index_base());
ResolvedPaint::Skew {
x_skew_angle: transform.x_skew_angle().apply_float_delta(deltas[0]),
y_skew_angle: transform.y_skew_angle().apply_float_delta(deltas[1]),
around_center: None,
paint: transform.paint()?,
}
}
Paint::SkewAroundCenter(transform) => ResolvedPaint::Skew {
x_skew_angle: transform.x_skew_angle().to_f32(),
y_skew_angle: transform.y_skew_angle().to_f32(),
around_center: Some(Point::new(
transform.center_x().to_i16() as f32,
transform.center_y().to_i16() as f32,
)),
paint: transform.paint()?,
},
Paint::VarSkewAroundCenter(transform) => {
let deltas = instance.var_deltas::<4>(transform.var_index_base());
ResolvedPaint::Skew {
x_skew_angle: transform.x_skew_angle().apply_float_delta(deltas[0]),
y_skew_angle: transform.y_skew_angle().apply_float_delta(deltas[1]),
around_center: Some(Point::new(
transform.center_x().apply_float_delta(deltas[2]),
transform.center_y().apply_float_delta(deltas[3]),
)),
paint: transform.paint()?,
}
}
Paint::Composite(composite) => ResolvedPaint::Composite {
source_paint: composite.source_paint()?,
mode: composite.composite_mode(),
backdrop_paint: composite.backdrop_paint()?,
},
})
}

555
vendor/skrifa/src/color/mod.rs vendored Normal file
View File

@@ -0,0 +1,555 @@
//! Drawing color glyphs.
//!
//! # Examples
//! ## Retrieve the clip box of a COLRv1 glyph if it has one:
//!
//! ```
//! # use core::result::Result;
//! # use skrifa::{instance::{Size, Location}, color::{ColorGlyphFormat, ColorPainter, PaintError}, GlyphId, MetadataProvider};
//! # fn get_colr_bb(font: read_fonts::FontRef, color_painter_impl : &mut impl ColorPainter, glyph_id : GlyphId, size: Size) -> Result<(), PaintError> {
//! match font.color_glyphs()
//! .get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
//! .expect("Glyph not found.")
//! .bounding_box(&Location::default(), size)
//! {
//! Some(bounding_box) => {
//! println!("Bounding box is {:?}", bounding_box);
//! }
//! None => {
//! println!("Glyph has no clip box.");
//! }
//! }
//! # Ok(())
//! # }
//! ```
//!
//! ## Paint a COLRv1 glyph given a font, and a glyph id and a [`ColorPainter`] implementation:
//! ```
//! # use core::result::Result;
//! # use skrifa::{instance::{Size, Location}, color::{ColorGlyphFormat, ColorPainter, PaintError}, GlyphId, MetadataProvider};
//! # fn paint_colr(font: read_fonts::FontRef, color_painter_impl : &mut impl ColorPainter, glyph_id : GlyphId) -> Result<(), PaintError> {
//! let color_glyph = font.color_glyphs()
//! .get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
//! .expect("Glyph not found");
//! color_glyph.paint(&Location::default(), color_painter_impl)
//! # }
//! ```
//!
mod instance;
mod transform;
mod traversal;
#[cfg(test)]
mod traversal_tests;
use raw::{tables::colr, FontRef};
#[cfg(test)]
use serde::{Deserialize, Serialize};
pub use read_fonts::tables::colr::{CompositeMode, Extend};
use read_fonts::{
types::{BoundingBox, GlyphId, Point},
ReadError, TableProvider,
};
use std::{fmt::Debug, ops::Range};
use traversal::{
get_clipbox_font_units, traverse_v0_range, traverse_with_callbacks, PaintDecycler,
};
pub use transform::Transform;
use crate::prelude::{LocationRef, Size};
use self::instance::{resolve_paint, PaintId};
/// An error during drawing a COLR glyph.
///
/// This covers inconsistencies in the COLRv1 paint graph as well as downstream
/// parse errors from read-fonts.
#[derive(Debug, Clone)]
pub enum PaintError {
ParseError(ReadError),
GlyphNotFound(GlyphId),
PaintCycleDetected,
DepthLimitExceeded,
}
impl std::fmt::Display for PaintError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
PaintError::ParseError(read_error) => {
write!(f, "Error parsing font data: {read_error}")
}
PaintError::GlyphNotFound(glyph_id) => {
write!(f, "No COLRv1 glyph found for glyph id: {glyph_id}")
}
PaintError::PaintCycleDetected => write!(f, "Paint cycle detected in COLRv1 glyph."),
PaintError::DepthLimitExceeded => write!(f, "Depth limit exceeded in COLRv1 glyph."),
}
}
}
impl From<ReadError> for PaintError {
fn from(value: ReadError) -> Self {
PaintError::ParseError(value)
}
}
/// A color stop of a gradient.
///
/// All gradient callbacks of [`ColorPainter`] normalize color stops to be in the range of 0
/// to 1.
#[derive(Copy, Clone, PartialEq, Debug, Default)]
#[cfg_attr(test, derive(Serialize, Deserialize))]
// This repr(C) is required so that C-side FFI's
// are able to cast the ColorStop slice to a C-side array pointer.
#[repr(C)]
pub struct ColorStop {
pub offset: f32,
/// Specifies a color from the `CPAL` table.
pub palette_index: u16,
/// Additional alpha value, to be multiplied with the color above before use.
pub alpha: f32,
}
// Design considerations for choosing a slice of ColorStops as `color_stop`
// type: In principle, a local `Vec<ColorStop>` allocation would not required if
// we're willing to walk the `ResolvedColorStop` iterator to find the minimum
// and maximum color stops. Then we could scale the color stops based on the
// minimum and maximum. But performing the min/max search would require
// re-applying the deltas at least once, after which we would pass the scaled
// stops to client side and have the client sort the collected items once
// again. If we do want to pre-ort them, and still use use an
// `Iterator<Item=ColorStop>` instead as the `color_stops` field, then we would
// need a Fontations-side allocations to sort, and an extra allocation on the
// client side to `.collect()` from the provided iterator before passing it to
// drawing API.
//
/// A fill type of a COLRv1 glyph (solid fill or various gradient types).
///
/// The client receives the information about the fill type in the
/// [`fill`](ColorPainter::fill) callback of the [`ColorPainter`] trait.
#[derive(Debug, PartialEq)]
pub enum Brush<'a> {
/// A solid fill with the color specified by `palette_index`. The respective
/// color from the CPAL table then needs to be multiplied with `alpha`.
Solid { palette_index: u16, alpha: f32 },
/// A linear gradient, normalized from the P0, P1 and P2 representation in
/// the COLRv1 table to a linear gradient between two points `p0` and
/// `p1`. If there is only one color stop, the client should draw a solid
/// fill with that color. The `color_stops` are normalized to the range from
/// 0 to 1.
LinearGradient {
p0: Point<f32>,
p1: Point<f32>,
color_stops: &'a [ColorStop],
extend: Extend,
},
/// A radial gradient, with color stops normalized to the range of 0 to 1.
/// Caution: This normalization can mean that negative radii occur. It is
/// the client's responsibility to truncate the color line at the 0
/// position, interpolating between `r0` and `r1` and compute an
/// interpolated color at that position.
RadialGradient {
c0: Point<f32>,
r0: f32,
c1: Point<f32>,
r1: f32,
color_stops: &'a [ColorStop],
extend: Extend,
},
/// A sweep gradient, also called conical gradient. The color stops are
/// normalized to the range from 0 to 1 and the returned angles are to be
/// interpreted in _clockwise_ direction (swapped from the meaning in the
/// font file). The stop normalization may mean that the angles may be
/// larger or smaller than the range of 0 to 360. Note that only the range
/// from 0 to 360 degrees is to be drawn, see
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients>.
SweepGradient {
c0: Point<f32>,
start_angle: f32,
end_angle: f32,
color_stops: &'a [ColorStop],
extend: Extend,
},
}
/// Signals success of request to draw a COLRv1 sub glyph from cache.
///
/// Result of [`paint_cached_color_glyph`](ColorPainter::paint_cached_color_glyph)
/// through which the client signals whether a COLRv1 glyph referenced by
/// another COLRv1 glyph was drawn from cache or whether the glyph's subgraph
/// should be traversed by the skria side COLRv1 implementation.
pub enum PaintCachedColorGlyph {
/// The specified COLRv1 glyph has been successfully painted client side.
Ok,
/// The client does not implement drawing COLRv1 glyphs from cache and the
/// Fontations side COLRv1 implementation is asked to traverse the
/// respective PaintColorGlyph sub graph.
Unimplemented,
}
/// A group of required painting callbacks to be provided by the client.
///
/// Each callback is executing a particular drawing or canvas transformation
/// operation. The trait's callback functions are invoked when
/// [`paint`](ColorGlyph::paint) is called with a [`ColorPainter`] trait
/// object. The documentation for each function describes what actions are to be
/// executed using the client side 2D graphics API, usually by performing some
/// kind of canvas operation.
pub trait ColorPainter {
/// Push the specified transform by concatenating it to the current
/// transformation matrix.
fn push_transform(&mut self, transform: Transform);
/// Restore the transformation matrix to the state before the previous
/// [`push_transform`](ColorPainter::push_transform) call.
fn pop_transform(&mut self);
/// Apply a clip path in the shape of glyph specified by `glyph_id`.
fn push_clip_glyph(&mut self, glyph_id: GlyphId);
/// Apply a clip rectangle specified by `clip_rect`.
fn push_clip_box(&mut self, clip_box: BoundingBox<f32>);
/// Restore the clip state to the state before a previous
/// [`push_clip_glyph`](ColorPainter::push_clip_glyph) or
/// [`push_clip_box`](ColorPainter::push_clip_box) call.
fn pop_clip(&mut self);
/// Fill the current clip area with the specified gradient fill.
fn fill(&mut self, brush: Brush<'_>);
/// Combined clip and fill operation.
///
/// Apply the clip path determined by the specified `glyph_id`, then fill it
/// with the specified [`brush`](Brush), applying the `_brush_transform`
/// transformation matrix to the brush. The default implementation works
/// based on existing methods in this trait. It is recommended for clients
/// to override the default implementaition with a custom combined clip and
/// fill operation. In this way overriding likely results in performance
/// gains depending on performance characteristics of the 2D graphics stack
/// that these calls are mapped to.
fn fill_glyph(
&mut self,
glyph_id: GlyphId,
brush_transform: Option<Transform>,
brush: Brush<'_>,
) {
self.push_clip_glyph(glyph_id);
if let Some(wrap_in_transform) = brush_transform {
self.push_transform(wrap_in_transform);
self.fill(brush);
self.pop_transform();
} else {
self.fill(brush);
}
self.pop_clip();
}
/// Optionally implement this method: Draw an unscaled COLRv1 glyph given
/// the current transformation matrix (as accumulated by
/// [`push_transform`](ColorPainter::push_transform) calls).
fn paint_cached_color_glyph(
&mut self,
_glyph: GlyphId,
) -> Result<PaintCachedColorGlyph, PaintError> {
Ok(PaintCachedColorGlyph::Unimplemented)
}
/// Open a new layer, and merge the layer down using `composite_mode` when
/// [`pop_layer`](ColorPainter::pop_layer) is called, signalling that this layer is done drawing.
fn push_layer(&mut self, composite_mode: CompositeMode);
/// Merge the pushed layer down using `composite_mode` passed to the matching
/// [`push_layer`](ColorPainter::push_layer).
fn pop_layer(&mut self) {}
/// Alternative version of [`push_layer`](ColorPainter::push_layer) where the
/// `composite_mode` is also passed to the method. This is useful for
/// graphics libraries that need the compositing mode at layer pop time
/// and do not want to manually track the mode.
///
/// Only one of [`pop_layer`](ColorPainter::pop_layer) or this method
/// need to be implemented. By default, this simply calls
/// [`pop_layer`](ColorPainter::pop_layer).
fn pop_layer_with_mode(&mut self, _composite_mode: CompositeMode) {
self.pop_layer();
}
}
/// Distinguishes available color glyph formats.
#[derive(Clone, Copy)]
pub enum ColorGlyphFormat {
ColrV0,
ColrV1,
}
/// A representation of a color glyph that can be painted through a sequence of [`ColorPainter`] callbacks.
#[derive(Clone)]
pub struct ColorGlyph<'a> {
colr: colr::Colr<'a>,
root_paint_ref: ColorGlyphRoot<'a>,
}
#[derive(Clone)]
enum ColorGlyphRoot<'a> {
V0Range(Range<usize>),
V1Paint(colr::Paint<'a>, PaintId, GlyphId, Result<u16, ReadError>),
}
impl<'a> ColorGlyph<'a> {
/// Returns the version of the color table from which this outline was
/// selected.
pub fn format(&self) -> ColorGlyphFormat {
match &self.root_paint_ref {
ColorGlyphRoot::V0Range(_) => ColorGlyphFormat::ColrV0,
ColorGlyphRoot::V1Paint(..) => ColorGlyphFormat::ColrV1,
}
}
/// Returns the bounding box.
///
/// For COLRv1 glyphs, this is the clip box of the specified COLRv1 glyph,
/// or `None` if clip boxes are not present or if there is none for the
/// particular glyph.
///
/// Always returns `None` for COLRv0 glyphs because precomputed clip boxes
/// are never available.
///
/// The `size` argument can optionally be used to scale the bounding box
/// to a particular font size and `location` allows specifying a variation
/// instance.
pub fn bounding_box(
&self,
location: impl Into<LocationRef<'a>>,
size: Size,
) -> Option<BoundingBox<f32>> {
match &self.root_paint_ref {
ColorGlyphRoot::V1Paint(_paint, _paint_id, glyph_id, upem) => {
let instance =
instance::ColrInstance::new(self.colr.clone(), location.into().coords());
let resolved_bounding_box = get_clipbox_font_units(&instance, *glyph_id);
resolved_bounding_box.map(|bounding_box| {
let scale_factor = size.linear_scale((*upem).clone().unwrap_or(0));
bounding_box.scale(scale_factor)
})
}
_ => None,
}
}
/// Evaluates the paint graph at the specified location in variation space
/// and emits the results to the given painter.
///
///
/// For a COLRv1 glyph, traverses the COLRv1 paint graph and invokes drawing callbacks on a
/// specified [`ColorPainter`] trait object. The traversal operates in font
/// units and will call `ColorPainter` methods with font unit values. This
/// means, if you want to draw a COLRv1 glyph at a particular font size, the
/// canvas needs to have a transformation matrix applied so that it scales down
/// the drawing operations to `font_size / upem`.
///
/// # Arguments
///
/// * `glyph_id` the `GlyphId` to be drawn.
/// * `location` coordinates for specifying a variation instance. This can be empty.
/// * `painter` a client-provided [`ColorPainter`] implementation receiving drawing callbacks.
///
pub fn paint(
&self,
location: impl Into<LocationRef<'a>>,
painter: &mut impl ColorPainter,
) -> Result<(), PaintError> {
let instance =
instance::ColrInstance::new(self.colr.clone(), location.into().effective_coords());
let mut resolved_stops = traversal::ColorStopVec::default();
match &self.root_paint_ref {
ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, _) => {
let clipbox = get_clipbox_font_units(&instance, *glyph_id);
if let Some(rect) = clipbox {
painter.push_clip_box(rect);
}
let mut decycler = PaintDecycler::default();
let mut cycle_guard = decycler.enter(*paint_id)?;
traverse_with_callbacks(
&resolve_paint(&instance, paint)?,
&instance,
painter,
&mut cycle_guard,
&mut resolved_stops,
0,
)?;
if clipbox.is_some() {
painter.pop_clip();
}
Ok(())
}
ColorGlyphRoot::V0Range(range) => {
traverse_v0_range(range, &instance, painter)?;
Ok(())
}
}
}
}
/// Collection of color glyphs.
#[derive(Clone)]
pub struct ColorGlyphCollection<'a> {
colr: Option<colr::Colr<'a>>,
upem: Result<u16, ReadError>,
}
impl<'a> ColorGlyphCollection<'a> {
/// Creates a new collection of paintable color glyphs for the given font.
pub fn new(font: &FontRef<'a>) -> Self {
let colr = font.colr().ok();
let upem = font.head().map(|h| h.units_per_em());
Self { colr, upem }
}
/// Returns the color glyph representation for the given glyph identifier,
/// given a specific format.
pub fn get_with_format(
&self,
glyph_id: GlyphId,
glyph_format: ColorGlyphFormat,
) -> Option<ColorGlyph<'a>> {
let colr = self.colr.clone()?;
let root_paint_ref = match glyph_format {
ColorGlyphFormat::ColrV0 => {
let layer_range = colr.v0_base_glyph(glyph_id).ok()??;
ColorGlyphRoot::V0Range(layer_range)
}
ColorGlyphFormat::ColrV1 => {
let (paint, paint_id) = colr.v1_base_glyph(glyph_id).ok()??;
ColorGlyphRoot::V1Paint(paint, paint_id, glyph_id, self.upem.clone())
}
};
Some(ColorGlyph {
colr,
root_paint_ref,
})
}
/// Returns a color glyph representation for the given glyph identifier if
/// available, preferring a COLRv1 representation over a COLRv0
/// representation.
pub fn get(&self, glyph_id: GlyphId) -> Option<ColorGlyph<'a>> {
self.get_with_format(glyph_id, ColorGlyphFormat::ColrV1)
.or_else(|| self.get_with_format(glyph_id, ColorGlyphFormat::ColrV0))
}
}
#[cfg(test)]
mod tests {
use crate::{
color::traversal_tests::test_glyph_defs::PAINTCOLRGLYPH_CYCLE,
prelude::{LocationRef, Size},
MetadataProvider,
};
use read_fonts::{types::BoundingBox, FontRef};
use super::{Brush, ColorPainter, CompositeMode, GlyphId, Transform};
use crate::color::traversal_tests::test_glyph_defs::{COLORED_CIRCLES_V0, COLORED_CIRCLES_V1};
#[test]
fn has_colrv1_glyph_test() {
let colr_font = font_test_data::COLRV0V1_VARIABLE;
let font = FontRef::new(colr_font).unwrap();
let get_colrv1_glyph = |codepoint: &[char]| {
font.charmap().map(codepoint[0]).and_then(|glyph_id| {
font.color_glyphs()
.get_with_format(glyph_id, crate::color::ColorGlyphFormat::ColrV1)
})
};
assert!(get_colrv1_glyph(COLORED_CIRCLES_V0).is_none());
assert!(get_colrv1_glyph(COLORED_CIRCLES_V1).is_some());
}
struct DummyColorPainter {}
impl DummyColorPainter {
pub fn new() -> Self {
Self {}
}
}
impl Default for DummyColorPainter {
fn default() -> Self {
Self::new()
}
}
impl ColorPainter for DummyColorPainter {
fn push_transform(&mut self, _transform: Transform) {}
fn pop_transform(&mut self) {}
fn push_clip_glyph(&mut self, _glyph: GlyphId) {}
fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {}
fn pop_clip(&mut self) {}
fn fill(&mut self, _brush: Brush) {}
fn push_layer(&mut self, _composite_mode: CompositeMode) {}
fn pop_layer(&mut self) {}
}
#[test]
fn paintcolrglyph_cycle_test() {
let colr_font = font_test_data::COLRV0V1_VARIABLE;
let font = FontRef::new(colr_font).unwrap();
let cycle_glyph_id = font.charmap().map(PAINTCOLRGLYPH_CYCLE[0]).unwrap();
let colrv1_glyph = font
.color_glyphs()
.get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
assert!(colrv1_glyph.is_some());
let mut color_painter = DummyColorPainter::new();
let result = colrv1_glyph
.unwrap()
.paint(LocationRef::default(), &mut color_painter);
// Expected to fail with an error as the glyph contains a paint cycle.
assert!(result.is_err());
}
#[test]
fn no_cliplist_test() {
let colr_font = font_test_data::COLRV1_NO_CLIPLIST;
let font = FontRef::new(colr_font).unwrap();
let cycle_glyph_id = GlyphId::new(1);
let colrv1_glyph = font
.color_glyphs()
.get_with_format(cycle_glyph_id, crate::color::ColorGlyphFormat::ColrV1);
assert!(colrv1_glyph.is_some());
let mut color_painter = DummyColorPainter::new();
let result = colrv1_glyph
.unwrap()
.paint(LocationRef::default(), &mut color_painter);
assert!(result.is_ok());
}
#[test]
fn colrv0_no_bbox_test() {
let colr_font = font_test_data::COLRV0V1;
let font = FontRef::new(colr_font).unwrap();
let colrv0_glyph_id = GlyphId::new(168);
let colrv0_glyph = font
.color_glyphs()
.get_with_format(colrv0_glyph_id, super::ColorGlyphFormat::ColrV0)
.unwrap();
assert!(colrv0_glyph
.bounding_box(LocationRef::default(), Size::unscaled())
.is_none());
}
}

166
vendor/skrifa/src/color/transform.rs vendored Normal file
View File

@@ -0,0 +1,166 @@
//! Contains a [`Transform`] object holding values of an affine transformation matrix.
use std::ops::{Mul, MulAssign};
use read_fonts::ReadError;
use super::instance::ResolvedPaint;
#[cfg(feature = "libm")]
#[allow(unused_imports)]
use core_maths::*;
#[cfg(test)]
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(test, derive(Serialize, Deserialize))]
/// A transformation matrix to be applied to the drawing canvas.
///
/// Factors are specified in column-order, meaning that
/// for a vector `(x,y)` the transformed position `x'` of the vector
/// is calculated by
/// `x' = xx * x + xy * y + dx`,
/// and the transformed position y' is calculated by
/// `y' = yx * x + yy * y + dy`.
#[derive(Copy)]
pub struct Transform {
pub xx: f32,
pub yx: f32,
pub xy: f32,
pub yy: f32,
pub dx: f32,
pub dy: f32,
}
impl MulAssign for Transform {
fn mul_assign(&mut self, rhs: Self) {
*self = *self * rhs;
}
}
impl Mul for Transform {
type Output = Self;
fn mul(self, rhs: Self) -> Self::Output {
fn muladdmul(a: f32, b: f32, c: f32, d: f32) -> f32 {
a * b + c * d
}
Self {
xx: muladdmul(self.xx, rhs.xx, self.xy, rhs.yx),
xy: muladdmul(self.xx, rhs.xy, self.xy, rhs.yy),
dx: muladdmul(self.xx, rhs.dx, self.xy, rhs.dy) + self.dx,
yx: muladdmul(self.yx, rhs.xx, self.yy, rhs.yx),
yy: muladdmul(self.yx, rhs.xy, self.yy, rhs.yy),
dy: muladdmul(self.yx, rhs.dx, self.yy, rhs.dy) + self.dy,
}
}
}
impl Default for Transform {
fn default() -> Self {
Transform {
xx: 1.0,
yx: 0.0,
xy: 0.0,
yy: 1.0,
dx: 0.0,
dy: 0.0,
}
}
}
impl TryFrom<&ResolvedPaint<'_>> for Transform {
type Error = ReadError;
fn try_from(paint: &ResolvedPaint<'_>) -> Result<Self, Self::Error> {
match paint {
ResolvedPaint::Rotate {
angle,
around_center,
..
} => {
let sin_v = (angle * 180.0).to_radians().sin();
let cos_v = (angle * 180.0).to_radians().cos();
let mut out_transform = Transform {
xx: cos_v,
xy: -sin_v,
yx: sin_v,
yy: cos_v,
..Default::default()
};
fn scalar_dot_product(a: f32, b: f32, c: f32, d: f32) -> f32 {
a * b + c * d
}
if let Some(center) = around_center {
out_transform.dx = scalar_dot_product(sin_v, center.y, 1.0 - cos_v, center.x);
out_transform.dy = scalar_dot_product(-sin_v, center.x, 1.0 - cos_v, center.y);
}
Ok(out_transform)
}
ResolvedPaint::Scale {
scale_x,
scale_y,
around_center,
paint: _,
} => {
let mut out_transform = Transform {
xx: *scale_x,
yy: *scale_y,
..Transform::default()
};
if let Some(center) = around_center {
out_transform.dx = center.x - scale_x * center.x;
out_transform.dy = center.y - scale_y * center.y;
}
Ok(out_transform)
}
ResolvedPaint::Skew {
x_skew_angle,
y_skew_angle,
around_center,
paint: _,
} => {
let tan_x = (x_skew_angle * 180.0).to_radians().tan();
let tan_y = (y_skew_angle * 180.0).to_radians().tan();
let mut out_transform = Transform {
xy: -tan_x,
yx: tan_y,
..Transform::default()
};
if let Some(center) = around_center {
out_transform.dx = tan_x * center.y;
out_transform.dy = -tan_y * center.x;
}
Ok(out_transform)
}
ResolvedPaint::Transform {
xx,
yx,
xy,
yy,
dx,
dy,
paint: _,
} => Ok(Transform {
xx: *xx,
yx: *yx,
xy: *xy,
yy: *yy,
dx: *dx,
dy: *dy,
}),
ResolvedPaint::Translate { dx, dy, .. } => Ok(Transform {
dx: *dx,
dy: *dy,
..Default::default()
}),
_ => Err(ReadError::MalformedData(
"ResolvedPaint cannot be converted into a transform.",
)),
}
}
}

754
vendor/skrifa/src/color/traversal.rs vendored Normal file
View File

@@ -0,0 +1,754 @@
use std::{cmp::Ordering, ops::Range};
use read_fonts::{
tables::colr::{CompositeMode, Extend},
types::{BoundingBox, GlyphId, Point},
};
use super::{
instance::{
resolve_clip_box, resolve_paint, ColorStops, ColrInstance, ResolvedColorStop, ResolvedPaint,
},
Brush, ColorPainter, ColorStop, PaintCachedColorGlyph, PaintError, Transform,
};
use crate::decycler::{Decycler, DecyclerError};
#[cfg(feature = "libm")]
#[allow(unused_imports)]
use core_maths::*;
pub(crate) type PaintDecycler = Decycler<usize, MAX_TRAVERSAL_DEPTH>;
// Avoid heap allocations for any gradient with <= 32 color stops. This number
// was chosen to keep stack size < 512 bytes.
//
// The largest gradient in Noto Color Emoji has 13 stops.
//
// Only one ColorStopVec will be created per paint graph traversal.
//
// Usage of SmallVec as a response to Behdad's wonderful memory usage analysis:
// <https://docs.google.com/document/d/1S47f3E--yqvFdG7lmmufxRoFi_wMzotC03v8UvS_p54/edit?tab=t.0#heading=h.bfj7urloz3oe>
const MAX_INLINE_COLOR_STOPS: usize = 32;
pub(crate) type ColorStopVec = crate::collections::SmallVec<ColorStop, MAX_INLINE_COLOR_STOPS>;
impl From<DecyclerError> for PaintError {
fn from(value: DecyclerError) -> Self {
match value {
DecyclerError::CycleDetected => Self::PaintCycleDetected,
DecyclerError::DepthLimitExceeded => Self::DepthLimitExceeded,
}
}
}
/// Depth at which we will stop traversing and return an error.
///
/// Used to prevent stack overflows. Also allows us to avoid using a HashSet
/// in no_std builds.
///
/// This limit matches the one used in HarfBuzz:
/// HB_MAX_NESTING_LEVEL: <https://github.com/harfbuzz/harfbuzz/blob/c2f8f35a6cfce43b88552b3eb5c05062ac7007b2/src/hb-limits.hh#L53>
/// hb_paint_context_t: <https://github.com/harfbuzz/harfbuzz/blob/c2f8f35a6cfce43b88552b3eb5c05062ac7007b2/src/OT/Color/COLR/COLR.hh#L74>
const MAX_TRAVERSAL_DEPTH: usize = 64;
pub(crate) fn get_clipbox_font_units(
colr_instance: &ColrInstance,
glyph_id: GlyphId,
) -> Option<BoundingBox<f32>> {
let maybe_clipbox = (*colr_instance).v1_clip_box(glyph_id).ok().flatten()?;
Some(resolve_clip_box(colr_instance, &maybe_clipbox))
}
impl From<ResolvedColorStop> for ColorStop {
fn from(resolved_stop: ResolvedColorStop) -> Self {
ColorStop {
offset: resolved_stop.offset,
alpha: resolved_stop.alpha,
palette_index: resolved_stop.palette_index,
}
}
}
fn make_sorted_resolved_stops(
stops: &ColorStops,
instance: &ColrInstance,
out_stops: &mut ColorStopVec,
) {
let color_stop_iter = stops.resolve(instance).map(|stop| stop.into());
out_stops.clear();
for stop in color_stop_iter {
out_stops.push(stop);
}
out_stops.sort_by(|a, b| a.offset.partial_cmp(&b.offset).unwrap_or(Ordering::Equal));
}
struct CollectFillGlyphPainter<'a> {
brush_transform: Option<Transform>,
glyph_id: GlyphId,
parent_painter: &'a mut dyn ColorPainter,
pub optimization_success: bool,
}
impl<'a> CollectFillGlyphPainter<'a> {
fn new(parent_painter: &'a mut dyn ColorPainter, glyph_id: GlyphId) -> Self {
Self {
brush_transform: None,
glyph_id,
parent_painter,
optimization_success: true,
}
}
}
impl ColorPainter for CollectFillGlyphPainter<'_> {
fn push_transform(&mut self, transform: Transform) {
if self.optimization_success {
match self.brush_transform {
None => {
self.brush_transform = Some(transform);
}
Some(ref mut existing_transform) => {
*existing_transform *= transform;
}
}
}
}
fn pop_transform(&mut self) {
// Since we only support fill and and transform operations, we need to
// ignore a popped transform, as this would be called after traversing
// the graph backup after a fill was performed, but we want to preserve
// the transform in order to be able to return it.
}
fn fill(&mut self, brush: Brush<'_>) {
if self.optimization_success {
self.parent_painter
.fill_glyph(self.glyph_id, self.brush_transform, brush);
}
}
fn push_clip_glyph(&mut self, _: GlyphId) {
self.optimization_success = false;
}
fn push_clip_box(&mut self, _: BoundingBox<f32>) {
self.optimization_success = false;
}
fn pop_clip(&mut self) {
self.optimization_success = false;
}
fn push_layer(&mut self, _: CompositeMode) {
self.optimization_success = false;
}
fn pop_layer(&mut self) {
self.optimization_success = false;
}
}
pub(crate) fn traverse_with_callbacks(
paint: &ResolvedPaint,
instance: &ColrInstance,
painter: &mut impl ColorPainter,
decycler: &mut PaintDecycler,
resolved_stops: &mut ColorStopVec,
recurse_depth: usize,
) -> Result<(), PaintError> {
if recurse_depth >= MAX_TRAVERSAL_DEPTH {
return Err(PaintError::DepthLimitExceeded);
}
match paint {
ResolvedPaint::ColrLayers { range } => {
for layer_index in range.clone() {
// Perform cycle detection with paint id here, second part of the tuple.
let (layer_paint, paint_id) = (*instance).v1_layer(layer_index)?;
let mut cycle_guard = decycler.enter(paint_id)?;
traverse_with_callbacks(
&resolve_paint(instance, &layer_paint)?,
instance,
painter,
&mut cycle_guard,
resolved_stops,
recurse_depth + 1,
)?;
}
Ok(())
}
ResolvedPaint::Solid {
palette_index,
alpha,
} => {
painter.fill(Brush::Solid {
palette_index: *palette_index,
alpha: *alpha,
});
Ok(())
}
ResolvedPaint::LinearGradient {
x0,
y0,
x1,
y1,
x2,
y2,
color_stops,
extend,
} => {
let mut p0 = Point::new(*x0, *y0);
let p1 = Point::new(*x1, *y1);
let p2 = Point::new(*x2, *y2);
let dot_product = |a: Point<f32>, b: Point<f32>| -> f32 { a.x * b.x + a.y * b.y };
let cross_product = |a: Point<f32>, b: Point<f32>| -> f32 { a.x * b.y - a.y * b.x };
let project_onto = |vector: Point<f32>, point: Point<f32>| -> Point<f32> {
let length = (point.x * point.x + point.y * point.y).sqrt();
if length == 0.0 {
return Point::default();
}
let mut point_normalized = point / length;
point_normalized *= dot_product(vector, point) / length;
point_normalized
};
make_sorted_resolved_stops(color_stops, instance, resolved_stops);
// If p0p1 or p0p2 are degenerate probably nothing should be drawn.
// If p0p1 and p0p2 are parallel then one side is the first color and the other side is
// the last color, depending on the direction.
// For now, just use the first color.
if p1 == p0 || p2 == p0 || cross_product(p1 - p0, p2 - p0) == 0.0 {
if let Some(stop) = resolved_stops.first() {
painter.fill(Brush::Solid {
palette_index: stop.palette_index,
alpha: stop.alpha,
});
};
return Ok(());
}
// Follow implementation note in nanoemoji:
// https://github.com/googlefonts/nanoemoji/blob/0ac6e7bb4d8202db692574d8530a9b643f1b3b3c/src/nanoemoji/svg.py#L188
// to compute a new gradient end point P3 as the orthogonal
// projection of the vector from p0 to p1 onto a line perpendicular
// to line p0p2 and passing through p0.
let mut perpendicular_to_p2 = p2 - p0;
perpendicular_to_p2 = Point::new(perpendicular_to_p2.y, -perpendicular_to_p2.x);
let mut p3 = p0 + project_onto(p1 - p0, perpendicular_to_p2);
match (
resolved_stops.first().cloned(),
resolved_stops.last().cloned(),
) {
(None, _) | (_, None) => {}
(Some(first_stop), Some(last_stop)) => {
let mut color_stop_range = last_stop.offset - first_stop.offset;
// Nothing can be drawn for this situation.
if color_stop_range == 0.0 && extend != &Extend::Pad {
return Ok(());
}
// In the Pad case, for providing normalized stops in the 0 to 1 range to the client,
// insert a color stop at the end. Adding this stop will paint the equivalent gradient,
// because: All font-specified color stops are in the same spot, mode is pad, so
// everything before this spot is painted with the first color, everything after this spot
// is painted with the last color. Not adding this stop would skip the projection below along
// the p0-p3 axis and result in specifying non-normalized color stops to the shader.
if color_stop_range == 0.0 && extend == &Extend::Pad {
let mut extra_stop = last_stop;
extra_stop.offset += 1.0;
resolved_stops.push(extra_stop);
color_stop_range = 1.0;
}
debug_assert!(color_stop_range != 0.0);
if color_stop_range != 1.0 || first_stop.offset != 0.0 {
let p0_p3 = p3 - p0;
let p0_offset = p0_p3 * first_stop.offset;
let p3_offset = p0_p3 * last_stop.offset;
p3 = p0 + p3_offset;
p0 += p0_offset;
let scale_factor = 1.0 / color_stop_range;
let start_offset = first_stop.offset;
for stop in resolved_stops.iter_mut() {
stop.offset = (stop.offset - start_offset) * scale_factor;
}
}
painter.fill(Brush::LinearGradient {
p0,
p1: p3,
color_stops: resolved_stops.as_slice(),
extend: *extend,
});
}
}
Ok(())
}
ResolvedPaint::RadialGradient {
x0,
y0,
radius0,
x1,
y1,
radius1,
color_stops,
extend,
} => {
let mut c0 = Point::new(*x0, *y0);
let mut c1 = Point::new(*x1, *y1);
let mut radius0 = *radius0;
let mut radius1 = *radius1;
make_sorted_resolved_stops(color_stops, instance, resolved_stops);
match (
resolved_stops.first().cloned(),
resolved_stops.last().cloned(),
) {
(None, _) | (_, None) => {}
(Some(first_stop), Some(last_stop)) => {
let mut color_stop_range = last_stop.offset - first_stop.offset;
// Nothing can be drawn for this situation.
if color_stop_range == 0.0 && extend != &Extend::Pad {
return Ok(());
}
// In the Pad case, for providing normalized stops in the 0 to 1 range to the client,
// insert a color stop at the end. See LinearGradient for more details.
if color_stop_range == 0.0 && extend == &Extend::Pad {
let mut extra_stop = last_stop;
extra_stop.offset += 1.0;
resolved_stops.push(extra_stop);
color_stop_range = 1.0;
}
debug_assert!(color_stop_range != 0.0);
// If the colorStopRange is 0 at this point, the default behavior of the shader is to
// clamp to 1 color stops that are above 1, clamp to 0 for color stops that are below 0,
// and repeat the outer color stops at 0 and 1 if the color stops are inside the
// range. That will result in the correct rendering.
if color_stop_range != 1.0 || first_stop.offset != 0.0 {
let c0_to_c1 = c1 - c0;
let radius_diff = radius1 - radius0;
let scale_factor = 1.0 / color_stop_range;
let c0_offset = c0_to_c1 * first_stop.offset;
let c1_offset = c0_to_c1 * last_stop.offset;
let stops_start_offset = first_stop.offset;
// Order of reassignments is important to avoid shadowing variables.
c1 = c0 + c1_offset;
c0 += c0_offset;
radius1 = radius0 + radius_diff * last_stop.offset;
radius0 += radius_diff * first_stop.offset;
for stop in resolved_stops.iter_mut() {
stop.offset = (stop.offset - stops_start_offset) * scale_factor;
}
}
painter.fill(Brush::RadialGradient {
c0,
r0: radius0,
c1,
r1: radius1,
color_stops: resolved_stops.as_slice(),
extend: *extend,
});
}
}
Ok(())
}
ResolvedPaint::SweepGradient {
center_x,
center_y,
start_angle,
end_angle,
color_stops,
extend,
} => {
// OpenType 1.9.1 adds a shift to the angle to ease specification of a 0 to 360
// degree sweep.
let sweep_angle_to_degrees = |angle| angle * 180.0 + 180.0;
let start_angle = sweep_angle_to_degrees(start_angle);
let end_angle = sweep_angle_to_degrees(end_angle);
// Stop normalization for sweep:
let sector_angle = end_angle - start_angle;
make_sorted_resolved_stops(color_stops, instance, resolved_stops);
if resolved_stops.is_empty() {
return Ok(());
}
match (
resolved_stops.first().cloned(),
resolved_stops.last().cloned(),
) {
(None, _) | (_, None) => {}
(Some(first_stop), Some(last_stop)) => {
let mut color_stop_range = last_stop.offset - first_stop.offset;
let mut start_angle_scaled = start_angle + sector_angle * first_stop.offset;
let mut end_angle_scaled = start_angle + sector_angle * last_stop.offset;
let start_offset = first_stop.offset;
// Nothing can be drawn for this situation.
if color_stop_range == 0.0 && extend != &Extend::Pad {
return Ok(());
}
// In the Pad case, if the color_stop_range is 0 insert a color stop at the end before
// normalizing. Adding this stop will paint the equivalent gradient, because: All font
// specified color stops are in the same spot, mode is pad, so everything before this
// spot is painted with the first color, everything after this spot is painted with
// the last color. Not adding this stop will skip the projection and result in
// specifying non-normalized color stops to the shader.
if color_stop_range == 0.0 && extend == &Extend::Pad {
let mut offset_last = last_stop;
offset_last.offset += 1.0;
resolved_stops.push(offset_last);
color_stop_range = 1.0;
}
debug_assert!(color_stop_range != 0.0);
let scale_factor = 1.0 / color_stop_range;
for shift_stop in resolved_stops.iter_mut() {
shift_stop.offset = (shift_stop.offset - start_offset) * scale_factor;
}
// /* https://docs.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients
// * "The angles are expressed in counter-clockwise degrees from
// * the direction of the positive x-axis on the design
// * grid. [...] The color line progresses from the start angle
// * to the end angle in the counter-clockwise direction;" -
// * Convert angles and stops from counter-clockwise to clockwise
// * for the shader if the gradient is not already reversed due to
// * start angle being larger than end angle. */
start_angle_scaled = 360.0 - start_angle_scaled;
end_angle_scaled = 360.0 - end_angle_scaled;
if start_angle_scaled >= end_angle_scaled {
(start_angle_scaled, end_angle_scaled) =
(end_angle_scaled, start_angle_scaled);
resolved_stops.reverse();
for stop in resolved_stops.iter_mut() {
stop.offset = 1.0 - stop.offset;
}
}
// https://learn.microsoft.com/en-us/typography/opentype/spec/colr#sweep-gradients
// "If the color line's extend mode is reflect or repeat
// and start and end angle are equal, nothing shall be drawn."
if start_angle_scaled == end_angle_scaled && extend != &Extend::Pad {
return Ok(());
}
painter.fill(Brush::SweepGradient {
c0: Point::new(*center_x, *center_y),
start_angle: start_angle_scaled,
end_angle: end_angle_scaled,
color_stops: resolved_stops.as_slice(),
extend: *extend,
});
}
}
Ok(())
}
ResolvedPaint::Glyph { glyph_id, paint } => {
let glyph_id = (*glyph_id).into();
let mut optimizer = CollectFillGlyphPainter::new(painter, glyph_id);
let mut result = traverse_with_callbacks(
&resolve_paint(instance, paint)?,
instance,
&mut optimizer,
decycler,
resolved_stops,
recurse_depth + 1,
);
// In case the optimization was not successful, just push a clip, and continue unoptimized traversal.
if !optimizer.optimization_success {
painter.push_clip_glyph(glyph_id);
result = traverse_with_callbacks(
&resolve_paint(instance, paint)?,
instance,
painter,
decycler,
resolved_stops,
recurse_depth + 1,
);
painter.pop_clip();
}
result
}
ResolvedPaint::ColrGlyph { glyph_id } => {
let glyph_id = (*glyph_id).into();
match (*instance).v1_base_glyph(glyph_id)? {
Some((base_glyph, base_glyph_paint_id)) => {
let mut cycle_guard = decycler.enter(base_glyph_paint_id)?;
let draw_result = painter.paint_cached_color_glyph(glyph_id)?;
match draw_result {
PaintCachedColorGlyph::Ok => Ok(()),
PaintCachedColorGlyph::Unimplemented => {
let clipbox = get_clipbox_font_units(instance, glyph_id);
if let Some(rect) = clipbox {
painter.push_clip_box(rect);
}
let result = traverse_with_callbacks(
&resolve_paint(instance, &base_glyph)?,
instance,
painter,
&mut cycle_guard,
resolved_stops,
recurse_depth + 1,
);
if clipbox.is_some() {
painter.pop_clip();
}
result
}
}
}
None => Err(PaintError::GlyphNotFound(glyph_id)),
}
}
ResolvedPaint::Transform {
paint: next_paint, ..
}
| ResolvedPaint::Translate {
paint: next_paint, ..
}
| ResolvedPaint::Scale {
paint: next_paint, ..
}
| ResolvedPaint::Rotate {
paint: next_paint, ..
}
| ResolvedPaint::Skew {
paint: next_paint, ..
} => {
painter.push_transform(paint.try_into()?);
let result = traverse_with_callbacks(
&resolve_paint(instance, next_paint)?,
instance,
painter,
decycler,
resolved_stops,
recurse_depth + 1,
);
painter.pop_transform();
result
}
ResolvedPaint::Composite {
source_paint,
mode,
backdrop_paint,
} => {
painter.push_layer(CompositeMode::SrcOver);
let mut result = traverse_with_callbacks(
&resolve_paint(instance, backdrop_paint)?,
instance,
painter,
decycler,
resolved_stops,
recurse_depth + 1,
);
result?;
painter.push_layer(*mode);
result = traverse_with_callbacks(
&resolve_paint(instance, source_paint)?,
instance,
painter,
decycler,
resolved_stops,
recurse_depth + 1,
);
painter.pop_layer_with_mode(*mode);
painter.pop_layer_with_mode(CompositeMode::SrcOver);
result
}
}
}
pub(crate) fn traverse_v0_range(
range: &Range<usize>,
instance: &ColrInstance,
painter: &mut impl ColorPainter,
) -> Result<(), PaintError> {
for layer_index in range.clone() {
let (layer_glyph, palette_index) = (*instance).v0_layer(layer_index)?;
painter.fill_glyph(
layer_glyph.into(),
None,
Brush::Solid {
palette_index,
alpha: 1.0,
},
);
}
Ok(())
}
#[cfg(test)]
mod tests {
use raw::types::GlyphId;
use read_fonts::{types::BoundingBox, FontRef, TableProvider};
use crate::{
color::{
instance::ColrInstance, traversal::get_clipbox_font_units,
traversal_tests::test_glyph_defs::CLIPBOX, Brush, ColorGlyphFormat, ColorPainter,
CompositeMode, Transform,
},
prelude::LocationRef,
MetadataProvider,
};
#[test]
fn clipbox_test() {
let colr_font = font_test_data::COLRV0V1_VARIABLE;
let font = FontRef::new(colr_font).unwrap();
let test_glyph_id = font.charmap().map(CLIPBOX[0]).unwrap();
let upem = font.head().unwrap().units_per_em();
let base_bounding_box = BoundingBox {
x_min: 0.0,
x_max: upem as f32 / 2.0,
y_min: upem as f32 / 2.0,
y_max: upem as f32,
};
// Fractional value needed to match variation scaling of clipbox.
const CLIPBOX_SHIFT: f32 = 200.0122;
macro_rules! test_entry {
($axis:literal, $shift:expr, $field:ident) => {
(
$axis,
$shift,
BoundingBox {
$field: base_bounding_box.$field + ($shift),
..base_bounding_box
},
)
};
}
let test_data_expectations = [
("", 0.0, base_bounding_box),
test_entry!("CLXI", CLIPBOX_SHIFT, x_min),
test_entry!("CLXA", -CLIPBOX_SHIFT, x_max),
test_entry!("CLYI", CLIPBOX_SHIFT, y_min),
test_entry!("CLYA", -CLIPBOX_SHIFT, y_max),
];
for axis_test in test_data_expectations {
let axis_coordinate = (axis_test.0, axis_test.1);
let location = font.axes().location([axis_coordinate]);
let color_instance = ColrInstance::new(font.colr().unwrap(), location.coords());
let clip_box = get_clipbox_font_units(&color_instance, test_glyph_id);
assert!(clip_box.is_some());
assert!(
clip_box.unwrap() == axis_test.2,
"Clip boxes do not match. Actual: {:?}, expected: {:?}",
clip_box.unwrap(),
axis_test.2
);
}
}
struct NopPainter;
impl ColorPainter for NopPainter {
fn push_transform(&mut self, _transform: Transform) {
// nop
}
fn pop_transform(&mut self) {
// nop
}
fn push_clip_glyph(&mut self, _glyph_id: GlyphId) {
// nop
}
fn push_clip_box(&mut self, _clip_box: BoundingBox<f32>) {
// nop
}
fn pop_clip(&mut self) {
// nop
}
fn fill(&mut self, _brush: Brush<'_>) {
// nop
}
fn push_layer(&mut self, _composite_mode: CompositeMode) {
// nop
}
fn pop_layer(&mut self) {
// nop
}
}
#[test]
fn no_panic_on_empty_colorline() {
// Minimized test case from <https://issues.oss-fuzz.com/issues/375768991>.
let test_case = &[
0, 1, 0, 0, 0, 3, 32, 32, 32, 32, 32, 32, 0, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32,
32, 32, 32, 32, 32, 67, 79, 76, 82, 32, 32, 32, 32, 0, 0, 0, 229, 0, 0, 0, 178, 99,
109, 97, 112, 32, 32, 32, 32, 0, 0, 0, 10, 0, 0, 1, 32, 32, 32, 32, 255, 32, 32, 32, 0,
4, 32, 255, 32, 32, 0, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 32, 0, 0,
32, 32, 0, 0, 0, 57, 32, 32, 32, 32, 32, 32, 32, 255, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
32, 0, 0, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 0, 4, 32, 32, 32, 32, 32, 32, 32,
32, 32, 0, 0, 0, 1, 32, 32, 32, 32, 32, 32, 255, 0, 0, 0, 40, 32, 32, 32, 32, 32, 32,
32, 255, 255, 32, 32, 32, 4, 0, 0, 32, 32, 32, 32, 32, 0, 0, 0, 0, 0, 0, 0, 0, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 0, 0, 32, 32, 32, 255, 255,
255, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
255, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32,
];
let font = FontRef::new(test_case).unwrap();
font.cmap().unwrap();
font.colr().unwrap();
let color_glyph = font
.color_glyphs()
.get_with_format(GlyphId::new(8447), ColorGlyphFormat::ColrV1)
.unwrap();
let _ = color_glyph.paint(LocationRef::default(), &mut NopPainter);
}
}

View File

@@ -0,0 +1,403 @@
#[cfg(test)]
pub mod test_glyph_defs;
use read_fonts::{
tables::colr::{CompositeMode, Extend},
types::{BoundingBox, GlyphId, Point},
FontRef,
};
use serde::{Deserialize, Serialize};
use std::{
env,
fs::OpenOptions,
io::{self, BufRead, Write},
string::String,
};
use crate::{
alloc::vec::Vec,
color::{
transform::Transform, traversal_tests::test_glyph_defs::*, Brush, ColorPainter, ColorStop,
},
setting::VariationSetting,
MetadataProvider,
};
#[derive(Serialize, Deserialize, Default, PartialEq)]
struct PaintDump {
glyph_id: u32,
ops: Vec<PaintOps>,
}
impl From<Brush<'_>> for BrushParams {
fn from(brush: Brush) -> Self {
match brush {
Brush::Solid {
palette_index,
alpha,
} => BrushParams::Solid {
palette_index,
alpha,
},
Brush::LinearGradient {
p0,
p1,
color_stops,
extend,
} => BrushParams::LinearGradient {
p0,
p1,
color_stops: color_stops.to_vec(),
extend,
},
Brush::RadialGradient {
c0,
r0,
c1,
r1,
color_stops,
extend,
} => BrushParams::RadialGradient {
c0,
r0,
c1,
r1,
color_stops: color_stops.to_vec(),
extend,
},
Brush::SweepGradient {
c0,
start_angle,
end_angle,
color_stops,
extend,
} => BrushParams::SweepGradient {
c0,
start_angle,
end_angle,
color_stops: color_stops.to_vec(),
extend,
},
}
}
}
// Needed as a mirror struct with owned ColorStops for serialization, deserialization.
#[derive(Serialize, Deserialize, PartialEq)]
pub enum BrushParams {
Solid {
palette_index: u16,
alpha: f32,
},
// Normalized to a straight line between points p0 and p1,
// color stops normalized to align with both points.
LinearGradient {
p0: Point<f32>,
p1: Point<f32>,
color_stops: Vec<ColorStop>,
extend: Extend,
},
RadialGradient {
c0: Point<f32>,
r0: f32,
c1: Point<f32>,
r1: f32,
color_stops: Vec<ColorStop>,
extend: Extend,
},
SweepGradient {
c0: Point<f32>,
start_angle: f32,
end_angle: f32,
color_stops: Vec<ColorStop>,
extend: Extend,
},
}
// Wrapping Transform for tests, as the results of trigonometric functions, in
// particular the tan() cases in PaintSkew need floating point PartialEq
// comparisons with an epsilon because the result of the tan() function differs
// on different platforms/archictectures.
#[derive(Serialize, Deserialize)]
struct DumpTransform(Transform);
// Using the same value as in SK_ScalarNearlyZero from Skia (see SkScalar.h).
const NEARLY_EQUAL_TOLERANCE: f32 = 1.0 / (1 << 12) as f32;
fn nearly_equal(a: f32, b: f32) -> bool {
(a - b).abs() < NEARLY_EQUAL_TOLERANCE
}
impl PartialEq<DumpTransform> for DumpTransform {
fn eq(&self, other: &DumpTransform) -> bool {
nearly_equal(self.0.xx, other.0.xx)
&& nearly_equal(self.0.xy, other.0.xy)
&& nearly_equal(self.0.yx, other.0.yx)
&& nearly_equal(self.0.yy, other.0.yy)
&& nearly_equal(self.0.dx, other.0.dx)
&& nearly_equal(self.0.dy, other.0.dy)
}
}
impl From<Transform> for DumpTransform {
fn from(value: Transform) -> Self {
Self(value)
}
}
#[derive(Serialize, Deserialize, PartialEq)]
enum PaintOps {
PushTransform {
transform: DumpTransform,
},
PopTransform,
PushClipGlyph {
gid: u32,
},
PushClipBox {
clip_box: BoundingBox<f32>,
},
PopClip,
FillBrush {
brush: BrushParams,
},
FillGlyph {
gid: u32,
transform: DumpTransform,
brush: BrushParams,
},
PushLayer {
composite_mode: u8,
},
PopLayer,
}
impl ColorPainter for PaintDump {
fn push_transform(&mut self, transform: Transform) {
self.ops.push(PaintOps::PushTransform {
transform: transform.into(),
});
}
fn pop_transform(&mut self) {
self.ops.push(PaintOps::PopTransform);
}
fn push_clip_glyph(&mut self, glyph: GlyphId) {
self.ops.push(PaintOps::PushClipGlyph {
gid: glyph.to_u32(),
});
}
fn push_clip_box(&mut self, clip_box: BoundingBox<f32>) {
self.ops.push(PaintOps::PushClipBox { clip_box });
}
fn pop_clip(&mut self) {
self.ops.push(PaintOps::PopClip);
}
fn fill(&mut self, brush: Brush) {
self.ops.push(PaintOps::FillBrush {
brush: brush.into(),
});
}
fn fill_glyph(&mut self, glyph_id: GlyphId, transform: Option<Transform>, brush: Brush) {
self.ops.push(PaintOps::FillGlyph {
gid: glyph_id.to_u32(),
transform: transform.unwrap_or_default().into(),
brush: brush.into(),
});
}
fn push_layer(&mut self, composite_mode: CompositeMode) {
self.ops.push(PaintOps::PushLayer {
composite_mode: composite_mode as u8,
});
}
fn pop_layer(&mut self) {
self.ops.push(PaintOps::PopLayer);
}
}
impl PaintDump {
pub fn new(gid: u32) -> Self {
Self {
glyph_id: gid,
..Default::default()
}
}
}
fn location_to_filename<I>(set_name: &str, settings: I) -> String
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
let formatted_settings: Vec<String> = settings
.into_iter()
.map(|entry| {
let entry_setting = entry.into();
format!("{:}_{:}", entry_setting.selector, entry_setting.value)
})
.collect();
let suffix = match formatted_settings.len() {
0 => String::new(),
_ => format!("_{}", formatted_settings.join("_")),
};
format!("colrv1_{}{}", set_name.to_lowercase(), suffix)
}
fn should_rebaseline() -> bool {
env::var("REBASELINE_COLRV1_TESTS").is_ok()
}
// To regenerate the baselines, set the environment variable `REBASELINE_COLRV1_TESTS`
// when running tests, for example like this:
// $ REBASELINE_COLRV1_TESTS=1 cargo test color::traversal
fn colrv1_traversal_test(
set_name: &str,
test_chars: &[char],
settings: &[(&str, f32)],
required_format: crate::color::ColorGlyphFormat,
) {
let colr_font = font_test_data::COLRV0V1_VARIABLE;
let font = FontRef::new(colr_font).unwrap();
let location = font.axes().location(settings);
let dumpfile_path = format!(
"../font-test-data/test_data/colrv1_json/{}",
location_to_filename(set_name, settings)
);
let test_gids = test_chars
.iter()
.map(|codepoint| font.charmap().map(*codepoint).unwrap());
let paint_dumps_iter = test_gids.map(|gid| {
let mut color_painter = PaintDump::new(gid.to_u32());
let color_glyph = font.color_glyphs().get_with_format(gid, required_format);
assert!(color_glyph.is_some());
let result = color_glyph
.unwrap()
.paint(location.coords(), &mut color_painter);
assert!(result.is_ok());
color_painter
});
if should_rebaseline() {
let mut file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(dumpfile_path)
.unwrap();
paint_dumps_iter.for_each(|dump| {
writeln!(file, "{}", serde_json::to_string(&dump).unwrap())
.expect("Writing to file failed.")
});
} else {
let expected = font_test_data::colrv1_json::expected(set_name, settings);
let mut lines = io::BufReader::new(expected.as_bytes()).lines();
for dump in paint_dumps_iter {
match lines.next() {
Some(line) => {
assert!(
dump == serde_json::from_str(
line.as_ref().expect("Failed to read expectations line from file.")
)
.expect("Failed to parse expectations line."),
"Result did not match expectation for glyph id: {}\nActual: {}\nExpected: {}\n",
dump.glyph_id, serde_json::to_string(&dump).unwrap(), &line.unwrap()
)
}
None => panic!("Expectation not found for glyph id: {}", dump.glyph_id),
}
}
}
}
macro_rules! colrv1_traversal_tests {
($($test_name:ident: $glyph_set:ident, $settings:expr,)*) => {
$(
#[test]
fn $test_name() {
colrv1_traversal_test(stringify!($glyph_set), $glyph_set, $settings, crate::color::ColorGlyphFormat::ColrV1);
}
)*
}
}
colrv1_traversal_tests!(
clipbox_default:CLIPBOX,&[],
clipbox_var_1:CLIPBOX, &[("CLIO", 200.0)],
comp_mode_default:COMPOSITE_MODE,&[],
extend_mode_default:EXTEND_MODE,&[],
extend_mode_var1:EXTEND_MODE,&[("COL1", -0.25), ("COL3", 0.25)],
extend_mode_var2:EXTEND_MODE,&[("COL1", 0.5), ("COL3", -0.5)],
extend_mode_var3:EXTEND_MODE,&[("COL3", 0.5)],
extend_mode_var4:EXTEND_MODE,&[("COL3", 1.0)],
extend_mode_var5:EXTEND_MODE,&[("COL1", -1.5)],
extend_mode_var6:EXTEND_MODE,&[("GRR0", -200.0), ("GRR1", -300.0)],
extend_mode_var7:EXTEND_MODE,&[("GRX0", -1000.0), ("GRX1", -1000.0), ("GRR0", -1000.0), ("GRR1", -900.0)],
extend_mode_var8:EXTEND_MODE,&[("GRX0", 1000.0), ("GRX1", -1000.0), ("GRR0", -1000.0), ("GRR1", 200.0)],
extend_mode_var9:EXTEND_MODE,&[("GRR0", -50.0), ("COL3", -2.0), ("COL2", -2.0), ("COL1", -0.9)],
extend_mode_var10:EXTEND_MODE,&[("GRR0", -50.0), ("COL3", -2.0), ("COL2", -2.0), ("COL1", -1.1)],
extend_mode_var11:EXTEND_MODE,&[("COL3", 1.0), ("COL2", 1.5), ("COL1", 2.0)],
extend_mode_var12:EXTEND_MODE,&[("COL2", -0.3)],
extend_mode_var13:EXTEND_MODE,&[("GRR0", 430.0), ("GRR1", 40.0)],
foreground_color_default:FOREGROUND_COLOR,&[],
gradient_skewed:GRADIENT_P2_SKEWED,&[],
gradient_stops_repeat:GRADIENT_STOPS_REPEAT,&[],
paint_rotate_default:PAINT_ROTATE,&[],
paint_rotate_var1:PAINT_ROTATE,&[("ROTA", 40.0)],
paint_rotate_var2:PAINT_ROTATE,&[("ROTX", -250.0), ("ROTY", -250.0)],
paint_scale_default:PAINT_SCALE,&[],
paint_scale_var1:PAINT_SCALE,&[("SCOX", 200.0), ("SCOY", 200.0)],
paint_scale_var2:PAINT_SCALE,&[("SCSX", 0.25), ("SCOY", 0.25)],
paint_scale_var3:PAINT_SCALE,&[("SCSX", -1.0), ("SCOY", -1.0)],
paint_skew_default:PAINT_SKEW,&[],
paint_skew_var1:PAINT_SKEW,&[("SKXA", 20.0)],
paint_skew_var2:PAINT_SKEW,&[("SKYA", 20.0)],
paint_skew_var3:PAINT_SKEW,&[("SKCX", 200.0),("SKCY", 200.0)],
paint_transform_default:PAINT_TRANSFORM,&[],
paint_translate_default:PAINT_TRANSLATE,&[],
paint_translate_var_1:PAINT_TRANSLATE,&[("TLDX", 100.0), ("TLDY", 100.0)],
paint_sweep_default:SWEEP_VARSWEEP,&[],
paint_sweep_var1:SWEEP_VARSWEEP,&[("SWPS", 0.0)],
paint_sweep_var2:SWEEP_VARSWEEP,&[("SWPS", 90.0)],
paint_sweep_var3:SWEEP_VARSWEEP,&[("SWPE", -90.0)],
paint_sweep_var4:SWEEP_VARSWEEP,&[("SWPE", -45.0)],
paint_sweep_var5:SWEEP_VARSWEEP,&[("SWPS", -45.0),("SWPE", 45.0)],
paint_sweep_var6:SWEEP_VARSWEEP,&[("SWC1", -0.25), ("SWC2", 0.083333333), ("SWC3", 0.083333333), ("SWC4", 0.25)],
paint_sweep_var7:SWEEP_VARSWEEP,&[("SWPS", 45.0), ("SWPE", -45.0), ("SWC1", -0.25), ("SWC2", -0.416687), ("SWC3", -0.583313), ("SWC4", -0.75)],
variable_alpha_default:VARIABLE_ALPHA,&[],
variable_alpha_var1:VARIABLE_ALPHA,&[("APH1", -0.7)],
variable_alpha_var2:VARIABLE_ALPHA,&[("APH2", -0.7), ("APH3", -0.2)],
nocycle_multi_colrglyph:NO_CYCLE_MULTI_COLRGLYPH,&[],
sweep_coincident:SWEEP_COINCIDENT,&[],
paint_glyph_nested:PAINT_GLYPH_NESTED,&[],
);
macro_rules! colrv0_traversal_tests {
($($test_name:ident: $glyph_set:ident,)*) => {
$(
#[test]
fn $test_name() {
colrv1_traversal_test(stringify!($glyph_set), $glyph_set, &[], crate::color::ColorGlyphFormat::ColrV0);
}
)*
}
}
colrv0_traversal_tests!(
colored_circles:COLORED_CIRCLES_V0,
);

View File

@@ -0,0 +1,214 @@
pub(crate) const GRADIENT_STOPS_REPEAT: &[char] =
&['\u{f0100}', '\u{f0101}', '\u{f0102}', '\u{f0103}'];
pub(crate) const SWEEP_VARSWEEP: &[char] = &[
'\u{f0200}',
'\u{f0201}',
'\u{f0202}',
'\u{f0203}',
'\u{f0204}',
'\u{f0205}',
'\u{f0206}',
'\u{f0207}',
'\u{f0208}',
'\u{f0209}',
'\u{f020a}',
'\u{f020b}',
'\u{f020c}',
'\u{f020d}',
'\u{f020e}',
'\u{f020f}',
'\u{f0210}',
'\u{f0211}',
'\u{f0212}',
'\u{f0213}',
'\u{f0214}',
'\u{f0215}',
'\u{f0216}',
'\u{f0217}',
'\u{f0218}',
'\u{f0219}',
'\u{f021a}',
'\u{f021b}',
'\u{f021c}',
'\u{f021d}',
'\u{f021e}',
'\u{f021f}',
'\u{f0220}',
'\u{f0221}',
'\u{f0222}',
'\u{f0223}',
'\u{f0224}',
'\u{f0225}',
'\u{f0226}',
'\u{f0227}',
'\u{f0228}',
'\u{f0229}',
'\u{f022a}',
'\u{f022b}',
'\u{f022c}',
'\u{f022d}',
'\u{f022e}',
'\u{f022f}',
'\u{f0230}',
'\u{f0231}',
'\u{f0232}',
'\u{f0233}',
'\u{f0234}',
'\u{f0235}',
'\u{f0236}',
'\u{f0237}',
'\u{f0238}',
'\u{f0239}',
'\u{f023a}',
'\u{f023b}',
'\u{f023c}',
'\u{f023d}',
'\u{f023e}',
'\u{f023f}',
'\u{f0240}',
'\u{f0241}',
'\u{f0242}',
'\u{f0243}',
'\u{f0244}',
'\u{f0245}',
'\u{f0246}',
'\u{f0247}',
];
pub(crate) const PAINT_SCALE: &[char] = &[
'\u{f0300}',
'\u{f0301}',
'\u{f0302}',
'\u{f0303}',
'\u{f0304}',
'\u{f0305}',
];
pub(crate) const EXTEND_MODE: &[char] = &[
'\u{f0500}',
'\u{f0501}',
'\u{f0502}',
'\u{f0503}',
'\u{f0504}',
'\u{f0505}',
'\u{f0506}',
'\u{f0507}',
'\u{f0508}',
];
pub(crate) const PAINT_ROTATE: &[char] = &['\u{f0600}', '\u{f0601}', '\u{f0602}', '\u{f0603}'];
pub(crate) const PAINT_SKEW: &[char] = &[
'\u{f0700}',
'\u{f0701}',
'\u{f0702}',
'\u{f0703}',
'\u{f0704}',
'\u{f0705}',
];
pub(crate) const PAINT_TRANSFORM: &[char] = &['\u{f0800}', '\u{f0801}', '\u{f0802}', '\u{f0803}'];
pub(crate) const PAINT_TRANSLATE: &[char] = &[
'\u{f0900}',
'\u{f0901}',
'\u{f0902}',
'\u{f0903}',
'\u{f0904}',
'\u{f0905}',
'\u{f0906}',
];
pub(crate) const COMPOSITE_MODE: &[char] = &[
'\u{f0a00}',
'\u{f0a01}',
'\u{f0a02}',
'\u{f0a03}',
'\u{f0a04}',
'\u{f0a05}',
'\u{f0a06}',
'\u{f0a07}',
'\u{f0a08}',
'\u{f0a09}',
'\u{f0a0a}',
'\u{f0a0b}',
'\u{f0a0c}',
'\u{f0a0d}',
'\u{f0a0e}',
'\u{f0a0f}',
'\u{f0a10}',
'\u{f0a11}',
'\u{f0a12}',
'\u{f0a13}',
'\u{f0a14}',
'\u{f0a15}',
'\u{f0a16}',
'\u{f0a17}',
'\u{f0a18}',
'\u{f0a19}',
'\u{f0a1a}',
'\u{f0a1b}',
];
pub(crate) const FOREGROUND_COLOR: &[char] = &[
'\u{f0b00}',
'\u{f0b01}',
'\u{f0b02}',
'\u{f0b03}',
'\u{f0b04}',
'\u{f0b05}',
'\u{f0b06}',
'\u{f0b07}',
];
pub(crate) const CLIPBOX: &[char] = &[
'\u{f0c00}',
'\u{f0c01}',
'\u{f0c02}',
'\u{f0c03}',
'\u{f0c04}',
];
pub(crate) const GRADIENT_P2_SKEWED: &[char] = &['\u{f0d00}'];
pub(crate) const VARIABLE_ALPHA: &[char] = &['\u{f1000}'];
pub(crate) const NO_CYCLE_MULTI_COLRGLYPH: &[char] = &['\u{f1200}'];
pub(crate) const SWEEP_COINCIDENT: &[char] = &[
'\u{f1300}',
'\u{f1301}',
'\u{f1302}',
'\u{f1303}',
'\u{f1304}',
'\u{f1305}',
'\u{f1306}',
'\u{f1307}',
'\u{f1308}',
'\u{f1309}',
'\u{f130a}',
'\u{f130b}',
'\u{f130c}',
'\u{f130d}',
'\u{f130e}',
'\u{f130f}',
'\u{f1310}',
'\u{f1311}',
'\u{f1312}',
'\u{f1313}',
'\u{f1314}',
'\u{f1315}',
'\u{f1316}',
'\u{f1317}',
];
pub(crate) const COLORED_CIRCLES_V0: &[char] = &['\u{F0E00}'];
pub(crate) const COLORED_CIRCLES_V1: &[char] = &['\u{F0E01}'];
pub(crate) const PAINTCOLRGLYPH_CYCLE: &[char] = &['\u{f1100}', '\u{f1101}', '\u{f1200}'];
pub(crate) const PAINT_GLYPH_NESTED: &[char] = &[
'\u{f1400}',
'\u{f1401}',
'\u{f1402}',
'\u{f1403}',
'\u{f1404}',
'\u{f1405}',
'\u{f1406}',
'\u{f1407}',
'\u{f1408}',
'\u{f1409}',
'\u{f140a}',
'\u{f140b}',
'\u{f140c}',
'\u{f140d}',
'\u{f140e}',
'\u{f140f}',
];

178
vendor/skrifa/src/decycler.rs vendored Normal file
View File

@@ -0,0 +1,178 @@
//! Support for cycle detection in DFS graph traversals.
use core::ops::{Deref, DerefMut};
#[derive(Copy, Clone, Debug)]
pub(crate) enum DecyclerError {
DepthLimitExceeded,
CycleDetected,
}
/// Cycle detector for DFS traversal of a graph.
///
/// The graph is expected to have unique node identifiers of type `T`
/// and traversal depth is limited to the constant `D`.
///
/// This is based on `hb_decycler_t` in HarfBuzz (<https://github.com/harfbuzz/harfbuzz/blob/a2ea5d28cb5387f4de2049802474b817be15ad5b/src/hb-decycler.hh>)
/// which is an extension of Floyd's tortoise and hare algorithm (<https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_tortoise_and_hare>)
/// to DFS traversals.
///
/// Unlike the implementation in HB which supports traversals of arbitrary
/// depth, this is limited to a constant value. Instead of building a
/// forward-linked list of nodes (on the stack) to track the traversal chain,
/// we simply use a fixed size array, indexed by depth, which imposes the
/// limit.
///
/// It _might_ be possible to implement the HB algorithm in safe Rust but
/// satisfying the borrow checker would be a challenge and we require a depth
/// limit to prevent stack overflows anyway. Improvements welcome!
pub(crate) struct Decycler<T, const D: usize> {
node_ids: [T; D],
depth: usize,
}
impl<T, const D: usize> Decycler<T, D>
where
T: Copy + PartialEq + Default,
{
pub fn new() -> Self {
Self {
node_ids: [T::default(); D],
depth: 0,
}
}
/// Enters a new graph node with the given value that uniquely
/// identifies the current node.
///
/// Returns an error when a cycle is detected or the max depth of the
/// traversal is exceeded. Otherwise, increases the current depth and
/// returns a guard object that will decrease the depth when dropped.
///
/// The guard object derefs to the decycler, so it can be passed to
/// a recursive traversal function to check for cycles in descendent
/// nodes in a graph.
pub fn enter(&mut self, node_id: T) -> Result<DecyclerGuard<T, D>, DecyclerError> {
if self.depth < D {
if self.depth == 0 || self.node_ids[self.depth / 2] != node_id {
self.node_ids[self.depth] = node_id;
self.depth += 1;
Ok(DecyclerGuard { decycler: self })
} else {
Err(DecyclerError::CycleDetected)
}
} else {
Err(DecyclerError::DepthLimitExceeded)
}
}
}
impl<T, const D: usize> Default for Decycler<T, D>
where
T: Copy + PartialEq + Default,
{
fn default() -> Self {
Self::new()
}
}
pub(crate) struct DecyclerGuard<'a, T, const D: usize> {
decycler: &'a mut Decycler<T, D>,
}
impl<T, const D: usize> Deref for DecyclerGuard<'_, T, D> {
type Target = Decycler<T, D>;
fn deref(&self) -> &Self::Target {
self.decycler
}
}
impl<T, const D: usize> DerefMut for DecyclerGuard<'_, T, D> {
fn deref_mut(&mut self) -> &mut Self::Target {
self.decycler
}
}
impl<T, const D: usize> Drop for DecyclerGuard<'_, T, D> {
fn drop(&mut self) {
self.decycler.depth -= 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn graph_with_cycles() {
let tree = Tree {
nodes: vec![
Node::new(vec![1, 2]),
Node::new(vec![2, 3]),
Node::new(vec![]),
Node::new(vec![0, 1]),
],
};
let result = tree.traverse(&mut TestDecycler::new());
assert!(matches!(result, Err(DecyclerError::CycleDetected)));
}
#[test]
fn exceeds_max_depth() {
let mut nodes = (0..MAX_DEPTH)
.map(|ix| Node::new(vec![ix + 1]))
.collect::<Vec<_>>();
nodes.push(Node::new(vec![]));
let tree = Tree { nodes };
let result = tree.traverse(&mut TestDecycler::new());
assert!(matches!(result, Err(DecyclerError::DepthLimitExceeded)));
}
#[test]
fn well_formed_tree() {
let mut nodes = (0..MAX_DEPTH - 1)
.map(|ix| Node::new(vec![ix + 1]))
.collect::<Vec<_>>();
nodes.push(Node::new(vec![]));
let tree = Tree { nodes };
let result = tree.traverse(&mut TestDecycler::new());
assert!(result.is_ok());
}
const MAX_DEPTH: usize = 64;
type TestDecycler = Decycler<usize, MAX_DEPTH>;
struct Node {
child_ids: Vec<usize>,
}
impl Node {
fn new(child_ids: Vec<usize>) -> Self {
Self { child_ids }
}
}
struct Tree {
nodes: Vec<Node>,
}
impl Tree {
fn traverse(&self, decycler: &mut TestDecycler) -> Result<(), DecyclerError> {
self.traverse_impl(decycler, 0)
}
fn traverse_impl(
&self,
decycler: &mut TestDecycler,
node_id: usize,
) -> Result<(), DecyclerError> {
let mut cycle_guard = decycler.enter(node_id)?;
let node = &self.nodes[node_id];
for child_id in &node.child_ids {
self.traverse_impl(&mut cycle_guard, *child_id)?;
}
Ok(())
}
}
}

3
vendor/skrifa/src/font.rs vendored Normal file
View File

@@ -0,0 +1,3 @@
//! Basic representation of an in-memory font resource.
pub use read_fonts::FontRef;

424
vendor/skrifa/src/glyph_name.rs vendored Normal file
View File

@@ -0,0 +1,424 @@
//! Support for accessing glyph names.
use core::ops::Range;
use raw::{
tables::{
cff::Cff,
post::Post,
postscript::{Charset, CharsetIter, StringId as Sid},
},
types::GlyphId,
FontRef, TableProvider,
};
/// "Names must be no longer than 63 characters; some older implementations
/// can assume a length limit of 31 characters."
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/post#version-20>
const MAX_GLYPH_NAME_LEN: usize = 63;
/// Mapping from glyph identifiers to names.
///
/// This sources glyph names from the `post` and `CFF` tables in that order.
/// If glyph names are not available in either, then they are synthesized
/// as `gidDDD` where `DDD` is the glyph identifier in decimal. Use the
/// [`source`](Self::source) to determine which source was chosen.
#[derive(Clone)]
pub struct GlyphNames<'a> {
inner: Inner<'a>,
}
#[derive(Clone)]
enum Inner<'a> {
// Second field is num_glyphs
Post(Post<'a>, u32),
Cff(Cff<'a>, Charset<'a>),
Synthesized(u32),
}
impl<'a> GlyphNames<'a> {
/// Creates a new object for accessing glyph names from the given font.
pub fn new(font: &FontRef<'a>) -> Self {
let num_glyphs = font
.maxp()
.map(|maxp| maxp.num_glyphs() as u32)
.unwrap_or_default();
if let Ok(post) = font.post() {
if post.num_names() != 0 {
return Self {
inner: Inner::Post(post, num_glyphs),
};
}
}
if let Some((cff, charset)) = font
.cff()
.ok()
.and_then(|cff| Some((cff.clone(), cff.charset(0).ok()??)))
{
return Self {
inner: Inner::Cff(cff, charset),
};
}
Self {
inner: Inner::Synthesized(num_glyphs),
}
}
/// Returns the chosen source for glyph names.
pub fn source(&self) -> GlyphNameSource {
match &self.inner {
Inner::Post(..) => GlyphNameSource::Post,
Inner::Cff(..) => GlyphNameSource::Cff,
Inner::Synthesized(..) => GlyphNameSource::Synthesized,
}
}
/// Returns the number of glyphs in the font.
pub fn num_glyphs(&self) -> u32 {
match &self.inner {
Inner::Post(_, n) | Inner::Synthesized(n) => *n,
Inner::Cff(_, charset) => charset.num_glyphs(),
}
}
/// Returns the name for the given glyph identifier.
pub fn get(&self, glyph_id: GlyphId) -> Option<GlyphName> {
if glyph_id.to_u32() >= self.num_glyphs() {
return None;
}
let name = match &self.inner {
Inner::Post(post, _) => GlyphName::from_post(post, glyph_id),
Inner::Cff(cff, charset) => charset
.string_id(glyph_id)
.ok()
.and_then(|sid| GlyphName::from_cff_sid(cff, sid)),
_ => None,
};
// If name is empty string, synthesize it
if !name.as_ref().is_some_and(|s| !s.is_empty()) {
return Some(GlyphName::synthesize(glyph_id));
}
Some(name.unwrap_or_else(|| GlyphName::synthesize(glyph_id)))
}
/// Returns an iterator yielding the identifier and name for all glyphs in
/// the font.
pub fn iter(&self) -> impl Iterator<Item = (GlyphId, GlyphName)> + 'a + Clone {
match &self.inner {
Inner::Post(post, n) => Iter::Post(0..*n, post.clone()),
Inner::Cff(cff, charset) => Iter::Cff(cff.clone(), charset.iter()),
Inner::Synthesized(n) => Iter::Synthesized(0..*n),
}
}
}
/// Specifies the chosen source for glyph names.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum GlyphNameSource {
/// Glyph names are sourced from the `post` table.
Post,
/// Glyph names are sourced from the `CFF` table.
Cff,
/// Glyph names are synthesized in the format `gidDDD` where `DDD` is
/// the glyph identifier in decimal.
Synthesized,
}
/// The name of a glyph.
#[derive(Clone)]
pub struct GlyphName {
name: [u8; MAX_GLYPH_NAME_LEN],
len: u8,
is_synthesized: bool,
}
impl GlyphName {
/// Returns the underlying name as a string.
pub fn as_str(&self) -> &str {
let bytes = &self.name[..self.len as usize];
core::str::from_utf8(bytes).unwrap_or_default()
}
/// Returns true if the glyph name was synthesized, i.e. not found in any
/// source.
pub fn is_synthesized(&self) -> bool {
self.is_synthesized
}
fn from_bytes(bytes: &[u8]) -> Self {
let mut name = Self::default();
name.append(bytes);
name
}
fn from_post(post: &Post, glyph_id: GlyphId) -> Option<Self> {
glyph_id
.try_into()
.ok()
.and_then(|id| post.glyph_name(id))
.map(|s| s.as_bytes())
.map(Self::from_bytes)
}
fn from_cff_sid(cff: &Cff, sid: Sid) -> Option<Self> {
cff.string(sid)
.and_then(|s| core::str::from_utf8(s.bytes()).ok())
.map(|s| s.as_bytes())
.map(Self::from_bytes)
}
fn synthesize(glyph_id: GlyphId) -> Self {
use core::fmt::Write;
let mut name = Self {
is_synthesized: true,
..Self::default()
};
let _ = write!(GlyphNameWrite(&mut name), "gid{}", glyph_id.to_u32());
name
}
/// Appends the given bytes to `self` while keeping the maximum length
/// at 63 bytes.
///
/// This exists primarily to support the [`core::fmt::Write`] impl
/// (which is used for generating synthesized glyph names) because
/// we have no guarantee of how many times `write_str` might be called
/// for a given format.
fn append(&mut self, bytes: &[u8]) {
// We simply truncate when length exceeds the max since glyph names
// are expected to be <= 63 chars
let start = self.len as usize;
let available = MAX_GLYPH_NAME_LEN - start;
let copy_len = available.min(bytes.len());
self.name[start..start + copy_len].copy_from_slice(&bytes[..copy_len]);
self.len = (start + copy_len) as u8;
}
}
impl Default for GlyphName {
fn default() -> Self {
Self {
name: [0; MAX_GLYPH_NAME_LEN],
len: 0,
is_synthesized: false,
}
}
}
impl core::fmt::Debug for GlyphName {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("GlyphName")
.field("name", &self.as_str())
.field("is_synthesized", &self.is_synthesized)
.finish()
}
}
impl core::fmt::Display for GlyphName {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl core::ops::Deref for GlyphName {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl PartialEq<&str> for GlyphName {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
struct GlyphNameWrite<'a>(&'a mut GlyphName);
impl core::fmt::Write for GlyphNameWrite<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
self.0.append(s.as_bytes());
Ok(())
}
}
#[derive(Clone)]
enum Iter<'a> {
Post(Range<u32>, Post<'a>),
Cff(Cff<'a>, CharsetIter<'a>),
Synthesized(Range<u32>),
}
impl Iter<'_> {
fn next_name(&mut self) -> Option<Result<(GlyphId, GlyphName), GlyphId>> {
match self {
Self::Post(range, post) => {
let gid = GlyphId::new(range.next()?);
Some(
GlyphName::from_post(post, gid)
.map(|name| (gid, name))
.ok_or(gid),
)
}
Self::Cff(cff, iter) => {
let (gid, sid) = iter.next()?;
Some(
GlyphName::from_cff_sid(cff, sid)
.map(|name| (gid, name))
.ok_or(gid),
)
}
Self::Synthesized(range) => {
let gid = GlyphId::new(range.next()?);
Some(Ok((gid, GlyphName::synthesize(gid))))
}
}
}
}
impl Iterator for Iter<'_> {
type Item = (GlyphId, GlyphName);
fn next(&mut self) -> Option<Self::Item> {
match self.next_name()? {
Ok((gid, name)) if name.is_empty() => Some((gid, GlyphName::synthesize(gid))),
Ok(gid_name) => Some(gid_name),
Err(gid) => Some((gid, GlyphName::synthesize(gid))),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use raw::{FontData, FontRead};
#[test]
fn synthesized_glyph_names() {
let count = 58;
let names = GlyphNames {
inner: Inner::Synthesized(58),
};
let names_buf = (0..count).map(|i| format!("gid{i}")).collect::<Vec<_>>();
let expected_names = names_buf.iter().map(|s| s.as_str()).collect::<Vec<_>>();
for (_, name) in names.iter() {
assert!(name.is_synthesized())
}
check_names(&names, &expected_names, GlyphNameSource::Synthesized);
}
#[test]
fn synthesize_for_empty_names() {
let mut post_data = font_test_data::post::SIMPLE.to_vec();
// last name in this post data is "hola" so pop 5 bytes and then
// push a 0 to simulate an empty name
post_data.truncate(post_data.len() - 5);
post_data.push(0);
let post = Post::read(FontData::new(&post_data)).unwrap();
let gid = GlyphId::new(9);
assert!(post.glyph_name(gid.try_into().unwrap()).unwrap().is_empty());
let names = GlyphNames {
inner: Inner::Post(post, 10),
};
assert_eq!(names.get(gid).unwrap(), "gid9");
assert_eq!(names.iter().last().unwrap().1, "gid9");
}
#[test]
fn cff_glyph_names() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let names = GlyphNames::new(&font);
assert_eq!(names.source(), GlyphNameSource::Cff);
let expected_names = [".notdef", "i", "j", "k", "l"];
check_names(&names, &expected_names, GlyphNameSource::Cff);
}
#[test]
fn post_glyph_names() {
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
let names = GlyphNames::new(&font);
let expected_names = [
".notdef",
"space",
"A",
"I",
"T",
"Aacute",
"Agrave",
"Iacute",
"Igrave",
"Amacron",
"Imacron",
"acutecomb",
"gravecomb",
"macroncomb",
"A.001",
"A.002",
"A.003",
"A.004",
"A.005",
"A.006",
"A.007",
"A.008",
"A.009",
"A.010",
];
check_names(&names, &expected_names, GlyphNameSource::Post);
}
#[test]
fn post_glyph_names_partial() {
let font = FontRef::new(font_test_data::HVAR_WITH_TRUNCATED_ADVANCE_INDEX_MAP).unwrap();
let mut names = GlyphNames::new(&font);
let Inner::Post(_, len) = &mut names.inner else {
panic!("it's a post table!");
};
// Increase count by 4 so we synthesize the remaining names
*len += 4;
let expected_names = [
".notdef",
"space",
"A",
"I",
"T",
"Aacute",
"Agrave",
"Iacute",
"Igrave",
"Amacron",
"Imacron",
"acutecomb",
"gravecomb",
"macroncomb",
"A.001",
"A.002",
"A.003",
"A.004",
"A.005",
"A.006",
"A.007",
"A.008",
"A.009",
"A.010",
// synthesized names...
"gid24",
"gid25",
"gid26",
"gid27",
];
check_names(&names, &expected_names, GlyphNameSource::Post);
}
fn check_names(names: &GlyphNames, expected_names: &[&str], expected_source: GlyphNameSource) {
assert_eq!(names.source(), expected_source);
let iter_names = names.iter().collect::<Vec<_>>();
assert_eq!(iter_names.len(), expected_names.len());
for (i, expected) in expected_names.iter().enumerate() {
let gid = GlyphId::new(i as u32);
let name = names.get(gid).unwrap();
assert_eq!(name, expected);
assert_eq!(iter_names[i].0, gid);
assert_eq!(iter_names[i].1, expected);
}
}
}

248
vendor/skrifa/src/instance.rs vendored Normal file
View File

@@ -0,0 +1,248 @@
//! Helpers for selecting a font size and location in variation space.
use read_fonts::types::Fixed;
use crate::collections::SmallVec;
/// Type for a normalized variation coordinate.
pub type NormalizedCoord = read_fonts::types::F2Dot14;
/// Font size in pixels per em units.
///
/// Sizes in this crate are represented as a ratio of pixels to the size of
/// the em square defined by the font. This is equivalent to the `px` unit
/// in CSS (assuming a DPI scale factor of 1.0).
///
/// To retrieve metrics and outlines in font units, use the [unscaled](Self::unscaled)
/// constructor on this type.
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub struct Size(Option<f32>);
impl Size {
/// Creates a new font size from the given value in pixels per em units.
pub fn new(ppem: f32) -> Self {
Self(Some(ppem))
}
/// Creates a new font size for generating unscaled metrics or outlines in
/// font units.
pub fn unscaled() -> Self {
Self(None)
}
/// Returns the raw size in pixels per em units.
///
/// Results in `None` if the size is unscaled.
pub fn ppem(self) -> Option<f32> {
self.0
}
/// Computes a linear scale factor for this font size and the given units
/// per em value which can be retrieved from the [Metrics](crate::metrics::Metrics)
/// type or from the [head](read_fonts::tables::head::Head) table.
///
/// Returns 1.0 for an unscaled size or when `units_per_em` is 0.
pub fn linear_scale(self, units_per_em: u16) -> f32 {
match self.0 {
Some(ppem) if units_per_em != 0 => ppem / units_per_em as f32,
_ => 1.0,
}
}
/// Computes a fixed point linear scale factor that matches FreeType.
pub(crate) fn fixed_linear_scale(self, units_per_em: u16) -> Fixed {
// FreeType computes a 16.16 scale factor that converts to 26.6.
// This is done in two steps, assuming use of FT_Set_Pixel_Size:
// 1) height is multiplied by 64:
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/49781ab72b2dfd0f78172023921d08d08f323ade/src/base/ftobjs.c#L3596>
// 2) this value is divided by UPEM:
// (here, scaled_h=height and h=upem)
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/49781ab72b2dfd0f78172023921d08d08f323ade/src/base/ftobjs.c#L3312>
match self.0 {
Some(ppem) if units_per_em > 0 => {
Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(units_per_em as i32)
}
_ => {
// This is an identity scale for the pattern
// `mul_div(value, scale, 64)`
Fixed::from_bits(0x10000 * 64)
}
}
}
}
/// Reference to an ordered sequence of normalized variation coordinates.
///
/// To convert from user coordinates see [`crate::AxisCollection::location`].
///
/// This type represents a position in the variation space where each
/// coordinate corresponds to an axis (in the same order as the `fvar` table)
/// and is a normalized value in the range `[-1..1]`.
///
/// See [Coordinate Scales and Normalization](https://learn.microsoft.com/en-us/typography/opentype/spec/otvaroverview#coordinate-scales-and-normalization)
/// for further details.
///
/// If the array is larger in length than the number of axes, extraneous
/// values are ignored. If it is smaller, unrepresented axes are assumed to be
/// at their default positions (i.e. 0).
///
/// A value of this type constructed with `default()` represents the default
/// position for each axis.
///
/// Normalized coordinates are ignored for non-variable fonts.
#[derive(Copy, Clone, Default, Debug)]
pub struct LocationRef<'a>(&'a [NormalizedCoord]);
impl<'a> LocationRef<'a> {
/// Creates a new sequence of normalized coordinates from the given array.
pub fn new(coords: &'a [NormalizedCoord]) -> Self {
Self(coords)
}
/// Returns the underlying array of normalized coordinates.
pub fn coords(&self) -> &'a [NormalizedCoord] {
self.0
}
/// Returns true if this represents the default location in variation
/// space.
///
/// This is represented a set of normalized coordinates that is either
/// empty or contains all zeroes.
pub fn is_default(&self) -> bool {
self.0.is_empty() || self.0.iter().all(|coord| *coord == NormalizedCoord::ZERO)
}
/// Returns the underlying coordinate array if any of the entries are
/// non-zero. Otherwise returns the empty slice.
///
/// This allows internal routines to bypass expensive variation code
/// paths by just checking for an empty slice.
pub(crate) fn effective_coords(&self) -> &'a [NormalizedCoord] {
if self.is_default() {
&[]
} else {
self.0
}
}
}
impl<'a> From<&'a [NormalizedCoord]> for LocationRef<'a> {
fn from(value: &'a [NormalizedCoord]) -> Self {
Self(value)
}
}
impl<'a> IntoIterator for LocationRef<'a> {
type IntoIter = core::slice::Iter<'a, NormalizedCoord>;
type Item = &'a NormalizedCoord;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl<'a> IntoIterator for &'_ LocationRef<'a> {
type IntoIter = core::slice::Iter<'a, NormalizedCoord>;
type Item = &'a NormalizedCoord;
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
/// Maximum number of coords to store inline in a `Location` object.
///
/// This value was chosen to maximize use of space in the underlying
/// `SmallVec` storage.
const MAX_INLINE_COORDS: usize = 8;
/// Ordered sequence of normalized variation coordinates.
///
/// To produce from user coordinates see [`crate::AxisCollection::location`].
///
/// This is an owned version of [`LocationRef`]. See the documentation on that
/// type for more detail.
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct Location {
coords: SmallVec<NormalizedCoord, MAX_INLINE_COORDS>,
}
impl Location {
/// Creates a new location with the given number of normalized coordinates.
///
/// Each element will be initialized to the default value (0.0).
pub fn new(len: usize) -> Self {
Self {
coords: SmallVec::with_len(len, NormalizedCoord::default()),
}
}
/// Returns the underlying slice of normalized coordinates.
pub fn coords(&self) -> &[NormalizedCoord] {
self.coords.as_slice()
}
/// Returns a mutable reference to the underlying slice of normalized
/// coordinates.
pub fn coords_mut(&mut self) -> &mut [NormalizedCoord] {
self.coords.as_mut_slice()
}
}
impl Default for Location {
fn default() -> Self {
Self {
coords: SmallVec::new(),
}
}
}
impl<'a> From<&'a Location> for LocationRef<'a> {
fn from(value: &'a Location) -> Self {
LocationRef(value.coords())
}
}
impl<'a> IntoIterator for &'a Location {
type IntoIter = core::slice::Iter<'a, NormalizedCoord>;
type Item = &'a NormalizedCoord;
fn into_iter(self) -> Self::IntoIter {
self.coords().iter()
}
}
impl<'a> IntoIterator for &'a mut Location {
type IntoIter = core::slice::IterMut<'a, NormalizedCoord>;
type Item = &'a mut NormalizedCoord;
fn into_iter(self) -> Self::IntoIter {
self.coords_mut().iter_mut()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{FontRef, MetadataProvider};
#[test]
fn effective_coords() {
let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
let location = font.axes().location([("AVAR", 50.0), ("AVWK", 75.0)]);
let loc_ref = LocationRef::from(&location);
assert!(!loc_ref.is_default());
assert_eq!(loc_ref.effective_coords().len(), 2);
}
#[test]
fn effective_coords_for_default() {
let font = FontRef::new(font_test_data::AVAR2_CHECKER).unwrap();
let location = font.axes().location([("AVAR", 0.0), ("AVWK", 0.0)]);
let loc_ref = LocationRef::from(&location);
assert!(loc_ref.is_default());
assert_eq!(loc_ref.effective_coords().len(), 0);
assert_eq!(loc_ref.coords().len(), 2);
}
}

73
vendor/skrifa/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,73 @@
//! A robust, ergonomic, high performance crate for OpenType fonts.
//!
//! Skrifa is a mid level library that provides access to various types
//! of [`metadata`](MetadataProvider) contained in a font as well as support
//! for loading glyph [`outlines`](outline).
//!
//! It is described as "mid level" because the library is designed to sit
//! above low level font parsing (provided by [`read-fonts`](https://crates.io/crates/read-fonts))
//! and below a higher level text layout engine.
//!
//! See the [readme](https://github.com/googlefonts/fontations/blob/main/skrifa/README.md)
//! for additional details.
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![cfg_attr(not(any(test, feature = "std")), no_std)]
#[cfg(not(any(feature = "libm", feature = "std")))]
compile_error!("Either feature \"std\" or \"libm\" must be enabled for this crate.");
#[cfg(not(any(test, feature = "std")))]
#[macro_use]
extern crate core as std;
#[macro_use]
extern crate alloc;
/// Expose our "raw" underlying parser crate.
pub extern crate read_fonts as raw;
pub mod attribute;
pub mod bitmap;
pub mod charmap;
pub mod color;
pub mod font;
pub mod instance;
pub mod metrics;
pub mod outline;
pub mod setting;
pub mod string;
mod collections;
mod decycler;
mod glyph_name;
mod provider;
mod variation;
pub use glyph_name::{GlyphName, GlyphNameSource, GlyphNames};
#[doc(inline)]
pub use outline::{OutlineGlyph, OutlineGlyphCollection};
pub use variation::{Axis, AxisCollection, NamedInstance, NamedInstanceCollection};
/// Useful collection of common types suitable for glob importing.
pub mod prelude {
#[doc(no_inline)]
pub use super::{
font::FontRef,
instance::{LocationRef, NormalizedCoord, Size},
GlyphId, MetadataProvider, Tag,
};
}
pub use read_fonts::{
types::{GlyphId, GlyphId16, Tag},
FontRef,
};
#[doc(inline)]
pub use provider::MetadataProvider;
/// Limit for recursion when loading TrueType composite glyphs.
const GLYF_COMPOSITE_RECURSION_LIMIT: usize = 32;

577
vendor/skrifa/src/metrics.rs vendored Normal file
View File

@@ -0,0 +1,577 @@
//! Global font and glyph specific metrics.
//!
//! Metrics are various measurements that define positioning and layout
//! characteristics for a font. They come in two flavors:
//!
//! * Global metrics: these are applicable to all glyphs in a font and generally
//! define values that are used for the layout of a collection of glyphs. For example,
//! the ascent, descent and leading values determine the position of the baseline where
//! a glyph should be rendered as well as the suggested spacing above and below it.
//!
//! * Glyph metrics: these apply to single glyphs. For example, the advance
//! width value describes the distance between two consecutive glyphs on a line.
//!
//! ### Selecting an "instance"
//! Both global and glyph specific metrics accept two additional pieces of information
//! to select the desired instance of a font:
//! * Size: represented by the [Size] type, this determines the scaling factor that is
//! applied to all metrics.
//! * Normalized variation coordinates: represented by the [LocationRef] type,
//! these define the position in design space for a variable font. For a non-variable
//! font, these coordinates are ignored and you can pass [LocationRef::default()]
//! as an argument for this parameter.
//!
use read_fonts::{
tables::{
glyf::Glyf, gvar::Gvar, hmtx::LongMetric, hvar::Hvar, loca::Loca, os2::SelectionFlags,
},
types::{BigEndian, Fixed, GlyphId},
FontRef, TableProvider,
};
use super::instance::{LocationRef, NormalizedCoord, Size};
/// Type for a bounding box with single precision floating point coordinates.
pub type BoundingBox = read_fonts::types::BoundingBox<f32>;
/// Metrics for a text decoration.
///
/// This represents the suggested offset and thickness of an underline
/// or strikeout text decoration.
#[derive(Copy, Clone, PartialEq, Default, Debug)]
pub struct Decoration {
/// Offset to the top of the decoration from the baseline.
pub offset: f32,
/// Thickness of the decoration.
pub thickness: f32,
}
/// Metrics that apply to all glyphs in a font.
///
/// These are retrieved for a specific position in the design space.
///
/// This metrics here are derived from the following tables:
/// * [head](https://learn.microsoft.com/en-us/typography/opentype/spec/head): `units_per_em`, `bounds`
/// * [maxp](https://learn.microsoft.com/en-us/typography/opentype/spec/maxp): `glyph_count`
/// * [post](https://learn.microsoft.com/en-us/typography/opentype/spec/post): `is_monospace`, `italic_angle`, `underline`
/// * [OS/2](https://learn.microsoft.com/en-us/typography/opentype/spec/os2): `average_width`, `cap_height`,
/// `x_height`, `strikeout`, as well as the line metrics: `ascent`, `descent`, `leading` if the `USE_TYPOGRAPHIC_METRICS`
/// flag is set or the `hhea` line metrics are zero (the Windows metrics are used as a last resort).
/// * [hhea](https://learn.microsoft.com/en-us/typography/opentype/spec/hhea): `max_width`, as well as the line metrics:
/// `ascent`, `descent`, `leading` if they are non-zero and the `USE_TYPOGRAPHIC_METRICS` flag is not set in the OS/2 table
///
/// For variable fonts, deltas are computed using the [MVAR](https://learn.microsoft.com/en-us/typography/opentype/spec/MVAR)
/// table.
#[derive(Copy, Clone, PartialEq, Default, Debug)]
pub struct Metrics {
/// Number of font design units per em unit.
pub units_per_em: u16,
/// Number of glyphs in the font.
pub glyph_count: u16,
/// True if the font is not proportionally spaced.
pub is_monospace: bool,
/// Italic angle in counter-clockwise degrees from the vertical. Zero for upright text,
/// negative for text that leans to the right.
pub italic_angle: f32,
/// Distance from the baseline to the top of the alignment box.
pub ascent: f32,
/// Distance from the baseline to the bottom of the alignment box.
pub descent: f32,
/// Recommended additional spacing between lines.
pub leading: f32,
/// Distance from the baseline to the top of a typical English capital.
pub cap_height: Option<f32>,
/// Distance from the baseline to the top of the lowercase "x" or
/// similar character.
pub x_height: Option<f32>,
/// Average width of all non-zero width characters in the font.
pub average_width: Option<f32>,
/// Maximum advance width of all characters in the font.
pub max_width: Option<f32>,
/// Metrics for an underline decoration.
pub underline: Option<Decoration>,
/// Metrics for a strikeout decoration.
pub strikeout: Option<Decoration>,
/// Union of minimum and maximum extents for all glyphs in the font.
pub bounds: Option<BoundingBox>,
}
impl Metrics {
/// Creates new metrics for the given font, size, and location in
/// normalized variation space.
pub fn new<'a>(font: &FontRef<'a>, size: Size, location: impl Into<LocationRef<'a>>) -> Self {
let head = font.head();
let mut metrics = Metrics {
units_per_em: head.map(|head| head.units_per_em()).unwrap_or_default(),
..Default::default()
};
let coords = location.into().effective_coords();
let scale = size.linear_scale(metrics.units_per_em);
if let Ok(head) = font.head() {
metrics.bounds = Some(BoundingBox {
x_min: head.x_min() as f32 * scale,
y_min: head.y_min() as f32 * scale,
x_max: head.x_max() as f32 * scale,
y_max: head.y_max() as f32 * scale,
});
}
if let Ok(maxp) = font.maxp() {
metrics.glyph_count = maxp.num_glyphs();
}
if let Ok(post) = font.post() {
metrics.is_monospace = post.is_fixed_pitch() != 0;
metrics.italic_angle = post.italic_angle().to_f64() as f32;
metrics.underline = Some(Decoration {
offset: post.underline_position().to_i16() as f32 * scale,
thickness: post.underline_thickness().to_i16() as f32 * scale,
});
}
let hhea = font.hhea();
if let Ok(hhea) = &hhea {
metrics.max_width = Some(hhea.advance_width_max().to_u16() as f32 * scale);
}
// Choosing proper line metrics is a challenge due to the changing
// spec, backward compatibility and broken fonts.
//
// We use the same strategy as FreeType:
// 1. Use the OS/2 metrics if the table exists and the USE_TYPO_METRICS
// flag is set.
// 2. Otherwise, use the hhea metrics.
// 3. If hhea metrics are zero and the OS/2 table exists:
// 3a. Use the typo metrics if they are non-zero
// 3b. Otherwise, use the win metrics
//
// See: https://github.com/freetype/freetype/blob/5c37b6406258ec0d7ab64b8619c5ea2c19e3c69a/src/sfnt/sfobjs.c#L1311
let os2 = font.os2().ok();
let mut used_typo_metrics = false;
if let Some(os2) = &os2 {
if os2
.fs_selection()
.contains(SelectionFlags::USE_TYPO_METRICS)
{
metrics.ascent = os2.s_typo_ascender() as f32 * scale;
metrics.descent = os2.s_typo_descender() as f32 * scale;
metrics.leading = os2.s_typo_line_gap() as f32 * scale;
used_typo_metrics = true;
}
metrics.average_width = Some(os2.x_avg_char_width() as f32 * scale);
metrics.cap_height = os2.s_cap_height().map(|v| v as f32 * scale);
metrics.x_height = os2.sx_height().map(|v| v as f32 * scale);
metrics.strikeout = Some(Decoration {
offset: os2.y_strikeout_position() as f32 * scale,
thickness: os2.y_strikeout_size() as f32 * scale,
});
}
if !used_typo_metrics {
if let Ok(hhea) = font.hhea() {
metrics.ascent = hhea.ascender().to_i16() as f32 * scale;
metrics.descent = hhea.descender().to_i16() as f32 * scale;
metrics.leading = hhea.line_gap().to_i16() as f32 * scale;
}
if metrics.ascent == 0.0 && metrics.descent == 0.0 {
if let Some(os2) = &os2 {
if os2.s_typo_ascender() != 0 || os2.s_typo_descender() != 0 {
metrics.ascent = os2.s_typo_ascender() as f32 * scale;
metrics.descent = os2.s_typo_descender() as f32 * scale;
metrics.leading = os2.s_typo_line_gap() as f32 * scale;
} else {
metrics.ascent = os2.us_win_ascent() as f32 * scale;
// Win descent is always positive while other descent values are negative. Negate it
// to ensure we return consistent metrics.
metrics.descent = -(os2.us_win_descent() as f32 * scale);
}
}
}
}
if let (Ok(mvar), true) = (font.mvar(), !coords.is_empty()) {
use read_fonts::tables::mvar::tags::*;
let metric_delta =
|tag| mvar.metric_delta(tag, coords).unwrap_or_default().to_f64() as f32 * scale;
metrics.ascent += metric_delta(HASC);
metrics.descent += metric_delta(HDSC);
metrics.leading += metric_delta(HLGP);
if let Some(cap_height) = &mut metrics.cap_height {
*cap_height += metric_delta(CPHT);
}
if let Some(x_height) = &mut metrics.x_height {
*x_height += metric_delta(XHGT);
}
if let Some(underline) = &mut metrics.underline {
underline.offset += metric_delta(UNDO);
underline.thickness += metric_delta(UNDS);
}
if let Some(strikeout) = &mut metrics.strikeout {
strikeout.offset += metric_delta(STRO);
strikeout.thickness += metric_delta(STRS);
}
}
metrics
}
}
/// Glyph specific metrics.
#[derive(Clone)]
pub struct GlyphMetrics<'a> {
glyph_count: u32,
fixed_scale: FixedScaleFactor,
h_metrics: &'a [LongMetric],
default_advance_width: u16,
lsbs: &'a [BigEndian<i16>],
hvar: Option<Hvar<'a>>,
gvar: Option<Gvar<'a>>,
loca_glyf: Option<(Loca<'a>, Glyf<'a>)>,
coords: &'a [NormalizedCoord],
}
impl<'a> GlyphMetrics<'a> {
/// Creates new glyph metrics from the given font, size, and location in
/// normalized variation space.
pub fn new(font: &FontRef<'a>, size: Size, location: impl Into<LocationRef<'a>>) -> Self {
let glyph_count = font
.maxp()
.map(|maxp| maxp.num_glyphs() as u32)
.unwrap_or_default();
let upem = font
.head()
.map(|head| head.units_per_em())
.unwrap_or_default();
let fixed_scale = FixedScaleFactor(size.fixed_linear_scale(upem));
let coords = location.into().effective_coords();
let (h_metrics, default_advance_width, lsbs) = font
.hmtx()
.map(|hmtx| {
let h_metrics = hmtx.h_metrics();
let default_advance_width = h_metrics.last().map(|m| m.advance.get()).unwrap_or(0);
let lsbs = hmtx.left_side_bearings();
(h_metrics, default_advance_width, lsbs)
})
.unwrap_or_default();
let hvar = font.hvar().ok();
let gvar = font.gvar().ok();
let loca_glyf = if let (Ok(loca), Ok(glyf)) = (font.loca(None), font.glyf()) {
Some((loca, glyf))
} else {
None
};
Self {
glyph_count,
fixed_scale,
h_metrics,
default_advance_width,
lsbs,
hvar,
gvar,
loca_glyf,
coords,
}
}
/// Returns the number of available glyphs in the font.
pub fn glyph_count(&self) -> u32 {
self.glyph_count
}
/// Returns the advance width for the specified glyph.
///
/// If normalized coordinates were provided when constructing glyph metrics and
/// an `HVAR` table is present, applies the appropriate delta.
///
/// Returns `None` if `glyph_id >= self.glyph_count()` or the underlying font
/// data is invalid.
pub fn advance_width(&self, glyph_id: GlyphId) -> Option<f32> {
if glyph_id.to_u32() >= self.glyph_count {
return None;
}
let mut advance = self
.h_metrics
.get(glyph_id.to_u32() as usize)
.map(|metric| metric.advance())
.unwrap_or(self.default_advance_width) as i32;
if let Some(hvar) = &self.hvar {
advance += hvar
.advance_width_delta(glyph_id, self.coords)
// FreeType truncates metric deltas...
// https://github.com/freetype/freetype/blob/7838c78f53f206ac5b8e9cefde548aa81cb00cf4/src/truetype/ttgxvar.c#L1027
.map(|delta| delta.to_f64() as i32)
.unwrap_or(0);
} else if self.gvar.is_some() {
advance += self.metric_deltas_from_gvar(glyph_id).unwrap_or_default()[1];
}
Some(self.fixed_scale.apply(advance))
}
/// Returns the left side bearing for the specified glyph.
///
/// If normalized coordinates were provided when constructing glyph metrics and
/// an `HVAR` table is present, applies the appropriate delta.
///
/// Returns `None` if `glyph_id >= self.glyph_count()` or the underlying font
/// data is invalid.
pub fn left_side_bearing(&self, glyph_id: GlyphId) -> Option<f32> {
if glyph_id.to_u32() >= self.glyph_count {
return None;
}
let gid_index = glyph_id.to_u32() as usize;
let mut lsb = self
.h_metrics
.get(gid_index)
.map(|metric| metric.side_bearing())
.unwrap_or_else(|| {
self.lsbs
.get(gid_index.saturating_sub(self.h_metrics.len()))
.map(|lsb| lsb.get())
.unwrap_or_default()
}) as i32;
if let Some(hvar) = &self.hvar {
lsb += hvar
.lsb_delta(glyph_id, self.coords)
// FreeType truncates metric deltas...
// https://github.com/freetype/freetype/blob/7838c78f53f206ac5b8e9cefde548aa81cb00cf4/src/truetype/ttgxvar.c#L1027
.map(|delta| delta.to_f64() as i32)
.unwrap_or(0);
} else if self.gvar.is_some() {
lsb += self.metric_deltas_from_gvar(glyph_id).unwrap_or_default()[0];
}
Some(self.fixed_scale.apply(lsb))
}
/// Returns the bounding box for the specified glyph.
///
/// Note that variations are not reflected in the bounding box returned by
/// this method.
///
/// Returns `None` if `glyph_id >= self.glyph_count()`, the underlying font
/// data is invalid, or the font does not contain TrueType outlines.
pub fn bounds(&self, glyph_id: GlyphId) -> Option<BoundingBox> {
let (loca, glyf) = self.loca_glyf.as_ref()?;
Some(match loca.get_glyf(glyph_id, glyf).ok()? {
Some(glyph) => BoundingBox {
x_min: self.fixed_scale.apply(glyph.x_min() as i32),
y_min: self.fixed_scale.apply(glyph.y_min() as i32),
x_max: self.fixed_scale.apply(glyph.x_max() as i32),
y_max: self.fixed_scale.apply(glyph.y_max() as i32),
},
// Empty glyphs have an empty bounding box
None => BoundingBox::default(),
})
}
}
impl GlyphMetrics<'_> {
fn metric_deltas_from_gvar(&self, glyph_id: GlyphId) -> Option<[i32; 2]> {
let (loca, glyf) = self.loca_glyf.as_ref()?;
let mut deltas = self
.gvar
.as_ref()?
.phantom_point_deltas(glyf, loca, self.coords, glyph_id)
.ok()
.flatten()?;
deltas[1] -= deltas[0];
Some([deltas[0], deltas[1]].map(|delta| delta.x.to_i32()))
}
}
#[derive(Copy, Clone)]
struct FixedScaleFactor(Fixed);
impl FixedScaleFactor {
#[inline(always)]
fn apply(self, value: i32) -> f32 {
// Match FreeType metric scaling
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/base/ftadvanc.c#L50>
self.0
.mul_div(Fixed::from_bits(value), Fixed::from_bits(64))
.to_f32()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MetadataProvider as _;
use font_test_data::{SIMPLE_GLYF, VAZIRMATN_VAR};
use read_fonts::FontRef;
#[test]
fn metrics() {
let font = FontRef::new(SIMPLE_GLYF).unwrap();
let metrics = font.metrics(Size::unscaled(), LocationRef::default());
let expected = Metrics {
units_per_em: 1024,
glyph_count: 3,
bounds: Some(BoundingBox {
x_min: 51.0,
y_min: -250.0,
x_max: 998.0,
y_max: 950.0,
}),
average_width: Some(1275.0),
max_width: None,
x_height: Some(512.0),
cap_height: Some(717.0),
is_monospace: false,
italic_angle: 0.0,
ascent: 950.0,
descent: -250.0,
leading: 0.0,
underline: None,
strikeout: Some(Decoration {
offset: 307.0,
thickness: 51.0,
}),
};
assert_eq!(metrics, expected);
}
#[test]
fn metrics_missing_os2() {
let font = FontRef::new(VAZIRMATN_VAR).unwrap();
let metrics = font.metrics(Size::unscaled(), LocationRef::default());
let expected = Metrics {
units_per_em: 2048,
glyph_count: 4,
bounds: Some(BoundingBox {
x_min: 29.0,
y_min: 0.0,
x_max: 1310.0,
y_max: 1847.0,
}),
average_width: None,
max_width: Some(1336.0),
x_height: None,
cap_height: None,
is_monospace: false,
italic_angle: 0.0,
ascent: 2100.0,
descent: -1100.0,
leading: 0.0,
underline: None,
strikeout: None,
};
assert_eq!(metrics, expected);
}
#[test]
fn glyph_metrics() {
let font = FontRef::new(VAZIRMATN_VAR).unwrap();
let glyph_metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
// (advance_width, lsb) in glyph order
let expected = &[
(908.0, 100.0),
(1336.0, 29.0),
(1336.0, 29.0),
(633.0, 57.0),
];
let result = (0..4)
.map(|i| {
let gid = GlyphId::new(i as u32);
let advance_width = glyph_metrics.advance_width(gid).unwrap();
let lsb = glyph_metrics.left_side_bearing(gid).unwrap();
(advance_width, lsb)
})
.collect::<Vec<_>>();
assert_eq!(expected, &result[..]);
}
/// Asserts that the results generated with Size::unscaled() and
/// Size::new(upem) are equal.
///
/// See <https://github.com/googlefonts/fontations/issues/590#issuecomment-1711595882>
#[test]
fn glyph_metrics_unscaled_matches_upem_scale() {
let font = FontRef::new(VAZIRMATN_VAR).unwrap();
let upem = font.head().unwrap().units_per_em() as f32;
let unscaled_metrics = font.glyph_metrics(Size::unscaled(), LocationRef::default());
let upem_metrics = font.glyph_metrics(Size::new(upem), LocationRef::default());
for i in 0..unscaled_metrics.glyph_count() {
let gid = GlyphId::new(i);
assert_eq!(
unscaled_metrics.advance_width(gid),
upem_metrics.advance_width(gid)
);
assert_eq!(
unscaled_metrics.left_side_bearing(gid),
upem_metrics.left_side_bearing(gid)
);
}
}
#[test]
fn glyph_metrics_var() {
let font = FontRef::new(VAZIRMATN_VAR).unwrap();
let coords = &[NormalizedCoord::from_f32(-0.8)];
let glyph_metrics = font.glyph_metrics(Size::unscaled(), LocationRef::new(coords));
// (advance_width, lsb) in glyph order
let expected = &[
(908.0, 100.0),
(1246.0, 29.0),
(1246.0, 29.0),
(556.0, 57.0),
];
let result = (0..4)
.map(|i| {
let gid = GlyphId::new(i as u32);
let advance_width = glyph_metrics.advance_width(gid).unwrap();
let lsb = glyph_metrics.left_side_bearing(gid).unwrap();
(advance_width, lsb)
})
.collect::<Vec<_>>();
assert_eq!(expected, &result[..]);
}
#[test]
fn glyph_metrics_missing_hvar() {
let font = FontRef::new(VAZIRMATN_VAR).unwrap();
let glyph_count = font.maxp().unwrap().num_glyphs();
// Test a few different locations in variation space
for coord in [-1.0, -0.8, 0.0, 0.75, 1.0] {
let coords = &[NormalizedCoord::from_f32(coord)];
let location = LocationRef::new(coords);
let glyph_metrics = font.glyph_metrics(Size::unscaled(), location);
let mut glyph_metrics_no_hvar = glyph_metrics.clone();
// Setting hvar to None forces use of gvar for metric deltas
glyph_metrics_no_hvar.hvar = None;
for gid in 0..glyph_count {
let gid = GlyphId::from(gid);
assert_eq!(
glyph_metrics.advance_width(gid),
glyph_metrics_no_hvar.advance_width(gid)
);
assert_eq!(
glyph_metrics.left_side_bearing(gid),
glyph_metrics_no_hvar.left_side_bearing(gid)
);
}
}
}
/// Ensure our fixed point scaling code matches FreeType for advances.
///
/// <https://github.com/googlefonts/fontations/issues/590>
#[test]
fn match_freetype_glyph_metric_scaling() {
// fontations:
// gid: 36 advance: 15.33600044250488281250 gid: 68 advance: 13.46399974822998046875 gid: 47 advance: 12.57600021362304687500 gid: 79 advance: 6.19199991226196289062
// ft:
// gid: 36 advance: 15.33595275878906250000 gid: 68 advance: 13.46395874023437500000 gid: 47 advance: 12.57595825195312500000 gid: 79 advance: 6.19198608398437500000
// with font.setSize(24);
//
// Raw advances for gids 36, 68, 47, and 79 in NotoSans-Regular
let font_unit_advances = [639, 561, 524, 258];
#[allow(clippy::excessive_precision)]
let scaled_advances = [
15.33595275878906250000,
13.46395874023437500000,
12.57595825195312500000,
6.19198608398437500000,
];
let fixed_scale = FixedScaleFactor(Size::new(24.0).fixed_linear_scale(1000));
for (font_unit_advance, expected_scaled_advance) in
font_unit_advances.iter().zip(scaled_advances)
{
let scaled_advance = fixed_scale.apply(*font_unit_advance);
assert_eq!(scaled_advance, expected_scaled_advance);
}
}
}

View File

@@ -0,0 +1,949 @@
//! Edge hinting.
//!
//! Let's actually do some grid fitting. Here we align edges to the pixel
//! grid. This is the final step before applying the edge adjustments to
//! the original outline points.
use super::super::{
metrics::{fixed_mul_div, pix_floor, pix_round, Scale, ScaledAxisMetrics, ScaledWidth},
style::ScriptGroup,
topo::{Axis, Edge},
};
/// Main Latin grid-fitting routine.
///
/// Note: this is one huge function in FreeType, broken up into several below.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2999>
pub(crate) fn hint_edges(
axis: &mut Axis,
metrics: &ScaledAxisMetrics,
group: ScriptGroup,
scale: &Scale,
mut top_to_bottom_hinting: bool,
) {
if axis.dim != Axis::VERTICAL {
top_to_bottom_hinting = false;
}
// First align horizontal edges to blue zones if needed
let anchor_ix = align_edges_to_blues(axis, metrics, group, scale);
// Now align the stem edges
let (serif_count, anchor_ix) = align_stem_edges(
axis,
metrics,
group,
scale,
top_to_bottom_hinting,
anchor_ix,
);
let edges = axis.edges.as_mut_slice();
// Special case for lowercase m
if axis.dim == Axis::HORIZONTAL && (edges.len() == 6 || edges.len() == 12) {
hint_lowercase_m(edges, group);
}
// Handle serifs and single segment edges
if serif_count > 0 || anchor_ix.is_none() {
align_remaining_edges(axis, group, top_to_bottom_hinting, serif_count, anchor_ix);
}
}
/// Align horizontal edges to blue zones.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3030>
fn align_edges_to_blues(
axis: &mut Axis,
metrics: &ScaledAxisMetrics,
group: ScriptGroup,
scale: &Scale,
) -> Option<usize> {
let mut anchor_ix = None;
// For default script group, only do vertical blues
if group == ScriptGroup::Default && axis.dim != Axis::VERTICAL {
return anchor_ix;
}
for edge_ix in 0..axis.edges.len() {
let edges = axis.edges.as_mut_slice();
let edge = &edges[edge_ix];
if edge.flags & Edge::DONE != 0 {
continue;
}
let edge2_ix = edge.link_ix.map(|x| x as usize);
let edge2 = edge2_ix.map(|ix| &edges[ix]);
// If we have two neutral zones, skip one of them.
if let (true, Some(edge2)) = (edge.blue_edge.is_some(), edge2) {
if edge2.blue_edge.is_some() {
let skip_ix = if edge2.flags & Edge::NEUTRAL != 0 {
edge2_ix
} else if edge.flags & Edge::NEUTRAL != 0 {
Some(edge_ix)
} else {
None
};
if let Some(skip_ix) = skip_ix {
let skip_edge = &mut edges[skip_ix];
skip_edge.blue_edge = None;
skip_edge.flags &= !Edge::NEUTRAL;
}
}
}
// Flip edges if the other is aligned to a blue zone
let blue = edges[edge_ix].blue_edge;
let (blue, edge1_ix, edge2_ix) = if let Some(blue) = blue {
(blue, Some(edge_ix), edge2_ix)
} else if let Some(edge2_blue) = edge2_ix.and_then(|ix| edges[ix].blue_edge) {
(edge2_blue, edge2_ix, Some(edge_ix))
} else {
(Default::default(), None, None)
};
let Some(edge1_ix) = edge1_ix else {
continue;
};
let edge1 = &mut edges[edge1_ix];
edge1.pos = blue.fitted;
edge1.flags |= Edge::DONE;
if let Some(edge2_ix) = edge2_ix {
let edge2 = &mut edges[edge2_ix];
if edge2.blue_edge.is_none() {
edge2.flags |= Edge::DONE;
align_linked_edge(axis, metrics, group, scale, edge1_ix, edge2_ix);
}
}
if anchor_ix.is_none() {
anchor_ix = Some(edge_ix);
}
}
anchor_ix
}
/// Align stem edges, trying to main relative order of stems in the glyph.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3123>
fn align_stem_edges(
axis: &mut Axis,
metrics: &ScaledAxisMetrics,
group: ScriptGroup,
scale: &Scale,
top_to_bottom_hinting: bool,
mut anchor_ix: Option<usize>,
) -> (usize, Option<usize>) {
let mut serif_count = 0;
let mut last_stem_pos = None;
let mut delta = 0;
// Now align all other stem edges
// This code starts at: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3123>
for edge_ix in 0..axis.edges.len() {
let edges = axis.edges.as_mut_slice();
let edge = &edges[edge_ix];
if edge.flags & Edge::DONE != 0 {
continue;
}
// Skip all non-stem edges
let Some(edge2_ix) = edge.link_ix.map(|ix| ix as usize) else {
serif_count += 1;
continue;
};
// For CJK, skip stems that are too close. We'll deal with them later
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1912>
if group != ScriptGroup::Default {
if let Some(last_pos) = last_stem_pos {
if edge.pos < last_pos + 64 || edges[edge2_ix].pos < last_pos + 64 {
serif_count += 1;
continue;
}
}
}
// This shouldn't happen?
if edges[edge2_ix].blue_edge.is_some() {
edges[edge2_ix].flags |= Edge::DONE;
align_linked_edge(axis, metrics, group, scale, edge2_ix, edge_ix);
continue;
}
if group == ScriptGroup::Default {
// Now align the stem
// Note: the branches here are reversed from the FreeType code
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3155>
if let Some(anchor_ix) = anchor_ix {
let anchor = &edges[anchor_ix];
let edge = edges[edge_ix];
let edge2 = edges[edge2_ix];
let original_pos = anchor.pos + (edge.opos - anchor.opos);
let original_len = edge2.opos - edge.opos;
let original_center = original_pos + (original_len >> 1);
let cur_len = stem_width(
metrics,
group,
scale,
original_len,
0,
edge.flags,
edge2.flags,
);
if edge2.flags & Edge::DONE != 0 {
let new_pos = edge2.pos - cur_len;
edges[edge_ix].pos = new_pos;
} else if cur_len < 96 {
let cur_pos1 = pix_round(original_center);
let (u_off, d_off) = if cur_len <= 64 { (32, 32) } else { (38, 26) };
let delta1 = (original_center - (cur_pos1 - u_off)).abs();
let delta2 = (original_center - (cur_pos1 + d_off)).abs();
let cur_pos1 = if delta1 < delta2 {
cur_pos1 - u_off
} else {
cur_pos1 + d_off
};
edges[edge_ix].pos = cur_pos1 - cur_len / 2;
edges[edge2_ix].pos = cur_pos1 + cur_len / 2;
} else {
let cur_pos1 = pix_round(original_pos);
let delta1 = (cur_pos1 + (cur_len >> 1) - original_center).abs();
let cur_pos2 = pix_round(original_pos + original_len) - cur_len;
let delta2 = (cur_pos2 + (cur_len >> 1) - original_center).abs();
let new_pos = if delta1 < delta2 { cur_pos1 } else { cur_pos2 };
let new_pos2 = new_pos + cur_len;
edges[edge_ix].pos = new_pos;
edges[edge2_ix].pos = new_pos2;
}
edges[edge_ix].flags |= Edge::DONE;
edges[edge2_ix].flags |= Edge::DONE;
if edge_ix > 0 {
adjust_link(edges, edge_ix, LinkDir::Prev, top_to_bottom_hinting);
}
} else {
// No stem has been aligned yet
let edge = edges[edge_ix];
let edge2 = edges[edge2_ix];
let original_len = edge2.opos - edge.opos;
let cur_len = stem_width(
metrics,
group,
scale,
original_len,
0,
edge.flags,
edge2.flags,
);
// Some "voodoo" to specially round edges for small stem widths
let (u_off, d_off) = if cur_len <= 64 {
// width <= 1px
(32, 32)
} else {
// 1px < width < 1.5px
(38, 26)
};
if cur_len < 96 {
let original_center = edge.opos + (original_len >> 1);
let mut cur_pos1 = pix_round(original_center);
let error1 = (original_center - (cur_pos1 - u_off)).abs();
let error2 = (original_center - (cur_pos1 + d_off)).abs();
if error1 < error2 {
cur_pos1 -= u_off;
} else {
cur_pos1 += d_off;
}
let edge_pos = cur_pos1 - cur_len / 2;
edges[edge_ix].pos = edge_pos;
edges[edge2_ix].pos = edge_pos + cur_len;
} else {
edges[edge_ix].pos = pix_round(edge.opos);
}
edges[edge_ix].flags |= Edge::DONE;
align_linked_edge(axis, metrics, group, scale, edge_ix, edge2_ix);
anchor_ix = Some(edge_ix);
}
} else {
// More CJK divergence
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1937>
if edge2_ix < edge_ix {
last_stem_pos = Some(edge.pos);
edges[edge_ix].flags |= Edge::DONE;
align_linked_edge(axis, metrics, group, scale, edge2_ix, edge_ix);
continue;
}
if axis.dim != Axis::VERTICAL && anchor_ix.is_none() {
delta = hint_normal_stem_cjk(axis, metrics, group, scale, edge_ix, edge2_ix, delta);
} else {
hint_normal_stem_cjk(axis, metrics, group, scale, edge_ix, edge2_ix, delta);
}
anchor_ix = Some(edge_ix);
axis.edges[edge_ix].flags |= Edge::DONE;
let edge2 = &mut axis.edges[edge2_ix];
edge2.flags |= Edge::DONE;
last_stem_pos = Some(edge2.pos);
}
}
(serif_count, anchor_ix)
}
/// Make sure that lowercase m's maintain symmetry.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3365>
fn hint_lowercase_m(edges: &mut [Edge], group: ScriptGroup) {
let (edge1_ix, edge2_ix, edge3_ix) = if edges.len() == 6 {
(0, 2, 4)
} else {
(1, 5, 9)
};
let edge1 = &edges[edge1_ix];
let edge2 = &edges[edge2_ix];
let edge3 = &edges[edge3_ix];
let dist1 = edge2.opos - edge1.opos;
let dist2 = edge3.opos - edge2.opos;
let span = (dist1 - dist2).abs();
if group != ScriptGroup::Default {
// CJK has additional conditions on the following...
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L2090>
for (edge, ix) in [(edge1, edge1_ix), (edge2, edge2_ix), (edge3, edge3_ix)] {
if edge.link_ix != Some((ix + 1) as u16) {
return;
}
}
}
if span < 8 {
let delta = edge3.pos - (2 * edge2.pos - edge1.pos);
let link_ix = edge3.link_ix.map(|ix| ix as usize);
let edge3 = &mut edges[edge3_ix];
edge3.pos -= delta;
edge3.flags |= Edge::DONE;
if let Some(link_ix) = link_ix {
let link = &mut edges[link_ix];
link.pos -= delta;
link.flags |= Edge::DONE;
}
// Move serifs along with the stem
if edges.len() == 12 {
edges[8].pos -= delta;
edges[11].pos -= delta;
}
}
}
/// Align serif and single segment edges.
fn align_remaining_edges(
axis: &mut Axis,
group: ScriptGroup,
top_to_bottom_hinting: bool,
mut serif_count: usize,
mut anchor_ix: Option<usize>,
) {
if group == ScriptGroup::Default {
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3418>
for edge_ix in 0..axis.edges.len() {
let edges = &mut axis.edges;
let edge = &edges[edge_ix];
if edge.flags & Edge::DONE != 0 {
continue;
}
let mut delta = 1000;
if let Some(serif) = edge.serif(edges) {
delta = (serif.opos - edge.opos).abs();
}
if delta < 64 + 16 {
// delta is only < 1000 if edge.serif_ix is Some(_)
let serif_ix = edge.serif_ix.unwrap() as usize;
align_serif_edge(axis, serif_ix, edge_ix)
} else if let Some(anchor_ix) = anchor_ix {
let [before_ix, after_ix] = find_bounding_completed_edges(edges, edge_ix);
if let Some((before_ix, after_ix)) = before_ix.zip(after_ix) {
let before = &edges[before_ix];
let after = &edges[after_ix];
let new_pos = if after.opos == before.opos {
before.pos
} else {
before.pos
+ fixed_mul_div(
edge.opos - before.opos,
after.pos - before.pos,
after.opos - before.opos,
)
};
edges[edge_ix].pos = new_pos;
} else {
let anchor = &edges[anchor_ix];
let new_pos = anchor.pos + ((edge.opos - anchor.opos + 16) & !31);
edges[edge_ix].pos = new_pos;
}
} else {
anchor_ix = Some(edge_ix);
let new_pos = pix_round(edge.opos);
edges[edge_ix].pos = new_pos;
}
let edges = &mut axis.edges;
edges[edge_ix].flags |= Edge::DONE;
adjust_link(edges, edge_ix, LinkDir::Prev, top_to_bottom_hinting);
adjust_link(edges, edge_ix, LinkDir::Next, top_to_bottom_hinting);
}
} else {
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L2119>
for edge_ix in 0..axis.edges.len() {
let edge = &mut axis.edges[edge_ix];
if edge.flags & Edge::DONE != 0 {
continue;
}
if let Some(serif_ix) = edge.serif_ix.map(|ix| ix as usize) {
edge.flags |= Edge::DONE;
align_serif_edge(axis, serif_ix, edge_ix);
serif_count = serif_count.saturating_sub(1);
}
}
if serif_count == 0 {
return;
}
for edge_ix in 0..axis.edges.len() {
let edges = axis.edges.as_mut_slice();
let edge = &edges[edge_ix];
if edge.flags & Edge::DONE != 0 {
continue;
}
let [before_ix, after_ix] = find_bounding_completed_edges(edges, edge_ix);
match (before_ix, after_ix) {
(Some(before_ix), None) => {
align_serif_edge(axis, before_ix, edge_ix);
}
(None, Some(after_ix)) => {
align_serif_edge(axis, after_ix, edge_ix);
}
(Some(before_ix), Some(after_ix)) => {
let before = edges[before_ix];
let after = edges[after_ix];
if after.fpos == before.fpos {
edges[edge_ix].pos = before.pos;
} else {
edges[edge_ix].pos = before.pos
+ fixed_mul_div(
edge.fpos as i32 - before.fpos as i32,
after.pos - before.pos,
after.fpos as i32 - before.fpos as i32,
);
}
}
_ => {}
}
}
}
}
#[derive(Copy, Clone, PartialEq)]
enum LinkDir {
Prev,
Next,
}
/// Helper to adjust links based on hinting direction.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3499>
fn adjust_link(
edges: &mut [Edge],
edge_ix: usize,
link_dir: LinkDir,
top_to_bottom_hinting: bool,
) -> Option<()> {
let edge = &edges[edge_ix];
let (edge2, prev_edge) = if link_dir == LinkDir::Next {
let edge2 = edges.get(edge_ix + 1)?;
// Don't adjust next edge if it's not done yet
if edge2.flags & Edge::DONE == 0 {
return None;
}
(edge2, edges.get(edge_ix.checked_sub(1)?)?)
} else {
let edge = edges.get(edge_ix.checked_sub(1)?)?;
(edge, edge)
};
let pos1 = edge.pos;
let pos2 = edge2.pos;
let order_check = match (link_dir, top_to_bottom_hinting) {
(LinkDir::Prev, true) | (LinkDir::Next, false) => pos1 > pos2,
(LinkDir::Prev, false) | (LinkDir::Next, true) => pos1 < pos2,
};
if !order_check {
return None;
}
let link = edge.link(edges)?;
if (link.pos - prev_edge.pos).abs() > 16 {
let new_pos = edge2.pos;
edges[edge_ix].pos = new_pos;
}
Some(())
}
/// Returns the indices of the "completed" edges before and after the given
/// edge index.
fn find_bounding_completed_edges(edges: &[Edge], ix: usize) -> [Option<usize>; 2] {
let before_ix = edges
.get(..ix)
.unwrap_or_default()
.iter()
.enumerate()
.rev()
.filter_map(|(ix, edge)| (edge.flags & Edge::DONE != 0).then_some(ix))
.next();
let after_ix = edges
.iter()
.enumerate()
.skip(ix + 1)
.filter_map(|(ix, edge)| (edge.flags & Edge::DONE != 0).then_some(ix))
.next();
[before_ix, after_ix]
}
/// Snap a scaled width to one of the standard widths.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2697>
fn snap_width(widths: &[ScaledWidth], width: i32) -> i32 {
let (_, ref_width) =
widths
.iter()
.fold((64 + 32 + 2, width), |(best_dist, ref_width), candidate| {
let dist = (width - candidate.scaled).abs();
if dist < best_dist {
(dist, candidate.scaled)
} else {
(best_dist, ref_width)
}
});
let scaled = pix_round(ref_width);
if width >= ref_width {
if width < scaled + 48 {
ref_width
} else {
width
}
} else if width > scaled - 48 {
ref_width
} else {
width
}
}
/// Compute the snapped width of a given stem.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2746>
fn stem_width(
metrics: &ScaledAxisMetrics,
group: ScriptGroup,
scale: &Scale,
width: i32,
base_delta: i32,
base_flags: u8,
stem_flags: u8,
) -> i32 {
if scale.flags & Scale::STEM_ADJUST == 0
|| (group == ScriptGroup::Default && metrics.width_metrics.is_extra_light)
{
return width;
}
let is_vertical = metrics.dim == Axis::VERTICAL;
let sign = if width < 0 { -1 } else { 1 };
let mut dist = width.abs();
if (is_vertical && scale.flags & Scale::VERTICAL_SNAP == 0)
|| (!is_vertical && scale.flags & Scale::HORIZONTAL_SNAP == 0)
{
// Do smooth hinting
if group == ScriptGroup::Default {
if (stem_flags & Edge::SERIF != 0) && is_vertical && (dist < 3 * 64) {
// Don't touch widths of serifs
return dist * sign;
} else if base_flags & Edge::ROUND != 0 {
if dist < 80 {
dist = 64;
}
} else if dist < 56 {
dist = 56;
}
}
if !metrics.widths.is_empty() {
// Compare to standard width
let min_width = metrics.widths[0].scaled;
let delta = (dist - min_width).abs();
if delta < 40 {
dist = min_width.max(48);
return dist * sign;
}
if group == ScriptGroup::Default {
// Default/Latin behavior
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2809>
if dist < 3 * 64 {
let delta = dist & 63;
dist &= -64;
if delta < 10 {
dist += delta;
} else if delta < 32 {
dist += 10;
} else if delta < 54 {
dist += 54;
} else {
dist += delta;
}
} else {
let mut new_base_delta = 0;
if (width > 0 && base_delta > 0) || (width < 0 && base_delta < 0) {
if scale.size < 10.0 {
new_base_delta = base_delta;
} else if scale.size < 30.0 {
new_base_delta = (base_delta * (30.0 - scale.size) as i32) / 20;
}
}
dist = (dist - new_base_delta.abs() + 32) & !63;
}
}
}
if group != ScriptGroup::Default {
// Divergent CJK behavior
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1544>
if dist < 54 {
dist += (54 - dist) / 2;
} else if dist < 3 * 64 {
let delta = dist & 63;
dist &= -64;
if delta < 10 {
dist += delta;
} else if delta < 22 {
dist += 10;
} else if delta < 42 {
dist += delta;
} else if delta < 54 {
dist += 54;
} else {
dist += delta;
}
}
}
} else {
// Do strong hinting: snap to integer pixels
let original_dist = dist;
dist = snap_width(&metrics.widths, dist);
if is_vertical {
// Always round to integers in the vertical case
if dist >= 64 {
dist = (dist + 16) & !63;
} else {
dist = 64;
}
} else if scale.flags & Scale::MONO != 0 {
// Mono horizontal hinting: snap to integer with different
// threshold
if dist < 64 {
dist = 64;
} else {
dist = (dist + 32) & !63;
}
} else {
// Smooth horizontal hinting: strengthen small stems, round
// stems whose size is between 1 and 2 pixels
if dist < 48 {
dist = (dist + 64) >> 1;
} else if dist < 128 {
// Only round to integer if distortion is less than
// 1/4 pixel
dist = (dist + 22) & !63;
if group == ScriptGroup::Default {
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2914>
let delta = (dist - original_dist).abs();
if delta >= 16 {
dist = original_dist;
if dist < 48 {
dist = (dist + 64) >> 1;
}
}
}
} else {
// Round otherwise to prevent color fringes in LCD mode
dist = (dist + 32) & !63;
}
}
}
dist * sign
}
/// Align one stem edge relative to previous stem edge.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2943>
fn align_linked_edge(
axis: &mut Axis,
metrics: &ScaledAxisMetrics,
group: ScriptGroup,
scale: &Scale,
base_edge_ix: usize,
stem_edge_ix: usize,
) {
let edges = axis.edges.as_mut_slice();
let base_edge = &edges[base_edge_ix];
let stem_edge = &edges[stem_edge_ix];
let width = stem_edge.opos - base_edge.opos;
let base_delta = base_edge.pos - base_edge.opos;
let fitted_width = stem_width(
metrics,
group,
scale,
width,
base_delta,
base_edge.flags,
stem_edge.flags,
);
edges[stem_edge_ix].pos = base_edge.pos + fitted_width;
}
/// Shift the serif edge by the adjustment made to base edge.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2975>
fn align_serif_edge(axis: &mut Axis, base_edge_ix: usize, serif_edge_ix: usize) {
let edges = axis.edges.as_mut_slice();
let base_edge = &edges[base_edge_ix];
let serif_edge = &edges[serif_edge_ix];
edges[serif_edge_ix].pos = base_edge.pos + (serif_edge.opos - base_edge.opos);
}
/// Adjusts both edges of a stem and returns the delta.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1678>
fn hint_normal_stem_cjk(
axis: &mut Axis,
metrics: &ScaledAxisMetrics,
group: ScriptGroup,
scale: &Scale,
edge_ix: usize,
edge2_ix: usize,
anchor: i32,
) -> i32 {
const MAX_HORIZONTAL_GAP: i32 = 9;
const MAX_VERTICAL_GAP: i32 = 15;
const MAX_DELTA_ABS: i32 = 14;
let edge = axis.edges[edge_ix];
let edge2 = axis.edges[edge2_ix];
let do_stem_adjust = scale.flags & Scale::STEM_ADJUST != 0;
let threshold_delta = if do_stem_adjust {
0
} else {
let delta = if axis.dim == Axis::VERTICAL {
MAX_HORIZONTAL_GAP
} else {
MAX_VERTICAL_GAP
};
if edge.flags & Edge::ROUND != 0 && edge2.flags & Edge::ROUND != 0 {
delta
} else {
delta / 3
}
};
let threshold = 64 - threshold_delta;
let original_len = edge2.opos - edge.opos;
let cur_len = stem_width(
metrics,
group,
scale,
original_len,
0,
edge.flags,
edge2.flags,
);
let original_center = (edge.opos + edge2.opos) / 2 + anchor;
let cur_pos1 = original_center - cur_len / 2;
let cur_pos2 = cur_pos1 + cur_len;
let mut finish = |mut delta: i32| {
if !do_stem_adjust {
delta = delta.clamp(-MAX_DELTA_ABS, MAX_DELTA_ABS);
}
let adjustment = cur_pos1 + delta;
if edge.opos < edge2.opos {
axis.edges[edge_ix].pos = adjustment;
axis.edges[edge2_ix].pos = adjustment + cur_len;
} else {
axis.edges[edge2_ix].pos = adjustment;
axis.edges[edge_ix].pos = adjustment + cur_len;
}
delta
};
let mut d_off1 = cur_pos1 - pix_floor(cur_pos1);
let mut d_off2 = cur_pos2 - pix_floor(cur_pos2);
let mut delta = 0;
if d_off1 == 0 || d_off2 == 0 {
return finish(delta);
}
let mut u_off1 = 64 - d_off1;
let mut u_off2 = 64 - d_off2;
if cur_len <= threshold {
if d_off2 < cur_len {
delta = if u_off1 <= d_off2 { u_off1 } else { -d_off2 };
}
return finish(delta);
}
if threshold < 64
&& (d_off1 >= threshold
|| u_off1 >= threshold
|| d_off2 >= threshold
|| u_off2 >= threshold)
{
return finish(delta);
}
let mut offset = cur_len & 63;
if offset < 32 {
if u_off1 <= offset || d_off2 <= offset {
return finish(delta);
}
} else {
offset = 64 - threshold;
}
d_off1 = threshold - u_off1;
u_off1 -= offset;
u_off2 = threshold - d_off2;
d_off2 -= offset;
if d_off1 <= u_off1 {
u_off1 = -d_off1;
}
if d_off2 <= u_off2 {
u_off2 = -d_off2;
}
if u_off1.abs() <= u_off2.abs() {
delta = u_off1;
} else {
delta = u_off2;
}
finish(delta)
}
#[cfg(test)]
mod tests {
use super::{
super::super::{
metrics,
outline::Outline,
shape::{Shaper, ShaperMode},
style, topo,
},
*,
};
use crate::{attribute::Style, MetadataProvider};
use raw::{types::GlyphId, FontRef, TableProvider};
#[test]
fn edge_hinting_default() {
let expected_h_edges = [
(0, Edge::DONE | Edge::ROUND),
(133, Edge::DONE),
(187, Edge::DONE),
(192, Edge::DONE | Edge::ROUND),
];
let expected_v_edges = [
(-256, Edge::DONE),
(463, Edge::DONE),
(576, Edge::DONE | Edge::ROUND | Edge::SERIF),
(633, Edge::DONE),
];
check_edges(
font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
GlyphId::new(9),
style::StyleClass::HEBR,
&expected_h_edges,
&expected_v_edges,
);
}
#[test]
fn edge_hinting_cjk() {
let expected_h_edges = [
(128, Edge::DONE),
(193, Edge::DONE),
(473, 0),
(594, 0),
(704, Edge::DONE),
(673, Edge::DONE),
(767, Edge::DONE),
(832, Edge::DONE),
(896, Edge::DONE),
];
let expected_v_edges = [
(-64, Edge::DONE | Edge::ROUND),
(15, Edge::ROUND),
(142, Edge::ROUND),
(546, Edge::DONE),
(624, Edge::DONE),
(576, Edge::DONE),
(720, Edge::DONE),
(768, Edge::DONE),
(799, Edge::ROUND),
];
check_edges(
font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
GlyphId::new(9),
style::StyleClass::HANI,
&expected_h_edges,
&expected_v_edges,
);
}
fn check_edges(
font_data: &[u8],
glyph_id: GlyphId,
class: usize,
expected_h_edges: &[(i32, u8)],
expected_v_edges: &[(i32, u8)],
) {
let font = FontRef::new(font_data).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let class = &style::STYLE_CLASSES[class];
let unscaled_metrics =
metrics::compute_unscaled_style_metrics(&shaper, Default::default(), class);
let scale = metrics::Scale::new(
16.0,
font.head().unwrap().units_per_em() as i32,
Style::Normal,
Default::default(),
class.script.group,
);
let scaled_metrics = metrics::scale_style_metrics(&unscaled_metrics, scale);
let glyphs = font.outline_glyphs();
let glyph = glyphs.get(glyph_id).unwrap();
let mut outline = Outline::default();
outline.fill(&glyph, Default::default()).unwrap();
let mut axes = [
Axis::new(Axis::HORIZONTAL, outline.orientation),
Axis::new(Axis::VERTICAL, outline.orientation),
];
for (dim, axis) in axes.iter_mut().enumerate() {
topo::compute_segments(&mut outline, axis, class.script.group);
topo::link_segments(
&outline,
axis,
scaled_metrics.axes[dim].scale,
class.script.group,
unscaled_metrics.axes[dim].max_width(),
);
topo::compute_edges(
axis,
&scaled_metrics.axes[0],
class.script.hint_top_to_bottom,
scaled_metrics.axes[1].scale,
class.script.group,
);
if dim == Axis::VERTICAL {
topo::compute_blue_edges(
axis,
&scale,
&unscaled_metrics.axes[dim].blues,
&scaled_metrics.axes[dim].blues,
class.script.group,
);
}
hint_edges(
axis,
&scaled_metrics.axes[dim],
class.script.group,
&scale,
class.script.hint_top_to_bottom,
);
}
// Only pos and flags fields are modified by edge hinting
let h_edges = axes[Axis::HORIZONTAL]
.edges
.iter()
.map(|edge| (edge.pos, edge.flags))
.collect::<Vec<_>>();
let v_edges = axes[Axis::VERTICAL]
.edges
.iter()
.map(|edge| (edge.pos, edge.flags))
.collect::<Vec<_>>();
assert_eq!(h_edges, expected_h_edges);
assert_eq!(v_edges, expected_v_edges);
}
}

View File

@@ -0,0 +1,119 @@
//! Entry point to hinting algorithm.
mod edges;
mod outline;
use super::{
metrics::{scale_style_metrics, Scale, UnscaledStyleMetrics},
outline::Outline,
style::{GlyphStyle, ScriptGroup},
topo::{self, Axis},
};
/// Captures adjusted horizontal scale and outer edge positions to be used
/// for horizontal metrics adjustments.
#[derive(Copy, Clone, PartialEq, Default, Debug)]
pub(crate) struct EdgeMetrics {
pub left_opos: i32,
pub left_pos: i32,
pub right_opos: i32,
pub right_pos: i32,
}
#[derive(Copy, Clone, PartialEq, Default, Debug)]
pub(crate) struct HintedMetrics {
pub x_scale: i32,
/// This will be `None` when we've identified fewer than two horizontal
/// edges in the outline. This will occur for empty outlines and outlines
/// that are degenerate (all x coordinates have the same value, within
/// a threshold). In these cases, horizontal metrics will not be adjusted.
pub edge_metrics: Option<EdgeMetrics>,
}
/// Applies the complete hinting process to a latin outline.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3554>
pub(crate) fn hint_outline(
outline: &mut Outline,
metrics: &UnscaledStyleMetrics,
scale: &Scale,
glyph_style: Option<GlyphStyle>,
) -> HintedMetrics {
let scaled_metrics = scale_style_metrics(metrics, *scale);
let scale = &scaled_metrics.scale;
let mut axis = Axis::default();
let hint_top_to_bottom = metrics.style_class().script.hint_top_to_bottom;
outline.scale(&scaled_metrics.scale);
let mut hinted_metrics = HintedMetrics {
x_scale: scale.x_scale,
..Default::default()
};
let group = metrics.style_class().script.group;
// For default script group, we don't proceed with hinting if we're
// missing alignment zones. FreeType swaps in a "dummy" hinter here
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L475>
if group == ScriptGroup::Default && scaled_metrics.axes[1].blues.is_empty() {
return hinted_metrics;
}
for dim in 0..2 {
if (dim == Axis::HORIZONTAL && scale.flags & Scale::NO_HORIZONTAL != 0)
|| (dim == Axis::VERTICAL && scale.flags & Scale::NO_VERTICAL != 0)
{
continue;
}
axis.reset(dim, outline.orientation);
topo::compute_segments(outline, &mut axis, group);
topo::link_segments(
outline,
&mut axis,
scaled_metrics.axes[dim].scale,
group,
metrics.axes[dim].max_width(),
);
topo::compute_edges(
&mut axis,
&scaled_metrics.axes[dim],
hint_top_to_bottom,
scaled_metrics.scale.y_scale,
group,
);
if dim == Axis::VERTICAL {
if group != ScriptGroup::Default
|| glyph_style
.map(|style| !style.is_non_base())
.unwrap_or(true)
{
topo::compute_blue_edges(
&mut axis,
scale,
&metrics.axes[dim].blues,
&scaled_metrics.axes[dim].blues,
group,
);
}
} else {
hinted_metrics.x_scale = scaled_metrics.axes[0].scale;
}
edges::hint_edges(
&mut axis,
&scaled_metrics.axes[dim],
group,
scale,
hint_top_to_bottom,
);
outline::align_edge_points(outline, &axis, group, scale);
outline::align_strong_points(outline, &mut axis);
outline::align_weak_points(outline, dim);
if dim == 0 && axis.edges.len() > 1 {
let left = axis.edges.first().unwrap();
let right = axis.edges.last().unwrap();
hinted_metrics.edge_metrics = Some(EdgeMetrics {
left_pos: left.pos,
left_opos: left.opos,
right_pos: right.pos,
right_opos: right.opos,
});
}
}
hinted_metrics
}

View File

@@ -0,0 +1,661 @@
//! Apply edge hints to an outline.
//!
//! This happens in three passes:
//! 1. Align points that are directly attached to edges. These are the points
//! which originally generated the edge and are coincident with the edge
//! coordinate (within a threshold) for a given axis. This may include
//! points that were originally classified as weak.
//! 2. Interpolate non-weak points that were not touched by the previous pass.
//! This searches for the edges that enclose the point and interpolates the
//! coordinate based on the adjustment applied to those edges.
//! 3. Interpolate remaining untouched points. These are generally the weak
//! points: those that are very near other points or lacking a dominant
//! inward or outward direction.
//!
//! The final result is a fully hinted outline.
use super::super::{
metrics::{fixed_div, fixed_mul, Scale},
outline::{Outline, Point},
style::ScriptGroup,
topo::{Axis, Dimension},
};
use core::cmp::Ordering;
use raw::tables::glyf::PointMarker;
/// Align all points of an edge to the same coordinate value.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1324>
pub(crate) fn align_edge_points(
outline: &mut Outline,
axis: &Axis,
group: ScriptGroup,
scale: &Scale,
) -> Option<()> {
let edges = axis.edges.as_slice();
let segments = axis.segments.as_slice();
let points = outline.points.as_mut_slice();
// Snapping is configurable for CJK
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L2195>
let snap = group == ScriptGroup::Default
|| ((axis.dim == Axis::HORIZONTAL && scale.flags & Scale::HORIZONTAL_SNAP != 0)
|| (axis.dim == Axis::VERTICAL && scale.flags & Scale::VERTICAL_SNAP != 0));
for segment in segments {
let Some(edge) = segment.edge(edges) else {
continue;
};
let delta = edge.pos - edge.opos;
let mut point_ix = segment.first();
let last_ix = segment.last();
loop {
let point = points.get_mut(point_ix)?;
if axis.dim == Axis::HORIZONTAL {
if snap {
point.x = edge.pos;
} else {
point.x += delta;
}
point.flags.set_marker(PointMarker::TOUCHED_X);
} else {
if snap {
point.y = edge.pos;
} else {
point.y += delta;
}
point.flags.set_marker(PointMarker::TOUCHED_Y);
}
if point_ix == last_ix {
break;
}
point_ix = point.next();
}
}
Some(())
}
/// Align the strong points; equivalent to the TrueType `IP` instruction.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1399>
pub(crate) fn align_strong_points(outline: &mut Outline, axis: &mut Axis) -> Option<()> {
if axis.edges.is_empty() {
return Some(());
}
let dim = axis.dim;
let touch_flag = if dim == Axis::HORIZONTAL {
PointMarker::TOUCHED_X
} else {
PointMarker::TOUCHED_Y
};
let points = outline.points.as_mut_slice();
'points: for point in points {
// Skip points that are already touched; do weak interpolation in the
// next pass
if point
.flags
.has_marker(touch_flag | PointMarker::WEAK_INTERPOLATION)
{
continue;
}
let (u, ou) = if dim == Axis::VERTICAL {
(point.fy, point.oy)
} else {
(point.fx, point.ox)
};
let edges = axis.edges.as_mut_slice();
// Is the point before the first edge?
let edge = edges.first()?;
let delta = edge.fpos as i32 - u;
if delta >= 0 {
store_point(point, dim, edge.pos - (edge.opos - ou));
continue;
}
// Is the point after the last edge?
let edge = edges.last()?;
let delta = u - edge.fpos as i32;
if delta >= 0 {
store_point(point, dim, edge.pos + (ou - edge.opos));
continue;
}
// Find enclosing edges; for a small number of edges, use a linear
// search.
// Note: this is actually critical for matching FreeType in cases where
// we have more than one edge with the same fpos. When this happens,
// linear and binary searches can produce different results.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1489>
let min_ix = if edges.len() <= 8 {
if let Some((min_ix, edge)) = edges
.iter()
.enumerate()
.find(|(_ix, edge)| edge.fpos as i32 >= u)
{
if edge.fpos as i32 == u {
store_point(point, dim, edge.pos);
continue 'points;
}
min_ix
} else {
0
}
} else {
let mut min_ix = 0;
let mut max_ix = edges.len();
while min_ix < max_ix {
let mid_ix = (min_ix + max_ix) >> 1;
let edge = &edges[mid_ix];
let fpos = edge.fpos as i32;
match u.cmp(&fpos) {
Ordering::Less => max_ix = mid_ix,
Ordering::Greater => min_ix = mid_ix + 1,
Ordering::Equal => {
// We are on an edge
store_point(point, dim, edge.pos);
continue 'points;
}
}
}
min_ix
};
// Point is not on an edge
if let Some(before_ix) = min_ix.checked_sub(1) {
let edge_before = edges.get(before_ix)?;
let before_pos = edge_before.pos;
let before_fpos = edge_before.fpos as i32;
let scale = if edge_before.scale == 0 {
let edge_after = edges.get(min_ix)?;
let scale = fixed_div(
edge_after.pos - edge_before.pos,
edge_after.fpos as i32 - before_fpos,
);
edges[before_ix].scale = scale;
scale
} else {
edge_before.scale
};
store_point(point, dim, before_pos + fixed_mul(u - before_fpos, scale));
}
}
Some(())
}
/// Align the weak points; equivalent to the TrueType `IUP` instruction.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1673>
pub(crate) fn align_weak_points(outline: &mut Outline, dim: Dimension) -> Option<()> {
let touch_marker = if dim == Axis::HORIZONTAL {
for point in &mut outline.points {
point.u = point.x;
point.v = point.ox;
}
PointMarker::TOUCHED_X
} else {
for point in &mut outline.points {
point.u = point.y;
point.v = point.oy;
}
PointMarker::TOUCHED_Y
};
for contour in &outline.contours {
let points = outline.points.get_mut(contour.range())?;
// Find first touched point
let Some(first_touched_ix) = points
.iter()
.position(|point| point.flags.has_marker(touch_marker))
else {
continue;
};
let last_ix = points.len() - 1;
let mut point_ix = first_touched_ix;
let mut last_touched_ix;
'outer: loop {
// Skip any touched neighbors
while point_ix < last_ix && points.get(point_ix + 1)?.flags.has_marker(touch_marker) {
point_ix += 1;
}
last_touched_ix = point_ix;
// Find the next touched point
point_ix += 1;
loop {
if point_ix > last_ix {
break 'outer;
}
if points[point_ix].flags.has_marker(touch_marker) {
break;
}
point_ix += 1;
}
iup_interpolate(
points,
last_touched_ix + 1,
point_ix - 1,
last_touched_ix,
point_ix,
);
}
if last_touched_ix == first_touched_ix {
// Special case: only one point was touched
iup_shift(points, 0, last_ix, first_touched_ix);
} else {
// Interpolate the remainder
if last_touched_ix < last_ix {
iup_interpolate(
points,
last_touched_ix + 1,
last_ix,
last_touched_ix,
first_touched_ix,
);
}
if first_touched_ix > 0 {
iup_interpolate(
points,
0,
first_touched_ix - 1,
last_touched_ix,
first_touched_ix,
);
}
}
}
// Save interpolated values
if dim == Axis::HORIZONTAL {
for point in &mut outline.points {
point.x = point.u;
}
} else {
for point in &mut outline.points {
point.y = point.u;
}
}
Some(())
}
#[inline(always)]
fn store_point(point: &mut Point, dim: Dimension, u: i32) {
if dim == Axis::HORIZONTAL {
point.x = u;
point.flags.set_marker(PointMarker::TOUCHED_X);
} else {
point.y = u;
point.flags.set_marker(PointMarker::TOUCHED_Y);
}
}
/// Shift original coordinates of all points between `p1_ix` and `p2_ix`
/// (inclusive) to get hinted coordinates using the same difference as
/// given by the point at `ref_ix`.
///
/// The `u` and `v` members are the current and original coordinate values,
/// respectively.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1578>
fn iup_shift(points: &mut [Point], p1_ix: usize, p2_ix: usize, ref_ix: usize) -> Option<()> {
let ref_point = points.get(ref_ix)?;
let delta = ref_point.u - ref_point.v;
if delta == 0 {
return Some(());
}
for point in points.get_mut(p1_ix..ref_ix)? {
point.u = point.v + delta;
}
for point in points.get_mut(ref_ix + 1..=p2_ix)? {
point.u = point.v + delta;
}
Some(())
}
/// Interpolate the original coordinates all of points between `p1_ix` and
/// `p2_ix` (inclusive) to get hinted coordinates, using the points at
/// `ref1_ix` and `ref2_ix` as the reference points.
///
/// The `u` and `v` members are the current and original coordinate values,
/// respectively.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1605>
fn iup_interpolate(
points: &mut [Point],
p1_ix: usize,
p2_ix: usize,
ref1_ix: usize,
ref2_ix: usize,
) -> Option<()> {
if p1_ix > p2_ix {
return Some(());
}
let mut ref_point1 = points.get(ref1_ix)?;
let mut ref_point2 = points.get(ref2_ix)?;
if ref_point1.v > ref_point2.v {
core::mem::swap(&mut ref_point1, &mut ref_point2);
}
let (u1, v1) = (ref_point1.u, ref_point1.v);
let (u2, v2) = (ref_point2.u, ref_point2.v);
let d1 = u1 - v1;
let d2 = u2 - v2;
if u1 == u2 || v1 == v2 {
for point in points.get_mut(p1_ix..=p2_ix)? {
point.u = if point.v <= v1 {
point.v + d1
} else if point.v >= v2 {
point.v + d2
} else {
u1
};
}
} else {
let scale = fixed_div(u2 - u1, v2 - v1);
for point in points.get_mut(p1_ix..=p2_ix)? {
point.u = if point.v <= v1 {
point.v + d1
} else if point.v >= v2 {
point.v + d2
} else {
u1 + fixed_mul(point.v - v1, scale)
};
}
}
Some(())
}
#[cfg(test)]
mod tests {
use super::{
super::super::{
metrics::{compute_unscaled_style_metrics, Scale},
shape::{Shaper, ShaperMode},
style,
},
super::{EdgeMetrics, HintedMetrics},
*,
};
use crate::{attribute::Style, MetadataProvider};
use raw::{
types::{F2Dot14, GlyphId},
FontRef, TableProvider,
};
#[test]
fn hinted_coords_and_metrics_default() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let (outline, metrics) = hint_outline(
&font,
16.0,
Default::default(),
GlyphId::new(9),
&style::STYLE_CLASSES[style::StyleClass::HEBR],
);
// Expected values were painfully extracted from FreeType with some
// printf debugging
#[rustfmt::skip]
let expected_coords = [
(133, -256),
(133, 282),
(133, 343),
(146, 431),
(158, 463),
(158, 463),
(57, 463),
(30, 463),
(0, 495),
(0, 534),
(0, 548),
(2, 570),
(11, 604),
(17, 633),
(50, 633),
(50, 629),
(50, 604),
(77, 576),
(101, 576),
(163, 576),
(180, 576),
(192, 562),
(192, 542),
(192, 475),
(190, 457),
(187, 423),
(187, 366),
(187, 315),
(187, -220),
(178, -231),
(159, -248),
(146, -256),
];
let coords = outline
.points
.iter()
.map(|point| (point.x, point.y))
.collect::<Vec<_>>();
assert_eq!(coords, expected_coords);
let expected_metrics = HintedMetrics {
x_scale: 67109,
edge_metrics: Some(EdgeMetrics {
left_opos: 15,
left_pos: 0,
right_opos: 210,
right_pos: 192,
}),
};
assert_eq!(metrics, expected_metrics);
}
#[test]
fn hinted_coords_and_metrics_cjk() {
let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
let (outline, metrics) = hint_outline(
&font,
16.0,
Default::default(),
GlyphId::new(9),
&style::STYLE_CLASSES[style::StyleClass::HANI],
);
// Expected values were painfully extracted from FreeType with some
// printf debugging
let expected_coords = [
(279, 768),
(568, 768),
(618, 829),
(618, 829),
(634, 812),
(657, 788),
(685, 758),
(695, 746),
(692, 720),
(667, 720),
(288, 720),
(704, 704),
(786, 694),
(785, 685),
(777, 672),
(767, 670),
(767, 163),
(767, 159),
(750, 148),
(728, 142),
(716, 142),
(704, 142),
(402, 767),
(473, 767),
(473, 740),
(450, 598),
(338, 357),
(236, 258),
(220, 270),
(274, 340),
(345, 499),
(390, 675),
(344, 440),
(398, 425),
(464, 384),
(496, 343),
(501, 307),
(486, 284),
(458, 281),
(441, 291),
(434, 314),
(398, 366),
(354, 416),
(334, 433),
(832, 841),
(934, 830),
(932, 819),
(914, 804),
(896, 802),
(896, 30),
(896, 5),
(885, -35),
(848, -60),
(809, -65),
(807, -51),
(794, -27),
(781, -19),
(767, -11),
(715, 0),
(673, 5),
(673, 21),
(673, 21),
(707, 18),
(756, 15),
(799, 13),
(807, 13),
(821, 13),
(832, 23),
(832, 35),
(407, 624),
(594, 624),
(594, 546),
(396, 546),
(569, 576),
(558, 576),
(599, 614),
(677, 559),
(671, 552),
(654, 547),
(636, 545),
(622, 458),
(572, 288),
(488, 130),
(357, -5),
(259, -60),
(246, -45),
(327, 9),
(440, 150),
(516, 311),
(558, 486),
(128, 542),
(158, 581),
(226, 576),
(223, 562),
(207, 543),
(193, 539),
(193, -44),
(193, -46),
(175, -56),
(152, -64),
(141, -64),
(128, -64),
(195, 850),
(300, 820),
(295, 799),
(259, 799),
(234, 712),
(163, 543),
(80, 395),
(33, 338),
(19, 347),
(54, 410),
(120, 575),
(176, 759),
];
let coords = outline
.points
.iter()
.map(|point| (point.x, point.y))
.collect::<Vec<_>>();
assert_eq!(coords, expected_coords);
let expected_metrics = HintedMetrics {
x_scale: 67109,
edge_metrics: Some(EdgeMetrics {
left_opos: 141,
left_pos: 128,
right_opos: 933,
right_pos: 896,
}),
};
assert_eq!(metrics, expected_metrics);
}
/// Empty glyphs (like spaces) have no edges and therefore no edge
/// metrics
#[test]
fn missing_edge_metrics() {
let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
let (_outline, metrics) = hint_outline(
&font,
16.0,
Default::default(),
GlyphId::new(1),
&style::STYLE_CLASSES[style::StyleClass::LATN],
);
let expected_metrics = HintedMetrics {
x_scale: 65536,
edge_metrics: None,
};
assert_eq!(metrics, expected_metrics);
}
// Specific test case for <https://issues.skia.org/issues/344529168> which
// uses the Ahem <https://web-platform-tests.org/writing-tests/ahem.html>
// font
#[test]
fn skia_ahem_test_case() {
let font = FontRef::new(font_test_data::AHEM).unwrap();
let outline = hint_outline(
&font,
24.0,
Default::default(),
// This glyph is the typical Ahem block square; the link to the
// font description above more detail.
GlyphId::new(5),
&style::STYLE_CLASSES[style::StyleClass::LATN],
)
.0;
let expected_coords = [(0, 1216), (1536, 1216), (1536, -320), (0, -320)];
// See <https://issues.skia.org/issues/344529168#comment3>
// Note that Skia inverts y coords
let expected_float_coords = [(0.0, 19.0), (24.0, 19.0), (24.0, -5.0), (0.0, -5.0)];
let coords = outline
.points
.iter()
.map(|point| (point.x, point.y))
.collect::<Vec<_>>();
let float_coords = coords
.iter()
.map(|(x, y)| (*x as f32 / 64.0, *y as f32 / 64.0))
.collect::<Vec<_>>();
assert_eq!(coords, expected_coords);
assert_eq!(float_coords, expected_float_coords);
}
fn hint_outline(
font: &FontRef,
size: f32,
coords: &[F2Dot14],
gid: GlyphId,
style: &style::StyleClass,
) -> (Outline, HintedMetrics) {
let shaper = Shaper::new(font, ShaperMode::Nominal);
let glyphs = font.outline_glyphs();
let glyph = glyphs.get(gid).unwrap();
let mut outline = Outline::default();
outline.fill(&glyph, coords).unwrap();
let metrics = compute_unscaled_style_metrics(&shaper, coords, style);
let scale = Scale::new(
size,
font.head().unwrap().units_per_em() as i32,
Style::Normal,
Default::default(),
metrics.style_class().script.group,
);
let hinted_metrics = super::super::hint_outline(&mut outline, &metrics, &scale, None);
(outline, hinted_metrics)
}
}

View File

@@ -0,0 +1,181 @@
//! Autohinting state for a font instance.
use crate::{attribute::Style, prelude::Size, MetadataProvider};
use super::{
super::{
pen::PathStyle, AdjustedMetrics, DrawError, OutlineGlyph, OutlineGlyphCollection,
OutlinePen, Target,
},
metrics::{fixed_mul, pix_round, Scale, UnscaledStyleMetricsSet},
outline::Outline,
shape::{Shaper, ShaperMode},
style::GlyphStyleMap,
};
use alloc::sync::Arc;
use raw::{
types::{F26Dot6, F2Dot14},
FontRef, TableProvider,
};
/// We enable "best effort" mode by default but allow it to be disabled with
/// a feature for testing.
const SHAPER_MODE: ShaperMode = if cfg!(feature = "autohint_shaping") {
ShaperMode::BestEffort
} else {
ShaperMode::Nominal
};
/// Set of derived glyph styles that are used for automatic hinting.
///
/// These are invariant per font so can be precomputed and reused for multiple
/// instances when requesting automatic hinting with [`Engine::Auto`](super::super::hint::Engine::Auto).
#[derive(Clone, Debug)]
pub struct GlyphStyles(Arc<GlyphStyleMap>);
impl GlyphStyles {
/// Precomputes the full set of glyph styles for the given outlines.
pub fn new(outlines: &OutlineGlyphCollection) -> Self {
if let Some(font) = outlines.font() {
let glyph_count = font
.maxp()
.map(|maxp| maxp.num_glyphs() as u32)
.unwrap_or_default();
let shaper = Shaper::new(font, SHAPER_MODE);
Self(Arc::new(GlyphStyleMap::new(glyph_count, &shaper)))
} else {
Self(Default::default())
}
}
}
#[derive(Clone)]
pub(crate) struct Instance {
styles: GlyphStyles,
metrics: UnscaledStyleMetricsSet,
target: Target,
is_fixed_width: bool,
style: Style,
}
impl Instance {
pub fn new(
font: &FontRef,
outlines: &OutlineGlyphCollection,
coords: &[F2Dot14],
target: Target,
styles: Option<GlyphStyles>,
lazy_metrics: bool,
) -> Self {
let styles = styles.unwrap_or_else(|| GlyphStyles::new(outlines));
#[cfg(feature = "std")]
let metrics = if lazy_metrics {
UnscaledStyleMetricsSet::lazy(&styles.0)
} else {
UnscaledStyleMetricsSet::precomputed(font, coords, SHAPER_MODE, &styles.0)
};
#[cfg(not(feature = "std"))]
let metrics = UnscaledStyleMetricsSet::precomputed(font, coords, SHAPER_MODE, &styles.0);
let is_fixed_width = font
.post()
.map(|post| post.is_fixed_pitch() != 0)
.unwrap_or_default();
let style = font.attributes().style;
Self {
styles,
metrics,
target,
is_fixed_width,
style,
}
}
pub fn draw(
&self,
size: Size,
coords: &[F2Dot14],
glyph: &OutlineGlyph,
path_style: PathStyle,
pen: &mut impl OutlinePen,
) -> Result<AdjustedMetrics, DrawError> {
let font = glyph.font();
let glyph_id = glyph.glyph_id();
let style = self
.styles
.0
.style(glyph_id)
.ok_or(DrawError::GlyphNotFound(glyph_id))?;
let metrics = self
.metrics
.get(font, coords, SHAPER_MODE, &self.styles.0, glyph_id)
.ok_or(DrawError::GlyphNotFound(glyph_id))?;
let units_per_em = glyph.units_per_em() as i32;
let scale = Scale::new(
size.ppem().unwrap_or(units_per_em as f32),
units_per_em,
self.style,
self.target,
metrics.style_class().script.group,
);
let mut outline = Outline::default();
outline.fill(glyph, coords)?;
let hinted_metrics = super::hint::hint_outline(&mut outline, &metrics, &scale, Some(style));
let h_advance = outline.advance;
let mut pp1x = 0;
let mut pp2x = fixed_mul(h_advance, hinted_metrics.x_scale);
let is_light = self.target.is_light() || self.target.preserve_linear_metrics();
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afloader.c#L422>
if !is_light {
if let (true, Some(edge_metrics)) = (
scale.flags & Scale::NO_ADVANCE == 0,
hinted_metrics.edge_metrics,
) {
let old_rsb = pp2x - edge_metrics.right_opos;
let old_lsb = edge_metrics.left_opos;
let new_lsb = edge_metrics.left_pos;
let mut pp1x_uh = new_lsb - old_lsb;
let mut pp2x_uh = edge_metrics.right_pos + old_rsb;
if old_lsb < 24 {
pp1x_uh -= 8;
}
if old_rsb < 24 {
pp2x_uh += 8;
}
pp1x = pix_round(pp1x_uh);
pp2x = pix_round(pp2x_uh);
if pp1x >= new_lsb && old_lsb > 0 {
pp1x -= 64;
}
if pp2x <= edge_metrics.right_pos && old_rsb > 0 {
pp2x += 64;
}
} else {
pp1x = pix_round(pp1x);
pp2x = pix_round(pp2x);
}
} else {
pp1x = pix_round(pp1x);
pp2x = pix_round(pp2x);
}
if pp1x != 0 {
for point in &mut outline.points {
point.x -= pp1x;
}
}
let advance = if !is_light
&& (self.is_fixed_width || (metrics.digits_have_same_width && style.is_digit()))
{
fixed_mul(h_advance, scale.x_scale)
} else if h_advance != 0 {
pp2x - pp1x
} else {
0
};
outline.to_path(path_style, pen)?;
Ok(AdjustedMetrics {
has_overlaps: glyph.has_overlaps().unwrap_or_default(),
lsb: None,
advance_width: Some(F26Dot6::from_bits(pix_round(advance)).to_f32()),
})
}
}

View File

@@ -0,0 +1,952 @@
//! Latin blue values.
use super::{
super::{
super::{unscaled::UnscaledOutlineBuf, OutlineGlyphCollection},
shape::{ShapedCluster, Shaper},
style::{ScriptGroup, StyleClass},
},
ScaledWidth,
};
use crate::{collections::SmallVec, FontRef, MetadataProvider};
use raw::types::F2Dot14;
use raw::TableProvider;
/// Maximum number of blue values.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afblue.h#L328>
const MAX_BLUES: usize = 8;
// Chosen to maximize opportunity to avoid heap allocation while keeping stack
// size < 2k.
const MAX_INLINE_POINTS: usize = 256;
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afblue.h#L73>
const BLUE_STRING_MAX_LEN: usize = 51;
/// Defines the zone(s) that are associated with a blue value.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
#[repr(transparent)]
pub(crate) struct BlueZones(u16);
impl BlueZones {
// These properties ostensibly come from
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afblue.h#L317>
// but are modified to match those at
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L68>
// so that when don't need to keep two sets and adjust during blue
// computation.
pub const NONE: Self = Self(0);
pub const TOP: Self = Self(1 << 1);
pub const SUB_TOP: Self = Self(1 << 2);
pub const NEUTRAL: Self = Self(1 << 3);
pub const ADJUSTMENT: Self = Self(1 << 4);
pub const X_HEIGHT: Self = Self(1 << 5);
pub const LONG: Self = Self(1 << 6);
pub const HORIZONTAL: Self = Self(1 << 2);
pub const RIGHT: Self = Self::TOP;
pub const fn contains(self, other: Self) -> bool {
self.0 & other.0 == other.0
}
// Used for generated data structures because the bit-or operator
// cannot be const.
#[must_use]
pub const fn union(self, other: Self) -> Self {
Self(self.0 | other.0)
}
pub fn is_top_like(self) -> bool {
self & (Self::TOP | Self::SUB_TOP) != Self::NONE
}
pub fn is_top(self) -> bool {
self.contains(Self::TOP)
}
pub fn is_sub_top(self) -> bool {
self.contains(Self::SUB_TOP)
}
pub fn is_neutral(self) -> bool {
self.contains(Self::NEUTRAL)
}
pub fn is_x_height(self) -> bool {
self.contains(Self::X_HEIGHT)
}
pub fn is_long(self) -> bool {
self.contains(Self::LONG)
}
pub fn is_horizontal(self) -> bool {
self.contains(Self::HORIZONTAL)
}
pub fn is_right(self) -> bool {
self.contains(Self::RIGHT)
}
#[must_use]
pub fn retain_top_like_or_neutral(self) -> Self {
self & (Self::TOP | Self::SUB_TOP | Self::NEUTRAL)
}
}
impl core::ops::Not for BlueZones {
type Output = Self;
fn not(self) -> Self::Output {
Self(!self.0)
}
}
impl core::ops::BitOr for BlueZones {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl core::ops::BitOrAssign for BlueZones {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl core::ops::BitAnd for BlueZones {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
}
impl core::ops::BitAndAssign for BlueZones {
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0;
}
}
// FreeType keeps a single array of blue values per metrics set
// and mutates when the scale factor changes. We'll separate them so
// that we can reuse unscaled metrics as immutable state without
// recomputing them (which is the expensive part).
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L77>
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) struct UnscaledBlue {
pub position: i32,
pub overshoot: i32,
pub ascender: i32,
pub descender: i32,
pub zones: BlueZones,
}
pub(crate) type UnscaledBlues = SmallVec<UnscaledBlue, MAX_BLUES>;
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) struct ScaledBlue {
pub position: ScaledWidth,
pub overshoot: ScaledWidth,
pub zones: BlueZones,
pub is_active: bool,
}
pub(crate) type ScaledBlues = SmallVec<ScaledBlue, MAX_BLUES>;
/// Compute unscaled blues values for each axis.
pub(crate) fn compute_unscaled_blues(
shaper: &Shaper,
coords: &[F2Dot14],
style: &StyleClass,
) -> [UnscaledBlues; 2] {
match style.script.group {
ScriptGroup::Default => [
// Default group doesn't have horizontal blues
Default::default(),
compute_default_blues(shaper, coords, style),
],
ScriptGroup::Cjk => compute_cjk_blues(shaper, coords, style),
// Indic group doesn't use blue values (yet?)
ScriptGroup::Indic => Default::default(),
}
}
/// Compute unscaled blue values for the default script set.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L314>
fn compute_default_blues(shaper: &Shaper, coords: &[F2Dot14], style: &StyleClass) -> UnscaledBlues {
let mut blues = UnscaledBlues::new();
let (mut outline_buf, mut flats, mut rounds) = buffers();
let (glyphs, units_per_em) = things_all_blues_need(shaper.font());
let flat_threshold = units_per_em / 14;
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut shaped_cluster = ShapedCluster::default();
// Walk over each of the blue character sets for our script.
for (blue_str, blue_zones) in style.script.blues {
let mut ascender = i32::MIN;
let mut descender = i32::MAX;
let mut n_flats = 0;
let mut n_rounds = 0;
for cluster in blue_str.split(' ') {
let mut best_y_extremum = if blue_zones.is_top() {
i32::MIN
} else {
i32::MAX
};
let mut best_is_round = false;
cluster_shaper.shape(cluster, &mut shaped_cluster);
for (glyph, y_offset) in shaped_cluster
.iter()
.filter(|g| g.id.to_u32() != 0)
.filter_map(|g| Some((glyphs.get(g.id)?, g.y_offset)))
{
outline_buf.clear();
if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
continue;
}
let outline = outline_buf.as_ref();
// Reject glyphs that can't produce any rendering
if outline.points.len() <= 2 {
continue;
}
let mut best_y: Option<i16> = None;
// Find the extreme point depending on whether this is a top or
// bottom blue
let best_contour_and_point = if blue_zones.is_top_like() {
outline.find_last_contour(|point| {
if best_y.is_none() || Some(point.y) > best_y {
best_y = Some(point.y);
ascender = ascender.max(point.y as i32 + y_offset);
true
} else {
descender = descender.min(point.y as i32 + y_offset);
false
}
})
} else {
outline.find_last_contour(|point| {
if best_y.is_none() || Some(point.y) < best_y {
best_y = Some(point.y);
descender = descender.min(point.y as i32 + y_offset);
true
} else {
ascender = ascender.max(point.y as i32 + y_offset);
false
}
})
};
let Some((best_contour_range, best_point_ix)) = best_contour_and_point else {
continue;
};
let best_contour = &outline.points[best_contour_range];
// If we have a contour and point then best_y is guaranteed to
// be Some
let mut best_y = best_y.unwrap() as i32;
let best_x = best_contour[best_point_ix].x as i32;
// Now determine whether the point belongs to a straight or
// round segment by examining the previous and next points.
let [mut on_point_first, mut on_point_last] =
if best_contour[best_point_ix].is_on_curve() {
[Some(best_point_ix); 2]
} else {
[None; 2]
};
let mut segment_first = best_point_ix;
let mut segment_last = best_point_ix;
// Look for the previous and next points on the contour that
// are not on the same Y coordinate, then threshold the
// "closeness"
for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
let dist = (prev.y as i32 - best_y).abs();
// Allow a small distance or angle (20 == roughly 2.9 degrees)
if dist > 5 && ((prev.x as i32 - best_x).abs() <= (20 * dist)) {
break;
}
segment_first = ix;
if prev.is_on_curve() {
on_point_first = Some(ix);
if on_point_last.is_none() {
on_point_last = Some(ix);
}
}
}
let mut next_ix = 0;
for (ix, next) in cycle_forward(best_contour, best_point_ix) {
// Save next_ix which is used in "long" blue computation
// later
next_ix = ix;
let dist = (next.y as i32 - best_y).abs();
// Allow a small distance or angle (20 == roughly 2.9 degrees)
if dist > 5 && ((next.x as i32 - best_x).abs() <= (20 * dist)) {
break;
}
segment_last = ix;
if next.is_on_curve() {
on_point_last = Some(ix);
if on_point_first.is_none() {
on_point_first = Some(ix);
}
}
}
if blue_zones.is_long() {
// Taken verbatim from FreeType:
//
// "If this flag is set, we have an additional constraint to
// get the blue zone distance: Find a segment of the topmost
// (or bottommost) contour that is longer than a heuristic
// threshold. This ensures that small bumps in the outline
// are ignored (for example, the `vertical serifs' found in
// many Hebrew glyph designs).
//
// If this segment is long enough, we are done. Otherwise,
// search the segment next to the extremum that is long
// enough, has the same direction, and a not too large
// vertical distance from the extremum. Note that the
// algorithm doesn't check whether the found segment is
// actually the one (vertically) nearest to the extremum.""
//
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L641>
// heuristic threshold value
let length_threshold = units_per_em / 25;
let dist = (best_contour[segment_last].x as i32
- best_contour[segment_first].x as i32)
.abs();
if dist < length_threshold
&& satisfies_min_long_segment_len(
segment_first,
segment_last,
best_contour.len() - 1,
)
{
// heuristic threshold value
let height_threshold = units_per_em / 4;
// find previous point with different x value
let mut prev_ix = best_point_ix;
for (ix, prev) in cycle_backward(best_contour, best_point_ix) {
if prev.x as i32 != best_x {
prev_ix = ix;
break;
}
}
// skip for degenerate case
if prev_ix == best_point_ix {
continue;
}
let is_ltr = (best_contour[prev_ix].x as i32) < best_x;
let mut first = segment_last;
let mut last = first;
let mut p_first = None;
let mut p_last = None;
let mut hit = false;
loop {
if !hit {
// no hit, adjust first point
first = last;
// also adjust first and last on curve point
if best_contour[first].is_on_curve() {
p_first = Some(first);
p_last = Some(first);
} else {
p_first = None;
p_last = None;
}
hit = true;
}
if last < best_contour.len() - 1 {
last += 1;
} else {
last = 0;
}
if (best_y - best_contour[first].y as i32).abs() > height_threshold {
// vertical distance too large
hit = false;
continue;
}
let dist =
(best_contour[last].y as i32 - best_contour[first].y as i32).abs();
if dist > 5
&& (best_contour[last].x as i32 - best_contour[first].x as i32)
.abs()
<= 20 * dist
{
hit = false;
if last == segment_first {
break;
}
continue;
}
if best_contour[last].is_on_curve() {
p_last = Some(last);
if p_first.is_none() {
p_first = Some(last);
}
}
let first_x = best_contour[first].x as i32;
let last_x = best_contour[last].x as i32;
let is_cur_ltr = first_x < last_x;
let dx = (last_x - first_x).abs();
if is_cur_ltr == is_ltr && dx >= length_threshold {
loop {
if last < best_contour.len() - 1 {
last += 1;
} else {
last = 0;
}
let dy = (best_contour[last].y as i32
- best_contour[first].y as i32)
.abs();
if dy > 5
&& (best_contour[next_ix].x as i32
- best_contour[first].x as i32)
.abs()
<= 20 * dist
{
if last > 0 {
last -= 1;
} else {
last = best_contour.len() - 1;
}
break;
}
p_last = Some(last);
if best_contour[last].is_on_curve() {
p_last = Some(last);
if p_first.is_none() {
p_first = Some(last);
}
}
if last == segment_first {
break;
}
}
best_y = best_contour[first].y as i32;
segment_first = first;
segment_last = last;
on_point_first = p_first;
on_point_last = p_last;
break;
}
if last == segment_first {
break;
}
}
}
}
best_y += y_offset;
// Is the segment round?
// 1. horizontal distance between first and last oncurve point
// is larger than a heuristic flat threshold, then it's flat
// 2. either first or last point of segment is offcurve then
// it's round
let is_round = match (on_point_first, on_point_last) {
(Some(first), Some(last))
if (best_contour[last].x as i32 - best_contour[first].x as i32).abs()
> flat_threshold =>
{
false
}
_ => {
!best_contour[segment_first].is_on_curve()
|| !best_contour[segment_last].is_on_curve()
}
};
if is_round && blue_zones.is_neutral() {
// Ignore round segments for neutral zone
continue;
}
// This seems to ignore LATIN_SUB_TOP?
if blue_zones.is_top() {
if best_y > best_y_extremum {
best_y_extremum = best_y;
best_is_round = is_round;
}
} else if best_y < best_y_extremum {
best_y_extremum = best_y;
best_is_round = is_round;
}
}
if best_y_extremum != i32::MIN && best_y_extremum != i32::MAX {
if best_is_round {
rounds[n_rounds] = best_y_extremum;
n_rounds += 1;
} else {
flats[n_flats] = best_y_extremum;
n_flats += 1;
}
}
}
if n_flats == 0 && n_rounds == 0 {
continue;
}
rounds[..n_rounds].sort_unstable();
flats[..n_flats].sort_unstable();
let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
let val = rounds[n_rounds / 2];
(val, val)
} else if n_rounds == 0 {
let val = flats[n_flats / 2];
(val, val)
} else {
(flats[n_flats / 2], rounds[n_rounds / 2])
};
if blue_shoot != blue_ref {
let over_ref = blue_shoot > blue_ref;
if blue_zones.is_top_like() ^ over_ref {
let val = (blue_shoot + blue_ref) / 2;
blue_ref = val;
blue_shoot = val;
}
}
let mut blue = UnscaledBlue {
position: blue_ref,
overshoot: blue_shoot,
ascender,
descender,
zones: blue_zones.retain_top_like_or_neutral(),
};
if blue_zones.is_x_height() {
blue.zones |= BlueZones::ADJUSTMENT;
}
blues.push(blue);
}
// sort bottoms
let mut sorted_indices: [usize; MAX_BLUES] = core::array::from_fn(|ix| ix);
let blue_values = blues.as_mut_slice();
let len = blue_values.len();
if len == 0 {
return blues;
}
// sort from bottom to top
for i in 1..len {
for j in (1..=i).rev() {
let first = &blue_values[sorted_indices[j - 1]];
let second = &blue_values[sorted_indices[j]];
let a = if first.zones.is_top_like() {
first.position
} else {
first.overshoot
};
let b = if second.zones.is_top_like() {
second.position
} else {
second.overshoot
};
if b >= a {
break;
}
sorted_indices.swap(j, j - 1);
}
}
// and adjust tops
for i in 0..len - 1 {
let index1 = sorted_indices[i];
let index2 = sorted_indices[i + 1];
let first = &blue_values[index1];
let second = &blue_values[index2];
let a = if first.zones.is_top_like() {
first.overshoot
} else {
first.position
};
let b = if second.zones.is_top_like() {
second.overshoot
} else {
second.position
};
if a > b {
if first.zones.is_top_like() {
blue_values[index1].overshoot = b;
} else {
blue_values[index1].position = b;
}
}
}
blues
}
/// Given inclusive indices and a contour length, returns true if the segment
/// is of sufficient size to test for bumps when detecting "long" Hebrew
/// alignment zones.
fn satisfies_min_long_segment_len(first_ix: usize, last_ix: usize, contour_last: usize) -> bool {
let inclusive_diff = if first_ix <= last_ix {
last_ix - first_ix
} else {
// If first_ix > last_ix, then we want to capture the sum of the ranges
// [first_ix, contour_last] and [0, last_ix]
// We add 1 here to ensure the element that crosses the boundary is
// included. For example, if first_ix == contour_last and
// last_ix == 0, then we want the result to be 1
contour_last - first_ix + 1 + last_ix
};
// The +2 matches FreeType. The assumption is that this includes sufficient
// points to detect a bump and extend the segment?
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L663>
inclusive_diff + 2 <= contour_last
}
/// Compute unscaled blue values for the CJK script set.
///
/// Note: unlike the default code above, this produces two sets of blues,
/// one for horizontal zones and one for vertical zones, respectively. The
/// horizontal set is currently not generated because this has been
/// disabled in FreeType but the code remains because we may want to revisit
/// in the future.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L277>
fn compute_cjk_blues(
shaper: &Shaper,
coords: &[F2Dot14],
style: &StyleClass,
) -> [UnscaledBlues; 2] {
let mut blues = [UnscaledBlues::new(), UnscaledBlues::new()];
let (mut outline_buf, mut flats, mut fills) = buffers();
let (glyphs, _) = things_all_blues_need(shaper.font());
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut shaped_cluster = ShapedCluster::default();
// Walk over each of the blue character sets for our script.
for (blue_str, blue_zones) in style.script.blues {
let is_horizontal = blue_zones.is_horizontal();
// Note: horizontal blue zones are disabled by default and have been
// for many years in FreeType:
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L35>
// and <https://gitlab.freedesktop.org/freetype/freetype/-/commit/084abf0469d32a94b1c315bee10f621284694328>
if is_horizontal {
continue;
}
let is_right = blue_zones.is_right();
let is_top = blue_zones.is_top();
let blues = &mut blues[!is_horizontal as usize];
if blues.len() >= MAX_BLUES {
continue;
}
let mut n_flats = 0;
let mut n_fills = 0;
let mut is_fill = true;
for cluster in blue_str.split(' ') {
// The '|' character is used as a sentinel in the blue string that
// signifies a switch to characters that define "flat" values
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L380>
if cluster == "|" {
is_fill = false;
continue;
}
cluster_shaper.shape(cluster, &mut shaped_cluster);
for glyph in shaped_cluster
.iter()
.filter(|g| g.id.to_u32() != 0)
.filter_map(|g| glyphs.get(g.id))
{
outline_buf.clear();
if glyph.draw_unscaled(coords, None, &mut outline_buf).is_err() {
continue;
}
let outline = outline_buf.as_ref();
// Reject glyphs that can't produce any rendering
if outline.points.len() <= 2 {
continue;
}
// Step right up and find an extrema!
// Unwrap is safe because we know per ^ that we have at least 3 points
let best_pos = outline
.points
.iter()
.map(|p| if is_horizontal { p.x } else { p.y })
.reduce(
if (is_horizontal && is_right) || (!is_horizontal && is_top) {
|a: i16, c: i16| a.max(c)
} else {
|a: i16, c: i16| a.min(c)
},
)
.unwrap();
if is_fill {
fills[n_fills] = best_pos;
n_fills += 1;
} else {
flats[n_flats] = best_pos;
n_flats += 1;
}
}
}
if n_flats == 0 && n_fills == 0 {
continue;
}
// Now determine the reference and overshoot of the blue; simply
// take the median after a sort
fills[..n_fills].sort_unstable();
flats[..n_flats].sort_unstable();
let (mut blue_ref, mut blue_shoot) = if n_flats == 0 {
let value = fills[n_fills / 2] as i32;
(value, value)
} else if n_fills == 0 {
let value = flats[n_flats / 2] as i32;
(value, value)
} else {
(fills[n_fills / 2] as i32, flats[n_flats / 2] as i32)
};
// Make sure blue_ref >= blue_shoot for top/right or vice versa for
// bottom left
if blue_shoot != blue_ref {
let under_ref = blue_shoot < blue_ref;
if blue_zones.is_top() ^ under_ref {
blue_ref = (blue_shoot + blue_ref) / 2;
blue_shoot = blue_ref;
}
}
blues.push(UnscaledBlue {
position: blue_ref,
overshoot: blue_shoot,
ascender: 0,
descender: 0,
zones: *blue_zones & BlueZones::TOP,
});
}
blues
}
#[inline(always)]
fn buffers<T: Copy + Default>() -> (
UnscaledOutlineBuf<MAX_INLINE_POINTS>,
[T; BLUE_STRING_MAX_LEN],
[T; BLUE_STRING_MAX_LEN],
) {
(
UnscaledOutlineBuf::<MAX_INLINE_POINTS>::new(),
[T::default(); BLUE_STRING_MAX_LEN],
[T::default(); BLUE_STRING_MAX_LEN],
)
}
/// A thneed is something everyone needs
#[inline(always)]
fn things_all_blues_need<'a>(font: &FontRef<'a>) -> (OutlineGlyphCollection<'a>, i32) {
(
font.outline_glyphs(),
font.head()
.map(|head| head.units_per_em())
.unwrap_or_default() as i32,
)
}
/// Iterator that begins at `start + 1` and cycles through all items
/// of the slice in forward order, ending with `start`.
pub(super) fn cycle_forward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
let len = items.len();
let start = start + 1;
(0..len).map(move |ix| {
let real_ix = (ix + start) % len;
(real_ix, &items[real_ix])
})
}
/// Iterator that begins at `start - 1` and cycles through all items
/// of the slice in reverse order, ending with `start`.
pub(super) fn cycle_backward<T>(items: &[T], start: usize) -> impl Iterator<Item = (usize, &T)> {
let len = items.len();
(0..len).rev().map(move |ix| {
let real_ix = (ix + start) % len;
(real_ix, &items[real_ix])
})
}
#[cfg(test)]
mod tests {
use crate::outline::autohint::metrics::BlueZones;
use super::{
super::super::{
shape::{Shaper, ShaperMode},
style,
},
satisfies_min_long_segment_len, UnscaledBlue,
};
use raw::FontRef;
#[test]
fn latin_blues() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let style = &style::STYLE_CLASSES[super::StyleClass::LATN];
let blues = super::compute_default_blues(&shaper, &[], style);
let values = blues.as_slice();
let expected = [
UnscaledBlue {
position: 714,
overshoot: 725,
ascender: 725,
descender: -230,
zones: BlueZones::TOP,
},
UnscaledBlue {
position: 0,
overshoot: -10,
ascender: 725,
descender: -10,
zones: BlueZones::default(),
},
UnscaledBlue {
position: 760,
overshoot: 760,
ascender: 770,
descender: -240,
zones: BlueZones::TOP,
},
UnscaledBlue {
position: 536,
overshoot: 546,
ascender: 546,
descender: -10,
zones: BlueZones::TOP | BlueZones::ADJUSTMENT,
},
UnscaledBlue {
position: 0,
overshoot: -10,
ascender: 546,
descender: -10,
zones: BlueZones::default(),
},
UnscaledBlue {
position: -240,
overshoot: -240,
ascender: 760,
descender: -240,
zones: BlueZones::default(),
},
];
assert_eq!(values, &expected);
}
#[test]
fn hebrew_long_blues() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
// Hebrew triggers "long" blue code path
let style = &style::STYLE_CLASSES[super::StyleClass::HEBR];
let blues = super::compute_default_blues(&shaper, &[], style);
let values = blues.as_slice();
assert_eq!(values.len(), 3);
let expected = [
UnscaledBlue {
position: 592,
overshoot: 592,
ascender: 647,
descender: -240,
zones: BlueZones::TOP,
},
UnscaledBlue {
position: 0,
overshoot: -9,
ascender: 647,
descender: -9,
zones: BlueZones::default(),
},
UnscaledBlue {
position: -240,
overshoot: -240,
ascender: 647,
descender: -240,
zones: BlueZones::default(),
},
];
assert_eq!(values, &expected);
}
#[test]
fn cjk_blues() {
let font = FontRef::new(font_test_data::NOTOSERIFTC_AUTOHINT_METRICS).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let style = &style::STYLE_CLASSES[super::StyleClass::HANI];
let blues = super::compute_cjk_blues(&shaper, &[], style);
let values = blues[1].as_slice();
let expected = [
UnscaledBlue {
position: 837,
overshoot: 824,
ascender: 0,
descender: 0,
zones: BlueZones::TOP,
},
UnscaledBlue {
position: -78,
overshoot: -66,
ascender: 0,
descender: 0,
zones: BlueZones::default(),
},
];
assert_eq!(values, &expected);
}
#[test]
fn c2sc_shaped_blues() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[super::StyleClass::LATN_C2SC];
let blues = super::compute_default_blues(&shaper, &[], style);
let values = blues.as_slice();
// Captured from FreeType with HarfBuzz enabled
let expected = [
UnscaledBlue {
position: 571,
overshoot: 571,
ascender: 571,
descender: 0,
zones: BlueZones::TOP,
},
UnscaledBlue {
position: 0,
overshoot: 0,
ascender: 571,
descender: 0,
zones: BlueZones::default(),
},
];
assert_eq!(values, &expected);
}
/// Avoid subtraction overflow raised in
/// <https://github.com/googlefonts/fontations/issues/1218>
#[test]
fn long_segment_len_avoid_overflow() {
// Test font in issue above triggers overflow with
// first = 22, last = 0, contour_last = 22 (all inclusive).
// FreeType succeeds on this with suspicious signed
// arithmetic and we should too with our code that
// takes the boundary into account
assert!(satisfies_min_long_segment_len(22, 0, 22));
}
#[test]
fn cycle_iter_forward() {
let items = [0, 1, 2, 3, 4, 5, 6, 7];
let from_5 = super::cycle_forward(&items, 5)
.map(|(_, val)| *val)
.collect::<Vec<_>>();
assert_eq!(from_5, &[6, 7, 0, 1, 2, 3, 4, 5]);
let from_last = super::cycle_forward(&items, 7)
.map(|(_, val)| *val)
.collect::<Vec<_>>();
assert_eq!(from_last, &items);
// Don't panic on empty slice
let _ = super::cycle_forward::<i32>(&[], 5).count();
}
#[test]
fn cycle_iter_backward() {
let items = [0, 1, 2, 3, 4, 5, 6, 7];
let from_5 = super::cycle_backward(&items, 5)
.map(|(_, val)| *val)
.collect::<Vec<_>>();
assert_eq!(from_5, &[4, 3, 2, 1, 0, 7, 6, 5]);
let from_0 = super::cycle_backward(&items, 0)
.map(|(_, val)| *val)
.collect::<Vec<_>>();
assert_eq!(from_0, &[7, 6, 5, 4, 3, 2, 1, 0]);
// Don't panic on empty slice
let _ = super::cycle_backward::<i32>(&[], 5).count();
}
}

View File

@@ -0,0 +1,480 @@
//! Autohinting specific metrics.
mod blues;
mod scale;
mod widths;
use super::{
super::Target,
shape::{Shaper, ShaperMode},
style::{GlyphStyleMap, ScriptGroup, StyleClass},
topo::Dimension,
};
use crate::{attribute::Style, collections::SmallVec, FontRef};
use alloc::vec::Vec;
use raw::types::{F2Dot14, Fixed, GlyphId};
#[cfg(feature = "std")]
use std::sync::{Arc, RwLock};
pub(crate) use blues::{BlueZones, ScaledBlue, ScaledBlues, UnscaledBlue, UnscaledBlues};
pub(crate) use scale::{compute_unscaled_style_metrics, scale_style_metrics};
/// Maximum number of widths, same for Latin and CJK.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L65>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.h#L55>
pub(crate) const MAX_WIDTHS: usize = 16;
/// Unscaled metrics for a single axis.
///
/// This is the union of the Latin and CJK axis records.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L88>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.h#L73>
#[derive(Clone, Default, Debug)]
pub(crate) struct UnscaledAxisMetrics {
pub dim: Dimension,
pub widths: UnscaledWidths,
pub width_metrics: WidthMetrics,
pub blues: UnscaledBlues,
}
impl UnscaledAxisMetrics {
pub fn max_width(&self) -> Option<i32> {
self.widths.last().copied()
}
}
/// Scaled metrics for a single axis.
#[derive(Clone, Default, Debug)]
pub(crate) struct ScaledAxisMetrics {
pub dim: Dimension,
/// Font unit to 26.6 scale in the axis direction.
pub scale: i32,
/// 1/64 pixel delta in the axis direction.
pub delta: i32,
pub widths: ScaledWidths,
pub width_metrics: WidthMetrics,
pub blues: ScaledBlues,
}
/// Unscaled metrics for a single style and script.
///
/// This is the union of the root, Latin and CJK style metrics but
/// the latter two are actually identical.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aftypes.h#L413>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L109>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.h#L95>
#[derive(Clone, Default, Debug)]
pub(crate) struct UnscaledStyleMetrics {
/// Index of style class.
pub class_ix: u16,
/// Monospaced digits?
pub digits_have_same_width: bool,
/// Per-dimension unscaled metrics.
pub axes: [UnscaledAxisMetrics; 2],
}
impl UnscaledStyleMetrics {
pub fn style_class(&self) -> &'static StyleClass {
&super::style::STYLE_CLASSES[self.class_ix as usize]
}
}
/// The set of unscaled style metrics for a single font.
///
/// For a variable font, this is dependent on the location in variation space.
#[derive(Clone, Debug)]
pub(crate) enum UnscaledStyleMetricsSet {
Precomputed(Vec<UnscaledStyleMetrics>),
#[cfg(feature = "std")]
Lazy(Arc<RwLock<Vec<Option<UnscaledStyleMetrics>>>>),
}
impl UnscaledStyleMetricsSet {
/// Creates a precomputed style metrics set containing all metrics
/// required by the glyph map.
pub fn precomputed(
font: &FontRef,
coords: &[F2Dot14],
shaper_mode: ShaperMode,
style_map: &GlyphStyleMap,
) -> Self {
// The metrics_styles() iterator does not report exact size so we
// preallocate and extend here rather than collect to avoid
// over allocating memory.
let shaper = Shaper::new(font, shaper_mode);
let mut vec = Vec::with_capacity(style_map.metrics_count());
vec.extend(
style_map
.metrics_styles()
.map(|style| compute_unscaled_style_metrics(&shaper, coords, style)),
);
Self::Precomputed(vec)
}
/// Creates an unscaled style metrics set where each entry will be
/// initialized as needed.
#[cfg(feature = "std")]
pub fn lazy(style_map: &GlyphStyleMap) -> Self {
let vec = vec![None; style_map.metrics_count()];
Self::Lazy(Arc::new(RwLock::new(vec)))
}
/// Returns the unscaled style metrics for the given style map and glyph
/// identifier.
pub fn get(
&self,
font: &FontRef,
coords: &[F2Dot14],
shaper_mode: ShaperMode,
style_map: &GlyphStyleMap,
glyph_id: GlyphId,
) -> Option<UnscaledStyleMetrics> {
let style = style_map.style(glyph_id)?;
let index = style_map.metrics_index(style)?;
match self {
Self::Precomputed(metrics) => metrics.get(index).cloned(),
#[cfg(feature = "std")]
Self::Lazy(lazy) => {
let read = lazy.read().unwrap();
let entry = read.get(index)?;
if let Some(metrics) = &entry {
return Some(metrics.clone());
}
core::mem::drop(read);
// The std RwLock doesn't support upgrading and contention is
// expected to be low, so let's just race to compute the new
// metrics.
let shaper = Shaper::new(font, shaper_mode);
let style_class = style.style_class()?;
let metrics = compute_unscaled_style_metrics(&shaper, coords, style_class);
let mut entry = lazy.write().unwrap();
*entry.get_mut(index)? = Some(metrics.clone());
Some(metrics)
}
}
}
}
/// Scaled metrics for a single style and script.
#[derive(Clone, Default, Debug)]
pub(crate) struct ScaledStyleMetrics {
/// Multidimensional scaling factors and deltas.
pub scale: Scale,
/// Per-dimension scaled metrics.
pub axes: [ScaledAxisMetrics; 2],
}
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) struct WidthMetrics {
/// Used for creating edges.
pub edge_distance_threshold: i32,
/// Default stem thickness.
pub standard_width: i32,
/// Is standard width very light?
pub is_extra_light: bool,
}
pub(crate) type UnscaledWidths = SmallVec<i32, MAX_WIDTHS>;
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) struct ScaledWidth {
/// Width after applying scale.
pub scaled: i32,
/// Grid-fitted width.
pub fitted: i32,
}
pub(crate) type ScaledWidths = SmallVec<ScaledWidth, MAX_WIDTHS>;
/// Captures scaling parameters which may be modified during metrics
/// computation.
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct Scale {
/// Font unit to 26.6 scale in the X direction.
pub x_scale: i32,
/// Font unit to 26.6 scale in the Y direction.
pub y_scale: i32,
/// In 1/64 device pixels.
pub x_delta: i32,
/// In 1/64 device pixels.
pub y_delta: i32,
/// Font size in pixels per em.
pub size: f32,
/// From the source font.
pub units_per_em: i32,
/// Flags that determine hinting functionality.
pub flags: u32,
}
impl Scale {
/// Create initial scaling parameters from metrics and hinting target.
pub fn new(
size: f32,
units_per_em: i32,
font_style: Style,
target: Target,
group: ScriptGroup,
) -> Self {
let scale =
(Fixed::from_bits((size * 64.0) as i32) / Fixed::from_bits(units_per_em)).to_bits();
let mut flags = 0;
let is_italic = font_style != Style::Normal;
let is_mono = target == Target::Mono;
let is_light = target.is_light() || target.preserve_linear_metrics();
// Snap vertical stems for monochrome and horizontal LCD rendering.
if is_mono || target.is_lcd() {
flags |= Self::HORIZONTAL_SNAP;
}
// Snap horizontal stems for monochrome and vertical LCD rendering.
if is_mono || target.is_vertical_lcd() {
flags |= Self::VERTICAL_SNAP;
}
// Adjust stems to full pixels unless in LCD or light modes.
if !(target.is_lcd() || is_light) {
flags |= Self::STEM_ADJUST;
}
if is_mono {
flags |= Self::MONO;
}
if group == ScriptGroup::Default {
// Disable horizontal hinting completely for LCD, light hinting
// and italic fonts
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2674>
if target.is_lcd() || is_light || is_italic {
flags |= Self::NO_HORIZONTAL;
}
} else {
// CJK doesn't hint advances
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1432>
flags |= Self::NO_ADVANCE;
}
// CJK doesn't hint advances
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1432>
if group != ScriptGroup::Default {
flags |= Self::NO_ADVANCE;
}
Self {
x_scale: scale,
y_scale: scale,
x_delta: 0,
y_delta: 0,
size,
units_per_em,
flags,
}
}
}
/// Scaler flags that determine hinting settings.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aftypes.h#L115>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L143>
impl Scale {
/// Stem width snapping.
pub const HORIZONTAL_SNAP: u32 = 1 << 0;
/// Stem height snapping.
pub const VERTICAL_SNAP: u32 = 1 << 1;
/// Stem width/height adjustment.
pub const STEM_ADJUST: u32 = 1 << 2;
/// Monochrome rendering.
pub const MONO: u32 = 1 << 3;
/// Disable horizontal hinting.
pub const NO_HORIZONTAL: u32 = 1 << 4;
/// Disable vertical hinting.
pub const NO_VERTICAL: u32 = 1 << 5;
/// Disable advance hinting.
pub const NO_ADVANCE: u32 = 1 << 6;
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L59>
pub(crate) fn sort_and_quantize_widths(widths: &mut UnscaledWidths, threshold: i32) {
if widths.len() <= 1 {
return;
}
widths.sort_unstable();
let table = widths.as_mut_slice();
let mut cur_ix = 0;
let mut cur_val = table[cur_ix];
let last_ix = table.len() - 1;
let mut ix = 1;
// Compute and use mean values for clusters not larger than
// `threshold`.
while ix < table.len() {
if (table[ix] - cur_val) > threshold || ix == last_ix {
let mut sum = 0;
// Fix loop for end of array?
if (table[ix] - cur_val <= threshold) && ix == last_ix {
ix += 1;
}
for val in &mut table[cur_ix..ix] {
sum += *val;
*val = 0;
}
table[cur_ix] = sum / ix as i32;
if ix < last_ix {
cur_ix = ix + 1;
cur_val = table[cur_ix];
}
}
ix += 1;
}
cur_ix = 1;
// Compress array to remove zero values
for ix in 1..table.len() {
if table[ix] != 0 {
table[cur_ix] = table[ix];
cur_ix += 1;
}
}
widths.truncate(cur_ix);
}
// Fixed point helpers
//
// Note: lots of bit fiddling based fixed point math in the autohinter
// so we're opting out of using the strongly typed variants because they
// just add noise and reduce clarity.
pub(crate) fn fixed_mul(a: i32, b: i32) -> i32 {
(Fixed::from_bits(a) * Fixed::from_bits(b)).to_bits()
}
pub(crate) fn fixed_div(a: i32, b: i32) -> i32 {
(Fixed::from_bits(a) / Fixed::from_bits(b)).to_bits()
}
pub(crate) fn fixed_mul_div(a: i32, b: i32, c: i32) -> i32 {
Fixed::from_bits(a)
.mul_div(Fixed::from_bits(b), Fixed::from_bits(c))
.to_bits()
}
pub(crate) fn pix_round(a: i32) -> i32 {
(a + 32) & !63
}
pub(crate) fn pix_floor(a: i32) -> i32 {
a & !63
}
#[cfg(test)]
mod tests {
use super::{
super::{
shape::{Shaper, ShaperMode},
style::STYLE_CLASSES,
},
*,
};
use raw::TableProvider;
#[test]
fn sort_widths() {
// We use 10 and 20 as thresholds because the computation used
// is units_per_em / 100
assert_eq!(sort_widths_helper(&[1], 10), &[1]);
assert_eq!(sort_widths_helper(&[1], 20), &[1]);
assert_eq!(sort_widths_helper(&[60, 20, 40, 35], 10), &[20, 35, 13, 60]);
assert_eq!(sort_widths_helper(&[60, 20, 40, 35], 20), &[31, 60]);
}
fn sort_widths_helper(widths: &[i32], threshold: i32) -> Vec<i32> {
let mut widths2 = UnscaledWidths::new();
for width in widths {
widths2.push(*width);
}
sort_and_quantize_widths(&mut widths2, threshold);
widths2.into_iter().collect()
}
#[test]
fn precomputed_style_set() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let coords = &[];
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let glyph_count = font.maxp().unwrap().num_glyphs() as u32;
let style_map = GlyphStyleMap::new(glyph_count, &shaper);
let style_set =
UnscaledStyleMetricsSet::precomputed(&font, coords, ShaperMode::Nominal, &style_map);
let UnscaledStyleMetricsSet::Precomputed(set) = &style_set else {
panic!("we definitely made a precomputed style set");
};
// This font has Latin, Hebrew and CJK (for unassigned chars) styles
assert_eq!(STYLE_CLASSES[set[0].class_ix as usize].name, "Latin");
assert_eq!(STYLE_CLASSES[set[1].class_ix as usize].name, "Hebrew");
assert_eq!(
STYLE_CLASSES[set[2].class_ix as usize].name,
"CJKV ideographs"
);
assert_eq!(set.len(), 3);
}
#[test]
fn lazy_style_set() {
let font = FontRef::new(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS).unwrap();
let coords = &[];
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let glyph_count = font.maxp().unwrap().num_glyphs() as u32;
let style_map = GlyphStyleMap::new(glyph_count, &shaper);
let style_set = UnscaledStyleMetricsSet::lazy(&style_map);
let all_empty = lazy_set_presence(&style_set);
// Set starts out all empty
assert_eq!(all_empty, [false; 3]);
// First load a CJK glyph
let metrics2 = style_set
.get(
&font,
coords,
ShaperMode::Nominal,
&style_map,
GlyphId::new(0),
)
.unwrap();
assert_eq!(
STYLE_CLASSES[metrics2.class_ix as usize].name,
"CJKV ideographs"
);
let only_cjk = lazy_set_presence(&style_set);
assert_eq!(only_cjk, [false, false, true]);
// Then a Hebrew glyph
let metrics1 = style_set
.get(
&font,
coords,
ShaperMode::Nominal,
&style_map,
GlyphId::new(1),
)
.unwrap();
assert_eq!(STYLE_CLASSES[metrics1.class_ix as usize].name, "Hebrew");
let hebrew_and_cjk = lazy_set_presence(&style_set);
assert_eq!(hebrew_and_cjk, [false, true, true]);
// And finally a Latin glyph
let metrics0 = style_set
.get(
&font,
coords,
ShaperMode::Nominal,
&style_map,
GlyphId::new(15),
)
.unwrap();
assert_eq!(STYLE_CLASSES[metrics0.class_ix as usize].name, "Latin");
let all_present = lazy_set_presence(&style_set);
assert_eq!(all_present, [true; 3]);
}
fn lazy_set_presence(style_set: &UnscaledStyleMetricsSet) -> Vec<bool> {
let UnscaledStyleMetricsSet::Lazy(set) = &style_set else {
panic!("we definitely made a lazy style set");
};
set.read()
.unwrap()
.iter()
.map(|opt| opt.is_some())
.collect()
}
}

View File

@@ -0,0 +1,429 @@
//! Metrics scaling.
//!
//! Uses the widths and blues computations to generate unscaled metrics for a
//! given style/script.
//!
//! Then applies a scaling factor to those metrics, computes a potentially
//! modified scale, and tags active blue zones.
use super::super::{
metrics::{
fixed_div, fixed_mul, fixed_mul_div, pix_round, BlueZones, Scale, ScaledAxisMetrics,
ScaledBlue, ScaledStyleMetrics, ScaledWidth, UnscaledAxisMetrics, UnscaledBlue,
UnscaledStyleMetrics, WidthMetrics,
},
shape::Shaper,
style::{ScriptGroup, StyleClass},
topo::{Axis, Dimension},
};
use crate::{prelude::Size, MetadataProvider};
use raw::types::F2Dot14;
/// Computes unscaled metrics for the Latin writing system.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1134>
pub(crate) fn compute_unscaled_style_metrics(
shaper: &Shaper,
coords: &[F2Dot14],
style: &StyleClass,
) -> UnscaledStyleMetrics {
let charmap = shaper.charmap();
// We don't attempt to produce any metrics if we don't have a Unicode
// cmap
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1146>
if charmap.is_symbol() {
return UnscaledStyleMetrics {
class_ix: style.index as u16,
axes: [
UnscaledAxisMetrics {
dim: Axis::HORIZONTAL,
..Default::default()
},
UnscaledAxisMetrics {
dim: Axis::VERTICAL,
..Default::default()
},
],
..Default::default()
};
}
let [hwidths, vwidths] = super::widths::compute_widths(shaper, coords, style);
let [hblues, vblues] = super::blues::compute_unscaled_blues(shaper, coords, style);
let glyph_metrics = shaper.font().glyph_metrics(Size::unscaled(), coords);
let mut digit_advance = None;
let mut digits_have_same_width = true;
for ch in '0'..='9' {
if let Some(advance) = charmap
.map(ch)
.and_then(|gid| glyph_metrics.advance_width(gid))
{
if digit_advance.is_some() && digit_advance != Some(advance) {
digits_have_same_width = false;
break;
}
digit_advance = Some(advance);
}
}
UnscaledStyleMetrics {
class_ix: style.index as u16,
digits_have_same_width,
axes: [
UnscaledAxisMetrics {
dim: Axis::HORIZONTAL,
blues: hblues,
width_metrics: hwidths.0,
widths: hwidths.1,
},
UnscaledAxisMetrics {
dim: Axis::VERTICAL,
blues: vblues,
width_metrics: vwidths.0,
widths: vwidths.1,
},
],
}
}
/// Computes scaled metrics for the Latin writing system.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1491>
pub(crate) fn scale_style_metrics(
unscaled_metrics: &UnscaledStyleMetrics,
mut scale: Scale,
) -> ScaledStyleMetrics {
let scale_axis_fn = if unscaled_metrics.style_class().script.group == ScriptGroup::Default {
scale_default_axis_metrics
} else {
scale_cjk_axis_metrics
};
let mut scale_axis = |axis: &UnscaledAxisMetrics| {
scale_axis_fn(
axis.dim,
&axis.widths,
axis.width_metrics,
&axis.blues,
&mut scale,
)
};
let axes = [
scale_axis(&unscaled_metrics.axes[0]),
scale_axis(&unscaled_metrics.axes[1]),
];
ScaledStyleMetrics { scale, axes }
}
/// Computes scaled metrics for a single axis.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L1168>
fn scale_default_axis_metrics(
dim: Dimension,
widths: &[i32],
width_metrics: WidthMetrics,
blues: &[UnscaledBlue],
scale: &mut Scale,
) -> ScaledAxisMetrics {
let mut axis = ScaledAxisMetrics {
dim,
..Default::default()
};
if dim == Axis::HORIZONTAL {
axis.scale = scale.x_scale;
axis.delta = scale.x_delta;
} else {
axis.scale = scale.y_scale;
axis.delta = scale.y_delta;
};
// Correct Y scale to optimize alignment
if let Some(blue_ix) = blues
.iter()
.position(|blue| blue.zones.contains(BlueZones::ADJUSTMENT))
{
let unscaled_blue = &blues[blue_ix];
let scaled = fixed_mul(axis.scale, unscaled_blue.overshoot);
let fitted = (scaled + 40) & !63;
if scaled != fitted && dim == Axis::VERTICAL {
let new_scale = fixed_mul_div(axis.scale, fitted, scaled);
// Scaling should not adjust by more than 2 pixels
let mut max_height = scale.units_per_em;
for blue in blues {
max_height = max_height.max(blue.ascender).max(-blue.descender);
}
let mut dist = fixed_mul(max_height, new_scale - axis.scale).abs();
dist &= !127;
if dist == 0 {
axis.scale = new_scale;
scale.y_scale = new_scale;
}
}
}
// Now scale the widths
axis.width_metrics = width_metrics;
for unscaled_width in widths {
let scaled = fixed_mul(axis.scale, *unscaled_width);
axis.widths.push(ScaledWidth {
scaled,
fitted: scaled,
});
}
// Compute extra light property: this is a standard width that is
// less than 5/8 pixels
axis.width_metrics.is_extra_light =
fixed_mul(axis.width_metrics.standard_width, axis.scale) < (32 + 8);
if dim == Axis::VERTICAL {
// And scale the blue zones
for unscaled_blue in blues {
let scaled_position = fixed_mul(axis.scale, unscaled_blue.position) + axis.delta;
let scaled_overshoot = fixed_mul(axis.scale, unscaled_blue.overshoot) + axis.delta;
let mut blue = ScaledBlue {
position: ScaledWidth {
scaled: scaled_position,
fitted: scaled_position,
},
overshoot: ScaledWidth {
scaled: scaled_overshoot,
fitted: scaled_overshoot,
},
zones: unscaled_blue.zones,
is_active: false,
};
// Only activate blue zones less than 3/4 pixel tall
let dist = fixed_mul(unscaled_blue.position - unscaled_blue.overshoot, axis.scale);
if (-48..=48).contains(&dist) {
let mut delta = dist.abs();
if delta < 32 {
delta = 0;
} else if delta < 48 {
delta = 32;
} else {
delta = 64;
}
if dist < 0 {
delta = -delta;
}
blue.position.fitted = pix_round(blue.position.scaled);
blue.overshoot.fitted = blue.position.fitted - delta;
blue.is_active = true;
}
axis.blues.push(blue);
}
// Use sub-top blue zone if it doesn't overlap with another
// non-sub-top blue zone
for blue_ix in 0..axis.blues.len() {
let blue = axis.blues[blue_ix];
if !blue.zones.is_sub_top() || !blue.is_active {
continue;
}
for blue2 in &axis.blues {
if blue2.zones.is_sub_top() || !blue2.is_active {
continue;
}
if blue2.position.fitted <= blue.overshoot.fitted
&& blue2.overshoot.fitted >= blue.position.fitted
{
axis.blues[blue_ix].is_active = false;
break;
}
}
}
}
axis
}
/// Computes scaled metrics for a single axis for the CJK script group.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L661>
fn scale_cjk_axis_metrics(
dim: Dimension,
widths: &[i32],
width_metrics: WidthMetrics,
blues: &[UnscaledBlue],
scale: &mut Scale,
) -> ScaledAxisMetrics {
let mut axis = ScaledAxisMetrics {
dim,
..Default::default()
};
axis.dim = dim;
if dim == Axis::HORIZONTAL {
axis.scale = scale.x_scale;
axis.delta = scale.x_delta;
} else {
axis.scale = scale.y_scale;
axis.delta = scale.y_delta;
};
let scale = axis.scale;
// Scale the blue zones
for unscaled_blue in blues {
let position = fixed_mul(unscaled_blue.position, scale) + axis.delta;
let overshoot = fixed_mul(unscaled_blue.overshoot, scale) + axis.delta;
let mut blue = ScaledBlue {
position: ScaledWidth {
scaled: position,
fitted: position,
},
overshoot: ScaledWidth {
scaled: overshoot,
fitted: overshoot,
},
zones: unscaled_blue.zones,
is_active: false,
};
// A blue zone is only active if it is less than 3/4 pixels tall
let dist = fixed_mul(unscaled_blue.position - unscaled_blue.overshoot, scale);
if (-48..=48).contains(&dist) {
blue.position.fitted = pix_round(blue.position.scaled);
// For CJK, "overshoot" is actually undershoot
let delta1 = fixed_div(blue.position.fitted, scale) - unscaled_blue.overshoot;
let mut delta2 = fixed_mul(delta1.abs(), scale);
if delta2 < 32 {
delta2 = 0;
} else {
delta2 = pix_round(delta2);
}
if delta1 < 0 {
delta2 = -delta2;
}
blue.overshoot.fitted = blue.position.fitted - delta2;
blue.is_active = true;
}
axis.blues.push(blue);
}
// FreeType never seems to compute scaled width values. We'll just
// match this behavior for now.
// <https://github.com/googlefonts/fontations/issues/1129>
for _ in 0..widths.len() {
axis.widths.push(ScaledWidth::default());
}
axis.width_metrics = width_metrics;
axis
}
#[cfg(test)]
mod tests {
use super::{
super::super::{shape::ShaperMode, style},
*,
};
use crate::attribute::Style;
use raw::{FontRef, TableProvider};
#[test]
fn scaled_metrics_default() {
// Note: expected values scraped from a FreeType debugging
// session
let scaled_metrics = make_scaled_metrics(
font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
StyleClass::HEBR,
);
// Check scale and deltas
assert_eq!(scaled_metrics.scale.x_scale, 67109);
assert_eq!(scaled_metrics.scale.y_scale, 67109);
assert_eq!(scaled_metrics.scale.x_delta, 0);
assert_eq!(scaled_metrics.scale.y_delta, 0);
// Horizontal widths
let h_axis = &scaled_metrics.axes[0];
let expected_h_widths = [55];
// No horizontal blues
check_axis(h_axis, &expected_h_widths, &[]);
// Not extra light
assert!(!h_axis.width_metrics.is_extra_light);
// Vertical widths
let v_axis = &scaled_metrics.axes[1];
let expected_v_widths = [22, 112];
// Vertical blues
#[rustfmt::skip]
let expected_v_blues = [
// ((scaled_pos, fitted_pos), (scaled_shoot, fitted_shoot), flags, is_active)
ScaledBlue::from(((606, 576), (606, 576), BlueZones::TOP, true)),
ScaledBlue::from(((0, 0), (-9, 0), BlueZones::default(), true)),
ScaledBlue::from(((-246, -256), (-246, -256), BlueZones::default(), true)),
];
check_axis(v_axis, &expected_v_widths, &expected_v_blues);
// This one is extra light
assert!(v_axis.width_metrics.is_extra_light);
}
#[test]
fn cjk_scaled_metrics() {
// Note: expected values scraped from a FreeType debugging
// session
let scaled_metrics = make_scaled_metrics(
font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
StyleClass::HANI,
);
// Check scale and deltas
assert_eq!(scaled_metrics.scale.x_scale, 67109);
assert_eq!(scaled_metrics.scale.y_scale, 67109);
assert_eq!(scaled_metrics.scale.x_delta, 0);
assert_eq!(scaled_metrics.scale.y_delta, 0);
// Horizontal widths
let h_axis = &scaled_metrics.axes[0];
let expected_h_widths = [0];
check_axis(h_axis, &expected_h_widths, &[]);
// Not extra light
assert!(!h_axis.width_metrics.is_extra_light);
// Vertical widths
let v_axis = &scaled_metrics.axes[1];
let expected_v_widths = [0];
// Vertical blues
#[rustfmt::skip]
let expected_v_blues = [
// ((scaled_pos, fitted_pos), (scaled_shoot, fitted_shoot), flags, is_active)
ScaledBlue::from(((857, 832), (844, 832), BlueZones::TOP, true)),
ScaledBlue::from(((-80, -64), (-68, -64), BlueZones::default(), true)),
];
// No horizontal blues
check_axis(v_axis, &expected_v_widths, &expected_v_blues);
// Also not extra light
assert!(!v_axis.width_metrics.is_extra_light);
}
fn make_scaled_metrics(font_data: &[u8], style_class: usize) -> ScaledStyleMetrics {
let font = FontRef::new(font_data).unwrap();
let class = &style::STYLE_CLASSES[style_class];
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let unscaled_metrics = compute_unscaled_style_metrics(&shaper, Default::default(), class);
let scale = Scale::new(
16.0,
font.head().unwrap().units_per_em() as i32,
Style::Normal,
Default::default(),
class.script.group,
);
scale_style_metrics(&unscaled_metrics, scale)
}
fn check_axis(
axis: &ScaledAxisMetrics,
expected_widths: &[i32],
expected_blues: &[ScaledBlue],
) {
let widths = axis
.widths
.iter()
.map(|width| width.scaled)
.collect::<Vec<_>>();
assert_eq!(widths, expected_widths);
assert_eq!(axis.blues.as_slice(), expected_blues);
}
impl From<(i32, i32)> for ScaledWidth {
fn from(value: (i32, i32)) -> Self {
Self {
scaled: value.0,
fitted: value.1,
}
}
}
impl From<((i32, i32), (i32, i32), BlueZones, bool)> for ScaledBlue {
fn from(value: ((i32, i32), (i32, i32), BlueZones, bool)) -> Self {
Self {
position: value.0.into(),
overshoot: value.1.into(),
zones: value.2,
is_active: value.3,
}
}
}
}

View File

@@ -0,0 +1,205 @@
//! Latin standard stem width computation.
use super::super::{
derived_constant,
metrics::{self, UnscaledWidths, WidthMetrics, MAX_WIDTHS},
outline::Outline,
shape::{ShapedCluster, Shaper},
style::{ScriptGroup, StyleClass},
topo::{compute_segments, link_segments, Axis},
};
use crate::MetadataProvider;
use raw::{types::F2Dot14, TableProvider};
/// Compute all stem widths and initialize standard width and height for the
/// given script.
///
/// Returns width metrics and unscaled widths for each dimension.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L54>
pub(super) fn compute_widths(
shaper: &Shaper,
coords: &[F2Dot14],
style: &StyleClass,
) -> [(WidthMetrics, UnscaledWidths); 2] {
let mut result: [(WidthMetrics, UnscaledWidths); 2] = Default::default();
let font = shaper.font();
let glyphs = font.outline_glyphs();
let units_per_em = font
.head()
.map(|head| head.units_per_em() as i32)
.unwrap_or_default();
let mut outline = Outline::default();
let mut axis = Axis::default();
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut shaped_cluster = ShapedCluster::default();
// We take the first available glyph from the standard character set.
let glyph = style
.script
.std_chars
.split(' ')
.filter_map(|cluster| {
cluster_shaper.shape(cluster, &mut shaped_cluster);
// Reject input that maps to more than a single glyph
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L128>
match shaped_cluster.as_slice() {
[glyph] if glyph.id.to_u32() != 0 => glyphs.get(glyph.id),
_ => None,
}
})
.next();
if let Some(glyph) = glyph {
if outline.fill(&glyph, coords).is_ok() && !outline.points.is_empty() {
// Now process each dimension
for (dim, (_metrics, widths)) in result.iter_mut().enumerate() {
axis.reset(dim, outline.orientation);
// Segment computation for widths always uses the default
// script group
compute_segments(&mut outline, &mut axis, ScriptGroup::Default);
link_segments(&outline, &mut axis, 0, ScriptGroup::Default, None);
let segments = axis.segments.as_slice();
for (segment_ix, segment) in segments.iter().enumerate() {
let segment_ix = segment_ix as u16;
let Some(link_ix) = segment.link_ix else {
continue;
};
let link = &segments[link_ix as usize];
if link_ix > segment_ix && link.link_ix == Some(segment_ix) {
let dist = (segment.pos as i32 - link.pos as i32).abs();
if widths.len() < MAX_WIDTHS {
widths.push(dist);
} else {
break;
}
}
}
// FreeTypes `af_sort_and_quantize_widths()` has the side effect
// of always updating the width count to 1 when we don't find
// any...
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L121>
if widths.is_empty() {
widths.push(0);
}
// The value 100 is heuristic
metrics::sort_and_quantize_widths(widths, units_per_em / 100);
}
}
}
for (metrics, widths) in result.iter_mut() {
// Now set derived values
let stdw = widths
.first()
.copied()
.unwrap_or_else(|| derived_constant(units_per_em, 50));
// Heuristic value of 20% of the smallest width
metrics.edge_distance_threshold = stdw / 5;
metrics.standard_width = stdw;
metrics.is_extra_light = false;
}
result
}
#[cfg(test)]
mod tests {
use super::{
super::super::{shape::ShaperMode, style},
*,
};
use raw::FontRef;
#[test]
fn computed_widths() {
// Expected data produced by internal routines in FreeType. Scraped
// from a debugger
check_widths(
font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
super::StyleClass::HEBR,
[
(
WidthMetrics {
edge_distance_threshold: 10,
standard_width: 54,
is_extra_light: false,
},
&[54],
),
(
WidthMetrics {
edge_distance_threshold: 4,
standard_width: 21,
is_extra_light: false,
},
&[21, 109],
),
],
);
}
#[test]
fn fallback_widths() {
// Expected data produced by internal routines in FreeType. Scraped
// from a debugger
check_widths(
font_test_data::CANTARELL_VF_TRIMMED,
super::StyleClass::LATN,
[
(
WidthMetrics {
edge_distance_threshold: 4,
standard_width: 24,
is_extra_light: false,
},
&[],
),
(
WidthMetrics {
edge_distance_threshold: 4,
standard_width: 24,
is_extra_light: false,
},
&[],
),
],
);
}
#[test]
fn cjk_computed_widths() {
// Expected data produced by internal routines in FreeType. Scraped
// from a debugger
check_widths(
font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
super::StyleClass::HANI,
[
(
WidthMetrics {
edge_distance_threshold: 13,
standard_width: 65,
is_extra_light: false,
},
&[65],
),
(
WidthMetrics {
edge_distance_threshold: 5,
standard_width: 29,
is_extra_light: false,
},
&[29],
),
],
);
}
fn check_widths(font_data: &[u8], style_class: usize, expected: [(WidthMetrics, &[i32]); 2]) {
let font = FontRef::new(font_data).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let script = &style::STYLE_CLASSES[style_class];
let [(hori_metrics, hori_widths), (vert_metrics, vert_widths)] =
compute_widths(&shaper, Default::default(), script);
assert_eq!(hori_metrics, expected[0].0);
assert_eq!(hori_widths.as_slice(), expected[0].1);
assert_eq!(vert_metrics, expected[1].0);
assert_eq!(vert_widths.as_slice(), expected[1].1);
}
}

View File

@@ -0,0 +1,19 @@
//! Runtime autohinting support.
mod hint;
mod instance;
mod metrics;
mod outline;
mod shape;
mod style;
mod topo;
pub use instance::GlyphStyles;
pub(crate) use instance::Instance;
/// All constants are defined based on a UPEM of 2048.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.h#L34>
fn derived_constant(units_per_em: i32, value: i32) -> i32 {
value * units_per_em / 2048
}

View File

@@ -0,0 +1,768 @@
//! Outline representation and helpers for autohinting.
use super::{
super::{
path,
pen::PathStyle,
unscaled::{UnscaledOutlineSink, UnscaledPoint},
DrawError, LocationRef, OutlineGlyph, OutlinePen,
},
metrics::Scale,
};
use crate::collections::SmallVec;
use core::ops::Range;
use raw::{
tables::glyf::{PointFlags, PointMarker},
types::{F26Dot6, F2Dot14},
};
/// Hinting directions.
///
/// The values are such that `dir1 + dir2 == 0` when the directions are
/// opposite.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L45>
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
#[repr(i8)]
pub(crate) enum Direction {
#[default]
None = 4,
Right = 1,
Left = -1,
Up = 2,
Down = -2,
}
impl Direction {
/// Computes a direction from a vector.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L751>
pub fn new(dx: i32, dy: i32) -> Self {
let (dir, long_arm, short_arm) = if dy >= dx {
if dy >= -dx {
(Direction::Up, dy, dx)
} else {
(Direction::Left, -dx, dy)
}
} else if dy >= -dx {
(Direction::Right, dx, dy)
} else {
(Direction::Down, -dy, dx)
};
// Return no direction if arm lengths do not differ enough.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L789>
if long_arm <= 14 * short_arm.abs() {
Direction::None
} else {
dir
}
}
pub fn is_opposite(self, other: Self) -> bool {
self as i8 + other as i8 == 0
}
pub fn is_same_axis(self, other: Self) -> bool {
(self as i8).abs() == (other as i8).abs()
}
pub fn normalize(self) -> Self {
// FreeType uses absolute value for this.
match self {
Self::Left => Self::Right,
Self::Down => Self::Up,
_ => self,
}
}
}
/// The overall orientation of an outline.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum Orientation {
Clockwise,
CounterClockwise,
}
/// Outline point with a lot of context for hinting.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L239>
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) struct Point {
/// Describes the type and hinting state of the point.
pub flags: PointFlags,
/// X coordinate in font units.
pub fx: i32,
/// Y coordinate in font units.
pub fy: i32,
/// Scaled X coordinate.
pub ox: i32,
/// Scaled Y coordinate.
pub oy: i32,
/// Hinted X coordinate.
pub x: i32,
/// Hinted Y coordinate.
pub y: i32,
/// Direction of inwards vector.
pub in_dir: Direction,
/// Direction of outwards vector.
pub out_dir: Direction,
/// Context dependent coordinate.
pub u: i32,
/// Context dependent coordinate.
pub v: i32,
/// Index of next point in contour.
pub next_ix: u16,
/// Index of previous point in contour.
pub prev_ix: u16,
}
impl Point {
pub fn is_on_curve(&self) -> bool {
self.flags.is_on_curve()
}
/// Returns the index of the next point in the contour.
pub fn next(&self) -> usize {
self.next_ix as usize
}
/// Returns the index of the previous point in the contour.
pub fn prev(&self) -> usize {
self.prev_ix as usize
}
#[inline(always)]
fn as_contour_point(&self) -> path::ContourPoint<F26Dot6> {
path::ContourPoint {
x: F26Dot6::from_bits(self.x),
y: F26Dot6::from_bits(self.y),
flags: self.flags,
}
}
}
// Matches FreeType's inline usage
//
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L332>
const MAX_INLINE_POINTS: usize = 96;
const MAX_INLINE_CONTOURS: usize = 8;
#[derive(Default)]
pub(crate) struct Outline {
pub units_per_em: i32,
pub orientation: Option<Orientation>,
pub points: SmallVec<Point, MAX_INLINE_POINTS>,
pub contours: SmallVec<Contour, MAX_INLINE_CONTOURS>,
pub advance: i32,
}
impl Outline {
/// Fills the outline from the given glyph.
pub fn fill(&mut self, glyph: &OutlineGlyph, coords: &[F2Dot14]) -> Result<(), DrawError> {
self.clear();
let advance = glyph.draw_unscaled(LocationRef::new(coords), None, self)?;
self.advance = advance;
self.units_per_em = glyph.units_per_em() as i32;
// Heuristic value
let near_limit = 20 * self.units_per_em / 2048;
self.link_points();
self.mark_near_points(near_limit);
self.compute_directions(near_limit);
self.simplify_topology();
self.check_remaining_weak_points();
self.compute_orientation();
Ok(())
}
/// Applies dimension specific scaling factors and deltas to each
/// point in the outline.
pub fn scale(&mut self, scale: &Scale) {
use super::metrics::fixed_mul;
for point in &mut self.points {
let x = fixed_mul(point.fx, scale.x_scale) + scale.x_delta;
let y = fixed_mul(point.fy, scale.y_scale) + scale.y_delta;
point.ox = x;
point.x = x;
point.oy = y;
point.y = y;
}
}
pub fn clear(&mut self) {
self.units_per_em = 0;
self.points.clear();
self.contours.clear();
self.advance = 0;
}
pub fn to_path(
&self,
style: PathStyle,
pen: &mut impl OutlinePen,
) -> Result<(), path::ToPathError> {
for contour in &self.contours {
let Some(points) = self.points.get(contour.range()) else {
continue;
};
if let Some(last_point) = points.last().map(Point::as_contour_point) {
path::contour_to_path(
points.iter().map(Point::as_contour_point),
last_point,
style,
pen,
)?;
}
}
Ok(())
}
}
impl Outline {
/// Sets next and previous indices for each point.
fn link_points(&mut self) {
let points = self.points.as_mut_slice();
for contour in &self.contours {
let Some(points) = points.get_mut(contour.range()) else {
continue;
};
let first_ix = contour.first() as u16;
let mut prev_ix = contour.last() as u16;
for (ix, point) in points.iter_mut().enumerate() {
let ix = ix as u16 + first_ix;
point.prev_ix = prev_ix;
prev_ix = ix;
point.next_ix = ix + 1;
}
points.last_mut().unwrap().next_ix = first_ix;
}
}
/// Computes the near flag for each contour.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1017>
fn mark_near_points(&mut self, near_limit: i32) {
let points = self.points.as_mut_slice();
for contour in &self.contours {
let mut prev_ix = contour.last();
for ix in contour.range() {
let point = points[ix];
let prev = &mut points[prev_ix];
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1017>
let out_x = point.fx - prev.fx;
let out_y = point.fy - prev.fy;
if out_x.abs() + out_y.abs() < near_limit {
prev.flags.set_marker(PointMarker::NEAR);
}
prev_ix = ix;
}
}
}
/// Compute directions of in and out vectors.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1064>
fn compute_directions(&mut self, near_limit: i32) {
let near_limit2 = 2 * near_limit - 1;
let points = self.points.as_mut_slice();
for contour in &self.contours {
// Walk backward to find the first non-near point.
let mut first_ix = contour.first();
let mut ix = first_ix;
let mut prev_ix = contour.prev(first_ix);
let mut point = points[first_ix];
while prev_ix != first_ix {
let prev = points[prev_ix];
let out_x = point.fx - prev.fx;
let out_y = point.fy - prev.fy;
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1102>
if out_x.abs() + out_y.abs() >= near_limit2 {
break;
}
point = prev;
ix = prev_ix;
prev_ix = contour.prev(prev_ix);
}
first_ix = ix;
// Abuse u and v fields to store deltas to the next and previous
// non-near points, respectively.
let first = &mut points[first_ix];
first.u = first_ix as _;
first.v = first_ix as _;
let mut next_ix = first_ix;
let mut ix = first_ix;
// Now loop over all points in the contour to compute in and
// out directions
let mut out_x = 0;
let mut out_y = 0;
loop {
let point_ix = next_ix;
next_ix = contour.next(point_ix);
let point = points[point_ix];
let next = &mut points[next_ix];
// Accumulate the deltas until we surpass near_limit
out_x += next.fx - point.fx;
out_y += next.fy - point.fy;
if out_x.abs() + out_y.abs() < near_limit {
next.flags.set_marker(PointMarker::WEAK_INTERPOLATION);
// The original code is a do-while loop, so make
// sure we keep this condition before the continue
if next_ix == first_ix {
break;
}
continue;
}
let out_dir = Direction::new(out_x, out_y);
next.in_dir = out_dir;
next.v = ix as _;
let cur = &mut points[ix];
cur.u = next_ix as _;
cur.out_dir = out_dir;
// Adjust directions for all intermediate points
let mut inter_ix = contour.next(ix);
while inter_ix != next_ix {
let point = &mut points[inter_ix];
point.in_dir = out_dir;
point.out_dir = out_dir;
inter_ix = contour.next(inter_ix);
}
ix = next_ix;
points[ix].u = first_ix as _;
points[first_ix].v = ix as _;
out_x = 0;
out_y = 0;
if next_ix == first_ix {
break;
}
}
}
}
/// Simplify so that we can identify local extrema more reliably.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1181>
fn simplify_topology(&mut self) {
let points = self.points.as_mut_slice();
for i in 0..points.len() {
let point = points[i];
if point.flags.has_marker(PointMarker::WEAK_INTERPOLATION) {
continue;
}
if point.in_dir == Direction::None && point.out_dir == Direction::None {
let u_index = point.u as usize;
let v_index = point.v as usize;
let next_u = points[u_index];
let prev_v = points[v_index];
let in_x = point.fx - prev_v.fx;
let in_y = point.fy - prev_v.fy;
let out_x = next_u.fx - point.fx;
let out_y = next_u.fy - point.fy;
if (in_x ^ out_x) >= 0 && (in_y ^ out_y) >= 0 {
// Both vectors point into the same quadrant
points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION);
points[v_index].u = u_index as _;
points[u_index].v = v_index as _;
}
}
}
}
/// Check for remaining weak points.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L1226>
fn check_remaining_weak_points(&mut self) {
let points = self.points.as_mut_slice();
for i in 0..points.len() {
let point = points[i];
let mut make_weak = false;
if point.flags.has_marker(PointMarker::WEAK_INTERPOLATION) {
// Already weak
continue;
}
if !point.flags.is_on_curve() {
// Control points are always weak
make_weak = true;
} else if point.out_dir == point.in_dir {
if point.out_dir != Direction::None {
// Point lies on a vertical or horizontal segment but
// not at start or end
make_weak = true;
} else {
let u_index = point.u as usize;
let v_index = point.v as usize;
let next_u = points[u_index];
let prev_v = points[v_index];
if is_corner_flat(
point.fx - prev_v.fx,
point.fy - prev_v.fy,
next_u.fx - point.fx,
next_u.fy - point.fy,
) {
// One of the vectors is more dominant
make_weak = true;
points[v_index].u = u_index as _;
points[u_index].v = v_index as _;
}
}
} else if point.in_dir.is_opposite(point.out_dir) {
// Point forms a "spike"
make_weak = true;
}
if make_weak {
points[i].flags.set_marker(PointMarker::WEAK_INTERPOLATION);
}
}
}
/// Computes the overall winding order of the outline.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftoutln.c#L1049>
fn compute_orientation(&mut self) {
self.orientation = None;
let points = self.points.as_slice();
if points.is_empty() {
return;
}
fn point_to_i64(point: &Point) -> (i64, i64) {
(point.fx as i64, point.fy as i64)
}
let mut area = 0i64;
for contour in &self.contours {
let last_ix = contour.last();
let first_ix = contour.first();
let (mut prev_x, mut prev_y) = point_to_i64(&points[last_ix]);
for point in &points[first_ix..=last_ix] {
let (x, y) = point_to_i64(point);
area += (y - prev_y) * (x + prev_x);
(prev_x, prev_y) = (x, y);
}
}
use core::cmp::Ordering;
self.orientation = match area.cmp(&0) {
Ordering::Less => Some(Orientation::CounterClockwise),
Ordering::Greater => Some(Orientation::Clockwise),
Ordering::Equal => None,
};
}
}
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftcalc.c#L1026>
fn is_corner_flat(in_x: i32, in_y: i32, out_x: i32, out_y: i32) -> bool {
let ax = in_x + out_x;
let ay = in_y + out_y;
fn hypot(x: i32, y: i32) -> i32 {
let x = x.abs();
let y = y.abs();
if x > y {
x + ((3 * y) >> 3)
} else {
y + ((3 * x) >> 3)
}
}
let d_in = hypot(in_x, in_y);
let d_out = hypot(out_x, out_y);
let d_hypot = hypot(ax, ay);
(d_in + d_out - d_hypot) < (d_hypot >> 4)
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct Contour {
first_ix: u16,
last_ix: u16,
}
impl Contour {
pub fn first(self) -> usize {
self.first_ix as usize
}
pub fn last(self) -> usize {
self.last_ix as usize
}
pub fn next(self, index: usize) -> usize {
if index >= self.last_ix as usize {
self.first_ix as usize
} else {
index + 1
}
}
pub fn prev(self, index: usize) -> usize {
if index <= self.first_ix as usize {
self.last_ix as usize
} else {
index - 1
}
}
pub fn range(self) -> Range<usize> {
self.first()..self.last() + 1
}
}
impl UnscaledOutlineSink for Outline {
fn try_reserve(&mut self, additional: usize) -> Result<(), DrawError> {
if self.points.try_reserve(additional) {
Ok(())
} else {
Err(DrawError::InsufficientMemory)
}
}
fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError> {
let new_point = Point {
flags: point.flags,
fx: point.x as i32,
fy: point.y as i32,
..Default::default()
};
let new_point_ix: u16 = self
.points
.len()
.try_into()
.map_err(|_| DrawError::InsufficientMemory)?;
if point.is_contour_start {
self.contours.push(Contour {
first_ix: new_point_ix,
last_ix: new_point_ix,
});
} else if let Some(last_contour) = self.contours.last_mut() {
last_contour.last_ix += 1;
} else {
// If our first point is not marked as contour start, just
// create a new contour.
self.contours.push(Contour {
first_ix: new_point_ix,
last_ix: new_point_ix,
});
}
self.points.push(new_point);
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::super::{pen::SvgPen, DrawSettings};
use super::*;
use crate::{prelude::Size, MetadataProvider};
use raw::{types::GlyphId, FontRef, TableProvider};
#[test]
fn direction_from_vectors() {
assert_eq!(Direction::new(-100, 0), Direction::Left);
assert_eq!(Direction::new(100, 0), Direction::Right);
assert_eq!(Direction::new(0, -100), Direction::Down);
assert_eq!(Direction::new(0, 100), Direction::Up);
assert_eq!(Direction::new(7, 100), Direction::Up);
// This triggers the too close heuristic
assert_eq!(Direction::new(8, 100), Direction::None);
}
#[test]
fn direction_axes() {
use Direction::*;
let hori = [Left, Right];
let vert = [Up, Down];
for h in hori {
for h2 in hori {
assert!(h.is_same_axis(h2));
if h != h2 {
assert!(h.is_opposite(h2));
} else {
assert!(!h.is_opposite(h2));
}
}
for v in vert {
assert!(!h.is_same_axis(v));
assert!(!h.is_opposite(v));
}
}
for v in vert {
for v2 in vert {
assert!(v.is_same_axis(v2));
if v != v2 {
assert!(v.is_opposite(v2));
} else {
assert!(!v.is_opposite(v2));
}
}
}
}
#[test]
fn fill_outline() {
let outline = make_outline(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, 8);
use Direction::*;
let expected = &[
// (x, y, in_dir, out_dir, flags)
(107, 0, Left, Left, 3),
(85, 0, Left, None, 2),
(55, 26, None, Up, 2),
(55, 71, Up, Up, 3),
(55, 332, Up, Up, 3),
(55, 360, Up, None, 2),
(67, 411, None, None, 2),
(93, 459, None, None, 2),
(112, 481, None, Up, 1),
(112, 504, Up, Right, 1),
(168, 504, Right, Down, 1),
(168, 483, Down, None, 1),
(153, 473, None, None, 2),
(126, 428, None, None, 2),
(109, 366, None, Down, 2),
(109, 332, Down, Down, 3),
(109, 109, Down, Right, 1),
(407, 109, Right, Right, 3),
(427, 109, Right, None, 2),
(446, 136, None, None, 2),
(453, 169, None, Up, 2),
(453, 178, Up, Up, 3),
(453, 374, Up, Up, 3),
(453, 432, Up, None, 2),
(400, 483, None, Left, 2),
(362, 483, Left, Left, 3),
(109, 483, Left, Left, 3),
(86, 483, Left, None, 2),
(62, 517, None, Up, 2),
(62, 555, Up, Up, 3),
(62, 566, Up, None, 2),
(64, 587, None, None, 2),
(71, 619, None, None, 2),
(76, 647, None, Right, 1),
(103, 647, Right, Down, 9),
(103, 644, Down, Down, 3),
(103, 619, Down, None, 2),
(131, 592, None, Right, 2),
(155, 592, Right, Right, 3),
(386, 592, Right, Right, 3),
(437, 592, Right, None, 2),
(489, 552, None, None, 2),
(507, 485, None, Down, 2),
(507, 443, Down, Down, 3),
(507, 75, Down, Down, 3),
(507, 40, Down, None, 2),
(470, 0, None, Left, 2),
(436, 0, Left, Left, 3),
];
let points = outline
.points
.iter()
.map(|point| {
(
point.fx,
point.fy,
point.in_dir,
point.out_dir,
point.flags.to_bits(),
)
})
.collect::<Vec<_>>();
assert_eq!(&points, expected);
}
#[test]
fn orientation() {
let tt_outline = make_outline(font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS, 8);
// TrueType outlines are counter clockwise
assert_eq!(tt_outline.orientation, Some(Orientation::CounterClockwise));
let ps_outline = make_outline(font_test_data::CANTARELL_VF_TRIMMED, 4);
// PostScript outlines are clockwise
assert_eq!(ps_outline.orientation, Some(Orientation::Clockwise));
}
fn make_outline(font_data: &[u8], glyph_id: u32) -> Outline {
let font = FontRef::new(font_data).unwrap();
let glyphs = font.outline_glyphs();
let glyph = glyphs.get(GlyphId::from(glyph_id)).unwrap();
let mut outline = Outline::default();
outline.fill(&glyph, Default::default()).unwrap();
outline
}
#[test]
fn mostly_off_curve_to_path_scan_backward() {
compare_path_conversion(font_test_data::MOSTLY_OFF_CURVE, PathStyle::FreeType);
}
#[test]
fn mostly_off_curve_to_path_scan_forward() {
compare_path_conversion(font_test_data::MOSTLY_OFF_CURVE, PathStyle::HarfBuzz);
}
#[test]
fn starting_off_curve_to_path_scan_backward() {
compare_path_conversion(font_test_data::STARTING_OFF_CURVE, PathStyle::FreeType);
}
#[test]
fn starting_off_curve_to_path_scan_forward() {
compare_path_conversion(font_test_data::STARTING_OFF_CURVE, PathStyle::HarfBuzz);
}
#[test]
fn cubic_to_path_scan_backward() {
compare_path_conversion(font_test_data::CUBIC_GLYF, PathStyle::FreeType);
}
#[test]
fn cubic_to_path_scan_forward() {
compare_path_conversion(font_test_data::CUBIC_GLYF, PathStyle::HarfBuzz);
}
#[test]
fn cff_to_path_scan_backward() {
compare_path_conversion(font_test_data::CANTARELL_VF_TRIMMED, PathStyle::FreeType);
}
#[test]
fn cff_to_path_scan_forward() {
compare_path_conversion(font_test_data::CANTARELL_VF_TRIMMED, PathStyle::HarfBuzz);
}
/// Ensures autohint path conversion matches the base scaler path
/// conversion for all glyphs in the given font with a certain
/// path style.
fn compare_path_conversion(font_data: &[u8], path_style: PathStyle) {
let font = FontRef::new(font_data).unwrap();
let glyph_count = font.maxp().unwrap().num_glyphs();
let glyphs = font.outline_glyphs();
let mut results = Vec::new();
// And all glyphs
for gid in 0..glyph_count {
let glyph = glyphs.get(GlyphId::from(gid)).unwrap();
// Unscaled, unhinted code path
let mut base_svg = SvgPen::default();
let settings = DrawSettings::unhinted(Size::unscaled(), LocationRef::default())
.with_path_style(path_style);
glyph.draw(settings, &mut base_svg).unwrap();
let base_svg = base_svg.to_string();
// Autohinter outline code path
let mut outline = Outline::default();
outline.fill(&glyph, Default::default()).unwrap();
// The to_path method uses the (x, y) coords which aren't filled
// until we scale (and we aren't doing that here) so update
// them with 26.6 values manually
for point in &mut outline.points {
point.x = point.fx << 6;
point.y = point.fy << 6;
}
let mut autohint_svg = SvgPen::default();
outline.to_path(path_style, &mut autohint_svg).unwrap();
let autohint_svg = autohint_svg.to_string();
if base_svg != autohint_svg {
results.push((gid, base_svg, autohint_svg));
}
}
if !results.is_empty() {
let report: String = results
.into_iter()
.map(|(gid, expected, got)| {
format!("[glyph {gid}]\nexpected: {expected}\n got: {got}")
})
.collect::<Vec<_>>()
.join("\n");
panic!("outline to path comparison failed:\n{report}");
}
}
}

View File

@@ -0,0 +1,851 @@
//! Shaping support for autohinting.
use super::style::{GlyphStyle, StyleClass};
use crate::{charmap::Charmap, collections::SmallVec, FontRef, GlyphId, MetadataProvider};
use core::ops::Range;
use raw::{
tables::{
gsub::{
ChainedSequenceContext, Gsub, SequenceContext, SingleSubst, SubstitutionLookupList,
SubstitutionSubtables,
},
layout::{Feature, ScriptTags},
varc::CoverageTable,
},
types::Tag,
ReadError, TableProvider,
};
// To prevent infinite recursion in contextual lookups. Matches HB
// <https://github.com/harfbuzz/harfbuzz/blob/c7ef6a2ed58ae8ec108ee0962bef46f42c73a60c/src/hb-limits.hh#L53>
const MAX_NESTING_DEPTH: usize = 64;
/// Determines the fidelity with which we apply shaping in the
/// autohinter.
///
/// Shaping only affects glyph style classification and the glyphs that
/// are chosen for metrics computations. We keep the `Nominal` mode around
/// to enable validation of internal algorithms against a configuration that
/// is known to match FreeType. The `BestEffort` mode should always be
/// used for actual rendering.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ShaperMode {
/// Characters are mapped to nominal glyph identifiers and layout tables
/// are not used for style coverage.
///
/// This matches FreeType when HarfBuzz support is not enabled.
Nominal,
/// Simple substitutions are applied according to script rules and layout
/// tables are used to extend style coverage beyond the character map.
#[allow(unused)]
BestEffort,
}
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct ShapedGlyph {
pub id: GlyphId,
/// This may be used for computing vertical alignment zones, particularly
/// for glyphs like super/subscripts which might have adjustments in GPOS.
///
/// Note that we don't do the same in the horizontal direction which
/// means that we don't care about the x-offset.
pub y_offset: i32,
}
/// Arbitrarily chosen to cover our max input size plus some extra to account
/// for expansion from multiple substitution tables.
const SHAPED_CLUSTER_INLINE_SIZE: usize = 16;
/// Container for storing the result of shaping a cluster.
///
/// Some of our input "characters" for metrics computations are actually
/// multi-character [grapheme clusters](https://www.unicode.org/reports/tr29/#Grapheme_Cluster_Boundaries)
/// that may expand to multiple glyphs.
pub(crate) type ShapedCluster = SmallVec<ShapedGlyph, SHAPED_CLUSTER_INLINE_SIZE>;
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub(crate) enum ShaperCoverageKind {
/// Shaper coverage that traverses a specific script.
Script,
/// Shaper coverage that also includes the `Dflt` script.
///
/// This is used as a catch all after all styles are processed.
Default,
}
/// Maps characters to glyphs and handles extended style coverage beyond
/// glyphs that are available in the character map.
///
/// Roughly covers the functionality in <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c>.
pub(crate) struct Shaper<'a> {
font: FontRef<'a>,
#[allow(unused)]
mode: ShaperMode,
charmap: Charmap<'a>,
gsub: Option<Gsub<'a>>,
}
impl<'a> Shaper<'a> {
pub fn new(font: &FontRef<'a>, mode: ShaperMode) -> Self {
let charmap = font.charmap();
let gsub = (mode != ShaperMode::Nominal)
.then(|| font.gsub().ok())
.flatten();
Self {
font: font.clone(),
mode,
charmap,
gsub,
}
}
pub fn font(&self) -> &FontRef<'a> {
&self.font
}
pub fn charmap(&self) -> &Charmap<'a> {
&self.charmap
}
pub fn lookup_count(&self) -> u16 {
self.gsub
.as_ref()
.and_then(|gsub| gsub.lookup_list().ok())
.map(|list| list.lookup_count())
.unwrap_or_default()
}
pub fn cluster_shaper(&'a self, style: &StyleClass) -> ClusterShaper<'a> {
if self.mode == ShaperMode::BestEffort {
// For now, only apply substitutions for styles with an associated
// feature
if let Some(feature_tag) = style.feature {
if let Some((lookup_list, feature)) = self.gsub.as_ref().and_then(|gsub| {
let script_list = gsub.script_list().ok()?;
let selected_script =
script_list.select(&ScriptTags::from_unicode(style.script.tag))?;
let script = script_list.get(selected_script.index).ok()?;
let lang_sys = script.default_lang_sys()?.ok()?;
let feature_list = gsub.feature_list().ok()?;
let feature_ix = lang_sys.feature_index_for_tag(&feature_list, feature_tag)?;
let feature = feature_list.get(feature_ix).ok()?.element;
let lookup_list = gsub.lookup_list().ok()?;
Some((lookup_list, feature))
}) {
return ClusterShaper {
shaper: self,
lookup_list: Some(lookup_list),
kind: ClusterShaperKind::SingleFeature(feature),
};
}
}
}
ClusterShaper {
shaper: self,
lookup_list: None,
kind: ClusterShaperKind::Nominal,
}
}
/// Uses layout tables to compute coverage for the given style.
///
/// Returns `true` if any glyph styles were updated for this style.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L99>
pub(crate) fn compute_coverage(
&self,
style: &StyleClass,
coverage_kind: ShaperCoverageKind,
glyph_styles: &mut [GlyphStyle],
visited_set: &mut VisitedLookupSet<'_>,
) -> bool {
let Some(gsub) = self.gsub.as_ref() else {
return false;
};
let (Ok(script_list), Ok(feature_list), Ok(lookup_list)) =
(gsub.script_list(), gsub.feature_list(), gsub.lookup_list())
else {
return false;
};
let mut script_tags: [Option<Tag>; 3] = [None; 3];
for (a, b) in script_tags
.iter_mut()
.zip(ScriptTags::from_unicode(style.script.tag).iter())
{
*a = Some(*b);
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L153>
const DEFAULT_SCRIPT: Tag = Tag::new(b"Dflt");
if coverage_kind == ShaperCoverageKind::Default {
if script_tags[0].is_none() {
script_tags[0] = Some(DEFAULT_SCRIPT);
} else if script_tags[1].is_none() {
script_tags[1] = Some(DEFAULT_SCRIPT);
} else if script_tags[1] != Some(DEFAULT_SCRIPT) {
script_tags[2] = Some(DEFAULT_SCRIPT);
}
} else {
// Script classes contain some non-standard tags used for special
// purposes. We ignore these
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L167>
const NON_STANDARD_TAGS: &[Option<Tag>] = &[
// Khmer symbols
Some(Tag::new(b"Khms")),
// Latin subscript fallbacks
Some(Tag::new(b"Latb")),
// Latin superscript fallbacks
Some(Tag::new(b"Latp")),
];
if NON_STANDARD_TAGS.contains(&script_tags[0]) {
return false;
}
}
// Check each requested script that is available in GSUB
let mut gsub_handler = GsubHandler::new(
&self.charmap,
&lookup_list,
style,
glyph_styles,
visited_set,
);
for script in script_tags.iter().filter_map(|tag| {
tag.and_then(|tag| script_list.index_for_tag(tag))
.and_then(|ix| script_list.script_records().get(ix as usize))
.and_then(|rec| rec.script(script_list.offset_data()).ok())
}) {
// And all language systems for each script
for langsys in script
.lang_sys_records()
.iter()
.filter_map(|rec| rec.lang_sys(script.offset_data()).ok())
.chain(script.default_lang_sys().transpose().ok().flatten())
{
for feature_ix in langsys.feature_indices() {
let Some(feature) = feature_list
.feature_records()
.get(feature_ix.get() as usize)
.and_then(|rec| {
// If our style has a feature tag, we only look at that specific
// feature; otherwise, handle all of them
if style.feature == Some(rec.feature_tag()) || style.feature.is_none() {
rec.feature(feature_list.offset_data()).ok()
} else {
None
}
})
else {
continue;
};
// And now process associated lookups
for index in feature.lookup_list_indices().iter() {
// We only care about errors here for testing
let _ = gsub_handler.process_lookup(index.get());
}
}
}
}
if let Some(range) = gsub_handler.finish() {
// If we get a range then we captured at least some glyphs so
// let's try to assign our current style
let mut result = false;
for glyph_style in &mut glyph_styles[range] {
// We only want to return true here if we actually assign the
// style to avoid computing unnecessary metrics
result |= glyph_style.maybe_assign_gsub_output_style(style);
}
result
} else {
false
}
}
}
pub(crate) struct ClusterShaper<'a> {
shaper: &'a Shaper<'a>,
lookup_list: Option<SubstitutionLookupList<'a>>,
kind: ClusterShaperKind<'a>,
}
impl ClusterShaper<'_> {
pub(crate) fn shape(&mut self, input: &str, output: &mut ShapedCluster) {
// First fill the output cluster with the nominal character
// to glyph id mapping
output.clear();
for ch in input.chars() {
output.push(ShapedGlyph {
id: self.shaper.charmap.map(ch).unwrap_or_default(),
y_offset: 0,
});
}
match self.kind.clone() {
ClusterShaperKind::Nominal => {
// In nominal mode, reject clusters with multiple glyphs
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L639>
if self.shaper.mode == ShaperMode::Nominal && output.len() > 1 {
output.clear();
}
}
ClusterShaperKind::SingleFeature(feature) => {
let mut did_subst = false;
for lookup_ix in feature.lookup_list_indices() {
let mut glyph_ix = 0;
while glyph_ix < output.len() {
did_subst |= self.apply_lookup(lookup_ix.get(), output, glyph_ix, 0);
glyph_ix += 1;
}
}
// Reject clusters that weren't modified by the feature.
// FreeType detects this by shaping twice and comparing gids
// but we just track substitutions
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L528>
if !did_subst {
output.clear();
}
}
}
}
fn apply_lookup(
&self,
lookup_index: u16,
cluster: &mut ShapedCluster,
glyph_ix: usize,
nesting_depth: usize,
) -> bool {
if nesting_depth > MAX_NESTING_DEPTH {
return false;
}
let Some(glyph) = cluster.get_mut(glyph_ix) else {
return false;
};
let Some(subtables) = self
.lookup_list
.as_ref()
.and_then(|list| list.lookups().get(lookup_index as usize).ok())
.and_then(|lookup| lookup.subtables().ok())
else {
return false;
};
match subtables {
// For now, just applying single substitutions because we're
// currently only handling shaping for "feature" styles like
// c2sc (caps to small caps) which are (almost?) always
// single substs
SubstitutionSubtables::Single(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
SingleSubst::Format1(table) => {
let Some(_) = table.coverage().ok().and_then(|cov| cov.get(glyph.id))
else {
continue;
};
let delta = table.delta_glyph_id() as i32;
glyph.id = GlyphId::from((glyph.id.to_u32() as i32 + delta) as u16);
return true;
}
SingleSubst::Format2(table) => {
let Some(cov_ix) =
table.coverage().ok().and_then(|cov| cov.get(glyph.id))
else {
continue;
};
let Some(subst) = table.substitute_glyph_ids().get(cov_ix as usize)
else {
continue;
};
glyph.id = subst.get().into();
return true;
}
}
}
}
SubstitutionSubtables::Multiple(_tables) => {}
SubstitutionSubtables::Ligature(_tables) => {}
SubstitutionSubtables::Alternate(_tables) => {}
SubstitutionSubtables::Contextual(_tables) => {}
SubstitutionSubtables::ChainContextual(_tables) => {}
SubstitutionSubtables::Reverse(_tables) => {}
}
false
}
}
#[derive(Clone)]
enum ClusterShaperKind<'a> {
Nominal,
SingleFeature(Feature<'a>),
}
/// Captures glyphs from the GSUB table that aren't present in cmap.
///
/// FreeType does this in a few phases:
/// 1. Collect all lookups for a given set of scripts and features.
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L174>
/// 2. For each lookup, collect all _output_ glyphs.
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L201>
/// 3. If the style represents a specific feature, make sure at least one of
/// the characters in the associated blue string would be substituted by
/// those lookups. If none would be substituted, then we don't assign the
/// style to any glyphs because we don't have any modified alignment
/// zones.
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afshaper.c#L264>
///
/// We roll these into one pass over the lookups below so that we don't have
/// to allocate a lookup set or iterate them twice. Note that since
/// substitutions are checked for individual characters, we ignore ligatures
/// and contextual lookups (and alternates since they aren't applicable).
struct GsubHandler<'a, 'b> {
charmap: &'a Charmap<'a>,
lookup_list: &'a SubstitutionLookupList<'a>,
style: &'a StyleClass,
glyph_styles: &'a mut [GlyphStyle],
// Set to true when we need to check if any substitutions are available
// for our blue strings. This is the case when style.feature != None
need_blue_substs: bool,
// Keep track of our range of touched gids in the style list
min_gid: usize,
max_gid: usize,
lookup_depth: usize,
visited_set: &'a mut VisitedLookupSet<'b>,
}
impl<'a, 'b> GsubHandler<'a, 'b> {
fn new(
charmap: &'a Charmap<'a>,
lookup_list: &'a SubstitutionLookupList,
style: &'a StyleClass,
glyph_styles: &'a mut [GlyphStyle],
visited_set: &'a mut VisitedLookupSet<'b>,
) -> Self {
let min_gid = glyph_styles.len();
// If we have a feature, then we need to check the blue string to see
// if any substitutions are available. If not, we don't enable this
// style because it won't have any affect on alignment zones
let need_blue_substs = style.feature.is_some();
Self {
charmap,
lookup_list,
style,
glyph_styles,
need_blue_substs,
min_gid,
max_gid: 0,
lookup_depth: 0,
visited_set,
}
}
fn process_lookup(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
// General protection against stack overflows
if self.lookup_depth == MAX_NESTING_DEPTH {
return Err(ProcessLookupError::ExceededMaxDepth);
}
// Skip lookups that have already been processed
if !self.visited_set.insert(lookup_index) {
return Ok(());
}
self.lookup_depth += 1;
// Actually process the lookup
let result = self.process_lookup_inner(lookup_index);
// Out we go again
self.lookup_depth -= 1;
result
}
#[inline(always)]
fn process_lookup_inner(&mut self, lookup_index: u16) -> Result<(), ProcessLookupError> {
let Ok(subtables) = self
.lookup_list
.lookups()
.get(lookup_index as usize)
.and_then(|lookup| lookup.subtables())
else {
return Ok(());
};
match subtables {
SubstitutionSubtables::Single(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
SingleSubst::Format1(table) => {
let Ok(coverage) = table.coverage() else {
continue;
};
let delta = table.delta_glyph_id() as i32;
for gid in coverage.iter() {
self.capture_glyph((gid.to_u32() as i32 + delta) as u16 as u32);
}
// Check input coverage for blue strings if
// required and if we're not under a contextual
// lookup
if self.need_blue_substs && self.lookup_depth == 1 {
self.check_blue_coverage(Ok(coverage));
}
}
SingleSubst::Format2(table) => {
for gid in table.substitute_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
// See above
if self.need_blue_substs && self.lookup_depth == 1 {
self.check_blue_coverage(table.coverage());
}
}
}
}
}
SubstitutionSubtables::Multiple(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for seq in table.sequences().iter().filter_map(|seq| seq.ok()) {
for gid in seq.substitute_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
}
// See above
if self.need_blue_substs && self.lookup_depth == 1 {
self.check_blue_coverage(table.coverage());
}
}
}
SubstitutionSubtables::Ligature(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for set in table.ligature_sets().iter().filter_map(|set| set.ok()) {
for lig in set.ligatures().iter().filter_map(|lig| lig.ok()) {
self.capture_glyph(lig.ligature_glyph().to_u32());
}
}
}
}
SubstitutionSubtables::Alternate(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for set in table.alternate_sets().iter().filter_map(|set| set.ok()) {
for gid in set.alternate_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
}
}
}
SubstitutionSubtables::Contextual(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
SequenceContext::Format1(table) => {
for set in table
.seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in set.seq_rules().iter().filter_map(|rule| rule.ok()) {
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
SequenceContext::Format2(table) => {
for set in table
.class_seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in
set.class_seq_rules().iter().filter_map(|rule| rule.ok())
{
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
SequenceContext::Format3(table) => {
for rec in table.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
}
SubstitutionSubtables::ChainContextual(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
match table {
ChainedSequenceContext::Format1(table) => {
for set in table
.chained_seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in
set.chained_seq_rules().iter().filter_map(|rule| rule.ok())
{
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
ChainedSequenceContext::Format2(table) => {
for set in table
.chained_class_seq_rule_sets()
.iter()
.filter_map(|set| set.transpose().ok().flatten())
{
for rule in set
.chained_class_seq_rules()
.iter()
.filter_map(|rule| rule.ok())
{
for rec in rule.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
ChainedSequenceContext::Format3(table) => {
for rec in table.seq_lookup_records() {
self.process_lookup(rec.lookup_list_index())?;
}
}
}
}
}
SubstitutionSubtables::Reverse(tables) => {
for table in tables.iter().filter_map(|table| table.ok()) {
for gid in table.substitute_glyph_ids() {
self.capture_glyph(gid.get().to_u32());
}
}
}
}
Ok(())
}
/// Finishes processing for this set of GSUB lookups and
/// returns the range of touched glyphs.
fn finish(self) -> Option<Range<usize>> {
self.visited_set.clear();
if self.min_gid > self.max_gid {
// We didn't touch any glyphs
return None;
}
let range = self.min_gid..self.max_gid + 1;
if self.need_blue_substs {
// We didn't find any substitutions for our blue strings so
// we ignore the style. Clear the GSUB marker for any touched
// glyphs
for glyph in &mut self.glyph_styles[range] {
glyph.clear_from_gsub();
}
None
} else {
Some(range)
}
}
/// Checks the given coverage table for any characters in the blue
/// strings associated with our current style.
fn check_blue_coverage(&mut self, coverage: Result<CoverageTable<'a>, ReadError>) {
let Ok(coverage) = coverage else {
return;
};
for (blue_str, _) in self.style.script.blues {
if blue_str
.chars()
.filter_map(|ch| self.charmap.map(ch))
.filter_map(|gid| coverage.get(gid))
.next()
.is_some()
{
// Condition satisfied, so don't check any further subtables
self.need_blue_substs = false;
return;
}
}
}
fn capture_glyph(&mut self, gid: u32) {
let gid = gid as usize;
if let Some(style) = self.glyph_styles.get_mut(gid) {
style.set_from_gsub_output();
self.min_gid = gid.min(self.min_gid);
self.max_gid = gid.max(self.max_gid);
}
}
}
pub(crate) struct VisitedLookupSet<'a>(&'a mut [u8]);
impl<'a> VisitedLookupSet<'a> {
pub fn new(storage: &'a mut [u8]) -> Self {
Self(storage)
}
/// If the given lookup index is not already in the set, adds it and
/// returns `true`. Returns `false` otherwise.
///
/// This follows the behavior of `HashSet::insert`.
fn insert(&mut self, lookup_index: u16) -> bool {
let byte_ix = lookup_index as usize / 8;
let bit_mask = 1 << (lookup_index % 8) as u8;
if let Some(byte) = self.0.get_mut(byte_ix) {
if *byte & bit_mask == 0 {
*byte |= bit_mask;
true
} else {
false
}
} else {
false
}
}
fn clear(&mut self) {
self.0.fill(0);
}
}
#[derive(PartialEq, Debug)]
enum ProcessLookupError {
ExceededMaxDepth,
}
#[cfg(test)]
mod tests {
use super::{super::style, *};
use font_test_data::bebuffer::BeBuffer;
use raw::{FontData, FontRead};
#[test]
fn small_caps_subst() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut cluster = ShapedCluster::new();
cluster_shaper.shape("H", &mut cluster);
assert_eq!(cluster.len(), 1);
// from ttx, gid 8 is small caps "H"
assert_eq!(cluster[0].id, GlyphId::new(8));
}
#[test]
fn small_caps_nominal() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN_C2SC];
let mut cluster_shaper = shaper.cluster_shaper(style);
let mut cluster = ShapedCluster::new();
cluster_shaper.shape("H", &mut cluster);
assert_eq!(cluster.len(), 1);
// from ttx, gid 1 is "H"
assert_eq!(cluster[0].id, GlyphId::new(1));
}
#[test]
fn exceed_max_depth() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
// Build a lookup chain exceeding our max depth
let mut bad_lookup_builder = BadLookupBuilder::default();
for i in 0..MAX_NESTING_DEPTH {
// each lookup calls the next
bad_lookup_builder.lookups.push(i as u16 + 1);
}
let lookup_list_buf = bad_lookup_builder.build();
let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
let mut set_buf = [0u8; 8192];
let mut visited_set = VisitedLookupSet(&mut set_buf);
let mut gsub_handler = GsubHandler::new(
&shaper.charmap,
&lookup_list,
style,
&mut [],
&mut visited_set,
);
assert_eq!(
gsub_handler.process_lookup(0),
Err(ProcessLookupError::ExceededMaxDepth)
);
}
#[test]
fn dont_cycle_forever() {
let font = FontRef::new(font_test_data::NOTOSERIF_AUTOHINT_SHAPING).unwrap();
let shaper = Shaper::new(&font, ShaperMode::BestEffort);
let style = &style::STYLE_CLASSES[style::StyleClass::LATN];
// Build a lookup chain that cycles; 0 calls 1 which calls 0
let mut bad_lookup_builder = BadLookupBuilder::default();
bad_lookup_builder.lookups.push(1);
bad_lookup_builder.lookups.push(0);
let lookup_list_buf = bad_lookup_builder.build();
let lookup_list = SubstitutionLookupList::read(FontData::new(&lookup_list_buf)).unwrap();
let mut set_buf = [0u8; 8192];
let mut visited_set = VisitedLookupSet(&mut set_buf);
let mut gsub_handler = GsubHandler::new(
&shaper.charmap,
&lookup_list,
style,
&mut [],
&mut visited_set,
);
gsub_handler.process_lookup(0).unwrap();
}
#[test]
fn visited_set() {
let count = 2341u16;
let n_bytes = (count as usize).div_ceil(8);
let mut set_buf = vec![0u8; n_bytes];
let mut set = VisitedLookupSet::new(&mut set_buf);
for i in 0..count {
assert!(set.insert(i));
assert!(!set.insert(i));
}
for byte in &set_buf[0..set_buf.len() - 1] {
assert_eq!(*byte, 0xFF);
}
assert_eq!(*set_buf.last().unwrap(), 0b00011111);
}
#[derive(Default)]
struct BadLookupBuilder {
/// Just a list of nested lookup indices for each generated lookup
lookups: Vec<u16>,
}
impl BadLookupBuilder {
fn build(&self) -> Vec<u8> {
// Full byte size of a contextual format 3 lookup with one
// subtable and one nested lookup
const CONTEXT3_FULL_SIZE: usize = 18;
let mut buf = BeBuffer::default();
// LookupList table
// count
buf = buf.push(self.lookups.len() as u16);
// offsets for each lookup
let base_offset = 2 + 2 * self.lookups.len();
for i in 0..self.lookups.len() {
buf = buf.push((base_offset + i * CONTEXT3_FULL_SIZE) as u16);
}
// now the actual lookups
for nested_ix in &self.lookups {
// lookup type: GSUB contextual substitution
buf = buf.push(5u16);
// lookup flag
buf = buf.push(0u16);
// subtable count
buf = buf.push(1u16);
// offset to single subtable (always 8 bytes from start of lookup)
buf = buf.push(8u16);
// start of subtable, format == 3
buf = buf.push(3u16);
// number of glyphs in sequence
buf = buf.push(0u16);
// sequence lookup count
buf = buf.push(1u16);
// (no coverage offsets)
// sequence lookup (sequence index, lookup index)
buf = buf.push(0u16).push(*nested_ix);
}
buf.to_vec()
}
}
}

View File

@@ -0,0 +1,587 @@
//! Styles, scripts and glyph style mapping.
use super::metrics::BlueZones;
use super::shape::{ShaperCoverageKind, VisitedLookupSet};
use alloc::vec::Vec;
use raw::types::{GlyphId, Tag};
/// Defines the script and style associated with a single glyph.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
#[repr(transparent)]
pub(crate) struct GlyphStyle(pub(super) u16);
impl GlyphStyle {
// The following flags roughly correspond to those defined in FreeType
// here: https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L76
// but with different values because we intend to store "meta style"
// information differently.
const STYLE_INDEX_MASK: u16 = 0xFF;
const UNASSIGNED: u16 = Self::STYLE_INDEX_MASK;
// A non-base character, perhaps more commonly referred to as a "mark"
const NON_BASE: u16 = 0x100;
const DIGIT: u16 = 0x200;
// Used as intermediate state to mark when a glyph appears as GSUB output
// for a given script
const FROM_GSUB_OUTPUT: u16 = 0x8000;
pub const fn is_unassigned(self) -> bool {
self.0 & Self::STYLE_INDEX_MASK == Self::UNASSIGNED
}
pub const fn is_non_base(self) -> bool {
self.0 & Self::NON_BASE != 0
}
pub const fn is_digit(self) -> bool {
self.0 & Self::DIGIT != 0
}
pub fn style_class(self) -> Option<&'static StyleClass> {
StyleClass::from_index(self.style_index()?)
}
pub fn style_index(self) -> Option<u16> {
let ix = self.0 & Self::STYLE_INDEX_MASK;
if ix != Self::UNASSIGNED {
Some(ix)
} else {
None
}
}
fn maybe_assign(&mut self, other: Self) {
// FreeType walks the style array in order so earlier styles
// have precedence. Since we walk the cmap and binary search
// on the full range mapping, our styles are mapped in a
// different order. This check allows us to replace a currently
// mapped style if the new style index is lower which matches
// FreeType's behavior.
//
// Note that we keep the extra bits because FreeType allows
// setting the NON_BASE bit to an already mapped style.
if other.0 & Self::STYLE_INDEX_MASK <= self.0 & Self::STYLE_INDEX_MASK {
self.0 = (self.0 & !Self::STYLE_INDEX_MASK) | other.0;
}
}
pub(super) fn set_from_gsub_output(&mut self) {
self.0 |= Self::FROM_GSUB_OUTPUT
}
pub(super) fn clear_from_gsub(&mut self) {
self.0 &= !Self::FROM_GSUB_OUTPUT;
}
/// Assign a style if we've been marked as GSUB output _and_ the
/// we don't currently have an assigned style.
///
/// This also clears the GSUB output bit.
///
/// Returns `true` if this style was applied.
pub(super) fn maybe_assign_gsub_output_style(&mut self, style: &StyleClass) -> bool {
let style_ix = style.index as u16;
if self.0 & Self::FROM_GSUB_OUTPUT != 0 && self.is_unassigned() {
self.clear_from_gsub();
self.0 = (self.0 & !Self::STYLE_INDEX_MASK) | style_ix;
true
} else {
false
}
}
}
impl Default for GlyphStyle {
fn default() -> Self {
Self(Self::UNASSIGNED)
}
}
/// Sentinel for unused styles in [`GlyphStyleMap::metrics_map`].
const UNMAPPED_STYLE: u8 = 0xFF;
/// Maps glyph identifiers to glyph styles.
///
/// Also keeps track of the styles that are actually used so we can allocate
/// an appropriately sized metrics array.
#[derive(Debug)]
pub(crate) struct GlyphStyleMap {
/// List of styles, indexed by glyph id.
styles: Vec<GlyphStyle>,
/// Maps an actual style class index into a compacted index for the
/// metrics table.
///
/// Uses `0xFF` to signify unused styles.
metrics_map: [u8; MAX_STYLES],
/// Number of metrics styles in use.
metrics_count: u8,
}
impl GlyphStyleMap {
/// Computes a new glyph style map for the given glyph count and character
/// map.
///
/// Roughly based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L126>
pub fn new(glyph_count: u32, shaper: &Shaper) -> Self {
let lookup_count = shaper.lookup_count() as usize;
if lookup_count > 0 {
// If we're processing lookups, allocate some temporary memory to
// store the visited set
let lookup_set_byte_size = lookup_count.div_ceil(8);
super::super::memory::with_temporary_memory(lookup_set_byte_size, |bytes| {
Self::new_inner(glyph_count, shaper, VisitedLookupSet::new(bytes))
})
} else {
Self::new_inner(glyph_count, shaper, VisitedLookupSet::new(&mut []))
}
}
fn new_inner(glyph_count: u32, shaper: &Shaper, mut visited_set: VisitedLookupSet) -> Self {
let mut map = Self {
styles: vec![GlyphStyle::default(); glyph_count as usize],
metrics_map: [UNMAPPED_STYLE; MAX_STYLES],
metrics_count: 0,
};
// Step 1: compute styles for glyphs covered by OpenType features
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L233>
for style in super::style::STYLE_CLASSES {
if style.feature.is_some()
&& shaper.compute_coverage(
style,
ShaperCoverageKind::Script,
&mut map.styles,
&mut visited_set,
)
{
map.use_style(style.index);
}
}
// Step 2: compute styles for glyphs contained in the cmap
// cmap entries are sorted so we keep track of the most recent range to
// avoid a binary search per character
let mut last_range: Option<(usize, StyleRange)> = None;
for (ch, gid) in shaper.charmap().mappings() {
let Some(style) = map.styles.get_mut(gid.to_u32() as usize) else {
continue;
};
// Charmaps enumerate in order so we're likely to encounter at least
// a few codepoints in the same range.
if let Some(last) = last_range {
if last.1.contains(ch) {
style.maybe_assign(last.1.style);
continue;
}
}
let ix = match STYLE_RANGES.binary_search_by(|x| x.first.cmp(&ch)) {
Ok(i) => i,
Err(i) => i.saturating_sub(1),
};
let Some(range) = STYLE_RANGES.get(ix).copied() else {
continue;
};
if range.contains(ch) {
style.maybe_assign(range.style);
if let Some(style_ix) = range.style.style_index() {
map.use_style(style_ix as usize);
}
last_range = Some((ix, range));
}
}
// Step 3a: compute script based coverage
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L239>
for style in super::style::STYLE_CLASSES {
if style.feature.is_none()
&& shaper.compute_coverage(
style,
ShaperCoverageKind::Script,
&mut map.styles,
&mut visited_set,
)
{
map.use_style(style.index);
}
}
// Step 3b: compute coverage for "default" script which is always set
// to Latin in FreeType
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L248>
let default_style = &STYLE_CLASSES[StyleClass::LATN];
if shaper.compute_coverage(
default_style,
ShaperCoverageKind::Default,
&mut map.styles,
&mut visited_set,
) {
map.use_style(default_style.index);
}
// Step 4: Assign a default to all remaining glyphs
// For some reason, FreeType uses Hani as a default fallback style so
// let's do the same.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.h#L69>
let mut need_hani = false;
for style in map.styles.iter_mut() {
if style.is_unassigned() {
style.0 &= !GlyphStyle::STYLE_INDEX_MASK;
style.0 |= StyleClass::HANI as u16;
need_hani = true;
}
}
if need_hani {
map.use_style(StyleClass::HANI);
}
// Step 5: Mark ASCII digits
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afglobal.c#L251>
for digit_char in '0'..='9' {
if let Some(style) = shaper
.charmap()
.map(digit_char)
.and_then(|gid| map.styles.get_mut(gid.to_u32() as usize))
{
style.0 |= GlyphStyle::DIGIT;
}
}
map
}
pub fn style(&self, glyph_id: GlyphId) -> Option<GlyphStyle> {
self.styles.get(glyph_id.to_u32() as usize).copied()
}
/// Returns a compacted metrics index for the given glyph style.
pub fn metrics_index(&self, style: GlyphStyle) -> Option<usize> {
let ix = style.style_index()? as usize;
let metrics_ix = *self.metrics_map.get(ix)? as usize;
if metrics_ix != UNMAPPED_STYLE as usize {
Some(metrics_ix)
} else {
None
}
}
/// Returns the required size of the compacted metrics array.
pub fn metrics_count(&self) -> usize {
self.metrics_count as usize
}
/// Returns an ordered iterator yielding each style class referenced by
/// this map.
pub fn metrics_styles(&self) -> impl Iterator<Item = &'static StyleClass> + '_ {
// Need to build a reverse map so that these are properly ordered
let mut reverse_map = [UNMAPPED_STYLE; MAX_STYLES];
for (ix, &entry) in self.metrics_map.iter().enumerate() {
if entry != UNMAPPED_STYLE {
reverse_map[entry as usize] = ix as u8;
}
}
reverse_map
.into_iter()
.enumerate()
.filter_map(move |(mapped, style_ix)| {
if mapped == UNMAPPED_STYLE as usize {
None
} else {
STYLE_CLASSES.get(style_ix as usize)
}
})
}
fn use_style(&mut self, style_ix: usize) {
let mapped = &mut self.metrics_map[style_ix];
if *mapped == UNMAPPED_STYLE {
// This the first time we've seen this style so record
// it in the metrics map
*mapped = self.metrics_count;
self.metrics_count += 1;
}
}
}
impl Default for GlyphStyleMap {
fn default() -> Self {
Self {
styles: Default::default(),
metrics_map: [UNMAPPED_STYLE; MAX_STYLES],
metrics_count: 0,
}
}
}
/// Determines which algorithms the autohinter will use while generating
/// metrics and processing a glyph outline.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub(crate) enum ScriptGroup {
/// All scripts that are not CJK or Indic.
///
/// FreeType calls this Latin.
#[default]
Default,
Cjk,
Indic,
}
/// Defines the basic properties for each script supported by the
/// autohinter.
#[derive(Clone, Debug)]
pub(crate) struct ScriptClass {
#[allow(unused)]
pub name: &'static str,
/// Group that defines how glyphs belonging to this script are hinted.
pub group: ScriptGroup,
/// Unicode tag for the script.
#[allow(unused)]
pub tag: Tag,
/// True if outline edges are processed top to bottom.
pub hint_top_to_bottom: bool,
/// Characters used to define standard width and height of stems.
pub std_chars: &'static str,
/// "Blue" characters used to define alignment zones.
pub blues: &'static [(&'static str, BlueZones)],
}
/// Defines the basic properties for each style supported by the
/// autohinter.
///
/// There's mostly a 1:1 correspondence between styles and scripts except
/// in the cases where style coverage is determined by OpenType feature
/// coverage.
#[derive(Clone, Debug)]
pub(crate) struct StyleClass {
#[allow(unused)]
pub name: &'static str,
/// Index of self in the STYLE_CLASSES array.
pub index: usize,
/// Associated Unicode script.
pub script: &'static ScriptClass,
/// OpenType feature tag for styles that derive coverage from layout
/// tables.
#[allow(unused)]
pub feature: Option<Tag>,
}
impl StyleClass {
pub(crate) fn from_index(index: u16) -> Option<&'static StyleClass> {
STYLE_CLASSES.get(index as usize)
}
}
/// Associates a basic glyph style with a range of codepoints.
#[derive(Copy, Clone, Debug)]
pub(super) struct StyleRange {
pub first: u32,
pub last: u32,
pub style: GlyphStyle,
}
impl StyleRange {
pub fn contains(&self, ch: u32) -> bool {
(self.first..=self.last).contains(&ch)
}
}
// The following are helpers for generated code.
const fn base_range(first: u32, last: u32, style_index: u16) -> StyleRange {
StyleRange {
first,
last,
style: GlyphStyle(style_index),
}
}
const fn non_base_range(first: u32, last: u32, style_index: u16) -> StyleRange {
StyleRange {
first,
last,
style: GlyphStyle(style_index | GlyphStyle::NON_BASE),
}
}
const MAX_STYLES: usize = STYLE_CLASSES.len();
use super::shape::Shaper;
include!("../../../generated/generated_autohint_styles.rs");
#[cfg(test)]
mod tests {
use super::{super::shape::ShaperMode, *};
use crate::{raw::TableProvider, FontRef, MetadataProvider};
/// Ensure that style mapping accurately applies the DIGIT bit to
/// ASCII digit glyphs.
#[test]
fn capture_digit_styles() {
let font = FontRef::new(font_test_data::AHEM).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
let charmap = font.charmap();
let mut digit_count = 0;
for (ch, gid) in charmap.mappings() {
let style = style_map.style(gid).unwrap();
let is_char_digit = char::from_u32(ch).unwrap().is_ascii_digit();
assert_eq!(style.is_digit(), is_char_digit);
digit_count += is_char_digit as u32;
}
// This font has all 10 ASCII digits
assert_eq!(digit_count, 10);
}
#[test]
fn glyph_styles() {
// generated by printf debugging in FreeType
// (gid, Option<(script_name, is_non_base_char)>)
// where "is_non_base_char" more common means "is_mark"
let expected = &[
(0, Some(("CJKV ideographs", false))),
(1, Some(("Latin", true))),
(2, Some(("Armenian", true))),
(3, Some(("Hebrew", true))),
(4, Some(("Arabic", false))),
(5, Some(("Arabic", false))),
(6, Some(("Arabic", true))),
(7, Some(("Devanagari", true))),
(8, Some(("Devanagari", false))),
(9, Some(("Bengali", true))),
(10, Some(("Bengali", false))),
(11, Some(("Gurmukhi", true))),
(12, Some(("Gurmukhi", false))),
(13, Some(("Gujarati", true))),
(14, Some(("Gujarati", true))),
(15, Some(("Oriya", true))),
(16, Some(("Oriya", false))),
(17, Some(("Tamil", true))),
(18, Some(("Tamil", false))),
(19, Some(("Telugu", true))),
(20, Some(("Telugu", false))),
(21, Some(("Kannada", true))),
(22, Some(("Kannada", false))),
(23, Some(("Malayalam", true))),
(24, Some(("Malayalam", false))),
(25, Some(("Sinhala", true))),
(26, Some(("Sinhala", false))),
(27, Some(("Thai", true))),
(28, Some(("Thai", false))),
(29, Some(("Lao", true))),
(30, Some(("Lao", false))),
(31, Some(("Tibetan", true))),
(32, Some(("Tibetan", false))),
(33, Some(("Myanmar", true))),
(34, Some(("Ethiopic", true))),
(35, Some(("Buhid", true))),
(36, Some(("Buhid", false))),
(37, Some(("Khmer", true))),
(38, Some(("Khmer", false))),
(39, Some(("Mongolian", true))),
(40, Some(("Canadian Syllabics", false))),
(41, Some(("Limbu", true))),
(42, Some(("Limbu", false))),
(43, Some(("Khmer Symbols", false))),
(44, Some(("Sundanese", true))),
(45, Some(("Ol Chiki", false))),
(46, Some(("Georgian (Mkhedruli)", false))),
(47, Some(("Sundanese", false))),
(48, Some(("Latin Superscript Fallback", false))),
(49, Some(("Latin", true))),
(50, Some(("Greek", true))),
(51, Some(("Greek", false))),
(52, Some(("Latin Subscript Fallback", false))),
(53, Some(("Coptic", true))),
(54, Some(("Coptic", false))),
(55, Some(("Georgian (Khutsuri)", false))),
(56, Some(("Tifinagh", false))),
(57, Some(("Ethiopic", false))),
(58, Some(("Cyrillic", true))),
(59, Some(("CJKV ideographs", true))),
(60, Some(("CJKV ideographs", false))),
(61, Some(("Lisu", false))),
(62, Some(("Vai", false))),
(63, Some(("Cyrillic", true))),
(64, Some(("Bamum", true))),
(65, Some(("Syloti Nagri", true))),
(66, Some(("Syloti Nagri", false))),
(67, Some(("Saurashtra", true))),
(68, Some(("Saurashtra", false))),
(69, Some(("Kayah Li", true))),
(70, Some(("Kayah Li", false))),
(71, Some(("Myanmar", false))),
(72, Some(("Tai Viet", true))),
(73, Some(("Tai Viet", false))),
(74, Some(("Cherokee", false))),
(75, Some(("Armenian", false))),
(76, Some(("Hebrew", false))),
(77, Some(("Arabic", false))),
(78, Some(("Carian", false))),
(79, Some(("Gothic", false))),
(80, Some(("Deseret", false))),
(81, Some(("Shavian", false))),
(82, Some(("Osmanya", false))),
(83, Some(("Osage", false))),
(84, Some(("Cypriot", false))),
(85, Some(("Avestan", true))),
(86, Some(("Avestan", true))),
(87, Some(("Old Turkic", false))),
(88, Some(("Hanifi Rohingya", false))),
(89, Some(("Chakma", true))),
(90, Some(("Chakma", false))),
(91, Some(("Mongolian", false))),
(92, Some(("CJKV ideographs", false))),
(93, Some(("Medefaidrin", false))),
(94, Some(("Glagolitic", true))),
(95, Some(("Glagolitic", true))),
(96, Some(("Adlam", true))),
(97, Some(("Adlam", false))),
];
check_styles(font_test_data::AUTOHINT_CMAP, ShaperMode::Nominal, expected);
}
#[test]
fn shaped_glyph_styles() {
// generated by printf debugging in FreeType
// (gid, Option<(script_name, is_non_base_char)>)
// where "is_non_base_char" more common means "is_mark"
let expected = &[
(0, Some(("CJKV ideographs", false))),
(1, Some(("Latin", false))),
(2, Some(("Latin", false))),
(3, Some(("Latin", false))),
(4, Some(("Latin", false))),
// Note: ligatures starting with 'f' are assigned the Cyrillic
// script which matches FreeType
(5, Some(("Cyrillic", false))),
(6, Some(("Cyrillic", false))),
(7, Some(("Cyrillic", false))),
// Capture the Latin c2sc feature
(8, Some(("Latin small capitals from capitals", false))),
];
check_styles(
font_test_data::NOTOSERIF_AUTOHINT_SHAPING,
ShaperMode::BestEffort,
expected,
);
}
fn check_styles(font_data: &[u8], mode: ShaperMode, expected: &[(u32, Option<(&str, bool)>)]) {
let font = FontRef::new(font_data).unwrap();
let shaper = Shaper::new(&font, mode);
let num_glyphs = font.maxp().unwrap().num_glyphs() as u32;
let style_map = GlyphStyleMap::new(num_glyphs, &shaper);
let results = style_map
.styles
.iter()
.enumerate()
.map(|(gid, style)| {
(
gid as u32,
style
.style_class()
.map(|style_class| (style_class.name, style.is_non_base())),
)
})
.collect::<Vec<_>>();
for (i, result) in results.iter().enumerate() {
assert_eq!(result, &expected[i]);
}
// Ensure each style has a remapped metrics index
for style in &style_map.styles {
style_map.metrics_index(*style).unwrap();
}
}
}

View File

@@ -0,0 +1,823 @@
//! Edge detection.
//!
//! Edges are sets of segments that all lie within a threshold based on
//! stem widths.
//!
//! Here we compute edges from the segment list, assign properties (round,
//! serif, links) and then associate them with blue zones.
use super::{
super::{
metrics::{fixed_div, fixed_mul, Scale, ScaledAxisMetrics, ScaledBlue, UnscaledBlue},
outline::Direction,
style::ScriptGroup,
},
Axis, Edge, Segment,
};
/// Links segments to edges, using feature analysis for selection.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2128>
pub(crate) fn compute_edges(
axis: &mut Axis,
metrics: &ScaledAxisMetrics,
top_to_bottom_hinting: bool,
y_scale: i32,
group: ScriptGroup,
) {
axis.edges.clear();
let scale = metrics.scale;
// This is always passed as 0 in functions that take hinting direction
// in CJK
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1114>
let top_to_bottom_hinting = if axis.dim == Axis::HORIZONTAL || group != ScriptGroup::Default {
false
} else {
top_to_bottom_hinting
};
// Ignore horizontal segments less than 1 pixel in length
let segment_length_threshold = if axis.dim == Axis::HORIZONTAL {
fixed_div(64, y_scale)
} else {
0
};
// Also ignore segments with a width delta larger than 0.5 pixels
let segment_width_threshold = fixed_div(32, scale);
// Ensure that edge distance threshold is less than or equal to
// 0.25 pixels
let initial_threshold = metrics.width_metrics.edge_distance_threshold;
const EDGE_DISTANCE_THRESHOLD_MAX: i32 = 64 / 4;
let edge_distance_threshold = if group == ScriptGroup::Default {
fixed_div(
fixed_mul(initial_threshold, scale).min(EDGE_DISTANCE_THRESHOLD_MAX),
scale,
)
} else {
// CJK uses a slightly different computation here
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1043>
let threshold = fixed_mul(initial_threshold, scale);
if threshold > EDGE_DISTANCE_THRESHOLD_MAX {
fixed_div(EDGE_DISTANCE_THRESHOLD_MAX, scale)
} else {
initial_threshold
}
};
// Now build the sorted table of edges by looping over all segments
// to find a matching edge, adding a new one if not found.
// We can't iterate segments because we make mutable calls on `axis`
// below which causes overlapping borrows
for segment_ix in 0..axis.segments.len() {
let segment = &axis.segments[segment_ix];
if group == ScriptGroup::Default {
// Ignore segments that are too short, too wide or direction-less
if (segment.height as i32) < segment_length_threshold
|| (segment.delta as i32 > segment_width_threshold)
|| segment.dir == Direction::None
{
continue;
}
// Ignore serif edges that are smaller than 1.5 pixels
if segment.serif_ix.is_some()
&& (2 * segment.height as i32) < (3 * segment_length_threshold)
{
continue;
}
}
// Look for a corresponding edge for this segment
let mut best_dist = i32::MAX;
let mut best_edge_ix = None;
for edge_ix in 0..axis.edges.len() {
let edge = &axis.edges[edge_ix];
let dist = (segment.pos as i32 - edge.fpos as i32).abs();
if dist < edge_distance_threshold && edge.dir == segment.dir && dist < best_dist {
if group == ScriptGroup::Default {
best_edge_ix = Some(edge_ix);
break;
}
// For CJK, we add some additional checks
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1073>
if let Some(link) = segment.link(&axis.segments).copied() {
// Check whether all linked segments of the candidate edge
// can make a single edge
let first_ix = edge.first_ix as usize;
let mut seg1 = &axis.segments[first_ix];
let mut dist2 = 0;
loop {
if let Some(link1) = seg1.link(&axis.segments).copied() {
dist2 = (link.pos as i32 - link1.pos as i32).abs();
if dist2 >= edge_distance_threshold {
break;
}
}
if seg1.edge_next_ix == Some(first_ix as u16) {
break;
}
if let Some(next) = seg1.next_in_edge(&axis.segments) {
seg1 = next;
} else {
break;
}
}
if dist2 >= edge_distance_threshold {
continue;
}
}
best_dist = dist;
best_edge_ix = Some(edge_ix);
}
}
if let Some(edge_ix) = best_edge_ix {
axis.append_segment_to_edge(segment_ix, edge_ix);
} else {
// We couldn't find an edge, so add a new one for this segment
let opos = fixed_mul(segment.pos as i32, scale);
let edge = Edge {
fpos: segment.pos,
opos,
pos: opos,
dir: segment.dir,
first_ix: segment_ix as u16,
last_ix: segment_ix as u16,
..Default::default()
};
axis.insert_edge(edge, top_to_bottom_hinting);
axis.segments[segment_ix].edge_next_ix = Some(segment_ix as u16);
}
}
if group == ScriptGroup::Default {
// Loop again to find single point segments without a direction and
// associate them with an existing edge if possible
for segment_ix in 0..axis.segments.len() {
let segment = &axis.segments[segment_ix];
if segment.dir != Direction::None {
continue;
}
// Try to find an edge that coincides with this segment within the
// threshold
if let Some(edge_ix) = axis
.edges
.iter()
.enumerate()
.filter_map(|(ix, edge)| {
((segment.pos as i32 - edge.fpos as i32).abs() < edge_distance_threshold)
.then_some(ix)
})
.next()
{
// We found an edge, link everything up
axis.append_segment_to_edge(segment_ix, edge_ix);
}
}
}
link_segments_to_edges(axis);
compute_edge_properties(axis);
}
/// Edges get reordered as they're built so we need to assign edge indices to
/// segments in a second pass.
fn link_segments_to_edges(axis: &mut Axis) {
let segments = axis.segments.as_mut_slice();
for edge_ix in 0..axis.edges.len() {
let edge = &axis.edges[edge_ix];
let mut ix = edge.first_ix as usize;
let last_ix = edge.last_ix as usize;
loop {
let segment = &mut segments[ix];
segment.edge_ix = Some(edge_ix as u16);
if ix == last_ix {
break;
}
ix = segment
.edge_next_ix
.map(|ix| ix as usize)
.unwrap_or(last_ix);
}
}
}
/// Compute the edge properties based on the series of segments that make
/// up the edge.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2339>
fn compute_edge_properties(axis: &mut Axis) {
let edges = axis.edges.as_mut_slice();
let segments = axis.segments.as_slice();
for edge_ix in 0..edges.len() {
let mut roundness = 0;
let mut straightness = 0;
let edge = edges[edge_ix];
let mut segment_ix = edge.first_ix as usize;
let last_segment_ix = edge.last_ix as usize;
loop {
// This loop can modify the current edge, so make sure we
// reload it here
let edge = edges[edge_ix];
let segment = &segments[segment_ix];
let next_segment_ix = segment.edge_next_ix;
// Check roundness
if segment.flags & Segment::ROUND != 0 {
roundness += 1;
} else {
straightness += 1;
}
// Check for serifs
let is_serif = if let Some(serif_ix) = segment.serif_ix {
let serif = &segments[serif_ix as usize];
serif.edge_ix.is_some() && serif.edge_ix != Some(edge_ix as u16)
} else {
false
};
// Check for links
if is_serif
|| (segment.link_ix.is_some()
&& segments[segment.link_ix.unwrap() as usize]
.edge_ix
.is_some())
{
let (edge2_ix, segment2_ix) = if is_serif {
(edge.serif_ix, segment.serif_ix)
} else {
(edge.link_ix, segment.link_ix)
};
let edge2_ix = if let (Some(edge2_ix), Some(segment2_ix)) = (edge2_ix, segment2_ix)
{
let edge2 = &edges[edge2_ix as usize];
let edge_delta = (edge.fpos as i32 - edge2.fpos as i32).abs();
let segment2 = &segments[segment2_ix as usize];
let segment_delta = (segment.pos as i32 - segment2.pos as i32).abs();
if segment_delta < edge_delta {
segment2.edge_ix
} else {
Some(edge2_ix)
}
} else if let Some(segment2_ix) = segment2_ix {
segments[segment2_ix as usize].edge_ix
} else {
edge2_ix
};
if is_serif {
edges[edge_ix].serif_ix = edge2_ix;
edges[edge2_ix.unwrap() as usize].flags |= Edge::SERIF;
} else {
edges[edge_ix].link_ix = edge2_ix;
}
}
if segment_ix == last_segment_ix {
break;
}
segment_ix = next_segment_ix
.map(|ix| ix as usize)
.unwrap_or(last_segment_ix);
}
let edge = &mut edges[edge_ix];
edge.flags = Edge::NORMAL;
if roundness > 0 && roundness >= straightness {
edge.flags |= Edge::ROUND;
}
// Drop serifs for linked edges
if edge.serif_ix.is_some() && edge.link_ix.is_some() {
edge.serif_ix = None;
}
}
}
/// Compute all edges which lie within blue zones.
///
/// For Latin, this is only done for the vertical axis.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2503>
pub(crate) fn compute_blue_edges(
axis: &mut Axis,
scale: &Scale,
unscaled_blues: &[UnscaledBlue],
blues: &[ScaledBlue],
group: ScriptGroup,
) {
// For the default script group, don't compute blues in the horizontal
// direction
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L3572>
if axis.dim != Axis::VERTICAL && group == ScriptGroup::Default {
return;
}
let axis_scale = if axis.dim == Axis::HORIZONTAL {
scale.x_scale
} else {
scale.y_scale
};
// Initial threshold
let initial_best_dest = fixed_mul(scale.units_per_em / 40, axis_scale).min(64 / 2);
for edge in &mut axis.edges {
let mut best_blue = None;
let mut best_is_neutral = false;
// Initial threshold as a fraction of em size with a max distance
// of 0.5 pixels
let mut best_dist = initial_best_dest;
for (unscaled_blue, blue) in unscaled_blues.iter().zip(blues) {
// Ignore inactive blue zones
if !blue.is_active {
continue;
}
let is_top = blue.zones.is_top_like();
let is_neutral = blue.zones.is_neutral();
let is_major_dir = edge.dir == axis.major_dir;
// Both directions are handled for neutral blues
if is_top ^ is_major_dir || is_neutral {
// Compare to reference position
let (ref_pos, matching_blue) = if group == ScriptGroup::Default {
(unscaled_blue.position, blue.position)
} else {
// For CJK, we take the blue with the smallest delta
// from the edge
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afcjk.c#L1356>
if (edge.fpos as i32 - unscaled_blue.position).abs()
> (edge.fpos as i32 - unscaled_blue.overshoot).abs()
{
(unscaled_blue.overshoot, blue.overshoot)
} else {
(unscaled_blue.position, blue.position)
}
};
let dist = fixed_mul((edge.fpos as i32 - ref_pos).abs(), axis_scale);
if dist < best_dist {
best_dist = dist;
best_blue = Some(matching_blue);
best_is_neutral = is_neutral;
}
if group == ScriptGroup::Default {
// Now compare to overshoot position for the default script
// group
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L2579>
if edge.flags & Edge::ROUND != 0 && dist != 0 && !is_neutral {
let is_under_ref = (edge.fpos as i32) < unscaled_blue.position;
if is_top ^ is_under_ref {
let dist = fixed_mul(
(edge.fpos as i32 - unscaled_blue.overshoot).abs(),
axis_scale,
);
if dist < best_dist {
best_dist = dist;
best_blue = Some(blue.overshoot);
best_is_neutral = is_neutral;
}
}
}
}
}
}
if let Some(best_blue) = best_blue {
edge.blue_edge = Some(best_blue);
if best_is_neutral {
edge.flags |= Edge::NEUTRAL;
}
}
}
}
#[cfg(test)]
mod tests {
use super::{
super::super::{
metrics::{self, ScaledWidth},
outline::Outline,
shape::{Shaper, ShaperMode},
style,
},
super::segments,
*,
};
use crate::{attribute::Style, MetadataProvider};
use raw::{types::GlyphId, FontRef, TableProvider};
#[test]
fn edges_default() {
let expected_h_edges = [
Edge {
fpos: 15,
opos: 15,
pos: 15,
flags: Edge::ROUND,
dir: Direction::Up,
blue_edge: None,
link_ix: Some(3),
serif_ix: None,
scale: 0,
first_ix: 1,
last_ix: 1,
},
Edge {
fpos: 123,
opos: 126,
pos: 126,
flags: 0,
dir: Direction::Up,
blue_edge: None,
link_ix: Some(2),
serif_ix: None,
scale: 0,
first_ix: 0,
last_ix: 0,
},
Edge {
fpos: 186,
opos: 190,
pos: 190,
flags: 0,
dir: Direction::Down,
blue_edge: None,
link_ix: Some(1),
serif_ix: None,
scale: 0,
first_ix: 4,
last_ix: 4,
},
Edge {
fpos: 205,
opos: 210,
pos: 210,
flags: Edge::ROUND,
dir: Direction::Down,
blue_edge: None,
link_ix: Some(0),
serif_ix: None,
scale: 0,
first_ix: 3,
last_ix: 3,
},
];
let expected_v_edges = [
Edge {
fpos: -240,
opos: -246,
pos: -246,
flags: 0,
dir: Direction::Left,
blue_edge: Some(ScaledWidth {
scaled: -246,
fitted: -256,
}),
link_ix: None,
serif_ix: Some(1),
scale: 0,
first_ix: 3,
last_ix: 3,
},
Edge {
fpos: 481,
opos: 493,
pos: 493,
flags: 0,
dir: Direction::Left,
blue_edge: None,
link_ix: Some(2),
serif_ix: None,
scale: 0,
first_ix: 0,
last_ix: 0,
},
Edge {
fpos: 592,
opos: 606,
pos: 606,
flags: Edge::ROUND | Edge::SERIF,
dir: Direction::Right,
blue_edge: Some(ScaledWidth {
scaled: 606,
fitted: 576,
}),
link_ix: Some(1),
serif_ix: None,
scale: 0,
first_ix: 2,
last_ix: 2,
},
Edge {
fpos: 647,
opos: 663,
pos: 663,
flags: 0,
dir: Direction::Right,
blue_edge: None,
link_ix: None,
serif_ix: Some(2),
scale: 0,
first_ix: 1,
last_ix: 1,
},
];
check_edges(
font_test_data::NOTOSERIFHEBREW_AUTOHINT_METRICS,
GlyphId::new(9),
style::StyleClass::HEBR,
&expected_h_edges,
&expected_v_edges,
);
}
#[test]
fn edges_cjk() {
let expected_h_edges = [
Edge {
fpos: 138,
opos: 141,
pos: 141,
flags: 0,
dir: Direction::Up,
blue_edge: None,
link_ix: Some(1),
serif_ix: None,
scale: 0,
first_ix: 8,
last_ix: 8,
},
Edge {
fpos: 201,
opos: 206,
pos: 206,
flags: 0,
dir: Direction::Down,
blue_edge: None,
link_ix: Some(0),
serif_ix: None,
scale: 0,
first_ix: 7,
last_ix: 7,
},
Edge {
fpos: 458,
opos: 469,
pos: 469,
flags: 0,
dir: Direction::Down,
blue_edge: None,
link_ix: None,
serif_ix: None,
scale: 0,
first_ix: 2,
last_ix: 2,
},
Edge {
fpos: 569,
opos: 583,
pos: 583,
flags: 0,
dir: Direction::Down,
blue_edge: None,
link_ix: None,
serif_ix: None,
scale: 0,
first_ix: 6,
last_ix: 6,
},
Edge {
fpos: 670,
opos: 686,
pos: 686,
flags: 0,
dir: Direction::Up,
blue_edge: None,
link_ix: Some(6),
serif_ix: None,
scale: 0,
first_ix: 1,
last_ix: 1,
},
Edge {
fpos: 693,
opos: 710,
pos: 710,
flags: 0,
dir: Direction::Up,
blue_edge: None,
link_ix: None,
serif_ix: Some(7),
scale: 0,
first_ix: 4,
last_ix: 4,
},
Edge {
fpos: 731,
opos: 749,
pos: 749,
flags: 0,
dir: Direction::Down,
blue_edge: None,
link_ix: Some(4),
serif_ix: None,
scale: 0,
first_ix: 0,
last_ix: 0,
},
Edge {
fpos: 849,
opos: 869,
pos: 869,
flags: 0,
dir: Direction::Up,
blue_edge: None,
link_ix: Some(8),
serif_ix: None,
scale: 0,
first_ix: 5,
last_ix: 5,
},
Edge {
fpos: 911,
opos: 933,
pos: 933,
flags: 0,
dir: Direction::Down,
blue_edge: None,
link_ix: Some(7),
serif_ix: None,
scale: 0,
first_ix: 3,
last_ix: 3,
},
];
let expected_v_edges = [
Edge {
fpos: -78,
opos: -80,
pos: -80,
flags: Edge::ROUND,
dir: Direction::Left,
blue_edge: Some(ScaledWidth {
scaled: -80,
fitted: -64,
}),
link_ix: None,
serif_ix: None,
scale: 0,
first_ix: 8,
last_ix: 8,
},
Edge {
fpos: 3,
opos: 3,
pos: 3,
flags: Edge::ROUND,
dir: Direction::Right,
blue_edge: None,
link_ix: None,
serif_ix: None,
scale: 0,
first_ix: 4,
last_ix: 4,
},
Edge {
fpos: 133,
opos: 136,
pos: 136,
flags: Edge::ROUND,
dir: Direction::Left,
blue_edge: None,
link_ix: None,
serif_ix: None,
scale: 0,
first_ix: 2,
last_ix: 2,
},
Edge {
fpos: 547,
opos: 560,
pos: 560,
flags: 0,
dir: Direction::Left,
blue_edge: None,
link_ix: None,
serif_ix: Some(5),
scale: 0,
first_ix: 6,
last_ix: 6,
},
Edge {
fpos: 576,
opos: 590,
pos: 590,
flags: 0,
dir: Direction::Right,
blue_edge: None,
link_ix: Some(5),
serif_ix: None,
scale: 0,
first_ix: 5,
last_ix: 5,
},
Edge {
fpos: 576,
opos: 590,
pos: 590,
flags: 0,
dir: Direction::Left,
blue_edge: None,
link_ix: Some(4),
serif_ix: None,
scale: 0,
first_ix: 7,
last_ix: 7,
},
Edge {
fpos: 729,
opos: 746,
pos: 746,
flags: 0,
dir: Direction::Left,
blue_edge: None,
link_ix: Some(7),
serif_ix: None,
scale: 0,
first_ix: 1,
last_ix: 1,
},
Edge {
fpos: 758,
opos: 776,
pos: 776,
flags: 0,
dir: Direction::Right,
blue_edge: None,
link_ix: Some(6),
serif_ix: None,
scale: 0,
first_ix: 0,
last_ix: 3,
},
Edge {
fpos: 788,
opos: 807,
pos: 807,
flags: Edge::ROUND,
dir: Direction::Left,
blue_edge: None,
link_ix: None,
serif_ix: None,
scale: 0,
first_ix: 9,
last_ix: 9,
},
];
check_edges(
font_test_data::NOTOSERIFTC_AUTOHINT_METRICS,
GlyphId::new(9),
style::StyleClass::HANI,
&expected_h_edges,
&expected_v_edges,
);
}
fn check_edges(
font_data: &[u8],
glyph_id: GlyphId,
style_class: usize,
expected_h_edges: &[Edge],
expected_v_edges: &[Edge],
) {
let font = FontRef::new(font_data).unwrap();
let shaper = Shaper::new(&font, ShaperMode::Nominal);
let class = &style::STYLE_CLASSES[style_class];
let unscaled_metrics =
metrics::compute_unscaled_style_metrics(&shaper, Default::default(), class);
let scale = metrics::Scale::new(
16.0,
font.head().unwrap().units_per_em() as i32,
Style::Normal,
Default::default(),
class.script.group,
);
let scaled_metrics = metrics::scale_style_metrics(&unscaled_metrics, scale);
let glyphs = font.outline_glyphs();
let glyph = glyphs.get(glyph_id).unwrap();
let mut outline = Outline::default();
outline.fill(&glyph, Default::default()).unwrap();
let mut axes = [
Axis::new(Axis::HORIZONTAL, outline.orientation),
Axis::new(Axis::VERTICAL, outline.orientation),
];
for (dim, axis) in axes.iter_mut().enumerate() {
segments::compute_segments(&mut outline, axis, class.script.group);
segments::link_segments(
&outline,
axis,
scaled_metrics.axes[dim].scale,
class.script.group,
unscaled_metrics.axes[dim].max_width(),
);
compute_edges(
axis,
&scaled_metrics.axes[dim],
class.script.hint_top_to_bottom,
scaled_metrics.axes[1].scale,
class.script.group,
);
compute_blue_edges(
axis,
&scale,
&unscaled_metrics.axes[dim].blues,
&scaled_metrics.axes[dim].blues,
class.script.group,
);
}
assert_eq!(axes[Axis::HORIZONTAL].edges.as_slice(), expected_h_edges);
assert_eq!(axes[Axis::VERTICAL].edges.as_slice(), expected_v_edges);
}
}

View File

@@ -0,0 +1,246 @@
//! Topology analysis of segments and edges.
mod edges;
mod segments;
use super::{
metrics::ScaledWidth,
outline::{Direction, Orientation, Point},
};
use crate::collections::SmallVec;
pub(crate) use edges::{compute_blue_edges, compute_edges};
pub(crate) use segments::{compute_segments, link_segments};
/// Maximum number of segments and edges stored inline.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L306>
const MAX_INLINE_SEGMENTS: usize = 18;
const MAX_INLINE_EDGES: usize = 12;
/// Either horizontal or vertical.
///
/// A type alias because it's used as an index.
pub type Dimension = usize;
/// Segments and edges for one dimension of an outline.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L309>
#[derive(Clone, Default, Debug)]
pub(crate) struct Axis {
/// Either horizontal or vertical.
pub dim: Dimension,
/// Depends on dimension and outline orientation.
pub major_dir: Direction,
/// Collection of segments for the axis.
pub segments: SmallVec<Segment, MAX_INLINE_SEGMENTS>,
/// Collection of edges for the axis.
pub edges: SmallVec<Edge, MAX_INLINE_EDGES>,
}
impl Axis {
/// X coordinates, i.e. vertical segments and edges.
pub const HORIZONTAL: Dimension = 0;
/// Y coordinates, i.e. horizontal segments and edges.
pub const VERTICAL: Dimension = 1;
}
impl Axis {
#[cfg(test)]
pub fn new(dim: Dimension, orientation: Option<Orientation>) -> Self {
let mut axis = Self::default();
axis.reset(dim, orientation);
axis
}
pub fn reset(&mut self, dim: Dimension, orientation: Option<Orientation>) {
self.dim = dim;
self.major_dir = match (dim, orientation) {
(Self::HORIZONTAL, Some(Orientation::Clockwise)) => Direction::Down,
(Self::VERTICAL, Some(Orientation::Clockwise)) => Direction::Right,
(Self::HORIZONTAL, _) => Direction::Up,
(Self::VERTICAL, _) => Direction::Left,
_ => Direction::None,
};
self.segments.clear();
self.edges.clear();
}
}
impl Axis {
/// Inserts the given edge into the sorted edge list.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.c#L197>
pub fn insert_edge(&mut self, edge: Edge, top_to_bottom_hinting: bool) {
self.edges.push(edge);
let edges = self.edges.as_mut_slice();
// If this is the first edge, we're done.
if edges.len() == 1 {
return;
}
// Now move it into place
let mut ix = edges.len() - 1;
while ix > 0 {
let prev_ix = ix - 1;
let prev_fpos = edges[prev_ix].fpos;
if (top_to_bottom_hinting && prev_fpos > edge.fpos)
|| (!top_to_bottom_hinting && prev_fpos < edge.fpos)
{
break;
}
// Edges with the same position and minor direction should appear
// before those with the major direction
if prev_fpos == edge.fpos && edge.dir == self.major_dir {
break;
}
let prev_edge = edges[prev_ix];
edges[ix] = prev_edge;
ix -= 1;
}
edges[ix] = edge;
}
/// Links the given segment and edge.
pub fn append_segment_to_edge(&mut self, segment_ix: usize, edge_ix: usize) {
let edge = &mut self.edges[edge_ix];
let first_ix = edge.first_ix;
let last_ix = edge.last_ix;
edge.last_ix = segment_ix as u16;
let segment = &mut self.segments[segment_ix];
segment.edge_next_ix = Some(first_ix);
self.segments[last_ix as usize].edge_next_ix = Some(segment_ix as u16);
}
}
/// Sequence of points with a single dominant direction.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L262>
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub(crate) struct Segment {
/// Flags describing the properties of the segment.
pub flags: u8,
/// Dominant direction of the segment.
pub dir: Direction,
/// Position of the segment.
pub pos: i16,
/// Deviation from segment position.
pub delta: i16,
/// Minimum coordinate of the segment.
pub min_coord: i16,
/// Maximum coordinate of the segment.
pub max_coord: i16,
/// Hinted segment height.
pub height: i16,
/// Used during stem matching.
pub score: i32,
/// Used during stem matching.
pub len: i32,
/// Index of best candidate for a stem link.
pub link_ix: Option<u16>,
/// Index of best candidate for a serif link.
pub serif_ix: Option<u16>,
/// Index of first point in the outline.
pub first_ix: u16,
/// Index of last point in the outline.
pub last_ix: u16,
/// Index of edge that is associated with the segment.
pub edge_ix: Option<u16>,
/// Index of next segment in edge's segment list.
pub edge_next_ix: Option<u16>,
}
/// Segment flags.
///
/// Note: these are the same as edge flags.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L227>
impl Segment {
pub const NORMAL: u8 = 0;
pub const ROUND: u8 = 1;
pub const SERIF: u8 = 2;
pub const DONE: u8 = 4;
pub const NEUTRAL: u8 = 8;
}
impl Segment {
pub fn first(&self) -> usize {
self.first_ix as usize
}
pub fn first_point<'a>(&self, points: &'a [Point]) -> &'a Point {
&points[self.first()]
}
pub fn last(&self) -> usize {
self.last_ix as usize
}
pub fn last_point<'a>(&self, points: &'a [Point]) -> &'a Point {
&points[self.last()]
}
pub fn edge<'a>(&self, edges: &'a [Edge]) -> Option<&'a Edge> {
edges.get(self.edge_ix.map(|ix| ix as usize)?)
}
/// Returns the next segment in this segment's parent edge.
pub fn next_in_edge<'a>(&self, segments: &'a [Segment]) -> Option<&'a Segment> {
segments.get(self.edge_next_ix.map(|ix| ix as usize)?)
}
pub fn link<'a>(&self, segments: &'a [Segment]) -> Option<&'a Segment> {
segments.get(self.link_ix.map(|ix| ix as usize)?)
}
}
/// Sequence of segments used for grid-fitting.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L286>
#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
pub(crate) struct Edge {
/// Original, unscaled position in font units.
pub fpos: i16,
/// Original, scaled position.
pub opos: i32,
/// Current position.
pub pos: i32,
/// Edge flags.
pub flags: u8,
/// Edge direction.
pub dir: Direction,
/// Present if this is a blue edge.
pub blue_edge: Option<ScaledWidth>,
/// Index of linked edge.
pub link_ix: Option<u16>,
/// Index of primary edge for serif.
pub serif_ix: Option<u16>,
/// Used to speed up edge interpolation.
pub scale: i32,
/// Index of first segment in edge.
pub first_ix: u16,
/// Index of last segment in edge.
pub last_ix: u16,
}
/// Edge flags.
///
/// Note: these are the same as segment flags.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/afhints.h#L227>
impl Edge {
pub const NORMAL: u8 = Segment::NORMAL;
pub const ROUND: u8 = Segment::ROUND;
pub const SERIF: u8 = Segment::SERIF;
pub const DONE: u8 = Segment::DONE;
pub const NEUTRAL: u8 = Segment::NEUTRAL;
}
impl Edge {
pub fn link<'a>(&self, edges: &'a [Edge]) -> Option<&'a Edge> {
edges.get(self.link_ix.map(|ix| ix as usize)?)
}
pub fn serif<'a>(&self, edges: &'a [Edge]) -> Option<&'a Edge> {
edges.get(self.serif_ix.map(|ix| ix as usize)?)
}
}

File diff suppressed because it is too large Load Diff

1496
vendor/skrifa/src/outline/cff/hint.rs vendored Normal file

File diff suppressed because it is too large Load Diff

874
vendor/skrifa/src/outline/cff/mod.rs vendored Normal file
View File

@@ -0,0 +1,874 @@
//! Support for scaling CFF outlines.
mod hint;
use super::{GlyphHMetrics, OutlinePen};
use hint::{HintParams, HintState, HintingSink};
use raw::FontRef;
use read_fonts::{
tables::{
postscript::{
charstring::{self, CommandSink},
dict, BlendState, Error, FdSelect, Index,
},
variations::ItemVariationStore,
},
types::{F2Dot14, Fixed, GlyphId},
FontData, FontRead, ReadError, TableProvider,
};
use std::ops::Range;
/// Type for loading, scaling and hinting outlines in CFF/CFF2 tables.
///
/// The skrifa crate provides a higher level interface for this that handles
/// caching and abstracting over the different outline formats. Consider using
/// that if detailed control over resources is not required.
///
/// # Subfonts
///
/// CFF tables can contain multiple logical "subfonts" which determine the
/// state required for processing some subset of glyphs. This state is
/// accessed using the [`FDArray and FDSelect`](https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=28)
/// operators to select an appropriate subfont for any given glyph identifier.
/// This process is exposed on this type with the
/// [`subfont_index`](Self::subfont_index) method to retrieve the subfont
/// index for the requested glyph followed by using the
/// [`subfont`](Self::subfont) method to create an appropriately configured
/// subfont for that glyph.
#[derive(Clone)]
pub(crate) struct Outlines<'a> {
pub(crate) font: FontRef<'a>,
pub(crate) glyph_metrics: GlyphHMetrics<'a>,
offset_data: FontData<'a>,
global_subrs: Index<'a>,
top_dict: TopDict<'a>,
version: u16,
units_per_em: u16,
}
impl<'a> Outlines<'a> {
/// Creates a new scaler for the given font.
///
/// This will choose an underlying CFF2 or CFF table from the font, in that
/// order.
pub fn new(font: &FontRef<'a>) -> Option<Self> {
let units_per_em = font.head().ok()?.units_per_em();
Self::from_cff2(font, units_per_em).or_else(|| Self::from_cff(font, units_per_em))
}
pub fn from_cff(font: &FontRef<'a>, units_per_em: u16) -> Option<Self> {
let cff1 = font.cff().ok()?;
let glyph_metrics = GlyphHMetrics::new(font)?;
// "The Name INDEX in the CFF data must contain only one entry;
// that is, there must be only one font in the CFF FontSet"
// So we always pass 0 for Top DICT index when reading from an
// OpenType font.
// <https://learn.microsoft.com/en-us/typography/opentype/spec/cff>
let top_dict_data = cff1.top_dicts().get(0).ok()?;
let top_dict = TopDict::new(cff1.offset_data().as_bytes(), top_dict_data, false).ok()?;
Some(Self {
font: font.clone(),
glyph_metrics,
offset_data: cff1.offset_data(),
global_subrs: cff1.global_subrs().into(),
top_dict,
version: 1,
units_per_em,
})
}
pub fn from_cff2(font: &FontRef<'a>, units_per_em: u16) -> Option<Self> {
let cff2 = font.cff2().ok()?;
let glyph_metrics = GlyphHMetrics::new(font)?;
let table_data = cff2.offset_data().as_bytes();
let top_dict = TopDict::new(table_data, cff2.top_dict_data(), true).ok()?;
Some(Self {
font: font.clone(),
glyph_metrics,
offset_data: cff2.offset_data(),
global_subrs: cff2.global_subrs().into(),
top_dict,
version: 2,
units_per_em,
})
}
pub fn is_cff2(&self) -> bool {
self.version == 2
}
pub fn units_per_em(&self) -> u16 {
self.units_per_em
}
/// Returns the number of available glyphs.
pub fn glyph_count(&self) -> usize {
self.top_dict.charstrings.count() as usize
}
/// Returns the number of available subfonts.
pub fn subfont_count(&self) -> u32 {
// All CFF fonts have at least one logical subfont.
self.top_dict.font_dicts.count().max(1)
}
/// Returns the subfont (or Font DICT) index for the given glyph
/// identifier.
pub fn subfont_index(&self, glyph_id: GlyphId) -> u32 {
// For CFF tables, an FDSelect index will be present for CID-keyed
// fonts. Otherwise, the Top DICT will contain an entry for the
// "global" Private DICT.
// See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=27>
//
// CFF2 tables always contain a Font DICT and an FDSelect is only
// present if the size of the DICT is greater than 1.
// See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#10-font-dict-index-font-dicts-and-fdselect>
//
// In both cases, we return a subfont index of 0 when FDSelect is missing.
self.top_dict
.fd_select
.as_ref()
.and_then(|select| select.font_index(glyph_id))
.unwrap_or(0) as u32
}
/// Creates a new subfont for the given index, size, normalized
/// variation coordinates and hinting state.
///
/// The index of a subfont for a particular glyph can be retrieved with
/// the [`subfont_index`](Self::subfont_index) method.
pub fn subfont(
&self,
index: u32,
size: Option<f32>,
coords: &[F2Dot14],
) -> Result<Subfont, Error> {
let private_dict_range = self.private_dict_range(index)?;
let blend_state = self
.top_dict
.var_store
.clone()
.map(|store| BlendState::new(store, coords, 0))
.transpose()?;
let private_dict = PrivateDict::new(self.offset_data, private_dict_range, blend_state)?;
let scale = match size {
Some(ppem) if self.units_per_em > 0 => {
// Note: we do an intermediate scale to 26.6 to ensure we
// match FreeType
Some(
Fixed::from_bits((ppem * 64.) as i32)
/ Fixed::from_bits(self.units_per_em as i32),
)
}
_ => None,
};
// When hinting, use a modified scale factor
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L279>
let hint_scale = Fixed::from_bits((scale.unwrap_or(Fixed::ONE).to_bits() + 32) / 64);
let hint_state = HintState::new(&private_dict.hint_params, hint_scale);
Ok(Subfont {
is_cff2: self.is_cff2(),
scale,
subrs_offset: private_dict.subrs_offset,
hint_state,
store_index: private_dict.store_index,
})
}
/// Loads and scales an outline for the given subfont instance, glyph
/// identifier and normalized variation coordinates.
///
/// Before calling this method, use [`subfont_index`](Self::subfont_index)
/// to retrieve the subfont index for the desired glyph and then
/// [`subfont`](Self::subfont) to create an instance of the subfont for a
/// particular size and location in variation space.
/// Creating subfont instances is not free, so this process is exposed in
/// discrete steps to allow for caching.
///
/// The result is emitted to the specified pen.
pub fn draw(
&self,
subfont: &Subfont,
glyph_id: GlyphId,
coords: &[F2Dot14],
hint: bool,
pen: &mut impl OutlinePen,
) -> Result<(), Error> {
let charstring_data = self.top_dict.charstrings.get(glyph_id.to_u32() as usize)?;
let subrs = subfont.subrs(self)?;
let blend_state = subfont.blend_state(self, coords)?;
let mut pen_sink = PenSink::new(pen);
let mut simplifying_adapter = NopFilteringSink::new(&mut pen_sink);
// Only apply hinting if we have a scale
if hint && subfont.scale.is_some() {
let mut hinting_adapter =
HintingSink::new(&subfont.hint_state, &mut simplifying_adapter);
charstring::evaluate(
charstring_data,
self.global_subrs.clone(),
subrs,
blend_state,
&mut hinting_adapter,
)?;
hinting_adapter.finish();
} else {
let mut scaling_adapter =
ScalingSink26Dot6::new(&mut simplifying_adapter, subfont.scale);
charstring::evaluate(
charstring_data,
self.global_subrs.clone(),
subrs,
blend_state,
&mut scaling_adapter,
)?;
}
simplifying_adapter.finish();
Ok(())
}
fn private_dict_range(&self, subfont_index: u32) -> Result<Range<usize>, Error> {
if self.top_dict.font_dicts.count() != 0 {
// If we have a font dict array, extract the private dict range
// from the font dict at the given index.
let font_dict_data = self.top_dict.font_dicts.get(subfont_index as usize)?;
let mut range = None;
for entry in dict::entries(font_dict_data, None) {
if let dict::Entry::PrivateDictRange(r) = entry? {
range = Some(r);
break;
}
}
range
} else {
// Use the private dict range from the top dict.
// Note: "A Private DICT is required but may be specified as having
// a length of 0 if there are no non-default values to be stored."
// <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5176.CFF.pdf#page=25>
let range = self.top_dict.private_dict_range.clone();
Some(range.start as usize..range.end as usize)
}
.ok_or(Error::MissingPrivateDict)
}
}
/// Specifies local subroutines and hinting parameters for some subset of
/// glyphs in a CFF or CFF2 table.
///
/// This type is designed to be cacheable to avoid re-evaluating the private
/// dict every time a charstring is processed.
///
/// For variable fonts, this is dependent on a location in variation space.
#[derive(Clone)]
pub(crate) struct Subfont {
is_cff2: bool,
scale: Option<Fixed>,
subrs_offset: Option<usize>,
pub(crate) hint_state: HintState,
store_index: u16,
}
impl Subfont {
/// Returns the local subroutine index.
pub fn subrs<'a>(&self, scaler: &Outlines<'a>) -> Result<Option<Index<'a>>, Error> {
if let Some(subrs_offset) = self.subrs_offset {
let offset_data = scaler.offset_data.as_bytes();
let index_data = offset_data.get(subrs_offset..).unwrap_or_default();
Ok(Some(Index::new(index_data, self.is_cff2)?))
} else {
Ok(None)
}
}
/// Creates a new blend state for the given normalized variation
/// coordinates.
pub fn blend_state<'a>(
&self,
scaler: &Outlines<'a>,
coords: &'a [F2Dot14],
) -> Result<Option<BlendState<'a>>, Error> {
if let Some(var_store) = scaler.top_dict.var_store.clone() {
Ok(Some(BlendState::new(var_store, coords, self.store_index)?))
} else {
Ok(None)
}
}
}
/// Entries that we parse from the Private DICT to support charstring
/// evaluation.
#[derive(Default)]
struct PrivateDict {
hint_params: HintParams,
subrs_offset: Option<usize>,
store_index: u16,
}
impl PrivateDict {
fn new(
data: FontData,
range: Range<usize>,
blend_state: Option<BlendState<'_>>,
) -> Result<Self, Error> {
let private_dict_data = data.read_array(range.clone())?;
let mut dict = Self::default();
for entry in dict::entries(private_dict_data, blend_state) {
use dict::Entry::*;
match entry? {
BlueValues(values) => dict.hint_params.blues = values,
FamilyBlues(values) => dict.hint_params.family_blues = values,
OtherBlues(values) => dict.hint_params.other_blues = values,
FamilyOtherBlues(values) => dict.hint_params.family_other_blues = values,
BlueScale(value) => dict.hint_params.blue_scale = value,
BlueShift(value) => dict.hint_params.blue_shift = value,
BlueFuzz(value) => dict.hint_params.blue_fuzz = value,
LanguageGroup(group) => dict.hint_params.language_group = group,
// Subrs offset is relative to the private DICT
SubrsOffset(offset) => {
dict.subrs_offset = Some(
range
.start
.checked_add(offset)
.ok_or(ReadError::OutOfBounds)?,
)
}
VariationStoreIndex(index) => dict.store_index = index,
_ => {}
}
}
Ok(dict)
}
}
/// Entries that we parse from the Top DICT that are required to support
/// charstring evaluation.
#[derive(Clone, Default)]
struct TopDict<'a> {
charstrings: Index<'a>,
font_dicts: Index<'a>,
fd_select: Option<FdSelect<'a>>,
private_dict_range: Range<u32>,
var_store: Option<ItemVariationStore<'a>>,
}
impl<'a> TopDict<'a> {
fn new(table_data: &'a [u8], top_dict_data: &'a [u8], is_cff2: bool) -> Result<Self, Error> {
let mut items = TopDict::default();
for entry in dict::entries(top_dict_data, None) {
match entry? {
dict::Entry::CharstringsOffset(offset) => {
items.charstrings =
Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
}
dict::Entry::FdArrayOffset(offset) => {
items.font_dicts =
Index::new(table_data.get(offset..).unwrap_or_default(), is_cff2)?;
}
dict::Entry::FdSelectOffset(offset) => {
items.fd_select = Some(FdSelect::read(FontData::new(
table_data.get(offset..).unwrap_or_default(),
))?);
}
dict::Entry::PrivateDictRange(range) => {
items.private_dict_range = range.start as u32..range.end as u32;
}
dict::Entry::VariationStoreOffset(offset) if is_cff2 => {
// IVS is preceded by a 2 byte length, but ensure that
// we don't overflow
// See <https://github.com/googlefonts/fontations/issues/1223>
let offset = offset.checked_add(2).ok_or(ReadError::OutOfBounds)?;
items.var_store = Some(ItemVariationStore::read(FontData::new(
table_data.get(offset..).unwrap_or_default(),
))?);
}
_ => {}
}
}
Ok(items)
}
}
/// Command sink that sends the results of charstring evaluation to
/// an [OutlinePen].
struct PenSink<'a, P>(&'a mut P);
impl<'a, P> PenSink<'a, P> {
fn new(pen: &'a mut P) -> Self {
Self(pen)
}
}
impl<P> CommandSink for PenSink<'_, P>
where
P: OutlinePen,
{
fn move_to(&mut self, x: Fixed, y: Fixed) {
self.0.move_to(x.to_f32(), y.to_f32());
}
fn line_to(&mut self, x: Fixed, y: Fixed) {
self.0.line_to(x.to_f32(), y.to_f32());
}
fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
self.0.curve_to(
cx0.to_f32(),
cy0.to_f32(),
cx1.to_f32(),
cy1.to_f32(),
x.to_f32(),
y.to_f32(),
);
}
fn close(&mut self) {
self.0.close();
}
}
/// Command sink adapter that applies a scaling factor.
///
/// This assumes a 26.6 scaling factor packed into a Fixed and thus,
/// this is not public and exists only to match FreeType's exact
/// scaling process.
struct ScalingSink26Dot6<'a, S> {
inner: &'a mut S,
scale: Option<Fixed>,
}
impl<'a, S> ScalingSink26Dot6<'a, S> {
fn new(sink: &'a mut S, scale: Option<Fixed>) -> Self {
Self { scale, inner: sink }
}
fn scale(&self, coord: Fixed) -> Fixed {
// The following dance is necessary to exactly match FreeType's
// application of scaling factors. This seems to be the result
// of merging the contributed Adobe code while not breaking the
// FreeType public API.
//
// The first two steps apply to both scaled and unscaled outlines:
//
// 1. Multiply by 1/64
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psft.c#L284>
let a = coord * Fixed::from_bits(0x0400);
// 2. Truncate the bottom 10 bits. Combined with the division by 64,
// converts to font units.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psobjs.c#L2219>
let b = Fixed::from_bits(a.to_bits() >> 10);
if let Some(scale) = self.scale {
// Scaled case:
// 3. Multiply by the original scale factor (to 26.6)
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/cff/cffgload.c#L721>
let c = b * scale;
// 4. Convert from 26.6 to 16.16
Fixed::from_bits(c.to_bits() << 10)
} else {
// Unscaled case:
// 3. Convert from integer to 16.16
Fixed::from_bits(b.to_bits() << 16)
}
}
}
impl<S: CommandSink> CommandSink for ScalingSink26Dot6<'_, S> {
fn hstem(&mut self, y: Fixed, dy: Fixed) {
self.inner.hstem(y, dy);
}
fn vstem(&mut self, x: Fixed, dx: Fixed) {
self.inner.vstem(x, dx);
}
fn hint_mask(&mut self, mask: &[u8]) {
self.inner.hint_mask(mask);
}
fn counter_mask(&mut self, mask: &[u8]) {
self.inner.counter_mask(mask);
}
fn move_to(&mut self, x: Fixed, y: Fixed) {
self.inner.move_to(self.scale(x), self.scale(y));
}
fn line_to(&mut self, x: Fixed, y: Fixed) {
self.inner.line_to(self.scale(x), self.scale(y));
}
fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
self.inner.curve_to(
self.scale(cx1),
self.scale(cy1),
self.scale(cx2),
self.scale(cy2),
self.scale(x),
self.scale(y),
);
}
fn close(&mut self) {
self.inner.close();
}
}
/// Command sink adapter that suppresses degenerate move and line commands.
///
/// FreeType avoids emitting empty contours and zero length lines to prevent
/// artifacts when stem darkening is enabled. We don't support stem darkening
/// because it's not enabled by any of our clients but we remove the degenerate
/// elements regardless to match the output.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L1786>
struct NopFilteringSink<'a, S> {
start: Option<(Fixed, Fixed)>,
last: Option<(Fixed, Fixed)>,
pending_move: Option<(Fixed, Fixed)>,
inner: &'a mut S,
}
impl<'a, S> NopFilteringSink<'a, S>
where
S: CommandSink,
{
fn new(inner: &'a mut S) -> Self {
Self {
start: None,
last: None,
pending_move: None,
inner,
}
}
fn flush_pending_move(&mut self) {
if let Some((x, y)) = self.pending_move.take() {
if let Some((last_x, last_y)) = self.start {
if self.last != self.start {
self.inner.line_to(last_x, last_y);
}
}
self.start = Some((x, y));
self.last = None;
self.inner.move_to(x, y);
}
}
pub fn finish(&mut self) {
if let Some((x, y)) = self.start {
if self.last != self.start {
self.inner.line_to(x, y);
}
self.inner.close();
}
}
}
impl<S> CommandSink for NopFilteringSink<'_, S>
where
S: CommandSink,
{
fn hstem(&mut self, y: Fixed, dy: Fixed) {
self.inner.hstem(y, dy);
}
fn vstem(&mut self, x: Fixed, dx: Fixed) {
self.inner.vstem(x, dx);
}
fn hint_mask(&mut self, mask: &[u8]) {
self.inner.hint_mask(mask);
}
fn counter_mask(&mut self, mask: &[u8]) {
self.inner.counter_mask(mask);
}
fn move_to(&mut self, x: Fixed, y: Fixed) {
self.pending_move = Some((x, y));
}
fn line_to(&mut self, x: Fixed, y: Fixed) {
if self.pending_move == Some((x, y)) {
return;
}
self.flush_pending_move();
if self.last == Some((x, y)) || (self.last.is_none() && self.start == Some((x, y))) {
return;
}
self.inner.line_to(x, y);
self.last = Some((x, y));
}
fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
self.flush_pending_move();
self.last = Some((x, y));
self.inner.curve_to(cx1, cy1, cx2, cy2, x, y);
}
fn close(&mut self) {
if self.pending_move.is_none() {
self.inner.close();
self.start = None;
self.last = None;
}
}
}
#[cfg(test)]
mod tests {
use super::{super::pen::SvgPen, *};
use crate::{
outline::{HintingInstance, HintingOptions},
prelude::{LocationRef, Size},
MetadataProvider,
};
use dict::Blues;
use font_test_data::bebuffer::BeBuffer;
use raw::tables::cff2::Cff2;
use read_fonts::FontRef;
#[test]
fn unscaled_scaling_sink_produces_integers() {
let nothing = &mut ();
let sink = ScalingSink26Dot6::new(nothing, None);
for coord in [50.0, 50.1, 50.125, 50.5, 50.9] {
assert_eq!(sink.scale(Fixed::from_f64(coord)).to_f32(), 50.0);
}
}
#[test]
fn scaled_scaling_sink() {
let ppem = 20.0;
let upem = 1000.0;
// match FreeType scaling with intermediate conversion to 26.6
let scale = Fixed::from_bits((ppem * 64.) as i32) / Fixed::from_bits(upem as i32);
let nothing = &mut ();
let sink = ScalingSink26Dot6::new(nothing, Some(scale));
let inputs = [
// input coord, expected scaled output
(0.0, 0.0),
(8.0, 0.15625),
(16.0, 0.3125),
(32.0, 0.640625),
(72.0, 1.4375),
(128.0, 2.5625),
];
for (coord, expected) in inputs {
assert_eq!(
sink.scale(Fixed::from_f64(coord)).to_f32(),
expected,
"scaling coord {coord}"
);
}
}
#[test]
fn read_cff_static() {
let font = FontRef::new(font_test_data::NOTO_SERIF_DISPLAY_TRIMMED).unwrap();
let cff = Outlines::new(&font).unwrap();
assert!(!cff.is_cff2());
assert!(cff.top_dict.var_store.is_none());
assert!(cff.top_dict.font_dicts.count() == 0);
assert!(!cff.top_dict.private_dict_range.is_empty());
assert!(cff.top_dict.fd_select.is_none());
assert_eq!(cff.subfont_count(), 1);
assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
assert_eq!(cff.global_subrs.count(), 17);
}
#[test]
fn read_cff2_static() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let cff = Outlines::new(&font).unwrap();
assert!(cff.is_cff2());
assert!(cff.top_dict.var_store.is_some());
assert!(cff.top_dict.font_dicts.count() != 0);
assert!(cff.top_dict.private_dict_range.is_empty());
assert!(cff.top_dict.fd_select.is_none());
assert_eq!(cff.subfont_count(), 1);
assert_eq!(cff.subfont_index(GlyphId::new(1)), 0);
assert_eq!(cff.global_subrs.count(), 0);
}
#[test]
fn read_example_cff2_table() {
let cff2 = Cff2::read(FontData::new(font_test_data::cff2::EXAMPLE)).unwrap();
let top_dict =
TopDict::new(cff2.offset_data().as_bytes(), cff2.top_dict_data(), true).unwrap();
assert!(top_dict.var_store.is_some());
assert!(top_dict.font_dicts.count() != 0);
assert!(top_dict.private_dict_range.is_empty());
assert!(top_dict.fd_select.is_none());
assert_eq!(cff2.global_subrs().count(), 0);
}
#[test]
fn cff2_variable_outlines_match_freetype() {
compare_glyphs(
font_test_data::CANTARELL_VF_TRIMMED,
font_test_data::CANTARELL_VF_TRIMMED_GLYPHS,
);
}
#[test]
fn cff_static_outlines_match_freetype() {
compare_glyphs(
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED,
font_test_data::NOTO_SERIF_DISPLAY_TRIMMED_GLYPHS,
);
}
#[test]
fn unhinted_ends_with_close() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let glyph = font.outline_glyphs().get(GlyphId::new(1)).unwrap();
let mut svg = SvgPen::default();
glyph.draw(Size::unscaled(), &mut svg).unwrap();
assert!(svg.to_string().ends_with('Z'));
}
#[test]
fn hinted_ends_with_close() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let glyphs = font.outline_glyphs();
let hinter = HintingInstance::new(
&glyphs,
Size::unscaled(),
LocationRef::default(),
HintingOptions::default(),
)
.unwrap();
let glyph = glyphs.get(GlyphId::new(1)).unwrap();
let mut svg = SvgPen::default();
glyph.draw(&hinter, &mut svg).unwrap();
assert!(svg.to_string().ends_with('Z'));
}
/// Ensure we don't reject an empty Private DICT
#[test]
fn empty_private_dict() {
let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
let outlines = super::Outlines::new(&font).unwrap();
assert!(outlines.top_dict.private_dict_range.is_empty());
assert!(outlines.private_dict_range(0).unwrap().is_empty());
}
/// Fuzzer caught add with overflow when computing subrs offset.
/// See <https://issues.oss-fuzz.com/issues/377965575>
#[test]
fn subrs_offset_overflow() {
// A private DICT with an overflowing subrs offset
let private_dict = BeBuffer::new()
.push(0u32) // pad so that range doesn't start with 0 and we overflow
.push(29u8) // integer operator
.push(-1i32) // integer value
.push(19u8) // subrs offset operator
.to_vec();
// Just don't panic with overflow
assert!(
PrivateDict::new(FontData::new(&private_dict), 4..private_dict.len(), None).is_err()
);
}
// Fuzzer caught add with overflow when computing offset to
// var store.
// See <https://issues.oss-fuzz.com/issues/377574377>
#[test]
fn top_dict_ivs_offset_overflow() {
// A top DICT with a var store offset of -1 which will cause an
// overflow
let top_dict = BeBuffer::new()
.push(29u8) // integer operator
.push(-1i32) // integer value
.push(24u8) // var store offset operator
.to_vec();
// Just don't panic with overflow
assert!(TopDict::new(&[], &top_dict, true).is_err());
}
/// Actually apply a scale when the computed scale factor is
/// equal to Fixed::ONE.
///
/// Specifically, when upem = 512 and ppem = 8, this results in
/// a scale factor of 65536 which was being interpreted as an
/// unscaled draw request.
#[test]
fn proper_scaling_when_factor_equals_fixed_one() {
let font = FontRef::new(font_test_data::MATERIAL_ICONS_SUBSET).unwrap();
assert_eq!(font.head().unwrap().units_per_em(), 512);
let glyphs = font.outline_glyphs();
let glyph = glyphs.get(GlyphId::new(1)).unwrap();
let mut svg = SvgPen::with_precision(6);
glyph
.draw((Size::new(8.0), LocationRef::default()), &mut svg)
.unwrap();
// This was initially producing unscaled values like M405.000...
assert!(svg.starts_with("M6.328125,7.000000 L1.671875,7.000000"));
}
/// For the given font data and extracted outlines, parse the extracted
/// outline data into a set of expected values and compare these with the
/// results generated by the scaler.
///
/// This will compare all outlines at various sizes and (for variable
/// fonts), locations in variation space.
fn compare_glyphs(font_data: &[u8], expected_outlines: &str) {
use super::super::testing;
let font = FontRef::new(font_data).unwrap();
let expected_outlines = testing::parse_glyph_outlines(expected_outlines);
let outlines = super::Outlines::new(&font).unwrap();
let mut path = testing::Path::default();
for expected_outline in &expected_outlines {
if expected_outline.size == 0.0 && !expected_outline.coords.is_empty() {
continue;
}
let size = (expected_outline.size != 0.0).then_some(expected_outline.size);
path.elements.clear();
let subfont = outlines
.subfont(
outlines.subfont_index(expected_outline.glyph_id),
size,
&expected_outline.coords,
)
.unwrap();
outlines
.draw(
&subfont,
expected_outline.glyph_id,
&expected_outline.coords,
false,
&mut path,
)
.unwrap();
if path.elements != expected_outline.path {
panic!(
"mismatch in glyph path for id {} (size: {}, coords: {:?}): path: {:?} expected_path: {:?}",
expected_outline.glyph_id,
expected_outline.size,
expected_outline.coords,
&path.elements,
&expected_outline.path
);
}
}
}
// We were overwriting family_other_blues with family_blues.
#[test]
fn capture_family_other_blues() {
let private_dict_data = &font_test_data::cff2::EXAMPLE[0x4f..=0xc0];
let store =
ItemVariationStore::read(FontData::new(&font_test_data::cff2::EXAMPLE[18..])).unwrap();
let coords = &[F2Dot14::from_f32(0.0)];
let blend_state = BlendState::new(store, coords, 0).unwrap();
let private_dict = PrivateDict::new(
FontData::new(private_dict_data),
0..private_dict_data.len(),
Some(blend_state),
)
.unwrap();
assert_eq!(
private_dict.hint_params.family_other_blues,
Blues::new([-249.0, -239.0].map(Fixed::from_f64).into_iter())
)
}
}

92
vendor/skrifa/src/outline/error.rs vendored Normal file
View File

@@ -0,0 +1,92 @@
//! Error types associated with outlines.
use core::fmt;
use read_fonts::types::GlyphId;
pub use read_fonts::{tables::postscript::Error as CffError, ReadError};
pub use super::glyf::HintError;
pub use super::path::ToPathError;
/// Errors that may occur when drawing glyphs.
#[derive(Clone, Debug)]
pub enum DrawError {
/// No viable sources were available.
NoSources,
/// The requested glyph was not present in the font.
GlyphNotFound(GlyphId),
/// Exceeded memory limits when loading a glyph.
InsufficientMemory,
/// Exceeded a recursion limit when loading a glyph.
RecursionLimitExceeded(GlyphId),
/// Glyph outline contains too many points.
TooManyPoints(GlyphId),
/// Error occurred during hinting.
HintingFailed(HintError),
/// An anchor point had invalid indices.
InvalidAnchorPoint(GlyphId, u16),
/// Error occurred while loading a PostScript (CFF/CFF2) glyph.
PostScript(CffError),
/// Conversion from outline to path failed.
ToPath(ToPathError),
/// Error occurred when reading font data.
Read(ReadError),
/// HarfBuzz style drawing with hints is not supported
// Error rather than silently returning unhinted per f2f discussion.
HarfBuzzHintingUnsupported,
}
impl From<HintError> for DrawError {
fn from(value: HintError) -> Self {
Self::HintingFailed(value)
}
}
impl From<ToPathError> for DrawError {
fn from(e: ToPathError) -> Self {
Self::ToPath(e)
}
}
impl From<ReadError> for DrawError {
fn from(e: ReadError) -> Self {
Self::Read(e)
}
}
impl From<CffError> for DrawError {
fn from(value: CffError) -> Self {
Self::PostScript(value)
}
}
impl fmt::Display for DrawError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::NoSources => write!(f, "No glyph sources are available for the given font"),
Self::GlyphNotFound(gid) => write!(f, "Glyph {gid} was not found in the given font"),
Self::InsufficientMemory => write!(f, "exceeded memory limits"),
Self::RecursionLimitExceeded(gid) => write!(
f,
"Recursion limit ({}) exceeded when loading composite component {gid}",
super::GLYF_COMPOSITE_RECURSION_LIMIT,
),
Self::TooManyPoints(gid) => write!(f, "Glyph {gid} contains more than 64k points"),
Self::HintingFailed(e) => write!(f, "{e}"),
Self::InvalidAnchorPoint(gid, index) => write!(
f,
"Invalid anchor point index ({index}) for composite glyph {gid}",
),
Self::PostScript(e) => write!(f, "{e}"),
Self::ToPath(e) => write!(f, "{e}"),
Self::Read(e) => write!(f, "{e}"),
Self::HarfBuzzHintingUnsupported => write!(
f,
"HarfBuzz style paths with hinting is not (yet?) supported"
),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DrawError {}

357
vendor/skrifa/src/outline/glyf/deltas.rs vendored Normal file
View File

@@ -0,0 +1,357 @@
use core::ops::RangeInclusive;
use raw::tables::glyf::PointCoord;
use read_fonts::{
tables::glyf::{PointFlags, PointMarker},
tables::gvar::{GlyphDelta, Gvar},
tables::variations::TupleVariation,
types::{F2Dot14, Fixed, GlyphId, Point},
ReadError,
};
use super::PHANTOM_POINT_COUNT;
/// Compute a set of deltas for the component offsets of a composite glyph.
///
/// Interpolation is meaningless for component offsets so this is a
/// specialized function that skips the expensive bits.
pub(super) fn composite_glyph<D: PointCoord>(
gvar: &Gvar,
glyph_id: GlyphId,
coords: &[F2Dot14],
deltas: &mut [Point<D>],
) -> Result<(), ReadError> {
compute_deltas_for_glyph(gvar, glyph_id, coords, deltas, |scalar, tuple, deltas| {
for tuple_delta in tuple.deltas() {
let ix = tuple_delta.position as usize;
if let Some(delta) = deltas.get_mut(ix) {
*delta += tuple_delta.apply_scalar(scalar);
}
}
Ok(())
})?;
Ok(())
}
pub(super) struct SimpleGlyph<'a, C: PointCoord> {
pub points: &'a [Point<C>],
pub flags: &'a mut [PointFlags],
pub contours: &'a [u16],
}
/// Compute a set of deltas for the points in a simple glyph.
///
/// This function will use interpolation to infer missing deltas for tuples
/// that contain sparse sets. The `iup_buffer` buffer is temporary storage
/// used for this and the length must be >= glyph.points.len().
pub(super) fn simple_glyph<C, D>(
gvar: &Gvar,
glyph_id: GlyphId,
coords: &[F2Dot14],
glyph: SimpleGlyph<C>,
iup_buffer: &mut [Point<D>],
deltas: &mut [Point<D>],
) -> Result<(), ReadError>
where
C: PointCoord,
D: PointCoord,
D: From<C>,
{
if iup_buffer.len() < glyph.points.len() || glyph.points.len() < PHANTOM_POINT_COUNT {
return Err(ReadError::InvalidArrayLen);
}
for delta in deltas.iter_mut() {
*delta = Default::default();
}
if gvar.glyph_variation_data(glyph_id).is_err() {
// Empty variation data for a glyph is not an error.
return Ok(());
};
let SimpleGlyph {
points,
flags,
contours,
} = glyph;
compute_deltas_for_glyph(gvar, glyph_id, coords, deltas, |scalar, tuple, deltas| {
// Infer missing deltas by interpolation.
// Prepare our working buffer by converting the points to 16.16
// and clearing the HAS_DELTA flags.
for ((flag, point), iup_point) in flags.iter_mut().zip(points).zip(&mut iup_buffer[..]) {
*iup_point = point.map(D::from);
flag.clear_marker(PointMarker::HAS_DELTA);
}
tuple.accumulate_sparse_deltas(iup_buffer, flags, scalar)?;
interpolate_deltas(points, flags, contours, &mut iup_buffer[..])
.ok_or(ReadError::OutOfBounds)?;
for ((delta, point), iup_point) in deltas.iter_mut().zip(points).zip(iup_buffer.iter()) {
*delta += *iup_point - point.map(D::from);
}
Ok(())
})?;
Ok(())
}
/// The common parts of simple and complex glyph processing
fn compute_deltas_for_glyph<C, D>(
gvar: &Gvar,
glyph_id: GlyphId,
coords: &[F2Dot14],
deltas: &mut [Point<D>],
mut apply_tuple_missing_deltas_fn: impl FnMut(
Fixed,
TupleVariation<GlyphDelta>,
&mut [Point<D>],
) -> Result<(), ReadError>,
) -> Result<(), ReadError>
where
C: PointCoord,
D: PointCoord,
D: From<C>,
{
for delta in deltas.iter_mut() {
*delta = Default::default();
}
let Ok(Some(var_data)) = gvar.glyph_variation_data(glyph_id) else {
// Empty variation data for a glyph is not an error.
return Ok(());
};
for (tuple, scalar) in var_data.active_tuples_at(coords) {
// Fast path: tuple contains all points, we can simply accumulate
// the deltas directly.
if tuple.has_deltas_for_all_points() {
tuple.accumulate_dense_deltas(deltas, scalar)?;
} else {
// Slow path is, annoyingly, different for simple vs composite
// so let the caller handle it
apply_tuple_missing_deltas_fn(scalar, tuple, deltas)?;
}
}
Ok(())
}
/// Interpolate points without delta values, similar to the IUP hinting
/// instruction.
///
/// Modeled after the FreeType implementation:
/// <https://github.com/freetype/freetype/blob/bbfcd79eacb4985d4b68783565f4b494aa64516b/src/truetype/ttgxvar.c#L3881>
fn interpolate_deltas<C, D>(
points: &[Point<C>],
flags: &[PointFlags],
contours: &[u16],
out_points: &mut [Point<D>],
) -> Option<()>
where
C: PointCoord,
D: PointCoord,
D: From<C>,
{
let mut jiggler = Jiggler { points, out_points };
let mut point_ix = 0usize;
for &end_point_ix in contours {
let end_point_ix = end_point_ix as usize;
let first_point_ix = point_ix;
// Search for first point that has a delta.
while point_ix <= end_point_ix && !flags.get(point_ix)?.has_marker(PointMarker::HAS_DELTA) {
point_ix += 1;
}
// If we didn't find any deltas, no variations in the current tuple
// apply, so skip it.
if point_ix > end_point_ix {
continue;
}
let first_delta_ix = point_ix;
let mut cur_delta_ix = point_ix;
point_ix += 1;
// Search for next point that has a delta...
while point_ix <= end_point_ix {
if flags.get(point_ix)?.has_marker(PointMarker::HAS_DELTA) {
// ... and interpolate intermediate points.
jiggler.interpolate(
cur_delta_ix + 1..=point_ix - 1,
RefPoints(cur_delta_ix, point_ix),
)?;
cur_delta_ix = point_ix;
}
point_ix += 1;
}
// If we only have a single delta, shift the contour.
if cur_delta_ix == first_delta_ix {
jiggler.shift(first_point_ix..=end_point_ix, cur_delta_ix)?;
} else {
// Otherwise, handle remaining points at beginning and end of
// contour.
jiggler.interpolate(
cur_delta_ix + 1..=end_point_ix,
RefPoints(cur_delta_ix, first_delta_ix),
)?;
if first_delta_ix > 0 {
jiggler.interpolate(
first_point_ix..=first_delta_ix - 1,
RefPoints(cur_delta_ix, first_delta_ix),
)?;
}
}
}
Some(())
}
struct RefPoints(usize, usize);
struct Jiggler<'a, C, D>
where
C: PointCoord,
D: PointCoord,
D: From<C>,
{
points: &'a [Point<C>],
out_points: &'a mut [Point<D>],
}
impl<C, D> Jiggler<'_, C, D>
where
C: PointCoord,
D: PointCoord,
D: From<C>,
{
/// Shift the coordinates of all points in the specified range using the
/// difference given by the point at `ref_ix`.
///
/// Modeled after the FreeType implementation: <https://github.com/freetype/freetype/blob/bbfcd79eacb4985d4b68783565f4b494aa64516b/src/truetype/ttgxvar.c#L3776>
fn shift(&mut self, range: RangeInclusive<usize>, ref_ix: usize) -> Option<()> {
let ref_in = self.points.get(ref_ix)?.map(D::from);
let ref_out = self.out_points.get(ref_ix)?;
let delta = *ref_out - ref_in;
if delta.x == D::zeroed() && delta.y == D::zeroed() {
return Some(());
}
// Apply the reference point delta to the entire range excluding the
// reference point itself which would apply the delta twice.
for out_point in self.out_points.get_mut(*range.start()..ref_ix)? {
*out_point += delta;
}
for out_point in self.out_points.get_mut(ref_ix + 1..=*range.end())? {
*out_point += delta;
}
Some(())
}
/// Interpolate the coordinates of all points in the specified range using
/// `ref1_ix` and `ref2_ix` as the reference point indices.
///
/// Modeled after the FreeType implementation: <https://github.com/freetype/freetype/blob/bbfcd79eacb4985d4b68783565f4b494aa64516b/src/truetype/ttgxvar.c#L3813>
///
/// For details on the algorithm, see: <https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers>
fn interpolate(&mut self, range: RangeInclusive<usize>, ref_points: RefPoints) -> Option<()> {
if range.is_empty() {
return Some(());
}
// FreeType uses pointer tricks to handle x and y coords with a single piece of code.
// Try a macro instead.
macro_rules! interp_coord {
($coord:ident) => {
let RefPoints(mut ref1_ix, mut ref2_ix) = ref_points;
if self.points.get(ref1_ix)?.$coord > self.points.get(ref2_ix)?.$coord {
core::mem::swap(&mut ref1_ix, &mut ref2_ix);
}
let in1 = D::from(self.points.get(ref1_ix)?.$coord);
let in2 = D::from(self.points.get(ref2_ix)?.$coord);
let out1 = self.out_points.get(ref1_ix)?.$coord;
let out2 = self.out_points.get(ref2_ix)?.$coord;
// If the reference points have the same coordinate but different delta,
// inferred delta is zero. Otherwise interpolate.
if in1 != in2 || out1 == out2 {
let scale = if in1 != in2 {
(out2 - out1) / (in2 - in1)
} else {
D::zeroed()
};
let d1 = out1 - in1;
let d2 = out2 - in2;
for (point, out_point) in self
.points
.get(range.clone())?
.iter()
.zip(self.out_points.get_mut(range.clone())?)
{
let mut out = D::from(point.$coord);
if out <= in1 {
out += d1;
} else if out >= in2 {
out += d2;
} else {
out = out1 + (out - in1) * scale;
}
out_point.$coord = out;
}
}
};
}
interp_coord!(x);
interp_coord!(y);
Some(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_points(tuples: &[(i32, i32)]) -> Vec<Point<i32>> {
tuples.iter().map(|&(x, y)| Point::new(x, y)).collect()
}
fn make_working_points_and_flags(
points: &[Point<i32>],
deltas: &[Point<i32>],
) -> (Vec<Point<Fixed>>, Vec<PointFlags>) {
let working_points = points
.iter()
.zip(deltas)
.map(|(point, delta)| point.map(Fixed::from_i32) + delta.map(Fixed::from_i32))
.collect();
let flags = deltas
.iter()
.map(|delta| {
let mut flags = PointFlags::default();
if delta.x != 0 || delta.y != 0 {
flags.set_marker(PointMarker::HAS_DELTA);
}
flags
})
.collect();
(working_points, flags)
}
#[test]
fn shift() {
let points = make_points(&[(245, 630), (260, 700), (305, 680)]);
// Single delta triggers a full contour shift.
let deltas = make_points(&[(20, -10), (0, 0), (0, 0)]);
let (mut working_points, flags) = make_working_points_and_flags(&points, &deltas);
interpolate_deltas(&points, &flags, &[2], &mut working_points).unwrap();
let expected = &[
Point::new(265, 620).map(Fixed::from_i32),
Point::new(280, 690).map(Fixed::from_i32),
Point::new(325, 670).map(Fixed::from_i32),
];
assert_eq!(&working_points, expected);
}
#[test]
fn interpolate() {
// Test taken from the spec:
// https://learn.microsoft.com/en-us/typography/opentype/spec/gvar#inferred-deltas-for-un-referenced-point-numbers
// with a minor adjustment to account for the precision of our fixed point math.
let points = make_points(&[(245, 630), (260, 700), (305, 680)]);
let deltas = make_points(&[(28, -62), (0, 0), (-42, -57)]);
let (mut working_points, flags) = make_working_points_and_flags(&points, &deltas);
interpolate_deltas(&points, &flags, &[2], &mut working_points).unwrap();
assert_eq!(
working_points[1],
Point::new(
Fixed::from_f64(260.0 + 10.4999237060547),
Fixed::from_f64(700.0 - 57.0)
)
);
}
}

View File

@@ -0,0 +1,97 @@
//! Tracking function call state.
use super::{definition::Definition, error::HintErrorKind, program::Program};
// FreeType provides a call stack with a depth of 32.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L502>
const MAX_DEPTH: usize = 32;
/// Record of an active invocation of a function or instruction
/// definition.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.h#L90>
#[derive(Copy, Clone, Default)]
pub struct CallRecord {
pub caller_program: Program,
pub return_pc: usize,
pub current_count: u32,
pub definition: Definition,
}
/// Tracker for nested active function or instruction calls.
#[derive(Default)]
pub struct CallStack {
records: [CallRecord; MAX_DEPTH],
len: usize,
}
impl CallStack {
pub fn clear(&mut self) {
self.len = 0;
}
pub fn push(&mut self, record: CallRecord) -> Result<(), HintErrorKind> {
let top = self
.records
.get_mut(self.len)
.ok_or(HintErrorKind::CallStackOverflow)?;
*top = record;
self.len += 1;
Ok(())
}
pub fn peek(&self) -> Option<&CallRecord> {
self.records.get(self.len.checked_sub(1)?)
}
pub fn pop(&mut self) -> Result<CallRecord, HintErrorKind> {
let record = *self.peek().ok_or(HintErrorKind::CallStackUnderflow)?;
self.len -= 1;
Ok(record)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stack_overflow() {
let mut stack = CallStack::default();
for i in 0..MAX_DEPTH {
stack.push(record_with_key(i)).unwrap();
}
assert!(matches!(
stack.push(CallRecord::default()),
Err(HintErrorKind::CallStackOverflow)
));
}
#[test]
fn stack_underflow() {
assert!(matches!(
CallStack::default().pop(),
Err(HintErrorKind::CallStackUnderflow)
));
}
#[test]
fn stack_push_pop() {
let mut stack = CallStack::default();
for i in 0..MAX_DEPTH {
stack.push(record_with_key(i)).unwrap();
}
for i in (0..MAX_DEPTH).rev() {
assert_eq!(stack.pop().unwrap().definition.key(), i as i32);
}
}
fn record_with_key(key: usize) -> CallRecord {
CallRecord {
caller_program: Program::Glyph,
return_pc: 0,
current_count: 1,
definition: Definition::new(Program::Font, 0..0, key as i32),
}
}
}

View File

@@ -0,0 +1,157 @@
//! Copy-on-write buffer for CVT and storage area.
/// Backing store for the CVT and storage area.
///
/// The CVT and storage area are initialized in the control value program
/// with values that are relevant to a particular size and hinting
/// configuration. However, some fonts contain code in glyph programs
/// that write to these buffers. Any modifications made in a glyph program
/// should not affect future glyphs and thus should not persist beyond
/// execution of that program. To solve this problem, a copy of the buffer
/// is made on the first write in a glyph program and all changes are
/// discarded on completion.
///
/// For more context, see <https://gitlab.freedesktop.org/freetype/freetype/-/merge_requests/23>
///
/// # Implementation notes
///
/// The current implementation defers the copy but not the allocation. This
/// is to support the guarantee of no heap allocation when operating on user
/// provided memory. Investigation of hinted Noto fonts suggests that writing
/// to CVT/Storage in glyph programs is common for ttfautohinted fonts so the
/// speculative allocation is likely worthwhile.
pub struct CowSlice<'a> {
data: &'a [i32],
data_mut: &'a mut [i32],
/// True if we've initialized the mutable slice
use_mut: bool,
}
impl<'a> CowSlice<'a> {
/// Creates a new copy-on-write slice with the given buffers.
///
/// The `data` buffer is expected to contain the initial data and the content
/// of `data_mut` is ignored unless the [`set`](Self::set) method is called
/// in which case a copy will be made from `data` to `data_mut` and the
/// mutable buffer will be used for all further access.
///
/// Returns [`CowSliceSizeMismatchError`] if `data.len() != data_mut.len()`.
pub fn new(
data: &'a [i32],
data_mut: &'a mut [i32],
) -> Result<Self, CowSliceSizeMismatchError> {
if data.len() != data_mut.len() {
return Err(CowSliceSizeMismatchError(data.len(), data_mut.len()));
}
Ok(Self {
data,
data_mut,
use_mut: false,
})
}
/// Creates a new copy-on-write slice with the given mutable buffer.
///
/// This avoids an extra copy and allocation in contexts where the data is
/// already assumed to be mutable (i.e. when executing `fpgm` and `prep`
/// programs).
pub fn new_mut(data_mut: &'a mut [i32]) -> Self {
Self {
use_mut: true,
data: &[],
data_mut,
}
}
/// Returns the value at the given index.
///
/// If mutable data has been initialized, reads from that buffer. Otherwise
/// reads from the immutable buffer.
pub fn get(&self, index: usize) -> Option<i32> {
if self.use_mut {
self.data_mut.get(index).copied()
} else {
self.data.get(index).copied()
}
}
/// Writes a value to the given index.
///
/// If the mutable buffer hasn't been initialized, first performs a full
/// buffer copy.
pub fn set(&mut self, index: usize, value: i32) -> Option<()> {
// Copy from immutable to mutable buffer if we haven't already
if !self.use_mut {
self.data_mut.copy_from_slice(self.data);
self.use_mut = true;
}
*self.data_mut.get_mut(index)? = value;
Some(())
}
pub fn len(&self) -> usize {
if self.use_mut {
self.data_mut.len()
} else {
self.data.len()
}
}
}
/// Error returned when the sizes of the immutable and mutable buffers
/// mismatch when constructing a [`CowSlice`].
#[derive(Clone, Debug)]
pub struct CowSliceSizeMismatchError(usize, usize);
impl std::fmt::Display for CowSliceSizeMismatchError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"size mismatch for immutable and mutable buffers: data.len() = {}, data_mut.len() = {}",
self.0, self.1
)
}
}
#[cfg(test)]
mod tests {
use super::{CowSlice, CowSliceSizeMismatchError};
#[test]
fn size_mismatch_error() {
let data_mut = &mut [0, 0];
let result = CowSlice::new(&[1, 2, 3], data_mut);
assert!(matches!(result, Err(CowSliceSizeMismatchError(3, 2))))
}
#[test]
fn copy_on_write() {
let data = std::array::from_fn::<_, 16, _>(|i| i as i32);
let mut data_mut = [0i32; 16];
let mut slice = CowSlice::new(&data, &mut data_mut).unwrap();
// Not mutable yet
assert!(!slice.use_mut);
for i in 0..data.len() {
assert_eq!(slice.get(i).unwrap(), i as i32);
}
// Modify all values
for i in 0..data.len() {
let value = slice.get(i).unwrap();
slice.set(i, value * 2).unwrap();
}
// Now we're mutable
assert!(slice.use_mut);
for i in 0..data.len() {
assert_eq!(slice.get(i).unwrap(), i as i32 * 2);
}
}
#[test]
fn out_of_bounds() {
let data_mut = &mut [1, 2];
let slice = CowSlice::new_mut(data_mut);
assert_eq!(slice.get(0), Some(1));
assert_eq!(slice.get(1), Some(2));
assert_eq!(slice.get(2), None);
}
}

View File

@@ -0,0 +1,34 @@
//! Control value table.
use super::{cow_slice::CowSlice, error::HintErrorKind, F26Dot6};
/// Backing store for the control value table.
///
/// This is just a wrapper for [`CowSlice`] that converts out of bounds
/// accesses to appropriate errors.
pub struct Cvt<'a>(CowSlice<'a>);
impl Cvt<'_> {
pub fn get(&self, index: usize) -> Result<F26Dot6, HintErrorKind> {
self.0
.get(index)
.map(F26Dot6::from_bits)
.ok_or(HintErrorKind::InvalidCvtIndex(index))
}
pub fn set(&mut self, index: usize, value: F26Dot6) -> Result<(), HintErrorKind> {
self.0
.set(index, value.to_bits())
.ok_or(HintErrorKind::InvalidCvtIndex(index))
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl<'a> From<CowSlice<'a>> for Cvt<'a> {
fn from(value: CowSlice<'a>) -> Self {
Self(value)
}
}

View File

@@ -0,0 +1,265 @@
//! Management of function and instruction definitions.
use core::ops::Range;
use super::{error::HintErrorKind, program::Program};
/// Code range and properties for a function or instruction definition.
// Note: this type is designed to support allocation from user memory
// so make sure the fields are all tightly packed and only use integral
// types.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttobjs.h#L158>
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
#[repr(C)]
pub struct Definition {
start: u32,
end: u32,
/// The function number for an FDEF or opcode for an IDEF.
key: i32,
_pad: u16,
program: u8,
is_active: u8,
}
impl Definition {
/// Creates a new definition with the given program, code range and
/// key.
///
/// The key is either a function number or opcode for function and
/// instruction definitions respectively.
pub fn new(program: Program, code_range: Range<usize>, key: i32) -> Self {
Self {
program: program as u8,
// Table sizes are specified in u32 so valid ranges will
// always fit.
start: code_range.start as u32,
end: code_range.end as u32,
key,
_pad: 0,
is_active: 1,
}
}
/// Returns the program that contains this definition.
pub fn program(&self) -> Program {
match self.program {
0 => Program::Font,
1 => Program::ControlValue,
_ => Program::Glyph,
}
}
/// Returns the function number or opcode.
#[cfg(test)]
pub fn key(&self) -> i32 {
self.key
}
/// Returns the byte range of the code for this definition in the source
/// program.
pub fn code_range(&self) -> Range<usize> {
self.start as usize..self.end as usize
}
/// Returns true if this definition entry has been defined by a program.
pub fn is_active(&self) -> bool {
self.is_active != 0
}
}
/// Map of function number or opcode to code definitions.
///
/// The `Ref` vs `Mut` distinction exists because these can be modified
/// from the font and control value programs but not from a glyph program.
/// In addition, hinting instance state is immutable once initialized so
/// this captures that in a type safe way.
pub enum DefinitionMap<'a> {
Ref(&'a [Definition]),
Mut(&'a mut [Definition]),
}
impl DefinitionMap<'_> {
/// Attempts to allocate a new definition entry with the given key.
///
/// Overriding a definition is legal, so if an existing active entry
/// is found with the same key, that one will be returned. Otherwise,
/// an inactive entry will be chosen.
pub fn allocate(&mut self, key: i32) -> Result<&mut Definition, HintErrorKind> {
let Self::Mut(defs) = self else {
return Err(HintErrorKind::DefinitionInGlyphProgram);
};
// First, see if we can use key as an index.
//
// For function definitions in well-behaved fonts (that is, where
// function numbers fall within 0..max_function_defs) this will
// always work.
//
// For instruction definitions, this will likely never work
// because the number of instruction definitions is usually small
// (nearly always 0) and the available opcodes are in the higher
// ranges of u8 space.
let ix = if defs
.get(key as usize)
.filter(|def| !def.is_active() || def.key == key)
.is_some()
{
// If the entry is inactive or the key matches, we're good.
key as usize
} else {
// Otherwise, walk backward looking for an active entry with
// a matching key. Keep track of the inactive entry with the
// highest index.
let mut last_inactive_ix = None;
for (i, def) in defs.iter().enumerate().rev() {
if def.is_active() {
if def.key == key {
last_inactive_ix = Some(i);
break;
}
} else if last_inactive_ix.is_none() {
last_inactive_ix = Some(i);
}
}
last_inactive_ix.ok_or(HintErrorKind::TooManyDefinitions)?
};
let def = defs.get_mut(ix).ok_or(HintErrorKind::TooManyDefinitions)?;
*def = Definition::new(Program::Font, 0..0, key);
Ok(def)
}
/// Returns the definition with the given key.
pub fn get(&self, key: i32) -> Result<&Definition, HintErrorKind> {
let defs = match self {
Self::Mut(defs) => *defs,
Self::Ref(defs) => *defs,
};
// Fast path, try to use key as index.
if let Some(def) = defs.get(key as usize) {
if def.is_active() && def.key == key {
return Ok(def);
}
}
// Otherwise, walk backward doing a linear search.
for def in defs.iter().rev() {
if def.is_active() && def.key == key {
return Ok(def);
}
}
Err(HintErrorKind::InvalidDefinition(key as _))
}
/// Returns a reference to the underlying definition slice.
#[cfg(test)]
fn as_slice(&self) -> &[Definition] {
match self {
Self::Ref(defs) => defs,
Self::Mut(defs) => defs,
}
}
/// If the map is mutable, resets all definitions to the default
/// value.
pub fn reset(&mut self) {
if let Self::Mut(defs) = self {
defs.fill(Default::default())
}
}
}
/// State containing font defined functions and instructions.
pub struct DefinitionState<'a> {
pub functions: DefinitionMap<'a>,
pub instructions: DefinitionMap<'a>,
}
impl<'a> DefinitionState<'a> {
pub fn new(functions: DefinitionMap<'a>, instructions: DefinitionMap<'a>) -> Self {
Self {
functions,
instructions,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn too_many_and_invalid() {
let mut buf = vec![Default::default(); 32];
let mut map = DefinitionMap::Mut(&mut buf);
for i in 0..32 {
map.allocate(i).unwrap();
}
assert!(matches!(
map.allocate(33),
Err(HintErrorKind::TooManyDefinitions)
));
assert!(matches!(
map.get(33),
Err(HintErrorKind::InvalidDefinition(33))
));
}
/// Test dense allocation where all keys map directly to indices. This is
/// the case for function definitions in well behaved fonts.
#[test]
fn allocate_dense() {
let mut buf = vec![Default::default(); 32];
let mut map = DefinitionMap::Mut(&mut buf);
for i in 0..32 {
map.allocate(i).unwrap();
}
for (i, def) in map.as_slice().iter().enumerate() {
let key = i as i32;
map.get(key).unwrap();
assert_eq!(def.key, key);
}
}
/// Test sparse allocation where keys never map to indices. This is
/// generally the case for instruction definitions and would apply
/// to fonts with function definition numbers that all fall outside
/// the range 0..max_function_defs.
#[test]
fn allocate_sparse() {
let mut buf = vec![Default::default(); 3];
let mut map = DefinitionMap::Mut(&mut buf);
let keys = [42, 88, 107];
for key in keys {
map.allocate(key).unwrap();
}
for key in keys {
assert_eq!(map.get(key).unwrap().key, key);
}
}
/// Test mixed allocation where some keys map to indices and others are
/// subject to fallback allocation. This would be the case for fonts
/// with function definition numbers where some fall inside the range
/// 0..max_function_defs but others don't.
#[test]
fn allocate_mixed() {
let mut buf = vec![Default::default(); 10];
let mut map = DefinitionMap::Mut(&mut buf);
let keys = [
0, 1, 2, 3, // Directly mapped to indices
123456, -42, -5555, // Fallback allocated
5, // Also directly mapped
7, // Would be direct but blocked by prior fallback
];
for key in keys {
map.allocate(key).unwrap();
}
// Check backing store directly to ensure the expected allocation
// pattern.
let expected = [0, 1, 2, 3, 0, 5, 7, -5555, -42, 123456];
let mapped_keys: Vec<_> = map.as_slice().iter().map(|def| def.key).collect();
assert_eq!(&expected, mapped_keys.as_slice());
// Check that all keys are mapped
for key in keys {
assert_eq!(map.get(key).unwrap().key, key);
}
}
}

View File

@@ -0,0 +1,244 @@
//! Arithmetic and math instructions.
//!
//! Implements 10 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#arithmetic-and-math-instructions>
use super::{super::math, Engine, HintErrorKind, OpResult};
impl Engine<'_> {
/// ADD[] (0x60)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 + n1)
///
/// Pops n1 and n2 off the stack and pushes the sum of the two elements
/// onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#add>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2866>
pub(super) fn op_add(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.wrapping_add(b)))
}
/// SUB[] (0x61)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 - n1)
///
/// Pops n1 and n2 off the stack and pushes the difference of the two
/// elements onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#subtract>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2879>
pub(super) fn op_sub(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.wrapping_sub(b)))
}
/// DIV[] (0x62)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 / n1)
///
/// Pops n1 and n2 off the stack and pushes onto the stack the quotient
/// obtained by dividing n2 by n1. Note that this truncates rather than
/// rounds the value.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#divide>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2892>
pub(super) fn op_div(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| {
if b == 0 {
Err(HintErrorKind::DivideByZero)
} else {
Ok(math::mul_div_no_round(a, 64, b))
}
})
}
/// MUL[] (0x63)
///
/// Pops: n1, n2 (F26Dot6)
/// Pushes: (n2 * n1)
///
/// Pops n1 and n2 off the stack and pushes onto the stack the product of
/// the two elements.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#multiply>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2909>
pub(super) fn op_mul(&mut self) -> OpResult {
self.value_stack
.apply_binary(|a, b| Ok(math::mul_div(a, b, 64)))
}
/// ABS[] (0x64)
///
/// Pops: n
/// Pushes: |n|: absolute value of n (F26Dot6)
///
/// Pops n off the stack and pushes onto the stack the absolute value of n.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#absolute-value>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2922>
pub(super) fn op_abs(&mut self) -> OpResult {
self.value_stack.apply_unary(|n| Ok(n.wrapping_abs()))
}
/// NEG[] (0x65)
///
/// Pops: n1
/// Pushes: -n1: negation of n1 (F26Dot6)
///
/// This instruction pops n1 off the stack and pushes onto the stack the
/// negated value of n1.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#negate>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2936>
pub(super) fn op_neg(&mut self) -> OpResult {
self.value_stack.apply_unary(|n1| Ok(n1.wrapping_neg()))
}
/// FLOOR[] (0x66)
///
/// Pops: n1: number whose floor is desired (F26Dot6)
/// Pushes: n: floor of n1 (F26Dot6)
///
/// Pops n1 and returns n, the greatest integer value less than or equal to n1.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#floor>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2949>
pub(super) fn op_floor(&mut self) -> OpResult {
self.value_stack.apply_unary(|n1| Ok(math::floor(n1)))
}
/// CEILING[] (0x67)
///
/// Pops: n1: number whose ceiling is desired (F26Dot6)
/// Pushes: n: ceiling of n1 (F26Dot6)
///
/// Pops n1 and returns n, the least integer value greater than or equal to n1.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#ceiling>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2962>
pub(super) fn op_ceiling(&mut self) -> OpResult {
self.value_stack.apply_unary(|n1| Ok(math::ceil(n1)))
}
/// MAX[] (0x8B)
///
/// Pops: e1, e2
/// Pushes: maximum of e1 and e2
///
/// Pops two elements, e1 and e2, from the stack and pushes the larger of
/// these two quantities onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#maximum-of-top-two-stack-elements>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3171>
pub(super) fn op_max(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.max(b)))
}
/// MIN[] (0x8C)
///
/// Pops: e1, e2
/// Pushes: minimum of e1 and e2
///
/// Pops two elements, e1 and e2, from the stack and pushes the smaller
/// of these two quantities onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#minimum-of-top-two-stack-elements>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3185>
pub(super) fn op_min(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok(a.min(b)))
}
}
#[cfg(test)]
mod tests {
use super::{super::MockEngine, math, HintErrorKind};
/// Test the binary operations that don't require fixed point
/// arithmetic.
#[test]
fn simple_binops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let input = &[a, b];
engine.test_exec(input, a + b, |engine| {
engine.op_add().unwrap();
});
engine.test_exec(input, a - b, |engine| {
engine.op_sub().unwrap();
});
engine.test_exec(input, a.max(b), |engine| {
engine.op_max().unwrap();
});
engine.test_exec(input, a.min(b), |engine| {
engine.op_min().unwrap();
});
}
}
}
/// Test the unary operations that don't require fixed point
/// arithmetic.
#[test]
fn simple_unops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
let input = &[a];
engine.test_exec(input, -a, |engine| {
engine.op_neg().unwrap();
});
engine.test_exec(input, a.abs(), |engine| {
engine.op_abs().unwrap();
});
}
}
#[test]
fn f26dot6_binops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let a = a * 64 + 30;
let b = b * 64 - 30;
let input = &[a, b];
engine.test_exec(input, math::mul_div(a, b, 64), |engine| {
engine.op_mul().unwrap();
});
if b != 0 {
engine.test_exec(input, math::mul_div_no_round(a, 64, b), |engine| {
engine.op_div().unwrap();
});
} else {
engine.value_stack.push(a).unwrap();
engine.value_stack.push(b).unwrap();
assert!(matches!(engine.op_div(), Err(HintErrorKind::DivideByZero)));
}
}
}
}
#[test]
fn f26dot6_unops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let a = a * 64 + b;
let input = &[a];
engine.test_exec(input, math::floor(a), |engine| {
engine.op_floor().unwrap();
});
engine.test_exec(input, math::ceil(a), |engine| {
engine.op_ceiling().unwrap();
});
}
}
}
}

View File

@@ -0,0 +1,319 @@
//! Managing the flow of control.
//!
//! Implements 6 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-flow-of-control>
use read_fonts::tables::glyf::bytecode::Opcode;
use super::{Engine, HintErrorKind, OpResult};
impl Engine<'_> {
/// If test.
///
/// IF[] (0x58)
///
/// Pops: e: stack element
///
/// Tests the element popped off the stack: if it is zero (FALSE), the
/// instruction pointer is jumped to the next ELSE or EIF instruction
/// in the instruction stream. If the element at the top of the stack is
/// nonzero (TRUE), the next instruction in the instruction stream is
/// executed. Execution continues until an ELSE instruction is encountered
/// or an EIF instruction ends the IF. If an else statement is found before
/// the EIF, the instruction pointer is moved to the EIF statement.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#if-test>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3334>
pub(super) fn op_if(&mut self) -> OpResult {
if self.value_stack.pop()? == 0 {
// The condition variable is false so we jump to the next
// ELSE or EIF but we have to skip intermediate IF/ELSE/EIF
// instructions.
let mut nest_depth = 1;
let mut out = false;
while !out {
let opcode = self.decode_next_opcode()?;
match opcode {
Opcode::IF => nest_depth += 1,
Opcode::ELSE => out = nest_depth == 1,
Opcode::EIF => {
nest_depth -= 1;
out = nest_depth == 0;
}
_ => {}
}
}
}
Ok(())
}
/// Else.
///
/// ELSE[] (0x1B)
///
/// Marks the start of the sequence of instructions that are to be executed
/// if an IF instruction encounters a FALSE value on the stack. This
/// sequence of instructions is terminated with an EIF instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#else>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3378>
pub(super) fn op_else(&mut self) -> OpResult {
let mut nest_depth = 1;
while nest_depth != 0 {
let opcode = self.decode_next_opcode()?;
match opcode {
Opcode::IF => nest_depth += 1,
Opcode::EIF => nest_depth -= 1,
_ => {}
}
}
Ok(())
}
/// End if.
///
/// EIF[] (0x59)
///
/// Marks the end of an IF[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#end-if>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3411>
pub(super) fn op_eif(&mut self) -> OpResult {
// Nothing
Ok(())
}
/// Jump relative on true.
///
/// JROT[] (0x78)
///
/// Pops: e: stack element
/// offset: number of bytes to move the instruction pointer
///
/// Pops and tests the element value, and then pops the offset. If the
/// element value is non-zero (TRUE), the signed offset will be added
/// to the instruction pointer and execution will be resumed at the address
/// obtained. Otherwise, the jump is not taken and the next instruction in
/// the instruction stream is executed. The jump is relative to the position
/// of the instruction itself. That is, the instruction pointer is still
/// pointing at the JROT[ ] instruction when offset is added to obtain the
/// new address.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump-relative-on-true>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3459>
pub(super) fn op_jrot(&mut self) -> OpResult {
let e = self.value_stack.pop()?;
self.do_jump(e != 0)
}
/// Jump.
///
/// JMPR[] (0x1C)
///
/// Pops: offset: number of bytes to move the instruction pointer
///
/// The signed offset is added to the instruction pointer and execution
/// is resumed at the new location in the instruction steam. The jump is
/// relative to the position of the instruction itself. That is, the
/// instruction pointer is still pointing at the JROT[] instruction when
/// offset is added to obtain the new address.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3424>
pub(super) fn op_jmpr(&mut self) -> OpResult {
self.do_jump(true)
}
/// Jump relative on false.
///
/// JROF[] (0x78)
///
/// Pops: e: stack element
/// offset: number of bytes to move the instruction pointer
///
/// Pops and tests the element value, and then pops the offset. If the
/// element value is non-zero (TRUE), the signed offset will be added
/// to the instruction pointer and execution will be resumed at the address
/// obtained. Otherwise, the jump is not taken and the next instruction in
/// the instruction stream is executed. The jump is relative to the position
/// of the instruction itself. That is, the instruction pointer is still
/// pointing at the JROT[ ] instruction when offset is added to obtain the
/// new address.
///
/// Pops and tests the element value, and then pops the offset. If the
/// element value is zero (FALSE), the signed offset will be added to the
/// nstruction pointer and execution will be resumed at the address
/// obtainted. Otherwise, the jump is not taken and the next instruction
/// in the instruction stream is executed. The jump is relative to the
/// position of the instruction itself. That is, the instruction pointer is
/// still pointing at the JROT[ ] instruction when the offset is added to
/// obtain the new address.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#jump-relative-on-false>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3474>
pub(super) fn op_jrof(&mut self) -> OpResult {
let e = self.value_stack.pop()?;
self.do_jump(e == 0)
}
/// Common code for jump instructions.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3424>
fn do_jump(&mut self, test: bool) -> OpResult {
// Offset is relative to previous jump instruction and decoder is
// already pointing to next instruction, so subtract one
let jump_offset = self.value_stack.pop()?.wrapping_sub(1);
if test {
if jump_offset < 0 {
if jump_offset == -1 {
// If the offset is -1, we'll just loop in place... forever
return Err(HintErrorKind::InvalidJump);
}
self.loop_budget.doing_backward_jump()?;
}
self.program.decoder.pc = self
.program
.decoder
.pc
.wrapping_add_signed(jump_offset as isize);
}
Ok(())
}
fn decode_next_opcode(&mut self) -> Result<Opcode, HintErrorKind> {
Ok(self
.program
.decoder
.decode()
.ok_or(HintErrorKind::UnexpectedEndOfBytecode)??
.opcode)
}
}
#[cfg(test)]
mod tests {
use super::{super::MockEngine, HintErrorKind, Opcode};
#[test]
fn if_else() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// Some code with nested ifs
#[rustfmt::skip]
let ops = [
IF,
ADD, // 1
SUB,
IF,
MUL, // 4
DIV,
ELSE, // 8
IUP0, // 7
IUP1,
EIF,
ELSE, // 10
RUTG, // 11
IF,
EIF,
EIF // 14
];
let bytecode = ops.map(|op| op as u8);
engine.program.decoder.bytecode = bytecode.as_slice();
// Outer if
{
// push a true value to enter the first branch
engine.program.decoder.pc = 1;
engine.value_stack.push(1).unwrap();
engine.op_if().unwrap();
assert_eq!(engine.program.decoder.pc, 1);
// false enters the else branch
engine.program.decoder.pc = 1;
engine.value_stack.push(0).unwrap();
engine.op_if().unwrap();
assert_eq!(engine.program.decoder.pc, 11);
}
// Inner if
{
// push a true value to enter the first branch
engine.program.decoder.pc = 4;
engine.value_stack.push(1).unwrap();
engine.op_if().unwrap();
assert_eq!(engine.program.decoder.pc, 4);
// false enters the else branch
engine.program.decoder.pc = 4;
engine.value_stack.push(0).unwrap();
engine.op_if().unwrap();
assert_eq!(engine.program.decoder.pc, 7);
}
// Else with nested if
{
// This jumps to the instruction after the next EIF, skipping any
// nested conditional blocks
engine.program.decoder.pc = 10;
engine.op_else().unwrap();
assert_eq!(engine.program.decoder.pc, 15);
engine.program.decoder.pc = 8;
engine.op_else().unwrap();
assert_eq!(engine.program.decoder.pc, 10);
}
}
#[test]
fn jumps() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// Unconditional jump
{
engine.program.decoder.pc = 1000;
engine.value_stack.push(100).unwrap();
engine.op_jmpr().unwrap();
assert_eq!(engine.program.decoder.pc, 1099);
}
// Jump if true
{
engine.program.decoder.pc = 1000;
// first test false condition, pc shouldn't change
engine.value_stack.push(100).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_jrot().unwrap();
assert_eq!(engine.program.decoder.pc, 1000);
// then true condition
engine.value_stack.push(100).unwrap();
engine.value_stack.push(1).unwrap();
engine.op_jrot().unwrap();
assert_eq!(engine.program.decoder.pc, 1099);
}
// Jump if false
{
engine.program.decoder.pc = 1000;
// first test true condition, pc shouldn't change
engine.value_stack.push(-100).unwrap();
engine.value_stack.push(1).unwrap();
engine.op_jrof().unwrap();
assert_eq!(engine.program.decoder.pc, 1000);
// then false condition
engine.value_stack.push(-100).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_jrof().unwrap();
assert_eq!(engine.program.decoder.pc, 899);
}
// Exhaust backward jump loop budget
{
engine.loop_budget.limit = 40;
for i in 0..45 {
engine.value_stack.push(-5).unwrap();
let result = engine.op_jmpr();
if i < 39 {
result.unwrap();
} else {
assert!(matches!(
result,
Err(HintErrorKind::ExceededExecutionBudget)
));
}
}
}
}
}

View File

@@ -0,0 +1,163 @@
//! Managing the control value table.
//!
//! Implements 3 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-control-value-table>
use super::{super::math::mul, Engine, F26Dot6, OpResult};
impl Engine<'_> {
/// Write control value table in pixel units.
///
/// WCVTP[] (0x44)
///
/// Pops: value: number in pixels (F26Dot6 fixed point number),
/// location: Control Value Table location (uint32)
///
/// Pops a location and a value from the stack and puts that value in the
/// specified location in the Control Value Table. This instruction assumes
/// the value is in pixels and not in FUnits.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#write-control-value-table-in-pixel-units>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3044>
pub(super) fn op_wcvtp(&mut self) -> OpResult {
let value = self.value_stack.pop_f26dot6()?;
let location = self.value_stack.pop_usize()?;
let result = self.cvt.set(location, value);
if self.graphics.is_pedantic {
result
} else {
Ok(())
}
}
/// Write control value table in font units.
///
/// WCVTF[] (0x70)
///
/// Pops: value: number in pixels (F26Dot6 fixed point number),
/// location: Control Value Table location (uint32)
///
/// Pops a location and a value from the stack and puts the specified
/// value in the specified address in the Control Value Table. This
/// instruction assumes the value is expressed in FUnits and not pixels.
/// The value is scaled before being written to the table.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#write-control-value-table-in-funits>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3067>
pub(super) fn op_wcvtf(&mut self) -> OpResult {
let value = self.value_stack.pop()?;
let location = self.value_stack.pop_usize()?;
let result = self.cvt.set(
location,
F26Dot6::from_bits(mul(value, self.graphics.scale)),
);
if self.graphics.is_pedantic {
result
} else {
Ok(())
}
}
/// Read control value table.
///
/// RCVT[] (0x45)
///
/// Pops: location: CVT entry number
/// Pushes: value: CVT value (F26Dot6)
///
/// Pops a location from the stack and pushes the value in the location
/// specified in the Control Value Table onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#read-control-value-table>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3090>
pub(super) fn op_rcvt(&mut self) -> OpResult {
let location = self.value_stack.pop()? as usize;
let maybe_value = self.cvt.get(location);
let value = if self.graphics.is_pedantic {
maybe_value?
} else {
maybe_value.unwrap_or_default()
};
self.value_stack.push(value.to_bits())
}
}
#[cfg(test)]
mod tests {
use super::super::{super::math, HintErrorKind, MockEngine};
#[test]
fn write_read() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for i in 0..8 {
engine.value_stack.push(i).unwrap();
engine.value_stack.push(i * 2).unwrap();
engine.op_wcvtp().unwrap();
}
for i in 0..8 {
engine.value_stack.push(i).unwrap();
engine.op_rcvt().unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), i * 2);
}
}
#[test]
fn write_scaled_read() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let scale = 64;
engine.graphics.scale = scale;
for i in 0..8 {
engine.value_stack.push(i).unwrap();
engine.value_stack.push(i * 2).unwrap();
// WCVTF takes a value in font units and converts to pixels
// with the current scale
engine.op_wcvtf().unwrap();
}
for i in 0..8 {
engine.value_stack.push(i).unwrap();
engine.op_rcvt().unwrap();
let value = engine.value_stack.pop().unwrap();
assert_eq!(value, math::mul(i * 2, scale));
}
}
#[test]
fn pedantry() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let oob_index = 1000;
// Disable pedantic mode: OOB writes are ignored, OOB reads
// push 0
engine.graphics.is_pedantic = false;
engine.value_stack.push(oob_index).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_wcvtp().unwrap();
engine.value_stack.push(oob_index).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_wcvtf().unwrap();
engine.value_stack.push(oob_index).unwrap();
engine.op_rcvt().unwrap();
// Enable pedantic mode: OOB reads/writes error
engine.graphics.is_pedantic = true;
engine.value_stack.push(oob_index).unwrap();
engine.value_stack.push(0).unwrap();
assert_eq!(
engine.op_wcvtp(),
Err(HintErrorKind::InvalidCvtIndex(oob_index as _))
);
engine.value_stack.push(oob_index).unwrap();
engine.value_stack.push(0).unwrap();
assert_eq!(
engine.op_wcvtf(),
Err(HintErrorKind::InvalidCvtIndex(oob_index as _))
);
engine.value_stack.push(oob_index).unwrap();
assert_eq!(
engine.op_rcvt(),
Err(HintErrorKind::InvalidCvtIndex(oob_index as _))
);
}
}

View File

@@ -0,0 +1,288 @@
//! Reading and writing data.
//!
//! Implements 7 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#reading-and-writing-data>
use super::{
super::{math, zone::ZonePointer},
Engine, OpResult,
};
impl Engine<'_> {
/// Get coordinate project in to the projection vector.
///
/// GC\[a\] (0x46 - 0x47)
///
/// a: 0: use current position of point p
/// 1: use the position of point p in the original outline
///
/// Pops: p: point number
/// Pushes: value: coordinate location (F26Dot6)
///
/// Measures the coordinate value of point p on the current
/// projection_vector and pushes the value onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#get-coordinate-projected-onto-the-projection_vector>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L4512>
pub(super) fn op_gc(&mut self, opcode: u8) -> OpResult {
let p = self.value_stack.pop_usize()?;
let gs = &mut self.graphics;
if !gs.is_pedantic && !gs.in_bounds([(gs.zp2, p)]) {
self.value_stack.push(0)?;
return Ok(());
}
let value = if (opcode & 1) != 0 {
gs.dual_project(gs.zp2().original(p)?, Default::default())
} else {
gs.project(gs.zp2().point(p)?, Default::default())
};
self.value_stack.push(value.to_bits())?;
Ok(())
}
/// Set coordinate from the stack using projection vector and freedom
/// vector.
///
/// SCFS[] (0x48)
///
/// Pops: value: distance from origin to move point (F26Dot6)
/// p: point number
///
/// Moves point p from its current position along the freedom_vector so
/// that its component along the projection_vector becomes the value popped
/// off the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#sets-coordinate-from-the-stack-using-projection_vector-and-freedom_vector>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L4550>
pub(super) fn op_scfs(&mut self) -> OpResult {
let value = self.value_stack.pop_f26dot6()?;
let p = self.value_stack.pop_usize()?;
let gs = &mut self.graphics;
let projection = gs.project(gs.zp2().point(p)?, Default::default());
gs.move_point(gs.zp2, p, value.wrapping_sub(projection))?;
if gs.zp2.is_twilight() {
let twilight = gs.zone_mut(ZonePointer::Twilight);
*twilight.original_mut(p)? = twilight.point(p)?;
}
Ok(())
}
/// Measure distance.
///
/// MD\[a\] (0x46 - 0x47)
///
/// a: 0: measure distance in grid-fitted outline
/// 1: measure distance in original outline
///
/// Pops: p1: point number
/// p2: point number
/// Pushes: distance (F26Dot6)
///
/// Measures the distance between outline point p1 and outline point p2.
/// The value returned is in pixels (F26Dot6) If distance is negative, it
/// was measured against the projection vector. Reversing the order in
/// which the points are listed will change the sign of the result.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#measure-distance>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L4593>
pub(super) fn op_md(&mut self, opcode: u8) -> OpResult {
let p1 = self.value_stack.pop_usize()?;
let p2 = self.value_stack.pop_usize()?;
let gs = &self.graphics;
if !gs.is_pedantic && !gs.in_bounds([(gs.zp0, p2), (gs.zp1, p1)]) {
self.value_stack.push(0)?;
return Ok(());
}
let distance = if (opcode & 1) != 0 {
// measure in grid fitted outline
gs.project(gs.zp0().point(p2)?, gs.zp1().point(p1)?)
.to_bits()
} else if gs.zp0.is_twilight() || gs.zp1.is_twilight() {
// special case for twilight zone
gs.dual_project(gs.zp0().original(p2)?, gs.zp1().original(p1)?)
.to_bits()
} else {
// measure in original unscaled outline
math::mul(
gs.dual_project_unscaled(gs.zp0().unscaled(p2), gs.zp1().unscaled(p1)),
gs.unscaled_to_pixels(),
)
};
self.value_stack.push(distance)
}
/// Measure pixels per em.
///
/// MPPEM[] (0x4B)
///
/// Pushes: ppem: pixels per em (uint32)
///
/// This instruction pushes the number of pixels per em onto the stack.
/// Pixels per em is a function of the resolution of the rendering device
/// and the current point size and the current transformation matrix.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#measure-pixels-per-em>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2609>
pub(super) fn op_mppem(&mut self) -> OpResult {
self.value_stack.push(self.graphics.ppem)
}
/// Measure point size.
///
/// MPS[] (0x4C)
///
/// Pushes: pointSize: the size in points of the current glyph (F26Dot6)
///
/// Measure point size can be used to obtain a value which serves as the
/// basis for choosing whether to branch to an alternative path through the
/// instruction stream. It makes it possible to treat point sizes below or
/// above a certain threshold differently.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#measure-point-size>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2623>
pub(super) fn op_mps(&mut self) -> OpResult {
// Note: FreeType computes this at
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttdriver.c#L392>
// which is mul_div(ppem, 64 * 72, resolution) where resolution
// is always 72 for our purposes (Skia), resulting in ppem * 64.
self.value_stack.push(self.graphics.ppem * 64)
}
}
#[cfg(test)]
mod tests {
use super::super::{super::zone::ZonePointer, math, Engine, MockEngine};
use raw::types::F26Dot6;
#[test]
fn measure_ppem_and_point_size() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let ppem = 20;
engine.graphics.ppem = ppem;
engine.op_mppem().unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), ppem);
engine.op_mps().unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), ppem * 64);
}
#[test]
fn gc() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
set_test_vectors(&mut engine);
// current point projected coord
let point = engine.graphics.zones[1].point_mut(1).unwrap();
point.x = F26Dot6::from_bits(132);
point.y = F26Dot6::from_bits(-256);
engine.value_stack.push(1).unwrap();
engine.op_gc(0).unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), 4);
// original point projected coord
let point = engine.graphics.zones[1].original_mut(1).unwrap();
point.x = F26Dot6::from_bits(-64);
point.y = F26Dot6::from_bits(521);
engine.value_stack.push(1).unwrap();
engine.op_gc(1).unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), 176);
}
#[test]
fn scfs() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
set_test_vectors(&mut engine);
// This instruction is a nop in backward compatibility mode
// and before IUP.
engine.graphics.backward_compatibility = false;
engine.graphics.did_iup_x = true;
engine.graphics.did_iup_y = true;
// use the twilight zone to test the optional code path
engine.graphics.zp2 = ZonePointer::Twilight;
let point = engine.graphics.zones[0].point_mut(1).unwrap();
point.x = F26Dot6::from_bits(132);
point.y = F26Dot6::from_bits(-256);
// assert we're not currently the same
assert_ne!(
engine.graphics.zones[0].point(1).unwrap(),
engine.graphics.zones[0].original(1).unwrap()
);
// push point number
engine.value_stack.push(1).unwrap();
// push value to match
engine.value_stack.push(42).unwrap();
// set coordinate from stack!
engine.op_scfs().unwrap();
let point = engine.graphics.zones[0].point(1).unwrap();
assert_eq!(point.x.to_bits(), 166);
assert_eq!(point.y.to_bits(), -239);
// ensure that we set original = point
assert_eq!(point, engine.graphics.zones[0].original(1).unwrap());
}
#[test]
fn md_scaled() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
set_test_vectors(&mut engine);
// first path, measure in grid fitted outline
let zone = engine.graphics.zone_mut(ZonePointer::Glyph);
let point1 = zone.point_mut(1).unwrap();
point1.x = F26Dot6::from_bits(132);
point1.y = F26Dot6::from_bits(-256);
let point2 = zone.point_mut(3).unwrap();
point2.x = F26Dot6::from_bits(-64);
point2.y = F26Dot6::from_bits(100);
// now measure
engine.value_stack.push(1).unwrap();
engine.value_stack.push(3).unwrap();
engine.op_md(1).unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), 16);
}
#[test]
fn md_unscaled() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
set_test_vectors(&mut engine);
// second path, measure in original unscaled outline.
// unscaled points are set in mock engine but we need a scale
engine.graphics.scale = 375912;
engine.value_stack.push(1).unwrap();
engine.value_stack.push(3).unwrap();
engine.op_md(0).unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), 11);
}
#[test]
fn md_twilight() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
set_test_vectors(&mut engine);
// final path, measure in original outline, in twilight zone
engine.graphics.zp0 = ZonePointer::Twilight;
engine.graphics.zp1 = ZonePointer::Twilight;
// set some points
let zone = engine.graphics.zone_mut(ZonePointer::Twilight);
let point1 = zone.original_mut(1).unwrap();
point1.x = F26Dot6::from_bits(132);
point1.y = F26Dot6::from_bits(-256);
let point2 = zone.original_mut(3).unwrap();
point2.x = F26Dot6::from_bits(-64);
point2.y = F26Dot6::from_bits(100);
// now measure
engine.value_stack.push(1).unwrap();
engine.value_stack.push(3).unwrap();
engine.op_md(0).unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), 16);
}
fn set_test_vectors(engine: &mut Engine) {
let v = math::normalize14(100, 50);
engine.graphics.proj_vector = v;
engine.graphics.dual_proj_vector = v;
engine.graphics.freedom_vector = v;
engine.graphics.update_projection_state();
}
}

View File

@@ -0,0 +1,522 @@
//! Defining and using functions and instructions.
//!
//! Implements 5 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#defining-and-using-functions-and-instructions>
use read_fonts::tables::glyf::bytecode::Opcode;
use super::{
super::{definition::Definition, program::Program},
Engine, HintErrorKind, OpResult,
};
/// [Functions|Instructions] may not exceed 64K in size.
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#function-definition>
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#instruction-definition>
const MAX_DEFINITION_SIZE: usize = u16::MAX as usize;
impl Engine<'_> {
/// Function definition.
///
/// FDEF[] (0x2C)
///
/// Pops: f: function identifier number
///
/// Marks the start of a function definition. The argument f is a number
/// that uniquely identifies this function. A function definition can
/// appear only in the Font Program or the CVT program; attempts to invoke
/// the FDEF instruction within a glyph program will result in an error.
/// Functions may not exceed 64K in size.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#function-definition>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3496>
pub(super) fn op_fdef(&mut self) -> OpResult {
let f = self.value_stack.pop()?;
self.do_def(DefKind::Function, f)
}
/// End function definition.
///
/// ENDF[] (0x2D)
///
/// Marks the end of a function definition or an instruction definition.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#end-function-definition>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3578>
pub(super) fn op_endf(&mut self) -> OpResult {
self.program.leave()
}
/// Call function.
///
/// CALL[] (0x2B)
///
/// Pops: f: function identifier number
///
/// Calls the function identified by the number f.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#call-function>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3623>
pub(super) fn op_call(&mut self) -> OpResult {
let f = self.value_stack.pop()?;
self.do_call(DefKind::Function, 1, f)
}
/// Loop and call function.
///
/// LOOPCALL[] (0x2a)
///
/// Pops: f: function identifier number
/// count: number of times to call the function
///
/// Calls the function f, count number of times.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#loop-and-call-function>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3704>
pub(super) fn op_loopcall(&mut self) -> OpResult {
let f = self.value_stack.pop()?;
let count = self.value_stack.pop()?;
if count > 0 {
self.loop_budget.doing_loop_call(count as usize)?;
self.do_call(DefKind::Function, count as u32, f)
} else {
Ok(())
}
}
/// Instruction definition.
///
/// IDEF[] (0x89)
///
/// Pops: opcode
///
/// Begins the definition of an instruction. The instruction definition
/// terminates when at ENDF, which is encountered in the instruction
/// stream. Subsequent executions of the opcode popped will be directed
/// to the contents of this instruction definition (IDEF). IDEFs must be
/// defined in the Font Program or the CVT Program; attempts to invoke the
/// IDEF instruction within a glyph program will result in an error. An
/// IDEF affects only undefined opcodes. If the opcode in question is
/// already defined, the interpreter will ignore the IDEF. This is to be
/// used as a patching mechanism for future instructions. Instructions
/// may not exceed 64K in size.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#instruction-definition>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3788>
pub(super) fn op_idef(&mut self) -> OpResult {
let opcode = self.value_stack.pop()?;
self.do_def(DefKind::Instruction, opcode)
}
/// Catch all for unhandled opcodes which will attempt to dispatch to a
/// user defined instruction.
pub(super) fn op_unknown(&mut self, opcode: u8) -> OpResult {
self.do_call(DefKind::Instruction, 1, opcode as i32)
}
/// Common code for FDEF and IDEF.
fn do_def(&mut self, kind: DefKind, key: i32) -> OpResult {
if self.program.initial == Program::Glyph {
return Err(HintErrorKind::DefinitionInGlyphProgram);
}
let defs = match kind {
DefKind::Function => &mut self.definitions.functions,
DefKind::Instruction => &mut self.definitions.instructions,
};
let def = defs.allocate(key)?;
let start = self.program.decoder.pc;
while let Some(ins) = self.program.decoder.decode() {
let ins = ins?;
match ins.opcode {
Opcode::FDEF | Opcode::IDEF => return Err(HintErrorKind::NestedDefinition),
Opcode::ENDF => {
let range = start..ins.pc + 1;
if self.graphics.is_pedantic && range.len() > MAX_DEFINITION_SIZE {
*def = Default::default();
return Err(HintErrorKind::DefinitionTooLarge);
}
*def = Definition::new(self.program.current, range, key);
return Ok(());
}
_ => {}
}
}
Err(HintErrorKind::UnexpectedEndOfBytecode)
}
/// Common code for CALL, LOOPCALL and unknown opcode handling.
fn do_call(&mut self, kind: DefKind, count: u32, key: i32) -> OpResult {
if count == 0 {
return Ok(());
}
let def = match kind {
DefKind::Function => self.definitions.functions.get(key),
DefKind::Instruction => match self.definitions.instructions.get(key) {
// Remap an invalid definition error to unhandled opcode
Err(HintErrorKind::InvalidDefinition(opcode)) => Err(
HintErrorKind::UnhandledOpcode(Opcode::from_byte(opcode as u8)),
),
result => result,
},
};
self.program.enter(*def?, count)
}
}
enum DefKind {
Function,
Instruction,
}
#[cfg(test)]
mod tests {
use super::{
super::{
super::program::{Program, ProgramState},
Engine, MockEngine,
},
HintErrorKind, Opcode, MAX_DEFINITION_SIZE,
};
/// Define two functions, one of which calls the other with
/// both CALL and LOOPCALL.
#[test]
fn define_function_call_loopcall() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(PUSHB001), 1, 0,
// FDEF 0: adds 2 to top stack value
op(FDEF),
op(PUSHB000), 2,
op(ADD),
op(ENDF),
// FDEF 1: calls FDEF 0 once, loop calls 5 times, then
// negates the result
op(FDEF),
op(PUSHB000), 0,
op(CALL),
op(PUSHB001), 5, 0,
op(LOOPCALL),
op(NEG),
op(ENDF),
];
// Execute this code to define our functions
engine.set_font_code(&font_code);
engine.run().unwrap();
// Call FDEF 1 with value of 10 on the stack:
// * calls FDEF 0 which adds 2
// * loop calls FDEF 0 an additional 5 times which adds a total of 10
// * then negates the result
// leaving -22 on the stack
engine.value_stack.push(10).unwrap();
engine.value_stack.push(1).unwrap();
engine.op_call().unwrap();
engine.run().unwrap();
assert_eq!(engine.value_stack.pop().ok(), Some(-22));
}
/// Control value programs can override functions defined in the font
/// program based on instance state.
#[test]
fn override_function() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(PUSHB001), 0, 0,
// FDEF 0: adds 2 to top stack value
op(FDEF),
op(PUSHB000), 2,
op(ADD),
op(ENDF),
// Redefine FDEF 0: subtract 2 instead
op(FDEF),
op(PUSHB000), 2,
op(SUB),
op(ENDF),
];
// Execute this code to define our functions
engine.set_font_code(&font_code);
engine.run().unwrap();
// Call FDEF 0 with value of 10 on the stack:
// * should subtract 2 rather than add
// leaving 8 on the stack
engine.value_stack.push(10).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_call().unwrap();
engine.run().unwrap();
assert_eq!(engine.value_stack.pop().ok(), Some(8));
}
/// Executes a call from a CV program into a font program.
///
/// Tests ProgramState bytecode/decoder management.
#[test]
fn call_different_program() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(PUSHB000), 0,
// FDEF 0: adds 2 to top stack value
op(FDEF),
op(PUSHB000), 2,
op(ADD),
op(ENDF),
];
#[rustfmt::skip]
let cv_code = [
// Call function defined in font program and negate result
op(PUSHB001), 40, 0,
op(CALL),
op(NEG)
];
let glyph_code = &[];
// Run font program first to define the function
engine.program = ProgramState::new(&font_code, &cv_code, glyph_code, Program::Font);
engine.run().unwrap();
// Now run CV program which calls into the font program
engine.program = ProgramState::new(&font_code, &cv_code, glyph_code, Program::ControlValue);
engine.run().unwrap();
// Executing CV program:
// * pushes 40 to the stack
// * calls FDEF 0 in font program which adds 2
// * returns to CV program
// * negates the value
// leaving -42 on the stack
assert_eq!(engine.value_stack.pop().ok(), Some(-42));
}
/// Fail when we exceed loop call budget.
#[test]
fn loopcall_budget() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let limit = engine.loop_budget.limit;
#[rustfmt::skip]
let font_code = [
op(PUSHB001), 1, 0,
// FDEF 0: does nothing
op(FDEF),
op(ENDF),
// FDEF 1: loop calls FDEF 0 twice, exceeding the budget on the
// second attempt
op(FDEF),
op(PUSHB001), limit as u8, 0,
op(LOOPCALL),
op(PUSHB001), 1, 0,
op(LOOPCALL), // pc = 13
op(ENDF),
];
// Execute this code to define our functions
engine.set_font_code(&font_code);
engine.run().unwrap();
// Call FDEF 1 which attempts to loop call FDEF 0 (limit + 1) times
engine.value_stack.push(10).unwrap();
engine.value_stack.push(1).unwrap();
engine.op_call().unwrap();
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::ExceededExecutionBudget));
assert_eq!(err.pc, 13);
}
/// Defines an instruction using an available opcode and executes it.
#[test]
fn define_instruction_and_use() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
// IDEF 0x93: adds 2 to top stack value
op(PUSHB000), op(INS93),
op(IDEF),
op(PUSHB000), 2,
op(ADD),
op(ENDF),
// FDEF 0: uses defined instruction 0x93 and negates the result
op(PUSHB000), 0,
op(FDEF),
op(INS93),
op(NEG),
op(ENDF),
];
// Execute this code to define our functions
engine.set_font_code(&font_code);
engine.run().unwrap();
// Call FDEF 0 with value of 10 on the stack:
// * executes defined instruction 0x93
// * then negates the result
// leaving -12 on the stack
engine.value_stack.push(10).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_call().unwrap();
engine.run().unwrap();
assert_eq!(engine.value_stack.pop().ok(), Some(-12));
}
// Invalid to nest definitions.
#[test]
fn nested_definition() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(PUSHB001), 1, 0,
op(FDEF), // pc = 3
op(FDEF),
op(ENDF),
op(ENDF),
];
// Execute this code to define our functions
engine.set_font_code(&font_code);
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::NestedDefinition));
assert_eq!(err.pc, 3);
}
// Invalid to modify definitions from the glyph program.
#[test]
fn definition_in_glyph_program() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(PUSHB000), 0,
op(FDEF), // pc = 2
op(ENDF),
];
engine.set_font_code(&font_code);
engine.program.initial = Program::Glyph;
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::DefinitionInGlyphProgram));
assert_eq!(err.pc, 2);
}
#[test]
fn undefined_function() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
engine.value_stack.push(111).unwrap();
assert!(matches!(
engine.op_call(),
Err(HintErrorKind::InvalidDefinition(111))
));
}
/// Fun function that just calls itself :)
#[test]
fn infinite_recursion() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
// FDEF 0: call FDEF 0
op(PUSHB000), 0,
op(FDEF),
op(PUSHB000), 0,
op(CALL), // pc = 5
op(ENDF),
];
engine.set_font_code(&font_code);
engine.run().unwrap();
// Call stack overflow
engine.value_stack.push(0).unwrap();
engine.op_call().unwrap();
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::CallStackOverflow));
assert_eq!(err.pc, 5);
}
#[test]
fn call_stack_underflow() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(ENDF)
];
engine.set_font_code(&font_code);
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::CallStackUnderflow));
assert_eq!(err.pc, 0);
}
#[test]
fn unhandled_opcode() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(Opcode::INS28),
];
engine.set_font_code(&font_code);
let err = engine.run().unwrap_err();
assert!(matches!(
err.kind,
HintErrorKind::UnhandledOpcode(Opcode::INS28)
));
assert_eq!(err.pc, 0);
}
#[test]
fn too_many_definitions() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
#[rustfmt::skip]
let font_code = [
op(PUSHB101), 0, 1, 2, 3, 4, 5,
op(FDEF), op(ENDF),
op(FDEF), op(ENDF),
op(FDEF), op(ENDF),
op(FDEF), op(ENDF),
op(FDEF), op(ENDF),
op(FDEF), op(ENDF),
];
engine.set_font_code(&font_code);
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::TooManyDefinitions));
assert_eq!(err.pc, 17);
}
#[test]
fn big_definition() {
use Opcode::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let mut font_code = vec![];
font_code.extend_from_slice(&[op(PUSHB000), 0, op(FDEF)]);
font_code.extend(core::iter::repeat_n(op(NEG), MAX_DEFINITION_SIZE + 1));
font_code.push(op(ENDF));
engine.set_font_code(&font_code);
engine.graphics.is_pedantic = true;
engine.value_stack.push(1).unwrap();
let err = engine.run().unwrap_err();
assert!(matches!(err.kind, HintErrorKind::DefinitionTooLarge));
assert_eq!(err.pc, 2);
}
fn op(opcode: Opcode) -> u8 {
opcode as u8
}
impl<'a> Engine<'a> {
fn set_font_code(&mut self, code: &'a [u8]) {
self.program.bytecode[0] = code;
self.program.decoder.bytecode = code;
self.program.current = Program::Font;
}
}
}

View File

@@ -0,0 +1,258 @@
//! Managing delta exceptions.
//!
//! Implements 6 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-exceptions>
use super::{super::graphics::CoordAxis, Engine, F26Dot6, OpResult};
use read_fonts::tables::glyf::bytecode::Opcode;
impl Engine<'_> {
/// Delta exception P1, P2 and P3.
///
/// DELTAP1[] (0x5D)
/// DELTAP2[] (0x71)
/// DELTAP3[] (0x72)
///
/// Pops: n: number of pairs of exception specifications and points (uint32)
/// p1, arg1, p2, arg2, ..., pnn argn: n pairs of exception specifications
/// and points (pairs of uint32s)
///
/// DELTAP moves the specified points at the size and by the
/// amount specified in the paired argument. An arbitrary number of points
/// and arguments can be specified.
///
/// The only difference between the instructions is the bias added to the
/// point adjustment.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#delta-exception-p1>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6509>
pub(super) fn op_deltap(&mut self, opcode: Opcode) -> OpResult {
let gs = &mut self.graphics;
let ppem = gs.ppem as u32;
let point_count = gs.zp0().points.len();
let n = self.value_stack.pop_count_checked()?;
// Each exception requires two values on the stack so limit our
// count to prevent looping in non-pedantic mode (where the stack ops
// will produce 0 instead of an underflow error)
let n = n.min(self.value_stack.len() / 2);
let bias = match opcode {
Opcode::DELTAP2 => 16,
Opcode::DELTAP3 => 32,
_ => 0,
} + gs.delta_base as u32;
let back_compat = gs.backward_compatibility;
let did_iup = gs.did_iup_x && gs.did_iup_y;
for _ in 0..n {
let point_ix = self.value_stack.pop_usize()?;
let mut b = self.value_stack.pop()?;
// FreeType notes that some popular fonts contain invalid DELTAP
// instructions so out of bounds points are ignored.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6537>
if point_ix >= point_count {
continue;
}
let mut c = (b as u32 & 0xF0) >> 4;
c += bias;
if ppem == c {
// Blindly copying FreeType here
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6565>
b = (b & 0xF) - 8;
if b >= 0 {
b += 1;
}
b *= 1 << (6 - gs.delta_shift as i32);
let distance = F26Dot6::from_bits(b);
if back_compat {
if !did_iup
&& ((gs.is_composite && gs.freedom_vector.y != 0)
|| gs.zp0().is_touched(point_ix, CoordAxis::Y)?)
{
gs.move_point(gs.zp0, point_ix, distance)?;
}
} else {
gs.move_point(gs.zp0, point_ix, distance)?;
}
}
}
Ok(())
}
/// Delta exception C1, C2 and C3.
///
/// DELTAC1[] (0x73)
/// DELTAC2[] (0x74)
/// DELTAC3[] (0x75)
///
/// Pops: n: number of pairs of exception specifications and CVT entry numbers (uint32)
/// c1, arg1, c2, arg2,..., cn, argn: (pairs of uint32s)
///
/// DELTAC changes the value in each CVT entry specified at the size and
/// by the amount specified in its paired argument.
///
/// The only difference between the instructions is the bias added to the
/// adjustment.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#delta-exception-c1>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6604>
pub(super) fn op_deltac(&mut self, opcode: Opcode) -> OpResult {
let gs = &mut self.graphics;
let ppem = gs.ppem as u32;
let n = self.value_stack.pop_count_checked()?;
// Each exception requires two values on the stack so limit our
// count to prevent looping in non-pedantic mode (where the stack ops
// will produce 0 instead of an underflow error)
let n = n.min(self.value_stack.len() / 2);
let bias = match opcode {
Opcode::DELTAC2 => 16,
Opcode::DELTAC3 => 32,
_ => 0,
} + gs.delta_base as u32;
for _ in 0..n {
let cvt_ix = self.value_stack.pop_usize()?;
let mut b = self.value_stack.pop()?;
let mut c = (b as u32 & 0xF0) >> 4;
c += bias;
if ppem == c {
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6660>
b = (b & 0xF) - 8;
if b >= 0 {
b += 1;
}
b *= 1 << (6 - gs.delta_shift as i32);
let cvt_val = self.cvt.get(cvt_ix)?;
self.cvt.set(cvt_ix, cvt_val + F26Dot6::from_bits(b))?;
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::super::{super::zone::ZonePointer, HintErrorKind, MockEngine};
use raw::{
tables::glyf::bytecode::Opcode,
types::{F26Dot6, Point},
};
#[test]
fn deltap() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
engine.graphics.backward_compatibility = false;
engine.graphics.zp0 = ZonePointer::Glyph;
let raw_ppem = 16;
let raw_adjustment = 7;
for (point_ix, (ppem_bias, opcode)) in [
(0, Opcode::DELTAP1),
(16, Opcode::DELTAP2),
(32, Opcode::DELTAP3),
]
.iter()
.enumerate()
{
let ppem = raw_ppem + ppem_bias;
engine.graphics.ppem = ppem;
// packed ppem + adjustment entry
let packed_ppem = raw_ppem - engine.graphics.delta_base as i32;
engine
.value_stack
.push((packed_ppem << 4) | raw_adjustment)
.unwrap();
// point index
engine.value_stack.push(point_ix as _).unwrap();
// exception count
engine.value_stack.push(1).unwrap();
engine.op_deltap(*opcode).unwrap();
let point = engine.graphics.zones[1].point(point_ix).unwrap();
assert_eq!(point.map(F26Dot6::to_bits), Point::new(-8, 0));
}
}
#[test]
fn deltac() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let raw_ppem = 16;
let raw_adjustment = 7;
for (cvt_ix, (ppem_bias, opcode)) in [
(0, Opcode::DELTAC1),
(16, Opcode::DELTAC2),
(32, Opcode::DELTAC3),
]
.iter()
.enumerate()
{
let ppem = raw_ppem + ppem_bias;
engine.graphics.ppem = ppem;
// packed ppem + adjustment entry
let packed_ppem = raw_ppem - engine.graphics.delta_base as i32;
engine
.value_stack
.push((packed_ppem << 4) | raw_adjustment)
.unwrap();
// cvt index
engine.value_stack.push(cvt_ix as _).unwrap();
// exception count
engine.value_stack.push(1).unwrap();
engine.op_deltac(*opcode).unwrap();
let value = engine.cvt.get(cvt_ix).unwrap();
assert_eq!(value.to_bits(), -8);
}
}
/// Fuzzer detected timeout when the count supplied for deltap was
/// negative. Converting to unsigned resulted in an absurdly high
/// number leading to timeout.
/// See <https://issues.oss-fuzz.com/issues/42538387>
/// and <https://github.com/googlefonts/fontations/issues/1290>
#[test]
fn deltap_negative_count() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// We don't care about the parameters to the instruction except
// for the count which is set to -1
let stack = [0, 0, -1];
// Non-pedantic mode: we end up with a count of 0 so do nothing
for value in &stack {
engine.value_stack.push(*value).unwrap();
}
// This just shouldn't hang the tests
engine.op_deltap(Opcode::DELTAP3).unwrap();
// Pedantic mode: raise an error
engine.value_stack.is_pedantic = true;
for value in &stack {
engine.value_stack.push(*value).unwrap();
}
assert!(matches!(
engine.op_deltap(Opcode::DELTAP3),
Err(HintErrorKind::InvalidStackValue(-1))
));
}
/// Copy of the above test for DELTAC
#[test]
fn deltac_negative_count() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// We don't care about the parameters to the instruction except
// for the count which is set to -1
let stack = [0, 0, -1];
// Non-pedantic mode: we end up with a count of 0 so do nothing
for value in &stack {
engine.value_stack.push(*value).unwrap();
}
// This just shouldn't hang the tests
engine.op_deltac(Opcode::DELTAC3).unwrap();
// Pedantic mode: raise an error
engine.value_stack.is_pedantic = true;
for value in &stack {
engine.value_stack.push(*value).unwrap();
}
assert!(matches!(
engine.op_deltac(Opcode::DELTAC3),
Err(HintErrorKind::InvalidStackValue(-1))
));
}
}

View File

@@ -0,0 +1,243 @@
//! Instruction decoding and dispatch.
use read_fonts::tables::glyf::bytecode::Opcode;
use super::{super::program::Program, Engine, HintError, HintErrorKind, Instruction};
/// Maximum number of instructions we will execute in `Engine::run()`. This
/// is used to ensure termination of a hinting program.
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/include/freetype/config/ftoption.h#L744>
const MAX_RUN_INSTRUCTIONS: usize = 1_000_000;
impl<'a> Engine<'a> {
/// Resets state for the specified program and executes all instructions.
pub fn run_program(&mut self, program: Program, is_pedantic: bool) -> Result<(), HintError> {
self.reset(program, is_pedantic);
self.run()
}
/// Set internal state for running the specified program.
pub fn reset(&mut self, program: Program, is_pedantic: bool) {
self.program.reset(program);
// Reset overall graphics state, keeping the retained bits.
self.graphics.reset();
self.graphics.is_pedantic = is_pedantic;
self.loop_budget.reset();
// Program specific setup.
match program {
Program::Font => {
self.definitions.functions.reset();
self.definitions.instructions.reset();
}
Program::ControlValue => {
self.graphics.backward_compatibility = false;
}
Program::Glyph => {
// Instruct control bit 1 says we reset retained graphics state
// to default values.
if self.graphics.instruct_control & 2 != 0 {
self.graphics.reset_retained();
}
// Set backward compatibility mode
if self.graphics.target.preserve_linear_metrics() {
self.graphics.backward_compatibility = true;
} else if self.graphics.target.is_smooth() {
self.graphics.backward_compatibility =
(self.graphics.instruct_control & 0x4) == 0;
} else {
self.graphics.backward_compatibility = false;
}
}
}
}
/// Decodes and dispatches all instructions until completion or error.
pub fn run(&mut self) -> Result<(), HintError> {
let mut count = 0;
while let Some(ins) = self.decode() {
let ins = ins?;
self.dispatch(&ins)?;
count += 1;
if count > MAX_RUN_INSTRUCTIONS {
return Err(HintError {
program: self.program.current,
glyph_id: None,
pc: ins.pc,
opcode: Some(ins.opcode),
kind: HintErrorKind::ExceededExecutionBudget,
});
}
}
Ok(())
}
/// Decodes the next instruction from the current program.
pub fn decode(&mut self) -> Option<Result<Instruction<'a>, HintError>> {
let ins = self.program.decoder.decode()?;
Some(ins.map_err(|_| HintError {
program: self.program.current,
glyph_id: None,
pc: self.program.decoder.pc,
opcode: None,
kind: HintErrorKind::UnexpectedEndOfBytecode,
}))
}
/// Executes the appropriate code for the given instruction.
pub fn dispatch(&mut self, ins: &Instruction) -> Result<(), HintError> {
let current_program = self.program.current;
self.dispatch_inner(ins).map_err(|kind| HintError {
program: current_program,
glyph_id: None,
pc: ins.pc,
opcode: Some(ins.opcode),
kind,
})
}
fn dispatch_inner(&mut self, ins: &Instruction) -> Result<(), HintErrorKind> {
use Opcode::*;
let opcode = ins.opcode;
let raw_opcode = opcode as u8;
match ins.opcode {
SVTCA0 | SVTCA1 | SPVTCA0 | SPVTCA1 | SFVTCA0 | SFVTCA1 => self.op_svtca(raw_opcode)?,
SPVTL0 | SPVTL1 | SFVTL0 | SFVTL1 => self.op_svtl(raw_opcode)?,
SPVFS => self.op_spvfs()?,
SFVFS => self.op_sfvfs()?,
GPV => self.op_gpv()?,
GFV => self.op_gfv()?,
SFVTPV => self.op_sfvtpv()?,
ISECT => self.op_isect()?,
SRP0 => self.op_srp0()?,
SRP1 => self.op_srp1()?,
SRP2 => self.op_srp2()?,
SZP0 => self.op_szp0()?,
SZP1 => self.op_szp1()?,
SZP2 => self.op_szp2()?,
SZPS => self.op_szps()?,
SLOOP => self.op_sloop()?,
RTG => self.op_rtg()?,
RTHG => self.op_rthg()?,
SMD => self.op_smd()?,
ELSE => self.op_else()?,
JMPR => self.op_jmpr()?,
SCVTCI => self.op_scvtci()?,
SSWCI => self.op_sswci()?,
SSW => self.op_ssw()?,
DUP => self.op_dup()?,
POP => self.op_pop()?,
CLEAR => self.op_clear()?,
SWAP => self.op_swap()?,
DEPTH => self.op_depth()?,
CINDEX => self.op_cindex()?,
MINDEX => self.op_mindex()?,
ALIGNPTS => self.op_alignpts()?,
// UNUSED: 0x28
UTP => self.op_utp()?,
LOOPCALL => self.op_loopcall()?,
CALL => self.op_call()?,
FDEF => self.op_fdef()?,
ENDF => self.op_endf()?,
MDAP0 | MDAP1 => self.op_mdap(raw_opcode)?,
IUP0 | IUP1 => self.op_iup(raw_opcode)?,
SHP0 | SHP1 => self.op_shp(raw_opcode)?,
SHC0 | SHC1 => self.op_shc(raw_opcode)?,
SHZ0 | SHZ1 => self.op_shz(raw_opcode)?,
SHPIX => self.op_shpix()?,
IP => self.op_ip()?,
MSIRP0 | MSIRP1 => self.op_msirp(raw_opcode)?,
ALIGNRP => self.op_alignrp()?,
RTDG => self.op_rtdg()?,
MIAP0 | MIAP1 => self.op_miap(raw_opcode)?,
NPUSHB | NPUSHW => self.op_push(&ins.inline_operands)?,
WS => self.op_ws()?,
RS => self.op_rs()?,
WCVTP => self.op_wcvtp()?,
RCVT => self.op_rcvt()?,
GC0 | GC1 => self.op_gc(raw_opcode)?,
SCFS => self.op_scfs()?,
MD0 | MD1 => self.op_md(raw_opcode)?,
MPPEM => self.op_mppem()?,
MPS => self.op_mps()?,
FLIPON => self.op_flipon()?,
FLIPOFF => self.op_flipoff()?,
// Should be unused in production fonts, but we may want to
// support debugging at some point. Just pops a value from
// the stack.
DEBUG => {
self.value_stack.pop()?;
}
LT => self.op_lt()?,
LTEQ => self.op_lteq()?,
GT => self.op_gt()?,
GTEQ => self.op_gteq()?,
EQ => self.op_eq()?,
NEQ => self.op_neq()?,
ODD => self.op_odd()?,
EVEN => self.op_even()?,
IF => self.op_if()?,
EIF => self.op_eif()?,
AND => self.op_and()?,
OR => self.op_or()?,
NOT => self.op_not()?,
DELTAP1 => self.op_deltap(opcode)?,
SDB => self.op_sdb()?,
SDS => self.op_sds()?,
ADD => self.op_add()?,
SUB => self.op_sub()?,
DIV => self.op_div()?,
MUL => self.op_mul()?,
ABS => self.op_abs()?,
NEG => self.op_neg()?,
FLOOR => self.op_floor()?,
CEILING => self.op_ceiling()?,
ROUND00 | ROUND01 | ROUND10 | ROUND11 => self.op_round()?,
// "No round" means do nothing :)
NROUND00 | NROUND01 | NROUND10 | NROUND11 => {}
WCVTF => self.op_wcvtf()?,
DELTAP2 | DELTAP3 => self.op_deltap(opcode)?,
DELTAC1 | DELTAC2 | DELTAC3 => self.op_deltac(opcode)?,
SROUND => self.op_sround()?,
S45ROUND => self.op_s45round()?,
JROT => self.op_jrot()?,
JROF => self.op_jrof()?,
ROFF => self.op_roff()?,
// UNUSED: 0x7B
RUTG => self.op_rutg()?,
RDTG => self.op_rdtg()?,
SANGW => self.op_sangw()?,
// Unsupported instruction, do nothing
AA => {}
FLIPPT => self.op_flippt()?,
FLIPRGON => self.op_fliprgon()?,
FLIPRGOFF => self.op_fliprgoff()?,
// UNUSED: 0x83 | 0x84
SCANCTRL => self.op_scanctrl()?,
SDPVTL0 | SDPVTL1 => self.op_sdpvtl(raw_opcode)?,
GETINFO => self.op_getinfo()?,
IDEF => self.op_idef()?,
ROLL => self.op_roll()?,
MAX => self.op_max()?,
MIN => self.op_min()?,
SCANTYPE => self.op_scantype()?,
INSTCTRL => self.op_instctrl()?,
// UNUSED: 0x8F | 0x90 (ADJUST?)
GETVARIATION => self.op_getvariation()?,
GETDATA => self.op_getdata()?,
_ => {
// FreeType handles MIRP, MDRP and pushes here.
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L7629>
if opcode >= MIRP00000 {
self.op_mirp(raw_opcode)?
} else if opcode >= MDRP00000 {
self.op_mdrp(raw_opcode)?
} else if opcode >= PUSHB000 {
self.op_push(&ins.inline_operands)?;
} else {
return self.op_unknown(opcode as u8);
}
}
}
Ok(())
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,305 @@
//! Logical functions.
//!
//! Implements 11 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#logical-functions>
use super::{Engine, F26Dot6, OpResult};
impl Engine<'_> {
/// Less than.
///
/// LT[] (0x50)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// First pops e2, then pops e1 off the stack and compares them: if e1 is
/// less than e2, 1, signifying TRUE, is pushed onto the stack. If e1 is
/// not less than e2, 0, signifying FALSE, is placed onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#less-than>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2721>
pub(super) fn op_lt(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok((a < b) as i32))
}
/// Less than or equal.
///
/// LTEQ[] (0x51)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// Pops e2 and e1 off the stack and compares them. If e1 is less than or
/// equal to e2, 1, signifying TRUE, is pushed onto the stack. If e1 is
/// not less than or equal to e2, 0, signifying FALSE, is placed onto the
/// stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#less-than-or-equal>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2734>
pub(super) fn op_lteq(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok((a <= b) as i32))
}
/// Greater than.
///
/// GT[] (0x52)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// First pops e2 then pops e1 off the stack and compares them. If e1 is
/// greater than e2, 1, signifying TRUE, is pushed onto the stack. If e1
/// is not greater than e2, 0, signifying FALSE, is placed onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#greater-than>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2747>
pub(super) fn op_gt(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok((a > b) as i32))
}
/// Greater than or equal.
///
/// GTEQ[] (0x53)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// Pops e1 and e2 off the stack and compares them. If e1 is greater than
/// or equal to e2, 1, signifying TRUE, is pushed onto the stack. If e1
/// is not greater than or equal to e2, 0, signifying FALSE, is placed
/// onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#greater-than-or-equal>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2760>
pub(super) fn op_gteq(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok((a >= b) as i32))
}
/// Equal.
///
/// EQ[] (0x54)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// Pops e1 and e2 off the stack and compares them. If they are equal, 1,
/// signifying TRUE is pushed onto the stack. If they are not equal, 0,
/// signifying FALSE is placed onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#equal>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2773>
pub(super) fn op_eq(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok((a == b) as i32))
}
/// Not equal.
///
/// NEQ[] (0x55)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// Pops e1 and e2 from the stack and compares them. If they are not equal,
/// 1, signifying TRUE, is pushed onto the stack. If they are equal, 0,
/// signifying FALSE, is placed on the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#not-equal>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2786>
pub(super) fn op_neq(&mut self) -> OpResult {
self.value_stack.apply_binary(|a, b| Ok((a != b) as i32))
}
/// Odd.
///
/// ODD[] (0x56)
///
/// Pops: e1
/// Pushes: Boolean value
///
/// Tests whether the number at the top of the stack is odd. Pops e1 from
/// the stack and rounds it as specified by the round_state before testing
/// it. After the value is rounded, it is shifted from a fixed point value
/// to an integer value (any fractional values are ignored). If the integer
/// value is odd, one, signifying TRUE, is pushed onto the stack. If it is
/// even, zero, signifying FALSE is placed onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#odd>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2799>
pub(super) fn op_odd(&mut self) -> OpResult {
let round_state = self.graphics.round_state;
self.value_stack.apply_unary(|e1| {
Ok((round_state.round(F26Dot6::from_bits(e1)).to_bits() & 127 == 64) as i32)
})
}
/// Even.
///
/// EVEN[] (0x57)
///
/// Pops: e1
/// Pushes: Boolean value
///
/// Tests whether the number at the top of the stack is even. Pops e1 off
/// the stack and rounds it as specified by the round_state before testing
/// it. If the rounded number is even, one, signifying TRUE, is pushed onto
/// the stack if it is odd, zero, signifying FALSE, is placed onto the
/// stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#even>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2813>
pub(super) fn op_even(&mut self) -> OpResult {
let round_state = self.graphics.round_state;
self.value_stack.apply_unary(|e1| {
Ok((round_state.round(F26Dot6::from_bits(e1)).to_bits() & 127 == 0) as i32)
})
}
/// Logical and.
///
/// AND[] (0x5A)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// Pops e1 and e2 off the stack and pushes onto the stack the result of a
/// logical and of the two elements. Zero is returned if either or both of
/// the elements are FALSE (have the value zero). One is returned if both
/// elements are TRUE (have a non zero value).
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#logical-and>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2827>
pub(super) fn op_and(&mut self) -> OpResult {
self.value_stack
.apply_binary(|a, b| Ok((a != 0 && b != 0) as i32))
}
/// Logical or.
///
/// OR[] (0x5B)
///
/// Pops: e1, e2
/// Pushes: Boolean value
///
/// Pops e1 and e2 off the stack and pushes onto the stack the result of a
/// logical or operation between the two elements. Zero is returned if both
/// of the elements are FALSE. One is returned if either one or both of the
/// elements are TRUE (has a nonzero value).
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#logical-or>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2840>
pub(super) fn op_or(&mut self) -> OpResult {
self.value_stack
.apply_binary(|a, b| Ok((a != 0 || b != 0) as i32))
}
/// Logical not.
///
/// NOT[] (0x5C)
///
/// Pops: e
/// Pushes: (not e): logical negation of e
///
/// Pops e off the stack and returns the result of a logical NOT operation
/// performed on e. If originally zero, one is pushed onto the stack if
/// originally nonzero, zero is pushed onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#logical-not>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2853>
pub(super) fn op_not(&mut self) -> OpResult {
self.value_stack.apply_unary(|e| Ok((e == 0) as i32))
}
}
#[cfg(test)]
mod tests {
use super::super::MockEngine;
#[test]
fn compare_ops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let input = &[a, b];
engine.test_exec(input, a < b, |engine| {
engine.op_lt().unwrap();
});
engine.test_exec(input, a <= b, |engine| {
engine.op_lteq().unwrap();
});
engine.test_exec(input, a > b, |engine| {
engine.op_gt().unwrap();
});
engine.test_exec(input, a >= b, |engine| {
engine.op_gteq().unwrap();
});
engine.test_exec(input, a == b, |engine| {
engine.op_eq().unwrap();
});
engine.test_exec(input, a != b, |engine| {
engine.op_neq().unwrap();
});
}
}
}
#[test]
fn parity_ops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// These operate on 26.6 so values are multiple of 64
let cases = [
// (input, is_even)
(0, true),
(64, false),
(128, true),
(192, false),
(256, true),
(57, false),
(-128, true),
];
for (input, is_even) in cases {
engine.test_exec(&[input], is_even, |engine| {
engine.op_even().unwrap();
});
}
for (input, is_even) in cases {
engine.test_exec(&[input], !is_even, |engine| {
engine.op_odd().unwrap();
});
}
}
#[test]
fn not_op() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
engine.test_exec(&[0], 1, |engine| {
engine.op_not().unwrap();
});
engine.test_exec(&[234234], 0, |engine| {
engine.op_not().unwrap();
});
}
#[test]
fn and_or_ops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for a in -10..=10 {
for b in -10..=10 {
let input = &[a, b];
let a = a != 0;
let b = b != 0;
engine.test_exec(input, a && b, |engine| {
engine.op_and().unwrap();
});
engine.test_exec(input, a || b, |engine| {
engine.op_or().unwrap();
});
}
}
}
}

View File

@@ -0,0 +1,322 @@
//! Miscellaneous instructions.
//!
//! Implements 3 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#miscellaneous-instructions>
use super::{Engine, OpResult};
impl Engine<'_> {
/// Get information.
///
/// GETINFO[] (0x88)
///
/// Pops: selector: integer
/// Pushes: result: integer
///
/// GETINFO is used to obtain data about the font scaler version and the
/// characteristics of the current glyph. The instruction pops a selector
/// used to determine the type of information desired and pushes a result
/// onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#get-information>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6689>
pub(super) fn op_getinfo(&mut self) -> OpResult {
use getinfo::*;
let selector = self.value_stack.pop()?;
let mut result = 0;
// Interpreter version (selector bit: 0, result bits: 0-7)
if (selector & VERSION_SELECTOR_BIT) != 0 {
result = 40;
}
// Glyph rotated (selector bit: 1, result bit: 8)
if (selector & GLYPH_ROTATED_SELECTOR_BIT) != 0 && self.graphics.is_rotated {
result |= GLYPH_ROTATED_RESULT_BIT;
}
// Glyph stretched (selector bit: 2, result bit: 9)
if (selector & GLYPH_STRETCHED_SELECTOR_BIT) != 0 && self.graphics.is_stretched {
result |= GLYPH_STRETCHED_RESULT_BIT;
}
// Font variations (selector bit: 3, result bit: 10)
if (selector & FONT_VARIATIONS_SELECTOR_BIT) != 0 && self.axis_count != 0 {
result |= FONT_VARIATIONS_RESULT_BIT;
}
// The following only apply for smooth hinting.
if self.graphics.target.is_smooth() {
// Subpixel hinting [cleartype enabled] (selector bit: 6, result bit: 13)
// (always enabled)
if (selector & SUBPIXEL_HINTING_SELECTOR_BIT) != 0 {
result |= SUBPIXEL_HINTING_RESULT_BIT;
}
// Vertical LCD subpixels? (selector bit: 8, result bit: 15)
if (selector & VERTICAL_LCD_SELECTOR_BIT) != 0 && self.graphics.target.is_vertical_lcd()
{
result |= VERTICAL_LCD_RESULT_BIT;
}
// Subpixel positioned? (selector bit: 10, result bit: 17)
// (always enabled)
if (selector & SUBPIXEL_POSITIONED_SELECTOR_BIT) != 0 {
result |= SUBPIXEL_POSITIONED_RESULT_BIT;
}
// Symmetrical smoothing (selector bit: 11, result bit: 18)
// Note: FreeType always enables this but we allow direct control
// with our own flag.
// See <https://github.com/googlefonts/fontations/issues/1080>
if (selector & SYMMETRICAL_SMOOTHING_SELECTOR_BIT) != 0
&& self.graphics.target.symmetric_rendering()
{
result |= SYMMETRICAL_SMOOTHING_RESULT_BIT;
}
// ClearType hinting and grayscale rendering (selector bit: 12, result bit: 19)
if (selector & GRAYSCALE_CLEARTYPE_SELECTOR_BIT) != 0
&& self.graphics.target.is_grayscale_cleartype()
{
result |= GRAYSCALE_CLEARTYPE_RESULT_BIT;
}
}
self.value_stack.push(result)
}
/// Get variation.
///
/// GETVARIATION[] (0x91)
///
/// Pushes: Normalized axes coordinates, one for each axis in the font.
///
/// GETVARIATION is used to obtain the current normalized variation
/// coordinates for each axis. The coordinate for the first axis, as
/// defined in the 'fvar' table, is pushed first on the stack, followed
/// by each consecutive axis until the coordinate for the last axis is
/// on the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#get-variation>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6813>
pub(super) fn op_getvariation(&mut self) -> OpResult {
// For non-variable fonts, this falls back to IDEF resolution.
let axis_count = self.axis_count as usize;
if axis_count != 0 {
// Make sure we push `axis_count` coords regardless of the value
// provided by the user.
for coord in self
.coords
.iter()
.copied()
.chain(std::iter::repeat(Default::default()))
.take(axis_count)
{
self.value_stack.push(coord.to_bits() as i32)?;
}
Ok(())
} else {
self.op_unknown(0x91)
}
}
/// Get data.
///
/// GETDATA[] (0x92)
///
/// Pushes: 17
///
/// Undocumented and nobody knows what this does. FreeType just
/// returns 17 for variable fonts and falls back to IDEF lookup
/// otherwise.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6851>
pub(super) fn op_getdata(&mut self) -> OpResult {
if self.axis_count != 0 {
self.value_stack.push(17)
} else {
self.op_unknown(0x92)
}
}
}
/// Constants for the GETINFO instruction. Extracted here
/// to enable access from tests.
mod getinfo {
// Interpreter version (selector bit: 0, result bits: 0-7)
pub const VERSION_SELECTOR_BIT: i32 = 1 << 0;
// Glyph rotated (selector bit: 1, result bit: 8)
pub const GLYPH_ROTATED_SELECTOR_BIT: i32 = 1 << 1;
pub const GLYPH_ROTATED_RESULT_BIT: i32 = 1 << 8;
// Glyph stretched (selector bit: 2, result bit: 9)
pub const GLYPH_STRETCHED_SELECTOR_BIT: i32 = 1 << 2;
pub const GLYPH_STRETCHED_RESULT_BIT: i32 = 1 << 9;
// Font variations (selector bit: 3, result bit: 10)
pub const FONT_VARIATIONS_SELECTOR_BIT: i32 = 1 << 3;
pub const FONT_VARIATIONS_RESULT_BIT: i32 = 1 << 10;
// Subpixel hinting [cleartype enabled] (selector bit: 6, result bit: 13)
// (always enabled)
pub const SUBPIXEL_HINTING_SELECTOR_BIT: i32 = 1 << 6;
pub const SUBPIXEL_HINTING_RESULT_BIT: i32 = 1 << 13;
// Vertical LCD subpixels? (selector bit: 8, result bit: 15)
pub const VERTICAL_LCD_SELECTOR_BIT: i32 = 1 << 8;
pub const VERTICAL_LCD_RESULT_BIT: i32 = 1 << 15;
// Subpixel positioned? (selector bit: 10, result bit: 17)
// (always enabled)
pub const SUBPIXEL_POSITIONED_SELECTOR_BIT: i32 = 1 << 10;
pub const SUBPIXEL_POSITIONED_RESULT_BIT: i32 = 1 << 17;
// Symmetrical smoothing (selector bit: 11, result bit: 18)
// Note: FreeType always enables this but we deviate when our own
// preserve linear metrics flag is enabled.
pub const SYMMETRICAL_SMOOTHING_SELECTOR_BIT: i32 = 1 << 11;
pub const SYMMETRICAL_SMOOTHING_RESULT_BIT: i32 = 1 << 18;
// ClearType hinting and grayscale rendering (selector bit: 12, result bit: 19)
pub const GRAYSCALE_CLEARTYPE_SELECTOR_BIT: i32 = 1 << 12;
pub const GRAYSCALE_CLEARTYPE_RESULT_BIT: i32 = 1 << 19;
}
#[cfg(test)]
mod tests {
use super::super::{
super::super::super::{SmoothMode, Target},
Engine, HintErrorKind, MockEngine,
};
use raw::types::F2Dot14;
use read_fonts::tables::glyf::bytecode::Opcode;
#[test]
fn getinfo() {
use super::getinfo::*;
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// version
engine.getinfo_test(VERSION_SELECTOR_BIT, 40);
// not rotated
engine.getinfo_test(GLYPH_ROTATED_SELECTOR_BIT, 0);
// rotated
engine.graphics.is_rotated = true;
engine.getinfo_test(GLYPH_ROTATED_SELECTOR_BIT, GLYPH_ROTATED_RESULT_BIT);
// not stretched
engine.getinfo_test(GLYPH_STRETCHED_SELECTOR_BIT, 0);
// stretched
engine.graphics.is_stretched = true;
engine.getinfo_test(GLYPH_STRETCHED_SELECTOR_BIT, GLYPH_STRETCHED_RESULT_BIT);
// stretched and rotated
engine.getinfo_test(
GLYPH_ROTATED_SELECTOR_BIT | GLYPH_STRETCHED_SELECTOR_BIT,
GLYPH_ROTATED_RESULT_BIT | GLYPH_STRETCHED_RESULT_BIT,
);
// not variable
engine.getinfo_test(FONT_VARIATIONS_SELECTOR_BIT, 0);
// variable
engine.axis_count = 1;
engine.getinfo_test(FONT_VARIATIONS_SELECTOR_BIT, FONT_VARIATIONS_RESULT_BIT);
// in strong hinting mode, the following selectors are always disabled
engine.graphics.target = Target::Mono;
for selector in [
SUBPIXEL_HINTING_SELECTOR_BIT,
VERTICAL_LCD_SELECTOR_BIT,
SUBPIXEL_POSITIONED_SELECTOR_BIT,
SYMMETRICAL_SMOOTHING_SELECTOR_BIT,
GRAYSCALE_CLEARTYPE_SELECTOR_BIT,
] {
engine.getinfo_test(selector, 0);
}
// set back to smooth mode
engine.graphics.target = Target::default();
for (selector, result) in [
// default smooth mode is grayscale cleartype
(
GRAYSCALE_CLEARTYPE_SELECTOR_BIT,
GRAYSCALE_CLEARTYPE_RESULT_BIT,
),
// always enabled in smooth mode
(SUBPIXEL_HINTING_SELECTOR_BIT, SUBPIXEL_HINTING_RESULT_BIT),
(
SUBPIXEL_POSITIONED_SELECTOR_BIT,
SUBPIXEL_POSITIONED_RESULT_BIT,
),
] {
engine.getinfo_test(selector, result);
}
// vertical lcd
engine.graphics.target = Target::Smooth {
mode: SmoothMode::VerticalLcd,
preserve_linear_metrics: true,
symmetric_rendering: false,
};
engine.getinfo_test(VERTICAL_LCD_SELECTOR_BIT, VERTICAL_LCD_RESULT_BIT);
// symmetical smoothing is disabled
engine.getinfo_test(SYMMETRICAL_SMOOTHING_SELECTOR_BIT, 0);
// grayscale cleartype is disabled when lcd_subpixel is not None
engine.getinfo_test(GRAYSCALE_CLEARTYPE_SELECTOR_BIT, 0);
// reset to default to disable preserve linear metrics
engine.graphics.target = Target::default();
// now symmetrical smoothing is enabled
engine.getinfo_test(
SYMMETRICAL_SMOOTHING_SELECTOR_BIT,
SYMMETRICAL_SMOOTHING_RESULT_BIT,
);
}
#[test]
fn getvariation() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// no variations should trigger unknown opcode
assert!(matches!(
engine.op_getvariation(),
Err(HintErrorKind::UnhandledOpcode(Opcode::GETVARIATION))
));
// set the axis count to a non-zero value to enable variations
engine.axis_count = 2;
// and creates some coords
let coords = [
F2Dot14::from_f32(-1.0),
F2Dot14::from_f32(0.5),
F2Dot14::from_f32(1.0),
];
let coords_bits = coords.map(|x| x.to_bits() as i32);
// too few, pad with zeros
engine.coords = &coords[0..1];
engine.op_getvariation().unwrap();
assert_eq!(engine.value_stack.len(), 2);
assert_eq!(engine.value_stack.values(), &[coords_bits[0], 0]);
engine.value_stack.clear();
// too many, truncate
engine.coords = &coords[0..3];
engine.op_getvariation().unwrap();
assert_eq!(engine.value_stack.len(), 2);
assert_eq!(engine.value_stack.values(), &coords_bits[0..2]);
engine.value_stack.clear();
// just right
engine.coords = &coords[0..2];
engine.op_getvariation().unwrap();
assert_eq!(engine.value_stack.len(), 2);
assert_eq!(engine.value_stack.values(), &coords_bits[0..2]);
}
#[test]
fn getdata() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
// no variations should trigger unknown opcode
assert!(matches!(
engine.op_getdata(),
Err(HintErrorKind::UnhandledOpcode(Opcode::GETDATA))
));
// set the axis count to a non-zero value to enable variations
engine.axis_count = 1;
engine.op_getdata().unwrap();
// :shrug:
assert_eq!(engine.value_stack.pop().unwrap(), 17);
}
impl Engine<'_> {
fn getinfo_test(&mut self, selector: i32, expected: i32) {
self.value_stack.push(selector).unwrap();
self.op_getinfo().unwrap();
assert_eq!(self.value_stack.pop().unwrap(), expected);
}
}
}

View File

@@ -0,0 +1,269 @@
//! TrueType bytecode interpreter.
mod arith;
mod control_flow;
mod cvt;
mod data;
mod definition;
mod delta;
mod dispatch;
mod graphics;
mod logical;
mod misc;
mod outline;
mod round;
mod stack;
mod storage;
use read_fonts::{
tables::glyf::bytecode::Instruction,
types::{F26Dot6, F2Dot14, Point},
};
use super::{
super::Outlines,
cvt::Cvt,
definition::DefinitionState,
error::{HintError, HintErrorKind},
graphics::{GraphicsState, RetainedGraphicsState},
math,
program::ProgramState,
storage::Storage,
value_stack::ValueStack,
zone::Zone,
};
pub type OpResult = Result<(), HintErrorKind>;
/// TrueType bytecode interpreter.
pub struct Engine<'a> {
program: ProgramState<'a>,
graphics: GraphicsState<'a>,
definitions: DefinitionState<'a>,
cvt: Cvt<'a>,
storage: Storage<'a>,
value_stack: ValueStack<'a>,
loop_budget: LoopBudget,
axis_count: u16,
coords: &'a [F2Dot14],
}
impl<'a> Engine<'a> {
#[allow(clippy::too_many_arguments)]
pub fn new(
outlines: &Outlines,
program: ProgramState<'a>,
graphics: RetainedGraphicsState,
definitions: DefinitionState<'a>,
cvt: impl Into<Cvt<'a>>,
storage: impl Into<Storage<'a>>,
value_stack: ValueStack<'a>,
twilight: Zone<'a>,
glyph: Zone<'a>,
axis_count: u16,
coords: &'a [F2Dot14],
is_composite: bool,
) -> Self {
let point_count = if glyph.points.is_empty() {
None
} else {
Some(glyph.points.len())
};
let graphics = GraphicsState {
retained: graphics,
zones: [twilight, glyph],
is_composite,
..Default::default()
};
Self {
program,
graphics,
definitions,
cvt: cvt.into(),
storage: storage.into(),
value_stack,
loop_budget: LoopBudget::new(outlines, point_count),
axis_count,
coords,
}
}
pub fn backward_compatibility(&self) -> bool {
self.graphics.backward_compatibility
}
pub fn retained_graphics_state(&self) -> &RetainedGraphicsState {
&self.graphics.retained
}
}
/// Tracks budgets for loops to limit execution time.
struct LoopBudget {
/// Maximum number of times we can do backward jumps or
/// loop calls.
limit: usize,
/// Current number of backward jumps executed.
backward_jumps: usize,
/// Current number of loop call iterations executed.
loop_calls: usize,
}
impl LoopBudget {
fn new(outlines: &Outlines, point_count: Option<usize>) -> Self {
let cvt_len = outlines.cvt_len as usize;
// Compute limits for loop calls and backward jumps.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6955>
let limit = if let Some(point_count) = point_count {
(point_count * 10).max(50) + (cvt_len / 10).max(50)
} else {
300 + 22 * cvt_len
};
// FreeType has two variables for neg_jump_counter_max and
// loopcall_counter_max but sets them to the same value so
// we'll just use a single limit.
Self {
limit,
backward_jumps: 0,
loop_calls: 0,
}
}
fn reset(&mut self) {
self.backward_jumps = 0;
self.loop_calls = 0;
}
fn doing_backward_jump(&mut self) -> Result<(), HintErrorKind> {
self.backward_jumps += 1;
if self.backward_jumps > self.limit {
Err(HintErrorKind::ExceededExecutionBudget)
} else {
Ok(())
}
}
fn doing_loop_call(&mut self, count: usize) -> Result<(), HintErrorKind> {
self.loop_calls += count;
if self.loop_calls > self.limit {
Err(HintErrorKind::ExceededExecutionBudget)
} else {
Ok(())
}
}
}
#[cfg(test)]
use mock::MockEngine;
#[cfg(test)]
mod mock {
use super::{
super::{
cow_slice::CowSlice,
definition::{Definition, DefinitionMap, DefinitionState},
program::{Program, ProgramState},
zone::Zone,
Point, PointFlags,
},
Engine, F26Dot6, GraphicsState, LoopBudget, ValueStack,
};
/// Mock engine for testing.
pub(super) struct MockEngine {
cvt_storage: Vec<i32>,
value_stack: Vec<i32>,
definitions: Vec<Definition>,
unscaled: Vec<Point<i32>>,
points: Vec<Point<F26Dot6>>,
point_flags: Vec<PointFlags>,
contours: Vec<u16>,
twilight: Vec<Point<F26Dot6>>,
twilight_flags: Vec<PointFlags>,
}
impl MockEngine {
pub fn new() -> Self {
Self {
cvt_storage: vec![0; 32],
value_stack: vec![0; 32],
definitions: vec![Default::default(); 8],
unscaled: vec![Default::default(); 32],
points: vec![Default::default(); 64],
point_flags: vec![Default::default(); 32],
contours: vec![31],
twilight: vec![Default::default(); 32],
twilight_flags: vec![Default::default(); 32],
}
}
pub fn engine(&mut self) -> Engine {
let font_code = &[];
let cv_code = &[];
let glyph_code = &[];
let (cvt, storage) = self.cvt_storage.split_at_mut(16);
let (function_defs, instruction_defs) = self.definitions.split_at_mut(5);
let definition = DefinitionState::new(
DefinitionMap::Mut(function_defs),
DefinitionMap::Mut(instruction_defs),
);
for (i, point) in self.unscaled.iter_mut().enumerate() {
let i = i as i32;
point.x = 57 + i * 2;
point.y = -point.x * 3;
}
let (points, original) = self.points.split_at_mut(32);
let glyph_zone = Zone::new(
&self.unscaled,
original,
points,
&mut self.point_flags,
&self.contours,
);
let (points, original) = self.twilight.split_at_mut(16);
let twilight_zone = Zone::new(&[], original, points, &mut self.twilight_flags, &[]);
let mut graphics_state = GraphicsState {
zones: [twilight_zone, glyph_zone],
..Default::default()
};
graphics_state.update_projection_state();
Engine {
graphics: graphics_state,
cvt: CowSlice::new_mut(cvt).into(),
storage: CowSlice::new_mut(storage).into(),
value_stack: ValueStack::new(&mut self.value_stack, false),
program: ProgramState::new(font_code, cv_code, glyph_code, Program::Font),
loop_budget: LoopBudget {
limit: 10,
backward_jumps: 0,
loop_calls: 0,
},
definitions: definition,
axis_count: 0,
coords: &[],
}
}
}
impl Default for MockEngine {
fn default() -> Self {
Self::new()
}
}
impl Engine<'_> {
/// Helper to push values to the stack, invoke a callback and check
/// the expected result.
pub(super) fn test_exec(
&mut self,
push: &[i32],
expected_result: impl Into<i32>,
mut f: impl FnMut(&mut Engine),
) {
for &val in push {
self.value_stack.push(val).unwrap();
}
f(self);
assert_eq!(self.value_stack.pop().ok(), Some(expected_result.into()));
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
//! Compensating for the engine characteristics (rounding).
//!
//! Implements 4 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#compensating-for-the-engine-characteristics>
use super::{Engine, OpResult};
impl Engine<'_> {
/// Round value.
///
/// ROUND\[ab\] (0x68 - 0x6B)
///
/// Pops: n1
/// Pushes: n2
///
/// Rounds a value according to the state variable round_state while
/// compensating for the engine. n1 is popped off the stack and,
/// depending on the engine characteristics, is increased or decreased
/// by a set amount. The number obtained is then rounded and pushed
/// back onto the stack as n2.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#round-value>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3143>
pub(super) fn op_round(&mut self) -> OpResult {
let n1 = self.value_stack.pop_f26dot6()?;
let n2 = self.graphics.round(n1);
self.value_stack.push(n2.to_bits())
}
}
#[cfg(test)]
mod tests {
use super::super::{super::round::RoundMode, MockEngine};
#[test]
fn round() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
use RoundMode::*;
let cases = [
(Grid, &[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)]),
(
HalfGrid,
&[(0, 32), (32, 32), (-32, -32), (64, 96), (50, 32)],
),
(
DoubleGrid,
&[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 64)],
),
(DownToGrid, &[(0, 0), (32, 0), (-32, 0), (64, 64), (50, 0)]),
(
UpToGrid,
&[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
),
(Off, &[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 50)]),
];
for (mode, values) in cases {
match mode {
Grid => engine.op_rtg().unwrap(),
HalfGrid => engine.op_rthg().unwrap(),
DoubleGrid => engine.op_rtdg().unwrap(),
DownToGrid => engine.op_rdtg().unwrap(),
UpToGrid => engine.op_rutg().unwrap(),
Off => engine.op_roff().unwrap(),
_ => unreachable!(),
}
for (input, expected) in values {
engine.value_stack.push(*input).unwrap();
engine.op_round().unwrap();
let result = engine.value_stack.pop().unwrap();
assert_eq!(*expected, result);
}
}
}
}

View File

@@ -0,0 +1,224 @@
//! Managing the stack and pushing data onto the interpreter stack.
//!
//! Implements 26 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-stack>
//! and <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#pushing-data-onto-the-interpreter-stack>
use read_fonts::tables::glyf::bytecode::InlineOperands;
use super::{Engine, OpResult};
impl Engine<'_> {
/// Duplicate top stack element.
///
/// DUP[] (0x20)
///
/// Pops: e
/// Pushes: e, e
///
/// Duplicates the element at the top of the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#duplicate-top-stack-element>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2650>
pub(super) fn op_dup(&mut self) -> OpResult {
self.value_stack.dup()
}
/// Pop top stack element.
///
/// POP[] (0x21)
///
/// Pops: e
///
/// Pops the top element of the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#pop-top-stack-element>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2663>
pub(super) fn op_pop(&mut self) -> OpResult {
self.value_stack.pop()?;
Ok(())
}
/// Clear the entire stack.
///
/// CLEAR[] (0x22)
///
/// Pops: all the items on the stack
///
/// Clears all elements from the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#clear-the-entire-stack>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2676>
pub(super) fn op_clear(&mut self) -> OpResult {
self.value_stack.clear();
Ok(())
}
/// Swap the top two elements on the stack.
///
/// SWAP[] (0x23)
///
/// Pops: e2, e1
/// Pushes: e1, e2
///
/// Swaps the top two elements of the stack making the old top element the
/// second from the top and the old second element the top element.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#swap-the-top-two-elements-on-the-stack>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2689>
pub(super) fn op_swap(&mut self) -> OpResult {
self.value_stack.swap()
}
/// Returns the depth of the stack.
///
/// DEPTH[] (0x24)
///
/// Pushes: n; number of elements
///
/// Pushes n, the number of elements currently in the stack onto the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#returns-the-depth-of-the-stack>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2707>
pub(super) fn op_depth(&mut self) -> OpResult {
let n = self.value_stack.len();
self.value_stack.push(n as i32)
}
/// Copy the indexed element to the top of the stack.
///
/// CINDEX[] (0x25)
///
/// Pops: k: stack element number
/// Pushes: ek: indexed element
///
/// Puts a copy of the kth stack element on the top of the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#copy-the-indexed-element-to-the-top-of-the-stack>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3232>
pub(super) fn op_cindex(&mut self) -> OpResult {
self.value_stack.copy_index()
}
/// Move the indexed element to the top of the stack.
///
/// MINDEX[] (0x26)
///
/// Pops: k: stack element number
/// Pushes: ek: indexed element
///
/// Moves the indexed element to the top of the stack.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-the-indexed-element-to-the-top-of-the-stack>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3199>
pub(super) fn op_mindex(&mut self) -> OpResult {
self.value_stack.move_index()
}
/// Roll the top three stack elements.
///
/// ROLL[] (0x8a)
///
/// Pops: a, b, c (top three stack elements)
/// Pushes: b, a, c (elements reordered)
///
/// Performs a circular shift of the top three objects on the stack with
/// the effect being to move the third element to the top of the stack
/// and to move the first two elements down one position. ROLL is
/// equivalent to MINDEX[] 3.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#roll-the-top-three-stack-elements>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3258>
pub(super) fn op_roll(&mut self) -> OpResult {
self.value_stack.roll()
}
/// Push data onto the interpreter stack.
///
/// NPUSHB[] (0x8a)
///
/// Takes n unsigned bytes from the instruction stream, where n is an
/// unsigned integer in the range (0..255), and pushes them onto the stack.
/// n itself is not pushed onto the stack.
///
/// NPUSHW[] (0x41)
///
/// Takes n 16-bit signed words from the instruction stream, where n is an
/// unsigned integer in the range (0..255), and pushes them onto the stack.
/// n itself is not pushed onto the stack.
///
/// PUSHB\[abc\] (0xB0 - 0xB7)
///
/// Takes the specified number of bytes from the instruction stream and
/// pushes them onto the interpreter stack.
/// The variables a, b, and c are binary digits representing numbers from
/// 000 to 111 (0-7 in binary). Because the actual number of bytes (n) is
/// from 1 to 8, 1 is automatically added to the ABC figure to obtain the
/// actual number of bytes pushed.
///
/// PUSHW\[abc\] (0xB8 - 0xBF)
///
/// Takes the specified number of words from the instruction stream and
/// pushes them onto the interpreter stack.
/// The variables a, b, and c are binary digits representing numbers from
/// 000 to 111 (0-7 binary). Because the actual number of bytes (n) is from
/// 1 to 8, 1 is automatically added to the abc figure to obtain the actual
/// number of bytes pushed.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#pushing-data-onto-the-interpreter-stack>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3858>
pub(super) fn op_push(&mut self, operands: &InlineOperands) -> OpResult {
self.value_stack.push_inline_operands(operands)
}
}
#[cfg(test)]
mod tests {
use super::super::MockEngine;
use read_fonts::tables::glyf::bytecode::MockInlineOperands;
#[test]
fn stack_ops() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let byte_args = MockInlineOperands::from_bytes(&[2, 4, 6, 8]);
let word_args = MockInlineOperands::from_words(&[-2000, 4000, -6000, 8000]);
let initial_stack = byte_args
.operands()
.values()
.chain(word_args.operands().values())
.collect::<Vec<_>>();
// Push instructions
engine.op_push(&byte_args.operands()).unwrap();
engine.op_push(&word_args.operands()).unwrap();
assert_eq!(engine.value_stack.values(), initial_stack);
// DEPTH[]
engine.op_depth().unwrap();
assert_eq!(
engine.value_stack.pop().ok(),
Some(initial_stack.len() as i32)
);
// POP[]
engine.op_pop().unwrap();
engine.op_pop().unwrap();
assert_eq!(
engine.value_stack.values(),
&initial_stack[..initial_stack.len() - 2]
);
// SWAP[]
engine.op_swap().unwrap();
assert_eq!(&engine.value_stack.values()[4..], &[4000, -2000]);
// ROLL[]
engine.op_roll().unwrap();
assert_eq!(&engine.value_stack.values()[3..], &[4000, -2000, 8]);
// CINDEX[]
engine.value_stack.push(4).unwrap();
engine.op_cindex().unwrap();
assert_eq!(engine.value_stack.peek(), Some(6));
// MINDEX[]
engine.value_stack.push(3).unwrap();
engine.op_mindex().unwrap();
assert_eq!(engine.value_stack.peek(), Some(-2000));
}
}

View File

@@ -0,0 +1,110 @@
//! Managing the storage area.
//!
//! Implements 2 instructions.
//!
//! See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-storage-area>
use super::{Engine, OpResult};
impl Engine<'_> {
/// Read store.
///
/// RS[] (0x43)
///
/// Pops: location: Storage Area location
/// Pushes: value: Storage Area value
///
/// This instruction reads a 32 bit value from the Storage Area location
/// popped from the stack and pushes the value read onto the stack. It pops
/// an address from the stack and pushes the value found in that Storage
/// Area location to the top of the stack. The number of available storage
/// locations is specified in the maxProfile table in the font file.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#read-store>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2975>
pub(super) fn op_rs(&mut self) -> OpResult {
let location = self.value_stack.pop_usize()?;
let maybe_value = self.storage.get(location);
let value = if self.graphics.is_pedantic {
maybe_value?
} else {
maybe_value.unwrap_or(0)
};
self.value_stack.push(value)
}
/// Write store.
///
/// WS[] (0x42)
///
/// Pops: value: Storage Area value,
/// location: Storage Area location
///
/// This instruction writes a 32 bit value into the storage location
/// indexed by locations. It works by popping a value and then a location
/// from the stack. The value is placed in the Storage Area location
/// specified by that address. The number of storage locations is specified
/// in the maxProfile table in the font file.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#write-store>
/// and <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L3000>
pub(super) fn op_ws(&mut self) -> OpResult {
let value = self.value_stack.pop()?;
let location = self.value_stack.pop_usize()?;
let result = self.storage.set(location, value);
if self.graphics.is_pedantic {
result
} else {
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::super::{HintErrorKind, MockEngine};
#[test]
fn write_read() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
for i in 0..8 {
engine.value_stack.push(i).unwrap();
engine.value_stack.push(i * 2).unwrap();
engine.op_ws().unwrap();
}
for i in 0..8 {
engine.value_stack.push(i).unwrap();
engine.op_rs().unwrap();
assert_eq!(engine.value_stack.pop().unwrap(), i * 2);
}
}
#[test]
fn pedantry() {
let mut mock = MockEngine::new();
let mut engine = mock.engine();
let oob_index = 1000;
// Disable pedantic mode: OOB writes are ignored, OOB reads
// push 0
engine.graphics.is_pedantic = false;
engine.value_stack.push(oob_index).unwrap();
engine.value_stack.push(0).unwrap();
engine.op_ws().unwrap();
engine.value_stack.push(oob_index).unwrap();
engine.op_rs().unwrap();
// Enable pedantic mode: OOB reads/writes error
engine.graphics.is_pedantic = true;
engine.value_stack.push(oob_index).unwrap();
engine.value_stack.push(0).unwrap();
assert_eq!(
engine.op_ws(),
Err(HintErrorKind::InvalidStorageIndex(oob_index as _))
);
engine.value_stack.push(oob_index).unwrap();
assert_eq!(
engine.op_rs(),
Err(HintErrorKind::InvalidStorageIndex(oob_index as _))
);
}
}

View File

@@ -0,0 +1,120 @@
//! Hinting error definitions.
use read_fonts::tables::glyf::bytecode::{DecodeError, Opcode};
use super::program::Program;
use crate::GlyphId;
/// Errors that may occur when interpreting TrueType bytecode.
#[derive(Clone, PartialEq, Debug)]
pub enum HintErrorKind {
UnexpectedEndOfBytecode,
UnhandledOpcode(Opcode),
DefinitionInGlyphProgram,
NestedDefinition,
DefinitionTooLarge,
TooManyDefinitions,
InvalidDefinition(usize),
ValueStackOverflow,
ValueStackUnderflow,
CallStackOverflow,
CallStackUnderflow,
InvalidStackValue(i32),
InvalidPointIndex(usize),
InvalidPointRange(usize, usize),
InvalidContourIndex(usize),
InvalidCvtIndex(usize),
InvalidStorageIndex(usize),
DivideByZero,
InvalidZoneIndex(i32),
NegativeLoopCounter,
InvalidJump,
ExceededExecutionBudget,
}
impl core::fmt::Display for HintErrorKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnexpectedEndOfBytecode => write!(f, "unexpected end of bytecode"),
Self::UnhandledOpcode(opcode) => write!(f, "unhandled instruction opcode {opcode}"),
Self::DefinitionInGlyphProgram => {
write!(
f,
"function or instruction definition present in glyph program"
)
}
Self::NestedDefinition => write!(f, "nested function or instruction definition"),
Self::DefinitionTooLarge => write!(
f,
"function or instruction definition exceeded the maximum size of 64k"
),
Self::TooManyDefinitions => write!(f, "too many function or instruction definitions"),
Self::InvalidDefinition(key) => {
write!(f, "function or instruction definition {key} not found")
}
Self::ValueStackOverflow => write!(f, "value stack overflow"),
Self::ValueStackUnderflow => write!(f, "value stack underflow"),
Self::CallStackOverflow => write!(f, "call stack overflow"),
Self::CallStackUnderflow => write!(f, "call stack underflow"),
Self::InvalidStackValue(value) => write!(
f,
"stack value {value} was invalid for the current operation"
),
Self::InvalidPointIndex(index) => write!(f, "point index {index} was out of bounds"),
Self::InvalidPointRange(start, end) => {
write!(f, "point range {start}..{end} was out of bounds")
}
Self::InvalidContourIndex(index) => {
write!(f, "contour index {index} was out of bounds")
}
Self::InvalidCvtIndex(index) => write!(f, "cvt index {index} was out of bounds"),
Self::InvalidStorageIndex(index) => {
write!(f, "storage area index {index} was out of bounds")
}
Self::DivideByZero => write!(f, "attempt to divide by 0"),
Self::InvalidZoneIndex(index) => write!(
f,
"zone index {index} was invalid (only 0 or 1 are permitted)"
),
Self::NegativeLoopCounter => {
write!(f, "attempt to set the loop counter to a negative value")
}
Self::InvalidJump => write!(f, "the target of a jump instruction was invalid"),
Self::ExceededExecutionBudget => write!(f, "too many instructions executed"),
}
}
}
impl From<DecodeError> for HintErrorKind {
fn from(_: DecodeError) -> Self {
Self::UnexpectedEndOfBytecode
}
}
/// Hinting error with additional context.
#[derive(Clone, Debug)]
pub struct HintError {
pub program: Program,
pub glyph_id: Option<GlyphId>,
pub pc: usize,
pub opcode: Option<Opcode>,
pub kind: HintErrorKind,
}
impl core::fmt::Display for HintError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.program {
Program::ControlValue => write!(f, "prep")?,
Program::Font => write!(f, "fpgm")?,
Program::Glyph => write!(f, "glyf")?,
}
if let Some(glyph_id) = self.glyph_id {
write!(f, "[{}]", glyph_id.to_u32())?;
}
let (opcode, colon) = match self.opcode {
Some(opcode) => (opcode.name(), ":"),
_ => ("", ""),
};
write!(f, "@{}:{opcode}{colon} {}", self.pc, self.kind)
}
}

View File

@@ -0,0 +1,316 @@
//! Graphics state for the TrueType interpreter.
use super::{
round::RoundState,
zone::{Zone, ZonePointer},
F26Dot6, Point, Target,
};
use core::ops::{Deref, DerefMut};
/// Describes the axis to which a measurement or point movement operation
/// applies.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum CoordAxis {
#[default]
Both,
X,
Y,
}
/// Context in which instructions are executed.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html>
#[derive(Debug)]
pub struct GraphicsState<'a> {
/// Fields of the graphics state that persist between calls to the interpreter.
pub retained: RetainedGraphicsState,
/// A unit vector whose direction establishes an axis along which
/// distances are measured.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#projection%20vector>
pub proj_vector: Point<i32>,
/// Current axis for the projection vector.
pub proj_axis: CoordAxis,
/// A second projection vector set to a line defined by the original
/// outline location of two points. The dual projection vector is used
/// when it is necessary to measure distances from the scaled outline
/// before any instructions were executed.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#dual%20projection%20vector>
pub dual_proj_vector: Point<i32>,
/// Current axis for the dual projection vector.
pub dual_proj_axis: CoordAxis,
/// A unit vector that establishes an axis along which points can move.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#freedom%20vector>
pub freedom_vector: Point<i32>,
/// Current axis for point movement.
pub freedom_axis: CoordAxis,
/// Dot product of freedom and projection vectors.
pub fdotp: i32,
/// Determines the manner in which values are rounded.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#round%20state>
pub round_state: RoundState,
/// First reference point.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#rp0>
pub rp0: usize,
/// Second reference point.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#rp1>
pub rp1: usize,
/// Third reference point.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#rp1>
pub rp2: usize,
/// Makes it possible to repeat certain instructions a designated number of
/// times. The default value of one assures that unless the value of loop
/// is altered, these instructions will execute one time.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#loop>
pub loop_counter: u32,
/// First zone pointer.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#zp0>
pub zp0: ZonePointer,
/// Second zone pointer.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#zp1>
pub zp1: ZonePointer,
/// Third zone pointer.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#zp2>
pub zp2: ZonePointer,
/// Outline data for each zone.
///
/// This array contains the twilight and glyph zones, in that order.
pub zones: [Zone<'a>; 2],
/// True if the current glyph is a composite.
pub is_composite: bool,
/// If true, enables a set of backward compatibility heuristics that
/// prevent certain modifications to the outline. The purpose is to
/// support "modern" vertical only hinting that attempts to preserve
/// outline shape and metrics in the horizontal direction. This is
/// enabled by default, but fonts (and specific glyphs) can opt out
/// of this behavior using the INSTCTRL instruction. In practice,
/// opting out is usually only done by "ClearType native" fonts.
///
/// See <https://learn.microsoft.com/en-us/typography/cleartype/truetypecleartype>
/// for more background and some gory details.
///
/// Defaults to true.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.h#L344>
pub backward_compatibility: bool,
/// If true, enables more strict error checking.
///
/// Defaults to false.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.h#L195>
pub is_pedantic: bool,
/// Set to true when IUP has been executed in the horizontal direction.
pub did_iup_x: bool,
/// Set to true when IUP has been executed in the vertical direction.
pub did_iup_y: bool,
}
impl GraphicsState<'_> {
/// Returns the factor for scaling unscaled points to pixels.
///
/// For composite glyphs, "unscaled" points are already scaled so we
/// return the identity.
pub fn unscaled_to_pixels(&self) -> i32 {
if self.is_composite {
1 << 16
} else {
self.scale
}
}
/// Resets the non-retained portions of the graphics state.
pub fn reset(&mut self) {
let GraphicsState {
retained,
zones,
is_composite,
..
} = core::mem::take(self);
*self = GraphicsState {
retained,
zones,
is_composite,
..Default::default()
};
self.update_projection_state();
}
/// Resets the retained portion of the graphics state to default
/// values while saving the user instance settings.
pub fn reset_retained(&mut self) {
let scale = self.scale;
let ppem = self.ppem;
let mode = self.target;
self.retained = RetainedGraphicsState {
scale,
ppem,
target: mode,
..Default::default()
}
}
}
impl Default for GraphicsState<'_> {
fn default() -> Self {
// For table of default values, see <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_graphics_state>
// All vectors are set to the x-axis (normalized in 2.14)
let vector = Point::new(0x4000, 0);
Self {
retained: RetainedGraphicsState::default(),
proj_vector: vector,
proj_axis: CoordAxis::Both,
dual_proj_vector: vector,
dual_proj_axis: CoordAxis::Both,
freedom_vector: vector,
freedom_axis: CoordAxis::Both,
fdotp: 0x4000,
round_state: RoundState::default(),
rp0: 0,
rp1: 0,
rp2: 0,
loop_counter: 1,
zp0: ZonePointer::default(),
zp1: ZonePointer::default(),
zp2: ZonePointer::default(),
zones: [Zone::default(), Zone::default()],
is_composite: false,
backward_compatibility: true,
is_pedantic: false,
did_iup_x: false,
did_iup_y: false,
}
}
}
/// The persistent graphics state.
///
/// Some of the graphics state is set by the control value program and
/// persists between runs of the interpreter. This struct captures that
/// state.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html>
#[derive(Copy, Clone, Debug)]
pub struct RetainedGraphicsState {
/// Controls whether the sign of control value table entries will be
/// changed to match the sign of the actual distance measurement with
/// which it is compared.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#auto%20flip>
pub auto_flip: bool,
/// Limits the regularizing effects of control value table entries to
/// cases where the difference between the table value and the measurement
/// taken from the original outline is sufficiently small.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#control_value_cut-in>
pub control_value_cutin: F26Dot6,
/// Establishes the base value used to calculate the range of point sizes
/// to which a given DELTAC[] or DELTAP[] instruction will apply.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#delta%20base>
pub delta_base: u16,
/// Determines the range of movement and smallest magnitude of movement
/// (the step) in a DELTAC[] or DELTAP[] instruction.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#delta%20shift>
pub delta_shift: u16,
/// Makes it possible to turn off instructions under some circumstances.
/// When set to TRUE, no instructions will be executed
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#instruct%20control>
pub instruct_control: u8,
/// Establishes the smallest possible value to which a distance will be
/// rounded.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#minimum%20distance>
pub min_distance: F26Dot6,
/// Determines whether the interpreter will activate dropout control for
/// the current glyph.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#scan%20control>
pub scan_control: bool,
/// Type associated with `scan_control`.
pub scan_type: i32,
/// The distance difference below which the interpreter will replace a
/// CVT distance or an actual distance in favor of the single width value.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#single_width_cut_in>
pub single_width_cutin: F26Dot6,
/// The value used in place of the control value table distance or the
/// actual distance value when the difference between that distance and
/// the single width value is less than the single width cut-in.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#single_width_value>
pub single_width: F26Dot6,
/// The user requested hinting target.
pub target: Target,
/// The scale factor for the current instance. Conversion from font units
/// to 26.6 for current ppem.
pub scale: i32,
/// The nominal pixels per em value for the current instance.
pub ppem: i32,
/// True if a rotation is being applied.
pub is_rotated: bool,
/// True if a non-uniform scale is being applied.
pub is_stretched: bool,
}
impl RetainedGraphicsState {
pub fn new(scale: i32, ppem: i32, target: Target) -> Self {
Self {
scale,
ppem,
target,
..Default::default()
}
}
}
impl Default for RetainedGraphicsState {
fn default() -> Self {
// For table of default values, see <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_graphics_state>
Self {
auto_flip: true,
// 17/16 pixels in 26.6
// (17 * 64 / 16) = 68
control_value_cutin: F26Dot6::from_bits(68),
delta_base: 9,
delta_shift: 3,
instruct_control: 0,
// 1 pixel in 26.6
min_distance: F26Dot6::from_bits(64),
scan_control: false,
scan_type: 0,
single_width_cutin: F26Dot6::ZERO,
single_width: F26Dot6::ZERO,
target: Default::default(),
scale: 0,
ppem: 0,
is_rotated: false,
is_stretched: false,
}
}
}
impl Deref for GraphicsState<'_> {
type Target = RetainedGraphicsState;
fn deref(&self) -> &Self::Target {
&self.retained
}
}
impl DerefMut for GraphicsState<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.retained
}
}

View File

@@ -0,0 +1,275 @@
//! Instance state for TrueType hinting.
use super::{
super::Outlines,
cow_slice::CowSlice,
definition::{Definition, DefinitionMap, DefinitionState},
engine::Engine,
error::HintError,
graphics::RetainedGraphicsState,
program::{Program, ProgramState},
value_stack::ValueStack,
zone::Zone,
HintOutline, PointFlags, Target,
};
use alloc::vec::Vec;
use raw::{
types::{F26Dot6, F2Dot14, Fixed, Point},
TableProvider,
};
#[derive(Clone, Default)]
pub struct HintInstance {
functions: Vec<Definition>,
instructions: Vec<Definition>,
cvt: Vec<i32>,
storage: Vec<i32>,
graphics: RetainedGraphicsState,
twilight_scaled: Vec<Point<F26Dot6>>,
twilight_original_scaled: Vec<Point<F26Dot6>>,
twilight_flags: Vec<PointFlags>,
axis_count: u16,
max_stack: usize,
}
impl HintInstance {
pub fn reconfigure(
&mut self,
outlines: &Outlines,
scale: i32,
ppem: i32,
target: Target,
coords: &[F2Dot14],
) -> Result<(), HintError> {
self.setup(outlines, scale, coords);
let twilight_contours = [self.twilight_scaled.len() as u16];
let twilight = Zone::new(
&[],
&mut self.twilight_original_scaled,
&mut self.twilight_scaled,
&mut self.twilight_flags,
&twilight_contours,
);
let glyph = Zone::default();
let mut stack_buf = vec![0; self.max_stack];
let value_stack = ValueStack::new(&mut stack_buf, false);
let graphics = RetainedGraphicsState::new(scale, ppem, target);
let mut engine = Engine::new(
outlines,
ProgramState::new(outlines.fpgm, outlines.prep, &[], Program::Font),
graphics,
DefinitionState::new(
DefinitionMap::Mut(&mut self.functions),
DefinitionMap::Mut(&mut self.instructions),
),
CowSlice::new_mut(&mut self.cvt),
CowSlice::new_mut(&mut self.storage),
value_stack,
twilight,
glyph,
self.axis_count,
coords,
false,
);
// Run the font program (fpgm)
engine.run_program(Program::Font, false)?;
// Run the control value program (prep)
engine.run_program(Program::ControlValue, false)?;
// Save the retained state from the CV program
self.graphics = *engine.retained_graphics_state();
Ok(())
}
/// Returns true if we should actually apply hinting.
///
/// Hinting can be completely disabled by the control value program.
pub fn is_enabled(&self) -> bool {
// If bit 0 is set, disables hinting entirely
self.graphics.instruct_control & 1 == 0
}
/// Returns true if backward compatibility mode has been activated
/// by the hinter settings or the `prep` table.
pub fn backward_compatibility(&self) -> bool {
// Set backward compatibility mode
if self.graphics.target.preserve_linear_metrics() {
true
} else if self.graphics.target.is_smooth() {
(self.graphics.instruct_control & 0x4) == 0
} else {
false
}
}
pub fn hint(
&self,
outlines: &Outlines,
outline: &mut HintOutline,
is_pedantic: bool,
) -> Result<(), HintError> {
// Twilight zone
let twilight_count = outline.twilight_scaled.len();
let twilight_contours = [twilight_count as u16];
outline
.twilight_original_scaled
.copy_from_slice(&self.twilight_original_scaled);
outline
.twilight_scaled
.copy_from_slice(&self.twilight_scaled);
outline.twilight_flags.copy_from_slice(&self.twilight_flags);
let twilight = Zone::new(
&[],
outline.twilight_original_scaled,
outline.twilight_scaled,
outline.twilight_flags,
&twilight_contours,
);
// Glyph zone
let glyph = Zone::new(
outline.unscaled,
outline.original_scaled,
outline.scaled,
outline.flags,
outline.contours,
);
let value_stack = ValueStack::new(outline.stack, is_pedantic);
let cvt = CowSlice::new(&self.cvt, outline.cvt).unwrap();
let storage = CowSlice::new(&self.storage, outline.storage).unwrap();
let mut engine = Engine::new(
outlines,
ProgramState::new(
outlines.fpgm,
outlines.prep,
outline.bytecode,
Program::Glyph,
),
self.graphics,
DefinitionState::new(
DefinitionMap::Ref(&self.functions),
DefinitionMap::Ref(&self.instructions),
),
cvt,
storage,
value_stack,
twilight,
glyph,
self.axis_count,
outline.coords,
outline.is_composite,
);
engine
.run_program(Program::Glyph, is_pedantic)
.map_err(|mut e| {
e.glyph_id = Some(outline.glyph_id);
e
})?;
// If we're not running in backward compatibility mode, capture
// modified phantom points.
if !engine.backward_compatibility() {
for (i, p) in (outline.scaled[outline.scaled.len() - 4..])
.iter()
.enumerate()
{
outline.phantom[i] = *p;
}
}
Ok(())
}
/// Captures limits, resizes buffers and scales the CVT.
fn setup(&mut self, outlines: &Outlines, scale: i32, coords: &[F2Dot14]) {
let axis_count = outlines
.gvar
.as_ref()
.map(|gvar| gvar.axis_count())
.unwrap_or_default();
self.functions.clear();
self.functions
.resize(outlines.max_function_defs as usize, Definition::default());
self.instructions.resize(
outlines.max_instruction_defs as usize,
Definition::default(),
);
self.cvt.clear();
let cvt = outlines.font.cvt().unwrap_or_default();
if let Ok(cvar) = outlines.font.cvar() {
// First accumulate all the deltas in 16.16
self.cvt.resize(cvt.len(), 0);
let _ = cvar.deltas(axis_count, coords, &mut self.cvt);
// Now add the base CVT values
for (value, base_value) in self.cvt.iter_mut().zip(cvt.iter()) {
// Deltas are converted from 16.16 to 26.6
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgxvar.c#L3822>
let delta = Fixed::from_bits(*value).to_f26dot6().to_bits();
let base_value = base_value.get() as i32 * 64;
*value = base_value + delta;
}
} else {
// CVT values are converted to 26.6 on load
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttpload.c#L350>
self.cvt
.extend(cvt.iter().map(|value| (value.get() as i32) * 64));
}
// More weird scaling. This is due to the fact that CVT values are
// already in 26.6
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttobjs.c#L996>
let scale = Fixed::from_bits(scale >> 6);
for value in &mut self.cvt {
*value = (Fixed::from_bits(*value) * scale).to_bits();
}
self.storage.clear();
self.storage.resize(outlines.max_storage as usize, 0);
let max_twilight_points = outlines.max_twilight_points as usize;
self.twilight_scaled.clear();
self.twilight_scaled
.resize(max_twilight_points, Default::default());
self.twilight_original_scaled.clear();
self.twilight_original_scaled
.resize(max_twilight_points, Default::default());
self.twilight_flags.clear();
self.twilight_flags
.resize(max_twilight_points, Default::default());
self.axis_count = axis_count;
self.max_stack = outlines.max_stack_elements as usize;
self.graphics = RetainedGraphicsState::default();
}
}
#[cfg(test)]
impl HintInstance {
/// Enable instruct control bit 1 which effectively disables hinting.
///
/// This mimics what the `prep` table might do for various configurations
/// and font sizes. Used for testing.
pub fn simulate_prep_flag_suppress_hinting(&mut self) {
self.graphics.instruct_control |= 1;
}
}
#[cfg(test)]
mod tests {
use super::{super::super::Outlines, HintInstance};
use read_fonts::{types::F2Dot14, FontRef};
#[test]
fn scaled_cvar_cvt() {
let font = FontRef::new(font_test_data::CVAR).unwrap();
let outlines = Outlines::new(&font).unwrap();
let mut instance = HintInstance::default();
let coords = [0.5, -0.5].map(F2Dot14::from_f32);
let ppem = 16;
// ppem * 64 / upem
let scale = 67109;
instance
.reconfigure(&outlines, scale, ppem, Default::default(), &coords)
.unwrap();
let expected = [
778, 10, 731, 0, 731, 10, 549, 10, 0, 0, 0, -10, 0, -10, -256, -10, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 95, 137, 99, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 60, 0, 81, 0, 0, 0, 0, 0, 51, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
];
assert_eq!(&instance.cvt, &expected);
}
}

View File

@@ -0,0 +1,324 @@
//! Fixed point math helpers that are specific to TrueType hinting.
//!
//! These are implemented in terms of font-types types when possible. It
//! likely makes sense to use more strongly typed fixed point values
//! in the future.
use read_fonts::types::{Fixed, Point};
pub fn floor(x: i32) -> i32 {
x & !63
}
pub fn round(x: i32) -> i32 {
floor(x + 32)
}
pub fn ceil(x: i32) -> i32 {
floor(x + 63)
}
fn floor_pad(x: i32, n: i32) -> i32 {
x & !(n - 1)
}
pub fn round_pad(x: i32, n: i32) -> i32 {
floor_pad(x + n / 2, n)
}
#[inline(always)]
pub fn mul(a: i32, b: i32) -> i32 {
(Fixed::from_bits(a) * Fixed::from_bits(b)).to_bits()
}
pub fn div(a: i32, b: i32) -> i32 {
(Fixed::from_bits(a) / Fixed::from_bits(b)).to_bits()
}
/// Fixed point multiply and divide: a * b / c
pub fn mul_div(a: i32, b: i32, c: i32) -> i32 {
Fixed::from_bits(a)
.mul_div(Fixed::from_bits(b), Fixed::from_bits(c))
.to_bits()
}
/// Fixed point multiply and divide without rounding: a * b / c
///
/// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftcalc.c#L200>
pub fn mul_div_no_round(mut a: i32, mut b: i32, mut c: i32) -> i32 {
let mut s = 1;
if a < 0 {
a = -a;
s = -1;
}
if b < 0 {
b = -b;
s = -s;
}
if c < 0 {
c = -c;
s = -s;
}
let d = if c > 0 {
((a as i64) * (b as i64)) / c as i64
} else {
0x7FFFFFFF
};
if s < 0 {
-(d as i32)
} else {
d as i32
}
}
/// Multiplication for 2.14 fixed point.
///
/// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1234>
pub fn mul14(a: i32, b: i32) -> i32 {
let mut v = a as i64 * b as i64;
v += 0x2000 + (v >> 63);
(v >> 14) as i32
}
/// Normalize a vector in 2.14 fixed point.
///
/// Direct port of <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftcalc.c#L800>
pub fn normalize14(x: i32, y: i32) -> Point<i32> {
use core::num::Wrapping;
let (mut sx, mut sy) = (Wrapping(1i32), Wrapping(1i32));
let mut ux = Wrapping(x as u32);
let mut uy = Wrapping(y as u32);
const ZERO: Wrapping<u32> = Wrapping(0);
let mut result = Point::default();
if x < 0 {
ux = ZERO - ux;
sx = -sx;
}
if y < 0 {
uy = ZERO - uy;
sy = -sy;
}
if ux == ZERO {
result.x = x / 4;
if uy.0 > 0 {
result.y = (sy * Wrapping(0x10000) / Wrapping(4)).0;
}
return result;
}
if uy == ZERO {
result.y = y / 4;
if ux.0 > 0 {
result.x = (sx * Wrapping(0x10000) / Wrapping(4)).0;
}
return result;
}
let mut len = if ux > uy {
ux + (uy >> 1)
} else {
uy + (ux >> 1)
};
let mut shift = Wrapping(len.0.leading_zeros() as i32);
shift -= Wrapping(15)
+ if len >= (Wrapping(0xAAAAAAAAu32) >> shift.0 as usize) {
Wrapping(1)
} else {
Wrapping(0)
};
if shift.0 > 0 {
let s = shift.0 as usize;
ux <<= s;
uy <<= s;
len = if ux > uy {
ux + (uy >> 1)
} else {
uy + (ux >> 1)
};
} else {
let s = -shift.0 as usize;
ux >>= s;
uy >>= s;
len >>= s;
}
let mut b = Wrapping(0x10000) - Wrapping(len.0 as i32);
let x = Wrapping(ux.0 as i32);
let y = Wrapping(uy.0 as i32);
let mut z;
let mut u;
let mut v;
loop {
u = Wrapping((x + ((x * b) >> 16)).0 as u32);
v = Wrapping((y + ((y * b) >> 16)).0 as u32);
z = Wrapping(-((u * u + v * v).0 as i32)) / Wrapping(0x200);
z = z * ((Wrapping(0x10000) + b) >> 8) / Wrapping(0x10000);
b += z;
if z <= Wrapping(0) {
break;
}
}
Point::new(
(Wrapping(u.0 as i32) * sx / Wrapping(4)).0,
(Wrapping(v.0 as i32) * sy / Wrapping(4)).0,
)
}
#[cfg(test)]
mod tests {
use raw::types::{F2Dot14, Fixed};
/// Tolerance value for floating point sanity checks.
/// Tests with large sets of values show this is the best we can
/// expect from the fixed point implementations.
const FLOAT_TOLERANCE: f32 = 1e-4;
#[test]
fn mul_div_no_round() {
let cases = [
// computed with FT_MulDiv_NoRound():
// ((a, b, c), result) where result = a * b / c
((-326, -11474, 9942), 376),
((-6781, 13948, 11973), -7899),
((3517, 15622, 8075), 6804),
((-6127, 15026, 2276), -40450),
((11257, 14828, 2542), 65664),
((-12797, -16280, -9086), -22929),
((-7994, -3340, 9583), 2786),
((-16101, -13780, -1427), -155481),
((10304, -16331, 15480), -10870),
((-15879, 11912, -4650), 40677),
((-5015, 6382, -15977), 2003),
((2080, -11930, -15457), 1605),
((-11071, 13350, 16138), -9158),
((16084, -13564, -770), 283329),
((14304, -10377, -21), 7068219),
((-14056, -8853, -5488), -22674),
((-10319, 14797, 8554), -17850),
((-7820, 6826, 10555), -5057),
((7257, 15928, 8159), 14167),
((14929, 11579, -13204), -13091),
((2808, 12070, -14697), -2306),
((-13818, 8544, -1649), 71595),
((3265, 7325, -1373), -17418),
((14832, 10586, -6440), -24380),
((4123, 8274, -2022), -16871),
((4645, -4149, -7242), 2661),
((-3891, 8366, 5771), -5640),
((-15447, -3428, -9335), -5672),
((13670, -14311, -11122), 17589),
((12590, -6592, 13159), -6306),
((-8369, -10193, 5051), 16888),
((-9539, 5167, 2595), -18993),
];
for ((a, b, c), expected_result) in cases {
let result = super::mul_div_no_round(a, b, c);
assert_eq!(result, expected_result);
let fa = Fixed::from_bits(a as _).to_f32();
let fb = Fixed::from_bits(b as _).to_f32();
let fc = Fixed::from_bits(c as _).to_f32();
let fresult = fa * fb / fc;
let fexpected_result = Fixed::from_bits(expected_result as _).to_f32();
assert!((fresult - fexpected_result).abs() < FLOAT_TOLERANCE);
}
}
#[test]
fn mul14() {
let cases = [
// computed with TT_MulFix14():
// ((a, b), result) where result = a * b
((6236, -10078), -3836),
((-6803, -5405), 2244),
((-10006, -12852), 7849),
((-15434, -4102), 3864),
((-8681, 9269), -4911),
((9449, -9130), -5265),
((12643, 2161), 1668),
((-6115, 9284), -3465),
((316, 3390), 65),
((15077, -12901), -11872),
((-12182, 11613), -8635),
((-7213, 8246), -3630),
((13482, 8096), 6662),
((5690, 15016), 5215),
((-5991, 12613), -4612),
((13112, -8404), -6726),
((13524, 6786), 5601),
((7156, 3291), 1437),
((-2978, 353), -64),
((-1755, 14626), -1567),
((14402, 7886), 6932),
((7124, 15730), 6840),
((-12679, 14830), -11476),
((-9374, -12999), 7437),
((12301, -4685), -3517),
((5324, 2066), 671),
((6783, -4946), -2048),
((12078, -968), -714),
((-10137, 14116), -8734),
((-13946, 11585), -9861),
((-678, -2205), 91),
((-2629, -3319), 533),
];
for ((a, b), expected_result) in cases {
let result = super::mul14(a, b);
assert_eq!(result, expected_result);
let fa = F2Dot14::from_bits(a as _).to_f32();
let fb = F2Dot14::from_bits(b as _).to_f32();
let fresult = fa * fb;
let fexpected_result = F2Dot14::from_bits(expected_result as _).to_f32();
assert!((fresult - fexpected_result).abs() < FLOAT_TOLERANCE);
}
}
#[test]
fn normalize14() {
let cases = [
// computed with FT_Vector_NormLen():
// (input vector, expected normalized vector)
((-13660, 11807), (-12395, 10713)),
((-10763, 9293), (-12401, 10707)),
((-3673, 673), (-16115, 2952)),
((15886, -2964), (16106, -3005)),
((15442, -2871), (16108, -2994)),
((-6308, 5744), (-12114, 11031)),
((9410, -10415), (10983, -12156)),
((-10620, -14856), (-9528, -13328)),
((-9372, 12029), (-10069, 12924)),
((-1272, -1261), (-11635, -11534)),
((-7076, -5517), (-12920, -10074)),
((-10297, 179), (-16381, 284)),
((9256, -13235), (9389, -13426)),
((5315, -12449), (6433, -15068)),
((8064, 15213), (7673, 14476)),
((-8665, 41), (-16383, 77)),
((-3455, -4720), (-9677, -13220)),
((13449, -5152), (15299, -5861)),
((-15605, 8230), (-14492, 7643)),
((4716, -13690), (5336, -15490)),
((12904, -11422), (12268, -10859)),
((2825, -6396), (6619, -14987)),
((4654, 15245), (4783, 15670)),
((-14769, 15133), (-11443, 11725)),
((-8090, -9057), (-10914, -12219)),
((-472, 1953), (-3848, 15925)),
((-12563, 1040), (-16328, 1351)),
((-7938, 15587), (-7435, 14599)),
((-9701, 5356), (-14343, 7919)),
((-642, -14484), (-725, -16367)),
((12963, -9690), (13123, -9809)),
((7067, 5361), (13053, 9902)),
((0x4000, 0), (0x4000, 0)),
((0, 0x4000), (0, 0x4000)),
((-0x4000, 0), (-0x4000, 0)),
((0, -0x4000), (0, -0x4000)),
];
for ((x, y), expected) in cases {
let n = super::normalize14(x, y);
assert_eq!((n.x, n.y), expected);
// Ensure the length of the vector is nearly 1.0
let fx = F2Dot14::from_bits(n.x as _).to_f32();
let fy = F2Dot14::from_bits(n.y as _).to_f32();
let flen = (fx * fx + fy * fy).sqrt();
assert!((flen - 1.0).abs() <= FLOAT_TOLERANCE);
}
}
}

View File

@@ -0,0 +1,47 @@
//! TrueType hinting.
mod call_stack;
mod cow_slice;
mod cvt;
mod definition;
mod engine;
mod error;
mod graphics;
mod instance;
mod math;
mod program;
mod projection;
mod round;
mod storage;
mod value_stack;
mod zone;
use super::super::Target;
use read_fonts::{
tables::glyf::PointFlags,
types::{F26Dot6, F2Dot14, GlyphId, Point},
};
pub use error::HintError;
pub use instance::HintInstance;
/// Outline data that is passed to the hinter.
pub struct HintOutline<'a> {
pub glyph_id: GlyphId,
pub unscaled: &'a [Point<i32>],
pub scaled: &'a mut [Point<F26Dot6>],
pub original_scaled: &'a mut [Point<F26Dot6>],
pub flags: &'a mut [PointFlags],
pub contours: &'a [u16],
pub phantom: &'a mut [Point<F26Dot6>],
pub bytecode: &'a [u8],
pub stack: &'a mut [i32],
pub cvt: &'a mut [i32],
pub storage: &'a mut [i32],
pub twilight_scaled: &'a mut [Point<F26Dot6>],
pub twilight_original_scaled: &'a mut [Point<F26Dot6>],
pub twilight_flags: &'a mut [PointFlags],
pub is_composite: bool,
pub coords: &'a [F2Dot14],
}

View File

@@ -0,0 +1,163 @@
//! TrueType program management.
use raw::tables::glyf::bytecode::Decoder;
use super::{
call_stack::{CallRecord, CallStack},
definition::Definition,
error::HintErrorKind,
};
/// Describes the source for a piece of bytecode.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
#[repr(u8)]
pub enum Program {
/// Program that initializes the function and instruction tables. Stored
/// in the `fpgm` table.
#[default]
Font = 0,
/// Program that initializes CVT and storage based on font size and other
/// parameters. Stored in the `prep` table.
ControlValue = 1,
/// Glyph specified program. Stored per-glyph in the `glyf` table.
Glyph = 2,
}
/// State for managing active programs and decoding instructions.
pub struct ProgramState<'a> {
/// Bytecode for each of the three program types, indexed by `Program`.
pub bytecode: [&'a [u8]; 3],
/// The initial program when execution begins.
pub initial: Program,
/// The currently active program.
pub current: Program,
/// Instruction decoder for the currently active program.
pub decoder: Decoder<'a>,
/// Tracks nested function and instruction invocations.
pub call_stack: CallStack,
}
impl<'a> ProgramState<'a> {
pub fn new(
font_code: &'a [u8],
cv_code: &'a [u8],
glyph_code: &'a [u8],
initial_program: Program,
) -> Self {
let bytecode = [font_code, cv_code, glyph_code];
Self {
bytecode,
initial: initial_program,
current: initial_program,
decoder: Decoder::new(bytecode[initial_program as usize], 0),
call_stack: CallStack::default(),
}
}
/// Resets the state for execution of the given program.
pub fn reset(&mut self, program: Program) {
self.initial = program;
self.current = program;
self.decoder = Decoder::new(self.bytecode[program as usize], 0);
self.call_stack.clear();
}
/// Jumps to the code in the given definition and sets it up for
/// execution `count` times.
pub fn enter(&mut self, definition: Definition, count: u32) -> Result<(), HintErrorKind> {
let program = definition.program();
let pc = definition.code_range().start;
let bytecode = self.bytecode[program as usize];
self.call_stack.push(CallRecord {
caller_program: self.current,
return_pc: self.decoder.pc,
current_count: count,
definition,
})?;
self.current = program;
self.decoder = Decoder::new(bytecode, pc);
Ok(())
}
/// Leaves the code from the definition on the top of the stack.
///
/// If the top call record has a loop count greater than 1, restarts
/// execution from the beginning of the definition. Otherwise, resumes
/// execution at the previously active definition.
pub fn leave(&mut self) -> Result<(), HintErrorKind> {
let mut record = self.call_stack.pop()?;
if record.current_count > 1 {
// This is a loop call with some iterations remaining.
record.current_count -= 1;
self.decoder.pc = record.definition.code_range().start;
self.call_stack.push(record)?;
} else {
self.current = record.caller_program;
// Reset the decoder to the calling program and program counter.
self.decoder.bytecode = self.bytecode[record.caller_program as usize];
self.decoder.pc = record.return_pc;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
/// Test accounting of program, bytecode and program counter through
/// enter/leave cycles.
#[test]
fn accounting() {
let font_code = &[0][..];
let cv_code = &[1][..];
let glyph_code = &[2][..];
let mut state = ProgramState::new(font_code, cv_code, glyph_code, Program::Glyph);
// We start at glyph code
assert_eq!(state.active_state(), (Program::Glyph, glyph_code, 0));
let font_def = Definition::new(Program::Font, 10..20, 0);
let cv_def = Definition::new(Program::ControlValue, 33..111, 1);
// Now move to CV code
state.enter(cv_def, 1).unwrap();
assert_eq!(state.active_state(), (Program::ControlValue, cv_code, 33));
// Bump the program counter to test capture of return_pc
state.decoder.pc += 20;
// And to font code
state.enter(font_def, 1).unwrap();
assert_eq!(state.active_state(), (Program::Font, font_code, 10));
// Back to CV code
state.leave().unwrap();
assert_eq!(state.active_state(), (Program::ControlValue, cv_code, 53));
// And to the original glyph code
state.leave().unwrap();
assert_eq!(state.active_state(), (Program::Glyph, glyph_code, 0));
}
/// Ensure calls with a count of `n` require `n` leaves before returning
/// to previous frame. Also ensure program counter is reset to start of
/// definition at each leave.
#[test]
fn loop_call() {
let font_code = &[0][..];
let cv_code = &[1][..];
let glyph_code = &[2][..];
let mut state = ProgramState::new(font_code, cv_code, glyph_code, Program::Glyph);
let font_def = Definition::new(Program::Font, 10..20, 0);
// "Execute" font definition 3 times
state.enter(font_def, 3).unwrap();
for _ in 0..3 {
assert_eq!(state.active_state(), (Program::Font, font_code, 10));
// Modify program counter to ensure we reset on leave
state.decoder.pc += 22;
state.leave().unwrap();
}
// Should be back to glyph code
assert_eq!(state.active_state(), (Program::Glyph, glyph_code, 0));
}
impl<'a> ProgramState<'a> {
fn active_state(&self) -> (Program, &'a [u8], usize) {
(self.current, self.decoder.bytecode, self.decoder.pc)
}
}
}

View File

@@ -0,0 +1,177 @@
//! Point projection.
use super::graphics::{CoordAxis, GraphicsState};
use raw::types::{F26Dot6, Point};
impl GraphicsState<'_> {
/// Updates cached state that is derived from projection vectors.
pub fn update_projection_state(&mut self) {
// 1.0 in 2.14 fixed point.
const ONE: i32 = 0x4000;
// Based on Compute_Funcs() at
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2482>.
// FreeType uses function pointers to select between various "modes"
// but we use the CoordAxis type instead.
if self.freedom_vector.x == ONE {
self.fdotp = self.proj_vector.x;
} else if self.freedom_vector.y == ONE {
self.fdotp = self.proj_vector.y;
} else {
let px = self.proj_vector.x;
let py = self.proj_vector.y;
let fx = self.freedom_vector.x;
let fy = self.freedom_vector.y;
self.fdotp = (px * fx + py * fy) >> 14;
}
self.proj_axis = CoordAxis::Both;
if self.proj_vector.x == ONE {
self.proj_axis = CoordAxis::X;
} else if self.proj_vector.y == ONE {
self.proj_axis = CoordAxis::Y;
}
self.dual_proj_axis = CoordAxis::Both;
if self.dual_proj_vector.x == ONE {
self.dual_proj_axis = CoordAxis::X;
} else if self.dual_proj_vector.y == ONE {
self.dual_proj_axis = CoordAxis::Y;
}
self.freedom_axis = CoordAxis::Both;
if self.fdotp == ONE {
if self.freedom_vector.x == ONE {
self.freedom_axis = CoordAxis::X;
} else if self.freedom_vector.y == ONE {
self.freedom_axis = CoordAxis::Y;
}
}
// At small sizes, fdotp can become too small resulting in overflows
// and spikes.
if self.fdotp.abs() < 0x400 {
self.fdotp = ONE;
}
}
/// Computes the projection of vector given by (v1 - v2) along the
/// current projection vector.
#[inline(always)]
pub fn project(&self, v1: Point<F26Dot6>, v2: Point<F26Dot6>) -> F26Dot6 {
match self.proj_axis {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2431>
CoordAxis::X => v1.x - v2.x,
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2461>
CoordAxis::Y => v1.y - v2.y,
CoordAxis::Both => {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2373>
let dx = v1.x - v2.x;
let dy = v1.y - v2.y;
F26Dot6::from_bits(dot14(
dx.to_bits(),
dy.to_bits(),
self.proj_vector.x,
self.proj_vector.y,
))
}
}
}
/// Computes the projection of vector given by (v1 - v2) along the
/// current dual projection vector.
#[inline(always)]
pub fn dual_project(&self, v1: Point<F26Dot6>, v2: Point<F26Dot6>) -> F26Dot6 {
match self.dual_proj_axis {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2431>
CoordAxis::X => v1.x - v2.x,
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2461>
CoordAxis::Y => v1.y - v2.y,
CoordAxis::Both => {
// https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2402
let dx = v1.x - v2.x;
let dy = v1.y - v2.y;
F26Dot6::from_bits(dot14(
dx.to_bits(),
dy.to_bits(),
self.dual_proj_vector.x,
self.dual_proj_vector.y,
))
}
}
}
/// Computes the projection of vector given by (v1 - v2) along the
/// current dual projection vector for unscaled points.
#[inline(always)]
pub fn dual_project_unscaled(&self, v1: Point<i32>, v2: Point<i32>) -> i32 {
match self.dual_proj_axis {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2431>
CoordAxis::X => v1.x - v2.x,
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2461>
CoordAxis::Y => v1.y - v2.y,
CoordAxis::Both => {
// https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2402
let dx = v1.x - v2.x;
let dy = v1.y - v2.y;
dot14(dx, dy, self.dual_proj_vector.x, self.dual_proj_vector.y)
}
}
}
}
/// Dot product for vectors in 2.14 fixed point.
fn dot14(ax: i32, ay: i32, bx: i32, by: i32) -> i32 {
let mut v1 = ax as i64 * bx as i64;
let v2 = ay as i64 * by as i64;
v1 += v2;
v1 += 0x2000 + (v1 >> 63);
(v1 >> 14) as i32
}
#[cfg(test)]
mod tests {
use super::{super::math, CoordAxis, F26Dot6, GraphicsState, Point};
#[test]
fn project_one_axis() {
let mut state = GraphicsState {
proj_vector: math::normalize14(1, 0),
..Default::default()
};
state.update_projection_state();
assert_eq!(state.proj_axis, CoordAxis::X);
assert_eq!(state.proj_vector, Point::new(0x4000, 0));
let cases = &[
(Point::new(0, 0), Point::new(0, 0), 0),
(Point::new(100, 100), Point::new(0, 0), 100),
(Point::new(42, 100), Point::new(100, 0), -58),
(Point::new(0, 0), Point::new(100, 100), -100),
];
test_project_cases(&state, cases);
}
#[test]
fn project_both_axes() {
let mut state = GraphicsState {
proj_vector: math::normalize14(0x4000, 0x4000),
..Default::default()
};
state.update_projection_state();
assert_eq!(state.proj_axis, CoordAxis::Both);
let cases = &[
(Point::new(0, 0), Point::new(0, 0), 0),
(Point::new(100, 100), Point::new(0, 0), 141),
(Point::new(42, 100), Point::new(100, 0), 30),
(Point::new(0, 0), Point::new(100, 100), -141),
];
test_project_cases(&state, cases);
}
fn test_project_cases(state: &GraphicsState, cases: &[(Point<i32>, Point<i32>, i32)]) {
for (v1, v2, expected) in cases.iter().copied() {
let v1 = v1.map(F26Dot6::from_bits);
let v2 = v2.map(F26Dot6::from_bits);
let result = state.project(v1, v2).to_bits();
assert_eq!(
result, expected,
"project({v1:?}, {v2:?}) = {result} (expected {expected})"
);
}
}
}

View File

@@ -0,0 +1,240 @@
//! Rounding state.
use super::{super::F26Dot6, graphics::GraphicsState};
/// Rounding strategies supported by the interpreter.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum RoundMode {
/// Distances are rounded to the closest grid line.
///
/// Set by `RTG` instruction.
#[default]
Grid,
/// Distances are rounded to the nearest half grid line.
///
/// Set by `RTHG` instruction.
HalfGrid,
/// Distances are rounded to the closest half or integer pixel.
///
/// Set by `RTDG` instruction.
DoubleGrid,
/// Distances are rounded down to the closest integer grid line.
///
/// Set by `RDTG` instruction.
DownToGrid,
/// Distances are rounded up to the closest integer pixel boundary.
///
/// Set by `RUTG` instruction.
UpToGrid,
/// Rounding is turned off.
///
/// Set by `ROFF` instruction.
Off,
/// Allows fine control over the effects of the round state variable by
/// allowing you to set the values of three components of the round_state:
/// period, phase, and threshold.
///
/// More formally, maps the domain of 26.6 fixed point numbers into a set
/// of discrete values that are separated by equal distances.
///
/// Set by `SROUND` instruction.
Super,
/// Analogous to `Super`. The grid period is sqrt(2)/2 pixels rather than 1
/// pixel. It is useful for measuring at a 45 degree angle with the
/// coordinate axes.
///
/// Set by `S45ROUND` instruction.
Super45,
}
/// Graphics state that controls rounding.
///
/// See <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM04/Chap4.html#round%20state>
#[derive(Copy, Clone, Debug)]
pub struct RoundState {
pub mode: RoundMode,
pub threshold: i32,
pub phase: i32,
pub period: i32,
}
impl Default for RoundState {
fn default() -> Self {
Self {
mode: RoundMode::Grid,
threshold: 0,
phase: 0,
period: 64,
}
}
}
impl RoundState {
pub fn round(&self, distance: F26Dot6) -> F26Dot6 {
use super::math;
use RoundMode::*;
let distance = distance.to_bits();
let result = match self.mode {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1958>
HalfGrid => {
if distance >= 0 {
(math::floor(distance) + 32).max(0)
} else {
(-(math::floor(-distance) + 32)).min(0)
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1913>
Grid => {
if distance >= 0 {
math::round(distance).max(0)
} else {
(-math::round(-distance)).min(0)
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2094>
DoubleGrid => {
if distance >= 0 {
math::round_pad(distance, 32).max(0)
} else {
(-math::round_pad(-distance, 32)).min(0)
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2005>
DownToGrid => {
if distance >= 0 {
math::floor(distance).max(0)
} else {
(-math::floor(-distance)).min(0)
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2049>
UpToGrid => {
if distance >= 0 {
math::ceil(distance).max(0)
} else {
(-math::ceil(-distance)).min(0)
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2145>
Super => {
if distance >= 0 {
let val =
((distance + (self.threshold - self.phase)) & -self.period) + self.phase;
if val < 0 {
self.phase
} else {
val
}
} else {
let val =
-(((self.threshold - self.phase) - distance) & -self.period) - self.phase;
if val > 0 {
-self.phase
} else {
val
}
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L2199>
Super45 => {
if distance >= 0 {
let val = (((distance + (self.threshold - self.phase)) / self.period)
* self.period)
+ self.phase;
if val < 0 {
self.phase
} else {
val
}
} else {
let val = -((((self.threshold - self.phase) - distance) / self.period)
* self.period)
- self.phase;
if val > 0 {
-self.phase
} else {
val
}
}
}
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1870>
Off => distance,
};
F26Dot6::from_bits(result)
}
}
impl GraphicsState<'_> {
pub fn round(&self, distance: F26Dot6) -> F26Dot6 {
self.round_state.round(distance)
}
}
#[cfg(test)]
mod tests {
use super::{F26Dot6, RoundMode, RoundState};
#[test]
fn round_to_grid() {
round_cases(
RoundMode::Grid,
&[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
);
}
#[test]
fn round_to_half_grid() {
round_cases(
RoundMode::HalfGrid,
&[(0, 32), (32, 32), (-32, -32), (64, 96), (50, 32)],
);
}
#[test]
fn round_to_double_grid() {
round_cases(
RoundMode::DoubleGrid,
&[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 64)],
);
}
#[test]
fn round_down_to_grid() {
round_cases(
RoundMode::DownToGrid,
&[(0, 0), (32, 0), (-32, 0), (64, 64), (50, 0)],
);
}
#[test]
fn round_up_to_grid() {
round_cases(
RoundMode::UpToGrid,
&[(0, 0), (32, 64), (-32, -64), (64, 64), (50, 64)],
);
}
#[test]
fn round_off() {
round_cases(
RoundMode::Off,
&[(0, 0), (32, 32), (-32, -32), (64, 64), (50, 50)],
);
}
fn round_cases(mode: RoundMode, cases: &[(i32, i32)]) {
for (value, expected) in cases.iter().copied() {
let value = F26Dot6::from_bits(value);
let expected = F26Dot6::from_bits(expected);
let state = RoundState {
mode,
..Default::default()
};
let result = state.round(value);
assert_eq!(
result, expected,
"mismatch in rounding: {mode:?}({value}) = {result} (expected {expected})"
);
}
}
}

View File

@@ -0,0 +1,29 @@
//! Storage area.
use super::{cow_slice::CowSlice, error::HintErrorKind};
/// Backing store for the storage area.
///
/// This is just a wrapper for [`CowSlice`] that converts out of bounds
/// accesses to appropriate errors.
pub struct Storage<'a>(CowSlice<'a>);
impl Storage<'_> {
pub fn get(&self, index: usize) -> Result<i32, HintErrorKind> {
self.0
.get(index)
.ok_or(HintErrorKind::InvalidStorageIndex(index))
}
pub fn set(&mut self, index: usize, value: i32) -> Result<(), HintErrorKind> {
self.0
.set(index, value)
.ok_or(HintErrorKind::InvalidStorageIndex(index))
}
}
impl<'a> From<CowSlice<'a>> for Storage<'a> {
fn from(value: CowSlice<'a>) -> Self {
Self(value)
}
}

View File

@@ -0,0 +1,388 @@
//! Value stack for TrueType interpreter.
//!
use raw::types::F26Dot6;
use read_fonts::tables::glyf::bytecode::InlineOperands;
use super::error::HintErrorKind;
use HintErrorKind::{ValueStackOverflow, ValueStackUnderflow};
/// Value stack for the TrueType interpreter.
///
/// This uses a slice as the backing store rather than a `Vec` to enable
/// support for allocation from user buffers.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#managing-the-stack>
pub struct ValueStack<'a> {
values: &'a mut [i32],
len: usize,
pub(super) is_pedantic: bool,
}
impl<'a> ValueStack<'a> {
pub fn new(values: &'a mut [i32], is_pedantic: bool) -> Self {
Self {
values,
len: 0,
is_pedantic,
}
}
/// Returns the depth of the stack
/// <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#returns-the-depth-of-the-stack>
pub fn len(&self) -> usize {
self.len
}
#[cfg(test)]
fn is_empty(&self) -> bool {
self.len == 0
}
// This is used in tests and also useful for tracing.
#[allow(dead_code)]
pub fn values(&self) -> &[i32] {
&self.values[..self.len]
}
pub fn push(&mut self, value: i32) -> Result<(), HintErrorKind> {
let ptr = self
.values
.get_mut(self.len)
.ok_or(HintErrorKind::ValueStackOverflow)?;
*ptr = value;
self.len += 1;
Ok(())
}
/// Pushes values that have been decoded from the instruction stream
/// onto the stack.
///
/// Implements the PUSHB[], PUSHW[], NPUSHB[] and NPUSHW[] instructions.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#pushing-data-onto-the-interpreter-stack>
pub fn push_inline_operands(&mut self, operands: &InlineOperands) -> Result<(), HintErrorKind> {
let push_count = operands.len();
let stack_base = self.len;
for (stack_value, value) in self
.values
.get_mut(stack_base..stack_base + push_count)
.ok_or(ValueStackOverflow)?
.iter_mut()
.zip(operands.values())
{
*stack_value = value;
}
self.len += push_count;
Ok(())
}
pub fn peek(&mut self) -> Option<i32> {
if self.len > 0 {
self.values.get(self.len - 1).copied()
} else {
None
}
}
/// Pops a value from the stack.
///
/// Implements the POP[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#pop-top-stack-element>
pub fn pop(&mut self) -> Result<i32, HintErrorKind> {
if let Some(value) = self.peek() {
self.len -= 1;
Ok(value)
} else if self.is_pedantic {
Err(ValueStackUnderflow)
} else {
Ok(0)
}
}
/// Convenience method for instructions that expect values in 26.6 format.
pub fn pop_f26dot6(&mut self) -> Result<F26Dot6, HintErrorKind> {
Ok(F26Dot6::from_bits(self.pop()?))
}
/// Convenience method for instructions that pop values that are used as an
/// index.
pub fn pop_usize(&mut self) -> Result<usize, HintErrorKind> {
Ok(self.pop()? as usize)
}
/// Convenience method for popping a value intended as a count.
///
/// When a negative value is encountered, returns an error in pedantic mode
/// or 0 otherwise.
pub fn pop_count_checked(&mut self) -> Result<usize, HintErrorKind> {
let value = self.pop()?;
if value < 0 && self.is_pedantic {
Err(HintErrorKind::InvalidStackValue(value))
} else {
Ok(value.max(0) as usize)
}
}
/// Applies a unary operation.
///
/// Pops `a` from the stack and pushes `op(a)`.
pub fn apply_unary(
&mut self,
mut op: impl FnMut(i32) -> Result<i32, HintErrorKind>,
) -> Result<(), HintErrorKind> {
let a = self.pop()?;
self.push(op(a)?)
}
/// Applies a binary operation.
///
/// Pops `b` and `a` from the stack and pushes `op(a, b)`.
pub fn apply_binary(
&mut self,
mut op: impl FnMut(i32, i32) -> Result<i32, HintErrorKind>,
) -> Result<(), HintErrorKind> {
let b = self.pop()?;
let a = self.pop()?;
self.push(op(a, b)?)
}
/// Clear the entire stack.
///
/// Implements the CLEAR[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#clear-the-entire-stack>
pub fn clear(&mut self) {
self.len = 0;
}
/// Duplicate top stack element.
///
/// Implements the DUP[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#duplicate-top-stack-element>
pub fn dup(&mut self) -> Result<(), HintErrorKind> {
if let Some(value) = self.peek() {
self.push(value)
} else if self.is_pedantic {
Err(ValueStackUnderflow)
} else {
self.push(0)
}
}
/// Swap the top two elements on the stack.
///
/// Implements the SWAP[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#swap-the-top-two-elements-on-the-stack>
pub fn swap(&mut self) -> Result<(), HintErrorKind> {
let a = self.pop()?;
let b = self.pop()?;
self.push(a)?;
self.push(b)
}
/// Copy the indexed element to the top of the stack.
///
/// Implements the CINDEX[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#copy-the-indexed-element-to-the-top-of-the-stack>
pub fn copy_index(&mut self) -> Result<(), HintErrorKind> {
let top_ix = self.len.checked_sub(1).ok_or(ValueStackUnderflow)?;
let index = *self.values.get(top_ix).ok_or(ValueStackUnderflow)? as usize;
let element_ix = top_ix.checked_sub(index).ok_or(ValueStackUnderflow)?;
self.values[top_ix] = self.values[element_ix];
Ok(())
}
/// Moves the indexed element to the top of the stack.
///
/// Implements the MINDEX[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#move-the-indexed-element-to-the-top-of-the-stack>
pub fn move_index(&mut self) -> Result<(), HintErrorKind> {
let top_ix = self.len.checked_sub(1).ok_or(ValueStackUnderflow)?;
let index = *self.values.get(top_ix).ok_or(ValueStackUnderflow)? as usize;
let element_ix = top_ix.checked_sub(index).ok_or(ValueStackUnderflow)?;
let new_top_ix = top_ix.checked_sub(1).ok_or(ValueStackUnderflow)?;
let value = self.values[element_ix];
self.values
.copy_within(element_ix + 1..self.len, element_ix);
self.values[new_top_ix] = value;
self.len -= 1;
Ok(())
}
/// Roll the top three stack elements.
///
/// Implements the ROLL[] instruction.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructions#roll-the-top-three-stack-elements>
pub fn roll(&mut self) -> Result<(), HintErrorKind> {
let a = self.pop()?;
let b = self.pop()?;
let c = self.pop()?;
self.push(b)?;
self.push(a)?;
self.push(c)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{HintErrorKind, ValueStack};
use read_fonts::tables::glyf::bytecode::MockInlineOperands;
// The following are macros because functions can't return a new ValueStack
// with a borrowed parameter.
macro_rules! make_stack {
($values:expr) => {
ValueStack {
values: $values,
len: $values.len(),
is_pedantic: true,
}
};
}
macro_rules! make_empty_stack {
($values:expr) => {
ValueStack {
values: $values,
len: 0,
is_pedantic: true,
}
};
}
#[test]
fn push() {
let mut stack = make_empty_stack!(&mut [0; 4]);
for i in 0..4 {
stack.push(i).unwrap();
assert_eq!(stack.peek(), Some(i));
}
assert!(matches!(
stack.push(0),
Err(HintErrorKind::ValueStackOverflow)
));
}
#[test]
fn push_args() {
let mut stack = make_empty_stack!(&mut [0; 32]);
let values = [-5, 2, 2845, 92, -26, 42, i16::MIN, i16::MAX];
let mock_args = MockInlineOperands::from_words(&values);
stack.push_inline_operands(&mock_args.operands()).unwrap();
let mut popped = vec![];
while !stack.is_empty() {
popped.push(stack.pop().unwrap());
}
assert!(values
.iter()
.rev()
.map(|x| *x as i32)
.eq(popped.iter().copied()));
}
#[test]
fn pop() {
let mut stack = make_stack!(&mut [0, 1, 2, 3]);
for i in (0..4).rev() {
assert_eq!(stack.pop().ok(), Some(i));
}
assert!(matches!(
stack.pop(),
Err(HintErrorKind::ValueStackUnderflow)
));
}
#[test]
fn dup() {
let mut stack = make_stack!(&mut [1, 2, 3, 0]);
// pop extra element so we have room for dup
stack.pop().unwrap();
stack.dup().unwrap();
assert_eq!(stack.values(), &[1, 2, 3, 3]);
}
#[test]
fn swap() {
let mut stack = make_stack!(&mut [1, 2, 3]);
stack.swap().unwrap();
assert_eq!(stack.values(), &[1, 3, 2]);
}
#[test]
fn copy_index() {
let mut stack = make_stack!(&mut [4, 10, 2, 1, 3]);
stack.copy_index().unwrap();
assert_eq!(stack.values(), &[4, 10, 2, 1, 10]);
}
#[test]
fn move_index() {
let mut stack = make_stack!(&mut [4, 10, 2, 1, 3]);
stack.move_index().unwrap();
assert_eq!(stack.values(), &[4, 2, 1, 10]);
}
#[test]
fn roll() {
let mut stack = make_stack!(&mut [1, 2, 3]);
stack.roll().unwrap();
assert_eq!(stack.values(), &[2, 3, 1]);
}
#[test]
fn unnop() {
let mut stack = make_stack!(&mut [42]);
stack.apply_unary(|a| Ok(-a)).unwrap();
assert_eq!(stack.peek(), Some(-42));
stack.apply_unary(|a| Ok(!a)).unwrap();
assert_eq!(stack.peek(), Some(!-42));
}
#[test]
fn binop() {
let mut stack = make_empty_stack!(&mut [0; 32]);
for value in 1..=5 {
stack.push(value).unwrap();
}
stack.apply_binary(|a, b| Ok(a + b)).unwrap();
assert_eq!(stack.peek(), Some(9));
stack.apply_binary(|a, b| Ok(a * b)).unwrap();
assert_eq!(stack.peek(), Some(27));
stack.apply_binary(|a, b| Ok(a - b)).unwrap();
assert_eq!(stack.peek(), Some(-25));
stack.apply_binary(|a, b| Ok(a / b)).unwrap();
assert_eq!(stack.peek(), Some(0));
}
// Subtract with overflow when stack size is 1 and element index is 0
// https://oss-fuzz.com/testcase-detail/5765856825507840
#[test]
fn move_index_avoid_overflow() {
let mut stack = make_stack!(&mut [0]);
// Don't panic
let _ = stack.move_index();
}
#[test]
fn pop_count_checked() {
let mut stack = make_stack!(&mut [-1, 128, -1, 128]);
stack.is_pedantic = true;
assert_eq!(stack.pop_count_checked(), Ok(128));
// In pedantic mode, return an error for negative values
assert!(matches!(
stack.pop_count_checked(),
Err(HintErrorKind::InvalidStackValue(-1))
));
stack.is_pedantic = false;
assert_eq!(stack.pop_count_checked(), Ok(128));
// In non-pedantic mode, return 0 instead of error
assert_eq!(stack.pop_count_checked(), Ok(0));
}
}

View File

@@ -0,0 +1,835 @@
//! Glyph zones.
use read_fonts::{
tables::glyf::{PointFlags, PointMarker},
types::{F26Dot6, Point},
};
use super::{
error::HintErrorKind,
graphics::{CoordAxis, GraphicsState},
math,
};
use HintErrorKind::{InvalidPointIndex, InvalidPointRange};
/// Reference to either the twilight or glyph zone.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#zones>
#[derive(Copy, Clone, PartialEq, Default, Debug)]
#[repr(u8)]
pub enum ZonePointer {
Twilight = 0,
#[default]
Glyph = 1,
}
impl ZonePointer {
pub fn is_twilight(self) -> bool {
self == Self::Twilight
}
}
impl TryFrom<i32> for ZonePointer {
type Error = HintErrorKind;
fn try_from(value: i32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Twilight),
1 => Ok(Self::Glyph),
_ => Err(HintErrorKind::InvalidZoneIndex(value)),
}
}
}
/// Glyph zone for TrueType hinting.
///
/// See <https://learn.microsoft.com/en-us/typography/opentype/spec/tt_instructing_glyphs#zones>
#[derive(Default, Debug)]
pub struct Zone<'a> {
/// Outline points prior to applying scale.
pub unscaled: &'a [Point<i32>],
/// Copy of the outline points after applying scale.
pub original: &'a mut [Point<F26Dot6>],
/// Scaled outline points.
pub points: &'a mut [Point<F26Dot6>],
pub flags: &'a mut [PointFlags],
pub contours: &'a [u16],
}
impl<'a> Zone<'a> {
/// Creates a new hinting zone.
pub fn new(
unscaled: &'a [Point<i32>],
original: &'a mut [Point<F26Dot6>],
points: &'a mut [Point<F26Dot6>],
flags: &'a mut [PointFlags],
contours: &'a [u16],
) -> Self {
Self {
unscaled,
original,
points,
flags,
contours,
}
}
pub fn point(&self, index: usize) -> Result<Point<F26Dot6>, HintErrorKind> {
self.points
.get(index)
.copied()
.ok_or(InvalidPointIndex(index))
}
pub fn point_mut(&mut self, index: usize) -> Result<&mut Point<F26Dot6>, HintErrorKind> {
self.points.get_mut(index).ok_or(InvalidPointIndex(index))
}
pub fn original(&self, index: usize) -> Result<Point<F26Dot6>, HintErrorKind> {
self.original
.get(index)
.copied()
.ok_or(InvalidPointIndex(index))
}
pub fn original_mut(&mut self, index: usize) -> Result<&mut Point<F26Dot6>, HintErrorKind> {
self.original.get_mut(index).ok_or(InvalidPointIndex(index))
}
pub fn unscaled(&self, index: usize) -> Point<i32> {
// Unscaled points in the twilight zone are always (0, 0). This allows
// us to avoid the allocation for that zone and back it with an empty
// slice.
self.unscaled.get(index).copied().unwrap_or_default()
}
pub fn contour(&self, index: usize) -> Result<u16, HintErrorKind> {
self.contours
.get(index)
.copied()
.ok_or(HintErrorKind::InvalidContourIndex(index))
}
pub fn touch(&mut self, index: usize, axis: CoordAxis) -> Result<(), HintErrorKind> {
let flag = self.flags.get_mut(index).ok_or(InvalidPointIndex(index))?;
flag.set_marker(axis.touched_marker());
Ok(())
}
pub fn untouch(&mut self, index: usize, axis: CoordAxis) -> Result<(), HintErrorKind> {
let flag = self.flags.get_mut(index).ok_or(InvalidPointIndex(index))?;
flag.clear_marker(axis.touched_marker());
Ok(())
}
pub fn is_touched(&self, index: usize, axis: CoordAxis) -> Result<bool, HintErrorKind> {
let flag = self.flags.get(index).ok_or(InvalidPointIndex(index))?;
Ok(flag.has_marker(axis.touched_marker()))
}
pub fn flip_on_curve(&mut self, index: usize) -> Result<(), HintErrorKind> {
let flag = self.flags.get_mut(index).ok_or(InvalidPointIndex(index))?;
flag.flip_on_curve();
Ok(())
}
pub fn set_on_curve(
&mut self,
start: usize,
end: usize,
on: bool,
) -> Result<(), HintErrorKind> {
let flags = self
.flags
.get_mut(start..end)
.ok_or(InvalidPointRange(start, end))?;
if on {
for flag in flags {
flag.set_on_curve();
}
} else {
for flag in flags {
flag.clear_on_curve();
}
}
Ok(())
}
/// Interpolate untouched points.
///
/// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6391>
pub fn iup(&mut self, axis: CoordAxis) -> Result<(), HintErrorKind> {
let mut point = 0;
for i in 0..self.contours.len() {
let mut end_point = self.contour(i)? as usize;
let first_point = point;
if end_point >= self.points.len() {
end_point = self.points.len() - 1;
}
while point <= end_point && !self.is_touched(point, axis)? {
point += 1;
}
if point <= end_point {
let first_touched = point;
let mut cur_touched = point;
point += 1;
while point <= end_point {
if self.is_touched(point, axis)? {
self.iup_interpolate(axis, cur_touched + 1, point - 1, cur_touched, point)?;
cur_touched = point;
}
point += 1;
}
if cur_touched == first_touched {
self.iup_shift(axis, first_point, end_point, cur_touched)?;
} else {
self.iup_interpolate(
axis,
cur_touched + 1,
end_point,
cur_touched,
first_touched,
)?;
if first_touched > 0 {
self.iup_interpolate(
axis,
first_point,
first_touched - 1,
cur_touched,
first_touched,
)?;
}
}
}
}
Ok(())
}
/// Shift the range of points p1..=p2 based on the delta given by the
/// reference point p.
///
/// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6262>
fn iup_shift(
&mut self,
axis: CoordAxis,
p1: usize,
p2: usize,
p: usize,
) -> Result<(), HintErrorKind> {
if p1 > p2 || p1 > p || p > p2 {
return Ok(());
}
macro_rules! shift_coord {
($coord:ident) => {
let delta = self.point(p)?.$coord - self.original(p)?.$coord;
if delta != F26Dot6::ZERO {
let (first, second) = self
.points
.get_mut(p1..=p2)
.ok_or(InvalidPointRange(p1, p2 + 1))?
.split_at_mut(p - p1);
for point in first
.iter_mut()
.chain(second.get_mut(1..).ok_or(InvalidPointIndex(p - p1))?)
{
point.$coord += delta;
}
}
};
}
if axis == CoordAxis::X {
shift_coord!(x);
} else {
shift_coord!(y);
}
Ok(())
}
/// Interpolate the range of points p1..=p2 based on the deltas
/// given by the two reference points.
///
/// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L6284>
fn iup_interpolate(
&mut self,
axis: CoordAxis,
p1: usize,
p2: usize,
mut ref1: usize,
mut ref2: usize,
) -> Result<(), HintErrorKind> {
if p1 > p2 {
return Ok(());
}
let max_points = self.points.len();
if ref1 >= max_points || ref2 >= max_points {
return Ok(());
}
macro_rules! interpolate_coord {
($coord:ident) => {
let mut orus1 = self.unscaled(ref1).$coord;
let mut orus2 = self.unscaled(ref2).$coord;
if orus1 > orus2 {
use core::mem::swap;
swap(&mut orus1, &mut orus2);
swap(&mut ref1, &mut ref2);
}
let org1 = self.original(ref1)?.$coord;
let org2 = self.original(ref2)?.$coord;
let cur1 = self.point(ref1)?.$coord;
let cur2 = self.point(ref2)?.$coord;
let delta1 = cur1 - org1;
let delta2 = cur2 - org2;
let iter = self
.original
.get(p1..=p2)
.ok_or(InvalidPointRange(p1, p2 + 1))?
.iter()
.zip(
self.unscaled
.get(p1..=p2)
.ok_or(InvalidPointRange(p1, p2 + 1))?,
)
.zip(
self.points
.get_mut(p1..=p2)
.ok_or(InvalidPointRange(p1, p2 + 1))?,
);
if cur1 == cur2 || orus1 == orus2 {
for ((orig, _unscaled), point) in iter {
let a = orig.$coord;
point.$coord = if a <= org1 {
a + delta1
} else if a >= org2 {
a + delta2
} else {
cur1
};
}
} else {
let scale = math::div((cur2 - cur1).to_bits(), orus2 - orus1);
for ((orig, unscaled), point) in iter {
let a = orig.$coord;
point.$coord = if a <= org1 {
a + delta1
} else if a >= org2 {
a + delta2
} else {
cur1 + F26Dot6::from_bits(math::mul(unscaled.$coord - orus1, scale))
};
}
}
};
}
if axis == CoordAxis::X {
interpolate_coord!(x);
} else {
interpolate_coord!(y);
}
Ok(())
}
}
impl<'a> GraphicsState<'a> {
/// Takes an array of (zone pointer, point index) pairs and returns true if
/// all accesses would be valid.
pub fn in_bounds<const N: usize>(&self, pairs: [(ZonePointer, usize); N]) -> bool {
for (zp, index) in pairs {
if index > self.zone(zp).points.len() {
return false;
}
}
true
}
#[inline(always)]
pub fn zone(&self, pointer: ZonePointer) -> &Zone<'a> {
&self.zones[pointer as usize]
}
#[inline(always)]
pub fn zone_mut(&mut self, pointer: ZonePointer) -> &mut Zone<'a> {
&mut self.zones[pointer as usize]
}
#[inline(always)]
pub fn zp0(&self) -> &Zone<'a> {
self.zone(self.zp0)
}
#[inline(always)]
pub fn zp0_mut(&mut self) -> &mut Zone<'a> {
self.zone_mut(self.zp0)
}
#[inline(always)]
pub fn zp1(&self) -> &Zone {
self.zone(self.zp1)
}
#[inline(always)]
pub fn zp1_mut(&mut self) -> &mut Zone<'a> {
self.zone_mut(self.zp1)
}
#[inline(always)]
pub fn zp2(&self) -> &Zone {
self.zone(self.zp2)
}
#[inline(always)]
pub fn zp2_mut(&mut self) -> &mut Zone<'a> {
self.zone_mut(self.zp2)
}
}
impl GraphicsState<'_> {
/// Moves the requested original point by the given distance.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1743>
pub(crate) fn move_original(
&mut self,
zone: ZonePointer,
point_ix: usize,
distance: F26Dot6,
) -> Result<(), HintErrorKind> {
let fv = self.freedom_vector;
let fdotp = self.fdotp;
let axis = self.freedom_axis;
let point = self.zone_mut(zone).original_mut(point_ix)?;
match axis {
CoordAxis::X => point.x += distance,
CoordAxis::Y => point.y += distance,
CoordAxis::Both => {
let distance = distance.to_bits();
if fv.x != 0 {
point.x += F26Dot6::from_bits(math::mul_div(distance, fv.x, fdotp));
}
if fv.y != 0 {
point.y += F26Dot6::from_bits(math::mul_div(distance, fv.y, fdotp));
}
}
}
Ok(())
}
/// Moves the requested scaled point by the given distance.
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1771>
pub(crate) fn move_point(
&mut self,
zone: ZonePointer,
point_ix: usize,
distance: F26Dot6,
) -> Result<(), HintErrorKind> {
// Note: we never adjust x in backward compatibility mode and we never
// adjust y in backward compatibility mode after IUP has been done in
// both directions.
//
// The primary motivation is to avoid horizontal adjustments in cases
// where subpixel rendering provides better fidelity.
//
// For more detail, see <https://learn.microsoft.com/en-us/typography/cleartype/truetypecleartype>
let back_compat = self.backward_compatibility;
let back_compat_and_did_iup = back_compat && self.did_iup_x && self.did_iup_y;
let zone = &mut self.zones[zone as usize];
let point = zone.point_mut(point_ix)?;
match self.freedom_axis {
CoordAxis::X => {
if !back_compat {
point.x += distance;
}
zone.touch(point_ix, CoordAxis::X)?;
}
CoordAxis::Y => {
if !back_compat_and_did_iup {
point.y += distance;
}
zone.touch(point_ix, CoordAxis::Y)?;
}
CoordAxis::Both => {
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L1669>
let fv = self.freedom_vector;
let distance = distance.to_bits();
if fv.x != 0 {
if !back_compat {
point.x += F26Dot6::from_bits(math::mul_div(distance, fv.x, self.fdotp));
}
zone.touch(point_ix, CoordAxis::X)?;
}
if fv.y != 0 {
if !back_compat_and_did_iup {
zone.point_mut(point_ix)?.y +=
F26Dot6::from_bits(math::mul_div(distance, fv.y, self.fdotp));
}
zone.touch(point_ix, CoordAxis::Y)?;
}
}
}
Ok(())
}
/// Moves the requested scaled point in the zone referenced by zp2 by the
/// given delta.
///
/// This is a helper function for SHP, SHC, SHZ, and SHPIX instructions.
///
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5170>
pub(crate) fn move_zp2_point(
&mut self,
point_ix: usize,
dx: F26Dot6,
dy: F26Dot6,
do_touch: bool,
) -> Result<(), HintErrorKind> {
// See notes above in move_point() about how this is used.
let back_compat = self.backward_compatibility;
let back_compat_and_did_iup = back_compat && self.did_iup_x && self.did_iup_y;
let fv = self.freedom_vector;
let zone = self.zp2_mut();
if fv.x != 0 {
if !back_compat {
zone.point_mut(point_ix)?.x += dx;
}
if do_touch {
zone.touch(point_ix, CoordAxis::X)?;
}
}
if fv.y != 0 {
if !back_compat_and_did_iup {
zone.point_mut(point_ix)?.y += dy;
}
if do_touch {
zone.touch(point_ix, CoordAxis::Y)?;
}
}
Ok(())
}
/// Computes the adjustment made to a point along the current freedom vector.
/// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttinterp.c#L5126>
pub(crate) fn point_displacement(
&mut self,
opcode: u8,
) -> Result<PointDisplacement, HintErrorKind> {
let (zone, point_ix) = if (opcode & 1) != 0 {
(self.zp0, self.rp1)
} else {
(self.zp1, self.rp2)
};
let zone_data = self.zone(zone);
let point = zone_data.point(point_ix)?;
let original_point = zone_data.original(point_ix)?;
let distance = self.project(point, original_point);
let fv = self.freedom_vector;
let dx = F26Dot6::from_bits(math::mul_div(distance.to_bits(), fv.x, self.fdotp));
let dy = F26Dot6::from_bits(math::mul_div(distance.to_bits(), fv.y, self.fdotp));
Ok(PointDisplacement {
zone,
point_ix,
dx,
dy,
})
}
}
#[derive(PartialEq, Debug)]
pub(crate) struct PointDisplacement {
pub zone: ZonePointer,
pub point_ix: usize,
pub dx: F26Dot6,
pub dy: F26Dot6,
}
impl CoordAxis {
fn touched_marker(self) -> PointMarker {
match self {
CoordAxis::Both => PointMarker::TOUCHED,
CoordAxis::X => PointMarker::TOUCHED_X,
CoordAxis::Y => PointMarker::TOUCHED_Y,
}
}
}
#[cfg(test)]
mod tests {
use super::{math, CoordAxis, GraphicsState, PointDisplacement, Zone, ZonePointer};
use raw::{
tables::glyf::{PointFlags, PointMarker},
types::{F26Dot6, Point},
};
#[test]
fn flip_on_curve_point() {
let on_curve = PointFlags::on_curve();
let off_curve = PointFlags::off_curve_quad();
let mut zone = Zone {
unscaled: &mut [],
original: &mut [],
points: &mut [],
contours: &[],
flags: &mut [on_curve, off_curve, off_curve, on_curve],
};
for i in 0..4 {
zone.flip_on_curve(i).unwrap();
}
assert_eq!(zone.flags, &[off_curve, on_curve, on_curve, off_curve]);
}
#[test]
fn set_on_curve_regions() {
let on_curve = PointFlags::on_curve();
let off_curve = PointFlags::off_curve_quad();
let mut zone = Zone {
unscaled: &mut [],
original: &mut [],
points: &mut [],
contours: &[],
flags: &mut [on_curve, off_curve, off_curve, on_curve],
};
zone.set_on_curve(0, 2, true).unwrap();
zone.set_on_curve(2, 4, false).unwrap();
assert_eq!(zone.flags, &[on_curve, on_curve, off_curve, off_curve]);
}
#[test]
fn iup_shift() {
let [untouched, touched] = point_markers();
// A single touched point shifts the whole contour
let mut original = f26dot6_points([(0, 0), (10, 10), (20, 20)]);
let mut points = f26dot6_points([(-5, -20), (10, 10), (20, 20)]);
let mut zone = Zone {
unscaled: &mut [],
original: &mut original,
points: &mut points,
contours: &[3],
flags: &mut [touched, untouched, untouched],
};
zone.iup(CoordAxis::X).unwrap();
assert_eq!(zone.points, &f26dot6_points([(-5, -20), (5, 10), (15, 20)]),);
zone.iup(CoordAxis::Y).unwrap();
assert_eq!(zone.points, &f26dot6_points([(-5, -20), (5, -10), (15, 0)]),);
}
#[test]
fn iup_interpolate() {
let [untouched, touched] = point_markers();
// Two touched points interpolates the intermediate point(s)
let mut original = f26dot6_points([(0, 0), (10, 10), (20, 20)]);
let mut points = f26dot6_points([(-5, -20), (10, 10), (27, 56)]);
let mut zone = Zone {
unscaled: &mut [
Point::new(0, 0),
Point::new(500, 500),
Point::new(1000, 1000),
],
original: &mut original,
points: &mut points,
contours: &[3],
flags: &mut [touched, untouched, touched],
};
zone.iup(CoordAxis::X).unwrap();
assert_eq!(
zone.points,
&f26dot6_points([(-5, -20), (11, 10), (27, 56)]),
);
zone.iup(CoordAxis::Y).unwrap();
assert_eq!(
zone.points,
&f26dot6_points([(-5, -20), (11, 18), (27, 56)]),
);
}
#[test]
fn move_point_x() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(100, 0);
let point_ix = 0;
let orig_x = gs.zones[1].point(point_ix).unwrap().x;
let dx = F26Dot6::from_bits(10);
// backward compatibility is on by default and we don't move x coord
gs.move_point(ZonePointer::Glyph, 0, dx).unwrap();
assert_eq!(orig_x, gs.zones[1].point(point_ix).unwrap().x);
// disable so we actually move
gs.backward_compatibility = false;
gs.move_point(ZonePointer::Glyph, 0, dx).unwrap();
let new_x = gs.zones[1].point(point_ix).unwrap().x;
assert_ne!(orig_x, new_x);
assert_eq!(new_x, orig_x + dx)
}
#[test]
fn move_point_y() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(0, 100);
let point_ix = 0;
let orig_y = gs.zones[1].point(point_ix).unwrap().y;
let dy = F26Dot6::from_bits(10);
// movement in y is prevented post-iup when backward
// compatibility is enabled
gs.did_iup_x = true;
gs.did_iup_y = true;
gs.move_point(ZonePointer::Glyph, 0, dy).unwrap();
assert_eq!(orig_y, gs.zones[1].point(point_ix).unwrap().y);
// allow movement
gs.did_iup_x = false;
gs.did_iup_y = false;
gs.move_point(ZonePointer::Glyph, 0, dy).unwrap();
let new_y = gs.zones[1].point(point_ix).unwrap().y;
assert_ne!(orig_y, new_y);
assert_eq!(new_y, orig_y + dy)
}
#[test]
fn move_point_x_and_y() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(100, 50);
let point_ix = 0;
let orig_point = gs.zones[1].point(point_ix).unwrap();
let dist = F26Dot6::from_bits(10);
// prevent movement in x and y
gs.did_iup_x = true;
gs.did_iup_y = true;
gs.move_point(ZonePointer::Glyph, 0, dist).unwrap();
assert_eq!(orig_point, gs.zones[1].point(point_ix).unwrap());
// allow movement
gs.backward_compatibility = false;
gs.did_iup_x = false;
gs.did_iup_y = false;
gs.move_point(ZonePointer::Glyph, 0, dist).unwrap();
let point = gs.zones[1].point(point_ix).unwrap();
assert_eq!(point.map(F26Dot6::to_bits), Point::new(4, -16));
}
#[test]
fn move_original_x() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(100, 0);
let point_ix = 0;
let orig_x = gs.zones[1].original(point_ix).unwrap().x;
let dx = F26Dot6::from_bits(10);
gs.move_original(ZonePointer::Glyph, 0, dx).unwrap();
let new_x = gs.zones[1].original(point_ix).unwrap().x;
assert_eq!(new_x, orig_x + dx)
}
#[test]
fn move_original_y() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(0, 100);
let point_ix = 0;
let orig_y = gs.zones[1].original(point_ix).unwrap().y;
let dy = F26Dot6::from_bits(10);
gs.move_original(ZonePointer::Glyph, 0, dy).unwrap();
let new_y = gs.zones[1].original(point_ix).unwrap().y;
assert_eq!(new_y, orig_y + dy)
}
#[test]
fn move_original_x_and_y() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(100, 50);
let point_ix = 0;
let dist = F26Dot6::from_bits(10);
gs.move_original(ZonePointer::Glyph, 0, dist).unwrap();
let point = gs.zones[1].original(point_ix).unwrap();
assert_eq!(point.map(F26Dot6::to_bits), Point::new(9, 4));
}
#[test]
fn move_zp2_point() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(100, 50);
gs.zp2 = ZonePointer::Glyph;
let point_ix = 0;
let orig_point = gs.zones[1].point(point_ix).unwrap();
let dx = F26Dot6::from_bits(10);
let dy = F26Dot6::from_bits(-10);
// prevent movement in x and y
gs.did_iup_x = true;
gs.did_iup_y = true;
gs.move_zp2_point(point_ix, dx, dy, false).unwrap();
assert_eq!(orig_point, gs.zones[1].point(point_ix).unwrap());
// allow movement
gs.backward_compatibility = false;
gs.did_iup_x = false;
gs.did_iup_y = false;
gs.move_zp2_point(point_ix, dx, dy, false).unwrap();
let point = gs.zones[1].point(point_ix).unwrap();
assert_eq!(point, orig_point + Point::new(dx, dy));
}
#[test]
fn point_displacement() {
let mut mock = MockGraphicsState::new();
let mut gs = mock.graphics_state(100, 50);
gs.zp0 = ZonePointer::Glyph;
gs.rp1 = 0;
assert_eq!(
gs.point_displacement(1).unwrap(),
PointDisplacement {
zone: ZonePointer::Glyph,
point_ix: 0,
dx: F26Dot6::from_f64(-0.1875),
dy: F26Dot6::from_f64(-0.09375),
}
);
gs.rp2 = 2;
assert_eq!(
gs.point_displacement(0).unwrap(),
PointDisplacement {
zone: ZonePointer::Glyph,
point_ix: 2,
dx: F26Dot6::from_f64(0.390625),
dy: F26Dot6::from_f64(0.203125),
}
);
}
struct MockGraphicsState {
points: [Point<F26Dot6>; 3],
original: [Point<F26Dot6>; 3],
contours: [u16; 1],
flags: [PointFlags; 3],
}
impl MockGraphicsState {
fn new() -> Self {
Self {
points: f26dot6_points([(-5, -20), (10, 10), (20, 20)]),
original: f26dot6_points([(0, 0), (10, 10), (20, -42)]),
flags: [PointFlags::default(); 3],
contours: [3],
}
}
fn graphics_state(&mut self, fv_x: i32, fv_y: i32) -> GraphicsState {
let glyph = Zone {
unscaled: &mut [],
original: &mut self.original,
points: &mut self.points,
contours: &self.contours,
flags: &mut self.flags,
};
let v = math::normalize14(fv_x, fv_y);
let mut gs = GraphicsState {
zones: [Zone::default(), glyph],
freedom_vector: v,
proj_vector: v,
zp0: ZonePointer::Glyph,
..Default::default()
};
gs.update_projection_state();
gs
}
}
fn point_markers() -> [PointFlags; 2] {
let untouched = PointFlags::default();
let mut touched = untouched;
touched.set_marker(PointMarker::TOUCHED);
[untouched, touched]
}
fn f26dot6_points<const N: usize>(points: [(i32, i32); N]) -> [Point<F26Dot6>; N] {
points.map(|point| Point::new(F26Dot6::from_bits(point.0), F26Dot6::from_bits(point.1)))
}
}

271
vendor/skrifa/src/outline/glyf/memory.rs vendored Normal file
View File

@@ -0,0 +1,271 @@
//! Memory allocation for TrueType scaling.
use std::mem::{align_of, size_of};
use read_fonts::{
tables::glyf::PointFlags,
types::{F26Dot6, Fixed, Point},
};
use super::{super::Hinting, Outline};
/// Buffers used during HarfBuzz-style glyph scaling.
pub(crate) struct HarfBuzzOutlineMemory<'a> {
pub points: &'a mut [Point<f32>],
pub contours: &'a mut [u16],
pub flags: &'a mut [PointFlags],
pub deltas: &'a mut [Point<f32>],
pub iup_buffer: &'a mut [Point<f32>],
pub composite_deltas: &'a mut [Point<f32>],
}
impl<'a> HarfBuzzOutlineMemory<'a> {
pub(super) fn new(outline: &Outline, buf: &'a mut [u8]) -> Option<Self> {
let (points, buf) = alloc_slice(buf, outline.points)?;
let (contours, buf) = alloc_slice(buf, outline.contours)?;
let (flags, buf) = alloc_slice(buf, outline.points)?;
// Don't allocate any delta buffers if we don't have variations
let (deltas, iup_buffer, composite_deltas, _buf) = if outline.has_variations {
let (deltas, buf) = alloc_slice(buf, outline.max_simple_points)?;
let (iup_buffer, buf) = alloc_slice(buf, outline.max_simple_points)?;
let (composite_deltas, buf) = alloc_slice(buf, outline.max_component_delta_stack)?;
(deltas, iup_buffer, composite_deltas, buf)
} else {
(
Default::default(),
Default::default(),
Default::default(),
buf,
)
};
Some(Self {
points,
contours,
flags,
deltas,
iup_buffer,
composite_deltas,
})
}
}
/// Buffers used during glyph scaling.
pub(crate) struct FreeTypeOutlineMemory<'a> {
pub unscaled: &'a mut [Point<i32>],
pub scaled: &'a mut [Point<F26Dot6>],
pub original_scaled: &'a mut [Point<F26Dot6>],
pub contours: &'a mut [u16],
pub flags: &'a mut [PointFlags],
pub deltas: &'a mut [Point<Fixed>],
pub iup_buffer: &'a mut [Point<Fixed>],
pub composite_deltas: &'a mut [Point<Fixed>],
pub stack: &'a mut [i32],
pub cvt: &'a mut [i32],
pub storage: &'a mut [i32],
pub twilight_scaled: &'a mut [Point<F26Dot6>],
pub twilight_original_scaled: &'a mut [Point<F26Dot6>],
pub twilight_flags: &'a mut [PointFlags],
}
impl<'a> FreeTypeOutlineMemory<'a> {
pub(super) fn new(outline: &Outline, buf: &'a mut [u8], hinting: Hinting) -> Option<Self> {
let hinted = outline.has_hinting && hinting == Hinting::Embedded;
let (scaled, buf) = alloc_slice(buf, outline.points)?;
let (unscaled, buf) = alloc_slice(buf, outline.max_other_points)?;
// We only need original scaled points when hinting
let (original_scaled, buf) = if hinted {
alloc_slice(buf, outline.max_other_points)?
} else {
(Default::default(), buf)
};
// Don't allocate any delta buffers if we don't have variations
let (deltas, iup_buffer, composite_deltas, buf) = if outline.has_variations {
let (deltas, buf) = alloc_slice(buf, outline.max_simple_points)?;
let (iup_buffer, buf) = alloc_slice(buf, outline.max_simple_points)?;
let (composite_deltas, buf) = alloc_slice(buf, outline.max_component_delta_stack)?;
(deltas, iup_buffer, composite_deltas, buf)
} else {
(
Default::default(),
Default::default(),
Default::default(),
buf,
)
};
// Hinting value stack
let (stack, buf) = if hinted {
alloc_slice(buf, outline.max_stack)?
} else {
(Default::default(), buf)
};
// Copy-on-write buffers for CVT and storage area
let (cvt, storage, buf) = if hinted {
let (cvt, buf) = alloc_slice(buf, outline.cvt_count)?;
let (storage, buf) = alloc_slice(buf, outline.storage_count)?;
(cvt, storage, buf)
} else {
(Default::default(), Default::default(), buf)
};
// Twilight zone point buffers
let (twilight_scaled, twilight_original_scaled, buf) = if hinted {
let (scaled, buf) = alloc_slice(buf, outline.max_twilight_points)?;
let (original_scaled, buf) = alloc_slice(buf, outline.max_twilight_points)?;
(scaled, original_scaled, buf)
} else {
(Default::default(), Default::default(), buf)
};
let (contours, buf) = alloc_slice(buf, outline.contours)?;
let (flags, buf) = alloc_slice(buf, outline.points)?;
// Twilight zone point flags
let twilight_flags = if hinted {
alloc_slice(buf, outline.max_twilight_points)?.0
} else {
Default::default()
};
Some(Self {
unscaled,
scaled,
original_scaled,
contours,
flags,
deltas,
iup_buffer,
composite_deltas,
stack,
cvt,
storage,
twilight_scaled,
twilight_original_scaled,
twilight_flags,
})
}
}
/// Allocates a mutable slice of `T` of the given length from the specified
/// buffer.
///
/// Returns the allocated slice and the remainder of the buffer.
fn alloc_slice<T>(buf: &mut [u8], len: usize) -> Option<(&mut [T], &mut [u8])>
where
T: bytemuck::AnyBitPattern + bytemuck::NoUninit,
{
if len == 0 {
return Some((Default::default(), buf));
}
// 1) Ensure we slice the buffer at a position that is properly aligned
// for T.
let base_ptr = buf.as_ptr() as usize;
let aligned_ptr = align_up(base_ptr, align_of::<T>());
let aligned_offset = aligned_ptr - base_ptr;
let buf = buf.get_mut(aligned_offset..)?;
// 2) Ensure we have enough space in the buffer to allocate our slice.
let len_in_bytes = len * size_of::<T>();
if len_in_bytes > buf.len() {
return None;
}
let (slice_buf, rest) = buf.split_at_mut(len_in_bytes);
// Bytemuck handles all safety guarantees here.
let slice = bytemuck::try_cast_slice_mut(slice_buf).ok()?;
Some((slice, rest))
}
fn align_up(len: usize, alignment: usize) -> usize {
len + (len.wrapping_neg() & (alignment - 1))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn unaligned_buffer() {
let mut buf = [0u8; 40];
let alignment = align_of::<i32>();
let addr = buf.as_ptr() as usize;
let mut unaligned_addr = addr;
// Force an unaligned offset
if unaligned_addr % alignment == 0 {
unaligned_addr += 1;
}
let unaligned_offset = unaligned_addr - addr;
let unaligned = &mut buf[unaligned_offset..];
assert!(unaligned.as_ptr() as usize % alignment != 0);
let (slice, _) = alloc_slice::<i32>(unaligned, 8).unwrap();
assert_eq!(slice.as_ptr() as usize % alignment, 0);
}
#[test]
fn fail_unaligned_buffer() {
let mut buf = [0u8; 40];
let alignment = align_of::<i32>();
let addr = buf.as_ptr() as usize;
let mut unaligned_addr = addr;
// Force an unaligned offset
if unaligned_addr % alignment == 0 {
unaligned_addr += 1;
}
let unaligned_offset = unaligned_addr - addr;
let unaligned = &mut buf[unaligned_offset..];
assert_eq!(alloc_slice::<i32>(unaligned, 16), None);
}
#[test]
fn outline_memory() {
let outline_info = Outline {
glyph: None,
glyph_id: Default::default(),
points: 10,
contours: 4,
max_simple_points: 4,
max_other_points: 4,
max_component_delta_stack: 4,
max_stack: 0,
cvt_count: 0,
storage_count: 0,
max_twilight_points: 0,
has_hinting: false,
has_variations: true,
has_overlaps: false,
};
let required_size = outline_info.required_buffer_size(Hinting::None);
let mut buf = vec![0u8; required_size];
let memory = FreeTypeOutlineMemory::new(&outline_info, &mut buf, Hinting::None).unwrap();
assert_eq!(memory.scaled.len(), outline_info.points);
assert_eq!(memory.unscaled.len(), outline_info.max_other_points);
// We don't allocate this buffer when hinting is disabled
assert_eq!(memory.original_scaled.len(), 0);
assert_eq!(memory.flags.len(), outline_info.points);
assert_eq!(memory.contours.len(), outline_info.contours);
assert_eq!(memory.deltas.len(), outline_info.max_simple_points);
assert_eq!(memory.iup_buffer.len(), outline_info.max_simple_points);
assert_eq!(
memory.composite_deltas.len(),
outline_info.max_component_delta_stack
);
}
#[test]
fn fail_outline_memory() {
let outline_info = Outline {
glyph: None,
glyph_id: Default::default(),
points: 10,
contours: 4,
max_simple_points: 4,
max_other_points: 4,
max_component_delta_stack: 4,
max_stack: 0,
cvt_count: 0,
storage_count: 0,
max_twilight_points: 0,
has_hinting: false,
has_variations: true,
has_overlaps: false,
};
// Required size adds 4 bytes slop to account for internal alignment
// requirements. So subtract 5 to force a failure.
let not_enough = outline_info.required_buffer_size(Hinting::None) - 5;
let mut buf = vec![0u8; not_enough];
assert!(FreeTypeOutlineMemory::new(&outline_info, &mut buf, Hinting::None).is_none());
}
}

1401
vendor/skrifa/src/outline/glyf/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,179 @@
//! TrueType outline types.
use std::mem::size_of;
use super::super::{
path::{to_path, ToPathError},
pen::PathStyle,
DrawError, Hinting, OutlinePen,
};
use raw::tables::glyf::PointCoord;
use read_fonts::{
tables::glyf::{Glyph, PointFlags},
types::{F26Dot6, Fixed, GlyphId, Point},
};
/// Maximum number of points we support in a single outline including
/// composites.
///
/// TrueType uses a 16 bit integer to store contour end points so
/// we must keep the total count within this value.
///
/// The maxp <https://learn.microsoft.com/en-us/typography/opentype/spec/maxp>
/// table encodes `maxCompositePoints` as a `uint16` so the spec enforces
/// this limit.
const MAX_POINTS: usize = u16::MAX as usize;
/// Represents the information necessary to scale a glyph outline.
///
/// Contains a reference to the glyph data itself as well as metrics that
/// can be used to compute the memory requirements for scaling the glyph.
#[derive(Clone, Default)]
pub struct Outline<'a> {
pub glyph_id: GlyphId,
/// The associated top-level glyph for the outline.
pub glyph: Option<Glyph<'a>>,
/// Sum of the point counts of all simple glyphs in an outline.
pub points: usize,
/// Sum of the contour counts of all simple glyphs in an outline.
pub contours: usize,
/// Maximum number of points in a single simple glyph.
pub max_simple_points: usize,
/// "Other" points are the unscaled or original scaled points.
///
/// The size of these buffer is the same and this value tracks the size
/// for one (not both) of the buffers. This is the maximum of
/// `max_simple_points` and the total number of points for all component
/// glyphs in a single composite glyph.
pub max_other_points: usize,
/// Maximum size of the component delta stack.
///
/// For composite glyphs in variable fonts, delta values are computed
/// for each component. This tracks the maximum stack depth necessary
/// to store those values during processing.
pub max_component_delta_stack: usize,
/// Number of entries in the hinting value stack.
pub max_stack: usize,
/// Number of CVT entries for copy-on-write support.
pub cvt_count: usize,
/// Number of storage area entries for copy-on-write support.
pub storage_count: usize,
/// Maximum number of points in the twilight zone for hinting.
pub max_twilight_points: usize,
/// True if any component of a glyph has bytecode instructions.
pub has_hinting: bool,
/// True if the glyph requires variation delta processing.
pub has_variations: bool,
/// True if the glyph contains any simple or compound overlap flags.
pub has_overlaps: bool,
}
impl Outline<'_> {
/// Returns the minimum size in bytes required to scale an outline based
/// on the computed sizes.
pub fn required_buffer_size(&self, hinting: Hinting) -> usize {
let mut size = 0;
let hinting = self.has_hinting && hinting == Hinting::Embedded;
// Scaled, unscaled and (for hinting) original scaled points
size += self.points * size_of::<Point<F26Dot6>>();
// Unscaled and (if hinted) original scaled points
size += self.max_other_points * size_of::<Point<i32>>() * if hinting { 2 } else { 1 };
// Contour end points
size += self.contours * size_of::<u16>();
// Point flags
size += self.points * size_of::<PointFlags>();
if self.has_variations {
// Interpolation buffer for delta IUP
size += self.max_simple_points * size_of::<Point<Fixed>>();
// Delta buffer for points
size += self.max_simple_points * size_of::<Point<Fixed>>();
// Delta buffer for composite components
size += self.max_component_delta_stack * size_of::<Point<Fixed>>();
}
if hinting {
// Hinting value stack
size += self.max_stack * size_of::<i32>();
// CVT and storage area copy-on-write buffers
size += (self.cvt_count + self.storage_count) * size_of::<i32>();
// Twilight zone storage. Two point buffers plus one point flags buffer
size += self.max_twilight_points
* (size_of::<Point<F26Dot6>>() * 2 + size_of::<PointFlags>());
}
if size != 0 {
// If we're given a buffer that is not aligned, we'll need to
// adjust, so add our maximum alignment requirement in bytes.
size += std::mem::align_of::<i32>();
}
size
}
pub(super) fn ensure_point_count_limit(&self) -> Result<(), DrawError> {
if self.points > MAX_POINTS {
Err(DrawError::TooManyPoints(self.glyph_id))
} else {
Ok(())
}
}
}
#[derive(Debug)]
pub struct ScaledOutline<'a, C>
where
C: PointCoord,
{
pub points: &'a mut [Point<C>],
pub flags: &'a mut [PointFlags],
pub contours: &'a mut [u16],
pub phantom_points: [Point<C>; 4],
pub hdmx_width: Option<u8>,
}
impl<'a, C> ScaledOutline<'a, C>
where
C: PointCoord,
{
pub(crate) fn new(
points: &'a mut [Point<C>],
phantom_points: [Point<C>; 4],
flags: &'a mut [PointFlags],
contours: &'a mut [u16],
hdmx_width: Option<u8>,
) -> Self {
let x_shift = phantom_points[0].x;
if x_shift != C::zeroed() {
for point in points.iter_mut() {
point.x = point.x - x_shift;
}
}
Self {
points,
flags,
contours,
phantom_points,
hdmx_width,
}
}
pub fn adjusted_lsb(&self) -> C {
self.phantom_points[0].x
}
pub fn adjusted_advance_width(&self) -> C {
// Prefer widths from hdmx, otherwise take difference between first
// two phantom points
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L1996>
if let Some(hdmx_width) = self.hdmx_width {
C::from_i32(hdmx_width as i32)
} else {
self.phantom_points[1].x - self.phantom_points[0].x
}
}
pub fn to_path(
&self,
path_style: PathStyle,
pen: &mut impl OutlinePen,
) -> Result<(), ToPathError> {
to_path(self.points, self.flags, self.contours, path_style, pen)
}
}

600
vendor/skrifa/src/outline/hint.rs vendored Normal file
View File

@@ -0,0 +1,600 @@
//! Support for applying embedded hinting instructions.
use super::{
autohint, cff,
glyf::{self, FreeTypeScaler},
pen::PathStyle,
AdjustedMetrics, DrawError, GlyphStyles, Hinting, LocationRef, NormalizedCoord,
OutlineCollectionKind, OutlineGlyph, OutlineGlyphCollection, OutlineKind, OutlinePen, Size,
};
use crate::alloc::{boxed::Box, vec::Vec};
/// Configuration settings for a hinting instance.
#[derive(Clone, Default, Debug)]
pub struct HintingOptions {
/// Specifies the hinting engine to use.
///
/// Defaults to [`Engine::AutoFallback`].
pub engine: Engine,
/// Defines the properties of the intended target of a hinted outline.
///
/// Defaults to a target with [`SmoothMode::Normal`] which is equivalent
/// to `FT_RENDER_MODE_NORMAL` in FreeType.
pub target: Target,
}
impl From<Target> for HintingOptions {
fn from(value: Target) -> Self {
Self {
engine: Engine::AutoFallback,
target: value,
}
}
}
/// Specifies the backend to use when applying hints.
#[derive(Clone, Default, Debug)]
pub enum Engine {
/// The TrueType or PostScript interpreter.
Interpreter,
/// The automatic hinter that performs just-in-time adjustment of
/// outlines.
///
/// Glyph styles can be precomputed per font and may be provided here
/// as an optimization to avoid recomputing them for each instance.
Auto(Option<GlyphStyles>),
/// Selects the engine based on the same rules that FreeType uses when
/// neither of the `FT_LOAD_NO_AUTOHINT` or `FT_LOAD_FORCE_AUTOHINT`
/// load flags are specified.
///
/// Specifically, PostScript (CFF/CFF2) fonts will always use the hinting
/// engine in the PostScript interpreter and TrueType fonts will use the
/// interpreter for TrueType instructions if one of the `fpgm` or `prep`
/// tables is non-empty, falling back to the automatic hinter otherwise.
///
/// This uses [`OutlineGlyphCollection::prefer_interpreter`] to make a
/// selection.
#[default]
AutoFallback,
}
impl Engine {
/// Converts the `AutoFallback` variant into either `Interpreter` or
/// `Auto` based on the given outline set's preference for interpreter
/// mode.
fn resolve_auto_fallback(self, outlines: &OutlineGlyphCollection) -> Engine {
match self {
Self::Interpreter => Self::Interpreter,
Self::Auto(styles) => Self::Auto(styles),
Self::AutoFallback => {
if outlines.prefer_interpreter() {
Self::Interpreter
} else {
Self::Auto(None)
}
}
}
}
}
impl From<Engine> for HintingOptions {
fn from(value: Engine) -> Self {
Self {
engine: value,
target: Default::default(),
}
}
}
/// Defines the target settings for hinting.
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum Target {
/// Strong hinting style that should only be used for aliased, monochromatic
/// rasterization.
///
/// Corresponds to `FT_LOAD_TARGET_MONO` in FreeType.
Mono,
/// Hinting style that is suitable for anti-aliased rasterization.
///
/// Corresponds to the non-monochrome load targets in FreeType. See
/// [`SmoothMode`] for more detail.
Smooth {
/// The basic mode for smooth hinting.
///
/// Defaults to [`SmoothMode::Normal`].
mode: SmoothMode,
/// If true, TrueType bytecode may assume that the resulting outline
/// will be rasterized with supersampling in the vertical direction.
///
/// When this is enabled, ClearType fonts will often generate wider
/// horizontal stems that may lead to blurry images when rendered with
/// an analytical area rasterizer (such as the one in FreeType).
///
/// The effect of this setting is to control the "ClearType symmetric
/// rendering bit" of the TrueType `GETINFO` instruction. For more
/// detail, see this [issue](https://github.com/googlefonts/fontations/issues/1080).
///
/// FreeType has no corresponding setting and behaves as if this is
/// always enabled.
///
/// This only applies to the TrueType interpreter.
///
/// Defaults to `true`.
symmetric_rendering: bool,
/// If true, prevents adjustment of the outline in the horizontal
/// direction and preserves inter-glyph spacing.
///
/// This is useful for performing layout without concern that hinting
/// will modify the advance width of a glyph. Specifically, it means
/// that layout will not require evaluation of glyph outlines.
///
/// FreeType has no corresponding setting and behaves as if this is
/// always disabled.
///
/// This applies to the TrueType interpreter and the automatic hinter.
///
/// Defaults to `false`.
preserve_linear_metrics: bool,
},
}
impl Default for Target {
fn default() -> Self {
SmoothMode::Normal.into()
}
}
/// Mode selector for a smooth hinting target.
#[derive(Copy, Clone, PartialEq, Eq, Default, Debug)]
pub enum SmoothMode {
/// The standard smooth hinting mode.
///
/// Corresponds to `FT_LOAD_TARGET_NORMAL` in FreeType.
#[default]
Normal,
/// Hinting with a lighter touch, typically meaning less aggressive
/// adjustment in the horizontal direction.
///
/// Corresponds to `FT_LOAD_TARGET_LIGHT` in FreeType.
Light,
/// Hinting that is optimized for subpixel rendering with horizontal LCD
/// layouts.
///
/// Corresponds to `FT_LOAD_TARGET_LCD` in FreeType.
Lcd,
/// Hinting that is optimized for subpixel rendering with vertical LCD
/// layouts.
///
/// Corresponds to `FT_LOAD_TARGET_LCD_V` in FreeType.
VerticalLcd,
}
impl From<SmoothMode> for Target {
fn from(value: SmoothMode) -> Self {
Self::Smooth {
mode: value,
symmetric_rendering: true,
preserve_linear_metrics: false,
}
}
}
/// Modes that control hinting when using embedded instructions.
///
/// Only the TrueType interpreter supports all hinting modes.
///
/// # FreeType compatibility
///
/// The following table describes how to map FreeType hinting modes:
///
/// | FreeType mode | Variant |
/// |-----------------------|--------------------------------------------------------------------------------------|
/// | FT_LOAD_TARGET_MONO | Strong |
/// | FT_LOAD_TARGET_NORMAL | Smooth { lcd_subpixel: None, preserve_linear_metrics: false } |
/// | FT_LOAD_TARGET_LCD | Smooth { lcd_subpixel: Some(LcdLayout::Horizontal), preserve_linear_metrics: false } |
/// | FT_LOAD_TARGET_LCD_V | Smooth { lcd_subpixel: Some(LcdLayout::Vertical), preserve_linear_metrics: false } |
///
/// Note: `FT_LOAD_TARGET_LIGHT` is equivalent to `FT_LOAD_TARGET_NORMAL` since
/// FreeType 2.7.
///
/// The default value of this type is equivalent to `FT_LOAD_TARGET_NORMAL`.
#[doc(hidden)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum HintingMode {
/// Strong hinting mode that should only be used for aliased, monochromatic
/// rasterization.
///
/// Corresponds to `FT_LOAD_TARGET_MONO` in FreeType.
Strong,
/// Lighter hinting mode that is intended for anti-aliased rasterization.
Smooth {
/// If set, enables support for optimized hinting that takes advantage
/// of subpixel layouts in LCD displays and corresponds to
/// `FT_LOAD_TARGET_LCD` or `FT_LOAD_TARGET_LCD_V` in FreeType.
///
/// If unset, corresponds to `FT_LOAD_TARGET_NORMAL` in FreeType.
lcd_subpixel: Option<LcdLayout>,
/// If true, prevents adjustment of the outline in the horizontal
/// direction and preserves inter-glyph spacing.
///
/// This is useful for performing layout without concern that hinting
/// will modify the advance width of a glyph. Specifically, it means
/// that layout will not require evaluation of glyph outlines.
///
/// FreeType has no corresponding setting.
preserve_linear_metrics: bool,
},
}
impl Default for HintingMode {
fn default() -> Self {
Self::Smooth {
lcd_subpixel: None,
preserve_linear_metrics: false,
}
}
}
impl From<HintingMode> for HintingOptions {
fn from(value: HintingMode) -> Self {
let target = match value {
HintingMode::Strong => Target::Mono,
HintingMode::Smooth {
lcd_subpixel,
preserve_linear_metrics,
} => {
let mode = match lcd_subpixel {
Some(LcdLayout::Horizontal) => SmoothMode::Lcd,
Some(LcdLayout::Vertical) => SmoothMode::VerticalLcd,
None => SmoothMode::Normal,
};
Target::Smooth {
mode,
preserve_linear_metrics,
symmetric_rendering: true,
}
}
};
target.into()
}
}
/// Specifies direction of pixel layout for LCD based subpixel hinting.
#[doc(hidden)]
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum LcdLayout {
/// Subpixels are ordered horizontally.
///
/// Corresponds to `FT_LOAD_TARGET_LCD` in FreeType.
Horizontal,
/// Subpixels are ordered vertically.
///
/// Corresponds to `FT_LOAD_TARGET_LCD_V` in FreeType.
Vertical,
}
/// Hinting instance that uses information embedded in the font to perform
/// grid-fitting.
#[derive(Clone)]
pub struct HintingInstance {
size: Size,
coords: Vec<NormalizedCoord>,
target: Target,
kind: HinterKind,
}
impl HintingInstance {
/// Creates a new embedded hinting instance for the given outline
/// collection, size, location in variation space and hinting mode.
pub fn new<'a>(
outline_glyphs: &OutlineGlyphCollection,
size: Size,
location: impl Into<LocationRef<'a>>,
options: impl Into<HintingOptions>,
) -> Result<Self, DrawError> {
let options = options.into();
let mut hinter = Self {
size: Size::unscaled(),
coords: vec![],
target: options.target,
kind: HinterKind::None,
};
hinter.reconfigure(outline_glyphs, size, location, options)?;
Ok(hinter)
}
/// Returns the currently configured size.
pub fn size(&self) -> Size {
self.size
}
/// Returns the currently configured normalized location in variation space.
pub fn location(&self) -> LocationRef {
LocationRef::new(&self.coords)
}
/// Returns the currently configured hinting target.
pub fn target(&self) -> Target {
self.target
}
/// Resets the hinter state for a new font instance with the given
/// outline collection and settings.
pub fn reconfigure<'a>(
&mut self,
outlines: &OutlineGlyphCollection,
size: Size,
location: impl Into<LocationRef<'a>>,
options: impl Into<HintingOptions>,
) -> Result<(), DrawError> {
self.size = size;
self.coords.clear();
self.coords
.extend_from_slice(location.into().effective_coords());
let options = options.into();
self.target = options.target;
let engine = options.engine.resolve_auto_fallback(outlines);
// Reuse memory if the font contains the same outline format
let current_kind = core::mem::replace(&mut self.kind, HinterKind::None);
match engine {
Engine::Interpreter => match &outlines.kind {
OutlineCollectionKind::Glyf(glyf) => {
let mut hint_instance = match current_kind {
HinterKind::Glyf(instance) => instance,
_ => Box::<glyf::HintInstance>::default(),
};
let ppem = size.ppem();
let scale = glyf.compute_scale(ppem).1.to_bits();
hint_instance.reconfigure(
glyf,
scale,
ppem.unwrap_or_default() as i32,
self.target,
&self.coords,
)?;
self.kind = HinterKind::Glyf(hint_instance);
}
OutlineCollectionKind::Cff(cff) => {
let mut subfonts = match current_kind {
HinterKind::Cff(subfonts) => subfonts,
_ => vec![],
};
subfonts.clear();
let ppem = size.ppem();
for i in 0..cff.subfont_count() {
subfonts.push(cff.subfont(i, ppem, &self.coords)?);
}
self.kind = HinterKind::Cff(subfonts);
}
OutlineCollectionKind::None => {}
},
Engine::Auto(styles) => {
let Some(font) = outlines.font() else {
return Ok(());
};
let instance = autohint::Instance::new(
font,
outlines,
&self.coords,
self.target,
styles,
true,
);
self.kind = HinterKind::Auto(instance);
}
_ => {}
}
Ok(())
}
/// Returns true if hinting should actually be applied for this instance.
///
/// Some TrueType fonts disable hinting dynamically based on the instance
/// configuration.
pub fn is_enabled(&self) -> bool {
match &self.kind {
HinterKind::Glyf(instance) => instance.is_enabled(),
HinterKind::Cff(_) | HinterKind::Auto(_) => true,
_ => false,
}
}
pub(super) fn draw(
&self,
glyph: &OutlineGlyph,
memory: Option<&mut [u8]>,
path_style: PathStyle,
pen: &mut impl OutlinePen,
is_pedantic: bool,
) -> Result<AdjustedMetrics, DrawError> {
let ppem = self.size.ppem();
let coords = self.coords.as_slice();
match (&self.kind, &glyph.kind) {
(HinterKind::Auto(instance), _) => {
instance.draw(self.size, coords, glyph, path_style, pen)
}
(HinterKind::Glyf(instance), OutlineKind::Glyf(glyf, outline)) => {
if matches!(path_style, PathStyle::HarfBuzz) {
return Err(DrawError::HarfBuzzHintingUnsupported);
}
super::with_glyf_memory(outline, Hinting::Embedded, memory, |buf| {
let scaled_outline = FreeTypeScaler::hinted(
glyf,
outline,
buf,
ppem,
coords,
instance,
is_pedantic,
)?
.scale(&outline.glyph, outline.glyph_id)?;
scaled_outline.to_path(path_style, pen)?;
Ok(AdjustedMetrics {
has_overlaps: outline.has_overlaps,
lsb: Some(scaled_outline.adjusted_lsb().to_f32()),
// When hinting is requested, we round the advance
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/base/ftobjs.c#L889>
advance_width: Some(
scaled_outline.adjusted_advance_width().round().to_f32(),
),
})
})
}
(HinterKind::Cff(subfonts), OutlineKind::Cff(cff, glyph_id, subfont_ix)) => {
let Some(subfont) = subfonts.get(*subfont_ix as usize) else {
return Err(DrawError::NoSources);
};
cff.draw(subfont, *glyph_id, &self.coords, true, pen)?;
Ok(AdjustedMetrics::default())
}
_ => Err(DrawError::NoSources),
}
}
}
#[derive(Clone)]
enum HinterKind {
/// Represents a hinting instance that is associated with an empty outline
/// collection.
None,
Glyf(Box<glyf::HintInstance>),
Cff(Vec<cff::Subfont>),
Auto(autohint::Instance),
}
// Internal helpers for deriving various flags from the mode which
// change the behavior of certain instructions.
// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/truetype/ttgload.c#L2222>
impl Target {
pub(crate) fn is_smooth(&self) -> bool {
matches!(self, Self::Smooth { .. })
}
pub(crate) fn is_grayscale_cleartype(&self) -> bool {
match self {
Self::Smooth { mode, .. } => matches!(mode, SmoothMode::Normal | SmoothMode::Light),
_ => false,
}
}
pub(crate) fn is_light(&self) -> bool {
matches!(
self,
Self::Smooth {
mode: SmoothMode::Light,
..
}
)
}
pub(crate) fn is_lcd(&self) -> bool {
matches!(
self,
Self::Smooth {
mode: SmoothMode::Lcd,
..
}
)
}
pub(crate) fn is_vertical_lcd(&self) -> bool {
matches!(
self,
Self::Smooth {
mode: SmoothMode::VerticalLcd,
..
}
)
}
pub(crate) fn symmetric_rendering(&self) -> bool {
matches!(
self,
Self::Smooth {
symmetric_rendering: true,
..
}
)
}
pub(crate) fn preserve_linear_metrics(&self) -> bool {
matches!(
self,
Self::Smooth {
preserve_linear_metrics: true,
..
}
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
outline::{pen::NullPen, DrawSettings},
raw::TableProvider,
FontRef, MetadataProvider,
};
// FreeType ignores the hdmx table when backward compatibility mode
// is enabled in the TrueType interpreter.
#[test]
fn ignore_hdmx_when_back_compat_enabled() {
let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
let outlines = font.outline_glyphs();
// Double quote was the most egregious failure
let gid = font.charmap().map('"').unwrap();
let font_size = 16;
let hinter = HintingInstance::new(
&outlines,
Size::new(font_size as f32),
LocationRef::default(),
HintingOptions::default(),
)
.unwrap();
let HinterKind::Glyf(tt_hinter) = &hinter.kind else {
panic!("this is definitely a TrueType hinter");
};
// Make sure backward compatibility mode is enabled
assert!(tt_hinter.backward_compatibility());
let outline = outlines.get(gid).unwrap();
let metrics = outline.draw(&hinter, &mut NullPen).unwrap();
// FreeType computes an advance width of 7 when hinting but hdmx contains 5
let scaler_advance = metrics.advance_width.unwrap();
assert_eq!(scaler_advance, 7.0);
let hdmx_advance = font
.hdmx()
.unwrap()
.record_for_size(font_size)
.unwrap()
.widths()[gid.to_u32() as usize];
assert_eq!(hdmx_advance, 5);
}
// When hinting is disabled by the prep table, FreeType still returns
// rounded advance widths
#[test]
fn round_advance_when_prep_disables_hinting() {
let font = FontRef::new(font_test_data::TINOS_SUBSET).unwrap();
let outlines = font.outline_glyphs();
let gid = font.charmap().map('"').unwrap();
let size = Size::new(16.0);
let location = LocationRef::default();
let mut hinter =
HintingInstance::new(&outlines, size, location, HintingOptions::default()).unwrap();
let HinterKind::Glyf(tt_hinter) = &mut hinter.kind else {
panic!("this is definitely a TrueType hinter");
};
tt_hinter.simulate_prep_flag_suppress_hinting();
let outline = outlines.get(gid).unwrap();
// And we still have a rounded advance
let metrics = outline.draw(&hinter, &mut NullPen).unwrap();
assert_eq!(metrics.advance_width, Some(7.0));
// Unhinted advance has some fractional bits
let metrics = outline
.draw(DrawSettings::unhinted(size, location), &mut NullPen)
.unwrap();
assert_eq!(metrics.advance_width, Some(6.53125));
}
}

View File

@@ -0,0 +1,374 @@
//! Name detection for fonts that require hinting to be run for correct
//! contours (FreeType calls these "tricky" fonts).
use crate::{string::StringId, FontRef, MetadataProvider, Tag};
pub(super) fn require_interpreter(font: &FontRef) -> bool {
is_hint_reliant_by_name(font) || matches_hint_reliant_id_list(FontId::from_font(font))
}
fn is_hint_reliant_by_name(font: &FontRef) -> bool {
font.localized_strings(StringId::FAMILY_NAME)
.english_or_first()
.map(|name| {
let mut buf = [0u8; MAX_HINT_RELIANT_NAME_LEN];
let mut len = 0;
let mut chars = name.chars();
for ch in chars.by_ref().take(MAX_HINT_RELIANT_NAME_LEN) {
buf[len] = ch as u8;
len += 1;
}
if chars.next().is_some() {
return false;
}
matches_hint_reliant_name_list(core::str::from_utf8(&buf[..len]).unwrap_or_default())
})
.unwrap_or_default()
}
/// Is this name on the list of fonts that require hinting?
///
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L174>
fn matches_hint_reliant_name_list(name: &str) -> bool {
let name = skip_pdf_random_tag(name);
HINT_RELIANT_NAMES
.iter()
// FreeType uses strstr(name, tricky_name) so we use contains() to
// match behavior.
.any(|tricky_name| name.contains(*tricky_name))
}
/// Fonts embedded in PDFs add random prefixes. Strip these
/// for tricky font comparison purposes.
///
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L153>
fn skip_pdf_random_tag(name: &str) -> &str {
let bytes = name.as_bytes();
// Random tag is 6 uppercase letters followed by a +
if bytes.len() < 8 || bytes[6] != b'+' || !bytes.iter().take(6).all(|b| b.is_ascii_uppercase())
{
return name;
}
core::str::from_utf8(&bytes[7..]).unwrap_or(name)
}
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L180>
#[rustfmt::skip]
const HINT_RELIANT_NAMES: &[&str] = &[
"cpop", /* dftt-p7.ttf; version 1.00, 1992 [DLJGyShoMedium] */
"DFGirl-W6-WIN-BF", /* dftt-h6.ttf; version 1.00, 1993 */
"DFGothic-EB", /* DynaLab Inc. 1992-1995 */
"DFGyoSho-Lt", /* DynaLab Inc. 1992-1995 */
"DFHei", /* DynaLab Inc. 1992-1995 [DFHei-Bd-WIN-HK-BF] */
/* covers "DFHei-Md-HK-BF", maybe DynaLab Inc. */
"DFHSGothic-W5", /* DynaLab Inc. 1992-1995 */
"DFHSMincho-W3", /* DynaLab Inc. 1992-1995 */
"DFHSMincho-W7", /* DynaLab Inc. 1992-1995 */
"DFKaiSho-SB", /* dfkaisb.ttf */
"DFKaiShu", /* covers "DFKaiShu-Md-HK-BF", maybe DynaLab Inc. */
"DFKai-SB", /* kaiu.ttf; version 3.00, 1998 [DFKaiShu-SB-Estd-BF] */
"DFMing", /* DynaLab Inc. 1992-1995 [DFMing-Md-WIN-HK-BF] */
/* covers "DFMing-Bd-HK-BF", maybe DynaLab Inc. */
"DLC", /* dftt-m7.ttf; version 1.00, 1993 [DLCMingBold] */
/* dftt-f5.ttf; version 1.00, 1993 [DLCFongSung] */
/* covers following */
/* "DLCHayMedium", dftt-b5.ttf; version 1.00, 1993 */
/* "DLCHayBold", dftt-b7.ttf; version 1.00, 1993 */
/* "DLCKaiMedium", dftt-k5.ttf; version 1.00, 1992 */
/* "DLCLiShu", dftt-l5.ttf; version 1.00, 1992 */
/* "DLCRoundBold", dftt-r7.ttf; version 1.00, 1993 */
"HuaTianKaiTi?", /* htkt2.ttf */
"HuaTianSongTi?", /* htst3.ttf */
"Ming(for ISO10646)", /* hkscsiic.ttf; version 0.12, 2007 [Ming] */
/* iicore.ttf; version 0.07, 2007 [Ming] */
"MingLiU", /* mingliu.ttf */
/* mingliu.ttc; version 3.21, 2001 */
"MingMedium", /* dftt-m5.ttf; version 1.00, 1993 [DLCMingMedium] */
"PMingLiU", /* mingliu.ttc; version 3.21, 2001 */
"MingLi43", /* mingli.ttf; version 1.00, 1992 */
];
const MAX_HINT_RELIANT_NAME_LEN: usize = 18;
#[derive(Copy, Clone, PartialEq, Default, Debug)]
struct TableId {
checksum: u32,
len: u32,
}
impl TableId {
fn from_font_and_tag(font: &FontRef, tag: Tag) -> Option<Self> {
let data = font.table_data(tag)?;
Some(Self {
// Note: FreeType always just computes the checksum
// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L281>
checksum: raw::tables::compute_checksum(data.as_bytes()),
len: data.len() as u32,
})
}
}
#[derive(Copy, Clone, PartialEq, Default, Debug)]
struct FontId {
cvt: TableId,
fpgm: TableId,
prep: TableId,
}
impl FontId {
fn from_font(font: &FontRef) -> Self {
Self {
cvt: TableId::from_font_and_tag(font, Tag::new(b"cvt ")).unwrap_or_default(),
fpgm: TableId::from_font_and_tag(font, Tag::new(b"fpgm")).unwrap_or_default(),
prep: TableId::from_font_and_tag(font, Tag::new(b"prep")).unwrap_or_default(),
}
}
}
/// Checks for fonts that require hinting based on the length and checksum of
/// the cvt, fpgm and prep tables.
///
/// Roughly equivalent to <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L309>
fn matches_hint_reliant_id_list(font_id: FontId) -> bool {
HINT_RELIANT_IDS.contains(&font_id)
}
/// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/truetype/ttobjs.c#L314>
#[rustfmt::skip]
const HINT_RELIANT_IDS: &[FontId] = &[
// MingLiU 1995
FontId {
cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
prep: TableId { checksum: 0xA344A1EA, len: 0x000001E1 },
},
// MingLiU 1996-
FontId {
cvt: TableId { checksum: 0x05BCF058, len: 0x000002E4 },
fpgm: TableId { checksum: 0x28233BF1, len: 0x000087C4 },
prep: TableId { checksum: 0xA344A1EB, len: 0x000001E1 },
},
// DFGothic-EB
FontId {
cvt: TableId { checksum: 0x12C3EBB2, len: 0x00000350 },
fpgm: TableId { checksum: 0xB680EE64, len: 0x000087A7 },
prep: TableId { checksum: 0xCE939563, len: 0x00000758 },
},
// DFGyoSho-Lt
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
fpgm: TableId { checksum: 0xCE5956E9, len: 0x0000BC85 },
prep: TableId { checksum: 0x8272F416, len: 0x00000045 },
},
// DFHei-Md-HK-BF
FontId {
cvt: TableId { checksum: 0x1257EB46, len: 0x00000350 },
fpgm: TableId { checksum: 0xF699D160, len: 0x0000715F },
prep: TableId { checksum: 0xD222F568, len: 0x000003BC },
},
// DFHSGothic-W5
FontId {
cvt: TableId { checksum: 0x1262EB4E, len: 0x00000350 },
fpgm: TableId { checksum: 0xE86A5D64, len: 0x00007940 },
prep: TableId { checksum: 0x7850F729, len: 0x000005FF },
},
// DFHSMincho-W3
FontId {
cvt: TableId { checksum: 0x122DEB0A, len: 0x00000350 },
fpgm: TableId { checksum: 0x3D16328A, len: 0x0000859B },
prep: TableId { checksum: 0xA93FC33B, len: 0x000002CB },
},
// DFHSMincho-W7
FontId {
cvt: TableId { checksum: 0x125FEB26, len: 0x00000350 },
fpgm: TableId { checksum: 0xA5ACC982, len: 0x00007EE1 },
prep: TableId { checksum: 0x90999196, len: 0x0000041F },
},
// DFKaiShu
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
fpgm: TableId { checksum: 0x5A30CA3B, len: 0x00009063 },
prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
},
// DFKaiShu, variant
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000350 },
fpgm: TableId { checksum: 0xA6E78C01, len: 0x00008998 },
prep: TableId { checksum: 0x13A42602, len: 0x0000007E },
},
// DFKaiShu-Md-HK-BF
FontId {
cvt: TableId { checksum: 0x11E5EAD4, len: 0x00000360 },
fpgm: TableId { checksum: 0x9DB282B2, len: 0x0000C06E },
prep: TableId { checksum: 0x53E6D7CA, len: 0x00000082 },
},
// DFMing-Bd-HK-BF
FontId {
cvt: TableId { checksum: 0x1243EB18, len: 0x00000350 },
fpgm: TableId { checksum: 0xBA0A8C30, len: 0x000074AD },
prep: TableId { checksum: 0xF3D83409, len: 0x0000037B },
},
// DLCLiShu
FontId {
cvt: TableId { checksum: 0x07DCF546, len: 0x00000308 },
fpgm: TableId { checksum: 0x40FE7C90, len: 0x00008E2A },
prep: TableId { checksum: 0x608174B5, len: 0x0000007A },
},
// DLCHayBold
FontId {
cvt: TableId { checksum: 0xEB891238, len: 0x00000308 },
fpgm: TableId { checksum: 0xD2E4DCD4, len: 0x0000676F },
prep: TableId { checksum: 0x8EA5F293, len: 0x000003B8 },
},
// HuaTianKaiTi
FontId {
cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
fpgm: TableId { checksum: 0x9C9E48B8, len: 0x0000BEA2 },
prep: TableId { checksum: 0x70020112, len: 0x00000008 },
},
// HuaTianSongTi
FontId {
cvt: TableId { checksum: 0xFFFBFFFC, len: 0x00000008 },
fpgm: TableId { checksum: 0x0A5A0483, len: 0x00017C39 },
prep: TableId { checksum: 0x70020112, len: 0x00000008 },
},
// NEC fadpop7.ttf
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
prep: TableId { checksum: 0xA39B58E3, len: 0x0000117C },
},
// NEC fadrei5.ttf
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x33C41652, len: 0x000000E5 },
prep: TableId { checksum: 0x26D6C52A, len: 0x00000F6A },
},
// NEC fangot7.ttf
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x6DB1651D, len: 0x0000019D },
prep: TableId { checksum: 0x6C6E4B03, len: 0x00002492 },
},
// NEC fangyo5.ttf
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x40C92555, len: 0x000000E5 },
prep: TableId { checksum: 0xDE51FAD0, len: 0x0000117C },
},
// NEC fankyo5.ttf
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x85E47664, len: 0x000000E5 },
prep: TableId { checksum: 0xA6C62831, len: 0x00001CAA },
},
// NEC fanrgo5.ttf
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x2D891CFD, len: 0x0000019D },
prep: TableId { checksum: 0xA0604633, len: 0x00001DE8 },
},
// NEC fangot5.ttc
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x40AA774C, len: 0x000001CB },
prep: TableId { checksum: 0x9B5CAA96, len: 0x00001F9A },
},
// NEC fanmin3.ttc
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x0D3DE9CB, len: 0x00000141 },
prep: TableId { checksum: 0xD4127766, len: 0x00002280 },
},
// NEC FA-Gothic, 1996
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x4A692698, len: 0x000001F0 },
prep: TableId { checksum: 0x340D4346, len: 0x00001FCA },
},
// NEC FA-Minchou, 1996
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0xCD34C604, len: 0x00000166 },
prep: TableId { checksum: 0x6CF31046, len: 0x000022B0 },
},
// NEC FA-RoundGothicB, 1996
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0x5DA75315, len: 0x0000019D },
prep: TableId { checksum: 0x40745A5F, len: 0x000022E0 },
},
// NEC FA-RoundGothicM, 1996
FontId {
cvt: TableId { checksum: 0x00000000, len: 0x00000000 },
fpgm: TableId { checksum: 0xF055FC48, len: 0x000001C2 },
prep: TableId { checksum: 0x3900DED3, len: 0x00001E18 },
},
// MINGLI.TTF, 1992
FontId {
cvt: TableId { checksum: 0x00170003, len: 0x00000060 },
fpgm: TableId { checksum: 0xDBB4306E, len: 0x000058AA },
prep: TableId { checksum: 0xD643482A, len: 0x00000035 },
},
// DFHei-Bd-WIN-HK-BF, issue #1087
FontId {
cvt: TableId { checksum: 0x1269EB58, len: 0x00000350 },
fpgm: TableId { checksum: 0x5CD5957A, len: 0x00006A4E },
prep: TableId { checksum: 0xF758323A, len: 0x00000380 },
},
// DFMing-Md-WIN-HK-BF, issue #1087
FontId {
cvt: TableId { checksum: 0x122FEB0B, len: 0x00000350 },
fpgm: TableId { checksum: 0x7F10919A, len: 0x000070A9 },
prep: TableId { checksum: 0x7CD7E7B7, len: 0x0000025C },
},
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ensure_max_name_len() {
let max_len = HINT_RELIANT_NAMES
.iter()
.fold(0, |acc, name| acc.max(name.len()));
assert_eq!(max_len, MAX_HINT_RELIANT_NAME_LEN);
}
#[test]
fn skip_pdf_tags() {
// length must be at least 8
assert_eq!(skip_pdf_random_tag("ABCDEF+"), "ABCDEF+");
// first six chars must be ascii uppercase
assert_eq!(skip_pdf_random_tag("AbCdEF+Arial"), "AbCdEF+Arial");
// no numbers
assert_eq!(skip_pdf_random_tag("Ab12EF+Arial"), "Ab12EF+Arial");
// missing +
assert_eq!(skip_pdf_random_tag("ABCDEFArial"), "ABCDEFArial");
// too long
assert_eq!(skip_pdf_random_tag("ABCDEFG+Arial"), "ABCDEFG+Arial");
// too short
assert_eq!(skip_pdf_random_tag("ABCDE+Arial"), "ABCDE+Arial");
// just right
assert_eq!(skip_pdf_random_tag("ABCDEF+Arial"), "Arial");
}
#[test]
fn all_hint_reliant_names() {
for name in HINT_RELIANT_NAMES {
assert!(matches_hint_reliant_name_list(name));
}
}
#[test]
fn non_hint_reliant_names() {
for not_tricky in ["Roboto", "Arial", "Helvetica", "Blah", ""] {
assert!(!matches_hint_reliant_name_list(not_tricky));
}
}
}

29
vendor/skrifa/src/outline/memory.rs vendored Normal file
View File

@@ -0,0 +1,29 @@
//! Support for temporary memory allocation, making use of the stack for
//! small sizes.
/// Invokes the callback with a memory buffer of the requested size.
pub(super) fn with_temporary_memory<R>(size: usize, mut f: impl FnMut(&mut [u8]) -> R) -> R {
// Wrap in a function and prevent inlining to avoid stack allocation
// and zeroing if we don't take this code path.
#[inline(never)]
fn stack_mem<const STACK_SIZE: usize, R>(size: usize, mut f: impl FnMut(&mut [u8]) -> R) -> R {
f(&mut [0u8; STACK_SIZE][..size])
}
// Use bucketed stack allocations (up to 16k) to prevent excessive zeroing
// of memory
if size <= 512 {
stack_mem::<512, _>(size, f)
} else if size <= 1024 {
stack_mem::<1024, _>(size, f)
} else if size <= 2048 {
stack_mem::<2048, _>(size, f)
} else if size <= 4096 {
stack_mem::<4096, _>(size, f)
} else if size <= 8192 {
stack_mem::<8192, _>(size, f)
} else if size <= 16384 {
stack_mem::<16384, _>(size, f)
} else {
f(&mut vec![0u8; size])
}
}

51
vendor/skrifa/src/outline/metrics.rs vendored Normal file
View File

@@ -0,0 +1,51 @@
//! Helper for loading (possibly variable) horizontal glyph metrics.
use raw::{
tables::{hmtx::Hmtx, hvar::Hvar},
types::{F2Dot14, GlyphId},
FontRef, TableProvider,
};
/// Access to horizontal glyph metrics.
#[derive(Clone)]
pub(crate) struct GlyphHMetrics<'a> {
pub hmtx: Hmtx<'a>,
pub hvar: Option<Hvar<'a>>,
}
impl<'a> GlyphHMetrics<'a> {
pub fn new(font: &FontRef<'a>) -> Option<Self> {
// Note: hmtx is required and HVAR is optional
let hmtx = font.hmtx().ok()?;
let hvar = font.hvar().ok();
Some(Self { hmtx, hvar })
}
/// Returns the advance width (in font units) for the given glyph and
/// the location in variation space represented by the set of normalized
/// coordinates in 2.14 fixed point.
pub fn advance_width(&self, gid: GlyphId, coords: &'a [F2Dot14]) -> i32 {
let mut advance = self.hmtx.advance(gid).unwrap_or_default() as i32;
if let (false, Some(hvar)) = (coords.is_empty(), &self.hvar) {
advance += hvar
.advance_width_delta(gid, coords)
.map(|delta| delta.to_i32())
.unwrap_or(0);
}
advance
}
/// Returns the left side bearing (in font units) for the given glyph and
/// the location in variation space represented by the set of normalized
/// coordinates in 2.14 fixed point.
pub fn lsb(&self, gid: GlyphId, coords: &'a [F2Dot14]) -> i32 {
let mut lsb = self.hmtx.side_bearing(gid).unwrap_or_default() as i32;
if let (false, Some(hvar)) = (coords.is_empty(), &self.hvar) {
lsb += hvar
.lsb_delta(gid, coords)
.map(|delta| delta.to_i32())
.unwrap_or(0);
}
lsb
}
}

1445
vendor/skrifa/src/outline/mod.rs vendored Normal file

File diff suppressed because it is too large Load Diff

390
vendor/skrifa/src/outline/path.rs vendored Normal file
View File

@@ -0,0 +1,390 @@
//! TrueType style outline to path conversion.
use super::pen::{OutlinePen, PathStyle};
use core::fmt;
use raw::{
tables::glyf::{PointCoord, PointFlags},
types::Point,
};
/// Errors that can occur when converting an outline to a path.
#[derive(Clone, Debug)]
pub enum ToPathError {
/// Contour end point at this index was less than its preceding end point.
ContourOrder(usize),
/// Expected a quadratic off-curve point at this index.
ExpectedQuad(usize),
/// Expected a quadratic off-curve or on-curve point at this index.
ExpectedQuadOrOnCurve(usize),
/// Expected a cubic off-curve point at this index.
ExpectedCubic(usize),
/// Expected number of points to == number of flags
PointFlagMismatch { num_points: usize, num_flags: usize },
}
impl fmt::Display for ToPathError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::ContourOrder(ix) => write!(
f,
"Contour end point at index {ix} was less than preceding end point"
),
Self::ExpectedQuad(ix) => write!(f, "Expected quadatic off-curve point at index {ix}"),
Self::ExpectedQuadOrOnCurve(ix) => write!(
f,
"Expected quadatic off-curve or on-curve point at index {ix}"
),
Self::ExpectedCubic(ix) => write!(f, "Expected cubic off-curve point at index {ix}"),
Self::PointFlagMismatch {
num_points,
num_flags,
} => write!(
f,
"Number of points ({num_points}) and flags ({num_flags}) must match"
),
}
}
}
/// Converts a `glyf` outline described by points, flags and contour end points
/// to a sequence of path elements and invokes the appropriate callback on the
/// given pen for each.
///
/// The input points can have any coordinate type that implements
/// [`PointCoord`]. Output points are always generated in `f32`.
///
/// This is roughly equivalent to [`FT_Outline_Decompose`](https://freetype.org/freetype2/docs/reference/ft2-outline_processing.html#ft_outline_decompose).
///
/// See [`contour_to_path`] for a more general function that takes an iterator
/// if your outline data is in a different format.
pub(crate) fn to_path<C: PointCoord>(
points: &[Point<C>],
flags: &[PointFlags],
contours: &[u16],
path_style: PathStyle,
pen: &mut impl OutlinePen,
) -> Result<(), ToPathError> {
for contour_ix in 0..contours.len() {
let start_ix = (contour_ix > 0)
.then(|| contours[contour_ix - 1] as usize + 1)
.unwrap_or_default();
let end_ix = contours[contour_ix] as usize;
if end_ix < start_ix || end_ix >= points.len() {
return Err(ToPathError::ContourOrder(contour_ix));
}
let points = &points[start_ix..=end_ix];
if points.is_empty() {
continue;
}
let flags = flags
.get(start_ix..=end_ix)
.ok_or(ToPathError::PointFlagMismatch {
num_points: points.len(),
num_flags: flags.len(),
})?;
let last_point = points.last().unwrap();
let last_flags = flags.last().unwrap();
let last_point = ContourPoint {
x: last_point.x,
y: last_point.y,
flags: *last_flags,
};
contour_to_path(
points.iter().zip(flags).map(|(point, flags)| ContourPoint {
x: point.x,
y: point.y,
flags: *flags,
}),
last_point,
path_style,
pen,
)
.map_err(|e| match &e {
ToPathError::ExpectedCubic(ix) => ToPathError::ExpectedCubic(ix + start_ix),
ToPathError::ExpectedQuad(ix) => ToPathError::ExpectedQuad(ix + start_ix),
ToPathError::ExpectedQuadOrOnCurve(ix) => {
ToPathError::ExpectedQuadOrOnCurve(ix + start_ix)
}
_ => e,
})?
}
Ok(())
}
/// Combination of point coordinates and flags.
#[derive(Copy, Clone, Default, Debug)]
pub(crate) struct ContourPoint<T> {
pub x: T,
pub y: T,
pub flags: PointFlags,
}
impl<T> ContourPoint<T>
where
T: PointCoord,
{
fn point_f32(&self) -> Point<f32> {
Point::new(self.x.to_f32(), self.y.to_f32())
}
fn midpoint(&self, other: Self) -> ContourPoint<T> {
let (x, y) = (self.x.midpoint(other.x), self.y.midpoint(other.y));
Self {
x,
y,
flags: other.flags,
}
}
}
/// Generates a path from an iterator of contour points.
///
/// Note that this requires the last point of the contour to be passed
/// separately to support FreeType style path conversion when the contour
/// begins with an off curve point. The points iterator should still
/// yield the last point as well.
///
/// This is more general than [`to_path`] and exists to support cases (such as
/// autohinting) where the source outline data is in a different format.
pub(crate) fn contour_to_path<C: PointCoord>(
points: impl Iterator<Item = ContourPoint<C>>,
last_point: ContourPoint<C>,
style: PathStyle,
pen: &mut impl OutlinePen,
) -> Result<(), ToPathError> {
let mut points = points.enumerate().peekable();
let Some((_, first_point)) = points.peek().copied() else {
// This is an empty contour
return Ok(());
};
// We don't accept an off curve cubic as the first point
if first_point.flags.is_off_curve_cubic() {
return Err(ToPathError::ExpectedQuadOrOnCurve(0));
}
// For FreeType style, we may need to omit the last point if we find the
// first on curve there
let mut omit_last = false;
// For HarfBuzz style, may skip up to two points in finding the start, so
// process these at the end
let mut trailing_points = [None; 2];
// Find our starting point
let start_point = if first_point.flags.is_off_curve_quad() {
// We're starting with an off curve, so select our first move based on
// the path style
match style {
PathStyle::FreeType => {
if last_point.flags.is_on_curve() {
// The last point is an on curve, so let's start there
omit_last = true;
last_point
} else {
// It's also an off curve, so take implicit midpoint
last_point.midpoint(first_point)
}
}
PathStyle::HarfBuzz => {
// Always consume the first point
points.next();
// Then check the next point
let Some((_, next_point)) = points.peek().copied() else {
// This is a single point contour
return Ok(());
};
if next_point.flags.is_on_curve() {
points.next();
trailing_points = [Some((0, first_point)), Some((1, next_point))];
// Next is on curve, so let's start there
next_point
} else {
// It's also an off curve, so take the implicit midpoint
trailing_points = [Some((0, first_point)), None];
first_point.midpoint(next_point)
}
}
}
} else {
// We're starting with an on curve, so consume the point
points.next();
first_point
};
let point = start_point.point_f32();
pen.move_to(point.x, point.y);
let mut state = PendingState::default();
if omit_last {
while let Some((ix, point)) = points.next() {
if points.peek().is_none() {
break;
}
state.emit(ix, point, pen)?;
}
} else {
for (ix, point) in points {
state.emit(ix, point, pen)?;
}
}
for (ix, point) in trailing_points.iter().filter_map(|x| *x) {
state.emit(ix, point, pen)?;
}
state.finish(0, start_point, pen)?;
Ok(())
}
#[derive(Copy, Clone, Default)]
enum PendingState<C> {
/// No pending points.
#[default]
Empty,
/// Pending off-curve quad point.
PendingQuad(ContourPoint<C>),
/// Single pending off-curve cubic point.
PendingCubic(ContourPoint<C>),
/// Two pending off-curve cubic points.
TwoPendingCubics(ContourPoint<C>, ContourPoint<C>),
}
impl<C> PendingState<C>
where
C: PointCoord,
{
#[inline(always)]
fn emit(
&mut self,
ix: usize,
point: ContourPoint<C>,
pen: &mut impl OutlinePen,
) -> Result<(), ToPathError> {
let flags = point.flags;
match *self {
Self::Empty => {
if flags.is_off_curve_quad() {
*self = Self::PendingQuad(point);
} else if flags.is_off_curve_cubic() {
*self = Self::PendingCubic(point);
} else {
let p = point.point_f32();
pen.line_to(p.x, p.y);
}
}
Self::PendingQuad(quad) => {
if flags.is_off_curve_quad() {
let c0 = quad.point_f32();
let p = quad.midpoint(point).point_f32();
pen.quad_to(c0.x, c0.y, p.x, p.y);
*self = Self::PendingQuad(point);
} else if flags.is_off_curve_cubic() {
return Err(ToPathError::ExpectedQuadOrOnCurve(ix));
} else {
let c0 = quad.point_f32();
let p = point.point_f32();
pen.quad_to(c0.x, c0.y, p.x, p.y);
*self = Self::Empty;
}
}
Self::PendingCubic(cubic) => {
if flags.is_off_curve_cubic() {
*self = Self::TwoPendingCubics(cubic, point);
} else {
return Err(ToPathError::ExpectedCubic(ix));
}
}
Self::TwoPendingCubics(cubic0, cubic1) => {
if flags.is_off_curve_quad() {
return Err(ToPathError::ExpectedCubic(ix));
} else if flags.is_off_curve_cubic() {
let c0 = cubic0.point_f32();
let c1 = cubic1.point_f32();
let p = cubic1.midpoint(point).point_f32();
pen.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
*self = Self::PendingCubic(point);
} else {
let c0 = cubic0.point_f32();
let c1 = cubic1.point_f32();
let p = point.point_f32();
pen.curve_to(c0.x, c0.y, c1.x, c1.y, p.x, p.y);
*self = Self::Empty;
}
}
}
Ok(())
}
fn finish(
mut self,
start_ix: usize,
mut start_point: ContourPoint<C>,
pen: &mut impl OutlinePen,
) -> Result<(), ToPathError> {
match self {
Self::Empty => {}
_ => {
// We always want to end with an explicit on-curve
start_point.flags = PointFlags::on_curve();
self.emit(start_ix, start_point, pen)?;
}
}
pen.close();
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{super::pen::SvgPen, *};
use raw::types::F26Dot6;
fn assert_off_curve_path_to_svg(expected: &str, path_style: PathStyle, all_off_curve: bool) {
fn pt(x: i32, y: i32) -> Point<F26Dot6> {
Point::new(x, y).map(F26Dot6::from_bits)
}
let mut flags = [PointFlags::off_curve_quad(); 4];
if !all_off_curve {
flags[1] = PointFlags::on_curve();
}
let contours = [3];
// This test is meant to prevent a bug where the first move-to was computed improperly
// for a contour consisting of all off curve points.
// In this case, the start of the path should be the midpoint between the first and last points.
// For this test case (in 26.6 fixed point): [(640, 128) + (128, 128)] / 2 = (384, 128)
// which becomes (6.0, 2.0) when converted to floating point.
let points = [pt(640, 128), pt(256, 64), pt(640, 64), pt(128, 128)];
let mut pen = SvgPen::with_precision(1);
to_path(&points, &flags, &contours, path_style, &mut pen).unwrap();
assert_eq!(pen.as_ref(), expected);
}
#[test]
fn all_off_curve_to_path_scan_backward() {
assert_off_curve_path_to_svg(
"M6.0,2.0 Q10.0,2.0 7.0,1.5 Q4.0,1.0 7.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Z",
PathStyle::FreeType,
true,
);
}
#[test]
fn all_off_curve_to_path_scan_forward() {
assert_off_curve_path_to_svg(
"M7.0,1.5 Q4.0,1.0 7.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Q10.0,2.0 7.0,1.5 Z",
PathStyle::HarfBuzz,
true,
);
}
#[test]
fn start_off_curve_to_path_scan_backward() {
assert_off_curve_path_to_svg(
"M6.0,2.0 Q10.0,2.0 4.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Z",
PathStyle::FreeType,
false,
);
}
#[test]
fn start_off_curve_to_path_scan_forward() {
assert_off_curve_path_to_svg(
"M4.0,1.0 Q10.0,1.0 6.0,1.5 Q2.0,2.0 6.0,2.0 Q10.0,2.0 4.0,1.0 Z",
PathStyle::HarfBuzz,
false,
);
}
}

247
vendor/skrifa/src/outline/pen.rs vendored Normal file
View File

@@ -0,0 +1,247 @@
//! Types for collecting the output when drawing a glyph outline.
use alloc::{string::String, vec::Vec};
use core::fmt::{self, Write};
/// Interface for accepting a sequence of path commands.
pub trait OutlinePen {
/// Emit a command to begin a new subpath at (x, y).
fn move_to(&mut self, x: f32, y: f32);
/// Emit a line segment from the current point to (x, y).
fn line_to(&mut self, x: f32, y: f32);
/// Emit a quadratic bezier segment from the current point with a control
/// point at (cx0, cy0) and ending at (x, y).
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32);
/// Emit a cubic bezier segment from the current point with control
/// points at (cx0, cy0) and (cx1, cy1) and ending at (x, y).
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32);
/// Emit a command to close the current subpath.
fn close(&mut self);
}
/// Single element of a path.
#[derive(Copy, Clone, PartialEq, PartialOrd, Debug)]
pub enum PathElement {
/// Begin a new subpath at (x, y).
MoveTo { x: f32, y: f32 },
/// Draw a line from the current point to (x, y).
LineTo { x: f32, y: f32 },
/// Draw a quadratic bezier from the current point with a control point at
/// (cx0, cy0) and ending at (x, y).
QuadTo { cx0: f32, cy0: f32, x: f32, y: f32 },
/// Draw a cubic bezier from the current point with control points at
/// (cx0, cy0) and (cx1, cy1) and ending at (x, y).
CurveTo {
cx0: f32,
cy0: f32,
cx1: f32,
cy1: f32,
x: f32,
y: f32,
},
/// Close the current subpath.
Close,
}
/// Style for path conversion.
///
/// The order to process points in a glyf point stream is ambiguous when the
/// first point is off-curve. Major implementations differ. Which one would
/// you like to match?
///
/// **If you add a new one make sure to update the fuzzer.**
#[derive(Debug, Default, Copy, Clone)]
pub enum PathStyle {
/// If the first point is off-curve, check if the last is on-curve
/// If it is, start there. If it isn't, start at the implied midpoint
/// between first and last.
#[default]
FreeType,
/// If the first point is off-curve, check if the second is on-curve.
/// If it is, start there. If it isn't, start at the implied midpoint
/// between first and second.
///
/// Matches hb-draw's interpretation of a point stream.
HarfBuzz,
}
impl OutlinePen for Vec<PathElement> {
fn move_to(&mut self, x: f32, y: f32) {
self.push(PathElement::MoveTo { x, y })
}
fn line_to(&mut self, x: f32, y: f32) {
self.push(PathElement::LineTo { x, y })
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.push(PathElement::QuadTo { cx0, cy0, x, y })
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.push(PathElement::CurveTo {
cx0,
cy0,
cx1,
cy1,
x,
y,
})
}
fn close(&mut self) {
self.push(PathElement::Close)
}
}
/// Pen that drops all drawing output into the ether.
pub struct NullPen;
impl OutlinePen for NullPen {
fn move_to(&mut self, _x: f32, _y: f32) {}
fn line_to(&mut self, _x: f32, _y: f32) {}
fn quad_to(&mut self, _cx0: f32, _cy0: f32, _x: f32, _y: f32) {}
fn curve_to(&mut self, _cx0: f32, _cy0: f32, _cx1: f32, _cy1: f32, _x: f32, _y: f32) {}
fn close(&mut self) {}
}
/// Pen that generates SVG style path data.
#[derive(Clone, Default, Debug)]
pub struct SvgPen(String, Option<usize>);
impl SvgPen {
/// Creates a new SVG pen that formats floating point values with the
/// standard behavior.
pub fn new() -> Self {
Self::default()
}
/// Creates a new SVG pen with the given precision (the number of digits
/// that will be printed after the decimal).
pub fn with_precision(precision: usize) -> Self {
Self(String::default(), Some(precision))
}
/// Clears the content of the internal string.
pub fn clear(&mut self) {
self.0.clear();
}
fn maybe_push_space(&mut self) {
if !self.0.is_empty() {
self.0.push(' ');
}
}
}
impl core::ops::Deref for SvgPen {
type Target = str;
fn deref(&self) -> &Self::Target {
self.0.as_str()
}
}
impl OutlinePen for SvgPen {
fn move_to(&mut self, x: f32, y: f32) {
self.maybe_push_space();
let _ = if let Some(prec) = self.1 {
write!(self.0, "M{x:.0$},{y:.0$}", prec)
} else {
write!(self.0, "M{x},{y}")
};
}
fn line_to(&mut self, x: f32, y: f32) {
self.maybe_push_space();
let _ = if let Some(prec) = self.1 {
write!(self.0, "L{x:.0$},{y:.0$}", prec)
} else {
write!(self.0, "L{x},{y}")
};
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.maybe_push_space();
let _ = if let Some(prec) = self.1 {
write!(self.0, "Q{cx0:.0$},{cy0:.0$} {x:.0$},{y:.0$}", prec)
} else {
write!(self.0, "Q{cx0},{cy0} {x},{y}")
};
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.maybe_push_space();
let _ = if let Some(prec) = self.1 {
write!(
self.0,
"C{cx0:.0$},{cy0:.0$} {cx1:.0$},{cy1:.0$} {x:.0$},{y:.0$}",
prec
)
} else {
write!(self.0, "C{cx0},{cy0} {cx1},{cy1} {x},{y}")
};
}
fn close(&mut self) {
self.maybe_push_space();
self.0.push('Z');
}
}
impl AsRef<str> for SvgPen {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl From<String> for SvgPen {
fn from(value: String) -> Self {
Self(value, None)
}
}
impl From<SvgPen> for String {
fn from(value: SvgPen) -> Self {
value.0
}
}
impl fmt::Display for SvgPen {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn svg_pen_precision() {
let svg_data = [None, Some(1), Some(4)].map(|prec| {
let mut pen = match prec {
None => SvgPen::new(),
Some(prec) => SvgPen::with_precision(prec),
};
pen.move_to(1.0, 2.45556);
pen.line_to(1.2, 4.0);
pen.quad_to(2.0345, 3.56789, -0.157, -425.07);
pen.curve_to(-37.0010, 4.5, 2.0, 1.0, -0.5, -0.25);
pen.close();
pen.to_string()
});
let expected = [
"M1,2.45556 L1.2,4 Q2.0345,3.56789 -0.157,-425.07 C-37.001,4.5 2,1 -0.5,-0.25 Z",
"M1.0,2.5 L1.2,4.0 Q2.0,3.6 -0.2,-425.1 C-37.0,4.5 2.0,1.0 -0.5,-0.2 Z",
"M1.0000,2.4556 L1.2000,4.0000 Q2.0345,3.5679 -0.1570,-425.0700 C-37.0010,4.5000 2.0000,1.0000 -0.5000,-0.2500 Z"
];
for (result, expected) in svg_data.iter().zip(&expected) {
assert_eq!(result, expected);
}
}
}

173
vendor/skrifa/src/outline/testing.rs vendored Normal file
View File

@@ -0,0 +1,173 @@
//! Helpers for unit testing
use super::OutlinePen;
use core::str::FromStr;
use raw::{
tables::glyf::PointFlags,
types::{F26Dot6, F2Dot14, GlyphId, Point},
};
#[derive(Copy, Clone, PartialEq, Debug)]
// clippy doesn't like the common To suffix
#[allow(clippy::enum_variant_names)]
pub enum PathElement {
MoveTo([f32; 2]),
LineTo([f32; 2]),
QuadTo([f32; 4]),
CurveTo([f32; 6]),
}
#[derive(Default)]
pub struct Path {
pub elements: Vec<PathElement>,
last_end: Option<[f32; 2]>,
}
impl OutlinePen for Path {
fn move_to(&mut self, x: f32, y: f32) {
self.elements.push(PathElement::MoveTo([x, y]));
self.last_end = Some([x, y]);
}
fn line_to(&mut self, x: f32, y: f32) {
self.elements.push(PathElement::LineTo([x, y]));
self.last_end = Some([x, y]);
}
fn quad_to(&mut self, x0: f32, y0: f32, x1: f32, y1: f32) {
self.elements.push(PathElement::QuadTo([x0, y0, x1, y1]));
self.last_end = Some([x1, y1]);
}
fn curve_to(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, x2: f32, y2: f32) {
self.elements
.push(PathElement::CurveTo([x0, y0, x1, y1, x2, y2]));
self.last_end = Some([x2, y2]);
}
fn close(&mut self) {
// FT_Outline_Decompose does not generate close commands, so for
// testing purposes, we insert a line to same point as the most
// recent move_to (if the last command didn't end at the same point)
// which copies FreeType's behavior.
let last_move = self
.elements
.iter()
.rev()
.find(|element| matches!(*element, PathElement::MoveTo(_)))
.copied();
if let Some(PathElement::MoveTo(point)) = last_move {
if Some(point) != self.last_end {
self.elements.push(PathElement::LineTo(point));
}
}
}
}
#[derive(Clone, Default, Debug)]
pub struct GlyphOutline {
pub glyph_id: GlyphId,
pub size: f32,
pub coords: Vec<F2Dot14>,
pub points: Vec<Point<F26Dot6>>,
pub contours: Vec<u16>,
pub flags: Vec<PointFlags>,
pub path: Vec<PathElement>,
}
pub fn parse_glyph_outlines(source: &str) -> Vec<GlyphOutline> {
let mut outlines = vec![];
let mut cur_outline = GlyphOutline::default();
for line in source.lines() {
let line = line.trim();
if line == "-" {
outlines.push(cur_outline.clone());
} else if line.starts_with("glyph") {
cur_outline = GlyphOutline::default();
let parts = line.split(' ').collect::<Vec<_>>();
cur_outline.glyph_id = GlyphId::new(parts[1].parse().unwrap());
cur_outline.size = parts[2].parse().unwrap();
} else if line.starts_with("coords") {
for coord in line.split(' ').skip(1) {
cur_outline
.coords
.push(F2Dot14::from_f32(coord.parse().unwrap()));
}
} else if line.starts_with("contours") {
for contour in line.split(' ').skip(1) {
cur_outline.contours.push(contour.parse().unwrap());
}
} else if line.starts_with("points") {
let is_scaled = cur_outline.size != 0.0;
for mut point in parse_points(line.strip_prefix("points").unwrap().trim()) {
if !is_scaled {
point[0] <<= 6;
point[1] <<= 6;
}
cur_outline.points.push(Point {
x: F26Dot6::from_bits(point[0]),
y: F26Dot6::from_bits(point[1]),
});
}
} else if line.starts_with("tags") {
for tag in line.split(' ').skip(1) {
cur_outline
.flags
.push(PointFlags::from_bits(tag.parse().unwrap()));
}
} else {
match line.as_bytes()[0] {
b'm' => {
let points = parse_points(line.strip_prefix("m ").unwrap().trim());
cur_outline.path.push(PathElement::MoveTo(points[0]));
}
b'l' => {
let points = parse_points(line.strip_prefix("l ").unwrap().trim());
cur_outline.path.push(PathElement::LineTo(points[0]));
}
b'q' => {
let points = parse_points(line.strip_prefix("q ").unwrap().trim());
cur_outline.path.push(PathElement::QuadTo([
points[0][0],
points[0][1],
points[1][0],
points[1][1],
]));
}
b'c' => {
let points = parse_points(line.strip_prefix("c ").unwrap().trim());
cur_outline.path.push(PathElement::CurveTo([
points[0][0],
points[0][1],
points[1][0],
points[1][1],
points[2][0],
points[2][1],
]));
}
_ => panic!("unexpected path element"),
}
}
}
outlines
}
fn parse_points<F>(source: &str) -> Vec<[F; 2]>
where
F: FromStr + Copy + Default,
<F as FromStr>::Err: core::fmt::Debug,
{
let mut points = vec![];
for point in source.split(' ') {
let point = point.trim();
if point.is_empty() {
continue;
}
let mut components = [F::default(); 2];
for (i, component) in point.trim().split(',').take(2).enumerate() {
components[i] = F::from_str(component).unwrap();
}
points.push(components);
}
points
}

367
vendor/skrifa/src/outline/unscaled.rs vendored Normal file
View File

@@ -0,0 +1,367 @@
//! Compact representation of an unscaled, unhinted outline.
#![allow(dead_code)]
use super::DrawError;
use crate::collections::SmallVec;
use core::ops::Range;
use raw::{
tables::glyf::PointFlags,
types::{F26Dot6, Point},
};
#[derive(Copy, Clone, Default, Debug)]
pub(super) struct UnscaledPoint {
pub x: i16,
pub y: i16,
pub flags: PointFlags,
pub is_contour_start: bool,
}
impl UnscaledPoint {
pub fn from_glyf_point(
point: Point<F26Dot6>,
flags: PointFlags,
is_contour_start: bool,
) -> Self {
let point = point.map(|x| (x.to_bits() >> 6) as i16);
Self {
x: point.x,
y: point.y,
flags: flags.without_markers(),
is_contour_start,
}
}
pub fn is_on_curve(self) -> bool {
self.flags.is_on_curve()
}
}
pub(super) trait UnscaledOutlineSink {
fn try_reserve(&mut self, additional: usize) -> Result<(), DrawError>;
fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError>;
fn extend(&mut self, points: impl IntoIterator<Item = UnscaledPoint>) -> Result<(), DrawError> {
for point in points.into_iter() {
self.push(point)?;
}
Ok(())
}
}
// please can I have smallvec?
pub(super) struct UnscaledOutlineBuf<const INLINE_CAP: usize>(SmallVec<UnscaledPoint, INLINE_CAP>);
impl<const INLINE_CAP: usize> UnscaledOutlineBuf<INLINE_CAP> {
pub fn new() -> Self {
Self(SmallVec::new())
}
pub fn clear(&mut self) {
self.0.clear();
}
pub fn as_ref(&self) -> UnscaledOutlineRef {
UnscaledOutlineRef {
points: self.0.as_slice(),
}
}
}
impl<const INLINE_CAP: usize> UnscaledOutlineSink for UnscaledOutlineBuf<INLINE_CAP> {
fn try_reserve(&mut self, additional: usize) -> Result<(), DrawError> {
if !self.0.try_reserve(additional) {
Err(DrawError::InsufficientMemory)
} else {
Ok(())
}
}
fn push(&mut self, point: UnscaledPoint) -> Result<(), DrawError> {
self.0.push(point);
Ok(())
}
}
#[derive(Copy, Clone, Debug)]
pub(super) struct UnscaledOutlineRef<'a> {
pub points: &'a [UnscaledPoint],
}
impl UnscaledOutlineRef<'_> {
/// Returns the range of contour points and the index of the point within
/// that contour for the last point where `f` returns true.
///
/// This is common code used for finding extrema when materializing blue
/// zones.
///
/// For example: <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/autofit/aflatin.c#L509>
pub fn find_last_contour(
&self,
mut f: impl FnMut(&UnscaledPoint) -> bool,
) -> Option<(Range<usize>, usize)> {
if self.points.is_empty() {
return None;
}
let mut best_contour = 0..0;
// Index of the best point relative to the start of the best contour
let mut best_point = 0;
let mut cur_contour = 0..0;
let mut found_best_in_cur_contour = false;
for (point_ix, point) in self.points.iter().enumerate() {
if point.is_contour_start {
if found_best_in_cur_contour {
best_contour = cur_contour;
}
cur_contour = point_ix..point_ix;
found_best_in_cur_contour = false;
// Ignore single point contours
match self.points.get(point_ix + 1) {
Some(next_point) if next_point.is_contour_start => continue,
None => continue,
_ => {}
}
}
cur_contour.end += 1;
if f(point) {
best_point = point_ix - cur_contour.start;
found_best_in_cur_contour = true;
}
}
if found_best_in_cur_contour {
best_contour = cur_contour;
}
if !best_contour.is_empty() {
Some((best_contour, best_point))
} else {
None
}
}
}
/// Adapts an UnscaledOutlineSink to be fed from a pen while tracking
/// memory allocation errors.
pub(super) struct UnscaledPenAdapter<'a, T> {
sink: &'a mut T,
failed: bool,
}
impl<'a, T> UnscaledPenAdapter<'a, T> {
pub fn new(sink: &'a mut T) -> Self {
Self {
sink,
failed: false,
}
}
pub fn finish(self) -> Result<(), DrawError> {
if self.failed {
Err(DrawError::InsufficientMemory)
} else {
Ok(())
}
}
}
impl<T> UnscaledPenAdapter<'_, T>
where
T: UnscaledOutlineSink,
{
fn push(&mut self, x: f32, y: f32, flags: PointFlags, is_contour_start: bool) {
if self
.sink
.push(UnscaledPoint {
x: x as i16,
y: y as i16,
flags,
is_contour_start,
})
.is_err()
{
self.failed = true;
}
}
}
impl<T: UnscaledOutlineSink> super::OutlinePen for UnscaledPenAdapter<'_, T> {
fn move_to(&mut self, x: f32, y: f32) {
self.push(x, y, PointFlags::on_curve(), true);
}
fn line_to(&mut self, x: f32, y: f32) {
self.push(x, y, PointFlags::on_curve(), false);
}
fn quad_to(&mut self, cx0: f32, cy0: f32, x: f32, y: f32) {
self.push(cx0, cy0, PointFlags::off_curve_quad(), false);
self.push(x, y, PointFlags::on_curve(), false);
}
fn curve_to(&mut self, cx0: f32, cy0: f32, cx1: f32, cy1: f32, x: f32, y: f32) {
self.push(cx0, cy0, PointFlags::off_curve_cubic(), false);
self.push(cx1, cy1, PointFlags::off_curve_cubic(), false);
self.push(x, y, PointFlags::on_curve(), false);
}
fn close(&mut self) {}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{prelude::LocationRef, MetadataProvider};
use raw::{types::GlyphId, FontRef};
#[test]
fn read_glyf_outline() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
let glyph = font.outline_glyphs().get(GlyphId::new(5)).unwrap();
let mut outline = UnscaledOutlineBuf::<32>::new();
glyph
.draw_unscaled(LocationRef::default(), None, &mut outline)
.unwrap();
let outline = outline.as_ref();
let expected = [
// contour 0
(400, 80, 1),
(400, 360, 1),
(320, 360, 1),
(320, 600, 1),
(320, 633, 0),
(367, 680, 0),
(400, 680, 1),
(560, 680, 1),
(593, 680, 0),
(640, 633, 0),
(640, 600, 1),
(640, 360, 1),
(560, 360, 1),
(560, 80, 1),
// contour 1
(480, 720, 1),
(447, 720, 0),
(400, 767, 0),
(400, 800, 1),
(400, 833, 0),
(447, 880, 0),
(480, 880, 1),
(513, 880, 0),
(560, 833, 0),
(560, 800, 1),
(560, 767, 0),
(513, 720, 0),
];
let points = outline
.points
.iter()
.map(|point| (point.x, point.y, point.flags.to_bits()))
.collect::<Vec<_>>();
assert_eq!(points, expected);
}
#[test]
#[cfg(feature = "spec_next")]
fn read_cubic_glyf_outline() {
let font = FontRef::new(font_test_data::CUBIC_GLYF).unwrap();
let glyph = font.outline_glyphs().get(GlyphId::new(2)).unwrap();
let mut outline = UnscaledOutlineBuf::<32>::new();
glyph
.draw_unscaled(LocationRef::default(), None, &mut outline)
.unwrap();
let outline = outline.as_ref();
let expected = [
// contour 0
(278, 710, 1),
(278, 470, 1),
(300, 500, 128),
(800, 500, 128),
(998, 470, 1),
(998, 710, 1),
];
let points = outline
.points
.iter()
.map(|point| (point.x, point.y, point.flags.to_bits()))
.collect::<Vec<_>>();
assert_eq!(points, expected);
}
#[test]
fn read_cff_outline() {
let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
let glyph = font.outline_glyphs().get(GlyphId::new(2)).unwrap();
let mut outline = UnscaledOutlineBuf::<32>::new();
glyph
.draw_unscaled(LocationRef::default(), None, &mut outline)
.unwrap();
let outline = outline.as_ref();
let expected = [
// contour 0
(83, 0, 1),
(163, 0, 1),
(163, 482, 1),
(83, 482, 1),
// contour 1
(124, 595, 1),
(160, 595, 128),
(181, 616, 128),
(181, 652, 1),
(181, 688, 128),
(160, 709, 128),
(124, 709, 1),
(88, 709, 128),
(67, 688, 128),
(67, 652, 1),
(67, 616, 128),
(88, 595, 128),
(124, 595, 1),
];
let points = outline
.points
.iter()
.map(|point| (point.x, point.y, point.flags.to_bits()))
.collect::<Vec<_>>();
assert_eq!(points, expected);
}
#[test]
fn find_vertical_extrema() {
let font = FontRef::new(font_test_data::MATERIAL_SYMBOLS_SUBSET).unwrap();
let glyph = font.outline_glyphs().get(GlyphId::new(5)).unwrap();
let mut outline = UnscaledOutlineBuf::<32>::new();
glyph
.draw_unscaled(LocationRef::default(), None, &mut outline)
.unwrap();
let outline = outline.as_ref();
// Find the maximum Y value and its containing contour
let mut top_y = None;
let (top_contour, top_point_ix) = outline
.find_last_contour(|point| {
if top_y.is_none() || Some(point.y) > top_y {
top_y = Some(point.y);
true
} else {
false
}
})
.unwrap();
assert_eq!(top_contour, 14..26);
assert_eq!(top_point_ix, 5);
assert_eq!(top_y, Some(880));
// Find the minimum Y value and its containing contour
let mut bottom_y = None;
let (bottom_contour, bottom_point_ix) = outline
.find_last_contour(|point| {
if bottom_y.is_none() || Some(point.y) < bottom_y {
bottom_y = Some(point.y);
true
} else {
false
}
})
.unwrap();
assert_eq!(bottom_contour, 0..14);
assert_eq!(bottom_point_ix, 0);
assert_eq!(bottom_y, Some(80));
}
}

123
vendor/skrifa/src/provider.rs vendored Normal file
View File

@@ -0,0 +1,123 @@
use crate::GlyphNames;
use super::{
attribute::Attributes,
charmap::Charmap,
color::ColorGlyphCollection,
instance::{LocationRef, Size},
metrics::{GlyphMetrics, Metrics},
outline::OutlineGlyphCollection,
string::{LocalizedStrings, StringId},
variation::{AxisCollection, NamedInstanceCollection},
FontRef,
};
use crate::bitmap::BitmapStrikes;
/// Interface for types that can provide font metadata.
pub trait MetadataProvider<'a>: Sized {
/// Returns the primary attributes for font classification-- stretch,
/// style and weight.
fn attributes(&self) -> Attributes;
/// Returns the collection of variation axes.
fn axes(&self) -> AxisCollection<'a>;
/// Returns the collection of named variation instances.
fn named_instances(&self) -> NamedInstanceCollection<'a>;
/// Returns an iterator over the collection of localized strings for the
/// given informational string identifier.
fn localized_strings(&self, id: StringId) -> LocalizedStrings<'a>;
/// Returns the mapping from glyph identifiers to names.
fn glyph_names(&self) -> GlyphNames<'a>;
/// Returns the global font metrics for the specified size and location in
/// normalized variation space.
fn metrics(&self, size: Size, location: impl Into<LocationRef<'a>>) -> Metrics;
/// Returns the glyph specific metrics for the specified size and location
/// in normalized variation space.
fn glyph_metrics(&self, size: Size, location: impl Into<LocationRef<'a>>) -> GlyphMetrics<'a>;
/// Returns the character to nominal glyph identifier mapping.
fn charmap(&self) -> Charmap<'a>;
/// Returns the collection of scalable glyph outlines.
///
/// If the font contains multiple outline sources, this method prioritizes
/// `glyf`, `CFF2` and `CFF` in that order. To select a specific outline
/// source, use the [`OutlineGlyphCollection::with_format`] method.
fn outline_glyphs(&self) -> OutlineGlyphCollection<'a>;
// Returns a collection of paintable color glyphs.
fn color_glyphs(&self) -> ColorGlyphCollection<'a>;
/// Returns a collection of bitmap strikes.
fn bitmap_strikes(&self) -> BitmapStrikes<'a>;
}
impl<'a> MetadataProvider<'a> for FontRef<'a> {
/// Returns the primary attributes for font classification-- stretch,
/// style and weight.
fn attributes(&self) -> Attributes {
Attributes::new(self)
}
/// Returns the collection of variation axes.
fn axes(&self) -> AxisCollection<'a> {
AxisCollection::new(self)
}
/// Returns the collection of named variation instances.
fn named_instances(&self) -> NamedInstanceCollection<'a> {
NamedInstanceCollection::new(self)
}
/// Returns an iterator over the collection of localized strings for the
/// given informational string identifier.
fn localized_strings(&self, id: StringId) -> LocalizedStrings<'a> {
LocalizedStrings::new(self, id)
}
/// Returns the mapping from glyph identifiers to names.
fn glyph_names(&self) -> GlyphNames<'a> {
GlyphNames::new(self)
}
/// Returns the global font metrics for the specified size and location in
/// normalized variation space.
fn metrics(&self, size: Size, location: impl Into<LocationRef<'a>>) -> Metrics {
Metrics::new(self, size, location)
}
/// Returns the glyph specific metrics for the specified size and location
/// in normalized variation space.
fn glyph_metrics(&self, size: Size, location: impl Into<LocationRef<'a>>) -> GlyphMetrics<'a> {
GlyphMetrics::new(self, size, location)
}
/// Returns the character to nominal glyph identifier mapping.
fn charmap(&self) -> Charmap<'a> {
Charmap::new(self)
}
/// Returns the collection of scalable glyph outlines.
///
/// If the font contains multiple outline sources, this method prioritizes
/// `glyf`, `CFF2` and `CFF` in that order. To select a specific outline
/// source, use the [`OutlineGlyphCollection::with_format`] method.
fn outline_glyphs(&self) -> OutlineGlyphCollection<'a> {
OutlineGlyphCollection::new(self)
}
// Returns a collection of paintable color glyphs.
fn color_glyphs(&self) -> ColorGlyphCollection<'a> {
ColorGlyphCollection::new(self)
}
/// Returns a collection of bitmap strikes.
fn bitmap_strikes(&self) -> BitmapStrikes<'a> {
BitmapStrikes::new(self)
}
}

97
vendor/skrifa/src/setting.rs vendored Normal file
View File

@@ -0,0 +1,97 @@
//! Definitions for specifying variations and typographic features.
use super::Tag;
use core::str::FromStr;
/// Setting defined by a selector tag and an associated value.
///
/// This type is a generic container for properties that can be activated
/// or defined by a `(Tag, T)` pair where the tag selects the target
/// setting and the generic value of type `T` specifies the value for that
/// setting.
///
/// ## Usage
/// Current usage is for specifying variation axis settings (similar to the
/// CSS property [font-variation-settings](https://developer.mozilla.org/en-US/docs/Web/CSS/font-variation-settings)).
/// See [`VariationSetting`].
///
/// In the future, this will likely also be used for specifying feature settings
/// (analogous to the CSS property [font-feature-settings](https://developer.mozilla.org/en-US/docs/Web/CSS/font-feature-settings))
/// for selecting OpenType [features](https://learn.microsoft.com/en-us/typography/opentype/spec/featuretags).
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub struct Setting<T> {
/// Tag that specifies the target setting.
pub selector: Tag,
/// The desired value for the setting.
pub value: T,
}
impl<T> Setting<T> {
/// Creates a new setting from the given selector tag and its associated
/// value.
pub fn new(selector: Tag, value: T) -> Self {
Self { selector, value }
}
}
// This is provided so that &[VariationSetting] can be passed to the
// variation_settings() method of ScalerBuilder.
impl<T: Copy> From<&'_ Setting<T>> for Setting<T> {
fn from(value: &'_ Setting<T>) -> Self {
*value
}
}
impl<T> From<(Tag, T)> for Setting<T> {
fn from(s: (Tag, T)) -> Self {
Self {
selector: s.0,
value: s.1,
}
}
}
impl<T: Copy> From<&(Tag, T)> for Setting<T> {
fn from(s: &(Tag, T)) -> Self {
Self {
selector: s.0,
value: s.1,
}
}
}
impl<T> From<(&str, T)> for Setting<T> {
fn from(s: (&str, T)) -> Self {
Self {
selector: Tag::from_str(s.0).unwrap_or_default(),
value: s.1,
}
}
}
impl<T: Copy> From<&(&str, T)> for Setting<T> {
fn from(s: &(&str, T)) -> Self {
Self {
selector: Tag::from_str(s.0).unwrap_or_default(),
value: s.1,
}
}
}
/// Type for specifying a variation axis setting in user coordinates.
///
/// The `selector` field should contain a tag that corresponds to a
/// variation axis while the `value` field specifies the desired position
/// on the axis in user coordinates (i.e. within the range defined by
/// the minimum and maximum values of the axis).
///
/// # Example
/// ```
/// use skrifa::{Tag, setting::VariationSetting};
///
/// // For convenience, a conversion from (&str, f32) is provided.
/// let slightly_bolder: VariationSetting = ("wght", 720.0).into();
///
/// assert_eq!(slightly_bolder, VariationSetting::new(Tag::new(b"wght"), 720.0));
/// ```
pub type VariationSetting = Setting<f32>;

649
vendor/skrifa/src/string.rs vendored Normal file
View File

@@ -0,0 +1,649 @@
//! Localized strings describing font names and other metadata.
//!
//! This provides higher level interfaces for accessing the data in the
//! OpenType [name](https://learn.microsoft.com/en-us/typography/opentype/spec/name)
//! table.
//!
//! # Example
//! The following function will print all localized strings from the set
//! of predefined identifiers in a font:
//! ```
//! use skrifa::{string::StringId, MetadataProvider};
//!
//! fn print_well_known_strings<'a>(font: &impl MetadataProvider<'a>) {
//! for id in StringId::predefined() {
//! let strings = font.localized_strings(id);
//! if strings.clone().next().is_some() {
//! println!("[{:?}]", id);
//! for string in font.localized_strings(id) {
//! println!("{:?} {}", string.language(), string.to_string());
//! }
//! }
//! }
//! }
//! ```
use read_fonts::{
tables::name::{CharIter, Name, NameRecord, NameString},
FontRef, TableProvider,
};
use core::fmt;
#[doc(inline)]
pub use read_fonts::types::NameId as StringId;
/// Iterator over the characters of a string.
#[derive(Clone)]
pub struct Chars<'a> {
inner: Option<CharIter<'a>>,
}
impl Iterator for Chars<'_> {
type Item = char;
fn next(&mut self) -> Option<Self::Item> {
self.inner.as_mut()?.next()
}
}
/// Iterator over a collection of localized strings for a specific identifier.
#[derive(Clone)]
pub struct LocalizedStrings<'a> {
name: Option<Name<'a>>,
records: core::slice::Iter<'a, NameRecord>,
id: StringId,
}
impl<'a> LocalizedStrings<'a> {
/// Creates a new localized string iterator from the given font and string identifier.
pub fn new(font: &FontRef<'a>, id: StringId) -> Self {
let name = font.name().ok();
let records = name
.as_ref()
.map(|name| name.name_record().iter())
.unwrap_or([].iter());
Self { name, records, id }
}
/// Returns the informational string identifier for this iterator.
pub fn id(&self) -> StringId {
self.id
}
/// Returns the best available English string or the first string in the sequence.
///
/// This prefers the following languages, in order: "en-US", "en",
/// "" (empty, for bare Unicode platform strings which don't have an associated
/// language).
///
/// If none of these are found, returns the first string, or `None` if the sequence
/// is empty.
pub fn english_or_first(self) -> Option<LocalizedString<'a>> {
let mut best_rank = -1;
let mut best_string = None;
for (i, string) in self.enumerate() {
let rank = match (i, string.language()) {
(_, Some("en-US")) => return Some(string),
(_, Some("en")) => 2,
(_, None) => 1,
(0, _) => 0,
_ => continue,
};
if rank > best_rank {
best_rank = rank;
best_string = Some(string);
}
}
best_string
}
}
impl<'a> Iterator for LocalizedStrings<'a> {
type Item = LocalizedString<'a>;
fn next(&mut self) -> Option<Self::Item> {
let name = self.name.as_ref()?;
loop {
let record = self.records.next()?;
if record.name_id() == self.id {
return Some(LocalizedString::new(name, record));
}
}
}
}
impl Default for LocalizedStrings<'_> {
fn default() -> Self {
Self {
name: None,
records: [].iter(),
id: StringId::default(),
}
}
}
/// String containing a name or other font metadata in a specific language.
#[derive(Clone, Debug)]
pub struct LocalizedString<'a> {
language: Option<Language>,
value: Option<NameString<'a>>,
}
impl<'a> LocalizedString<'a> {
pub fn new(name: &Name<'a>, record: &NameRecord) -> Self {
let language = Language::new(name, record);
let value = record.string(name.string_data()).ok();
Self { language, value }
}
/// Returns the BCP-47 language identifier for the localized string.
pub fn language(&self) -> Option<&str> {
self.language.as_ref().map(|language| language.as_str())
}
/// Returns an iterator over the characters of the localized string.
pub fn chars(&self) -> Chars<'a> {
Chars {
inner: self.value.map(|value| value.chars()),
}
}
}
impl fmt::Display for LocalizedString<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for ch in self.chars() {
ch.fmt(f)?;
}
Ok(())
}
}
/// This value is chosen arbitrarily to accommodate common language tags that
/// are almost always <= 11 bytes (LLL-SSSS-RR where L is primary language, S
/// is script and R is region) and to keep the Language enum at a reasonable
/// 32 bytes in size.
const MAX_INLINE_LANGUAGE_LEN: usize = 30;
#[derive(Copy, Clone, Debug)]
#[repr(u8)]
enum Language {
Inline {
buf: [u8; MAX_INLINE_LANGUAGE_LEN],
len: u8,
},
Static(&'static str),
}
impl Language {
fn new(name: &Name, record: &NameRecord) -> Option<Self> {
let language_id = record.language_id();
// For version 1 name tables, prefer language tags:
// https://learn.microsoft.com/en-us/typography/opentype/spec/name#naming-table-version-1
const BASE_LANGUAGE_TAG_ID: u16 = 0x8000;
if name.version() == 1 && language_id >= BASE_LANGUAGE_TAG_ID {
let index = (language_id - BASE_LANGUAGE_TAG_ID) as usize;
let language_string = name
.lang_tag_record()?
.get(index)?
.lang_tag(name.string_data())
.ok()?;
Self::from_name_string(&language_string)
} else {
match record.platform_id() {
// We only match Macintosh and Windows language ids.
1 | 3 => Self::from_language_id(language_id),
_ => None,
}
}
}
/// Decodes a language tag string into an inline ASCII byte sequence.
fn from_name_string(s: &NameString) -> Option<Self> {
let mut buf = [0u8; MAX_INLINE_LANGUAGE_LEN];
let mut len = 0;
for ch in s.chars() {
// From "Tags for Identifying Languages" <https://www.rfc-editor.org/rfc/rfc5646.html#page-6>:
// "Although [RFC5234] refers to octets, the language tags described in
// this document are sequences of characters from the US-ASCII [ISO646]
// repertoire"
// Therefore we assume that non-ASCII characters signal an invalid language tag.
if !ch.is_ascii() || len == MAX_INLINE_LANGUAGE_LEN {
return None;
}
buf[len] = ch as u8;
len += 1;
}
Some(Self::Inline {
buf,
len: len as u8,
})
}
fn from_language_id(language_id: u16) -> Option<Self> {
Some(Self::Static(language_id_to_bcp47(language_id)?))
}
fn as_str(&self) -> &str {
match self {
Self::Inline { buf: data, len } => {
let data = &data[..*len as usize];
core::str::from_utf8(data).unwrap_or_default()
}
Self::Static(str) => str,
}
}
}
/// Converts an OpenType language identifier to a BCP-47 language tag.
fn language_id_to_bcp47(language_id: u16) -> Option<&'static str> {
match LANGUAGE_ID_TO_BCP47.binary_search_by(|entry| entry.0.cmp(&language_id)) {
Ok(ix) => LANGUAGE_ID_TO_BCP47.get(ix).map(|entry| entry.1),
_ => None,
}
}
/// Mapping of OpenType name table language identifier to BCP-47 language tag.
/// Borrowed from Skia: <https://skia.googlesource.com/skia/+/refs/heads/main/src/sfnt/SkOTTable_name.cpp#98>
const LANGUAGE_ID_TO_BCP47: &[(u16, &str)] = &[
/* A mapping from Mac Language Designators to BCP 47 codes.
* The following list was constructed more or less manually.
* Apple now uses BCP 47 (post OSX10.4), so there will be no new entries.
*/
(0, "en"), //English
(1, "fr"), //French
(2, "de"), //German
(3, "it"), //Italian
(4, "nl"), //Dutch
(5, "sv"), //Swedish
(6, "es"), //Spanish
(7, "da"), //Danish
(8, "pt"), //Portuguese
(9, "nb"), //Norwegian
(10, "he"), //Hebrew
(11, "ja"), //Japanese
(12, "ar"), //Arabic
(13, "fi"), //Finnish
(14, "el"), //Greek
(15, "is"), //Icelandic
(16, "mt"), //Maltese
(17, "tr"), //Turkish
(18, "hr"), //Croatian
(19, "zh-Hant"), //Chinese (Traditional)
(20, "ur"), //Urdu
(21, "hi"), //Hindi
(22, "th"), //Thai
(23, "ko"), //Korean
(24, "lt"), //Lithuanian
(25, "pl"), //Polish
(26, "hu"), //Hungarian
(27, "et"), //Estonian
(28, "lv"), //Latvian
(29, "se"), //Sami
(30, "fo"), //Faroese
(31, "fa"), //Farsi (Persian)
(32, "ru"), //Russian
(33, "zh-Hans"), //Chinese (Simplified)
(34, "nl"), //Dutch
(35, "ga"), //Irish(Gaelic)
(36, "sq"), //Albanian
(37, "ro"), //Romanian
(38, "cs"), //Czech
(39, "sk"), //Slovak
(40, "sl"), //Slovenian
(41, "yi"), //Yiddish
(42, "sr"), //Serbian
(43, "mk"), //Macedonian
(44, "bg"), //Bulgarian
(45, "uk"), //Ukrainian
(46, "be"), //Byelorussian
(47, "uz"), //Uzbek
(48, "kk"), //Kazakh
(49, "az-Cyrl"), //Azerbaijani (Cyrillic)
(50, "az-Arab"), //Azerbaijani (Arabic)
(51, "hy"), //Armenian
(52, "ka"), //Georgian
(53, "mo"), //Moldavian
(54, "ky"), //Kirghiz
(55, "tg"), //Tajiki
(56, "tk"), //Turkmen
(57, "mn-Mong"), //Mongolian (Traditional)
(58, "mn-Cyrl"), //Mongolian (Cyrillic)
(59, "ps"), //Pashto
(60, "ku"), //Kurdish
(61, "ks"), //Kashmiri
(62, "sd"), //Sindhi
(63, "bo"), //Tibetan
(64, "ne"), //Nepali
(65, "sa"), //Sanskrit
(66, "mr"), //Marathi
(67, "bn"), //Bengali
(68, "as"), //Assamese
(69, "gu"), //Gujarati
(70, "pa"), //Punjabi
(71, "or"), //Oriya
(72, "ml"), //Malayalam
(73, "kn"), //Kannada
(74, "ta"), //Tamil
(75, "te"), //Telugu
(76, "si"), //Sinhalese
(77, "my"), //Burmese
(78, "km"), //Khmer
(79, "lo"), //Lao
(80, "vi"), //Vietnamese
(81, "id"), //Indonesian
(82, "tl"), //Tagalog
(83, "ms-Latn"), //Malay (Roman)
(84, "ms-Arab"), //Malay (Arabic)
(85, "am"), //Amharic
(86, "ti"), //Tigrinya
(87, "om"), //Oromo
(88, "so"), //Somali
(89, "sw"), //Swahili
(90, "rw"), //Kinyarwanda/Ruanda
(91, "rn"), //Rundi
(92, "ny"), //Nyanja/Chewa
(93, "mg"), //Malagasy
(94, "eo"), //Esperanto
(128, "cy"), //Welsh
(129, "eu"), //Basque
(130, "ca"), //Catalan
(131, "la"), //Latin
(132, "qu"), //Quechua
(133, "gn"), //Guarani
(134, "ay"), //Aymara
(135, "tt"), //Tatar
(136, "ug"), //Uighur
(137, "dz"), //Dzongkha
(138, "jv-Latn"), //Javanese (Roman)
(139, "su-Latn"), //Sundanese (Roman)
(140, "gl"), //Galician
(141, "af"), //Afrikaans
(142, "br"), //Breton
(143, "iu"), //Inuktitut
(144, "gd"), //Scottish (Gaelic)
(145, "gv"), //Manx (Gaelic)
(146, "ga"), //Irish (Gaelic with Lenition)
(147, "to"), //Tongan
(148, "el"), //Greek (Polytonic) Note: ISO 15924 does not have an equivalent script name.
(149, "kl"), //Greenlandic
(150, "az-Latn"), //Azerbaijani (Roman)
(151, "nn"), //Nynorsk
/* A mapping from Windows LCID to BCP 47 codes.
* This list is the sorted, curated output of tools/win_lcid.cpp.
* Note that these are sorted by value for quick binary lookup, and not logically by lsb.
* The 'bare' language ids (e.g. 0x0001 for Arabic) are omitted
* as they do not appear as valid language ids in the OpenType specification.
*/
(0x0401, "ar-SA"), //Arabic
(0x0402, "bg-BG"), //Bulgarian
(0x0403, "ca-ES"), //Catalan
(0x0404, "zh-TW"), //Chinese (Traditional)
(0x0405, "cs-CZ"), //Czech
(0x0406, "da-DK"), //Danish
(0x0407, "de-DE"), //German
(0x0408, "el-GR"), //Greek
(0x0409, "en-US"), //English
(0x040a, "es-ES_tradnl"), //Spanish
(0x040b, "fi-FI"), //Finnish
(0x040c, "fr-FR"), //French
(0x040d, "he-IL"), //Hebrew
(0x040d, "he"), //Hebrew
(0x040e, "hu-HU"), //Hungarian
(0x040e, "hu"), //Hungarian
(0x040f, "is-IS"), //Icelandic
(0x0410, "it-IT"), //Italian
(0x0411, "ja-JP"), //Japanese
(0x0412, "ko-KR"), //Korean
(0x0413, "nl-NL"), //Dutch
(0x0414, "nb-NO"), //Norwegian (Bokmål)
(0x0415, "pl-PL"), //Polish
(0x0416, "pt-BR"), //Portuguese
(0x0417, "rm-CH"), //Romansh
(0x0418, "ro-RO"), //Romanian
(0x0419, "ru-RU"), //Russian
(0x041a, "hr-HR"), //Croatian
(0x041b, "sk-SK"), //Slovak
(0x041c, "sq-AL"), //Albanian
(0x041d, "sv-SE"), //Swedish
(0x041e, "th-TH"), //Thai
(0x041f, "tr-TR"), //Turkish
(0x0420, "ur-PK"), //Urdu
(0x0421, "id-ID"), //Indonesian
(0x0422, "uk-UA"), //Ukrainian
(0x0423, "be-BY"), //Belarusian
(0x0424, "sl-SI"), //Slovenian
(0x0425, "et-EE"), //Estonian
(0x0426, "lv-LV"), //Latvian
(0x0427, "lt-LT"), //Lithuanian
(0x0428, "tg-Cyrl-TJ"), //Tajik (Cyrillic)
(0x0429, "fa-IR"), //Persian
(0x042a, "vi-VN"), //Vietnamese
(0x042b, "hy-AM"), //Armenian
(0x042c, "az-Latn-AZ"), //Azeri (Latin)
(0x042d, "eu-ES"), //Basque
(0x042e, "hsb-DE"), //Upper Sorbian
(0x042f, "mk-MK"), //Macedonian (FYROM)
(0x0432, "tn-ZA"), //Setswana
(0x0434, "xh-ZA"), //isiXhosa
(0x0435, "zu-ZA"), //isiZulu
(0x0436, "af-ZA"), //Afrikaans
(0x0437, "ka-GE"), //Georgian
(0x0438, "fo-FO"), //Faroese
(0x0439, "hi-IN"), //Hindi
(0x043a, "mt-MT"), //Maltese
(0x043b, "se-NO"), //Sami (Northern)
(0x043e, "ms-MY"), //Malay
(0x043f, "kk-KZ"), //Kazakh
(0x0440, "ky-KG"), //Kyrgyz
(0x0441, "sw-KE"), //Kiswahili
(0x0442, "tk-TM"), //Turkmen
(0x0443, "uz-Latn-UZ"), //Uzbek (Latin)
(0x0443, "uz"), //Uzbek
(0x0444, "tt-RU"), //Tatar
(0x0445, "bn-IN"), //Bengali
(0x0446, "pa-IN"), //Punjabi
(0x0447, "gu-IN"), //Gujarati
(0x0448, "or-IN"), //Oriya
(0x0449, "ta-IN"), //Tamil
(0x044a, "te-IN"), //Telugu
(0x044b, "kn-IN"), //Kannada
(0x044c, "ml-IN"), //Malayalam
(0x044d, "as-IN"), //Assamese
(0x044e, "mr-IN"), //Marathi
(0x044f, "sa-IN"), //Sanskrit
(0x0450, "mn-Cyrl"), //Mongolian (Cyrillic)
(0x0451, "bo-CN"), //Tibetan
(0x0452, "cy-GB"), //Welsh
(0x0453, "km-KH"), //Khmer
(0x0454, "lo-LA"), //Lao
(0x0456, "gl-ES"), //Galician
(0x0457, "kok-IN"), //Konkani
(0x045a, "syr-SY"), //Syriac
(0x045b, "si-LK"), //Sinhala
(0x045d, "iu-Cans-CA"), //Inuktitut (Syllabics)
(0x045e, "am-ET"), //Amharic
(0x0461, "ne-NP"), //Nepali
(0x0462, "fy-NL"), //Frisian
(0x0463, "ps-AF"), //Pashto
(0x0464, "fil-PH"), //Filipino
(0x0465, "dv-MV"), //Divehi
(0x0468, "ha-Latn-NG"), //Hausa (Latin)
(0x046a, "yo-NG"), //Yoruba
(0x046b, "quz-BO"), //Quechua
(0x046c, "nso-ZA"), //Sesotho sa Leboa
(0x046d, "ba-RU"), //Bashkir
(0x046e, "lb-LU"), //Luxembourgish
(0x046f, "kl-GL"), //Greenlandic
(0x0470, "ig-NG"), //Igbo
(0x0478, "ii-CN"), //Yi
(0x047a, "arn-CL"), //Mapudungun
(0x047c, "moh-CA"), //Mohawk
(0x047e, "br-FR"), //Breton
(0x0480, "ug-CN"), //Uyghur
(0x0481, "mi-NZ"), //Maori
(0x0482, "oc-FR"), //Occitan
(0x0483, "co-FR"), //Corsican
(0x0484, "gsw-FR"), //Alsatian
(0x0485, "sah-RU"), //Yakut
(0x0486, "qut-GT"), //K'iche
(0x0487, "rw-RW"), //Kinyarwanda
(0x0488, "wo-SN"), //Wolof
(0x048c, "prs-AF"), //Dari
(0x0491, "gd-GB"), //Scottish Gaelic
(0x0801, "ar-IQ"), //Arabic
(0x0804, "zh-Hans"), //Chinese (Simplified)
(0x0807, "de-CH"), //German
(0x0809, "en-GB"), //English
(0x080a, "es-MX"), //Spanish
(0x080c, "fr-BE"), //French
(0x0810, "it-CH"), //Italian
(0x0813, "nl-BE"), //Dutch
(0x0814, "nn-NO"), //Norwegian (Nynorsk)
(0x0816, "pt-PT"), //Portuguese
(0x081a, "sr-Latn-CS"), //Serbian (Latin)
(0x081d, "sv-FI"), //Swedish
(0x082c, "az-Cyrl-AZ"), //Azeri (Cyrillic)
(0x082e, "dsb-DE"), //Lower Sorbian
(0x082e, "dsb"), //Lower Sorbian
(0x083b, "se-SE"), //Sami (Northern)
(0x083c, "ga-IE"), //Irish
(0x083e, "ms-BN"), //Malay
(0x0843, "uz-Cyrl-UZ"), //Uzbek (Cyrillic)
(0x0845, "bn-BD"), //Bengali
(0x0850, "mn-Mong-CN"), //Mongolian (Traditional Mongolian)
(0x085d, "iu-Latn-CA"), //Inuktitut (Latin)
(0x085f, "tzm-Latn-DZ"), //Tamazight (Latin)
(0x086b, "quz-EC"), //Quechua
(0x0c01, "ar-EG"), //Arabic
(0x0c04, "zh-Hant"), //Chinese (Traditional)
(0x0c07, "de-AT"), //German
(0x0c09, "en-AU"), //English
(0x0c0a, "es-ES"), //Spanish
(0x0c0c, "fr-CA"), //French
(0x0c1a, "sr-Cyrl-CS"), //Serbian (Cyrillic)
(0x0c3b, "se-FI"), //Sami (Northern)
(0x0c6b, "quz-PE"), //Quechua
(0x1001, "ar-LY"), //Arabic
(0x1004, "zh-SG"), //Chinese (Simplified)
(0x1007, "de-LU"), //German
(0x1009, "en-CA"), //English
(0x100a, "es-GT"), //Spanish
(0x100c, "fr-CH"), //French
(0x101a, "hr-BA"), //Croatian (Latin)
(0x103b, "smj-NO"), //Sami (Lule)
(0x1401, "ar-DZ"), //Arabic
(0x1404, "zh-MO"), //Chinese (Traditional)
(0x1407, "de-LI"), //German
(0x1409, "en-NZ"), //English
(0x140a, "es-CR"), //Spanish
(0x140c, "fr-LU"), //French
(0x141a, "bs-Latn-BA"), //Bosnian (Latin)
(0x141a, "bs"), //Bosnian
(0x143b, "smj-SE"), //Sami (Lule)
(0x143b, "smj"), //Sami (Lule)
(0x1801, "ar-MA"), //Arabic
(0x1809, "en-IE"), //English
(0x180a, "es-PA"), //Spanish
(0x180c, "fr-MC"), //French
(0x181a, "sr-Latn-BA"), //Serbian (Latin)
(0x183b, "sma-NO"), //Sami (Southern)
(0x1c01, "ar-TN"), //Arabic
(0x1c09, "en-ZA"), //English
(0x1c0a, "es-DO"), //Spanish
(0x1c1a, "sr-Cyrl-BA"), //Serbian (Cyrillic)
(0x1c3b, "sma-SE"), //Sami (Southern)
(0x1c3b, "sma"), //Sami (Southern)
(0x2001, "ar-OM"), //Arabic
(0x2009, "en-JM"), //English
(0x200a, "es-VE"), //Spanish
(0x201a, "bs-Cyrl-BA"), //Bosnian (Cyrillic)
(0x201a, "bs-Cyrl"), //Bosnian (Cyrillic)
(0x203b, "sms-FI"), //Sami (Skolt)
(0x203b, "sms"), //Sami (Skolt)
(0x2401, "ar-YE"), //Arabic
(0x2409, "en-029"), //English
(0x240a, "es-CO"), //Spanish
(0x241a, "sr-Latn-RS"), //Serbian (Latin)
(0x243b, "smn-FI"), //Sami (Inari)
(0x2801, "ar-SY"), //Arabic
(0x2809, "en-BZ"), //English
(0x280a, "es-PE"), //Spanish
(0x281a, "sr-Cyrl-RS"), //Serbian (Cyrillic)
(0x2c01, "ar-JO"), //Arabic
(0x2c09, "en-TT"), //English
(0x2c0a, "es-AR"), //Spanish
(0x2c1a, "sr-Latn-ME"), //Serbian (Latin)
(0x3001, "ar-LB"), //Arabic
(0x3009, "en-ZW"), //English
(0x300a, "es-EC"), //Spanish
(0x301a, "sr-Cyrl-ME"), //Serbian (Cyrillic)
(0x3401, "ar-KW"), //Arabic
(0x3409, "en-PH"), //English
(0x340a, "es-CL"), //Spanish
(0x3801, "ar-AE"), //Arabic
(0x380a, "es-UY"), //Spanish
(0x3c01, "ar-BH"), //Arabic
(0x3c0a, "es-PY"), //Spanish
(0x4001, "ar-QA"), //Arabic
(0x4009, "en-IN"), //English
(0x400a, "es-BO"), //Spanish
(0x4409, "en-MY"), //English
(0x440a, "es-SV"), //Spanish
(0x4809, "en-SG"), //English
(0x480a, "es-HN"), //Spanish
(0x4c0a, "es-NI"), //Spanish
(0x500a, "es-PR"), //Spanish
(0x540a, "es-US"), //Spanish
];
#[cfg(test)]
mod tests {
use crate::MetadataProvider;
use super::*;
use read_fonts::FontRef;
#[test]
fn localized() {
let font = FontRef::new(font_test_data::NAMES_ONLY).unwrap();
let mut subfamily_names = font
.localized_strings(StringId::SUBFAMILY_NAME)
.map(|s| (s.language().unwrap().to_string(), s.to_string()))
.collect::<Vec<_>>();
subfamily_names.sort_by(|a, b| a.0.cmp(&b.0));
let expected = [
(String::from("ar-SA"), String::from("عادي")),
(String::from("el-GR"), String::from("Κανονικά")),
(String::from("en"), String::from("Regular")),
(String::from("eu-ES"), String::from("Arrunta")),
(String::from("pl-PL"), String::from("Normalny")),
(String::from("zh-Hans"), String::from("正常")),
];
assert_eq!(subfamily_names.as_slice(), expected);
}
#[test]
fn find_by_language() {
let font = FontRef::new(font_test_data::NAMES_ONLY).unwrap();
assert_eq!(
font.localized_strings(StringId::SUBFAMILY_NAME)
.find(|s| s.language() == Some("pl-PL"))
.unwrap()
.to_string(),
"Normalny"
);
}
#[test]
fn english_or_first() {
let font = FontRef::new(font_test_data::NAMES_ONLY).unwrap();
assert_eq!(
font.localized_strings(StringId::SUBFAMILY_NAME)
.english_or_first()
.unwrap()
.to_string(),
"Regular"
);
}
}

519
vendor/skrifa/src/variation.rs vendored Normal file
View File

@@ -0,0 +1,519 @@
//! Axes of variation in a variable font.
use read_fonts::{
tables::avar::Avar,
tables::fvar::{self, Fvar},
types::{Fixed, Tag},
FontRef, TableProvider,
};
use crate::{
collections::SmallVec,
instance::{Location, NormalizedCoord},
setting::VariationSetting,
string::StringId,
};
/// Axis of variation in a variable font.
///
/// In variable fonts, an axis usually refers to a single aspect of a
/// typeface's design that can be altered by the user.
///
/// See <https://fonts.google.com/knowledge/glossary/axis_in_variable_fonts>
#[derive(Clone)]
pub struct Axis {
index: usize,
record: fvar::VariationAxisRecord,
}
impl Axis {
/// Returns the tag that identifies the axis.
pub fn tag(&self) -> Tag {
self.record.axis_tag()
}
/// Returns the index of the axis in its owning collection.
pub fn index(&self) -> usize {
self.index
}
/// Returns the localized string identifier for the name of the axis.
pub fn name_id(&self) -> StringId {
self.record.axis_name_id()
}
/// Returns true if the axis should be hidden in user interfaces.
pub fn is_hidden(&self) -> bool {
const AXIS_HIDDEN_FLAG: u16 = 0x1;
self.record.flags() & AXIS_HIDDEN_FLAG != 0
}
/// Returns the minimum value of the axis.
pub fn min_value(&self) -> f32 {
self.record.min_value().to_f64() as _
}
/// Returns the default value of the axis.
pub fn default_value(&self) -> f32 {
self.record.default_value().to_f64() as _
}
/// Returns the maximum value of the axis.
pub fn max_value(&self) -> f32 {
self.record.max_value().to_f64() as _
}
/// Returns a normalized coordinate for the given user coordinate.
///
/// The value will be clamped to the range specified by the minimum
/// and maximum values.
///
/// This does not apply any axis variation remapping.
pub fn normalize(&self, coord: f32) -> NormalizedCoord {
self.record
.normalize(Fixed::from_f64(coord as _))
.to_f2dot14()
}
}
/// Collection of axes in a variable font.
///
/// Converts user ([fvar](https://learn.microsoft.com/en-us/typography/opentype/spec/fvar))
/// locations to normalized locations. See [`Self::location`].
///
/// See the [`Axis`] type for more detail.
#[derive(Clone)]
pub struct AxisCollection<'a> {
fvar: Option<Fvar<'a>>,
avar: Option<Avar<'a>>,
}
impl<'a> AxisCollection<'a> {
/// Creates a new axis collection from the given font.
pub fn new(font: &FontRef<'a>) -> Self {
let fvar = font.fvar().ok();
let avar = font.avar().ok();
Self { fvar, avar }
}
/// Returns the number of variation axes in the font.
pub fn len(&self) -> usize {
self.fvar
.as_ref()
.map(|fvar| fvar.axis_count() as usize)
.unwrap_or(0)
}
/// Returns true if the collection is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the axis at the given index.
pub fn get(&self, index: usize) -> Option<Axis> {
let record = *self.fvar.as_ref()?.axes().ok()?.get(index)?;
Some(Axis { index, record })
}
/// Returns the axis with the given tag.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let opsz = Tag::new(b"opsz");
/// assert_eq!(font.axes().get_by_tag(opsz).unwrap().tag(), opsz);
/// # }
/// ```
pub fn get_by_tag(&self, tag: Tag) -> Option<Axis> {
self.iter().find(|axis| axis.tag() == tag)
}
/// Given an iterator of variation settings in user space, computes an
/// ordered sequence of normalized coordinates.
///
/// * Setting selectors that don't match an axis are ignored.
/// * Setting values are clamped to the range of their associated axis
/// before normalization.
/// * If more than one setting for an axis is provided, the last one is
/// used.
/// * Omitted settings are set to 0.0, representing the default position
/// in variation space.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let location = font.axes().location([("wght", 250.0), ("wdth", 75.0)]);
/// # }
/// ```
pub fn location<I>(&self, settings: I) -> Location
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
let mut location = Location::new(self.len());
self.location_to_slice(settings, location.coords_mut());
location
}
/// Given an iterator of variation settings in user space, computes an
/// ordered sequence of normalized coordinates and stores them in the
/// target slice.
///
/// * Setting selectors that don't match an axis are ignored.
/// * Setting values are clamped to the range of their associated axis
/// before normalization.
/// * If more than one setting for an axis is provided, the last one is
/// used.
/// * If no setting for an axis is provided, the associated coordinate is
/// set to the normalized value 0.0, representing the default position
/// in variation space.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let axes = font.axes();
/// let mut location = vec![NormalizedCoord::default(); axes.len()];
/// axes.location_to_slice([("wght", 250.0), ("wdth", 75.0)], &mut location);
/// # }
/// ```
pub fn location_to_slice<I>(&self, settings: I, location: &mut [NormalizedCoord])
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
if let Some(fvar) = self.fvar.as_ref() {
fvar.user_to_normalized(
self.avar.as_ref(),
settings
.into_iter()
.map(|setting| setting.into())
.map(|setting| (setting.selector, Fixed::from_f64(setting.value as f64))),
location,
);
} else {
location.fill(NormalizedCoord::default());
}
}
/// Given an iterator of variation settings in user space, returns a
/// new iterator yielding those settings that are valid for this axis
/// collection.
///
/// * Setting selectors that don't match an axis are dropped.
/// * If more than one setting for an axis is provided, the last one is
/// retained.
/// * Setting values are clamped to the range of their associated axis.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// // Assuming a font contains a single "wght" (weight) axis with range
/// // 100-900:
/// let axes = font.axes();
/// let filtered: Vec<_> = axes
/// .filter([("wght", 400.0), ("opsz", 100.0), ("wght", 1200.0)])
/// .collect();
/// // The first "wght" and "opsz" settings are dropped and the final
/// // "wght" axis is clamped to the maximum value of 900.
/// assert_eq!(&filtered, &[("wght", 900.0).into()]);
/// # }
/// ```
pub fn filter<I>(&self, settings: I) -> impl Iterator<Item = VariationSetting> + Clone
where
I: IntoIterator,
I::Item: Into<VariationSetting>,
{
#[derive(Copy, Clone, Default)]
struct Entry {
tag: Tag,
min: f32,
max: f32,
value: f32,
present: bool,
}
let mut results = SmallVec::<_, 8>::with_len(self.len(), Entry::default());
for (axis, result) in self.iter().zip(results.as_mut_slice()) {
result.tag = axis.tag();
result.min = axis.min_value();
result.max = axis.max_value();
result.value = axis.default_value();
}
for setting in settings {
let setting = setting.into();
for entry in results.as_mut_slice() {
if entry.tag == setting.selector {
entry.value = setting.value.max(entry.min).min(entry.max);
entry.present = true;
}
}
}
results
.into_iter()
.filter(|entry| entry.present)
.map(|entry| VariationSetting::new(entry.tag, entry.value))
}
/// Returns an iterator over the axes in the collection.
pub fn iter(&self) -> impl Iterator<Item = Axis> + 'a + Clone {
let copy = self.clone();
(0..self.len()).filter_map(move |i| copy.get(i))
}
}
/// Named instance of a variation.
///
/// A set of fixed axis positions selected by the type designer and assigned a
/// name.
///
/// See <https://fonts.google.com/knowledge/glossary/instance>
#[derive(Clone)]
pub struct NamedInstance<'a> {
axes: AxisCollection<'a>,
record: fvar::InstanceRecord<'a>,
}
impl<'a> NamedInstance<'a> {
/// Returns the string identifier for the subfamily name of the instance.
pub fn subfamily_name_id(&self) -> StringId {
self.record.subfamily_name_id
}
/// Returns the string identifier for the PostScript name of the instance.
pub fn postscript_name_id(&self) -> Option<StringId> {
self.record.post_script_name_id
}
/// Returns an iterator over the ordered sequence of user space coordinates
/// that define the instance, one coordinate per axis.
pub fn user_coords(&self) -> impl Iterator<Item = f32> + 'a + Clone {
self.record
.coordinates
.iter()
.map(|coord| coord.get().to_f64() as _)
}
/// Computes a location in normalized variation space for this instance.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let location = font.named_instances().get(0).unwrap().location();
/// # }
/// ```
pub fn location(&self) -> Location {
let mut location = Location::new(self.axes.len());
self.location_to_slice(location.coords_mut());
location
}
/// Computes a location in normalized variation space for this instance and
/// stores the result in the given slice.
///
/// # Examples
///
/// ```rust
/// # use skrifa::prelude::*;
/// # fn wrapper(font: &FontRef) {
/// let instance = font.named_instances().get(0).unwrap();
/// let mut location = vec![NormalizedCoord::default(); instance.user_coords().count()];
/// instance.location_to_slice(&mut location);
/// # }
/// ```
pub fn location_to_slice(&self, location: &mut [NormalizedCoord]) {
let settings = self
.axes
.iter()
.map(|axis| axis.tag())
.zip(self.user_coords());
self.axes.location_to_slice(settings, location);
}
}
/// Collection of named instances in a variable font.
///
/// See the [`NamedInstance`] type for more detail.
#[derive(Clone)]
pub struct NamedInstanceCollection<'a> {
axes: AxisCollection<'a>,
}
impl<'a> NamedInstanceCollection<'a> {
/// Creates a new instance collection from the given font.
pub fn new(font: &FontRef<'a>) -> Self {
Self {
axes: AxisCollection::new(font),
}
}
/// Returns the number of instances in the collection.
pub fn len(&self) -> usize {
self.axes
.fvar
.as_ref()
.map(|fvar| fvar.instance_count() as usize)
.unwrap_or(0)
}
/// Returns true if the collection is empty.
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Returns the instance at the given index.
pub fn get(&self, index: usize) -> Option<NamedInstance<'a>> {
let record = self.axes.fvar.as_ref()?.instances().ok()?.get(index).ok()?;
Some(NamedInstance {
axes: self.axes.clone(),
record,
})
}
/// Returns an iterator over the instances in the collection.
pub fn iter(&self) -> impl Iterator<Item = NamedInstance<'a>> + 'a + Clone {
let copy = self.clone();
(0..self.len()).filter_map(move |i| copy.get(i))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::MetadataProvider as _;
use font_test_data::VAZIRMATN_VAR;
use read_fonts::FontRef;
use std::str::FromStr;
#[test]
fn axis() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
let axis = font.axes().get(0).unwrap();
assert_eq!(axis.index(), 0);
assert_eq!(axis.tag(), Tag::new(b"wght"));
assert_eq!(axis.min_value(), 100.0);
assert_eq!(axis.default_value(), 400.0);
assert_eq!(axis.max_value(), 900.0);
assert_eq!(axis.name_id(), StringId::new(257));
assert_eq!(
font.localized_strings(axis.name_id())
.english_or_first()
.unwrap()
.to_string(),
"Weight"
);
}
#[test]
fn named_instances() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
let named_instances = font.named_instances();
let thin = named_instances.get(0).unwrap();
assert_eq!(thin.subfamily_name_id(), StringId::new(258));
assert_eq!(
font.localized_strings(thin.subfamily_name_id())
.english_or_first()
.unwrap()
.to_string(),
"Thin"
);
assert_eq!(thin.location().coords(), &[NormalizedCoord::from_f32(-1.0)]);
let regular = named_instances.get(3).unwrap();
assert_eq!(regular.subfamily_name_id(), StringId::new(261));
assert_eq!(
font.localized_strings(regular.subfamily_name_id())
.english_or_first()
.unwrap()
.to_string(),
"Regular"
);
assert_eq!(
regular.location().coords(),
&[NormalizedCoord::from_f32(0.0)]
);
let bold = named_instances.get(6).unwrap();
assert_eq!(bold.subfamily_name_id(), StringId::new(264));
assert_eq!(
font.localized_strings(bold.subfamily_name_id())
.english_or_first()
.unwrap()
.to_string(),
"Bold"
);
assert_eq!(
bold.location().coords(),
&[NormalizedCoord::from_f32(0.6776123)]
);
}
#[test]
fn location() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
let axes = font.axes();
let axis = axes.get_by_tag(Tag::from_str("wght").unwrap()).unwrap();
assert_eq!(
axes.location([("wght", -1000.0)]).coords(),
&[NormalizedCoord::from_f32(-1.0)]
);
assert_eq!(
axes.location([("wght", 100.0)]).coords(),
&[NormalizedCoord::from_f32(-1.0)]
);
assert_eq!(
axes.location([("wght", 200.0)]).coords(),
&[NormalizedCoord::from_f32(-0.5)]
);
assert_eq!(
axes.location([("wght", 400.0)]).coords(),
&[NormalizedCoord::from_f32(0.0)]
);
// avar table maps 0.8 to 0.83875
assert_eq!(
axes.location(&[(
"wght",
axis.default_value() + (axis.max_value() - axis.default_value()) * 0.8,
)])
.coords(),
&[NormalizedCoord::from_f32(0.83875)]
);
assert_eq!(
axes.location([("wght", 900.0)]).coords(),
&[NormalizedCoord::from_f32(1.0)]
);
assert_eq!(
axes.location([("wght", 1251.5)]).coords(),
&[NormalizedCoord::from_f32(1.0)]
);
}
#[test]
fn filter() {
let font = FontRef::from_index(VAZIRMATN_VAR, 0).unwrap();
// This font contains one wght axis with the range 100-900 and default
// value of 400.
let axes = font.axes();
// Drop axes that are not present in the font
let drop_missing: Vec<_> = axes.filter(&[("slnt", 25.0), ("wdth", 50.0)]).collect();
assert_eq!(&drop_missing, &[]);
// Clamp an out of range value
let clamp: Vec<_> = axes.filter(&[("wght", 50.0)]).collect();
assert_eq!(&clamp, &[("wght", 100.0).into()]);
// Combination of the above two: drop the missing axis and clamp out of range value
let drop_missing_and_clamp: Vec<_> =
axes.filter(&[("slnt", 25.0), ("wght", 1000.0)]).collect();
assert_eq!(&drop_missing_and_clamp, &[("wght", 900.0).into()]);
// Ensure we take the later value in the case of duplicates
let drop_duplicate_and_missing: Vec<_> = axes
.filter(&[("wght", 400.0), ("opsz", 100.0), ("wght", 120.5)])
.collect();
assert_eq!(&drop_duplicate_and_missing, &[("wght", 120.5).into()]);
}
}