@goodandready/dsh-image-gen 0.11.35 → 0.11.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/frontend-assets.js +5 -3
- package/lib/security.js +46 -0
- package/lib/tools/processing-advanced.js +12 -13
- package/lib/vision-ocr-helpers.js +5 -7
- package/package.json +1 -1
package/lib/frontend-assets.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { resolveInside } from './security.js'
|
|
1
|
+
import { resolveInside, escapeXml, sanitizeSvgColor } from './security.js'
|
|
2
2
|
import sharp from 'sharp'
|
|
3
3
|
// @ts-check
|
|
4
4
|
import path from 'node:path'
|
|
@@ -593,9 +593,11 @@ export function buildIcoBuffer(pngFrames) {
|
|
|
593
593
|
|
|
594
594
|
async function getDefaultIconBytes(name, themeColor) {
|
|
595
595
|
const initial = (name || 'A').trim().charAt(0).toUpperCase() || 'A'
|
|
596
|
+
const escapedInitial = escapeXml(initial)
|
|
597
|
+
const safeThemeColor = sanitizeSvgColor(themeColor || '#0f172a', '#0f172a')
|
|
596
598
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
|
597
|
-
<rect width="512" height="512" rx="100" fill="${
|
|
598
|
-
<text x="50%" y="54%" font-family="system-ui, -apple-system, sans-serif" font-size="280" font-weight="bold" fill="#ffffff" dominant-baseline="middle" text-anchor="middle">${
|
|
599
|
+
<rect width="512" height="512" rx="100" fill="${safeThemeColor}"/>
|
|
600
|
+
<text x="50%" y="54%" font-family="system-ui, -apple-system, sans-serif" font-size="280" font-weight="bold" fill="#ffffff" dominant-baseline="middle" text-anchor="middle">${escapedInitial}</text>
|
|
599
601
|
</svg>`
|
|
600
602
|
return sharp(Buffer.from(svg)).png().toBuffer()
|
|
601
603
|
}
|
package/lib/security.js
CHANGED
|
@@ -294,3 +294,49 @@ export function resolveInside(baseDir, candidatePath) {
|
|
|
294
294
|
|
|
295
295
|
return resolved
|
|
296
296
|
}
|
|
297
|
+
|
|
298
|
+
const SAFE_HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/
|
|
299
|
+
const SAFE_RGB_COLOR = /^rgba?\(\s*\d{1,3}\s*,\s*\d{1,3}\s*,\s*\d{1,3}(?:\s*,\s*(?:0|1|0?\.\d+)\s*)?\)$/i
|
|
300
|
+
const SAFE_HSL_COLOR = /^hsla?\(\s*\d{1,3}(?:deg)?\s*,\s*\d{1,3}%\s*,\s*\d{1,3}%(?:\s*,\s*(?:0|1|0?\.\d+)\s*)?\)$/i
|
|
301
|
+
const SAFE_NAMED_COLOR = /^(?:transparent|currentColor|none|[a-zA-Z]{3,20})$/i
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Escapes characters for safe inclusion in XML/SVG documents and attributes (#414).
|
|
305
|
+
* Replaces &, <, >, ", ' with their corresponding XML entities.
|
|
306
|
+
*
|
|
307
|
+
* @param {string|unknown} val
|
|
308
|
+
* @returns {string} XML-safe string
|
|
309
|
+
*/
|
|
310
|
+
export function escapeXml(val) {
|
|
311
|
+
if (val === null || val === undefined) return ''
|
|
312
|
+
return String(val)
|
|
313
|
+
.replace(/&/g, '&')
|
|
314
|
+
.replace(/</g, '<')
|
|
315
|
+
.replace(/>/g, '>')
|
|
316
|
+
.replace(/"/g, '"')
|
|
317
|
+
.replace(/'/g, ''')
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Sanitizes a color value for safe inclusion in SVG attributes (fill, stroke, stop-color) (#414).
|
|
322
|
+
* If the input is not a recognized safe color (hex, rgb/rgba, hsl/hsla, or safe named color),
|
|
323
|
+
* returns the provided fallback color.
|
|
324
|
+
*
|
|
325
|
+
* @param {string|unknown} color
|
|
326
|
+
* @param {string} [fallback='#000000']
|
|
327
|
+
* @returns {string} Sanitized color string
|
|
328
|
+
*/
|
|
329
|
+
export function sanitizeSvgColor(color, fallback = '#000000') {
|
|
330
|
+
if (typeof color !== 'string') return fallback
|
|
331
|
+
const trimmed = color.trim()
|
|
332
|
+
if (
|
|
333
|
+
SAFE_HEX_COLOR.test(trimmed) ||
|
|
334
|
+
SAFE_RGB_COLOR.test(trimmed) ||
|
|
335
|
+
SAFE_HSL_COLOR.test(trimmed) ||
|
|
336
|
+
SAFE_NAMED_COLOR.test(trimmed)
|
|
337
|
+
) {
|
|
338
|
+
return trimmed
|
|
339
|
+
}
|
|
340
|
+
return fallback
|
|
341
|
+
}
|
|
342
|
+
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
toLosslessJson,
|
|
11
11
|
saveAttachmentSafe,
|
|
12
12
|
} from '../providers.js'
|
|
13
|
-
import { sanitizeErrorAndLogs, resolveInside } from '../security.js'
|
|
13
|
+
import { sanitizeErrorAndLogs, resolveInside, escapeXml, sanitizeSvgColor } from '../security.js'
|
|
14
14
|
import { resolveConversationImage } from '../resolve-image.js'
|
|
15
15
|
|
|
16
16
|
export function registerProcessingAdvancedTools(ctx, deps) {
|
|
@@ -126,6 +126,7 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
126
126
|
const mime = src.mediaType || 'image/png'
|
|
127
127
|
const dataUri = `data:${mime};base64,${b64}`
|
|
128
128
|
const label = labels[idx] || String(idx + 1)
|
|
129
|
+
const escapedLabel = escapeXml(label)
|
|
129
130
|
|
|
130
131
|
tilesSvg += `
|
|
131
132
|
<g transform="translate(${x}, ${y})">
|
|
@@ -133,14 +134,11 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
133
134
|
<rect width="${tileW}" height="${tileH}" rx="8" fill="#18191c" stroke="#2c2e33" stroke-width="1"/>
|
|
134
135
|
<image href="${dataUri}" width="${tileW}" height="${tileH}" preserveAspectRatio="xMidYMid slice" clip-path="url(#clip-${idx})"/>
|
|
135
136
|
<rect x="12" y="12" width="28" height="24" rx="4" fill="rgba(0,0,0,0.65)"/>
|
|
136
|
-
<text x="26" y="29" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="13" font-weight="700" text-anchor="middle">${
|
|
137
|
+
<text x="26" y="29" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="13" font-weight="700" text-anchor="middle">${escapedLabel}</text>
|
|
137
138
|
</g>`
|
|
138
139
|
})
|
|
139
140
|
|
|
140
|
-
const escapedTitle = title
|
|
141
|
-
.replace(/&/g, '&')
|
|
142
|
-
.replace(/</g, '<')
|
|
143
|
-
.replace(/>/g, '>')
|
|
141
|
+
const escapedTitle = escapeXml(title || '')
|
|
144
142
|
|
|
145
143
|
const svgContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
146
144
|
<svg xmlns="http://www.w3.org/2000/svg" width="${totalW}" height="${totalH}" viewBox="0 0 ${totalW} ${totalH}">
|
|
@@ -247,7 +245,7 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
247
245
|
const source = await resolveConversationImage(ctx, exec, args.image)
|
|
248
246
|
const targetRatioKey = args.aspect_ratio || '1:1'
|
|
249
247
|
const mode = args.mode || 'auto_focus'
|
|
250
|
-
const bgColor = args.background || '#0b0c0e'
|
|
248
|
+
const bgColor = sanitizeSvgColor(args.background || '#0b0c0e', '#0b0c0e')
|
|
251
249
|
|
|
252
250
|
const ratioMap = {
|
|
253
251
|
'1:1': 1.0,
|
|
@@ -413,8 +411,11 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
413
411
|
|
|
414
412
|
const projectName = args.project_name || 'Project Brand'
|
|
415
413
|
const tagline = args.tagline || 'Modern AI-driven application workspace'
|
|
416
|
-
const themeColor = args.theme_color || '#4f46e5'
|
|
414
|
+
const themeColor = sanitizeSvgColor(args.theme_color || '#4f46e5', '#4f46e5')
|
|
417
415
|
const initial = projectName.charAt(0).toUpperCase()
|
|
416
|
+
const escapedInitial = escapeXml(initial)
|
|
417
|
+
const escapedProject = escapeXml(projectName)
|
|
418
|
+
const escapedTagline = escapeXml(tagline)
|
|
418
419
|
|
|
419
420
|
let logoDataUri = null
|
|
420
421
|
if (args.logo_image) {
|
|
@@ -439,7 +440,7 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
439
440
|
const faviconSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
440
441
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
|
441
442
|
<rect width="64" height="64" rx="16" fill="${themeColor}"/>
|
|
442
|
-
${logoDataUri ? `<image href="${logoDataUri}" x="8" y="8" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>` : `<text x="32" y="44" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="34" font-weight="900" text-anchor="middle">${
|
|
443
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="8" y="8" width="48" height="48" preserveAspectRatio="xMidYMid meet"/>` : `<text x="32" y="44" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="34" font-weight="900" text-anchor="middle">${escapedInitial}</text>`}
|
|
443
444
|
</svg>`
|
|
444
445
|
const faviconPath = path.join(targetDir, 'favicon.svg')
|
|
445
446
|
await writeFile(faviconPath, faviconSvg, 'utf8')
|
|
@@ -459,7 +460,7 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
459
460
|
</linearGradient>
|
|
460
461
|
</defs>
|
|
461
462
|
<rect width="${size}" height="${size}" rx="${rx}" fill="url(#brandGrad-${size})"/>
|
|
462
|
-
${logoDataUri ? `<image href="${logoDataUri}" x="${size * 0.15}" y="${size * 0.15}" width="${size * 0.7}" height="${size * 0.7}" preserveAspectRatio="xMidYMid meet"/>` : `<text x="${size / 2}" y="${yOffset}" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="${fontSize}" font-weight="900" text-anchor="middle">${
|
|
463
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="${size * 0.15}" y="${size * 0.15}" width="${size * 0.7}" height="${size * 0.7}" preserveAspectRatio="xMidYMid meet"/>` : `<text x="${size / 2}" y="${yOffset}" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="${fontSize}" font-weight="900" text-anchor="middle">${escapedInitial}</text>`}
|
|
463
464
|
</svg>`
|
|
464
465
|
const iconPath = path.join(targetDir, `icon-${size}.svg`)
|
|
465
466
|
await writeFile(iconPath, iconSvg, 'utf8')
|
|
@@ -486,8 +487,6 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
486
487
|
exportedFiles.push(manifestPath)
|
|
487
488
|
|
|
488
489
|
// 4. OpenGraph Social Card (1200x630)
|
|
489
|
-
const escapedProject = projectName.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
490
|
-
const escapedTagline = tagline.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
491
490
|
const ogCardSvg = `<?xml version="1.0" encoding="UTF-8"?>
|
|
492
491
|
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
493
492
|
<defs>
|
|
@@ -511,7 +510,7 @@ export function registerProcessingAdvancedTools(ctx, deps) {
|
|
|
511
510
|
|
|
512
511
|
<g transform="translate(860, 200)">
|
|
513
512
|
<rect width="180" height="180" rx="36" fill="${themeColor}" fill-opacity="0.15" stroke="${themeColor}" stroke-width="2"/>
|
|
514
|
-
${logoDataUri ? `<image href="${logoDataUri}" x="20" y="20" width="140" height="140" preserveAspectRatio="xMidYMid meet"/>` : `<text x="90" y="125" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="96" font-weight="900" text-anchor="middle">${
|
|
513
|
+
${logoDataUri ? `<image href="${logoDataUri}" x="20" y="20" width="140" height="140" preserveAspectRatio="xMidYMid meet"/>` : `<text x="90" y="125" fill="#ffffff" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="96" font-weight="900" text-anchor="middle">${escapedInitial}</text>`}
|
|
515
514
|
</g>
|
|
516
515
|
|
|
517
516
|
<text x="130" y="480" fill="#64748b" font-family="-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif" font-size="14" font-weight="500">Auto-generated by @goodandready/dsh-image-gen</text>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// vision-ocr-helpers.js — OCR text localization and seamless replacement (#180).
|
|
2
|
+
import { escapeXml, sanitizeSvgColor } from './security.js'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Parses bounding box representations into normalized [x1, y1, x2, y2] (0-1000 scale).
|
|
@@ -87,6 +88,7 @@ export function buildTextInpaintMaskSvg(width, height, bboxes, paddingPx = 8) {
|
|
|
87
88
|
export function buildTextOverlaySvg(width, height, items, fontFamily = 'sans-serif') {
|
|
88
89
|
const w = Math.max(1, width || 1024)
|
|
89
90
|
const h = Math.max(1, height || 1024)
|
|
91
|
+
const safeFont = escapeXml(fontFamily)
|
|
90
92
|
|
|
91
93
|
const textNodes = (items || []).map((item) => {
|
|
92
94
|
const norm = normalizeBbox(item.bbox, w, h)
|
|
@@ -98,18 +100,14 @@ export function buildTextOverlaySvg(width, height, items, fontFamily = 'sans-ser
|
|
|
98
100
|
const boxH = Math.max(1, Math.round(((y2n - y1n) / 1000) * h))
|
|
99
101
|
|
|
100
102
|
const computedFontSize = item.fontSize || Math.max(12, Math.round(boxH * 0.72))
|
|
101
|
-
const color = item.color || '#FFFFFF'
|
|
103
|
+
const color = sanitizeSvgColor(item.color || '#FFFFFF', '#FFFFFF')
|
|
102
104
|
const centerX = pxX1 + Math.round(boxW / 2)
|
|
103
105
|
const baselineY = pxY1 + Math.round(boxH * 0.75)
|
|
104
106
|
|
|
105
107
|
// Escape XML entities
|
|
106
|
-
const safeText =
|
|
107
|
-
.replace(/&/g, '&')
|
|
108
|
-
.replace(/</g, '<')
|
|
109
|
-
.replace(/>/g, '>')
|
|
110
|
-
.replace(/"/g, '"')
|
|
108
|
+
const safeText = escapeXml(item.text)
|
|
111
109
|
|
|
112
|
-
return `<text x="${centerX}" y="${baselineY}" text-anchor="middle" font-family="${
|
|
110
|
+
return `<text x="${centerX}" y="${baselineY}" text-anchor="middle" font-family="${safeFont}" font-size="${computedFontSize}" font-weight="700" fill="${color}" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.6))">${safeText}</text>`
|
|
113
111
|
}).filter(Boolean).join('\n ')
|
|
114
112
|
|
|
115
113
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-image-gen",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.36",
|
|
4
4
|
"description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers — the FAL queue, any OpenAI-compatible images API, or a ChatGPT/Grok subscription with no API key at all. The picture is shown inline in the conversation; the model receives either a link (works with any chat model) or the image itself (needs dsh-vision-bridge or a vision-capable model).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|