export hpgl load from folder

This commit is contained in:
Lukas Cremer
2026-02-05 22:50:36 +01:00
parent 82e5ef4b73
commit 235365063f
6 changed files with 174 additions and 29 deletions

File diff suppressed because one or more lines are too long

View File

@@ -33,36 +33,13 @@ def _flatten_cubic(p0, p1, p2, p3, n=10):
yield (x, y) yield (x, y)
def text_to_hpgl(text, font_path, size_pt=72, units_per_pt=14): def _path_to_segments(vertices, codes, scale):
""" """Convert a single path (vertices, codes) to list of (PU/PD, x, y) in HPGL units."""
Convert text to HPGL string using the given font file.
size_pt: font size in points.
units_per_pt: HPGL units per point (default ~0.35 mm per pt).
Returns HPGL string (IN;SP1;PU x,y;PD;PA x,y;...).
"""
try:
from matplotlib.textpath import TextPath
from matplotlib.font_manager import FontProperties
except ImportError:
raise RuntimeError('matplotlib is required for text-to-HPGL. Install with: pip install matplotlib')
if not text or not font_path or not os.path.isfile(font_path):
raise ValueError('Need non-empty text and a valid font file path.')
prop = FontProperties(fname=font_path)
path = TextPath((0, 0), text, size=size_pt, prop=prop)
vertices = path.vertices
codes = path.codes
if codes is None: if codes is None:
codes = [MOVETO] + [LINETO] * (len(vertices) - 1) codes = [MOVETO] + [LINETO] * (len(vertices) - 1)
# Scale to HPGL units (y flip: matplotlib y up, HPGL typically y up too; we keep same)
scale = units_per_pt
segments = [] segments = []
i = 0 i = 0
contour_start = None contour_start = None
while i < len(codes): while i < len(codes):
code = codes[i] code = codes[i]
if code == MOVETO: if code == MOVETO:
@@ -94,9 +71,44 @@ def text_to_hpgl(text, font_path, size_pt=72, units_per_pt=14):
i += 3 i += 3
else: else:
i += 1 i += 1
return segments
def text_to_hpgl(text, font_path, size_pt=72, units_per_pt=14):
"""
Convert text to HPGL string using the given font file.
Newlines in text start a new line (stacked vertically).
size_pt: font size in points.
units_per_pt: HPGL units per point (default ~0.35 mm per pt).
Returns HPGL string (IN;SP1;PU x,y;PD;PA x,y;...).
"""
try:
from matplotlib.textpath import TextPath
from matplotlib.font_manager import FontProperties
except ImportError:
raise RuntimeError('matplotlib is required for text-to-HPGL. Install with: pip install matplotlib')
if not text or not font_path or not os.path.isfile(font_path):
raise ValueError('Need non-empty text and a valid font file path.')
# Normalize line endings and split into lines
lines = text.replace('\r\n', '\n').replace('\r', '\n').split('\n')
scale = units_per_pt
line_height_pt = size_pt * 1.2 # line spacing
prop = FontProperties(fname=font_path)
all_segments = []
for line_index, line in enumerate(lines):
if not line.strip():
continue
# Each line at its own Y: first line at 0, next at -line_height_pt, etc. (y down)
path = TextPath((0, -line_index * line_height_pt), line, size=size_pt, prop=prop)
vertices = path.vertices
codes = path.codes
all_segments.extend(_path_to_segments(vertices, codes, scale))
out = ['IN', 'SP1'] out = ['IN', 'SP1']
for seg in segments: for seg in all_segments:
if seg[0] == 'PU': if seg[0] == 'PU':
out.append('PU{},{}'.format(seg[1], seg[2])) out.append('PU{},{}'.format(seg[1], seg[2]))
else: else:

View File

