use crate::api::path::Path;
use crate::font::FontError;
use crate::font::tables::{ParsedTables, be_i8, be_i16, be_u8, be_u16};
const MAX_COMPOUND_DEPTH: u32 = 16;
const ON_CURVE: u8 = 0x01;
const X_SHORT: u8 = 0x02;
const Y_SHORT: u8 = 0x04;
const REPEAT_FLAG: u8 = 0x08;
const X_SAME_OR_POS: u8 = 0x10;
const Y_SAME_OR_POS: u8 = 0x20;
const ARG_1_AND_2_ARE_WORDS: u16 = 0x0001;
const ARGS_ARE_XY_VALUES: u16 = 0x0002;
const WE_HAVE_A_SCALE: u16 = 0x0008;
const MORE_COMPONENTS: u16 = 0x0020;
const WE_HAVE_AN_X_AND_Y_SCALE: u16 = 0x0040;
const WE_HAVE_A_TWO_BY_TWO: u16 = 0x0080;
#[derive(Clone, Copy)]
struct GlyphTransform {
offset_x: f64,
offset_y: f64,
scale: f64,
m00: f64,
m01: f64,
m10: f64,
m11: f64,
}
impl GlyphTransform {
fn new(offset_x: f64, offset_y: f64, scale: f64) -> Self {
Self {
offset_x,
offset_y,
scale,
m00: 1.0,
m01: 0.0,
m10: 0.0,
m11: 1.0,
}
}
#[inline]
fn apply(&self, x_design: i32, y_design: i32) -> (f64, f64) {
let xd = x_design as f64;
let yd = y_design as f64;
let tx = xd * self.m00 + yd * self.m01;
let ty = xd * self.m10 + yd * self.m11;
(
tx * self.scale + self.offset_x,
-ty * self.scale + self.offset_y,
)
}
}
pub(crate) fn append_glyph_outline(
glyph_id: u16,
offset_x: f64,
offset_y: f64,
scale: f64,
tables: &ParsedTables,
data: &[u8],
path: &mut Path,
) -> Result<(), FontError> {
let xform = GlyphTransform::new(offset_x, offset_y, scale);
append_glyph_recursive(glyph_id, xform, tables, data, path, 0)
}
fn append_glyph_recursive(
glyph_id: u16,
xform: GlyphTransform,
tables: &ParsedTables,
data: &[u8],
path: &mut Path,
depth: u32,
) -> Result<(), FontError> {
if depth > MAX_COMPOUND_DEPTH {
return Err(FontError::InvalidData("compound glyph recursion too deep"));
}
let gid = glyph_id as usize;
if gid + 1 >= tables.loca_offsets.len() {
return Err(FontError::InvalidData("glyph id out of range"));
}
let glyf_start = tables.loca_offsets[gid];
let glyf_end = tables.loca_offsets[gid + 1];
if glyf_start == glyf_end {
return Ok(());
}
let abs_start = tables.glyf_offset as usize + glyf_start as usize;
let abs_end = tables.glyf_offset as usize + glyf_end as usize;
if abs_end > data.len() || abs_start >= abs_end {
return Err(FontError::InvalidData("glyph data out of range"));
}
let glyph_data = &data[abs_start..abs_end];
if glyph_data.len() < 10 {
return Err(FontError::InvalidData("glyph header too short"));
}
let number_of_contours = be_i16(glyph_data, 0)?;
if number_of_contours >= 0 {
parse_simple_glyph(glyph_data, number_of_contours as usize, xform, path)
} else {
parse_compound_glyph(glyph_data, xform, tables, data, path, depth)
}
}
fn parse_simple_glyph(
glyph_data: &[u8],
number_of_contours: usize,
xform: GlyphTransform,
path: &mut Path,
) -> Result<(), FontError> {
if number_of_contours == 0 {
return Ok(());
}
let end_pts_off = 10;
let end_pts_end = end_pts_off + number_of_contours * 2;
if end_pts_end + 2 > glyph_data.len() {
return Err(FontError::InvalidData(
"simple glyph contour data too short",
));
}
let mut end_pts = Vec::with_capacity(number_of_contours);
for i in 0..number_of_contours {
end_pts.push(be_u16(glyph_data, end_pts_off + i * 2)? as usize);
}
let num_points = end_pts.last().map_or(0, |&e| e + 1);
if num_points == 0 {
return Ok(());
}
let instr_len = be_u16(glyph_data, end_pts_end)? as usize;
let flags_off = end_pts_end + 2 + instr_len;
if flags_off > glyph_data.len() {
return Err(FontError::InvalidData(
"simple glyph instructions exceed data",
));
}
let mut flags = Vec::with_capacity(num_points);
let mut cursor = flags_off;
while flags.len() < num_points {
if cursor >= glyph_data.len() {
return Err(FontError::InvalidData("simple glyph flags truncated"));
}
let flag = glyph_data[cursor];
cursor += 1;
flags.push(flag);
if flag & REPEAT_FLAG != 0 {
if cursor >= glyph_data.len() {
return Err(FontError::InvalidData(
"simple glyph repeat count truncated",
));
}
let repeat_count = glyph_data[cursor] as usize;
cursor += 1;
for _ in 0..repeat_count {
if flags.len() >= num_points {
break;
}
flags.push(flag);
}
}
}
let mut x_coords = Vec::with_capacity(num_points);
let mut x: i32 = 0;
for &flag in &flags {
if flag & X_SHORT != 0 {
if cursor >= glyph_data.len() {
return Err(FontError::InvalidData("simple glyph x data truncated"));
}
let dx = glyph_data[cursor] as i32;
cursor += 1;
if flag & X_SAME_OR_POS != 0 {
x += dx;
} else {
x -= dx;
}
} else if flag & X_SAME_OR_POS == 0 {
if cursor + 1 >= glyph_data.len() {
return Err(FontError::InvalidData("simple glyph x data truncated"));
}
let dx = i16::from_be_bytes([glyph_data[cursor], glyph_data[cursor + 1]]) as i32;
cursor += 2;
x += dx;
}
x_coords.push(x);
}
let mut y_coords = Vec::with_capacity(num_points);
let mut y: i32 = 0;
for &flag in &flags {
if flag & Y_SHORT != 0 {
if cursor >= glyph_data.len() {
return Err(FontError::InvalidData("simple glyph y data truncated"));
}
let dy = glyph_data[cursor] as i32;
cursor += 1;
if flag & Y_SAME_OR_POS != 0 {
y += dy;
} else {
y -= dy;
}
} else if flag & Y_SAME_OR_POS == 0 {
if cursor + 1 >= glyph_data.len() {
return Err(FontError::InvalidData("simple glyph y data truncated"));
}
let dy = i16::from_be_bytes([glyph_data[cursor], glyph_data[cursor + 1]]) as i32;
cursor += 2;
y += dy;
}
y_coords.push(y);
}
let mut contour_start = 0;
for &end_pt in &end_pts {
let contour_end = end_pt + 1;
if contour_end > num_points || contour_start >= contour_end {
contour_start = contour_end;
continue;
}
emit_contour(
&flags[contour_start..contour_end],
&x_coords[contour_start..contour_end],
&y_coords[contour_start..contour_end],
xform,
path,
);
contour_start = contour_end;
}
Ok(())
}
fn emit_contour(
flags: &[u8],
x_coords: &[i32],
y_coords: &[i32],
xform: GlyphTransform,
path: &mut Path,
) {
let n = flags.len();
if n == 0 {
return;
}
let tx = |i: usize| -> (f64, f64) { xform.apply(x_coords[i], y_coords[i]) };
let on_curve = |i: usize| -> bool { flags[i] & ON_CURVE != 0 };
let (start_x, start_y, first_idx);
if on_curve(0) {
let (sx, sy) = tx(0);
start_x = sx;
start_y = sy;
first_idx = 1;
} else if on_curve(n - 1) {
let (sx, sy) = tx(n - 1);
start_x = sx;
start_y = sy;
first_idx = 0;
} else {
let (x0, y0) = tx(0);
let (x1, y1) = tx(n - 1);
start_x = (x0 + x1) * 0.5;
start_y = (y0 + y1) * 0.5;
first_idx = 0;
};
path.move_to(start_x, start_y);
let mut i = first_idx;
while i < n {
if on_curve(i) {
let (px, py) = tx(i);
path.line_to(px, py);
i += 1;
} else {
let (cpx, cpy) = tx(i);
let next = (i + 1) % n;
if next == first_idx && first_idx == 0 && !on_curve(0) && !on_curve(n - 1) {
path.quad_to(cpx, cpy, start_x, start_y);
i += 1;
} else if next < n && on_curve(next) {
let (ex, ey) = tx(next);
path.quad_to(cpx, cpy, ex, ey);
i += 2;
} else {
let (nx, ny) = tx(next);
let mid_x = (cpx + nx) * 0.5;
let mid_y = (cpy + ny) * 0.5;
path.quad_to(cpx, cpy, mid_x, mid_y);
i += 1;
}
}
}
path.close();
}
fn parse_compound_glyph(
glyph_data: &[u8],
xform: GlyphTransform,
tables: &ParsedTables,
data: &[u8],
path: &mut Path,
depth: u32,
) -> Result<(), FontError> {
let mut cursor = 10usize;
loop {
if cursor + 4 > glyph_data.len() {
return Err(FontError::InvalidData("compound glyph truncated"));
}
let comp_flags = be_u16(glyph_data, cursor)?;
let glyph_index = be_u16(glyph_data, cursor + 2)?;
cursor += 4;
let (arg1, arg2);
if comp_flags & ARG_1_AND_2_ARE_WORDS != 0 {
if cursor + 4 > glyph_data.len() {
return Err(FontError::InvalidData("compound args truncated"));
}
if comp_flags & ARGS_ARE_XY_VALUES != 0 {
arg1 = be_i16(glyph_data, cursor)? as f64;
arg2 = be_i16(glyph_data, cursor + 2)? as f64;
} else {
arg1 = be_u16(glyph_data, cursor)? as f64;
arg2 = be_u16(glyph_data, cursor + 2)? as f64;
}
cursor += 4;
} else {
if cursor + 2 > glyph_data.len() {
return Err(FontError::InvalidData("compound args truncated"));
}
if comp_flags & ARGS_ARE_XY_VALUES != 0 {
arg1 = be_i8(glyph_data, cursor)? as f64;
arg2 = be_i8(glyph_data, cursor + 1)? as f64;
} else {
arg1 = be_u8(glyph_data, cursor)? as f64;
arg2 = be_u8(glyph_data, cursor + 1)? as f64;
}
cursor += 2;
}
let (mut cm00, mut cm01, mut cm10, mut cm11) = (1.0f64, 0.0f64, 0.0f64, 1.0f64);
if comp_flags & WE_HAVE_A_SCALE != 0 {
if cursor + 2 > glyph_data.len() {
return Err(FontError::InvalidData("compound scale truncated"));
}
let s = f2dot14(be_i16(glyph_data, cursor)?);
cursor += 2;
cm00 = s;
cm11 = s;
} else if comp_flags & WE_HAVE_AN_X_AND_Y_SCALE != 0 {
if cursor + 4 > glyph_data.len() {
return Err(FontError::InvalidData("compound xy scale truncated"));
}
cm00 = f2dot14(be_i16(glyph_data, cursor)?);
cm11 = f2dot14(be_i16(glyph_data, cursor + 2)?);
cursor += 4;
} else if comp_flags & WE_HAVE_A_TWO_BY_TWO != 0 {
if cursor + 8 > glyph_data.len() {
return Err(FontError::InvalidData("compound 2x2 matrix truncated"));
}
cm00 = f2dot14(be_i16(glyph_data, cursor)?);
cm01 = f2dot14(be_i16(glyph_data, cursor + 2)?);
cm10 = f2dot14(be_i16(glyph_data, cursor + 4)?);
cm11 = f2dot14(be_i16(glyph_data, cursor + 6)?);
cursor += 8;
}
let new_m00 = xform.m00 * cm00 + xform.m01 * cm10;
let new_m01 = xform.m00 * cm01 + xform.m01 * cm11;
let new_m10 = xform.m10 * cm00 + xform.m11 * cm10;
let new_m11 = xform.m10 * cm01 + xform.m11 * cm11;
let dx = if comp_flags & ARGS_ARE_XY_VALUES != 0 {
arg1
} else {
0.0
};
let dy = if comp_flags & ARGS_ARE_XY_VALUES != 0 {
arg2
} else {
0.0
};
let child_xform = GlyphTransform {
offset_x: xform.offset_x + (dx * xform.m00 + dy * xform.m01) * xform.scale,
offset_y: xform.offset_y - (dx * xform.m10 + dy * xform.m11) * xform.scale,
scale: xform.scale,
m00: new_m00,
m01: new_m01,
m10: new_m10,
m11: new_m11,
};
append_glyph_recursive(glyph_index, child_xform, tables, data, path, depth + 1)?;
if comp_flags & MORE_COMPONENTS == 0 {
break;
}
}
Ok(())
}
fn f2dot14(value: i16) -> f64 {
value as f64 / 16384.0
}