Windows-Unterstützung: .bat-Skripte, Font-Werkzeuge in Python

- libs/glyphextract.py ersetzt die grep/sed/bc-Logik von extract-glyph.sh,
  extract-font.sh und fonttest.sh (die .sh rufen es nur noch auf);
  schreibt FONTNAME= und <font>.cfg nach $PICCREATE_OUT
- bin/*.bat: setenv, piccreate, svgconvert, webscan, extract-glyph,
  extract-font, fonttest, setup-venv; .gitattributes für CRLF
- piccreate.py: Hintergrund als file:// URL (Windows-Pfade)
- svgconvert.py: magick statt convert, Configs/CSV als UTF-8
- webscan.py: Installationshinweise für Windows
- doc/README.md: Abschnitt Windows, Font-Werkzeuge aktualisiert

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-10-09 09:29:02 +02:00
co-authored by Claude Opus 5.5
parent 4897883938
commit 29c066b45a
18 changed files with 471 additions and 209 deletions
+246
View File
@@ -0,0 +1,246 @@
#!/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 options.infile:
# ein Buchstabe (extract-glyph.sh)
if not os.path.isfile(options.infile) or not os.path.getsize(options.infile):
print('input file does not exist')
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
name = options.font or options.test
source = font_source(name)
configs = []
for svg_path in sorted_glyphs(source):
base, config = write_glyph(svg_path, out_dir, extract=bool(options.font))
print(base)
configs.append((base, config))
if options.font:
# ein 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 [options]
Example usage
-------------
one glyph, to test it later 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/) including the font config:
$ %prog -f ausgangsschrift
test pictures of all glyphs of a font:
$ %prog -t ausgangsschrift
""", version='%prog ' + __version__)
parser.add_option("-i", "--infile", dest="infile",
help="svg file of one glyph")
parser.add_option("-e", "--extract", dest="extract", action='store_true', default=False,
help="with -i: extract the glyph for the font instead of a test picture")
parser.add_option("-f", "--font", dest="font",
help="directory with the glyphs of a font: create glyphs and font config")
parser.add_option("-t", "--test", dest="test",
help="directory with the glyphs of a font: create test pictures")
parser.add_option("-o", "--outdir", dest="outdir",
default=os.environ.get('PICCREATE_OUT', '.'),
help="directory for the output. default is $PICCREATE_OUT")
(options, args) = parser.parse_args()
if sum(1 for o in (options.infile, options.font, options.test) if o) != 1:
parser.error('please give exactly one of -i, -f or -t')
sys.exit(main())
# vim: set tw=80 ft=python ts=4 sw=4:
+4 -2
View File
@@ -10,6 +10,7 @@ import optparse
import os
import re
import configparser
import pathlib
from utils import CSVImport, CompleteEnding, get_translation
from string import Template
@@ -39,12 +40,13 @@ def create_picture_frame( config ):
try:
params['background'] = config.get('globals', 'background')
imagetpl = ''' <image width="${width}" height="${height}"
xlink:href="file://${background}">
xlink:href="${background}">
</image>
'''
background = config.get('globals', 'background')
font_path = os.path.expandvars(os.path.join('$PICCREATE_BACKGROUNDS', background ))
params['background'] = os.path.normpath(font_path)
# als file:// URL, damit es auch mit Windows-Pfaden (C:\...) klappt
params['background'] = pathlib.Path(os.path.abspath(font_path)).as_uri()
svgbegin = svgbegin + Template( imagetpl ).substitute(params)
except configparser.NoOptionError:
# dann eben kein Hintergrundbild
+1 -1
View File
@@ -96,7 +96,7 @@ def bufcount(filename):
if not os.path.exists(filename):
raise Exception
try:
fh = open(filename)
fh = open(filename, encoding='utf-8', errors='replace')
lines = 0
buf_size = 1024 * 1024
read_f = fh.read # loop optimization
+18 -8
View File
@@ -11,6 +11,7 @@ import cProfile
import optparse
import configparser
import re
import shutil
import subprocess
import csv
from string import Template
@@ -18,10 +19,14 @@ from concurrent import futures
from progressbar import EasyProgressBar
# ImageMagick 7 heisst "magick"; unter Windows ist "convert" ein Systemprogramm
IMAGEMAGICK = shutil.which('magick') or shutil.which('convert')
def get_translation(cfg_path, sec_name):
""" lädt die Zuordnung zwischen einem .csv Eingabefile und den Attributen"""
config = configparser.RawConfigParser()
config.read(cfg_path)
config.read(cfg_path, encoding='utf-8')
trans = dict(config.items(sec_name))
return dict([[v,k] for k,v in trans.items()])
@@ -42,12 +47,14 @@ def convertSVG(in_path,filename,pic_config):
if options.verbose:
print (filename, 'already exist!')
else:
background = " -background " + pic_config['background'] + " "
resize = " -resize " + pic_config['width'] + "x" + pic_config['height'] + " "
density = " -density " + pic_config['density']
cmd = "convert"
cmdline = [IMAGEMAGICK,
'-density', pic_config['density'],
'-background', pic_config['background'],
inpath,
'-resize', pic_config['width'] + "x" + pic_config['height'],
jpgpath]
if not options.dry:
subprocess.check_call(cmd + density + background + inpath + resize + jpgpath, shell = True)
subprocess.check_call(cmdline)
if options.verbose:
print ('start converting', filename)
Epb.slide_progbar()
@@ -111,6 +118,9 @@ font-weight:normal;font-stretch:normal;font-family:Ubuntu;\
def main():
"""get all information from the switches an start the parallel conversion"""
if not IMAGEMAGICK and not options.dry:
print('ImageMagick not found. Please install it (https://imagemagick.org).')
return None
#set the workpath and the default config
work_path = os.path.expandvars('$PICCREATE_OUT')
if options.svgfolder:
@@ -149,7 +159,7 @@ def main():
print('\"'+ config_path +'\" does not exist. Please give the name of a config file!')
return None
config = configparser.RawConfigParser()
config.read(config_path)
config.read(config_path, encoding='utf-8')
# fill the config
pic_config = dict(config.items(options.format))
if options.background:
@@ -173,7 +183,7 @@ def main():
print ("you must give a config file for checking the svg files")
return None
svglist = []
with open(csv_path, newline='') as csvfile:
with open(csv_path, newline='', encoding='utf-8') as csvfile:
reader = csv.DictReader(csvfile,delimiter = ';')
for row in reader:
if row[pic_config['customerid']] not in filelist:
+18 -6
View File
@@ -37,6 +37,10 @@ INSTALL_HINTS = {
export PICCREATE_BROWSER=/usr/bin/chromium
or the Playwright browser:
{venv}/bin/playwright install chromium''',
'windows': r'''Windows (or simply run bin\setup-venv.bat):
py -m venv {venv}
{venv}\Scripts\pip install -r {project}\requirements.txt
{venv}\Scripts\playwright install chromium''',
}
BROWSER_HINTS = {
@@ -48,11 +52,16 @@ BROWSER_HINTS = {
export PICCREATE_BROWSER=/usr/bin/chromium
or the Playwright browser:
{venv}/bin/playwright install chromium''',
'windows': r'''Windows:
{venv}\Scripts\playwright install chromium''',
}
def linux_distro():
''' liefert 'ubuntu' oder 'arch' (aus /etc/os-release), sonst None '''
def platform_name():
''' liefert 'windows', 'ubuntu' oder 'arch' (aus /etc/os-release),
sonst None '''
if sys.platform == 'win32':
return 'windows'
try:
with open('/etc/os-release', encoding='utf-8') as fh:
info = dict(line.rstrip('\n').split('=', 1) for line in fh if '=' in line)
@@ -67,10 +76,13 @@ def linux_distro():
def print_hint(hints):
''' gibt die Installationshinweise für diese Distribution aus
(bei unbekannter Distribution alle) '''
distro = linux_distro()
keys = [distro] if distro in hints else sorted(hints)
''' gibt die Installationshinweise für dieses System aus
(bei unbekanntem Linux die für alle Linux-Distributionen) '''
distro = platform_name()
if distro in hints:
keys = [distro]
else:
keys = sorted(k for k in hints if k != 'windows')
for key in keys:
print(hints[key].format(venv=VENV, project=PROJECT_DIR), file=sys.stderr)