aboutsummaryrefslogtreecommitdiff
path: root/bin
diff options
context:
space:
mode:
Diffstat (limited to 'bin')
-rw-r--r--bin/.eslintrc.js266
-rw-r--r--bin/bdf.js260
-rw-r--r--bin/bdf.py224
-rw-r--r--bin/bdfcheck.js455
-rw-r--r--bin/bdfcheck.py380
-rw-r--r--bin/bdfexp.js287
-rw-r--r--bin/bdfexp.py245
-rw-r--r--bin/bdftofnt.js109
-rw-r--r--bin/bdftofnt.py148
-rw-r--r--bin/bdftopsf.js65
-rw-r--r--bin/bdftopsf.py125
-rw-r--r--bin/bmpf.js159
-rw-r--r--bin/bmpf.py147
-rw-r--r--bin/fncli.js51
-rw-r--r--bin/fncli.py32
-rw-r--r--bin/fnio.js95
-rw-r--r--bin/fnio.py135
-rw-r--r--bin/fnutil.js74
-rw-r--r--bin/fnutil.py62
-rw-r--r--bin/otb1cli.js129
-rw-r--r--bin/otb1cli.py99
-rw-r--r--bin/otb1exp.js895
-rw-r--r--bin/otb1exp.py808
-rw-r--r--bin/otb1get.js706
-rw-r--r--bin/otb1get.py663
-rw-r--r--bin/ucstoany.js73
-rw-r--r--bin/ucstoany.py96
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__':