diff --git a/scripts/GenerateNLVersion.kt b/scripts/GenerateNLVersion.kt new file mode 100644 index 00000000..f1bc11ea --- /dev/null +++ b/scripts/GenerateNLVersion.kt @@ -0,0 +1,125 @@ +import org.w3c.dom.Attr +import org.w3c.dom.Document +import org.w3c.dom.Node +import org.w3c.dom.NodeList +import java.io.File +import java.io.FileOutputStream +import java.util.concurrent.TimeUnit +import javax.xml.parsers.DocumentBuilderFactory +import javax.xml.transform.TransformerFactory +import javax.xml.transform.dom.DOMSource +import javax.xml.transform.stream.StreamResult +import javax.xml.xpath.XPathConstants +import javax.xml.xpath.XPathFactory + +/** + * The scripts generates no ligature version of JetBrains Mono called JetBrains Mono NL + * + * ttx command is required to run this script + * + * @author Konstantin Bulenkov + */ +@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") +fun main() { + File("./ttf/") + .listFiles { _, name -> name.endsWith(".ttf") && !name.startsWith("JetBrainsMonoNL") } + .forEach { + val ttx = it.nameWithoutExtension + ".ttx" + val dir = it.parentFile + File(dir, ttx).deleteAndLog() + val doc = ttf2Document(it) + File(dir, ttx).deleteAndLog() + if (doc != null) { + generateNoLigaturesFont(File(dir, it.name), doc) + } + } +} + +fun ttf2Document(file: File): Document? { + "ttx ${file.name}".runCommand(file.parentFile) + val ttx = file.parentFile.listFiles { _, name -> name == "${file.nameWithoutExtension}.ttx" }?.first() ?: return null + val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() + return documentBuilder.parse(ttx) +} + +fun generateNoLigaturesFont(file: File, doc: Document) { + val nlName = file.nameWithoutExtension.replace("JetBrainsMono", "JetBrainsMonoNL") + val ttx = "$nlName.ttx" + val ttf = "$nlName.ttf" + val dir = File(file.parentFile, "No ligatures") + File(dir, ttf).deleteAndLog() + doc.removeLigas("/ttFont/GlyphOrder", "GlyphID") + doc.removeLigas("/ttFont/glyf", "TTGlyph") + doc.removeLigas("/ttFont/hmtx", "mtx") + doc.removeLigas("/ttFont/post/extraNames", "psName") + doc.removeLigas("/ttFont/GDEF/GlyphClassDef", "ClassDef", attName = "glyph") + doc.removeNode("/ttFont/GPOS") + doc.removeNode("/ttFont/GSUB") + + val xPath = XPathFactory.newInstance().newXPath() + val nameRecords = (xPath.evaluate("/ttFont/name/namerecord", doc, XPathConstants.NODESET) as NodeList).asList() + nameRecords.forEach { + if (!it.textContent.contains("trademark")) { + it.textContent = it.textContent + .replace("JetBrains Mono", "JetBrains Mono NL") + .replace("JetBrainsMono", "JetBrainsMonoNL") + } + } + + val ttxFile = File(dir, ttx) + doc.saveAs(ttxFile) + "ttx $ttx".runCommand(dir) + ttxFile.deleteAndLog() +} + +class NodeListWrapper(val nodeList: NodeList) : AbstractList(), RandomAccess { + override val size: Int + get() = nodeList.length + + override fun get(index: Int): Node = nodeList.item(index) +} + +////////////////////// Utility functions and data classes ////////////////////// + +fun NodeList.asList(): List = NodeListWrapper(this) + +fun String.runCommand(workingDir: File) { + ProcessBuilder(*split(" ").toTypedArray()) + .directory(workingDir) + .redirectOutput(ProcessBuilder.Redirect.INHERIT) + .redirectError(ProcessBuilder.Redirect.INHERIT) + .start() + .waitFor(1, TimeUnit.MINUTES) +} + +fun Document.saveAs(file: File) { + val transformer = TransformerFactory.newInstance().newTransformer() + transformer.transform(DOMSource(this), StreamResult(FileOutputStream(file))) +} + +fun Document.removeLigas(parentPath: String, nodeName: String, attName:String = "name") { + val xPath = XPathFactory.newInstance().newXPath() + val parent = xPath.evaluate(parentPath, this, XPathConstants.NODE) as Node + val nodeFilter = "$parentPath/$nodeName[substring(@$attName, string-length(@$attName)-4) = '.liga']" + val nodes = (xPath.evaluate(nodeFilter, this, XPathConstants.NODESET) as NodeList).asList() + nodes.forEach { parent.removeChild(it) } +} + +fun Document.removeNode(path: String) { + val xPath = XPathFactory.newInstance().newXPath() + val parent = xPath.evaluate(path.substringBeforeLast("/"), this, XPathConstants.NODE) + if (parent is Node) { + val child = xPath.evaluate(path, this, XPathConstants.NODE) + if (child is Node) { + parent.removeChild(child) + } + } +} + +fun File.deleteAndLog() { + if (!exists()) return + println("Deleting $absolutePath") + val result = delete() + println("[$result]".toUpperCase()) + if (!result) deleteOnExit() +} \ No newline at end of file diff --git a/scripts/UpdateMetaData.kt b/scripts/UpdateMetaData.kt deleted file mode 100644 index b291fb1f..00000000 --- a/scripts/UpdateMetaData.kt +++ /dev/null @@ -1,297 +0,0 @@ -import org.w3c.dom.Attr -import org.w3c.dom.Document -import org.w3c.dom.Node -import org.w3c.dom.NodeList -import java.io.File -import java.io.FileOutputStream -import java.util.concurrent.TimeUnit -import javax.xml.parsers.DocumentBuilderFactory -import javax.xml.transform.TransformerFactory -import javax.xml.transform.dom.DOMSource -import javax.xml.transform.stream.StreamResult -import javax.xml.xpath.XPathConstants -import javax.xml.xpath.XPathFactory - -/** - * The script updates ttf metadata and fixes fields that are not possible to fix using FontLab. - * These fixes include: - * - * 1. Update PostScript and PONOSE metadata to make the font monospaced on Windows - * 2. Proper names for Medium and ExtraBold - * - * ttx command is required to run this script - * - * @author Konstantin Bulenkov - */ -@Suppress("RECEIVER_NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") -fun main() { - File("./ttf/") - .listFiles { _, name -> name.endsWith(".ttf") && !name.startsWith("JetBrainsMonoNL") } - .forEach { - val ttx = it.nameWithoutExtension + ".ttx" - val dir = it.parentFile - File(dir, ttx).deleteAndLog() - val doc = updateMetaData(it) - it.deleteAndLog() - "ttx $ttx".runCommand(dir) - File(dir, ttx).deleteAndLog() - if (doc != null) { - generateNoLigaturesFont(File(dir, it.name), doc) - } - } -} - -fun updateMetaData(file: File): Document? { - "ttx ${file.name}".runCommand(file.parentFile) - val ttx = file.parentFile.listFiles { _, name -> name == file.nameWithoutExtension + ".ttx" }?.first() - if (ttx == null) return null - val documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() - val doc = documentBuilder.parse(ttx) - updateMonospaceFlags(file, doc) - updateNameRecords(file, doc) - doc.saveAs(ttx) - return doc -} - -fun updateNameRecords(file: File, doc: Document) { - val xPath = XPathFactory.newInstance().newXPath() - val nameRecords = NAME_RECORDS[file.name] - if (nameRecords != null) { - nameRecords.forEach { - val nameID = it.nameID - val platformID = it.platformID - val path = "/ttFont/name/namerecord[@nameID='$nameID'][@platformID='$platformID']" - val result = xPath.evaluate(path, doc, XPathConstants.NODE) - val nameRecord: Node - if (result == null) { - val nameNode = xPath.evaluate("/ttFont/name", doc, XPathConstants.NODE) as Node - nameRecord = doc.createElement("namerecord") as Node - nameNode.appendChild(nameRecord) - val nameIDAttr = doc.createAttribute("nameID") - val platformIDAttr = doc.createAttribute("platformID") - val langIDAttr = doc.createAttribute("langID") - val platEncIDAttr = doc.createAttribute("platEncID") - nameIDAttr.value = nameID.toString() - platformIDAttr.value = platformID.toString() - langIDAttr.value = "0x409" - platEncIDAttr.value = "1" - nameRecord.attributes.setNamedItem(nameIDAttr) - nameRecord.attributes.setNamedItem(platformIDAttr) - nameRecord.attributes.setNamedItem(langIDAttr) - nameRecord.attributes.setNamedItem(platEncIDAttr) - } else { - nameRecord = result as Node - } - nameRecord.textContent = it.value - } - } -} - -fun generateNoLigaturesFont(file: File, doc: Document) { - val nlName = file.nameWithoutExtension.replace("JetBrainsMono", "JetBrainsMonoNL") - val ttx = nlName + ".ttx" - val ttf = nlName + ".ttf" - val dir = file.parentFile - File(dir, ttf).deleteAndLog() - doc.removeLigas("/ttFont/GlyphOrder", "GlyphID") - doc.removeLigas("/ttFont/glyf", "TTGlyph") - doc.removeLigas("/ttFont/hmtx", "mtx") - doc.removeLigas("/ttFont/post/extraNames", "psName") - doc.removeLigas("/ttFont/GDEF/GlyphClassDef", "ClassDef", attName = "glyph") - doc.removeNode("/ttFont/GPOS") - doc.removeNode("/ttFont/GSUB") - - val xPath = XPathFactory.newInstance().newXPath() - val nameRecords = (xPath.evaluate("/ttFont/name/namerecord", doc, XPathConstants.NODESET) as NodeList).asList() - nameRecords.forEach { - if (!it.textContent.contains("trademark")) { - it.textContent = it.textContent - .replace("JetBrains Mono", "JetBrains Mono NL") - .replace("JetBrainsMono", "JetBrainsMonoNL") - } - } - - val ttxFile = File(dir, ttx) - doc.saveAs(ttxFile) - "ttx $ttx".runCommand(dir) - ttxFile.deleteAndLog() -} - -class NodeListWrapper(val nodeList: NodeList) : AbstractList(), RandomAccess { - override val size: Int - get() = nodeList.length - - override fun get(index: Int): Node = nodeList.item(index) -} - -private fun updateMonospaceFlags(file: File, doc: Document) { - val panose = PANOSE_TABLE[file.name] - if (panose != null) { - val xPath = XPathFactory.newInstance().newXPath() - panose.javaClass.declaredFields.forEach { - val node = xPath.evaluate("/ttFont/OS_2/panose/${it.name}", doc, XPathConstants.NODE) as Node - it.isAccessible = true - (node.attributes.item(0) as Attr).value = it.getInt(panose).toString() - } - } -} - -////////////////////// Utility functions and data classes ////////////////////// - -fun NodeList.asList(): List = NodeListWrapper(this) - -fun String.runCommand(workingDir: File) { - ProcessBuilder(*split(" ").toTypedArray()) - .directory(workingDir) - .redirectOutput(ProcessBuilder.Redirect.INHERIT) - .redirectError(ProcessBuilder.Redirect.INHERIT) - .start() - .waitFor(1, TimeUnit.MINUTES) -} - -fun Document.saveAs(file: File) { - val transformer = TransformerFactory.newInstance().newTransformer() - transformer.transform(DOMSource(this), StreamResult(FileOutputStream(file))) -} - -fun Document.removeLigas(parentPath: String, nodeName: String, attName:String = "name") { - val xPath = XPathFactory.newInstance().newXPath() - val parent = xPath.evaluate(parentPath, this, XPathConstants.NODE) as Node - val nodeFilter = "$parentPath/$nodeName[substring(@$attName, string-length(@$attName)-4) = '.liga']" - val nodes = (xPath.evaluate(nodeFilter, this, XPathConstants.NODESET) as NodeList).asList() - nodes.forEach { parent.removeChild(it) } -} - -fun Document.removeNode(path: String) { - val xPath = XPathFactory.newInstance().newXPath() - val parent = xPath.evaluate(path.substringBeforeLast("/"), this, XPathConstants.NODE) as Node - val child = xPath.evaluate(path, this, XPathConstants.NODE) as Node - parent.removeChild(child) -} - -fun File.deleteAndLog() { - if (!exists()) return - println("Deleting $absolutePath") - val result = delete() - println("[$result]".toUpperCase()) - if (!result) deleteOnExit() -} - -data class NameRecord(val nameID: Int, - val platformID: Int, - val value: String); - -data class PANOSE(val bFamilyType: Int = 2, - val bSerifStyle: Int = 11, - val bWeight: Int, - val bProportion: Int = 9, - val bContrast: Int, - val bStrokeVariation: Int = 1, - val bArmStyle: Int = 2, - val bLetterForm: Int = 5, - val bMidline: Int = 0, - val bXHeight: Int = 4) - -////////////////////// Font data ////////////////////// - -val PANOSE_TABLE = mapOf( - "JetBrainsMono-Regular.ttf" to PANOSE(bWeight = 5, bContrast = 2), - "JetBrainsMono-Italic.ttf" to PANOSE(bWeight = 5, bContrast = 2), - "JetBrainsMono-Medium.ttf" to PANOSE(bWeight = 6, bContrast = 2), - "JetBrainsMono-Medium-Italic.ttf" to PANOSE(bWeight = 6, bContrast = 2), - "JetBrainsMono-Bold.ttf" to PANOSE(bWeight = 8, bContrast = 3), - "JetBrainsMono-Bold-Italic.ttf" to PANOSE(bWeight = 8, bContrast = 3), - "JetBrainsMono-ExtraBold.ttf" to PANOSE(bWeight = 9, bContrast = 3), - "JetBrainsMono-ExtraBold-Italic.ttf" to PANOSE(bWeight = 9, bContrast = 3) -) - -val NAME_RECORDS = mapOf>( - "JetBrainsMono-Regular.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono"), - NameRecord(2, 3, "Regular"), - NameRecord(4, 3, "JetBrains Mono Regular"), - NameRecord(6, 3, "JetBrainsMono-Regular"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "Regular"), - NameRecord(4, 1, "JetBrains Mono Regular"), - NameRecord(6, 1, "JetBrainsMono-Regular")), - - "JetBrainsMono-Italic.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono"), - NameRecord(2, 3, "Italic"), - NameRecord(4, 3, "JetBrains Mono Italic"), - NameRecord(6, 3, "JetBrainsMono-Italic"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "Italic"), - NameRecord(4, 1, "JetBrains Mono Italic"), - NameRecord(6, 1, "JetBrainsMono-Italic")), - - "JetBrainsMono-Bold.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono"), - NameRecord(2, 3, "Bold"), - NameRecord(4, 3, "JetBrains Mono Bold"), - NameRecord(6, 3, "JetBrainsMono-Bold"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "Bold"), - NameRecord(4, 1, "JetBrains Mono Bold"), - NameRecord(6, 1, "JetBrainsMono-Bold")), - - "JetBrainsMono-Bold-Italic.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono"), - NameRecord(2, 3, "Bold Italic"), - NameRecord(4, 3, "JetBrains Mono Bold Italic"), - NameRecord(6, 3, "JetBrainsMono-BoldItalic"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "Bold Italic"), - NameRecord(4, 1, "JetBrains Mono Bold Italic"), - NameRecord(6, 1, "JetBrainsMono-BoldItalic")), - - "JetBrainsMono-Medium.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono Medium"), - NameRecord(2, 3, "Regular"), - NameRecord(4, 3, "JetBrains Mono Medium"), - NameRecord(6, 3, "JetBrainsMono-Medium"), - NameRecord(16, 3, "JetBrains Mono "), - NameRecord(17, 3, "Medium"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "Medium"), - NameRecord(4, 1, "JetBrains Mono Medium"), - NameRecord(6, 1, "JetBrainsMono-Medium")), - - "JetBrainsMono-Medium-Italic.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono Medium"), - NameRecord(2, 3, "Italic"), - NameRecord(4, 3, "JetBrains Mono Medium Italic"), - NameRecord(6, 3, "JetBrainsMono-MediumItalic"), - NameRecord(16, 3, "JetBrains Mono "), - NameRecord(17, 3, "Medium Italic"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "Medium Italic"), - NameRecord(4, 1, "JetBrains Mono Medium Italic"), - NameRecord(6, 1, "JetBrainsMono-MediumItalic")), - - - "JetBrainsMono-ExtraBold.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono ExtraBold"), - NameRecord(2, 3, "Regular"), - NameRecord(4, 3, "JetBrains Mono ExtraBold"), - NameRecord(6, 3, "JetBrainsMono-ExtraBold"), - NameRecord(16, 3, "JetBrains Mono "), - NameRecord(17, 3, "ExtraBold"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "ExtraBold"), - NameRecord(4, 1, "JetBrains Mono ExtraBold"), - NameRecord(6, 1, "JetBrainsMono-ExtraBold")), - - "JetBrainsMono-ExtraBold-Italic.ttf" to listOf( - NameRecord(1, 3, "JetBrains Mono ExtraBold"), - NameRecord(2, 3, "Italic"), - NameRecord(4, 3, "JetBrains Mono ExtraBold Italic"), - NameRecord(6, 3, "JetBrainsMono-ExtraBoldItalic"), - NameRecord(16, 3, "JetBrains Mono "), - NameRecord(17, 3, "ExtraBold Italic"), - NameRecord(1, 1, "JetBrains Mono"), - NameRecord(2, 1, "ExtraBold Italic"), - NameRecord(4, 1, "JetBrains Mono ExtraBold Italic"), - NameRecord(6, 1, "JetBrainsMono-ExtraBoldItalic")) -)