刘光辉
3 小时以前 06f314545ac3514d7ce28939920866ae8188d31c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
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) {}
}