Skip to content

Commit fd15a41

Browse files
committed
refactor: update font decoding to use woff2 decoder with JNI bindings instead of woff converter
1 parent 656a3e2 commit fd15a41

File tree

7 files changed

+352
-182
lines changed

7 files changed

+352
-182
lines changed
Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
name: Build Native Libraries
2+
3+
# Triggered manually from the Actions tab, or via workflow_dispatch API
4+
on:
5+
workflow_dispatch:
6+
7+
jobs:
8+
build-linux:
9+
runs-on: ubuntu-latest
10+
steps:
11+
- uses: actions/checkout@v6
12+
13+
- name: Setup Java
14+
uses: actions/setup-java@v5
15+
with:
16+
distribution: temurin
17+
java-version: 21
18+
19+
- name: Clone google/woff2
20+
run: git clone --recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2
21+
22+
- name: Build brotli (static)
23+
run: |
24+
cmake -S /tmp/woff2/brotli -B /tmp/brotli-build \
25+
-DCMAKE_BUILD_TYPE=Release \
26+
-DBUILD_SHARED_LIBS=OFF \
27+
-DCMAKE_INSTALL_PREFIX=/tmp/install \
28+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
29+
cmake --build /tmp/brotli-build --config Release -j$(nproc)
30+
cmake --install /tmp/brotli-build
31+
32+
- name: Build woff2 (static)
33+
run: |
34+
cmake -S /tmp/woff2 -B /tmp/woff2-build \
35+
-DCMAKE_BUILD_TYPE=Release \
36+
-DBUILD_SHARED_LIBS=OFF \
37+
-DCMAKE_PREFIX_PATH=/tmp/install \
38+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
39+
cmake --build /tmp/woff2-build --config Release -j$(nproc)
40+
41+
- name: Build JNI shared library
42+
run: |
43+
g++ -shared -std=c++11 -O2 -fPIC \
44+
-I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" \
45+
-I/tmp/woff2/include -I/tmp/install/include \
46+
tools/idea-plugin/native/woff2decoder.cpp \
47+
/tmp/woff2-build/libwoff2dec.a \
48+
/tmp/woff2-build/libwoff2common.a \
49+
/tmp/install/lib/libbrotlidec-static.a \
50+
/tmp/install/lib/libbrotlicommon-static.a \
51+
-o libwoff2decoder.so
52+
53+
- name: Verify library
54+
run: |
55+
file libwoff2decoder.so
56+
ls -lh libwoff2decoder.so
57+
58+
- name: Upload artifact
59+
uses: actions/upload-artifact@v4
60+
with:
61+
name: native-linux-x64
62+
path: libwoff2decoder.so
63+
64+
build-windows:
65+
runs-on: windows-latest
66+
steps:
67+
- uses: actions/checkout@v6
68+
69+
- name: Setup Java
70+
uses: actions/setup-java@v5
71+
with:
72+
distribution: temurin
73+
java-version: 21
74+
75+
- name: Setup MSVC
76+
uses: ilammy/msvc-dev-cmd@v1
77+
78+
- name: Clone google/woff2
79+
run: git clone --recursive --depth 1 https://github.com/google/woff2.git C:\woff2
80+
81+
- name: Build brotli (static)
82+
run: |
83+
cmake -S C:\woff2\brotli -B C:\brotli-build `
84+
-DCMAKE_BUILD_TYPE=Release `
85+
-DBUILD_SHARED_LIBS=OFF `
86+
-DCMAKE_INSTALL_PREFIX=C:\install `
87+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
88+
cmake --build C:\brotli-build --config Release -j $env:NUMBER_OF_PROCESSORS
89+
cmake --install C:\brotli-build --config Release
90+
91+
- name: Build woff2 (static)
92+
run: |
93+
cmake -S C:\woff2 -B C:\woff2-build `
94+
-DCMAKE_BUILD_TYPE=Release `
95+
-DBUILD_SHARED_LIBS=OFF `
96+
-DCMAKE_PREFIX_PATH=C:\install `
97+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
98+
cmake --build C:\woff2-build --config Release -j $env:NUMBER_OF_PROCESSORS
99+
100+
- name: Build JNI DLL
101+
run: |
102+
$jni_include = "$env:JAVA_HOME\include"
103+
$jni_include_win = "$env:JAVA_HOME\include\win32"
104+
105+
# Find the static libs (MSVC puts them in Release/ subdirectory)
106+
$woff2dec = Get-ChildItem -Path C:\woff2-build -Recurse -Filter "woff2dec.lib" | Select-Object -First 1
107+
$woff2common = Get-ChildItem -Path C:\woff2-build -Recurse -Filter "woff2common.lib" | Select-Object -First 1
108+
$brotlidec = Get-ChildItem -Path C:\install -Recurse -Filter "brotlidec*.lib" | Where-Object { $_.Name -match 'static' -or $_.Name -eq 'brotlidec.lib' } | Select-Object -First 1
109+
$brotlicommon = Get-ChildItem -Path C:\install -Recurse -Filter "brotlicommon*.lib" | Where-Object { $_.Name -match 'static' -or $_.Name -eq 'brotlicommon.lib' } | Select-Object -First 1
110+
111+
Write-Host "woff2dec: $($woff2dec.FullName)"
112+
Write-Host "woff2common: $($woff2common.FullName)"
113+
Write-Host "brotlidec: $($brotlidec.FullName)"
114+
Write-Host "brotlicommon: $($brotlicommon.FullName)"
115+
116+
cl /EHsc /O2 /std:c++14 /LD `
117+
/I"$jni_include" /I"$jni_include_win" `
118+
/I"C:\woff2\include" /I"C:\install\include" `
119+
tools\idea-plugin\native\woff2decoder.cpp `
120+
$($woff2dec.FullName) `
121+
$($woff2common.FullName) `
122+
$($brotlidec.FullName) `
123+
$($brotlicommon.FullName) `
124+
/Fe:woff2decoder.dll /link /DLL
125+
126+
- name: Verify DLL
127+
run: |
128+
Get-Item woff2decoder.dll | Select-Object Name, Length
129+
130+
- name: Upload artifact
131+
uses: actions/upload-artifact@v4
132+
with:
133+
name: native-windows-x64
134+
path: woff2decoder.dll
135+
136+
build-macos:
137+
runs-on: macos-latest
138+
steps:
139+
- uses: actions/checkout@v6
140+
141+
- name: Setup Java
142+
uses: actions/setup-java@v5
143+
with:
144+
distribution: temurin
145+
java-version: 21
146+
147+
- name: Clone google/woff2
148+
run: git clone --recursive --depth 1 https://github.com/google/woff2.git /tmp/woff2
149+
150+
- name: Build arm64
151+
run: |
152+
# Brotli
153+
cmake -S /tmp/woff2/brotli -B /tmp/brotli-arm64 \
154+
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
155+
-DCMAKE_INSTALL_PREFIX=/tmp/install-arm64 \
156+
-DCMAKE_OSX_ARCHITECTURES=arm64 \
157+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
158+
cmake --build /tmp/brotli-arm64 --config Release -j$(sysctl -n hw.ncpu)
159+
cmake --install /tmp/brotli-arm64
160+
161+
# Woff2
162+
cmake -S /tmp/woff2 -B /tmp/woff2-arm64 \
163+
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
164+
-DCMAKE_PREFIX_PATH=/tmp/install-arm64 \
165+
-DCMAKE_OSX_ARCHITECTURES=arm64 \
166+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
167+
cmake --build /tmp/woff2-arm64 --config Release -j$(sysctl -n hw.ncpu)
168+
169+
# JNI bridge
170+
c++ -shared -std=c++11 -O2 -arch arm64 \
171+
-I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" \
172+
-I/tmp/woff2/include -I/tmp/install-arm64/include \
173+
tools/idea-plugin/native/woff2decoder.cpp \
174+
/tmp/woff2-arm64/libwoff2dec.a \
175+
/tmp/woff2-arm64/libwoff2common.a \
176+
/tmp/install-arm64/lib/libbrotlidec-static.a \
177+
/tmp/install-arm64/lib/libbrotlicommon-static.a \
178+
-o /tmp/libwoff2decoder-arm64.dylib
179+
180+
- name: Build x86_64
181+
run: |
182+
# Brotli
183+
cmake -S /tmp/woff2/brotli -B /tmp/brotli-x64 \
184+
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
185+
-DCMAKE_INSTALL_PREFIX=/tmp/install-x64 \
186+
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
187+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
188+
cmake --build /tmp/brotli-x64 --config Release -j$(sysctl -n hw.ncpu)
189+
cmake --install /tmp/brotli-x64
190+
191+
# Woff2
192+
cmake -S /tmp/woff2 -B /tmp/woff2-x64 \
193+
-DCMAKE_BUILD_TYPE=Release -DBUILD_SHARED_LIBS=OFF \
194+
-DCMAKE_PREFIX_PATH=/tmp/install-x64 \
195+
-DCMAKE_OSX_ARCHITECTURES=x86_64 \
196+
-DCMAKE_POLICY_VERSION_MINIMUM=3.5
197+
cmake --build /tmp/woff2-x64 --config Release -j$(sysctl -n hw.ncpu)
198+
199+
# JNI bridge
200+
c++ -shared -std=c++11 -O2 -arch x86_64 \
201+
-I"$JAVA_HOME/include" -I"$JAVA_HOME/include/darwin" \
202+
-I/tmp/woff2/include -I/tmp/install-x64/include \
203+
tools/idea-plugin/native/woff2decoder.cpp \
204+
/tmp/woff2-x64/libwoff2dec.a \
205+
/tmp/woff2-x64/libwoff2common.a \
206+
/tmp/install-x64/lib/libbrotlidec-static.a \
207+
/tmp/install-x64/lib/libbrotlicommon-static.a \
208+
-o /tmp/libwoff2decoder-x64.dylib
209+
210+
- name: Create universal binary
211+
run: |
212+
lipo -create \
213+
/tmp/libwoff2decoder-arm64.dylib \
214+
/tmp/libwoff2decoder-x64.dylib \
215+
-output libwoff2decoder.dylib
216+
file libwoff2decoder.dylib
217+
ls -lh libwoff2decoder.dylib
218+
219+
- name: Upload artifact
220+
uses: actions/upload-artifact@v4
221+
with:
222+
name: native-macos
223+
path: libwoff2decoder.dylib
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#include <jni.h>
2+
#include <string>
3+
#include <cstdio>
4+
#include <woff2/decode.h>
5+
6+
extern "C"
7+
JNIEXPORT jbyteArray JNICALL
8+
Java_io_github_composegears_valkyrie_util_font_Woff2Decoder_decodeBytes(
9+
JNIEnv *env, jobject thiz, jbyteArray input_bytes) {
10+
11+
auto *input_data = reinterpret_cast<uint8_t *>(env->GetByteArrayElements(input_bytes, nullptr));
12+
jsize input_len = env->GetArrayLength(input_bytes);
13+
14+
std::string output(
15+
std::min(woff2::ComputeWOFF2FinalSize(input_data, input_len),
16+
woff2::kDefaultMaxSize),
17+
0);
18+
19+
woff2::WOFF2StringOut out(&output);
20+
21+
if (!woff2::ConvertWOFF2ToTTF(input_data, input_len, &out)) {
22+
fprintf(stderr, "woff2decoder: decompression error\n");
23+
env->ReleaseByteArrayElements(input_bytes, reinterpret_cast<jbyte *>(input_data), 0);
24+
return nullptr;
25+
}
26+
27+
output.resize(out.Size());
28+
29+
jbyteArray arr = env->NewByteArray(static_cast<jsize>(out.Size()));
30+
env->SetByteArrayRegion(arr, 0, static_cast<jsize>(out.Size()),
31+
reinterpret_cast<const jbyte *>(output.data()));
32+
33+
env->ReleaseByteArrayElements(input_bytes, reinterpret_cast<jbyte *>(input_data), 0);
34+
35+
return arr;
36+
}