@@ -29,6 +29,7 @@ app.secret_key = webui_secret_key()
app.config['MAX_CONTENT_LENGTH'] = webui_max_upload_mb() * 1024 * 1024 app.config['MAX_CONTENT_LENGTH'] = webui_max_upload_mb() * 1024 * 1024
UPLOAD_DIR = os.path.join(ROOT, 'webui', 'uploads') UPLOAD_DIR = os.path.join(ROOT, 'webui', 'uploads')
os.makedirs(UPLOAD_DIR, exist_ok=True) os.makedirs(UPLOAD_DIR, exist_ok=True)
OUTPUT_HPGL_DIR = os.path.join(ROOT, 'output', 'hpgl')
FONT_DIRS = [ FONT_DIRS = [
os.path.join(ROOT, 'font'), os.path.join(ROOT, 'font'),
os.path.join(ROOT, '.git', 'font'), os.path.join(ROOT, '.git', 'font'),
@@ -119,6 +120,45 @@ def api_text_to_hpgl():
return jsonify({'ok': True, 'filename': session['upload_filename'], 'preview_url': '/api/svg'}) return jsonify({'ok': True, 'filename': session['upload_filename'], 'preview_url': '/api/svg'})
@app.route('/api/hpgl_files')
def list_hpgl_files():
"""List HPGL filenames in output/hpgl/."""
if not os.path.isdir(OUTPUT_HPGL_DIR):
return jsonify({'files': []})
files = []
for name in sorted(os.listdir(OUTPUT_HPGL_DIR)):
if name.startswith('.'):
continue
path = os.path.join(OUTPUT_HPGL_DIR, name)
if not os.path.isfile(path):
continue
low = name.lower()
if low.endswith('.hpgl') or low.endswith('.plt') or '.' not in name:
files.append(name)
return jsonify({'files': files})
@app.route('/api/load_hpgl', methods=['POST'])
def load_hpgl_from_folder():
"""Set current program from a file in output/hpgl/."""
data = request.get_json() or {}
filename = (data.get('filename') or '').strip()
if not filename:
return jsonify({'error': 'No filename'}), 400
if os.path.basename(filename) != filename or '..' in filename:
return jsonify({'error': 'Invalid filename'}), 400
path = os.path.join(OUTPUT_HPGL_DIR, filename)
if not os.path.isfile(path):
return jsonify({'error': 'File not found'}), 404
try:
Program.parsefile(path)
except Exception as e:
return jsonify({'error': 'Invalid HPGL: {}'.format(str(e))}), 400
session['upload_path'] = path
session['upload_filename'] = filename
return jsonify({'ok': True, 'filename': filename, 'preview_url': '/api/svg'})
@app.route('/api/upload', methods=['POST']) @app.route('/api/upload', methods=['POST'])
def upload(): def upload():
if 'file' not in request.files: if 'file' not in request.files:
@@ -144,6 +184,25 @@ def upload():
}) })
@app.route('/api/download_hpgl')
def download_hpgl():
"""Return current program as downloadable HPGL file."""
p = _get_program()
if p is None:
return jsonify({'error': 'No file loaded'}), 404
filename = (session.get('upload_filename') or 'export').strip()
if not filename.lower().endswith('.hpgl') and not filename.lower().endswith('.plt'):
filename = filename + '.hpgl'
return (
str(p),
200,
{
'Content-Type': 'application/octet-stream',
'Content-Disposition': 'attachment; filename="{}"'.format(filename),
},
)
@app.route('/api/dimensions') @app.route('/api/dimensions')
def dimensions(): def dimensions():
"""Return current program size in plotter units (e.g. 0.025 mm per unit).""" """Return current program size in plotter units (e.g. 0.025 mm per unit)."""

View File

