Harbor

branch main
showing the latest snapshot on main
gvar.odin 7.2 KB · Plain text
gvar.odin 0644 Raw
package font

import "core:mem"
import "core:math"

// ============================================================================
// GVAR — Glyph Variations (TrueType outline deltas)
// ============================================================================

// Apply glyph variation deltas to a set of points.
// Takes raw glyph points (x, y pairs as i16) and modifies them in-place
// based on the normalized variation coordinates.
// Returns true if deltas were applied.
apply_gvar_deltas :: proc(
	info: ^Font_Info,
	glyph_index: i32,
	coords: ^Var_Coords,
	points_x: []f32,
	points_y: []f32,
) -> bool {
	gvar := find_table(info.data, u32(info.fontstart), "gvar")
	if gvar == 0 do return false

	data := info.data[gvar:]
	version := ttULONG(data)
	if version != 0x00010000 do return false

	axis_count := i32(ttUSHORT(data[4:]))
	shared_tuple_count := i32(ttUSHORT(data[6:]))
	shared_tuples_off := ttULONG(data[8:])
	glyph_count := i32(ttUSHORT(data[12:]))
	flags := ttUSHORT(data[14:])
	gvar_data_off := ttULONG(data[16:])

	if glyph_index < 0 || glyph_index >= glyph_count do return false

	long_offsets := (flags & 1) != 0
	num_points := i32(len(points_x))

	// Get offset to this glyph's variation data
	var_off, var_end: u32
	if long_offsets {
		off_arr := data[20:]
		var_off = gvar_data_off + ttULONG(off_arr[u32(glyph_index) * 4:])
		var_end = gvar_data_off + ttULONG(off_arr[u32(glyph_index + 1) * 4:])
	} else {
		off_arr := data[20:]
		var_off = gvar_data_off + u32(ttUSHORT(off_arr[u32(glyph_index) * 2:])) * 2
		var_end = gvar_data_off + u32(ttUSHORT(off_arr[u32(glyph_index + 1) * 2:])) * 2
	}

	if var_off == var_end do return false // no variation data
	if var_off >= var_end do return false

	// Parse glyph variation data header
	gd := data[var_off:]
	tuple_count_raw := ttUSHORT(gd)
	data_offset := u32(ttUSHORT(gd[2:]))
	tuple_count := i32(tuple_count_raw & 0x0FFF)
	has_shared_points := (tuple_count_raw & 0x8000) != 0

	serialized := gd[data_offset:] // packed deltas start here
	ser_off: u32 = 0

	// Parse shared points if present
	shared_points: [256]i32
	shared_point_count: i32 = 0
	if has_shared_points {
		shared_point_count, ser_off = unpack_points(serialized, shared_points[:])
	}

	// Process each tuple
	hdr_off: u32 = 4
	for ti in 0..<tuple_count {
		var_data_size := u32(ttUSHORT(gd[hdr_off:]))
		tuple_index := ttUSHORT(gd[hdr_off + 2:])
		hdr_off += 4

		has_embedded_peak := (tuple_index & 0x8000) != 0
		has_intermediate := (tuple_index & 0x4000) != 0
		has_private_points := (tuple_index & 0x2000) != 0
		tuple_idx := i32(tuple_index & 0x0FFF)

		// Read peak tuple
		peak: [MAX_VAR_AXES]f32
		if has_embedded_peak {
			for ai in 0..<min(axis_count, MAX_VAR_AXES) {
				peak[ai] = f32(i16(ttUSHORT(gd[hdr_off:]))) / 16384.0
				hdr_off += 2
			}
		} else if tuple_idx < shared_tuple_count {
			st := data[shared_tuples_off + u32(tuple_idx) * u32(axis_count) * 2:]
			for ai in 0..<min(axis_count, MAX_VAR_AXES) {
				peak[ai] = f32(i16(ttUSHORT(st[u32(ai) * 2:]))) / 16384.0
			}
		}

		// Read intermediate tuples
		start_tuple, end_tuple: [MAX_VAR_AXES]f32
		if has_intermediate {
			for ai in 0..<min(axis_count, MAX_VAR_AXES) {
				start_tuple[ai] = f32(i16(ttUSHORT(gd[hdr_off:]))) / 16384.0
				hdr_off += 2
			}
			for ai in 0..<min(axis_count, MAX_VAR_AXES) {
				end_tuple[ai] = f32(i16(ttUSHORT(gd[hdr_off:]))) / 16384.0
				hdr_off += 2
			}
		}

		// Compute scalar for this tuple
		scalar := compute_tuple_scalar(coords, &peak, has_intermediate, &start_tuple, &end_tuple, axis_count)
		if scalar == 0 {
			ser_off += var_data_size
			continue
		}

		// Determine which points get deltas
		pt_indices: [256]i32
		pt_count: i32 = 0
		local_ser_off := ser_off
		all_points := false

		if has_private_points {
			pt_count, local_ser_off = unpack_points(serialized[ser_off:], pt_indices[:])
			local_ser_off += ser_off
			if pt_count == 0 {
				all_points = true
				pt_count = num_points
			}
		} else if shared_point_count > 0 {
			for i in 0..<min(shared_point_count, 256) {
				pt_indices[i] = shared_points[i]
			}
			pt_count = shared_point_count
		} else {
			all_points = true
			pt_count = num_points
		}

		// Unpack deltas (X then Y)
		x_deltas: [512]i16
		y_deltas: [512]i16
		capped := min(pt_count, 512)

		dx_bytes := unpack_deltas(serialized[local_ser_off:], x_deltas[:capped])
		local_ser_off += u32(dx_bytes)
		dy_bytes := unpack_deltas(serialized[local_ser_off:], y_deltas[:capped])

		// Apply deltas
		for di in 0..<capped {
			idx := di if all_points else pt_indices[di]
			if idx >= 0 && idx < num_points {
				points_x[idx] += f32(x_deltas[di]) * scalar
				points_y[idx] += f32(y_deltas[di]) * scalar
			}
		}

		ser_off += var_data_size
	}

	return true
}

