Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/tables/cff/cff2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ fn parse_char_string(
_parse_char_string(&mut ctx, data, 0, &mut parser)?;
// let _ = _parse_char_string(&mut ctx, data, 0.0, 0.0, &mut stack, 0, &mut inner_builder)?;

// CFF2 has no endchar, so the last contour ends with the charstring.
if !parser.is_first_move_to {
parser.builder.close();
}

let bbox = parser.builder.bbox;

// Check that bbox was changed.
Expand Down
34 changes: 28 additions & 6 deletions tests/tables/cff2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -334,11 +334,33 @@ fn minimal_glyph_outlines() {

let (result, path) = outline(&Cff2::new(vec![char_string]).build());

// CFF2 has no `endchar`, and the parser never closes the last contour.
assert_eq!(path, "M 100 0 L 150 50 ");
// CFF2 has no `endchar`; the last contour is closed when the charstring ends.
assert_eq!(path, "M 100 0 L 150 50 Z ");
assert_eq!(result.unwrap(), rect(100, 0, 150, 50));
}

// Every contour must be closed, the last one included: CFF1 closes it on `endchar`, which CFF2
// lacks. Rasterizers that don't close paths themselves fill an open last contour solid.
#[test]
fn every_contour_is_closed() {
let mut char_string = Vec::new();
char_string.extend_from_slice(&cs_int(100));
char_string.push(operator::HORIZONTAL_MOVE_TO);
char_string.extend_from_slice(&cs_int(50));
char_string.extend_from_slice(&cs_int(50));
char_string.push(operator::LINE_TO);
char_string.extend_from_slice(&cs_int(100));
char_string.push(operator::VERTICAL_MOVE_TO);
char_string.extend_from_slice(&cs_int(-50));
char_string.extend_from_slice(&cs_int(50));
char_string.push(operator::LINE_TO);

let (result, path) = outline(&Cff2::new(vec![char_string]).build());

assert_eq!(path, "M 100 0 L 150 50 Z M 150 150 L 100 200 Z ");
assert_eq!(result.unwrap(), rect(100, 0, 150, 200));
}

// Regression test for https://github.com/harfbuzz/ttf-parser/issues/239. The `vstore` Top DICT
// entry is optional, but `parse_char_string` used to resolve variation index 0 before running
// a single operator, which fails on an absent store — so a static CFF2 font could not outline
Expand All @@ -356,7 +378,7 @@ fn glyph_without_a_variation_store_outlines() {
font.regions = None;
let (result, path) = outline(&font.build());

assert_eq!(path, "M 100 0 L 150 50 ");
assert_eq!(path, "M 100 0 L 150 50 Z ");
assert_eq!(result.unwrap(), rect(100, 0, 150, 50));
}

Expand Down Expand Up @@ -393,7 +415,7 @@ fn blend_applies_region_deltas_to_its_operands() {

let (result, path) = outline(&Cff2::new(vec![char_string]).build());

assert_eq!(path, "M 110 0 L 160 50 ");
assert_eq!(path, "M 110 0 L 160 50 Z ");
assert_eq!(result.unwrap(), rect(110, 0, 160, 50));
}

Expand Down Expand Up @@ -475,15 +497,15 @@ fn global_subroutine_chain_within_budget_outlines() {
// The same depth without amplification is 8 invocations and must still work.
let (result, path) = outline(&fanout_font(1, 8, false));

assert_eq!(path, "M 100 0 L 150 50 ");
assert_eq!(path, "M 100 0 L 150 50 Z ");
assert_eq!(result.unwrap(), rect(100, 0, 150, 50));
}

#[test]
fn local_subroutine_chain_within_budget_outlines() {
let (result, path) = outline(&fanout_font(1, 8, true));

assert_eq!(path, "M 100 0 L 150 50 ");
assert_eq!(path, "M 100 0 L 150 50 Z ");
assert_eq!(result.unwrap(), rect(100, 0, 150, 50));
}

Expand Down