bitsnpicas probably not decent

This commit is contained in:
minjaesong
2026-02-23 11:18:09 +09:00
parent b5f01a4d41
commit 208466bbb2
13 changed files with 1510 additions and 85 deletions

1
.idea/modules.xml generated
View File

@@ -4,6 +4,7 @@
<modules> <modules>
<module fileurl="file://$PROJECT_DIR$/BuildJAR_TerrarumSansBitmap.iml" filepath="$PROJECT_DIR$/BuildJAR_TerrarumSansBitmap.iml" /> <module fileurl="file://$PROJECT_DIR$/BuildJAR_TerrarumSansBitmap.iml" filepath="$PROJECT_DIR$/BuildJAR_TerrarumSansBitmap.iml" />
<module fileurl="file://$PROJECT_DIR$/FontTestGDX/FontTestGDX.iml" filepath="$PROJECT_DIR$/FontTestGDX/FontTestGDX.iml" /> <module fileurl="file://$PROJECT_DIR$/FontTestGDX/FontTestGDX.iml" filepath="$PROJECT_DIR$/FontTestGDX/FontTestGDX.iml" />
<module fileurl="file://$PROJECT_DIR$/OTFbuild/OTFbuild.iml" filepath="$PROJECT_DIR$/OTFbuild/OTFbuild.iml" />
</modules> </modules>
</component> </component>
</project> </project>

106
.idea/workspace.xml generated
View File

@@ -9,84 +9,10 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="22c5bc80-996c-4846-b173-7dc8c2096fe3" name="Default" comment="fix: characters not on overriden charset would not print"> <list default="true" id="22c5bc80-996c-4846-b173-7dc8c2096fe3" name="Default" comment="why are you still looking for tga.gz">
<change beforePath="$PROJECT_DIR$/.idea/artifacts/TerrarumSansBitmap.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/artifacts/TerrarumSansBitmap.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/modules.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/kotlinc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/kotlinc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/libraries/KotlinJavaRuntime.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/libraries/KotlinJavaRuntime.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/BuildJAR_TerrarumSansBitmap.iml" beforeDir="false" afterPath="$PROJECT_DIR$/BuildJAR_TerrarumSansBitmap.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/FontTestGDX/FontTestGDX.iml" beforeDir="false" afterPath="$PROJECT_DIR$/FontTestGDX/FontTestGDX.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/FontTestGDX/src/FontTestGDX.kt" beforeDir="false" afterPath="$PROJECT_DIR$/FontTestGDX/src/FontTestGDX.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/FontTestGDX/src/TypewriterGDX.kt" beforeDir="false" afterPath="$PROJECT_DIR$/FontTestGDX/src/TypewriterGDX.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/LICENSE.md" beforeDir="false" afterPath="$PROJECT_DIR$/LICENSE.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/assets/alphabetic_presentation_forms_extrawide_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/ascii_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/bengali_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/braille_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/cjkpunct_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/control_pictures_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/currencies_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/cyrilic_bulgarian_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/cyrilic_serbian_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/cyrilic_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/devanagari_internal_extrawide_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/devanagari_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/diacritical_marks_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/enclosed_alphanumeric_supplement_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/futhark.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/greek_polytonic_xyswap_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/greek_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/halfwidth_fullwidth_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/hangul_johab.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/hayeren_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/hentaigana_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/internal_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/ipa_ext_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/kana_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/kartuli_allcaps_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/kartuli_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/latinExtA_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/latinExtB_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/latinExtC_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/latinExtD_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/latinExt_additional_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/letterlike_symbols_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/phonetic_extensions_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/pua_codestyle_ascii_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/puae000-e0ff.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/richtext_furigana.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/sundanese_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/tamil_extrawide_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/thai_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/tsalagi_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/cr0.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/cr1.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/cr2.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/cr3.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/cr4.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/cr5.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/crlf.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/deadkey.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/movingkey.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/shiftin.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/shiftout.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/audio/space.wav" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/typewriter_intl_qwerty.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/typewriter/typewriter_ko_3set-390.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/unipunct_variable.tga" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/assets/wenquanyi.tga.gz" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/samples/wikipedia_x86.png" beforeDir="false" afterPath="$PROJECT_DIR$/samples/wikipedia_x86.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt" beforeDir="false" afterPath="$PROJECT_DIR$/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt" beforeDir="false" afterPath="$PROJECT_DIR$/src/net/torvald/terrarumsansbitmap/gdx/TerrarumSansBitmap.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/net/torvald/terrarumtypewriterbitmap/gdx/TerrarumTypewriterBitmap.kt" beforeDir="false" afterPath="$PROJECT_DIR$/src/net/torvald/terrarumtypewriterbitmap/gdx/TerrarumTypewriterBitmap.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/terrarum_sans_cyrillic_2.png" beforeDir="false" afterPath="$PROJECT_DIR$/terrarum_sans_cyrillic_2.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/alphnum_glyphs_master.kra" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/alphnum_glyphs_master.kra" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/alphnum_glyphs_resized.kra" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/alphnum_glyphs_resized.kra" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/hangul_3set_glyphs_master.kra" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/hangul_3set_glyphs_master.kra" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_input_template.psd" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_input_template.psd" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_intl_qwerty.psd" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_intl_qwerty.psd" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_ko_3set-390.psd" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_ko_3set-390.psd" afterDir="false" />
<change beforePath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_ko_3set_glyphs_resized.kra" beforeDir="false" afterPath="$PROJECT_DIR$/work_files/typewriter_input/typewriter_ko_3set_glyphs_resized.kra" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -242,12 +168,6 @@
<option name="Make" enabled="true" /> <option name="Make" enabled="true" />
</method> </method>
</configuration> </configuration>
<configuration default="true" type="JetRunConfigurationType">
<module name="BuildJAR_TerrarumSansBitmap" />
<method v="2">
<option name="Make" enabled="true" />
</method>
</configuration>
<configuration default="true" type="KotlinStandaloneScriptRunConfigurationType"> <configuration default="true" type="KotlinStandaloneScriptRunConfigurationType">
<option name="filePath" /> <option name="filePath" />
<method v="2"> <method v="2">
@@ -298,7 +218,23 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1726151824465</updated> <updated>1726151824465</updated>
</task> </task>
<option name="localTasksCounter" value="3" /> <task id="LOCAL-00003" summary="moving assets inside classpath">
<option name="closed" value="true" />
<created>1771460240293</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1771460240293</updated>
</task>
<task id="LOCAL-00004" summary="why are you still looking for tga.gz">
<option name="closed" value="true" />
<created>1771551906182</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1771551906182</updated>
</task>
<option name="localTasksCounter" value="5" />
<servers /> <servers />
</component> </component>
<component name="TodoView"> <component name="TodoView">
@@ -324,7 +260,9 @@
<component name="VcsManagerConfiguration"> <component name="VcsManagerConfiguration">
<MESSAGE value="Old hangul rendering fix" /> <MESSAGE value="Old hangul rendering fix" />
<MESSAGE value="fix: characters not on overriden charset would not print" /> <MESSAGE value="fix: characters not on overriden charset would not print" />
<option name="LAST_COMMIT_MESSAGE" value="fix: characters not on overriden charset would not print" /> <MESSAGE value="moving assets inside classpath" />
<MESSAGE value="why are you still looking for tga.gz" />
<option name="LAST_COMMIT_MESSAGE" value="why are you still looking for tga.gz" />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

26
OTFbuild/OTFbuild.iml Normal file
View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager">
<output url="file://$MODULE_DIR$/out/production/OTFbuild" />
<output-test url="file://$MODULE_DIR$/out/test/OTFbuild" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/out" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="KotlinJavaRuntime" level="project" />
<orderEntry type="module-library">
<library>
<CLASSES>
<root url="jar://$MODULE_DIR$/bitsnpicas_runtime/BitsNPicas.jar!/" />
</CLASSES>
<JAVADOC />
<SOURCES>
<root url="file://$MODULE_DIR$/bitsnpicas_source_codes/src" />
</SOURCES>
</library>
</orderEntry>
</component>
</module>

80
OTFbuild/build_otf.sh Executable file
View File

