Files
M1730-Scale/web/app.py
T

188 lines
6.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import base64
import pathlib
from flask import Flask, render_template, request, Response
from html import escape
from fontTools.ttLib import TTFont
from fontTools.pens.svgPathPen import SVGPathPen
app = Flask(__name__)
VERSION = "0.7"
_LOGO_PATH = pathlib.Path(__file__).parent / 'static' / 'logo.png'
_LOGO_B64 = base64.b64encode(_LOGO_PATH.read_bytes()).decode()
_FONT_PATH = pathlib.Path('/usr/local/share/fonts/г/ГОСТ_тип_А.ttf')
_FONT_B64 = base64.b64encode(_FONT_PATH.read_bytes()).decode()
_FONT_FACE = (
'<defs><style>'
'@font-face {'
'font-family:"ГОСТ тип А";'
f'src:url("data:font/truetype;base64,{_FONT_B64}") format("truetype");'
'}'
'</style></defs>'
)
_FONT_OBJ = TTFont(str(_FONT_PATH))
_GLYPH_SET = _FONT_OBJ.getGlyphSet()
_CMAP = _FONT_OBJ.getBestCmap()
_HMTX = _FONT_OBJ['hmtx'].metrics
_UPM = _FONT_OBJ['head'].unitsPerEm
# SVG absolute coordinate constants (1 user unit = 1 mm)
ZERO_X = 60.844130
MAX_X = 180.251230
BAR_Y = 107.54670
SCALE_LEN = MAX_X - ZERO_X # 119.4071 mm
BIG_H = 5.0895
SMALL_H = 2.6683
_UNIT_AREA_WIDTH = 18.445
_UNIT_SCALE_X = 0.96486402
_UNIT_CHAR_EM = 0.65
def _unit_font_size(unit: str) -> float:
n = max(1, len(unit))
return min(18.7981, _UNIT_AREA_WIDTH / (n * _UNIT_SCALE_X * _UNIT_CHAR_EM))
def _fmt(v: float) -> str:
if v == int(v):
return str(int(v))
return f"{v:.4g}"
def _text_to_paths(text, x, y, font_size, anchor='start', fill='#1a1a1a', wrap_transform=None):
s = font_size / _UPM
if anchor == 'middle':
total_w = sum(
_HMTX.get(_CMAP.get(ord(ch)), (0, 0))[0] * s
for ch in text if _CMAP.get(ord(ch))
)
x -= total_w / 2
parts = []
cx = x
for ch in text:
gn = _CMAP.get(ord(ch))
if gn is None:
continue
pen = SVGPathPen(_GLYPH_SET)
_GLYPH_SET[gn].draw(pen)
d = pen.getCommands()
if d:
t = f'translate({cx:.5f},{y:.5f}) scale({s:.8f},{-s:.8f})'
parts.append(f'<path style="fill:{fill}" transform="{t}" d="{d}"/>')
cx += _HMTX.get(gn, (0, 0))[0] * s
inner = '\n'.join(parts)
if wrap_transform:
return f'<g transform="{wrap_transform}">{inner}</g>'
return inner
def generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, label_count, use_paths=True):
big_interval = SCALE_LEN / big_ticks
big_xs = [ZERO_X + i * big_interval for i in range(big_ticks + 1)]
label_count = max(2, min(label_count, big_ticks + 1))
label_indices = [round(k * big_ticks / (label_count - 1)) for k in range(label_count)]
parts = [
'<?xml version="1.0" encoding="UTF-8" standalone="no"?>',
'<svg width="157.18373mm" height="26.683672mm"'
' viewBox="0 0 157.18373 26.683672" version="1.1"'
' xmlns="http://www.w3.org/2000/svg">',
]
if use_paths:
parts.append(' <g transform="translate(-37.352226,-90.454928)">')
else:
parts += [_FONT_FACE, ' <g transform="translate(-37.352226,-90.454928)">']
parts.append(
' <path style="fill:#ffffff;stroke:#1a1a1a;stroke-width:0.184;'
'stroke-linecap:round;stroke-miterlimit:10"'
' d="M 37.444226,90.546928 V 117.0466 H 194.44395 V 90.546928 Z'
' M 58.443978,107.54692 H 183.64409 v 6.19963 H 58.443978 Z" />'
)
if use_paths:
parts += [
' ' + _text_to_paths(
unit, 41.455711, 109.03796, _unit_font_size(unit),
wrap_transform=f'scale({_UNIT_SCALE_X},1.0364155)'
),
' ' + _text_to_paths(range_label, 184.79707, 113.6755, 3.60539),
]
else:
fs = _unit_font_size(unit)
parts += [
f' <text style="font-size:{fs:.4f}px;font-family:\'ГОСТ тип А\',monospace;fill:#1a1a1a"'
f' x="41.455711" y="109.03796"'
f' transform="scale({_UNIT_SCALE_X},1.0364155)">{escape(unit)}</text>',
f' <text style="font-size:3.60539px;font-family:\'ГОСТ тип А\',monospace;fill:#1a1a1a"'
f' x="184.79707" y="113.6755">{escape(range_label)}</text>',
]
# Major ticks
for x in big_xs:
parts.append(
f' <line x1="{x:.5f}" y1="{BAR_Y}" x2="{x:.5f}" y2="{BAR_Y - BIG_H:.5f}"'
f' style="stroke:#1a1a1a;stroke-width:0.555;stroke-linecap:butt" />'
)
# Minor ticks
if small_ticks > 0:
sub = small_ticks + 1
for i in range(big_ticks):
for j in range(1, small_ticks + 1):
x = big_xs[i] + j * big_interval / sub
parts.append(
f' <line x1="{x:.5f}" y1="{BAR_Y}" x2="{x:.5f}" y2="{BAR_Y - SMALL_H:.5f}"'
f' style="stroke:#1a1a1a;stroke-width:0.302;stroke-linecap:butt" />'
)
# Numeric labels
for idx in label_indices:
x = big_xs[idx]
val = min_val + (max_val - min_val) * idx / big_ticks
label = _fmt(val)
if use_paths:
parts.append(' ' + _text_to_paths(label, x, 101.49598, 8.89194, anchor='middle'))
else:
parts.append(
f' <text style="font-size:8.89194px;font-family:\'ГОСТ тип А\',monospace;fill:#1a1a1a"'
f' text-anchor="middle" x="{x:.5f}" y="101.49598">{escape(label)}</text>'
)
parts += [' </g>', '</svg>']
return '\n'.join(parts)
@app.route('/')
def index():
return render_template('index.html', logo_b64=_LOGO_B64, version=VERSION)
@app.route('/generate', methods=['POST'])
def generate():
unit = request.form.get('unit', '%')
min_val = float(request.form.get('min', 0))
max_val = float(request.form.get('max', 100))
range_label = request.form.get('range_label', '5mA')
big_ticks = max(1, min(50, int(request.form.get('big_ticks', 10))))
small_ticks = max(0, min(20, int(request.form.get('small_ticks', 4))))
label_count = max(2, int(request.form.get('label_count', 6)))
use_paths = request.form.get('output_mode', 'paths') == 'paths'
svg = generate_svg(unit, min_val, max_val, range_label, big_ticks, small_ticks, label_count, use_paths)
return Response(svg, mimetype='image/svg+xml')
if __name__ == '__main__':
app.run(debug=True)