Harbor

branch main
showing the latest snapshot on main
shapes.odin 23.0 KB · Plain text
font/shapes.odin 0644 Raw
package font

import "core:mem"
import "core:math"
import "core:fmt"
import "base:runtime"

DEBUG_TRACK :: false

@(private)
track_vertex :: #force_inline proc(c: ^CS_Ctx, x: i32, y: i32) {
	when DEBUG_TRACK {
		if x < 20 {
			fmt.printf("track: x=%d y=%d (current min_x=%d, started=%v)\n", x, y, c.min_x, c.started)
		}
	}
	if x > c.max_x || !c.started { c.max_x = x }
	if y > c.max_y || !c.started { c.max_y = y }
	if x < c.min_x || !c.started { c.min_x = x }
	if y < c.min_y || !c.started { c.min_y = y }
	c.started = true
}

@(private)
csctx_v :: #force_inline proc(c: ^CS_Ctx, type: u8, x: i32, y: i32, cx: i32, cy: i32, cx1: i32, cy1: i32) {
	if c.bounds {
		track_vertex(c, x, y)
		if type == VCUBIC {
			track_vertex(c, cx, cy)
			track_vertex(c, cx1, cy1)
		}
	} else {
		set_vertex(&c.pvertices[c.num_vertices], type, x, y, cx, cy)
		c.pvertices[c.num_vertices].cx1 = i16(cx1)
		c.pvertices[c.num_vertices].cy1 = i16(cy1)
	}
	c.num_vertices += 1
}

@(private)
csctx_close_shape :: #force_inline proc(ctx: ^CS_Ctx) {
	if ctx.first_x != ctx.x || ctx.first_y != ctx.y {
		csctx_v(ctx, VLINE, i32(ctx.first_x), i32(ctx.first_y), 0, 0, 0, 0)
	}
}

@(private)
csctx_rmove_to :: #force_inline proc(ctx: ^CS_Ctx, dx: f32, dy: f32) {
	csctx_close_shape(ctx)
	ctx.first_x = ctx.x + dx
	ctx.first_y = ctx.y + dy
	ctx.x = ctx.first_x
	ctx.y = ctx.first_y
	when DEBUG_TRACK {
		fmt.printf("  rmoveto: dx=%.1f dy=%.1f -> (%.1f, %.1f)\n", dx, dy, ctx.x, ctx.y)
	}
	csctx_v(ctx, VMOVE, i32(ctx.x), i32(ctx.y), 0, 0, 0, 0)
}

@(private)
csctx_rline_to :: #force_inline proc(ctx: ^CS_Ctx, dx: f32, dy: f32) {
	ctx.x += dx
	ctx.y += dy
	when DEBUG_TRACK {
		if i32(ctx.x) < 20 {
			fmt.printf("  rlineto: dx=%.1f -> x=%.1f\n", dx, ctx.x)
		}
	}
	csctx_v(ctx, VLINE, i32(ctx.x), i32(ctx.y), 0, 0, 0, 0)
}

@(private)
csctx_rccurve_to :: #force_inline proc(ctx: ^CS_Ctx, dx1: f32, dy1: f32, dx2: f32, dy2: f32, dx3: f32, dy3: f32) {
	cx1 := ctx.x + dx1
	cy1 := ctx.y + dy1
	cx2 := cx1 + dx2
	cy2 := cy1 + dy2
	ctx.x = cx2 + dx3
	ctx.y = cy2 + dy3
	when DEBUG_TRACK {
		min_x := min(cx1, min(cx2, ctx.x))
		if i32(min_x) <= 15 {
			fmt.printf("  curve: cx1=%.2f cx2=%.2f end=%.2f (min=%.2f)\n", cx1, cx2, ctx.x, min_x)
		}
	}
	csctx_v(ctx, VCUBIC, i32(ctx.x), i32(ctx.y), i32(cx1), i32(cy1), i32(cx2), i32(cy2))
}

// Type 2 CharString interpreter
@(private)
run_charstring :: proc(info: ^Font_Info, glyph_index: i32, c: ^CS_Ctx) -> bool {
	in_header := true
	maskbits: i32 = 0
	subr_stack_height: i32 = 0
	sp: i32 = 0
	clear_stack: bool
	b0: u8
	has_subrs := false

	s: [48]f32
	subr_stack: [10]Buf
	subrs := info.subrs

	// If CID font, get the appropriate subrs for this glyph
	if info.fdselect.size != 0 {
		subrs = cid_get_glyph_subrs(info, glyph_index)
	}

	// Get the charstring for this glyph
	b := cff_index_get(info.charstrings, glyph_index)

	for b.cursor < b.size {
		i: i32 = 0
		clear_stack = true
		b0 = buf_get8(&b)

		switch b0 {
		// Push number onto stack
		case 1, 3, 18, 23: // hstem, vstem, hstemhm, vstemhm
			maskbits += (sp / 2)
			clear_stack = true
		case 19, 20: // hintmask, cntrmask
			if in_header {
				maskbits += (sp / 2)
			}
			in_header = false
			// Skip the hint mask bytes
			buf_skip(&b, (maskbits + 7) / 8)
			clear_stack = true
		case 14: // endchar
			csctx_close_shape(c)
			return true
		case 21: // rmoveto
			in_header = false
			if sp < 2 do return false
			csctx_rmove_to(c, s[sp-2], s[sp-1])
			clear_stack = true
		case 4: // vmoveto
			in_header = false
			if sp < 1 do return false
			csctx_rmove_to(c, 0, s[sp-1])
			clear_stack = true
		case 22: // hmoveto
			in_header = false
			if sp < 1 do return false
			csctx_rmove_to(c, s[sp-1], 0)
			clear_stack = true
		case 5: // rlineto
			if sp < 2 do return false
			for i < sp {
				csctx_rline_to(c, s[i], s[i+1])
				i += 2
			}
			clear_stack = true
		case 7: // vlineto
			if sp < 1 do return false
			for i < sp {
				if (i & 1) == 0 {
					csctx_rline_to(c, 0, s[i])
				} else {
					csctx_rline_to(c, s[i], 0)
				}
				i += 1
			}
			clear_stack = true
		case 6: // hlineto
			if sp < 1 do return false
			for i < sp {
				if (i & 1) == 0 {
					csctx_rline_to(c, s[i], 0)
				} else {
					csctx_rline_to(c, 0, s[i])
				}
				i += 1
			}
			clear_stack = true
		case 8: // rrcurveto
			if sp < 6 do return false
			for i + 6 <= sp {
				csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5])
				i += 6
			}
			clear_stack = true
		case 24: // rcurveline
			if sp < 8 do return false
			for i + 6 <= sp - 2 {
				csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5])
				i += 6
			}
			if i + 2 > sp do return false
			csctx_rline_to(c, s[i], s[i+1])
			clear_stack = true
		case 25: // rlinecurve
			if sp < 8 do return false
			for i + 2 <= sp - 6 {
				csctx_rline_to(c, s[i], s[i+1])
				i += 2
			}
			if i + 6 > sp do return false
			csctx_rccurve_to(c, s[i], s[i+1], s[i+2], s[i+3], s[i+4], s[i+5])
			clear_stack = true
		case 26: // vvcurveto
			if sp < 4 do return false
			f: f32 = 0.0
			if (sp & 1) != 0 {
				f = s[i]
				i += 1
			}
			for i + 4 <= sp {
				csctx_rccurve_to(c, f, s[i], s[i+1], s[i+2], 0, s[i+3])
				f = 0
				i += 4
			}
			clear_stack = true
		case 27: // hhcurveto
			if sp < 4 do return false
			f: f32 = 0.0
			if (sp & 1) != 0 {
				f = s[i]
				i += 1
			}
			for i + 4 <= sp {
				csctx_rccurve_to(c, s[i], f, s[i+1], s[i+2], s[i+3], 0)
				f = 0
				i += 4
			}
			clear_stack = true
		case 30: // vhcurveto
			if sp < 4 do return false
			for {
				if i + 4 > sp do break
				csctx_rccurve_to(c, 0, s[i], s[i+1], s[i+2], s[i+3], s[i + 4] if sp - i == 5 else 0)
				i += 4
				if i + 4 > sp do break
				csctx_rccurve_to(c, s[i], 0, s[i+1], s[i+2], s[i + 4] if sp - i == 5 else 0, s[i+3])
				i += 4
			}
			clear_stack = true
		case 31: // hvcurveto
			if sp < 4 do return false
			for {
				if i + 4 > sp do break
				csctx_rccurve_to(c, s[i], 0, s[i+1], s[i+2], s[i + 4] if sp - i == 5 else 0, s[i+3])
				i += 4
				if i + 4 > sp do break
				csctx_rccurve_to(c, 0, s[i], s[i+1], s[i+2], s[i+3], s[i + 4] if sp - i == 5 else 0)
				i += 4
			}
			clear_stack = true
		case 10: // callsubr
			if sp < 1 do return false
			if subr_stack_height >= 10 do return false
			v := i32(s[sp-1])
			sp -= 1
			clear_stack = false
			subr_stack[subr_stack_height] = b
			subr_stack_height += 1
			b = get_subr(subrs, v)
			when DEBUG_TRACK {
				fmt.printf("  callsubr %d -> size=%d\n", v, b.size)
			}
			if b.size == 0 do return false
		case 29: // callgsubr
			if sp < 1 do return false
			if subr_stack_height >= 10 do return false
			v := i32(s[sp-1])
			sp -= 1
			clear_stack = false
			subr_stack[subr_stack_height] = b
			subr_stack_height += 1
			b = get_subr(info.gsubrs, v)
			if b.size == 0 do return false
		case 11: // return
			if subr_stack_height <= 0 do return false
			subr_stack_height -= 1
			b = subr_stack[subr_stack_height]
			clear_stack = false
		case 12:
			// Two-byte operators
			b1 := buf_get8(&b)
			switch b1 {
			case 34: // hflex
				if sp < 7 do return false
				csctx_rccurve_to(c, s[0], 0, s[1], s[2], s[3], 0)
				csctx_rccurve_to(c, s[4], 0, s[5], -s[2], s[6], 0)
				clear_stack = true
			case 35: // flex
				if sp < 13 do return false
				csctx_rccurve_to(c, s[0], s[1], s[2], s[3], s[4], s[5])
				csctx_rccurve_to(c, s[6], s[7], s[8], s[9], s[10], s[11])
				clear_stack = true
			case 36: // hflex1
				if sp < 9 do return false
				csctx_rccurve_to(c, s[0], s[1], s[2], s[3], s[4], 0)
				csctx_rccurve_to(c, s[5], 0, s[6], s[7], s[8], -(s[1]+s[3]+s[7]))
				clear_stack = true
			case 37: // flex1
				if sp < 11 do return false
				dx := s[0]+s[2]+s[4]+s[6]+s[8]
				dy := s[1]+s[3]+s[5]+s[7]+s[9]
				if abs(dx) > abs(dy) {
					csctx_rccurve_to(c, s[0], s[1], s[2], s[3], s[4], s[5])
					csctx_rccurve_to(c, s[6], s[7], s[8], s[9], s[10], -dy)
				} else {
					csctx_rccurve_to(c, s[0], s[1], s[2], s[3], s[4], s[5])
					csctx_rccurve_to(c, s[6], s[7], s[8], s[9], -dx, s[10])
				}
				clear_stack = true
			case:
				return false
			}
		case:
			if b0 != 255 && b0 != 28 && (b0 < 32 || b0 > 254) {
				return false
			}
			// Push number onto stack
			if b0 == 255 {
				f := f32(i32(buf_get32(&b))) / 65536.0
				if sp >= 48 do return false
				s[sp] = f
				sp += 1
				clear_stack = false
			} else if b0 == 28 {
				f := f32(i16(buf_get16(&b)))
				if sp >= 48 do return false
				s[sp] = f
				sp += 1
				clear_stack = false
			} else if b0 >= 32 && b0 <= 246 {
				f := f32(i32(b0) - 139)
				if sp >= 48 do return false
				s[sp] = f
				sp += 1
				clear_stack = false
			} else if b0 >= 247 && b0 <= 250 {
				f := f32((i32(b0) - 247) * 256 + i32(buf_get8(&b)) + 108)
				if sp >= 48 do return false
				s[sp] = f
				sp += 1
				clear_stack = false
			} else if b0 >= 251 && b0 <= 254 {
				f := f32(-(i32(b0) - 251) * 256 - i32(buf_get8(&b)) - 108)
				if sp >= 48 do return false
				s[sp] = f
				sp += 1
				clear_stack = false
			}
		}

		if clear_stack {
			sp = 0
		}
	}

	return false
}

