1 //! CFF hinting.
2
3 use read_fonts::{
4 tables::postscript::{charstring::CommandSink, dict::Blues},
5 types::Fixed,
6 };
7
8 // "Default values for OS/2 typoAscender/Descender.."
9 // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L98>
10 const ICF_TOP: Fixed = Fixed::from_i32(880);
11 const ICF_BOTTOM: Fixed = Fixed::from_i32(-120);
12
13 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L141>
14 const MAX_BLUES: usize = 7;
15 const MAX_OTHER_BLUES: usize = 5;
16 const MAX_BLUE_ZONES: usize = MAX_BLUES + MAX_OTHER_BLUES;
17
18 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L47>
19 const MAX_HINTS: usize = 96;
20
21 // One bit per stem hint
22 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L80>
23 const HINT_MASK_SIZE: usize = (MAX_HINTS + 7) / 8;
24
25 // Constant for hint adjustment and em box hint placement.
26 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L114>
27 const MIN_COUNTER: Fixed = Fixed::from_bits(0x8000);
28
29 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psfixed.h#L55>
30 const EPSILON: Fixed = Fixed::from_bits(1);
31
32 /// Parameters used to generate the stem and counter zones for the hinting
33 /// algorithm.
34 #[derive(Clone)]
35 pub(crate) struct HintParams {
36 pub blues: Blues,
37 pub family_blues: Blues,
38 pub other_blues: Blues,
39 pub family_other_blues: Blues,
40 pub blue_scale: Fixed,
41 pub blue_shift: Fixed,
42 pub blue_fuzz: Fixed,
43 pub language_group: i32,
44 }
45
46 impl Default for HintParams {
default() -> Self47 fn default() -> Self {
48 Self {
49 blues: Blues::default(),
50 other_blues: Blues::default(),
51 family_blues: Blues::default(),
52 family_other_blues: Blues::default(),
53 // See <https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#table-16-private-dict-operators>
54 blue_scale: Fixed::from_f64(0.039625),
55 blue_shift: Fixed::from_i32(7),
56 blue_fuzz: Fixed::ONE,
57 language_group: 0,
58 }
59 }
60 }
61
62 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L129>
63 #[derive(Copy, Clone, PartialEq, Default, Debug)]
64 struct BlueZone {
65 is_bottom: bool,
66 cs_bottom_edge: Fixed,
67 cs_top_edge: Fixed,
68 cs_flat_edge: Fixed,
69 ds_flat_edge: Fixed,
70 }
71
72 /// Hinting state for a PostScript subfont.
73 ///
74 /// Note that hinter states depend on the scale, subfont index and
75 /// variation coordinates of a glyph. They can be retained and reused
76 /// if those values remain the same.
77 #[derive(Copy, Clone, PartialEq, Default)]
78 pub(crate) struct HintState {
79 scale: Fixed,
80 blue_scale: Fixed,
81 blue_shift: Fixed,
82 blue_fuzz: Fixed,
83 language_group: i32,
84 supress_overshoot: bool,
85 do_em_box_hints: bool,
86 boost: Fixed,
87 darken_y: Fixed,
88 zones: [BlueZone; MAX_BLUE_ZONES],
89 zone_count: usize,
90 }
91
92 impl HintState {
new(params: &HintParams, scale: Fixed) -> Self93 pub fn new(params: &HintParams, scale: Fixed) -> Self {
94 let mut state = Self {
95 scale,
96 blue_scale: params.blue_scale,
97 blue_shift: params.blue_shift,
98 blue_fuzz: params.blue_fuzz,
99 language_group: params.language_group,
100 supress_overshoot: false,
101 do_em_box_hints: false,
102 boost: Fixed::ZERO,
103 darken_y: Fixed::ZERO,
104 zones: [BlueZone::default(); MAX_BLUE_ZONES],
105 zone_count: 0,
106 };
107 state.build_zones(params);
108 state
109 }
110
zones(&self) -> &[BlueZone]111 fn zones(&self) -> &[BlueZone] {
112 &self.zones[..self.zone_count]
113 }
114
115 /// Initialize zones from the set of blues values.
116 ///
117 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L66>
build_zones(&mut self, params: &HintParams)118 fn build_zones(&mut self, params: &HintParams) {
119 self.do_em_box_hints = false;
120 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L141>
121 match (self.language_group, params.blues.values().len()) {
122 (1, 2) => {
123 let blues = params.blues.values();
124 if blues[0].0 < ICF_BOTTOM
125 && blues[0].1 < ICF_BOTTOM
126 && blues[1].0 > ICF_TOP
127 && blues[1].1 > ICF_TOP
128 {
129 // FreeType generates synthetic hints here. We'll do it
130 // later when building the hint map.
131 self.do_em_box_hints = true;
132 return;
133 }
134 }
135 (1, 0) => {
136 self.do_em_box_hints = true;
137 return;
138 }
139 _ => {}
140 }
141 let mut zones = [BlueZone::default(); MAX_BLUE_ZONES];
142 let mut max_zone_height = Fixed::ZERO;
143 let mut zone_ix = 0usize;
144 // Copy blues and other blues to a combined array of top and bottom zones.
145 for blue in params.blues.values().iter().take(MAX_BLUES) {
146 // FreeType loads blues as integers and then expands to 16.16
147 // at initialization. We load them as 16.16 so floor them here
148 // to ensure we match.
149 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L190>
150 let bottom = blue.0.floor();
151 let top = blue.1.floor();
152 let zone_height = top - bottom;
153 if zone_height < Fixed::ZERO {
154 // Reject zones with negative height
155 continue;
156 }
157 max_zone_height = max_zone_height.max(zone_height);
158 let zone = &mut zones[zone_ix];
159 zone.cs_bottom_edge = bottom;
160 zone.cs_top_edge = top;
161 if zone_ix == 0 {
162 // First blue value is bottom zone
163 zone.is_bottom = true;
164 zone.cs_flat_edge = top;
165 } else {
166 // Remaining blue values are top zones
167 zone.is_bottom = false;
168 // Adjust both edges of top zone upward by twice darkening amount
169 zone.cs_top_edge += twice(self.darken_y);
170 zone.cs_bottom_edge += twice(self.darken_y);
171 zone.cs_flat_edge = zone.cs_bottom_edge;
172 }
173 zone_ix += 1;
174 }
175 for blue in params.other_blues.values().iter().take(MAX_OTHER_BLUES) {
176 let bottom = blue.0.floor();
177 let top = blue.1.floor();
178 let zone_height = top - bottom;
179 if zone_height < Fixed::ZERO {
180 // Reject zones with negative height
181 continue;
182 }
183 max_zone_height = max_zone_height.max(zone_height);
184 let zone = &mut zones[zone_ix];
185 // All "other" blues are bottom zone
186 zone.is_bottom = true;
187 zone.cs_bottom_edge = bottom;
188 zone.cs_top_edge = top;
189 zone.cs_flat_edge = top;
190 zone_ix += 1;
191 }
192 // Adjust for family blues
193 let units_per_pixel = Fixed::ONE / self.scale;
194 for zone in &mut zones[..zone_ix] {
195 let flat = zone.cs_flat_edge;
196 let mut min_diff = Fixed::MAX;
197 if zone.is_bottom {
198 // In a bottom zone, the top edge is the flat edge.
199 // Search family other blues for bottom zones. Look for the
200 // closest edge that is within the one pixel threshold.
201 for blue in params.family_other_blues.values() {
202 let family_flat = blue.1;
203 let diff = (flat - family_flat).abs();
204 if diff < min_diff && diff < units_per_pixel {
205 zone.cs_flat_edge = family_flat;
206 min_diff = diff;
207 if diff == Fixed::ZERO {
208 break;
209 }
210 }
211 }
212 // Check the first member of family blues, which is a bottom
213 // zone
214 if !params.family_blues.values().is_empty() {
215 let family_flat = params.family_blues.values()[0].1;
216 let diff = (flat - family_flat).abs();
217 if diff < min_diff && diff < units_per_pixel {
218 zone.cs_flat_edge = family_flat;
219 }
220 }
221 } else {
222 // In a top zone, the bottom edge is the flat edge.
223 // Search family blues for top zones, skipping the first, which
224 // is a bottom zone. Look for closest family edge that is
225 // within the one pixel threshold.
226 for blue in params.family_blues.values().iter().skip(1) {
227 let family_flat = blue.0 + twice(self.darken_y);
228 let diff = (flat - family_flat).abs();
229 if diff < min_diff && diff < units_per_pixel {
230 zone.cs_flat_edge = family_flat;
231 min_diff = diff;
232 if diff == Fixed::ZERO {
233 break;
234 }
235 }
236 }
237 }
238 }
239 if max_zone_height > Fixed::ZERO && self.blue_scale > (Fixed::ONE / max_zone_height) {
240 // Clamp at maximum scale
241 self.blue_scale = Fixed::ONE / max_zone_height;
242 }
243 // Suppress overshoot and boost blue zones at small sizes
244 if self.scale < self.blue_scale {
245 self.supress_overshoot = true;
246 self.boost =
247 Fixed::from_f64(0.6) - Fixed::from_f64(0.6).mul_div(self.scale, self.blue_scale);
248 // boost must remain less than 0.5, or baseline could go negative
249 self.boost = self.boost.min(Fixed::from_bits(0x7FFF));
250 }
251 if self.darken_y != Fixed::ZERO {
252 self.boost = Fixed::ZERO;
253 }
254 // Set device space alignment for each zone; apply boost amount before
255 // rounding flat edge
256 let scale = self.scale;
257 let boost = self.boost;
258 for zone in &mut zones[..zone_ix] {
259 let boost = if zone.is_bottom { -boost } else { boost };
260 zone.ds_flat_edge = (zone.cs_flat_edge * scale + boost).round();
261 }
262 self.zones = zones;
263 self.zone_count = zone_ix;
264 }
265
266 /// Check whether a hint is captured by one of the blue zones.
267 ///
268 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L465>
capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool269 fn capture(&self, bottom_edge: &mut Hint, top_edge: &mut Hint) -> bool {
270 let fuzz = self.blue_fuzz;
271 let mut captured = false;
272 let mut adjustment = Fixed::ZERO;
273 for zone in self.zones() {
274 if zone.is_bottom
275 && bottom_edge.is_bottom()
276 && (zone.cs_bottom_edge - fuzz) <= bottom_edge.cs_coord
277 && bottom_edge.cs_coord <= (zone.cs_top_edge + fuzz)
278 {
279 // Bottom edge captured by bottom zone.
280 adjustment = if self.supress_overshoot {
281 zone.ds_flat_edge
282 } else if zone.cs_top_edge - bottom_edge.cs_coord >= self.blue_shift {
283 // Guarantee minimum of 1 pixel overshoot
284 bottom_edge
285 .ds_coord
286 .round()
287 .min(zone.ds_flat_edge - Fixed::ONE)
288 } else {
289 bottom_edge.ds_coord.round()
290 };
291 adjustment -= bottom_edge.ds_coord;
292 captured = true;
293 break;
294 }
295 if !zone.is_bottom
296 && top_edge.is_top()
297 && (zone.cs_bottom_edge - fuzz) <= top_edge.cs_coord
298 && top_edge.cs_coord <= (zone.cs_top_edge + fuzz)
299 {
300 // Top edge captured by top zone.
301 adjustment = if self.supress_overshoot {
302 zone.ds_flat_edge
303 } else if top_edge.cs_coord - zone.cs_bottom_edge >= self.blue_shift {
304 // Guarantee minimum of 1 pixel overshoot
305 top_edge
306 .ds_coord
307 .round()
308 .max(zone.ds_flat_edge + Fixed::ONE)
309 } else {
310 top_edge.ds_coord.round()
311 };
312 adjustment -= top_edge.ds_coord;
313 captured = true;
314 break;
315 }
316 }
317 if captured {
318 // Move both edges and mark them as "locked"
319 if bottom_edge.is_valid() {
320 bottom_edge.ds_coord += adjustment;
321 bottom_edge.lock();
322 }
323 if top_edge.is_valid() {
324 top_edge.ds_coord += adjustment;
325 top_edge.lock();
326 }
327 }
328 captured
329 }
330 }
331
332 /// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L85>
333 #[derive(Copy, Clone, Default)]
334 struct StemHint {
335 /// If true, device space position is valid
336 is_used: bool,
337 // Character space position
338 min: Fixed,
339 max: Fixed,
340 // Device space position after first use
341 ds_min: Fixed,
342 ds_max: Fixed,
343 }
344
345 // Hint flags
346 const GHOST_BOTTOM: u8 = 0x1;
347 const GHOST_TOP: u8 = 0x2;
348 const PAIR_BOTTOM: u8 = 0x4;
349 const PAIR_TOP: u8 = 0x8;
350 const LOCKED: u8 = 0x10;
351 const SYNTHETIC: u8 = 0x20;
352
353 /// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.h#L118>
354 #[derive(Copy, Clone, PartialEq, Default, Debug)]
355 struct Hint {
356 flags: u8,
357 /// Index in original stem hint array (if not synthetic)
358 index: u8,
359 cs_coord: Fixed,
360 ds_coord: Fixed,
361 scale: Fixed,
362 }
363
364 impl Hint {
is_valid(&self) -> bool365 fn is_valid(&self) -> bool {
366 self.flags != 0
367 }
368
is_bottom(&self) -> bool369 fn is_bottom(&self) -> bool {
370 self.flags & (GHOST_BOTTOM | PAIR_BOTTOM) != 0
371 }
372
is_top(&self) -> bool373 fn is_top(&self) -> bool {
374 self.flags & (GHOST_TOP | PAIR_TOP) != 0
375 }
376
is_pair(&self) -> bool377 fn is_pair(&self) -> bool {
378 self.flags & (PAIR_BOTTOM | PAIR_TOP) != 0
379 }
380
is_pair_top(&self) -> bool381 fn is_pair_top(&self) -> bool {
382 self.flags & PAIR_TOP != 0
383 }
384
is_locked(&self) -> bool385 fn is_locked(&self) -> bool {
386 self.flags & LOCKED != 0
387 }
388
is_synthetic(&self) -> bool389 fn is_synthetic(&self) -> bool {
390 self.flags & SYNTHETIC != 0
391 }
392
lock(&mut self)393 fn lock(&mut self) {
394 self.flags |= LOCKED
395 }
396
397 /// Hint initialization from an incoming stem hint.
398 ///
399 /// See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L89>
setup( &mut self, stem: &StemHint, index: u8, origin: Fixed, scale: Fixed, darken_y: Fixed, is_bottom: bool, )400 fn setup(
401 &mut self,
402 stem: &StemHint,
403 index: u8,
404 origin: Fixed,
405 scale: Fixed,
406 darken_y: Fixed,
407 is_bottom: bool,
408 ) {
409 // "Ghost hints" are used to align a single edge rather than a
410 // stem-- think the top and bottom edges of an uppercase
411 // sans-serif I.
412 // These are encoded internally with stem hints of width -21
413 // and -20 for bottom and top hints, respectively.
414 const GHOST_BOTTOM_WIDTH: Fixed = Fixed::from_i32(-21);
415 const GHOST_TOP_WIDTH: Fixed = Fixed::from_i32(-20);
416 let width = stem.max - stem.min;
417 if width == GHOST_BOTTOM_WIDTH {
418 if is_bottom {
419 self.cs_coord = stem.max;
420 self.flags = GHOST_BOTTOM;
421 } else {
422 self.flags = 0;
423 }
424 } else if width == GHOST_TOP_WIDTH {
425 if !is_bottom {
426 self.cs_coord = stem.min;
427 self.flags = GHOST_TOP;
428 } else {
429 self.flags = 0;
430 }
431 } else if width < Fixed::ZERO {
432 // If width < 0, this is an inverted pair. We follow FreeType and
433 // swap the coordinates
434 if is_bottom {
435 self.cs_coord = stem.max;
436 self.flags = PAIR_BOTTOM;
437 } else {
438 self.cs_coord = stem.min;
439 self.flags = PAIR_TOP;
440 }
441 } else {
442 // This is a normal pair
443 if is_bottom {
444 self.cs_coord = stem.min;
445 self.flags = PAIR_BOTTOM;
446 } else {
447 self.cs_coord = stem.max;
448 self.flags = PAIR_TOP;
449 }
450 }
451 if self.is_top() {
452 // For top hints, adjust character space position up by twice the
453 // darkening amount
454 self.cs_coord += twice(darken_y);
455 }
456 self.cs_coord += origin;
457 self.scale = scale;
458 self.index = index;
459 // If original stem hint was used, copy the position
460 if self.flags != 0 && stem.is_used {
461 if self.is_top() {
462 self.ds_coord = stem.ds_max;
463 } else {
464 self.ds_coord = stem.ds_min;
465 }
466 self.lock();
467 } else {
468 self.ds_coord = self.cs_coord * scale;
469 }
470 }
471 }
472
473 /// Collection of adjusted hint edges.
474 ///
475 /// <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L126>
476 #[derive(Copy, Clone)]
477 struct HintMap {
478 edges: [Hint; MAX_HINTS],
479 len: usize,
480 is_valid: bool,
481 scale: Fixed,
482 }
483
484 impl HintMap {
new(scale: Fixed) -> Self485 fn new(scale: Fixed) -> Self {
486 Self {
487 edges: [Hint::default(); MAX_HINTS],
488 len: 0,
489 is_valid: false,
490 scale,
491 }
492 }
493
clear(&mut self)494 fn clear(&mut self) {
495 self.len = 0;
496 self.is_valid = false;
497 }
498
499 /// Transform character space coordinate to device space.
500 ///
501 /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L331>
transform(&self, coord: Fixed) -> Fixed502 fn transform(&self, coord: Fixed) -> Fixed {
503 if self.len == 0 {
504 return coord * self.scale;
505 }
506 let limit = self.len - 1;
507 let mut i = 0;
508 while i < limit && coord >= self.edges[i + 1].cs_coord {
509 i += 1;
510 }
511 while i > 0 && coord < self.edges[i].cs_coord {
512 i -= 1;
513 }
514 let first_edge = &self.edges[0];
515 if i == 0 && coord < first_edge.cs_coord {
516 // Special case for points below first edge: use uniform scale
517 ((coord - first_edge.cs_coord) * self.scale) + first_edge.ds_coord
518 } else {
519 // Use highest edge where cs_coord >= edge.cs_coord
520 let edge = &self.edges[i];
521 ((coord - edge.cs_coord) * edge.scale) + edge.ds_coord
522 }
523 }
524
525 /// Insert hint edges into map, sorted by character space coordinate.
526 ///
527 /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L606>
insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>)528 fn insert(&mut self, bottom: &Hint, top: &Hint, initial: Option<&HintMap>) {
529 let (is_pair, mut first_edge) = if !bottom.is_valid() {
530 // Bottom is invalid: insert only top edge
531 (false, *top)
532 } else if !top.is_valid() {
533 // Top is invalid: insert only bottom edge
534 (false, *bottom)
535 } else {
536 // We have a valid pair!
537 (true, *bottom)
538 };
539 let mut second_edge = *top;
540 if is_pair && top.cs_coord < bottom.cs_coord {
541 // Paired edges must be in proper order. FT just ignores the hint.
542 return;
543 }
544 let edge_count = if is_pair { 2 } else { 1 };
545 if self.len + edge_count > MAX_HINTS {
546 // Won't fit. Again, ignore.
547 return;
548 }
549 // Find insertion index that keeps the edge list sorted
550 let mut insert_ix = 0;
551 while insert_ix < self.len {
552 if self.edges[insert_ix].cs_coord >= first_edge.cs_coord {
553 break;
554 }
555 insert_ix += 1;
556 }
557 // Discard hints that overlap in character space
558 if insert_ix < self.len {
559 let current = &self.edges[insert_ix];
560 // Existing edge is the same
561 if (current.cs_coord == first_edge.cs_coord)
562 // Pair straddles the next edge
563 || (is_pair && current.cs_coord <= second_edge.cs_coord)
564 // Inserting between paired edges
565 || current.is_pair_top()
566 {
567 return;
568 }
569 }
570 // Recompute device space locations using initial hint map
571 if !first_edge.is_locked() {
572 if let Some(initial) = initial {
573 if is_pair {
574 // Preserve stem width: position center of stem with
575 // initial hint map and two edges with nominal scale
576 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/57617782464411201ce7bbc93b086c1b4d7d84a5/src/psaux/pshints.c#L693>
577 let mid =
578 initial.transform(midpoint(first_edge.cs_coord, second_edge.cs_coord));
579 let half_width = half(second_edge.cs_coord - first_edge.cs_coord) * self.scale;
580 first_edge.ds_coord = mid - half_width;
581 second_edge.ds_coord = mid + half_width;
582 } else {
583 first_edge.ds_coord = initial.transform(first_edge.cs_coord);
584 }
585 }
586 }
587 // Now discard hints that overlap in device space:
588 if insert_ix > 0 && first_edge.ds_coord < self.edges[insert_ix - 1].ds_coord {
589 // Inserting after an existing edge
590 return;
591 }
592 if insert_ix < self.len
593 && ((is_pair && second_edge.ds_coord > self.edges[insert_ix].ds_coord)
594 || first_edge.ds_coord > self.edges[insert_ix].ds_coord)
595 {
596 // Inserting before an existing edge
597 return;
598 }
599 // If we're inserting in the middle, make room in the edge array
600 if insert_ix != self.len {
601 let mut src_index = self.len - 1;
602 let mut dst_index = self.len + edge_count - 1;
603 loop {
604 self.edges[dst_index] = self.edges[src_index];
605 if src_index == insert_ix {
606 break;
607 }
608 src_index -= 1;
609 dst_index -= 1;
610 }
611 }
612 self.edges[insert_ix] = first_edge;
613 if is_pair {
614 self.edges[insert_ix + 1] = second_edge;
615 }
616 self.len += edge_count;
617 }
618
619 /// Adjust hint pairs so that one of the two edges is on a pixel boundary.
620 ///
621 /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L396>
adjust(&mut self)622 fn adjust(&mut self) {
623 let mut saved = [(0usize, Fixed::ZERO); MAX_HINTS];
624 let mut saved_count = 0usize;
625 let mut i = 0;
626 // From FT with adjustments for variable names:
627 // "First pass is bottom-up (font hint order) without look-ahead.
628 // Locked edges are already adjusted.
629 // Unlocked edges begin with ds_coord from `initial_map'.
630 // Save edges that are not optimally adjusted in `saved' array,
631 // and process them in second pass."
632 let limit = self.len;
633 while i < limit {
634 let is_pair = self.edges[i].is_pair();
635 let j = if is_pair { i + 1 } else { i };
636 if !self.edges[i].is_locked() {
637 // We can adjust hint edges that are not locked
638 let frac_down = self.edges[i].ds_coord.fract();
639 let frac_up = self.edges[j].ds_coord.fract();
640 // There are four possibilities. We compute them all.
641 // (moves down are negative)
642 let down_move_down = Fixed::ZERO - frac_down;
643 let up_move_down = Fixed::ZERO - frac_up;
644 let down_move_up = if frac_down == Fixed::ZERO {
645 Fixed::ZERO
646 } else {
647 Fixed::ONE - frac_down
648 };
649 let up_move_up = if frac_up == Fixed::ZERO {
650 Fixed::ZERO
651 } else {
652 Fixed::ONE - frac_up
653 };
654 // Smallest move up
655 let move_up = down_move_up.min(up_move_up);
656 // Smallest move down
657 let move_down = down_move_down.max(up_move_down);
658 let mut save_edge = false;
659 let adjustment;
660 // Check for room to move up:
661 // 1. We're at the top of the array, or
662 // 2. The next edge is at or above the proposed move up
663 if j >= self.len - 1
664 || self.edges[j + 1].ds_coord
665 >= (self.edges[j].ds_coord + move_up + MIN_COUNTER)
666 {
667 // Also check for room to move down...
668 if i == 0
669 || self.edges[i - 1].ds_coord
670 <= (self.edges[i].ds_coord + move_down - MIN_COUNTER)
671 {
672 // .. and move the smallest distance
673 adjustment = if -move_down < move_up {
674 move_down
675 } else {
676 move_up
677 };
678 } else {
679 adjustment = move_up;
680 }
681 } else if i == 0
682 || self.edges[i - 1].ds_coord
683 <= (self.edges[i].ds_coord + move_down - MIN_COUNTER)
684 {
685 // We can move down
686 adjustment = move_down;
687 // True if the move is not optimum
688 save_edge = move_up < -move_down;
689 } else {
690 // We can't move either way without overlapping
691 adjustment = Fixed::ZERO;
692 save_edge = true;
693 }
694 // Capture non-optimal adjustments and save them for a second
695 // pass. This is only possible if the edge above is unlocked
696 // and can be moved.
697 if save_edge && j < self.len - 1 && !self.edges[j + 1].is_locked() {
698 // (index, desired adjustment)
699 saved[saved_count] = (j, move_up - adjustment);
700 saved_count += 1;
701 }
702 // Apply the adjustment
703 self.edges[i].ds_coord += adjustment;
704 if is_pair {
705 self.edges[j].ds_coord += adjustment;
706 }
707 }
708 // Compute the new edge scale
709 if i > 0 && self.edges[i].cs_coord != self.edges[i - 1].cs_coord {
710 let a = self.edges[i];
711 let b = self.edges[i - 1];
712 self.edges[i - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord);
713 }
714 if is_pair {
715 if self.edges[j].cs_coord != self.edges[j - 1].cs_coord {
716 let a = self.edges[j];
717 let b = self.edges[j - 1];
718 self.edges[j - 1].scale = (a.ds_coord - b.ds_coord) / (a.cs_coord - b.cs_coord);
719 }
720 i += 1;
721 }
722 i += 1;
723 }
724 // Second pass tries to move non-optimal edges up if the first
725 // pass created room
726 for (j, adjustment) in saved[..saved_count].iter().copied().rev() {
727 if self.edges[j + 1].ds_coord >= (self.edges[j].ds_coord + adjustment + MIN_COUNTER) {
728 self.edges[j].ds_coord += adjustment;
729 if self.edges[j].is_pair() {
730 self.edges[j - 1].ds_coord += adjustment;
731 }
732 }
733 }
734 }
735
736 /// Builds a hintmap from hints and mask.
737 ///
738 /// If `initial_map` is invalid, this recurses one level to initialize
739 /// it. If `is_initial` is true, simply build the initial map.
740 ///
741 /// Based on <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L814>
build( &mut self, state: &HintState, mask: Option<HintMask>, mut initial_map: Option<&mut HintMap>, stems: &mut [StemHint], origin: Fixed, is_initial: bool, )742 fn build(
743 &mut self,
744 state: &HintState,
745 mask: Option<HintMask>,
746 mut initial_map: Option<&mut HintMap>,
747 stems: &mut [StemHint],
748 origin: Fixed,
749 is_initial: bool,
750 ) {
751 let scale = state.scale;
752 let darken_y = Fixed::ZERO;
753 if !is_initial {
754 if let Some(initial_map) = &mut initial_map {
755 if !initial_map.is_valid {
756 // Note: recursive call here to build the initial map if it
757 // is provided and invalid
758 initial_map.build(state, Some(HintMask::all()), None, stems, origin, true);
759 }
760 }
761 }
762 let initial_map = initial_map.map(|x| x as &HintMap);
763 self.clear();
764 // If the mask is missing or invalid, assume all hints are active
765 let mut mask = mask.unwrap_or_else(HintMask::all);
766 if !mask.is_valid {
767 mask = HintMask::all();
768 }
769 if state.do_em_box_hints {
770 // FreeType generates these during blues initialization. Do
771 // it here just to avoid carrying the extra state in the
772 // already large HintState struct.
773 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psblues.c#L160>
774 let mut bottom = Hint::default();
775 bottom.cs_coord = ICF_BOTTOM - EPSILON;
776 bottom.ds_coord = (bottom.cs_coord * scale).round() - MIN_COUNTER;
777 bottom.scale = scale;
778 bottom.flags = GHOST_BOTTOM | LOCKED | SYNTHETIC;
779 let mut top = Hint::default();
780 top.cs_coord = ICF_TOP + EPSILON + twice(state.darken_y);
781 top.ds_coord = (top.cs_coord * scale).round() + MIN_COUNTER;
782 top.scale = scale;
783 top.flags = GHOST_TOP | LOCKED | SYNTHETIC;
784 let invalid = Hint::default();
785 self.insert(&bottom, &invalid, initial_map);
786 self.insert(&invalid, &top, initial_map);
787 }
788 let mut tmp_mask = mask;
789 // FreeType iterates over the hint mask with some fancy bit logic. We
790 // do the simpler thing and loop over the stems.
791 // <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L897>
792 for (i, stem) in stems.iter().enumerate() {
793 if !tmp_mask.get(i) {
794 continue;
795 }
796 let hint_ix = i as u8;
797 let mut bottom = Hint::default();
798 let mut top = Hint::default();
799 bottom.setup(stem, hint_ix, origin, scale, darken_y, true);
800 top.setup(stem, hint_ix, origin, scale, darken_y, false);
801 // Insert hints that are locked or captured by a blue zone
802 if bottom.is_locked() || top.is_locked() || state.capture(&mut bottom, &mut top) {
803 if is_initial {
804 self.insert(&bottom, &top, None);
805 } else {
806 self.insert(&bottom, &top, initial_map);
807 }
808 // Avoid processing this hint in the second pass
809 tmp_mask.clear(i);
810 }
811 }
812 if is_initial {
813 // Heuristic: insert a point at (0, 0) if it's not covered by a
814 // mapping. Ensures a lock at baseline for glyphs missing a
815 // baseline hint.
816 if self.len == 0
817 || self.edges[0].cs_coord > Fixed::ZERO
818 || self.edges[self.len - 1].cs_coord < Fixed::ZERO
819 {
820 let edge = Hint {
821 flags: GHOST_BOTTOM | LOCKED | SYNTHETIC,
822 scale,
823 ..Default::default()
824 };
825 let invalid = Hint::default();
826 self.insert(&edge, &invalid, None);
827 }
828 } else {
829 // Insert hints that were skipped in the first pass
830 for (i, stem) in stems.iter().enumerate() {
831 if !tmp_mask.get(i) {
832 continue;
833 }
834 let hint_ix = i as u8;
835 let mut bottom = Hint::default();
836 let mut top = Hint::default();
837 bottom.setup(stem, hint_ix, origin, scale, darken_y, true);
838 top.setup(stem, hint_ix, origin, scale, darken_y, false);
839 self.insert(&bottom, &top, initial_map);
840 }
841 }
842 // Adjust edges that are not locked to blue zones
843 self.adjust();
844 if !is_initial {
845 // Save position of edges that were used by the hint map.
846 for edge in &self.edges[..self.len] {
847 if edge.is_synthetic() {
848 continue;
849 }
850 let stem = &mut stems[edge.index as usize];
851 if edge.is_top() {
852 stem.ds_max = edge.ds_coord;
853 } else {
854 stem.ds_min = edge.ds_coord;
855 }
856 stem.is_used = true;
857 }
858 }
859 self.is_valid = true;
860 }
861 }
862
863 /// Bitmask that specifies which hints are currently active.
864 ///
865 /// "Each bit of the mask, starting with the most-significant bit of
866 /// the first byte, represents the corresponding hint zone in the
867 /// order in which the hints were declared at the beginning of
868 /// the charstring."
869 ///
870 /// See <https://adobe-type-tools.github.io/font-tech-notes/pdfs/5177.Type2.pdf#page=24>
871 /// Also <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.h#L70>
872 #[derive(Copy, Clone, PartialEq, Default)]
873 struct HintMask {
874 mask: [u8; HINT_MASK_SIZE],
875 is_valid: bool,
876 }
877
878 impl HintMask {
new(bytes: &[u8]) -> Option<Self>879 fn new(bytes: &[u8]) -> Option<Self> {
880 let len = bytes.len();
881 if len > HINT_MASK_SIZE {
882 return None;
883 }
884 let mut mask = Self::default();
885 mask.mask[..len].copy_from_slice(&bytes[..len]);
886 mask.is_valid = true;
887 Some(mask)
888 }
889
all() -> Self890 fn all() -> Self {
891 Self {
892 mask: [0xFF; HINT_MASK_SIZE],
893 is_valid: true,
894 }
895 }
896
clear(&mut self, bit: usize)897 fn clear(&mut self, bit: usize) {
898 self.mask[bit >> 3] &= !msb_mask(bit);
899 }
900
get(&self, bit: usize) -> bool901 fn get(&self, bit: usize) -> bool {
902 self.mask[bit >> 3] & msb_mask(bit) != 0
903 }
904 }
905
906 /// Returns a bit mask for the selected bit with the
907 /// most significant bit at index 0.
msb_mask(bit: usize) -> u8908 fn msb_mask(bit: usize) -> u8 {
909 1 << (7 - (bit & 0x7))
910 }
911
912 pub(super) struct HintingSink<'a, S> {
913 state: &'a HintState,
914 sink: &'a mut S,
915 stem_hints: [StemHint; MAX_HINTS],
916 stem_count: u8,
917 mask: HintMask,
918 initial_map: HintMap,
919 map: HintMap,
920 /// Most recent move_to in character space.
921 start_point: Option<[Fixed; 2]>,
922 /// Most recent line_to. First two elements are coords in character
923 /// space and the last two are in device space.
924 pending_line: Option<[Fixed; 4]>,
925 }
926
927 impl<'a, S: CommandSink> HintingSink<'a, S> {
new(state: &'a HintState, sink: &'a mut S) -> Self928 pub fn new(state: &'a HintState, sink: &'a mut S) -> Self {
929 let scale = state.scale;
930 Self {
931 state,
932 sink,
933 stem_hints: [StemHint::default(); MAX_HINTS],
934 stem_count: 0,
935 mask: HintMask::all(),
936 initial_map: HintMap::new(scale),
937 map: HintMap::new(scale),
938 start_point: None,
939 pending_line: None,
940 }
941 }
942
finish(&mut self)943 pub fn finish(&mut self) {
944 self.maybe_close_subpath();
945 }
946
maybe_close_subpath(&mut self)947 fn maybe_close_subpath(&mut self) {
948 // This requires some explanation. The hint mask can be modified
949 // during charstring evaluation which changes the set of hints that
950 // are applied. FreeType ensures that the closing line for any subpath
951 // is transformed with the same hint map as the starting point for the
952 // subpath. This is done by stashing a copy of the hint map that is
953 // active when a new subpath is started. Unlike FreeType, we make use
954 // of close elements, so we can cheat a bit here and avoid the
955 // extra hintmap. If we're closing an open subpath and have a pending
956 // line and the line is not equal to the start point in character
957 // space, then we emit the saved device space coordinates for the
958 // line. If the coordinates do match in character space, we omit
959 // that line. The unconditional close command ensures that the
960 // start and end points coincide.
961 // Note: this doesn't apply to subpaths that end in cubics.
962 match (self.start_point.take(), self.pending_line.take()) {
963 (Some(start), Some([cs_x, cs_y, ds_x, ds_y])) => {
964 if start != [cs_x, cs_y] {
965 self.sink.line_to(ds_x, ds_y);
966 }
967 self.sink.close();
968 }
969 (Some(_), _) => self.sink.close(),
970 _ => {}
971 }
972 }
973
flush_pending_line(&mut self)974 fn flush_pending_line(&mut self) {
975 if let Some([_, _, x, y]) = self.pending_line.take() {
976 self.sink.line_to(x, y);
977 }
978 }
979
hint(&mut self, coord: Fixed) -> Fixed980 fn hint(&mut self, coord: Fixed) -> Fixed {
981 if !self.map.is_valid {
982 self.build_hint_map(Some(self.mask), Fixed::ZERO);
983 }
984 trunc(self.map.transform(coord))
985 }
986
scale(&self, coord: Fixed) -> Fixed987 fn scale(&self, coord: Fixed) -> Fixed {
988 trunc(coord * self.state.scale)
989 }
990
add_stem(&mut self, min: Fixed, max: Fixed)991 fn add_stem(&mut self, min: Fixed, max: Fixed) {
992 let index = self.stem_count as usize;
993 if index >= MAX_HINTS || self.map.is_valid {
994 return;
995 }
996 let stem = &mut self.stem_hints[index];
997 stem.min = min;
998 stem.max = max;
999 stem.is_used = false;
1000 stem.ds_min = Fixed::ZERO;
1001 stem.ds_max = Fixed::ZERO;
1002 self.stem_count = index as u8 + 1;
1003 }
1004
build_hint_map(&mut self, mask: Option<HintMask>, origin: Fixed)1005 fn build_hint_map(&mut self, mask: Option<HintMask>, origin: Fixed) {
1006 self.map.build(
1007 self.state,
1008 mask,
1009 Some(&mut self.initial_map),
1010 &mut self.stem_hints[..self.stem_count as usize],
1011 origin,
1012 false,
1013 );
1014 }
1015 }
1016
1017 impl<'a, S: CommandSink> CommandSink for HintingSink<'a, S> {
hstem(&mut self, min: Fixed, max: Fixed)1018 fn hstem(&mut self, min: Fixed, max: Fixed) {
1019 self.add_stem(min, max);
1020 }
1021
hint_mask(&mut self, mask: &[u8])1022 fn hint_mask(&mut self, mask: &[u8]) {
1023 // For invalid hint masks, FreeType assumes all hints are active.
1024 // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/pshints.c#L844>
1025 let mask = HintMask::new(mask).unwrap_or_else(HintMask::all);
1026 if mask != self.mask {
1027 self.mask = mask;
1028 self.map.is_valid = false;
1029 }
1030 }
1031
counter_mask(&mut self, mask: &[u8])1032 fn counter_mask(&mut self, mask: &[u8]) {
1033 // For counter masks, we build a temporary hint map "just to
1034 // place and lock those stems participating in the counter
1035 // mask." Building the map modifies the stem hint array as a
1036 // side effect.
1037 // See <https://gitlab.freedesktop.org/freetype/freetype/-/blob/80a507a6b8e3d2906ad2c8ba69329bd2fb2a85ef/src/psaux/psintrp.c#L2617>
1038 let mask = HintMask::new(mask).unwrap_or_else(HintMask::all);
1039 let mut map = HintMap::new(self.state.scale);
1040 map.build(
1041 self.state,
1042 Some(mask),
1043 Some(&mut self.initial_map),
1044 &mut self.stem_hints[..self.stem_count as usize],
1045 Fixed::ZERO,
1046 false,
1047 );
1048 }
1049
move_to(&mut self, x: Fixed, y: Fixed)1050 fn move_to(&mut self, x: Fixed, y: Fixed) {
1051 self.maybe_close_subpath();
1052 self.start_point = Some([x, y]);
1053 let x = self.scale(x);
1054 let y = self.hint(y);
1055 self.sink.move_to(x, y);
1056 }
1057
line_to(&mut self, x: Fixed, y: Fixed)1058 fn line_to(&mut self, x: Fixed, y: Fixed) {
1059 self.flush_pending_line();
1060 let ds_x = self.scale(x);
1061 let ds_y = self.hint(y);
1062 self.pending_line = Some([x, y, ds_x, ds_y]);
1063 }
1064
curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed)1065 fn curve_to(&mut self, cx1: Fixed, cy1: Fixed, cx2: Fixed, cy2: Fixed, x: Fixed, y: Fixed) {
1066 self.flush_pending_line();
1067 let cx1 = self.scale(cx1);
1068 let cy1 = self.hint(cy1);
1069 let cx2 = self.scale(cx2);
1070 let cy2 = self.hint(cy2);
1071 let x = self.scale(x);
1072 let y = self.hint(y);
1073 self.sink.curve_to(cx1, cy1, cx2, cy2, x, y);
1074 }
1075
close(&mut self)1076 fn close(&mut self) {
1077 // We emit close commands based on the sequence of moves.
1078 // See `maybe_close_subpath`
1079 }
1080 }
1081
1082 /// FreeType converts from 16.16 to 26.6 by truncation. We keep our
1083 /// values in 16.16 so simply zero the low 10 bits to match the
1084 /// precision when converting to f32.
trunc(value: Fixed) -> Fixed1085 fn trunc(value: Fixed) -> Fixed {
1086 Fixed::from_bits(value.to_bits() & !0x3FF)
1087 }
1088
half(value: Fixed) -> Fixed1089 fn half(value: Fixed) -> Fixed {
1090 Fixed::from_bits(value.to_bits() / 2)
1091 }
1092
twice(value: Fixed) -> Fixed1093 fn twice(value: Fixed) -> Fixed {
1094 Fixed::from_bits(value.to_bits().wrapping_mul(2))
1095 }
1096
1097 /// Computes midpoint between `a` and `b`, avoiding overflow if the sum
1098 /// of the high 16 bits exceeds `i16::MAX`.
midpoint(a: Fixed, b: Fixed) -> Fixed1099 fn midpoint(a: Fixed, b: Fixed) -> Fixed {
1100 a + half(b - a)
1101 }
1102
1103 #[cfg(test)]
1104 mod tests {
1105 use read_fonts::{tables::postscript::charstring::CommandSink, types::F2Dot14, FontRef};
1106
1107 use super::{
1108 BlueZone, Blues, Fixed, Hint, HintMap, HintMask, HintParams, HintState, HintingSink,
1109 StemHint, GHOST_BOTTOM, GHOST_TOP, HINT_MASK_SIZE, LOCKED, PAIR_BOTTOM, PAIR_TOP,
1110 };
1111
make_hint_state() -> HintState1112 fn make_hint_state() -> HintState {
1113 fn make_blues(values: &[f64]) -> Blues {
1114 Blues::new(values.iter().copied().map(Fixed::from_f64))
1115 }
1116 // <BlueValues value="-15 0 536 547 571 582 714 726 760 772"/>
1117 // <OtherBlues value="-255 -240"/>
1118 // <BlueScale value="0.05"/>
1119 // <BlueShift value="7"/>
1120 // <BlueFuzz value="0"/>
1121 let params = HintParams {
1122 blues: make_blues(&[
1123 -15.0, 0.0, 536.0, 547.0, 571.0, 582.0, 714.0, 726.0, 760.0, 772.0,
1124 ]),
1125 other_blues: make_blues(&[-255.0, -240.0]),
1126 blue_scale: Fixed::from_f64(0.05),
1127 blue_shift: Fixed::from_i32(7),
1128 blue_fuzz: Fixed::ZERO,
1129 ..Default::default()
1130 };
1131 HintState::new(¶ms, Fixed::ONE / Fixed::from_i32(64))
1132 }
1133
1134 #[test]
scaled_blue_zones()1135 fn scaled_blue_zones() {
1136 let state = make_hint_state();
1137 assert!(!state.do_em_box_hints);
1138 assert_eq!(state.zone_count, 6);
1139 assert_eq!(state.boost, Fixed::from_bits(27035));
1140 assert!(state.supress_overshoot);
1141 // FreeType generates the following zones:
1142 let expected_zones = &[
1143 // csBottomEdge -983040 int
1144 // csTopEdge 0 int
1145 // csFlatEdge 0 int
1146 // dsFlatEdge 0 int
1147 // bottomZone 1 '\x1' unsigned char
1148 BlueZone {
1149 cs_bottom_edge: Fixed::from_bits(-983040),
1150 is_bottom: true,
1151 ..Default::default()
1152 },
1153 // csBottomEdge 35127296 int
1154 // csTopEdge 35848192 int
1155 // csFlatEdge 35127296 int
1156 // dsFlatEdge 589824 int
1157 // bottomZone 0 '\0' unsigned char
1158 BlueZone {
1159 cs_bottom_edge: Fixed::from_bits(35127296),
1160 cs_top_edge: Fixed::from_bits(35848192),
1161 cs_flat_edge: Fixed::from_bits(35127296),
1162 ds_flat_edge: Fixed::from_bits(589824),
1163 is_bottom: false,
1164 },
1165 // csBottomEdge 37421056 int
1166 // csTopEdge 38141952 int
1167 // csFlatEdge 37421056 int
1168 // dsFlatEdge 589824 int
1169 // bottomZone 0 '\0' unsigned char
1170 BlueZone {
1171 cs_bottom_edge: Fixed::from_bits(37421056),
1172 cs_top_edge: Fixed::from_bits(38141952),
1173 cs_flat_edge: Fixed::from_bits(37421056),
1174 ds_flat_edge: Fixed::from_bits(589824),
1175 is_bottom: false,
1176 },
1177 // csBottomEdge 46792704 int
1178 // csTopEdge 47579136 int
1179 // csFlatEdge 46792704 int
1180 // dsFlatEdge 786432 int
1181 // bottomZone 0 '\0' unsigned char
1182 BlueZone {
1183 cs_bottom_edge: Fixed::from_bits(46792704),
1184 cs_top_edge: Fixed::from_bits(47579136),
1185 cs_flat_edge: Fixed::from_bits(46792704),
1186 ds_flat_edge: Fixed::from_bits(786432),
1187 is_bottom: false,
1188 },
1189 // csBottomEdge 49807360 int
1190 // csTopEdge 50593792 int
1191 // csFlatEdge 49807360 int
1192 // dsFlatEdge 786432 int
1193 // bottomZone 0 '\0' unsigned char
1194 BlueZone {
1195 cs_bottom_edge: Fixed::from_bits(49807360),
1196 cs_top_edge: Fixed::from_bits(50593792),
1197 cs_flat_edge: Fixed::from_bits(49807360),
1198 ds_flat_edge: Fixed::from_bits(786432),
1199 is_bottom: false,
1200 },
1201 // csBottomEdge -16711680 int
1202 // csTopEdge -15728640 int
1203 // csFlatEdge -15728640 int
1204 // dsFlatEdge -262144 int
1205 // bottomZone 1 '\x1' unsigned char
1206 BlueZone {
1207 cs_bottom_edge: Fixed::from_bits(-16711680),
1208 cs_top_edge: Fixed::from_bits(-15728640),
1209 cs_flat_edge: Fixed::from_bits(-15728640),
1210 ds_flat_edge: Fixed::from_bits(-262144),
1211 is_bottom: true,
1212 },
1213 ];
1214 assert_eq!(state.zones(), expected_zones);
1215 }
1216
1217 #[test]
blue_zone_capture()1218 fn blue_zone_capture() {
1219 let state = make_hint_state();
1220 let bottom_edge = Hint {
1221 flags: PAIR_BOTTOM,
1222 ds_coord: Fixed::from_f64(2.3),
1223 ..Default::default()
1224 };
1225 let top_edge = Hint {
1226 flags: PAIR_TOP,
1227 // This value chosen to fit within the first "top" blue zone
1228 cs_coord: Fixed::from_bits(35127297),
1229 ds_coord: Fixed::from_f64(2.3),
1230 ..Default::default()
1231 };
1232 // Capture both
1233 {
1234 let (mut bottom_edge, mut top_edge) = (bottom_edge, top_edge);
1235 assert!(state.capture(&mut bottom_edge, &mut top_edge));
1236 assert!(bottom_edge.is_locked());
1237 assert!(top_edge.is_locked());
1238 }
1239 // Capture none
1240 {
1241 // Used to guarantee the edges are below all blue zones and will
1242 // not be captured
1243 let min_cs_coord = Fixed::MIN;
1244 let mut bottom_edge = Hint {
1245 cs_coord: min_cs_coord,
1246 ..bottom_edge
1247 };
1248 let mut top_edge = Hint {
1249 cs_coord: min_cs_coord,
1250 ..top_edge
1251 };
1252 assert!(!state.capture(&mut bottom_edge, &mut top_edge));
1253 assert!(!bottom_edge.is_locked());
1254 assert!(!top_edge.is_locked());
1255 }
1256 // Capture bottom, ignore invalid top
1257 {
1258 let mut bottom_edge = bottom_edge;
1259 let mut top_edge = Hint {
1260 // Empty flags == invalid hint
1261 flags: 0,
1262 ..top_edge
1263 };
1264 assert!(state.capture(&mut bottom_edge, &mut top_edge));
1265 assert!(bottom_edge.is_locked());
1266 assert!(!top_edge.is_locked());
1267 }
1268 // Capture top, ignore invalid bottom
1269 {
1270 let mut bottom_edge = Hint {
1271 // Empty flags == invalid hint
1272 flags: 0,
1273 ..bottom_edge
1274 };
1275 let mut top_edge = top_edge;
1276 assert!(state.capture(&mut bottom_edge, &mut top_edge));
1277 assert!(!bottom_edge.is_locked());
1278 assert!(top_edge.is_locked());
1279 }
1280 }
1281
1282 #[test]
hint_mask_ops()1283 fn hint_mask_ops() {
1284 const MAX_BITS: usize = HINT_MASK_SIZE * 8;
1285 let all_bits = HintMask::all();
1286 for i in 0..MAX_BITS {
1287 assert!(all_bits.get(i));
1288 }
1289 let odd_bits = HintMask::new(&[0b01010101; HINT_MASK_SIZE]).unwrap();
1290 for i in 0..MAX_BITS {
1291 assert_eq!(i & 1 != 0, odd_bits.get(i));
1292 }
1293 let mut cleared_bits = odd_bits;
1294 for i in 0..MAX_BITS {
1295 if i & 1 != 0 {
1296 cleared_bits.clear(i);
1297 }
1298 }
1299 assert_eq!(cleared_bits.mask, HintMask::default().mask);
1300 }
1301
1302 #[test]
hint_mapping()1303 fn hint_mapping() {
1304 let font = FontRef::new(font_test_data::CANTARELL_VF_TRIMMED).unwrap();
1305 let cff_font = super::super::Outlines::new(&font).unwrap();
1306 let state = cff_font
1307 .subfont(0, Some(8.0), &[F2Dot14::from_f32(-1.0); 2])
1308 .unwrap()
1309 .hint_state;
1310 let mut initial_map = HintMap::new(state.scale);
1311 let mut map = HintMap::new(state.scale);
1312 // Stem hints from Cantarell-VF.otf glyph id 2
1313 let mut stems = [
1314 StemHint {
1315 min: Fixed::from_bits(1376256),
1316 max: Fixed::ZERO,
1317 ..Default::default()
1318 },
1319 StemHint {
1320 min: Fixed::from_bits(16318464),
1321 max: Fixed::from_bits(17563648),
1322 ..Default::default()
1323 },
1324 StemHint {
1325 min: Fixed::from_bits(45481984),
1326 max: Fixed::from_bits(44171264),
1327 ..Default::default()
1328 },
1329 ];
1330 map.build(
1331 &state,
1332 Some(HintMask::all()),
1333 Some(&mut initial_map),
1334 &mut stems,
1335 Fixed::ZERO,
1336 false,
1337 );
1338 // FT generates the following hint map:
1339 //
1340 // index csCoord dsCoord scale flags
1341 // 0 0.00 0.00 526 gbL
1342 // 1 249.00 250.14 524 pb
1343 // 1 268.00 238.22 592 pt
1344 // 2 694.00 750.41 524 gtL
1345 let expected_edges = [
1346 Hint {
1347 index: 0,
1348 cs_coord: Fixed::from_f64(0.0),
1349 ds_coord: Fixed::from_f64(0.0),
1350 scale: Fixed::from_bits(526),
1351 flags: GHOST_BOTTOM | LOCKED,
1352 },
1353 Hint {
1354 index: 1,
1355 cs_coord: Fixed::from_bits(16318464),
1356 ds_coord: Fixed::from_bits(131072),
1357 scale: Fixed::from_bits(524),
1358 flags: PAIR_BOTTOM,
1359 },
1360 Hint {
1361 index: 1,
1362 cs_coord: Fixed::from_bits(17563648),
1363 ds_coord: Fixed::from_bits(141028),
1364 scale: Fixed::from_bits(592),
1365 flags: PAIR_TOP,
1366 },
1367 Hint {
1368 index: 2,
1369 cs_coord: Fixed::from_bits(45481984),
1370 ds_coord: Fixed::from_bits(393216),
1371 scale: Fixed::from_bits(524),
1372 flags: GHOST_TOP | LOCKED,
1373 },
1374 ];
1375 assert_eq!(expected_edges, &map.edges[..map.len]);
1376 // And FT generates the following mappings
1377 let mappings = [
1378 // (coord in font units, expected hinted coord in device space) in 16.16
1379 (0, 0), // 0 -> 0
1380 (44302336, 382564), // 676 -> 5.828125
1381 (45481984, 393216), // 694 -> 6
1382 (16318464, 131072), // 249 -> 2
1383 (17563648, 141028), // 268 -> 2.140625
1384 (49676288, 426752), // 758 -> 6.5
1385 (56754176, 483344), // 866 -> 7.375
1386 (57868288, 492252), // 883 -> 7.5
1387 (50069504, 429896), // 764 -> 6.546875
1388 ];
1389 for (coord, expected) in mappings {
1390 assert_eq!(
1391 map.transform(Fixed::from_bits(coord)),
1392 Fixed::from_bits(expected)
1393 );
1394 }
1395 }
1396
1397 #[test]
midpoint_avoids_overflow()1398 fn midpoint_avoids_overflow() {
1399 // We encountered an overflow in the HintMap::insert midpoint
1400 // calculation for glyph id 950 at size 74 in
1401 // KawkabMono-Bold v0.501 <https://github.com/aiaf/kawkab-mono/tree/v0.501>.
1402 // Test that our midpoint function doesn't overflow when the sum of
1403 // the high 16 bits of the two values exceeds i16::MAX.
1404 let a = i16::MAX as i32;
1405 let b = a - 1;
1406 assert!(a + b > i16::MAX as i32);
1407 let mid = super::midpoint(Fixed::from_i32(a), Fixed::from_i32(b));
1408 assert_eq!((a + b) / 2, mid.to_bits() >> 16);
1409 }
1410
1411 /// HintingSink is mostly pass-through. This test captures the logic
1412 /// around omission of pending lines that match subpath start.
1413 /// See HintingSink::maybe_close_subpath for details.
1414 #[test]
hinting_sink_omits_closing_line_that_matches_start()1415 fn hinting_sink_omits_closing_line_that_matches_start() {
1416 let state = HintState {
1417 scale: Fixed::ONE,
1418 ..Default::default()
1419 };
1420 let mut path = Path::default();
1421 let mut sink = HintingSink::new(&state, &mut path);
1422 let move1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)];
1423 let line2_3 = [Fixed::from_f64(2.0), Fixed::from_f64(3.0)];
1424 let line1_2 = [Fixed::from_f64(1.0), Fixed::from_f64(2.0)];
1425 let line3_4 = [Fixed::from_f64(3.0), Fixed::from_f64(4.0)];
1426 let curve = [
1427 Fixed::from_f64(3.0),
1428 Fixed::from_f64(4.0),
1429 Fixed::from_f64(5.0),
1430 Fixed::from_f64(6.0),
1431 Fixed::from_f64(1.0),
1432 Fixed::from_f64(2.0),
1433 ];
1434 // First subpath, closing line matches start
1435 sink.move_to(move1_2[0], move1_2[1]);
1436 sink.line_to(line2_3[0], line2_3[1]);
1437 sink.line_to(line1_2[0], line1_2[1]);
1438 // Second subpath, closing line does not match start
1439 sink.move_to(move1_2[0], move1_2[1]);
1440 sink.line_to(line2_3[0], line2_3[1]);
1441 sink.line_to(line3_4[0], line3_4[1]);
1442 // Third subpath, ends with cubic. Still emits a close command
1443 // even though end point matches start.
1444 sink.move_to(move1_2[0], move1_2[1]);
1445 sink.line_to(line2_3[0], line2_3[1]);
1446 sink.curve_to(curve[0], curve[1], curve[2], curve[3], curve[4], curve[5]);
1447 sink.finish();
1448 // Subpaths always end with a close command. If a final line coincides
1449 // with the start of a subpath, it is omitted.
1450 assert_eq!(
1451 &path.0,
1452 &[
1453 // First subpath
1454 MoveTo(move1_2),
1455 LineTo(line2_3),
1456 // line1_2 is omitted
1457 Close,
1458 // Second subpath
1459 MoveTo(move1_2),
1460 LineTo(line2_3),
1461 LineTo(line3_4),
1462 Close,
1463 // Third subpath
1464 MoveTo(move1_2),
1465 LineTo(line2_3),
1466 CurveTo(curve),
1467 Close,
1468 ]
1469 );
1470 }
1471
1472 #[derive(Copy, Clone, PartialEq, Debug)]
1473 enum Command {
1474 MoveTo([Fixed; 2]),
1475 LineTo([Fixed; 2]),
1476 CurveTo([Fixed; 6]),
1477 Close,
1478 }
1479
1480 use Command::*;
1481
1482 #[derive(Default)]
1483 struct Path(Vec<Command>);
1484
1485 impl CommandSink for Path {
move_to(&mut self, x: Fixed, y: Fixed)1486 fn move_to(&mut self, x: Fixed, y: Fixed) {
1487 self.0.push(MoveTo([x, y]));
1488 }
line_to(&mut self, x: Fixed, y: Fixed)1489 fn line_to(&mut self, x: Fixed, y: Fixed) {
1490 self.0.push(LineTo([x, y]));
1491 }
curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed)1492 fn curve_to(&mut self, cx0: Fixed, cy0: Fixed, cx1: Fixed, cy1: Fixed, x: Fixed, y: Fixed) {
1493 self.0.push(CurveTo([cx0, cy0, cx1, cy1, x, y]));
1494 }
close(&mut self)1495 fn close(&mut self) {
1496 self.0.push(Close);
1497 }
1498 }
1499 }
1500