Skip to content

Commit ebef738

Browse files
The bleed outline is traced, not scaled — and half a millimetre of it (#168)
Scaling the cut up by a percentage is not an offset. On a sticker three times as wide as it is tall, the same percentage adds three times as much ink at the sides as at the top, so the band came out fat on two edges and thin on the others and looked like the cut had slipped. A second distance transform at the bleed distance gives a band the same width the whole way round, which is what a bleed is. And half a millimetre rather than two. Two is what a shop asks for on a big sticker; on a 26mm one it is seven per cent — wide enough that the band bridges the gap between a hammer and the word beside it and the sticker becomes a blob. A bleed is a dilation, and a dilation joins anything closer together than twice its distance, so the number has to suit the sticker rather than the trade convention. Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 5d1c0b0 commit ebef738

4 files changed

Lines changed: 69 additions & 16 deletions

File tree

‎bin/rm-studio.mjs‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6450,6 +6450,7 @@ const server = createServer(async (req, res) => {
64506450
const per = 1024 / stickerMm;
64516451
const offsetPx = Math.round(offsetMm * per);
64526452
const roundPx = Math.round(roundMm * per);
6453+
const bleedPx = Math.round(Math.min(6, Math.max(0, Number(b.bleedMm ?? 0.5))) * per);
64536454
const items = [];
64546455
for (const rel of picked) {
64556456
const file = stickerFile(id, rel);
@@ -6461,7 +6462,7 @@ const server = createServer(async (req, res) => {
64616462
items.push({ name: rel, bytes: raw, type: { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp" }[ext] ?? "image/png" });
64626463
continue;
64636464
}
6464-
items.push({ name: rel, svg: raw.toString("utf8"), key: `v${DIE_VERSION}:${rel}:${Math.round(st.mtimeMs)}:${offsetPx}:${roundPx}` });
6465+
items.push({ name: rel, svg: raw.toString("utf8"), key: `v${DIE_VERSION}:${rel}:${Math.round(st.mtimeMs)}:${offsetPx}:${roundPx}:${bleedPx}` });
64656466
}
64666467
/*
64676468
* The ones already traced cost nothing; the rest go through the same
@@ -6494,7 +6495,7 @@ const server = createServer(async (req, res) => {
64946495
if (inkCache.size > 200) inkCache.delete(inkCache.keys().next().value);
64956496
if (drawn) inkCache.set(inkKey, drawn);
64966497
}
6497-
it.die = await dieLine(it.svg, { offsetPx, roundPx, drawn });
6498+
it.die = await dieLine(it.svg, { offsetPx, roundPx, bleedPx, drawn, page: pg });
64986499
if (dieCache.size > 400) dieCache.delete(dieCache.keys().next().value);
64996500
dieCache.set(it.key, it.die);
65006501
}
@@ -6513,7 +6514,7 @@ const server = createServer(async (req, res) => {
65136514
page,
65146515
stickerMm,
65156516
bleedMm: 3.175,
6516-
stickerBleedMm: Math.min(6, Math.max(0, Number(b.bleedMm ?? 2))),
6517+
stickerBleedMm: Math.min(6, Math.max(0, Number(b.bleedMm ?? 0.5))),
65176518
logo: null,
65186519
title: name,
65196520
});
@@ -6605,7 +6606,7 @@ const server = createServer(async (req, res) => {
66056606
const per = 1024 / stickerMm;
66066607
/* Traced once per size, because the offset is in millimetres on the
66076608
printed sticker and the sticker is a different size on each. */
6608-
const traced = body.die === false ? items : await withDieLines(items, { offsetPx: Math.round(offsetMm * per), roundPx: Math.round(roundMm * per) });
6609+
const traced = body.die === false ? items : await withDieLines(items, { offsetPx: Math.round(offsetMm * per), roundPx: Math.round(roundMm * per), bleedPx: Math.round(Math.min(6, Math.max(0, Number(body.bleedMm ?? 0.5))) * per) });
66096610
const out = join(dir, `${name}-sheet-${page}-cmyk.pdf`);
66106611
const made = await cutSheetToCmykPdf({
66116612
items: traced,
@@ -6614,7 +6615,7 @@ const server = createServer(async (req, res) => {
66146615
page,
66156616
stickerMm,
66166617
bleedMm: 3.175,
6617-
stickerBleedMm: Math.min(6, Math.max(0, Number(body.bleedMm ?? 2))),
6618+
stickerBleedMm: Math.min(6, Math.max(0, Number(body.bleedMm ?? 0.5))),
66186619
logo: null,
66196620
title: name,
66206621
profile: String(body.profile ?? ""),

‎lib/print-sheet.mjs‎

Lines changed: 58 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ export const CUT_MAGENTA = "#ec008c";
197197
* misalignment came back from a cache after it had been fixed. The version is
198198
* part of the key now.
199199
*/
200-
export const DIE_VERSION = 10;
200+
export const DIE_VERSION = 11;
201201

202202
export const CUT_PT = 0.75;
203203

@@ -251,7 +251,13 @@ export const SHEET_PAGES = Object.entries(PAGES).map(([id, p]) => ({ id, ...p })
251251
*/
252252
/*
253253
254-
* Two millimetres by default, which is what a sticker shop asks for.
254+
* Half a millimetre by default.
255+
*
256+
* Two is what a shop asks for on a big sticker, and on a 26mm one it is seven
257+
* per cent — wide enough that the band bridges the gap between a hammer and
258+
* the word beside it and the sticker becomes a blob. A bleed is a dilation and
259+
* a dilation joins anything closer together than twice its distance, so the
260+
* number has to suit the sticker rather than the trade convention.
255261
*
256262
* I set this to nothing once, having read a preview where the extra ink looked
257263
* like the cut being misplaced. It was not: it was the bleed, and taking it
@@ -265,7 +271,7 @@ export const SHEET_PAGES = Object.entries(PAGES).map(([id, p]) => ({ id, ...p })
265271
* doing both jobs, so setting a sticker's bleed moved the page size, and
266272
* setting the page's moved every cut.
267273
*/
268-
export function cutSheetSvg({ items, page = "letter", stickerMm = 50.8, gapMm = 4, marginMm = 10, bleedMm = 3.175, stickerBleedMm = 2, radiusMm = 3, logo = null, title = "Stickers", skin = true }) {
274+
export function cutSheetSvg({ items, page = "letter", stickerMm = 50.8, gapMm = 4, marginMm = 10, bleedMm = 3.175, stickerBleedMm = 0.5, radiusMm = 3, logo = null, title = "Stickers", skin = true }) {
269275
const p = PAGES[page] ?? PAGES.letter;
270276
const W = p.wMm;
271277
const H = p.hMm;
@@ -390,8 +396,8 @@ export function cutSheetSvg({ items, page = "letter", stickerMm = 50.8, gapMm =
390396
* extension produces anyway, and it has nothing in it to ghost.
391397
*/
392398
const bleedBand =
393-
item.die?.d && bleedWrap && item.die.edgeColor
394-
? `<g transform="${bleedWrap}"><g transform="${cutPlace}"><g${item.die.transform ? ` transform="${item.die.transform}"` : ""}><path d="${item.die.d}" fill="${item.die.edgeColor}"/></g></g></g>`
399+
item.die?.d && item.die.bleedD && item.die.edgeColor
400+
? `<g transform="${cutPlace}"><g${item.die.transform ? ` transform="${item.die.transform}"` : ""}><path d="${item.die.bleedD}" fill="${item.die.edgeColor}"/></g></g>`
395401
: "";
396402

397403
if (item.svg) {
@@ -674,7 +680,7 @@ export async function renderInk(svg, { size = 1024, padFrac = 0.18, page: lent =
674680
}
675681
}
676682

677-
export async function dieLine(svg, { size = 1024, offsetPx = 14, roundPx = null, page = null, drawn: given = null, fetchImpl = fetch } = {}) {
683+
export async function dieLine(svg, { size = 1024, offsetPx = 14, roundPx = null, bleedPx = 0, page = null, drawn: given = null, fetchImpl = fetch } = {}) {
678684
if ((await capture("potrace", ["--version"])).ok === false) return null;
679685
/*
680686
* Grow past the offset, then shrink back — a closing.
@@ -912,7 +918,50 @@ export async function dieLine(svg, { size = 1024, offsetPx = 14, roundPx = null,
912918
}
913919
const per = drawn.square.w / (size * scale);
914920
const square = { x: drawn.square.x - at(room) * per, y: drawn.square.y - at(room) * per, w: drawn.square.w + at(room) * 2 * per, h: drawn.square.h + at(room) * 2 * per };
915-
return { d: joined, transform, size: grownSize, square, art: drawn.square, shapeBox, pathBox, inkBox: drawn.inkPx, artSize: drawn.size ?? size, room: at(room), edgeColor };
921+
/*
922+
* The bleed outline, traced rather than scaled.
923+
*
924+
* Scaling the cut up by a percentage is not an offset: on a sticker
925+
* three times as wide as it is tall, the same percentage adds three
926+
* times as much ink at the sides as at the top, and the band comes out
927+
* fat on two edges and thin on the others. A second distance transform
928+
* at the bleed distance gives a band the same width all the way round,
929+
* which is what a bleed is.
930+
*/
931+
let bleedD = null;
932+
if (bleedPx > 0) {
933+
try {
934+
const wide = join(dir, "wide.png");
935+
const widePgm = join(dir, "wide.pgm");
936+
const wideSvg = join(dir, "wide.svg");
937+
await capture("magick", [
938+
png,
939+
"-alpha", "extract",
940+
"-threshold", "30%",
941+
...(scale < 1 ? ["-resize", `${(scale * 100).toFixed(4)}%`] : []),
942+
"-bordercolor", "black",
943+
"-border", `${at(room)}`,
944+
"+repage",
945+
"-negate",
946+
"-morphology", "Distance", "Euclidean:1",
947+
"-threshold", `${q(Math.round(offsetPx) + round + bleedPx)}`,
948+
"-negate",
949+
wide,
950+
]);
951+
await capture("magick", [wide, "-negate", "-depth", "8", widePgm]);
952+
const t2 = await capture("potrace", [widePgm, "-s", "-a", "1.0", "-O", "0.2", "-t", "8", "-o", wideSvg]);
953+
if (t2.ok) {
954+
const w2 = await readFile(wideSvg, "utf8");
955+
const d2 = [...w2.matchAll(/<path[^>]*\bd="([^"]+)"/g)].map((m) => m[1]);
956+
/* Its own transform is the same shape as the cut's, and it is
957+
placed by the same fit, so only the path differs. */
958+
if (d2.length) bleedD = d2.join(" ");
959+
}
960+
} catch {
961+
/* Without it there is simply no bleed. */
962+
}
963+
}
964+
return { d: joined, bleedD, transform, size: grownSize, square, art: drawn.square, shapeBox, pathBox, inkBox: drawn.inkPx, artSize: drawn.size ?? size, room: at(room), edgeColor };
916965
} catch {
917966
return null;
918967
} finally {
@@ -928,7 +977,7 @@ export async function dieLine(svg, { size = 1024, offsetPx = 14, roundPx = null,
928977
* one takes, so they are run in a small pool: enough to keep the machine busy,
929978
* not so many that thirty browsers open at once.
930979
*/
931-
export async function withDieLines(items, { offsetPx = 14, roundPx = null, size = 1024, lanes = 4 } = {}) {
980+
export async function withDieLines(items, { offsetPx = 14, roundPx = null, bleedPx = 0, size = 1024, lanes = 4 } = {}) {
932981
const out = new Array(items.length);
933982
if (!items.some((i) => i.svg)) return items.slice();
934983
const { chromium } = await import("playwright");
@@ -943,7 +992,7 @@ export async function withDieLines(items, { offsetPx = 14, roundPx = null, size
943992
while (next < items.length) {
944993
const i = next++;
945994
const item = items[i];
946-
out[i] = item.svg ? { ...item, die: await dieLine(item.svg, { offsetPx, roundPx, size, page }) } : item;
995+
out[i] = item.svg ? { ...item, die: await dieLine(item.svg, { offsetPx, roundPx, bleedPx, size, page }) } : item;
947996
}
948997
} finally {
949998
await page.close();

‎lib/studio.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16508,7 +16508,7 @@ function vStickers(m) {
1650816508
/* 76.2mm is three inches, which is what a sticker shop's default die is. */
1650916509
/* Filled from the vendor tables the first time the rail is drawn. */
1651016510
let sheetSizeList = []
16511-
const settings = { sticker: 'telegram', cutout: '', vectorize: '', endpoint: '', prompt: '', sheetName: 'stickers', columns: 4, size: 300, gap: 40, remote: '', printSizeMm: 76.2, printBleedMm: 2, printLayout: 'sheet', printPage: '4x6', dieOffsetMm: 0, dieRoundMm: 0, quoteQty: 250, quoteProduct: 'die-cut', quotePublish: true }
16511+
const settings = { sticker: 'telegram', cutout: '', vectorize: '', endpoint: '', prompt: '', sheetName: 'stickers', columns: 4, size: 300, gap: 40, remote: '', printSizeMm: 76.2, printBleedMm: 0.5, printLayout: 'sheet', printPage: '4x6', dieOffsetMm: 0, dieRoundMm: 0, quoteQty: 250, quoteProduct: 'die-cut', quotePublish: true }
1651216512
let affinityWatch = 0
1651316513
let makeBtn = null
1651416514

‎lib/verify.mjs‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3790,6 +3790,9 @@ console.log('\none way in')
37903790
check('and that colour is sampled with the transparency kept', /"-background", "none", "-resize", "1x1!"/.test(prSrc) && /srgba\?\\\(/.test(prSrc))
37913791
// The cut is fitted to the ink, and the ink is not always centred in its
37923792
// cell, so growing the band about the cell pushed it off to one side.
3793+
// Scaling the cut by a percentage is not an offset: on a sticker three times
3794+
// as wide as it is tall it adds three times as much ink at the sides.
3795+
check('the bleed outline is traced, not scaled', /let bleedD = null/.test(prSrc) && /q\(Math\.round\(offsetPx\) \+ round \+ bleedPx\)/.test(prSrc) && /path d="\$\{item\.die\.bleedD\}"/.test(prSrc))
37933796
check('and grown about the sticker, not the cell', /if \(inkMm\) cutMid = \{ x: inkMm\.x \+ inkMm\.w \/ 2/.test(prSrc) && /cutMid\?\.x \?\? cx \+ stickerMm \/ 2/.test(prSrc))
37943797
// Every derivation was defensible and the line still came out a fraction down
37953798
// and to the right: potrace does not put its output exactly where its bitmap
@@ -3806,7 +3809,7 @@ console.log('\none way in')
38063809
// the square it was traced in, each centred on the cell separately.
38073810
check('the drawing and the cut share one frame', /const frame = item\.die\?\.d \? item\.die\.square : box/.test(prSrc) && /const perPx = stickerMm \/ item\.die\.size/.test(prSrc))
38083811
check('and the bleed is a scale about the cell, not a second placement', /const bleedScale = item\.die\?\.d && stickerMm > 0/.test(prSrc) && /translate\(\$\{midX\} \$\{midY\}\) scale\(\$\{bleedScale\}\)/.test(prSrc))
3809-
check("a sticker's bleed is not the sheet's", /stickerBleedMm = 2/.test(prSrc) && /stickerBleedMm \* 2\) \/ stickerMm/.test(prSrc) && /bleedMm: 3\.175,\n\s+stickerBleedMm:/.test(srvMb))
3812+
check("a sticker's bleed is not the sheet's", /stickerBleedMm = 0\.5/.test(prSrc) && /stickerBleedMm \* 2\) \/ stickerMm/.test(prSrc) && /bleedMm: 3\.175,\n\s+stickerBleedMm:/.test(srvMb))
38103813
check('and the rail says which one it is', /'Sticker bleed \(mm\)', 'printBleedMm'/.test(uiMb))
38113814
check('but by default it is traced exactly as drawn', /const round = Math\.max\(0, Math\.round\(roundPx \?\? 0\)\)/.test(prSrc) && /const plain = need === 0/.test(prSrc) && /dieRoundMm: 0,/.test(uiMb))
38123815
check('and zero means no morphology at all, not a small one', /const shaping = plain\n\t\t\t\? \[\]/.test(prSrc))

0 commit comments

Comments
 (0)