@@ -0,0 +1,80 @@
#!/bin/bash
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
ASSETS_DIR="$PROJECT_DIR/src/assets"
OUTPUT_DIR="$SCRIPT_DIR"
BITSNPICAS_JAR="$SCRIPT_DIR/bitsnpicas_runtime/BitsNPicas.jar"
# Output paths
KBITX_OUTPUT="$OUTPUT_DIR/TerrarumSansBitmap.kbitx"
TTF_OUTPUT="$OUTPUT_DIR/TerrarumSansBitmap.ttf"
echo "=== Terrarum Sans Bitmap OTF Build Pipeline ==="
echo "Project: $PROJECT_DIR"
echo "Assets: $ASSETS_DIR"
echo ""
# Step 1: Compile the builder
echo "--- Step 1: Compiling OTFbuild module ---"
COMPILE_CLASSPATH="$BITSNPICAS_JAR"
SRC_DIR="$SCRIPT_DIR/src"
OUT_DIR="$SCRIPT_DIR/out"
mkdir -p "$OUT_DIR"
# Find all Kotlin source files
SRC_FILES=$(find "$SRC_DIR" -name "*.kt" | tr '\n' ' ')
# Try to find Kotlin compiler
if command -v kotlinc &> /dev/null; then
KOTLINC="kotlinc"
KOTLIN_STDLIB=""
else
# Try IntelliJ's bundled Kotlin
IDEA_CACHE="$HOME/.cache/JetBrains"
KOTLIN_DIST=$(find "$IDEA_CACHE" -path "*/kotlin-dist-for-ide/*/lib/kotlin-compiler.jar" 2>/dev/null | sort -V | tail -1)
if [ -n "$KOTLIN_DIST" ]; then
KOTLIN_LIB="$(dirname "$KOTLIN_DIST")"
KOTLINC_CP="$KOTLIN_LIB/kotlin-compiler.jar:$KOTLIN_LIB/kotlin-stdlib.jar:$KOTLIN_LIB/trove4j.jar:$KOTLIN_LIB/kotlin-reflect.jar:$KOTLIN_LIB/kotlin-script-runtime.jar:$KOTLIN_LIB/kotlin-daemon.jar:$KOTLIN_LIB/annotations-13.0.jar"
KOTLIN_STDLIB="$KOTLIN_LIB/kotlin-stdlib.jar:$KOTLIN_LIB/kotlin-stdlib-jdk7.jar:$KOTLIN_LIB/kotlin-stdlib-jdk8.jar"
echo "Using IntelliJ's Kotlin from: $KOTLIN_LIB"
else
echo "ERROR: kotlinc not found. Please install Kotlin compiler or build via IntelliJ IDEA."
echo ""
echo "Alternative: Build the OTFbuild module in IntelliJ IDEA, then run:"
echo " java -cp \"$OUT_DIR:$COMPILE_CLASSPATH\" net.torvald.otfbuild.MainKt \"$ASSETS_DIR\" \"$KBITX_OUTPUT\""
exit 1
fi
fi
if [ -n "$KOTLIN_STDLIB" ]; then
# Use IntelliJ's bundled Kotlin via java
java -cp "$KOTLINC_CP" org.jetbrains.kotlin.cli.jvm.K2JVMCompiler \
-cp "$COMPILE_CLASSPATH:$KOTLIN_STDLIB" -d "$OUT_DIR" $SRC_FILES
else
kotlinc -cp "$COMPILE_CLASSPATH" -d "$OUT_DIR" $SRC_FILES
KOTLIN_STDLIB=""
fi
# Step 2: Run the builder to generate KBITX
echo ""
echo "--- Step 2: Generating KBITX ---"
RUNTIME_CP="$OUT_DIR:$COMPILE_CLASSPATH"
if [ -n "$KOTLIN_STDLIB" ]; then
RUNTIME_CP="$RUNTIME_CP:$KOTLIN_STDLIB"
fi
java -cp "$RUNTIME_CP" net.torvald.otfbuild.MainKt "$ASSETS_DIR" "$KBITX_OUTPUT"
# Step 3: Convert KBITX to TTF via BitsNPicas
echo ""
echo "--- Step 3: Converting KBITX to TTF ---"
java -jar "$BITSNPICAS_JAR" convertbitmap \
-f ttf -o "$TTF_OUTPUT" \
"$KBITX_OUTPUT"
echo ""
echo "=== Build complete ==="
echo " KBITX: $KBITX_OUTPUT"
echo " TTF: $TTF_OUTPUT"

View File

@@ -0,0 +1,133 @@
package net.torvald.otfbuild
/**
* Ensures all Devanagari, Tamil, Sundanese, and Alphabetic Presentation Forms
* PUA glyphs are included in the font. Since BitsNPicas doesn't support OpenType
* GSUB/GPOS features, complex text shaping must be done by the application.
*
* All the relevant PUA codepoints are already in the sprite sheets and extracted
* by GlyphSheetParser. This processor:
* 1. Verifies that key PUA ranges have been loaded
* 2. Ensures Unicode pre-composed forms (U+0958U+095F) map correctly
* 3. Documents the mapping for reference
*
* The runtime normalise() function handles the actual Unicode → PUA mapping,
* but since we can't put GSUB tables into the KBITX/TTF, applications must
* use the PUA codepoints directly, or perform their own normalisation.
*/
class DevanagariTamilProcessor {
/**
* Verify that key PUA glyphs exist in the extracted set.
* Returns a set of codepoints that should be included but are missing.
*/
fun verify(glyphs: Map<Int, ExtractedGlyph>): Set<Int> {
val missing = mutableSetOf<Int>()
// Devanagari special syllables
val devanagariSpecials = listOf(
0xF0100, // Ru
0xF0101, // Ruu
0xF0102, // RRu
0xF0103, // RRuu
0xF0104, // Hu
0xF0105, // Huu
0xF0106, // RYA
0xF0107, // Half-RYA
0xF0108, // Open YA
0xF0109, // Open Half-YA
0xF010B, // Eyelash RA
0xF010C, // RA superscript
0xF010D, // RA superscript (complex)
0xF010E, // DDRA (Marwari)
0xF010F, // Alt Half SHA
)
// Devanagari presentation consonants (full forms)
val devaPresentation = (0xF0140..0xF022F).toList()
// Devanagari presentation consonants (half forms)
val devaHalf = (0xF0230..0xF031F).toList()
// Devanagari presentation consonants (with RA)
val devaRa = (0xF0320..0xF040F).toList()
// Devanagari presentation consonants (with RA, half forms)
val devaRaHalf = (0xF0410..0xF04FF).toList()
// Devanagari II variant forms
val devaII = (0xF0110..0xF012F).toList()
// Devanagari named ligatures
val devaLigatures = listOf(
0xF01A1, // K.SS
0xF01A2, // J.NY
0xF01A3, // T.T
0xF01A4, // N.T
0xF01A5, // N.N
0xF01A6, // S.V
0xF01A7, // SS.P
0xF01A8, // SH.C
0xF01A9, // SH.N
0xF01AA, // SH.V
0xF01AB, // J.Y
0xF01AC, // J.J.Y
0xF01BC, // K.T
// D-series ligatures
0xF01B0, 0xF01B1, 0xF01B2, 0xF01B3, 0xF01B4,
0xF01B5, 0xF01B6, 0xF01B7, 0xF01B8, 0xF01B9,
// Marwari
0xF01BA, 0xF01BB,
// Extended ligatures
0xF01BD, 0xF01BE, 0xF01BF,
0xF01C0, 0xF01C1, 0xF01C2, 0xF01C3, 0xF01C4, 0xF01C5,
0xF01C6, 0xF01C7, 0xF01C8, 0xF01C9, 0xF01CA, 0xF01CB,
0xF01CD, 0xF01CE, 0xF01CF,
0xF01D0, 0xF01D1, 0xF01D2, 0xF01D3, 0xF01D4, 0xF01D5,
0xF01D6, 0xF01D7, 0xF01D8, 0xF01D9, 0xF01DA,
0xF01DB, 0xF01DC, 0xF01DD, 0xF01DE, 0xF01DF,
0xF01E0, 0xF01E1, 0xF01E2, 0xF01E3,
)
// Tamil ligatures
val tamilLigatures = listOf(
0xF00C0, 0xF00C1, // TTA+I, TTA+II
0xF00ED, // KSSA
0xF00EE, // SHRII
0xF00F0, 0xF00F1, 0xF00F2, 0xF00F3, 0xF00F4, 0xF00F5, // consonant+I
) + (0xF00C2..0xF00D3).toList() + // consonant+U
(0xF00D4..0xF00E5).toList() // consonant+UU
// Sundanese internal forms
val sundanese = listOf(
0xF0500, // ING
0xF0501, // ENG
0xF0502, // EUNG
0xF0503, // IR
0xF0504, // ER
0xF0505, // EUR
0xF0506, // LU
)
// Alphabetic Presentation Forms (already in sheet 38)
// FB00FB06 (Latin ligatures), FB13FB17 (Armenian ligatures)
// Check all expected ranges
val allExpected = devanagariSpecials + devaPresentation + devaHalf + devaRa + devaRaHalf +
devaII + devaLigatures + tamilLigatures + sundanese
for (cp in allExpected) {
if (!glyphs.containsKey(cp)) {
missing.add(cp)
}
}
if (missing.isNotEmpty()) {
println(" [DevanagariTamilProcessor] ${missing.size} expected PUA glyphs missing")
// Only warn for the first few
missing.take(10).forEach { println(" Missing: U+${it.toString(16).uppercase().padStart(5, '0')}") }
if (missing.size > 10) println(" ... and ${missing.size - 10} more")
} else {
println(" [DevanagariTamilProcessor] All expected PUA glyphs present")
}
return missing
}
}

