Installing Packages from Koji

Quick script to install the version of PHP that’s been patched for the two critical vulns today

dnf install koji

mkdir -p /root/php-8.5.12-koji
cd /root/php-8.5.12-koji

koji download-build --arch=x86_64 --arch=noarch php-8.5.12-1.fc44

# Select only RPMs whose package name and architecture are already installed.
rpms=()
for file in ./*.rpm; do
package=$(rpm -qp --qf '%{NAME}.%{ARCH}' "$file") || exit 1
if rpm -q "$package" >/dev/null 2>&1; then
rpms+=("$file")
fi
done

if ((${#rpms[@]} == 0)); then
echo "No downloaded RPMs match installed packages."
exit 1
fi

printf 'Selected: %s\n' "${rpms[@]}"

dnf install "${rpms[@]}"

Bernette B70 Reminder: Back Up Your Position

The Bernette B70 has a standby mode (Eco) for when you need to pause a job. Very convenient – especially since we’re still at the “babysit it the whole time” level of machine embroidery usage. Except — it appears that (at least for long-term standby), the system loads before the USB drive is mounted. It has cached information for the job preview, but it doesn’t actually have access to the stepper instructions. The machine essentially loses its place.

Which is fine if you know where you were when you stopped. There’s a sequence control section (#3 below)

Which allows you to use the upper and lower knobs to advance through the instructions. Upper knob is a slow advance, lower makes bigger jumps. You can also click on the magnifying glass and type in where you were. If, of course, you had backed up where you were when you paused the job!

Threads – Changes

I had a few issues that prevented me from using Threads in its current form. I suspect my use case differs from the one they are coding to – I have resources and no need to limit myself to a small number of thread colors. Also, a hidden UI element seems to hide a few neighboring elements that I wanted to use.

Updates:

app.py
@@ -12,6 +12,7 @@ family of projects integrated:
“””
import base64
import json
+import logging
import os
import sys
import tempfile
@@ -36,8 +37,13 @@ from inkstitchlib import ai_naming
from inkstitchlib import density as density_map
from inkstitchlib import presets, sketch as sketch_mod

-JOBS = os.path.join(tempfile.gettempdir(), ‘stitchforge_jobs’)
-os.makedirs(JOBS, exist_ok=True)
+#JOBS = os.path.join(tempfile.gettempdir(), ‘stitchforge_jobs’)
+#os.makedirs(JOBS, exist_ok=True)
+from pathlib import Path
+
+JOBS = Path(__file__).resolve().parent / “stitchforge_jobs”
+JOBS.mkdir(parents=True, exist_ok=True)
+

app = FastAPI(title=’StitchForge’)

@@ -276,6 +282,8 @@ def studio():
@app.post(‘/api/analyze’)
async def analyze(image: UploadFile = File(…), colors: int = Form(3)):
“””Separate the image into colour layers and report stroke widths.”””
+ if not 1 <= colors <= segment.MAX_COLORS:
+ raise HTTPException(400, ‘colors must be between 1 and %d’ % segment.MAX_COLORS)
job = uuid.uuid4().hex[:12]
d = _job_dir(job, must_exist=False)
os.makedirs(d, exist_ok=True)
@@ -315,6 +323,8 @@ async def digitize(job: str = Form(…), colors: int = Form(3),
cap_mode: bool = Form(False),
trim_dist: float = Form(2.0),
fabric: str = Form(”), machine: str = Form(”)):
+ if not 1 <= colors <= segment.MAX_COLORS:
+ raise HTTPException(400, ‘colors must be between 1 and %d’ % segment.MAX_COLORS)
d = _job_dir(job)
src = None
for fn in os.listdir(d):
@@ -716,6 +726,8 @@ async def vectorize(image: UploadFile = File(…), colors: int = Form(4),
“””Look at the artwork and return editable vector layers — connected
regions as single objects, similar round shapes grouped, one layer per
large shape. No stitches are made until /api/stitch_layers.”””
+ if not 1 <= colors <= segment.MAX_COLORS:
+ raise HTTPException(400, ‘colors must be between 1 and %d’ % segment.MAX_COLORS)
job = uuid.uuid4().hex[:12]
d = _job_dir(job, must_exist=False)
os.makedirs(d, exist_ok=True)
@@ -723,7 +735,7 @@ async def vectorize(image: UploadFile = File(…), colors: int = Form(4),
with open(src, ‘wb’) as f:
f.write(await image.read())
try:
– lyrs, w, h = veclayers.vectorize(src, max(1, min(8, colors)),
+ lyrs, w, h = veclayers.vectorize(src, colors,
max(10.0, min(400.0, width_mm)))
except veclayers.LayerError as e:
raise HTTPException(400, str(e))
@@ -766,12 +778,20 @@ async def stitch_layers(data: dict):
“””Sew the arranged layers into a design (this is when stitches exist).”””
lyrs = data.get(‘layers’) or []
if not isinstance(lyrs, list) or not lyrs:
+ logging.getLogger(‘uvicorn.error’).warning(
+ ‘AI layer stitching rejected: no layers to stitch’)
raise HTTPException(400, ‘no layers to stitch’)
– if len(lyrs) > 80:
– raise HTTPException(400, ‘too many layers (max 80)’)
+ max_layers = 512
+ if len(lyrs) > max_layers:
+ logging.getLogger(‘uvicorn.error’).warning(
+ ‘AI layer stitching rejected: %d layers exceeds the limit of %d’,
+ len(lyrs), max_layers)
+ raise HTTPException(400, ‘too many layers (max %d)’ % max_layers)
try:
pat, block_layers = veclayers.stitch(lyrs, settings=_stitch_settings(data))
except veclayers.LayerError as e:
+ logging.getLogger(‘uvicorn.error’).warning(
+ ‘AI layer stitching rejected (%d layers): %s’, len(lyrs), e)
raise HTTPException(400, str(e))
except Exception as e:
traceback.print_exc()
@@ -796,8 +816,9 @@ def fills_list():
return [{‘id’: k, ‘name’: patterns.FILL_LABELS[k]} for k in veclayers.FILL_METHODS]

-_FILL_PREVIEW_DIR = os.path.join(tempfile.gettempdir(), ‘stitchforge_fills’)
–
+#_FILL_PREVIEW_DIR = os.path.join(tempfile.gettempdir(), ‘stitchforge_fills’)
+_FILL_PREVIEW_DIR = Path(__file__).resolve().parent / “stitchforge_fills”
+_FILL_PREVIEW_DIR.mkdir(parents=True, exist_ok=True)

@app.get(‘/api/fill_preview/{name}.png’)
def fill_preview(name: str):

digitizer/core.py

@@ -610,9 +610,10 @@ def smooth(p, k=9):
if len(p) < k or k < 3:
return p
ker = np.ones(k) / k
+ padded = np.pad(p, ((k // 2, (k – 1) // 2), (0, 0)), mode=’edge’)
out = p.copy()
– out[:, 0] = np.convolve(p[:, 0], ker, mode=’same’)
– out[:, 1] = np.convolve(p[:, 1], ker, mode=’same’)
+ out[:, 0] = np.convolve(padded[:, 0], ker, mode=’valid’)
+ out[:, 1] = np.convolve(padded[:, 1], ker, mode=’valid’)
out[:2], out[-2:] = p[:2], p[-2:] # don’t drag the tips inward
return out

@@ -1041,7 +1042,8 @@ def sew_blob(sewer, poly, spacing, max_span, border_w, travel=None,
if not heavy_underlay and mode == ‘heavy’:
mode = ‘auto’
if _do(phase, UNDER) and mode != ‘none’:
– sew_edge_run(sewer, poly, inset=min(0.6, border_w), travel=travel)
+ inset = min(0.6, border_w) if border_w > 0 else 0.6
+ sew_edge_run(sewer, poly, inset=inset, travel=travel)
ax = principal_angle(poly)
if mode == ‘heavy’:
for ang in (ax + 45, ax – 45):
@@ -1054,7 +1056,7 @@ def sew_blob(sewer, poly, spacing, max_span, border_w, travel=None,
if not _do(phase, TOP):
return

– ring = satin_ring(poly, border_w, spacing, start=sewer.pos)
+ ring = satin_ring(poly, border_w, spacing, start=sewer.pos) if border_w > 0 else None
core = poly.buffer(-border_w * 0.72) if ring else poly
if core.geom_type == ‘MultiPolygon’:
core = max(core.geoms, key=lambda g: g.area)

digitizer/engine.py

@@ -24,7 +24,7 @@ class Params:
underlay_spacing: float = 2.5
min_fill_area: float = 0.20
min_blob_area: float = 2.0 # below this, satin-ing a blob isn’t worth it
– border_width: float = 1.0 # satin outline band around blobs
+ border_width: float = 0.0 # optional satin outline band; zero disables it
fill_method: str = ‘tatami’ # tatami | contour | circular (Ink/Stitch fills)
fill_angle: float = 65.0
heavy_underlay: bool = True
@@ -155,10 +155,7 @@ def build_pattern(layers, canvas_mm, width_px, p: Params):
if phase != core.UNDER:
fill_methods.sew_area(s, g, p.fill_method, p.fill_angle,
p.row_spacing, p.max_stitch, travel=travel)
– # too wide to satin across, but it still needs a hard edge:
– # a satin outline over the fill is what stops a torso or a
– # head reading as ragged
– if q.area >= p.min_blob_area * 2:
+ if p.border_width > 0 and q.area >= p.min_blob_area * 2:
ring = core.satin_ring(g, p.border_width, p.row_spacing)
if ring:
core.sew_ring(s, ring, travel)

digitizer/segment.py

@@ -4,6 +4,7 @@ import cv2
from PIL import Image

MAX_WORK_PX = 1400 # working raster; bigger only costs time
+MAX_COLORS = 32

def load(path):
@@ -43,7 +44,7 @@ def quantize(rgba, n_colors, fg=None):
pix = rgb[fg]
if len(pix) < 16:
raise ValueError(‘the image looks empty once the background is removed’)
– n_colors = int(max(1, min(n_colors, 8, len(np.unique(pix, axis=0)))))
+ n_colors = int(max(1, min(n_colors, MAX_COLORS, len(np.unique(pix, axis=0)))))
crit = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 30, 0.5)
_, lbl, centers = cv2.kmeans(pix, n_colors, None, crit, 4,
cv2.KMEANS_PP_CENTERS)

inkstitchlib/layers.py

@@ -21,7 +21,7 @@ from shapely.geometry import Polygon, MultiPolygon, LineString
from digitizer import segment, core
from . import fills, patterns

-DEFAULT_PARAMS = {‘stitch’: ‘auto’, ‘fill_method’: ‘tatami’,
+DEFAULT_PARAMS = {‘stitch’: ‘fill’, ‘fill_method’: ‘tatami’,
‘angle’: ‘auto’, ‘density’: 0.4, ‘border_mm’: 1.0}

inkstitchlib/worksheet.py

@@ -444,6 +444,7 @@ def _production_page(c, pattern, report, layers, thread_matches, preview_png,
_text(c, cols[‘Name’], y, _fit(c, name, name_w, body, fs), fs)
_text(c, cols[‘Chart’], y, chart, fs, align=’right’)
y -= lh
+ next_block = min(len(blocks), i + 1) if blocks else 0
c.setStrokeColor(black)
c.setLineWidth(0.8)
c.line(col_x, y + lh – 3.5, x1, y + lh – 3.5)
@@ -455,6 +456,75 @@ def _production_page(c, pattern, report, layers, thread_matches, preview_png,
_text(c, x0 + 150, fy, ‘Design last saved : %s’ % _stamp(saved_at or now), 7.5)
_text(c, x0 + 345, fy, ‘Date printed: %s’ % _stamp(now), 7.5)
_text(c, x1 – 3, fy, ‘Page %d of %d’ % (c.getPageNumber(), pages), 7.5, align=’right’)
+ return next_block
+
+
+def _production_stop_page(c, blocks, layers, thread_matches, start, design_name,
+ client, saved_at, pages):
+ “””Draw a full-width continuation page for the production stop sequence.”””
+ W, H = letter
+ body = c._body
+ m = 0.22 * 72
+ x0, x1, y0, y1 = m, W – m, m, H – m
+ c.setStrokeColor(black)
+ c.setLineWidth(0.8)
+ c.rect(x0, y0, x1 – x0, y1 – y0)
+
+ _text(c, x0 + 8, y1 – 20, ‘Production Worksheet’, 12.5, bold=True, color=c._accent)
+ _text(c, x0 + 8, y1 – 35, design_name, 9)
+ _text(c, x0 + 8, y1 – 55, ‘Stop Sequence (continued)’, 10, bold=True)
+ c.line(x0, y1 – 64, x1, y1 – 64)
+
+ cols = {‘#’: x0 + 8, ‘N#’: x0 + 35, ‘Color’: x0 + 66, ‘St.’: x0 + 151,
+ ‘Code’: x0 + 158, ‘Name’: x0 + 245, ‘Chart’: x1 – 8}
+ y = y1 – 82
+ for key, align in ((‘#’, ‘l’), (‘N#’, ‘l’), (‘Color’, ‘l’), (‘St.’, ‘r’),
+ (‘Code’, ‘l’), (‘Name’, ‘l’), (‘Chart’, ‘r’)):
+ _text(c, cols[key], y, key, 8, bold=True,
+ align=’right’ if align == ‘r’ else ‘left’)
+ c.line(x0 + 5, y – 4, x1 – 5, y – 4)
+ y -= 16
+
+ unique = []
+ for layer in layers:
+ if layer[‘hex’].upper() not in unique:
+ unique.append(layer[‘hex’].upper())
+ index = start
+ while index < len(blocks) and y >= y0 + 30:
+ block = blocks[index]
+ layer = layers[index] if index < len(layers) else {
+ ‘hex’: ‘#888888’, ‘name’: ‘Colour %d’ % (index + 1)}
+ needle = unique.index(layer[‘hex’].upper()) + 1 \
+ if layer[‘hex’].upper() in unique else index + 1
+ match = thread_matches[index] if thread_matches and index < len(thread_matches) else None
+ code = (match.get(‘thread_number’) or ”) if match else ”
+ name = (match.get(‘thread_name’) if match else None) or layer.get(‘name’, ”)
+ chart = match.get(‘palette’, ‘Default’) if match else ‘Default’
+ fs = 7.5
+ if c.stringWidth(chart, body, fs) > 70:
+ chart = chart.split(‘ ‘)[0]
+ chart = _fit(c, chart, 70, body, fs)
+ name_w = cols[‘Chart’] – c.stringWidth(chart, body, fs) – cols[‘Name’] – 8
+
+ _text(c, cols[‘#’], y, ‘%d.’ % (index + 1), 8)
+ _text(c, cols[‘N#’], y, str(needle), 8)
+ c.setFillColor(HexColor(layer[‘hex’]))
+ c.setStrokeColor(black)
+ c.rect(cols[‘Color’], y – 2, 72, 9, fill=1)
+ _text(c, cols[‘St.’], y, ‘{:,}’.format(block[‘stitches’]), fs, align=’right’)
+ _text(c, cols[‘Code’], y, _fit(c, code, 80, body, fs), fs)
+ _text(c, cols[‘Name’], y, _fit(c, name, name_w, body, fs), fs)
+ _text(c, cols[‘Chart’], y, chart, fs, align=’right’)
+ y -= 14
+ index += 1
+
+ c.line(x0 + 5, y + 10, x1 – 5, y + 10)
+ now = datetime.datetime.now()
+ _text(c, x0 + 3, y0 + 4, ‘Authors:%s’ % ((‘ ‘ + client) if client else ”), 7.5)
+ _text(c, x0 + 150, y0 + 4, ‘Design last saved : %s’ % _stamp(saved_at or now), 7.5)
+ _text(c, x1 – 3, y0 + 4, ‘Page %d of %d’ % (c.getPageNumber(), pages),
+ 7.5, align=’right’)
+ return index

def _quote_page(c, stitches, quote_params, design_name, client=”, notes=”):
@@ -497,10 +567,17 @@ def build_production(path, pattern, report, layers, thread_matches=None,
c = Canvas(path, pagesize=letter)
c.setTitle(‘%s — production worksheet’ % design_name)
_apply_theme(c, theme, logo_path)
– _production_page(c, pattern, report, layers, thread_matches, preview_png,
– design_name, client, saved_at, title=title,
– pages=2 if quote_params else 1)
+ blocks = block_stats(pattern)
+ continuation_pages = int(math.ceil(max(0, len(blocks) – 32) / 48.0))
+ pages = 1 + continuation_pages + (1 if quote_params else 0)
+ next_block = _production_page(c, pattern, report, layers, thread_matches, preview_png,
+ design_name, client, saved_at, title=title, pages=pages)
c.showPage()
+ while next_block < len(blocks):
+ _apply_theme(c, theme, logo_path)
+ next_block = _production_stop_page(c, blocks, layers, thread_matches, next_block,
+ design_name, client, saved_at, pages)
+ c.showPage()
if quote_params:
_apply_theme(c, theme, logo_path)
_quote_page(c, report.get(‘stitches’, 0), quote_params, design_name)
@@ -582,6 +659,12 @@ def build(path, pattern, report, layers, thread_matches=None, preview_png=None,
c.line(MARGIN, y, PAGE_W – MARGIN, y)
y -= 16
for i, L in enumerate(layers):
+ if y < MARGIN + 24:
+ _footer(c)
+ c.showPage()
+ _apply_theme(c, theme, logo_path)
+ _header(c, design_name, ‘Thread sequence (continued)’)
+ y = PAGE_H – MARGIN – 48
c.setFillColor(HexColor(L[‘hex’]))
c.setStrokeColor(LINE)
c.rect(MARGIN, y – 3, 8 * mm, 5 * mm, fill=1)

static/clients.html

@@ -11,7 +11,7 @@
–bg:#f6f7f9; –surface:#ffffff; –ink:#1a1d23; –muted:#6b7280;
–line:#e9eaee; –line2:#e2e4e9;
–accent:#2563eb; –accent-press:#1d4ed8; –accent-soft:#eef4ff;
– –stage:#181c26; –ok:#188652; –warn:#a8620a; –warnbg:#fdf5e7; –danger:#c2410c;
+ –stage:#cccccc; –ok:#188652; –warn:#a8620a; –warnbg:#fdf5e7; –danger:#c2410c;
–r:16px; –shadow:0 1px 2px rgba(16,24,40,.05),0 8px 28px rgba(16,24,40,.07);
}
*{box-sizing:border-box}

static/index.html

@@ -294,7 +294,7 @@
details.sec[open]>summary::after{transform:rotate(-135deg)}
details.sec>*:not(summary){margin-bottom:6px}
details.sec>div:last-child,details.sec>p:last-child{margin-bottom:14px}
– details.sec:has(> .hide){display:none}
+ details.sec:has(> .hide):not(:has(> :not(summary):not(.hide))){display:none}
details.sec label{margin-top:8px}
details:not(.sec){background:var(–panel2);box-shadow:none;margin-top:10px}
details:not(.sec) summary{color:var(–ink)}
@@ -644,7 +644,7 @@
<div class=”unit”><input type=”number” id=”objGradTo” value=”1.5″ step=”0.1″ min=”0.3″ max=”4″><span>mm</span></div>
<div class=”tglrow”><span>Reverse direction</span><label class=”tog”><input type=”checkbox” id=”objGradFlip”><i></i></label></div>
</div>
– <div class=”tglrow”><span>Satin border</span><label class=”tog”><input type=”checkbox” id=”objBorder” checked><i></i></label></div>
+ <div class=”tglrow”><span>Satin border</span><label class=”tog”><input type=”checkbox” id=”objBorder”><i></i></label></div>
</details>
</div>
<details class=”sec” open><summary>Density Control<i class=”qhelp” title=”On the inside of a bend satin needle points crowd together. Row shortening pulls every other one back once the inner step drops below the trigger × the outer step.”>?</i></summary>
@@ -819,10 +819,7 @@
<option value=”sketch”>Sketch — one-colour outline</option>
</select></div>
<div id=”colorsWrap”><label>Thread colours</label>
– <select id=”colors”>
– <option>2</option><option selected>3</option><option>4</option>
– <option>5</option><option>6</option>
– </select></div>
+ <input id=”colors” type=”number” min=”2″ max=”32″ value=”3″></div>
</div>
<details class=”sec” open><summary>Fill Settings</summary>
<div id=”setDigitize”>
@@ -1009,8 +1006,8 @@
<div class=”row”>
<div><label>Stitch</label>
<select id=”vlStitch”>
– <option value=”auto” selected>Auto (satin/fill + border)</option>
– <option value=”fill”>Fill only</option>
+ <option value=”auto”>Auto (satin/fill + border)</option>
+ <option value=”fill” selected>Fill only</option>
<option value=”outline”>Satin outline</option>
<option value=”run”>Running outline</option>
<option value=”bean”>Bean-stitch outline</option>
@@ -3747,7 +3744,7 @@ function mmPos(el, e) { const p = worldPos(el, e); return [p[0] / PXMM, p[1] / P
const RUNS = [‘run’, ‘bean’, ‘satin’, ‘estitch’, ‘triangle’, ‘cross’, ‘motif’];
const DECOR = [‘estitch’, ‘triangle’, ‘cross’, ‘motif’];
const objDef = { color: ‘#1A3B69’, filled: true, run: ‘run’, pattern: ‘tatami’, angle: ‘auto’, density: 0.4, satinDensity: 0.32,
– border: true, width: 3, runLen: 2.5, stitchLen: 3.5, pull: 0.25, hand: 0, underpath: true, rowShort: true, trigger: 0.5,
+ border: false, width: 3, runLen: 2.5, stitchLen: 3.5, pull: 0.25, hand: 0, underpath: true, rowShort: true, trigger: 0.5,
underlay: ‘auto’, split: true, splitMax: 7, stagger: true, cycles: 4, amount: 0.3, gradient: false, gradTo: 1.5, gradFlip: false,
shortFrac: 0.45, underlays: null };
const UL_DEFAULT = { auto: [{ type: ‘contour’, len_mm: 1.5 }, { type: ‘center’, len_mm: 2.5 }], light: [{ type: ‘contour’, len_mm: 1.5 }], heavy: [{ type: ‘contour’, len_mm: 1.5 }, { type: ‘zigzag’, len_mm: 3 }], none: [] };
@@ -3835,7 +3832,7 @@ function syncObjBox() {
d.pattern = p.fill_method || ‘tatami’;
d.angle = p.angle === undefined ? ‘auto’ : p.angle;
if (!d.filled || isLine) d.satinDensity = p.density || 0.32; else d.density = p.density || 0.4;
– d.border = p.stitch !== ‘fill’;
+ d.border = p.stitch === ‘auto’ || p.stitch === ‘satin’;
d.width = p.width_mm || 3; d.runLen = p.run_len_mm || 2.5; d.stitchLen = p.stitch_len_mm || 3.5;
d.pull = p.pull_comp_mm === undefined ? +$(‘pullComp’).value || 0.25 : p.pull_comp_mm;
d.hand = p.hand || 0;

tests/test_app.py

@@ -1,9 +1,11 @@
“””End-to-end tests over the HTTP API (run with: pytest).”””
import io
import os
+import re
import sys
import tempfile

+import numpy as np
import pytest
from PIL import Image, ImageDraw

@@ -12,6 +14,7 @@ os.environ.setdefault(‘STITCHFORGE_DATA’,
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from fastapi.testclient import TestClient
+import pystitch
from app import app

client = TestClient(app)
@@ -51,6 +54,38 @@ def test_digitize_report(image_job):
assert d[‘threads’][0][‘thread_name’]

+def test_image_quantizer_supports_32_colors():
+ from digitizer import segment
+ colors = np.array([[i * 7 % 256, i * 13 % 256, i * 23 % 256, 255]
+ for i in range(32)], dtype=np.int16)
+ rgba = np.repeat(colors[:, None, :], 20, axis=1)
+ layers = segment.quantize(rgba, 32, np.ones(rgba.shape[:2], dtype=bool))
+ assert len(layers) == 32
+
+
+def test_image_color_limit_is_validated():
+ r = client.post(‘/api/analyze’,
+ files={‘image’: (‘t.png’, _test_png(), ‘image/png’)},
+ data={‘colors’: 33})
+ assert r.status_code == 400
+ assert r.json()[‘detail’] == ‘colors must be between 1 and 32’
+
+
+def test_vectorize_uses_requested_colors():
+ im = Image.new(‘RGBA’, (240, 80), (0, 0, 0, 0))
+ d = ImageDraw.Draw(im)
+ for i in range(12):
+ d.rectangle([i * 20 + 2, 10, i * 20 + 17, 69],
+ fill=(i * 19, i * 37 % 256, i * 61 % 256, 255))
+ buf = io.BytesIO()
+ im.save(buf, ‘PNG’)
+ r = client.post(‘/api/vectorize’,
+ files={‘image’: (‘colors.png’, buf.getvalue(), ‘image/png’)},
+ data={‘colors’: 12})
+ assert r.status_code == 200, r.text
+ assert len({layer[‘color’] for layer in r.json()[‘layers’]}) == 12
+
+
def test_exports(image_job):
job, _ = image_job
for fmt in (‘dst’, ‘pes’, ‘jef’, ‘exp’, ‘vp3’, ‘csv’):
@@ -429,6 +464,44 @@ def test_production_worksheet():
assert ps[‘max_stitch_mm’] > 0 and ps[‘thread_ft’] > ps[‘bobbin_ft’] > 0

+def test_worksheet_thread_lists_paginate(tmp_path, monkeypatch):
+ from inkstitchlib import worksheet
+ pattern = pystitch.EmbPattern()
+ layers = []
+ for index in range(40):
+ color = ((index * 47) % 256 << 16) | ((index * 83) % 256 << 8) | ((index * 131) % 256)
+ thread = pystitch.EmbThread()
+ thread.color = color
+ pattern.add_thread(thread)
+ if index:
+ pattern.color_change()
+ pattern.stitch_abs(index * 10, 0)
+ pattern.stitch_abs(index * 10 + 5, 5)
+ layers.append({‘name’: ‘Colour %d’ % (index + 1), ‘hex’: ‘#%06X’ % color})
+
+ drawn = []
+ original_text = worksheet._text
+
+ def capture_text(canvas, x, y, text, *args, **kwargs):
+ drawn.append(str(text))
+ return original_text(canvas, x, y, text, *args, **kwargs)
+
+ monkeypatch.setattr(worksheet, ‘_text’, capture_text)
+ report = {‘stitches’: 80, ‘width_mm’: 40, ‘height_mm’: 1,
+ ‘colour_changes’: 39, ‘travels’: 0, ‘trimmed’: 0}
+
+ production = tmp_path / ‘production.pdf’
+ worksheet.build(str(production), pattern, report, layers, layout=’production’)
+ assert ’40.’ in drawn
+ assert len(re.findall(rb’/Type\s*/Page\b’, production.read_bytes())) == 2
+
+ drawn.clear()
+ classic = tmp_path / ‘classic.pdf’
+ worksheet.build(str(classic), pattern, report, layers, layout=’classic’)
+ assert any(text.startswith(‘#40 ‘) for text in drawn)
+ assert len(re.findall(rb’/Type\s*/Page\b’, classic.read_bytes())) >= 3
+
+
def test_client_design_library():
# main page + studio routes
assert b’clients’ in client.get(‘/’).content.lower()

Threads – Pixel Art to Embroidery Stitch Digitization

Learning how to use Threads to digitize pixel art into embroidery patterns. I thought it might be easier to learn with a simpler design. So I created a quick lace/filigree cat silhouette.

If this is your first time designing for this hoop size, go to settings

Scroll down and use the drop-down to select “Custom Size” in the hoop selection

Input the dimensions of the hoop and give it a name (The 6×10 frame for our Bernette is called “Bernette B70 – Large”

You can also adjust some values to control the stitch density — these are the settings I am using for dense designs being embroidered on t-shirts.

Upload the image —

Adjust the size (if needed), select the number of thread colors. For now, I’m leaving the fill method set to the defaults!

Scroll down and click the “AI Layers” button

This will break your image into layers and begin the digitization process.

Click on each layer – you can adjust the settings used to compute stitches for the layer. You can change the color, use a different fill pattern, adjust density settings.

Alternately – you can click “Digitize” which doesn’t provide as much editing prior to digitization.

Don’t forget to save the digitized project

You can sort the patterns by “Client” — I made sections for each of us

Clicking on the project will bring up a summary where you can download a DST file for use in an embroidery machine or the “Worksheet”, which is a PDF with the thread order.

 

Ink/Stitch and Threads

I had been trying to use InkScape and Ink/Stitch to digitize images for embroidery patterns. While I am certain that not really knowing what I’m doing is a huge component … I never manage to get a good pattern. The digitization process also sucks up all the resources on my laptop before falling over and requiring a restart. I was going to borrow Scott’s gaming laptop (lots of memory, lots of CPU, and GPU). Then I came across Threads — a Python Uvicorn server that uses the Ink/Stitch API to digitize images into embroidery patterns.

There was a significant limitation that I assume comes from the documented usage: you cannot create a pattern with a large number of unique colors. You can select up to six thread colors. However, I wasn’t going to run the tool on a laptop. I’m going to run it on a VM on our big server. Plenty of memory and processor available. And it can chug along, doing its thing, without impacting other VMs. So I created a fork that allows up to 32 unique thread colors (more than you probably want).

I want to digitize this mushroom design I created for Scott:

Blender Script: Moving Object

This script moves the selected object to be centered on the world origin but it sits on the z plane rather than half of it being below the z plane.

 

import bpy
from mathutils import Vector

# Put the origin at the center of volume
bpy.ops.object.origin_set(
type='ORIGIN_CENTER_OF_VOLUME',
center='MEDIAN',
)

for obj in bpy.context.selected_objects:
# The bounding-box corners are stored in object-local coordinates.
# Transform them to world coordinates before finding the lowest point.
lowest_z = min(
(obj.matrix_world @ Vector(corner)).z
for corner in obj.bound_box
)

world_matrix = obj.matrix_world.copy()
world_matrix.translation.x = 0.0
world_matrix.translation.y = 0.0
world_matrix.translation.z -= lowest_z
obj.matrix_world = world_matrix

Changing Password in an RDP session in an RDP session

You can use Ctrl-Alt-Del to bring up a menu to invoke a password change on your local machine. When you RDP to a box, there’s Ctrl-Alt-End but when you are using a jump box (rdp to box1 to rdp to box2), ctrl-alt-end lets you change the password on Box1, but how do you change the password on Box2?

explorer.exe shell:::{2559a1f2-21d7-11d4-bdaf-00c04f60b9f0}

Run that command

And you get the menu to change that password