Harbor

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

// ============================================================================
// GSUB — OpenType Glyph Substitution
// ============================================================================

// Substitution result for a single glyph position
GSUB_Subst :: struct {
	glyph:       i32,   // replacement glyph ID
	skip:        i32,   // number of additional input glyphs consumed (for ligatures)
}

// Apply all default GSUB substitutions to a glyph buffer.
// Processes the default script/language with all default features.
// Modifies glyphs in-place. Returns the new length (may shrink due to ligatures).
apply_gsub_default :: proc(info: ^Font_Info, glyphs: []i32) -> i32 {
	if info.gsub == 0 do return i32(len(glyphs))
	data := info.data
	gsub := u32(info.gsub)

	// Validate GSUB header
	major := ttUSHORT(data[gsub:])
	if major != 1 do return i32(len(glyphs))

	script_list  := gsub + u32(ttUSHORT(data[gsub + 4:]))
	feature_list := gsub + u32(ttUSHORT(data[gsub + 6:]))
	lookup_list  := gsub + u32(ttUSHORT(data[gsub + 8:]))

	// Find default LangSys: try "DFLT" then "latn" scripts, then first script
	lang_sys := gsub_find_default_langsys(data, script_list)
	if lang_sys == 0 do return i32(len(glyphs))

	// Collect lookup indices from all features referenced by the default LangSys
	feature_count := i32(ttUSHORT(data[lang_sys + 4:]))
	feature_indices := data[lang_sys + 6:]

	// Process required feature if present
	req_feature := i32(ttUSHORT(data[lang_sys + 2:]))

	// Work buffer — copy input glyphs
	buf: [1024]i32
	n := min(i32(len(glyphs)), 1024)
	for i in 0..<n {
		buf[i] = glyphs[i]
	}

	// Apply required feature
	if req_feature != 0xFFFF {
		n = gsub_apply_feature(info, data, gsub, feature_list, lookup_list, req_feature, buf[:n])
	}

	// Apply each default feature
	for fi in 0..<feature_count {
		feat_idx := i32(ttUSHORT(feature_indices[fi * 2:]))
		n = gsub_apply_feature(info, data, gsub, feature_list, lookup_list, feat_idx, buf[:n])
	}

	// Write back
	for i in 0..<n {
		glyphs[i] = buf[i]
	}
	return n
}

// Apply a specific GSUB feature tag (e.g. "liga", "smcp") to a glyph buffer.
// Returns the new glyph count.
apply_gsub_feature :: proc(info: ^Font_Info, tag: string, glyphs: []i32) -> i32 {
	if info.gsub == 0 || len(tag) != 4 do return i32(len(glyphs))
	data := info.data
	gsub := u32(info.gsub)

	major := ttUSHORT(data[gsub:])
	if major != 1 do return i32(len(glyphs))

	feature_list := gsub + u32(ttUSHORT(data[gsub + 6:]))
	lookup_list  := gsub + u32(ttUSHORT(data[gsub + 8:]))

	// Find the feature by tag
	feat_count := i32(ttUSHORT(data[feature_list:]))
	feat_records := data[feature_list + 2:]

	buf: [1024]i32
	n := min(i32(len(glyphs)), 1024)
	for i in 0..<n {
		buf[i] = glyphs[i]
	}

	for fi in 0..<feat_count {
		rec := feat_records[fi * 6:]
		if rec[0] == tag[0] && rec[1] == tag[1] && rec[2] == tag[2] && rec[3] == tag[3] {
			n = gsub_apply_feature(info, data, gsub, feature_list, lookup_list, fi, buf[:n])
		}
	}

	for i in 0..<n {
		glyphs[i] = buf[i]
	}
	return n
}

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

