u
This commit is contained in:
@@ -966,6 +966,40 @@ export const serper = {
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* POST /api/serper/image
|
||||
* Crawl image from URL, crop white/transparency margins, resize to max 1000px, return AVIF Blob.
|
||||
*
|
||||
* @param {string} url - External image URL to crawl and process
|
||||
* @param {object} [options]
|
||||
* @param {AbortSignal} [options.signal] - Abort controller signal
|
||||
* @returns {Promise<Blob>} AVIF image blob
|
||||
*/
|
||||
async fetchImage(url, { signal } = {}) {
|
||||
const res = await fetch('/api/serper/image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ url }),
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
let message = res.statusText;
|
||||
try {
|
||||
const data = await res.json();
|
||||
message = data.error || data.message || message;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
const err = new Error(message || `Request failed (${res.status})`);
|
||||
err.status = res.status;
|
||||
throw err;
|
||||
}
|
||||
return await res.blob();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -31,72 +31,6 @@ import { articleWithUploadedPhoto } from '../../utils/articleDetail';
|
||||
import { prepareArticlePhoto } from '../../utils/prepareArticlePhoto';
|
||||
import { requireObject } from '../../utils/apiShape';
|
||||
|
||||
/**
|
||||
* Downloads image as a Blob, trying full resolution with CORS first,
|
||||
* falling back to canvas crossOrigin draw, and finally the Serper SSE dataUrl.
|
||||
* @param {string} url
|
||||
* @param {string|null} fallbackDataUrl
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
async function downloadImageBlob(url, fallbackDataUrl) {
|
||||
// Strategy 1: Direct fetch with CORS
|
||||
if (url) {
|
||||
try {
|
||||
const resp = await fetch(url, { mode: 'cors' });
|
||||
if (resp.ok) {
|
||||
const blob = await resp.blob();
|
||||
if (blob && blob.type.startsWith('image/')) {
|
||||
return blob;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// CORS or network failure
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 2: Attempt loading with HTMLImageElement crossOrigin
|
||||
if (url) {
|
||||
try {
|
||||
const blob = await new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.crossOrigin = 'anonymous';
|
||||
img.onload = () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = img.naturalWidth || img.width;
|
||||
canvas.height = img.naturalHeight || img.height;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
reject(new Error('Canvas 2D-Kontext nicht verfügbar'));
|
||||
return;
|
||||
}
|
||||
ctx.drawImage(img, 0, 0);
|
||||
canvas.toBlob((b) => {
|
||||
if (b) resolve(b);
|
||||
else reject(new Error('Canvas toBlob fehlgeschlagen'));
|
||||
}, 'image/png');
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
img.onerror = reject;
|
||||
img.src = url;
|
||||
});
|
||||
if (blob) return blob;
|
||||
} catch {
|
||||
// Canvas tainted or image load failed
|
||||
}
|
||||
}
|
||||
|
||||
// Strategy 3: Streamed WebP/JPEG dataUrl from Serper SSE
|
||||
if (fallbackDataUrl) {
|
||||
const resp = await fetch(fallbackDataUrl);
|
||||
const blob = await resp.blob();
|
||||
if (blob) return blob;
|
||||
}
|
||||
|
||||
throw new Error('Das Bild konnte nicht heruntergeladen werden (CORS- oder Netzwerkfehler).');
|
||||
}
|
||||
|
||||
function computeDefaultQuery(article) {
|
||||
if (!article) return '';
|
||||
@@ -277,19 +211,29 @@ export default class ArticleImageSearchDialog extends PureComponent {
|
||||
|
||||
this.setState({
|
||||
processingId: img.id,
|
||||
processingStep: 'Herunterladen...',
|
||||
processingStep: 'Lade & optimiere Originalbild...',
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. Download image as Blob
|
||||
const blob = await downloadImageBlob(img.url, img.dataUrl);
|
||||
// 1. Crawl image via backend POST /api/serper/image (bypasses CORS, crops margins, returns max 1000px AVIF)
|
||||
let avifBlob;
|
||||
try {
|
||||
avifBlob = await serper.fetchImage(img.url);
|
||||
} catch (crawlErr) {
|
||||
console.warn('Backend /api/serper/image crawl failed, trying thumbnail fallback:', crawlErr);
|
||||
if (img.dataUrl) {
|
||||
this.setState({ processingStep: 'Fallback: Konvertiere Vorschau...' });
|
||||
const resp = await fetch(img.dataUrl);
|
||||
const thumbBlob = await resp.blob();
|
||||
avifBlob = await prepareArticlePhoto(thumbBlob);
|
||||
} else {
|
||||
throw crawlErr;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Prepare article photo (scale to max 1000px, encode to AVIF)
|
||||
this.setState({ processingStep: 'In AVIF konvertieren...' });
|
||||
const file = await prepareArticlePhoto(blob);
|
||||
|
||||
// 3. Upload to backend
|
||||
// 2. Upload AVIF to article
|
||||
this.setState({ processingStep: 'Bild wird gespeichert...' });
|
||||
const file = new File([avifBlob], 'article-photo.avif', { type: 'image/avif' });
|
||||
const res = await this.context.article.addImage(articleData.sku, file);
|
||||
requireObject(res, 'POST /api/article/:sku/image');
|
||||
requireObject(res.image, 'image');
|
||||
|
||||
@@ -310,6 +310,25 @@ export async function encodeCanvasToAvif(canvas, filename = 'brandpic.avif') {
|
||||
* @returns {Promise<File>}
|
||||
*/
|
||||
export async function downloadAndPrepareLogoAvif(url, fallbackDataUrl) {
|
||||
if (url) {
|
||||
try {
|
||||
const resp = await fetch('/api/serper/image', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify({ url }),
|
||||
});
|
||||
if (resp.ok) {
|
||||
const avifBlob = await resp.blob();
|
||||
if (avifBlob && avifBlob.size > 0) {
|
||||
return new File([avifBlob], 'brandpic.avif', { type: 'image/avif' });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Fallback to client-side processing if /api/serper/image fails
|
||||
}
|
||||
}
|
||||
|
||||
let img;
|
||||
try {
|
||||
img = await downloadLogoImage(url, fallbackDataUrl);
|
||||
|
||||
Reference in New Issue
Block a user