First jameswitzema.net commit
This commit is contained in:
@@ -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'
|
||||
}
|
||||
};
|
||||
@@ -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
|
||||
@@ -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
|
||||
});
|
||||
@@ -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')
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
});
|
||||
@@ -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)
|
||||
@@ -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
|
||||
});
|
||||
@@ -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)
|
||||
@@ -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
|
||||
});
|
||||
@@ -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')
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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
|
||||
});
|
||||
@@ -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)
|
||||
@@ -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
|
||||
});
|
||||
@@ -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)
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user