View File

@@ -0,0 +1,321 @@
package net.torvald.otfbuild
import com.kreative.bitsnpicas.BitmapFontGlyph
import java.io.File
/**
* Glyph properties extracted from tag column.
* Mirrors GlyphProps from the runtime but is standalone.
*/
data class ExtractedGlyphProps(
val width: Int,
val isLowHeight: Boolean = false,
val nudgeX: Int = 0,
val nudgeY: Int = 0,
val alignWhere: Int = 0,
val writeOnTop: Int = -1,
val stackWhere: Int = 0,
val hasKernData: Boolean = false,
val isKernYtype: Boolean = false,
val kerningMask: Int = 255,
val directiveOpcode: Int = 0,
val directiveArg1: Int = 0,
val directiveArg2: Int = 0,
val extInfo: IntArray = IntArray(15),
) {
companion object {
const val ALIGN_LEFT = 0
const val ALIGN_RIGHT = 1
const val ALIGN_CENTRE = 2
const val ALIGN_BEFORE = 3
const val STACK_UP = 0
const val STACK_DOWN = 1
const val STACK_BEFORE_N_AFTER = 2
const val STACK_UP_N_DOWN = 3
const val STACK_DONT = 4
}
fun requiredExtInfoCount(): Int =
if (stackWhere == STACK_BEFORE_N_AFTER) 2
else if (directiveOpcode in 0b10000_000..0b10000_111) 7
else 0
fun isPragma(pragma: String) = when (pragma) {
"replacewith" -> directiveOpcode in 0b10000_000..0b10000_111
else -> false
}
val isIllegal: Boolean get() = directiveOpcode == 255
}
data class ExtractedGlyph(
val codepoint: Int,
val props: ExtractedGlyphProps,
val bitmap: Array<ByteArray>, // [row][col], 0 or -1(0xFF)
)
/**
* Extracts glyph bitmaps and properties from TGA sprite sheets.
* Ported from TerrarumSansBitmap.buildWidthTable() and related methods.
*/
class GlyphSheetParser(private val assetsDir: String) {
private fun Boolean.toInt() = if (this) 1 else 0
/** @return 32-bit number: if alpha channel is zero, return 0; else return the original value */
private fun Int.tagify() = if (this and 0xFF == 0) 0 else this
/**
* Parse all sheets and return a map of codepoint -> (props, bitmap).
*/
fun parseAll(): Map<Int, ExtractedGlyph> {
val result = HashMap<Int, ExtractedGlyph>(65536)
SheetConfig.fileList.forEachIndexed { sheetIndex, filename ->
val file = File(assetsDir, filename)
if (!file.exists()) {
println(" [SKIP] $filename not found")
return@forEachIndexed
}
val isVariable = SheetConfig.isVariable(filename)
val isXYSwapped = SheetConfig.isXYSwapped(filename)
val isExtraWide = SheetConfig.isExtraWide(filename)
val cellW = SheetConfig.getCellWidth(sheetIndex)
val cellH = SheetConfig.getCellHeight(sheetIndex)
val cols = SheetConfig.getColumns(sheetIndex)
val image = TgaReader.read(file)
val statusParts = mutableListOf<String>()
if (isVariable) statusParts.add("VARIABLE")
if (isXYSwapped) statusParts.add("XYSWAP")
if (isExtraWide) statusParts.add("EXTRAWIDE")
if (statusParts.isEmpty()) statusParts.add("STATIC")
println(" Loading [${statusParts.joinToString()}] $filename")
if (isVariable) {
parseVariableSheet(image, sheetIndex, cellW, cellH, cols, isXYSwapped, result)
} else {
parseFixedSheet(image, sheetIndex, cellW, cellH, cols, result)
}
}
// Add fixed-width overrides
addFixedWidthOverrides(result)
return result
}
/**
* Parse a variable-width sheet: extract tag column for properties, bitmap for glyph.
*/
private fun parseVariableSheet(
image: TgaImage,
sheetIndex: Int,
cellW: Int,
cellH: Int,
cols: Int,
isXYSwapped: Boolean,
result: HashMap<Int, ExtractedGlyph>
) {
val codeRangeList = SheetConfig.codeRange[sheetIndex]
val binaryCodeOffset = cellW - 1 // tag column is last pixel column of cell
codeRangeList.forEachIndexed { index, code ->
val cellX: Int
val cellY: Int
if (isXYSwapped) {
cellX = (index / cols) * cellW // row becomes X
cellY = (index % cols) * cellH // col becomes Y
} else {
cellX = (index % cols) * cellW
cellY = (index / cols) * cellH
}
val codeStartX = cellX + binaryCodeOffset
val codeStartY = cellY
// Parse tag column
val width = (0..4).fold(0) { acc, y ->
acc or ((image.getPixel(codeStartX, codeStartY + y).and(0xFF) != 0).toInt() shl y)
}
val isLowHeight = image.getPixel(codeStartX, codeStartY + 5).and(0xFF) != 0
// Kerning data
val kerningBit1 = image.getPixel(codeStartX, codeStartY + 6).tagify()
val kerningBit2 = image.getPixel(codeStartX, codeStartY + 7).tagify()
val kerningBit3 = image.getPixel(codeStartX, codeStartY + 8).tagify()
var isKernYtype = (kerningBit1 and 0x80000000.toInt()) != 0
var kerningMask = kerningBit1.ushr(8).and(0xFFFFFF)
val hasKernData = kerningBit1 and 0xFF != 0
if (!hasKernData) {
isKernYtype = false
kerningMask = 255
}
// Compiler directives
val compilerDirectives = image.getPixel(codeStartX, codeStartY + 9).tagify()
val directiveOpcode = compilerDirectives.ushr(24).and(255)
val directiveArg1 = compilerDirectives.ushr(16).and(255)
val directiveArg2 = compilerDirectives.ushr(8).and(255)
// Nudge
val nudgingBits = image.getPixel(codeStartX, codeStartY + 10).tagify()
val nudgeX = nudgingBits.ushr(24).toByte().toInt()
val nudgeY = nudgingBits.ushr(16).toByte().toInt()
// Diacritics anchors (we don't store them in ExtractedGlyphProps for now but could)
// For alignment and width, they are useful during composition but not in final output
// Alignment
val alignWhere = (0..1).fold(0) { acc, y ->
acc or ((image.getPixel(codeStartX, codeStartY + y + 15).and(0xFF) != 0).toInt() shl y)
}
// Write on top
var writeOnTop = image.getPixel(codeStartX, codeStartY + 17) // NO .tagify()
if (writeOnTop and 0xFF == 0) writeOnTop = -1
else {
writeOnTop = if (writeOnTop.ushr(8) == 0xFFFFFF) 0 else writeOnTop.ushr(28) and 15
}
// Stack where
val stackWhere0 = image.getPixel(codeStartX, codeStartY + 18).tagify()
val stackWhere1 = image.getPixel(codeStartX, codeStartY + 19).tagify()
val stackWhere = if (stackWhere0 == 0x00FF00FF && stackWhere1 == 0x00FF00FF)
ExtractedGlyphProps.STACK_DONT
else (0..1).fold(0) { acc, y ->
acc or ((image.getPixel(codeStartX, codeStartY + y + 18).and(0xFF) != 0).toInt() shl y)
}
val extInfo = IntArray(15)
val props = ExtractedGlyphProps(
width, isLowHeight, nudgeX, nudgeY, alignWhere, writeOnTop, stackWhere,
hasKernData, isKernYtype, kerningMask, directiveOpcode, directiveArg1, directiveArg2, extInfo
)
// Parse extInfo if needed
val extCount = props.requiredExtInfoCount()
if (extCount > 0) {
for (x in 0 until extCount) {
var info = 0
for (y in 0..19) {
if (image.getPixel(cellX + x, cellY + y).and(0xFF) != 0) {
info = info or (1 shl y)
}
}
extInfo[x] = info
}
}
// Extract glyph bitmap: all pixels in cell except tag column
val bitmapW = cellW - 1 // exclude tag column
val bitmap = Array(cellH) { row ->
ByteArray(bitmapW) { col ->
val px = image.getPixel(cellX + col, cellY + row)
if (px and 0xFF != 0) 0xFF.toByte() else 0
}
}
result[code] = ExtractedGlyph(code, props, bitmap)
}
}
/**
* Parse a fixed-width sheet (Hangul, Unihan, Runic, Custom Sym).
*/
private fun parseFixedSheet(
image: TgaImage,
sheetIndex: Int,
cellW: Int,
cellH: Int,
cols: Int,
result: HashMap<Int, ExtractedGlyph>
) {
val codeRangeList = SheetConfig.codeRange[sheetIndex]
val fixedWidth = when (sheetIndex) {
SheetConfig.SHEET_CUSTOM_SYM -> 20
SheetConfig.SHEET_HANGUL -> SheetConfig.W_HANGUL_BASE
SheetConfig.SHEET_RUNIC -> 9
SheetConfig.SHEET_UNIHAN -> SheetConfig.W_UNIHAN
else -> cellW
}
codeRangeList.forEachIndexed { index, code ->
val cellX = (index % cols) * cellW
val cellY = (index / cols) * cellH
val bitmap = Array(cellH) { row ->
ByteArray(cellW) { col ->
val px = image.getPixel(cellX + col, cellY + row)
if (px and 0xFF != 0) 0xFF.toByte() else 0
}
}
val props = ExtractedGlyphProps(fixedWidth)
result[code] = ExtractedGlyph(code, props, bitmap)
}
}
/**
* Apply fixed-width overrides as in buildWidthTableFixed().
*/
private fun addFixedWidthOverrides(result: HashMap<Int, ExtractedGlyph>) {
// Hangul compat jamo
SheetConfig.codeRangeHangulCompat.forEach { code ->
if (!result.containsKey(code)) {
result[code] = ExtractedGlyph(code, ExtractedGlyphProps(SheetConfig.W_HANGUL_BASE), emptyBitmap())
}
}
// Zero-width ranges
(0xD800..0xDFFF).forEach { result[it] = ExtractedGlyph(it, ExtractedGlyphProps(0), emptyBitmap()) }
(0x100000..0x10FFFF).forEach { result[it] = ExtractedGlyph(it, ExtractedGlyphProps(0), emptyBitmap()) }
(0xFFFA0..0xFFFFF).forEach { result[it] = ExtractedGlyph(it, ExtractedGlyphProps(0), emptyBitmap()) }
// Insular letter
result[0x1D79]?.let { /* already in sheet */ } ?: run {
result[0x1D79] = ExtractedGlyph(0x1D79, ExtractedGlyphProps(9), emptyBitmap())
}
// Replacement character at U+007F
result[0x7F]?.let { existing ->
result[0x7F] = existing.copy(props = existing.props.copy(width = 15))
}
// Null char
result[0] = ExtractedGlyph(0, ExtractedGlyphProps(0), emptyBitmap())
}
private fun emptyBitmap() = Array(SheetConfig.H) { ByteArray(SheetConfig.W_VAR_INIT) }
/**
* Extracts raw Hangul jamo bitmaps from the Hangul sheet for composition.
* Returns a function: (index, row) -> bitmap
*/
fun getHangulJamoBitmaps(): (Int, Int) -> Array<ByteArray> {
val filename = SheetConfig.fileList[SheetConfig.SHEET_HANGUL]
val file = File(assetsDir, filename)
if (!file.exists()) {
println(" [WARNING] Hangul sheet not found")
return { _, _ -> Array(SheetConfig.H) { ByteArray(SheetConfig.W_HANGUL_BASE) } }
}
val image = TgaReader.read(file)
val cellW = SheetConfig.W_HANGUL_BASE
val cellH = SheetConfig.H
return { index: Int, row: Int ->
val cellX = index * cellW
val cellY = row * cellH
Array(cellH) { r ->
ByteArray(cellW) { c ->
val px = image.getPixel(cellX + c, cellY + r)
if (px and 0xFF != 0) 0xFF.toByte() else 0
}
}
}
}
}

