export hpgl load from folder
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -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:
|
||||||
|
|||||||
59
webui/app.py
59
webui/app.py
@@ -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)."""
|
||||||
|
|||||||
@@ -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);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user