records, int id, String value) {
+ if (value != null && !value.isBlank()) {
+ records.add(new NameRecord(id, value));
+ }
+ }
+
+ private byte[] os2() {
+ FontMetadata metadata = options.getMetadata();
+ int firstChar = glyphs.stream().skip(1).mapToInt(Glyph::getCodePoint).filter(code -> code <= 0xFFFF).min().orElse(0);
+ int lastChar = glyphs.stream().skip(1).mapToInt(Glyph::getCodePoint).filter(code -> code <= 0xFFFF).max().orElse(0xFFFF);
+ long[] unicodeRanges = unicodeRanges();
+ FontBuffer out = new FontBuffer();
+ out.u16(4);
+ out.i16(500);
+ out.u16(metadata.getWeightClass());
+ out.u16(metadata.getWidthClass());
+ out.u16(metadata.getEmbeddingRestrictions().getFsType());
+ for (int i = 0; i < 10; i++) {
+ out.i16(0);
+ }
+ out.i16(0);
+ for (int i = 0; i < 10; i++) {
+ out.u8(0);
+ }
+ for (long range : unicodeRanges) {
+ out.u32(range);
+ }
+ out.tag(metadata.getVendorId());
+ out.u16(0);
+ out.u16(firstChar);
+ out.u16(lastChar);
+ out.i16(options.getAscent());
+ out.i16(options.getDescent());
+ out.i16(0);
+ out.u16(options.getAscent());
+ out.u16(-options.getDescent());
+ out.u32(0);
+ out.u32(0);
+ out.i16((int) Math.round(options.getUnitsPerEm() * 0.5));
+ out.i16((int) Math.round(options.getUnitsPerEm() * 0.7));
+ out.u16(0);
+ out.u16(32);
+ out.u16(0);
+ return out.bytes();
+ }
+
+ private long[] unicodeRanges() {
+ long[] ranges = new long[4];
+ for (Glyph glyph : glyphs.stream().skip(1).toList()) {
+ int bit = unicodeRangeBit(glyph.getCodePoint());
+ if (0 <= bit) {
+ ranges[bit / 32] |= 1L << (bit % 32);
+ }
+ }
+ return ranges;
+ }
+
+ private int unicodeRangeBit(int codePoint) {
+ if (codePoint <= 0x007F) {
+ return 0;
+ }
+ if (codePoint <= 0x00FF) {
+ return 1;
+ }
+ if (0x0100 <= codePoint && codePoint <= 0x017F) {
+ return 2;
+ }
+ if (0x0370 <= codePoint && codePoint <= 0x03FF) {
+ return 7;
+ }
+ if (0x0400 <= codePoint && codePoint <= 0x052F) {
+ return 9;
+ }
+ if (0x0590 <= codePoint && codePoint <= 0x05FF) {
+ return 11;
+ }
+ if (0x0600 <= codePoint && codePoint <= 0x06FF) {
+ return 13;
+ }
+ if (0x0900 <= codePoint && codePoint <= 0x097F) {
+ return 15;
+ }
+ if (0x4E00 <= codePoint && codePoint <= 0x9FFF) {
+ return 59;
+ }
+ if (0xE000 <= codePoint && codePoint <= 0xF8FF) {
+ return 60;
+ }
+ return -1;
+ }
+
+ private byte[] post() {
+ FontBuffer out = new FontBuffer();
+ out.u32(0x00030000);
+ out.u32(0);
+ out.i16(0);
+ out.i16(0);
+ out.u32(0);
+ out.u32(0);
+ out.u32(0);
+ out.u32(0);
+ out.u32(0);
+ return out.bytes();
+ }
+
+ private record GlyphMetrics(int leftSideBearing, int rightSideBearing, int xMaxExtent) {
+ }
+}
diff --git a/src/main/java/com/imyeyu/font/icon/util/UnicodeParser.java b/src/main/java/com/imyeyu/font/icon/util/UnicodeParser.java
new file mode 100644
index 0000000..bae8555
--- /dev/null
+++ b/src/main/java/com/imyeyu/font/icon/util/UnicodeParser.java
@@ -0,0 +1,44 @@
+package com.imyeyu.font.icon.util;
+
+/// Unicode 码点解析器
+///
+/// @author Codex
+/// @since 2026-07-13 16:26
+public final class UnicodeParser {
+
+ private UnicodeParser() {
+ }
+
+ public static int parseRequired(String unicode) {
+ int codePoint = parse(unicode);
+ if (codePoint <= 0 || !Character.isValidCodePoint(codePoint)) {
+ throw new IllegalArgumentException("invalid Unicode code point: " + unicode);
+ }
+ return codePoint;
+ }
+
+ public static int parse(String unicode) {
+ if (unicode == null) {
+ return -1;
+ }
+ String value = unicode.trim()
+ .replace("", "")
+ .replace("", "")
+ .replace("", "")
+ .replace("\\u", "")
+ .replace("\\U", "")
+ .replace("U+", "")
+ .replace("u+", "")
+ .replace("0x", "")
+ .replace("0X", "")
+ .replace(";", "");
+ try {
+ if (value.length() == 1) {
+ return value.codePointAt(0);
+ }
+ return Integer.parseInt(value, 16);
+ } catch (NumberFormatException e) {
+ return -1;
+ }
+ }
+}
diff --git a/src/test/java/com/imyeyu/font/icon/BuildTTF.java b/src/test/java/com/imyeyu/font/icon/BuildTTF.java
new file mode 100644
index 0000000..cb5c3b3
--- /dev/null
+++ b/src/test/java/com/imyeyu/font/icon/BuildTTF.java
@@ -0,0 +1,13 @@
+import com.imyeyu.io.IO;
+import com.imyeyu.network.FileRequest;
+
+private static final String API_TTF_FILE = "http://localhost:8091/icon/export/ttf";
+
+private static final Path TEST_FOLDER = Path.of("target/ttf-test");
+private static final Path HTML_TO = TEST_FOLDER.resolve("index.html");
+private static final Path TTF_PATH = TEST_FOLDER.resolve("timi-icon.ttf");
+
+void main() throws Exception {
+ IO.resourceToDisk(getClass(), "index.html", HTML_TO.toFile().getAbsolutePath());
+ FileRequest.get(API_TTF_FILE).toFile(TTF_PATH.toFile());
+}
diff --git a/src/test/resources/index.html b/src/test/resources/index.html
new file mode 100644
index 0000000..aff4577
--- /dev/null
+++ b/src/test/resources/index.html
@@ -0,0 +1,330 @@
+
+
+
+
+
+ TTF 字体预览
+
+
+
+ TTF 字体预览
+ 读取同目录的 timi-icon.ttf,解析字体映射并展示可用字形
+ 正在加载...
+
+
+
+
+