@(private)
gsub_find_default_langsys :: proc(data: [^]u8, script_list: u32) -> u32 {
	script_count := i32(ttUSHORT(data[script_list:]))
	records := data[script_list + 2:]

	// Priority: "DFLT", "latn", first available
	try_tags := [?]string{"DFLT", "latn"}

	for t in try_tags {
		for si in 0..<script_count {
			rec := records[si * 6:]
			if rec[0] == t[0] && rec[1] == t[1] && rec[2] == t[2] && rec[3] == t[3] {
				script_offset := script_list + u32(ttUSHORT(rec[4:]))
				default_lang_sys_offset := u32(ttUSHORT(data[script_offset:]))
				if default_lang_sys_offset != 0 {
					return script_offset + default_lang_sys_offset
				}
			}
		}
	}

	// Fallback: first script's default langsys
	if script_count > 0 {
		script_offset := script_list + u32(ttUSHORT(records[4:]))
		default_lang_sys_offset := u32(ttUSHORT(data[script_offset:]))
		if default_lang_sys_offset != 0 {
			return script_offset + default_lang_sys_offset
		}
	}

	return 0
}

@(private)
gsub_apply_feature :: proc(info: ^Font_Info, data: [^]u8, gsub: u32, feature_list: u32, lookup_list: u32, feat_idx: i32, glyphs: []i32) -> i32 {
	feat_count := i32(ttUSHORT(data[feature_list:]))
	if feat_idx < 0 || feat_idx >= feat_count do return i32(len(glyphs))

	feat_records := data[feature_list + 2:]
	feat_offset := feature_list + u32(ttUSHORT(feat_records[feat_idx * 6 + 4:]))

	lookup_count := i32(ttUSHORT(data[feat_offset + 2:]))
	lookup_indices := data[feat_offset + 4:]

	n := i32(len(glyphs))
	for li in 0..<lookup_count {
		lookup_idx := i32(ttUSHORT(lookup_indices[li * 2:]))
		n = gsub_apply_lookup(info, data, gsub, lookup_list, lookup_idx, glyphs[:n])
	}
	return n
}

@(private)
gsub_apply_lookup :: proc(info: ^Font_Info, data: [^]u8, gsub: u32, lookup_list: u32, lookup_idx: i32, glyphs: []i32) -> i32 {
	lookup_count := i32(ttUSHORT(data[lookup_list:]))
	if lookup_idx < 0 || lookup_idx >= lookup_count do return i32(len(glyphs))

	lookup_offset := lookup_list + u32(ttUSHORT(data[lookup_list + 2 + u32(lookup_idx) * 2:]))
	lookup_type := i32(ttUSHORT(data[lookup_offset:]))
	// lookup_flag := ttUSHORT(data[lookup_offset + 2:])
	subtable_count := i32(ttUSHORT(data[lookup_offset + 4:]))

	n := i32(len(glyphs))

	for sti in 0..<subtable_count {
		subtable_off := lookup_offset + u32(ttUSHORT(data[lookup_offset + 6 + u32(sti) * 2:]))
		actual_type := lookup_type
		actual_off := subtable_off

		// Handle Extension Substitution (Type 7) — unwrap to actual subtable
		if lookup_type == 7 {
			ext_format := ttUSHORT(data[subtable_off:])
			if ext_format == 1 {
				actual_type = i32(ttUSHORT(data[subtable_off + 2:]))
				actual_off = subtable_off + u32(ttULONG(data[subtable_off + 4:]))
			} else {
				continue
			}
		}

		switch actual_type {
		case 1:
			n = gsub_single_subst(data, actual_off, glyphs[:n])
		case 2:
			n = gsub_multiple_subst(data, actual_off, glyphs[:n])
		case 4:
			n = gsub_ligature_subst(data, actual_off, glyphs[:n])
		case 6:
			n = gsub_chaining_context_subst(info, data, gsub, actual_off, glyphs[:n])
		}
	}
	return n
}

