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 0000000..3a9c579 Binary files /dev/null and b/src/main/resources/font/timi-icon.mapping differ diff --git a/src/main/resources/font/timi-icon.ttf b/src/main/resources/font/timi-icon.ttf new file mode 100644 index 0000000..4fb2ed5 Binary files /dev/null and b/src/main/resources/font/timi-icon.ttf differ diff --git a/src/test/java/test/SerializeMapping.java b/src/test/java/test/SerializeMapping.java new file mode 100644 index 0000000..d7d6a50 --- /dev/null +++ b/src/test/java/test/SerializeMapping.java @@ -0,0 +1,80 @@ +package test; + +import com.google.gson.GsonBuilder; +import com.imyeyu.io.IO; +import com.imyeyu.utils.Encoder; +import com.imyeyu.utils.Time; +import org.dom4j.Document; +import org.dom4j.Element; +import org.dom4j.io.SAXReader; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.ObjectOutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * IcoMoon 生成的 xml 映射序列化为 Map<图标名, Unicode> 对象 + * + * @author 夜雨 + * @since 2022-07-28 21:16 + */ +public class SerializeMapping { + + public static void main(String[] args) throws Exception { + String subVersion = "a"; + + String versionExt = Time.ymd.format(Time.now()) + subVersion; + // 输出映射文件 + File jsonFile = new File("src/test/resources/timi-icon_%s.json".formatted(versionExt)); + File serializeFile = new File("src/main/resources/font/timi-icon.mapping"); + System.out.println("serialize to " + serializeFile.getAbsolutePath()); + + // 读取 SVG 映射 + String svgPath = "timi-icon.svg"; + Document dom = new SAXReader().read(Thread.currentThread().getContextClassLoader().getResourceAsStream(svgPath)); + List 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