@goodandready/dsh-image-gen 0.11.11 → 0.11.12

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.
@@ -0,0 +1,119 @@
1
+ // vision-ocr-helpers.js — OCR text localization and seamless replacement (#180).
2
+
3
+ /**
4
+ * Parses bounding box representations into normalized [x1, y1, x2, y2] (0-1000 scale).
5
+ *
6
+ * @param {Array<number>|string} raw
7
+ * @param {number} [imgWidth=1000]
8
+ * @param {number} [imgHeight=1000]
9
+ * @returns {[number, number, number, number]|null}
10
+ */
11
+ export function normalizeBbox(raw, imgWidth = 1000, imgHeight = 1000) {
12
+ if (!raw) return null
13
+ let coords = []
14
+ if (Array.isArray(raw)) {
15
+ coords = raw.map(Number)
16
+ } else if (typeof raw === 'string') {
17
+ coords = raw
18
+ .replace(/[\[\]]/g, '')
19
+ .split(/[,;\s]+/)
20
+ .map(Number)
21
+ .filter((n) => !Number.isNaN(n))
22
+ }
23
+
24
+ if (coords.length < 4) return null
25
+
26
+ let [x1, y1, x2, y2] = coords
27
+ // If coordinates exceed 1000, normalize from pixel dimensions
28
+ if (x1 > 1000 || x2 > 1000 || y1 > 1000 || y2 > 1000) {
29
+ x1 = Math.round((x1 / imgWidth) * 1000)
30
+ x2 = Math.round((x2 / imgWidth) * 1000)
31
+ y1 = Math.round((y1 / imgHeight) * 1000)
32
+ y2 = Math.round((y2 / imgHeight) * 1000)
33
+ }
34
+
35
+ const minX = Math.max(0, Math.min(x1, x2))
36
+ const maxX = Math.min(1000, Math.max(x1, x2))
37
+ const minY = Math.max(0, Math.min(y1, y2))
38
+ const maxY = Math.min(1000, Math.max(y1, y2))
39
+
40
+ return [minX, minY, maxX, maxY]
41
+ }
42
+
43
+ /**
44
+ * Builds an SVG inpaint mask from detected text bounding boxes.
45
+ * Solid black background (#000000) with solid white rectangles (#FFFFFF) for text regions to inpaint.
46
+ *
47
+ * @param {number} width Image pixel width
48
+ * @param {number} height Image pixel height
49
+ * @param {Array<[number, number, number, number]>} bboxes Normalized [x1, y1, x2, y2]
50
+ * @param {number} [paddingPx=8]
51
+ * @returns {string} SVG mask document string
52
+ */
53
+ export function buildTextInpaintMaskSvg(width, height, bboxes, paddingPx = 8) {
54
+ const w = Math.max(1, width || 1024)
55
+ const h = Math.max(1, height || 1024)
56
+
57
+ const rects = (bboxes || []).map((box) => {
58
+ const norm = normalizeBbox(box, w, h)
59
+ if (!norm) return ''
60
+ const [x1n, y1n, x2n, y2n] = norm
61
+ const pxX1 = Math.max(0, Math.round((x1n / 1000) * w) - paddingPx)
62
+ const pxY1 = Math.max(0, Math.round((y1n / 1000) * h) - paddingPx)
63
+ const pxX2 = Math.min(w, Math.round((x2n / 1000) * w) + paddingPx)
64
+ const pxY2 = Math.min(h, Math.round((y2n / 1000) * h) + paddingPx)
65
+ const rectW = Math.max(1, pxX2 - pxX1)
66
+ const rectH = Math.max(1, pxY2 - pxY1)
67
+
68
+ return `<rect x="${pxX1}" y="${pxY1}" width="${rectW}" height="${rectH}" fill="#FFFFFF" rx="4" />`
69
+ }).filter(Boolean).join('\n ')
70
+
71
+ return `<?xml version="1.0" encoding="UTF-8"?>
72
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
73
+ <rect width="${w}" height="${h}" fill="#000000" />
74
+ ${rects}
75
+ </svg>`
76
+ }
77
+
78
+ /**
79
+ * Builds an SVG typography overlay with replacement strings placed at original coordinates.
80
+ *
81
+ * @param {number} width
82
+ * @param {number} height
83
+ * @param {Array<{ text: string, bbox: [number, number, number, number], color?: string, fontSize?: number }>} items
84
+ * @param {string} [fontFamily='system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif']
85
+ * @returns {string} SVG overlay document string
86
+ */
87
+ export function buildTextOverlaySvg(width, height, items, fontFamily = 'sans-serif') {
88
+ const w = Math.max(1, width || 1024)
89
+ const h = Math.max(1, height || 1024)
90
+
91
+ const textNodes = (items || []).map((item) => {
92
+ const norm = normalizeBbox(item.bbox, w, h)
93
+ if (!norm || !item.text) return ''
94
+ const [x1n, y1n, x2n, y2n] = norm
95
+ const pxX1 = Math.round((x1n / 1000) * w)
96
+ const pxY1 = Math.round((y1n / 1000) * h)
97
+ const boxW = Math.max(1, Math.round(((x2n - x1n) / 1000) * w))
98
+ const boxH = Math.max(1, Math.round(((y2n - y1n) / 1000) * h))
99
+
100
+ const computedFontSize = item.fontSize || Math.max(12, Math.round(boxH * 0.72))
101
+ const color = item.color || '#FFFFFF'
102
+ const centerX = pxX1 + Math.round(boxW / 2)
103
+ const baselineY = pxY1 + Math.round(boxH * 0.75)
104
+
105
+ // Escape XML entities
106
+ const safeText = String(item.text)
107
+ .replace(/&/g, '&amp;')
108
+ .replace(/</g, '&lt;')
109
+ .replace(/>/g, '&gt;')
110
+ .replace(/"/g, '&quot;')
111
+
112
+ return `<text x="${centerX}" y="${baselineY}" text-anchor="middle" font-family="${fontFamily}" font-size="${computedFontSize}" font-weight="700" fill="${color}" filter="drop-shadow(0 2px 4px rgba(0,0,0,0.6))">${safeText}</text>`
113
+ }).filter(Boolean).join('\n ')
114
+
115
+ return `<?xml version="1.0" encoding="UTF-8"?>
116
+ <svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">
117
+ ${textNodes}
118
+ </svg>`
119
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-image-gen",
3
- "version": "0.11.11",
4
- "description": "Image generation for DeepSeek Harness: a generate_image tool with pluggable providers \u2014 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).",
3
+ "version": "0.11.12",
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",
7
7
  "dsh",