// Lookup Type 1: Single Substitution (1:1 replacement)
@(private)
gsub_single_subst :: proc(data: [^]u8, subtable: u32, glyphs: []i32) -> i32 {
	format := ttUSHORT(data[subtable:])
	coverage_off := subtable + u32(ttUSHORT(data[subtable + 2:]))

	switch format {
	case 1: // Delta
		delta := i32(ttSHORT(data[subtable + 4:]))
		for i in 0..<len(glyphs) {
			ci := get_coverage_index(data[coverage_off:], glyphs[i])
			if ci >= 0 {
				glyphs[i] = (glyphs[i] + delta) & 0xFFFF
			}
		}
	case 2: // Substitute array
		glyph_count := i32(ttUSHORT(data[subtable + 4:]))
		subst_array := data[subtable + 6:]
		for i in 0..<len(glyphs) {
			ci := get_coverage_index(data[coverage_off:], glyphs[i])
			if ci >= 0 && ci < glyph_count {
				glyphs[i] = i32(ttUSHORT(subst_array[ci * 2:]))
			}
		}
	}
	return i32(len(glyphs))
}

// Lookup Type 2: Multiple Substitution (1:M replacement)
@(private)
gsub_multiple_subst :: proc(data: [^]u8, subtable: u32, glyphs: []i32) -> i32 {
	format := ttUSHORT(data[subtable:])
	if format != 1 do return i32(len(glyphs))

	coverage_off := subtable + u32(ttUSHORT(data[subtable + 2:]))
	seq_count := i32(ttUSHORT(data[subtable + 4:]))
	seq_offsets := data[subtable + 6:]

	n := i32(len(glyphs))
	i: i32 = 0
	for i < n {
		ci := get_coverage_index(data[coverage_off:], glyphs[i])
		if ci >= 0 && ci < seq_count {
			seq_off := subtable + u32(ttUSHORT(seq_offsets[ci * 2:]))
			replace_count := i32(ttUSHORT(data[seq_off:]))
			replace_glyphs := data[seq_off + 2:]

			if replace_count == 1 {
				// Simple 1:1, just replace in place
				glyphs[i] = i32(ttUSHORT(replace_glyphs))
			} else if replace_count == 0 {
				// Delete glyph
				for j in i..<n-1 {
					glyphs[j] = glyphs[j+1]
				}
				n -= 1
				continue // don't advance i
			} else if replace_count > 1 && n + replace_count - 1 <= i32(len(glyphs)) {
				// Expand: shift right to make room
				shift := replace_count - 1
				for j := n - 1; j > i; j -= 1 {
					glyphs[j + shift] = glyphs[j]
				}
				for ri in 0..<replace_count {
					glyphs[i + ri] = i32(ttUSHORT(replace_glyphs[ri * 2:]))
				}
				n += shift
				i += replace_count
				continue
			}
		}
		i += 1
	}
	return n
}

// Lookup Type 4: Ligature Substitution (N:1 replacement)
@(private)
gsub_ligature_subst :: proc(data: [^]u8, subtable: u32, glyphs: []i32) -> i32 {
	format := ttUSHORT(data[subtable:])
	if format != 1 do return i32(len(glyphs))

	coverage_off := subtable + u32(ttUSHORT(data[subtable + 2:]))
	lig_set_count := i32(ttUSHORT(data[subtable + 4:]))
	lig_set_offsets := data[subtable + 6:]

	n := i32(len(glyphs))
	i: i32 = 0
	for i < n {
		ci := get_coverage_index(data[coverage_off:], glyphs[i])
		if ci >= 0 && ci < lig_set_count {
			lig_set_off := subtable + u32(ttUSHORT(lig_set_offsets[ci * 2:]))
			lig_count := i32(ttUSHORT(data[lig_set_off:]))
			lig_offsets := data[lig_set_off + 2:]

			matched := false
			for li in 0..<lig_count {
				lig_off := lig_set_off + u32(ttUSHORT(lig_offsets[li * 2:]))
				lig_glyph := i32(ttUSHORT(data[lig_off:]))
				comp_count := i32(ttUSHORT(data[lig_off + 2:])) // includes first glyph
				components := data[lig_off + 4:]

				// Check if remaining glyphs match the ligature components
				if i + comp_count > n do continue

				match := true
				for ci2 in 0..<comp_count - 1 {
					if glyphs[i + 1 + ci2] != i32(ttUSHORT(components[ci2 * 2:])) {
						match = false
						break
					}
				}

				if match {
					// Replace first glyph with ligature
					glyphs[i] = lig_glyph
					// Remove consumed glyphs
					remove_count := comp_count - 1
					for j := i + 1; j < n - remove_count; j += 1 {
						glyphs[j] = glyphs[j + remove_count]
					}
					n -= remove_count
					matched = true
					break
				}
			}

			if matched {
				i += 1
				continue
			}
		}
		i += 1
	}
	return n
}

