Skip to content

Commit 6e29725

Browse files
Merge remote-tracking branch 'origin/master'
2 parents 308f6ac + 11bd018 commit 6e29725

File tree

2 files changed

+125
-297
lines changed

2 files changed

+125
-297
lines changed

scripts/GenerateNLVersion.kt

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import org.w3c.dom.Attr
2+
import org.w3c.dom.Document
3+
import org.w3c.dom.Node
4+
import org.w3c.dom.NodeList
5+
import java.io.File
6+
import java.io.FileOutputStream
7+
import java.util.concurrent.TimeUnit
8+
import javax.xml.parsers.DocumentBuilderFactory
9+
import javax.xml.transform.TransformerFactory
10+
import javax.xml.transform.dom.DOMSource
11+
import javax.xml.transform.stream.StreamResult
12+
import javax.xml.xpath.XPathConstants
13+
import javax.xml.xpath.XPathFactory
14+
15+
/**
16+
* The scripts generates no ligature version of JetBrains Mono called JetBrains Mono NL
17+
*
18+
* ttx command is required to run this script
19+
*
20+
* @author Konstantin Bulenkov
21+
*/
22+
@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
23+
fun main() {
24+
File("./ttf/")
25+
.listFiles { _, name -> name.endsWith(".ttf") && !name.startsWith("JetBrainsMonoNL") }
26+
.forEach {
27+
val ttx = it.nameWithoutExtension + ".ttx"
28+
val dir = it.parentFile
29+
File(dir, ttx).deleteAndLog()
30+
val doc = ttf2Document(it)
31+
File(dir, ttx).deleteAndLog()
32+
if (doc != null) {
33+
generateNoLigaturesFont(File(dir, it.name), doc)
34+
}
35+
}
36+
}
37+
38+
fun ttf2Document(file: File): Document? {
39+
"ttx ${file.name}".runCommand(file.parentFile)
40+
val ttx = file.parentFile.listFiles { _, name -> name == "${file.nameWithoutExtension}.ttx" }?.first() ?: return null
41+
val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder()
42+
return documentBuilder.parse(ttx)
43+
}
44+
45+
fun generateNoLigaturesFont(file: File, doc: Document) {
46+
val nlName = file.nameWithoutExtension.replace("JetBrainsMono", "JetBrainsMonoNL")
47+
val ttx = "$nlName.ttx"
48+
val ttf = "$nlName.ttf"
49+
val dir = File(file.parentFile, "No ligatures")
50+
File(dir, ttf).deleteAndLog()
51+
doc.removeLigas("/ttFont/GlyphOrder", "GlyphID")
52+
doc.removeLigas("/ttFont/glyf", "TTGlyph")
53+
doc.removeLigas("/ttFont/hmtx", "mtx")
54+
doc.removeLigas("/ttFont/post/extraNames", "psName")
55+
doc.removeLigas("/ttFont/GDEF/GlyphClassDef", "ClassDef", attName = "glyph")
56+
doc.removeNode("/ttFont/GPOS")
57+
doc.removeNode("/ttFont/GSUB")
58+
59+
val xPath = XPathFactory.newInstance().newXPath()
60+
val nameRecords = (xPath.evaluate("/ttFont/name/namerecord", doc, XPathConstants.NODESET) as NodeList).asList()
61+
nameRecords.forEach {
62+
if (!it.textContent.contains("trademark")) {
63+
it.textContent = it.textContent
64+
.replace("JetBrains Mono", "JetBrains Mono NL")
65+
.replace("JetBrainsMono", "JetBrainsMonoNL")
66+
}
67+
}
68+
69+
val ttxFile = File(dir, ttx)
70+
doc.saveAs(ttxFile)
71+
"ttx $ttx".runCommand(dir)
72+
ttxFile.deleteAndLog()
73+
}
74+
75+
class NodeListWrapper(val nodeList: NodeList) : AbstractList<Node>(), RandomAccess {
76+
override val size: Int
77+
get() = nodeList.length
78+
79+
override fun get(index: Int): Node = nodeList.item(index)
80+
}
81+
82+
////////////////////// Utility functions and data classes //////////////////////
83+
84+
fun NodeList.asList(): List<Node> = NodeListWrapper(this)
85+
86+
fun String.runCommand(workingDir: File) {
87+
ProcessBuilder(*split(" ").toTypedArray())
88+
.directory(workingDir)
89+
.redirectOutput(ProcessBuilder.Redirect.INHERIT)
90+
.redirectError(ProcessBuilder.Redirect.INHERIT)
91+
.start()
92+
.waitFor(1, TimeUnit.MINUTES)
93+
}
94+
95+
fun Document.saveAs(file: File) {
96+
val transformer = TransformerFactory.newInstance().newTransformer()
97+
transformer.transform(DOMSource(this), StreamResult(FileOutputStream(file)))
98+
}
99+
100+
fun Document.removeLigas(parentPath: String, nodeName: String, attName:String = "name") {
101+
val xPath = XPathFactory.newInstance().newXPath()
102+
val parent = xPath.evaluate(parentPath, this, XPathConstants.NODE) as Node
103+
val nodeFilter = "$parentPath/$nodeName[substring(@$attName, string-length(@$attName)-4) = '.liga']"
104+
val nodes = (xPath.evaluate(nodeFilter, this, XPathConstants.NODESET) as NodeList).asList()
105+
nodes.forEach { parent.removeChild(it) }
106+
}
107+
108+
fun Document.removeNode(path: String) {
109+
val xPath = XPathFactory.newInstance().newXPath()
110+
val parent = xPath.evaluate(path.substringBeforeLast("/"), this, XPathConstants.NODE)
111+
if (parent is Node) {
112+
val child = xPath.evaluate(path, this, XPathConstants.NODE)
113+
if (child is Node) {
114+
parent.removeChild(child)
115+
}
116+
}
117+
}
118+
119+
fun File.deleteAndLog() {
120+
if (!exists()) return
121+
println("Deleting $absolutePath")
122+
val result = delete()
123+
println("[$result]".toUpperCase())
124+
if (!result) deleteOnExit()
125+
}

0 commit comments

Comments
 (0)