0 | ||| Measuring the visible bounds of text is incredibly hard
 1 | ||| and tons of material have been written about this.
 2 | |||
 3 | ||| In this module, we take a pragmatic approach that produces
 4 | ||| reasonable results without having to load and parse font files.
 5 | ||| The drawback of this: The steps described below have to be repeated
 6 | ||| for every new font we'd like to support.
 7 | |||
 8 | ||| A detailed introduction to typography and how fonts are specified
 9 | ||| can be found [here](https://learn.microsoft.com/en-us/typography/opentype/spec/otff).
10 | |||
11 | ||| In general, we need to know the height and width of a piece of printed text
12 | ||| to properly align it with the rest of the drawing.
13 | |||
14 | ||| Text height:
15 | |||   there are different types of "height" when it comes to text, and I won't go
16 | |||   into the details here. Suffice to say that we are interested in vertically aligning
17 | |||   atom labels, charges, implicit hydrogen count and mass numbers in a
18 | |||   way that feels natural. For this, wir are mostly interested in "capHeight" of
19 | |||   a font: The height of capital letters (without descenders). This, together
20 | |||   with the Em-square size can be read from font files.
21 | |||
22 | ||| Text width:
23 | |||   While computing the height of a piece of text is non-trivial, computing its
24 | |||   width is insane. Every glyph has its own specific width, sometimes depending
25 | |||   on its neighbouring glyphs (see ligatures and kerning). Fortunately, there
26 | |||   is a quite simple method to get good approximations without being overly
27 | |||   complicated. It is described on [stackoverflow](https://stackoverflow.com/questions/16007743/roughly-approximate-the-width-of-a-string-of-text-in-python)
28 | |||   We use Python (because it has support for almost everything) to parse
29 | |||   the true type font file we are interested in and generate a dictionary
30 | |||   of the glyphs and their widths we are interested in. Using this to compute
31 | |||   the width of a piece of text at a given font size is efficient and simple
32 | |||   but not perfectly exact because it ignores kerning. It also requires large
33 | |||   dictionaries if we want to support lots of unicode characters.
34 | |||
35 | ||| Note: The Python script used to extract the glyph widths can be found in the
36 | |||       `resources` directory.
37 | module Text.Measure
38 |
39 | import Data.SortedMap
40 |
41 | %default total
42 |
43 | -- based on [stackoverflow](https://stackoverflow.com/questions/16007743/roughly-approximate-the-width-of-a-string-of-text-in-python)
44 | widths : List (Char,Bits32)
45 | widths = [('0',278),('1',278),('2',278),('3',278),('4',278),('5',278),('6',278),('7',278),('8',278),('9',278),('a',279),('b',278),('c',250),('d',278),('e',278),('f',140),('g',278),('h',278),('i',111),('j',124),('k',251),('l',111),('m',417),('n',278),('o',278),('p',278),('q',278),('r',167),('s',250),('t',139),('u',278),('v',250),('w',364),('x',250),('y',250),('z',250),('A',334),('B',334),('C',361),('D',361),('E',334),('F',305),('G',389),('H',361),('I',139),('J',250),('K',334),('L',278),('M',417),('N',361),('O',389),('P',334),('Q',389),('R',361),('S',334),('T',305),('U',361),('V',334),('W',472),('X',334),('Y',334),('Z',305),('!',139),('"',177),('#',278),('$',278),('%',445),('&',334),('\'',95),('(',167),(')',167),('*',195),('+',292),(',',139),('-',167),('.',139),('/',139),(':',139),(';',139),('<',292),('=',292),('>',292),('?',278),('@',508),('[',139),('\\',139),(']',139),('^',235),('_',292),('`',167),('{',167),('|',130),('}',167),('~',292),(' ',139)]
46 |
47 | widthMap : SortedMap Char Bits32
48 | widthMap = fromList widths
49 |
50 | averageWidth : Bits32
51 | averageWidth = sum widthMap `div` cast (length widths)
52 |
53 | charWidth : Bits32 -> Char -> Bits32
54 | charWidth n c = maybe (n + averageWidth) (n+) $ lookup c widthMap
55 |
56 | -- text was measure at a font size of 500, so we divide by that
57 | -- when computing the width at a different font size.
58 | textWidth : Nat -> String -> Double
59 | textWidth fs s = cast (foldl charWidth 0 (unpack s)) * (cast fs) / 500.0
60 |
61 | ||| Metrics of a piece of text.
62 | public export
63 | record TextDims where
64 |   constructor TD
65 |   lineHeight : Double
66 |   capHeight  : Double
67 |   txtWidth   : Double
68 |
69 | ||| Utility for measuring text metrics.
70 | public export
71 | record Measure where
72 |   [noHints]
73 |   constructor M
74 |   measure : Nat -> (font, txt : String) -> TextDims
75 |
76 | ||| This is a primitive but efficient implementation of `Measure`, which
77 | ||| is described in the module docs. A more exact implementation could
78 | ||| make use of a browser canvas and use text metrics from the DOM, but
79 | ||| this is not available when we are not drawing molecules in the browser.
80 | |||
81 | ||| This implementation assumes a typical roman font similar to Arial.
82 | ||| It is based on the metrics of "Liberation Sans", which should have the
83 | ||| same layout as Arial or Helvetica.
84 | |||
85 | ||| About magic numbers: 2048 is the Em square size, 1409 the cap height,
86 | ||| 307 the line height. These have to be multiplied with the font size.
87 | ||| Text widths are approxiamted by summing up glyph width stored in a
88 | ||| dictionary.
89 | export
90 | defaultMeasure : Measure
91 | defaultMeasure =
92 |   M $ \fs,f,s => case length s of
93 |     0 => TD 0 0 0
94 |     n =>
95 |       let fsd := cast {to = Double} fs
96 |        in TD (fsd * 307.0 / 2048.0) (fsd * 1409.0 / 2048.0) (textWidth fs s)
97 |