Skip to content
danfickle edited this page Aug 21, 2020 · 1 revision

Sometimes it can be useful to pull in all the fonts in a directory rather than explicitly listing them.

This code sample is based on one kindly provided by @ieugen in #454.

Usage

        Path fontDirectory = Paths.get("/users/home/dan/fonts/");

        // PERF: Should only be called once, as each font must be parsed for font family name.
        List<CSSFont> fonts = AutoFont.findFontsInDirectory(fontDirectory);

        // Use this in your template for the font-family property.
        String fontFamily = AutoFont.toCSSEscapedFontFamily(fonts);

        // Add fonts to builder.
        AutoFont.toBuilder(builder, fonts);

Code

package com.example;

import java.io.IOException;
import java.nio.file.FileVisitOption;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import java.awt.Font;
import java.awt.FontFormatException;

import com.openhtmltopdf.outputdevice.helper.BaseRendererBuilder.FontStyle;
import com.openhtmltopdf.pdfboxout.PdfRendererBuilder;

/**
 * A tool for listing all the fonts in a directory.
 */
public class AutoFont {

    private AutoFont() { }

    /**
     * Returns a list of fonts in a given directory.
     * NOTE: Should not be used repeatedly as each font found is parsed to get the family name.
     *
     * @param validFileExtensions list of file extensions that are fonts - usually Collections.singletonList("ttf")
     * @param recurse whether to look in sub-directories recursively
     * @param followLinks whether to follow symbolic links in the file system
     * @return a list of fonts.
     */
    public static List<CSSFont> findFontsInDirectory(
        Path directory, List<String> validFileExtensions, boolean recurse, boolean followLinks) throws IOException {

        FontFileProcessor processor = new FontFileProcessor(validFileExtensions);

        int maxDepth = recurse ? Integer.MAX_VALUE : 1;
        Set<FileVisitOption> options = followLinks ? EnumSet.of(FileVisitOption.FOLLOW_LINKS) : EnumSet.noneOf(FileVisitOption.class);

        Files.walkFileTree(directory, options, maxDepth, processor);

        return processor.getFontsAdded();
    }

    /**
     * Returns a list of fonts in a given directory. Recursively searches directory and
     * sub-directories for .ttf files. Follows symbolic links.
     * NOTE: Should not be used repeatedly as each font found is parsed to get the family name.
     */
    public static List<CSSFont> findFontsInDirectory(Path directory) throws IOException {
        return findFontsInDirectory(directory, Collections.singletonList("ttf"), true, true);
    }

    /**
     * Get a string containing added font families (duplicates removed) in a format suitable
     * for the CSS font-family property.
     * 
     * WARNING: Basic escaping, may not be robust to attack.
     */
    public static String toCSSEscapedFontFamily(List<CSSFont> fontsList) {
        return fontsList.stream()
           .map(fnt -> '\'' + fnt.familyCssEscaped() + '\'')
           .distinct()
           .collect(Collectors.joining(", "));
    }

    /**
     * Adds all fonts in the list to the builder.
     */
    public static void toBuilder(PdfRendererBuilder builder, List<CSSFont> fonts) {
        for (CSSFont font : fonts) {
            builder.useFont(font.path.toFile(), font.family, font.weight, font.style, true);
        }
    }

    public static class CSSFont {
        public final Path path;
        public final String family;

        /**
         * WARNING: Heuristics are used to determine if a font is bold (700) or normal (400) weight.
         */
        public final int weight;

        /**
         * WARNING: Heuristics are used to determine if a font is italic or normal style.
         */
        public final FontStyle style;

        public CSSFont(Path path, String family, int weight, FontStyle style) {
            this.path = path;
            this.family = family;
            this.weight = weight;
            this.style = style;
        }

        /**
         * WARNING: Basic escaping, may not be robust to attack.
         */
        public String familyCssEscaped() {
            return this.family.replace("'", "\\'");
        }

        @Override
        public int hashCode() {
            return Objects.hash(path, family, weight, style);
        }

        @Override
        public boolean equals(Object other) {
            if (other == this) {
                return true;
            }
            
            if (other == null ||
                other.getClass() != this.getClass()) {
                return false;
            }

            CSSFont b = (CSSFont) other;

            return Objects.equals(this.path, b.path) &&
                   Objects.equals(this.family, b.family) &&
                   this.weight == b.weight &&
                   this.style == b.style;
        }
    }


    public static class FontFileProcessor extends SimpleFileVisitor<Path> {
        private final List<String> validFileExtensions;
        private final List<CSSFont> fontsAdded = new ArrayList<>();

        public FontFileProcessor(List<String> validFileExtensions) {
            this.validFileExtensions = new ArrayList<>(validFileExtensions);
        }

        public List<CSSFont> getFontsAdded() {
            return this.fontsAdded;
        }

        @Override
        public FileVisitResult visitFile(Path font, BasicFileAttributes attrs) throws IOException {
            if (attrs.isRegularFile() && isValidFont(font)) {
                try {
                    Font f = Font.createFont(Font.TRUETYPE_FONT, font.toFile());
                    
                    String family = f.getFamily();
                    // Short of parsing the font ourselves there doesn't seem to be a way
                    // of getting the font properties, so we use heuristics based on font name.
                    String name = f.getFontName(Locale.US).toLowerCase(Locale.US);
                    int weight = name.contains("bold") ? 700 : 400;
                    FontStyle style = name.contains("italic") ? FontStyle.ITALIC : FontStyle.NORMAL;

                    CSSFont fnt = new CSSFont(font, family, weight, style);

                    onValidFont(fnt);
                    fontsAdded.add(fnt);
                } catch (FontFormatException ffe) {
                    onInvalidFont(font, ffe);
                }
            }

            return FileVisitResult.CONTINUE;
        }

        protected void onValidFont(CSSFont font) {
            System.out.format("Adding font with path = '%s', name = '%s', weight = %d, style = %s%n", font.path, font.family, font.weight, font.style.name());
        }

        protected void onInvalidFont(Path font, FontFormatException ffe) {
            System.err.println("Ignoring font file with invalid font format: " + font);
            System.err.println("Exception details: ");
            ffe.printStackTrace();
        }
  
        protected boolean isValidFont(Path font) {
            return this.validFileExtensions.stream()
                     .anyMatch(ext -> font.toString().endsWith(ext));
        }
    }
}