const COLORS = { '⬛': [120, 124, 126], '🟨': [201, 180, 88], '🟩': [106, 170, 100], }; const GLYPHS = { A: ['01110', '10001', '10001', '11111', '10001', '10001', '10001'], B: ['11110', '10001', '10001', '11110', '10001', '10001', '11110'], C: ['01111', '10000', '10000', '10000', '10000', '10000', '01111'], D: ['11110', '10001', '10001', '10001', '10001', '10001', '11110'], E: ['11111', '10000', '10000', '11110', '10000', '10000', '11111'], F: ['11111', '10000', '10000', '11110', '10000', '10000', '10000'], G: ['01111', '10000', '10000', '10111', '10001', '10001', '01111'], H: ['10001', '10001', '10001', '11111', '10001', '10001', '10001'], I: ['11111', '00100', '00100', '00100', '00100', '00100', '11111'], J: ['00111', '00010', '00010', '00010', '00010', '10010', '01100'], K: ['10001', '10010', '10100', '11000', '10100', '10010', '10001'], L: ['10000', '10000', '10000', '10000', '10000', '10000', '11111'], M: ['10001', '11011', '10101', '10101', '10001', '10001', '10001'], N: ['10001', '11001', '10101', '10011', '10001', '10001', '10001'], O: ['01110', '10001', '10001', '10001', '10001', '10001', '01110'], P: ['11110', '10001', '10001', '11110', '10000', '10000', '10000'], Q: ['01110', '10001', '10001', '10001', '10101', '10010', '01101'], R: ['11110', '10001', '10001', '11110', '10100', '10010', '10001'], S: ['01111', '10000', '10000', '01110', '00001', '00001', '11110'], T: ['11111', '00100', '00100', '00100', '00100', '00100', '00100'], U: ['10001', '10001', '10001', '10001', '10001', '10001', '01110'], V: ['10001', '10001', '10001', '10001', '10001', '01010', '00100'], W: ['10001', '10001', '10001', '10101', '10101', '11011', '10001'], X: ['10001', '10001', '01010', '00100', '01010', '10001', '10001'], Y: ['10001', '10001', '01010', '00100', '00100', '00100', '00100'], Z: ['11111', '00001', '00010', '00100', '01000', '10000', '11111'], }; function parseState(state) { const rows = state.split('|'); if (rows.length !== 6) throw new Error('state must contain exactly six rows'); return rows.map((row) => { const characters = Array.from(row.trim()); const tiles = []; for (let index = 0; index < characters.length; index += 1) { const symbol = characters[index]; if (COLORS[symbol]) { const next = characters[index + 1]; const letter = next && /^[A-Za-z]$/.test(next) ? next.toUpperCase() : ''; if (letter) index += 1; tiles.push({ color: COLORS[symbol], letter }); } else if (/^[A-Za-z]$/.test(symbol)) { tiles.push({ color: COLORS['⬛'], letter: symbol.toUpperCase() }); } else { throw new Error('state contains an unsupported tile'); } } if (tiles.length !== 5) throw new Error('each row must contain exactly five tiles'); return tiles; }); } function crc32(bytes) { let crc = 0xffffffff; for (const byte of bytes) { crc ^= byte; for (let bit = 0; bit < 8; bit += 1) { crc = (crc >>> 1) ^ (crc & 1 ? 0xedb88320 : 0); } } return (crc ^ 0xffffffff) >>> 0; } function uint32(value) { return new Uint8Array([(value >>> 24) & 255, (value >>> 16) & 255, (value >>> 8) & 255, value & 255]); } function chunk(type, data) { const typeBytes = new TextEncoder().encode(type); const combined = new Uint8Array(typeBytes.length + data.length); combined.set(typeBytes); combined.set(data, typeBytes.length); return concat(uint32(data.length), combined, uint32(crc32(combined))); } function concat(...parts) { const result = new Uint8Array(parts.reduce((total, part) => total + part.length, 0)); let offset = 0; for (const part of parts) { result.set(part, offset); offset += part.length; } return result; } function drawGlyph(pixels, size, glyph, startX, startY, scale) { glyph.forEach((line, gy) => [...line].forEach((filled, gx) => { if (filled !== '1') return; for (let dy = 0; dy < scale; dy += 1) { for (let dx = 0; dx < scale; dx += 1) { const offset = ((startY + gy * scale + dy) * size + startX + gx * scale + dx) * 4; pixels[offset] = 255; pixels[offset + 1] = 255; pixels[offset + 2] = 255; pixels[offset + 3] = 255; } } })); } function makePng(rows) { const size = 600; const pixels = new Uint8Array(size * size * 4); pixels.fill(255); const padding = 20; const tile = 72; const gap = 8; const scale = 6; const labelX = padding + tile * 5 + gap * 5; const labelWidth = 100; rows.forEach((row, rowIndex) => row.forEach((item, columnIndex) => { const x0 = padding + columnIndex * (tile + gap); const y0 = padding + rowIndex * (tile + gap); for (let y = y0; y < y0 + tile; y += 1) { for (let x = x0; x < x0 + tile; x += 1) { const offset = (y * size + x) * 4; pixels[offset] = item.color[0]; pixels[offset + 1] = item.color[1]; pixels[offset + 2] = item.color[2]; } } const glyph = GLYPHS[item.letter]; if (!glyph) return; const startX = x0 + Math.floor((tile - 5 * scale) / 2); const startY = y0 + Math.floor((tile - 7 * scale) / 2); drawGlyph(pixels, size, glyph, startX, startY, scale); })); const labelScale = 4; const labelText = 'MORI'; const labelTextWidth = labelText.length * 5 * labelScale + (labelText.length - 1) * labelScale; rows.forEach((row, rowIndex) => { const y0 = padding + rowIndex * (tile + gap); for (let y = y0; y < y0 + tile; y += 1) { for (let x = labelX; x < labelX + labelWidth; x += 1) { const offset = (y * size + x) * 4; pixels[offset] = 120; pixels[offset + 1] = 124; pixels[offset + 2] = 126; pixels[offset + 3] = 255; } } let textX = labelX + Math.floor((labelWidth - labelTextWidth) / 2); const textY = y0 + Math.floor((tile - 7 * labelScale) / 2); for (const letter of labelText) { const glyph = GLYPHS[letter]; drawGlyph(pixels, size, glyph, textX, textY, labelScale); textX += 5 * labelScale + labelScale; } }); const scanlines = new Uint8Array(size * (size * 4 + 1)); for (let y = 0; y < size; y += 1) { scanlines[y * (size * 4 + 1)] = 0; scanlines.set(pixels.subarray(y * size * 4, (y + 1) * size * 4), y * (size * 4 + 1) + 1); } // Build a zlib stream containing stored (uncompressed) DEFLATE blocks. const blocks = []; for (let offset = 0; offset < scanlines.length;) { const length = Math.min(65535, scanlines.length - offset); const final = offset + length === scanlines.length; const block = new Uint8Array(5 + length); block[0] = final ? 1 : 0; block[1] = length & 255; block[2] = length >>> 8; block[3] = (~length) & 255; block[4] = (~length) >>> 8; block.set(scanlines.subarray(offset, offset + length), 5); blocks.push(block); offset += length; } const adler = (() => { let a = 1; let b = 0; for (const value of scanlines) { a = (a + value) % 65521; b = (b + a) % 65521; } return ((b << 16) | a) >>> 0; })(); const zlib = concat(new Uint8Array([0x78, 0x01]), ...blocks, uint32(adler)); const header = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); const ihdr = concat(uint32(size), uint32(size), new Uint8Array([8, 6, 0, 0, 0])); return concat(header, chunk('IHDR', ihdr), chunk('IDAT', zlib), chunk('IEND', new Uint8Array())); } function headers(stateLength) { return { 'Content-Type': 'image/png', 'Content-Disposition': 'inline; filename="wordle.png"', 'Cache-Control': 'no-store', 'X-Wordle-State-Length': String(stateLength), 'X-Wordle-Renderer': 'png', }; } export async function onRequestGet({ request }) { const state = new URL(request.url).searchParams.get('state'); if (state === null || state.length === 0) { return new Response('Missing or invalid state', { status: 400, headers: headers(0) }); } let rows; try { rows = parseState(state); } catch (error) { return new Response('Missing or invalid state', { status: 400, headers: headers(state.length) }); } try { const pngBytes = makePng(rows); return new Response(pngBytes, { status: 200, headers: headers(state.length) }); } catch (error) { return new Response('Rendering failed', { status: 500, headers: headers(state.length) }); } }