View File

@@ -0,0 +1,124 @@
package net.torvald.otfbuild
import com.kreative.bitsnpicas.BitmapFontGlyph
/**
* Composes 11,172 Hangul syllables (U+AC00U+D7A3) from jamo sprite pieces.
* Also composes Hangul Compatibility Jamo (U+3130U+318F).
*
* Ported from TerrarumSansBitmap.kt Hangul assembly logic.
*/
class HangulCompositor(private val parser: GlyphSheetParser) {
private val getJamoBitmap = parser.getHangulJamoBitmaps()
private val cellW = SheetConfig.W_HANGUL_BASE
private val cellH = SheetConfig.H
/**
* Compose all Hangul syllables and compatibility jamo.
* @return Map of codepoint to BitmapFontGlyph
*/
fun compose(): Map<Int, Pair<BitmapFontGlyph, Int>> {
val result = HashMap<Int, Pair<BitmapFontGlyph, Int>>(12000)
// Compose Hangul Compatibility Jamo (U+3130U+318F)
// These are standalone jamo from row 0 of the sheet
for (c in 0x3130..0x318F) {
val index = c - 0x3130
val bitmap = getJamoBitmap(index, 0)
val glyph = bitmapToGlyph(bitmap, cellW, cellH)
result[c] = glyph to cellW
}
// Compose 11,172 Hangul syllables (U+AC00U+D7A3)
println(" Composing 11,172 Hangul syllables...")
for (c in 0xAC00..0xD7A3) {
val cInt = c - 0xAC00
val indexCho = cInt / (SheetConfig.JUNG_COUNT * SheetConfig.JONG_COUNT)
val indexJung = cInt / SheetConfig.JONG_COUNT % SheetConfig.JUNG_COUNT
val indexJong = cInt % SheetConfig.JONG_COUNT // 0 = no jongseong
// Map to jamo codepoints
val choCP = 0x1100 + indexCho
val jungCP = 0x1161 + indexJung
val jongCP = if (indexJong > 0) 0x11A8 + indexJong - 1 else 0
// Get sheet indices
val iCho = SheetConfig.toHangulChoseongIndex(choCP)
val iJung = SheetConfig.toHangulJungseongIndex(jungCP) ?: 0
val iJong = if (jongCP != 0) SheetConfig.toHangulJongseongIndex(jongCP) ?: 0 else 0
// Get row positions
val choRow = SheetConfig.getHanInitialRow(iCho, iJung, iJong)
val jungRow = SheetConfig.getHanMedialRow(iCho, iJung, iJong)
val jongRow = SheetConfig.getHanFinalRow(iCho, iJung, iJong)
// Get jamo bitmaps
val choBitmap = getJamoBitmap(iCho, choRow)
val jungBitmap = getJamoBitmap(iJung, jungRow)
// Compose
val composed = composeBitmaps(choBitmap, jungBitmap, cellW, cellH)
if (indexJong > 0) {
val jongBitmap = getJamoBitmap(iJong, jongRow)
composeBitmapInto(composed, jongBitmap, cellW, cellH)
}
// Determine advance width
val advanceWidth = if (iJung in SheetConfig.hangulPeaksWithExtraWidth) cellW + 1 else cellW
val glyph = bitmapToGlyph(composed, advanceWidth, cellH)
result[c] = glyph to advanceWidth
}
println(" Hangul composition done: ${result.size} glyphs")
return result
}
/**
* Compose two bitmaps by OR-ing them together.
*/
private fun composeBitmaps(a: Array<ByteArray>, b: Array<ByteArray>, w: Int, h: Int): Array<ByteArray> {
val result = Array(h) { row ->
ByteArray(w) { col ->
val av = a.getOrNull(row)?.getOrNull(col)?.toInt()?.and(0xFF) ?: 0
val bv = b.getOrNull(row)?.getOrNull(col)?.toInt()?.and(0xFF) ?: 0
if (av != 0 || bv != 0) 0xFF.toByte() else 0
}
}
return result
}
/**
* OR a bitmap into an existing one.
*/
private fun composeBitmapInto(target: Array<ByteArray>, source: Array<ByteArray>, w: Int, h: Int) {
for (row in 0 until minOf(h, target.size, source.size)) {
for (col in 0 until minOf(w, target[row].size, source[row].size)) {
if (source[row][col].toInt() and 0xFF != 0) {
target[row][col] = 0xFF.toByte()
}
}
}
}
companion object {
/**
* Convert a byte[][] bitmap to BitmapFontGlyph.
*/
fun bitmapToGlyph(bitmap: Array<ByteArray>, advanceWidth: Int, cellH: Int): BitmapFontGlyph {
val h = bitmap.size
val w = if (h > 0) bitmap[0].size else 0
val glyphData = Array(h) { row ->
ByteArray(w) { col -> bitmap[row][col] }
}
// BitmapFontGlyph(byte[][] glyph, int offset, int width, int ascent)
// offset = x offset (left side bearing), width = advance width, ascent = baseline from top
val glyph = BitmapFontGlyph()
glyph.setGlyph(glyphData)
glyph.setXY(0, cellH) // y = ascent from top of em square to baseline
glyph.setCharacterWidth(advanceWidth)
return glyph
}
}
}

