Skip to content

Instantly share code, notes, and snippets.

@timboudreau
Created August 8, 2019 20:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save timboudreau/5fe88764db08627948a87d8a4cb31a99 to your computer and use it in GitHub Desktop.
Save timboudreau/5fe88764db08627948a87d8a4cb31a99 to your computer and use it in GitHub Desktop.
import java.awt.DisplayMode;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.image.VolatileImage;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
public class ComputedFontSizeTester {
// Monitor diagonals for determining a font size based
// on monitor size
private static final int TRADITIONAL_MONITOR_480 = 640 * 480;
private static final int TRADITIONAL_MONITOR_600 = 800 * 600;
private static final int TRADITIONAL_MONITOR_768 = 1024 * 768;
private static final int TRADITIONAL_MONITOR_1024 = 1280 * 1024;
private static final int TRADITIONAL_MONITOR_1600 = 1600 * 1200;
private static final int HI_DEF_MONITOR_720 = 1280 * 720;
private static final int HI_DEF_MONITOR_900 = 1600 * 900;
private static final int HI_DEF_MONITOR_FHD = 1920 * 1080;
private static final int HI_DEF_MONITOR_QHD = 2560 * 1440;
private static final int HI_DEF_MONITOR_4K = 3840 * 2160;
private static final int HI_DEF_MONITOR_8K = 7680 * 4320;
private static final int DEFAULT_CHARS_PER_INCH = 10;
public static void main(String[] args) throws InterruptedException {
System.out.println("TARGET: " + adjustFontSizeForScreenSize(11));
System.exit(0);
}
private static DisplayMode findLargestDisplayMode(DisplayMode[] mode) {
Arrays.sort(mode, new DisplayModeComparator());
return mode[0];
}
static final class DisplayModeComparator implements Comparator<DisplayMode> {
@Override
public int compare(DisplayMode a, DisplayMode b) {
return Integer.compare(b.getWidth() * b.getHeight(), a.getWidth() * a.getHeight());
}
}
private static boolean sameResolution(DisplayMode a, DisplayMode b) {
return a.getWidth() == b.getWidth() && a.getHeight() == b.getHeight();
}
private static GraphicsDevice getLargestGraphicsDevice() {
List<GraphicsDevice> devices = new ArrayList<>();
for (GraphicsDevice device : GraphicsEnvironment.getLocalGraphicsEnvironment().getScreenDevices()) {
if (device.getType() == GraphicsDevice.TYPE_RASTER_SCREEN) {
devices.add(device);
}
}
Collections.sort(devices, new GraphicsDeviceComparator());
return devices.isEmpty() ? null : devices.get(0);
}
private static AffineTransform createScalingTransform(DisplayMode a, DisplayMode b) {
double xScale = (double) a.getWidth() / (double) b.getWidth();
double yScale = (double) a.getHeight() / (double) b.getHeight();
return AffineTransform.getScaleInstance(xScale, yScale);
}
public static int adjustFontSizeForScreenSize(int initialTarget) {
if (GraphicsEnvironment.isHeadless()) { // possible in tests
return initialTarget;
}
GraphicsDevice largest = getLargestGraphicsDevice();
// On Linux, anyway, the "normalizing" transform the graphics device
// will return will be the same at *all* resolutions. So we should not
// assume that is useful.
// Instead, we get the largest possible graphics configuration - which
// is most likely to be the native monitor resolution, and scale based
// on that.
DisplayMode dm = largest.getDisplayMode();
DisplayMode largestDisplayMode = findLargestDisplayMode(largest.getDisplayModes());
// It may be that the "default configuration" is not the one in use,
// which is why we always get the native screen resolution, but it
// is the one we get
GraphicsConfiguration config = largest.getDefaultConfiguration();
int diagonal = dm.getWidth() * dm.getHeight();
// Get the screen dpi. Note that this method has some weird behavior
// at least on my current Linux install - each time the screen resolution
// is reduced and then increased, the next time you run a JVM, the
// return value will have increased by 1.
int pixelsPerInch = java.awt.Toolkit.getDefaultToolkit().getScreenResolution();
DecimalFormat fmt = new DecimalFormat("###0.#");
System.out.println("CURRENT DISPLAY MODE: " + dm.getWidth() + "x" + dm.getHeight());
System.out.println("LARGEST DISPLAY MODE: " + largestDisplayMode.getWidth() + "x" + largestDisplayMode.getHeight());
System.out.println("TOOLKIT REPORTS DPI " + pixelsPerInch);
System.out.println("SCREEN DIAGONAL IN INCHES: " + fmt.format(Math.sqrt((double) diagonal) / (double) pixelsPerInch));
System.out.println("SCREEN SIZE IN INCHES: " + fmt.format((double) largestDisplayMode.getWidth() / (double) pixelsPerInch) + "\" x "
+ fmt.format((double) largestDisplayMode.getHeight() / (double) pixelsPerInch) + "\"");
// Get the default transform for the device.
AffineTransform defaultTransform = config.getDefaultTransform();
// This gets us a transform to 72 dots per inch
AffineTransform normalizingTransform = config.getNormalizingTransform();
AffineTransform scalingTransform;
if (!sameResolution(largestDisplayMode, dm)) {
scalingTransform = createScalingTransform(largestDisplayMode, dm);
} else {
scalingTransform = new AffineTransform();
}
// We will invert this, so that we can scale a font with it rather
// than applying it to a graphics - that will let us measure the screen
// width of a font in inches
AffineTransform invertedTransform = new AffineTransform(normalizingTransform);
int baseFontSize = baseFontSizeByDiagonal(diagonal, initialTarget);
try {
invertedTransform.invert();
} catch (NoninvertibleTransformException ex) {
// Won't appen
Logger.getLogger(ComputedFontSizeTester.class.getName()).log(Level.SEVERE, null, ex);
return baseFontSize;
}
VolatileImage img = config.createCompatibleVolatileImage(1, 1);
Graphics2D g = img.createGraphics();
g.setTransform(defaultTransform);
try {
return findBestFontSize(g, scalingTransform, baseFontSize, pixelsPerInch);
} finally {
g.dispose();
}
}
private static int findBestFontSize(Graphics2D g, AffineTransform scalingTransform, int baseFontSize, int pixelsPerInch) {
int result = baseFontSize;
// Get a ten character of ComputedFontSizeTester's which is a wide character in most fonts
String test = cpiMeasurementString();
int adjustmentDirection = 1;
boolean first = true;
System.out.println("find best size with " + scalingTransform + " and heuristic font size " + baseFontSize + " @ " + pixelsPerInch + " DPI");
// In practice, the default values are within 1-2 point sizes of the
// ideal value, so this does not loop heavily unless an exception was
// thrown earlier
int lastStringWidth = Integer.MAX_VALUE;
for (int i = 0; i < 48; i++) {
if (result + (i * adjustmentDirection) < 0) {
break;
}
// Create a scaled font. Use the JDK's sans serif since it is
// always present, and most fonts are similar - at the time we
// are called, the L&F is incompletely initialized, so trying to
// get the value is likely to get one that will be replaced (and
// it probably is Sans Serif anyway).
Font f = new Font("Sans Serif", Font.PLAIN, result + (i * adjustmentDirection));
f = f.deriveFont(scalingTransform);
// Transform it into our inverted 72dpi space
FontMetrics fm = g.getFontMetrics(f);
// Get the width of this string - 72px = one inch, so we search
// for the nearest value between a string width < 72 and a string
// width > 72
int w = fm.stringWidth(test);
System.out.println(test.length() + " chars @ " + f.getSize() + "pt = " + w + "px - iteration " + i);
if (first) {
// First loop - unless it's exact, just figure out which
// direction we need to loop in
if (w > pixelsPerInch) {
adjustmentDirection = -1;
} else if (w < pixelsPerInch) {
adjustmentDirection = 1;
} else {
result += (i * adjustmentDirection);
System.out.println("exact 72 dpi match for " + result);
break;
}
first = false;
} else {
// If we are at a boundary, set the value to return and break
if (w < pixelsPerInch && adjustmentDirection == -1) {
int newResult = result + (i * adjustmentDirection);
if (Math.abs(pixelsPerInch - w) < Math.abs(pixelsPerInch - lastStringWidth)) {
result = newResult;
}
break;
} else if (w > pixelsPerInch && adjustmentDirection == 1) {
int newResult = result + (i * adjustmentDirection);
if (Math.abs(pixelsPerInch - w) < Math.abs(pixelsPerInch - lastStringWidth)) {
result = newResult;
}
break;
}
}
lastStringWidth = w;
}
System.out.println("Computed target font size " + result);
return result;
}
private static final class GraphicsDeviceComparator implements Comparator<GraphicsDevice> {
@Override
public int compare(GraphicsDevice a, GraphicsDevice b) {
DisplayMode dm = a.getDisplayMode();
int awh = dm.getWidth() * dm.getHeight();
int bwh = dm.getWidth() * dm.getHeight();
return -Integer.compare(awh, bwh);
}
}
/**
* Find base values based on the diagonal pixel count of the screen.
*
* @param diagonal The reported width * height of the screen
* @param defaultBaseSize The default base font size
* @return
*/
static int baseFontSizeByDiagonal(int diagonal, int defaultBaseSize) {
switch (diagonal) {
case HI_DEF_MONITOR_8K:
return 36;
case HI_DEF_MONITOR_4K:
return 28;
case HI_DEF_MONITOR_QHD:
return 20;
case HI_DEF_MONITOR_FHD:
return 18;
case HI_DEF_MONITOR_900:
return 14;
case HI_DEF_MONITOR_720:
return 13;
case TRADITIONAL_MONITOR_1600:
return 15;
case TRADITIONAL_MONITOR_1024:
return 13;
case TRADITIONAL_MONITOR_768:
return 12;
case TRADITIONAL_MONITOR_600:
return 11;
case TRADITIONAL_MONITOR_480:
return 10;
default:
// If we did not get handed an exact match, find the
// nearest. Simply setting your window manager to have
// screen margins will result in an inexact match.
int[] allResolutions = new int[]{
TRADITIONAL_MONITOR_1600, TRADITIONAL_MONITOR_1024,
TRADITIONAL_MONITOR_768,
TRADITIONAL_MONITOR_600, TRADITIONAL_MONITOR_480,
HI_DEF_MONITOR_8K, HI_DEF_MONITOR_4K, HI_DEF_MONITOR_QHD,
HI_DEF_MONITOR_FHD, HI_DEF_MONITOR_900, HI_DEF_MONITOR_720
};
Arrays.sort(allResolutions);
int direction = 0;
// Find the nearest and recursively call this method, this time
// with a constant that will be caught by ths switch above.
for (int i = 0; i < allResolutions.length; i++) {
int currentResolution = allResolutions[i];
int currentDirection = Integer.compare(diagonal, currentResolution);
if (direction == 0) {
direction = currentDirection;
} else if (currentDirection != direction) {
return baseFontSizeByDiagonal(allResolutions[i],
defaultBaseSize);
}
}
}
return defaultBaseSize;
}
private static String cpiMeasurementString() {
char[] chars = new char[targetCharsPerInch()];
Arrays.fill(chars, 'X'); // NOI18N
return new String(chars);
}
/**
* Get the default target characters per inch, or the value returned by the
* system property.
*
* @return An integer for the target characters per inch
*/
private static int targetCharsPerInch() {
String value = System.getProperty("screenTargetCpi"); // NOI18N
if (value != null) {
try {
int result = Integer.parseInt(value);
if (result < 0) {
throw new NumberFormatException("screenTargetCpi must be > 0"); // NOI18N
}
return result;
} catch (NumberFormatException nfe) {
Logger.getLogger(ComputedFontSizeTester.class.getName()).log(Level.WARNING,
"Bad value for system property screenTargetCpi: ''{0}''", // NOI18N
value);
}
}
return DEFAULT_CHARS_PER_INCH;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment