Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883// bdf-glyph, 25.05.27.04.25// Loads a font file from the aethetic.computer assets directory and adds parsed// `json` files there as needed, sending them to the client on-demand.
import fetch from "node-fetch";import zlib from "zlib";import { promisify } from "util";import https from "https"; // Added for custom agentimport fs from "fs";import path from "path";import { S3Client, GetObjectCommand, PutObjectCommand,} from "@aws-sdk/client-s3";import { shell } from "../../backend/shell.mjs";
const gunzip = promisify(zlib.gunzip);
const dev = process.env.NETLIFY_DEV || process.env.NODE_ENV === 'development';shell.log(`🔧 Development mode: ${dev}, NETLIFY_DEV env: ${process.env.NETLIFY_DEV}`);
// CORS headers to allow IPFS originconst corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Methods": "GET, OPTIONS", "Access-Control-Allow-Headers": "Content-Type",};
// S3 client for assets storage (production only)const s3Assets = dev ? null : new S3Client({ endpoint: "https://" + process.env.ART_ENDPOINT, // Same as 'assets' region: "us-east-1", credentials: { accessKeyId: process.env.ART_KEY, // Same for 'assets' secretAccessKey: process.env.ART_SECRET, // Same for 'assets' }, });
// Get local cache directory for development based on fontfunction getLocalCacheDir(fontName = "unifont-16.0.03") { return `/workspaces/aesthetic-computer/system/public/assets/type/${fontName}`;}
// Helper function to display glyph as ASCII artfunction logGlyphAsASCII( charToFind, charCodeToFind, glyphData, source = "Unknown",) { shell.log( `Character: ${charToFind} Code: ${charCodeToFind} 📐 ${glyphData.resolution[0]}x${glyphData.resolution[1]} From: ${source}`, );
// Create a 2D array to represent the bitmap const [width, height] = glyphData.resolution; const bitmap = Array(height) .fill() .map(() => Array(width).fill(" "));
// Fill the bitmap with block characters based on commands for (const command of glyphData.commands) { if (command.name === "point" && command.args.length >= 2) { const [x, y] = command.args; if (x >= 0 && x < width && y >= 0 && y < height) { bitmap[y][x] = "█"; } } else if (command.name === "line" && command.args.length >= 4) { const [x1, y1, x2, y2] = command.args; // Draw line using Bresenham's algorithm (simplified for axis-aligned lines) if (y1 === y2) { // Horizontal line const startX = Math.min(x1, x2); const endX = Math.max(x1, x2); for (let x = startX; x <= endX; x++) { if (x >= 0 && x < width && y1 >= 0 && y1 < height) { bitmap[y1][x] = "█"; } } } else if (x1 === x2) { // Vertical line const startY = Math.min(y1, y2); const endY = Math.max(y1, y2); for (let y = startY; y <= endY; y++) { if (x1 >= 0 && x1 < width && y >= 0 && y < height) { bitmap[y][x1] = "█"; } } } else { // Diagonal line (basic implementation) const dx = Math.abs(x2 - x1); const dy = Math.abs(y2 - y1); const sx = x1 < x2 ? 1 : -1; const sy = y1 < y2 ? 1 : -1; let err = dx - dy; let x = x1, y = y1; while (true) { if (x >= 0 && x < width && y >= 0 && y < height) { bitmap[y][x] = "█"; } if (x === x2 && y === y2) break; const e2 = 2 * err; if (e2 > -dy) { err -= dy; x += sx; } if (e2 < dx) { err += dx; y += sy; } } } } }
// Find first and last non-empty rows to trim whitespace let firstNonEmptyRow = -1; let lastNonEmptyRow = -1;
for (let row = 0; row < height; row++) { const rowString = bitmap[row].join(""); if (rowString.trim() !== "") { if (firstNonEmptyRow === -1) firstNonEmptyRow = row; lastNonEmptyRow = row; } }
// Print the bitmap as ASCII art with colors and padding if (firstNonEmptyRow !== -1) { // Add at least 1 blue row above const emptyRow = " ".repeat(width); const paddedEmptyRow = "\x1b[44m" + emptyRow + "\x1b[0m"; console.log(paddedEmptyRow);
// Print the actual character rows for (let row = firstNonEmptyRow; row <= lastNonEmptyRow; row++) { const rowString = bitmap[row].join(""); // ANSI escape codes: \x1b[33m = yellow foreground, \x1b[44m = blue background, \x1b[0m = reset // Apply blue background to entire row, yellow foreground to block characters const coloredRow = "\x1b[44m" + rowString.replace(/█/g, "\x1b[33m█\x1b[44m") + "\x1b[0m"; console.log(coloredRow); }
// Add at least 1 blue row below console.log(paddedEmptyRow); }}
// Function to optimize point commands into horizontal/vertical line commands where possiblefunction optimizePointsToLines(pointCommands) { if (pointCommands.length === 0) return [];
// First, deduplicate points to ensure no coordinate appears twice const uniquePoints = new Map(); pointCommands.forEach(cmd => { const key = `${cmd.args[0]},${cmd.args[1]}`; if (!uniquePoints.has(key)) { uniquePoints.set(key, { x: cmd.args[0], y: cmd.args[1] }); } });
// Convert back to array const points = Array.from(uniquePoints.values()); const usedPoints = new Set(); const commands = [];
// Sort points for easier processing points.sort((a, b) => a.y !== b.y ? a.y - b.y : a.x - b.x);
for (let i = 0; i < points.length; i++) { if (usedPoints.has(i)) continue;
const point = points[i]; let lineEnd = null; const linePoints = [i];
// Check for horizontal line (same Y, consecutive X) for (let j = i + 1; j < points.length; j++) { if (usedPoints.has(j)) continue; const nextPoint = points[j]; // If we've moved to a different row, stop checking for horizontal lines if (nextPoint.y !== point.y) break; // Check if this point continues the horizontal line const expectedX = point.x + linePoints.length; if (nextPoint.x === expectedX) { linePoints.push(j); lineEnd = nextPoint; } else { break; } }
// If we found a horizontal line with at least 2 points, use it if (linePoints.length >= 2) { commands.push({ name: "line", args: [point.x, point.y, lineEnd.x, lineEnd.y] }); linePoints.forEach(idx => usedPoints.add(idx)); continue; }
// Check for vertical line (same X, consecutive Y) lineEnd = null; const verticalLinePoints = [i]; for (let j = i + 1; j < points.length; j++) { if (usedPoints.has(j)) continue; const nextPoint = points[j]; // Check if this point continues the vertical line if (nextPoint.x === point.x && nextPoint.y === point.y + verticalLinePoints.length) { verticalLinePoints.push(j); lineEnd = nextPoint; } }
// If we found a vertical line with at least 2 points, use it if (verticalLinePoints.length >= 2) { commands.push({ name: "line", args: [point.x, point.y, lineEnd.x, lineEnd.y] }); verticalLinePoints.forEach(idx => usedPoints.add(idx)); continue; }
// No line found, keep as individual point commands.push({ name: "point", args: [point.x, point.y] }); usedPoints.add(i); }
return commands;}
// Check if cached JSON exists for a character codeasync function getCachedGlyph(charCode, fontName = "unifont-16.0.03") { const key = `${charCode.toString(16).toUpperCase()}.json`; const localCacheDir = getLocalCacheDir(fontName);
if (dev) { // Development mode: check local filesystem try { const filePath = path.join(localCacheDir, key); if (fs.existsSync(filePath)) { const jsonString = fs.readFileSync(filePath, "utf-8"); return { data: JSON.parse(jsonString), source: `Local filesystem: ${filePath}`, }; } } catch (error) { shell.log( `No local cached glyph found for character code ${charCode}: ${error.message}`, ); } return null; } else { // Production mode: check S3 try { const s3Key = `type/${fontName}/${key}`; const getObjectParams = { Bucket: "assets-aesthetic-computer", Key: s3Key, };
const command = new GetObjectCommand(getObjectParams); const response = await s3Assets.send(command);
// Convert the stream to string const chunks = []; for await (const chunk of response.Body) { chunks.push(chunk); } const jsonString = Buffer.concat(chunks).toString("utf-8");
return { data: JSON.parse(jsonString), source: `S3: https://assets.aesthetic.computer/type/${fontName}/${key}`, }; } catch (error) { // File doesn't exist or other error - will fall back to BDF parsing shell.log( `No cached glyph found on S3 for character code ${charCode}: ${error.message}`, ); return null; } }}
// Cache parsed glyph data to S3 or local filesystemasync function cacheGlyph(charCode, glyphData, fontName = "unifont-16.0.03") { const key = `${charCode.toString(16).toUpperCase()}.json`; const localCacheDir = getLocalCacheDir(fontName);
if (dev) { // Development mode: save to local filesystem try { // Ensure the directory exists if (!fs.existsSync(localCacheDir)) { fs.mkdirSync(localCacheDir, { recursive: true }); }
const filePath = path.join(localCacheDir, key); fs.writeFileSync(filePath, JSON.stringify(glyphData), "utf-8");
shell.log(`Cached glyph data locally for character code ${charCode}`); } catch (error) { // Log error but don't fail the request shell.error( `Failed to cache glyph locally for character code ${charCode}:`, error.message, ); } } else { // Production mode: save to S3 try { const s3Key = `type/${fontName}/${key}`; const putObjectParams = { Bucket: "assets-aesthetic-computer", Key: s3Key, Body: JSON.stringify(glyphData), ContentType: "application/json", ACL: "public-read", };
const command = new PutObjectCommand(putObjectParams); await s3Assets.send(command);
shell.log(`Cached glyph data to S3 for character code ${charCode}`); } catch (error) { // Log error but don't fail the request shell.error( `Failed to cache glyph to S3 for character code ${charCode}:`, error.message, ); } }}
// Parse a single character and return glyph data from BDF textasync function parseGlyphFromBDF(charCodeToFind, charToFind, bdfText, fontParam) { const lines = bdfText.split(/\r?\n/); let inChar = false; let inBitmap = false; let currentCharCode = -1; let bbx = null; let dwidth = null; let bitmapLines = []; let fontAscent = 8; let fontDescent = 0;
for (const line of lines) { if (line.startsWith("FONT_ASCENT")) { fontAscent = parseInt(line.split(" ")[1]); } else if (line.startsWith("FONT_DESCENT")) { fontDescent = parseInt(line.split(" ")[1]); } else if (line.startsWith("STARTCHAR")) { inChar = true; inBitmap = false; currentCharCode = -1; bbx = null; dwidth = null; bitmapLines = []; } else if (inChar && line.startsWith("ENCODING")) { currentCharCode = parseInt(line.split(" ")[1]); } else if (inChar && line.startsWith("DWIDTH")) { const parts = line.split(" "); dwidth = { x: parseInt(parts[1]), y: parseInt(parts[2]) }; } else if (inChar && line.startsWith("BBX")) { const parts = line.split(" "); bbx = { width: parseInt(parts[1]), height: parseInt(parts[2]), xOffset: parseInt(parts[3]), yOffset: parseInt(parts[4]), }; } else if (inChar && line.trim() === "BITMAP") { inBitmap = true; } else if (inChar && line.startsWith("ENDCHAR")) { if (currentCharCode === charCodeToFind && bbx && bbx.width > 0 && bbx.height > 0) { const pointCommands = []; const bytesPerRow = Math.ceil(bbx.width / 8);
for (let r = 0; r < bbx.height; r++) { if (r >= bitmapLines.length) break; const hexRow = bitmapLines[r].trim(); const paddedHexRow = hexRow.length % 2 !== 0 ? hexRow + "0" : hexRow; const rowBytes = Buffer.from(paddedHexRow, "hex");
for (let c = 0; c < bbx.width; c++) { const byteIndex = Math.floor(c / 8); const bitIndex = c % 8; if (byteIndex < rowBytes.length) { if ((rowBytes[byteIndex] >> (7 - bitIndex)) & 1) { pointCommands.push({ name: "point", args: [c, r] }); } } } }
const commands = optimizePointsToLines(pointCommands);
const glyphData = { resolution: [bbx.width, bbx.height], offset: [bbx.xOffset, bbx.yOffset], baselineOffset: [bbx.xOffset, fontAscent - bbx.height - bbx.yOffset], advance: dwidth ? dwidth.x : 4, bbx: { width: bbx.width, height: bbx.height, xOffset: bbx.xOffset, yOffset: bbx.yOffset }, dwidth: dwidth, fontMetrics: { ascent: fontAscent, descent: fontDescent }, commands: commands, };
// Cache for future single-char requests await cacheGlyph(charCodeToFind, glyphData, fontParam); return glyphData; } inChar = false; inBitmap = false; } else if (inChar && inBitmap && bbx && bitmapLines.length < bbx.height) { bitmapLines.push(line.trim()); } } return null; // Glyph not found}
export const handler = async (event) => { // Handle OPTIONS preflight requests if (event.httpMethod === "OPTIONS") { return { statusCode: 204, headers: corsHeaders, body: "", }; }
const charParam = event.queryStringParameters.char; const charsParam = event.queryStringParameters.chars; // Batch mode: comma-separated code points let fontParam = event.queryStringParameters.font || "unifont-16.0.03"; const nocache = event.queryStringParameters.nocache === "true" || dev; // Skip cache in dev mode or with nocache=true // Debug logging for all requests shell.log(`🔍 BDF-GLYPH CALLED: char=${charParam}, chars=${charsParam}, font=${fontParam}, nocache=${nocache}`); shell.log(`🔍 Query params:`, JSON.stringify(event.queryStringParameters)); // Normalize font name for unifont if (fontParam === "unifont") { fontParam = "unifont-16.0.03"; } // BATCH MODE: Handle multiple characters in one request if (charsParam) { const codePoints = charsParam.split(",").map(s => s.trim()).filter(s => s.length > 0); if (codePoints.length === 0) { return { statusCode: 400, body: JSON.stringify({ error: "Empty 'chars' parameter." }), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; } shell.log(`📦 Batch glyph request: ${codePoints.length} characters for font ${fontParam}`); // Results map: codePoint -> glyphData or null const results = {}; const needsFetch = []; // Code points not in cache // Check cache for each character for (const codePointStr of codePoints) { const charCode = parseInt(codePointStr, 16); if (isNaN(charCode)) continue; const cachedGlyph = await getCachedGlyph(charCode, fontParam); if (cachedGlyph) { results[codePointStr] = cachedGlyph.data; } else { needsFetch.push({ codePointStr, charCode }); } } // If any need fetching from BDF, load the BDF file once if (needsFetch.length > 0) { const assetsBaseUrl = dev ? process.env.URL || "http://localhost:8888" : "https://assets.aesthetic.computer"; let bdfFileName; if (fontParam === "unifont-16.0.03" || fontParam === "unifont") { bdfFileName = "unifont-16.0.03.bdf.gz"; } else { bdfFileName = `${fontParam}.bdf`; } const bdfFileUrl = dev ? `${assetsBaseUrl}/assets/type/${bdfFileName}` : `${assetsBaseUrl}/type/${bdfFileName}`; try { let fetchOptions = {}; if (dev && bdfFileUrl.startsWith("https://localhost")) { const agent = new https.Agent({ rejectUnauthorized: false }); fetchOptions.agent = agent; } const response = await fetch(bdfFileUrl, fetchOptions); if (!response.ok) { throw new Error(`Failed to fetch BDF file: ${response.statusText}`); } let bdfText; if (bdfFileName.endsWith('.gz')) { const compressedData = await response.buffer(); bdfText = (await gunzip(compressedData)).toString("utf-8"); } else { bdfText = await response.text(); } // Parse each needed character from the BDF for (const { codePointStr, charCode } of needsFetch) { let charToFind; try { charToFind = String.fromCodePoint(charCode); } catch (e) { results[codePointStr] = null; continue; } const glyphData = await parseGlyphFromBDF(charCode, charToFind, bdfText, fontParam); results[codePointStr] = glyphData; } } catch (error) { shell.error("Error in batch bdf-glyph:", error); // Mark unfetched as null for (const { codePointStr } of needsFetch) { if (!(codePointStr in results)) { results[codePointStr] = null; } } } } return { statusCode: 200, body: JSON.stringify({ glyphs: results }), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; } // SINGLE CHARACTER MODE (existing behavior) if (!charParam) { return { statusCode: 400, body: JSON.stringify({ error: "Missing 'char' parameter." }), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; }
let charToFind, charCodeToFind;
// Check if the parameter is a Unicode code point (hex string) or actual character if (/^[0-9A-F_]+$/i.test(charParam)) { // Handle code point format (e.g., "1F600" or "1F600_200D_1F32B") // For now, take the first code point if multiple are provided const codePointStr = charParam.split("_")[0]; charCodeToFind = parseInt(codePointStr, 16);
// Convert code point back to character for display purposes try { charToFind = String.fromCodePoint(charCodeToFind); } catch (error) { return { statusCode: 400, body: JSON.stringify({ error: `Invalid Unicode code point: ${codePointStr}`, }), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; } } else if (charParam.length === 1) { // Handle single character format charToFind = charParam; charCodeToFind = charParam.charCodeAt(0); } else { // Handle multi-byte characters (emoji, etc.) charToFind = charParam; charCodeToFind = charParam.codePointAt(0); }
// Check for cached glyph data first (skip if nocache) if (!nocache) { const cachedGlyph = await getCachedGlyph(charCodeToFind, fontParam); if (cachedGlyph) { // Display ASCII art for cached glyph logGlyphAsASCII( charToFind, charCodeToFind, cachedGlyph.data, cachedGlyph.source, );
return { statusCode: 200, body: JSON.stringify(cachedGlyph.data), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; } } else { shell.log(`🔄 Skipping cache lookup (nocache=${nocache})`); }
shell.log( `No cached glyph found, parsing BDF for character '${charToFind}' (code ${charCodeToFind}) from font '${fontParam}'`, );
const assetsBaseUrl = dev ? process.env.URL || "http://localhost:8888" : "https://assets.aesthetic.computer";
// shell.log(`🌐 Assets base URL: ${assetsBaseUrl} (dev mode: ${dev})`);
// Determine BDF file extension and path based on font let bdfFileName; if (fontParam === "unifont-16.0.03" || fontParam === "unifont") { bdfFileName = "unifont-16.0.03.bdf.gz"; // Compressed unifont } else { bdfFileName = `${fontParam}.bdf`; // Uncompressed for other fonts }
const bdfFileUrl = dev ? `${assetsBaseUrl}/assets/type/${bdfFileName}` : `${assetsBaseUrl}/type/${bdfFileName}`;
// In dev mode, add cache-busting timestamp to always get fresh BDF file const actualBdfUrl = dev ? `${assetsBaseUrl}/assets/type/${bdfFileName}?t=${Date.now()}` : (fontParam === "MatrixChunky8" ? `${assetsBaseUrl}/type/MatrixChunky8.bdf` : bdfFileUrl);
if (dev) { shell.log(`🔄 Dev mode: fetching fresh BDF from ${actualBdfUrl}`); }
// shell.log(`Attempting to fetch BDF from: ${actualBdfUrl}`);
try { let fetchOptions = {}; if (dev && bdfFileUrl.startsWith("https://localhost")) { const agent = new https.Agent({ rejectUnauthorized: false, }); fetchOptions.agent = agent; shell.log( `Using custom agent for ${bdfFileUrl} to ignore self-signed certificate in local dev.`, ); }
const response = await fetch(actualBdfUrl, fetchOptions); // Pass fetchOptions if (!response.ok) { throw new Error( `Failed to fetch BDF file: ${response.statusText} from ${bdfFileUrl}`, ); } let bdfText; if (bdfFileName.endsWith('.gz')) { // Handle compressed unifont const compressedData = await response.buffer(); bdfText = (await gunzip(compressedData)).toString("utf-8"); } else { // Handle uncompressed BDF files bdfText = await response.text(); }
// console.log( // "BDF File fetched and decompressed. First 200 chars:", // bdfText.substring(0, 200), // ); // Log start of BDF
const lines = bdfText.split(/\r?\n/); // More robust line splitting let inChar = false; let currentCharCode = -1; let bbx = null; let dwidth = null; // Device width (character advance) let bitmapLines = []; let glyphFound = false; // Capture font-level metrics for proper baseline calculation let fontAscent = 8; // Default for MatrixChunky8 let fontDescent = 0; // Default for MatrixChunky8
for (const line of lines) { if (line.startsWith("FONT_ASCENT")) { fontAscent = parseInt(line.split(" ")[1]); // shell.log(`Found FONT_ASCENT: ${fontAscent}`); } else if (line.startsWith("FONT_DESCENT")) { fontDescent = parseInt(line.split(" ")[1]); // shell.log(`Found FONT_DESCENT: ${fontDescent}`); } else if (line.startsWith("STARTCHAR")) { inChar = true; currentCharCode = -1; bbx = null; dwidth = null; bitmapLines = []; } else if (inChar && line.startsWith("ENCODING")) { currentCharCode = parseInt(line.split(" ")[1]); // console.log(`Found ENCODING: ${currentCharCode}`); // Optional: very verbose if (currentCharCode === charCodeToFind) { // shell.log(`Target ENCODING ${charCodeToFind} found for a character.`); } } else if (inChar && line.startsWith("DWIDTH")) { const parts = line.split(" "); dwidth = { x: parseInt(parts[1]), y: parseInt(parts[2]) }; if (currentCharCode === charCodeToFind) { // shell.log(`Found DWIDTH for target character ${charCodeToFind}: ${dwidth.x}`); } // Extra logging for character 40 (parenthesis) if (currentCharCode === 40) { // shell.log(`🔤 Character '(' (code 40) DWIDTH: x=${dwidth.x}, y=${dwidth.y} | advance will be: ${dwidth.x}`); } } else if (inChar && line.startsWith("BBX")) { const parts = line.split(" "); bbx = { width: parseInt(parts[1]), height: parseInt(parts[2]), xOffset: parseInt(parts[3]), yOffset: parseInt(parts[4]), }; } else if (inChar && line.startsWith("BITMAP")) { // Next lines are bitmap data until ENDCHAR // The actual bitmap lines are collected in the else if block below } else if (inChar && line.startsWith("ENDCHAR")) { if (currentCharCode === charCodeToFind) { // shell.log( // `Processing ENDCHAR for target ${charCodeToFind}. BBX:`, // bbx, // `Bitmap lines collected: ${bitmapLines.length}`, // ); } if ( currentCharCode === charCodeToFind && bbx && bbx.width > 0 && bbx.height > 0 ) { glyphFound = true; // shell.log( // `Glyph ${charToFind} (code ${charCodeToFind}) found and processed. BBX width: ${bbx.width}, height: ${bbx.height}`, // ); // Process bitmapLines const pointCommands = []; const bytesPerRow = Math.ceil(bbx.width / 8);
for (let r = 0; r < bbx.height; r++) { if (r >= bitmapLines.length) break; // Should not happen with valid BDF const hexRow = bitmapLines[r].trim(); // Ensure hexRow is padded to be an even number of characters for Buffer.from const paddedHexRow = hexRow.length % 2 !== 0 ? hexRow + "0" : hexRow; const rowBytes = Buffer.from(paddedHexRow, "hex");
for (let c = 0; c < bbx.width; c++) { const byteIndex = Math.floor(c / 8); const bitIndex = c % 8; if (byteIndex < rowBytes.length) { if ((rowBytes[byteIndex] >> (7 - bitIndex)) & 1) { // Keep bitmap coordinates as-is (top-left relative) // The key fix will be in how we interpret the Y offset pointCommands.push({ name: "point", args: [c, r] }); } } } }
// Optimize points into lines where possible const commands = optimizePointsToLines(pointCommands);
// Count command types for detailed logging const commandCounts = { point: 0, line: 0 }; commands.forEach(cmd => { if (commandCounts.hasOwnProperty(cmd.name)) { commandCounts[cmd.name]++; } });
// shell.log( // `Optimized ${pointCommands.length} points into ${commands.length} commands: ` + // `${commandCounts.line} lines, ${commandCounts.point} points` // );
const glyphData = { resolution: [bbx.width, bbx.height], offset: [bbx.xOffset, bbx.yOffset], // Keep original offset for debugging baselineOffset: [bbx.xOffset, fontAscent - bbx.height - bbx.yOffset], // Bottom-aligned baseline offset advance: dwidth ? dwidth.x : 4, // Character advance width (DWIDTH) bbx: { width: bbx.width, height: bbx.height, xOffset: bbx.xOffset, yOffset: bbx.yOffset }, // Full BBX data for detailed positioning dwidth: dwidth, // Device width information fontMetrics: { ascent: fontAscent, descent: fontDescent }, // Font-level metrics commands: commands, };
// Extra logging for character 40 (parenthesis) if (charCodeToFind === 40) { // shell.log(`🚀 Final glyph data for '(' (code 40):`, { // advance: glyphData.advance, // dwidth: glyphData.dwidth, // resolution: glyphData.resolution, // bbx: glyphData.bbx // }); }
// Log glyph as ASCII art using block characters // logGlyphAsASCII(charToFind, charCodeToFind, glyphData, "Generated");
// Cache the parsed glyph data (skip in dev/nocache mode) if (!nocache) { await cacheGlyph(charCodeToFind, glyphData, fontParam); } else { shell.log(`🔄 Skipping cache write (nocache=${nocache})`); }
return { statusCode: 200, body: JSON.stringify(glyphData), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; } inChar = false; } else if (inChar && bbx && bitmapLines.length < bbx.height) { // This line is part of the BITMAP data bitmapLines.push(line.trim()); } }
if (!glyphFound) { shell.log( `Glyph for character '${charToFind}' (code ${charCodeToFind}) was not found after parsing the entire file.`, ); return { statusCode: 404, body: JSON.stringify({ error: `Glyph for character '${charToFind}' (code ${charCodeToFind}) not found.`, }), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; } } catch (error) { shell.error("Error in bdf-glyph function:", error); return { statusCode: 500, body: JSON.stringify({ error: error.message }), headers: { ...corsHeaders, "Content-Type": "application/json" }, }; }};