View File

@@ -0,0 +1,218 @@
package net.torvald.otfbuild
import com.kreative.bitsnpicas.BitmapFont
import com.kreative.bitsnpicas.BitmapFontGlyph
import com.kreative.bitsnpicas.Font
import com.kreative.bitsnpicas.exporter.KbitxBitmapFontExporter
import java.io.File
/**
* Orchestrates the entire font building pipeline:
* 1. Parse all TGA sheets
* 2. Create BitmapFont with metrics
* 3. Add all extracted glyphs
* 4. Compose Hangul syllables
* 5. Verify Devanagari/Tamil PUA glyphs
* 6. Generate kerning pairs
* 7. Export to KBITX
*/
class KbitxBuilder(private val assetsDir: String) {
fun build(outputPath: String) {
println("=== Terrarum Sans Bitmap OTF Builder ===")
println("Assets: $assetsDir")
println("Output: $outputPath")
println()
// 1. Create BitmapFont with metrics
println("[1/7] Creating BitmapFont...")
val font = BitmapFont(
16, // emAscent: baseline to top of em square
4, // emDescent: baseline to bottom of em square
16, // lineAscent
4, // lineDescent
8, // xHeight
12, // capHeight
0 // lineGap
)
// Set font names
font.setName(Font.NAME_FAMILY, "Terrarum Sans Bitmap")
font.setName(Font.NAME_STYLE, "Regular")
font.setName(Font.NAME_VERSION, "Version 1.0")
font.setName(Font.NAME_FAMILY_AND_STYLE, "Terrarum Sans Bitmap Regular")
font.setName(Font.NAME_COPYRIGHT, "Copyright (c) 2017-2026 see CONTRIBUTORS.txt")
font.setName(Font.NAME_DESCRIPTION, "Bitmap font for Terrarum game engine")
font.setName(Font.NAME_LICENSE_DESCRIPTION, "MIT License")
// 2. Parse all TGA sheets
println("[2/7] Parsing TGA sprite sheets...")
val parser = GlyphSheetParser(assetsDir)
val allGlyphs = parser.parseAll()
println(" Parsed ${allGlyphs.size} glyphs from sheets")
// 3. Add all extracted glyphs to BitmapFont
println("[3/7] Adding glyphs to BitmapFont...")
var addedCount = 0
var skippedCount = 0
for ((codepoint, extracted) in allGlyphs) {
// Skip zero-width control characters and surrogates — don't add empty glyphs
if (extracted.props.width <= 0 && codepoint != 0x7F) {
// Still add zero-width glyphs that have actual bitmap data
val hasPixels = extracted.bitmap.any { row -> row.any { it.toInt() and 0xFF != 0 } }
if (!hasPixels) {
skippedCount++
continue
}
}
// Skip internal-only codepoints that would cause issues
if (codepoint in 0x100000..0x10FFFF || codepoint in 0xD800..0xDFFF) {
skippedCount++
continue
}
val glyph = extractedToBitmapFontGlyph(extracted)
font.putCharacter(codepoint, glyph)
addedCount++
}
println(" Added $addedCount glyphs, skipped $skippedCount")
// 4. Compose Hangul syllables
println("[4/7] Composing Hangul syllables...")
val hangulCompositor = HangulCompositor(parser)
val hangulGlyphs = hangulCompositor.compose()
for ((codepoint, pair) in hangulGlyphs) {
val (glyph, _) = pair
font.putCharacter(codepoint, glyph)
}
println(" Added ${hangulGlyphs.size} Hangul glyphs")
// 5. Verify Devanagari/Tamil PUA
println("[5/7] Verifying Devanagari/Tamil PUA glyphs...")
val devaTamilProcessor = DevanagariTamilProcessor()
devaTamilProcessor.verify(allGlyphs)
// 6. Generate kerning pairs
println("[6/7] Generating kerning pairs...")
val kemingMachine = KemingMachine()
val kernPairs = kemingMachine.generateKerningPairs(allGlyphs)
for ((pair, offset) in kernPairs) {
font.setKernPair(pair, offset)
}
println(" Added ${kernPairs.size} kerning pairs")
// 7. Add spacing characters
println("[7/7] Finalising...")
addSpacingCharacters(font, allGlyphs)
// Add .notdef from U+007F (replacement character)
allGlyphs[0x7F]?.let {
val notdefGlyph = extractedToBitmapFontGlyph(it)
font.putNamedGlyph(".notdef", notdefGlyph)
}
// Contract glyphs to trim whitespace
font.contractGlyphs()
// Auto-fill any missing name fields
font.autoFillNames()
// Count glyphs
val totalGlyphs = font.characters(false).size
println()
println("Total glyph count: $totalGlyphs")
// Export
println("Exporting to KBITX: $outputPath")
val exporter = KbitxBitmapFontExporter()
exporter.exportFontToFile(font, File(outputPath))
println("Done!")
}
private fun extractedToBitmapFontGlyph(extracted: ExtractedGlyph): BitmapFontGlyph {
val bitmap = extracted.bitmap
val props = extracted.props
val h = bitmap.size
val w = if (h > 0) bitmap[0].size else 0
val glyphData = Array(h) { row ->
ByteArray(w) { col -> bitmap[row][col] }
}
val glyph = BitmapFontGlyph()
glyph.setGlyph(glyphData)
// y = distance from top of glyph to baseline
// For most glyphs this is 16 (baseline at row 16 from top in a 20px cell)
// For Unihan: baseline at row 14 (offset by 2 from the 16px cell centred in 20px)
val sheetIndex = getSheetIndex(extracted.codepoint)
val baseline = when (sheetIndex) {
SheetConfig.SHEET_UNIHAN -> 14
SheetConfig.SHEET_CUSTOM_SYM -> 16
else -> 16
}
glyph.setXY(0, baseline)
glyph.setCharacterWidth(props.width)
return glyph
}
private fun getSheetIndex(codepoint: Int): Int {
// Check fixed sheets first
if (codepoint in 0xF0000..0xF005F) return SheetConfig.SHEET_BULGARIAN_VARW
if (codepoint in 0xF0060..0xF00BF) return SheetConfig.SHEET_SERBIAN_VARW
for (i in SheetConfig.codeRange.indices.reversed()) {
if (codepoint in SheetConfig.codeRange[i]) return i
}
return SheetConfig.SHEET_UNKNOWN
}
/**
* Add spacing characters as empty glyphs with correct advance widths.
*/
private fun addSpacingCharacters(font: BitmapFont, allGlyphs: Map<Int, ExtractedGlyph>) {
val figWidth = allGlyphs[0x30]?.props?.width ?: 9
val punctWidth = allGlyphs[0x2E]?.props?.width ?: 6
val em = 12 + 1 // as defined in the original
fun Int.halveWidth() = this / 2 + 1
val spacings = mapOf(
SheetConfig.NQSP to em.halveWidth(),
SheetConfig.MQSP to em,
SheetConfig.ENSP to em.halveWidth(),
SheetConfig.EMSP to em,
SheetConfig.THREE_PER_EMSP to (em / 3 + 1),
SheetConfig.QUARTER_EMSP to (em / 4 + 1),
SheetConfig.SIX_PER_EMSP to (em / 6 + 1),
SheetConfig.FSP to figWidth,
SheetConfig.PSP to punctWidth,
SheetConfig.THSP to 2,
SheetConfig.HSP to 1,
SheetConfig.ZWSP to 0,
SheetConfig.ZWNJ to 0,
SheetConfig.ZWJ to 0,
SheetConfig.SHY to 0,
)
for ((cp, width) in spacings) {
val glyph = BitmapFontGlyph()
glyph.setGlyph(Array(SheetConfig.H) { ByteArray(0) })
glyph.setXY(0, 16)
glyph.setCharacterWidth(width)
font.putCharacter(cp, glyph)
}
// NBSP: same width as space
val spaceWidth = allGlyphs[32]?.props?.width ?: 7
val nbspGlyph = BitmapFontGlyph()
nbspGlyph.setGlyph(Array(SheetConfig.H) { ByteArray(0) })
nbspGlyph.setXY(0, 16)
nbspGlyph.setCharacterWidth(spaceWidth)
font.putCharacter(SheetConfig.NBSP, nbspGlyph)
}
}

View File

@@ -0,0 +1,120 @@
package net.torvald.otfbuild
import com.kreative.bitsnpicas.GlyphPair
/**
* Generates kerning pairs from shape rules.
* Ported from TerrarumSansBitmap.kt "The Keming Machine" section.
*/
class KemingMachine {
private class Ing(val s: String) {
private var careBits = 0
private var ruleBits = 0
init {
s.forEachIndexed { index, char ->
when (char) {
'@' -> {
careBits = careBits or SheetConfig.kemingBitMask[index]
ruleBits = ruleBits or SheetConfig.kemingBitMask[index]
}
'`' -> {
careBits = careBits or SheetConfig.kemingBitMask[index]
}
}
}
}
fun matches(shapeBits: Int) = ((shapeBits and careBits) == ruleBits)
override fun toString() = "C:${careBits.toString(2).padStart(16, '0')}-R:${ruleBits.toString(2).padStart(16, '0')}"
}
private data class Kem(val first: Ing, val second: Ing, val bb: Int = 2, val yy: Int = 1)
private val kerningRules: List<Kem>
init {
val baseRules = listOf(
Kem(Ing("_`_@___`__"), Ing("`_`___@___")),
Kem(Ing("_@_`___`__"), Ing("`_________")),
Kem(Ing("_@_@___`__"), Ing("`___@_@___"), 1, 1),
Kem(Ing("_@_@_`_`__"), Ing("`_____@___")),
Kem(Ing("___`_`____"), Ing("`___@_`___")),
Kem(Ing("___`_`____"), Ing("`_@___`___")),
)
// Automatically create mirrored versions
val mirrored = baseRules.map { rule ->
val left = rule.first.s
val right = rule.second.s
val newLeft = StringBuilder()
val newRight = StringBuilder()
for (c in left.indices step 2) {
newLeft.append(right[c + 1]).append(right[c])
newRight.append(left[c + 1]).append(left[c])
}
Kem(Ing(newLeft.toString()), Ing(newRight.toString()), rule.bb, rule.yy)
}
kerningRules = baseRules + mirrored
}
/**
* Generate kerning pairs from all glyphs that have kerning data.
* @return Map of GlyphPair to kern offset (negative values = tighter)
*/
fun generateKerningPairs(glyphs: Map<Int, ExtractedGlyph>): Map<GlyphPair, Int> {
val result = HashMap<GlyphPair, Int>()
// Collect all codepoints with kerning data
val kernableGlyphs = glyphs.filter { it.value.props.hasKernData }
if (kernableGlyphs.isEmpty()) {
println(" [KemingMachine] No glyphs with kern data found")
return result
}
println(" [KemingMachine] ${kernableGlyphs.size} glyphs with kern data")
// Special rule: lowercase r + dot
for (r in SheetConfig.lowercaseRs) {
for (d in SheetConfig.dots) {
if (glyphs.containsKey(r) && glyphs.containsKey(d)) {
result[GlyphPair(r, d)] = -1
}
}
}
// Apply kerning rules to all pairs
val kernCodes = kernableGlyphs.keys.toIntArray()
var pairsFound = 0
for (leftCode in kernCodes) {
val leftProps = kernableGlyphs[leftCode]!!.props
val maskL = leftProps.kerningMask
for (rightCode in kernCodes) {
val rightProps = kernableGlyphs[rightCode]!!.props
val maskR = rightProps.kerningMask
for (rule in kerningRules) {
if (rule.first.matches(maskL) && rule.second.matches(maskR)) {
val contraction = if (leftProps.isKernYtype || rightProps.isKernYtype) rule.yy else rule.bb
if (contraction > 0) {
result[GlyphPair(leftCode, rightCode)] = -contraction
pairsFound++
}
break // first matching rule wins
}
}
}
}
println(" [KemingMachine] Generated $pairsFound kerning pairs (+ ${SheetConfig.lowercaseRs.size * SheetConfig.dots.size} r-dot pairs)")
return result
}
}

View File

@@ -0,0 +1,7 @@
package net.torvald.otfbuild
fun main(args: Array<String>) {
val assetsDir = args.getOrElse(0) { "src/assets" }
val outputPath = args.getOrElse(1) { "OTFbuild/TerrarumSansBitmap.kbitx" }
KbitxBuilder(assetsDir).build(outputPath)
}

View File