tools/idea-plugin/src/main/kotlin/io/github/composegears/valkyrie/ui/screen/webimport/standard/bootstrap/data/BootstrapRepository.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package io.github.composegears.valkyrie.ui.screen.webimport.standard.bootstrap.data
22

3-
import io.github.composegears.valkyrie.util.font.WoffToTtfConverter
3+
import io.github.composegears.valkyrie.util.font.Woff2Decoder
44
import io.ktor.client.HttpClient
55
import io.ktor.client.request.get
66
import io.ktor.client.statement.bodyAsChannel
@@ -20,7 +20,7 @@ class BootstrapRepository(
2020
) {
2121
companion object {
2222
private const val UNPKG_BASE = "https://unpkg.com/bootstrap-icons@latest"
23-
private const val FONT_URL = "$UNPKG_BASE/font/fonts/bootstrap-icons.woff"
23+
private const val FONT_URL = "$UNPKG_BASE/font/fonts/bootstrap-icons.woff2"
2424
private const val JSON_URL = "$UNPKG_BASE/font/bootstrap-icons.json"
2525
private const val ICONS_BASE_URL = "$UNPKG_BASE/icons"
2626
}
@@ -35,8 +35,9 @@ class BootstrapRepository(
3535
suspend fun loadFontBytes(): ByteArray = withContext(Dispatchers.IO) {
3636
fontMutex.withLock {
3737
fontBytesCache ?: run {
38-
val woffBytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
39-
val ttfBytes = WoffToTtfConverter.convert(woffBytes)
38+
val woff2Bytes = httpClient.get(FONT_URL).bodyAsChannel().toByteArray()
39+
val ttfBytes = Woff2Decoder.decodeBytes(woff2Bytes)
40+
?: error("Failed to decode WOFF2 font")
4041
fontBytesCache = ttfBytes
4142
ttfBytes
4243
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.github.composegears.valkyrie.util.font
2+
3+
import java.io.File
4+
import java.io.FileNotFoundException
5+
6+
/**
7+
* Loads platform-specific native libraries from JAR resources.
8+
*
9+
* Native binaries are stored under `/native/{platform}/` in the JAR:
10+
* - `native/macos/libwoff2decoder.dylib` (universal: arm64 + x86_64)
11+
* - `native/linux-x64/libwoff2decoder.so`
12+
* - `native/windows-x64/woff2decoder.dll`
13+
*/
14+
internal object NativeLibraryLoader {
15+
16+
private val loaded = mutableSetOf<String>()
17+
18+
@Synchronized
19+
fun load(libName: String) {
20+
if (libName in loaded) return
21+
22+
val platform = detectPlatform()
23+
val fileName = System.mapLibraryName(libName)
24+
val resourcePath = "/native/$platform/$fileName"
25+
26+
val resourceStream = NativeLibraryLoader::class.java.getResourceAsStream(resourcePath)
27+
?: throw FileNotFoundException(
28+
"Native library not found for platform '$platform': $resourcePath",
29+
)
30+
31+
val tempDir = File(System.getProperty("java.io.tmpdir"), "valkyrie-native")
32+
tempDir.mkdirs()
33+
34+
val tempFile = File(tempDir, fileName)
35+
resourceStream.use { input ->
36+
tempFile.outputStream().use { output ->
37+
input.copyTo(output)
38+
}
39+
}
40+
tempFile.deleteOnExit()
41+
42+
System.load(tempFile.absolutePath)
43+
loaded.add(libName)
44+
}
45+
46+
private fun detectPlatform(): String {
47+
val os = System.getProperty("os.name").lowercase()
48+
val arch = System.getProperty("os.arch").lowercase()
49+
50+
return when {
51+
os.contains("mac") || os.contains("darwin") -> "macos"
52+
os.contains("linux") -> when {
53+
arch == "amd64" || arch == "x86_64" -> "linux-x64"
54+
else -> error("Unsupported Linux architecture: $arch")
55+
}
56+
os.contains("windows") -> when {
57+
arch == "amd64" || arch == "x86_64" -> "windows-x64"
58+
else -> error("Unsupported Windows architecture: $arch")
59+
}
60+
else -> error("Unsupported operating system: $os")
61+
}
62+
}
63+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package io.github.composegears.valkyrie.util.font
2+
3+
/**
4+
* Decodes WOFF2 font files to TTF/OTF format using the Google woff2 native library via JNI.
5+
*
6+
* The underlying C++ implementation statically links:
7+
* - [google/woff2](https://github.com/google/woff2) (MIT license)
8+
* - [google/brotli](https://github.com/google/brotli) (MIT license)
9+
*
10+
* Supported platforms: macOS (arm64/x86_64), Linux (x86_64), Windows (x86_64).
11+
*/
12+
object Woff2Decoder {
13+
14+
init {
15+
NativeLibraryLoader.load("woff2decoder")
16+
}
17+
18+
/**
19+
* Decodes WOFF2 font bytes to TTF/OTF format.
20+
*
21+
* @param inBytes WOFF2 font file contents as a byte array
22+
* @return TTF/OTF font bytes, or `null` if decompression fails
23+
*/
24+
external fun decodeBytes(inBytes: ByteArray): ByteArray?
25+
}

0 commit comments

Comments
 (0)