u
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user