Skip to content

Commit f26f56a

Browse files
committed
add in embedded image searching and extracting for snap
1 parent b2f416a commit f26f56a

2 files changed

Lines changed: 157 additions & 7 deletions

File tree

src/processors/snap/helpers.ts

Lines changed: 79 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
import { AACTree, AACSemanticCategory, AACSemanticIntent } from '../../core/treeStructure';
1+
import {
2+
AACTree,
3+
AACSemanticCategory,
4+
AACSemanticIntent,
5+
AACButton,
6+
} from '../../core/treeStructure';
27
import * as fs from 'fs';
38
import * as path from 'path';
49
import Database from 'better-sqlite3';
510
import { dotNetTicksToDate } from '../../utils/dotnetTicks';
11+
import { ProcessorInput } from '../../utils/io';
612

713
// Minimal Snap helpers (stubs) to align with processors/<engine>/helpers pattern
814
// NOTE: Snap buttons currently do not populate resolvedImageEntry; these helpers
@@ -57,18 +63,84 @@ export function getPageTokenImageMap(tree: AACTree, pageId: string): Map<string,
5763

5864
/**
5965
* Collect all image entry paths referenced in a Snap tree.
60-
* Currently empty until resolvedImageEntry is populated by the processor.
66+
* Returns the set of symbol identifiers (e.g., "SYM:12345") that are referenced by buttons.
6167
*/
62-
export function getAllowedImageEntries(_tree: AACTree): Set<string> {
63-
return new Set<string>();
68+
export function getAllowedImageEntries(tree: AACTree): Set<string> {
69+
const out = new Set<string>();
70+
Object.values(tree.pages).forEach((page) => {
71+
page.buttons.forEach((btn: AACButton) => {
72+
// Extract image_id from parameters if it exists
73+
if (btn.parameters?.image_id && typeof btn.parameters.image_id === 'string') {
74+
out.add(btn.parameters.image_id);
75+
}
76+
// Also add resolvedImageEntry if it's a symbol identifier
77+
if (btn.resolvedImageEntry && typeof btn.resolvedImageEntry === 'string') {
78+
const entry = btn.resolvedImageEntry;
79+
if (entry.startsWith('SYM:')) {
80+
out.add(entry);
81+
}
82+
}
83+
});
84+
});
85+
return out;
6486
}
6587

6688
/**
6789
* Read a binary asset from a Snap pageset.
68-
* Not implemented yet; provided for API symmetry with other processors.
90+
* @param dbOrFile Path to Snap .sps/.spb file or Buffer containing the file data
91+
* @param entryPath Symbol identifier (e.g., "SYM:12345")
92+
* @returns Image data buffer or null if not found
6993
*/
70-
export function openImage(_dbOrFile: string | Buffer, _entryPath: string): Buffer | null {
71-
return null;
94+
export function openImage(dbOrFile: ProcessorInput, entryPath: string): Buffer | null {
95+
let dbPath: string;
96+
let cleanupNeeded = false;
97+
98+
// Handle Buffer input by writing to temp file
99+
if (Buffer.isBuffer(dbOrFile)) {
100+
if (typeof fs.mkdtempSync !== 'function') {
101+
return null; // Not in Node environment
102+
}
103+
const tempDir = fs.mkdtempSync(path.join(process.cwd(), 'snap-'));
104+
dbPath = path.join(tempDir, 'temp.sps');
105+
fs.writeFileSync(dbPath, dbOrFile);
106+
cleanupNeeded = true;
107+
} else if (typeof dbOrFile === 'string') {
108+
dbPath = dbOrFile;
109+
} else {
110+
return null;
111+
}
112+
113+
let db: Database.Database | null = null;
114+
try {
115+
db = new Database(dbPath, { readonly: true });
116+
117+
// Query PageSetData for the symbol
118+
const row = db
119+
.prepare('SELECT Id, Identifier, Data FROM PageSetData WHERE Identifier = ?')
120+
.get(entryPath) as { Id: number; Identifier: string; Data: Buffer } | undefined;
121+
122+
if (row && row.Data && row.Data.length > 0) {
123+
return row.Data;
124+
}
125+
126+
return null;
127+
} catch (error) {
128+
console.warn(`[Snap helpers] Failed to open image ${entryPath}:`, error);
129+
return null;
130+
} finally {
131+
if (db) {
132+
db.close();
133+
}
134+
if (cleanupNeeded && dbPath) {
135+
try {
136+
fs.unlinkSync(dbPath);
137+
const dir = path.dirname(dbPath);
138+
fs.rmdirSync(dir);
139+
} catch (e) {
140+
// Ignore cleanup errors
141+
}
142+
}
143+
}
72144
}
73145

74146
/**

src/processors/snapProcessor.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,49 @@ interface SnapPage {
6060
BackgroundColor?: number;
6161
}
6262

63+
/**
64+
* Detect image MIME type from binary data using magic bytes
65+
* @param buffer Image data buffer
66+
* @returns MIME type string (defaults to 'image/png' if unknown)
67+
*/
68+
function detectImageMimeType(buffer: Buffer): string {
69+
if (!buffer || buffer.length < 8) {
70+
return 'image/png';
71+
}
72+
73+
// Check for PNG: 89 50 4E 47 0D 0A 1A 0A
74+
if (buffer[0] === 0x89 && buffer[1] === 0x50 && buffer[2] === 0x4e && buffer[3] === 0x47) {
75+
return 'image/png';
76+
}
77+
78+
// Check for JPEG: FF D8 FF
79+
if (buffer[0] === 0xff && buffer[1] === 0xd8 && buffer[2] === 0xff) {
80+
return 'image/jpeg';
81+
}
82+
83+
// Check for GIF: 47 49 46 38 (GIF8)
84+
if (buffer[0] === 0x47 && buffer[1] === 0x49 && buffer[2] === 0x46 && buffer[3] === 0x38) {
85+
return 'image/gif';
86+
}
87+
88+
// Check for WebP: 52 49 46 46 ... 57 45 42 50 (RIFF...WEBP)
89+
if (
90+
buffer[0] === 0x52 &&
91+
buffer[1] === 0x49 &&
92+
buffer[2] === 0x46 &&
93+
buffer[3] === 0x46 &&
94+
buffer[8] === 0x57 &&
95+
buffer[9] === 0x45 &&
96+
buffer[10] === 0x42 &&
97+
buffer[11] === 0x50
98+
) {
99+
return 'image/webp';
100+
}
101+
102+
// Default to PNG
103+
return 'image/png';
104+
}
105+
63106
class SnapProcessor extends BaseProcessor {
64107
private symbolResolver: unknown | null = null;
65108
private loadAudio: boolean = false;
@@ -519,6 +562,38 @@ class SnapProcessor extends BaseProcessor {
519562
}
520563
}
521564

565+
// Load symbol image if available
566+
// Note: PageSetImageId references embedded images in PageSetData table
567+
// LibrarySymbolId references external symbol libraries (SymbolStix, etc.)
568+
let buttonImage: string | undefined;
569+
const buttonParameters: { image_id?: string; imageData?: Buffer } = {};
570+
if (btnRow.PageSetImageId && btnRow.PageSetImageId > 0) {
571+
try {
572+
const imageData = db
573+
.prepare(
574+
`
575+
SELECT Id, Identifier, Data FROM PageSetData WHERE Id = ?
576+
`
577+
)
578+
.get(btnRow.PageSetImageId) as
579+
| { Id: number; Identifier: string; Data: Buffer }
580+
| undefined;
581+
582+
if (imageData && imageData.Data && imageData.Data.length > 0) {
583+
const mimeType = detectImageMimeType(imageData.Data);
584+
const base64 = imageData.Data.toString('base64');
585+
buttonImage = `data:${mimeType};base64,${base64}`;
586+
buttonParameters.image_id = imageData.Identifier;
587+
buttonParameters.imageData = imageData.Data;
588+
}
589+
} catch (e) {
590+
console.warn(
591+
`[SnapProcessor] Failed to load image for button ${btnRow.Id} (PageSetImageId: ${btnRow.PageSetImageId}):`,
592+
e
593+
);
594+
}
595+
}
596+
522597
// Create semantic action for Snap button
523598
let semanticAction: AACSemanticAction | undefined;
524599

@@ -569,6 +644,9 @@ class SnapProcessor extends BaseProcessor {
569644
semantic_id: btnRow.LibrarySymbolId
570645
? `snap_symbol_${btnRow.LibrarySymbolId}`
571646
: undefined, // Extract semantic_id from LibrarySymbolId
647+
image: buttonImage,
648+
resolvedImageEntry: buttonImage,
649+
parameters: Object.keys(buttonParameters).length > 0 ? buttonParameters : undefined,
572650
style: {
573651
backgroundColor: btnRow.BackgroundColor
574652
? `#${btnRow.BackgroundColor.toString(16)}`

0 commit comments

Comments
 (0)