import { Buffer } from "node:buffer"; import { nativeImage } from "electron"; import { resolveDesktopApiURL } from "../transport/api-client"; export interface ImageAssetBlob { blob: Blob; width: number; height: number; fileName: string; } export interface CropRect { x: number; y: number; width: number; height: number; } const passthroughImageTypes = new Set(["image/png", "image/jpeg", "image/jpg", "image/gif"]); export function buildAssetURLCandidates(sourceUrl: string): string[] { const trimmed = sourceUrl.trim(); if (!trimmed) { return []; } if (/^(data|blob):/i.test(trimmed)) { return [trimmed]; } const candidates: string[] = []; const pushCandidate = (value: string) => { if (!candidates.includes(value)) { candidates.push(value); } }; try { const parsed = new URL(trimmed, resolveDesktopApiURL("/")); if (parsed.pathname.startsWith("/api/")) { const apiAssetUrl = new URL(resolveDesktopApiURL(`${parsed.pathname}${parsed.search}`)); if (apiAssetUrl.pathname.startsWith("/api/public/assets/")) { apiAssetUrl.searchParams.set("format", "png"); } pushCandidate(apiAssetUrl.toString()); } pushCandidate(parsed.toString()); } catch { pushCandidate(trimmed); } return candidates; } export async function fetchImageAssetBlob(sourceUrl: string): Promise { for (const candidate of buildAssetURLCandidates(sourceUrl)) { const response = await fetch(candidate).catch(() => null); if (!response?.ok) { continue; } const sourceBlob = await response.blob().catch(() => null); if (!sourceBlob || !sourceBlob.type.startsWith("image/")) { continue; } const image = nativeImage.createFromBuffer(Buffer.from(await sourceBlob.arrayBuffer())); if (image.isEmpty()) { continue; } const size = image.getSize(); const sourceType = sourceBlob.type.toLowerCase(); if (passthroughImageTypes.has(sourceType)) { return { blob: sourceBlob, width: size.width, height: size.height, fileName: sourceType === "image/gif" ? "image.gif" : sourceType === "image/jpeg" || sourceType === "image/jpg" ? "image.jpg" : "image.png", }; } return { blob: new Blob([new Uint8Array(image.toPNG())], { type: "image/png" }), width: size.width, height: size.height, fileName: "image.png", }; } return null; } export function getCenteredCropRect(width: number, height: number, ratio: number): CropRect { let cropWidth = width; let cropHeight = cropWidth / ratio; if (cropHeight > height) { cropHeight = height; cropWidth = cropHeight * ratio; } return { x: (width - cropWidth) / 2, y: (height - cropHeight) / 2, width: cropWidth, height: cropHeight, }; } export async function cropImageAssetBlob(source: ImageAssetBlob, ratio: number): Promise { const image = nativeImage.createFromBuffer(Buffer.from(await source.blob.arrayBuffer())); if (image.isEmpty()) { return null; } const crop = getCenteredCropRect(source.width, source.height, ratio); const cropped = image.crop({ x: Math.max(0, Math.round(crop.x)), y: Math.max(0, Math.round(crop.y)), width: Math.max(1, Math.round(crop.width)), height: Math.max(1, Math.round(crop.height)), }); if (cropped.isEmpty()) { return null; } const size = cropped.getSize(); return { blob: new Blob([new Uint8Array(cropped.toPNG())], { type: "image/png" }), width: size.width, height: size.height, fileName: "image.png", }; }