Initial project

This commit is contained in:
Timi
2025-07-14 14:20:00 +08:00
parent 6266e06db1
commit 843eaef0cd
14 changed files with 795 additions and 94 deletions

View File

@@ -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.. 等字号时渲染最清晰准确
*
* <p>图标名称可在 <a href="https://icon.imyeyu.net/">https://icon.imyeyu.net/</a> 查询</p>
*
* <pre>
* // 静态类,不需要实例化,直接调用静态方法
* TimiFXIcon.fromName("FILE");
* </pre>
*
* @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&lt;字号, 字体&gt; */
private static final Map<Integer, Font> FONT_MAPPING = new HashMap<>();
/** 映射缓存 Map&lt;图标名, Unicode&gt; */
private static Map<String, Character> 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<String, Character>) ois.readObject();
ois.close();
is.close();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
/**
* 获取名称字符映射表
*
* @return 名称字符映射表
*/
public static Map<String, Character> 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);
}
}
}

View File

@@ -0,0 +1,2 @@
/** 主程序 */
package com.imyeyu.fx.icon;

View File

@@ -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<IntBuffer> 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<IntBuffer> 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;
}
}

View File

@@ -0,0 +1,2 @@
/** 工具 */
package com.imyeyu.fx.icon.util;

Binary file not shown.

Binary file not shown.

View File

@@ -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&lt;图标名, Unicode&gt; 对象
*
* @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<String, String> json = new HashMap<>();
Map<String, Character> 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");
}
}