This commit is contained in:
seb
2026-09-27 18:01:08 +02:00
parent 9f97e3f3c6
commit 65b4ce5098
6 changed files with 516 additions and 1 deletions

View File

@@ -1,6 +1,7 @@
import sharp from 'sharp';
import * as dbCompanySetting from '../../db/companysetting.js';
import env from '../../config/env.js';
import { crawlAndProcessImage } from '../../services/imageCrop.js';
const CONCURRENCY = 6;
const FETCH_TIMEOUT_MS = 6000;
@@ -342,6 +343,113 @@ export function register(app, basePath = '') {
}
};
/**
* @openapi
* /serper/image:
* post:
* tags: [Serper]
* summary: Crawl image from URL, crop white/transparency borders, and return AVIF
* description: |
* Fetches the image at the provided `url`, crops surrounding white and
* transparent border margins, resizes to max 1000px, and returns the AVIF image.
* security:
* - cookieAuth: []
* x-markdown: serper/image.md
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* required: [url]
* properties:
* url:
* type: string
* description: Absolute HTTP/HTTPS URL of the image to crawl
* example: "https://example.com/logo.png"
* responses:
* 200:
* description: AVIF image bytes
* headers:
* Content-Type:
* schema:
* type: string
* example: image/avif
* X-Image-Width:
* schema:
* type: integer
* example: 540
* X-Image-Height:
* schema:
* type: integer
* example: 179
* content:
* image/avif:
* schema:
* type: string
* format: binary
* 400:
* description: URL missing, invalid input, or image processing error
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 401:
* description: Unauthorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 403:
* description: Not authorized
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
* 502:
* description: Bad Gateway (crawling the upstream image URL failed)
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/Error'
*/
const handleImageCrop = async (req, res) => {
const selected = requireSelectedCompany(req);
if (selected.error) {
return res.status(400).json({ error: selected.error });
}
const rawUrl = typeof req.body?.url === 'string' ? req.body.url.trim() : '';
if (!rawUrl) {
return res.status(400).json({ error: 'url is required' });
}
const abortController = new AbortController();
const onClose = () => abortController.abort();
req.on('close', onClose);
res.on('close', onClose);
try {
const result = await crawlAndProcessImage(rawUrl, {
signal: abortController.signal,
});
if (res.writableEnded || res.destroyed) return;
res.setHeader('Content-Type', 'image/avif');
res.setHeader('Content-Length', String(result.buffer.length));
res.setHeader('X-Image-Width', String(result.width));
res.setHeader('X-Image-Height', String(result.height));
return res.status(200).send(result.buffer);
} catch (err) {
if (abortController.signal.aborted || res.writableEnded || res.destroyed) return;
const message = err?.message || 'Failed to process image';
const isUpstream = message.startsWith('Failed to crawl image:');
const status = isUpstream ? 502 : 400;
return res.status(status).json({ error: message });
}
};
app.post(`${basePath}/serper/images`, handleImagesSearch);
app.post(`${basePath}/search/images`, handleImagesSearch);
app.post(`${basePath}/serper/image`, handleImageCrop);
}