Harbor

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

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

// ============================================================================
// AUTOHINTER — Lightweight grid-fitting without TrueType bytecode
// ============================================================================

// Hint mode controls the aggressiveness of grid-fitting
Hint_Mode :: enum {
	None,    // No hinting (default, best for high-DPI)
	Light,   // Vertical only — snap Y stems to pixel grid, preserve X
	Full,    // Vertical + horizontal stem snapping
}

// Autohint settings
Hint_Settings :: struct {
	mode:     Hint_Mode,
	ppem:     f32,        // pixels per em (derived from scale)
}

// Apply autohinting to glyph vertices in-place.
// Modifies vertex Y coordinates (and optionally X) to align with pixel grid.
// scale is the scale_for_pixel_height value.
autohint_glyph :: proc(
	info: ^Font_Info,
	vertices: [^]Vertex,
	num_verts: i32,
	scale: f32,
	mode: Hint_Mode,
) {
	if mode == .None || num_verts <= 0 do return

	// Get key vertical metrics in font units
	ascent, descent, line_gap: i32
	get_font_vmetrics(info, &ascent, &descent, &line_gap)

	// Get OS/2 metrics for x-height and cap-height
	x_height: i32 = 0
	cap_height: i32 = 0
	os2: OS2_Metrics
	if get_os2_metrics(info, &os2) {
		x_height = i32(os2.x_height)
		cap_height = i32(os2.cap_height)
	}

	// Define alignment zones (font-unit Y values → snapped pixel Y)
	zones: [6]Hint_Zone
	zone_count: i32 = 0

	// Baseline at 0
	zones[zone_count] = make_zone(0, scale)
	zone_count += 1

	// Ascender
	if ascent != 0 {
		zones[zone_count] = make_zone(ascent, scale)
		zone_count += 1
	}

	// Descender
	if descent != 0 {
		zones[zone_count] = make_zone(descent, scale)
		zone_count += 1
	}

	// Cap height
	if cap_height > 0 {
		zones[zone_count] = make_zone(cap_height, scale)
		zone_count += 1
	}

	// x-height
	if x_height > 0 {
		zones[zone_count] = make_zone(x_height, scale)
		zone_count += 1
	}

	// Apply Y-direction grid fitting
	for i in 0..<num_verts {
		v := &vertices[i]
		fy := f32(v.y)

		// Snap to nearest alignment zone
		snapped := snap_to_zone(fy, zones[:zone_count], scale)
		v.y = i16(snapped)

		// For curves, also snap control points proportionally
		if v.type == VCURVE || v.type == VCUBIC {
			fcy := f32(v.cy)
			v.cy = i16(snap_to_zone(fcy, zones[:zone_count], scale))
		}
		if v.type == VCUBIC {
			fcy1 := f32(v.cy1)
			v.cy1 = i16(snap_to_zone(fcy1, zones[:zone_count], scale))
		}

		// Full mode: also regularize X stems
		if mode == .Full {
			v.x = i16(snap_stem_x(f32(v.x), scale))
			if v.type == VCURVE || v.type == VCUBIC {
				v.cx = i16(snap_stem_x(f32(v.cx), scale))
			}
			if v.type == VCUBIC {
				v.cx1 = i16(snap_stem_x(f32(v.cx1), scale))
			}
		}
	}
}

// Render a hinted glyph bitmap
get_glyph_bitmap_hinted :: proc(
	info: ^Font_Info,
	scale_x: f32, scale_y: f32,
	glyph: i32,
	mode: Hint_Mode,
	width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32,
	allocator := context.allocator,
) -> [^]u8 {
	arena_backing: [256 * 1024]u8
	arena: mem.Arena
	mem.arena_init(&arena, arena_backing[:])
	context.allocator = mem.arena_allocator(&arena)

	vertices_temp: ^Vertex = nil
	// Get uncached shape (we'll modify it)
	nv: i32
	if info.is_cff {
		nv = get_glyph_shape_t2(info, glyph, cast(^^Vertex)&vertices_temp)
	} else {
		nv = get_glyph_shape_tt(info, glyph, cast(^^Vertex)&vertices_temp)
	}
	vertices := ([^]Vertex)(vertices_temp)

	if nv <= 0 || vertices == nil {
		if width != nil  do width^  = 0
		if height != nil do height^ = 0
		return nil
	}

	// Apply autohinting
	autohint_glyph(info, vertices, nv, scale_y, mode)

	scale_x := scale_x; scale_y := scale_y
	if scale_x == 0 do scale_x = scale_y
	if scale_y == 0 { if scale_x == 0 do return nil; scale_y = scale_x }

	ix0, iy0, ix1, iy1: i32
	get_glyph_bitmap_box_subpixel(info, glyph, scale_x, scale_y, 0, 0, &ix0, &iy0, &ix1, &iy1)

	gbm: Bitmap
	gbm.w = ix1 - ix0; gbm.h = iy1 - iy0
	if width  != nil do width^  = gbm.w
	if height != nil do height^ = gbm.h
	if xoff   != nil do xoff^   = ix0
	if yoff   != nil do yoff^   = iy0

	if gbm.w != 0 && gbm.h != 0 {
		ptr, _ := mem.alloc(int(gbm.w * gbm.h), allocator = allocator)
		gbm.pixels = ([^]u8)(ptr)
		if gbm.pixels != nil {
			gbm.stride = gbm.w
			rasterize_glyph(&gbm, 0.35, vertices, nv, scale_x, scale_y, 0, 0, ix0, iy0, 1)
		}
	}
	return gbm.pixels
}

get_codepoint_bitmap_hinted :: proc(
	info: ^Font_Info, scale: f32, codepoint: i32, mode: Hint_Mode,
	width: ^i32, height: ^i32, xoff: ^i32, yoff: ^i32,
	allocator := context.allocator,
) -> [^]u8 {
	return get_glyph_bitmap_hinted(info, scale, scale, find_glyph_index(info, codepoint),
		mode, width, height, xoff, yoff, allocator)
}

// ============================================================================
// Internal
// ============================================================================

@(private)
Hint_Zone :: struct {
	font_y:    f32,   // Y coordinate in font units
	pixel_y:   f32,   // Snapped pixel Y (rounded font_y * scale)
}

@(private)
make_zone :: proc(font_y: i32, scale: f32) -> Hint_Zone {
	pixel_y := f32(font_y) * scale
	return {
		font_y  = f32(font_y),
		pixel_y = math.round(pixel_y),
	}
}

// Snap a font-unit Y coordinate to the nearest alignment zone
@(private)
snap_to_zone :: proc(font_y: f32, zones: []Hint_Zone, scale: f32) -> f32 {
	// Zone capture radius in font units
	radius := 1.0 / scale * 0.5 // half-pixel in font units

	best_dist := radius
	result := font_y

	for z in zones {
		dist := abs(font_y - z.font_y)
		if dist < best_dist {
			best_dist = dist
			// Shift this coordinate by the same delta the zone was shifted
			delta := z.pixel_y - z.font_y * scale
			result = font_y + delta / scale
		}
	}

	return result
}

// Snap X coordinate to half-pixel grid for consistent stem widths
@(private)
snap_stem_x :: proc(font_x: f32, scale: f32) -> f32 {
	px := font_x * scale
	// Snap to half-pixel boundaries for stem consistency
	snapped := math.round(px * 2) / 2
	return snapped / scale
}