Created
August 8, 2019 20:01
-
-
Save timboudreau/5fe88764db08627948a87d8a4cb31a99 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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