@willwade/aac-processors 0.1.20 → 0.2.0

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.
Files changed (113) hide show
  1. package/dist/browser/core/baseProcessor.js +4 -0
  2. package/dist/browser/processors/applePanelsProcessor.js +33 -40
  3. package/dist/browser/processors/astericsGridProcessor.js +31 -26
  4. package/dist/browser/processors/dotProcessor.js +11 -12
  5. package/dist/browser/processors/gridset/colorUtils.js +354 -0
  6. package/dist/browser/processors/gridset/helpers.js +60 -53
  7. package/dist/browser/processors/gridset/index.js +61 -0
  8. package/dist/browser/processors/gridset/styleHelpers.js +205 -0
  9. package/dist/browser/processors/gridset/symbolExtractor.js +331 -0
  10. package/dist/browser/processors/gridset/symbolSearch.js +248 -0
  11. package/dist/browser/processors/gridset/symbols.js +39 -72
  12. package/dist/browser/processors/gridsetProcessor.js +39 -48
  13. package/dist/browser/processors/obfProcessor.js +39 -53
  14. package/dist/browser/processors/opmlProcessor.js +11 -12
  15. package/dist/browser/processors/snap/helpers.js +57 -49
  16. package/dist/browser/processors/snapProcessor.js +48 -51
  17. package/dist/browser/processors/touchchatProcessor.js +60 -52
  18. package/dist/browser/utilities/analytics/history.js +24 -18
  19. package/dist/browser/utilities/analytics/metrics/comparison.js +16 -16
  20. package/dist/browser/utilities/analytics/metrics/vocabulary.js +2 -2
  21. package/dist/browser/utilities/analytics/reference/browser.js +16 -16
  22. package/dist/browser/utilities/analytics/reference/index.js +44 -35
  23. package/dist/browser/utils/io.js +78 -21
  24. package/dist/browser/utils/sqlite.js +8 -10
  25. package/dist/browser/utils/zip.js +43 -43
  26. package/dist/browser/validation/baseValidator.js +5 -0
  27. package/dist/browser/validation/gridsetValidator.js +12 -20
  28. package/dist/browser/validation/obfValidator.js +6 -5
  29. package/dist/browser/validation/snapValidator.js +11 -7
  30. package/dist/browser/validation/touchChatValidator.js +23 -13
  31. package/dist/cli/index.js +22 -24
  32. package/dist/core/baseProcessor.d.ts +7 -7
  33. package/dist/core/baseProcessor.js +4 -0
  34. package/dist/processors/applePanelsProcessor.js +32 -39
  35. package/dist/processors/astericsGridProcessor.d.ts +4 -4
  36. package/dist/processors/astericsGridProcessor.js +30 -25
  37. package/dist/processors/dotProcessor.js +10 -11
  38. package/dist/processors/excelProcessor.d.ts +3 -3
  39. package/dist/processors/excelProcessor.js +14 -20
  40. package/dist/processors/gridset/helpers.d.ts +12 -14
  41. package/dist/processors/gridset/helpers.js +60 -79
  42. package/dist/processors/gridset/imageDebug.d.ts +3 -5
  43. package/dist/processors/gridset/imageDebug.js +4 -4
  44. package/dist/processors/gridset/password.d.ts +1 -1
  45. package/dist/processors/gridset/symbolExtractor.d.ts +5 -3
  46. package/dist/processors/gridset/symbolExtractor.js +15 -38
  47. package/dist/processors/gridset/symbolSearch.d.ts +11 -10
  48. package/dist/processors/gridset/symbolSearch.js +29 -51
  49. package/dist/processors/gridset/symbols.d.ts +8 -6
  50. package/dist/processors/gridset/symbols.js +38 -71
  51. package/dist/processors/gridset/wordlistHelpers.d.ts +4 -6
  52. package/dist/processors/gridset/wordlistHelpers.js +15 -74
  53. package/dist/processors/gridsetProcessor.d.ts +2 -2
  54. package/dist/processors/gridsetProcessor.js +38 -70
  55. package/dist/processors/obfProcessor.d.ts +2 -2
  56. package/dist/processors/obfProcessor.js +38 -75
  57. package/dist/processors/obfsetProcessor.js +2 -3
  58. package/dist/processors/opmlProcessor.js +10 -11
  59. package/dist/processors/snap/helpers.d.ts +9 -9
  60. package/dist/processors/snap/helpers.js +58 -76
  61. package/dist/processors/snapProcessor.d.ts +2 -2
  62. package/dist/processors/snapProcessor.js +47 -50
  63. package/dist/processors/touchchatProcessor.d.ts +2 -2
  64. package/dist/processors/touchchatProcessor.js +59 -51
  65. package/dist/types/aac.d.ts +2 -2
  66. package/dist/utilities/analytics/history.d.ts +8 -8
  67. package/dist/utilities/analytics/history.js +24 -18
  68. package/dist/utilities/analytics/index.d.ts +3 -2
  69. package/dist/utilities/analytics/index.js +9 -10
  70. package/dist/utilities/analytics/metrics/comparison.d.ts +1 -1
  71. package/dist/utilities/analytics/metrics/comparison.js +16 -16
  72. package/dist/utilities/analytics/metrics/vocabulary.d.ts +1 -1
  73. package/dist/utilities/analytics/metrics/vocabulary.js +2 -2
  74. package/dist/utilities/analytics/reference/browser.d.ts +9 -9
  75. package/dist/utilities/analytics/reference/browser.js +16 -16
  76. package/dist/utilities/analytics/reference/index.d.ts +25 -23
  77. package/dist/utilities/analytics/reference/index.js +43 -34
  78. package/dist/utilities/symbolTools.d.ts +8 -6
  79. package/dist/utilities/symbolTools.js +21 -18
  80. package/dist/utils/io.d.ts +24 -6
  81. package/dist/utils/io.js +79 -25
  82. package/dist/utils/sqlite.d.ts +3 -1
  83. package/dist/utils/sqlite.js +7 -9
  84. package/dist/utils/zip.d.ts +7 -3
  85. package/dist/utils/zip.js +43 -43
  86. package/dist/validation/applePanelsValidator.d.ts +2 -1
  87. package/dist/validation/applePanelsValidator.js +10 -11
  88. package/dist/validation/astericsValidator.d.ts +2 -1
  89. package/dist/validation/astericsValidator.js +5 -4
  90. package/dist/validation/baseValidator.d.ts +2 -2
  91. package/dist/validation/baseValidator.js +5 -0
  92. package/dist/validation/dotValidator.d.ts +2 -1
  93. package/dist/validation/dotValidator.js +5 -4
  94. package/dist/validation/excelValidator.d.ts +2 -1
  95. package/dist/validation/excelValidator.js +5 -4
  96. package/dist/validation/gridsetValidator.d.ts +2 -1
  97. package/dist/validation/gridsetValidator.js +11 -22
  98. package/dist/validation/index.d.ts +2 -2
  99. package/dist/validation/index.js +5 -4
  100. package/dist/validation/obfValidator.d.ts +2 -1
  101. package/dist/validation/obfValidator.js +5 -4
  102. package/dist/validation/obfsetValidator.d.ts +2 -1
  103. package/dist/validation/obfsetValidator.js +5 -4
  104. package/dist/validation/opmlValidator.d.ts +2 -1
  105. package/dist/validation/opmlValidator.js +5 -4
  106. package/dist/validation/snapValidator.d.ts +2 -1
  107. package/dist/validation/snapValidator.js +10 -6
  108. package/dist/validation/touchChatValidator.d.ts +4 -6
  109. package/dist/validation/touchChatValidator.js +22 -12
  110. package/dist/validation/validationTypes.d.ts +8 -1
  111. package/package.json +1 -1
  112. package/dist/core/fileProcessor.d.ts +0 -7
  113. package/dist/core/fileProcessor.js +0 -57
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Grid3 Style Helpers
3
+ *
4
+ * Utilities for creating and managing Grid3 styles, including default styles,
5
+ * style XML generation, and style conversion utilities.
6
+ */
7
+ import { XMLBuilder } from 'fast-xml-parser';
8
+ import { ensureAlphaChannel, darkenColor } from './colorUtils';
9
+ /**
10
+ * Cell background shapes supported by Grid 3
11
+ * Maps to Grid 3's CellBackgroundShape enum
12
+ */
13
+ export var CellBackgroundShape;
14
+ (function (CellBackgroundShape) {
15
+ CellBackgroundShape[CellBackgroundShape["Rectangle"] = 0] = "Rectangle";
16
+ CellBackgroundShape[CellBackgroundShape["RoundedRectangle"] = 1] = "RoundedRectangle";
17
+ CellBackgroundShape[CellBackgroundShape["FoldedCorner"] = 2] = "FoldedCorner";
18
+ CellBackgroundShape[CellBackgroundShape["Octagon"] = 3] = "Octagon";
19
+ CellBackgroundShape[CellBackgroundShape["Folder"] = 4] = "Folder";
20
+ CellBackgroundShape[CellBackgroundShape["Ellipse"] = 5] = "Ellipse";
21
+ CellBackgroundShape[CellBackgroundShape["SpeechBubble"] = 6] = "SpeechBubble";
22
+ CellBackgroundShape[CellBackgroundShape["ThoughtBubble"] = 7] = "ThoughtBubble";
23
+ CellBackgroundShape[CellBackgroundShape["Star"] = 8] = "Star";
24
+ CellBackgroundShape[CellBackgroundShape["Circle"] = 9] = "Circle";
25
+ CellBackgroundShape[CellBackgroundShape["ColouredCorner"] = 10] = "ColouredCorner";
26
+ })(CellBackgroundShape || (CellBackgroundShape = {}));
27
+ /**
28
+ * Human-readable shape names
29
+ */
30
+ export const SHAPE_NAMES = {
31
+ [CellBackgroundShape.Rectangle]: 'Rectangle',
32
+ [CellBackgroundShape.RoundedRectangle]: 'Rounded Rectangle',
33
+ [CellBackgroundShape.FoldedCorner]: 'Folded Corner',
34
+ [CellBackgroundShape.Octagon]: 'Octagon',
35
+ [CellBackgroundShape.Folder]: 'Folder',
36
+ [CellBackgroundShape.Ellipse]: 'Ellipse',
37
+ [CellBackgroundShape.SpeechBubble]: 'Speech Bubble',
38
+ [CellBackgroundShape.ThoughtBubble]: 'Thought Bubble',
39
+ [CellBackgroundShape.Star]: 'Star',
40
+ [CellBackgroundShape.Circle]: 'Circle',
41
+ [CellBackgroundShape.ColouredCorner]: 'Coloured Corner',
42
+ };
43
+ /**
44
+ * Default Grid3 styles for common use cases
45
+ * Colors are in 8-digit ARGB hex format (#AARRGGBBFF)
46
+ */
47
+ export const DEFAULT_GRID3_STYLES = {
48
+ Default: {
49
+ BackColour: '#E2EDF8FF',
50
+ TileColour: '#FFFFFFFF',
51
+ BorderColour: '#000000FF',
52
+ FontColour: '#000000FF',
53
+ FontName: 'Arial',
54
+ FontSize: '16',
55
+ },
56
+ Workspace: {
57
+ BackColour: '#FFFFFFFF',
58
+ TileColour: '#FFFFFFFF',
59
+ BorderColour: '#CCCCCCFF',
60
+ FontColour: '#000000FF',
61
+ FontName: 'Arial',
62
+ FontSize: '14',
63
+ },
64
+ 'Auto content': {
65
+ BackColour: '#E8F4F8FF',
66
+ TileColour: '#E8F4F8FF',
67
+ BorderColour: '#2C82C9FF',
68
+ FontColour: '#000000FF',
69
+ FontName: 'Arial',
70
+ FontSize: '14',
71
+ },
72
+ 'Vocab cell': {
73
+ BackColour: '#E8F4F8FF',
74
+ TileColour: '#E8F4F8FF',
75
+ BorderColour: '#2C82C9FF',
76
+ FontColour: '#000000FF',
77
+ FontName: 'Arial',
78
+ FontSize: '14',
79
+ },
80
+ 'Keyboard key': {
81
+ BackColour: '#F0F0F0FF',
82
+ TileColour: '#F0F0F0FF',
83
+ BorderColour: '#808080FF',
84
+ FontColour: '#000000FF',
85
+ FontName: 'Arial',
86
+ FontSize: '12',
87
+ },
88
+ };
89
+ /**
90
+ * Category-specific styles for navigation and organization
91
+ */
92
+ export const CATEGORY_STYLES = {
93
+ 'Actions category style': {
94
+ BackColour: '#4472C4FF',
95
+ TileColour: '#4472C4FF',
96
+ BorderColour: '#2F5496FF',
97
+ FontColour: '#FFFFFFFF',
98
+ FontName: 'Arial',
99
+ FontSize: '16',
100
+ },
101
+ 'People category style': {
102
+ BackColour: '#ED7D31FF',
103
+ TileColour: '#ED7D31FF',
104
+ BorderColour: '#C65911FF',
105
+ FontColour: '#FFFFFFFF',
106
+ FontName: 'Arial',
107
+ FontSize: '16',
108
+ },
109
+ 'Places category style': {
110
+ BackColour: '#A5A5A5FF',
111
+ TileColour: '#A5A5A5FF',
112
+ BorderColour: '#595959FF',
113
+ FontColour: '#FFFFFFFF',
114
+ FontName: 'Arial',
115
+ FontSize: '16',
116
+ },
117
+ 'Descriptive category style': {
118
+ BackColour: '#70AD47FF',
119
+ TileColour: '#70AD47FF',
120
+ BorderColour: '#4F7C2FFF',
121
+ FontColour: '#FFFFFFFF',
122
+ FontName: 'Arial',
123
+ FontSize: '16',
124
+ },
125
+ 'Social category style': {
126
+ BackColour: '#FFC000FF',
127
+ TileColour: '#FFC000FF',
128
+ BorderColour: '#BF8F00FF',
129
+ FontColour: '#000000FF',
130
+ FontName: 'Arial',
131
+ FontSize: '16',
132
+ },
133
+ 'Questions category style': {
134
+ BackColour: '#5B9BD5FF',
135
+ TileColour: '#5B9BD5FF',
136
+ BorderColour: '#2E5C8AFF',
137
+ FontColour: '#FFFFFFFF',
138
+ FontName: 'Arial',
139
+ FontSize: '16',
140
+ },
141
+ 'Little words category style': {
142
+ BackColour: '#C55A11FF',
143
+ TileColour: '#C55A11FF',
144
+ BorderColour: '#8B3F0AFF',
145
+ FontColour: '#FFFFFFFF',
146
+ FontName: 'Arial',
147
+ FontSize: '16',
148
+ },
149
+ };
150
+ /**
151
+ * Re-export ensureAlphaChannel from colorUtils for backward compatibility
152
+ * @deprecated Use ensureAlphaChannel from colorUtils instead
153
+ */
154
+ export { ensureAlphaChannel } from './colorUtils';
155
+ /**
156
+ * Create a Grid3 style XML string with default and category styles
157
+ * @param includeCategories - Whether to include category-specific styles (default: true)
158
+ * @returns XML string for Settings0/styles.xml
159
+ */
160
+ export function createDefaultStylesXml(includeCategories = true) {
161
+ const builder = new XMLBuilder({
162
+ ignoreAttributes: false,
163
+ format: true,
164
+ indentBy: ' ',
165
+ });
166
+ const styles = { ...DEFAULT_GRID3_STYLES };
167
+ if (includeCategories) {
168
+ Object.assign(styles, CATEGORY_STYLES);
169
+ }
170
+ const styleArray = Object.entries(styles).map(([key, style]) => ({
171
+ '@_Key': key,
172
+ BackColour: style.BackColour,
173
+ TileColour: style.TileColour,
174
+ BorderColour: style.BorderColour,
175
+ FontColour: style.FontColour,
176
+ FontName: style.FontName,
177
+ FontSize: style.FontSize?.toString(),
178
+ }));
179
+ const stylesData = {
180
+ StyleData: {
181
+ '@_xmlns:xsi': 'http://www.w3.org/2001/XMLSchema-instance',
182
+ Styles: {
183
+ Style: styleArray,
184
+ },
185
+ },
186
+ };
187
+ return builder.build(stylesData);
188
+ }
189
+ /**
190
+ * Create a custom category style
191
+ * @param categoryName - Name of the category
192
+ * @param backgroundColor - Background color in hex format
193
+ * @param fontColor - Font color in hex format (default: white)
194
+ * @returns Grid3Style object
195
+ */
196
+ export function createCategoryStyle(categoryName, backgroundColor, fontColor = '#FFFFFFFF') {
197
+ return {
198
+ BackColour: ensureAlphaChannel(backgroundColor),
199
+ TileColour: ensureAlphaChannel(backgroundColor),
200
+ BorderColour: ensureAlphaChannel(darkenColor(backgroundColor, 30)),
201
+ FontColour: ensureAlphaChannel(fontColor),
202
+ FontName: 'Arial',
203
+ FontSize: '16',
204
+ };
205
+ }
@@ -0,0 +1,331 @@
1
+ /**
2
+ * Grid 3 Symbol Extraction Strategy
3
+ *
4
+ * For converting Grid 3 gridsets to other formats (like Asterics),
5
+ * we need to handle symbol library references properly.
6
+ *
7
+ * Strategy:
8
+ * 1. Check if image is embedded in gridset (extract directly)
9
+ * 2. If symbol library reference:
10
+ * a. Check if we can extract from .pix file (limited support)
11
+ * b. Provide reference/URL for manual resolution
12
+ * c. For Tawasol: provide alternative sources
13
+ */
14
+ import { resolveSymbolReference, parseSymbolReference } from './symbols';
15
+ import { defaultFileAdapter } from '../../utils/io';
16
+ import { getZipAdapter } from '../../utils/zip';
17
+ /**
18
+ * Known open-license symbol sources
19
+ */
20
+ const OPEN_LICENSE_SYMBOLS = {
21
+ tawasl: {
22
+ name: 'Tawasol',
23
+ attribution: 'Tawasol symbols by Mada (Qatar Assistive Technology Center)',
24
+ license: 'CC BY-SA 4.0',
25
+ url: 'https://mada.org.qa/en/resources/tawasol-symbols',
26
+ alternativeSources: ['https://github.com/mada-qatar/Tawasol'],
27
+ },
28
+ blissx: {
29
+ name: 'Blissymbols',
30
+ attribution: 'Blissymbolics Communication International',
31
+ license: 'CC BY-ND 3.0',
32
+ url: 'https://blissymbolics.org',
33
+ },
34
+ symoji: {
35
+ name: 'Symoji',
36
+ attribution: 'Smartbox Assistive Technology',
37
+ license: 'Proprietary - Free use in Grid 3',
38
+ },
39
+ };
40
+ /**
41
+ * Extract image data for a button
42
+ * @param gridsetBuffer - Gridset ZIP buffer
43
+ * @param resolvedImageEntry - Path to embedded image in gridset
44
+ * @param symbolReference - Symbol library reference
45
+ * @param options - Extraction options
46
+ * @returns Extracted image data
47
+ */
48
+ export async function extractButtonImage(gridsetBuffer, resolvedImageEntry, symbolReference, options = {}, fileAdapter = defaultFileAdapter, zipAdapter) {
49
+ // Priority 1: Use embedded image if available
50
+ if (resolvedImageEntry && options.preferEmbedded !== false) {
51
+ try {
52
+ const zip = zipAdapter
53
+ ? await zipAdapter(gridsetBuffer)
54
+ : await getZipAdapter(gridsetBuffer, fileAdapter);
55
+ const entries = zip.listFiles();
56
+ const entry = entries.find((e) => e === resolvedImageEntry);
57
+ if (entry) {
58
+ const data = Buffer.from(await zip.readFile(entry));
59
+ const format = detectImageFormat(data);
60
+ return {
61
+ found: true,
62
+ data,
63
+ format,
64
+ source: 'embedded',
65
+ reference: resolvedImageEntry,
66
+ };
67
+ }
68
+ }
69
+ catch (error) {
70
+ console.warn(`Failed to extract embedded image: ${String(error)}`);
71
+ }
72
+ }
73
+ // Priority 2: Check symbol library reference
74
+ if (symbolReference) {
75
+ return await extractSymbolLibraryImage(symbolReference, options);
76
+ }
77
+ // Not found
78
+ return {
79
+ found: false,
80
+ source: 'not-found',
81
+ };
82
+ }
83
+ /**
84
+ * Extract image from symbol library
85
+ * @param reference - Symbol reference like "[tawasl]/food/apple.png"
86
+ * @param options - Extraction options
87
+ * @returns Extracted image or reference info
88
+ */
89
+ export async function extractSymbolLibraryImage(reference, options = {}) {
90
+ const ref = parseSymbolReferenceSafe(reference);
91
+ if (!ref || !ref.isValid) {
92
+ return {
93
+ found: false,
94
+ source: 'not-found',
95
+ reference,
96
+ };
97
+ }
98
+ // Get library metadata
99
+ const libInfo = OPEN_LICENSE_SYMBOLS[ref.library];
100
+ // Resolve symbol reference and extract from .symbols file
101
+ const resolved = await resolveSymbolReference(reference, {
102
+ grid3Path: options.grid3Path,
103
+ });
104
+ const metadata = {
105
+ library: ref.library,
106
+ symbolPath: ref.path,
107
+ attribution: libInfo?.attribution,
108
+ license: libInfo?.license,
109
+ };
110
+ if (!resolved.found) {
111
+ // Symbol not found in library
112
+ if (options.onMissingSymbol) {
113
+ options.onMissingSymbol(ref);
114
+ }
115
+ return {
116
+ found: false,
117
+ source: 'symbol-library',
118
+ reference: reference,
119
+ metadata,
120
+ error: resolved.error,
121
+ };
122
+ }
123
+ // Successfully extracted!
124
+ const data = resolved.data;
125
+ const format = data ? detectImageFormat(data) : 'unknown';
126
+ return {
127
+ found: true,
128
+ data,
129
+ format,
130
+ source: 'symbol-library',
131
+ reference: reference,
132
+ metadata,
133
+ };
134
+ }
135
+ /**
136
+ * Convert extracted image to Asterics Grid format
137
+ * @param extracted - Extracted image
138
+ * @returns GridImage object for Asterics
139
+ */
140
+ export function convertToAstericsImage(extracted) {
141
+ const image = {};
142
+ if (extracted.found && extracted.data) {
143
+ // Embed as base64
144
+ image.data = Buffer.from(extracted.data).toString('base64');
145
+ }
146
+ // Even if embedded, add attribution for symbol libraries
147
+ if (extracted.source === 'symbol-library') {
148
+ if (extracted.metadata?.attribution) {
149
+ image.author = extracted.metadata.attribution;
150
+ }
151
+ if (extracted.metadata?.license) {
152
+ image.searchProviderName = extracted.metadata.license;
153
+ }
154
+ }
155
+ // If not found but we have a reference, keep it for manual handling
156
+ if (!extracted.found && extracted.reference) {
157
+ image.url = `symbol:${extracted.reference}`;
158
+ if (extracted.metadata?.attribution) {
159
+ image.author = extracted.metadata.attribution;
160
+ }
161
+ }
162
+ return image;
163
+ }
164
+ /**
165
+ * Analyze symbol usage for a gridset
166
+ * @param tree - AAC tree
167
+ * @returns Symbol usage report
168
+ */
169
+ export function analyzeSymbolExtraction(tree) {
170
+ const report = {
171
+ total: 0,
172
+ embedded: 0,
173
+ symbolLibraries: 0,
174
+ notFound: 0,
175
+ byLibrary: {},
176
+ missingSymbols: [],
177
+ };
178
+ for (const pageId in tree.pages) {
179
+ const page = tree.pages[pageId];
180
+ if (page.buttons) {
181
+ for (const button of page.buttons) {
182
+ report.total++;
183
+ // Embedded image
184
+ if (button.resolvedImageEntry && !button.symbolLibrary) {
185
+ report.embedded++;
186
+ continue;
187
+ }
188
+ // Symbol library reference
189
+ if (button.symbolLibrary) {
190
+ report.symbolLibraries++;
191
+ report.byLibrary[button.symbolLibrary] =
192
+ (report.byLibrary[button.symbolLibrary] || 0) + 1;
193
+ const ref = `[${button.symbolLibrary}]${button.symbolPath || ''}`;
194
+ const libInfo = OPEN_LICENSE_SYMBOLS[button.symbolLibrary];
195
+ report.missingSymbols.push({
196
+ reference: ref,
197
+ library: button.symbolLibrary,
198
+ path: button.symbolPath || '',
199
+ attribution: libInfo?.attribution,
200
+ license: libInfo?.license,
201
+ });
202
+ continue;
203
+ }
204
+ // Not found
205
+ if (!button.resolvedImageEntry && !button.symbolLibrary) {
206
+ report.notFound++;
207
+ }
208
+ }
209
+ }
210
+ }
211
+ return report;
212
+ }
213
+ /**
214
+ * Suggest extraction strategy based on report
215
+ */
216
+ export function suggestExtractionStrategy(report) {
217
+ const suggestions = [];
218
+ if (report.embedded > 0) {
219
+ suggestions.push(`✓ Can extract ${report.embedded} embedded images directly`);
220
+ }
221
+ if (report.symbolLibraries > 0) {
222
+ suggestions.push(`⚠ ${report.symbolLibraries} symbol library references found:`);
223
+ Object.entries(report.byLibrary).forEach(([lib, count]) => {
224
+ const libInfo = OPEN_LICENSE_SYMBOLS[lib];
225
+ if (libInfo) {
226
+ suggestions.push(` - ${lib}: ${count} symbols (${libInfo.license})`);
227
+ if (libInfo.alternativeSources) {
228
+ suggestions.push(` Alternative: ${libInfo.alternativeSources.join(', ')}`);
229
+ }
230
+ }
231
+ else {
232
+ suggestions.push(` - ${lib}: ${count} symbols (Proprietary - requires Grid 3)`);
233
+ }
234
+ });
235
+ }
236
+ if (report.notFound > 0) {
237
+ suggestions.push(`✗ ${report.notFound} images not found`);
238
+ }
239
+ return suggestions.join('\n');
240
+ }
241
+ /**
242
+ * Detect image format from buffer
243
+ */
244
+ function detectImageFormat(buffer) {
245
+ if (buffer.length < 4)
246
+ return 'unknown';
247
+ // PNG: 89 50 4E 47
248
+ if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
249
+ return 'png';
250
+ }
251
+ // JPEG: FF D8 FF
252
+ if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
253
+ return 'jpg';
254
+ }
255
+ // GIF: 47 49 46 38
256
+ if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
257
+ return 'gif';
258
+ }
259
+ // SVG (check for <svg text)
260
+ const header = buffer.slice(0, Math.min(100, buffer.length)).toString('ascii').toLowerCase();
261
+ if (header.includes('<svg')) {
262
+ return 'svg';
263
+ }
264
+ return 'unknown';
265
+ }
266
+ /**
267
+ * Safe parse of symbol reference
268
+ */
269
+ function parseSymbolReferenceSafe(reference) {
270
+ try {
271
+ return parseSymbolReference(reference);
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
277
+ /**
278
+ * Export symbol references to CSV for manual extraction
279
+ */
280
+ export async function exportSymbolReferencesToCsv(report, outputPath, fileAdapter = defaultFileAdapter) {
281
+ const { writeTextToPath } = fileAdapter;
282
+ const lines = ['Reference,Library,Path,Attribution,License'];
283
+ for (const symbol of report.missingSymbols) {
284
+ lines.push(`"${symbol.reference}","${symbol.library}","${symbol.path}","${symbol.attribution || ''}","${symbol.license || ''}"`);
285
+ }
286
+ await writeTextToPath(outputPath, lines.join('\n'));
287
+ }
288
+ export function createSymbolManifest(tree, gridsetName) {
289
+ const manifest = {
290
+ generatedAt: new Date().toISOString(),
291
+ gridset: gridsetName,
292
+ totalSymbols: 0,
293
+ embedded: 0,
294
+ fromLibraries: 0,
295
+ libraries: {},
296
+ symbols: [],
297
+ };
298
+ for (const pageId in tree.pages) {
299
+ const page = tree.pages[pageId];
300
+ if (page.buttons) {
301
+ for (const button of page.buttons) {
302
+ manifest.totalSymbols++;
303
+ if (button.resolvedImageEntry && !button.symbolLibrary) {
304
+ manifest.embedded++;
305
+ continue;
306
+ }
307
+ if (button.symbolLibrary) {
308
+ manifest.fromLibraries++;
309
+ if (!manifest.libraries[button.symbolLibrary]) {
310
+ const libInfo = OPEN_LICENSE_SYMBOLS[button.symbolLibrary];
311
+ manifest.libraries[button.symbolLibrary] = {
312
+ count: 0,
313
+ attribution: libInfo?.attribution,
314
+ license: libInfo?.license,
315
+ url: libInfo?.url,
316
+ };
317
+ }
318
+ manifest.libraries[button.symbolLibrary].count++;
319
+ const ref = `[${button.symbolLibrary}]${button.symbolPath || ''}`;
320
+ manifest.symbols.push({
321
+ pageId,
322
+ buttonId: button.id,
323
+ reference: ref,
324
+ label: button.label,
325
+ });
326
+ }
327
+ }
328
+ }
329
+ }
330
+ return manifest;
331
+ }