Files
piccreate/libs/glyphextract.py
T
Michael StanglandClaude Opus 5.5 6bdaa62a7d Optionsschalter vereinheitlicht, .sh-Skripte von überall aufrufbar
- 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>
2026-10-09 09:37:20 +02:00

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: