← back to Allnewsdaily
Fix daily-short crash: reject SVG stock images instead of crashing the whole render
ba6d50f6cbbd79622c472679007737efdc5f9224 · 2026-09-11 06:30:14 -0700 · Steve Abrams
Root cause (TK-11458): Openverse sometimes returns an SVG (a world/BRICS map)
tagged as a photo match; fetch-stock.mjs only checked the content-type against
/image\// which matches image/svg+xml, so the SVG got saved as e.g. img/2.jpg.
ImageMagick content-sniffs the file regardless of extension, renders it as
vector art via its SVG coder, and tries to render embedded <text> labels
through its font engine — an unresolvable font-family there throws
"unable to read font `'" from RenderFreetype and kills the entire 8-card
render (reproduced standalone: `magick img/2.jpg ...` -> identical error).
This was NOT a font-resolution bug in render-short.js's FONT_PRIMARY/
FONT_FALLBACK logic (unrelated, correctly resolved) and NOT a concurrency
bug (the pipeline is fully synchronous execFileSync, no Promise.all/async).
Fix, both ends:
- fetch-stock.mjs: reject image/svg+xml in downloadFirstUsable(), mirroring
the exclusion grabLogo() already had for logos but that was never mirrored
onto the stock-photo downloader.
- render-short.js: imgPath() now validates real magic bytes (JPEG/PNG/GIF/
WEBP/BMP) before handing a file to magick; anything else (SVG, HTML error
page, truncated download) is treated as "no image" and falls back to the
existing gradient-card background instead of crashing the pipeline.
Verified: re-ran render-short.js directly against today's already-fetched
stories.json/script.json/vo.mp3/img (reused, no ElevenLabs re-spend). The new
guard caught TWO bad SVGs in this batch (img/2.jpg and img/3.jpg, the latter
a literal "BRICS members" Wikipedia map matching the BRICS headline) and both
cards rendered cleanly on the gradient fallback. All 8 cards + final out.mp4
(1080x1920, 46.30s, VO AAC) produced with no errors or residual warnings.
Files touched
M scripts/short/fetch-stock.mjsM scripts/short/render-short.js
Diff
commit ba6d50f6cbbd79622c472679007737efdc5f9224
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Fri Sep 11 06:30:14 2026 -0700
Fix daily-short crash: reject SVG stock images instead of crashing the whole render
Root cause (TK-11458): Openverse sometimes returns an SVG (a world/BRICS map)
tagged as a photo match; fetch-stock.mjs only checked the content-type against
/image\// which matches image/svg+xml, so the SVG got saved as e.g. img/2.jpg.
ImageMagick content-sniffs the file regardless of extension, renders it as
vector art via its SVG coder, and tries to render embedded <text> labels
through its font engine — an unresolvable font-family there throws
"unable to read font `'" from RenderFreetype and kills the entire 8-card
render (reproduced standalone: `magick img/2.jpg ...` -> identical error).
This was NOT a font-resolution bug in render-short.js's FONT_PRIMARY/
FONT_FALLBACK logic (unrelated, correctly resolved) and NOT a concurrency
bug (the pipeline is fully synchronous execFileSync, no Promise.all/async).
Fix, both ends:
- fetch-stock.mjs: reject image/svg+xml in downloadFirstUsable(), mirroring
the exclusion grabLogo() already had for logos but that was never mirrored
onto the stock-photo downloader.
- render-short.js: imgPath() now validates real magic bytes (JPEG/PNG/GIF/
WEBP/BMP) before handing a file to magick; anything else (SVG, HTML error
page, truncated download) is treated as "no image" and falls back to the
existing gradient-card background instead of crashing the pipeline.
Verified: re-ran render-short.js directly against today's already-fetched
stories.json/script.json/vo.mp3/img (reused, no ElevenLabs re-spend). The new
guard caught TWO bad SVGs in this batch (img/2.jpg and img/3.jpg, the latter
a literal "BRICS members" Wikipedia map matching the BRICS headline) and both
cards rendered cleanly on the gradient fallback. All 8 cards + final out.mp4
(1080x1920, 46.30s, VO AAC) produced with no errors or residual warnings.
---
scripts/short/fetch-stock.mjs | 6 ++++--
scripts/short/render-short.js | 35 ++++++++++++++++++++++++++++++++++-
2 files changed, 38 insertions(+), 3 deletions(-)
diff --git a/scripts/short/fetch-stock.mjs b/scripts/short/fetch-stock.mjs
index 5891aa1..0e10786 100644
--- a/scripts/short/fetch-stock.mjs
+++ b/scripts/short/fetch-stock.mjs
@@ -40,8 +40,10 @@ async function downloadFirstUsable(results, outPath) {
try {
const r = await tfetch(src, {}, 10000);
if (!r.ok) continue;
- const ct = r.headers.get('content-type') || '';
- if (!/image\//.test(ct)) continue;
+ const ct = (r.headers.get('content-type') || '').split(';')[0].trim();
+ if (!/image\//.test(ct) || ct === 'image/svg+xml') continue; // skip svg — not a raster image; IM will
+ // content-sniff it as vector and try to render any embedded <text> via its font engine, which can
+ // crash the whole render on an unresolvable font-family (see TK-11458).
const buf = Buffer.from(await r.arrayBuffer());
if (buf.length < 6000) continue; // skip tiny/broken
fs.writeFileSync(outPath, buf);
diff --git a/scripts/short/render-short.js b/scripts/short/render-short.js
index 5f0a294..86faab2 100644
--- a/scripts/short/render-short.js
+++ b/scripts/short/render-short.js
@@ -149,7 +149,40 @@ if (target > HARD_CAP_SEC) {
// ---------------------------------------------------------------------------
function cleanIntro(t) { return String(t).replace(/^\s*All News Daily\.\s*/i, '').trim() || t; }
-function imgPath(name) { const p = path.join(IMG_DIR, `${name}.jpg`); return fs.existsSync(p) ? p : null; }
+// Guard against a stock "photo" that is actually SVG/HTML/text saved with a .jpg extension
+// (TK-11458 — Openverse handed back an SVG world-map that fetch-stock.mjs's content-type check
+// let through; ImageMagick content-sniffs the file regardless of extension, treats it as vector
+// art, and tries to render any embedded <text> label via its font engine — an unresolvable
+// font-family there crashes the ENTIRE 8-card render with a cryptic RenderFreetype error). This
+// checks real magic bytes so any non-raster file (this SVG case, a truncated download, an HTML
+// error page saved by mistake) is treated as "no image" and falls back to the gradient card
+// below instead of taking the whole pipeline down.
+function isRasterImage(p) {
+ try {
+ const fd = fs.openSync(p, 'r');
+ const buf = Buffer.alloc(16);
+ const n = fs.readSync(fd, buf, 0, 16, 0);
+ fs.closeSync(fd);
+ const b = buf.subarray(0, n);
+ if (b.length >= 3 && b[0] === 0xFF && b[1] === 0xD8 && b[2] === 0xFF) return true; // JPEG
+ if (b.length >= 8 && b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4E && b[3] === 0x47) return true; // PNG
+ if (b.length >= 6 && ['GIF87a', 'GIF89a'].includes(b.toString('ascii', 0, 6))) return true; // GIF
+ if (b.length >= 12 && b.toString('ascii', 0, 4) === 'RIFF' && b.toString('ascii', 8, 12) === 'WEBP') return true; // WEBP
+ if (b.length >= 2 && b[0] === 0x42 && b[1] === 0x4D) return true; // BMP
+ return false; // anything else (SVG/XML/HTML/text/etc.) — not a raster image, reject
+ } catch (_) { return false; }
+}
+
+function imgPath(name) {
+ const p = path.join(IMG_DIR, `${name}.jpg`);
+ if (!fs.existsSync(p)) return null;
+ if (!isRasterImage(p)) {
+ console.warn(`[render-short] WARN: ${name}.jpg is not a real raster image (magic-byte check ` +
+ `failed — likely an SVG/HTML mis-saved as .jpg) — using gradient background instead.`);
+ return null;
+ }
+ return p;
+}
function logoPath(n) { try { const f = fs.readdirSync(LOGO_DIR).find((x) => x.startsWith(String(n) + '.')); return f ? path.join(LOGO_DIR, f) : null; } catch { return null; } }
const segments = [];
← c46e8cf auto-data-snapshot: 2026-09-10T20:45:30 (1 data files) — ver
·
back to Allnewsdaily
·
Vary daily Short layouts and narration with verified media p 40c4f7b →