Harbor

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

// ============================================================================
// KERNING
// ============================================================================

// Get the length of the kerning table (number of entries)
get_kerning_table_length :: proc(info: ^Font_Info) -> i32 {
	data := info.data[info.kern:]

	// we only look at the first table. it must be 'horizontal' and format 0.
	if info.kern == 0 {
		return 0
	}
	if ttUSHORT(data[2:]) < 1 { // number of tables, need at least 1
		return 0
	}
	if ttUSHORT(data[8:]) != 1 { // horizontal flag must be set in format
		return 0
	}

	return i32(ttUSHORT(data[10:]))
}

// Get the complete kerning table
// Returns how many entries were written (never more than table_length)
get_kerning_table :: proc(info: ^Font_Info, table: [^]Kerning_Entry, table_length: i32) -> i32 {
	data := info.data[info.kern:]

	// we only look at the first table. it must be 'horizontal' and format 0.
	if info.kern == 0 {
		return 0
	}
	if ttUSHORT(data[2:]) < 1 { // number of tables, need at least 1
		return 0
	}
	if ttUSHORT(data[8:]) != 1 { // horizontal flag must be set in format
		return 0
	}

	length := i32(ttUSHORT(data[10:]))
	if table_length < length {
		length = table_length
	}

	for k: i32 = 0; k < length; k += 1 {
		table[k].glyph1 = i32(ttUSHORT(data[18 + k*6:]))
		table[k].glyph2 = i32(ttUSHORT(data[20 + k*6:]))
		table[k].advance = i32(ttSHORT(data[22 + k*6:]))
	}

	return length
}

// Private: Get kerning advance from kern table using binary search
@(private)
get_glyph_kern_info_advance :: proc(info: ^Font_Info, glyph1: i32, glyph2: i32) -> i32 {
	data := info.data[info.kern:]

	// we only look at the first table. it must be 'horizontal' and format 0.
	if info.kern == 0 {
		return 0
	}
	if ttUSHORT(data[2:]) < 1 { // number of tables, need at least 1
		return 0
	}
	if ttUSHORT(data[8:]) != 1 { // horizontal flag must be set in format
		return 0
	}

	l: i32 = 0
	r: i32 = i32(ttUSHORT(data[10:])) - 1
	needle := u32(glyph1) << 16 | u32(glyph2)

	for l <= r {
		m := (l + r) >> 1
		straw := ttULONG(data[18 + m*6:]) // note: unaligned read
		if needle < straw {
			r = m - 1
		} else if needle > straw {
			l = m + 1
		} else {
			return i32(ttSHORT(data[22 + m*6:]))
		}
	}
	return 0
}

// Private: Get coverage index from a GPOS coverage table
@(private)
get_coverage_index :: proc(coverage_table: [^]u8, glyph: i32) -> i32 {
	coverage_format := ttUSHORT(coverage_table)

	switch coverage_format {
	case 1:
		glyph_count := i32(ttUSHORT(coverage_table[2:]))

		// Binary search
		l: i32 = 0
		r: i32 = glyph_count - 1
		needle := glyph
		for l <= r {
			glyph_array := coverage_table[4:]
			m := (l + r) >> 1
			glyph_id := i32(ttUSHORT(glyph_array[2 * m:]))
			straw := glyph_id
			if needle < straw {
				r = m - 1
			} else if needle > straw {
				l = m + 1
			} else {
				return m
			}
		}

	case 2:
		range_count := i32(ttUSHORT(coverage_table[2:]))
		range_array := coverage_table[4:]

		// Binary search
		l: i32 = 0
		r: i32 = range_count - 1
		needle := glyph
		for l <= r {
			m := (l + r) >> 1
			range_record := range_array[6 * m:]
			straw_start := i32(ttUSHORT(range_record))
			straw_end := i32(ttUSHORT(range_record[2:]))
			if needle < straw_start {
				r = m - 1
			} else if needle > straw_end {
				l = m + 1
			} else {
				start_coverage_index := i32(ttUSHORT(range_record[4:]))
				return start_coverage_index + glyph - straw_start
			}
		}

	case:
		return -1 // unsupported
	}

	return -1
}

// Private: Get glyph class from a GPOS class definition table
@(private)
get_glyph_class :: proc(class_def_table: [^]u8, glyph: i32) -> i32 {
	class_def_format := ttUSHORT(class_def_table)

	switch class_def_format {
	case 1:
		start_glyph_id := i32(ttUSHORT(class_def_table[2:]))
		glyph_count := i32(ttUSHORT(class_def_table[4:]))
		class_def1_value_array := class_def_table[6:]

		if glyph >= start_glyph_id && glyph < start_glyph_id + glyph_count {
			return i32(ttUSHORT(class_def1_value_array[2 * (glyph - start_glyph_id):]))
		}

	case 2:
		class_range_count := i32(ttUSHORT(class_def_table[2:]))
		class_range_records := class_def_table[4:]

		// Binary search
		l: i32 = 0
		r: i32 = class_range_count - 1
		needle := glyph
		for l <= r {
			m := (l + r) >> 1
			class_range_record := class_range_records[6 * m:]
			straw_start := i32(ttUSHORT(class_range_record))
			straw_end := i32(ttUSHORT(class_range_record[2:]))
			if needle < straw_start {
				r = m - 1
			} else if needle > straw_end {
				l = m + 1
			} else {
				return i32(ttUSHORT(class_range_record[4:]))
			}
		}

	case:
		return -1 // Unsupported definition type
	}

	// "All glyphs not assigned to a class fall into class 0". (OpenType spec)
	return 0
}

// Count bits set in a ValueFormat field to determine ValueRecord size
@(private)
value_record_size :: proc(format: u16) -> i32 {
	// Each bit in the format field indicates a 16-bit value is present
	count: i32 = 0
	f := format
	for f != 0 {
		count += i32(f & 1)
		f >>= 1
	}
	return count * 2 // each field is 2 bytes
}

// Extract XAdvance from a ValueRecord given its format
@(private)
value_record_x_advance :: proc(record: [^]u8, format: u16) -> i32 {
	if format & 4 == 0 do return 0 // XAdvance not present
	// XAdvance is the 3rd field (bit 2). Count preceding fields.
	offset: i32 = 0
	if format & 1 != 0 do offset += 2 // XPlacement
	if format & 2 != 0 do offset += 2 // YPlacement
	return i32(ttSHORT(record[offset:]))
}