// CFF glyph info extraction (bounding box)
@(private)
get_glyph_info_t2 :: proc(info: ^Font_Info, glyph_index: i32, x0: ^i32, y0: ^i32, x1: ^i32, y1: ^i32) -> i32 {
	c := CS_Ctx{bounds = true}
	r := run_charstring(info, glyph_index, &c)
	when DEBUG_TRACK {
		if c.min_x <= 20 {
			fmt.printf("  glyph %d: vertices=%d box=(%d,%d)-(%d,%d)\n",
				glyph_index, c.num_vertices, c.min_x, c.min_y, c.max_x, c.max_y)
		}
	}
	if x0 != nil { x0^ = c.min_x if r else 0 }
	if y0 != nil { y0^ = c.min_y if r else 0 }
	if x1 != nil { x1^ = c.max_x if r else 0 }
	if y1 != nil { y1^ = c.max_y if r else 0 }
	return c.num_vertices if r else 0
}

// CFF glyph shape extraction
@(private)
get_glyph_shape_t2 :: proc(info: ^Font_Info, glyph_index: i32, pvertices: ^^Vertex) -> i32 {
	// First pass: count vertices
	count_ctx := CS_Ctx{bounds = true}
	if !run_charstring(info, glyph_index, &count_ctx) {
		pvertices^ = nil
		return 0
	}

	if count_ctx.num_vertices == 0 {
		pvertices^ = nil
		return 0
	}

	// Allocate vertices
	ptr, _ := mem.alloc(int(count_ctx.num_vertices) * size_of(Vertex))
	if ptr == nil {
		pvertices^ = nil
		return 0
	}

	// Second pass: fill vertices
	output_ctx := CS_Ctx{bounds = false, pvertices = ([^]Vertex)(ptr)}
	if !run_charstring(info, glyph_index, &output_ctx) {
		mem.free(ptr)
		pvertices^ = nil
		return 0
	}

	pvertices^ = ([^]Vertex)(ptr)
	return output_ctx.num_vertices
}

