34e54b9f78
Introduce a reusable helper that produces upload-ready blobs for adapters that share the same image-asset semantics (dongchedi, sohuhao, wangyihao, weixin-gzh, zol). Centralizes URL candidate expansion and passthrough mime detection so each adapter focuses on its own publish protocol. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
139 lines
3.6 KiB
TypeScript
139 lines
3.6 KiB
TypeScript
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<ImageAssetBlob | null> {
|
|
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<ImageAssetBlob | null> {
|
|
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",
|
|
};
|
|
}
|