First jameswitzema.net commit

This commit is contained in:
Lao Tzu
2026-05-29 11:30:10 -07:00
commit 32cc108c4a
200 changed files with 1200635 additions and 0 deletions
+135
View File
@@ -0,0 +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'
}
};
+425
View File
@@ -0,0 +1,425 @@
[MASTER]
# A comma-separated list of package or module names from where C extensions may
# be loaded. Extensions are loading into the active Python interpreter and may
# run arbitrary code
extension-pkg-whitelist=
# Add files or directories to the blacklist. They should be base names, not
# paths.
ignore=CVS
# Add files or directories matching the regex patterns to the blacklist. The
# regex matches against base names, not paths.
ignore-patterns=
# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
#init-hook=
# Use multiple processes to speed up Pylint.
jobs=4
# List of plugins (as comma separated values of python modules names) to load,
# usually to register additional checkers.
load-plugins=
# Pickle collected data for later comparisons.
persistent=yes
# Specify a configuration file.
#rcfile=
# Allow loading of arbitrary C extensions. Extensions are imported into the
# active Python interpreter and may run arbitrary code.
unsafe-load-any-extension=no
[MESSAGES CONTROL]
# Only show warnings with the listed confidence levels. Leave empty to show
# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED
confidence=
# Disable the message, report, category or checker with the given id(s). You
# can either give multiple identifiers separated by comma (,) or put this
# option multiple times (only on the command line, not in the configuration
# file where it should appear only once).You can also use "--disable=all" to
# disable everything first and then reenable specific checks. For example, if
# you want to run only the similarities checker, you can use "--disable=all
# --enable=similarities". If you want to run only the classes checker, but have
# no Warning level messages displayed, use"--disable=all --enable=classes
# --disable=W"
disable=print-statement,parameter-unpacking,unpacking-in-except,old-raise-syntax,backtick,long-suffix,old-ne-operator,old-octal-literal,import-star-module-level,raw-checker-failed,bad-inline-option,locally-disabled,locally-enabled,file-ignored,suppressed-message,useless-suppression,deprecated-pragma,apply-builtin,basestring-builtin,buffer-builtin,cmp-builtin,coerce-builtin,execfile-builtin,file-builtin,long-builtin,raw_input-builtin,reduce-builtin,standarderror-builtin,unicode-builtin,xrange-builtin,coerce-method,delslice-method,getslice-method,setslice-method,no-absolute-import,old-division,dict-iter-method,dict-view-method,next-method-called,metaclass-assignment,indexing-exception,raising-string,reload-builtin,oct-method,hex-method,nonzero-method,cmp-method,input-builtin,round-builtin,intern-builtin,unichr-builtin,map-builtin-not-iterating,zip-builtin-not-iterating,range-builtin-not-iterating,filter-builtin-not-iterating,using-cmp-argument,eq-without-hash,div-method,idiv-method,rdiv-method,exception-message-attribute,invalid-str-codec,sys-max-int,bad-python3-import,deprecated-string-function,deprecated-str-translate-call,bad-continuation,missing-docstring,redefined-builtin,unnecessary-lambda,too-few-public-methods,too-many-locals,too-many-branches,too-many-statements,broad-except,consider-using-ternary
# Enable the message, report, category or checker with the given id(s). You can
# either give multiple identifier separated by comma (,) or put this option
# multiple time (only on the command line, not in the configuration file where
# it should appear only once). See also the "--disable" option for examples.
enable=
[REPORTS]
# Python expression which should return a note less than 10 (10 is the highest
# note). You have access to the variables errors warning, statement which
# respectively contain the number of errors / warnings messages and the total
# number of statements analyzed. This is used by the global evaluation report
# (RP0004).
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
# Template used to display messages. This is a python new-style format string
# used to format the message information. See doc for all details
#msg-template=
# Set the output format. Available formats are text, parseable, colorized, json
# and msvs (visual studio).You can also give a reporter class, eg
# mypackage.mymodule.MyReporterClass.
output-format=text
# Tells whether to display a full report or only the messages
reports=no
# Activate the evaluation score.
score=no
[REFACTORING]
# Maximum number of nested blocks for function / method body
max-nested-blocks=8
[BASIC]
# Naming hint for argument names
argument-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct argument names
argument-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Naming hint for attribute names
attr-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct attribute names
attr-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Bad variable names which should always be refused, separated by a comma
bad-names=foo,bar,baz,toto,tutu,tata
# Naming hint for class attribute names
class-attribute-name-hint=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Regular expression matching correct class attribute names
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
# Naming hint for class names
class-name-hint=[A-Z_][a-zA-Z0-9]+$
# Regular expression matching correct class names
class-rgx=[A-Z_][a-zA-Z0-9]+$
# Naming hint for constant names
const-name-hint=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Regular expression matching correct constant names
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
# Minimum line length for functions/classes that require docstrings, shorter
# ones are exempt.
docstring-min-length=-1
# Naming hint for function names
function-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct function names
function-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Good variable names which should always be accepted, separated by a comma
good-names=i,j,k,ex,Run,_,s,e,x,y,n
# Include a hint for the correct naming format with invalid-name
include-naming-hint=no
# Naming hint for inline iteration names
inlinevar-name-hint=[A-Za-z_][A-Za-z0-9_]*$
# Regular expression matching correct inline iteration names
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
# Naming hint for method names
method-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct method names
method-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Naming hint for module names
module-name-hint=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Regular expression matching correct module names
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
# Colon-delimited sets of names that determine each other's naming style when
# the name regexes allow several styles.
name-group=
# Regular expression which should only match function or class names that do
# not require a docstring.
no-docstring-rgx=^_
# List of decorators that produce properties, such as abc.abstractproperty. Add
# to this list to register other decorators that produce valid properties.
property-classes=abc.abstractproperty
# Naming hint for variable names
variable-name-hint=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
# Regular expression matching correct variable names
variable-rgx=(([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$
[FORMAT]
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
expected-line-ending-format=LF
# Regexp for a line that is allowed to be longer than the limit.
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
# Number of spaces of indent required inside a hanging or continued line.
indent-after-paren=4
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
# tab).
indent-string='\t'
# Maximum number of characters on a single line.
max-line-length=120
# Maximum number of lines in a module
max-module-lines=1000
# List of optional constructs for which whitespace checking is disabled. `dict-
# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}.
# `trailing-comma` allows a space between comma and closing bracket: (a, ).
# `empty-line` allows space-only lines.
no-space-check=trailing-comma,dict-separator
# Allow the body of a class to be on the same line as the declaration if body
# contains single statement.
single-line-class-stmt=no
# Allow the body of an if to be on the same line as the test if there is no
# else.
single-line-if-stmt=no
[LOGGING]
# Logging modules to check that the string format arguments are in logging
# function parameter format
logging-modules=logging
[MISCELLANEOUS]
# List of note tags to take in consideration, separated by a comma.
notes=FIXME,XXX,TODO
[SIMILARITIES]
# Ignore comments when computing similarities.
ignore-comments=yes
# Ignore docstrings when computing similarities.
ignore-docstrings=yes
# Ignore imports when computing similarities.
ignore-imports=no
# Minimum lines number of a similarity.
min-similarity-lines=4
[SPELLING]
# Spelling dictionary name. Available dictionaries: none. To make it working
# install python-enchant package.
spelling-dict=
# List of comma separated words that should not be checked.
spelling-ignore-words=
# A path to a file that contains private dictionary; one word per line.
spelling-private-dict-file=
# Tells whether to store unknown words to indicated private dictionary in
# --spelling-private-dict-file option instead of raising a message.
spelling-store-unknown-words=no
[TYPECHECK]
# List of decorators that produce context managers, such as
# contextlib.contextmanager. Add to this list to register other decorators that
# produce valid context managers.
contextmanager-decorators=contextlib.contextmanager
# List of members which are set dynamically and missed by pylint inference
# system, and so shouldn't trigger E1101 when accessed. Python regular
# expressions are accepted.
generated-members=
# Tells whether missing members accessed in mixin class should be ignored. A
# mixin class is detected if its name ends with "mixin" (case insensitive).
ignore-mixin-members=yes
# This flag controls whether pylint should warn about no-member and similar
# checks whenever an opaque object is returned when inferring. The inference
# can return multiple potential results while evaluating a Python object, but
# some branches might not be evaluated, which results in partial inference. In
# that case, it might be useful to still emit no-member and other checks for
# the rest of the inferred objects.
ignore-on-opaque-inference=yes
# List of class names for which member attributes should not be checked (useful
# for classes with dynamically set attributes). This supports the use of
# qualified names.
ignored-classes=optparse.Values,thread._local,_thread._local
# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis. It
# supports qualified module names, as well as Unix pattern matching.
ignored-modules=
# Show a hint with possible names when a member name was not found. The aspect
# of finding the hint is based on edit distance.
missing-member-hint=yes
# The minimum edit distance a name should have in order to be considered a
# similar match for a missing member name.
missing-member-hint-distance=1
# The total number of similar names that should be taken in consideration when
# showing a hint for a missing member.
missing-member-max-choices=1
[VARIABLES]
# List of additional names supposed to be defined in builtins. Remember that
# you should avoid to define new builtins when possible.
additional-builtins=
# Tells whether unused global variables should be treated as a violation.
allow-global-unused-variables=yes
# List of strings which can identify a callback function by name. A callback
# name must start or end with one of those strings.
callbacks=cb_,_cb
# A regular expression matching the name of dummy variables (i.e. expectedly
# not used).
dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
# Argument names that match this expression will be ignored. Default to name
# with leading underscore
ignored-argument-names=_.*|^ignored_|^unused_
# Tells whether we should check for unused import in __init__ files.
init-import=no
# List of qualified module names which can have objects that can redefine
# builtins.
redefining-builtins-modules=six.moves,future.builtins
[CLASSES]
# List of method names used to declare (i.e. assign) instance attributes.
defining-attr-methods=__init__,__new__,setUp
# List of member names, which should be excluded from the protected access
# warning.
exclude-protected=_asdict,_fields,_replace,_source,_make
# List of valid names for the first argument in a class method.
valid-classmethod-first-arg=cls
# List of valid names for the first argument in a metaclass class method.
valid-metaclass-classmethod-first-arg=mcs
[DESIGN]
# Maximum number of arguments for function / method
max-args=5
# Maximum number of attributes for a class (see R0902).
max-attributes=10
# Maximum number of boolean expressions in a if statement
max-bool-expr=5
# Maximum number of branch for function / method body
max-branches=12
# Maximum number of locals for function / method body
max-locals=15
# Maximum number of parents for a class (see R0901).
max-parents=7
# Maximum number of public methods for a class (see R0904).
max-public-methods=20
# Maximum number of return / yield for function / method body
max-returns=6
# Maximum number of statements in function / method body
max-statements=50
# Minimum number of public methods for a class (see R0903).
min-public-methods=2
[IMPORTS]
# Allow wildcard imports from modules that define __all__.
allow-wildcard-with-all=no
# Analyse import fallback blocks. This can be used to support both Python 2 and
# 3 compatible code, which means that the block might have code that exists
# only in one or another interpreter, leading to false positives when analysed.
analyse-fallback-blocks=no
# Deprecated modules which should not be used, separated by a comma
deprecated-modules=optparse,tkinter.tix
# Create a graph of external dependencies in the given file (report RP0402 must
# not be disabled)
ext-import-graph=
# Create a graph of every (i.e. internal and external) dependencies in the
# given file (report RP0402 must not be disabled)
import-graph=
# Create a graph of internal dependencies in the given file (report RP0402 must
# not be disabled)
int-import-graph=
# Force import order to recognize a module as part of the standard
# compatibility libraries.
known-standard-library=
# Force import order to recognize a module as part of a third party library.
known-third-party=enchant
[EXCEPTIONS]
# Exceptions that will emit a warning when being caught. Defaults to
# "Exception"
overgeneral-exceptions=Exception
+330
View File
@@ -0,0 +1,330 @@
/*
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');
// -- Width --
const DPARSE_LIMIT = 512;
const SPARSE_LIMIT = 32000;
class Width {
constructor(x, y) {
this.x = x;
this.y = y;
}
static parse(name, value, limit) {
const words = fnutil.splitWords(name, value, 2);
return new Width(fnutil.parseDec(name + '.x', words[0], -limit, limit),
fnutil.parseDec(name + '.y', words[1], -limit, limit));
}
static parseS(name, value) {
return Width.parse(name, value, SPARSE_LIMIT);
}
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;
this.height = height;
this.xoff = xoff;
this.yoff = yoff;
}
static parse(name, value) {
const words = fnutil.splitWords(name, value, 4);
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() {
return (this.width + 7) >> 3;
}
toString() {
return `${this.width} ${this.height} ${this.xoff} ${this.yoff}`;
}
}
// -- Props --
function skipComments(line) {
return line.startsWith('COMMENT') ? null : line;
}
class Props extends Map {
forEach(callback) {
super.forEach((value, name) => callback(name, value));
}
read(input, name, callback) {
return this.parse(input.readLines(skipComments), name, callback);
}
parse(line, name, callback) {
if (line == null || !line.startsWith(name)) {
throw new Error(name + ' expected');
}
const value = line.substring(name.length).trimLeft();
this.set(name, value);
return callback == null ? value : callback(name, value);
}
set(name, value) {
super.set(name, value.toString());
}
}
// -- Base --
class Base {
constructor() {
this.props = new Props();
this.bbx = null;
}
}
// -- Char --
class Char extends Base {
constructor() {
super();
this.code = -1;
this.swidth = null;
this.dwidth = null;
this.data = null;
}
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.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 = input.readLines(skipComments);
if (line != null && line.startsWith('ATTRIBUTES')) {
this.props.parse(line, 'ATTRIBUTES');
line = input.readLines(skipComments);
}
// BITMAP
if (this.props.parse(line, 'BITMAP') !== '') {
throw new Error('BITMAP expected');
}
const rowLen = this.bbx.rowSize() * 2;
let bitmap = '';
for (let y = 0; y < this.bbx.height; y++) {
line = input.readLines(skipComments);
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 {
throw new Error('invalid bitmap line length');
}
}
this.data = Buffer.from(bitmap, 'hex');
// FINAL
if (input.readLines(skipComments) !== 'ENDCHAR') {
throw new Error('ENDCHAR expected');
}
return this;
}
static read(input) {
return (new Char())._read(input);
}
write(output) {
let header = '';
this.props.forEach((name, value) => {
header += (name + ' ' + value).trim() + '\n';
});
output.writeLine(header + this.bitmap() + 'ENDCHAR');
}
}
// -- Font --
const XLFD = {
FOUNDRY: 1,
FAMILY_NAME: 2,
WEIGHT_NAME: 3,
SLANT: 4,
SETWIDTH_NAME: 5,
ADD_STYLE_NAME: 6,
PIXEL_SIZE: 7,
POINT_SIZE: 8,
RESOLUTION_X: 9,
RESOLUTION_Y: 10,
SPACING: 11,
AVERAGE_WIDTH: 12,
CHARSET_REGISTRY: 13,
CHARSET_ENCODING: 14
};
const CHARS_MAX = 65535;
class Font extends Base {
constructor() {
super();
this.chars = [];
this.defaultCode = -1;
}
get bold() {
return this.xlfd[XLFD.WEIGHT_NAME].toLowerCase().includes('bold');
}
get italic() {
return ['I', 'O'].indexOf(this.xlfd[XLFD.SLANT]) !== -1;
}
get proportional() {
return this.xlfd[XLFD.SPACING] === 'P';
}
_read(input) {
// HEADER
let line = input.readLine();
if (this.props.parse(line, 'STARTFONT') !== '2.1') {
throw new Error('STARTFONT 2.1 expected');
}
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.props.read(input, 'SIZE');
this.bbx = this.props.read(input, 'FONTBOUNDINGBOX', BBX.parse);
line = input.readLines(skipComments);
if (line != null && line.startsWith('STARTPROPERTIES')) {
const numProps = this.props.parse(line, 'STARTPROPERTIES', fnutil.parseDec);
for (let i = 0; i < numProps; i++) {
line = input.readLines(skipComments);
if (line == null) {
throw new Error('property expected');
}
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 (this.props.get(name) != null) {
throw new Error('duplicate property');
}
if (name === 'DEFAULT_CHAR') {
this.defaultCode = fnutil.parseDec(name, value);
}
this.props.set(name, value);
}
if (this.props.read(input, 'ENDPROPERTIES') !== '') {
throw new Error('ENDPROPERTIES expected');
}
line = input.readLines(skipComments);
}
// GLYPHS
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));
}
if (this.defaultCode !== -1 && this.chars.find(char => char.code === this.defaultCode) === -1) {
throw new Error('invalid DEFAULT_CHAR');
}
// FINAL
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, false);
}
write(output) {
this.props.forEach((name, value) => output.writeProp(name, value));
this.chars.forEach(char => char.write(output));
output.writeLine('ENDFONT');
}
}
// -- Export --
module.exports = Object.freeze({
DPARSE_LIMIT,
SPARSE_LIMIT,
Width,
BBX,
skipComments,
Props,
Base,
Char,
XLFD,
CHARS_MAX,
Font
});
+309
View File
@@ -0,0 +1,309 @@
#
# 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
# -- Width --
DPARSE_LIMIT = 512
SPARSE_LIMIT = 32000
class Width:
def __init__(self, x, y):
self.x = x
self.y = y
@staticmethod
def parse(name, value, limit):
words = fnutil.split_words(name, value, 2)
return Width(fnutil.parse_dec(name + '.x', words[0], -limit, limit),
fnutil.parse_dec(name + '.y', words[1], -limit, limit))
@staticmethod
def parse_s(name, value):
return Width.parse(name, value, SPARSE_LIMIT)
@staticmethod
def parse_d(name, value):
return Width.parse(name, value, DPARSE_LIMIT)
def __str__(self):
return '%d %d' % (self.x, self.y)
# -- BXX --
class BBX:
def __init__(self, width, height, xoff, yoff):
self.width = width
self.height = height
self.xoff = xoff
self.yoff = yoff
@staticmethod
def parse(name, value):
words = fnutil.split_words(name, value, 4)
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):
return (self.width + 7) >> 3
def __str__(self):
return '%d %d %d %d' % (self.width, self.height, self.xoff, self.yoff)
# -- Props --
def skip_comments(line):
return None if line[:7] == b'COMMENT' else line
class Props(OrderedDict):
def __iter__(self):
return self.items().__iter__()
def read(self, input, name, callback=None):
return self.parse(input.read_lines(skip_comments), name, callback)
def parse(self, line, name, callback=None):
if not line or not line.startswith(bytes(name, 'ascii')):
raise Exception(name + ' expected')
value = line[len(name):].lstrip()
self[name] = value
return value if callback is None else callback(name, value)
def set(self, 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
# -- 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):
Base.__init__(self)
self.code = -1
self.swidth = None
self.dwidth = None
self.data = None
def bitmap(self):
bitmap = ''
row_size = self.bbx.row_size()
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
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 = input.read_lines(skip_comments)
# BITMAP
if self.props.parse(line, 'BITMAP'):
raise Exception('BITMAP expected')
row_len = self.bbx.row_size() * 2
self.data = bytearray()
for _ in range(0, self.bbx.height):
line = input.read_lines(skip_comments)
if not line:
raise Exception('bitmap data expected')
if len(line) == row_len:
self.data += codecs.decode(line, 'hex')
else:
raise Exception('invalid bitmap length')
# FINAL
if input.read_lines(skip_comments) != b'ENDCHAR':
raise Exception('ENDCHAR expected')
return self
@staticmethod
def read(input):
return Char()._read(input) # pylint: disable=protected-access
def write(self, output):
for [name, value] in self.props:
output.write_prop(name, value)
output.write_line(self.bitmap() + b'ENDCHAR')
# -- Font --
@unique
class XLFD(IntEnum):
FOUNDRY = 1
FAMILY_NAME = 2
WEIGHT_NAME = 3
SLANT = 4
SETWIDTH_NAME = 5
ADD_STYLE_NAME = 6
PIXEL_SIZE = 7
POINT_SIZE = 8
RESOLUTION_X = 9
RESOLUTION_Y = 10
SPACING = 11
AVERAGE_WIDTH = 12
CHARSET_REGISTRY = 13
CHARSET_ENCODING = 14
CHARS_MAX = 65535
class Font(Base):
def __init__(self):
Base.__init__(self)
self.xlfd = []
self.chars = []
self.default_code = -1
@property
def bold(self):
return b'bold' in self.xlfd[XLFD.WEIGHT_NAME].lower()
@property
def italic(self):
return self.xlfd[XLFD.SLANT] in [b'I', b'O']
@property
def proportional(self):
return self.xlfd[XLFD.SPACING] == b'P'
def _read(self, input):
# HEADER
line = input.read_line()
if self.props.parse(line, 'STARTFONT') != b'2.1':
raise Exception('STARTFONT 2.1 expected')
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')
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 = input.read_lines(skip_comments)
if line is None:
raise Exception('property expected')
match = re.fullmatch(br'(\w+)\s+([-\d"].*)', line)
if not match:
raise Exception('invalid property format')
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[name] = value
if self.props.read(input, 'ENDPROPERTIES') != b'':
raise Exception('ENDPROPERTIES expected')
line = input.read_lines(skip_comments)
# GLYPHS
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))
if next((char.code for char in self.chars if char.code == self.default_code), -1) != self.default_code:
raise Exception('invalid DEFAULT_CHAR')
# FINAL
if input.read_lines(skip_comments) != b'ENDFONT':
raise Exception('ENDFONT expected')
if input.read_line() is not None:
raise Exception('garbage after ENDFONT')
return self
@staticmethod
def read(input):
return Font()._read(input) # pylint: disable=protected-access
def write(self, output):
for [name, value] in self.props:
output.write_prop(name, value)
for char in self.chars:
char.write(output)
output.write_line(b'ENDFONT')
+455
View File
@@ -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);
}
+380
View File
@@ -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)
+287
View File
@@ -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);
}
+245
View File
@@ -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)
+256
View File
@@ -0,0 +1,256 @@
/*
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');
const bdfexp = require('./bdfexp.js');
// -- Params --
class Params extends fncli.Params {
constructor() {
super();
this.charSet = -1;
this.minChar = -1;
this.fntFamily = 0;
this.output = null;
}
}
// -- Options --
const HELP = ('' +
'usage: bdftofnt [-c CHARSET] [-m MINCHAR] [-f FAMILY] [-o OUTPUT] [INPUT]\n' +
'Convert a BDF font to Windows FNT\n' +
'\n' +
' -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, 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 with unicode encoding.\n');
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' ];
class Options extends fncli.Options {
constructor() {
super(['-c', '-m', '-f', '-o'], HELP, VERSION);
}
parse(name, value, params) {
switch (name) {
case '-c':
params.charSet = fnutil.parseDec('CHARSET', value, 0, 255);
break;
case '-m':
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 FAMILY');
}
break;
case '-o':
params.output = value;
break;
default:
this.fallback(name, params);
}
}
}
// -- Main --
const FNT_HEADER_SIZE = 118;
const FNT_CHARSETS = [238, 204, 0, 161, 162, 177, 178, 186, 163];
function mainProgram(nonopt, parsed) {
if (nonopt.length > 1) {
throw new Error('invalid number of arguments, try --help');
}
let charSet = parsed.charSet;
let minChar = parsed.minChar;
// READ INPUT
let ifs = new fnio.InputFileStream(nonopt[0]);
try {
var font = bdfexp.Font.read(ifs);
ifs.close();
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
// COMPUTE
if (charSet === -1) {
const encoding = font.xlfd[bdf.XLFD.CHARSET_ENCODING];
if (encoding.toLowerCase().match(/^(cp)?125[0-8]$/)) {
charSet = FNT_CHARSETS[parseInt(encoding.substring(encoding.length - 1), 10)];
} else {
charSet = 255;
}
}
try {
const numChars = font.chars.length;
if (numChars > 256) {
throw new Error('too many characters, the maximum is 256');
}
if (minChar === -1) {
if (numChars === 192 || numChars === 256) {
minChar = 256 - numChars;
} else {
minChar = font.chars[0].code;
}
}
var maxChar = minChar + numChars - 1;
if (maxChar >= 256) {
throw new Error('the maximum character code is too big, (re)specify -m');
}
// HEADER
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.bbx.rowSize();
ctable.push(char.bbx.width);
ctable.push(vtell);
vtell += rowSize * font.bbx.height;
widthBytes += rowSize;
});
if (vtell > 0xFFFF) {
throw new Error('too much character data');
}
// SENTINEL
var sentinel = 2 - widthBytes % 2;
ctable.push(sentinel * 8);
ctable.push(vtell);
vtell += sentinel * font.bbx.height;
widthBytes += sentinel;
if (widthBytes > 0xFFFF) {
throw new Error('the total character width is too big');
}
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
// WRITE
let ofs = new fnio.OutputFileStream(parsed.output, null);
try {
// HEADER
const family = font.xlfd[bdf.XLFD.FAMILY_NAME];
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.writeZStr(copyright, 60 - copyright.length);
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.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(font.proportional ? 0 : font.bbx.width);
ofs.write16(font.bbx.height);
ofs.write8((parsed.fntFamily << 4) + Number(font.proportional));
ofs.write16(font.avgWidth);
ofs.write16(font.bbx.width);
ofs.write8(minChar);
ofs.write8(maxChar);
let defaultIndex = maxChar - minChar;
let breakIndex = 0;
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);
ofs.write32(0); // device name
ofs.write32(vtell);
ofs.write32(0); // gdi bits pointer
ofs.write32(bitsOffset);
ofs.write8(0); // reserved
// CTABLE
ctable.forEach(value => ofs.write16(value));
// GLYPHS
const data = Buffer.alloc(font.bbx.height * font.bbx.rowSize());
font.chars.forEach(char => {
const rowSize = char.bbx.rowSize();
let counter = 0;
// MS coordinates
for (let n = 0; n < rowSize; n++) {
for (let y = 0; y < font.bbx.height; y++) {
data[counter++] = char.data[rowSize * y + n];
}
}
ofs.write(data.slice(0, counter));
});
ofs.write(Buffer.alloc(sentinel * font.bbx.height));
// FAMILY
ofs.writeZStr(family, 1);
ofs.close();
} catch (e) {
e.message = ofs.location() + e.message + ofs.destroy();
throw e;
}
}
if (require.main === module) {
fncli.start('bdftofnt.js', new Options(), new Params(), mainProgram);
}
+222
View File
@@ -0,0 +1,222 @@
#
# 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 fnutil
import fncli
import fnio
import bdf
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_name = None
# -- Options --
HELP = ('' +
'usage: bdftofnt [-c CHARSET] [-m MINCHAR] [-f FAMILY] [-o OUTPUT] [INPUT]\n' +
'Convert a BDF font to Windows FNT\n' +
'\n' +
' -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, 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 with unicode encoding.\n')
VERSION = 'bdftofnt 1.62, Copyright (C) 2017-2020 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_LICENSE
FNT_FAMILIES = ['DontCare', 'Roman', 'Swiss', 'Modern', 'Decorative']
class Options(fncli.Options):
def __init__(self):
fncli.Options.__init__(self, ['-c', '-m', '-f', '-o'], HELP, VERSION)
def parse(self, name, value, params):
if name == '-c':
params.char_set = fnutil.parse_dec('CHARSET', value, 0, 255)
elif name == '-m':
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 FAMILY')
elif name == '-o':
params.output_name = value
else:
self.fallback(name, params)
# -- Main --
FNT_HEADER_SIZE = 118
FNT_CHARSETS = [238, 204, 0, 161, 162, 177, 178, 186, 163]
def main_program(nonopt, parsed):
if len(nonopt) > 1:
raise Exception('invalid number of arguments, try --help')
char_set = parsed.char_set
min_char = parsed.min_char
# READ INPUT
ifs = fnio.InputFileStream(nonopt[0] if nonopt else None)
font = ifs.process(bdfexp.Font.read)
# COMPUTE
if char_set == -1:
encoding = font.xlfd[bdf.XLFD.CHARSET_ENCODING]
if re.fullmatch(b'(cp)?125[0-8]', encoding.lower()):
char_set = FNT_CHARSETS[int(encoding[-1:])]
else:
char_set = 255
try:
num_chars = len(font.chars)
if num_chars > 256:
raise Exception('too many characters, the maximum is 256')
if min_char == -1:
if num_chars in [192, 256]:
min_char = 256 - num_chars
else:
min_char = font.chars[0].code
max_char = min_char + num_chars - 1
if max_char >= 256:
raise Exception('the maximum character code is too big, (re)specify -m')
# HEADER
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.bbx.row_size()
ctable.append(char.bbx.width)
ctable.append(vtell)
vtell += row_size * font.bbx.height
width_bytes += row_size
if vtell > 0xFFFF:
raise Exception('too much character data')
# SENTINEL
sentinel = 2 - width_bytes % 2
ctable.append(sentinel * 8)
ctable.append(vtell)
vtell += sentinel * font.bbx.height
width_bytes += sentinel
if width_bytes > 0xFFFF:
raise Exception('the total character width is too big')
except Exception as ex:
ex.message = ifs.location() + getattr(ex, 'message', str(ex))
raise
# WRITE
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''
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
if font.default_code != -1:
default_index = next(index for index, char in enumerate(font.chars) if char.code == font.default_code)
if min_char <= 0x20 <= max_char:
break_index = 0x20 - min_char
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:
output.write16(value)
# GLYPHS
data = bytearray(font.bbx.height * font.bbx.row_size())
for char in font.chars:
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
output.write(data[:counter])
output.write(bytes(sentinel * font.bbx.height))
# FAMILY
output.write_zstr(family, 1)
fnio.write_file(parsed.output_name, write_fnt, encoding=None)
if __name__ == '__main__':
fncli.start('bdftofnt.py', Options(), Params(), main_program)
+293
View File
@@ -0,0 +1,293 @@
/*
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';
const fnutil = require('./fnutil.js');
const fncli = require('./fncli.js');
const fnio = require('./fnio.js');
const bdfexp = require('./bdfexp.js');
// -- Params --
class Params extends fncli.Params {
constructor() {
super();
this.version = -1;
this.exchange = -1;
this.output = null;
}
}
// -- 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' +
'\n' +
' -1, -2 write a PSF version 1 or 2 font (default = 1 if possible)\n' +
' -r, --raw write a RAW font\n' +
' -g, --vga exchange the characters at positions 0...31 with these at\n' +
' 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, 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 monospaced unicode-encoded BDF 2.1 font.\n' +
'\n' +
'The tables are text files with two or more hexadecimal unicodes per line:\n' +
'a character code from the BDF, and extra code(s) for it. All extra codes\n' +
'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.58, Copyright (C) 2017-2019 Dimitar Toshkov Zhekov\n\n' + fnutil.GPL2PLUS_VERSION;
class Options extends fncli.Options {
constructor() {
super(['-o'], HELP, VERSION);
}
parse(name, value, params) {
switch (name) {
case '-1':
case '-2':
params.version = parseInt(name[1]);
break;
case '-r':
case '--raw':
params.version = 0;
break;
case '-g':
case '--vga':
params.exchange = true;
break;
case '-G':
params.exchange = false;
break;
case '-o':
params.output = value;
break;
default:
this.fallback(name, params);
}
}
}
// -- Main --
function mainProgram(nonopt, parsed) {
const bdfile = nonopt.length > 0 && nonopt[0].toLowerCase().endsWith('.bdf');
let version = parsed.version;
let exchange = parsed.exchange;
let ver1Unicodes = true;
// READ INPUT
let ifs = new fnio.InputFileStream(bdfile ? nonopt[0] : null);
try {
var font = bdfexp.Font.read(ifs);
ifs.close();
font.chars.forEach(char => {
const prefix = `char ${char.code}: `;
if (char.bbx.width !== font.bbx.width) {
throw new Error(prefix + 'output width not equal to maximum output width');
}
if (char.code === 65534) {
throw new Error(prefix + 'not a character, use 65535 for empty position');
}
if (char.code >= 65536) {
if (version === 1) {
throw new Error(prefix + '-1 requires unicodes <= 65535');
}
ver1Unicodes = false;
}
});
// VERSION
var ver1NumChars = (font.chars.length === 256 || font.chars.length === 512);
if (version === 1) {
if (!ver1NumChars) {
throw new Error('-1 requires a font with 256 or 512 characters');
}
if (font.bbx.width !== 8) {
throw new Error('-1 requires a font with width 8');
}
}
// EXCHANGE
var vgaNumChars = font.chars.length >= 224 && font.chars.length <= 512;
var vgaTextSize = font.bbx.width === 8 && [8, 14, 16].indexOf(font.bbx.height) !== -1;
if (exchange === true) {
if (!vgaNumChars) {
throw new Error('-g/--vga requires a font with 224...512 characters');
}
if (!vgaTextSize) {
throw new Error('-g/--vga requires an 8x8, 8x14 or 8x16 font');
}
}
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
// READ TABLES
let tables = [];
function loadExtra(line) {
const words = line.split(/\s+/);
if (words.length < 2) {
throw new Error('invalid format');
}
const uni = fnutil.parseHex('unicode', words[0]);
let table = tables[uni];
if (uni === 0xFFFE) {
throw new Error('FFFE is not a character');
}
if (font.chars.findIndex(char => char.code === uni) !== -1) {
if (uni > fnutil.UNICODE_BMP_MAX) {
ver1Unicodes = false;
}
if (table == null) {
table = tables[uni] = [];
}
words.slice(1).forEach(word => {
const dup = fnutil.parseHex('extra code', word);
if (dup === 0xFFFF) {
throw new Error('FFFF is not a character');
}
if (dup > fnutil.UNICODE_BMP_MAX) {
ver1Unicodes = false;
}
if (table.indexOf(dup) === -1 || table.indexOf(0xFFFE) !== -1) {
table.push(dup);
}
});
if (version === 1 && !ver1Unicodes) {
throw new Error('-1 requires unicodes <= ' + fnutil.UNICODE_BMP_MAX.toString(16));
}
}
}
nonopt.slice(Number(bdfile)).forEach(name => {
ifs = new fnio.InputFileStream(name);
try {
ifs.readLines(loadExtra);
ifs.close();
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
});
// VERSION
if (version === -1) {
version = ver1NumChars && ver1Unicodes && font.bbx.width === 8 ? 1 : 2;
}
// EXCHANGE
if (exchange === -1) {
exchange = vgaTextSize && version >= 1 && vgaNumChars && font.chars[0].code === 0x00A3;
}
if (exchange) {
const control = font.chars.splice(0, 32, ...font.chars.splice(192, 32));
font.chars.splice(192, 0, ...control);
}
// WRITE
let ofs = new fnio.OutputFileStream(parsed.output, null);
try {
// HEADER
if (version === 1) {
ofs.write8(0x36);
ofs.write8(0x04);
ofs.write8((font.chars.length >> 8) + 1);
ofs.write8(font.bbx.height);
} else if (version === 2) {
ofs.write32(0x864AB572);
ofs.write32(0x00000000);
ofs.write32(0x00000020);
ofs.write32(0x00000001);
ofs.write32(font.chars.length);
ofs.write32(font.chars[0].data.length);
ofs.write32(font.bbx.height);
ofs.write32(font.bbx.width);
}
// GLYPHS
font.chars.forEach(char => ofs.write(char.data));
// UNICODES
if (version > 0) {
const writeUnicode = function(code) {
if (version === 1) {
ofs.write16(code);
} else if (code <= 0x7F) {
ofs.write8(code);
} else if (code === 0xFFFE || code === 0xFFFF) {
ofs.write8(code & 0xFF);
} else {
if (code <= 0x7FF) {
ofs.write8(0xC0 + (code >> 6));
} else {
if (code <= 0xFFFF) {
ofs.write8(0xE0 + (code >> 12));
} else {
ofs.write8(0xF0 + (code >> 18));
ofs.write8(0x80 + ((code >> 12) & 0x3F));
}
ofs.write8(0x80 + ((code >> 6) & 0x3F));
}
ofs.write8(0x80 + (code & 0x3F));
}
};
font.chars.forEach(char => {
if (char.code !== 0xFFFF) {
writeUnicode(char.code);
}
if (tables[char.code] != null) {
tables[char.code].forEach(extra => writeUnicode(extra));
}
writeUnicode(0xFFFF);
});
}
// FINISH
ofs.close();
} catch (e) {
e.message = ofs.location() + e.message + ofs.destroy();
throw e;
}
}
if (require.main === module) {
fncli.start('bdftopsf.js', new Options(), new Params(), mainProgram);
}
+241
View File
@@ -0,0 +1,241 @@
#
# 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 fnutil
import fncli
import fnio
import bdfexp
# -- Params --
class Params(fncli.Params):
def __init__(self):
fncli.Params.__init__(self)
self.version = -1
self.exchange = -1
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' +
'\n' +
' -1, -2 write a PSF version 1 or 2 font (default = 1 if possible)\n' +
' -r, --raw write a RAW font\n' +
' -g, --vga exchange the characters at positions 0...31 with these at\n' +
' 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, 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 monospaced unicode-encoded BDF 2.1 font.\n' +
'\n' +
'The tables are text files with two or more hexadecimal unicodes per line:\n' +
'a character code from the BDF, and extra code(s) for it. All extra codes\n' +
'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.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 in ['-1', '-2']:
params.version = int(name[1])
elif name in ['-r', '--raw']:
params.version = 0
elif name in ['-g', '--vga']:
params.exchange = True
elif name == '-G':
params.exchange = False
elif name == '-o':
params.output_name = value
else:
self.fallback(name, params)
# -- Main --
def main_program(nonopt, parsed):
version = parsed.version
exchange = parsed.exchange
bdfile = len(nonopt) > 0 and nonopt[0].lower().endswith('.bdf')
ver1_unicodes = True
# READ INPUT
ifs = fnio.InputFileStream(nonopt[0] if bdfile else None)
font = ifs.process(bdfexp.Font.read)
try:
for char in font.chars:
prefix = 'char %d: ' % char.code
if char.bbx.width != font.bbx.width:
raise Exception(prefix + 'output width not equal to maximum output width')
if char.code == 65534:
raise Exception(prefix + 'not a character, use 65535 for empty position')
if char.code >= 65536:
if version == 1:
raise Exception(prefix + '-1 requires unicodes <= 65535')
ver1_unicodes = False
# VERSION
ver1_num_chars = len(font.chars) == 256 or len(font.chars) == 512
if version == 1:
if not ver1_num_chars:
raise Exception('-1 requires a font with 256 or 512 characters')
if font.bbx.width != 8:
raise Exception('-1 requires a font with width 8')
# EXCHANGE
vga_num_chars = len(font.chars) >= 224 and len(font.chars) <= 512
vga_text_size = font.bbx.width == 8 and font.bbx.height in [8, 14, 16]
if exchange is True:
if not vga_num_chars:
raise Exception('-g/--vga requires a font with 224...512 characters')
if not vga_text_size:
raise Exception('-g/--vga requires an 8x8, 8x14 or 8x16 font')
except Exception as ex:
ex.message = ifs.location() + getattr(ex, 'message', str(ex))
raise
# READ TABLES
tables = dict()
def load_extra(line):
nonlocal ver1_unicodes
words = line.split()
if len(words) < 2:
raise Exception('invalid format')
uni = fnutil.parse_hex('unicode', words[0])
if uni == 0xFFFE:
raise Exception('FFFE is not a character')
if next((char for char in font.chars if char.code == uni), None):
if uni > fnutil.UNICODE_BMP_MAX:
ver1_unicodes = False
if uni not in tables:
tables[uni] = []
table = tables[uni]
for word in words[1:]:
dup = fnutil.parse_hex('extra code', word)
if dup == 0xFFFF:
raise Exception('FFFF is not a character')
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 <= %X' % fnutil.UNICODE_BMP_MAX)
for table_name in nonopt[int(bdfile):]:
fnio.read_file(table_name, lambda ifs: ifs.read_lines(load_extra))
# VERSION
if version == -1:
version = 1 if ver1_num_chars and ver1_unicodes and font.bbx.width == 8 else 2
# EXCHANGE
if exchange == -1:
exchange = vga_text_size and version >= 1 and vga_num_chars and font.chars[0].code == 0x00A3
if exchange:
font.chars = font.chars[192:224] + font.chars[32:192] + font.chars[0:32] + font.chars[224:]
# WRITE
def write_psf(output):
# HEADER
if version == 1:
output.write8(0x36)
output.write8(0x04)
output.write8((len(font.chars) >> 8) + 1)
output.write8(font.bbx.height)
elif version == 2:
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:
output.write(char.data)
# UNICODES
if version > 0:
def write_unicode(code):
if version == 1:
output.write16(code)
elif code <= 0x7F:
output.write8(code)
elif code in [0xFFFE, 0xFFFF]:
output.write8(code & 0xFF)
else:
if code <= 0x7FF:
output.write8(0xC0 + (code >> 6))
else:
if code <= 0xFFFF:
output.write8(0xE0 + (code >> 12))
else:
output.write8(0xF0 + (code >> 18))
output.write8(0x80 + ((code >> 12) & 0x3F))
output.write8(0x80 + ((code >> 6) & 0x3F))
output.write8(0x80 + (code & 0x3F))
for char in font.chars:
if char.code != 0xFFFF:
write_unicode(char.code)
if char.code in tables:
for extra in tables[char.code]:
write_unicode(extra)
write_unicode(0xFFFF)
fnio.write_file(parsed.output_name, write_psf, encoding=None)
if __name__ == '__main__':
fncli.start('bdftopsf.py', Options(), Params(), main_program)
+180
View File
@@ -0,0 +1,180 @@
/*
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';
// -- Params --
class Params {
constructor() {
this.excstk = false;
}
}
// -- Options --
class Options {
constructor(needArgs, helpText, versionText) {
needArgs.forEach(name => {
if (!name.match(/^(-[^-]|--[^=]+)$/)) {
throw new Error(`invalid option name "${name}"`);
}
});
this.needArgs = needArgs;
this.helpText = helpText;
this.versionText = versionText;
}
posixlyCorrect() { // eslint-disable-line class-methods-use-this
return process.env['POSIXLY_CORRECT'] != null;
}
needsArg(name) {
return this.needArgs.includes(name);
}
fallback(name, params) {
if (name === '--excstk') {
params.excstk = true;
} else if (name === '--help' && this.helpText != null) {
process.stdout.write(this.helpText);
process.exit(0);
} else if (name === '--version' && this.versionText != null) {
process.stdout.write(this.versionText);
process.exit(0);
} else {
let suffix = this.needsArg(name) ? ' (taking an argument?)' : '';
suffix += (this.helpText != null) ? ', try --help' : '';
throw new Error(`unknown option "${name}"${suffix}`);
}
}
reader(args, skip = 2) {
return new Options.Reader(this, args, skip);
}
}
Options.Reader = class {
constructor(options, args, skip) {
this.options = options;
this.args = args;
this.skip = skip;
}
forEach(callback) {
let optind;
for (optind = this.skip; optind < this.args.length; optind++) {
let arg = this.args[optind];
if (arg === '-' || !arg.startsWith('-')) {
if (this.options.posixlyCorrect()) {
break;
}
callback(null, arg);
} else if (arg === '--') {
optind++;
break;
} else {
let name, value;
if (!arg.startsWith('--')) {
for (;;) {
name = arg.substring(0, 2);
value = (name !== arg) ? arg.substring(2) : null;
if (this.options.needsArg(name) || value == null) {
break;
}
callback(name, null);
arg = '-' + value;
}
} else if (arg.indexOf('=') >= 3) {
name = arg.split('=', 1)[0];
if (!this.options.needsArg(name)) {
throw new Error(`option "${name}" does not take an argument`);
}
value = arg.substring(name.length + 1);
} else {
name = arg;
value = null;
}
if (value == null && Number(this.options.needsArg(name)) > 0) {
if (++optind === this.args.length) {
throw new Error(`option "${name}" requires an argument`);
}
value = this.args[optind];
}
callback(name, value);
}
}
this.args.slice(optind).forEach(value => callback(null, value));
}
};
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
const parsed = (params != null) ? params : new Params();
try {
const version = process.version.match(/^v?(\d+)\.(\d+)/);
if (version.length < 3) {
throw new Error('unable to obtain node version');
} else if ((parseInt(version[1]) * 1000 + parseInt(version[2])) < 6009) {
throw new Error('node version 6.9.0 or later required');
}
if (params == null) {
return mainProgram(options.reader(process.argv), name => options.fallback(name, parsed));
} else {
let nonopt = [];
options.reader(process.argv).forEach((name, value) => {
if (name == null) {
nonopt.push(value);
} else {
options.parse(name, value, parsed);
}
});
return mainProgram(nonopt, parsed);
}
} catch (e) {
if (parsed.excstk) {
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);
}
}
// -- Exports --
module.exports = Object.freeze({
Params,
Options,
start
});
+162
View File
@@ -0,0 +1,162 @@
#
# 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.
#
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:
if not re.fullmatch('(-[^-]|--[^=]+)', name):
raise Exception('invalid option name "%s"' % name)
self.need_args = need_args
self.help_text = help_text
self.version_text = version_text
def posixly_correct(self): # pylint: disable=no-self-use
return 'POSIXLY_CORRECT' in os.environ
def needs_arg(self, name):
return name in self.need_args
def fallback(self, name, params):
if name == '--excstk':
params.excstk = True
elif name == '--help' and self.help_text is not None:
sys.stdout.write(self.help_text)
sys.exit(0)
elif name == '--version' and self.version_text is not None:
sys.stdout.write(self.version_text)
sys.exit(0)
else:
suffix = ' (taking an argument?)' if self.needs_arg(name) else ''
suffix += ', try --help' if self.help_text is not None else ''
raise Exception('unknown option "%s"%s' % (name, suffix))
def reader(self, args, skip_zero=True):
return Options.Reader(self, args, skip_zero)
class Reader:
def __init__(self, options, args, skip_zero):
self.options = options
self.args = args
self.skip_zero = skip_zero
def __iter__(self):
return Options.Reader.Iterator(self)
class Iterator:
def __init__(self, reader):
self.options = reader.options
self.args = reader.args
self.optind = int(reader.skip_zero)
self.chrind = 1
self.endopt = False
def __next__(self):
if self.chrind == 0:
self.optind += 1
self.chrind = 1
if self.optind == len(self.args):
raise StopIteration
arg = self.args[self.optind]
if self.endopt or arg == '-' or not arg.startswith('-'):
self.endopt = self.options.posixly_correct()
name = None
value = arg
elif arg == '--':
self.chrind = 0
self.endopt = True
return next(self)
elif not arg.startswith('--'):
name = '-' + arg[self.chrind]
self.chrind += 1
if self.chrind < len(arg):
if not self.options.needs_arg(name):
return (name, None)
value = arg[self.chrind:]
else:
value = None
elif '=' in arg and arg.index('=') >= 3:
name = arg.split('=', 1)[0]
if not self.options.needs_arg(name):
raise Exception('option "%s" does not take an argument' % name)
value = arg[len(name) + 1:]
else:
name = arg
value = None
if value is None and int(self.options.needs_arg(name)) > 0:
self.optind += 1
if self.optind == len(self.args):
raise Exception('option "%s" requires an argument' % name)
value = self.args[self.optind]
self.chrind = 0
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')
if params is None:
return main_program(options.reader(sys.argv), lambda name: options.fallback(name, parsed))
nonopt = []
for [name, value] in options.reader(sys.argv):
if name is None:
nonopt.append(value)
else:
options.parse(name, value, parsed)
return main_program(nonopt, parsed)
except Exception as ex:
if parsed.excstk:
raise # loses the message information, but preserves the start() caller stack info
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)
+210
View File
@@ -0,0 +1,210 @@
/*
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 tty = require('tty');
const fs = require('fs');
// -- InputFileStream --
const BLOCK_SIZE = 4096;
class InputFileStream {
constructor(fileName, encoding = 'binary') {
if (fileName != null) {
this.fd = fs.openSync(fileName, 'r');
this.stName = fileName;
} else {
this.fd = process.stdin.fd;
this.stName = '<stdin>';
}
this.encoding = encoding;
this.unseek();
this.lines = [];
this.index = 0;
this.buffer = Buffer.alloc(BLOCK_SIZE);
this.remainder = '';
}
close() {
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 = ' ';
if (this.eof) {
location = 'EOF: ';
} else if (this.lineNo > 0) {
location = `${this.lineNo}: `;
}
return `${this.stName}:${location}`;
}
_readBlock() {
for (;;) {
try {
return fs.readSync(this.fd, this.buffer, 0, BLOCK_SIZE);
} catch (e) {
if (e.code === 'EOF') {
return 0;
}
if (e.code !== 'EAGAIN') {
this.unseek();
throw e;
}
}
}
}
readLine() {
return this.readLines(line => line);
}
readLines(callback) {
let line;
do {
while (this.index < this.lines.length) {
this.lineNo++;
line = callback(this.lines[this.index++].trimRight());
if (line != null) {
return line;
}
}
var count = this._readBlock();
this.index = 0;
this.lines = (this.remainder + this.buffer.toString(this.encoding, 0, count)).split('\n');
this.remainder = this.lines.pop();
this.eof = false;
} while (count > 0);
if (this.remainder.length > 0) {
this.lineNo++;
line = callback(this.remainder.trimRight());
this.remainder = '';
} else {
this.eof = true;
line = null;
}
return line;
}
unseek() {
this.lineNo = 0;
this.eof = false;
}
}
// -- OutputFileStream --
class OutputFileStream {
constructor(fileName, encoding = 'binary') {
if (fileName != null) {
this.fd = fs.openSync(fileName, 'w');
this.stName = fileName;
} else {
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;
}
close() {
this.closeAttempt = true;
fs.closeSync(this.fd);
}
destroy() {
let errors = '';
if (this.fd !== process.stdout.fd) {
if (!this.closeAttempt) {
try {
fs.closeSync(this.fd);
} catch (e) {
errors += `\n${this.stName}: close: ${e.message}`;
}
}
try {
fs.unlinkSync(this.stName);
} catch (e) {
errors += `\n${this.stName}: unlink: ${e.message}`;
}
}
return errors;
}
location() {
return this.stName + ': ';
}
write(buffer) {
fs.writeSync(this.fd, buffer, 0, buffer.length);
}
write8(value) {
this.fbbuf.writeUInt8(value, 0);
fs.writeSync(this.fd, this.fbbuf, 0, 1);
}
write16(value) {
this.fbbuf.writeUInt16LE(value, 0);
fs.writeSync(this.fd, this.fbbuf, 0, 2);
}
write32(value) {
this.fbbuf.writeUInt32LE(value, 0);
fs.writeSync(this.fd, this.fbbuf, 0, 4);
}
writeLine(text) {
fs.writeSync(this.fd, text + '\n', null, this.encoding);
}
writeProp(name, value) {
this.writeLine((name + ' ' + value).trimRight());
}
writeZStr(bstr, numZeros) {
fs.writeSync(this.fd, bstr, null, 'binary');
this.write(Buffer.alloc(numZeros));
}
}
// -- Export --
module.exports = Object.freeze({
InputFileStream,
OutputFileStream
});
+176
View File
@@ -0,0 +1,176 @@
#
# 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 codecs
import struct
import sys
import os
# -- InputFileStream --
class InputFileStream:
def __init__(self, file_name, encoding='binary'):
if file_name is not None:
self.file = open(file_name, 'r') if encoding is None else open(file_name, 'rb')
self.st_name = file_name
else:
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.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)
def read_lines(self, callback):
try:
for line in self.file:
self.line_no += 1
self.eof = False
line = callback(line.rstrip())
if line is not None:
return line
except OSError:
self.unseek()
raise
self.eof = True
return None
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
else:
self.file = sys.stdout.buffer
self.st_name = '<stdout>'
if encoding is None and self.file.isatty():
raise Exception(self.location() + 'binary output may not be send to a terminal')
self.encoding = (None if encoding == 'binary' else encoding)
self.close_attempt = False
def abort(self):
errors = ''
if self.file != sys.stdout.buffer:
if not self.close_attempt:
try:
self.close()
except Exception as ex:
errors += '\n%sclose: %s' % (self.location(), str(ex))
try:
os.remove(self.st_name)
except Exception as 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 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.write(struct.pack('B', value))
def write16(self, value):
self.write(struct.pack('<H', value))
def write32(self, value):
self.write(struct.pack('<L', value))
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).rstrip())
def write_zstr(self, bstr, 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)
+130
View File
@@ -0,0 +1,130 @@
/*
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';
// -- 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) {
throw new Error(`invalid ${name} format`);
}
const value = parseInt(s, 10);
if (minValue != null && value < minValue) {
throw new Error(`${name} must be >= ${minValue}`);
}
if (maxValue != null && value > maxValue) {
throw new Error(`${name} must be <= ${maxValue}`);
}
return value;
}
function parseHex(name, s, minValue = 0, maxValue = UNICODE_MAX) {
if (s.match(/^\s*(0[xX])?[\dA-Fa-f]+\s*$/) == null) {
throw new Error(`invalid ${name} format`);
}
const value = parseInt(s, 16);
if (minValue != null && value < minValue) {
throw new Error(`${name} must be >= ` + minValue.toString(16).toUpperCase());
}
if (maxValue != null && value > maxValue) {
throw new Error(`${name} must be <= ` + maxValue.toString(16).toUpperCase());
}
return value;
}
function unihex(code) {
return ('000' + code.toString(16).toUpperCase()).replace(/0+(?=[\dA-F]{4})/, '');
}
function round(value) {
const esround = Math.round(value);
return esround - Number(esround % 2 !== 0 && esround - value === 0.5);
}
function quote(s) {
return '"' + s.replace(/"/g, '""') + '"';
}
function unquote(s, name) {
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
s = s.substring(1, s.length - 1).replace(/""/g, '"');
} else if (name != null) {
throw new Error(name + ' must be quoted');
}
return s;
}
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) {
const words = value.split(/\s+/, count + 1);
if (words.length !== count) {
throw new Error(`${name} must contain ${count} values`);
}
return words;
}
const GPL2PLUS_LICENSE = ('' +
'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' +
'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
});
+98
View File
@@ -0,0 +1,98 @@
#
# 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 sys
# -- Various --
UNICODE_MAX = 1114111 # 0x10FFFF
UNICODE_BMP_MAX = 65535 # 0xFFFF
def parse_dec(name, s, min_value=0, max_value=UNICODE_MAX):
try:
value = int(s)
except ValueError:
raise Exception('invalid %s format' % name)
if min_value is not None and value < min_value:
raise Exception('%s must be >= %d' % (name, min_value))
if max_value is not None and value > max_value:
raise Exception('%s must be <= %d' % (name, max_value))
return value
def parse_hex(name, s, min_value=0, max_value=UNICODE_MAX):
try:
value = int(s, 16)
except ValueError:
raise Exception('invalid %s format' % name)
if min_value is not None and value < min_value:
raise Exception('%s must be >= %X' % (name, min_value))
if max_value is not None and value > max_value:
raise Exception('%s must be <= %X' % (name, max_value))
return value
def quote(bstr):
return b'"%s"' % bstr.replace(b'"', b'""')
def unquote(bstr, name=None):
if len(bstr) >= 2 and bstr.startswith(b'"') and bstr.endswith(b'"'):
bstr = bstr[1 : len(bstr) - 1].replace(b'""', b'"')
elif name is not None:
raise Exception(name + ' must be quoted')
return bstr
def message(prefix, severity, text):
sys.stderr.write('%s%s%s\n' % (prefix, severity + ': ' if severity else '', text))
def warning(prefix, text):
message(prefix, 'warning', text)
def split_words(name, value, count):
words = value.split(None, count)
if len(words) != count:
raise Exception('%s must contain %d values' % (name, count))
return words
GPL2PLUS_LICENSE = ('' +
'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' +
'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')
+129
View File
@@ -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);
}
+99
View File
@@ -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)
+895
View File
@@ -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
});
+808
View File
@@ -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)
+706
View File
@@ -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
});
+663
View File
@@ -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)
+245
View File
@@ -0,0 +1,245 @@
/*
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';
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.filter = false;
this.family = null;
this.output = null;
}
}
// -- Options --
const HELP = ('' +
'usage: ucstoany [-f] [-F FAMILY] [-o OUTPUT] INPUT REGISTRY ENCODING TABLE...\n' +
'Generate a BDF font subset.\n' +
'\n' +
' -f, --filter Discard characters with unicode FFFF; with registry ISO10646,\n' +
' encode the first 32 characters with their indexes; with other\n' +
' registries, encode all characters with indexes\n' +
' -F FAMILY output font family name (default = input)\n' +
' -o OUTPUT output file (default = stdout)\n' +
' TABLE text file, one hexadecimal unicode per line\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' +
'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;
class Options extends fncli.Options {
constructor() {
super(['-F', '-o'], HELP, VERSION);
}
parse(name, value, params) {
switch (name) {
case '-f':
case '--filter':
params.filter = true;
break;
case '-F':
if (value.includes('-')) {
throw new Error('FAMILY may not contain "-"');
}
params.family = value;
break;
case '-o':
params.output = value;
break;
default:
this.fallback(name, params);
}
}
}
// -- Main --
function mainProgram(nonopt, parsed) {
if (nonopt.length < 4) {
throw new Error('invalid number of arguments, try --help');
}
const input = nonopt[0];
const registry = nonopt[1];
const encoding = nonopt[2];
let newCodes = [];
if (!registry.match(/^[A-Za-z][\w.:()]*$/) || !encoding.match(/^[\w.:()]+$/)) {
throw new Error('invalid registry or encoding');
}
// READ INPUT
let ifs = new fnio.InputFileStream(input);
try {
var oldFont = bdf.Font.read(ifs);
ifs.close();
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
// READ TABLES
nonopt.slice(3).forEach(name => {
ifs = new fnio.InputFileStream(name);
try {
ifs.readLines(line => {
newCodes.push(fnutil.parseHex('unicode', line));
});
ifs.close();
} catch (e) {
e.message = ifs.location() + e.message;
throw e;
}
});
if (newCodes.length === 0) {
throw new Error('no characters in the output font');
}
// CREATE GLYPHS
const newFont = new bdf.Font();
const charMap = [];
let index = 0;
let unstart = 0;
if (parsed.filter) {
unstart = (registry === 'ISO10646') ? 32 : bdf.CHARS_MAX;
}
// faster than Map() for <= 4K chars
oldFont.chars.forEach(char => (charMap[char.code] = char));
newCodes.forEach(code => {
let oldChar = charMap[code];
const uniFFFF = (oldChar == null);
if (code === 0xFFFF && parsed.filter) {
index++;
return;
}
if (uniFFFF) {
if (code !== 0xFFFF) {
throw new Error(`${input} does not contain U+${fnutil.unihex(code)}`);
}
if (oldFont.defaultCode !== -1) {
oldChar = charMap[oldFont.defaultCode];
} else {
oldChar = charMap[0xFFFD];
if (oldChar == null) {
throw new Error(`${input} does not contain U+FFFF, and no replacement found`);
}
}
}
const newChar = Object.assign(new bdf.Char(), oldChar);
newChar.code = index >= unstart ? code : index;
index++;
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) {
newChar.props.set('STARTCHAR', 'uniFFFF');
} else if (oldChar.code === oldFont.defaultCode || (oldChar.code === 0xFFFD && newFont.defaultCode === -1)) {
newFont.defaultCode = newChar.code;
}
});
// CREATE HEADER
let numProps;
const family = (parsed.family != null) ? parsed.family : oldFont.xlfd[bdf.XLFD.FAMILY_NAME];
oldFont.props.forEach((name, value) => {
switch (name) {
case 'FONT':
newFont.xlfd = oldFont.xlfd.slice();
newFont.xlfd[bdf.XLFD.FAMILY_NAME] = family;
newFont.xlfd[bdf.XLFD.CHARSET_REGISTRY] = registry;
newFont.xlfd[bdf.XLFD.CHARSET_ENCODING] = encoding;
value = newFont.xlfd.join('-');
break;
case 'STARTPROPERTIES':
numProps = fnutil.parseDec(name, value, 1);
break;
case 'FAMILY_NAME':
value = fnutil.quote(family);
break;
case 'CHARSET_REGISTRY':
value = fnutil.quote(registry);
break;
case 'CHARSET_ENCODING':
value = fnutil.quote(encoding);
break;
case 'DEFAULT_CHAR':
if (newFont.defaultCode !== -1) {
value = newFont.defaultCode;
} else {
numProps -= 1;
return;
}
break;
case 'ENDPROPERTIES':
if (newFont.defaultCode !== -1 && newFont.props.get('DEFAULT_CHAR') == null) {
newFont.props.set('DEFAULT_CHAR', newFont.defaultCode);
numProps += 1;
}
newFont.props.set('STARTPROPERTIES', numProps);
break;
case 'CHARS':
value = newFont.chars.length;
break;
}
newFont.props.set(name, value);
});
// COPY FIELDS
newFont.bbx = oldFont.bbx;
// WRITE OUTPUT
let ofs = new fnio.OutputFileStream(parsed.output);
try {
newFont.write(ofs);
ofs.close();
} catch (e) {
e.message = ofs.location() + e.message + ofs.destroy();
throw e;
}
}
if (require.main === module) {
fncli.start('ucstoany.js', new Options(), new Params(), mainProgram);
}
+188
View File
@@ -0,0 +1,188 @@
#
# 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 copy
import fnutil
import fncli
import fnio
import bdf
# -- Params --
class Params(fncli.Params):
def __init__(self):
fncli.Params.__init__(self)
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' +
'\n' +
' -f, --filter Discard characters with unicode FFFF; with registry ISO10646,\n' +
' encode the first 32 characters with their indexes; with other\n' +
' registries, encode all characters with indexes\n' +
' -F FAMILY output font family name (default = input)\n' +
' -o OUTPUT output file (default = stdout)\n' +
' TABLE text file, one hexadecimal unicode per line\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' +
'Unlike ucs2any, all TABLE-s form a single subset of the input font.\n')
VERSION = 'ucstoany 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, ['-F', '-o'], HELP, VERSION)
def parse(self, name, value, params):
if name in ['-f', '--filter']:
params.filter_ffff = True
elif name == '-F':
params.family_name = bytes(value, 'ascii')
if '-' in value:
raise Exception('FAMILY may not contain "-"')
elif name == '-o':
params.output_name = value
else:
self.fallback(name, params)
# -- Main --
def main_program(nonopt, parsed):
# NON-OPTIONS
if len(nonopt) < 4:
raise Exception('invalid number of arguments, try --help')
input_name = nonopt[0]
registry = nonopt[1]
encoding = nonopt[2]
new_codes = []
if not re.fullmatch(r'[A-Za-z][\w.:()]*', registry) or not re.fullmatch(r'[\w.:()]+', encoding):
raise Exception('invalid registry or encoding')
# READ INPUT
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 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')
# CREATE GLYPHS
new_font = bdf.Font()
charmap = {char.code:char for char in old_font.chars}
index = 0
unstart = 0
family = parsed.family_name if parsed.family_name is not None else old_font.xlfd[bdf.XLFD.FAMILY_NAME]
if parsed.filter_ffff:
unstart = 32 if registry == 'ISO10646' else bdf.CHARS_MAX
for code in new_codes:
if code == 0xFFFF and parsed.filter_ffff:
index += 1
continue
if code in charmap:
old_char = charmap[code]
uni_ffff = False
else:
uni_ffff = True
if code != 0xFFFF:
raise Exception('%s does not contain U+%04X' % (input, code))
if old_font.default_code != -1:
old_char = charmap[old_font.default_code]
elif 0xFFFD in charmap:
old_char = charmap[0xFFFD]
else:
raise Exception('%s does not contain U+FFFF, and no replacement found' % input)
new_char = copy.copy(old_char)
new_char.code = code if index >= unstart else index
index += 1
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:
new_char.props.set('STARTCHAR', b'uniFFFF')
elif old_char.code == old_font.default_code or (old_char.code == 0xFFFD and new_font.default_code == -1):
new_font.default_code = new_char.code
# CREATE HEADER
registry = bytes(registry, 'ascii')
encoding = bytes(encoding, 'ascii')
for [name, value] in old_font.props:
if name == 'FONT':
new_font.xlfd = old_font.xlfd[:]
new_font.xlfd[bdf.XLFD.FAMILY_NAME] = family
new_font.xlfd[bdf.XLFD.CHARSET_REGISTRY] = registry
new_font.xlfd[bdf.XLFD.CHARSET_ENCODING] = encoding
value = b'-'.join(new_font.xlfd)
elif name == 'STARTPROPERTIES':
num_props = fnutil.parse_dec(name, value, 1)
elif name == 'FAMILY_NAME':
value = fnutil.quote(family)
elif name == 'CHARSET_REGISTRY':
value = fnutil.quote(registry)
elif name == 'CHARSET_ENCODING':
value = fnutil.quote(encoding)
elif name == 'DEFAULT_CHAR':
if new_font.default_code != -1:
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.set('DEFAULT_CHAR', new_font.default_code)
num_props += 1
new_font.props.set('STARTPROPERTIES', num_props)
elif name == 'CHARS':
value = len(new_font.chars)
new_font.props.set(name, value)
# COPY FIELDS
new_font.bbx = old_font.bbx
# WRITE OUTPUT
fnio.write_file(parsed.output_name, lambda ofs: new_font.write(ofs))
if __name__ == '__main__':
fncli.start('ucstoany.py', Options(), Params(), main_program)