refactor: consolidate PNG encoder and safeParseJson utilities (#12457)

- Create shared PNG encoder module (src/media/png-encode.ts)

- Refactor qr-image.ts and live-image-probe.ts to use shared encoder

- Add safeParseJson to utils.ts and plugin-sdk exports

- Update msteams and pairing-store to use centralized safeParseJson
This commit is contained in:
max
2026-02-09 00:21:54 -08:00
committed by GitHub
parent 5acb1e3c52
commit f0924d3c4e
9 changed files with 107 additions and 182 deletions

View File

@@ -1,6 +1,7 @@
import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { safeParseJson } from "openclaw/plugin-sdk";
import lockfile from "proper-lockfile";
const STORE_LOCK_OPTIONS = {
@@ -14,14 +15,6 @@ const STORE_LOCK_OPTIONS = {
stale: 30_000,
} as const;
function safeParseJson<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
export async function readJsonFile<T>(
filePath: string,
fallback: T,

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
Lists the longest and shortest code files in the project.
Lists the longest and shortest code files in the project, and counts duplicated function names across files. Useful for identifying potential refactoring targets and enforcing code size guidelines.
Threshold can be set to warn about files longer or shorter than a certain number of lines.
"""

View File

@@ -25,7 +25,6 @@ export function isContextOverflowError(errorMessage?: string): boolean {
lower.includes("exceeds model context window") ||
(hasRequestSizeExceeds && hasContextWindow) ||
lower.includes("context overflow:") ||
lower.includes("context overflow") ||
(lower.includes("413") && lower.includes("too large"))
);
}

View File

@@ -1,88 +1,4 @@
import { deflateSync } from "node:zlib";
const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i += 1) {
let c = i;
for (let k = 0; k < 8; k += 1) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[i] = c >>> 0;
}
return table;
})();
function crc32(buf: Buffer) {
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i += 1) {
crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function pngChunk(type: string, data: Buffer) {
const typeBuf = Buffer.from(type, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const crc = crc32(Buffer.concat([typeBuf, data]));
const crcBuf = Buffer.alloc(4);
crcBuf.writeUInt32BE(crc, 0);
return Buffer.concat([len, typeBuf, data, crcBuf]);
}
function encodePngRgba(buffer: Buffer, width: number, height: number) {
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let row = 0; row < height; row += 1) {
const rawOffset = row * (stride + 1);
raw[rawOffset] = 0; // filter: none
buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride);
}
const compressed = deflateSync(raw);
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
return Buffer.concat([
signature,
pngChunk("IHDR", ihdr),
pngChunk("IDAT", compressed),
pngChunk("IEND", Buffer.alloc(0)),
]);
}
function fillPixel(
buf: Buffer,
x: number,
y: number,
width: number,
r: number,
g: number,
b: number,
a = 255,
) {
if (x < 0 || y < 0) {
return;
}
if (x >= width) {
return;
}
const idx = (y * width + x) * 4;
if (idx < 0 || idx + 3 >= buf.length) {
return;
}
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = a;
}
import { encodePngRgba, fillPixel } from "../media/png-encode.js";
const GLYPH_ROWS_5X7: Record<string, number[]> = {
"0": [0b01110, 0b10001, 0b10011, 0b10101, 0b11001, 0b10001, 0b01110],

90
src/media/png-encode.ts Normal file
View File

@@ -0,0 +1,90 @@
/**
* Minimal PNG encoder for generating simple RGBA images without native dependencies.
* Used for QR codes, live probes, and other programmatic image generation.
*/
import { deflateSync } from "node:zlib";
const CRC_TABLE = (() => {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i += 1) {
let c = i;
for (let k = 0; k < 8; k += 1) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[i] = c >>> 0;
}
return table;
})();
/** Compute CRC32 checksum for a buffer (used in PNG chunk encoding). */
export function crc32(buf: Buffer): number {
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i += 1) {
crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
/** Create a PNG chunk with type, data, and CRC. */
export function pngChunk(type: string, data: Buffer): Buffer {
const typeBuf = Buffer.from(type, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const crc = crc32(Buffer.concat([typeBuf, data]));
const crcBuf = Buffer.alloc(4);
crcBuf.writeUInt32BE(crc, 0);
return Buffer.concat([len, typeBuf, data, crcBuf]);
}
/** Write a pixel to an RGBA buffer. Ignores out-of-bounds writes. */
export function fillPixel(
buf: Buffer,
x: number,
y: number,
width: number,
r: number,
g: number,
b: number,
a = 255,
): void {
if (x < 0 || y < 0 || x >= width) {
return;
}
const idx = (y * width + x) * 4;
if (idx < 0 || idx + 3 >= buf.length) {
return;
}
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = a;
}
/** Encode an RGBA buffer as a PNG image. */
export function encodePngRgba(buffer: Buffer, width: number, height: number): Buffer {
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let row = 0; row < height; row += 1) {
const rawOffset = row * (stride + 1);
raw[rawOffset] = 0; // filter: none
buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride);
}
const compressed = deflateSync(raw);
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
return Buffer.concat([
signature,
pngChunk("IHDR", ihdr),
pngChunk("IDAT", compressed),
pngChunk("IEND", Buffer.alloc(0)),
]);
}

View File

@@ -7,6 +7,7 @@ import type { ChannelId, ChannelPairingAdapter } from "../channels/plugins/types
import { getPairingAdapter } from "../channels/plugins/pairing.js";
import { resolveOAuthDir, resolveStateDir } from "../config/paths.js";
import { resolveRequiredHomeDir } from "../infra/home-dir.js";
import { safeParseJson } from "../utils.js";
const PAIRING_CODE_LENGTH = 8;
const PAIRING_CODE_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
@@ -72,14 +73,6 @@ function resolveAllowFromPath(
return path.join(resolveCredentialsDir(env), `${safeChannelKey(channel)}-allowFrom.json`);
}
function safeParseJson<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
async function readJsonFile<T>(
filePath: string,
fallback: T,

View File

@@ -229,7 +229,7 @@ export {
} from "../agents/tools/common.js";
export { formatDocsLink } from "../terminal/links.js";
export type { HookEntry } from "../hooks/types.js";
export { clamp, escapeRegExp, normalizeE164, sleep } from "../utils.js";
export { clamp, escapeRegExp, normalizeE164, safeParseJson, sleep } from "../utils.js";
export { stripAnsi } from "../terminal/ansi.js";
export { missingTargetError } from "../infra/outbound/target-errors.js";
export { registerLogTransport } from "../logging/logger.js";

View File

@@ -31,6 +31,17 @@ export function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
/**
* Safely parse JSON, returning null on error instead of throwing.
*/
export function safeParseJson<T>(raw: string): T | null {
try {
return JSON.parse(raw) as T;
} catch {
return null;
}
}
export type WebChannel = "web";
export function assertWebChannel(input: string): asserts input is WebChannel {

View File

@@ -1,6 +1,6 @@
import { deflateSync } from "node:zlib";
import QRCodeModule from "qrcode-terminal/vendor/QRCode/index.js";
import QRErrorCorrectLevelModule from "qrcode-terminal/vendor/QRCode/QRErrorCorrectLevel.js";
import { encodePngRgba, fillPixel } from "../media/png-encode.js";
type QRCodeConstructor = new (
typeNumber: number,
@@ -22,83 +22,6 @@ function createQrMatrix(input: string) {
return qr;
}
function fillPixel(
buf: Buffer,
x: number,
y: number,
width: number,
r: number,
g: number,
b: number,
a = 255,
) {
const idx = (y * width + x) * 4;
buf[idx] = r;
buf[idx + 1] = g;
buf[idx + 2] = b;
buf[idx + 3] = a;
}
function crcTable() {
const table = new Uint32Array(256);
for (let i = 0; i < 256; i += 1) {
let c = i;
for (let k = 0; k < 8; k += 1) {
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
}
table[i] = c >>> 0;
}
return table;
}
const CRC_TABLE = crcTable();
function crc32(buf: Buffer) {
let crc = 0xffffffff;
for (let i = 0; i < buf.length; i += 1) {
crc = CRC_TABLE[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8);
}
return (crc ^ 0xffffffff) >>> 0;
}
function pngChunk(type: string, data: Buffer) {
const typeBuf = Buffer.from(type, "ascii");
const len = Buffer.alloc(4);
len.writeUInt32BE(data.length, 0);
const crc = crc32(Buffer.concat([typeBuf, data]));
const crcBuf = Buffer.alloc(4);
crcBuf.writeUInt32BE(crc, 0);
return Buffer.concat([len, typeBuf, data, crcBuf]);
}
function encodePngRgba(buffer: Buffer, width: number, height: number) {
const stride = width * 4;
const raw = Buffer.alloc((stride + 1) * height);
for (let row = 0; row < height; row += 1) {
const rawOffset = row * (stride + 1);
raw[rawOffset] = 0; // filter: none
buffer.copy(raw, rawOffset + 1, row * stride, row * stride + stride);
}
const compressed = deflateSync(raw);
const signature = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
const ihdr = Buffer.alloc(13);
ihdr.writeUInt32BE(width, 0);
ihdr.writeUInt32BE(height, 4);
ihdr[8] = 8; // bit depth
ihdr[9] = 6; // color type RGBA
ihdr[10] = 0; // compression
ihdr[11] = 0; // filter
ihdr[12] = 0; // interlace
return Buffer.concat([
signature,
pngChunk("IHDR", ihdr),
pngChunk("IDAT", compressed),
pngChunk("IEND", Buffer.alloc(0)),
]);
}
export async function renderQrPngBase64(
input: string,
opts: { scale?: number; marginModules?: number } = {},