package jnpf.dms.fonts;
|
|
import jnpf.exception.DataException;
|
|
import java.awt.Font;
|
import java.awt.GraphicsEnvironment;
|
import java.io.IOException;
|
import java.nio.file.Files;
|
import java.nio.file.LinkOption;
|
import java.nio.file.Path;
|
import java.util.ArrayList;
|
import java.util.Collections;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.stream.Stream;
|
|
/** The directory mounted into OnlyOffice is also the source of every DMS watermark font. */
|
public final class DmsWatermarkFonts {
|
private static final DmsWatermarkFonts CONFIGURED = new DmsWatermarkFonts(System.getenv("ONLYOFFICE_CUSTOM_FONT_DIR"));
|
private final Path directory;
|
private long checkedAt;
|
private List<String> fingerprint = List.of();
|
private Map<String, Font> families = Map.of();
|
private Map<String, Font> aliases = Map.of();
|
|
public DmsWatermarkFonts(String directory) {
|
this.directory = directory == null || directory.isBlank() ? null : Path.of(directory).toAbsolutePath().normalize();
|
}
|
|
public static DmsWatermarkFonts configured() { return CONFIGURED; }
|
|
public synchronized List<Option> options() {
|
refresh();
|
return families.entrySet().stream()
|
.map(entry -> new Option(entry.getKey(), entry.getValue().getFamily(Locale.SIMPLIFIED_CHINESE)))
|
.toList();
|
}
|
|
public synchronized Font choose(String requested, String content, float pixels) {
|
if (directory == null) return legacyFont(requested, content, pixels);
|
refresh();
|
if (families.isEmpty()) throw new DataException("字体目录没有可用字体,请添加 TTF、OTF 或 TTC 字体文件");
|
if (requested != null && !requested.isBlank()) {
|
Font font = aliases.get(requested.trim().toLowerCase(Locale.ROOT));
|
if (font == null) throw new DataException("配置字体目录中不存在字体:" + requested);
|
if (font.canDisplayUpTo(content) != -1) throw new DataException("所选字体无法显示全部水印内容:" + requested);
|
return font.deriveFont(pixels);
|
}
|
return families.values().stream().filter(font -> font.canDisplayUpTo(content) == -1)
|
.findFirst().orElseThrow(() -> new DataException("字体目录中没有可显示水印内容的字体")).deriveFont(pixels);
|
}
|
|
private void refresh() {
|
if (directory == null) return;
|
long now = System.nanoTime();
|
if (checkedAt != 0 && now - checkedAt < 5_000_000_000L) return;
|
if (!Files.isDirectory(directory)) throw new DataException("ONLYOFFICE_CUSTOM_FONT_DIR 字体目录不存在或不可读");
|
List<Path> files;
|
List<String> current = new ArrayList<>();
|
try (Stream<Path> paths = Files.walk(directory)) {
|
files = paths.filter(path -> Files.isRegularFile(path, LinkOption.NOFOLLOW_LINKS))
|
.filter(DmsWatermarkFonts::supported).sorted().toList();
|
for (Path file : files) current.add(file + ":" + Files.size(file) + ":" + Files.getLastModifiedTime(file).toMillis());
|
} catch (IOException exception) {
|
throw new DataException("读取 ONLYOFFICE_CUSTOM_FONT_DIR 字体目录失败");
|
}
|
if (!current.equals(fingerprint)) {
|
Map<String, Font> loaded = new LinkedHashMap<>();
|
for (Path file : files) {
|
try {
|
for (Font font : Font.createFonts(file.toFile())) {
|
String family = font.getFamily(Locale.ENGLISH);
|
Font existing = loaded.get(family);
|
if (existing == null || font.getFontName(Locale.ENGLISH).equalsIgnoreCase(family)) loaded.put(family, font);
|
}
|
} catch (java.awt.FontFormatException | IOException exception) {
|
// A corrupt or unsupported font must never become a selectable option.
|
}
|
}
|
Map<String, Font> names = new LinkedHashMap<>();
|
loaded.values().forEach(font -> {
|
GraphicsEnvironment.getLocalGraphicsEnvironment().registerFont(font);
|
for (Locale locale : List.of(Locale.ENGLISH, Locale.SIMPLIFIED_CHINESE, Locale.getDefault())) {
|
names.put(font.getFamily(locale).toLowerCase(Locale.ROOT), font);
|
names.put(font.getFontName(locale).toLowerCase(Locale.ROOT), font);
|
}
|
});
|
families = Collections.unmodifiableMap(loaded);
|
aliases = Collections.unmodifiableMap(names);
|
fingerprint = current;
|
}
|
checkedAt = now;
|
}
|
|
private static boolean supported(Path path) {
|
String name = path.getFileName().toString().toLowerCase(Locale.ROOT);
|
return name.endsWith(".ttf") || name.endsWith(".otf") || name.endsWith(".ttc") || name.endsWith(".otc");
|
}
|
|
// Preserve installations that have not configured a custom directory.
|
private Font legacyFont(String requested, String content, float pixels) {
|
Set<String> available = Set.of(GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames());
|
List<String> candidates = new ArrayList<>();
|
if (requested != null && !requested.isBlank()) candidates.add(requested.trim());
|
Collections.addAll(candidates, "SimHei", "Noto Sans CJK SC", "Microsoft YaHei", "PingFang SC", "WenQuanYi Zen Hei", Font.SANS_SERIF);
|
for (String candidate : candidates) {
|
Font font = new Font(candidate, Font.PLAIN, 1);
|
if ((Font.SANS_SERIF.equals(candidate) || available.contains(candidate)) && font.canDisplayUpTo(content) == -1) return font.deriveFont(pixels);
|
}
|
throw new DataException("服务器未安装可显示水印内容的字体");
|
}
|
|
public record Option(String family, String displayName) {}
|
}
|