// Lookup Type 6: Chaining Context Substitution
@(private)
gsub_chaining_context_subst :: proc(info: ^Font_Info, data: [^]u8, gsub: u32, subtable: u32, glyphs: []i32) -> i32 {
	format := ttUSHORT(data[subtable:])
	if format != 3 do return i32(len(glyphs)) // Only format 3 for now

	lookup_list := gsub + u32(ttUSHORT(data[gsub + 8:]))
	n := i32(len(glyphs))

	// Parse chaining context format 3
	off: u32 = subtable + 2

	// Backtrack coverages
	backtrack_count := i32(ttUSHORT(data[off:]))
	off += 2
	backtrack_covs: [16]u32
	for bi in 0..<min(backtrack_count, 16) {
		backtrack_covs[bi] = subtable + u32(ttUSHORT(data[off:]))
		off += 2
	}
	if backtrack_count > 16 do off += u32(backtrack_count - 16) * 2

	// Input coverages
	input_count := i32(ttUSHORT(data[off:]))
	off += 2
	input_covs: [16]u32
	for ii in 0..<min(input_count, 16) {
		input_covs[ii] = subtable + u32(ttUSHORT(data[off:]))
		off += 2
	}
	if input_count > 16 do off += u32(input_count - 16) * 2

	// Lookahead coverages
	lookahead_count := i32(ttUSHORT(data[off:]))
	off += 2
	lookahead_covs: [16]u32
	for li in 0..<min(lookahead_count, 16) {
		lookahead_covs[li] = subtable + u32(ttUSHORT(data[off:]))
		off += 2
	}
	if lookahead_count > 16 do off += u32(lookahead_count - 16) * 2

	// Substitution lookup records
	subst_count := i32(ttUSHORT(data[off:]))
	off += 2
	subst_records := data[off:]

	// Apply to glyph buffer
	i: i32 = 0
	for i < n {
		// Check if enough glyphs for backtrack + input + lookahead
		if i < backtrack_count { i += 1; continue }
		if i + input_count + lookahead_count > n do break

		// Match input coverages
		input_match := true
		for ii in 0..<input_count {
			if get_coverage_index(data[input_covs[ii]:], glyphs[i + ii]) < 0 {
				input_match = false
				break
			}
		}
		if !input_match { i += 1; continue }

		// Match backtrack coverages (reverse order)
		backtrack_match := true
		for bi in 0..<backtrack_count {
			if get_coverage_index(data[backtrack_covs[bi]:], glyphs[i - 1 - bi]) < 0 {
				backtrack_match = false
				break
			}
		}
		if !backtrack_match { i += 1; continue }

		// Match lookahead coverages
		lookahead_match := true
		for li in 0..<lookahead_count {
			if get_coverage_index(data[lookahead_covs[li]:], glyphs[i + input_count + li]) < 0 {
				lookahead_match = false
				break
			}
		}
		if !lookahead_match { i += 1; continue }

		// All matched — apply substitution lookups
		for si in 0..<subst_count {
			seq_idx := i32(ttUSHORT(subst_records[si * 4:]))
			lookup_idx := i32(ttUSHORT(subst_records[si * 4 + 2:]))
			target := i + seq_idx
			if target >= 0 && target < n {
				// Apply the referenced lookup to just this position
				one_glyph := glyphs[target:target+1]
				gsub_apply_lookup(info, data, gsub, lookup_list, lookup_idx, one_glyph)
			}
		}

		i += input_count
	}
	return n
}