get_glyph_box :: proc(info: ^Font_Info, glyph_index: i32, x0: ^i32, y0: ^i32, x1: ^i32, y1: ^i32) -> bool {
	// Use cached font type for fast dispatch (eliminates repeated cff.size checks)
	if info.is_cff {
		get_glyph_info_t2(info, glyph_index, x0, y0, x1, y1)
		return true
	}

	g := get_glyf_offset(info, glyph_index)
	if g < 0 do return false

	if x0 != nil do x0^ = i32(ttSHORT(info.data[u32(g)+2:]))
	if y0 != nil do y0^ = i32(ttSHORT(info.data[u32(g)+4:]))
	if x1 != nil do x1^ = i32(ttSHORT(info.data[u32(g)+6:]))
	if y1 != nil do y1^ = i32(ttSHORT(info.data[u32(g)+8:]))

	return true
}

get_codepoint_box :: proc(info: ^Font_Info, codepoint: i32, x0: ^i32, y0: ^i32, x1: ^i32, y1: ^i32) -> bool {
	return get_glyph_box(info, find_glyph_index(info, codepoint), x0, y0, x1, y1)
}

// Phase 1.5 Functions - Glyph Shape Extraction

@(private)
set_vertex :: proc(v: ^Vertex, type: u8, x: i32, y: i32, cx: i32, cy: i32) {
	v.type = type
	v.x = i16(x)
	v.y = i16(y)
	v.cx = i16(cx)
	v.cy = i16(cy)
}

