- gleiche Aufgabe = gleicher Schalter in allen Programmen: -c/--config (Alias --cfg), -i, -o/--outdir, --section, --format, -f/--force, -j, -t/--test, -y/--dry, -v - svgconvert: -o statt -s (Alias --svgfolder), --section statt --format für die Sektion (Standard png), --format überschreibt das Bildformat - webscan: --timeout und --browser nur noch lang (kollidierten mit -t/-b) - glyphextract: -i nimmt Glyph oder Font, -e extrahiert (statt -f/-t) - setenv.sh ermittelt das Projektverzeichnis selbst, alle .sh laden es relativ zum Skript; extract-font/fonttest reichen weitere Optionen durch - Hilfetexte korrigiert, doc/README.md: Abschnitt "Gemeinsame Optionen" Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
244 lines
8.6 KiB
Python
244 lines
8.6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
""" erzeugt aus in Inkscape gezeichneten Buchstaben (SVG) die Glyphen und die
|
|
Fontconfig für piccreate (Ersatz für extract-glyph.sh, extract-font.sh und
|
|
fonttest.sh, läuft auch unter Windows).
|
|
|
|
Die Auswahl der Zeilen folgt bewusst den früheren grep/sed Aufrufen, damit
|
|
die erzeugten Glyphen gleich bleiben.
|
|
"""
|
|
|
|
__version__ = '0.1.0'
|
|
__license__ = 'BSD'
|
|
|
|
import glob
|
|
import locale
|
|
import optparse
|
|
import os
|
|
import re
|
|
import sys
|
|
from decimal import Decimal, InvalidOperation
|
|
|
|
# Wortgrenzen wie \< und \> bei grep
|
|
WB = r'(?<!\w)(?=\w)'
|
|
WE = r'(?<=\w)(?!\w)'
|
|
|
|
LINE_IN = re.compile('<' + WB + 'line' + WE + '|x1|x2|y1|y2|/>|style')
|
|
LINE_OUT = re.compile(WB + 'text' + WE + '|id=|http:')
|
|
CIRCLE_IN = re.compile('circle|translate|' + WB + 'r' + WE)
|
|
TEXT_IN = re.compile('<' + WB + 'text|text' + WE + '>|' + WB + 'x' + WE + '=|'
|
|
+ WB + 'y' + WE + '=|tspan|style|preserve')
|
|
TEXT_OUT = re.compile(WB + 'transform-center-x' + WE + '|' + WB + 'window-|'
|
|
+ WB + 'id' + WE + '=')
|
|
CENTER_X = re.compile(r'inkscape:transform-center-x=".*"')
|
|
CENTER_Y = re.compile(r'inkscape:transform-center-y=".*"')
|
|
PATTERN = re.compile(r'>(x[0-9]+)<')
|
|
NUMBER = re.compile(r'\s*[+-]?(\d+\.?\d*|\.\d+)([eE][+-]?\d+)?')
|
|
|
|
TEST_BEGIN = '<svg xmlns="http://www.w3.org/2000/svg" version="1.1">'
|
|
TEST_END = '</svg>'
|
|
GLYPH_BEGIN = '<g transform="translate(${posx},${posy})">'
|
|
GLYPH_END = '</g>'
|
|
|
|
|
|
def split_lines(text):
|
|
''' zerlegt einen Text in Zeilen (wie grep, ohne Zeilenende) '''
|
|
lines = text.split('\n')
|
|
if lines and lines[-1] == '':
|
|
lines.pop()
|
|
return lines
|
|
|
|
|
|
def strip_center(line):
|
|
line = CENTER_X.sub('', line)
|
|
return CENTER_Y.sub('', line)
|
|
|
|
|
|
def number_key(value):
|
|
''' Sortierschlüssel wie bei "sort -g": Zeilen ohne Zahl zuerst '''
|
|
m = NUMBER.match(value)
|
|
if not m:
|
|
return (0, 0.0, value)
|
|
return (1, float(m.group(0)), value)
|
|
|
|
|
|
def coordinates(glyph_lines, axis):
|
|
''' sammelt alle x- bzw. y-Koordinaten aus Texten, Kreisen und Linien '''
|
|
values = []
|
|
if axis == 'x':
|
|
att = re.compile(WB + 'x' + WE + '=')
|
|
else:
|
|
att = re.compile('y=')
|
|
for line in glyph_lines:
|
|
if att.search(line):
|
|
values.append(re.sub(r'^\s+', '', line).replace(axis + '=', '').replace('"', ''))
|
|
# Kreise: transform="translate(x,y)"
|
|
for line in glyph_lines:
|
|
if '<g' not in line and 'transform=' in line:
|
|
value = re.sub(r'^\s+', '', line).replace('translate', '').replace('transform=', '')
|
|
value = re.sub(r'["|()]', '', value)
|
|
if axis == 'x':
|
|
value = re.sub(r',.*', '', value)
|
|
else:
|
|
value = re.sub(r'.*,', '', value)
|
|
values.append(value)
|
|
# Linien: x1/x2 bzw. y1/y2
|
|
for name in (axis + '1=', axis + '2='):
|
|
for line in glyph_lines:
|
|
if name in line:
|
|
values.append(re.sub(r'^\s+', '', line).replace(name, '').replace('"', ''))
|
|
values = sorted(set(values), key=number_key)
|
|
if not values:
|
|
return '', ''
|
|
return values[0], values[-1]
|
|
|
|
|
|
def difference(maximum, minimum):
|
|
''' maximum - minimum, formatiert wie bc '''
|
|
try:
|
|
a, b = Decimal(maximum), Decimal(minimum)
|
|
except InvalidOperation:
|
|
return ''
|
|
scale = max(-a.as_tuple().exponent, -b.as_tuple().exponent, 0)
|
|
result = (a - b).quantize(Decimal(1).scaleb(-scale))
|
|
if result == 0:
|
|
return '0'
|
|
text = format(result, 'f')
|
|
return text.replace('-0.', '-.') if text.startswith('-0.') else re.sub(r'^0\.', '.', text)
|
|
|
|
|
|
def extract_glyph(svg_path, extract=True):
|
|
''' liefert (Glyph-SVG, Fontconfig-Sektion) für einen Buchstaben '''
|
|
with open(svg_path, encoding='utf-8', newline='') as fh:
|
|
source = split_lines(fh.read())
|
|
|
|
lines = [strip_center(l) for l in source if LINE_IN.search(l) and not LINE_OUT.search(l)]
|
|
circles = [strip_center(l) for l in source if CIRCLE_IN.search(l)]
|
|
texts = [strip_center(l) for l in source if TEXT_IN.search(l) and not TEXT_OUT.search(l)]
|
|
|
|
if extract:
|
|
glyph = [GLYPH_BEGIN] + texts + circles + lines + [GLYPH_END]
|
|
else:
|
|
glyph = [TEST_BEGIN] + texts + circles + lines + [TEST_END]
|
|
|
|
xmin, xmax = coordinates(glyph, 'x')
|
|
ymin, ymax = coordinates(glyph, 'y')
|
|
|
|
# Anzahl der Platzhalter x1..xn, dann durch Templatezeichen ${x1} ersetzen
|
|
npattern = sum(1 for l in glyph if re.search(r'>x[0-9]+<', l))
|
|
glyph = [PATTERN.sub(r'>${\1}<', l) for l in glyph]
|
|
|
|
base = os.path.splitext(os.path.basename(svg_path))[0]
|
|
config = [
|
|
'[' + base + ']',
|
|
'XSTEP=' + difference(xmax, xmin),
|
|
'XMIN=' + xmin,
|
|
'XMAX=' + xmax,
|
|
'YSTEP=' + difference(ymax, ymin),
|
|
'YMIN=' + ymin,
|
|
'YMAX=' + ymax,
|
|
'NPATTERN=' + str(npattern),
|
|
'FONTNAME=' + base + '.svg',
|
|
'#',
|
|
]
|
|
return '\n'.join(glyph) + '\n', '\n'.join(config) + '\n'
|
|
|
|
|
|
def write_glyph(svg_path, out_dir, extract=True):
|
|
''' schreibt Glyph und Fontconfig-Sektion ins Ausgabeverzeichnis '''
|
|
glyph, config = extract_glyph(svg_path, extract)
|
|
base = os.path.splitext(os.path.basename(svg_path))[0]
|
|
with open(os.path.join(out_dir, base + '.svg'), 'w', encoding='utf-8', newline='\n') as fh:
|
|
fh.write(glyph)
|
|
return base, config
|
|
|
|
|
|
def font_source(name):
|
|
''' sucht das Verzeichnis mit den Buchstaben eines Fonts '''
|
|
in_dir = os.path.join(os.environ.get('PICCREATE_IN', ''), name)
|
|
if os.path.isdir(in_dir):
|
|
return in_dir
|
|
if os.path.isdir(name):
|
|
return name
|
|
raise SystemExit('"' + name + '" is no directory. Please give a directory with .svg glyphs as input')
|
|
|
|
|
|
def sorted_glyphs(source):
|
|
''' alle .svg Dateien, sortiert wie die Shell (a A ä b B ...) '''
|
|
try:
|
|
locale.setlocale(locale.LC_COLLATE, '')
|
|
except locale.Error:
|
|
pass
|
|
return sorted(glob.glob(os.path.join(source, '*.svg')), key=locale.strxfrm)
|
|
|
|
|
|
def main():
|
|
out_dir = options.outdir
|
|
if not os.path.isdir(out_dir):
|
|
print('"' + out_dir + '" does not exist. Please give the directory for the output!')
|
|
return 1
|
|
|
|
if os.path.isfile(options.infile):
|
|
# ein Buchstabe (extract-glyph.sh)
|
|
if not os.path.getsize(options.infile):
|
|
print('input file is empty')
|
|
return 1
|
|
base, config = write_glyph(options.infile, out_dir, options.extract)
|
|
with open(os.path.join(out_dir, base + '.cfg'), 'w', encoding='utf-8', newline='\n') as fh:
|
|
fh.write(config)
|
|
return 0
|
|
|
|
# alle Buchstaben eines Fonts
|
|
source = font_source(options.infile)
|
|
configs = []
|
|
for svg_path in sorted_glyphs(source):
|
|
base, config = write_glyph(svg_path, out_dir, options.extract)
|
|
print(base)
|
|
configs.append((base, config))
|
|
|
|
if options.extract:
|
|
# Font (extract-font.sh): alle Sektionen in <font>.cfg
|
|
cfg_name = os.path.basename(os.path.normpath(source)) + '.cfg'
|
|
with open(os.path.join(out_dir, cfg_name), 'w', encoding='utf-8', newline='\n') as fh:
|
|
fh.write(''.join(config for _, config in configs))
|
|
print('->', os.path.join(out_dir, cfg_name))
|
|
else:
|
|
# Testbilder (fonttest.sh): je Buchstabe eine .cfg daneben
|
|
for base, config in configs:
|
|
with open(os.path.join(out_dir, base + '.cfg'), 'w', encoding='utf-8', newline='\n') as fh:
|
|
fh.write(config)
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = optparse.OptionParser(usage="""%prog -i <glyph.svg | font> [-e] [options]
|
|
|
|
Example usage
|
|
-------------
|
|
|
|
one glyph, test picture to check it with inkscape:
|
|
$ %prog -i ../in/ausgangsschrift/a.svg
|
|
one glyph, extract the points and text for the font:
|
|
$ %prog -i ../in/ausgangsschrift/a.svg -e
|
|
all glyphs of a font (directory in in/ or a path) including the font config:
|
|
$ %prog -i ausgangsschrift -e
|
|
test pictures of all glyphs of a font:
|
|
$ %prog -i ausgangsschrift
|
|
""", version='%prog ' + __version__)
|
|
|
|
parser.add_option("-i", "--infile", dest="infile",
|
|
help="svg file of one glyph or directory with the glyphs of a font")
|
|
parser.add_option("-e", "--extract", dest="extract", action='store_true', default=False,
|
|
help="extract the glyphs for the font (and write <font>.cfg) "
|
|
"instead of test pictures")
|
|
parser.add_option("-o", "--outdir", dest="outdir",
|
|
default=os.environ.get('PICCREATE_OUT', '.'),
|
|
help="directory where the files will be written. default is $PICCREATE_OUT")
|
|
|
|
(options, args) = parser.parse_args()
|
|
if not options.infile:
|
|
parser.error('please give a glyph or a font with -i')
|
|
sys.exit(main())
|
|
|
|
# vim: set tw=80 ft=python ts=4 sw=4:
|