@@ -23,7 +23,8 @@
scaleToBounds: document.getElementById('btnScaleToBounds'), scaleToBounds: document.getElementById('btnScaleToBounds'),
print: document.getElementById('btnPrint'), print: document.getElementById('btnPrint'),
checkPlotter: document.getElementById('btnCheckPlotter'), checkPlotter: document.getElementById('btnCheckPlotter'),
generateHpgl: document.getElementById('btnGenerateHpgl') generateHpgl: document.getElementById('btnGenerateHpgl'),
exportHpgl: document.getElementById('btnExportHpgl')
}; };
const tabFile = document.getElementById('tabFile'); const tabFile = document.getElementById('tabFile');
const tabText = document.getElementById('tabText'); const tabText = document.getElementById('tabText');
@@ -33,6 +34,7 @@
const fontSelect = document.getElementById('fontSelect'); const fontSelect = document.getElementById('fontSelect');
const textSize = document.getElementById('textSize'); const textSize = document.getElementById('textSize');
const textMessage = document.getElementById('textMessage'); const textMessage = document.getElementById('textMessage');
const hpglFolderSelect = document.getElementById('hpglFolderSelect');
function setMessage(el, text, type) { function setMessage(el, text, type) {
el.textContent = text || ''; el.textContent = text || '';
@@ -93,7 +95,7 @@
}); });
} }
var transformPrintButtons = ['flip', 'rotate90', 'rotate180', 'scaleUp', 'scaleDown', 'centralize', 'scaleToBounds', 'print']; var transformPrintButtons = ['flip', 'rotate90', 'rotate180', 'scaleUp', 'scaleDown', 'centralize', 'scaleToBounds', 'print', 'exportHpgl'];
function setLoaded(loaded) { function setLoaded(loaded) {
if (loaded) { if (loaded) {
@@ -264,9 +266,52 @@
tabText.classList.toggle('active', !isFile); tabText.classList.toggle('active', !isFile);
if (panelFile) panelFile.classList.toggle('hidden', !isFile); if (panelFile) panelFile.classList.toggle('hidden', !isFile);
if (panelText) panelText.classList.toggle('hidden', isFile); if (panelText) panelText.classList.toggle('hidden', isFile);
if (isFile && hpglFolderSelect && hpglFolderSelect.options.length <= 1) loadHpglFiles();
if (!isFile && fontSelect && fontSelect.options.length <= 1) loadFonts(); if (!isFile && fontSelect && fontSelect.options.length <= 1) loadFonts();
} }
function loadHpglFiles() {
if (!hpglFolderSelect) return;
fetch('/api/hpgl_files')
.then(function (r) { return r.json(); })
.then(function (d) {
var opts = hpglFolderSelect.querySelectorAll('option');
for (var i = opts.length - 1; i >= 1; i--) opts[i].remove();
(d.files || []).forEach(function (f) {
var o = document.createElement('option');
o.value = f;
o.textContent = f;
hpglFolderSelect.appendChild(o);
});
})
.catch(function () {});
}
if (hpglFolderSelect) {
hpglFolderSelect.addEventListener('change', function () {
var filename = (this.value || '').trim();
if (!filename) return;
setMessage(printMessage, '');
fetch('/api/load_hpgl', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ filename: filename })
})
.then(function (r) {
return r.json().then(function (d) {
if (!r.ok) throw new Error(d.error || 'Load failed');
return d;
});
})
.then(function () {
refreshStatus();
})
.catch(function (e) {
setMessage(printMessage, e.message || 'Load failed', 'error');
});
});
}
function loadFonts() { function loadFonts() {
if (!fontSelect) return; if (!fontSelect) return;
fetch('/api/fonts') fetch('/api/fonts')
@@ -287,6 +332,12 @@
if (tabFile) tabFile.addEventListener('click', function () { showTab('file'); }); if (tabFile) tabFile.addEventListener('click', function () { showTab('file'); });
if (tabText) tabText.addEventListener('click', function () { showTab('text'); }); if (tabText) tabText.addEventListener('click', function () { showTab('text'); });
if (buttons.exportHpgl) {
buttons.exportHpgl.addEventListener('click', function () {
window.location.href = '/api/download_hpgl';
});
}
if (buttons.generateHpgl && textInput && fontSelect) { if (buttons.generateHpgl && textInput && fontSelect) {
buttons.generateHpgl.addEventListener('click', function () { buttons.generateHpgl.addEventListener('click', function () {
var text = (textInput.value || '').trim(); var text = (textInput.value || '').trim();
@@ -324,6 +375,7 @@
} }
refreshStatus(); refreshStatus();
loadHpglFiles();
updatePlotterInfo(); updatePlotterInfo();
setInterval(updatePlotterInfo, 10000); setInterval(updatePlotterInfo, 10000);
})(); })();

View File

@@ -155,6 +155,23 @@ body {
background: var(--accent-hover); background: var(--accent-hover);
} }
.btn-export-hpgl {
width: 100%;
margin-top: 0.5rem;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
cursor: pointer;
}
.btn-export-hpgl:hover:not(:disabled) {
background: var(--border);
border-color: var(--accent);
}
.status { .status {
display: flex; display: flex;
gap: 1rem; gap: 1rem;

View File

@@ -24,6 +24,10 @@
<aside class="sidebar"> <aside class="sidebar">
<div id="panelFile" class="tab-panel"> <div id="panelFile" class="tab-panel">
<section class="upload-section"> <section class="upload-section">
<label class="upload-label" for="hpglFolderSelect">Load from folder</label>
<select id="hpglFolderSelect" class="font-select">
<option value="">— Select file —</option>
</select>
<label class="upload-label" for="fileInput">Upload HPGL</label> <label class="upload-label" for="fileInput">Upload HPGL</label>
<input type="file" id="fileInput" accept=".hpgl,.plt" class="file-input"> <input type="file" id="fileInput" accept=".hpgl,.plt" class="file-input">
<p class="hint">.hpgl or .plt files only</p> <p class="hint">.hpgl or .plt files only</p>
@@ -40,6 +44,7 @@
<label class="upload-label" for="textSize">Size (pt)</label> <label class="upload-label" for="textSize">Size (pt)</label>
<input type="number" id="textSize" class="text-size" value="72" min="8" max="500" step="1"> <input type="number" id="textSize" class="text-size" value="72" min="8" max="500" step="1">
<button type="button" class="btn btn-generate" id="btnGenerateHpgl">Generate HPGL</button> <button type="button" class="btn btn-generate" id="btnGenerateHpgl">Generate HPGL</button>
<button type="button" class="btn btn-export-hpgl" id="btnExportHpgl" disabled>Export HPGL</button>
<p id="textMessage" class="message"></p> <p id="textMessage" class="message"></p>
</section> </section>
</div> </div>