Files
oh-my-pi/packages/coding-agent/src/utils/image-loading.ts
T
can1357 af07faec11 feat: Bun 1.3.14
- Raised the Bun minimum version to >=1.3.14 across package metadata, install scripts, and changelog notes.
- Removed the Photon native image pipeline and added SIXEL-based `sixel` support in pi-natives.
- Migrated coding-agent image handling and resizing to `Bun.Image`, including updated tests and a JPEG quality bump to 80.
- Added HTTP/2 fetch bootstrap with HTTPS-only fallback and updated Bun build flags for autoload suppression/`--keep-names`.
2026-05-13 13:21:59 +02:00

103 lines
3.0 KiB
TypeScript

import * as fs from "node:fs/promises";
import type { ImageContent } from "@oh-my-pi/pi-ai";
import { formatBytes, readImageMetadata, SUPPORTED_IMAGE_MIME_TYPES } from "@oh-my-pi/pi-utils";
import { resolveReadPath } from "../tools/path-utils";
import { formatDimensionNote, resizeImage } from "./image-resize";
export const MAX_IMAGE_INPUT_BYTES = 20 * 1024 * 1024;
export const SUPPORTED_INPUT_IMAGE_MIME_TYPES = SUPPORTED_IMAGE_MIME_TYPES;
export interface LoadImageInputOptions {
path: string;
cwd: string;
autoResize: boolean;
maxBytes?: number;
resolvedPath?: string;
detectedMimeType?: string;
}
export interface LoadedImageInput {
resolvedPath: string;
mimeType: string;
data: string;
textNote: string;
dimensionNote?: string;
bytes: number;
}
export class ImageInputTooLargeError extends Error {
readonly bytes: number;
readonly maxBytes: number;
constructor(bytes: number, maxBytes: number) {
super(`Image file too large: ${formatBytes(bytes)} exceeds ${formatBytes(maxBytes)} limit.`);
this.name = "ImageInputTooLargeError";
this.bytes = bytes;
this.maxBytes = maxBytes;
}
}
export async function ensureSupportedImageInput(image: ImageContent): Promise<ImageContent | null> {
if (SUPPORTED_INPUT_IMAGE_MIME_TYPES.has(image.mimeType)) {
return image;
}
try {
const bytes = Buffer.from(image.data, "base64");
const data = await new Bun.Image(bytes).png().toBase64();
return { type: "image", data, mimeType: "image/png" };
} catch {
return null;
}
}
export async function loadImageInput(options: LoadImageInputOptions): Promise<LoadedImageInput | null> {
const maxBytes = options.maxBytes ?? MAX_IMAGE_INPUT_BYTES;
const resolvedPath = options.resolvedPath ?? resolveReadPath(options.path, options.cwd);
const metadata = options.detectedMimeType
? { mimeType: options.detectedMimeType }
: await readImageMetadata(resolvedPath);
const mimeType = metadata?.mimeType;
if (!mimeType) return null;
const stat = await Bun.file(resolvedPath).stat();
if (stat.size > maxBytes) {
throw new ImageInputTooLargeError(stat.size, maxBytes);
}
const inputBuffer = await fs.readFile(resolvedPath);
if (inputBuffer.byteLength > maxBytes) {
throw new ImageInputTooLargeError(inputBuffer.byteLength, maxBytes);
}
let outputData = Buffer.from(inputBuffer).toBase64();
let outputMimeType = mimeType;
let outputBytes = inputBuffer.byteLength;
let dimensionNote: string | undefined;
if (options.autoResize) {
try {
const resized = await resizeImage({ type: "image", data: outputData, mimeType });
outputData = resized.data;
outputMimeType = resized.mimeType;
outputBytes = resized.buffer.byteLength;
dimensionNote = formatDimensionNote(resized);
} catch {
// keep original image when resize fails
}
}
let textNote = `Read image file [${outputMimeType}]`;
if (dimensionNote) {
textNote += `\n${dimensionNote}`;
}
return {
resolvedPath,
mimeType: outputMimeType,
data: outputData,
textNote,
dimensionNote,
bytes: outputBytes,
};
}