@(private)
close_shape :: proc(vertices: [^]Vertex, num_vertices: i32, was_off: bool, start_off: bool,
	sx: i32, sy: i32, scx: i32, scy: i32, cx: i32, cy: i32) -> i32 {
	n := num_vertices
	if start_off {
		if was_off {
			set_vertex(&vertices[n], VCURVE, (cx+scx)>>1, (cy+scy)>>1, cx, cy)
			n += 1
		}
		set_vertex(&vertices[n], VCURVE, sx, sy, scx, scy)
		n += 1
	} else {
		if was_off {
			set_vertex(&vertices[n], VCURVE, sx, sy, cx, cy)
			n += 1
		} else {
			set_vertex(&vertices[n], VLINE, sx, sy, 0, 0)
			n += 1
		}
	}
	return n
}

// Count vertices for a TrueType glyph without allocating (for compound glyph pre-allocation)
@(private)
count_glyph_shape_tt :: proc(info: ^Font_Info, glyph_index: i32) -> i32 {
	data := info.data
	g := get_glyf_offset(info, glyph_index)
	if g < 0 do return 0

	number_of_contours := ttSHORT(data[u32(g):])

	if number_of_contours > 0 {
		// Simple glyph: vertex count is bounded by n + 2*number_of_contours
		end_pts_of_contours := data[u32(g) + 10:]
		n := i32(1 + ttUSHORT(end_pts_of_contours[u32(number_of_contours)*2-2:]))
		return n + 2*i32(number_of_contours)
	} else if number_of_contours < 0 {
		// Compound glyph: sum vertex counts of all components
		total: i32 = 0
		more := true
		comp := data[u32(g) + 10:]

		for more {
			flags := ttSHORT(comp)
			comp = comp[2:]
			gidx := ttSHORT(comp)
			comp = comp[2:]

			// Skip transform data based on flags
			if flags & COMP_ARGS_ARE_XY_VALUES != 0 {
				if flags & COMP_ARG_1_AND_2_ARE_WORDS != 0 {
					comp = comp[4:]
				} else {
					comp = comp[2:]
				}
			}
			if flags & COMP_WE_HAVE_A_SCALE != 0 {
				comp = comp[2:]
			} else if flags & COMP_WE_HAVE_AN_X_AND_Y_SCALE != 0 {
				comp = comp[4:]
			} else if flags & COMP_WE_HAVE_A_TWO_BY_TWO != 0 {
				comp = comp[8:]
			}

			// Recursively count component vertices
			total += count_glyph_shape_tt(info, i32(gidx))

			more = (flags & COMP_MORE_COMPONENTS) != 0
		}
		return total
	}
	return 0
}

