From 843eaef0cd4d9034b0433af330469ff4708f1d74 Mon Sep 17 00:00:00 2001 From: Timi Date: Mon, 14 Jul 2025 14:20:00 +0800 Subject: [PATCH] Initial project --- .gitignore | 128 ++---- .idea/.gitignore | 3 + .idea/encodings.xml | 7 + .idea/misc.xml | 14 + .idea/uiDesigner.xml | 124 ++++++ .idea/vcs.xml | 6 + pom.xml | 43 ++ .../java/com/imyeyu/fx/icon/TimiFXIcon.java | 384 ++++++++++++++++++ .../java/com/imyeyu/fx/icon/package-info.java | 2 + .../imyeyu/fx/icon/util/ImageConvertor.java | 96 +++++ .../com/imyeyu/fx/icon/util/package-info.java | 2 + src/main/resources/font/timi-icon.mapping | Bin 0 -> 2710 bytes src/main/resources/font/timi-icon.ttf | Bin 0 -> 16712 bytes src/test/java/test/SerializeMapping.java | 80 ++++ 14 files changed, 795 insertions(+), 94 deletions(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/imyeyu/fx/icon/TimiFXIcon.java create mode 100644 src/main/java/com/imyeyu/fx/icon/package-info.java create mode 100644 src/main/java/com/imyeyu/fx/icon/util/ImageConvertor.java create mode 100644 src/main/java/com/imyeyu/fx/icon/util/package-info.java create mode 100644 src/main/resources/font/timi-icon.mapping create mode 100644 src/main/resources/font/timi-icon.ttf create mode 100644 src/test/java/test/SerializeMapping.java diff --git a/.gitignore b/.gitignore index c6d98d1..5ff6309 100644 --- a/.gitignore +++ b/.gitignore @@ -1,98 +1,38 @@ -# ---> JetBrains -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/**/workspace.xml -.idea/**/tasks.xml -.idea/**/usage.statistics.xml -.idea/**/dictionaries -.idea/**/shelf - -# AWS User-specific -.idea/**/aws.xml - -# Generated files -.idea/**/contentModel.xml - -# Sensitive or high-churn files -.idea/**/dataSources/ -.idea/**/dataSources.ids -.idea/**/dataSources.local.xml -.idea/**/sqlDataSources.xml -.idea/**/dynamic.xml -.idea/**/uiDesigner.xml -.idea/**/dbnavigator.xml - -# Gradle -.idea/**/gradle.xml -.idea/**/libraries - -# Gradle and Maven with auto-import -# When using Gradle or Maven with auto-import, you should exclude module files, -# since they will be recreated, and may cause churn. Uncomment if using -# auto-import. -# .idea/artifacts -# .idea/compiler.xml -# .idea/jarRepositories.xml -# .idea/modules.xml -# .idea/*.iml -# .idea/modules -# *.iml -# *.ipr - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# SonarLint plugin -.idea/sonarlint/ - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests - -# Android studio 3.1+ serialized cache file -.idea/caches/build_file_checksums.ser - -# ---> Maven target/ -pom.xml.tag -pom.xml.releaseBackup -pom.xml.versionsBackup -pom.xml.next -release.properties -dependency-reduced-pom.xml -buildNumber.properties -.mvn/timing.properties -# https://github.com/takari/maven-wrapper#usage-without-binary-jar -.mvn/wrapper/maven-wrapper.jar +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ -# Eclipse m2e generated files -# Eclipse Core -.project -# JDT-specific (Eclipse Java Development Tools) +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated .classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..aa00ffa --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..fdc35ea --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml new file mode 100644 index 0000000..2b63946 --- /dev/null +++ b/.idea/uiDesigner.xml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..2b73913 --- /dev/null +++ b/pom.xml @@ -0,0 +1,43 @@ + + + 4.0.0 + + com.imyeyu.fx.icon + timi-fx-icon + 0.0.1 + + + 21.0.2 + 21 + 21 + UTF-8 + + + + + org.openjfx + javafx-controls + ${fx.version} + + + com.imyeyu.io + timi-io + 0.0.1 + test + + + org.dom4j + dom4j + 2.1.4 + test + + + com.google.code.gson + gson + 2.10 + test + + + diff --git a/src/main/java/com/imyeyu/fx/icon/TimiFXIcon.java b/src/main/java/com/imyeyu/fx/icon/TimiFXIcon.java new file mode 100644 index 0000000..2477365 --- /dev/null +++ b/src/main/java/com/imyeyu/fx/icon/TimiFXIcon.java @@ -0,0 +1,384 @@ +package com.imyeyu.fx.icon; + +import javafx.scene.SnapshotParameters; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.image.Image; +import javafx.scene.paint.Color; +import javafx.scene.paint.Paint; +import javafx.scene.text.Font; +import javafx.scene.text.Text; +import com.imyeyu.fx.icon.util.ImageConvertor; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.ObjectInputStream; +import java.util.HashMap; +import java.util.Map; + +/** + * JavaFX 像素风字体图标库,这些图标基于 16x 像素绘制,即图标在 16 32 64.. 等字号时渲染最清晰准确 + * + *

图标名称可在 https://icon.imyeyu.net/ 查询

+ * + *
+ *     // 静态类,不需要实例化,直接调用静态方法
+ *     TimiFXIcon.fromName("FILE");
+ * 
+ * + * @author 夜雨 + * @since 2022-07-28 09:57 + */ +public class TimiFXIcon { + + // ---------- 公开属性 ---------- + + /** 默认颜色 */ + public static final Paint COLOR = Paint.valueOf("#333"); + + /** 字体文件资源路径 */ + public static final String FONT_PATH = "font/timi-icon.ttf"; + + // ---------- 私有属性 ---------- + + /** 图标名称映射 Unicode 序列化文件路径 */ + private static final String NAME_MAPPING_PATH = "font/timi-icon.mapping"; + + /** 组件样式类 */ + private static final String STYLE_CLASS = "timi-icon"; + + /** 字体文件字节 */ + private static byte[] bytes; + + /** 用于转换渲染的画板 */ + private static final Canvas CANVAS = new Canvas(); + private static final GraphicsContext G2D = CANVAS.getGraphicsContext2D(); + + /** 图标字体缓存 Map<字号, 字体> */ + private static final Map FONT_MAPPING = new HashMap<>(); + + /** 映射缓存 Map<图标名, Unicode> */ + private static Map nameMapping; + + /** 这个类不可实例化 */ + private TimiFXIcon() { + } + + /** 读取图标与 Unicode 的映射,由项目测试环境生成 */ + @SuppressWarnings("unchecked") + private static synchronized void loadMapping() { + if (nameMapping == null) { + try { + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(NAME_MAPPING_PATH); + if (is == null) { + throw new NullPointerException("not found /font/timi-icon.ttf file"); + } + ObjectInputStream ois = new ObjectInputStream(is); + nameMapping = (Map) ois.readObject(); + ois.close(); + is.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + } + + /** + * 获取名称字符映射表 + * + * @return 名称字符映射表 + */ + public static Map getNameMapping() { + loadMapping(); + return new HashMap<>(nameMapping); // 阻止修改 + } + + /** + * 获取字体文件数据流 + * + * @return 字体文件数据流 + */ + public static InputStream getInputStream() { + if (bytes == null) { + try { + InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(FONT_PATH); + if (is == null) { + throw new NullPointerException("not found /font/timi-icon.ttf file"); + } + bytes = is.readAllBytes(); + is.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return new ByteArrayInputStream(bytes); + } + + /** + * 获取字体 + * + * @param size 字号 + * @return 字体 + */ + public static synchronized Font getFont(int size) { + if (FONT_MAPPING.containsKey(size)) { + return FONT_MAPPING.get(size); + } + Font font = Font.loadFont(getInputStream(), size); + FONT_MAPPING.put(size, font); + return font; + } + + // ---------- 根据名称获取图标字符 ---------- + + /** + * 根据名称获取图标字符 + * + * @param name 名称 + * @return 字符 + */ + public static char toChar(String name) { + if (nameMapping == null) { + loadMapping(); + } + Character c = nameMapping.get(name.toLowerCase()); + if (c == null) { + throw new NullPointerException("not found char from the name: " + name); + } + return c; + } + + // ---------- 根据名称获取图标文本 ---------- + + /** + * 根据名称获取图标 + * + * @param name 名称 + * @return 图标文本 + */ + public static Text fromName(String name) { + return fromName(name, 16, COLOR); + } + + /** + * 根据名称获取图标 + * + * @param name 名称 + * @param size 字号 + * @return 图标文本 + */ + public static Text fromName(String name, int size) { + return fromName(name, size, COLOR); + } + + /** + * 根据名称获取图标 + * + * @param name 名称 + * @param fill 填充色 + * @return 图标文本 + */ + public static Text fromName(String name, Paint fill) { + return fromName(name, 16, fill); + } + + /** + * 根据名称获取图标 + * + * @param name 名称 + * @param size 字号 + * @param fill 填充色 + * @return 图标文本 + */ + public static Text fromName(String name, int size, Paint fill) { + return fromChar(toChar(name), size, fill); + } + + // ---------- 根据字符获取图标文本 ---------- + + /** + * 根据字符获取图标 + * + * @param c 字符 + * @return 图标文本 + */ + public static Text fromChar(char c) { + return fromChar(c, 16, COLOR); + } + + /** + * 根据字符获取图标 + * + * @param c 字符 + * @param size 字号 + * @return 图标文本 + */ + public static Text fromChar(char c, int size) { + return fromChar(c, size, COLOR); + } + + /** + * 根据字符获取图标 + * + * @param c 字符 + * @param fill 填充色 + * @return 图标文本 + */ + public static Text fromChar(char c, Paint fill) { + return fromChar(c, 16, fill); + } + + /** + * 根据字符获取图标 + * + * @param c 字符 + * @param size 字号 + * @param fill 填充色 + * @return 图标文本 + */ + public static Text fromChar(char c, int size, Paint fill) { + Text text = new Text(String.valueOf(c)); + text.getStyleClass().setAll(STYLE_CLASS); + text.setFont(getFont(size)); + text.setFill(fill); + return text; + } + + // ---------- 根据名称获取图像 ---------- + + /** + * 根据名称获取图像 + * + * @param name 名称 + * @return 图像 + */ + public static Image imageFromName(String name) { + return imageFromName(name, 16, COLOR); + } + + /** + * 根据名称获取图像 + * + * @param name 名称 + * @param fill 填充 + * @return 图像 + */ + public static Image imageFromName(String name, Paint fill) { + return imageFromName(name, 16, fill); + } + + /** + * 根据名称获取图像 + * + * @param name 名称 + * @param size 尺寸 + * @return 图像 + */ + public static Image imageFromName(String name, int size) { + return imageFromName(name, size, COLOR); + } + + /** + * 根据名称获取图像 + * + * @param name 名称 + * @param size 尺寸 + * @param fill 填充 + * @return 图像 + */ + public static Image imageFromName(String name, int size, Paint fill) { + return imageFromChar(toChar(name), size, fill); + } + + // ---------- 根据字符获取图像 ---------- + + /** + * 图标字符转图像 + * + * @param c 字符 + * @return 图像 + */ + public static Image imageFromChar(char c) { + return imageFromChar(c, 16, COLOR); + } + + /** + * 图标字符转图像 + * + * @param c 字符 + * @param size 尺寸 + * @return 图像 + */ + public static Image imageFromChar(char c, int size) { + return imageFromChar(c, size, COLOR); + } + + /** + * 图标字符转图像 + * + * @param c 字符 + * @param fill 填充 + * @return 图像 + */ + public static Image imageFromChar(char c, Paint fill) { + return imageFromChar(c, 16, fill); + } + + /** + * 图标字符转图像 + * + * @param c 字符 + * @param size 尺寸 + * @param fill 填充 + * @return 图像 + */ + public static Image imageFromChar(char c, int size, Paint fill) { + CANVAS.setWidth(size); + CANVAS.setHeight(size); + G2D.clearRect(0, 0, CANVAS.getWidth(), CANVAS.getHeight()); + G2D.setFont(getFont(size)); + G2D.setFill(fill); + G2D.fillText(String.valueOf(c), 0, size - size / 16D); + + SnapshotParameters sp = new SnapshotParameters(); + sp.setFill(Color.TRANSPARENT); + return CANVAS.snapshot(sp, null); + } + + // ---------- 根据名称获取重绘图像 ---------- + + /** + * 根据名称获取图标(产生重绘,可用在 Stage 图标) + * + * @param name 名称 + * @return 图标 + */ + public static Image iconFromName(String name) { + return iconFromName(name, COLOR); + } + + /** + * 根据名称获取图标(产生重绘,可用在 Stage 图标) + * + * @param name 名称 + * @param fill 填充 + * @return 图标 + */ + public static Image iconFromName(String name, Paint fill) { + try { + BufferedImage bImage = ImageConvertor.toAWTImage(imageFromName(name, fill)); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ImageIO.write(bImage, "PNG", baos); + bImage.flush(); + baos.flush(); + baos.close(); + return new Image(new ByteArrayInputStream(baos.toByteArray())); + } catch (Exception e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/com/imyeyu/fx/icon/package-info.java b/src/main/java/com/imyeyu/fx/icon/package-info.java new file mode 100644 index 0000000..c308412 --- /dev/null +++ b/src/main/java/com/imyeyu/fx/icon/package-info.java @@ -0,0 +1,2 @@ +/** 主程序 */ +package com.imyeyu.fx.icon; diff --git a/src/main/java/com/imyeyu/fx/icon/util/ImageConvertor.java b/src/main/java/com/imyeyu/fx/icon/util/ImageConvertor.java new file mode 100644 index 0000000..2706bc2 --- /dev/null +++ b/src/main/java/com/imyeyu/fx/icon/util/ImageConvertor.java @@ -0,0 +1,96 @@ +package com.imyeyu.fx.icon.util; + +import javafx.scene.image.Image; +import javafx.scene.image.PixelFormat; +import javafx.scene.image.PixelReader; +import javafx.scene.image.PixelWriter; +import javafx.scene.image.WritableImage; +import javafx.scene.image.WritablePixelFormat; + +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.awt.image.DataBufferInt; +import java.awt.image.SampleModel; +import java.awt.image.SinglePixelPackedSampleModel; +import java.nio.IntBuffer; + +/** + * JavaFX 和 AWT 图片转换 + * + * @author 夜雨 + * @since 2022-10-09 23:16 + */ +public class ImageConvertor { + + /** + * AWT 图像转 JavaFX 图像 + * + * @param bimg AWT 图像 + * @return JavaFX 图像 + */ + public static WritableImage toFXImage(BufferedImage bimg) { + int bw = bimg.getWidth(); + int bh = bimg.getHeight(); + switch (bimg.getType()) { + case BufferedImage.TYPE_INT_ARGB: + case BufferedImage.TYPE_INT_ARGB_PRE: + break; + default: + BufferedImage converted = new BufferedImage(bw, bh, BufferedImage.TYPE_INT_ARGB_PRE); + Graphics2D g2d = converted.createGraphics(); + g2d.drawImage(bimg, 0, 0, null); + g2d.dispose(); + bimg = converted; + break; + } + WritableImage img = new WritableImage(bw, bh); + PixelWriter pw = img.getPixelWriter(); + DataBufferInt db = (DataBufferInt) bimg.getRaster().getDataBuffer(); + int[] data = db.getData(); + int offset = bimg.getRaster().getDataBuffer().getOffset(); + int scan = 0; + SampleModel sm = bimg.getRaster().getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride(); + } + + PixelFormat pf = (bimg.isAlphaPremultiplied() ? PixelFormat.getIntArgbPreInstance() : PixelFormat.getIntArgbInstance()); + pw.setPixels(0, 0, bw, bh, pf, data, offset, scan); + return img; + } + + /** + * JavaFX 图像转 AWT 图像 + * + * @param img JavaFX 图像 + * @return AWT 图像 + */ + public static BufferedImage toAWTImage(Image img) { + PixelReader pr = img.getPixelReader(); + int iw = (int) img.getWidth(); + int ih = (int) img.getHeight(); + PixelFormat fxFormat = pr.getPixelFormat(); + int prefImgType = switch (fxFormat.getType()) { + case BYTE_BGRA_PRE, INT_ARGB_PRE -> BufferedImage.TYPE_INT_ARGB_PRE; + case BYTE_BGRA, INT_ARGB -> BufferedImage.TYPE_INT_ARGB; + case BYTE_RGB -> BufferedImage.TYPE_INT_RGB; + case BYTE_INDEXED -> (fxFormat.isPremultiplied() ? BufferedImage.TYPE_INT_ARGB_PRE : BufferedImage.TYPE_INT_ARGB); + }; + BufferedImage bImg = new BufferedImage(iw, ih, prefImgType); + DataBufferInt db = (DataBufferInt) bImg.getRaster().getDataBuffer(); + int[] data = db.getData(); + int offset = bImg.getRaster().getDataBuffer().getOffset(); + int scan = 0; + SampleModel sm = bImg.getRaster().getSampleModel(); + if (sm instanceof SinglePixelPackedSampleModel) { + scan = ((SinglePixelPackedSampleModel) sm).getScanlineStride(); + } + WritablePixelFormat pf = switch (bImg.getType()) { + case BufferedImage.TYPE_INT_RGB, BufferedImage.TYPE_INT_ARGB_PRE -> PixelFormat.getIntArgbPreInstance(); + case BufferedImage.TYPE_INT_ARGB -> PixelFormat.getIntArgbInstance(); + default -> throw new InternalError("Failed to validate BufferedImage type"); + }; + pr.getPixels(0, 0, iw, ih, pf, data, offset, scan); + return bImg; + } +} diff --git a/src/main/java/com/imyeyu/fx/icon/util/package-info.java b/src/main/java/com/imyeyu/fx/icon/util/package-info.java new file mode 100644 index 0000000..9510956 --- /dev/null +++ b/src/main/java/com/imyeyu/fx/icon/util/package-info.java @@ -0,0 +1,2 @@ +/** 工具 */ +package com.imyeyu.fx.icon.util; diff --git a/src/main/resources/font/timi-icon.mapping b/src/main/resources/font/timi-icon.mapping new file mode 100644 index 0000000000000000000000000000000000000000..3a9c57911c70c88cef47bd82163f0eff5829065b GIT binary patch literal 2710 zcmYjT=XxA96qW5bE@^~7fY4j$#!hTHBw%n7AoSiQ+8ND`vYOFY((JAcgbsnw%Ukde z@bLuvgg3|!-hrcCUG4anb?26I&plV#-~VH?*0B9&d7m#ePO7Do+!m*KJv;aBpMM=Z z^Y;v!-e8ML^K65sPMcF~!4*c>LaS_8A3M(c-yef{#-{v#zd5#}R(uON4~%juUUiq& z3vLiC%<6>`|MU(W9!|5VbvE1Qsu9Eb;+u}`bh=ErvzzQ3o4L5<*g{if!YEmZU^@q= zruIOB;aA7ztuTFIlI38kVaj#otlXxsWu)?el1dCI{X35Bvb9K)f#5w+g=QR;JwB!jodlL3=re7EWu#6tP^E0eAJ2}lv5~Voud3|utxD7qgRSJ`e8y)Qi6W%#g6FiM$zShTP`n#H-UE(p zmZaPeeHY-aG<`pXQyB;Ri|>s0sr)x6KT=k0L_Z|_+UJ*wT<#df6EiQN+Q3MM$prT= z;hyT6hH#x9iXOyf5}Z?|Ev35#XU97+y>V=+6H*<7f;Jmr-9#_k<8_>-S9&mf82q0H z7$b7O$-$CEiq{hA=W@^qCH3MQPDH1bsKGfnjl~->-3blK3S)3dN8}nc#`-zd3MP^5 z1js^W zvf}>Lh75<1LRPsB;2weABsPpvK8SPGl@)2|Rg{?M%i?Q^^&rZhi%H9f8U1^BvH5`?UO`L3%AyF+NYt5UuIIkn5Rtnwwx-owK<9jpr3KOQ36(`L$tx$5?i|InMcCxT9qF$k+jvr_RktFtb z{&d~YE%;t*-~)2H6i%hC1kw018f&W7pnhye5uO7uj(WP(*5{x3lfcR G_wavnws&g) literal 0 HcmV?d00001 diff --git a/src/main/resources/font/timi-icon.ttf b/src/main/resources/font/timi-icon.ttf new file mode 100644 index 0000000000000000000000000000000000000000..4fb2ed5c7e4d2d74d487b80f022b64d41efe97e3 GIT binary patch literal 16712 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 glyphs = dom.getRootElement().element("defs").element("font").elements(); + System.out.println("read dom xml successes"); + + // 解析 Unicode 映射 + Map json = new HashMap<>(); + Map map = new HashMap<>(); + for (int i = 0; i < glyphs.size(); i++) { + if (glyphs.get(i) instanceof Element glyph) { + String name = glyph.attributeValue("glyph-name"); + String unicode = glyph.attributeValue("unicode"); + + if (name != null) { + json.put(name.trim(), Encoder.unicode(unicode)); + map.put(name.trim(), unicode.charAt(0)); + } + } + } + System.out.println("parse finish: map size = " + map.size()); + + // 序列化文件 + IO.toFile(jsonFile, new GsonBuilder().setPrettyPrinting().create().toJson(json).replaceAll(" ", "\t").replaceAll("\\\\\\\\", "\\\\")); + + FileOutputStream fos = new FileOutputStream(serializeFile); + ObjectOutputStream oos = new ObjectOutputStream(fos); + oos.writeObject(map); + oos.close(); + fos.close(); + + Path ttfFrom = new File("src/test/resources/timi-icon.ttf").toPath(); + Path ttfTo = new File("src/main/resources/font/timi-icon.ttf").toPath(); + Files.copy(ttfFrom, ttfTo, StandardCopyOption.REPLACE_EXISTING); + + IO.rename(new File("src/test/resources/timi-icon.svg"), "timi-icon_%s.svg".formatted(versionExt)); + IO.rename(new File("src/test/resources/timi-icon.eot"), "timi-icon_%s.eot".formatted(versionExt)); + IO.rename(new File("src/test/resources/timi-icon.ttf"), "timi-icon_%s.ttf".formatted(versionExt)); + IO.rename(new File("src/test/resources/timi-icon.woff"), "timi-icon_%s.woff".formatted(versionExt)); + + System.out.println("serialize mapping successes"); + } +} \ No newline at end of file