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..> 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.. 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.. 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) }