| author | paulson <lp15@cam.ac.uk> | 
| Fri, 01 Nov 2024 14:10:52 +0000 | |
| changeset 81293 | 6f0cd46be030 | 
| parent 80225 | d9ff4296e3b7 | 
| permissions | -rw-r--r-- | 
| 69328 | 1 | /* Title: Pure/Tools/fontforge.scala | 
| 2 | Author: Makarius | |
| 3 | ||
| 69333 | 4 | Support for fontforge and its scripting language: see | 
| 5 | /usr/share/doc/fontforge/scripting.html e.g. on Ubuntu Linux installation. | |
| 69328 | 6 | */ | 
| 7 | ||
| 8 | package isabelle | |
| 9 | ||
| 10 | ||
| 11 | import java.util.Locale | |
| 12 | ||
| 13 | ||
| 75393 | 14 | object Fontforge {
 | 
| 69328 | 15 | /** scripting language **/ | 
| 16 | ||
| 17 | type Script = String | |
| 18 | ||
| 19 | ||
| 20 | /* concrete syntax */ | |
| 21 | ||
| 75393 | 22 |   def string(s: String): Script = {
 | 
| 69329 | 23 | def err(c: Char): Nothing = | 
| 71163 | 24 |       error("Bad character \\u" + String.format(Locale.ROOT, "%04x", Integer.valueOf(c)) +
 | 
| 69329 | 25 | " in fontforge string " + quote(s)) | 
| 69328 | 26 | |
| 69329 | 27 |     val q = if (s.contains('"')) '\'' else '"'
 | 
| 69328 | 28 | |
| 75393 | 29 |     def escape(c: Char): String = {
 | 
| 69329 | 30 | if (c == '\u0000' || c == '\r' || c == q) err(c) | 
| 69328 | 31 | else if (c == '\n') "\\n" | 
| 32 | else if (c == '\\') "\\\\" | |
| 33 | else c.toString | |
| 34 | } | |
| 35 | ||
| 36 |     if (s.nonEmpty && s(0) == '\\') err('\\')
 | |
| 71601 | 37 | s.iterator.map(escape).mkString(q.toString, "", q.toString) | 
| 69328 | 38 | } | 
| 39 | ||
| 69333 | 40 | def file_name(path: Path): Script = string(File.standard_path(path)) | 
| 69328 | 41 | |
| 69333 | 42 | |
| 43 | /* commands */ | |
| 44 | ||
| 69335 | 45 | def command_list(cmds: List[String]): Script = cat_lines(cmds) | 
| 46 | def commands(cmds: String*): Script = command_list(cmds.toList) | |
| 47 | ||
| 69333 | 48 | // fonts | 
| 49 |   def open(path: Path): Script = "Open(" + file_name(path) +")"
 | |
| 69335 | 50 | def close: Script = "Close()" | 
| 69333 | 51 |   def generate(path: Path): Script = "Generate(" + file_name(path) +")"
 | 
| 52 | ||
| 53 | // selection | |
| 54 | def select_all: Script = "SelectAll()" | |
| 55 | def select_none: Script = "SelectNone()" | |
| 56 | def select_invert: Script = "SelectInvert()" | |
| 69335 | 57 | def select(args: Seq[Int]): Script = | 
| 58 |     command_list(select_none :: args.toList.map(code => "SelectMoreSingletons(" + code + ")"))
 | |
| 59 | ||
| 69333 | 60 | def clear: Script = "Clear()" | 
| 61 | def copy: Script = "Copy()" | |
| 62 | def paste: Script = "Paste()" | |
| 63 | ||
| 64 | ||
| 65 | ||
| 66 | /** execute fontforge program **/ | |
| 69328 | 67 | |
| 80224 
db92e0b6a11a
clarified signature: prefer symbolic isabelle.Path over physical java.io.File;
 wenzelm parents: 
75394diff
changeset | 68 | def execute(script: Script, args: String = "", cwd: Path = Path.current): Process_Result = | 
| 75394 | 69 |     Isabelle_System.with_tmp_file("fontforge") { script_file =>
 | 
| 69328 | 70 | File.write(script_file, script) | 
| 71 |       Isabelle_System.bash(File.bash_path(Path.explode("$ISABELLE_FONTFORGE")) +
 | |
| 72 | " -lang=ff -script " + File.bash_path(script_file) + " " + args) | |
| 75394 | 73 | } | 
| 69333 | 74 | |
| 75393 | 75 |   def font_domain(path: Path, strict: Boolean = false): List[Int] = {
 | 
| 69335 | 76 | val script = | 
| 77 | commands( | |
| 78 | open(path), | |
| 79 | "i = 0", | |
| 69370 | 80 | "while (i < CharCnt() && i < 0x110000)", | 
| 69335 | 81 |         "  if (" + (if (strict) "DrawsSomething" else "WorthOutputting") + "(i)); Print(i); endif",
 | 
| 82 | " i = i + 1", | |
| 83 | "endloop", | |
| 84 | close) | |
| 85 | Library.trim_split_lines(execute(script).check.out).map(Value.Int.parse) | |
| 86 | } | |
| 87 | ||
| 88 | ||
| 89 | /* font names */ | |
| 90 | ||
| 91 | sealed case class Font_Names( | |
| 92 | fontname: String = "", | |
| 93 | familyname: String = "", | |
| 94 | fullname: String = "", | |
| 95 | weight: String = "", | |
| 96 | copyright: String = "", | |
| 75393 | 97 | fontversion: String = "" | 
| 98 |   ) {
 | |
| 69335 | 99 | override def toString: String = fontname | 
| 100 | ||
| 101 |     def ttf: Path = Path.explode(fontname).ext("ttf")
 | |
| 102 | ||
| 75393 | 103 |     def update(prefix: String = "", version: String = ""): Font_Names = {
 | 
| 69335 | 104 | val fontversion1 = proper_string(version) getOrElse fontversion | 
| 105 | if (prefix == "") copy(fontversion = fontversion1) | |
| 106 |       else {
 | |
| 107 | copy(fontname = prefix + fontname, | |
| 108 | familyname = prefix + " " + familyname, | |
| 109 | fullname = prefix + " " + fullname, | |
| 110 | weight = weight, | |
| 111 | copyright = copyright, | |
| 112 | fontversion = fontversion1) | |
| 113 | } | |
| 114 | } | |
| 115 | ||
| 116 | def set: Script = | |
| 71601 | 117 | List(fontname, familyname, fullname, weight, copyright, fontversion).map(string). | 
| 69335 | 118 |         mkString("SetFontNames(", ",", ")")
 | 
| 119 | } | |
| 120 | ||
| 75393 | 121 |   def font_names(path: Path): Font_Names = {
 | 
| 69335 | 122 | val script = | 
| 123 | commands( | |
| 124 | open(path), | |
| 125 | "Print($fontname)", | |
| 126 | "Print($familyname)", | |
| 127 | "Print($fullname)", | |
| 128 | "Print($weight)", | |
| 129 | "Print($fontversion)", | |
| 130 | "Print($copyright)", | |
| 131 | close) | |
| 132 |     Library.trim_split_lines(execute(script).check.out) match {
 | |
| 133 | case fontname :: familyname :: fullname :: weight :: fontversion :: copyright => | |
| 134 | Font_Names(fontname, familyname, fullname, weight, cat_lines(copyright), fontversion) | |
| 135 |       case res => error(cat_lines("Bad font names: " :: res))
 | |
| 136 | } | |
| 69333 | 137 | } | 
| 69328 | 138 | } |