@willwade/aac-processors 0.1.17 → 0.1.19

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.
@@ -48,29 +48,29 @@ export { configureSqlJs } from './utils/sqlite';
48
48
  * @returns The appropriate processor instance
49
49
  * @throws Error if the file extension is not supported
50
50
  */
51
- export function getProcessor(filePathOrExtension) {
51
+ export function getProcessor(filePathOrExtension, options) {
52
52
  const extension = filePathOrExtension.includes('.')
53
53
  ? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
54
54
  : filePathOrExtension;
55
55
  switch (extension.toLowerCase()) {
56
56
  case '.dot':
57
- return new DotProcessor();
57
+ return new DotProcessor(options);
58
58
  case '.opml':
59
- return new OpmlProcessor();
59
+ return new OpmlProcessor(options);
60
60
  case '.obf':
61
61
  case '.obz':
62
- return new ObfProcessor();
62
+ return new ObfProcessor(options);
63
63
  case '.gridset':
64
- return new GridsetProcessor();
64
+ return new GridsetProcessor(options);
65
65
  case '.spb':
66
66
  case '.sps':
67
- return new SnapProcessor();
67
+ return new SnapProcessor(options);
68
68
  case '.ce':
69
- return new TouchChatProcessor();
69
+ return new TouchChatProcessor(options);
70
70
  case '.plist':
71
- return new ApplePanelsProcessor();
71
+ return new ApplePanelsProcessor(options);
72
72
  case '.grd':
73
- return new AstericsGridProcessor();
73
+ return new AstericsGridProcessor(options);
74
74
  default:
75
75
  throw new Error(`Unsupported file extension: ${extension}`);
76
76
  }
@@ -52,9 +52,11 @@ export function getAllowedImageEntries(tree) {
52
52
  * @param entryPath Entry name inside the zip
53
53
  * @returns Image data buffer or null if not found
54
54
  */
55
- export async function openImage(gridsetBuffer, entryPath, password = resolveGridsetPasswordFromEnv()) {
55
+ export async function openImage(gridsetBuffer, entryPath, password = resolveGridsetPasswordFromEnv(), zipAdapter) {
56
56
  try {
57
- const { zip } = await openZipFromInput(gridsetBuffer);
57
+ const { zip } = zipAdapter
58
+ ? await zipAdapter(gridsetBuffer)
59
+ : await openZipFromInput(gridsetBuffer);
58
60
  const entries = getZipEntriesFromAdapter(zip, password);
59
61
  const want = normalizeZipPath(entryPath);
60
62
  const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);
@@ -398,7 +398,10 @@ class GridsetProcessor extends BaseProcessor {
398
398
  const tree = new AACTree();
399
399
  let zipResult;
400
400
  try {
401
- zipResult = await openZipFromInput(readBinaryFromInput(filePathOrBuffer));
401
+ const zipInput = readBinaryFromInput(filePathOrBuffer);
402
+ zipResult = this.options.zipAdapter
403
+ ? await this.options.zipAdapter(zipInput)
404
+ : await openZipFromInput(zipInput);
402
405
  }
403
406
  catch (error) {
404
407
  throw new Error(`Invalid ZIP file format: ${error.message}`);
@@ -352,9 +352,20 @@ class ObfProcessor extends BaseProcessor {
352
352
  throw err;
353
353
  }
354
354
  }
355
- // If input is a buffer or string that parses as OBF JSON
356
- const asJson = tryParseObfJson(filePathOrBuffer);
357
- if (asJson) {
355
+ // Detect likely zip signature first
356
+ function isLikelyZip(input) {
357
+ if (typeof input === 'string') {
358
+ const lowered = input.toLowerCase();
359
+ return lowered.endsWith('.zip') || lowered.endsWith('.obz');
360
+ }
361
+ const bytes = readBinaryFromInput(input);
362
+ return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
363
+ }
364
+ // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
365
+ if (!isLikelyZip(filePathOrBuffer)) {
366
+ const asJson = tryParseObfJson(filePathOrBuffer);
367
+ if (!asJson)
368
+ throw new Error('Invalid OBF content: not JSON and not ZIP');
358
369
  console.log('[OBF] Detected buffer/string as OBF JSON');
359
370
  const page = await this.processBoard(asJson, '[bufferOrString]');
360
371
  tree.addPage(page);
@@ -372,20 +383,10 @@ class ObfProcessor extends BaseProcessor {
372
383
  tree.rootId = page.id;
373
384
  return tree;
374
385
  }
375
- // Otherwise, try as ZIP (.obz). Detect likely zip signature first; throw if neither JSON nor ZIP
376
- function isLikelyZip(input) {
377
- if (typeof input === 'string') {
378
- const lowered = input.toLowerCase();
379
- return lowered.endsWith('.zip') || lowered.endsWith('.obz');
380
- }
381
- const bytes = readBinaryFromInput(input);
382
- return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
383
- }
384
- if (!isLikelyZip(filePathOrBuffer)) {
385
- throw new Error('Invalid OBF content: not JSON and not ZIP');
386
- }
387
386
  try {
388
- const zipResult = await openZipFromInput(filePathOrBuffer);
387
+ const zipResult = this.options.zipAdapter
388
+ ? await this.options.zipAdapter(filePathOrBuffer)
389
+ : await openZipFromInput(filePathOrBuffer);
389
390
  this.zipFile = zipResult.zip;
390
391
  }
391
392
  catch (err) {
@@ -4,6 +4,11 @@ import * as path from 'path';
4
4
  import Database from 'better-sqlite3';
5
5
  import { dotNetTicksToDate } from '../../utils/dotnetTicks';
6
6
  // Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
7
+ // NOTE: Snap files can store different types of image data in PageSetData:
8
+ // - PNG/JPEG binaries: Actual images that can be displayed
9
+ // - Vector graphics: Custom format (d7 cd c6 9a) requiring rendering engine
10
+ //
11
+ // We extract PNG/JPEG images but skip vector graphics (requires renderer).
7
12
  // NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
8
13
  // therefore return empty collections until image resolution is implemented.
9
14
  function collectFiles(root, matcher, maxDepth = 3) {
@@ -105,6 +110,9 @@ export function openImage(dbOrFile, entryPath) {
105
110
  .prepare('SELECT Id, Identifier, Data FROM PageSetData WHERE Identifier = ?')
106
111
  .get(entryPath);
107
112
  if (row && row.Data && row.Data.length > 0) {
113
+ // Snap files can store different types of image data:
114
+ // 1. PNG/JPEG binaries (actual images) - return as-is
115
+ // 2. Vector graphics (custom format d7 cd c6 9a) - return but may not be displayable
108
116
  return row.Data;
109
117
  }
110
118
  return null;
@@ -4,6 +4,25 @@ import { generateCloneId } from '../utilities/analytics/utils/idGenerator';
4
4
  import { SnapValidator } from '../validation/snapValidator';
5
5
  import { getFs, getNodeRequire, getPath, isNodeRuntime } from '../utils/io';
6
6
  import { openSqliteDatabase, requireBetterSqlite3 } from '../utils/sqlite';
7
+ /**
8
+ * Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
9
+ * Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
10
+ * This function works in both environments.
11
+ */
12
+ function arrayBufferToBase64(data) {
13
+ // Node.js environment - Buffer has built-in base64 encoding
14
+ if (typeof Buffer !== 'undefined' && data instanceof Buffer) {
15
+ return data.toString('base64');
16
+ }
17
+ // Browser environment - use btoa with binary string conversion
18
+ const bytes = new Uint8Array(data);
19
+ let binary = '';
20
+ const len = bytes.byteLength;
21
+ for (let i = 0; i < len; i++) {
22
+ binary += String.fromCharCode(bytes[i]);
23
+ }
24
+ return btoa(binary);
25
+ }
7
26
  /**
8
27
  * Map Snap Visible value to AAC standard visibility
9
28
  * Snap: 0 = hidden, 1 (or non-zero) = visible
@@ -15,41 +34,6 @@ function mapSnapVisibility(visible) {
15
34
  }
16
35
  return visible === 0 ? 'Hidden' : 'Visible';
17
36
  }
18
- /**
19
- * Detect image MIME type from binary data using magic bytes
20
- * @param buffer Image data buffer
21
- * @returns MIME type string (defaults to 'image/png' if unknown)
22
- */
23
- function detectImageMimeType(buffer) {
24
- if (!buffer || buffer.length < 8) {
25
- return 'image/png';
26
- }
27
- // Check for PNG: 89 50 4E 47 0D 0A 1A 0A
28
- if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
29
- return 'image/png';
30
- }
31
- // Check for JPEG: FF D8 FF
32
- if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
33
- return 'image/jpeg';
34
- }
35
- // Check for GIF: 47 49 46 38 (GIF8)
36
- if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
37
- return 'image/gif';
38
- }
39
- // Check for WebP: 52 49 46 46 ... 57 45 42 50 (RIFF...WEBP)
40
- if (buffer[0] === 0x52 &&
41
- buffer[1] === 0x49 &&
42
- buffer[2] === 0x46 &&
43
- buffer[3] === 0x46 &&
44
- buffer[8] === 0x57 &&
45
- buffer[9] === 0x45 &&
46
- buffer[10] === 0x42 &&
47
- buffer[11] === 0x50) {
48
- return 'image/webp';
49
- }
50
- // Default to PNG
51
- return 'image/png';
52
- }
53
37
  class SnapProcessor extends BaseProcessor {
54
38
  constructor(symbolResolver = null, options = {}) {
55
39
  super(options);
@@ -465,14 +449,30 @@ class SnapProcessor extends BaseProcessor {
465
449
  `)
466
450
  .get(btnRow.PageSetImageId);
467
451
  if (imageData && imageData.Data && imageData.Data.length > 0) {
468
- const mimeType = detectImageMimeType(imageData.Data);
469
- const base64 = imageData.Data.toString('base64');
470
- buttonImage = `data:${mimeType};base64,${base64}`;
471
- buttonParameters.image_id = imageData.Identifier;
472
- // NOTE: We don't include imageData in parameters because Buffers don't serialize
473
- // correctly across server/client boundaries (Next.js SSR, JSON, etc.)
474
- // The data URL in buttonImage is sufficient for display purposes.
475
- // For conversions, images can be reloaded from the source file/database.
452
+ // Snap files can store different types of image data:
453
+ // 1. PNG/JPEG binaries (actual images) - extract and display
454
+ // 2. Vector graphics (custom format d7 cd c6 9a) - skip (requires renderer)
455
+ const data = imageData.Data;
456
+ // Check for PNG: 89 50 4E 47
457
+ const isPng = data.length > 4 &&
458
+ data[0] === 0x89 &&
459
+ data[1] === 0x50 &&
460
+ data[2] === 0x4e &&
461
+ data[3] === 0x47;
462
+ // Check for JPEG: FF D8 FF
463
+ const isJpeg = data.length > 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff;
464
+ if (isPng || isJpeg) {
465
+ // Actual PNG/JPEG image - can be displayed
466
+ const mimeType = isPng ? 'image/png' : 'image/jpeg';
467
+ const base64 = arrayBufferToBase64(data);
468
+ buttonImage = `data:${mimeType};base64,${base64}`;
469
+ buttonParameters.image_id = imageData.Identifier;
470
+ }
471
+ else {
472
+ // Vector graphics or other format - skip rendering
473
+ // Store identifier but don't create image URL
474
+ buttonParameters.image_id = imageData.Identifier;
475
+ }
476
476
  }
477
477
  }
478
478
  catch (e) {
@@ -64,7 +64,9 @@ class TouchChatProcessor extends BaseProcessor {
64
64
  this.sourceFile = filePathOrBuffer;
65
65
  // Step 1: Unzip
66
66
  const zipInput = readBinaryFromInput(filePathOrBuffer);
67
- const { zip } = await openZipFromInput(zipInput);
67
+ const { zip } = this.options.zipAdapter
68
+ ? await this.options.zipAdapter(zipInput)
69
+ : await openZipFromInput(zipInput);
68
70
  const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
69
71
  if (!vocabEntry) {
70
72
  throw new Error('No .c4v vocab DB found in TouchChat export');
@@ -3,7 +3,7 @@
3
3
  /* eslint-disable @typescript-eslint/no-unsafe-return */
4
4
  import * as xml2js from 'xml2js';
5
5
  import { BaseValidator } from './baseValidator';
6
- import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array } from '../utils/io';
6
+ import { decodeText, getBasename, getFs, readBinaryFromInput, toUint8Array, } from '../utils/io';
7
7
  import { openZipFromInput } from '../utils/zip';
8
8
  import { openSqliteDatabase } from '../utils/sqlite';
9
9
  /**
@@ -27,14 +27,14 @@ export class TouchChatValidator extends BaseValidator {
27
27
  /**
28
28
  * Check if content is TouchChat format
29
29
  */
30
- static async identifyFormat(content, filename) {
30
+ static async identifyFormat(content, filename, zipAdapter) {
31
31
  const name = filename.toLowerCase();
32
32
  if (name.endsWith('.ce')) {
33
33
  return true;
34
34
  }
35
35
  // Try to parse as ZIP and check for .c4v database
36
36
  try {
37
- const { zip } = await openZipFromInput(content);
37
+ const { zip } = zipAdapter ? await zipAdapter(content) : await openZipFromInput(content);
38
38
  const entries = zip.listFiles();
39
39
  if (entries.some((entry) => entry.toLowerCase().endsWith('.c4v'))) {
40
40
  return true;
@@ -43,6 +43,7 @@ import { AACTree, AACButton } from './treeStructure';
43
43
  import { StringCasing } from './stringCasing';
44
44
  import { ValidationResult } from '../validation/validationTypes';
45
45
  import { BinaryOutput, ProcessorInput } from '../utils/io';
46
+ import type { ZipAdapter } from '../utils/zip';
46
47
  export interface ProcessorOptions {
47
48
  excludeNavigationButtons?: boolean;
48
49
  excludeSystemButtons?: boolean;
@@ -52,6 +53,9 @@ export interface ProcessorOptions {
52
53
  grid3SymbolDir?: string;
53
54
  grid3Path?: string;
54
55
  grid3Locale?: string;
56
+ zipAdapter?: (input: ProcessorInput) => Promise<{
57
+ zip: ZipAdapter;
58
+ }>;
55
59
  }
56
60
  export interface ExtractedString {
57
61
  string: string;
@@ -23,7 +23,7 @@ export { TouchChatProcessor } from './processors/touchchatProcessor';
23
23
  export { ApplePanelsProcessor } from './processors/applePanelsProcessor';
24
24
  export { AstericsGridProcessor } from './processors/astericsGridProcessor';
25
25
  export * as Metrics from './metrics';
26
- import { BaseProcessor } from './core/baseProcessor';
26
+ import { BaseProcessor, ProcessorOptions } from './core/baseProcessor';
27
27
  export { configureSqlJs } from './utils/sqlite';
28
28
  /**
29
29
  * Factory function to get the appropriate processor for a file extension
@@ -31,7 +31,7 @@ export { configureSqlJs } from './utils/sqlite';
31
31
  * @returns The appropriate processor instance
32
32
  * @throws Error if the file extension is not supported
33
33
  */
34
- export declare function getProcessor(filePathOrExtension: string): BaseProcessor;
34
+ export declare function getProcessor(filePathOrExtension: string, options?: ProcessorOptions): BaseProcessor;
35
35
  /**
36
36
  * Get all supported file extensions
37
37
  * @returns Array of supported file extensions
@@ -89,29 +89,29 @@ Object.defineProperty(exports, "configureSqlJs", { enumerable: true, get: functi
89
89
  * @returns The appropriate processor instance
90
90
  * @throws Error if the file extension is not supported
91
91
  */
92
- function getProcessor(filePathOrExtension) {
92
+ function getProcessor(filePathOrExtension, options) {
93
93
  const extension = filePathOrExtension.includes('.')
94
94
  ? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
95
95
  : filePathOrExtension;
96
96
  switch (extension.toLowerCase()) {
97
97
  case '.dot':
98
- return new dotProcessor_2.DotProcessor();
98
+ return new dotProcessor_2.DotProcessor(options);
99
99
  case '.opml':
100
- return new opmlProcessor_2.OpmlProcessor();
100
+ return new opmlProcessor_2.OpmlProcessor(options);
101
101
  case '.obf':
102
102
  case '.obz':
103
- return new obfProcessor_2.ObfProcessor();
103
+ return new obfProcessor_2.ObfProcessor(options);
104
104
  case '.gridset':
105
- return new gridsetProcessor_2.GridsetProcessor();
105
+ return new gridsetProcessor_2.GridsetProcessor(options);
106
106
  case '.spb':
107
107
  case '.sps':
108
- return new snapProcessor_2.SnapProcessor();
108
+ return new snapProcessor_2.SnapProcessor(options);
109
109
  case '.ce':
110
- return new touchchatProcessor_2.TouchChatProcessor();
110
+ return new touchchatProcessor_2.TouchChatProcessor(options);
111
111
  case '.plist':
112
- return new applePanelsProcessor_2.ApplePanelsProcessor();
112
+ return new applePanelsProcessor_2.ApplePanelsProcessor(options);
113
113
  case '.grd':
114
- return new astericsGridProcessor_2.AstericsGridProcessor();
114
+ return new astericsGridProcessor_2.AstericsGridProcessor(options);
115
115
  default:
116
116
  throw new Error(`Unsupported file extension: ${extension}`);
117
117
  }
@@ -23,7 +23,7 @@ export * as Opml from './opml';
23
23
  export * as ApplePanels from './applePanels';
24
24
  export * as AstericsGrid from './astericsGrid';
25
25
  export * as Translation from './translation';
26
- import { BaseProcessor } from './core/baseProcessor';
26
+ import { BaseProcessor, ProcessorOptions } from './core/baseProcessor';
27
27
  /**
28
28
  * Factory function to get the appropriate processor for a file extension
29
29
  * @param filePathOrExtension - File path or extension (e.g., '.dot', '/path/to/file.obf')
@@ -34,7 +34,7 @@ import { BaseProcessor } from './core/baseProcessor';
34
34
  * const processor = getProcessor('/path/to/file.gridset');
35
35
  * const tree = processor.loadIntoTree('/path/to/file.gridset');
36
36
  */
37
- export declare function getProcessor(filePathOrExtension: string): BaseProcessor;
37
+ export declare function getProcessor(filePathOrExtension: string, options?: ProcessorOptions): BaseProcessor;
38
38
  /**
39
39
  * Get all supported file extensions
40
40
  * @returns Array of supported file extensions
@@ -88,35 +88,35 @@ const obfsetProcessor_1 = require("./processors/obfsetProcessor");
88
88
  * const processor = getProcessor('/path/to/file.gridset');
89
89
  * const tree = processor.loadIntoTree('/path/to/file.gridset');
90
90
  */
91
- function getProcessor(filePathOrExtension) {
91
+ function getProcessor(filePathOrExtension, options) {
92
92
  // Extract extension from file path
93
93
  const extension = filePathOrExtension.includes('.')
94
94
  ? filePathOrExtension.substring(filePathOrExtension.lastIndexOf('.'))
95
95
  : filePathOrExtension;
96
96
  switch (extension.toLowerCase()) {
97
97
  case '.dot':
98
- return new dotProcessor_1.DotProcessor();
98
+ return new dotProcessor_1.DotProcessor(options);
99
99
  case '.xlsx':
100
- return new excelProcessor_1.ExcelProcessor();
100
+ return new excelProcessor_1.ExcelProcessor(options);
101
101
  case '.opml':
102
- return new opmlProcessor_1.OpmlProcessor();
102
+ return new opmlProcessor_1.OpmlProcessor(options);
103
103
  case '.obf':
104
104
  case '.obz':
105
- return new obfProcessor_1.ObfProcessor();
105
+ return new obfProcessor_1.ObfProcessor(options);
106
106
  case '.obfset':
107
- return new obfsetProcessor_1.ObfsetProcessor();
107
+ return new obfsetProcessor_1.ObfsetProcessor(options);
108
108
  case '.gridset':
109
109
  case '.gridsetx':
110
- return new gridsetProcessor_1.GridsetProcessor();
110
+ return new gridsetProcessor_1.GridsetProcessor(options);
111
111
  case '.spb':
112
112
  case '.sps':
113
- return new snapProcessor_1.SnapProcessor();
113
+ return new snapProcessor_1.SnapProcessor(options);
114
114
  case '.ce':
115
- return new touchchatProcessor_1.TouchChatProcessor();
115
+ return new touchchatProcessor_1.TouchChatProcessor(options);
116
116
  case '.plist':
117
- return new applePanelsProcessor_1.ApplePanelsProcessor();
117
+ return new applePanelsProcessor_1.ApplePanelsProcessor(options);
118
118
  case '.grd':
119
- return new astericsGridProcessor_1.AstericsGridProcessor();
119
+ return new astericsGridProcessor_1.AstericsGridProcessor(options);
120
120
  default:
121
121
  throw new Error(`Unsupported file extension: ${extension}`);
122
122
  }
@@ -1,4 +1,6 @@
1
1
  import { AACTree, AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
2
+ import { type ZipAdapter } from '../../utils/zip';
3
+ import type { ProcessorInput } from '../../utils/io';
2
4
  /**
3
5
  * Build a map of button IDs to resolved image entry paths for a specific page.
4
6
  * Helpful when rewriting zip entry names or validating images referenced in a grid.
@@ -15,7 +17,9 @@ export declare function getAllowedImageEntries(tree: AACTree): Set<string>;
15
17
  * @param entryPath Entry name inside the zip
16
18
  * @returns Image data buffer or null if not found
17
19
  */
18
- export declare function openImage(gridsetBuffer: Uint8Array, entryPath: string, password?: string | undefined): Promise<Uint8Array | null>;
20
+ export declare function openImage(gridsetBuffer: Uint8Array, entryPath: string, password?: string | undefined, zipAdapter?: (input: ProcessorInput) => Promise<{
21
+ zip: ZipAdapter;
22
+ }>): Promise<Uint8Array | null>;
19
23
  /**
20
24
  * Generate a random GUID for Grid3 elements
21
25
  * Grid3 uses GUIDs for grid identification
@@ -96,9 +96,11 @@ function getAllowedImageEntries(tree) {
96
96
  * @param entryPath Entry name inside the zip
97
97
  * @returns Image data buffer or null if not found
98
98
  */
99
- async function openImage(gridsetBuffer, entryPath, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
99
+ async function openImage(gridsetBuffer, entryPath, password = (0, password_1.resolveGridsetPasswordFromEnv)(), zipAdapter) {
100
100
  try {
101
- const { zip } = await (0, zip_1.openZipFromInput)(gridsetBuffer);
101
+ const { zip } = zipAdapter
102
+ ? await zipAdapter(gridsetBuffer)
103
+ : await (0, zip_1.openZipFromInput)(gridsetBuffer);
102
104
  const entries = (0, password_1.getZipEntriesFromAdapter)(zip, password);
103
105
  const want = normalizeZipPath(entryPath);
104
106
  const entry = entries.find((e) => normalizeZipPath(e.entryName) === want);
@@ -4,6 +4,8 @@
4
4
  * These utilities help developers understand why images might not be resolving
5
5
  * correctly in Grid3 gridsets.
6
6
  */
7
+ import { type ZipAdapter } from '../../utils/zip';
8
+ import { type ProcessorInput } from '../../utils/io';
7
9
  export interface ImageIssue {
8
10
  gridName: string;
9
11
  cellX: number;
@@ -34,7 +36,9 @@ export interface ImageAuditResult {
34
36
  * console.log(`Cell (${issue.cellX}, ${issue.cellY}): ${issue.suggestion}`);
35
37
  * });
36
38
  */
37
- export declare function auditGridsetImages(gridsetBuffer: Uint8Array, password?: string | undefined): Promise<ImageAuditResult>;
39
+ export declare function auditGridsetImages(gridsetBuffer: Uint8Array, password?: string | undefined, zipAdapter?: (input: ProcessorInput) => Promise<{
40
+ zip: ZipAdapter;
41
+ }>): Promise<ImageAuditResult>;
38
42
  /**
39
43
  * Get a human-readable summary of image audit results
40
44
  */
@@ -26,7 +26,7 @@ const io_1 = require("../../utils/io");
26
26
  * console.log(`Cell (${issue.cellX}, ${issue.cellY}): ${issue.suggestion}`);
27
27
  * });
28
28
  */
29
- async function auditGridsetImages(gridsetBuffer, password = (0, password_2.resolveGridsetPasswordFromEnv)()) {
29
+ async function auditGridsetImages(gridsetBuffer, password = (0, password_2.resolveGridsetPasswordFromEnv)(), zipAdapter) {
30
30
  const issues = [];
31
31
  const availableImages = new Set();
32
32
  let totalCells = 0;
@@ -34,7 +34,9 @@ async function auditGridsetImages(gridsetBuffer, password = (0, password_2.resol
34
34
  let resolvedImages = 0;
35
35
  let unresolvedImages = 0;
36
36
  try {
37
- const { zip } = await (0, zip_1.openZipFromInput)(gridsetBuffer);
37
+ const { zip } = zipAdapter
38
+ ? await zipAdapter(gridsetBuffer)
39
+ : await (0, zip_1.openZipFromInput)(gridsetBuffer);
38
40
  const entries = (0, password_1.getZipEntriesFromAdapter)(zip, password);
39
41
  const parser = new fast_xml_parser_1.XMLParser();
40
42
  // Collect all image files in the gridset
@@ -8,6 +8,8 @@
8
8
  * Note: Wordlists are only supported in Grid3 format. Other AAC formats
9
9
  * do not have equivalent wordlist functionality.
10
10
  */
11
+ import { type ZipAdapter } from '../../utils/zip';
12
+ import { type ProcessorInput } from '../../utils/io';
11
13
  /**
12
14
  * Represents a single item in a wordlist
13
15
  */
@@ -64,7 +66,9 @@ export declare function wordlistToXml(wordlist: WordList): string;
64
66
  * console.log(`Grid "${gridName}" has ${wordlist.items.length} items`);
65
67
  * });
66
68
  */
67
- export declare function extractWordlists(gridsetBuffer: Uint8Array, password?: string | undefined): Promise<Map<string, WordList>>;
69
+ export declare function extractWordlists(gridsetBuffer: Uint8Array, password?: string | undefined, zipAdapter?: (input: ProcessorInput) => Promise<{
70
+ zip: ZipAdapter;
71
+ }>): Promise<Map<string, WordList>>;
68
72
  /**
69
73
  * Updates or adds a wordlist to a specific grid in a gridset
70
74
  *
@@ -127,11 +127,13 @@ function wordlistToXml(wordlist) {
127
127
  * console.log(`Grid "${gridName}" has ${wordlist.items.length} items`);
128
128
  * });
129
129
  */
130
- async function extractWordlists(gridsetBuffer, password = (0, password_1.resolveGridsetPasswordFromEnv)()) {
130
+ async function extractWordlists(gridsetBuffer, password = (0, password_1.resolveGridsetPasswordFromEnv)(), zipAdapter) {
131
131
  const wordlists = new Map();
132
132
  const parser = new fast_xml_parser_1.XMLParser();
133
133
  try {
134
- const { zip } = await (0, zip_1.openZipFromInput)(gridsetBuffer);
134
+ const { zip } = zipAdapter
135
+ ? await zipAdapter(gridsetBuffer)
136
+ : await (0, zip_1.openZipFromInput)(gridsetBuffer);
135
137
  const entries = (0, password_1.getZipEntriesFromAdapter)(zip, password);
136
138
  // Process each grid file
137
139
  for (const entry of entries) {
@@ -424,7 +424,10 @@ class GridsetProcessor extends baseProcessor_1.BaseProcessor {
424
424
  const tree = new treeStructure_1.AACTree();
425
425
  let zipResult;
426
426
  try {
427
- zipResult = await (0, zip_1.openZipFromInput)((0, io_1.readBinaryFromInput)(filePathOrBuffer));
427
+ const zipInput = (0, io_1.readBinaryFromInput)(filePathOrBuffer);
428
+ zipResult = this.options.zipAdapter
429
+ ? await this.options.zipAdapter(zipInput)
430
+ : await (0, zip_1.openZipFromInput)(zipInput);
428
431
  }
429
432
  catch (error) {
430
433
  throw new Error(`Invalid ZIP file format: ${error.message}`);
@@ -378,9 +378,20 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
378
378
  throw err;
379
379
  }
380
380
  }
381
- // If input is a buffer or string that parses as OBF JSON
382
- const asJson = tryParseObfJson(filePathOrBuffer);
383
- if (asJson) {
381
+ // Detect likely zip signature first
382
+ function isLikelyZip(input) {
383
+ if (typeof input === 'string') {
384
+ const lowered = input.toLowerCase();
385
+ return lowered.endsWith('.zip') || lowered.endsWith('.obz');
386
+ }
387
+ const bytes = (0, io_1.readBinaryFromInput)(input);
388
+ return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
389
+ }
390
+ // Check if input is a buffer or string that parses as OBF JSON; throw if neither JSON nor ZIP
391
+ if (!isLikelyZip(filePathOrBuffer)) {
392
+ const asJson = tryParseObfJson(filePathOrBuffer);
393
+ if (!asJson)
394
+ throw new Error('Invalid OBF content: not JSON and not ZIP');
384
395
  console.log('[OBF] Detected buffer/string as OBF JSON');
385
396
  const page = await this.processBoard(asJson, '[bufferOrString]');
386
397
  tree.addPage(page);
@@ -398,20 +409,10 @@ class ObfProcessor extends baseProcessor_1.BaseProcessor {
398
409
  tree.rootId = page.id;
399
410
  return tree;
400
411
  }
401
- // Otherwise, try as ZIP (.obz). Detect likely zip signature first; throw if neither JSON nor ZIP
402
- function isLikelyZip(input) {
403
- if (typeof input === 'string') {
404
- const lowered = input.toLowerCase();
405
- return lowered.endsWith('.zip') || lowered.endsWith('.obz');
406
- }
407
- const bytes = (0, io_1.readBinaryFromInput)(input);
408
- return bytes.length >= 2 && bytes[0] === 0x50 && bytes[1] === 0x4b;
409
- }
410
- if (!isLikelyZip(filePathOrBuffer)) {
411
- throw new Error('Invalid OBF content: not JSON and not ZIP');
412
- }
413
412
  try {
414
- const zipResult = await (0, zip_1.openZipFromInput)(filePathOrBuffer);
413
+ const zipResult = this.options.zipAdapter
414
+ ? await this.options.zipAdapter(filePathOrBuffer)
415
+ : await (0, zip_1.openZipFromInput)(filePathOrBuffer);
415
416
  this.zipFile = zipResult.zip;
416
417
  }
417
418
  catch (err) {
@@ -43,6 +43,11 @@ const path = __importStar(require("path"));
43
43
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
44
44
  const dotnetTicks_1 = require("../../utils/dotnetTicks");
45
45
  // Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
46
+ // NOTE: Snap files can store different types of image data in PageSetData:
47
+ // - PNG/JPEG binaries: Actual images that can be displayed
48
+ // - Vector graphics: Custom format (d7 cd c6 9a) requiring rendering engine
49
+ //
50
+ // We extract PNG/JPEG images but skip vector graphics (requires renderer).
46
51
  // NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
47
52
  // therefore return empty collections until image resolution is implemented.
48
53
  function collectFiles(root, matcher, maxDepth = 3) {
@@ -144,6 +149,9 @@ function openImage(dbOrFile, entryPath) {
144
149
  .prepare('SELECT Id, Identifier, Data FROM PageSetData WHERE Identifier = ?')
145
150
  .get(entryPath);
146
151
  if (row && row.Data && row.Data.length > 0) {
152
+ // Snap files can store different types of image data:
153
+ // 1. PNG/JPEG binaries (actual images) - return as-is
154
+ // 2. Vector graphics (custom format d7 cd c6 9a) - return but may not be displayable
147
155
  return row.Data;
148
156
  }
149
157
  return null;
@@ -7,6 +7,25 @@ const idGenerator_1 = require("../utilities/analytics/utils/idGenerator");
7
7
  const snapValidator_1 = require("../validation/snapValidator");
8
8
  const io_1 = require("../utils/io");
9
9
  const sqlite_1 = require("../utils/sqlite");
10
+ /**
11
+ * Convert a Buffer or Uint8Array to base64 string (browser and Node compatible)
12
+ * Node.js Buffers support toString('base64'), but Uint8Arrays in browser do not.
13
+ * This function works in both environments.
14
+ */
15
+ function arrayBufferToBase64(data) {
16
+ // Node.js environment - Buffer has built-in base64 encoding
17
+ if (typeof Buffer !== 'undefined' && data instanceof Buffer) {
18
+ return data.toString('base64');
19
+ }
20
+ // Browser environment - use btoa with binary string conversion
21
+ const bytes = new Uint8Array(data);
22
+ let binary = '';
23
+ const len = bytes.byteLength;
24
+ for (let i = 0; i < len; i++) {
25
+ binary += String.fromCharCode(bytes[i]);
26
+ }
27
+ return btoa(binary);
28
+ }
10
29
  /**
11
30
  * Map Snap Visible value to AAC standard visibility
12
31
  * Snap: 0 = hidden, 1 (or non-zero) = visible
@@ -18,41 +37,6 @@ function mapSnapVisibility(visible) {
18
37
  }
19
38
  return visible === 0 ? 'Hidden' : 'Visible';
20
39
  }
21
- /**
22
- * Detect image MIME type from binary data using magic bytes
23
- * @param buffer Image data buffer
24
- * @returns MIME type string (defaults to 'image/png' if unknown)
25
- */
26
- function detectImageMimeType(buffer) {
27
- if (!buffer || buffer.length < 8) {
28
- return 'image/png';
29
- }
30
- // Check for PNG: 89 50 4E 47 0D 0A 1A 0A
31
- if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
32
- return 'image/png';
33
- }
34
- // Check for JPEG: FF D8 FF
35
- if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
36
- return 'image/jpeg';
37
- }
38
- // Check for GIF: 47 49 46 38 (GIF8)
39
- if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
40
- return 'image/gif';
41
- }
42
- // Check for WebP: 52 49 46 46 ... 57 45 42 50 (RIFF...WEBP)
43
- if (buffer[0] === 0x52 &&
44
- buffer[1] === 0x49 &&
45
- buffer[2] === 0x46 &&
46
- buffer[3] === 0x46 &&
47
- buffer[8] === 0x57 &&
48
- buffer[9] === 0x45 &&
49
- buffer[10] === 0x42 &&
50
- buffer[11] === 0x50) {
51
- return 'image/webp';
52
- }
53
- // Default to PNG
54
- return 'image/png';
55
- }
56
40
  class SnapProcessor extends baseProcessor_1.BaseProcessor {
57
41
  constructor(symbolResolver = null, options = {}) {
58
42
  super(options);
@@ -468,14 +452,30 @@ class SnapProcessor extends baseProcessor_1.BaseProcessor {
468
452
  `)
469
453
  .get(btnRow.PageSetImageId);
470
454
  if (imageData && imageData.Data && imageData.Data.length > 0) {
471
- const mimeType = detectImageMimeType(imageData.Data);
472
- const base64 = imageData.Data.toString('base64');
473
- buttonImage = `data:${mimeType};base64,${base64}`;
474
- buttonParameters.image_id = imageData.Identifier;
475
- // NOTE: We don't include imageData in parameters because Buffers don't serialize
476
- // correctly across server/client boundaries (Next.js SSR, JSON, etc.)
477
- // The data URL in buttonImage is sufficient for display purposes.
478
- // For conversions, images can be reloaded from the source file/database.
455
+ // Snap files can store different types of image data:
456
+ // 1. PNG/JPEG binaries (actual images) - extract and display
457
+ // 2. Vector graphics (custom format d7 cd c6 9a) - skip (requires renderer)
458
+ const data = imageData.Data;
459
+ // Check for PNG: 89 50 4E 47
460
+ const isPng = data.length > 4 &&
461
+ data[0] === 0x89 &&
462
+ data[1] === 0x50 &&
463
+ data[2] === 0x4e &&
464
+ data[3] === 0x47;
465
+ // Check for JPEG: FF D8 FF
466
+ const isJpeg = data.length > 3 && data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff;
467
+ if (isPng || isJpeg) {
468
+ // Actual PNG/JPEG image - can be displayed
469
+ const mimeType = isPng ? 'image/png' : 'image/jpeg';
470
+ const base64 = arrayBufferToBase64(data);
471
+ buttonImage = `data:${mimeType};base64,${base64}`;
472
+ buttonParameters.image_id = imageData.Identifier;
473
+ }
474
+ else {
475
+ // Vector graphics or other format - skip rendering
476
+ // Store identifier but don't create image URL
477
+ buttonParameters.image_id = imageData.Identifier;
478
+ }
479
479
  }
480
480
  }
481
481
  catch (e) {
@@ -67,7 +67,9 @@ class TouchChatProcessor extends baseProcessor_1.BaseProcessor {
67
67
  this.sourceFile = filePathOrBuffer;
68
68
  // Step 1: Unzip
69
69
  const zipInput = (0, io_1.readBinaryFromInput)(filePathOrBuffer);
70
- const { zip } = await (0, zip_1.openZipFromInput)(zipInput);
70
+ const { zip } = this.options.zipAdapter
71
+ ? await this.options.zipAdapter(zipInput)
72
+ : await (0, zip_1.openZipFromInput)(zipInput);
71
73
  const vocabEntry = zip.listFiles().find((name) => name.endsWith('.c4v'));
72
74
  if (!vocabEntry) {
73
75
  throw new Error('No .c4v vocab DB found in TouchChat export');
@@ -1,5 +1,7 @@
1
1
  import { BaseValidator } from './baseValidator';
2
2
  import { ValidationResult } from './validationTypes';
3
+ import { type ProcessorInput } from '../utils/io';
4
+ import { type ZipAdapter } from '../utils/zip';
3
5
  /**
4
6
  * Validator for TouchChat files (.ce)
5
7
  * TouchChat files are ZIP archives that contain a .c4v SQLite database.
@@ -14,7 +16,9 @@ export declare class TouchChatValidator extends BaseValidator {
14
16
  /**
15
17
  * Check if content is TouchChat format
16
18
  */
17
- static identifyFormat(content: any, filename: string): Promise<boolean>;
19
+ static identifyFormat(content: any, filename: string, zipAdapter?: (input: ProcessorInput) => Promise<{
20
+ zip: ZipAdapter;
21
+ }>): Promise<boolean>;
18
22
  /**
19
23
  * Main validation method
20
24
  */
@@ -53,14 +53,14 @@ class TouchChatValidator extends baseValidator_1.BaseValidator {
53
53
  /**
54
54
  * Check if content is TouchChat format
55
55
  */
56
- static async identifyFormat(content, filename) {
56
+ static async identifyFormat(content, filename, zipAdapter) {
57
57
  const name = filename.toLowerCase();
58
58
  if (name.endsWith('.ce')) {
59
59
  return true;
60
60
  }
61
61
  // Try to parse as ZIP and check for .c4v database
62
62
  try {
63
- const { zip } = await (0, zip_1.openZipFromInput)(content);
63
+ const { zip } = zipAdapter ? await zipAdapter(content) : await (0, zip_1.openZipFromInput)(content);
64
64
  const entries = zip.listFiles();
65
65
  if (entries.some((entry) => entry.toLowerCase().endsWith('.c4v'))) {
66
66
  return true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@willwade/aac-processors",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "description": "A comprehensive TypeScript library for processing AAC (Augmentative and Alternative Communication) file formats with translation support",
5
5
  "main": "dist/index.js",
6
6
  "browser": "dist/browser/index.browser.js",