How to determine if 2 fonts have equivalent glyphs?
Asked Answered
E

1

8

This Q&A was inspired by Unicode char not rendering in Swing, what font is used in real? but since it does not answer the specific question asked, I decided to enter it as its own Q&A. The question here is..

How to determine if two fonts, for a given text, are effectively equivalent in returning identical character glyphs?

Empyreal answered 5/9, 2014 at 10:17 Comment(0)
E
8

The trick used here is to compare the GlyphVector returned for the String of interest. The crux of this approach can be seen in the method fontsAreEquivalentForText(Font,Font,String).

Here is an example output for Arial.

enter image description here

The GUI has three basic components.

  1. The text to test in a text field at the top of the GUI. Here we check the letters of the alphabet in the string The quick brown fox jumps over the lazy dog.
  2. The list of all fonts is shown on the left.
  3. When a font is selected from the list of all the fonts, it will pop a cancellable (it takes a while to check over 400 fonts!) dialog that progressively builds the list on the right. This list is the equivalent fonts (and should include the selected font, barring a programming error).

Code

import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.Area;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.event.*;

public class FontEquivalence {

    public boolean fontsAreEquivalentForText(Font f1, Font f2, String text) {
        Area area1 = new Area(
                f1.deriveFont(25f).
                createGlyphVector(fontRenderContext, text).
                getOutline());
        Area area2 = new Area(
                f2.deriveFont(25f).
                createGlyphVector(fontRenderContext, text).
                getOutline());
        return area2.equals(area1);
    }

    public void findEquivalentFonts(final Font font) {
        if (dialog == null) {
            dialog = getDialog(ui);
        }
        fontChecker = new SwingWorker() {

            @Override
            protected Object doInBackground() throws Exception {
                dialog.setLocationRelativeTo(ui);
                sameFontListModel.clear();
                String s = inputString.getText();
                int fontNumber = fonts.length;
                progress.setMaximum(fontNumber);
                int ii = 1;
                for (Font f : fonts) {
                    if (fontsAreEquivalentForText(f, font, s)) {
                        sameFontListModel.addElement(f);
                    }
                    progress.setValue(ii++);
                    if (fontChecker.isCancelled()) {
                        break;
                    }
                }
                dialog.setVisible(false);
                return null;
            }
        };
        fontChecker.execute();
        dialog.setVisible(true);
    }

    public JDialog getDialog(JComponent comp) {
        Container cont = comp.getTopLevelAncestor();
        Frame f = null;
        if (cont instanceof Frame) {
            f = (Frame) cont;
        }
        final JDialog d = new JDialog(f, 
                "Searching " + fonts.length + " fonts for equivalents..", 
                true);
        JPanel p = new JPanel(new BorderLayout(15, 15));
        p.setBorder(new EmptyBorder(40, 100, 40, 100));
        p.add(progress, BorderLayout.CENTER);

        JButton cancel = new JButton("Cancel");
        ActionListener al = (ActionEvent e) -> {
            fontChecker.cancel(true);
            
            d.setVisible(false);
        };
        cancel.addActionListener(al);
        JPanel control = new JPanel(new FlowLayout(FlowLayout.CENTER));
        control.add(cancel);
        p.add(control, BorderLayout.PAGE_END);

        d.add(p);
        d.pack();

        return d;
    }

    public JComponent getUI() {
        if (ui == null) {
            ui = new JPanel(new BorderLayout(2, 2));
            inputString = new JTextField(text, 15);
            inputString.setFont(inputString.getFont().deriveFont(20f));
            ui.add(inputString, BorderLayout.PAGE_START);
            GraphicsEnvironment ge = GraphicsEnvironment.
                    getLocalGraphicsEnvironment();
            fonts = ge.getAllFonts();
            final JList fontList = new JList(fonts);
            ListSelectionListener lsl = (ListSelectionEvent e) -> {
                if (!e.getValueIsAdjusting()) {
                    Font font = (Font) fontList.getSelectedValue();
                    findEquivalentFonts(font);
                }
            };
            fontList.addListSelectionListener(lsl);
            fontList.setCellRenderer(new FontCellRenderer());
            fontList.setVisibleRowCount(15);
            ui.add(new JScrollPane(fontList), BorderLayout.LINE_START);

            JList list = new JList(sameFontListModel);
            list.setCellRenderer(new FontCellRenderer());
            ui.add(new JScrollPane(list));

            BufferedImage bi = new BufferedImage(
                    1, 1, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = bi.createGraphics();
            fontRenderContext = g.getFontRenderContext();

            progress = new JProgressBar(0, fonts.length);
            progress.setStringPainted(true);
        }
        return ui;
    }

    JPanel ui = null;
    JTextField inputString;
    String text = "The quick brown fox jumps over the lazy dog.";
    Font[] fonts;
    DefaultListModel sameFontListModel = new DefaultListModel();
    FontRenderContext fontRenderContext;
    JDialog dialog;
    SwingWorker fontChecker;
    JProgressBar progress;

    public static void main(String[] args) {
        Runnable r = () -> {
            JFrame f = new JFrame("Font Equivalence");
            f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            f.setContentPane(new FontEquivalence().getUI());
            f.pack();
            f.setLocationByPlatform(true);
            f.setVisible(true);
        };
        SwingUtilities.invokeLater(r);
    }
}

class FontCellRenderer extends DefaultListCellRenderer {

    @Override
    public Component getListCellRendererComponent(
            JList list,
            Object value,
            int index,
            boolean isSelected,
            boolean cellHasFocus) {
        JLabel label = (JLabel) super.getListCellRendererComponent(
                list, value, index, isSelected, cellHasFocus);
        Font font = (Font) value;
        label.setFont(font.deriveFont(20f));
        label.setText(font.getName());
        return label;
    }
}

Update

StanislavL alerted me to some fragility in this method in a comment:

A note: Use Font.layoutGlyphVector() rather than createGlyphVector(). Current solution may generate wrong results for some fonts which reorder glyphs. From Javadoc about createGlyphVector()

This method does no other processing besides the mapping of glyphs to characters. This means that this method is not useful for some scripts, such as Arabic, Hebrew, Thai, and Indic, that require reordering, shaping, or ligature substitution.

I've seen something like this with Arabic and Hebrew rendering.

I will leave this code as-is, but for more details see Font.layoutGlyphVector(FontRenderContext,char[],start,limit,flags) which:

Returns a new GlyphVector object, performing full layout of the text if possible. Full layout is required for complex text, such as Arabic or Hindi. Support for different scripts depends on the font and implementation. ..

Empyreal answered 5/9, 2014 at 10:17 Comment(2)
+1 for the solution. A note: Use Font.layoutGlyphVector() rather than createGlyphVector(). Current solution may generate wrong results for some fonts which reorder glyphs. From Javadoc about createGlyphVector(): This method does no other processing besides the mapping of glyphs to characters. This means that this method is not useful for some scripts, such as Arabic, Hebrew, Thai, and Indic, that require reordering, shaping, or ligature substitution. I've seen something like this with Arabic and Hebrew rendering.Exedra
@Exedra Thanks for the excellent tip. :) I added and slightly expanded on it, as an edit to the question.Empyreal

© 2022 - 2024 — McMap. All rights reserved.