@(private)
get_glyph_shape_tt :: proc(info: ^Font_Info, glyph_index: i32, pvertices: ^^Vertex) -> i32 {
	data := info.data
	vertices: [^]Vertex = nil
	num_vertices: i32 = 0

	g := get_glyf_offset(info, glyph_index)

	pvertices^ = nil

	if g < 0 do return 0

	number_of_contours := ttSHORT(data[u32(g):])

	if number_of_contours > 0 {
		// Simple glyph
		flags: u8 = 0
		flagcount: u8 = 0
		end_pts_of_contours := data[u32(g) + 10:]
		ins := i32(ttUSHORT(data[u32(g) + 10 + u32(number_of_contours) * 2:]))
		points := data[u32(g) + 10 + u32(number_of_contours) * 2 + 2 + u32(ins):]

		n := i32(1 + ttUSHORT(end_pts_of_contours[u32(number_of_contours)*2-2:]))

		m := n + 2*i32(number_of_contours)  // loose bound on vertices needed
		ptr, _ := mem.alloc(int(m) * size_of(Vertex))
		vertices = ([^]Vertex)(ptr)
		if vertices == nil do return 0

		next_move: i32 = 0
		flagcount = 0

		// First pass: load uninterpreted data into allocated array
		// shifted to the end so we won't overwrite it
		off := m - n  // starting offset

		// Load flags
		for i in 0..<n {
			if flagcount == 0 {
				flags = points[0]
				points = points[1:]
				if flags & GLYPH_REPEAT != 0 {
					flagcount = points[0]
					points = points[1:]
				}
			} else {
				flagcount -= 1
			}
			vertices[off+i].type = flags
		}

		// Load x coordinates
		x: i32 = 0
		for i in 0..<n {
			flags = vertices[off+i].type
			if flags & GLYPH_X_SHORT_VECTOR != 0 {
				dx := i16(points[0])
				points = points[1:]
				x += i32(dx) if flags & GLYPH_X_IS_SAME != 0 else -i32(dx)
			} else {
				if flags & GLYPH_X_IS_SAME == 0 {
					x = x + i32(i16(points[0])*256 + i16(points[1]))
					points = points[2:]
				}
			}
			vertices[off+i].x = i16(x)
		}

		// Load y coordinates
		y: i32 = 0
		for i in 0..<n {
			flags = vertices[off+i].type
			if flags & GLYPH_Y_SHORT_VECTOR != 0 {
				dy := i16(points[0])
				points = points[1:]
				y += i32(dy) if flags & GLYPH_Y_IS_SAME != 0 else -i32(dy)
			} else {
				if flags & GLYPH_Y_IS_SAME == 0 {
					y = y + i32(i16(points[0])*256 + i16(points[1]))
					points = points[2:]
				}
			}
			vertices[off+i].y = i16(y)
		}

		// Convert to our format
		num_vertices = 0
		sx, sy, cx, cy, scx, scy: i32 = 0, 0, 0, 0, 0, 0
		was_off := false
		start_off := false
		j: i32 = 0

		i: i32 = 0
		for i < n {
			flags = vertices[off+i].type
			x = i32(vertices[off+i].x)
			y = i32(vertices[off+i].y)

			if next_move == i {
				if i != 0 {
					num_vertices = close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy)
				}

				// Start new contour
				start_off = (flags & GLYPH_ON_CURVE) == 0
				if start_off {
					// Start with off-curve point
					scx = x
					scy = y
					if (vertices[off+i+1].type & GLYPH_ON_CURVE) == 0 {
						// Next point also a curve point, interpolate on-point
						sx = (x + i32(vertices[off+i+1].x)) >> 1
						sy = (y + i32(vertices[off+i+1].y)) >> 1
					} else {
						// Use next point as start point
						sx = i32(vertices[off+i+1].x)
						sy = i32(vertices[off+i+1].y)
						i += 1
					}
				} else {
					sx = x
					sy = y
				}
				set_vertex(&vertices[num_vertices], VMOVE, sx, sy, 0, 0)
				num_vertices += 1
				was_off = false
				next_move = 1 + i32(ttUSHORT(end_pts_of_contours[u32(j)*2:]))
				j += 1
			} else {
				if (flags & GLYPH_ON_CURVE) == 0 { // off-curve point
					if was_off {
						// Two off-curve points in a row - interpolate midpoint
						set_vertex(&vertices[num_vertices], VCURVE, (cx+x)>>1, (cy+y)>>1, cx, cy)
						num_vertices += 1
					}
					cx = x
					cy = y
					was_off = true
				} else {
					if was_off {
						set_vertex(&vertices[num_vertices], VCURVE, x, y, cx, cy)
						num_vertices += 1
					} else {
						set_vertex(&vertices[num_vertices], VLINE, x, y, 0, 0)
						num_vertices += 1
					}
					was_off = false
				}
			}
			i += 1
		}
		num_vertices = close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy)
	} else if number_of_contours < 0 {
		// Compound glyph - pre-count vertices for single allocation (avoids O(n²) reallocation)
		total_verts := count_glyph_shape_tt(info, glyph_index)
		if total_verts == 0 do return 0

		ptr, _ := mem.alloc(int(total_verts) * size_of(Vertex))
		vertices = ([^]Vertex)(ptr)
		if vertices == nil do return 0

		more := true
		comp := data[u32(g) + 10:]
		num_vertices = 0

		for more {
			comp_num_verts: i32 = 0
			comp_verts_temp: ^Vertex = nil
			comp_verts: [^]Vertex = nil

			flags := ttSHORT(comp)
			comp = comp[2:]
			gidx := ttSHORT(comp)
			comp = comp[2:]

			mtx: [6]f32 = {1,0,0,1,0,0}

			if flags & COMP_ARGS_ARE_XY_VALUES != 0 {
				if flags & COMP_ARG_1_AND_2_ARE_WORDS != 0 {
					mtx[4] = f32(ttSHORT(comp))
					comp = comp[2:]
					mtx[5] = f32(ttSHORT(comp))
					comp = comp[2:]
				} else {
					mtx[4] = f32(ttCHAR(comp))
					comp = comp[1:]
					mtx[5] = f32(ttCHAR(comp))
					comp = comp[1:]
				}
			} else {
				// TODO: handle matching point composite glyphs
				return 0
			}

			if flags & COMP_WE_HAVE_A_SCALE != 0 {
				mtx[0] = f32(ttSHORT(comp)) / 16384.0
				mtx[3] = mtx[0]
				mtx[1] = 0
				mtx[2] = 0
				comp = comp[2:]
			} else if flags & COMP_WE_HAVE_AN_X_AND_Y_SCALE != 0 {
				mtx[0] = f32(ttSHORT(comp)) / 16384.0
				comp = comp[2:]
				mtx[1] = 0
				mtx[2] = 0
				mtx[3] = f32(ttSHORT(comp)) / 16384.0
				comp = comp[2:]
			} else if flags & COMP_WE_HAVE_A_TWO_BY_TWO != 0 {
				mtx[0] = f32(ttSHORT(comp)) / 16384.0
				comp = comp[2:]
				mtx[1] = f32(ttSHORT(comp)) / 16384.0
				comp = comp[2:]
				mtx[2] = f32(ttSHORT(comp)) / 16384.0
				comp = comp[2:]
				mtx[3] = f32(ttSHORT(comp)) / 16384.0
				comp = comp[2:]
			}

			// Find transformation scales
			m := math.sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1])
			n_val := math.sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3])

			// Get component vertices (bypass cache for sub-components)
			if info.is_cff {
				comp_num_verts = get_glyph_shape_t2(info, i32(gidx), cast(^^Vertex)&comp_verts_temp)
			} else {
				comp_num_verts = get_glyph_shape_tt(info, i32(gidx), cast(^^Vertex)&comp_verts_temp)
			}
			comp_verts = ([^]Vertex)(comp_verts_temp)
			if comp_num_verts > 0 {
				// Optimized transformation loop with reduced computation
				target_vertices := vertices[num_vertices:]
				for i in 0..<comp_num_verts {
					v := &comp_verts[i]
					dst := &target_vertices[i]

					// Cache vertex coordinates for reuse
					vx, vy := f32(v.x), f32(v.y)
					vcx, vcy := f32(v.cx), f32(v.cy)

					dst.type = v.type
					dst.x = i16(m * (mtx[0]*vx + mtx[2]*vy + mtx[4]))
					dst.y = i16(n_val * (mtx[1]*vx + mtx[3]*vy + mtx[5]))
					dst.cx = i16(m * (mtx[0]*vcx + mtx[2]*vcy + mtx[4]))
					dst.cy = i16(n_val * (mtx[1]*vcx + mtx[3]*vcy + mtx[5]))
					vcx1, vcy1 := f32(v.cx1), f32(v.cy1)
					dst.cx1 = i16(m * (mtx[0]*vcx1 + mtx[2]*vcy1 + mtx[4]))
					dst.cy1 = i16(n_val * (mtx[1]*vcx1 + mtx[3]*vcy1 + mtx[5]))
				}
				mem.free(comp_verts)
				num_vertices += comp_num_verts
			}

			// More components?
			more = (flags & COMP_MORE_COMPONENTS) != 0
		}
	}

	pvertices^ = vertices
	return num_vertices
}