@@ -0,0 +1,377 @@
package net.torvald.otfbuild
typealias CodePoint = Int
/**
* Ported from TerrarumSansBitmap.kt companion object.
* All sheet definitions, code ranges, index functions, and font metric constants.
*/
object SheetConfig {
// Font metrics
const val H = 20
const val H_UNIHAN = 16
const val W_HANGUL_BASE = 13
const val W_UNIHAN = 16
const val W_LATIN_WIDE = 9
const val W_VAR_INIT = 15
const val W_WIDEVAR_INIT = 31
const val HGAP_VAR = 1
const val SIZE_CUSTOM_SYM = 20
const val H_DIACRITICS = 3
const val H_STACKUP_LOWERCASE_SHIFTDOWN = 4
const val H_OVERLAY_LOWERCASE_SHIFTDOWN = 2
const val LINE_HEIGHT = 24
// Sheet indices
const val SHEET_ASCII_VARW = 0
const val SHEET_HANGUL = 1
const val SHEET_EXTA_VARW = 2
const val SHEET_EXTB_VARW = 3
const val SHEET_KANA = 4
const val SHEET_CJK_PUNCT = 5
const val SHEET_UNIHAN = 6
const val SHEET_CYRILIC_VARW = 7
const val SHEET_HALFWIDTH_FULLWIDTH_VARW = 8
const val SHEET_UNI_PUNCT_VARW = 9
const val SHEET_GREEK_VARW = 10
const val SHEET_THAI_VARW = 11
const val SHEET_HAYEREN_VARW = 12
const val SHEET_KARTULI_VARW = 13
const val SHEET_IPA_VARW = 14
const val SHEET_RUNIC = 15
const val SHEET_LATIN_EXT_ADD_VARW = 16
const val SHEET_CUSTOM_SYM = 17
const val SHEET_BULGARIAN_VARW = 18
const val SHEET_SERBIAN_VARW = 19
const val SHEET_TSALAGI_VARW = 20
const val SHEET_PHONETIC_EXT_VARW = 21
const val SHEET_DEVANAGARI_VARW = 22
const val SHEET_KARTULI_CAPS_VARW = 23
const val SHEET_DIACRITICAL_MARKS_VARW = 24
const val SHEET_GREEK_POLY_VARW = 25
const val SHEET_EXTC_VARW = 26
const val SHEET_EXTD_VARW = 27
const val SHEET_CURRENCIES_VARW = 28
const val SHEET_INTERNAL_VARW = 29
const val SHEET_LETTERLIKE_MATHS_VARW = 30
const val SHEET_ENCLOSED_ALPHNUM_SUPL_VARW = 31
const val SHEET_TAMIL_VARW = 32
const val SHEET_BENGALI_VARW = 33
const val SHEET_BRAILLE_VARW = 34
const val SHEET_SUNDANESE_VARW = 35
const val SHEET_DEVANAGARI2_INTERNAL_VARW = 36
const val SHEET_CODESTYLE_ASCII_VARW = 37
const val SHEET_ALPHABETIC_PRESENTATION_FORMS = 38
const val SHEET_HENTAIGANA_VARW = 39
const val SHEET_UNKNOWN = 254
val fileList = arrayOf(
"ascii_variable.tga",
"hangul_johab.tga",
"latinExtA_variable.tga",
"latinExtB_variable.tga",
"kana_variable.tga",
"cjkpunct_variable.tga",
"wenquanyi.tga",
"cyrilic_variable.tga",
"halfwidth_fullwidth_variable.tga",
"unipunct_variable.tga",
"greek_variable.tga",
"thai_variable.tga",
"hayeren_variable.tga",
"kartuli_variable.tga",
"ipa_ext_variable.tga",
"futhark.tga",
"latinExt_additional_variable.tga",
"puae000-e0ff.tga",
"cyrilic_bulgarian_variable.tga",
"cyrilic_serbian_variable.tga",
"tsalagi_variable.tga",
"phonetic_extensions_variable.tga",
"devanagari_variable.tga",
"kartuli_allcaps_variable.tga",
"diacritical_marks_variable.tga",
"greek_polytonic_xyswap_variable.tga",
"latinExtC_variable.tga",
"latinExtD_variable.tga",
"currencies_variable.tga",
"internal_variable.tga",
"letterlike_symbols_variable.tga",
"enclosed_alphanumeric_supplement_variable.tga",
"tamil_extrawide_variable.tga",
"bengali_variable.tga",
"braille_variable.tga",
"sundanese_variable.tga",
"devanagari_internal_extrawide_variable.tga",
"pua_codestyle_ascii_variable.tga",
"alphabetic_presentation_forms_extrawide_variable.tga",
"hentaigana_variable.tga",
)
val codeRange: Array<List<Int>> = arrayOf(
(0..0xFF).toList(),
(0x1100..0x11FF).toList() + (0xA960..0xA97F).toList() + (0xD7B0..0xD7FF).toList(),
(0x100..0x17F).toList(),
(0x180..0x24F).toList(),
(0x3040..0x30FF).toList() + (0x31F0..0x31FF).toList(),
(0x3000..0x303F).toList(),
(0x3400..0x9FFF).toList(),
(0x400..0x52F).toList(),
(0xFF00..0xFFFF).toList(),
(0x2000..0x209F).toList(),
(0x370..0x3CE).toList(),
(0xE00..0xE5F).toList(),
(0x530..0x58F).toList(),
(0x10D0..0x10FF).toList(),
(0x250..0x2FF).toList(),
(0x16A0..0x16FF).toList(),
(0x1E00..0x1EFF).toList(),
(0xE000..0xE0FF).toList(),
(0xF0000..0xF005F).toList(),
(0xF0060..0xF00BF).toList(),
(0x13A0..0x13F5).toList(),
(0x1D00..0x1DBF).toList(),
(0x900..0x97F).toList() + (0xF0100..0xF04FF).toList(),
(0x1C90..0x1CBF).toList(),
(0x300..0x36F).toList(),
(0x1F00..0x1FFF).toList(),
(0x2C60..0x2C7F).toList(),
(0xA720..0xA7FF).toList(),
(0x20A0..0x20CF).toList(),
(0xFFE00..0xFFF9F).toList(),
(0x2100..0x214F).toList(),
(0x1F100..0x1F1FF).toList(),
(0x0B80..0x0BFF).toList() + (0xF00C0..0xF00FF).toList(),
(0x980..0x9FF).toList(),
(0x2800..0x28FF).toList(),
(0x1B80..0x1BBF).toList() + (0x1CC0..0x1CCF).toList() + (0xF0500..0xF050F).toList(),
(0xF0110..0xF012F).toList(),
(0xF0520..0xF057F).toList(),
(0xFB00..0xFB17).toList(),
(0x1B000..0x1B16F).toList(),
)
val codeRangeHangulCompat = 0x3130..0x318F
val altCharsetCodepointOffsets = intArrayOf(
0,
0xF0000 - 0x400, // Bulgarian
0xF0060 - 0x400, // Serbian
0xF0520 - 0x20, // Codestyle
)
val altCharsetCodepointDomains = arrayOf(
0..0x10FFFF,
0x400..0x45F,
0x400..0x45F,
0x20..0x7F,
)
// Unicode spacing characters
const val NQSP = 0x2000
const val MQSP = 0x2001
const val ENSP = 0x2002
const val EMSP = 0x2003
const val THREE_PER_EMSP = 0x2004
const val QUARTER_EMSP = 0x2005
const val SIX_PER_EMSP = 0x2006
const val FSP = 0x2007
const val PSP = 0x2008
const val THSP = 0x2009
const val HSP = 0x200A
const val ZWSP = 0x200B
const val ZWNJ = 0x200C
const val ZWJ = 0x200D
const val SHY = 0xAD
const val NBSP = 0xA0
const val OBJ = 0xFFFC
const val FIXED_BLOCK_1 = 0xFFFD0
const val MOVABLE_BLOCK_M1 = 0xFFFE0
const val MOVABLE_BLOCK_1 = 0xFFFF0
const val CHARSET_OVERRIDE_DEFAULT = 0xFFFC0
const val CHARSET_OVERRIDE_BG_BG = 0xFFFC1
const val CHARSET_OVERRIDE_SR_SR = 0xFFFC2
const val CHARSET_OVERRIDE_CODESTYLE = 0xFFFC3
// Sheet type detection
fun isVariable(filename: String) = filename.endsWith("_variable.tga")
fun isXYSwapped(filename: String) = filename.contains("xyswap", ignoreCase = true)
fun isExtraWide(filename: String) = filename.contains("extrawide", ignoreCase = true)
/** Returns the cell width for a given sheet index. */
fun getCellWidth(sheetIndex: Int): Int = when {
isExtraWide(fileList[sheetIndex]) -> W_WIDEVAR_INIT
isVariable(fileList[sheetIndex]) -> W_VAR_INIT
sheetIndex == SHEET_UNIHAN -> W_UNIHAN
sheetIndex == SHEET_HANGUL -> W_HANGUL_BASE
sheetIndex == SHEET_CUSTOM_SYM -> SIZE_CUSTOM_SYM
sheetIndex == SHEET_RUNIC -> W_LATIN_WIDE
else -> W_VAR_INIT
}
/** Returns the cell height for a given sheet index. */
fun getCellHeight(sheetIndex: Int): Int = when (sheetIndex) {
SHEET_UNIHAN -> H_UNIHAN
SHEET_CUSTOM_SYM -> SIZE_CUSTOM_SYM
else -> H
}
/** Number of columns per row for the sheet. */
fun getColumns(sheetIndex: Int): Int = when (sheetIndex) {
SHEET_UNIHAN -> 256
else -> 16
}
// Index functions (X position in sheet)
fun indexX(c: CodePoint): Int = c % 16
fun unihanIndexX(c: CodePoint): Int = (c - 0x3400) % 256
// Index functions (Y position in sheet) — per sheet type
fun indexY(sheetIndex: Int, c: CodePoint): Int = when (sheetIndex) {
SHEET_ASCII_VARW -> c / 16
SHEET_UNIHAN -> unihanIndexY(c)
SHEET_EXTA_VARW -> (c - 0x100) / 16
SHEET_EXTB_VARW -> (c - 0x180) / 16
SHEET_KANA -> kanaIndexY(c)
SHEET_CJK_PUNCT -> (c - 0x3000) / 16
SHEET_CYRILIC_VARW -> (c - 0x400) / 16
SHEET_HALFWIDTH_FULLWIDTH_VARW -> (c - 0xFF00) / 16
SHEET_UNI_PUNCT_VARW -> (c - 0x2000) / 16
SHEET_GREEK_VARW -> (c - 0x370) / 16
SHEET_THAI_VARW -> (c - 0xE00) / 16
SHEET_CUSTOM_SYM -> (c - 0xE000) / 16
SHEET_HAYEREN_VARW -> (c - 0x530) / 16
SHEET_KARTULI_VARW -> (c - 0x10D0) / 16
SHEET_IPA_VARW -> (c - 0x250) / 16
SHEET_RUNIC -> (c - 0x16A0) / 16
SHEET_LATIN_EXT_ADD_VARW -> (c - 0x1E00) / 16
SHEET_BULGARIAN_VARW -> (c - 0xF0000) / 16
SHEET_SERBIAN_VARW -> (c - 0xF0060) / 16
SHEET_TSALAGI_VARW -> (c - 0x13A0) / 16
SHEET_PHONETIC_EXT_VARW -> (c - 0x1D00) / 16
SHEET_DEVANAGARI_VARW -> devanagariIndexY(c)
SHEET_KARTULI_CAPS_VARW -> (c - 0x1C90) / 16
SHEET_DIACRITICAL_MARKS_VARW -> (c - 0x300) / 16
SHEET_GREEK_POLY_VARW -> (c - 0x1F00) / 16
SHEET_EXTC_VARW -> (c - 0x2C60) / 16
SHEET_EXTD_VARW -> (c - 0xA720) / 16
SHEET_CURRENCIES_VARW -> (c - 0x20A0) / 16
SHEET_INTERNAL_VARW -> (c - 0xFFE00) / 16
SHEET_LETTERLIKE_MATHS_VARW -> (c - 0x2100) / 16
SHEET_ENCLOSED_ALPHNUM_SUPL_VARW -> (c - 0x1F100) / 16
SHEET_TAMIL_VARW -> tamilIndexY(c)
SHEET_BENGALI_VARW -> (c - 0x980) / 16
SHEET_BRAILLE_VARW -> (c - 0x2800) / 16
SHEET_SUNDANESE_VARW -> sundaneseIndexY(c)
SHEET_DEVANAGARI2_INTERNAL_VARW -> (c - 0xF0110) / 16
SHEET_CODESTYLE_ASCII_VARW -> (c - 0xF0520) / 16
SHEET_ALPHABETIC_PRESENTATION_FORMS -> (c - 0xFB00) / 16
SHEET_HENTAIGANA_VARW -> (c - 0x1B000) / 16
SHEET_HANGUL -> 0 // Hangul uses special row logic
else -> c / 16
}
private fun kanaIndexY(c: CodePoint): Int =
if (c in 0x31F0..0x31FF) 12
else (c - 0x3040) / 16
private fun unihanIndexY(c: CodePoint): Int = (c - 0x3400) / 256
private fun devanagariIndexY(c: CodePoint): Int =
(if (c < 0xF0000) (c - 0x0900) else (c - 0xF0080)) / 16
private fun tamilIndexY(c: CodePoint): Int =
(if (c < 0xF0000) (c - 0x0B80) else (c - 0xF0040)) / 16
private fun sundaneseIndexY(c: CodePoint): Int =
(if (c >= 0xF0500) (c - 0xF04B0) else if (c < 0x1BC0) (c - 0x1B80) else (c - 0x1C80)) / 16
// Hangul constants
const val JUNG_COUNT = 21
const val JONG_COUNT = 28
// Hangul shape arrays (sorted)
val jungseongI = sortedSetOf(21, 61)
val jungseongOU = sortedSetOf(9, 13, 14, 18, 34, 35, 39, 45, 51, 53, 54, 64, 73, 80, 83)
val jungseongOUComplex = (listOf(10, 11, 16) + (22..33).toList() + listOf(36, 37, 38) + (41..44).toList() +
(46..50).toList() + (56..59).toList() + listOf(63) + (67..72).toList() + (74..79).toList() +
(81..83).toList() + (85..91).toList() + listOf(93, 94)).toSortedSet()
val jungseongRightie = sortedSetOf(2, 4, 6, 8, 11, 16, 32, 33, 37, 42, 44, 48, 50, 71, 72, 75, 78, 79, 83, 86, 87, 88, 94)
val jungseongOEWI = sortedSetOf(12, 15, 17, 40, 52, 55, 89, 90, 91)
val jungseongEU = sortedSetOf(19, 62, 66)
val jungseongYI = sortedSetOf(20, 60, 65)
val jungseongUU = sortedSetOf(14, 15, 16, 17, 18, 27, 30, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 59, 67, 68, 73, 77, 78, 79, 80, 81, 82, 83, 84, 91)
val jungseongWide = (jungseongOU.toList() + jungseongEU.toList()).toSortedSet()
val choseongGiyeoks = sortedSetOf(0, 1, 15, 23, 30, 34, 45, 51, 56, 65, 82, 90, 100, 101, 110, 111, 115)
val hangulPeaksWithExtraWidth = sortedSetOf(2, 4, 6, 8, 11, 16, 32, 33, 37, 42, 44, 48, 50, 71, 75, 78, 79, 83, 86, 87, 88, 94)
val giyeokRemapping = hashMapOf(
5 to 19, 6 to 20, 7 to 21, 8 to 22, 11 to 23, 12 to 24,
)
fun isHangulChoseong(c: CodePoint) = c in 0x1100..0x115F || c in 0xA960..0xA97F
fun isHangulJungseong(c: CodePoint) = c in 0x1160..0x11A7 || c in 0xD7B0..0xD7C6
fun isHangulJongseong(c: CodePoint) = c in 0x11A8..0x11FF || c in 0xD7CB..0xD7FB
fun isHangulCompat(c: CodePoint) = c in codeRangeHangulCompat
fun toHangulChoseongIndex(c: CodePoint): Int =
if (c in 0x1100..0x115F) c - 0x1100
else if (c in 0xA960..0xA97F) c - 0xA960 + 96
else throw IllegalArgumentException("Not a choseong: U+${c.toString(16)}")
fun toHangulJungseongIndex(c: CodePoint): Int? =
if (c in 0x1160..0x11A7) c - 0x1160
else if (c in 0xD7B0..0xD7C6) c - 0xD7B0 + 72
else null
fun toHangulJongseongIndex(c: CodePoint): Int? =
if (c in 0x11A8..0x11FF) c - 0x11A8 + 1
else if (c in 0xD7CB..0xD7FB) c - 0xD7CB + 88 + 1
else null
fun getHanInitialRow(i: Int, p: Int, f: Int): Int {
var ret = when {
p in jungseongI -> 3
p in jungseongOEWI -> 11
p in jungseongOUComplex -> 7
p in jungseongOU -> 5
p in jungseongEU -> 9
p in jungseongYI -> 13
else -> 1
}
if (f != 0) ret += 1
return if (p in jungseongUU && i in choseongGiyeoks) {
giyeokRemapping[ret] ?: throw NullPointerException("i=$i p=$p f=$f ret=$ret")
} else ret
}
fun getHanMedialRow(i: Int, p: Int, f: Int): Int = if (f == 0) 15 else 16
fun getHanFinalRow(i: Int, p: Int, f: Int): Int =
if (p !in jungseongRightie) 17 else 18
// Kerning constants
val kemingBitMask: IntArray = intArrayOf(7, 6, 5, 4, 3, 2, 1, 0, 15, 14).map { 1 shl it }.toIntArray()
// Special characters for r+dot kerning
val lowercaseRs = sortedSetOf(0x72, 0x155, 0x157, 0x159, 0x211, 0x213, 0x27c, 0x1e59, 0x1e58, 0x1e5f)
val dots = sortedSetOf(0x2c, 0x2e)
// Devanagari internal encoding
fun Int.toDevaInternal(): Int {
if (this in 0x0915..0x0939) return this - 0x0915 + 0xF0140
else if (this in 0x0958..0x095F) return devanagariUnicodeNuqtaTable[this - 0x0958]
else throw IllegalArgumentException("No internal form for U+${this.toString(16)}")
}
val devanagariUnicodeNuqtaTable = intArrayOf(0xF0170, 0xF0171, 0xF0172, 0xF0177, 0xF017C, 0xF017D, 0xF0186, 0xF018A)
val devanagariConsonants = ((0x0915..0x0939).toList() + (0x0958..0x095F).toList() + (0x0978..0x097F).toList() +
(0xF0140..0xF04FF).toList() + (0xF0106..0xF0109).toList()).toHashSet()
}