// Private: Get kerning advance from GPOS table
@(private)
get_glyph_gpos_info_advance :: proc(info: ^Font_Info, glyph1: i32, glyph2: i32) -> i32 {
	if info.gpos == 0 {
		return 0
	}

	data := info.data[info.gpos:]

	if ttUSHORT(data) != 1 { // Major version 1
		return 0
	}
	minor_version := ttUSHORT(data[2:])
	if minor_version != 0 && minor_version != 1 {
		return 0
	}

	lookup_list_offset := ttUSHORT(data[8:])
	lookup_list := data[lookup_list_offset:]
	lookup_count := i32(ttUSHORT(lookup_list))

	for i: i32 = 0; i < lookup_count; i += 1 {
		lookup_offset := ttUSHORT(lookup_list[2 + 2*i:])
		lookup_table := lookup_list[lookup_offset:]

		lookup_type := i32(ttUSHORT(lookup_table))
		sub_table_count := i32(ttUSHORT(lookup_table[4:]))
		sub_table_offsets := lookup_table[6:]

		for sti: i32 = 0; sti < sub_table_count; sti += 1 {
			subtable_offset := ttUSHORT(sub_table_offsets[2*sti:])
			table := lookup_table[subtable_offset:]
			actual_type := lookup_type

			// Handle Extension Positioning (Type 9)
			if lookup_type == 9 {
				ext_format := ttUSHORT(table)
				if ext_format != 1 do continue
				actual_type = i32(ttUSHORT(table[2:]))
				table = table[ttULONG(table[4:]):]
			}

			if actual_type != 2 do continue // Only handle Pair Adjustment

			pos_format := ttUSHORT(table)
			coverage_offset := i32(ttUSHORT(table[2:]))
			coverage_index := get_coverage_index(table[coverage_offset:], glyph1)
			if coverage_index == -1 do continue

			value_format1 := ttUSHORT(table[4:])
			value_format2 := ttUSHORT(table[6:])
			vr1_size := value_record_size(value_format1)
			vr2_size := value_record_size(value_format2)

			switch pos_format {
			case 1: // Individual pair sets
				pair_set_count := i32(ttUSHORT(table[8:]))
				if coverage_index >= pair_set_count do continue
				pair_pos_offset := i32(ttUSHORT(table[10 + 2*coverage_index:]))
				pair_value_table := table[pair_pos_offset:]
				pair_value_count := i32(ttUSHORT(pair_value_table))
				pair_value_array := pair_value_table[2:]

				record_size := 2 + vr1_size + vr2_size // secondGlyph + vr1 + vr2
				needle := glyph2
				l: i32 = 0
				r := pair_value_count - 1
				for l <= r {
					m := (l + r) >> 1
					pair_value := pair_value_array[record_size * m:]
					second_glyph := i32(ttUSHORT(pair_value))
					if needle < second_glyph {
						r = m - 1
					} else if needle > second_glyph {
						l = m + 1
					} else {
						return value_record_x_advance(pair_value[2:], value_format1)
					}
				}

			case 2: // Class-based pairs
				class_def1_offset := i32(ttUSHORT(table[8:]))
				class_def2_offset := i32(ttUSHORT(table[10:]))
				glyph1class := get_glyph_class(table[class_def1_offset:], glyph1)
				glyph2class := get_glyph_class(table[class_def2_offset:], glyph2)

				class1_count := i32(ttUSHORT(table[12:]))
				class2_count := i32(ttUSHORT(table[14:]))

				if glyph1class < 0 || glyph1class >= class1_count do continue
				if glyph2class < 0 || glyph2class >= class2_count do continue

				record_pair_size := vr1_size + vr2_size
				class1_records := table[16:]
				offset := glyph1class * class2_count * record_pair_size + glyph2class * record_pair_size
				return value_record_x_advance(class1_records[offset:], value_format1)
			}
		}
	}

	return 0
}

// Get kerning advance for a glyph pair
// Uses cache for O(1) repeated lookups. GPOS takes priority over kern.
get_glyph_kern_advance :: proc(info: ^Font_Info, g1: i32, g2: i32) -> i32 {
	if info.gpos == 0 && info.kern == 0 do return 0

	key := (u64(u32(g1)) << 32) | u64(u32(g2))

	// Check cache
	if info.kern_cache_ready {
		if val, ok := info.kern_cache[key]; ok {
			return val
		}
	}

	// Compute
	x_advance: i32 = 0
	if info.gpos != 0 {
		x_advance = get_glyph_gpos_info_advance(info, g1, g2)
	} else if info.kern != 0 {
		x_advance = get_glyph_kern_info_advance(info, g1, g2)
	}

	// Store in cache
	if !info.kern_cache_ready {
		info.kern_cache = make(map[u64]i32)
		info.kern_cache_ready = true
	}
	info.kern_cache[key] = x_advance

	return x_advance
}

// Get kerning advance for a codepoint pair
get_codepoint_kern_advance :: proc(info: ^Font_Info, ch1: i32, ch2: i32) -> i32 {
	if info.kern == 0 && info.gpos == 0 { // if no kerning table, don't waste time looking up both codepoint->glyphs
		return 0
	}
	return get_glyph_kern_advance(info, find_glyph_index(info, ch1), find_glyph_index(info, ch2))
}