get_glyph_shape :: proc(info: ^Font_Info, glyph_index: i32, pvertices: ^^Vertex) -> i32 {
	// Check shape cache
	if info.shape_cache_ready {
		if cached, ok := info.shape_cache[glyph_index]; ok {
			pvertices^ = cached.vertices
			return cached.num_vertices
		}
	}

	nv: i32
	if info.is_cff {
		nv = get_glyph_shape_t2(info, glyph_index, pvertices)
	} else {
		nv = get_glyph_shape_tt(info, glyph_index, pvertices)
	}

	// Cache a persistent copy of the vertices using the heap allocator
	// (caller may have overridden context.allocator with a stack arena)
	if nv > 0 && pvertices^ != nil && info.shape_cache_ready {
		copy_size := int(nv) * size_of(Vertex)
		copy_ptr, _ := mem.alloc(copy_size, allocator = runtime.heap_allocator())
		if copy_ptr != nil {
			mem.copy(copy_ptr, pvertices^, copy_size)
			info.shape_cache[glyph_index] = {vertices = ([^]Vertex)(copy_ptr), num_vertices = nv}
		}
	}

	return nv
}

get_codepoint_shape :: proc(info: ^Font_Info, codepoint: i32, pvertices: ^^Vertex) -> i32 {
	return get_glyph_shape(info, find_glyph_index(info, codepoint), pvertices)
}

