diff options
Diffstat (limited to 'bin')
| -rw-r--r-- | bin/.eslintrc.js | 266 | ||||
| -rw-r--r-- | bin/bdf.js | 260 | ||||
| -rw-r--r-- | bin/bdf.py | 224 | ||||
| -rw-r--r-- | bin/bdfcheck.js | 455 | ||||
| -rw-r--r-- | bin/bdfcheck.py | 380 | ||||
| -rw-r--r-- | bin/bdfexp.js | 287 | ||||
| -rw-r--r-- | bin/bdfexp.py | 245 | ||||
| -rw-r--r-- | bin/bdftofnt.js | 109 | ||||
| -rw-r--r-- | bin/bdftofnt.py | 148 | ||||
| -rw-r--r-- | bin/bdftopsf.js | 65 | ||||
| -rw-r--r-- | bin/bdftopsf.py | 125 | ||||
| -rw-r--r-- | bin/bmpf.js | 159 | ||||
| -rw-r--r-- | bin/bmpf.py | 147 | ||||
| -rw-r--r-- | bin/fncli.js | 51 | ||||
| -rw-r--r-- | bin/fncli.py | 32 | ||||
| -rw-r--r-- | bin/fnio.js | 95 | ||||
| -rw-r--r-- | bin/fnio.py | 135 | ||||
| -rw-r--r-- | bin/fnutil.js | 74 | ||||
| -rw-r--r-- | bin/fnutil.py | 62 | ||||
| -rw-r--r-- | bin/otb1cli.js | 129 | ||||
| -rw-r--r-- | bin/otb1cli.py | 99 | ||||
| -rw-r--r-- | bin/otb1exp.js | 895 | ||||
| -rw-r--r-- | bin/otb1exp.py | 808 | ||||
| -rw-r--r-- | bin/otb1get.js | 706 | ||||
| -rw-r--r-- | bin/otb1get.py | 663 | ||||
| -rw-r--r-- | bin/ucstoany.js | 73 | ||||
| -rw-r--r-- | bin/ucstoany.py | 96 |
27 files changed, 5540 insertions, 1248 deletions
diff --git a/bin/.eslintrc.js b/bin/.eslintrc.js index 4bf8bba2435e..4be31d461279 100644 --- a/bin/.eslintrc.js +++ b/bin/.eslintrc.js @@ -1,135 +1,135 @@ module.exports = { - "env": { - "es6": true, - "node": true, - "browser": false - }, - "extends": "eslint:recommended", - "parserOptions": { - "sourceType": "module" - }, - "rules": { - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "warn", - "single" - ], - "semi": [ - "error", - "always" - ], - "curly": [ - "error", - "all" - ], - "brace-style": [ - "error", - "1tbs" - ], - "no-empty" : "warn", - "no-unused-vars" : "warn", - "no-console": "warn", - "consistent-return": "error", - "class-methods-use-this": "warn", - "eqeqeq": [ - "error", - "always", { - "null": "ignore" - } - ], - "no-alert": "warn", - "no-caller": "error", - "no-eval": "error", - "no-extend-native": "warn", - "no-implicit-coercion": "error", - "no-implied-eval": "error", - "no-invalid-this": "error", - "no-loop-func": "error", - "no-new-func": "warn", - "no-new-wrappers": "error", - "no-proto": "error", - "no-return-assign": "warn", - "no-return-await": "warn", - "no-script-url": "error", - "no-self-compare": "error", - "no-sequences": "error", - "no-throw-literal": "error", - "no-unmodified-loop-condition": "warn", - "no-unused-expressions": "warn", - "no-useless-return": "warn", - "no-warning-comments": "warn", - "prefer-promise-reject-errors": "warn", - "no-label-var": "error", - "no-shadow": [ - "warn", { - "builtinGlobals": true, - "hoist": "all" - } - ], - "no-shadow-restricted-names": "error", - "no-undefined": "error", - "no-use-before-define": "error", - "no-new-require": "error", - "no-path-concat": "error", - "camelcase": "error", - "comma-dangle": [ - "error", - "never" - ], - "eol-last": [ - "error", - "always" - ], - "func-call-spacing": "warn", - "lines-around-directive": [ - "warn", - "always" - ], - "max-params": [ - "warn", { - "max": 7 - } - ], - "max-statements-per-line": [ - "warn", { - "max": 1 - } - ], - "new-cap": [ - "error" - ], - "no-array-constructor": "warn", - "no-mixed-operators": [ - "error", { - "groups": [ - ["&", "|", "^", "~", "<<", ">>", ">>>"], - ["==", "!=", "===", "!==", ">", ">=", "<", "<="], - ["&&", "||"], - ["in", "instanceof"] - ], - "allowSamePrecedence": false - } - ], - "no-trailing-spaces": "warn", - "no-unneeded-ternary": "warn", - "no-whitespace-before-property": "error", - "operator-linebreak": "warn", - "semi-spacing": "warn", - "no-confusing-arrow": [ - "error", { - "allowParens": true - } - ], - "no-duplicate-imports": "warn", - "prefer-rest-params": "warn", - "prefer-spread": "warn", - "no-unsafe-negation": "warn" - } + 'env': { + 'es6': true, + 'node': true, + 'browser': false + }, + 'extends': 'eslint:recommended', + 'parserOptions': { + 'sourceType': 'module' + }, + 'rules': { + 'indent': [ + 'error', + 'tab' + ], + 'linebreak-style': [ + 'error', + 'unix' + ], + 'quotes': [ + 'warn', + 'single' + ], + 'semi': [ + 'error', + 'always' + ], + 'curly': [ + 'error', + 'all' + ], + 'brace-style': [ + 'error', + '1tbs' + ], + 'no-empty' : 'warn', + 'no-unused-vars' : 'warn', + 'no-console': 'warn', + 'consistent-return': 'error', + 'class-methods-use-this': 'warn', + 'eqeqeq': [ + 'error', + 'always', { + 'null': 'ignore' + } + ], + 'no-alert': 'warn', + 'no-caller': 'error', + 'no-eval': 'error', + 'no-extend-native': 'warn', + 'no-implicit-coercion': 'error', + 'no-implied-eval': 'error', + 'no-invalid-this': 'error', + 'no-loop-func': 'error', + 'no-new-func': 'warn', + 'no-new-wrappers': 'error', + 'no-proto': 'error', + 'no-return-assign': 'warn', + 'no-return-await': 'warn', + 'no-script-url': 'error', + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-throw-literal': 'error', + 'no-unmodified-loop-condition': 'warn', + 'no-unused-expressions': 'warn', + 'no-useless-return': 'warn', + 'no-warning-comments': 'warn', + 'prefer-promise-reject-errors': 'warn', + 'no-label-var': 'error', + 'no-shadow': [ + 'warn', { + 'builtinGlobals': true, + 'hoist': 'all' + } + ], + 'no-shadow-restricted-names': 'error', + 'no-undefined': 'error', + 'no-use-before-define': 'error', + 'no-new-require': 'error', + 'no-path-concat': 'error', + 'camelcase': 'error', + 'comma-dangle': [ + 'error', + 'never' + ], + 'eol-last': [ + 'error', + 'always' + ], + 'func-call-spacing': 'warn', + 'lines-around-directive': [ + 'warn', + 'always' + ], + 'max-params': [ + 'warn', { + 'max': 7 + } + ], + 'max-statements-per-line': [ + 'warn', { + 'max': 1 + } + ], + 'new-cap': [ + 'error' + ], + 'no-array-constructor': 'warn', + 'no-mixed-operators': [ + 'error', { + 'groups': [ + [ '&', '|', '^', '~', '<<', '>>', '>>>' ], + [ '==', '!=', '===', '!==', '>', '>=', '<', '<=' ], + [ '&&', '||' ], + [ 'in', 'instanceof' ] + ], + 'allowSamePrecedence': false + } + ], + 'no-trailing-spaces': 'warn', + 'no-unneeded-ternary': 'warn', + 'no-whitespace-before-property': 'error', + 'operator-linebreak': 'warn', + 'semi-spacing': 'warn', + 'no-confusing-arrow': [ + 'error', { + 'allowParens': true + } + ], + 'no-duplicate-imports': 'warn', + 'prefer-rest-params': 'warn', + 'prefer-spread': 'warn', + 'no-unsafe-negation': 'warn' + } }; diff --git a/bin/bdf.js b/bin/bdf.js index a43e10411bdc..eda562c8594c 100644 --- a/bin/bdf.js +++ b/bin/bdf.js @@ -1,25 +1,28 @@ -// -// Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ 'use strict'; const fnutil = require('./fnutil.js'); - -const WIDTH_MAX = 127; -const HEIGHT_MAX = 255; -const SWIDTH_MAX = 32000; +// -- Width -- +const DPARSE_LIMIT = 512; +const SPARSE_LIMIT = 32000; class Width { constructor(x, y) { @@ -27,23 +30,27 @@ class Width { this.y = y; } - static _parse(name, value, limitX, limitY) { + static parse(name, value, limit) { const words = fnutil.splitWords(name, value, 2); - return new Width(fnutil.parseDec(name + ' X', words[0], -limitX, limitX), - fnutil.parseDec(name + ' Y', words[1], -limitY, limitY)); + return new Width(fnutil.parseDec(name + '.x', words[0], -limit, limit), + fnutil.parseDec(name + '.y', words[1], -limit, limit)); } - static parseS(value) { - return Width._parse('SWIDTH', value, SWIDTH_MAX, SWIDTH_MAX); + static parseS(name, value) { + return Width.parse(name, value, SPARSE_LIMIT); } - static parseD(value) { - return Width._parse('DWIDTH', value, WIDTH_MAX, HEIGHT_MAX); + static parseD(name, value) { + return Width.parse(name, value, DPARSE_LIMIT); } -} + toString() { + return `${this.x} ${this.y}`; + } +} +// -- BBX -- class BBX { constructor(width, height, xoff, yoff) { this.width = width; @@ -55,10 +62,10 @@ class BBX { static parse(name, value) { const words = fnutil.splitWords(name, value, 4); - return new BBX(fnutil.parseDec('width', words[0], 1, WIDTH_MAX), - fnutil.parseDec('height', words[1], 1, HEIGHT_MAX), - fnutil.parseDec('bbxoff', words[2], -WIDTH_MAX, WIDTH_MAX), - fnutil.parseDec('bbyoff', words[3], -WIDTH_MAX, WIDTH_MAX)); + return new BBX(fnutil.parseDec(name + '.width', words[0], 1, DPARSE_LIMIT), + fnutil.parseDec(name + '.height', words[1], 1, DPARSE_LIMIT), + fnutil.parseDec(name + '.xoff', words[2], -DPARSE_LIMIT, DPARSE_LIMIT), + fnutil.parseDec(name + '.yoff', words[3], -DPARSE_LIMIT, DPARSE_LIMIT)); } rowSize() { @@ -70,93 +77,45 @@ class BBX { } } +// -- Props -- +function skipComments(line) { + return line.startsWith('COMMENT') ? null : line; +} -class Props { - constructor() { - this.names = []; - this.values = []; - } - - add(name, value) { - this.names.push(name); - this.values.push(value); - } - - clone() { - let props = new Props(); - - props.names = this.names.slice(); - props.values = this.values.slice(); - return props; - } - +class Props extends Map { forEach(callback) { - for (let index = 0; index < this.names.length; index++) { - callback(this.names[index], this.values[index]); - } + super.forEach((value, name) => callback(name, value)); } - get(name) { - return this.values[this.names.indexOf(name)]; + read(input, name, callback) { + return this.parse(input.readLines(skipComments), name, callback); } parse(line, name, callback) { - if (line === null || !line.startsWith(name)) { + if (line == null || !line.startsWith(name)) { throw new Error(name + ' expected'); } - let value = line.substring(name.length).trimLeft(); + const value = line.substring(name.length).trimLeft(); - this.add(name, value); + this.set(name, value); return callback == null ? value : callback(name, value); } - push(line) { - this.add('', line); - } - set(name, value) { - let index = this.names.indexOf(name); - - if (index !== -1) { - this.values[index] = value; - } else { - this.add(name, value); - } + super.set(name, value.toString()); } } - +// -- Base -- class Base { constructor() { this.props = new Props(); this.bbx = null; - this.finis = []; - } - - readFinish(input, endText) { - if (this.readNext(input, this.finis) !== endText) { - throw new Error(endText + ' expected'); - } - this.finis.push(endText); - } - - readNext(input, comout = this.props) { - return input.readLines(line => { - if (line.startsWith('COMMENT')) { - comout.push(line); - return null; - } - return line; - }); - } - - readProp(input, name, callback) { - return this.props.parse(this.readNext(input), name, callback); } } - +// -- Char -- class Char extends Base { constructor() { super(); @@ -166,25 +125,25 @@ class Char extends Base { this.data = null; } - static bitmap(data, rowSize) { - const bitmap = data.toString('hex').toUpperCase(); - const regex = new RegExp(`.{${rowSize << 1}}`, 'g'); + bitmap() { + const bitmap = this.data.toString('hex').toUpperCase(); + const regex = new RegExp(`.{${this.bbx.rowSize() << 1}}`, 'g'); return bitmap.replace(regex, '$&\n'); } _read(input) { // HEADER - this.readProp(input, 'STARTCHAR'); - this.code = this.readProp(input, 'ENCODING', fnutil.parseDec); - this.swidth = this.readProp(input, 'SWIDTH', (name, value) => Width.parseS(value)); - this.dwidth = this.readProp(input, 'DWIDTH', (name, value) => Width.parseD(value)); - this.bbx = this.readProp(input, 'BBX', BBX.parse); + this.props.read(input, 'STARTCHAR'); + this.code = this.props.read(input, 'ENCODING', fnutil.parseDec); + this.swidth = this.props.read(input, 'SWIDTH', Width.parseS); + this.dwidth = this.props.read(input, 'DWIDTH', Width.parseD); + this.bbx = this.props.read(input, 'BBX', BBX.parse); - let line = this.readNext(input); + let line = input.readLines(skipComments); - if (line !== null && line.startsWith('ATTRIBUTES')) { + if (line != null && line.startsWith('ATTRIBUTES')) { this.props.parse(line, 'ATTRIBUTES'); - line = this.readNext(input); + line = input.readLines(skipComments); } // BITMAP @@ -196,11 +155,14 @@ class Char extends Base { let bitmap = ''; for (let y = 0; y < this.bbx.height; y++) { - line = this.readNext(input); + line = input.readLines(skipComments); - if (line === null) { + if (line == null) { throw new Error('bitmap data expected'); } + if (line.match(/^[\dA-Fa-f]+$/) == null) { + throw new Error('invalid bitmap character(s)'); + } if (line.length === rowLen) { bitmap += line; } else { @@ -208,13 +170,11 @@ class Char extends Base { } } - // FINAL - this.readFinish(input, 'ENDCHAR'); + this.data = Buffer.from(bitmap, 'hex'); - if (bitmap.match(/^[\dA-Fa-f]+$/) != null) { - this.data = Buffer.from(bitmap, 'hex'); - } else { - throw new Error('invalid BITMAP data characters'); + // FINAL + if (input.readLines(skipComments) !== 'ENDCHAR') { + throw new Error('ENDCHAR expected'); } return this; } @@ -229,11 +189,11 @@ class Char extends Base { this.props.forEach((name, value) => { header += (name + ' ' + value).trim() + '\n'; }); - output.writeLine(header + Char.bitmap(this.data, this.bbx.rowSize()) + this.finis.join('\n')); + output.writeLine(header + this.bitmap() + 'ENDCHAR'); } } - +// -- Font -- const XLFD = { FOUNDRY: 1, FAMILY_NAME: 2, @@ -260,73 +220,70 @@ class Font extends Base { this.defaultCode = -1; } - getAscent() { - let ascent = this.props.get('FONT_ASCENT'); - - if (ascent != null) { - return fnutil.parseDec('FONT_ASCENT', ascent, -HEIGHT_MAX, HEIGHT_MAX); - } - return this.bbx.height + this.bbx.yoff; + get bold() { + return this.xlfd[XLFD.WEIGHT_NAME].toLowerCase().includes('bold'); } - getBold() { - return Number(this.xlfd[XLFD.WEIGHT_NAME].toLowerCase().includes('bold')); + get italic() { + return ['I', 'O'].indexOf(this.xlfd[XLFD.SLANT]) !== -1; } - getItalic() { - return Number(this.xlfd[XLFD.SLANT].match(/^[IO]/) != null); + get proportional() { + return this.xlfd[XLFD.SPACING] === 'P'; } _read(input) { // HEADER - let line = input.readLines(Font.skipEmpty); + let line = input.readLine(); if (this.props.parse(line, 'STARTFONT') !== '2.1') { throw new Error('STARTFONT 2.1 expected'); } - this.xlfd = this.readProp(input, 'FONT', (name, value) => value.split('-', 16)); + this.xlfd = this.props.read(input, 'FONT', (name, value) => value.split('-', 16)); if (this.xlfd.length !== 15 || this.xlfd[0] !== '') { throw new Error('non-XLFD font names are not supported'); } - this.readProp(input, 'SIZE'); - this.bbx = this.readProp(input, 'FONTBOUNDINGBOX', BBX.parse); - line = this.readNext(input); + this.props.read(input, 'SIZE'); + this.bbx = this.props.read(input, 'FONTBOUNDINGBOX', BBX.parse); + line = input.readLines(skipComments); - if (line !== null && line.startsWith('STARTPROPERTIES')) { + if (line != null && line.startsWith('STARTPROPERTIES')) { const numProps = this.props.parse(line, 'STARTPROPERTIES', fnutil.parseDec); for (let i = 0; i < numProps; i++) { - line = this.readNext(input); + line = input.readLines(skipComments); - if (line === null) { + if (line == null) { throw new Error('property expected'); } - let match = line.match(/^(\w+)\s+([-\d"].*)$/); + const match = line.match(/^(\w+)\s+([-\d"].*)$/); if (match == null) { throw new Error('invalid property format'); } - let name = match[1]; - let value = match[2]; + const name = match[1]; + const value = match[2]; + if (this.props.get(name) != null) { + throw new Error('duplicate property'); + } if (name === 'DEFAULT_CHAR') { this.defaultCode = fnutil.parseDec(name, value); } - - this.props.add(name, value); + this.props.set(name, value); } - if (this.readProp(input, 'ENDPROPERTIES') !== '') { + if (this.props.read(input, 'ENDPROPERTIES') !== '') { throw new Error('ENDPROPERTIES expected'); } - line = this.readNext(input); + line = input.readLines(skipComments); } // GLYPHS - const numChars = this.props.parse(line, 'CHARS', (name, value) => fnutil.parseDec(name, value, 1, CHARS_MAX)); + const numChars = fnutil.parseDec('CHARS', this.props.parse(line, 'CHARS'), 1, CHARS_MAX); for (let i = 0; i < numChars; i++) { this.chars.push(Char.read(input)); @@ -337,36 +294,35 @@ class Font extends Base { } // FINAL - this.readFinish(input, 'ENDFONT'); - - if (input.readLines(Font.skipEmpty) != null) { + if (input.readLines(skipComments) !== 'ENDFONT') { + throw new Error('ENDFONT expected'); + } + if (input.readLine() != null) { throw new Error('garbage after ENDFONT'); } return this; } static read(input) { - return (new Font())._read(input); - } - - static skipEmpty(line) { - return line.length > 0 ? line : null; + return (new Font())._read(input, false); } write(output) { this.props.forEach((name, value) => output.writeProp(name, value)); this.chars.forEach(char => char.write(output)); - output.writeLine(this.finis.join('\n')); + output.writeLine('ENDFONT'); } } - +// -- Export -- module.exports = Object.freeze({ - WIDTH_MAX, - HEIGHT_MAX, - SWIDTH_MAX, + DPARSE_LIMIT, + SPARSE_LIMIT, Width, BBX, + skipComments, + Props, + Base, Char, XLFD, CHARS_MAX, diff --git a/bin/bdf.py b/bin/bdf.py index 54a40ff02cec..7a6f7d87a2d1 100644 --- a/bin/bdf.py +++ b/bin/bdf.py @@ -1,27 +1,31 @@ # -# Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import re import codecs +from collections import OrderedDict from enum import IntEnum, unique import fnutil - -WIDTH_MAX = 127 -HEIGHT_MAX = 255 -SWIDTH_MAX = 32000 +# -- Width -- +DPARSE_LIMIT = 512 +SPARSE_LIMIT = 32000 class Width: def __init__(self, x, y): @@ -30,25 +34,27 @@ class Width: @staticmethod - def _parse(name, value, limit_x, limit_y): + def parse(name, value, limit): words = fnutil.split_words(name, value, 2) - return Width(fnutil.parse_dec('width x', words[0], -limit_x, limit_x), - fnutil.parse_dec('width y', words[1], -limit_y, limit_y)) + return Width(fnutil.parse_dec(name + '.x', words[0], -limit, limit), + fnutil.parse_dec(name + '.y', words[1], -limit, limit)) @staticmethod - def parse_s(value): - return Width._parse('SWIDTH', value, SWIDTH_MAX, SWIDTH_MAX) + def parse_s(name, value): + return Width.parse(name, value, SPARSE_LIMIT) @staticmethod - def parse_d(value): - return Width._parse('DWIDTH', value, WIDTH_MAX, HEIGHT_MAX) + def parse_d(name, value): + return Width.parse(name, value, DPARSE_LIMIT) -OFFSET_MIN = -128 -OFFSET_MAX = 127 + def __str__(self): + return '%d %d' % (self.x, self.y) + +# -- BXX -- class BBX: def __init__(self, width, height, xoff, yoff): self.width = width @@ -60,10 +66,10 @@ class BBX: @staticmethod def parse(name, value): words = fnutil.split_words(name, value, 4) - return BBX(fnutil.parse_dec('width', words[0], 1, WIDTH_MAX), - fnutil.parse_dec('height', words[1], 1, HEIGHT_MAX), - fnutil.parse_dec('bbxoff', words[2], -WIDTH_MAX, WIDTH_MAX), - fnutil.parse_dec('bbyoff', words[3], -WIDTH_MAX, WIDTH_MAX)) + return BBX(fnutil.parse_dec('width', words[0], 1, DPARSE_LIMIT), + fnutil.parse_dec('height', words[1], 1, DPARSE_LIMIT), + fnutil.parse_dec('bbxoff', words[2], -DPARSE_LIMIT, DPARSE_LIMIT), + fnutil.parse_dec('bbyoff', words[3], -DPARSE_LIMIT, DPARSE_LIMIT)) def row_size(self): @@ -74,48 +80,18 @@ class BBX: return '%d %d %d %d' % (self.width, self.height, self.xoff, self.yoff) -class Props: - def __init__(self): - self.names = [] - self.values = [] - - - def add(self, name, value): - self.names.append(name) - self.values.append(value) - - - def clone(self): - props = Props() - props.names = self.names[:] - props.values = self.values[:] - return props - - - def get(self, name): - try: - return self.values[self.names.index(name)] - except ValueError: - return None - - - class Iter: - def __init__(self, props): - self.index = 0 - self.props = props - +# -- Props -- +def skip_comments(line): + return None if line[:7] == b'COMMENT' else line - def __next__(self): - if self.index == len(self.props.names): - raise StopIteration - result = (self.props.names[self.index], self.props.values[self.index]) - self.index += 1 - return result +class Props(OrderedDict): + def __iter__(self): + return self.items().__iter__() - def __iter__(self): - return Props.Iter(self) + def read(self, input, name, callback=None): + return self.parse(input.read_lines(skip_comments), name, callback) def parse(self, line, name, callback=None): @@ -123,36 +99,23 @@ class Props: raise Exception(name + ' expected') value = line[len(name):].lstrip() - self.add(name, value) + self[name] = value return value if callback is None else callback(name, value) def set(self, name, value): - try: - self.values[self.names.index(name)] = value - except ValueError: - self.add(name, value) + self[name] = value if isinstance(value, (bytes, bytearray)) else bytes(str(value), 'ascii') +# -- Base -- class Base: def __init__(self): self.props = Props() self.bbx = None - self.finis = [] - - - def keep_comments(self, line): - if not line.startswith(b'COMMENT'): - return line - self.props.add('', line) - return None - - - def keep_finishes(self, line): - self.finis.append(line) - return None if line.startswith(b'COMMENT') else line +# -- Char +HEX_BYTES = (48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70) class Char(Base): def __init__(self): @@ -163,55 +126,51 @@ class Char(Base): self.data = None - @staticmethod - def bitmap(data, row_size): + def bitmap(self): bitmap = '' + row_size = self.bbx.row_size() - for index in range(0, len(data), row_size): - bitmap += data[index : index + row_size].hex() + '\n' + for index in range(0, len(self.data), row_size): + bitmap += self.data[index : index + row_size].hex() + '\n' return bytes(bitmap, 'ascii').upper() def _read(self, input): # HEADER - read_next = lambda: input.read_lines(lambda line: self.keep_comments(line)) - read_prop = lambda name, callback=None: self.props.parse(read_next(), name, callback) - - read_prop('STARTCHAR') - self.code = read_prop('ENCODING', fnutil.parse_dec) - self.swidth = read_prop('SWIDTH', lambda _, value: Width.parse_s(value)) - self.dwidth = read_prop('DWIDTH', lambda _, value: Width.parse_d(value)) - self.bbx = read_prop('BBX', BBX.parse) - line = read_next() + self.props.read(input, 'STARTCHAR') + self.code = self.props.read(input, 'ENCODING', fnutil.parse_dec) + self.swidth = self.props.read(input, 'SWIDTH', Width.parse_s) + self.dwidth = self.props.read(input, 'DWIDTH', Width.parse_d) + self.bbx = self.props.read(input, 'BBX', BBX.parse) + line = input.read_lines(skip_comments) if line and line.startswith(b'ATTRIBUTES'): self.props.parse(line, 'ATTRIBUTES') - line = read_next() + line = input.read_lines(skip_comments) # BITMAP - if self.props.parse(line, 'BITMAP') != b'': + if self.props.parse(line, 'BITMAP'): raise Exception('BITMAP expected') row_len = self.bbx.row_size() * 2 - bitmap = b'' + self.data = bytearray() for _ in range(0, self.bbx.height): - line = read_next() + line = input.read_lines(skip_comments) if not line: raise Exception('bitmap data expected') if len(line) == row_len: - bitmap += line + self.data += codecs.decode(line, 'hex') else: raise Exception('invalid bitmap length') # FINAL - if input.read_lines(lambda line: self.keep_finishes(line)) != b'ENDCHAR': + if input.read_lines(skip_comments) != b'ENDCHAR': raise Exception('ENDCHAR expected') - self.data = codecs.decode(bitmap, 'hex') # no spaces allowed return self @@ -224,9 +183,10 @@ class Char(Base): for [name, value] in self.props: output.write_prop(name, value) - output.write_line(Char.bitmap(self.data, self.bbx.row_size()) + b'\n'.join(self.finis)) + output.write_line(self.bitmap() + b'ENDCHAR') +# -- Font -- @unique class XLFD(IntEnum): FOUNDRY = 1 @@ -254,48 +214,44 @@ class Font(Base): self.default_code = -1 - def get_ascent(self): - ascent = self.props.get('FONT_ASCENT') - - if ascent is not None: - return fnutil.parse_dec('FONT_ASCENT', ascent, -HEIGHT_MAX, HEIGHT_MAX) + @property + def bold(self): + return b'bold' in self.xlfd[XLFD.WEIGHT_NAME].lower() - return self.bbx.height + self.bbx.yoff + @property + def italic(self): + return self.xlfd[XLFD.SLANT] in [b'I', b'O'] - def get_bold(self): - return int(b'bold' in self.xlfd[XLFD.WEIGHT_NAME].lower()) - - def get_italic(self): - return int(re.search(b'^[IO]', self.xlfd[XLFD.SLANT]) is not None) + @property + def proportional(self): + return self.xlfd[XLFD.SPACING] == b'P' def _read(self, input): # HEADER - read_next = lambda: input.read_lines(lambda line: self.keep_comments(line)) - read_prop = lambda name, callback=None: self.props.parse(read_next(), name, callback) - line = input.read_lines(Font.skip_empty) + line = input.read_line() if self.props.parse(line, 'STARTFONT') != b'2.1': raise Exception('STARTFONT 2.1 expected') - self.xlfd = read_prop('FONT', lambda name, value: value.split(b'-', 15)) + self.xlfd = self.props.read(input, 'FONT', lambda name, value: value.split(b'-', 15)) if len(self.xlfd) != 15 or self.xlfd[0] != b'': raise Exception('non-XLFD font names are not supported') - read_prop('SIZE') - self.bbx = read_prop('FONTBOUNDINGBOX', BBX.parse) - line = read_next() + self.props.read(input, 'SIZE') + self.bbx = self.props.read(input, 'FONTBOUNDINGBOX', BBX.parse) + line = input.read_lines(skip_comments) if line and line.startswith(b'STARTPROPERTIES'): num_props = self.props.parse(line, 'STARTPROPERTIES', fnutil.parse_dec) for _ in range(0, num_props): - line = read_next() + line = input.read_lines(skip_comments) - if not line: + if line is None: raise Exception('property expected') match = re.fullmatch(br'(\w+)\s+([-\d"].*)', line) @@ -306,18 +262,21 @@ class Font(Base): name = str(match.group(1), 'ascii') value = match.group(2) + if self.props.get(name) is not None: + raise Exception('duplicate property') + if name == 'DEFAULT_CHAR': self.default_code = fnutil.parse_dec(name, value) - self.props.add(name, value) + self.props[name] = value - if read_prop('ENDPROPERTIES') != b'': + if self.props.read(input, 'ENDPROPERTIES') != b'': raise Exception('ENDPROPERTIES expected') - line = read_next() + line = input.read_lines(skip_comments) # GLYPHS - num_chars = self.props.parse(line, 'CHARS', lambda name, value: fnutil.parse_dec(name, value, 1, CHARS_MAX)) + num_chars = fnutil.parse_dec('CHARS', self.props.parse(line, 'CHARS'), 1, CHARS_MAX) for _ in range(0, num_chars): self.chars.append(Char.read(input)) @@ -326,10 +285,10 @@ class Font(Base): raise Exception('invalid DEFAULT_CHAR') # FINAL - if input.read_lines(lambda line: self.keep_finishes(line)) != b'ENDFONT': + if input.read_lines(skip_comments) != b'ENDFONT': raise Exception('ENDFONT expected') - if input.read_lines(Font.skip_empty): + if input.read_line() is not None: raise Exception('garbage after ENDFONT') return self @@ -340,11 +299,6 @@ class Font(Base): return Font()._read(input) # pylint: disable=protected-access - @staticmethod - def skip_empty(line): - return line if line else None - - def write(self, output): for [name, value] in self.props: output.write_prop(name, value) @@ -352,4 +306,4 @@ class Font(Base): for char in self.chars: char.write(output) - output.write_line(b'\n'.join(self.finis)) + output.write_line(b'ENDFONT') diff --git a/bin/bdfcheck.js b/bin/bdfcheck.js new file mode 100644 index 000000000000..c62d24db5e41 --- /dev/null +++ b/bin/bdfcheck.js @@ -0,0 +1,455 @@ +/* + Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; + +const fnutil = require('./fnutil.js'); +const fncli = require('./fncli.js'); +const fnio = require('./fnio.js'); +const bdf = require('./bdf.js'); + +// -- Params -- +class Params extends fncli.Params { + constructor() { + super(); + this.asciiChars = true; + this.bbxExceeds = true; + this.duplCodes = -1; + this.extraBits = true; + this.attributes = true; + this.duplNames = -1; + this.duplProps = true; + this.commonSlant = true; + this.commonWeight = true; + this.xlfdFontNm = true; + this.yWidthZero = true; + } +} + +// -- Options -- +const HELP = ('' + + 'usage: bdfcheck [options] [INPUT...]\n' + + 'Check BDF font(s) for various problems\n' + + '\n' + + ' -A disable non-ascii characters check\n' + + ' -B disable BBX exceeding FONTBOUNDINGBOX checks\n' + + ' -c/-C enable/disable duplicate character codes check\n' + + ' (default = enabled for registry ISO10646)\n' + + ' -E disable extra bits check\n' + + ' -I disable ATTRIBUTES check\n' + + ' -n/-N enable duplicate character names check\n' + + ' (default = enabled for registry ISO10646)\n' + + ' -P disable duplicate properties check\n' + + ' -S disable common slant check\n' + + ' -W disable common weight check\n' + + ' -X disable XLFD font name check\n' + + ' -Y disable zero WIDTH Y check\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'File directives: COMMENT bdfcheck --enable|disable-<check-name>\n' + + ' (also available as long command line options)\n' + + '\n' + + 'Check names: ascii-chars, bbx-exceeds, duplicate-codes, extra-bits,\n' + + ' attributes, duplicate-names, duplicate-properties, common-slant,\n' + + ' common-weight, xlfd-font, ywidth-zero\n' + + '\n' + + 'The input BDF(s) must be v2.1 with unicode encoding.\n'); + +const VERSION = 'bdfcheck 1.61, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE; + +class Options extends fncli.Options { + constructor() { + super([], HELP, VERSION); + } + + parse(name, directive, params) { + const value = name.startsWith('--enable') || name[1].match('[a-z]'); + + switch (name) { + case '-A': + case '--enable-ascii-chars': + case '--disable-ascii-chars': + params.asciiChars = value; + break; + case '-B': + case '--enable-bbx-exceeds': + case '--disable-bbx-exceeds': + params.bbxExceeds = value; + break; + case '-c': + case '-C': + case '--enable-duplicate-codes': + case '--disable-duplicate-codes': + params.duplCodes = value; + break; + case '-E': + case '--enable-extra-bits': + case '--disable-extra-bits': + params.extraBits = value; + break; + case '-I': + case '--enable-attributes': + case '--disable-attributes': + params.attributes = value; + break; + case '-n': + case '-N': + case '--enable-duplicate-names': + case '--disable-duplicate-names': + params.duplNames = value; + break; + case '-P': + case '--enable-duplicate-properties': + case '--disable-duplicate-properties': + params.duplProps = value; + break; + case '-S': + case '--enable-common-slant': + case '--disable-common-slant': + params.commonSlant = value; + break; + case '-W': + case '--enable-common-weight': + case '--disable-common-weight': + params.commonWeight = value; + break; + case '-X': + case '--enable-xlfd-font': + case '--disable-xlfd-font': + params.xlfdFontNm = value; + break; + case '-Y': + case '--enable-ywidth-zero': + case '--disable-ywidth-zero': + params.yWidthZero = value; + break; + default: + return directive !== true && this.fallback(name, params); + } + + return directive !== true || name.startsWith('--'); + } +} + +// -- DupMap -- +class DupMap extends Map { + constructor(prefix, descript, severity) { + super(); + this.prefix = prefix; + this.descript = descript; + this.severity = severity; + } + + check() { + this.forEach((lines, value) => { + if (lines.length > 1) { + let text = `duplicate ${this.descript} ${value} at lines`; + + for (let index = 0; index < lines.length; index++) { + text += (index === 0 ? ' ' : index === lines.length - 1 ? ' and ' : ', '); + text += lines[index]; + } + fnutil.message(this.prefix, this.severity, text); + } + }); + } + + push(value, lineNo) { + let lines = this.get(value); + + if (lines != null) { + lines.push(lineNo); + } else { + this.set(value, [lineNo]); + } + } +} + +// -- InputFileStream -- +const MODE = Object.freeze({ + META: 0, + PROPS: 1, + BITMAP: 2 +}); + +class InputFileStream extends fnio.InputFileStream { + constructor(fileName, parsed) { + super(fileName); + this.parsed = parsed; + this.mode = MODE.META; + this.proplocs = new DupMap(this.location(), 'property'); + this.namelocs = new DupMap(this.location(), 'character name', 'warning'); + this.codelocs = new DupMap(this.location(), 'encoding', 'warning'); + this.HANDLERS = [ + [ 'STARTCHAR', value => this.appendName(value) ], + [ 'ENCODING', value => this.appendCode(value) ], + [ 'SWIDTH', value => this.checkWidth('SWIDTH', value, bdf.Width.parseS) ], + [ 'DWIDTH', value => this.checkWidth('DWIDTH', value, bdf.Width.parseD) ], + [ 'BBX', value => this.setLastBox(value) ], + [ 'BITMAP', () => this.setMode(MODE.BITMAP) ], + [ 'SIZE', InputFileStream.checkSize ], + [ 'ATTRIBUTES', value => this.checkAttr(value) ], + [ 'STARTPROPERTIES', () => this.setMode(MODE.PROPS) ], + [ 'FONTBOUNDINGBOX', value => this.setFontBox(value) ] + ]; + this.xlfdName = false; + this.lastBox = null; + this.fontBox = null; + this.options = new Options(); + } + + append(option, valocs, value) { + if (option) { + valocs.push(value, this.lineNo); + } + } + + appendCode(value) { + fnutil.parseDec('encoding', value); + this.append(this.parsed.duplCodes, this.codelocs, value); + } + + appendName(value) { + this.append(this.parsed.duplNames, this.namelocs, `"${value}"`); + } + + checkWidth(name, value, parse) { + if (this.parsed.yWidthZero && parse(name, value).y !== 0) { + fnutil.warning(this.location(), `non-zero ${name} Y`); + } + } + + setFontBox(value) { + this.fontBox = bdf.BBX.parse('FONTBOUNDINGBOX', value); + } + + setLastBox(value) { + const bbx = bdf.BBX.parse('BBX', value); + + if (this.parsed.bbxExceeds) { + let exceeds = []; + + if (bbx.xoff < this.fontBox.xoff) { + exceeds.push('xoff < FONTBOUNDINGBOX xoff'); + } + if (bbx.yoff < this.fontBox.yoff) { + exceeds.push('yoff < FONTBOUNDINGBOX yoff'); + } + if (bbx.width > this.fontBox.width) { + exceeds.push('width > FONTBOUNDINGBOX width'); + } + if (bbx.height > this.fontBox.height) { + exceeds.push('height > FONTBOUNDINGBOX height'); + } + exceeds.forEach(exceed => { + fnutil.message(this.location(), '', exceed); + }); + } + this.lastBox = bbx; + } + + setMode(newMode) { + this.mode = newMode; + } + + static checkSize(value) { + const words = fnutil.splitWords('SIZE', value, 3); + + fnutil.parseDec('point size', words[0], 1, null); + fnutil.parseDec('x resolution', words[1], 1, null); + fnutil.parseDec('y resolution', words[2], 1, null); + } + + checkAttr(value) { + if (!value.match(/^[\dA-Fa-f]{4}$/)) { + throw new Error('ATTRIBUTES must be 4 hex-encoded characters'); + } + if (this.parsed.attributes) { + fnutil.warning(this.location(), 'ATTRIBUTES may cause problems with freetype'); + } + } + + checkFont(value) { + const xlfd = value.substring(4).trimLeft().split('-', 16); + + if (xlfd.length === 15 && xlfd[0] === '') { + let unicode = (xlfd[bdf.XLFD.CHARSET_REGISTRY].toUpperCase() === 'ISO10646'); + + if (this.parsed.duplCodes === -1) { + this.parsed.duplCodes = unicode; + } + if (this.parsed.duplNames === -1) { + this.parsed.duplNames = unicode; + } + + if (this.parsed.commonWeight) { + let weight = xlfd[bdf.XLFD.WEIGHT_NAME]; + let compare = weight.toLowerCase(); + let consider = compare.includes('bold') ? 'Bold' : 'Normal'; + + if (compare === 'medium' || compare === 'regular') { + compare = 'normal'; + } + if (compare !== consider.toLowerCase()) { + fnutil.warning(this.location(), `weight "${weight}" may be considered ${consider}`); + } + } + + if (this.parsed.commonSlant) { + let slant = xlfd[bdf.XLFD.SLANT]; + let consider = slant.match(/^[IO]/) ? 'Italic' : 'Regular'; + + if (slant.match(/^[IOR]$/) == null) { + fnutil.warning(this.location(), `slant "${slant}" may be considered ${consider}`); + } + } + } else { + if (this.parsed.xlfdFontNm) { + fnutil.warning(this.location(), 'non-XLFD font name'); + } + value = 'FONT --------------'; + } + + return value; + } + + checkProp(line) { + const match = line.match(/^(\w+)\s+([-\d"].*)$/); + + if (match == null) { + throw new Error('invalid property format'); + } + + const name = match[1]; + const value = match[2]; + + if (value.startsWith('"')) { + if (value.length < 2 || !value.endsWith('"')) { + throw new Error('no closing double quote'); + } + if (value.substring(1, value.length - 1).match(/[^"]"[^"]/)) { + throw new Error('unescaped double quote'); + } + } else { + fnutil.parseDec('value', value, null, null); + } + + this.append(this.parsed.duplProps, this.proplocs, name); + return `P${this.lineNo} 1`; + } + + checkBitmap(line) { + if (line.length !== this.lastBox.rowSize() * 2) { + throw new Error('invalid bitmap length'); + } else if (line.match(/^[\dA-Fa-f]+$/) == null) { + throw new Error('invalid bitmap data'); + } else if (this.parsed.extraBits) { + const data = Buffer.from(line, 'hex'); + const checkX = (this.lastBox.width - 1) | 7; + const lastByte = data[data.length - 1]; + let bitNo = 7 - (this.lastBox.Width & 7); + + for (let x = this.lastBox.Width; x <= checkX; x++) { + if (lastByte & (1 << bitNo)) { + fnutil.warning(this.location(), `extra bit(s) starting with x=${x}`); + break; + } + bitNo--; + } + } + } + + checkLine(line) { + if (line.match(/[^\t\f\v\u0020-\u00ff]/)) { + throw new Error('control character(s)'); + } + if (this.parsed.asciiChars && line.match(/[\u007f-\u00ff]/)) { + fnutil.warning(this.location(), 'non-ascii character(s)'); + } + + switch (this.mode) { + case MODE.META: + if (!this.xlfdName && line.startsWith('FONT')) { + line = this.checkFont(line); + this.xlfdName = true; + } else { + this.HANDLERS.findIndex(function(handler) { + if (line.startsWith(handler[0])) { + handler[1](line.substring(handler[0].length).trimLeft()); + return true; + } + return false; + }); + } + break; + case MODE.PROPS: + if (line.startsWith('ENDPROPERTIES')) { + this.mode = MODE.META; + } else { + line = this.checkProp(line); + } + break; + default: // MODE.BITMAP + if (line.startsWith('ENDCHAR')) { + this.mode = MODE.META; + } else { + this.checkBitmap(line); + } + } + return line; + } + + readCheck(line, callback) { + const match = line.match(/^COMMENT\s*bdfcheck\s+(-.*)$/); + + if (match && !this.options.parse(match[1], true, this.parsed)) { + throw new Error('invalid bdfcheck directive'); + } + + line = callback(line); + return line != null ? this.checkLine(line) : null; + } + + readLines(callback) { + return super.readLines(line => this.readCheck(line, callback)); + } +} + +// -- Main -- +function mainProgram(nonopt, parsed) { + (nonopt.length >= 1 ? nonopt : [null]).forEach(input => { + let ifs = new InputFileStream(input, parsed); + + try { + bdf.Font.read(ifs); + ifs.close(); + } catch (e) { + e.message = ifs.location() + e.message; + throw e; + } + ifs.proplocs.check(); + ifs.namelocs.check(); + ifs.codelocs.check(); + }); +} + +if (require.main === module) { + fncli.start('bdfcheck.js', new Options(), new Params(), mainProgram); +} diff --git a/bin/bdfcheck.py b/bin/bdfcheck.py new file mode 100644 index 000000000000..c7790f04d33c --- /dev/null +++ b/bin/bdfcheck.py @@ -0,0 +1,380 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +import re +import codecs +from collections import OrderedDict +from enum import IntEnum, unique + +import fnutil +import fncli +import fnio +import bdf + +# -- Params -- +class Params(fncli.Params): # pylint: disable=too-many-instance-attributes + def __init__(self): + fncli.Params.__init__(self) + self.ascii_chars = True + self.bbx_exceeds = True + self.dupl_codes = -1 + self.extra_bits = True + self.attributes = True + self.dupl_names = -1 + self.dupl_props = True + self.common_slant = True + self.common_weight = True + self.xlfd_fontnm = True + self.ywidth_zero = True + + +# -- Options -- +HELP = ('' + + 'usage: bdfcheck [options] [INPUT...]\n' + + 'Check BDF font(s) for various problems\n' + + '\n' + + ' -A disable non-ascii characters check\n' + + ' -B disable BBX exceeding FONTBOUNDINGBOX checks\n' + + ' -c/-C enable/disable duplicate character codes check\n' + + ' (default = enabled for registry ISO10646)\n' + + ' -E disable extra bits check\n' + + ' -I disable ATTRIBUTES check\n' + + ' -n/-N enable duplicate character names check\n' + + ' (default = enabled for registry ISO10646)\n' + + ' -P disable duplicate properties check\n' + + ' -S disable common slant check\n' + + ' -W disable common weight check\n' + + ' -X disable XLFD font name check\n' + + ' -Y disable zero WIDTH Y check\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'File directives: COMMENT bdfcheck --enable|disable-<check-name>\n' + + ' (also available as long command line options)\n' + + '\n' + + 'Check names: ascii-chars, bbx-exceeds, duplicate-codes, extra-bits,\n' + + ' attributes, duplicate-names, duplicate-properties, common-slant,\n' + + ' common-weight, xlfd-font, ywidth-zero\n' + + '\n' + + 'The input BDF(s) must be v2.1 with unicode encoding.\n') + +VERSION = 'bdfcheck 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, [], HELP, VERSION) + + + def parse(self, name, directive, params): + value = name.startswith('--enable') or name[1].islower() + + if name in ['-A', '--enable-ascii-chars', '--disable-ascii-chars']: + params.ascii_chars = value + elif name in ['-B', '--enable-bbx-exceeds', '--disable-bbx-exceeds']: + params.bbx_exceeds = value + elif name in ['-c', '-C', '--enable-duplicate-codes', '--disable-duplicate-codes']: + params.dupl_codes = value + elif name in ['-E', '--enable-extra-bits', '--disable-extra-bits']: + params.extra_bits = value + elif name in ['-I', '--enable-attributes', '--disable-attributes']: + params.attributes = value + elif name in ['-n', '-N', '--enable-duplicate-names', '--disable-duplicate-names']: + params.dupl_names = value + elif name in ['-P', '--enable-duplicate-properties', '--disable-duplicate-properties']: + params.dupl_props = value + elif name in ['-S', '--enable-common-slant', '--disable-common-slant']: + params.common_slant = value + elif name in ['-W', '--enable-common-weight', '--disable-common-weight']: + params.common_weight = value + elif name in ['-X', '--enable-xlfd-font', '--disable-xlfd-font']: + params.xlfd_fontnm = value + elif name in ['-Y', '--enable-ywidth-zero', '--disable-ywidth-zero']: + params.ywidth_zero = value + else: + return directive is not True and self.fallback(name, params) + + return directive is not True or name.startswith('--') + + +# -- DupMap -- +class DupMap(OrderedDict): + def __init__(self, prefix, severity, descript, quote): + OrderedDict.__init__(self) + self.prefix = prefix + self.descript = descript + self.severity = severity + self.quote = quote + + + def check(self): + for value, lines in self.items(): + if len(lines) > 1: + text = 'duplicate %s %s at lines' % (self.descript, str(value)) + + for index, line in enumerate(lines): + text += ' ' if index == 0 else ' and ' if index == len(lines) - 1 else ', ' + text += str(line) + + fnutil.message(self.prefix, self.severity, text) + + + def push(self, value, line_no): + try: + self[value].append(line_no) + except KeyError: + self[value] = [line_no] + + +# -- InputFileStream -- +@unique +class MODE(IntEnum): + META = 0 + PROPS = 1 + BITMAP = 2 + +class InputFileStream(fnio.InputFileStream): + def __init__(self, file_name, parsed): + fnio.InputFileStream.__init__(self, file_name) + self.parsed = parsed + self.mode = MODE.META + self.proplocs = DupMap(self.location(), 'error', 'property', '') + self.namelocs = DupMap(self.location(), 'warning', 'character name', '"') + self.codelocs = DupMap(self.location(), 'warning', 'encoding', '') + self.handlers = [ + (b'STARTCHAR', lambda value: self.append_name(value)), + (b'ENCODING', lambda value: self.append_code(value)), + (b'SWIDTH', lambda value: self.check_width('SWIDTH', value, bdf.Width.parse_s)), + (b'DWIDTH', lambda value: self.check_width('DWIDTH', value, bdf.Width.parse_d)), + (b'BBX', lambda value: self.set_last_box(value)), + (b'BITMAP', lambda _: self.set_mode(MODE.BITMAP)), + (b'SIZE', InputFileStream.check_size), + (b'ATTRIBUTES', lambda value: self.check_attr(value)), + (b'STARTPROPERTIES', lambda _: self.set_mode(MODE.PROPS)), + (b'FONTBOUNDINGBOX', lambda value: self.set_font_box(value)), + ] + self.xlfd_name = False + self.last_box = None + self.font_box = None + self.options = Options() + + + def append(self, option, valocs, value): + if option: + valocs.push(str(value, 'ascii'), self.line_no) + + + def append_code(self, value): + fnutil.parse_dec('encoding', value) + self.append(self.parsed.dupl_codes, self.codelocs, value) + + + def append_name(self, value): + self.append(self.parsed.dupl_names, self.namelocs, b'"%s"' % value) + + + def check_width(self, name, value, parse): + if self.parsed.ywidth_zero and parse(name, value).y != 0: + fnutil.warning(self.location(), 'non-zero %s Y' % name) + + + def set_font_box(self, value): + self.font_box = bdf.BBX.parse('FONTBOUNDINGBOX', value) + + + def set_last_box(self, value): + bbx = bdf.BBX.parse('BBX', value) + + if self.parsed.bbx_exceeds: + exceeds = [] + + if bbx.xoff < self.font_box.xoff: + exceeds.append('xoff < FONTBOUNDINGBOX xoff') + + if bbx.yoff < self.font_box.yoff: + exceeds.append('yoff < FONTBOUNDINGBOX yoff') + + if bbx.width > self.font_box.width: + exceeds.append('width > FONTBOUNDINGBOX width') + + if bbx.height > self.font_box.height: + exceeds.append('height > FONTBOUNDINGBOX height') + + for exceed in exceeds: + fnutil.message(self.location(), '', exceed) + + self.last_box = bbx + + + def set_mode(self, new_mode): + self.mode = new_mode + + + def check(self): + self.process(bdf.Font.read) + self.proplocs.check() + self.namelocs.check() + self.codelocs.check() + + + @staticmethod + def check_size(value): + words = fnutil.split_words('SIZE', value, 3) + fnutil.parse_dec('point size', words[0], 1, None) + fnutil.parse_dec('x resolution', words[1], 1, None) + fnutil.parse_dec('y resolution', words[2], 1, None) + + + def check_attr(self, value): + if not re.fullmatch(br'[\dA-Fa-f]{4}', value): + raise Exception('ATTRIBUTES must be 4 hex-encoded characters') + + if self.parsed.attributes: + fnutil.warning(self.location(), 'ATTRIBUTES may cause problems with freetype') + + + def check_font(self, value): + xlfd = value[4:].lstrip().split(b'-', 15) + + if len(xlfd) == 15 and xlfd[0] == b'': + unicode = (xlfd[bdf.XLFD.CHARSET_REGISTRY].upper() == b'ISO10646') + + if self.parsed.dupl_codes == -1: + self.parsed.dupl_codes = unicode + + if self.parsed.dupl_names == -1: + self.parsed.dupl_names = unicode + + if self.parsed.common_weight: + weight = str(xlfd[bdf.XLFD.WEIGHT_NAME], 'ascii') + compare = weight.lower() + consider = 'Bold' if 'bold' in compare else 'Normal' + + if compare in ['medium', 'regular']: + compare = 'normal' + + if compare != consider.lower(): + fnutil.warning(self.location(), 'weight "%s" may be considered %s' % (weight, consider)) + + if self.parsed.common_slant: + slant = str(xlfd[bdf.XLFD.SLANT], 'ascii') + consider = 'Italic' if re.search('^[IO]', slant) else 'Regular' + + if not re.fullmatch('[IOR]', slant): + fnutil.warning(self.location(), 'slant "%s" may be considered %s' % (slant, consider)) + + else: + if self.parsed.xlfd_fontnm: + fnutil.warning(self.location(), 'non-XLFD font name') + + value = b'FONT --------------' + + return value + + + def check_prop(self, line): + match = re.fullmatch(br'(\w+)\s+([-\d"].*)', line) + + if not match: + raise Exception('invalid property format') + + name = match.group(1) + value = match.group(2) + + if value.startswith(b'"'): + if len(value) < 2 or not value.endswith(b'"'): + raise Exception('no closing double quote') + if re.search(b'[^"]"[^"]', value[1 : len(value) - 1]): + raise Exception('unescaped double quote') + else: + fnutil.parse_dec('value', value, None, None) + + self.append(self.parsed.dupl_props, self.proplocs, name) + return b'P%d 1' % self.line_no + + + def check_bitmap(self, line): + if len(line) != self.last_box.row_size() * 2: + raise Exception('invalid bitmap length') + + data = codecs.decode(line, 'hex') + + if self.parsed.extra_bits: + check_x = (self.last_box.width - 1) | 7 + last_byte = data[len(data) - 1] + bit_no = 7 - (self.last_box.width & 7) + + for x in range(self.last_box.width, check_x + 1): + if last_byte & (1 << bit_no): + fnutil.warning(self.location(), 'extra bit(s) starting with x=%d' % x) + break + bit_no -= 1 + + + def check_line(self, line): + if re.search(b'[^\t\f\v\x20-\xff]', line): + raise Exception('control character(s)') + + if self.parsed.ascii_chars and re.search(b'[\x7f-\xff]', line): + fnutil.warning(self.location(), 'non-ascii character(s)') + + if self.mode == MODE.META: + if not self.xlfd_name and line.startswith(b'FONT'): + line = self.check_font(line) + self.xlfd_name = True + else: + for handler in self.handlers: + if line.startswith(handler[0]): + handler[1](line[len(handler[0]):].lstrip()) + break + elif self.mode == MODE.PROPS: + if line.startswith(b'ENDPROPERTIES'): + self.mode = MODE.META + else: + line = self.check_prop(line) + else: # MODE.BITMAP + if line.startswith(b'ENDCHAR'): + self.mode = MODE.META + else: + self.check_bitmap(line) + + return line + + + def read_check(self, line, callback): + match = re.search(br'^COMMENT\s*bdfcheck\s+(-.*)$', line) + + if match and not self.options.parse(str(match[1], 'ascii'), True, self.parsed): + raise Exception('invalid bdfcheck directive') + + line = callback(line) + return self.check_line(line) if line is not None else None + + + def read_lines(self, callback): + return fnio.InputFileStream.read_lines(self, lambda line: self.read_check(line, callback)) + + +# -- Main -- +def main_program(nonopt, parsed): + for input_name in nonopt or [None]: + InputFileStream(input_name, parsed).check() + + +if __name__ == '__main__': + fncli.start('bdfcheck.py', Options(), Params(), main_program) diff --git a/bin/bdfexp.js b/bin/bdfexp.js new file mode 100644 index 000000000000..4a08131d0abb --- /dev/null +++ b/bin/bdfexp.js @@ -0,0 +1,287 @@ +/* + Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; + +const fnutil = require('./fnutil.js'); +const fncli = require('./fncli.js'); +const fnio = require('./fnio.js'); +const bdf = require('./bdf.js'); + +// -- Font -- +class Font extends bdf.Font { + constructor() { + super(); + this.minWidth = 0; // used in proportional() + this.avgWidth = 0; + } + + _expand(char) { + if (char.dwidth.x >= 0) { + if (char.bbx.xoff >= 0) { + var width = Math.max(char.bbx.xoff + char.bbx.width, char.dwidth.x); + var dstXOff = char.bbx.xoff; + var expXOff = 0; + } else { + width = Math.max(char.bbx.width, char.dwidth.x - char.bbx.xoff); + dstXOff = 0; + expXOff = char.bbx.xoff; + } + } else { + const revXOff = char.bbx.xoff + char.bbx.width; + + if (revXOff <= 0) { + width = -Math.min(char.dwidth.x, char.bbx.xoff); + dstXOff = width + char.bbx.xoff; + expXOff = -width; + } else { + width = Math.max(char.bbx.width, revXOff - char.dwidth.x); + dstXOff = width - char.bbx.width; + expXOff = revXOff - width; + } + } + + const height = this.bbx.height; + + if (width === char.bbx.width && height === char.bbx.height) { + return; + } + + const srcRowSize = char.bbx.rowSize(); + const dstRowSize = (width + 7) >> 3; + const dstYMax = this.pxAscender - char.bbx.yoff; + const dstYMin = dstYMax - char.bbx.height; + const copyRow = (dstXOff & 7) === 0; + const dstData = Buffer.alloc(dstRowSize * height); + + for (let dstY = dstYMin; dstY < dstYMax; dstY++) { + let srcByteNo = (dstY - dstYMin) * srcRowSize; + let dstByteNo = dstY * dstRowSize + (dstXOff >> 3); + + if (copyRow) { + char.data.copy(dstData, dstByteNo, srcByteNo, srcByteNo + srcRowSize); + } else { + let srcBitNo = 7; + let dstBitNo = 7 - (dstXOff & 7); + + for (let x = 0; x < char.bbx.width; x++) { + if (char.data[srcByteNo] & (1 << srcBitNo)) { + dstData[dstByteNo] |= (1 << dstBitNo); + } + if (--srcBitNo < 0) { + srcBitNo = 7; + srcByteNo++; + } + if (--dstBitNo < 0) { + dstBitNo = 7; + dstByteNo++; + } + } + } + } + + char.bbx = new bdf.BBX(width, height, expXOff, this.bbx.yoff); + char.props.set('BBX', char.bbx); + char.data = dstData; + } + + expand() { + // PREXPAND / VERTICAL + const ascent = this.props.get('FONT_ASCENT'); + const descent = this.props.get('FONT_DESCENT'); + let pxAscent = (ascent == null ? 0 : fnutil.parseDec('FONT_ASCENT', ascent, 0, bdf.DPARSE_LIMIT)); + let pxDescent = (descent == null ? 0 : fnutil.parseDec('FONT_DESCENT', descent, 0, bdf.DPARSE_LIMIT)); + + this.chars.forEach(char => { + pxAscent = Math.max(pxAscent, char.bbx.height + char.bbx.yoff); + pxDescent = Math.max(pxDescent, -char.bbx.yoff); + }); + this.bbx.height = pxAscent + pxDescent; + this.bbx.yoff = -pxDescent; + + // EXPAND / HORIZONTAL + let totalWidth = 0; + + this.minWidth = this.chars[0].bbx.width; + this.chars.forEach(char => { + this._expand(char); + this.minWidth = Math.min(this.minWidth, char.bbx.width); + this.bbx.width = Math.max(this.bbx.width, char.bbx.width); + this.bbx.xoff = Math.min(this.bbx.xoff, char.bbx.xoff); + totalWidth += char.bbx.width; + }); + this.avgWidth = fnutil.round(totalWidth / this.chars.length); + this.props.set('FONTBOUNDINGBOX', this.bbx); + } + + expandX() { + this.chars.forEach(char => { + if (char.dwidth.x !== char.bbx.width) { // preserve SWIDTH if possible + char.swidth.x = fnutil.round(char.bbx.width * 1000 / this.bbx.height); + char.props.set('SWIDTH', char.swidth); + char.dwidth.x = char.bbx.width; + char.props.set('DWIDTH', char.dwidth); + } + char.bbx.xoff = 0; + char.props.set('BBX', char.bbx); + }); + this.bbx.xoff = 0; + this.props.set('FONTBOUNDINGBOX', this.bbx); + } + + expandY() { + const props = new Map([ + [ 'FONT_ASCENT', this.pxAscender ], + [ 'FONT_DESCENT', -this.pxDescender ], + [ 'PIXEL_SIZE', this.bbx.height ] + ]); + + props.forEach((value, name) => { + if (this.props.get(name) != null) { + this.props.set(name, value); + } + }); + + this.xlfd[bdf.XLFD.PIXEL_SIZE] = this.bbx.height.toString(); + this.props.set('FONT', this.xlfd.join('-')); + } + + get proportional() { + return this.bbx.width > this.minWidth || super.proportional; + } + + get pxAscender() { + return this.bbx.height + this.bbx.yoff; + } + + get pxDescender() { + return this.bbx.yoff; + } + + _read(input) { + super._read(input); + this.expand(); + return this; + } + + static read(input) { + return (new Font())._read(input); + } + + _updateProp(name, value) { + if (this.props.get(name) != null) { + this.props.set(name, value); + } + } +} + +// -- Export -- +module.exports = Object.freeze({ + Font +}); + +// -- Params -- +class Params extends fncli.Params { + constructor() { + super(); + this.expandX = false; + this.expandY = false; + this.output = null; + } +} + +// -- Options -- +const HELP = ('' + + 'usage: bdfexp [-X] [-Y] [-o OUTPUT] [INPUT]\n' + + 'Expand BDF font bitmaps\n' + + '\n' + + ' -X zero xoffs, set character S/DWIDTH.X from the output\n' + + ' BBX.width if needed\n' + + ' -Y enlarge FONT_ASCENT, FONT_DESCENT and PIXEL_SIZE to\n' + + ' cover the font bounding box, if needed\n' + + ' -o OUTPUT output file (default = stdout)\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n'); + +const VERSION = 'bdfexp 1.60, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE; + +class Options extends fncli.Options { + constructor() { + super(['-o'], HELP, VERSION); + } + + parse(name, value, params) { + switch (name) { + case '-X': + params.expandX = true; + break; + case '-Y': + params.expandY = true; + break; + case '-o': + params.output = value; + break; + default: + this.fallback(name, params); + } + } +} + +// -- Main -- +function mainProgram(nonopt, parsed) { + if (nonopt.length > 1) { + throw new Error('invalid number of arguments, try --help'); + } + + // READ INPUT + let ifs = new fnio.InputFileStream(nonopt[0]); + + try { + var font = Font.read(ifs); + ifs.close(); + } catch (e) { + e.message = ifs.location() + e.message; + throw e; + } + + // EXTRA ACTIONS + if (parsed.expandX) { + font.expandX(); + } + if (parsed.expandY) { + font.expandY(); + } + + // WRITE OUTPUT + let ofs = new fnio.OutputFileStream(parsed.output); + + try { + font.write(ofs); + ofs.close(); + } catch (e) { + e.message = ofs.location() + e.message() + ofs.destroy(); + throw e; + } +} + +if (require.main === module) { + fncli.start('bdfexp.js', new Options(), new Params(), mainProgram); +} diff --git a/bin/bdfexp.py b/bin/bdfexp.py new file mode 100644 index 000000000000..8409935ee01b --- /dev/null +++ b/bin/bdfexp.py @@ -0,0 +1,245 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +from collections import OrderedDict + +import fnutil +import fncli +import fnio +import bdf + +# -- Font -- +class Font(bdf.Font): + def __init__(self): + bdf.Font.__init__(self) + self.min_width = 0 # used in proportional() + self.avg_width = 0 + + + def _expand(self, char): + if char.dwidth.x >= 0: + if char.bbx.xoff >= 0: + width = max(char.bbx.xoff + char.bbx.width, char.dwidth.x) + dst_xoff = char.bbx.xoff + exp_xoff = 0 + else: + width = max(char.bbx.width, char.dwidth.x - char.bbx.xoff) + dst_xoff = 0 + exp_xoff = char.bbx.xoff + else: + rev_xoff = char.bbx.xoff + char.bbx.width + + if rev_xoff <= 0: + width = -min(char.dwidth.x, char.bbx.xoff) + dst_xoff = width + char.bbx.xoff + exp_xoff = -width + else: + width = max(char.bbx.width, rev_xoff - char.dwidth.x) + dst_xoff = width - char.bbx.width + exp_xoff = rev_xoff - width + + height = self.bbx.height + + if width == char.bbx.width and height == char.bbx.height: + return + + src_row_size = char.bbx.row_size() + dst_row_size = (width + 7) >> 3 + dst_ymax = self.px_ascender - char.bbx.yoff + dst_ymin = dst_ymax - char.bbx.height + copy_row = (dst_xoff & 7) == 0 + dst_data = bytearray(dst_row_size * height) + + for dst_y in range(dst_ymin, dst_ymax): + src_byte_no = (dst_y - dst_ymin) * src_row_size + dst_byte_no = dst_y * dst_row_size + (dst_xoff >> 3) + + if copy_row: + dst_data[dst_byte_no : dst_byte_no + src_row_size] = \ + char.data[src_byte_no : src_byte_no + src_row_size] + else: + src_bit_no = 7 + dst_bit_no = 7 - (dst_xoff & 7) + + for _ in range(0, char.bbx.width): + if char.data[src_byte_no] & (1 << src_bit_no): + dst_data[dst_byte_no] |= (1 << dst_bit_no) + + if src_bit_no > 0: + src_bit_no -= 1 + else: + src_bit_no = 7 + src_byte_no += 1 + + if dst_bit_no > 0: + dst_bit_no -= 1 + else: + dst_bit_no = 7 + dst_byte_no += 1 + + char.bbx = bdf.BBX(width, height, exp_xoff, self.bbx.yoff) + char.props.set('BBX', char.bbx) + char.data = dst_data + + + def expand(self): + # PREXPAND / VERTICAL + ascent = self.props.get('FONT_ASCENT') + descent = self.props.get('FONT_DESCENT') + px_ascent = 0 if ascent is None else fnutil.parse_dec('FONT_ASCENT', ascent, 0, bdf.DPARSE_LIMIT) + px_descent = 0 if descent is None else fnutil.parse_dec('FONT_DESCENT', descent, 0, bdf.DPARSE_LIMIT) + + for char in self.chars: + px_ascent = max(px_ascent, char.bbx.height + char.bbx.yoff) + px_descent = max(px_descent, -char.bbx.yoff) + + self.bbx.height = px_ascent + px_descent + self.bbx.yoff = -px_descent + + # EXPAND / HORIZONTAL + total_width = 0 + self.min_width = self.chars[0].bbx.width + + for char in self.chars: + self._expand(char) + self.min_width = min(self.min_width, char.bbx.width) + self.bbx.width = max(self.bbx.width, char.bbx.width) + self.bbx.xoff = min(self.bbx.xoff, char.bbx.xoff) + total_width += char.bbx.width + + self.avg_width = round(total_width / len(self.chars)) + self.props.set('FONTBOUNDINGBOX', self.bbx) + + + def expand_x(self): + for char in self.chars: + if char.dwidth.x != char.bbx.width: + char.swidth.x = round(char.bbx.width * 1000 / self.bbx.height) + char.props.set('SWIDTH', char.swidth) + char.dwidth.x = char.bbx.width + char.props.set('DWIDTH', char.dwidth) + + char.bbx.xoff = 0 + char.props.set('BBX', char.bbx) + + self.bbx.xoff = 0 + self.props.set('FONTBOUNDINGBOX', self.bbx) + + + def expand_y(self): + props = OrderedDict(( + ('FONT_ASCENT', self.px_ascender), + ('FONT_DESCENT', -self.px_descender), + ('PIXEL_SIZE', self.bbx.height) + )) + + for [name, value] in props.items(): + if self.props.get(name) is not None: + self.props.set(name, value) + + self.xlfd[bdf.XLFD.PIXEL_SIZE] = bytes(str(self.bbx.height), 'ascii') + self.props.set('FONT', b'-'.join(self.xlfd)) + + + @property + def proportional(self): + return self.bbx.width > self.min_width or bdf.Font.proportional.fget(self) # pylint: disable=no-member + + @property + def px_ascender(self): + return self.bbx.height + self.bbx.yoff + + @property + def px_descender(self): + return self.bbx.yoff + + + def _read(self, input): + bdf.Font._read(self, input) + self.expand() + return self + + @staticmethod + def read(input): + return Font()._read(input) # pylint: disable=protected-access + + +# -- Params -- +class Params(fncli.Params): + def __init__(self): + fncli.Params.__init__(self) + self.expand_x = False + self.expand_y = False + self.output_name = None + + +# -- Options -- +HELP = ('' + + 'usage: bdfexp [-X] [-Y] [-o OUTPUT] [INPUT]\n' + + 'Expand BDF font bitmaps\n' + + '\n' + + ' -X zero xoffs, set character S/DWIDTH.X from the output\n' + + ' BBX.width if needed\n' + + ' -Y enlarge FONT_ASCENT, FONT_DESCENT and PIXEL_SIZE to\n' + + ' cover the font bounding box, if needed\n' + + ' -o OUTPUT output file (default = stdout)\n' + + ' --help display this help and exit\n' + + ' --version display the program version and license, and exit\n' + + ' --excstk display the exception stack on error\n' + + '\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n') + +VERSION = 'bdfexp 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(fncli.Options): + def __init__(self): + fncli.Options.__init__(self, ['-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name == '-X': + params.expand_x = True + elif name == '-Y': + params.expand_y = True + elif name == '-o': + params.output_name = value + else: + self.fallback(name, params) + + +# -- Main -- +def main_program(nonopt, parsed): + if len(nonopt) > 1: + raise Exception('invalid number of arguments, try --help') + + # READ INPUT + font = fnio.read_file(nonopt[0] if nonopt else None, Font.read) + + # EXTRA ACTIONS + if parsed.expand_x: + font.expand_x() + + if parsed.expand_y: + font.expand_y() + + # WRITE OUTPUT + fnio.write_file(parsed.output_name, lambda ofs: font.write(ofs)) + + +if __name__ == '__main__': + fncli.start('bdfexp.py', Options(), Params(), main_program) diff --git a/bin/bdftofnt.js b/bin/bdftofnt.js index fd5a7ebcab1a..ec1047449e5a 100644 --- a/bin/bdftofnt.js +++ b/bin/bdftofnt.js @@ -1,28 +1,30 @@ -// -// Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -'use strict'; + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. -const tty = require('tty'); + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; const fnutil = require('./fnutil.js'); const fncli = require('./fncli.js'); const fnio = require('./fnio.js'); const bdf = require('./bdf.js'); -const bmpf = require('./bmpf.js'); - +const bdfexp = require('./bdfexp.js'); +// -- Params -- class Params extends fncli.Params { constructor() { super(); @@ -33,7 +35,7 @@ class Params extends fncli.Params { } } - +// -- Options -- const HELP = ('' + 'usage: bdftofnt [-c CHARSET] [-m MINCHAR] [-f FAMILY] [-o OUTPUT] [INPUT]\n' + 'Convert a BDF font to Windows FNT\n' + @@ -41,14 +43,14 @@ const HELP = ('' + ' -c CHARSET fnt character set (default = 0, see wingdi.h ..._CHARSET)\n' + ' -m MINCHAR fnt minimum character code (8-bit CP decimal, not unicode)\n' + ' -f FAMILY fnt family: DontCare, Roman, Swiss, Modern or Decorative\n' + - ' -o OUTPUT output file (default = stdout, must not be a terminal)\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + ' --help display this help and exit\n' + ' --version display the program version and license, and exit\n' + ' --excstk display the exception stack on error\n' + '\n' + - 'The input must be a BDF 2.1 font encoded in the unicode range.\n'); + 'The input must be a BDF 2.1 font with unicode encoding.\n'); -const VERSION = 'bdftofnt 1.55, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE; +const VERSION = 'bdftofnt 1.60, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE; const FNT_FAMILIES = [ 'DontCare', 'Roman', 'Swiss', 'Modern', 'Decorative' ]; @@ -60,16 +62,16 @@ class Options extends fncli.Options { parse(name, value, params) { switch (name) { case '-c': - params.charSet = fnutil.parseDec('charset', value, 0, 255); + params.charSet = fnutil.parseDec('CHARSET', value, 0, 255); break; case '-m': - params.minChar = fnutil.parseDec('minchar', value, 0, 255); + params.minChar = fnutil.parseDec('MINCHAR', value, 0, 255); break; case '-f': params.fntFamily = FNT_FAMILIES.indexOf(value); if (params.fntFamily === -1) { - throw new Error('invalid fnt family'); + throw new Error('invalid FAMILY'); } break; case '-o': @@ -81,10 +83,11 @@ class Options extends fncli.Options { } } +// -- Main -- +const FNT_HEADER_SIZE = 118; +const FNT_CHARSETS = [238, 204, 0, 161, 162, 177, 178, 186, 163]; function mainProgram(nonopt, parsed) { - const WIN_FONTHEADERSIZE = 118; - if (nonopt.length > 1) { throw new Error('invalid number of arguments, try --help'); } @@ -93,10 +96,10 @@ function mainProgram(nonopt, parsed) { let minChar = parsed.minChar; // READ INPUT - let ifs = new fnio.InputStream(nonopt[0]); + let ifs = new fnio.InputFileStream(nonopt[0]); try { - var font = bmpf.Font.read(ifs); + var font = bdfexp.Font.read(ifs); ifs.close(); } catch (e) { e.message = ifs.location() + e.message; @@ -108,8 +111,6 @@ function mainProgram(nonopt, parsed) { const encoding = font.xlfd[bdf.XLFD.CHARSET_ENCODING]; if (encoding.toLowerCase().match(/^(cp)?125[0-8]$/)) { - const FNT_CHARSETS = [238, 204, 0, 161, 162, 177, 178, 186, 163]; - charSet = FNT_CHARSETS[parseInt(encoding.substring(encoding.length - 1), 10)]; } else { charSet = 255; @@ -137,16 +138,16 @@ function mainProgram(nonopt, parsed) { } // HEADER - var vtell = WIN_FONTHEADERSIZE + (numChars + 1) * 4; + var vtell = FNT_HEADER_SIZE + (numChars + 1) * 4; var bitsOffset = vtell; var ctable = []; var widthBytes = 0; // CTABLE/GLYPHS font.chars.forEach(char => { - const rowSize = char.rowSize(); + const rowSize = char.bbx.rowSize(); - ctable.push(char.width); + ctable.push(char.bbx.width); ctable.push(vtell); vtell += rowSize * font.bbx.height; widthBytes += rowSize; @@ -173,37 +174,32 @@ function mainProgram(nonopt, parsed) { } // WRITE - let ofs = new fnio.OutputStream(parsed.output); - - if (tty.isatty(ofs.fd)) { - throw new Error('binary output may not be send to a terminal, use -o or redirect/pipe it'); - } + let ofs = new fnio.OutputFileStream(parsed.output, null); try { // HEADER const family = font.xlfd[bdf.XLFD.FAMILY_NAME]; - const proportional = font.getProportional(); let copyright = font.props.get('COPYRIGHT'); copyright = (copyright != null) ? fnutil.unquote(copyright).substring(0, 60) : ''; - ofs.write16(0x0200); // font version - ofs.write32(vtell + family.length + 1); // total size + ofs.write16(0x0200); // font version + ofs.write32(vtell + family.length + 1); // total size ofs.writeZStr(copyright, 60 - copyright.length); - ofs.write16(0); // gdi, device type + ofs.write16(0); // gdi, device type ofs.write16(fnutil.round(font.bbx.height * 72 / 96)); - ofs.write16(96); // vertical resolution - ofs.write16(96); // horizontal resolution - ofs.write16(font.getAscent()); // base line - ofs.write16(0); // internal leading - ofs.write16(0); // external leading - ofs.write8(font.getItalic()); - ofs.write8(0); // underline - ofs.write8(0); // strikeout - ofs.write16(400 + 300 * font.getBold()); + ofs.write16(96); // vertical resolution + ofs.write16(96); // horizontal resolution + ofs.write16(font.pxAscender); // base line + ofs.write16(0); // internal leading + ofs.write16(0); // external leading + ofs.write8(Number(font.italic)); + ofs.write8(0); // underline + ofs.write8(0); // strikeout + ofs.write16(font.bold ? 700 : 400); ofs.write8(charSet); - ofs.write16(proportional ? 0 : font.avgWidth); + ofs.write16(font.proportional ? 0 : font.bbx.width); ofs.write16(font.bbx.height); - ofs.write8((parsed.fntFamily << 4) + proportional); + ofs.write8((parsed.fntFamily << 4) + Number(font.proportional)); ofs.write16(font.avgWidth); ofs.write16(font.bbx.width); ofs.write8(minChar); @@ -215,11 +211,9 @@ function mainProgram(nonopt, parsed) { if (font.defaultCode !== -1) { defaultIndex = font.chars.findIndex(char => char.code === font.defaultCode); } - if (minChar <= 0x20 && maxChar >= 0x20) { breakIndex = 0x20 - minChar; } - ofs.write8(defaultIndex); ofs.write8(breakIndex); ofs.write16(widthBytes); @@ -233,10 +227,10 @@ function mainProgram(nonopt, parsed) { ctable.forEach(value => ofs.write16(value)); // GLYPHS - let data = Buffer.alloc(font.bbx.height * font.bbx.rowSize()); + const data = Buffer.alloc(font.bbx.height * font.bbx.rowSize()); font.chars.forEach(char => { - const rowSize = char.rowSize(); + const rowSize = char.bbx.rowSize(); let counter = 0; // MS coordinates for (let n = 0; n < rowSize; n++) { @@ -257,7 +251,6 @@ function mainProgram(nonopt, parsed) { } } - if (require.main === module) { fncli.start('bdftofnt.js', new Options(), new Params(), mainProgram); } diff --git a/bin/bdftofnt.py b/bin/bdftofnt.py index e314ddc48a8a..0bfeed80840b 100644 --- a/bin/bdftofnt.py +++ b/bin/bdftofnt.py @@ -1,15 +1,19 @@ # -# Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import re @@ -18,18 +22,19 @@ import fnutil import fncli import fnio import bdf -import bmpf - +import bdfexp +# -- Params -- class Params(fncli.Params): def __init__(self): fncli.Params.__init__(self) self.char_set = -1 self.min_char = -1 self.fnt_family = 0 - self.output = None + self.output_name = None +# -- Options -- HELP = ('' + 'usage: bdftofnt [-c CHARSET] [-m MINCHAR] [-f FAMILY] [-o OUTPUT] [INPUT]\n' + 'Convert a BDF font to Windows FNT\n' + @@ -37,14 +42,14 @@ HELP = ('' + ' -c CHARSET fnt character set (default = 0, see wingdi.h ..._CHARSET)\n' + ' -m MINCHAR fnt minimum character code (8-bit CP decimal, not unicode)\n' + ' -f FAMILY fnt family: DontCare, Roman, Swiss, Modern or Decorative\n' + - ' -o OUTPUT output file (default = stdout, must not be a terminal)\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + ' --help display this help and exit\n' + ' --version display the program version and license, and exit\n' + ' --excstk display the exception stack on error\n' + '\n' + - 'The input must be a BDF font encoded in the unicode range.\n') + 'The input must be a BDF 2.1 font with unicode encoding.\n') -VERSION = 'bdftofnt 1.55, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE +VERSION = 'bdftofnt 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE FNT_FAMILIES = ['DontCare', 'Roman', 'Swiss', 'Modern', 'Decorative'] @@ -55,21 +60,22 @@ class Options(fncli.Options): def parse(self, name, value, params): if name == '-c': - params.char_set = fnutil.parse_dec('charset', value, 0, 255) + params.char_set = fnutil.parse_dec('CHARSET', value, 0, 255) elif name == '-m': - params.min_char = fnutil.parse_dec('minchar', value, 0, 255) + params.min_char = fnutil.parse_dec('MINCHAR', value, 0, 255) elif name == '-f': if value in FNT_FAMILIES: params.fnt_family = FNT_FAMILIES.index(value) else: - raise Exception('invalid fnt family') + raise Exception('invalid FAMILY') elif name == '-o': - params.output = value + params.output_name = value else: self.fallback(name, params) -WIN_FONTHEADERSIZE = 118 +# -- Main -- +FNT_HEADER_SIZE = 118 FNT_CHARSETS = [238, 204, 0, 161, 162, 177, 178, 186, 163] def main_program(nonopt, parsed): @@ -80,13 +86,8 @@ def main_program(nonopt, parsed): min_char = parsed.min_char # READ INPUT - ifs = fnio.InputStream(nonopt[0] if nonopt else None) - - try: - font = bmpf.Font.read(ifs) - ifs.close() - except Exception as ex: - raise Exception(ifs.location() + str(ex)) + ifs = fnio.InputFileStream(nonopt[0] if nonopt else None) + font = ifs.process(bdfexp.Font.read) # COMPUTE if char_set == -1: @@ -115,15 +116,15 @@ def main_program(nonopt, parsed): raise Exception('the maximum character code is too big, (re)specify -m') # HEADER - vtell = WIN_FONTHEADERSIZE + (num_chars + 1) * 4 + vtell = FNT_HEADER_SIZE + (num_chars + 1) * 4 bits_offset = vtell ctable = [] width_bytes = 0 # CTABLE/GLYPHS for char in font.chars: - row_size = char.row_size() - ctable.append(char.width) + row_size = char.bbx.row_size() + ctable.append(char.bbx.width) ctable.append(vtell) vtell += row_size * font.bbx.height width_bytes += row_size @@ -142,43 +143,38 @@ def main_program(nonopt, parsed): raise Exception('the total character width is too big') except Exception as ex: - raise Exception(ifs.location() + str(ex)) + ex.message = ifs.location() + getattr(ex, 'message', str(ex)) + raise # WRITE - ofs = fnio.OutputStream(parsed.output) - - if ofs.file.isatty(): - raise Exception('binary output may not be send to a terminal, use -o or redirect/pipe it') - - try: + def write_fnt(output): # HEADER family = font.xlfd[bdf.XLFD.FAMILY_NAME] copyright = font.props.get('COPYRIGHT') copyright = fnutil.unquote(copyright)[:60] if copyright is not None else b'' - proportional = font.get_proportional() - - ofs.write16(0x0200) # font version - ofs.write32(vtell + len(family) + 1) # total size - ofs.write_zstr(copyright, 60 - len(copyright)) - ofs.write16(0) # gdi, device type - ofs.write16(round(font.bbx.height * 72 / 96)) - ofs.write16(96) # vertical resolution - ofs.write16(96) # horizontal resolution - ofs.write16(font.get_ascent()) # base line - ofs.write16(0) # internal leading - ofs.write16(0) # external leading - ofs.write8(font.get_italic()) - ofs.write8(0) # underline - ofs.write8(0) # strikeout - ofs.write16(400 + 300 * font.get_bold()) - ofs.write8(char_set) - ofs.write16(0 if proportional else font.bbx.width) - ofs.write16(font.bbx.height) - ofs.write8((parsed.fnt_family << 4) + proportional) - ofs.write16(font.avg_width) - ofs.write16(font.bbx.width) - ofs.write8(min_char) - ofs.write8(max_char) + + output.write16(0x0200) # font version + output.write32(vtell + len(family) + 1) # total size + output.write_zstr(copyright, 60 - len(copyright)) + output.write16(0) # gdi, device type + output.write16(round(font.bbx.height * 72 / 96)) + output.write16(96) # vertical resolution + output.write16(96) # horizontal resolution + output.write16(font.px_ascender) # base line + output.write16(0) # internal leading + output.write16(0) # external leading + output.write8(int(font.italic)) + output.write8(0) # underline + output.write8(0) # strikeout + output.write16(700 if font.bold else 400) + output.write8(char_set) + output.write16(0 if font.proportional else font.bbx.width) + output.write16(font.bbx.height) + output.write8((parsed.fnt_family << 4) + int(font.proportional)) + output.write16(font.avg_width) + output.write16(font.bbx.width) + output.write8(min_char) + output.write8(max_char) default_index = max_char - min_char break_index = 0 @@ -189,39 +185,37 @@ def main_program(nonopt, parsed): if min_char <= 0x20 <= max_char: break_index = 0x20 - min_char - ofs.write8(default_index) - ofs.write8(break_index) - ofs.write16(width_bytes) - ofs.write32(0) # device name - ofs.write32(vtell) - ofs.write32(0) # gdi bits pointer - ofs.write32(bits_offset) - ofs.write8(0) # reserved + output.write8(default_index) + output.write8(break_index) + output.write16(width_bytes) + output.write32(0) # device name + output.write32(vtell) + output.write32(0) # gdi bits pointer + output.write32(bits_offset) + output.write8(0) # reserved # CTABLE for value in ctable: - ofs.write16(value) + output.write16(value) # GLYPHS data = bytearray(font.bbx.height * font.bbx.row_size()) for char in font.chars: - row_size = char.row_size() + row_size = char.bbx.row_size() counter = 0 # MS coordinates for n in range(0, row_size): for y in range(0, font.bbx.height): data[counter] = char.data[row_size * y + n] counter += 1 - ofs.write(data[:counter]) - ofs.write(bytes(sentinel * font.bbx.height)) + output.write(data[:counter]) + output.write(bytes(sentinel * font.bbx.height)) # FAMILY - ofs.write_zstr(family, 1) - ofs.close() + output.write_zstr(family, 1) - except Exception as ex: - raise Exception(ofs.location() + str(ex) + ofs.destroy()) + fnio.write_file(parsed.output_name, write_fnt, encoding=None) if __name__ == '__main__': diff --git a/bin/bdftopsf.js b/bin/bdftopsf.js index c7b0cef76293..5151278c7087 100644 --- a/bin/bdftopsf.js +++ b/bin/bdftopsf.js @@ -1,27 +1,29 @@ -// -// Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2017-2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -'use strict'; + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. -const tty = require('tty'); + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; const fnutil = require('./fnutil.js'); const fncli = require('./fncli.js'); const fnio = require('./fnio.js'); -const bmpf = require('./bmpf.js'); - +const bdfexp = require('./bdfexp.js'); +// -- Params -- class Params extends fncli.Params { constructor() { super(); @@ -31,7 +33,7 @@ class Params extends fncli.Params { } } - +// -- Options -- const HELP = ('' + 'usage: bdftopsf [-1|-2|-r] [-g|-G] [-o OUTPUT] [INPUT.bdf] [TABLE...]\n' + 'Convert a BDF font to PC Screen Font or raw font\n' + @@ -42,7 +44,7 @@ const HELP = ('' + ' 192...223 (default for VGA text mode compliant PSF fonts\n' + ' with 224 to 512 characters starting with unicode 00A3)\n' + ' -G do not exchange characters 0...31 and 192...223\n' + - ' -o OUTPUT output file (default = stdout, must not be a terminal)\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + ' --help display this help and exit\n' + ' --version display the program version and license, and exit\n' + ' --excstk display the exception stack on error\n' + @@ -54,7 +56,7 @@ const HELP = ('' + 'are stored sequentially in the PSF unicode table for their character.\n' + '<ss> is always specified as FFFE, although it is stored as FE in PSF2.\n'); -const VERSION = 'bdftopsf 1.50, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_VERSION; +const VERSION = 'bdftopsf 1.58, Copyright (C) 2017-2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_VERSION; class Options extends fncli.Options { constructor() { @@ -87,7 +89,7 @@ class Options extends fncli.Options { } } - +// -- Main -- function mainProgram(nonopt, parsed) { const bdfile = nonopt.length > 0 && nonopt[0].toLowerCase().endsWith('.bdf'); let version = parsed.version; @@ -95,16 +97,16 @@ function mainProgram(nonopt, parsed) { let ver1Unicodes = true; // READ INPUT - let ifs = new fnio.InputStream(bdfile ? nonopt[0] : null); + let ifs = new fnio.InputFileStream(bdfile ? nonopt[0] : null); try { - var font = bmpf.Font.read(ifs); + var font = bdfexp.Font.read(ifs); ifs.close(); font.chars.forEach(char => { const prefix = `char ${char.code}: `; - if (char.width !== font.bbx.width) { + if (char.bbx.width !== font.bbx.width) { throw new Error(prefix + 'output width not equal to maximum output width'); } if (char.code === 65534) { @@ -165,7 +167,7 @@ function mainProgram(nonopt, parsed) { } if (font.chars.findIndex(char => char.code === uni) !== -1) { - if (uni >= 0x10000) { + if (uni > fnutil.UNICODE_BMP_MAX) { ver1Unicodes = false; } if (table == null) { @@ -178,7 +180,7 @@ function mainProgram(nonopt, parsed) { if (dup === 0xFFFF) { throw new Error('FFFF is not a character'); } - if (dup >= 0x10000) { + if (dup > fnutil.UNICODE_BMP_MAX) { ver1Unicodes = false; } if (table.indexOf(dup) === -1 || table.indexOf(0xFFFE) !== -1) { @@ -186,13 +188,13 @@ function mainProgram(nonopt, parsed) { } }); if (version === 1 && !ver1Unicodes) { - throw new Error('-1 requires unicodes <= FFFF'); + throw new Error('-1 requires unicodes <= ' + fnutil.UNICODE_BMP_MAX.toString(16)); } } } nonopt.slice(Number(bdfile)).forEach(name => { - ifs = new fnio.InputStream(name); + ifs = new fnio.InputFileStream(name); try { ifs.readLines(loadExtra); @@ -219,11 +221,7 @@ function mainProgram(nonopt, parsed) { } // WRITE - let ofs = new fnio.OutputStream(parsed.output); - - if (tty.isatty(ofs.fd)) { - throw new Error('binary output may not be send to a terminal, use -o or redirect/pipe it'); - } + let ofs = new fnio.OutputFileStream(parsed.output, null); try { // HEADER @@ -290,7 +288,6 @@ function mainProgram(nonopt, parsed) { } } - if (require.main === module) { fncli.start('bdftopsf.js', new Options(), new Params(), mainProgram); } diff --git a/bin/bdftopsf.py b/bin/bdftopsf.py index e4d3089ccdfd..1ad8e1b713c0 100644 --- a/bin/bdftopsf.py +++ b/bin/bdftopsf.py @@ -1,33 +1,36 @@ # -# Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # - -import re import fnutil import fncli import fnio -import bmpf - +import bdfexp +# -- Params -- class Params(fncli.Params): def __init__(self): fncli.Params.__init__(self) self.version = -1 self.exchange = -1 - self.output = None + self.output_name = None +# -- Options -- HELP = ('' + 'usage: bdftopsf [-1|-2|-r] [-g|-G] [-o OUTPUT] [INPUT.bdf] [TABLE...]\n' + 'Convert a BDF font to PC Screen Font or raw font\n' + @@ -38,7 +41,7 @@ HELP = ('' + ' 192...223 (default for VGA text mode compliant PSF fonts\n' + ' with 224 to 512 characters starting with unicode 00A3)\n' + ' -G do not exchange characters 0...31 and 192...223\n' + - ' -o OUTPUT output file (default = stdout, must not be a terminal)\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + ' --help display this help and exit\n' + ' --version display the program version and license, and exit\n' + ' --excstk display the exception stack on error\n' + @@ -50,7 +53,7 @@ HELP = ('' + 'are stored sequentially in the PSF unicode table for their character.\n' + '<ss> is always specified as FFFE, although it is stored as FE in PSF2.\n') -VERSION = 'bdftopsf 1.50, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE +VERSION = 'bdftopsf 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE class Options(fncli.Options): def __init__(self): @@ -67,11 +70,12 @@ class Options(fncli.Options): elif name == '-G': params.exchange = False elif name == '-o': - params.output = value + params.output_name = value else: self.fallback(name, params) +# -- Main -- def main_program(nonopt, parsed): version = parsed.version exchange = parsed.exchange @@ -79,16 +83,14 @@ def main_program(nonopt, parsed): ver1_unicodes = True # READ INPUT - ifs = fnio.InputStream(nonopt[0] if bdfile else None) + ifs = fnio.InputFileStream(nonopt[0] if bdfile else None) + font = ifs.process(bdfexp.Font.read) try: - font = bmpf.Font.read(ifs) - ifs.close() - for char in font.chars: prefix = 'char %d: ' % char.code - if char.width != font.bbx.width: + if char.bbx.width != font.bbx.width: raise Exception(prefix + 'output width not equal to maximum output width') if char.code == 65534: @@ -121,15 +123,15 @@ def main_program(nonopt, parsed): raise Exception('-g/--vga requires an 8x8, 8x14 or 8x16 font') except Exception as ex: - raise Exception(ifs.location() + str(ex)) + ex.message = ifs.location() + getattr(ex, 'message', str(ex)) + raise # READ TABLES tables = dict() def load_extra(line): nonlocal ver1_unicodes - - words = re.split(br'\s+', line) + words = line.split() if len(words) < 2: raise Exception('invalid format') @@ -140,7 +142,7 @@ def main_program(nonopt, parsed): raise Exception('FFFE is not a character') if next((char for char in font.chars if char.code == uni), None): - if uni >= 0x10000: + if uni > fnutil.UNICODE_BMP_MAX: ver1_unicodes = False if uni not in tables: @@ -154,23 +156,17 @@ def main_program(nonopt, parsed): if dup == 0xFFFF: raise Exception('FFFF is not a character') - if dup >= 0x10000: + if dup > fnutil.UNICODE_BMP_MAX: ver1_unicodes = False if not dup in table or 0xFFFE in table: tables[uni].append(dup) if version == 1 and not ver1_unicodes: - raise Exception('-1 requires unicodes <= FFFF') - - for name in nonopt[int(bdfile):]: - ifs = fnio.InputStream(name) + raise Exception('-1 requires unicodes <= %X' % fnutil.UNICODE_BMP_MAX) - try: - ifs.read_lines(load_extra) - ifs.close() - except Exception as ex: - raise Exception(ifs.location() + str(ex)) + for table_name in nonopt[int(bdfile):]: + fnio.read_file(table_name, lambda ifs: ifs.read_lines(load_extra)) # VERSION if version == -1: @@ -184,54 +180,49 @@ def main_program(nonopt, parsed): font.chars = font.chars[192:224] + font.chars[32:192] + font.chars[0:32] + font.chars[224:] # WRITE - ofs = fnio.OutputStream(parsed.output) - - if ofs.file.isatty(): - raise Exception('binary output may not be send to a terminal, use -o or redirect/pipe it') - - try: + def write_psf(output): # HEADER if version == 1: - ofs.write8(0x36) - ofs.write8(0x04) - ofs.write8((len(font.chars) >> 8) + 1) - ofs.write8(font.bbx.height) + output.write8(0x36) + output.write8(0x04) + output.write8((len(font.chars) >> 8) + 1) + output.write8(font.bbx.height) elif version == 2: - ofs.write32(0x864AB572) - ofs.write32(0x00000000) - ofs.write32(0x00000020) - ofs.write32(0x00000001) - ofs.write32(len(font.chars)) - ofs.write32(len(font.chars[0].data)) - ofs.write32(font.bbx.height) - ofs.write32(font.bbx.width) + output.write32(0x864AB572) + output.write32(0x00000000) + output.write32(0x00000020) + output.write32(0x00000001) + output.write32(len(font.chars)) + output.write32(len(font.chars[0].data)) + output.write32(font.bbx.height) + output.write32(font.bbx.width) # GLYPHS for char in font.chars: - ofs.write(char.data) + output.write(char.data) # UNICODES if version > 0: def write_unicode(code): if version == 1: - ofs.write16(code) + output.write16(code) elif code <= 0x7F: - ofs.write8(code) + output.write8(code) elif code in [0xFFFE, 0xFFFF]: - ofs.write8(code & 0xFF) + output.write8(code & 0xFF) else: if code <= 0x7FF: - ofs.write8(0xC0 + (code >> 6)) + output.write8(0xC0 + (code >> 6)) else: if code <= 0xFFFF: - ofs.write8(0xE0 + (code >> 12)) + output.write8(0xE0 + (code >> 12)) else: - ofs.write8(0xF0 + (code >> 18)) - ofs.write8(0x80 + ((code >> 12) & 0x3F)) + output.write8(0xF0 + (code >> 18)) + output.write8(0x80 + ((code >> 12) & 0x3F)) - ofs.write8(0x80 + ((code >> 6) & 0x3F)) + output.write8(0x80 + ((code >> 6) & 0x3F)) - ofs.write8(0x80 + (code & 0x3F)) + output.write8(0x80 + (code & 0x3F)) for char in font.chars: if char.code != 0xFFFF: @@ -243,11 +234,7 @@ def main_program(nonopt, parsed): write_unicode(0xFFFF) - # FINISH - ofs.close() - - except Exception as ex: - raise Exception(ofs.location() + str(ex) + ofs.destroy()) + fnio.write_file(parsed.output_name, write_psf, encoding=None) if __name__ == '__main__': diff --git a/bin/bmpf.js b/bin/bmpf.js deleted file mode 100644 index 3632959a64a5..000000000000 --- a/bin/bmpf.js +++ /dev/null @@ -1,159 +0,0 @@ -// -// Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// - -'use strict'; - -const fnutil = require('./fnutil.js'); -const bdf = require('./bdf.js'); - - -class Char { - constructor(code, name, width, data) { - this.code = code; - this.name = name; - this.width = width; - this.data = data; - } - - static from(char, fbbox) { - const deltaYOff = char.bbx.yoff - fbbox.yoff; // ~DSB - let width; - let dstXOff; - - if (char.dwidth.x >= 0) { - if (char.bbx.xoff >= 0) { - width = Math.max(char.bbx.width + char.bbx.xoff, char.dwidth.x); - dstXOff = char.bbx.xoff; - } else { - width = Math.max(char.bbx.width, char.dwidth.x - char.bbx.xoff); - dstXOff = 0; - } - } else { - dstXOff = Math.max(char.bbx.xoff - char.dwidth.x, 0); - width = char.bbx.width + dstXOff; - } - - if (width > bdf.WIDTH_MAX) { - throw new Error(`char ${char.code}: output width > ${bdf.WIDTH_MAX}`); - } - if (char.bbx.yoff < fbbox.yoff) { - throw new Error(`char ${char.code}: BBX yoff < FONTBOUNDINGBOX yoff`); - } - - const height = fbbox.height; - const srcRowSize = char.bbx.rowSize(); - const dstRowSize = (width + 7) >> 3; - const dstYMax = height - deltaYOff; - const dstYMin = dstYMax - char.bbx.height; - const compatRow = dstXOff === 0 && width >= char.bbx.width; - let data; - - if (compatRow && srcRowSize === dstRowSize && dstYMin === 0 && dstYMax === height) { - data = char.data; - } else if (dstYMin < 0) { - throw new Error(`char ${char.code}: start row ${dstYMin}`); - } else { - data = Buffer.alloc(dstRowSize * height); - - for (let dstY = dstYMin; dstY < dstYMax; dstY++) { - let srcByteNo = (dstY - dstYMin) * srcRowSize; - let dstByteNo = dstY * dstRowSize + (dstXOff >> 3); - - if (compatRow) { - char.data.copy(data, dstByteNo, srcByteNo, srcByteNo + srcRowSize); - } else { - let srcBitNo = 7; - let dstBitNo = 7 - (dstXOff & 7); - - for (let x = 0; x < char.bbx.width; x++) { - if (char.data[srcByteNo] & (1 << srcBitNo)) { - data[dstByteNo] |= (1 << dstBitNo); - } - if (--srcBitNo < 0) { - srcBitNo = 7; - srcByteNo++; - } - if (--dstBitNo < 0) { - dstBitNo = 7; - dstByteNo++; - } - } - } - } - } - - return new Char(char.code, char.props.get('STARTCHAR'), width, data); - } - - packedSize() { - return (this.width * (this.data.length / this.rowSize()) + 7) >> 3; - } - - rowSize() { - return (this.width + 7) >> 3; - } - - write(output, height, yoffset) { - let header = `STARTCHAR ${this.name}\nENCODING ${this.code}\n`; - const swidth = fnutil.round(this.width * 1000 / height); - - header += `SWIDTH ${swidth} 0\nDWIDTH ${this.width} 0\nBBX ${this.width} ${height} 0 ${yoffset}\n`; - output.writeLine(header + 'BITMAP\n' + bdf.Char.bitmap(this.data, this.rowSize()) + 'ENDCHAR'); - } -} - - -class Font extends bdf.Font { - constructor() { - super(); - this.minWidth = bdf.WIDTH_MAX; - this.avgWidth = 0; - } - - _read(input) { - let totalWidth = 0; - - super._read(input); - this.chars = this.chars.map(char => Char.from(char, this.bbx)); - this.bbx.xoff = 0; - this.chars.forEach(char => { - this.minWidth = Math.min(this.minWidth, char.width); - this.bbx.width = Math.max(this.bbx.width, char.width); - totalWidth += char.width; - }); - this.avgWidth = fnutil.round(totalWidth / this.chars.length); - this.props.set('FONTBOUNDINGBOX', this.bbx.toString()); - return this; - } - - static read(input) { - return (new Font())._read(input); - } - - getProportional() { - return Number(this.bbx.width > this.minWidth); - } - - write(output) { - this.props.forEach((name, value) => output.writeProp(name, value)); - this.chars.forEach(char => char.write(output, this.bbx.height, this.bbx.yoff)); - output.writeLine('ENDFONT'); - } -} - - -module.exports = Object.freeze({ - Char, - Font -}); diff --git a/bin/bmpf.py b/bin/bmpf.py deleted file mode 100644 index 36ba446d3712..000000000000 --- a/bin/bmpf.py +++ /dev/null @@ -1,147 +0,0 @@ -# -# Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# - -import bdf - - -class Char: - def __init__(self, code, name, width, data): - self.code = code - self.name = name - self.width = width - self.data = data - - - @staticmethod - def from_bdf(char, fbbox): - delta_yoff = char.bbx.yoff - fbbox.yoff # ~DSB - - if delta_yoff < 0: - raise Exception('char %d: BBX yoff < FONTBOUNDINGBOX yoff' % char.code) - - if char.dwidth.x >= 0: - if char.bbx.xoff >= 0: - width = max(char.bbx.width + char.bbx.xoff, char.dwidth.x) - dst_xoff = char.bbx.xoff - else: - width = max(char.bbx.width, char.dwidth.x - char.bbx.xoff) - dst_xoff = 0 - else: - dst_xoff = max(char.bbx.xoff - char.dwidth.x, 0) - width = char.bbx.width + dst_xoff - - if width > bdf.WIDTH_MAX: - raise Exception('char %d: output width > %d' % (char.code, bdf.WIDTH_MAX)) - - height = fbbox.height - src_row_size = char.bbx.row_size() - dst_row_size = (width + 7) >> 3 - dst_ymax = height - delta_yoff - dst_ymin = dst_ymax - char.bbx.height - compat_row = dst_xoff == 0 and width >= char.bbx.width - - if compat_row and src_row_size == dst_row_size and dst_ymin == 0 and dst_ymax == height: - data = char.data - elif dst_ymin < 0: - raise Exception('char %d: start row %d' % (char.code, dst_ymin)) - elif compat_row: - src_byte_no = 0 - data = bytearray(dst_ymin * dst_row_size) - line_fill = bytes(dst_row_size - src_row_size) - - for dst_y in range(dst_ymin, dst_ymax): - data += char.data[src_byte_no : src_byte_no + src_row_size] + line_fill - src_byte_no += src_row_size - - data += bytes(delta_yoff * dst_row_size) - else: - data = bytearray(dst_row_size * height) - - for dst_y in range(dst_ymin, dst_ymax): - src_byte_no = (dst_y - dst_ymin) * src_row_size - dst_byte_no = dst_y * dst_row_size + (dst_xoff >> 3) - - src_bit_no = 7 - dst_bit_no = 7 - (dst_xoff & 7) - - for _ in range(0, char.bbx.width): - if char.data[src_byte_no] & (1 << src_bit_no): - data[dst_byte_no] |= (1 << dst_bit_no) - - if src_bit_no > 0: - src_bit_no -= 1 - else: - src_bit_no = 7 - src_byte_no += 1 - - if dst_bit_no > 0: - dst_bit_no -= 1 - else: - dst_bit_no = 7 - dst_byte_no += 1 - - return Char(char.code, char.props.get('STARTCHAR'), width, data) - - - def row_size(self): - return (self.width + 7) >> 3 - - - def write(self, output, max_width, yoffset): - output.write_line(b'STARTCHAR %s\nENCODING %d' % (self.name, self.code)) - output.write_line(b'SWIDTH %d 0\nDWIDTH %d 0' % (round(self.width * 1000 / max_width), self.width)) - output.write_line(b'BBX %d %d 0 %d' % (self.width, len(self.data) / self.row_size(), yoffset)) - output.write_line(b'BITMAP\n' + bdf.Char.bitmap(self.data, self.row_size()) + b'ENDCHAR') - - -class Font(bdf.Font): - def __init__(self): - bdf.Font.__init__(self) - self.min_width = bdf.WIDTH_MAX - self.avg_width = 0 - - - def _read(self, input): - bdf.Font._read(self, input) - self.chars = [Char.from_bdf(char, self.bbx) for char in self.chars] - self.bbx.xoff = 0 - total_width = 0 - - for char in self.chars: - self.min_width = min(self.min_width, char.width) - self.bbx.width = max(self.bbx.width, char.width) - total_width += char.width - - self.avg_width = round(total_width / len(self.chars)) - self.props.set('FONTBOUNDINGBOX', bytes(str(self.bbx), 'ascii')) - return self - - - @staticmethod - def read(input): - return Font()._read(input) # pylint: disable=protected-access - - - def get_proportional(self): - return int(self.bbx.width > self.min_width) - - - def write(self, output): - for [name, value] in self.props: - output.write_prop(name, value) - - for char in self.chars: - char.write(output, self.bbx.width, self.bbx.yoff) - - output.write_line(b'ENDFONT') diff --git a/bin/fncli.js b/bin/fncli.js index 148bbb09e111..a9e897c422ac 100644 --- a/bin/fncli.js +++ b/bin/fncli.js @@ -1,27 +1,31 @@ -// -// Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2018-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -'use strict'; + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ +'use strict'; + +// -- Params -- class Params { constructor() { this.excstk = false; } } - +// -- Options -- class Options { constructor(needArgs, helpText, versionText) { needArgs.forEach(name => { @@ -64,7 +68,6 @@ class Options { } } - Options.Reader = class { constructor(options, args, skip) { this.options = options; @@ -111,7 +114,7 @@ Options.Reader = class { value = null; } - if (value === null && Number(this.options.needsArg(name)) > 0) { + if (value == null && Number(this.options.needsArg(name)) > 0) { if (++optind === this.args.length) { throw new Error(`option "${name}" requires an argument`); } @@ -128,9 +131,9 @@ Options.Reader = class { Object.defineProperty(Options, 'Reader', { 'enumerable': false }); Object.defineProperty(Options.Reader, 'name', { value: 'Reader' }); - +// -- Main -- function start(programName, options, params, mainProgram) { // eslint-disable-line consistent-return - let parsed = (params != null) ? params : new Params(); + const parsed = (params != null) ? params : new Params(); try { const version = process.version.match(/^v?(\d+)\.(\d+)/); @@ -157,15 +160,19 @@ function start(programName, options, params, mainProgram) { // eslint-disable-l } } catch (e) { if (parsed.excstk) { - throw e; + if (e.stack != null) { + process.stderr.write(e.stack + '\n'); + } else { + throw e; + } } else { process.stderr.write(`${process.argv.length >= 2 ? process.argv[1] : programName}: ${e.message}\n`); - process.exit(1); } + process.exit(1); } } - +// -- Exports -- module.exports = Object.freeze({ Params, Options, diff --git a/bin/fncli.py b/bin/fncli.py index 2617c973a95b..bfe263745749 100644 --- a/bin/fncli.py +++ b/bin/fncli.py @@ -1,27 +1,32 @@ # -# Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2018-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import sys import os import re - +# -- Params -- class Params: def __init__(self): self.excstk = False +# -- Options -- class Options: def __init__(self, need_args, help_text, version_text): for name in need_args: @@ -126,10 +131,12 @@ class Options: return (name, value) +# -- Main -- def start(program_name, options, params, main_program): parsed = Params() if params is None else params try: + if sys.hexversion < 0x3050000: raise Exception('python 3.5.0 or later required') @@ -148,7 +155,8 @@ def start(program_name, options, params, main_program): except Exception as ex: if parsed.excstk: - raise + raise # loses the message information, but preserves the start() caller stack info - sys.stderr.write('%s: %s\n' % (sys.argv[0] if sys.argv[0] else program_name, str(ex))) + message = getattr(ex, 'message', str(ex)) + sys.stderr.write('%s: %s\n' % (sys.argv[0] if sys.argv[0] else program_name, message)) sys.exit(1) diff --git a/bin/fnio.js b/bin/fnio.js index 63100d4180a8..ad46344e4077 100644 --- a/bin/fnio.js +++ b/bin/fnio.js @@ -1,43 +1,31 @@ -// -// Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -'use strict'; - -const fs = require('fs'); - - -const BINARY_ENCODING = 'latin1'; - -(function() { - let orig = Buffer.alloc(256); + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. - for (let i = 0; i < 256; i++) { - orig[i] = i; - } + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. - const test = Buffer.from(orig.toString(BINARY_ENCODING), BINARY_ENCODING); + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ - if (orig.compare(test) !== 0) { - throw new Error(`the ${BINARY_ENCODING} encoding is not 8-bit clean`); - } -})(); +'use strict'; +const tty = require('tty'); +const fs = require('fs'); +// -- InputFileStream -- const BLOCK_SIZE = 4096; -class InputStream { - constructor(fileName, encoding = BINARY_ENCODING) { +class InputFileStream { + constructor(fileName, encoding = 'binary') { if (fileName != null) { this.fd = fs.openSync(fileName, 'r'); this.stName = fileName; @@ -46,8 +34,7 @@ class InputStream { this.stName = '<stdin>'; } this.encoding = encoding; - this.lineNo = 0; - this.eof = false; + this.unseek(); this.lines = []; this.index = 0; this.buffer = Buffer.alloc(BLOCK_SIZE); @@ -55,11 +42,14 @@ class InputStream { } close() { - this.lineNo = 0; - this.eof = false; + this.unseek(); fs.closeSync(this.fd); } + fstat() { + return (this.fd === process.stdin.fd || tty.isatty(this.fd)) ? null : fs.fstatSync(this.fd); + } + location() { let location = ' '; @@ -81,8 +71,7 @@ class InputStream { return 0; } if (e.code !== 'EAGAIN') { - this.lineNo = 0; - this.eof = false; + this.unseek(); throw e; } } @@ -125,11 +114,16 @@ class InputStream { return line; } -} + unseek() { + this.lineNo = 0; + this.eof = false; + } +} -class OutputStream { - constructor(fileName) { +// -- OutputFileStream -- +class OutputFileStream { + constructor(fileName, encoding = 'binary') { if (fileName != null) { this.fd = fs.openSync(fileName, 'w'); this.stName = fileName; @@ -137,6 +131,10 @@ class OutputStream { this.fd = process.stdout.fd; this.stName = '<stdout>'; } + if (encoding == null && tty.isatty(this.fd)) { + throw new Error(this.location() + 'binary output may not be send to a terminal'); + } + this.encoding = (encoding == null ? 'binary' : encoding); this.fbbuf = Buffer.alloc(4); this.closeAttempt = false; } @@ -191,23 +189,22 @@ class OutputStream { fs.writeSync(this.fd, this.fbbuf, 0, 4); } - writeLine(bstr) { - fs.writeSync(this.fd, bstr + '\n', null, BINARY_ENCODING); + writeLine(text) { + fs.writeSync(this.fd, text + '\n', null, this.encoding); } writeProp(name, value) { - this.writeLine((name + ' ' + value).trim()); + this.writeLine((name + ' ' + value).trimRight()); } writeZStr(bstr, numZeros) { - fs.writeSync(this.fd, bstr, null, BINARY_ENCODING); + fs.writeSync(this.fd, bstr, null, 'binary'); this.write(Buffer.alloc(numZeros)); } } - +// -- Export -- module.exports = Object.freeze({ - BINARY_ENCODING, - InputStream, - OutputStream + InputFileStream, + OutputFileStream }); diff --git a/bin/fnio.py b/bin/fnio.py index b9f07512fc3e..e2b31914e913 100644 --- a/bin/fnio.py +++ b/bin/fnio.py @@ -1,15 +1,19 @@ # -# Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import codecs @@ -17,42 +21,46 @@ import struct import sys import os - -BINARY_ENCODING = '_fnio_binary' - -class InputStream: - def __init__(self, file_name, encoding=BINARY_ENCODING): +# -- InputFileStream -- +class InputFileStream: + def __init__(self, file_name, encoding='binary'): if file_name is not None: - if encoding == BINARY_ENCODING: - self.file = open(file_name, 'rb') - else: - self.file = open(file_name, 'r', encoding=encoding) - + self.file = open(file_name, 'r') if encoding is None else open(file_name, 'rb') self.st_name = file_name else: - if encoding == BINARY_ENCODING: - self.file = sys.stdin.buffer - elif encoding is None: - self.file = sys.stdin - else: - self.file = codecs.getreader(encoding)(sys.stdin.buffer) - + self.file = sys.stdin if encoding is None else sys.stdin.buffer self.st_name = '<stdin>' + if encoding not in [None, 'binary']: + self.file = codecs.getreader(encoding)(self.file) + self.line_no = 0 self.eof = False def close(self): - self.line_no = 0 - self.eof = False + self.unseek() self.file.close() + def fstat(self): + return None if (self.file == sys.stdin.buffer or self.file.isatty()) else os.fstat(self.file.fileno()) + + def location(self): return '%s:%s' % (self.st_name, 'EOF: ' if self.eof else '%d: ' % self.line_no if self.line_no > 0 else ' ') + def process(self, callback): + try: + result = callback(self) + self.close() + return result + except Exception as ex: + ex.message = self.location() + getattr(ex, 'message', str(ex)) + raise + + def read_line(self): return self.read_lines(lambda line: line) @@ -66,16 +74,21 @@ class InputStream: if line is not None: return line except OSError: - self.line_no = 0 - self.eof = False + self.unseek() raise self.eof = True return None -class OutputStream: - def __init__(self, file_name): + def unseek(self): + self.line_no = 0 + self.eof = False + + +# -- OutputFileStream -- +class OutputFileStream: + def __init__(self, file_name, encoding='binary'): if file_name is not None: self.file = open(file_name, 'wb') self.st_name = file_name @@ -83,59 +96,81 @@ class OutputStream: self.file = sys.stdout.buffer self.st_name = '<stdout>' - self.close_attempt = False + if encoding is None and self.file.isatty(): + raise Exception(self.location() + 'binary output may not be send to a terminal') - - def close(self): - self.close_attempt = True - self.file.close() + self.encoding = (None if encoding == 'binary' else encoding) + self.close_attempt = False - def destroy(self): + def abort(self): errors = '' if self.file != sys.stdout.buffer: if not self.close_attempt: try: - self.file.close() + self.close() except Exception as ex: - errors += '\n%s: close: %s' % (self.st_name, str(ex)) + errors += '\n%sclose: %s' % (self.location(), str(ex)) try: os.remove(self.st_name) except Exception as ex: - errors += '\n%s: unlink: %s' % (self.st_name, str(ex)) + errors += '\n%sunlink: %s' % (self.location(), str(ex)) return errors + def close(self): + self.close_attempt = True + self.file.close() + + def location(self): return self.st_name + ': ' - def write(self, buffer): - self.file.write(buffer) + def process(self, callback): + try: + callback(self) + self.close() + except Exception as ex: + ex.message = self.location() + getattr(ex, 'message', str(ex)) + self.abort() + raise + + + def write(self, data): + self.file.write(data) def write8(self, value): - self.file.write(struct.pack('B', value)) + self.write(struct.pack('B', value)) def write16(self, value): - self.file.write(struct.pack('<H', value)) + self.write(struct.pack('<H', value)) def write32(self, value): - self.file.write(struct.pack('<L', value)) + self.write(struct.pack('<L', value)) - def write_line(self, bstr): - self.file.write(bstr + b'\n') + def write_line(self, text): + self.write((text if self.encoding is None else bytes(text, self.encoding)) + b'\n') def write_prop(self, name, value): - self.write_line((bytes(name, 'ascii') + b' ' + value).strip()) + self.write_line((bytes(name, 'ascii') + b' ' + value).rstrip()) def write_zstr(self, bstr, num_zeros): - self.file.write(bstr + bytes(num_zeros)) + self.write(bstr + bytes(num_zeros)) + + +# -- read/write file -- +def read_file(file_name, callback, encoding='binary'): + return InputFileStream(file_name, encoding).process(callback) + + +def write_file(file_name, callback, encoding='binary'): + return OutputFileStream(file_name, encoding).process(callback) diff --git a/bin/fnutil.js b/bin/fnutil.js index 7a3e685e3636..9b81661fa93d 100644 --- a/bin/fnutil.js +++ b/bin/fnutil.js @@ -1,21 +1,26 @@ -// -// Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2017-2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -'use strict'; + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ +'use strict'; + +// -- Various -- const UNICODE_MAX = 1114111; // 0x10FFFF +const UNICODE_BMP_MAX = 65535; // 0xFFFF function parseDec(name, s, minValue = 0, maxValue = UNICODE_MAX) { if (s.match(/^\s*-?\d+\s*$/) == null) { @@ -56,9 +61,9 @@ function unihex(code) { } function round(value) { - let result = Math.round(value); + const esround = Math.round(value); - return result - Number(result % 2 !== 0 && result - value === 0.5); + return esround - Number(esround % 2 !== 0 && esround - value === 0.5); } function quote(s) { @@ -75,13 +80,12 @@ function unquote(s, name) { return s; } -function warning(prefix, message) { - if (prefix.endsWith(':')) { - prefix += ' '; - } else if (prefix.length > 0 && !prefix.endsWith(': ')) { - prefix += ': '; - } - process.stderr.write(`${prefix}warning: ${message}\n`); +function message(prefix, severity, text) { + process.stderr.write(`${prefix}${severity ? severity + ': ' : ''}${text}\n`); +} + +function warning(prefix, text) { + message(prefix, 'warning', text); } function splitWords(name, value, count) { @@ -95,25 +99,31 @@ function splitWords(name, value, count) { } const GPL2PLUS_LICENSE = ('' + - 'This program is free software; you can redistribute it and/or\n' + - 'modify it under the terms of the GNU General Public License as\n' + - 'published by the Free Software Foundation; either version 2 of\n' + - 'the License, or (at your option) any later version.\n' + + 'This program is free software; you can redistribute it and/or modify it\n' + + 'under the terms of the GNU General Public License as published by the Free\n' + + 'Software Foundation; either version 2 of the License, or (at your option)\n' + + 'any later version.\n' + '\n' + - 'This program is distributed in the hope that it will be useful,\n' + - 'but WITHOUT ANY WARRANTY; without even the implied warranty of\n' + - 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n' + - 'GNU General Public License for more details.\n'); - + 'This program is distributed in the hope that it will be useful, but\n' + + 'WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n' + + 'or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License\n' + + 'for more details.\n' + + '\n' + + 'You should have received a copy of the GNU General Public License along\n' + + 'with this program; if not, write to the Free Software Foundation, Inc.,\n' + + '51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n'); +// -- Exports -- module.exports = Object.freeze({ UNICODE_MAX, + UNICODE_BMP_MAX, parseDec, parseHex, unihex, round, quote, unquote, + message, warning, splitWords, GPL2PLUS_LICENSE diff --git a/bin/fnutil.py b/bin/fnutil.py index 18af7b454e67..a11bc4b05e8c 100644 --- a/bin/fnutil.py +++ b/bin/fnutil.py @@ -1,23 +1,26 @@ # -# Copyright (c) 2018 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import sys -import re - +# -- Various -- UNICODE_MAX = 1114111 # 0x10FFFF - +UNICODE_BMP_MAX = 65535 # 0xFFFF def parse_dec(name, s, min_value=0, max_value=UNICODE_MAX): try: @@ -62,17 +65,16 @@ def unquote(bstr, name=None): return bstr -def warning(prefix, message): - if prefix.endswith(':'): - prefix += ' ' - elif prefix and not prefix.endswith(': '): - prefix += ': ' +def message(prefix, severity, text): + sys.stderr.write('%s%s%s\n' % (prefix, severity + ': ' if severity else '', text)) - sys.stderr.write('%swarning: %s\n' % (prefix, message)) +def warning(prefix, text): + message(prefix, 'warning', text) -def split_words(name, bstr, count): - words = re.split(br'\s+', bstr, count) + +def split_words(name, value, count): + words = value.split(None, count) if len(words) != count: raise Exception('%s must contain %d values' % (name, count)) @@ -81,12 +83,16 @@ def split_words(name, bstr, count): GPL2PLUS_LICENSE = ('' + - 'This program is free software; you can redistribute it and/or\n' + - 'modify it under the terms of the GNU General Public License as\n' + - 'published by the Free Software Foundation; either version 2 of\n' + - 'the License, or (at your option) any later version.\n' + + 'This program is free software; you can redistribute it and/or modify it\n' + + 'under the terms of the GNU General Public License as published by the Free\n' + + 'Software Foundation; either version 2 of the License, or (at your option)\n' + + 'any later version.\n' + + '\n' + + 'This program is distributed in the hope that it will be useful, but\n' + + 'WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY\n' + + 'or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License\n' + + 'for more details.\n' + '\n' + - 'This program is distributed in the hope that it will be useful,\n' + - 'but WITHOUT ANY WARRANTY; without even the implied warranty of\n' + - 'MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n' + - 'GNU General Public License for more details.\n') + 'You should have received a copy of the GNU General Public License along\n' + + 'with this program; if not, write to the Free Software Foundation, Inc.,\n' + + '51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.\n') diff --git a/bin/otb1cli.js b/bin/otb1cli.js new file mode 100644 index 000000000000..bc96d6fd91f4 --- /dev/null +++ b/bin/otb1cli.js @@ -0,0 +1,129 @@ +/* + Copyright (C) 2018-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; + +const fnutil = require('./fnutil.js'); +const fncli = require('./fncli.js'); +const fnio = require('./fnio.js'); +const otb1exp = require('./otb1exp.js'); + +// -- Params -- +class Params extends otb1exp.Params { + constructor() { + super(); + this.output = null; + this.encoding = 'utf-8'; + this.realTime = true; + } +} + +// -- Options -- +const HELP = ('' + + 'usage: otb1cli [options] [INPUT]\n' + + 'Convert a BDF font to OTB\n' + + '\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + + ' -d DIR-HINT set font direction hint (default = 0)\n' + + ' -e EM-SIZE set em size (default = 1024)\n' + + ' -g LINE-GAP set line gap (default = 0)\n' + + ' -l LOW-PPEM set lowest recorded PPEM (default = font height)\n' + + ' -E ENCODING BDF string properties encoding (default = utf-8)\n' + + ' -W WLANG-ID set Windows name-s language ID (default = 0x0409)\n' + + ' -T use the current date and time for created/modified\n' + + ' (default = get them from INPUT if not stdin/terminal)\n' + + ' -X set xMaxExtent = 0 (default = max character width)\n' + + ' -L write a single loca entry (default = CHARS entries)\n' + + ' -P write PostScript glyph names (default = no names)\n' + + '\n' + + 'Notes:\n' + + ' The input must be a BDF 2.1 font with unicode encoding.\n' + + ' All bitmaps are expanded first. Bitmap widths are used.\n' + + ' Overlapping characters are not supported.\n'); + +const VERSION = 'otb1cli 0.22, Copyright (C) 2018-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE; + +class Options extends otb1exp.Options { + constructor() { + super(['-o', '-E'], HELP, VERSION); + } + + parse(name, value, params) { + switch (name) { + case '-o': + params.output = value; + break; + case '-E': + params.encoding = value; + break; + case '-T': + params.realTime = false; + break; + default: + super.parse(name, value, params); + } + } +} + +// -- Main -- +function mainProgram(nonopt, parsed) { + if (nonopt.length > 1) { + throw new Error('invalid number of arguments, try --help'); + } + + // READ INPUT + let ifs = new fnio.InputFileStream(nonopt[0], parsed.encoding); + + try { + if (parsed.realTime) { + try { + const stat = ifs.fstat(); + + if (stat != null) { + parsed.created = stat.birthtime; + parsed.modified = stat.mtime; + } + } catch (e) { + fnutil.warning(ifs.location(), e.message); + } + } + + var font = otb1exp.Font.read(ifs, parsed); + ifs.close(); + } catch (e) { + e.message = ifs.location() + e.message; + throw e; + } + + // WRITE OUTPUT + let ofs = new fnio.OutputFileStream(parsed.output, null); + + try { + const table = new otb1exp.SFNT(font); + + ofs.write(table.data.slice(0, table.size)); + ofs.close(); + } catch (e) { + e.message = ofs.location() + e.message + ofs.destroy(); + throw e; + } +} + +if (require.main === module) { + fncli.start('otb1cli.js', new Options(), new Params(), mainProgram); +} diff --git a/bin/otb1cli.py b/bin/otb1cli.py new file mode 100644 index 000000000000..92ab07b937db --- /dev/null +++ b/bin/otb1cli.py @@ -0,0 +1,99 @@ +# +# Copyright (C) 2018-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +from datetime import datetime, timezone + +import fnutil +import fncli +import fnio +import otb1exp + +# -- Params -- +class Params(otb1exp.Params): + def __init__(self): + otb1exp.Params.__init__(self) + self.output_name = None + self.real_time = True + + +# -- Options -- +HELP = ('' + + 'usage: otb1cli [options] [INPUT]\n' + + 'Convert a BDF font to OTB\n' + + '\n' + + ' -o OUTPUT output file (default = stdout, may not be a terminal)\n' + + ' -d DIR-HINT set font direction hint (default = 0)\n' + + ' -e EM-SIZE set em size (default = 1024)\n' + + ' -g LINE-GAP set line gap (default = 0)\n' + + ' -l LOW-PPEM set lowest recorded PPEM (default = font height)\n' + + ' -E ENCODING BDF string properties encoding (default = utf-8)\n' + + ' -W WLANG-ID set Windows name-s language ID (default = 0x0409)\n' + + ' -T use the current date and time for created/modified\n' + + ' (default = get them from INPUT if not stdin/terminal)\n' + + ' -X set xMaxExtent = 0 (default = max character width)\n' + + ' -L write a single loca entry (default = CHARS entries)\n' + + ' -P write PostScript glyph names (default = no names)\n' + + '\n' + + 'Notes:\n' + + ' The input must be a BDF 2.1 font with unicode encoding.\n' + + ' All bitmaps are expanded first. Bitmap widths are used.\n' + + ' Overlapping characters are not supported.\n') + +VERSION = 'otb1cli 0.24, Copyright (C) 2018-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE + +class Options(otb1exp.Options): + def __init__(self): + otb1exp.Options.__init__(self, ['-o'], HELP, VERSION) + + + def parse(self, name, value, params): + if name == '-o': + params.output_name = value + elif name == '-T': + params.real_time = False + else: + otb1exp.Options.parse(self, name, value, params) + + +# -- Main -- +def main_program(nonopt, parsed): + if len(nonopt) > 1: + raise Exception('invalid number of arguments, try --help') + + # READ INPUT + def read_otb(ifs): + if parsed.real_time: + try: + stat = ifs.fstat() + if stat: + parsed.created = datetime.fromtimestamp(stat.st_ctime, timezone.utc) + parsed.modified = datetime.fromtimestamp(stat.st_mtime, timezone.utc) + except Exception as ex: + fnutil.warning(ifs.location(), str(ex)) + + return otb1exp.Font.read(ifs, parsed) + + font = fnio.read_file(nonopt[0] if nonopt else None, read_otb) + + # WRITE OUTPUT + sfnt = otb1exp.SFNT(font) + fnio.write_file(parsed.output_name, lambda ofs: ofs.write(sfnt.data), encoding=None) + + +if __name__ == '__main__': + fncli.start('otb1cli.py', Options(), Params(), main_program) diff --git a/bin/otb1exp.js b/bin/otb1exp.js new file mode 100644 index 000000000000..6aa09a9b5adf --- /dev/null +++ b/bin/otb1exp.js @@ -0,0 +1,895 @@ +/* + Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; + +const fnutil = require('./fnutil.js'); +const fncli = require('./fncli.js'); +const bdf = require('./bdf.js'); +const bdfexp = require('./bdfexp.js'); +const otb1get = require('./otb1get.js'); + +// -- Table -- +const TS_EMPTY = 0; +const TS_SMALL = 64; +const TS_LARGE = 1024; + +class Table { + constructor(size, name) { + this.data = Buffer.alloc(size); + this.size = 0; + this.tableName = name; + } + + checkSize(size) { + if (size !== this.size) { + throw new Error(`internal error: ${this.tableName} size = ${this.size} instead of ${size}`); + } + } + + checksum() { + let cksum = 0; + + for (let offset = 0; offset < this.size; offset += 4) { + cksum += this.data.readUInt32BE(offset); + } + + return cksum >>> 0; + } + + ensure(count) { + if (this.size + count > this.data.length) { + let newSize = this.data.length << 1; + + while (this.size + count > newSize) { + newSize <<= 1; + } + + const newData = Buffer.alloc(newSize); + + this.data.copy(newData, 0, 0, this.size); + this.data = newData; + } + } + + get padding() { + return ((this.size + 1) & 3) ^ 1; + } + + rewriteUInt32(value, offset) { + this.data.writeUInt32BE(value, offset); + } + + write(buffer) { + this.ensure(buffer.length); + buffer.copy(this.data, this.size); + this.size += buffer.length; + } + + writeRC(size, writer, name) { + this.ensure(size); + try { + writer(this.size); + } catch (e) { + e.message = e.message.replace('"value"', `"${this.tableName}.${name}"`); + throw e; + } + this.size += size; + } + + writeInt8(value, name) { + this.writeRC(1, (offset) => this.data.writeInt8(value, offset), name); + } + + writeInt16(value, name) { + this.writeRC(2, (offset) => this.data.writeInt16BE(value, offset), name); + } + + writeInt32(value, name) { + this.writeRC(4, (offset) => this.data.writeInt32BE(value, offset), name); + } + + writeInt64(value, name) { + this.writeRC(8, (offset) => this.data.writeInt64BE(value, offset), name); + } + + writeUInt8(value, name) { + this.writeRC(1, (offset) => this.data.writeUInt8(value, offset), name); + } + + writeUInt16(value, name) { + this.writeRC(2, (offset) => this.data.writeUInt16BE(value, offset), name); + } + + writeUInt32(value, name) { + this.writeRC(4, (offset) => this.data.writeUInt32BE(value, offset), name); + } + + writeUInt48(value, name) { + this.writeUInt16(name, 0); + this.writeRC(6, (offset) => this.data.writeUIntBE(value, offset, 6), name); + } + + writeFixed(value, name) { + this.writeRC(4, (offset) => this.data.writeInt32BE(fnutil.round(value * 65536), offset), name); + } + + writeTable(table) { + this.write(table.data.slice(0, table.size)); + } +} + +// -- Params -- +const EM_SIZE_MIN = 64; +const EM_SIZE_MAX = 16384; +const EM_SIZE_DEFAULT = 1024; + +class Params extends fncli.Params { + constructor() { + super(); + this.created = new Date(); + this.modified = this.created; + this.dirHint = 0; + this.emSize = EM_SIZE_DEFAULT; + this.lineGap = 0; + this.lowPPem = 0; + this.wLangId = 0x0409; + this.xMaxExtent = true; + this.singleLoca = false; + this.postNames = false; + } +} + +// -- Options -- +class Options extends fncli.Options { + constructor(needArgs, helpText, versionText) { + super(needArgs.concat(['-d', '-e', '-g', '-l', '-W']), helpText, versionText); + } + + parse(name, value, params) { + switch (name) { + case '-d': + params.dirHint = fnutil.parseDec('DIR-HINT', value, -2, 2); + break; + case '-e': + params.emSize = fnutil.parseDec('EM-SIZE', value, EM_SIZE_MIN, EM_SIZE_MAX); + break; + case '-g': + params.lineGap = fnutil.parseDec('LINE-GAP', value, 0, EM_SIZE_MAX << 1); + break; + case '-l': + params.lowPPem = fnutil.parseDec('LOW-PPEM', value, 1, bdf.DPARSE_LIMIT); + break; + case '-W': + params.wLangId = fnutil.parseHex('WLANG-ID', value, 0, 0x7FFF); + break; + case '-X': + params.xMaxExtent = false; + break; + case '-L': + params.singleLoca = true; + break; + case '-P': + params.postNames = true; + break; + default: + this.fallback(name, params); + } + } +} + +// -- Font -- +class Font extends bdfexp.Font { + constructor(params) { + super(); + this.params = params; + this.emAscender = 0; + this.emDescender = 0; + this.emMaxWidth = 0; + this.macStyle = 0; + this.lineSize = 0; + } + + get bmpOnly() { + return this.maxCode <= fnutil.UNICODE_BMP_MAX; + } + + get created() { + return Font.sfntime(this.params.created); + } + + emScale(value, divisor) { + return fnutil.round(value * this.params.emSize / (divisor || this.bbx.height)); + } + + get italicAngle() { + const value = this.props.get('ITALIC_ANGLE'); // must be integer + return value != null ? fnutil.parseDec('ITALIC_ANGLE', value, -45, 45) : this.italic ? -11.5 : 0; + } + + get maxCode() { + return this.chars.slice(-1)[0].code; + } + + get minCode() { + return this.chars[0].code; + } + + get modified() { + return Font.sfntime(this.params.modified); + } + + prepare() { + this.chars.sort((c1, c2) => c1.code - c2.code); + this.chars = this.chars.filter((c, index, array) => index === 0 || c.code !== array[index - 1].code); + this.props.set('CHARS', this.chars.length); + this.emAscender = this.emScale(this.pxAscender); + this.emDescender = this.emAscender - this.params.emSize; + this.emMaxWidth = this.emScaleWidth(this); + this.macStyle = Number(this.bold) + (Number(this.italic) << 1); + this.lineSize = this.emScale(fnutil.round(this.bbx.height / 17) || 1); + } + + _read(input) { + super._read(input); + this.prepare(); + return this; + } + + static read(input, params) { + return (new Font(params))._read(input); + } + + emScaleWidth(base) { + return this.emScale(base.bbx.width); + } + + static sfntime(stamp) { + return Math.floor((stamp - Date.UTC(1904, 0, 1)) / 1000); + } + + get underlinePosition() { + return fnutil.round((this.emDescender + this.lineSize) / 2); + } + + get xMaxExtent() { + return this.params.xMaxExtent ? this.emMaxWidth : 0; + } +} + +// -- BDAT -- +const BDAT_HEADER_SIZE = 4; +const BDAT_METRIC_SIZE = 5; + +class BDAT extends Table { + constructor(font) { + super(TS_LARGE, 'EBDT'); + // header + this.writeFixed(2, 'version'); + // format 1 data + font.chars.forEach(char => { + this.writeUInt8(font.bbx.height, 'height'); + this.writeUInt8(char.bbx.width, 'width'); + this.writeInt8(0, 'bearingX'); + this.writeInt8(font.pxAscender, 'bearingY'); + this.writeUInt8(char.bbx.width, 'advance'); + this.write(char.data); // imageData + }); + } + + static getCharSize(char) { + return BDAT_METRIC_SIZE + char.data.length; + } +} + +// -- BLOC -- +const BLOC_TABLE_SIZE_OFFSET = 12; +const BLOC_PREFIX_SIZE = 0x38; // header 0x08 + 1 bitmapSizeTable * 0x30 +const BLOC_INDEX_ARRAY_SIZE = 8; // 1 index record * 0x08 + +class BLOC extends Table { + constructor(font) { + super(TS_SMALL, 'EBLC'); + // header + this.writeFixed(2, 'version'); + this.writeUInt32(1, 'numSizes'); + // bitmapSizeTable + this.writeUInt32(BLOC_PREFIX_SIZE, 'indexSubTableArrayOffset'); + this.writeUInt32(0, 'indexTableSize'); // adjusted later + this.writeUInt32(1, 'numberOfIndexSubTables'); + this.writeUInt32(0, 'colorRef'); + // hori + this.writeInt8(font.pxAscender, 'hori ascender'); + this.writeInt8(font.pxDescender, 'hori descender'); + this.writeUInt8(font.bbx.width, 'hori widthMax'); + this.writeInt8(1, 'hori caretSlopeNumerator'); + this.writeInt8(0, 'hori caretSlopeDenominator'); + this.writeInt8(0, 'hori caretOffset'); + this.writeInt8(0, 'hori minOriginSB'); + this.writeInt8(0, 'hori minAdvanceSB'); + this.writeInt8(font.pxAscender, 'hori maxBeforeBL'); + this.writeInt8(font.pxDescender, 'hori minAfterBL'); + this.writeInt16(0, 'hori padd'); + // vert + this.writeInt8(0, 'vert ascender'); + this.writeInt8(0, 'vert descender'); + this.writeUInt8(0, 'vert widthMax'); + this.writeInt8(0, 'vert caretSlopeNumerator'); + this.writeInt8(0, 'vert caretSlopeDenominator'); + this.writeInt8(0, 'vert caretOffset'); + this.writeInt8(0, 'vert minOriginSB'); + this.writeInt8(0, 'vert minAdvanceSB'); + this.writeInt8(0, 'vert maxBeforeBL'); + this.writeInt8(0, 'vert minAfterBL'); + this.writeInt16(0, 'vert padd'); + // (bitmapSizeTable) + this.writeUInt16(0, 'startGlyphIndex'); + this.writeUInt16(font.chars.length - 1, 'endGlyphIndex'); + this.writeUInt8(font.bbx.height, 'ppemX'); + this.writeUInt8(font.bbx.height, 'ppemY'); + this.writeUInt8(1, 'bitDepth'); + this.writeUInt8(1, 'flags'); // small metrics are horizontal + // indexSubTableArray + this.writeUInt16(0, 'firstGlyphIndex'); + this.writeUInt16(font.chars.length - 1, 'lastGlyphIndex'); + this.writeUInt32(BLOC_INDEX_ARRAY_SIZE, 'additionalOffsetToIndexSubtable'); + // indexSubtableHeader + this.writeUInt16(font.proportional ? 1 : 2, 'indexFormat'); + this.writeUInt16(1, 'imageFormat'); // BDAT -> small metrics, byte-aligned + this.writeUInt32(BDAT_HEADER_SIZE, 'imageDataOffset'); + // indexSubtable data + if (font.proportional) { + let offset = 0; + + font.chars.forEach(char => { + this.writeUInt32(offset, 'offsetArray[]'); + offset += BDAT.getCharSize(char); + }); + this.writeUInt32(offset, 'offsetArray[]'); + } else { + this.writeUInt32(BDAT.getCharSize(font.chars[0]), 'imageSize'); + this.writeUInt8(font.bbx.height, 'height'); + this.writeUInt8(font.bbx.width, 'width'); + this.writeInt8(0, 'horiBearingX'); + this.writeInt8(font.pxAscender, 'horiBearingY'); + this.writeUInt8(font.bbx.width, 'horiAdvance'); + this.writeInt8(-(font.bbx.width >> 1), 'vertBearingX'); + this.writeInt8(0, 'vertBearingY'); + this.writeUInt8(font.bbx.height, 'vertAdvance'); + } + // adjust + this.rewriteUInt32(this.size - BLOC_PREFIX_SIZE, BLOC_TABLE_SIZE_OFFSET); + } +} + +// -- OS/2 -- +const OS_2_TABLE_SIZE = 96; + +class OS_2 extends Table { + constructor(font) { + super(TS_SMALL, 'OS/2'); + // Version 4 + const xAvgCharWidth = font.emScale(font.avgWidth); // otb1get.xAvgCharWidth(font); + const ulCharRanges = otb1get.ulCharRanges(font); + const ulCodePages = font.bmpOnly ? otb1get.ulCodePages(font) : [0, 0]; + // mostly from FontForge + const scriptXSize = font.emScale(30, 100); + const scriptYSize = font.emScale(40, 100); + const subscriptYOff = scriptYSize >> 1; + const xfactor = Math.tan(font.italicAngle * Math.PI / 180); + const subscriptXOff = 0; // stub, no overlapping characters yet + const superscriptYOff = font.emAscender - scriptYSize; + const superscriptXOff = -fnutil.round(xfactor * superscriptYOff); + // write + this.writeUInt16(4, 'version'); + this.writeInt16(xAvgCharWidth, 'xAvgCharWidth'); + this.writeUInt16(font.bold ? 700 : 400, 'usWeightClass'); + this.writeUInt16(5, 'usWidthClass'); // medium + this.writeInt16(0, 'fsType'); + this.writeInt16(scriptXSize, 'ySubscriptXSize'); + this.writeInt16(scriptYSize, 'ySubscriptYSize'); + this.writeInt16(subscriptXOff, 'ySubscriptXOffset'); + this.writeInt16(subscriptYOff, 'ySubscriptYOffset'); + this.writeInt16(scriptXSize, 'ySuperscriptXSize'); + this.writeInt16(scriptYSize, 'ySuperscriptYSize'); + this.writeInt16(superscriptXOff, 'ySuperscriptXOffset'); + this.writeInt16(superscriptYOff, 'ySuperscriptYOffset'); + this.writeInt16(font.lineSize, 'yStrikeoutSize'); + this.writeInt16(font.emScale(25, 100), 'yStrikeoutPosition'); + this.writeInt16(0, 'sFamilyClass'); // no classification + this.writeUInt8(2, 'bFamilyType'); // text and display + this.writeUInt8(0, 'bSerifStyle'); // any + this.writeUInt8(font.bold ? 8 : 6, 'bWeight'); + this.writeUInt8(font.proportional ? 3 : 9, 'bProportion'); + this.writeUInt8(0, 'bContrast'); + this.writeUInt8(0, 'bStrokeVariation'); + this.writeUInt8(0, 'bArmStyle'); + this.writeUInt8(0, 'bLetterform'); + this.writeUInt8(0, 'bMidline'); + this.writeUInt8(0, 'bXHeight'); + this.writeUInt32(ulCharRanges[0], 'ulCharRange1'); + this.writeUInt32(ulCharRanges[1], 'ulCharRange2'); + this.writeUInt32(ulCharRanges[2], 'ulCharRange3'); + this.writeUInt32(ulCharRanges[3], 'ulCharRange4'); + this.writeUInt32(0x586F7334, 'achVendID'); // 'Xos4' + this.writeUInt16(OS_2.fsSelection(font), 'fsSelection'); + this.writeUInt16(Math.min(font.minCode, fnutil.UNICODE_BMP_MAX), 'firstChar'); + this.writeUInt16(Math.min(font.maxCode, fnutil.UNICODE_BMP_MAX), 'lastChar'); + this.writeInt16(font.emAscender, 'sTypoAscender'); + this.writeInt16(font.emDescender, 'sTypoDescender'); + this.writeInt16(font.params.lineGap, 'sTypoLineGap'); + this.writeUInt16(font.emAscender, 'usWinAscent'); + this.writeUInt16(-font.emDescender, 'usWinDescent'); + this.writeUInt32(ulCodePages[0], 'ulCodePageRange1'); + this.writeUInt32(ulCodePages[1], 'ulCodePageRange2'); + this.writeInt16(font.emScale(font.pxAscender * 0.6), 'sxHeight'); // stub + this.writeInt16(font.emScale(font.pxAscender * 0.8), 'sCapHeight'); // stub + this.writeUInt16(OS_2.defaultChar(font), 'usDefaultChar'); + this.writeUInt16(OS_2.breakChar(font), 'usBreakChar'); + this.writeUInt16(1, 'usMaxContext'); + // check + this.checkSize(OS_2_TABLE_SIZE); + } + + static breakChar(font) { + return font.chars.findIndex(char => char.code === 0x20) !== -1 ? 0x20 : font.minCode; + } + + static defaultChar(font) { + if (font.defaultCode !== -1 && font.defaultCode <= fnutil.UNICODE_BMP_MAX) { + return font.defaultCode; + } + return font.minCode && font.maxCode; + } + + static fsSelection(font) { + const fsSelection = Number(font.bold) * 5 + Number(font.italic); + return fsSelection || (font.xlfd[bdf.XLFD.SLANT] === 'R' ? 0x40 : 0); + } +} + +// -- cmap -- +const CMAP_4_PREFIX_SIZE = 12; +const CMAP_4_FORMAT_SIZE = 16; +const CMAP_4_SEGMENT_SIZE = 8; + +const CMAP_12_PREFIX_SIZE = 20; +const CMAP_12_FORMAT_SIZE = 16; +const CMAP_12_GROUP_SIZE = 12; + +class CMapRange { + constructor(glyphIndex = 0, startCode = 0, finalCode = -2) { + this.glyphIndex = glyphIndex; + this.startCode = startCode; + this.finalCode = finalCode; + } + + get idDelta() { + return (this.glyphIndex - this.startCode) & 0xFFFF; + } +} + +class CMAP extends Table { + constructor(font) { + super(TS_LARGE, 'cmap'); + // make ranges + let ranges = []; + let range = new CMapRange(); + + for (let index = 0; index < font.chars.length; index++) { + let code = font.chars[index].code; + + if (code === range.finalCode + 1) { + range.finalCode++; + } else { + range = new CMapRange(index, code, code); + ranges.push(range); + } + } + // write + if (font.bmpOnly) { + if (font.maxCode < 0xFFFF) { + ranges.push(new CMapRange(0, 0xFFFF, 0xFFFF)); + } + this.writeFormat4(ranges); + } else { + this.writeFormat12(ranges); + } + } + + writeFormat4(ranges) { + // index + this.writeUInt16(0, 'version'); + this.writeUInt16(1, 'numberSubtables'); + // encoding subtables index + this.writeUInt16(3, 'platformID'); // Microsoft + this.writeUInt16(1, 'platformSpecificID'); // Unicode BMP (UCS-2) + this.writeUInt32(CMAP_4_PREFIX_SIZE, 'offset'); // for Unicode BMP (UCS-2) + // cmap format 4 + const segCount = ranges.length; + const subtableSize = CMAP_4_FORMAT_SIZE + segCount * CMAP_4_SEGMENT_SIZE; + const searchRange = 2 << Math.floor(Math.log2(segCount)); + + this.writeUInt16(4, 'format'); + this.writeUInt16(subtableSize, 'length'); + this.writeUInt16(0, 'language'); // none/independent + this.writeUInt16(segCount * 2, 'segCountX2'); + this.writeUInt16(searchRange, 'searchRange'); + this.writeUInt16(Math.log2(searchRange / 2), 'entrySelector'); + this.writeUInt16((segCount * 2) - searchRange, 'rangeShift'); + ranges.forEach(range => { + this.writeUInt16(range.finalCode, 'endCode'); + }); + this.writeUInt16(0, 'reservedPad'); + ranges.forEach(range => { + this.writeUInt16(range.startCode, 'startCode'); + }); + ranges.forEach(range => { + this.writeUInt16(range.idDelta, 'idDelta'); + }); + ranges.forEach(() => this.writeUInt16(0), 'idRangeOffset'); + // check + this.checkSize(CMAP_4_PREFIX_SIZE + subtableSize); + } + + writeFormat12(ranges) { + // index + this.writeUInt16(0, 'version'); + this.writeUInt16(2, 'numberSubtables'); + // encoding subtables + this.writeUInt16(0, 'platformID'); // Unicode + this.writeUInt16(4, 'platformSpecificID'); // Unicode 2.0+ full range + this.writeUInt32(CMAP_12_PREFIX_SIZE, 'offset'); // for Unicode 2.0+ full range + this.writeUInt16(3, 'platformID'); // Microsoft + this.writeUInt16(10, 'platformSpecificID'); // Unicode UCS-4 + this.writeUInt32(CMAP_12_PREFIX_SIZE, 'offset'); // for Unicode UCS-4 + // cmap format 12 + const subtableSize = CMAP_12_FORMAT_SIZE + ranges.length * CMAP_12_GROUP_SIZE; + + this.writeFixed(12, 'format'); + this.writeUInt32(subtableSize, 'length'); + this.writeUInt32(0, 'language'); // none/independent + this.writeUInt32(ranges.length, 'nGroups'); + this.ranges.forEach(range => { + this.writeUInt32(range.startCode, 'startCharCode'); + this.writeUInt32(range.finalCode, 'endCharCode'); + this.writeUInt32(range.glyphIndex, 'startGlyphID'); + }); + // check + this.checkSize(CMAP_12_PREFIX_SIZE + subtableSize); + } +} + +// -- glyf -- +class GLYF extends Table { + constructor() { + super(TS_EMPTY, 'glyf'); + } +} + +// -- head -- +const HEAD_TABLE_SIZE = 54; +const HEAD_CHECKSUM_OFFSET = 8; + +class HEAD extends Table { + constructor(font) { + super(TS_SMALL, 'head'); + this.writeFixed(1, 'version'); + this.writeFixed(1, 'fontRevision'); + this.writeUInt32(0, 'checksumAdjustment'); // adjusted later + this.writeUInt32(0x5F0F3CF5, 'magicNumber'); + this.writeUInt16(HEAD.flags(font), 'flags'); + this.writeUInt16(font.params.emSize, 'unitsPerEm'); + this.writeUInt48(font.created, 'created'); + this.writeUInt48(font.modified, 'modified'); + this.writeInt16(0, 'xMin'); + this.writeInt16(font.emDescender, 'yMin'); + this.writeInt16(font.emMaxWidth, 'xMax'); + this.writeInt16(font.emAscender, 'yMax'); + this.writeUInt16(font.macStyle, 'macStyle'); + this.writeUInt16(font.params.lowPPem || font.bbx.height, 'lowestRecPPEM'); + this.writeInt16(font.params.dirHint, 'fontDirectionHint'); + this.writeInt16(0, 'indexToLocFormat'); // short + this.writeInt16(0, 'glyphDataFormat'); // current + // check + this.checkSize(HEAD_TABLE_SIZE); + } + + static flags(font) { + return otb1get.containsRTL(font) ? 0x020B : 0x0B; // y0 base, x0 lsb, scale int + } +} + +// -- hhea -- +const HHEA_TABLE_SIZE = 36; + +class HHEA extends Table { + constructor(font) { + super(TS_SMALL, 'hhea'); + this.writeFixed(1, 'version'); + this.writeInt16(font.emAscender, 'ascender'); + this.writeInt16(font.emDescender, 'descender'); + this.writeInt16(font.params.lineGap, 'lineGap'); + this.writeUInt16(font.emMaxWidth, 'advanceWidthMax'); + this.writeInt16(0, 'minLeftSideBearing'); + this.writeInt16(0, 'minRightSideBearing'); + this.writeInt16(font.xMaxExtent, 'xMaxExtent'); + this.writeInt16(font.italic ? 100 : 1, 'caretSlopeRise'); + this.writeInt16(font.italic ? 20 : 0, 'caretSlopeRun'); + this.writeInt16(0, 'caretOffset'); + this.writeInt16(0, 'reserved'); + this.writeInt16(0, 'reserved'); + this.writeInt16(0, 'reserved'); + this.writeInt16(0, 'reserved'); + this.writeInt16(0, 'metricDataFormat'); // current + this.writeUInt16(font.chars.length, 'numOfLongHorMetrics'); + // check + this.checkSize(HHEA_TABLE_SIZE); + } +} + +// -- hmtx -- +class HMTX extends Table { + constructor(font) { + super(TS_LARGE, 'hmtx'); + font.chars.forEach(char => { + this.writeUInt16(font.emScaleWidth(char), 'advanceWidth'); + this.writeInt16(0, 'leftSideBearing'); + }); + } +} + +// -- loca -- +class LOCA extends Table { + constructor(font) { + super(TS_SMALL, 'loca'); + if (!font.params.singleLoca) { + font.chars.forEach(() => this.writeUInt16(0, 'offset')); + } + this.writeUInt16(0, 'offset'); + } +} + +// -- maxp -- +const MAXP_TABLE_SIZE = 32; + +class MAXP extends Table { + constructor(font) { + super(TS_SMALL, 'maxp'); + this.writeFixed(1, 'version'); + this.writeUInt16(font.chars.length, 'numGlyphs'); + this.writeUInt16(0, 'maxPoints'); + this.writeUInt16(0, 'maxContours'); + this.writeUInt16(0, 'maxComponentPoints'); + this.writeUInt16(0, 'maxComponentContours'); + this.writeUInt16(2, 'maxZones'); + this.writeUInt16(0, 'maxTwilightPoints'); + this.writeUInt16(1, 'maxStorage'); + this.writeUInt16(1, 'maxFunctionDefs'); + this.writeUInt16(0, 'maxInstructionDefs'); + this.writeUInt16(64, 'maxStackElements'); + this.writeUInt16(0, 'maxSizeOfInstructions'); + this.writeUInt16(0, 'maxComponentElements'); + this.writeUInt16(0, 'maxComponentDepth'); + // check + this.checkSize(MAXP_TABLE_SIZE); + } +} + +// -- name -- +const NAME_ID = { + COPYRIGHT: 0, + FONT_FAMILY: 1, + FONT_SUBFAMILY: 2, + UNIQUE_SUBFAMILY: 3, + FULL_FONT_NAME: 4, + LICENSE: 14 +}; + +const NAME_HEADER_SIZE = 6; +const NAME_RECORD_SIZE = 12; + +class NAME extends Table { + constructor(font) { + super(TS_LARGE, 'name'); + // compute names + let names = new Map(); + const copyright = font.props.get('COPYRIGHT'); + + if (copyright != null) { + names.set(NAME_ID.COPYRIGHT, fnutil.unquote(copyright)); + } + + const family = font.xlfd[bdf.XLFD.FAMILY_NAME]; + const style = ['Regular', 'Bold', 'Italic', 'Bold Italic'][font.macStyle]; + + names.set(NAME_ID.FONT_FAMILY, family); + names.set(NAME_ID.FONT_SUBFAMILY, style); + names.set(NAME_ID.UNIQUE_SUBFAMILY, `${family} ${style} bitmap height ${font.bbx.height}`); + names.set(NAME_ID.FULL_FONT_NAME, `${family} ${style}`); + + let license = font.props.get('LICENSE'); + const notice = font.props.get('NOTICE'); + + if (license == null && notice != null && notice.toLowerCase().includes('license')) { + license = notice; + } + if (license != null) { + names.set(NAME_ID.LICENSE, fnutil.unquote(license)); + } + // header + const count = names.size * (1 + 1); // Unicode + Microsoft + const stringOffset = NAME_HEADER_SIZE + NAME_RECORD_SIZE * count; + + this.writeUInt16(0, 'format'); + this.writeUInt16(count, 'count'); + this.writeUInt16(stringOffset, 'stringOffset'); + // name records / create values + let values = new Table(TS_LARGE, 'name'); + + names.forEach((str, nameID) => { + const value = Buffer.from(str, 'utf16le').swap16(); + const bmp = font.bmpOnly && value.length === str.length * 2; + // Unicode + this.writeUInt16(0, 'platformID'); // Unicode + this.writeUInt16(bmp ? 3 : 4, 'platformSpecificID'); + this.writeUInt16(0, 'languageID'); + this.writeUInt16(nameID, 'nameID'); + this.writeUInt16(value.length, 'length'); // in bytes + this.writeUInt16(values.size, 'offset'); + // Windows + this.writeUInt16(3, 'platformID'); // Microsoft + this.writeUInt16(bmp ? 1 : 10, 'platformSpecificID'); + this.writeUInt16(font.params.wLangId, 'languageID'); + this.writeUInt16(nameID, 'nameID'); + this.writeUInt16(value.length, 'length'); // in bytes + this.writeUInt16(values.size, 'offset'); + // value + values.write(value); + }); + // write values + this.writeTable(values); + // check + this.checkSize(stringOffset + values.size); + } +} + +// -- post -- +const POST_TABLE_SIZE = 32; + +class POST extends Table { + constructor(font) { + super(TS_SMALL, 'post'); + this.writeFixed(font.params.postNames ? 2 : 3, 'format'); + this.writeFixed(font.italicAngle, 'italicAngle'); + this.writeInt16(font.underlinePosition, 'underlinePosition'); + this.writeInt16(font.lineSize, 'underlineThickness'); + this.writeUInt32(font.proportional ? 0 : 1, 'isFixedPitch'); + this.writeUInt32(0, 'minMemType42'); + this.writeUInt32(0, 'maxMemType42'); + this.writeUInt32(0, 'minMemType1'); + this.writeUInt32(0, 'maxMemType1'); + // names + if (font.params.postNames) { + let postNames = otb1get.postMacNames(); + const postMacCount = postNames.length; + + this.writeUInt16(font.chars.length, 'numberOfGlyphs'); + font.chars.forEach(char => { + const name = char.props.get('STARTCHAR'); + const index = postNames.indexOf(name); + + if (index !== -1) { + this.writeUInt16(index, 'glyphNameIndex'); + } else { + this.writeUInt16(postNames.length, 'glyphNameIndex'); + postNames.push(name); + } + }); + + postNames.slice(postMacCount).forEach(name => { + this.writeUInt8(name.length, 'glyphNameLength'); + this.write(Buffer.from(name, 'binary')); + }); + // check + } else { + this.checkSize(POST_TABLE_SIZE); + } + } +} + +// -- SFNT -- +const SFNT_HEADER_SIZE = 12; +const SFNT_RECORD_SIZE = 16; +const SFNT_SUBTABLES = [ BDAT, BLOC, OS_2, CMAP, GLYF, HEAD, HHEA, HMTX, LOCA, MAXP, NAME, POST ]; + +class SFNT extends Table { + constructor(font) { + super(TS_LARGE, 'SFNT'); + // create tables + let tables = []; + + SFNT_SUBTABLES.forEach(Ctor => { + tables.push(new Ctor(font)); + }); + // header + const numTables = tables.length; + const entrySelector = Math.floor(Math.log2(numTables)); + const searchRange = 16 << entrySelector; + const contentOffset = SFNT_HEADER_SIZE + numTables * SFNT_RECORD_SIZE; + let offset = contentOffset; + let content = new Table(TS_LARGE, 'SFNT'); + let headChecksumOffset = -1; + + this.writeFixed(1, 'sfntVersion'); + this.writeUInt16(numTables, 'numTables'); + this.writeUInt16(searchRange, 'searchRange'); + this.writeUInt16(entrySelector, 'entrySelector'); + this.writeUInt16(numTables * 16 - searchRange, 'rangeShift'); + // table records / create content + tables.forEach(table => { + this.write(Buffer.from(table.tableName, 'binary')); + this.writeUInt32(table.checksum(), 'checkSum'); + this.writeUInt32(offset, 'offset'); + this.writeUInt32(table.size, 'length'); + // create content + if (table.tableName === 'head') { + headChecksumOffset = offset + HEAD_CHECKSUM_OFFSET; + } + const paddedSize = table.size + table.padding; + + content.write(table.data.slice(0, paddedSize)); + offset += paddedSize; + }); + // write content + this.writeTable(content); + // check + this.checkSize(contentOffset + content.size); + // adjust + if (headChecksumOffset !== -1) { + this.rewriteUInt32((0xB1B0AFBA - this.checksum()) >>> 0, headChecksumOffset); + } + } +} + +// -- Export -- +module.exports = Object.freeze({ + TS_EMPTY, + TS_SMALL, + TS_LARGE, + Table, + EM_SIZE_MIN, + EM_SIZE_MAX, + EM_SIZE_DEFAULT, + Params, + Options, + Font, + BDAT, + BLOC, + OS_2, + CMAP, + GLYF, + HEAD, + HHEA, + HMTX, + LOCA, + MAXP, + NAME, + POST, + SFNT +}); diff --git a/bin/otb1exp.py b/bin/otb1exp.py new file mode 100644 index 000000000000..51e274554f64 --- /dev/null +++ b/bin/otb1exp.py @@ -0,0 +1,808 @@ +# +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +import struct +import codecs +import math +from datetime import datetime, timezone +from itertools import groupby +from enum import IntEnum, unique +from collections import OrderedDict + +import fnutil +import fncli +import bdf +import bdfexp +import otb1get + +# -- Table -- +class Table: + def __init__(self, name): + self.data = bytearray(0) + self.table_name = name + + + def check_size(self, size): + if size != self.size: + raise Exception('internal error: %s size = %d instead of %d' % (self.table_name, self.size, size)) + + + def checksum(self): + cksum = 0 + data = self.data + self.padding + + for offset in range(0, self.size, 4): + cksum += struct.unpack('>L', data[offset : offset + 4])[0] + + return cksum & 0xFFFFFFFF + + + def pack(self, format, value, name): + try: + return struct.pack(format, value) + except struct.error as ex: + raise Exception('%s.%s: %s' % (self.table_name, name, str(ex))) + + + @property + def size(self): + return len(self.data) + + + @property + def padding(self): + return bytes(((self.size + 1) & 3) ^ 1) + + + def rewrite_uint32(self, value, offset): + self.data[offset : offset + 4] = struct.pack('>L', value) + + + def write(self, data): + self.data += data + + + def write_int8(self, value, name): + self.data += self.pack('b', value, name) + + + def write_uint8(self, value, name): + self.data += self.pack('B', value, name) + + + def write_int16(self, value, name): + self.data += self.pack('>h', value, name) + + + def write_uint16(self, value, name): + self.data += self.pack('>H', value, name) + + + def write_uint32(self, value, name): + self.data += self.pack('>L', value, name) + + + def write_uint64(self, value, name): + self.data += self.pack('>Q', value, name) + + + def write_fixed(self, value, name): + self.data += self.pack('>l', round(value * 65536), name) + + + def write_table(self, table): + self.data += table.data + + +# -- Params -- +EM_SIZE_MIN = 64 +EM_SIZE_MAX = 16384 +EM_SIZE_DEFAULT = 1024 + +class Params(fncli.Params): # pylint: disable=too-many-instance-attributes + def __init__(self): + fncli.Params.__init__(self) + self.created = datetime.now(timezone.utc) + self.modified = self.created + self.dir_hint = 0 + self.em_size = EM_SIZE_DEFAULT + self.line_gap = 0 + self.low_ppem = 0 + self.encoding = 'utf_8' + self.w_lang_id = 0x0409 + self.x_max_extent = True + self.single_loca = False + self.post_names = False + + +# -- Options -- +class Options(fncli.Options): + def __init__(self, need_args, help_text, version_text): + fncli.Options.__init__(self, need_args + ['-d', '-e', '-g', '-l', '-E', '-W'], help_text, version_text) + + + def parse(self, name, value, params): + if name == '-d': + params.dir_hint = fnutil.parse_dec('DIR-HINT', value, -2, 2) + elif name == '-e': + params.em_size = fnutil.parse_dec('EM-SIZE', value, EM_SIZE_MIN, EM_SIZE_MAX) + elif name == '-g': + params.line_gap = fnutil.parse_dec('LINE-GAP', value, 0, EM_SIZE_MAX << 1) + elif name == '-l': + params.low_ppem = fnutil.parse_dec('LOW-PPEM', value, 1, bdf.DPARSE_LIMIT) + elif name == '-E': + params.encoding = value + elif name == '-W': + params.w_lang_id = fnutil.parse_hex('WLANG-ID', value, 0, 0x7FFF) + elif name == '-X': + params.x_max_extent = False + elif name == '-L': + params.single_loca = True + elif name == '-P': + params.post_names = True + else: + self.fallback(name, params) + + +# -- Font -- +class Font(bdfexp.Font): + def __init__(self, params): + bdfexp.Font.__init__(self) + self.params = params + self.em_ascender = 0 + self.em_descender = 0 + self.em_max_width = 0 + self.mac_style = 0 + self.line_size = 0 + + + @property + def bmp_only(self): + return self.max_code <= fnutil.UNICODE_BMP_MAX + + @property + def created(self): + return Font.sfntime(self.params.created) + + def decode(self, data): + return codecs.decode(data, self.params.encoding) + + def em_scale(self, value, divisor=0): + return round(value * self.params.em_size / (divisor or self.bbx.height)) + + def em_scale_width(self, base): + return self.em_scale(base.bbx.width) + + @property + def italic_angle(self): + value = self.props.get('ITALIC_ANGLE') # must be integer + return fnutil.parse_dec('ITALIC_ANGLE', value, -45, 45) if value else -11.5 if self.italic else 0 + + @property + def max_code(self): + return self.chars[-1].code + + @property + def min_code(self): + return self.chars[0].code + + @property + def modified(self): + return Font.sfntime(self.params.modified) + + + def prepare(self): + self.chars.sort(key=lambda c: c.code) + self.chars = [next(elem[1]) for elem in groupby(self.chars, key=lambda c: c.code)] + self.props.set('CHARS', len(self.chars)) + self.em_ascender = self.em_scale(self.px_ascender) + self.em_descender = self.em_ascender - self.params.em_size + self.em_max_width = self.em_scale_width(self) + self.mac_style = int(self.bold) + (int(self.italic) << 1) + self.line_size = self.em_scale(round(self.bbx.height / 17) or 1) + + + def _read(self, input): + bdfexp.Font._read(self, input) + self.prepare() + return self + + @staticmethod + def read(input, params): # pylint: disable=arguments-differ + return Font(params)._read(input) # pylint: disable=protected-access + + + @staticmethod + def sfntime(stamp): + return math.floor((stamp - datetime(1904, 1, 1, tzinfo=timezone.utc)).total_seconds()) + + @property + def underline_position(self): + return round((self.em_descender + self.line_size) / 2) + + @property + def x_max_extent(self): + return self.em_max_width if self.params.x_max_extent else 0 + + +# -- BDAT -- +BDAT_HEADER_SIZE = 4 +BDAT_METRIC_SIZE = 5 + +class BDAT(Table): + def __init__(self, font): + Table.__init__(self, 'EBDT') + # header + self.write_fixed(2, 'version') + # format 1 data + for char in font.chars: + self.write_uint8(font.bbx.height, 'height') + self.write_uint8(char.bbx.width, 'width') + self.write_int8(0, 'bearingX') + self.write_int8(font.px_ascender, 'bearingY') + self.write_uint8(char.bbx.width, 'advance') + self.write(char.data) # imageData + + + @staticmethod + def get_char_size(char): + return BDAT_METRIC_SIZE + len(char.data) + + +# -- BLOC -- +BLOC_TABLE_SIZE_OFFSET = 12 +BLOC_PREFIX_SIZE = 0x38 # header 0x08 + 1 bitmapSizeTable * 0x30 +BLOC_INDEX_ARRAY_SIZE = 8 # 1 index record * 0x08 + +class BLOC(Table): + def __init__(self, font): + Table.__init__(self, 'EBLC') + # header + self.write_fixed(2, 'version') + self.write_uint32(1, 'numSizes') + # bitmapSizeTable + self.write_uint32(BLOC_PREFIX_SIZE, 'indexSubTableArrayOffset') + self.write_uint32(0, 'indexTableSize') # adjusted later + self.write_uint32(1, 'numberOfIndexSubTables') + self.write_uint32(0, 'colorRef') + # hori + self.write_int8(font.px_ascender, 'hori ascender') + self.write_int8(font.px_descender, 'hori descender') + self.write_uint8(font.bbx.width, 'hori widthMax') + self.write_int8(1, 'hori caretSlopeNumerator') + self.write_int8(0, 'hori caretSlopeDenominator') + self.write_int8(0, 'hori caretOffset') + self.write_int8(0, 'hori minOriginSB') + self.write_int8(0, 'hori minAdvanceSB') + self.write_int8(font.px_ascender, 'hori maxBeforeBL') + self.write_int8(font.px_descender, 'hori minAfterBL') + self.write_int16(0, 'hori padd') + # vert + self.write_int8(0, 'vert ascender') + self.write_int8(0, 'vert descender') + self.write_uint8(0, 'vert widthMax') + self.write_int8(0, 'vert caretSlopeNumerator') + self.write_int8(0, 'vert caretSlopeDenominator') + self.write_int8(0, 'vert caretOffset') + self.write_int8(0, 'vert minOriginSB') + self.write_int8(0, 'vert minAdvanceSB') + self.write_int8(0, 'vert maxBeforeBL') + self.write_int8(0, 'vert minAfterBL') + self.write_int16(0, 'vert padd') + # (bitmapSizeTable) + self.write_uint16(0, 'startGlyphIndex') + self.write_uint16(len(font.chars) - 1, 'endGlyphIndex') + self.write_uint8(font.bbx.height, 'ppemX') + self.write_uint8(font.bbx.height, 'ppemY') + self.write_uint8(1, 'bitDepth') + self.write_uint8(1, 'flags') # small metrics are horizontal + # indexSubTableArray + self.write_uint16(0, 'firstGlyphIndex') + self.write_uint16(len(font.chars) - 1, 'lastGlyphIndex') + self.write_uint32(BLOC_INDEX_ARRAY_SIZE, 'additionalOffsetToIndexSubtable') + # indexSubtableHeader + self.write_uint16(1 if font.proportional else 2, 'indexFormat') + self.write_uint16(1, 'imageFormat') # BDAT -> small metrics, byte-aligned + self.write_uint32(BDAT_HEADER_SIZE, 'imageDataOffset') + # indexSubtable data + if font.proportional: + offset = 0 + + for char in font.chars: + self.write_uint32(offset, 'offsetArray[]') + offset += BDAT.get_char_size(char) + + self.write_uint32(offset, 'offsetArray[]') + else: + self.write_uint32(BDAT.get_char_size(font.chars[0]), 'imageSize') + self.write_uint8(font.bbx.height, 'height') + self.write_uint8(font.bbx.width, 'width') + self.write_int8(0, 'horiBearingX') + self.write_int8(font.px_ascender, 'horiBearingY') + self.write_uint8(font.bbx.width, 'horiAdvance') + self.write_int8(-(font.bbx.width >> 1), 'vertBearingX') + self.write_int8(0, 'vertBearingY') + self.write_uint8(font.bbx.height, 'vertAdvance') + # adjust + self.rewrite_uint32(self.size - BLOC_PREFIX_SIZE, BLOC_TABLE_SIZE_OFFSET) + + +# -- OS/2 -- +OS_2_TABLE_SIZE = 96 + +class OS_2(Table): # pylint: disable=invalid-name + def __init__(self, font): + Table.__init__(self, 'OS/2') + # Version 4 + x_avg_char_width = font.em_scale(font.avg_width) # otb1get.x_avg_char_width(font) + ul_char_ranges = otb1get.ul_char_ranges(font) + ul_code_pages = otb1get.ul_code_pages(font) if font.bmp_only else [0, 0] + # mostly from FontForge + script_xsize = font.em_scale(30, 100) + script_ysize = font.em_scale(40, 100) + subscript_yoff = script_ysize >> 1 + xfactor = math.tan(font.italic_angle * math.pi / 180) + subscript_xoff = 0 # stub, no overlapping characters yet + superscript_yoff = font.em_ascender - script_ysize + superscript_xoff = -round(xfactor * superscript_yoff) + # write + self.write_uint16(4, 'version') + self.write_int16(x_avg_char_width, 'xAvgCharWidth') + self.write_uint16(700 if font.bold else 400, 'usWeightClass') + self.write_uint16(5, 'usWidthClass') # medium + self.write_int16(0, 'fsType') + self.write_int16(script_xsize, 'ySubscriptXSize') + self.write_int16(script_ysize, 'ySubscriptYSize') + self.write_int16(subscript_xoff, 'ySubscriptXOffset') + self.write_int16(subscript_yoff, 'ySubscriptYOffset') + self.write_int16(script_xsize, 'ySuperscriptXSize') + self.write_int16(script_ysize, 'ySuperscriptYSize') + self.write_int16(superscript_xoff, 'ySuperscriptXOffset') + self.write_int16(superscript_yoff, 'ySuperscriptYOffset') + self.write_int16(font.line_size, 'yStrikeoutSize') + self.write_int16(font.em_scale(25, 100), 'yStrikeoutPosition') + self.write_int16(0, 'sFamilyClass') # no classification + self.write_uint8(2, 'bFamilyType') # text and display + self.write_uint8(0, 'bSerifStyle') # any + self.write_uint8(8 if font.bold else 6, 'bWeight') + self.write_uint8(3 if font.proportional else 9, 'bProportion') + self.write_uint8(0, 'bContrast') + self.write_uint8(0, 'bStrokeVariation') + self.write_uint8(0, 'bArmStyle') + self.write_uint8(0, 'bLetterform') + self.write_uint8(0, 'bMidline') + self.write_uint8(0, 'bXHeight') + self.write_uint32(ul_char_ranges[0], 'ulCharRange1') + self.write_uint32(ul_char_ranges[1], 'ulCharRange2') + self.write_uint32(ul_char_ranges[2], 'ulCharRange3') + self.write_uint32(ul_char_ranges[3], 'ulCharRange4') + self.write_uint32(0x586F7334, 'achVendID') # 'Xos4' + self.write_uint16(OS_2.fs_selection(font), 'fsSelection') + self.write_uint16(min(font.min_code, fnutil.UNICODE_BMP_MAX), 'firstChar') + self.write_uint16(min(font.max_code, fnutil.UNICODE_BMP_MAX), 'lastChar') + self.write_int16(font.em_ascender, 'sTypoAscender') + self.write_int16(font.em_descender, 'sTypoDescender') + self.write_int16(font.params.line_gap, 'sTypoLineGap') + self.write_uint16(font.em_ascender, 'usWinAscent') + self.write_uint16(-font.em_descender, 'usWinDescent') + self.write_uint32(ul_code_pages[0], 'ulCodePageRange1') + self.write_uint32(ul_code_pages[1], 'ulCodePageRange2') + self.write_int16(font.em_scale(font.px_ascender * 0.6), 'sxHeight') # stub + self.write_int16(font.em_scale(font.px_ascender * 0.8), 'sCapHeight') # stub + self.write_uint16(OS_2.default_char(font), 'usDefaultChar') + self.write_uint16(OS_2.break_char(font), 'usBreakChar') + self.write_uint16(1, 'usMaxContext') + # check + self.check_size(OS_2_TABLE_SIZE) + + + @staticmethod + def break_char(font): + return 0x20 if next((char for char in font.chars if char.code == 0x20), None) else font.min_code + + + @staticmethod + def default_char(font): + if font.default_code != -1 and font.default_code <= fnutil.UNICODE_BMP_MAX: + return font.default_code + + return 0 if font.min_code == 0 else font.max_code + + + @staticmethod + def fs_selection(font): + fs_selection = int(font.bold) * 5 + int(font.italic) + return fs_selection if fs_selection != 0 else 0x40 if font.xlfd[bdf.XLFD.SLANT] == 'R' else 0 + + +# -- cmap -- +CMAP_4_PREFIX_SIZE = 12 +CMAP_4_FORMAT_SIZE = 16 +CMAP_4_SEGMENT_SIZE = 8 + +CMAP_12_PREFIX_SIZE = 20 +CMAP_12_FORMAT_SIZE = 16 +CMAP_12_GROUP_SIZE = 12 + +class CMapRange: + def __init__(self, glyph_index=0, start_code=0, final_code=-2): + self.glyph_index = glyph_index + self.start_code = start_code + self.final_code = final_code + + + @property + def id_delta(self): + return (self.glyph_index - self.start_code) & 0xFFFF + + +class CMAP(Table): + def __init__(self, font): + Table.__init__(self, 'cmap') + # make ranges + ranges = [] + range = CMapRange() + index = -1 + + for char in font.chars: + index += 1 + code = char.code + + if code == range.final_code + 1: + range.final_code += 1 + else: + range = CMapRange(index, code, code) + ranges.append(range) + # write + if font.bmp_only: + if font.max_code < 0xFFFF: + ranges.append(CMapRange(0, 0xFFFF, 0xFFFF)) + + self.write_format_4(ranges) + else: + self.write_format_12(ranges) + + + def write_format_4(self, ranges): + # index + self.write_uint16(0, 'version') + self.write_uint16(1, 'numberSubtables') + # encoding subtables index + self.write_uint16(3, 'platformID') # Microsoft + self.write_uint16(1, 'platformSpecificID') # Unicode BMP (UCS-2) + self.write_uint32(CMAP_4_PREFIX_SIZE, 'offset') # for Unicode BMP (UCS-2) + # cmap format 4 + seg_count = len(ranges) + subtable_size = CMAP_4_FORMAT_SIZE + seg_count * CMAP_4_SEGMENT_SIZE + search_range = 2 << math.floor(math.log2(seg_count)) + + self.write_uint16(4, 'format') + self.write_uint16(subtable_size, 'length') + self.write_uint16(0, 'language') # none/independent + self.write_uint16(seg_count * 2, 'segCountX2') + self.write_uint16(search_range, 'searchRange') + self.write_uint16(int(math.log2(search_range / 2)), 'entrySelector') + self.write_uint16((seg_count * 2) - search_range, 'rangeShift') + for range in ranges: + self.write_uint16(range.final_code, 'endCode') + self.write_uint16(0, 'reservedPad') + for range in ranges: + self.write_uint16(range.start_code, 'startCode') + for range in ranges: + self.write_uint16(range.id_delta, 'idDelta') + for _ in ranges: + self.write_uint16(0, 'idRangeOffset') + # check + self.check_size(CMAP_4_PREFIX_SIZE + subtable_size) + + + def write_format_12(self, ranges): + # index + self.write_uint16(0, 'version') + self.write_uint16(2, 'numberSubtables') + # encoding subtables + self.write_uint16(0, 'platformID') # Unicode + self.write_uint16(4, 'platformSpecificID') # Unicode 2.0+ full range + self.write_uint32(CMAP_12_PREFIX_SIZE, 'offset') # for Unicode 2.0+ full range + self.write_uint16(3, 'platformID') # Microsoft + self.write_uint16(10, 'platformSpecificID') # Unicode UCS-4 + self.write_uint32(CMAP_12_PREFIX_SIZE, 'offset') # for Unicode UCS-4 + # cmap format 12 + subtable_size = CMAP_12_FORMAT_SIZE + len(ranges) * CMAP_12_GROUP_SIZE + + self.write_fixed(12, 'format') + self.write_uint32(subtable_size, 'length') + self.write_uint32(0, 'language') # none/independent + self.write_uint32(len(ranges), 'nGroups') + for range in ranges: + self.write_uint32(range.start_code, 'startCharCode') + self.write_uint32(range.final_code, 'endCharCode') + self.write_uint32(range.glyph_index, 'startGlyphID') + # check + self.check_size(CMAP_12_PREFIX_SIZE + subtable_size) + + +# -- glyf -- +class GLYF(Table): + def __init__(self, _font): + Table.__init__(self, 'glyf') + + +# -- head -- +HEAD_TABLE_SIZE = 54 +HEAD_CHECKSUM_OFFSET = 8 + +class HEAD(Table): + def __init__(self, font): + Table.__init__(self, 'head') + self.write_fixed(1, 'version') + self.write_fixed(1, 'fontRevision') + self.write_uint32(0, 'checksumAdjustment') # adjusted later + self.write_uint32(0x5F0F3CF5, 'magicNumber') + self.write_uint16(HEAD.flags(font), 'flags') + self.write_uint16(font.params.em_size, 'unitsPerEm') + self.write_uint64(font.created, 'created') + self.write_uint64(font.modified, 'modified') + self.write_int16(0, 'xMin') + self.write_int16(font.em_descender, 'yMin') + self.write_int16(font.em_max_width, 'xMax') + self.write_int16(font.em_ascender, 'yMax') + self.write_uint16(font.mac_style, 'macStyle') + self.write_uint16(font.params.low_ppem or font.bbx.height, 'lowestRecPPEM') + self.write_int16(font.params.dir_hint, 'fontDirectionHint') + self.write_int16(0, 'indexToLocFormat') # short + self.write_int16(0, 'glyphDataFormat') # current + # check + self.check_size(HEAD_TABLE_SIZE) + + + @staticmethod + def flags(font): + return 0x20B if otb1get.contains_rtl(font) else 0x0B # y0 base, x0 lsb, scale int + + +# -- hhea -- +HHEA_TABLE_SIZE = 36 + +class HHEA(Table): + def __init__(self, font): + Table.__init__(self, 'hhea') + self.write_fixed(1, 'version') + self.write_int16(font.em_ascender, 'ascender') + self.write_int16(font.em_descender, 'descender') + self.write_int16(font.params.line_gap, 'lineGap') + self.write_uint16(font.em_max_width, 'advanceWidthMax') + self.write_int16(0, 'minLeftSideBearing') + self.write_int16(0, 'minRightSideBearing') + self.write_int16(font.x_max_extent, 'xMaxExtent') + self.write_int16(100 if font.italic else 1, 'caretSlopeRise') + self.write_int16(20 if font.italic else 0, 'caretSlopeRun') + self.write_int16(0, 'caretOffset') + self.write_int16(0, 'reserved') + self.write_int16(0, 'reserved') + self.write_int16(0, 'reserved') + self.write_int16(0, 'reserved') + self.write_int16(0, 'metricDataFormat') # current + self.write_uint16(len(font.chars), 'numOfLongHorMetrics') + # check + self.check_size(HHEA_TABLE_SIZE) + + +# -- hmtx -- +class HMTX(Table): + def __init__(self, font): + Table.__init__(self, 'hmtx') + for char in font.chars: + self.write_uint16(font.em_scale_width(char), 'advanceWidth') + self.write_int16(0, 'leftSideBearing') + + +# -- loca -- +class LOCA(Table): + def __init__(self, font): + Table.__init__(self, 'loca') + if not font.params.single_loca: + for _ in font.chars: + self.write_uint16(0, 'offset') + self.write_uint16(0, 'offset') + + +# -- maxp -- +MAXP_TABLE_SIZE = 32 + +class MAXP(Table): + def __init__(self, font): + Table.__init__(self, 'maxp') + self.write_fixed(1, 'version') + self.write_uint16(len(font.chars), 'numGlyphs') + self.write_uint16(0, 'maxPoints') + self.write_uint16(0, 'maxContours') + self.write_uint16(0, 'maxComponentPoints') + self.write_uint16(0, 'maxComponentContours') + self.write_uint16(2, 'maxZones') + self.write_uint16(0, 'maxTwilightPoints') + self.write_uint16(1, 'maxStorage') + self.write_uint16(1, 'maxFunctionDefs') + self.write_uint16(0, 'maxInstructionDefs') + self.write_uint16(64, 'maxStackElements') + self.write_uint16(0, 'maxSizeOfInstructions') + self.write_uint16(0, 'maxComponentElements') + self.write_uint16(0, 'maxComponentDepth') + # check + self.check_size(MAXP_TABLE_SIZE) + + +# -- name -- +@unique # pylint: disable=invalid-name +class NAME_ID(IntEnum): # pylint: disable=invalid-name + COPYRIGHT = 0 + FONT_FAMILY = 1 + FONT_SUBFAMILY = 2 + UNIQUE_SUBFAMILY = 3 + FULL_FONT_NAME = 4 + LICENSE = 14 + +NAME_HEADER_SIZE = 6 +NAME_RECORD_SIZE = 12 + +class NAME(Table): + def __init__(self, font): + Table.__init__(self, 'name') + # compute names + names = OrderedDict() + copyright = font.props.get('COPYRIGHT') + + if copyright is not None: + names[NAME_ID.COPYRIGHT] = fnutil.unquote(copyright) + + family = font.xlfd[bdf.XLFD.FAMILY_NAME] + style = [b'Regular', b'Bold', b'Italic', b'Bold Italic'][font.mac_style] + + names[NAME_ID.FONT_FAMILY] = family + names[NAME_ID.FONT_SUBFAMILY] = style + names[NAME_ID.UNIQUE_SUBFAMILY] = b'%s %s bitmap height %d' % (family, style, font.bbx.height) + names[NAME_ID.FULL_FONT_NAME] = b'%s %s' % (family, style) + + license = font.props.get('LICENSE') + notice = font.props.get('NOTICE') + + if license is None and notice is not None and b'license' in notice.lower(): + license = notice + + if license is not None: + names[NAME_ID.LICENSE] = fnutil.unquote(license) + + # header + count = len(names) * (1 + 1) # Unicode + Microsoft + string_offset = NAME_HEADER_SIZE + NAME_RECORD_SIZE * count + + self.write_uint16(0, 'format') + self.write_uint16(count, 'count') + self.write_uint16(string_offset, 'stringOffset') + # name records / create values + values = Table('name') + + for [name_id, bstr] in names.items(): + s = font.decode(bstr) + value = codecs.encode(s, 'utf_16_be') + bmp = font.bmp_only and len(value) == len(s) * 2 + # Unicode + self.write_uint16(0, 'platformID') # Unicode + self.write_uint16(3 if bmp else 4, 'platformSpecificID') + self.write_uint16(0, 'languageID') # none + self.write_uint16(name_id, 'nameID') + self.write_uint16(len(value), 'length') # in bytes + self.write_uint16(values.size, 'offset') + # Microsoft + self.write_uint16(3, 'platformID') # Microsoft + self.write_uint16(1 if bmp else 10, 'platformSpecificID') + self.write_uint16(font.params.w_lang_id, 'languageID') + self.write_uint16(name_id, 'nameID') + self.write_uint16(len(value), 'length') # in bytes + self.write_uint16(values.size, 'offset') + # value + values.write(value) + + # write values + self.write_table(values) + # check + self.check_size(string_offset + values.size) + + +# -- post -- +POST_TABLE_SIZE = 32 + +class POST(Table): + def __init__(self, font): + Table.__init__(self, 'post') + self.write_fixed(2 if font.params.post_names else 3, 'format') + self.write_fixed(font.italic_angle, 'italicAngle') + self.write_int16(font.underline_position, 'underlinePosition') + self.write_int16(font.line_size, 'underlineThickness') + self.write_uint32(0 if font.proportional else 1, 'isFixedPitch') + self.write_uint32(0, 'minMemType42') + self.write_uint32(0, 'maxMemType42') + self.write_uint32(0, 'minMemType1') + self.write_uint32(0, 'maxMemType1') + # names + if font.params.post_names: + self.write_uint16(len(font.chars), 'numberOfGlyphs') + post_names = otb1get.post_mac_names() + post_mac_count = len(post_names) + + for name in [char.props['STARTCHAR'] for char in font.chars]: + if name in post_names: + self.write_uint16(post_names.index(name), 'glyphNameIndex') + else: + self.write_uint16(len(post_names), 'glyphNameIndex') + post_names.append(name) + + for name in post_names[post_mac_count:]: + self.write_uint8(len(name), 'glyphNameLength') + self.write(name) + # check + else: + self.check_size(POST_TABLE_SIZE) + + +# -- SFNT -- +SFNT_HEADER_SIZE = 12 +SFNT_RECORD_SIZE = 16 +SFNT_SUBTABLES = (BDAT, BLOC, OS_2, CMAP, GLYF, HEAD, HHEA, HMTX, LOCA, MAXP, NAME, POST) + +class SFNT(Table): + def __init__(self, font): + Table.__init__(self, 'SFNT') + # create tables + tables = [] + for ctor in SFNT_SUBTABLES: + tables.append(ctor(font)) + # header + num_tables = len(tables) + entry_selector = math.floor(math.log2(num_tables)) + search_range = 16 << entry_selector + content_offset = SFNT_HEADER_SIZE + num_tables * SFNT_RECORD_SIZE + offset = content_offset + content = Table('SFNT') + head_checksum_offset = -1 + + self.write_fixed(1, 'sfntVersion') + self.write_uint16(num_tables, 'numTables') + self.write_uint16(search_range, 'searchRange') + self.write_uint16(entry_selector, 'entrySelector') + self.write_uint16(num_tables * 16 - search_range, 'rangeShift') + # table records / create content + for table in tables: + self.write(bytes(table.table_name, 'ascii')) + self.write_uint32(table.checksum(), 'checkSum') + self.write_uint32(offset, 'offset') + self.write_uint32(len(table.data), 'length') + # create content + if table.table_name == 'head': + head_checksum_offset = offset + HEAD_CHECKSUM_OFFSET + + padded_data = table.data + table.padding + content.write(padded_data) + offset += len(padded_data) + # write content + self.write_table(content) + # check + self.check_size(content_offset + len(content.data)) + # adjust + if head_checksum_offset != -1: + self.rewrite_uint32((0xB1B0AFBA - self.checksum()) & 0xFFFFFFFF, head_checksum_offset) diff --git a/bin/otb1get.js b/bin/otb1get.js new file mode 100644 index 000000000000..b4a058316cdb --- /dev/null +++ b/bin/otb1get.js @@ -0,0 +1,706 @@ +/* + Copyright (C) 2018-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +'use strict'; + +const fnutil = require('./fnutil.js'); + +// -- xAvgCharWidth -- +const WEIGHT_FACTORS = [ + [ 'a', 64 ], + [ 'b', 14 ], + [ 'c', 27 ], + [ 'd', 35 ], + [ 'e', 100 ], + [ 'f', 20 ], + [ 'g', 14 ], + [ 'h', 42 ], + [ 'i', 63 ], + [ 'j', 3 ], + [ 'k', 6 ], + [ 'l', 35 ], + [ 'm', 20 ], + [ 'n', 56 ], + [ 'o', 56 ], + [ 'p', 17 ], + [ 'q', 4 ], + [ 'r', 49 ], + [ 's', 56 ], + [ 't', 71 ], + [ 'u', 31 ], + [ 'v', 10 ], + [ 'w', 18 ], + [ 'x', 3 ], + [ 'y', 18 ], + [ 'z', 2 ], + [ ' ', 166 ] +]; + +function xAvgCharWidth(font) { + let xAvgTotalWidth = 0; + + for (let factor of WEIGHT_FACTORS) { + const char = font.chars.find(_char => _char.code === factor[0].charCodeAt(0)); + + if (char == null) { + return 0; + } + xAvgTotalWidth += font.scaleWidth(char) * factor[1]; + } + + return fnutil.round(xAvgTotalWidth / 1000); +} + +// -- ulCharRanges -- +const CHAR_RANGES = [ + [ 0, 0x0000, 0x007F ], + [ 1, 0x0080, 0x00FF ], + [ 2, 0x0100, 0x017F ], + [ 3, 0x0180, 0x024F ], + [ 4, 0x0250, 0x02AF ], + [ 4, 0x1D00, 0x1D7F ], + [ 4, 0x1D80, 0x1DBF ], + [ 5, 0x02B0, 0x02FF ], + [ 5, 0xA700, 0xA71F ], + [ 6, 0x0300, 0x036F ], + [ 6, 0x1DC0, 0x1DFF ], + [ 7, 0x0370, 0x03FF ], + [ 8, 0x2C80, 0x2CFF ], + [ 9, 0x0400, 0x04FF ], + [ 9, 0x0500, 0x052F ], + [ 9, 0x2DE0, 0x2DFF ], + [ 9, 0xA640, 0xA69F ], + [ 10, 0x0530, 0x058F ], + [ 11, 0x0590, 0x05FF ], + [ 12, 0xA500, 0xA63F ], + [ 13, 0x0600, 0x06FF ], + [ 13, 0x0750, 0x077F ], + [ 14, 0x07C0, 0x07FF ], + [ 15, 0x0900, 0x097F ], + [ 16, 0x0980, 0x09FF ], + [ 17, 0x0A00, 0x0A7F ], + [ 18, 0x0A80, 0x0AFF ], + [ 19, 0x0B00, 0x0B7F ], + [ 20, 0x0B80, 0x0BFF ], + [ 21, 0x0C00, 0x0C7F ], + [ 22, 0x0C80, 0x0CFF ], + [ 23, 0x0D00, 0x0D7F ], + [ 24, 0x0E00, 0x0E7F ], + [ 25, 0x0E80, 0x0EFF ], + [ 26, 0x10A0, 0x10FF ], + [ 26, 0x2D00, 0x2D2F ], + [ 27, 0x1B00, 0x1B7F ], + [ 28, 0x1100, 0x11FF ], + [ 29, 0x1E00, 0x1EFF ], + [ 29, 0x2C60, 0x2C7F ], + [ 29, 0xA720, 0xA7FF ], + [ 30, 0x1F00, 0x1FFF ], + [ 31, 0x2000, 0x206F ], + [ 31, 0x2E00, 0x2E7F ], + [ 32, 0x2070, 0x209F ], + [ 33, 0x20A0, 0x20CF ], + [ 34, 0x20D0, 0x20FF ], + [ 35, 0x2100, 0x214F ], + [ 36, 0x2150, 0x218F ], + [ 37, 0x2190, 0x21FF ], + [ 37, 0x27F0, 0x27FF ], + [ 37, 0x2900, 0x297F ], + [ 37, 0x2B00, 0x2BFF ], + [ 38, 0x2200, 0x22FF ], + [ 38, 0x2A00, 0x2AFF ], + [ 38, 0x27C0, 0x27EF ], + [ 38, 0x2980, 0x29FF ], + [ 39, 0x2300, 0x23FF ], + [ 40, 0x2400, 0x243F ], + [ 41, 0x2440, 0x245F ], + [ 42, 0x2460, 0x24FF ], + [ 43, 0x2500, 0x257F ], + [ 44, 0x2580, 0x259F ], + [ 45, 0x25A0, 0x25FF ], + [ 46, 0x2600, 0x26FF ], + [ 47, 0x2700, 0x27BF ], + [ 48, 0x3000, 0x303F ], + [ 49, 0x3040, 0x309F ], + [ 50, 0x30A0, 0x30FF ], + [ 50, 0x31F0, 0x31FF ], + [ 51, 0x3100, 0x312F ], + [ 51, 0x31A0, 0x31BF ], + [ 52, 0x3130, 0x318F ], + [ 53, 0xA840, 0xA87F ], + [ 54, 0x3200, 0x32FF ], + [ 55, 0x3300, 0x33FF ], + [ 56, 0xAC00, 0xD7AF ], + [ 57, 0xD800, 0xDFFF ], + [ 58, 0x10900, 0x1091F ], + [ 59, 0x4E00, 0x9FFF ], + [ 59, 0x2E80, 0x2EFF ], + [ 59, 0x2F00, 0x2FDF ], + [ 59, 0x2FF0, 0x2FFF ], + [ 59, 0x3400, 0x4DBF ], + [ 59, 0x20000, 0x2A6DF ], + [ 59, 0x3190, 0x319F ], + [ 60, 0xE000, 0xF8FF ], + [ 61, 0x31C0, 0x31EF ], + [ 61, 0xF900, 0xFAFF ], + [ 61, 0x2F800, 0x2FA1F ], + [ 62, 0xFB00, 0xFB4F ], + [ 63, 0xFB50, 0xFDFF ], + [ 64, 0xFE20, 0xFE2F ], + [ 65, 0xFE10, 0xFE1F ], + [ 65, 0xFE30, 0xFE4F ], + [ 66, 0xFE50, 0xFE6F ], + [ 67, 0xFE70, 0xFEFF ], + [ 68, 0xFF00, 0xFFEF ], + [ 69, 0xFFF0, 0xFFFF ], + [ 70, 0x0F00, 0x0FFF ], + [ 71, 0x0700, 0x074F ], + [ 72, 0x0780, 0x07BF ], + [ 73, 0x0D80, 0x0DFF ], + [ 74, 0x1000, 0x109F ], + [ 75, 0x1200, 0x137F ], + [ 75, 0x1380, 0x139F ], + [ 75, 0x2D80, 0x2DDF ], + [ 76, 0x13A0, 0x13FF ], + [ 77, 0x1400, 0x167F ], + [ 78, 0x1680, 0x169F ], + [ 79, 0x16A0, 0x16FF ], + [ 80, 0x1780, 0x17FF ], + [ 80, 0x19E0, 0x19FF ], + [ 81, 0x1800, 0x18AF ], + [ 82, 0x2800, 0x28FF ], + [ 83, 0xA000, 0xA48F ], + [ 83, 0xA490, 0xA4CF ], + [ 84, 0x1700, 0x171F ], + [ 84, 0x1720, 0x173F ], + [ 84, 0x1740, 0x175F ], + [ 84, 0x1760, 0x177F ], + [ 85, 0x10300, 0x1032F ], + [ 86, 0x10330, 0x1034F ], + [ 87, 0x10400, 0x1044F ], + [ 88, 0x1D000, 0x1D0FF ], + [ 88, 0x1D100, 0x1D1FF ], + [ 88, 0x1D200, 0x1D24F ], + [ 89, 0x1D400, 0x1D7FF ], + [ 90, 0xF0000, 0xFFFFD ], + [ 90, 0x100000, 0x10FFFD ], + [ 91, 0xFE00, 0xFE0F ], + [ 91, 0xE0100, 0xE01EF ], + [ 92, 0xE0000, 0xE007F ], + [ 93, 0x1900, 0x194F ], + [ 94, 0x1950, 0x197F ], + [ 95, 0x1980, 0x19DF ], + [ 96, 0x1A00, 0x1A1F ], + [ 97, 0x2C00, 0x2C5F ], + [ 98, 0x2D30, 0x2D7F ], + [ 99, 0x4DC0, 0x4DFF ], + [ 100, 0xA800, 0xA82F ], + [ 101, 0x10000, 0x1007F ], + [ 101, 0x10080, 0x100FF ], + [ 101, 0x10100, 0x1013F ], + [ 102, 0x10140, 0x1018F ], + [ 103, 0x10380, 0x1039F ], + [ 104, 0x103A0, 0x103DF ], + [ 105, 0x10450, 0x1047F ], + [ 106, 0x10480, 0x104AF ], + [ 107, 0x10800, 0x1083F ], + [ 108, 0x10A00, 0x10A5F ], + [ 109, 0x1D300, 0x1D35F ], + [ 110, 0x12000, 0x123FF ], + [ 110, 0x12400, 0x1247F ], + [ 111, 0x1D360, 0x1D37F ], + [ 112, 0x1B80, 0x1BBF ], + [ 113, 0x1C00, 0x1C4F ], + [ 114, 0x1C50, 0x1C7F ], + [ 115, 0xA880, 0xA8DF ], + [ 116, 0xA900, 0xA92F ], + [ 117, 0xA930, 0xA95F ], + [ 118, 0xAA00, 0xAA5F ], + [ 119, 0x10190, 0x101CF ], + [ 120, 0x101D0, 0x101FF ], + [ 121, 0x102A0, 0x102DF ], + [ 121, 0x10280, 0x1029F ], + [ 121, 0x10920, 0x1093F ], + [ 122, 0x1F030, 0x1F09F ], + [ 122, 0x1F000, 0x1F02F ] +]; + +function ulCharRanges(font) { + let charRanges = [0, 0, 0, 0]; + + font.chars.forEach(char => { + const unicode = char.code; + const range = CHAR_RANGES.find(_range => unicode >= _range[1] && unicode <= _range[2]); + + if (range != null) { + charRanges[range[0] >> 5] |= 1 << (range[0] & 0x1F); + } + }); + + if (font.maxCode >= 0x10000) { + charRanges[57 >> 5] |= 1 << (57 & 0x1F); + } + return [ charRanges[0] >>> 0, charRanges[1] >>> 0, charRanges[2] >>> 0, charRanges[3] >>> 0 ]; +} + +// -- ulCodePages -- +function ulCodePages(font) { + const spaceIndex = font.chars.findIndex(char => char.code === 0x20); + const ascii = Number(spaceIndex !== -1 && font.chars[spaceIndex + 0x5E].code === 0x7E); + const findf = (unicode) => Number(font.chars.findIndex(char => char.code === unicode) !== -1); + const graph = findf(0x2524); + const radic = findf(0x221A); + let codePages = [0, 0]; + + // conditions from FontForge + font.chars.forEach(char => { + switch (char.code) { + case 0x00DE: + codePages[0] |= (ascii) << 0; // 1252 Latin1 + break; + case 0x255A: + codePages[1] |= (ascii) << 30; // 850 WE/Latin1 + codePages[1] |= (ascii) << 31; // 437 US + break; + case 0x013D: + codePages[0] |= (ascii) << 1; // 1250 Latin 2: Eastern Europe + codePages[1] |= (ascii & graph) << 26; // 852 Latin 2 + break; + case 0x0411: + codePages[0] |= 1 << 2; // 1251 Cyrillic + codePages[1] |= (findf(0x255C) & graph) << 17; // 866 MS-DOS Russian + codePages[1] |= (findf(0x0405) & graph) << 25; // 855 IBM Cyrillic + break; + case 0x0386: + codePages[0] |= 1 << 3; // 1253 Greek + codePages[1] |= (findf(0x00BD) & graph) << 16; // 869 IBM Greek + codePages[1] |= (graph & radic) << 28; // 737 Greek; former 437 G + break; + case 0x0130: + codePages[0] |= (ascii) << 4; // 1254 Turkish + codePages[1] |= (ascii & graph) << 24; // 857 IBM Turkish + break; + case 0x05D0: + codePages[0] |= 1 << 5; // 1255 Hebrew + codePages[1] |= (graph & radic) << 21; // 862 Hebrew + break; + case 0x0631: + codePages[0] |= 1 << 6; // 1256 Arabic + codePages[1] |= (radic) << 19; // 864 Arabic + codePages[1] |= (graph) << 29; // 708 Arabic; ASMO 708 + break; + case 0x0157: + codePages[0] |= (ascii) << 7; // 1257 Windows Baltic + codePages[1] |= (ascii & graph) << 27; // 775 MS-DOS Baltic + break; + case 0x20AB: + codePages[0] |= 1 << 8; // 1258 Vietnamese + break; + case 0x0E45: + codePages[0] |= 1 << 16; // 874 Thai + break; + case 0x30A8: + codePages[0] |= 1 << 17; // 932 JIS/Japan + break; + case 0x3105: + codePages[0] |= 1 << 18; // 936 Chinese: Simplified chars + break; + case 0x3131: + codePages[0] |= 1 << 19; // 949 Korean Wansung + break; + case 0x592E: + codePages[0] |= 1 << 20; // 950 Chinese: Traditional chars + break; + case 0xACF4: + codePages[0] |= 1 << 21; // 1361 Korean Johab + break; + case 0x2030: + codePages[0] |= (findf(0x2211) & ascii) << 29; // Macintosh Character Set (Roman) + break; + case 0x2665: + codePages[0] |= (ascii) << 30; // OEM Character Set + break; + case 0x00C5: + codePages[1] |= (ascii & graph & radic) << 18; // 865 MS-DOS Nordic + break; + case 0x00E9: + codePages[1] |= (ascii & graph & radic) << 20; // 863 MS-DOS Canadian French + break; + case 0x00F5: + codePages[1] |= (ascii & graph & radic) << 23; // 860 MS-DOS Portuguese + break; + case 0x00FE: + codePages[1] |= (ascii & graph) << 22; // 861 MS-DOS Icelandic + break; + default : + if (char.code >= 0xF000 && char.code <= 0xF0FF) { + codePages[0] |= 1 << 31; // Symbol Character Set + } + break; + } + }); + + return [ codePages[0] >>> 0, codePages[1] >>> 0 ]; +} + +// -- containsRTL -- +const RTL_RANGES = [ + [ 0x05BE, 0x05BE ], + [ 0x05C0, 0x05C0 ], + [ 0x05C3, 0x05C3 ], + [ 0x05C6, 0x05C6 ], + [ 0x05D0, 0x05EA ], + [ 0x05EF, 0x05F4 ], + [ 0x0608, 0x0608 ], + [ 0x060B, 0x060B ], + [ 0x060D, 0x060D ], + [ 0x061B, 0x061C ], + [ 0x061E, 0x064A ], + [ 0x066D, 0x066F ], + [ 0x0671, 0x06D5 ], + [ 0x06E5, 0x06E6 ], + [ 0x06EE, 0x06EF ], + [ 0x06FA, 0x070D ], + [ 0x070F, 0x0710 ], + [ 0x0712, 0x072F ], + [ 0x074D, 0x07A5 ], + [ 0x07B1, 0x07B1 ], + [ 0x07C0, 0x07EA ], + [ 0x07F4, 0x07F5 ], + [ 0x07FA, 0x07FA ], + [ 0x07FE, 0x0815 ], + [ 0x081A, 0x081A ], + [ 0x0824, 0x0824 ], + [ 0x0828, 0x0828 ], + [ 0x0830, 0x083E ], + [ 0x0840, 0x0858 ], + [ 0x085E, 0x085E ], + [ 0x0860, 0x086A ], + [ 0x08A0, 0x08B4 ], + [ 0x08B6, 0x08BD ], + [ 0x200F, 0x200F ], + [ 0x202B, 0x202B ], + [ 0x202E, 0x202E ], + [ 0xFB1D, 0xFB1D ], + [ 0xFB1F, 0xFB28 ], + [ 0xFB2A, 0xFB36 ], + [ 0xFB38, 0xFB3C ], + [ 0xFB3E, 0xFB3E ], + [ 0xFB40, 0xFB41 ], + [ 0xFB43, 0xFB44 ], + [ 0xFB46, 0xFBC1 ], + [ 0xFBD3, 0xFD3D ], + [ 0xFD50, 0xFD8F ], + [ 0xFD92, 0xFDC7 ], + [ 0xFDF0, 0xFDFC ], + [ 0xFE70, 0xFE74 ], + [ 0xFE76, 0xFEFC ], + [ 0x10800, 0x10FFF ], + [ 0x1E800, 0x1EFFF ], + [ -1, 0 ] +]; + +function containsRTL(font) { + let index = 0; + + for (let char of font.chars) { + while (char.code > RTL_RANGES[index][1]) { + if (RTL_RANGES[++index][0] === -1) { + break; + } + } + if (char.code >= RTL_RANGES[index][0]) { + return 0x200; + } + } + return 0x000; +} + +// -- postMacIndex -- +const POST_MAC_NAMES = [ + '.notdef', + '.null', + 'nonmarkingreturn', + 'space', + 'exclam', + 'quotedbl', + 'numbersign', + 'dollar', + 'percent', + 'ampersand', + 'quotesingle', + 'parenleft', + 'parenright', + 'asterisk', + 'plus', + 'comma', + 'hyphen', + 'period', + 'slash', + 'zero', + 'one', + 'two', + 'three', + 'four', + 'five', + 'six', + 'seven', + 'eight', + 'nine', + 'colon', + 'semicolon', + 'less', + 'equal', + 'greater', + 'question', + 'at', + 'A', + 'B', + 'C', + 'D', + 'E', + 'F', + 'G', + 'H', + 'I', + 'J', + 'K', + 'L', + 'M', + 'N', + 'O', + 'P', + 'Q', + 'R', + 'S', + 'T', + 'U', + 'V', + 'W', + 'X', + 'Y', + 'Z', + 'bracketleft', + 'backslash', + 'bracketright', + 'asciicircum', + 'underscore', + 'grave', + 'a', + 'b', + 'c', + 'd', + 'e', + 'f', + 'g', + 'h', + 'i', + 'j', + 'k', + 'l', + 'm', + 'n', + 'o', + 'p', + 'q', + 'r', + 's', + 't', + 'u', + 'v', + 'w', + 'x', + 'y', + 'z', + 'braceleft', + 'bar', + 'braceright', + 'asciitilde', + 'Adieresis', + 'Aring', + 'Ccedilla', + 'Eacute', + 'Ntilde', + 'Odieresis', + 'Udieresis', + 'aacute', + 'agrave', + 'acircumflex', + 'adieresis', + 'atilde', + 'aring', + 'ccedilla', + 'eacute', + 'egrave', + 'ecircumflex', + 'edieresis', + 'iacute', + 'igrave', + 'icircumflex', + 'idieresis', + 'ntilde', + 'oacute', + 'ograve', + 'ocircumflex', + 'odieresis', + 'otilde', + 'uacute', + 'ugrave', + 'ucircumflex', + 'udieresis', + 'dagger', + 'degree', + 'cent', + 'sterling', + 'section', + 'bullet', + 'paragraph', + 'germandbls', + 'registered', + 'copyright', + 'trademark', + 'acute', + 'dieresis', + 'notequal', + 'AE', + 'Oslash', + 'infinity', + 'plusminus', + 'lessequal', + 'greaterequal', + 'yen', + 'mu', + 'partialdiff', + 'summation', + 'product', + 'pi', + 'integral', + 'ordfeminine', + 'ordmasculine', + 'Omega', + 'ae', + 'oslash', + 'questiondown', + 'exclamdown', + 'logicalnot', + 'radical', + 'florin', + 'approxequal', + 'Delta', + 'guillemotleft', + 'guillemotright', + 'ellipsis', + 'nonbreakingspace', + 'Agrave', + 'Atilde', + 'Otilde', + 'OE', + 'oe', + 'endash', + 'emdash', + 'quotedblleft', + 'quotedblright', + 'quoteleft', + 'quoteright', + 'divide', + 'lozenge', + 'ydieresis', + 'Ydieresis', + 'fraction', + 'currency', + 'guilsinglleft', + 'guilsinglright', + 'fi', + 'fl', + 'daggerdbl', + 'periodcentered', + 'quotesinglbase', + 'quotedblbase', + 'perthousand', + 'Acircumflex', + 'Ecircumflex', + 'Aacute', + 'Edieresis', + 'Egrave', + 'Iacute', + 'Icircumflex', + 'Idieresis', + 'Igrave', + 'Oacute', + 'Ocircumflex', + 'apple', + 'Ograve', + 'Uacute', + 'Ucircumflex', + 'Ugrave', + 'dotlessi', + 'circumflex', + 'tilde', + 'macron', + 'breve', + 'dotaccent', + 'ring', + 'cedilla', + 'hungarumlaut', + 'ogonek', + 'caron', + 'Lslash', + 'lslash', + 'Scaron', + 'scaron', + 'Zcaron', + 'zcaron', + 'brokenbar', + 'Eth', + 'eth', + 'Yacute', + 'yacute', + 'Thorn', + 'thorn', + 'minus', + 'multiply', + 'onesuperior', + 'twosuperior', + 'threesuperior', + 'onehalf', + 'onequarter', + 'threequarters', + 'franc', + 'Gbreve', + 'gbreve', + 'Idotaccent', + 'Scedilla', + 'scedilla', + 'Cacute', + 'cacute', + 'Ccaron', + 'ccaron', + 'dcroat' +]; + +function postMacNames() { + return POST_MAC_NAMES.slice(); +} + +// -- Export -- +module.exports = Object.freeze({ + xAvgCharWidth, + ulCharRanges, + ulCodePages, + containsRTL, + postMacNames +}); diff --git a/bin/otb1get.py b/bin/otb1get.py new file mode 100644 index 000000000000..65a4485f1a39 --- /dev/null +++ b/bin/otb1get.py @@ -0,0 +1,663 @@ +# +# Copyright (C) 2018-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +# + +# pylint: disable=bad-whitespace + +# -- x_avg_char_width -- +WEIGHT_FACTORS = ( + ( 'a', 64 ), + ( 'b', 14 ), + ( 'c', 27 ), + ( 'd', 35 ), + ( 'e', 100 ), + ( 'f', 20 ), + ( 'g', 14 ), + ( 'h', 42 ), + ( 'i', 63 ), + ( 'j', 3 ), + ( 'k', 6 ), + ( 'l', 35 ), + ( 'm', 20 ), + ( 'n', 56 ), + ( 'o', 56 ), + ( 'p', 17 ), + ( 'q', 4 ), + ( 'r', 49 ), + ( 's', 56 ), + ( 't', 71 ), + ( 'u', 31 ), + ( 'v', 10 ), + ( 'w', 18 ), + ( 'x', 3 ), + ( 'y', 18 ), + ( 'z', 2 ), + ( ' ', 166 ) +) + +def x_avg_char_width(font): + x_avg_total_width = 0 + + for factor in WEIGHT_FACTORS: + char = next((char for char in font.chars if char.code == ord(factor[0])), None) + + if char is None: + return 0 + + x_avg_total_width += font.scaleWidth(char) * factor[1] + + return round(x_avg_total_width / 1000) + + +# -- ul_char_ranges -- +CHAR_RANGES = ( + ( 0, 0x0000, 0x007F ), + ( 1, 0x0080, 0x00FF ), + ( 2, 0x0100, 0x017F ), + ( 3, 0x0180, 0x024F ), + ( 4, 0x0250, 0x02AF ), + ( 4, 0x1D00, 0x1D7F ), + ( 4, 0x1D80, 0x1DBF ), + ( 5, 0x02B0, 0x02FF ), + ( 5, 0xA700, 0xA71F ), + ( 6, 0x0300, 0x036F ), + ( 6, 0x1DC0, 0x1DFF ), + ( 7, 0x0370, 0x03FF ), + ( 8, 0x2C80, 0x2CFF ), + ( 9, 0x0400, 0x04FF ), + ( 9, 0x0500, 0x052F ), + ( 9, 0x2DE0, 0x2DFF ), + ( 9, 0xA640, 0xA69F ), + ( 10, 0x0530, 0x058F ), + ( 11, 0x0590, 0x05FF ), + ( 12, 0xA500, 0xA63F ), + ( 13, 0x0600, 0x06FF ), + ( 13, 0x0750, 0x077F ), + ( 14, 0x07C0, 0x07FF ), + ( 15, 0x0900, 0x097F ), + ( 16, 0x0980, 0x09FF ), + ( 17, 0x0A00, 0x0A7F ), + ( 18, 0x0A80, 0x0AFF ), + ( 19, 0x0B00, 0x0B7F ), + ( 20, 0x0B80, 0x0BFF ), + ( 21, 0x0C00, 0x0C7F ), + ( 22, 0x0C80, 0x0CFF ), + ( 23, 0x0D00, 0x0D7F ), + ( 24, 0x0E00, 0x0E7F ), + ( 25, 0x0E80, 0x0EFF ), + ( 26, 0x10A0, 0x10FF ), + ( 26, 0x2D00, 0x2D2F ), + ( 27, 0x1B00, 0x1B7F ), + ( 28, 0x1100, 0x11FF ), + ( 29, 0x1E00, 0x1EFF ), + ( 29, 0x2C60, 0x2C7F ), + ( 29, 0xA720, 0xA7FF ), + ( 30, 0x1F00, 0x1FFF ), + ( 31, 0x2000, 0x206F ), + ( 31, 0x2E00, 0x2E7F ), + ( 32, 0x2070, 0x209F ), + ( 33, 0x20A0, 0x20CF ), + ( 34, 0x20D0, 0x20FF ), + ( 35, 0x2100, 0x214F ), + ( 36, 0x2150, 0x218F ), + ( 37, 0x2190, 0x21FF ), + ( 37, 0x27F0, 0x27FF ), + ( 37, 0x2900, 0x297F ), + ( 37, 0x2B00, 0x2BFF ), + ( 38, 0x2200, 0x22FF ), + ( 38, 0x2A00, 0x2AFF ), + ( 38, 0x27C0, 0x27EF ), + ( 38, 0x2980, 0x29FF ), + ( 39, 0x2300, 0x23FF ), + ( 40, 0x2400, 0x243F ), + ( 41, 0x2440, 0x245F ), + ( 42, 0x2460, 0x24FF ), + ( 43, 0x2500, 0x257F ), + ( 44, 0x2580, 0x259F ), + ( 45, 0x25A0, 0x25FF ), + ( 46, 0x2600, 0x26FF ), + ( 47, 0x2700, 0x27BF ), + ( 48, 0x3000, 0x303F ), + ( 49, 0x3040, 0x309F ), + ( 50, 0x30A0, 0x30FF ), + ( 50, 0x31F0, 0x31FF ), + ( 51, 0x3100, 0x312F ), + ( 51, 0x31A0, 0x31BF ), + ( 52, 0x3130, 0x318F ), + ( 53, 0xA840, 0xA87F ), + ( 54, 0x3200, 0x32FF ), + ( 55, 0x3300, 0x33FF ), + ( 56, 0xAC00, 0xD7AF ), + ( 57, 0xD800, 0xDFFF ), + ( 58, 0x10900, 0x1091F ), + ( 59, 0x4E00, 0x9FFF ), + ( 59, 0x2E80, 0x2EFF ), + ( 59, 0x2F00, 0x2FDF ), + ( 59, 0x2FF0, 0x2FFF ), + ( 59, 0x3400, 0x4DBF ), + ( 59, 0x20000, 0x2A6DF ), + ( 59, 0x3190, 0x319F ), + ( 60, 0xE000, 0xF8FF ), + ( 61, 0x31C0, 0x31EF ), + ( 61, 0xF900, 0xFAFF ), + ( 61, 0x2F800, 0x2FA1F ), + ( 62, 0xFB00, 0xFB4F ), + ( 63, 0xFB50, 0xFDFF ), + ( 64, 0xFE20, 0xFE2F ), + ( 65, 0xFE10, 0xFE1F ), + ( 65, 0xFE30, 0xFE4F ), + ( 66, 0xFE50, 0xFE6F ), + ( 67, 0xFE70, 0xFEFF ), + ( 68, 0xFF00, 0xFFEF ), + ( 69, 0xFFF0, 0xFFFF ), + ( 70, 0x0F00, 0x0FFF ), + ( 71, 0x0700, 0x074F ), + ( 72, 0x0780, 0x07BF ), + ( 73, 0x0D80, 0x0DFF ), + ( 74, 0x1000, 0x109F ), + ( 75, 0x1200, 0x137F ), + ( 75, 0x1380, 0x139F ), + ( 75, 0x2D80, 0x2DDF ), + ( 76, 0x13A0, 0x13FF ), + ( 77, 0x1400, 0x167F ), + ( 78, 0x1680, 0x169F ), + ( 79, 0x16A0, 0x16FF ), + ( 80, 0x1780, 0x17FF ), + ( 80, 0x19E0, 0x19FF ), + ( 81, 0x1800, 0x18AF ), + ( 82, 0x2800, 0x28FF ), + ( 83, 0xA000, 0xA48F ), + ( 83, 0xA490, 0xA4CF ), + ( 84, 0x1700, 0x171F ), + ( 84, 0x1720, 0x173F ), + ( 84, 0x1740, 0x175F ), + ( 84, 0x1760, 0x177F ), + ( 85, 0x10300, 0x1032F ), + ( 86, 0x10330, 0x1034F ), + ( 87, 0x10400, 0x1044F ), + ( 88, 0x1D000, 0x1D0FF ), + ( 88, 0x1D100, 0x1D1FF ), + ( 88, 0x1D200, 0x1D24F ), + ( 89, 0x1D400, 0x1D7FF ), + ( 90, 0xF0000, 0xFFFFD ), + ( 90, 0x100000, 0x10FFFD ), + ( 91, 0xFE00, 0xFE0F ), + ( 91, 0xE0100, 0xE01EF ), + ( 92, 0xE0000, 0xE007F ), + ( 93, 0x1900, 0x194F ), + ( 94, 0x1950, 0x197F ), + ( 95, 0x1980, 0x19DF ), + ( 96, 0x1A00, 0x1A1F ), + ( 97, 0x2C00, 0x2C5F ), + ( 98, 0x2D30, 0x2D7F ), + ( 99, 0x4DC0, 0x4DFF ), + ( 100, 0xA800, 0xA82F ), + ( 101, 0x10000, 0x1007F ), + ( 101, 0x10080, 0x100FF ), + ( 101, 0x10100, 0x1013F ), + ( 102, 0x10140, 0x1018F ), + ( 103, 0x10380, 0x1039F ), + ( 104, 0x103A0, 0x103DF ), + ( 105, 0x10450, 0x1047F ), + ( 106, 0x10480, 0x104AF ), + ( 107, 0x10800, 0x1083F ), + ( 108, 0x10A00, 0x10A5F ), + ( 109, 0x1D300, 0x1D35F ), + ( 110, 0x12000, 0x123FF ), + ( 110, 0x12400, 0x1247F ), + ( 111, 0x1D360, 0x1D37F ), + ( 112, 0x1B80, 0x1BBF ), + ( 113, 0x1C00, 0x1C4F ), + ( 114, 0x1C50, 0x1C7F ), + ( 115, 0xA880, 0xA8DF ), + ( 116, 0xA900, 0xA92F ), + ( 117, 0xA930, 0xA95F ), + ( 118, 0xAA00, 0xAA5F ), + ( 119, 0x10190, 0x101CF ), + ( 120, 0x101D0, 0x101FF ), + ( 121, 0x102A0, 0x102DF ), + ( 121, 0x10280, 0x1029F ), + ( 121, 0x10920, 0x1093F ), + ( 122, 0x1F030, 0x1F09F ), + ( 122, 0x1F000, 0x1F02F ) +) + +def ul_char_ranges(font): + char_ranges = [0, 0, 0, 0] + + for char in font.chars: + range = next((range for range in CHAR_RANGES if range[1] <= char.code <= range[2]), None) + + if range is not None: + char_ranges[range[0] >> 5] |= 1 << (range[0] & 0x1F) + + if font.max_code >= 0x10000: + char_ranges[57 >> 5] |= 1 << (57 & 0x1F) + + return char_ranges + + +# -- ul_code_pages -- +def ul_code_pages(font): + space_index = next((index for index, char in enumerate(font.chars) if char.code == 0x20), len(font.chars)) + ascii = int(len(font.chars) >= space_index + 0x5F and font.chars[space_index + 0x5E].code == 0x7E) + findf = lambda unicode: int(next((char for char in font.chars if char.code == unicode), None) is not None) + graph = findf(0x2524) + radic = findf(0x221A) + code_pages = [0, 0] + + # conditions from FontForge + for char in font.chars: + unicode = char.code + + if unicode == 0x00DE: + code_pages[0] |= (ascii) << 0 # 1252 Latin1 + elif unicode == 0x255A: + code_pages[1] |= (ascii) << 30 # 850 WE/Latin1 + code_pages[1] |= (ascii) << 31 # 437 US + elif unicode == 0x013D: + code_pages[0] |= (ascii) << 1 # 1250 Latin 2: Eastern Europe + code_pages[1] |= (ascii & graph) << 26 # 852 Latin 2 + elif unicode == 0x0411: + code_pages[0] |= 1 << 2 # 1251 Cyrillic + code_pages[1] |= (findf(0x255C) & graph) << 17 # 866 MS-DOS Russian + code_pages[1] |= (findf(0x0405) & graph) << 25 # 855 IBM Cyrillic + elif unicode == 0x0386: + code_pages[0] |= 1 << 3 # 1253 Greek + code_pages[1] |= (findf(0x00BD) & graph) << 16 # 869 IBM Greek + code_pages[1] |= (graph & radic) << 28 # 737 Greek; former 437 G + elif unicode == 0x0130: + code_pages[0] |= (ascii) << 4 # 1254 Turkish + code_pages[1] |= (ascii & graph) << 24 # 857 IBM Turkish + elif unicode == 0x05D0: + code_pages[0] |= 1 << 5 # 1255 Hebrew + code_pages[1] |= (graph & radic) << 21 # 862 Hebrew + elif unicode == 0x0631: + code_pages[0] |= 1 << 6 # 1256 Arabic + code_pages[1] |= (radic) << 19 # 864 Arabic + code_pages[1] |= (graph) << 29 # 708 Arabic; ASMO 708 + elif unicode == 0x0157: + code_pages[0] |= (ascii) << 7 # 1257 Windows Baltic + code_pages[1] |= (ascii & graph) << 27 # 775 MS-DOS Baltic + elif unicode == 0x20AB: + code_pages[0] |= 1 << 8 # 1258 Vietnamese + elif unicode == 0x0E45: + code_pages[0] |= 1 << 16 # 874 Thai + elif unicode == 0x30A8: + code_pages[0] |= 1 << 17 # 932 JIS/Japan + elif unicode == 0x3105: + code_pages[0] |= 1 << 18 # 936 Chinese: Simplified chars + elif unicode == 0x3131: + code_pages[0] |= 1 << 19 # 949 Korean Wansung + elif unicode == 0x592E: + code_pages[0] |= 1 << 20 # 950 Chinese: Traditional chars + elif unicode == 0xACF4: + code_pages[0] |= 1 << 21 # 1361 Korean Johab + elif unicode == 0x2030: + code_pages[0] |= (findf(0x2211) & ascii) << 29 # Macintosh Character Set (Roman) + elif unicode == 0x2665: + code_pages[0] |= (ascii) << 30 # OEM Character Set + elif unicode == 0x00C5: + code_pages[1] |= (ascii & graph & radic) << 18 # 865 MS-DOS Nordic + elif unicode == 0x00E9: + code_pages[1] |= (ascii & graph & radic) << 20 # 863 MS-DOS Canadian French + elif unicode == 0x00F5: + code_pages[1] |= (ascii & graph & radic) << 23 # 860 MS-DOS Portuguese + elif unicode == 0x00FE: + code_pages[1] |= (ascii & graph) << 22 # 861 MS-DOS Icelandic + elif 0xF000 <= unicode <= 0xF0FF: + code_pages[0] |= 1 << 31 # Symbol Character Set + + return code_pages + + +# -- strong_rtl_flag -- +RTL_RANGES = ( + ( 0x05BE, 0x05BE ), + ( 0x05C0, 0x05C0 ), + ( 0x05C3, 0x05C3 ), + ( 0x05C6, 0x05C6 ), + ( 0x05D0, 0x05EA ), + ( 0x05EF, 0x05F4 ), + ( 0x0608, 0x0608 ), + ( 0x060B, 0x060B ), + ( 0x060D, 0x060D ), + ( 0x061B, 0x061C ), + ( 0x061E, 0x064A ), + ( 0x066D, 0x066F ), + ( 0x0671, 0x06D5 ), + ( 0x06E5, 0x06E6 ), + ( 0x06EE, 0x06EF ), + ( 0x06FA, 0x070D ), + ( 0x070F, 0x0710 ), + ( 0x0712, 0x072F ), + ( 0x074D, 0x07A5 ), + ( 0x07B1, 0x07B1 ), + ( 0x07C0, 0x07EA ), + ( 0x07F4, 0x07F5 ), + ( 0x07FA, 0x07FA ), + ( 0x07FE, 0x0815 ), + ( 0x081A, 0x081A ), + ( 0x0824, 0x0824 ), + ( 0x0828, 0x0828 ), + ( 0x0830, 0x083E ), + ( 0x0840, 0x0858 ), + ( 0x085E, 0x085E ), + ( 0x0860, 0x086A ), + ( 0x08A0, 0x08B4 ), + ( 0x08B6, 0x08BD ), + ( 0x200F, 0x200F ), + ( 0x202B, 0x202B ), + ( 0x202E, 0x202E ), + ( 0xFB1D, 0xFB1D ), + ( 0xFB1F, 0xFB28 ), + ( 0xFB2A, 0xFB36 ), + ( 0xFB38, 0xFB3C ), + ( 0xFB3E, 0xFB3E ), + ( 0xFB40, 0xFB41 ), + ( 0xFB43, 0xFB44 ), + ( 0xFB46, 0xFBC1 ), + ( 0xFBD3, 0xFD3D ), + ( 0xFD50, 0xFD8F ), + ( 0xFD92, 0xFDC7 ), + ( 0xFDF0, 0xFDFC ), + ( 0xFE70, 0xFE74 ), + ( 0xFE76, 0xFEFC ), + ( 0x10800, 0x10FFF ), + ( 0x1E800, 0x1EFFF ), + (-1, 0) +) + +def contains_rtl(font): + index = 0 + + for char in font.chars: + while char.code > RTL_RANGES[index][1]: + index += 1 + if RTL_RANGES[index][0] == -1: + break + + if char.code >= RTL_RANGES[index][0]: + return True + + return False + + +# -- post_mac_names -- +POST_MAC_NAMES = ( + b'.notdef', + b'.null', + b'nonmarkingreturn', + b'space', + b'exclam', + b'quotedbl', + b'numbersign', + b'dollar', + b'percent', + b'ampersand', + b'quotesingle', + b'parenleft', + b'parenright', + b'asterisk', + b'plus', + b'comma', + b'hyphen', + b'period', + b'slash', + b'zero', + b'one', + b'two', + b'three', + b'four', + b'five', + b'six', + b'seven', + b'eight', + b'nine', + b'colon', + b'semicolon', + b'less', + b'equal', + b'greater', + b'question', + b'at', + b'A', + b'B', + b'C', + b'D', + b'E', + b'F', + b'G', + b'H', + b'I', + b'J', + b'K', + b'L', + b'M', + b'N', + b'O', + b'P', + b'Q', + b'R', + b'S', + b'T', + b'U', + b'V', + b'W', + b'X', + b'Y', + b'Z', + b'bracketleft', + b'backslash', + b'bracketright', + b'asciicircum', + b'underscore', + b'grave', + b'a', + b'b', + b'c', + b'd', + b'e', + b'f', + b'g', + b'h', + b'i', + b'j', + b'k', + b'l', + b'm', + b'n', + b'o', + b'p', + b'q', + b'r', + b's', + b't', + b'u', + b'v', + b'w', + b'x', + b'y', + b'z', + b'braceleft', + b'bar', + b'braceright', + b'asciitilde', + b'Adieresis', + b'Aring', + b'Ccedilla', + b'Eacute', + b'Ntilde', + b'Odieresis', + b'Udieresis', + b'aacute', + b'agrave', + b'acircumflex', + b'adieresis', + b'atilde', + b'aring', + b'ccedilla', + b'eacute', + b'egrave', + b'ecircumflex', + b'edieresis', + b'iacute', + b'igrave', + b'icircumflex', + b'idieresis', + b'ntilde', + b'oacute', + b'ograve', + b'ocircumflex', + b'odieresis', + b'otilde', + b'uacute', + b'ugrave', + b'ucircumflex', + b'udieresis', + b'dagger', + b'degree', + b'cent', + b'sterling', + b'section', + b'bullet', + b'paragraph', + b'germandbls', + b'registered', + b'copyright', + b'trademark', + b'acute', + b'dieresis', + b'notequal', + b'AE', + b'Oslash', + b'infinity', + b'plusminus', + b'lessequal', + b'greaterequal', + b'yen', + b'mu', + b'partialdiff', + b'summation', + b'product', + b'pi', + b'integral', + b'ordfeminine', + b'ordmasculine', + b'Omega', + b'ae', + b'oslash', + b'questiondown', + b'exclamdown', + b'logicalnot', + b'radical', + b'florin', + b'approxequal', + b'Delta', + b'guillemotleft', + b'guillemotright', + b'ellipsis', + b'nonbreakingspace', + b'Agrave', + b'Atilde', + b'Otilde', + b'OE', + b'oe', + b'endash', + b'emdash', + b'quotedblleft', + b'quotedblright', + b'quoteleft', + b'quoteright', + b'divide', + b'lozenge', + b'ydieresis', + b'Ydieresis', + b'fraction', + b'currency', + b'guilsinglleft', + b'guilsinglright', + b'fi', + b'fl', + b'daggerdbl', + b'periodcentered', + b'quotesinglbase', + b'quotedblbase', + b'perthousand', + b'Acircumflex', + b'Ecircumflex', + b'Aacute', + b'Edieresis', + b'Egrave', + b'Iacute', + b'Icircumflex', + b'Idieresis', + b'Igrave', + b'Oacute', + b'Ocircumflex', + b'apple', + b'Ograve', + b'Uacute', + b'Ucircumflex', + b'Ugrave', + b'dotlessi', + b'circumflex', + b'tilde', + b'macron', + b'breve', + b'dotaccent', + b'ring', + b'cedilla', + b'hungarumlaut', + b'ogonek', + b'caron', + b'Lslash', + b'lslash', + b'Scaron', + b'scaron', + b'Zcaron', + b'zcaron', + b'brokenbar', + b'Eth', + b'eth', + b'Yacute', + b'yacute', + b'Thorn', + b'thorn', + b'minus', + b'multiply', + b'onesuperior', + b'twosuperior', + b'threesuperior', + b'onehalf', + b'onequarter', + b'threequarters', + b'franc', + b'Gbreve', + b'gbreve', + b'Idotaccent', + b'Scedilla', + b'scedilla', + b'Cacute', + b'cacute', + b'Ccaron', + b'ccaron', + b'dcroat' +) + +def post_mac_names(): + return list(POST_MAC_NAMES) diff --git a/bin/ucstoany.js b/bin/ucstoany.js index 15a4f149df7b..2be14b05f745 100644 --- a/bin/ucstoany.js +++ b/bin/ucstoany.js @@ -1,16 +1,20 @@ -// -// Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> -// -// This program is free software; you can redistribute it and/or -// modify it under the terms of the GNU General Public License as -// published by the Free Software Foundation; either version 2 of -// the License, or (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// +/* + Copyright (C) 2017-2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> + + This program is free software; you can redistribute it and/or modify it + under the terms of the GNU General Public License as published by the Free + Software Foundation; either version 2 of the License, or (at your option) + any later version. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ 'use strict'; @@ -19,7 +23,7 @@ const fncli = require('./fncli.js'); const fnio = require('./fnio.js'); const bdf = require('./bdf.js'); - +// -- Params -- class Params extends fncli.Params { constructor() { super(); @@ -29,7 +33,7 @@ class Params extends fncli.Params { } } - +// -- Options -- const HELP = ('' + 'usage: ucstoany [-f] [-F FAMILY] [-o OUTPUT] INPUT REGISTRY ENCODING TABLE...\n' + 'Generate a BDF font subset.\n' + @@ -44,7 +48,7 @@ const HELP = ('' + ' --version display the program version and license, and exit\n' + ' --excstk display the exception stack on error\n' + '\n' + - 'The input must be a BDF 2.1 font encoded in the unicode range.\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n' + 'Unlike ucs2any, all TABLE-s form a single subset of the input font.\n'); const VERSION = 'ucstoany 1.55, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE; @@ -62,7 +66,7 @@ class Options extends fncli.Options { break; case '-F': if (value.includes('-')) { - throw new Error('family name may not contain "-"'); + throw new Error('FAMILY may not contain "-"'); } params.family = value; break; @@ -75,7 +79,7 @@ class Options extends fncli.Options { } } - +// -- Main -- function mainProgram(nonopt, parsed) { if (nonopt.length < 4) { throw new Error('invalid number of arguments, try --help'); @@ -91,7 +95,7 @@ function mainProgram(nonopt, parsed) { } // READ INPUT - let ifs = new fnio.InputStream(input); + let ifs = new fnio.InputFileStream(input); try { var oldFont = bdf.Font.read(ifs); @@ -103,7 +107,7 @@ function mainProgram(nonopt, parsed) { // READ TABLES nonopt.slice(3).forEach(name => { - ifs = new fnio.InputStream(name); + ifs = new fnio.InputFileStream(name); try { ifs.readLines(line => { @@ -121,8 +125,8 @@ function mainProgram(nonopt, parsed) { } // CREATE GLYPHS - let newFont = new bdf.Font(); - let charMap = []; + const newFont = new bdf.Font(); + const charMap = []; let index = 0; let unstart = 0; @@ -135,7 +139,7 @@ function mainProgram(nonopt, parsed) { newCodes.forEach(code => { let oldChar = charMap[code]; - let uniFFFF = (oldChar == null); + const uniFFFF = (oldChar == null); if (code === 0xFFFF && parsed.filter) { index++; @@ -158,12 +162,13 @@ function mainProgram(nonopt, parsed) { } } - let newChar = Object.assign(new bdf.Char(), oldChar); + const newChar = Object.assign(new bdf.Char(), oldChar); newChar.code = index >= unstart ? code : index; index++; - newChar.props = newChar.props.clone(); - newChar.props.set('ENCODING', newChar.code.toString()); + newChar.props = new bdf.Props(); + oldChar.props.forEach((name, value) => newChar.props.set(name, value)); + newChar.props.set('ENCODING', newChar.code); newFont.chars.push(newChar); if (uniFFFF) { @@ -175,7 +180,7 @@ function mainProgram(nonopt, parsed) { // CREATE HEADER let numProps; - let family = (parsed.family !== null) ? parsed.family : oldFont.xlfd[bdf.XLFD.FAMILY_NAME]; + const family = (parsed.family != null) ? parsed.family : oldFont.xlfd[bdf.XLFD.FAMILY_NAME]; oldFont.props.forEach((name, value) => { switch (name) { @@ -200,7 +205,7 @@ function mainProgram(nonopt, parsed) { break; case 'DEFAULT_CHAR': if (newFont.defaultCode !== -1) { - value = newFont.defaultCode.toString(); + value = newFont.defaultCode; } else { numProps -= 1; return; @@ -208,24 +213,23 @@ function mainProgram(nonopt, parsed) { break; case 'ENDPROPERTIES': if (newFont.defaultCode !== -1 && newFont.props.get('DEFAULT_CHAR') == null) { - newFont.props.add('DEFAULT_CHAR', newFont.defaultCode.toString()); + newFont.props.set('DEFAULT_CHAR', newFont.defaultCode); numProps += 1; } - newFont.props.set('STARTPROPERTIES', numProps.toString()); + newFont.props.set('STARTPROPERTIES', numProps); break; case 'CHARS': - value = newFont.chars.length.toString(); + value = newFont.chars.length; break; } - newFont.props.add(name, value); + newFont.props.set(name, value); }); // COPY FIELDS newFont.bbx = oldFont.bbx; - newFont.finis = oldFont.finis; // WRITE OUTPUT - let ofs = new fnio.OutputStream(parsed.output); + let ofs = new fnio.OutputFileStream(parsed.output); try { newFont.write(ofs); @@ -236,7 +240,6 @@ function mainProgram(nonopt, parsed) { } } - if (require.main === module) { fncli.start('ucstoany.js', new Options(), new Params(), mainProgram); } diff --git a/bin/ucstoany.py b/bin/ucstoany.py index fd483e971bab..6715c2456f0c 100644 --- a/bin/ucstoany.py +++ b/bin/ucstoany.py @@ -1,15 +1,19 @@ # -# Copyright (c) 2019 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> +# Copyright (C) 2017-2020 Dimitar Toshkov Zhekov <dimitar.zhekov@gmail.com> # -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License as -# published by the Free Software Foundation; either version 2 of -# the License, or (at your option) any later version. +# This program is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the Free +# Software Foundation; either version 2 of the License, or (at your option) +# any later version. # -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY +# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License +# for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # import re @@ -20,15 +24,16 @@ import fncli import fnio import bdf - +# -- Params -- class Params(fncli.Params): def __init__(self): fncli.Params.__init__(self) - self.filter = False - self.family = None - self.output = None + self.filter_ffff = False + self.family_name = None + self.output_name = None +# -- Options -- HELP = ('' + 'usage: ucstoany [-f] [-F FAMILY] [-o OUTPUT] INPUT REGISTRY ENCODING TABLE...\n' + 'Generate a BDF font subset.\n' + @@ -43,11 +48,10 @@ HELP = ('' + ' --version display the program version and license, and exit\n' + ' --excstk display the exception stack on error\n' + '\n' + - 'The input must be a BDF 2.1 font encoded in the unicode range.\n' + - 'Any COMMENT-s are discarded, duplicate properties are collapsed.\n' + + 'The input must be a BDF 2.1 font with unicode encoding.\n' + 'Unlike ucs2any, all TABLE-s form a single subset of the input font.\n') -VERSION = 'ucstoany 1.55, Copyright (C) 2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE +VERSION = 'ucstoany 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE class Options(fncli.Options): def __init__(self): @@ -56,25 +60,24 @@ class Options(fncli.Options): def parse(self, name, value, params): if name in ['-f', '--filter']: - params.filter = True + params.filter_ffff = True elif name == '-F': - params.family = bytes(value, 'ascii') + params.family_name = bytes(value, 'ascii') if '-' in value: - raise Exception('family name may not contain "-"') + raise Exception('FAMILY may not contain "-"') elif name == '-o': - params.output = value + params.output_name = value else: self.fallback(name, params) +# -- Main -- def main_program(nonopt, parsed): - bstr = lambda number: bytes(str(number), 'ascii') - # NON-OPTIONS if len(nonopt) < 4: raise Exception('invalid number of arguments, try --help') - input = nonopt[0] + input_name = nonopt[0] registry = nonopt[1] encoding = nonopt[2] new_codes = [] @@ -83,26 +86,14 @@ def main_program(nonopt, parsed): raise Exception('invalid registry or encoding') # READ INPUT - ifs = fnio.InputStream(input) - - try: - old_font = bdf.Font.read(ifs) - ifs.close() - except Exception as ex: - raise Exception(ifs.location() + str(ex)) + old_font = fnio.read_file(input_name, bdf.Font.read) # READ TABLES def load_code(line): new_codes.append(fnutil.parse_hex('unicode', line)) - for name in nonopt[3:]: - ifs = fnio.InputStream(name) - - try: - ifs.read_lines(load_code) - ifs.close() - except Exception as ex: - raise Exception(ifs.location() + str(ex)) + for table_name in nonopt[3:]: + fnio.read_file(table_name, lambda ifs: ifs.read_lines(load_code)) if not new_codes: raise Exception('no characters in the output font') @@ -112,13 +103,13 @@ def main_program(nonopt, parsed): charmap = {char.code:char for char in old_font.chars} index = 0 unstart = 0 - family = parsed.family if parsed.family is not None else old_font.xlfd[bdf.XLFD.FAMILY_NAME] + family = parsed.family_name if parsed.family_name is not None else old_font.xlfd[bdf.XLFD.FAMILY_NAME] - if parsed.filter: + if parsed.filter_ffff: unstart = 32 if registry == 'ISO10646' else bdf.CHARS_MAX for code in new_codes: - if code == 0xFFFF and parsed.filter: + if code == 0xFFFF and parsed.filter_ffff: index += 1 continue @@ -141,8 +132,8 @@ def main_program(nonopt, parsed): new_char = copy.copy(old_char) new_char.code = code if index >= unstart else index index += 1 - new_char.props = new_char.props.clone() - new_char.props.set('ENCODING', bstr(new_char.code)) + new_char.props = copy.copy(old_char.props) + new_char.props.set('ENCODING', new_char.code) new_font.chars.append(new_char) if uni_ffff: @@ -171,33 +162,26 @@ def main_program(nonopt, parsed): value = fnutil.quote(encoding) elif name == 'DEFAULT_CHAR': if new_font.default_code != -1: - value = bstr(new_font.default_code) + value = new_font.default_code else: num_props -= 1 continue elif name == 'ENDPROPERTIES': if new_font.default_code != -1 and new_font.props.get('DEFAULT_CHAR') is None: - new_font.props.add('DEFAULT_CHAR', bstr(new_font.default_code)) + new_font.props.set('DEFAULT_CHAR', new_font.default_code) num_props += 1 - new_font.props.set('STARTPROPERTIES', bstr(num_props)) + new_font.props.set('STARTPROPERTIES', num_props) elif name == 'CHARS': - value = bstr(len(new_font.chars)) + value = len(new_font.chars) - new_font.props.add(name, value) + new_font.props.set(name, value) # COPY FIELDS new_font.bbx = old_font.bbx - new_font.finis = old_font.finis # WRITE OUTPUT - ofs = fnio.OutputStream(parsed.output) - - try: - new_font.write(ofs) - ofs.close() - except Exception as ex: - raise Exception(ofs.location() + str(ex) + ofs.destroy()) + fnio.write_file(parsed.output_name, lambda ofs: new_font.write(ofs)) if __name__ == '__main__': |