View File

@@ -0,0 +1,80 @@
package net.torvald.otfbuild
import java.io.File
import java.io.InputStream
/**
* Simple TGA reader for uncompressed true-colour images (Type 2).
* Returns RGBA8888 pixel data.
*/
class TgaImage(val width: Int, val height: Int, val pixels: IntArray) {
/** Get pixel at (x, y) as RGBA8888. */
fun getPixel(x: Int, y: Int): Int {
if (x < 0 || x >= width || y < 0 || y >= height) return 0
return pixels[y * width + x]
}
}
object TgaReader {
fun read(file: File): TgaImage = read(file.inputStream())
fun read(input: InputStream): TgaImage {
val data = input.use { it.readBytes() }
var pos = 0
fun u8() = data[pos++].toInt() and 0xFF
fun u16() = u8() or (u8() shl 8)
val idLength = u8()
val colourMapType = u8()
val imageType = u8()
// colour map spec (5 bytes)
u16(); u16(); u8()
// image spec
val xOrigin = u16()
val yOrigin = u16()
val width = u16()
val height = u16()
val bitsPerPixel = u8()
val descriptor = u8()
val topToBottom = (descriptor and 0x20) != 0
val bytesPerPixel = bitsPerPixel / 8
// skip ID
pos += idLength
// skip colour map
if (colourMapType != 0) {
throw UnsupportedOperationException("Colour-mapped TGA not supported")
}
if (imageType != 2) {
throw UnsupportedOperationException("Only uncompressed true-colour TGA is supported (type 2), got type $imageType")
}
if (bytesPerPixel !in 3..4) {
throw UnsupportedOperationException("Only 24-bit or 32-bit TGA supported, got ${bitsPerPixel}-bit")
}
val pixels = IntArray(width * height)
for (row in 0 until height) {
val y = if (topToBottom) row else (height - 1 - row)
for (x in 0 until width) {
val b = data[pos++].toInt() and 0xFF
val g = data[pos++].toInt() and 0xFF
val r = data[pos++].toInt() and 0xFF
val a = if (bytesPerPixel == 4) data[pos++].toInt() and 0xFF else 0xFF
// Store as RGBA8888
pixels[y * width + x] = (r shl 24) or (g shl 16) or (b shl 8) or a
}
}
return TgaImage(width, height, pixels)
}
}

View File

@@ -3022,7 +3022,7 @@ class TerrarumSansBitmap(
// The "Keming" Machine // // The Keming Machine //
private val kemingBitMask: IntArray = intArrayOf(7,6,5,4,3,2,1,0,15,14).map { 1 shl it }.toIntArray() private val kemingBitMask: IntArray = intArrayOf(7,6,5,4,3,2,1,0,15,14).map { 1 shl it }.toIntArray()