// Get glyph shape with variation applied. Applies gvar deltas to TrueType outlines.
// The returned vertices must be freed with free_shape.
get_glyph_shape_var :: proc(info: ^Font_Info, glyph_index: i32, coords: ^Var_Coords, pvertices: ^^Vertex) -> i32 {
	// Get the default shape (bypass cache since we're modifying)
	nv: i32
	if info.is_cff {
		nv = get_glyph_shape_t2(info, glyph_index, pvertices)
	} else {
		nv = get_glyph_shape_tt(info, glyph_index, pvertices)
	}

	if nv <= 0 || pvertices^ == nil do return nv

	// Apply gvar deltas to the vertices
	verts := ([^]Vertex)(pvertices^)

	// Extract point positions into flat arrays for gvar
	xs := make([]f32, int(nv))
	ys := make([]f32, int(nv))
	defer delete(xs)
	defer delete(ys)

	for i in 0..<nv {
		xs[i] = f32(verts[i].x)
		ys[i] = f32(verts[i].y)
	}

	if apply_gvar_deltas(info, glyph_index, coords, xs, ys) {
		// Write back modified positions
		for i in 0..<nv {
			verts[i].x = i16(xs[i])
			verts[i].y = i16(ys[i])
			// Also adjust control points for curves
			// (gvar deltas affect all points including on-curve and off-curve)
		}
	}

	return nv
}

// Get variable glyph shape by codepoint
get_codepoint_shape_var :: proc(info: ^Font_Info, codepoint: i32, coords: ^Var_Coords, pvertices: ^^Vertex) -> i32 {
	return get_glyph_shape_var(info, find_glyph_index(info, codepoint), coords, pvertices)
}

is_glyph_empty :: proc(info: ^Font_Info, glyph_index: i32) -> bool {
	if info.is_cff {
		return get_glyph_info_t2(info, glyph_index, nil, nil, nil, nil) == 0
	}
	g := get_glyf_offset(info, glyph_index)
	if g < 0 do return true
	number_of_contours := ttSHORT(info.data[u32(g):])
	return number_of_contours == 0
}

free_shape :: proc(info: ^Font_Info, vertices: ^Vertex) {
	// Don't free if this is a cached shape
	if info.shape_cache_ready {
		for _, cached in info.shape_cache {
			if ([^]Vertex)(vertices) == cached.vertices {
				return // owned by cache
			}
		}
	}
	mem.free(vertices)
}