// ============================================================================
// Internal helpers
// ============================================================================

@(private)
compute_tuple_scalar :: proc(
	coords: ^Var_Coords,
	peak: ^[MAX_VAR_AXES]f32,
	has_intermediate: bool,
	start_tuple: ^[MAX_VAR_AXES]f32,
	end_tuple: ^[MAX_VAR_AXES]f32,
	axis_count: i32,
) -> f32 {
	scalar: f32 = 1.0

	for ai in 0..<min(axis_count, MAX_VAR_AXES) {
		v := coords[ai]
		p := peak[ai]

		if p == 0 do continue
		if v == p do continue

		if has_intermediate {
			s := start_tuple[ai]
			e := end_tuple[ai]
			if v < s || v > e do return 0
			if v < p {
				if p != s do scalar *= (v - s) / (p - s)
			} else {
				if p != e do scalar *= (e - v) / (e - p)
			}
		} else {
			if v == 0 do return 0
			if (v < 0 && p > 0) || (v > 0 && p < 0) do return 0
			if (v < 0 && v < p) || (v > 0 && v > p) do return 0
			scalar *= v / p
		}
	}

	return scalar
}

// Unpack run-length encoded point indices.
// Returns (count, bytes_consumed). Count=0 means "all points".
@(private)
unpack_points :: proc(data: [^]u8, points: []i32) -> (i32, u32) {
	count: i32
	off: u32 = 0

	// First byte(s): total count
	b0 := data[0]
	off = 1
	if b0 & 0x80 != 0 {
		count = (i32(b0 & 0x7F) << 8) | i32(data[1])
		off = 2
	} else {
		count = i32(b0)
	}

	if count == 0 do return 0, off // "all points"

	n: i32 = 0
	for n < count && n < i32(len(points)) {
		ctrl := data[off]
		off += 1
		run_count := i32(ctrl & 0x7F) + 1
		is_words := (ctrl & 0x80) != 0

		for ri in 0..<run_count {
			if n >= i32(len(points)) do break
			if is_words {
				val := i32(ttUSHORT(data[off:]))
				off += 2
				points[n] = val + (points[n - 1] if n > 0 else 0)
			} else {
				val := i32(data[off])
				off += 1
				points[n] = val + (points[n - 1] if n > 0 else 0)
			}
			n += 1
		}
	}

	return count, off
}

// Unpack run-length encoded deltas. Returns bytes consumed.
@(private)
unpack_deltas :: proc(data: [^]u8, deltas: []i16) -> i32 {
	off: i32 = 0
	n: i32 = 0
	count := i32(len(deltas))

	for n < count {
		ctrl := data[off]
		off += 1
		run_count := i32(ctrl & 0x3F) + 1

		if ctrl & 0x80 != 0 {
			// DELTAS_ARE_ZERO
			for ri in 0..<run_count {
				if n >= count do break
				deltas[n] = 0
				n += 1
			}
		} else if ctrl & 0x40 != 0 {
			// DELTAS_ARE_WORDS (i16)
			for ri in 0..<run_count {
				if n >= count do break
				deltas[n] = i16(ttUSHORT(data[off:]))
				off += 2
				n += 1
			}
		} else {
			// DELTAS_ARE_BYTES (i8)
			for ri in 0..<run_count {
				if n >= count do break
				deltas[n] = i16(i8(data[off]))
				off += 1
				n += 1
			}
		}
	}

	return off
}