Page Menu
Home
Sealhub
Search
Configure Global Search
Log In
Files
F9583955
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
11 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/src/format-specific-options.ts b/src/format-specific-options.ts
index 9d4ecc8..f76dfe5 100644
--- a/src/format-specific-options.ts
+++ b/src/format-specific-options.ts
@@ -1,7 +1,9 @@
import sharp from "sharp";
export const format_specific_options = (<const>{
avif: (image) => image.avif({ quality: 80 }),
jpeg: (image) => image.jpeg({ mozjpeg: true, quality: 90 }),
- webp: (image) => image.webp({ quality: 90 }),
+ webp: (image) => {
+ return image.webp({ quality: 90 });
+ },
}) as Record<string, ((image: sharp.Sharp) => sharp.Sharp) | undefined>;
diff --git a/src/utils/cache/CacheManager.ts b/src/utils/cache/CacheManager.ts
index 438db87..d4d4f0d 100644
--- a/src/utils/cache/CacheManager.ts
+++ b/src/utils/cache/CacheManager.ts
@@ -1,291 +1,296 @@
import Queue from "better-queue";
import _locreq from "locreq";
export const locreq = _locreq(__dirname);
import hash from "object-hash";
import { ThumbnailCache } from "./ThumbnailCache";
import { SmartcropCache } from "./SmartCropCache";
import { DiskImageCache } from "./DiskImageCache";
import {
FilruParameters,
Task,
ThumbnailCacheParams,
} from "../../types/cacheManager";
import { correctExtension } from "../../types/imageRouter";
import {
CropResult,
ImageMap,
SmartcropMap,
SmartcropTask,
} from "./../../types/cacheManager";
import { isCorrectExtension } from "../utils";
import { ImageInfoTool } from "../ImageInfoTool";
import { applyCrop, getSmartCropResult } from "../smartCropImage";
import sharp from "sharp";
+import { format_specific_options } from "../../format-specific-options";
export class CacheManager {
private memoryCache: ThumbnailCache;
private diskImageCache: DiskImageCache;
private smartcropCache: SmartcropCache;
private imageQueue: Queue;
private smartcropQueue: Queue;
private enqueuedImagePromises: ImageMap = new Map();
private enqueuedSmartcropPromises: SmartcropMap = new Map();
private tmpPath = locreq.resolve("tmp-samrtcrop");
constructor(
thumbnailCacheParams: ThumbnailCacheParams,
localCacheParams: FilruParameters,
SmartcropCacheParams: FilruParameters,
maxImagesConcurrent: number,
public resolutionThreshold: number
) {
this.memoryCache = new ThumbnailCache(thumbnailCacheParams);
this.diskImageCache = new DiskImageCache(localCacheParams);
this.smartcropCache = new SmartcropCache(SmartcropCacheParams);
this.imageQueue = new Queue(
(task, cb) => {
void this.processImage(task, cb);
},
{
concurrent: maxImagesConcurrent,
}
);
this.smartcropQueue = new Queue(
(task, cb) => {
void this.processSmartCrop(task, cb);
},
{
concurrent: maxImagesConcurrent,
}
);
}
public async start(): Promise<void> {
await this.diskImageCache.start();
await this.smartcropCache.start();
}
public cachedGetProcessedImage(task: Task): Promise<Buffer> {
return this.enqueueProcessing<Buffer>(
this.enqueuedImagePromises,
this.imageQueue,
task
);
}
public cachedGetSmarctopAnalysisResult(
task: SmartcropTask
): Promise<CropResult> {
return this.enqueueProcessing<CropResult>(
this.enqueuedSmartcropPromises,
this.smartcropQueue,
task
);
}
public isInCache(task: Task): string | null {
const hash = this.getTaskHash(task);
const imagePromiseBuffer = this.get(hash, task.resolution);
if (imagePromiseBuffer) {
return imagePromiseBuffer.toString("base64");
}
return null;
}
private async set(
hash: string,
resolution: number,
buffer: Buffer
): Promise<void> {
if (resolution > this.resolutionThreshold) {
await this.diskImageCache.set(hash, buffer);
} else if (resolution <= this.resolutionThreshold && resolution >= 0) {
this.memoryCache.set(hash, buffer);
}
}
private get(
hash: string,
resolution: number
): Promise<Buffer> | Buffer | null {
if (resolution > this.resolutionThreshold) {
return this.diskImageCache.get(hash);
} else if (resolution <= this.resolutionThreshold && resolution >= 0) {
return this.memoryCache.get(hash);
}
return null;
}
private async enqueueProcessing<Buffer>(
enqueuedPromises: ImageMap,
queue: Queue,
data: Task
): Promise<Buffer>;
private async enqueueProcessing<CropResult>(
enqueuedPromises: SmartcropMap,
queue: Queue,
data: SmartcropTask
): Promise<CropResult>;
private async enqueueProcessing<T extends Buffer | CropResult>(
enqueuedPromises: Map<string, Promise<T>>,
queue: Queue,
data: Task | SmartcropTask
): Promise<T> {
const hash = this.getTaskHash(data);
if (this.isInImageQueue(enqueuedPromises, hash)) {
return enqueuedPromises.get(hash) as Promise<T>;
} else {
if (this.isTask(data)) {
const buffer = this.get(hash, data.resolution);
if ((await buffer) !== null) {
return buffer as Promise<T>;
}
} else {
const cropResult = this.smartcropCache.get(hash);
if ((await cropResult) !== null) {
return cropResult as Promise<T>;
}
}
}
const promise = new Promise<T>((resolve, reject) => {
queue.push(
data,
(error: Error | null, result: Buffer | CropResult | null) => {
if (error) {
reject(error);
} else if (result === null) {
reject(
new Error(
"Image processing resulted in null buffer."
)
);
} else {
resolve(result as T);
}
enqueuedPromises.delete(hash);
}
);
});
enqueuedPromises.set(hash, promise);
return promise;
}
private isInImageQueue(
enqueuedPromises: Map<string, Promise<unknown>>,
hash: string
): boolean {
return enqueuedPromises.has(hash);
}
private async processImage(
task: Task,
cb: (arg1: Error | null, arg2: Buffer | null) => void
): Promise<void> {
try {
const { hash, resolution, fileExtension, cropData } = task;
if (
isCorrectExtension(fileExtension) &&
this.chcekResolution(hash, resolution)
) {
try {
if (!cropData) {
const imageBuffer = await this.generateImage(
hash,
resolution,
fileExtension
);
const taskHash = this.getTaskHash(task);
await this.set(taskHash, resolution, imageBuffer);
cb(null, imageBuffer);
return;
} else {
const imageBuffer = await applyCrop(
this,
hash,
this.tmpPath,
resolution,
cropData,
fileExtension
);
const taskHash = this.getTaskHash(task);
await this.set(taskHash, resolution, imageBuffer);
cb(null, imageBuffer);
}
} catch (err) {
console.log("err form catach index: ", err);
cb(err, null);
}
} else {
cb(new Error(`Invalid image type: ${fileExtension}`), null);
}
} catch (error) {
cb(error, null);
}
}
private cropResultToBuffer(cropResult: CropResult): Buffer {
const cropResultJSON = JSON.stringify(cropResult);
const cropResultBuffer = Buffer.from(cropResultJSON, "utf-8");
return cropResultBuffer;
}
private async processSmartCrop(
task: SmartcropTask,
cb: (arg1: Error | null, arg2: CropResult | null) => void
): Promise<void> {
try {
const { hash, cropData } = task;
const taskHash = this.getTaskHash(task);
const cropResult = await getSmartCropResult(hash, cropData);
const cropResultBuffer = this.cropResultToBuffer(cropResult);
await this.smartcropCache.set(taskHash, cropResultBuffer);
cb(null, cropResult);
} catch (error) {
cb(error, null);
}
}
private async generateImage(
hash: string,
resolution: number,
fileExtension: correctExtension
): Promise<Buffer> {
const imageData = ImageInfoTool.getImageData(hash);
const { originalPath, lossless } = imageData;
- return await sharp(originalPath)
+ let image = sharp(originalPath)
.resize(resolution)
- .toFormat(fileExtension, lossless ? { lossless: true } : {})
- .toBuffer();
+ .toFormat(fileExtension, lossless ? { lossless: true } : {});
+ const apply_options = format_specific_options[fileExtension];
+ if (apply_options) {
+ image = apply_options(image);
+ }
+ return image.toBuffer();
}
private chcekResolution(hash: string, resolution: number) {
return !!ImageInfoTool.getImageData(hash).resolutions?.includes(
resolution
);
}
private getTaskHash(task: Task | SmartcropTask): string {
return hash(task);
}
private isTask(data: Task | SmartcropTask): data is Task {
return (data as Task).resolution !== undefined;
}
}
diff --git a/src/utils/smartCropImage.ts b/src/utils/smartCropImage.ts
index e98c8d5..9f51d71 100644
--- a/src/utils/smartCropImage.ts
+++ b/src/utils/smartCropImage.ts
@@ -1,102 +1,106 @@
import { CropResult } from "./../types/cacheManager";
import sharp from "sharp";
import smartcrop from "smartcrop-sharp";
import { Buffer } from "buffer";
import fs from "fs/promises";
import { randomBytes } from "crypto";
import { CacheManager } from "./cache/CacheManager";
import { ImageInfoTool } from "./ImageInfoTool";
import { SmartCropOptions, DirectCropOptions } from "../types/smartCropImage";
import { CropDescription } from "../types/imageRouter";
import { format_specific_options } from "../format-specific-options";
function isSmartCropOptions(
value: SmartCropOptions | DirectCropOptions
): value is SmartCropOptions {
return (
(value as DirectCropOptions).x === undefined &&
(value as DirectCropOptions).y === undefined
);
}
function isDirectCropOptions(
value: SmartCropOptions | DirectCropOptions
): value is DirectCropOptions {
return (
(value as DirectCropOptions).x !== undefined &&
(value as DirectCropOptions).y !== undefined
);
}
async function getSmartCropResult(
hash: string,
options: SmartCropOptions | DirectCropOptions
): Promise<CropResult> {
const src = ImageInfoTool.getImageData(hash).originalPath;
const result = await smartcrop.crop(src, {
width: options.width,
height: options.height,
});
return result;
}
async function applyCrop(
context: CacheManager,
hash: string,
tmp_path: string,
resolution: number,
options: Exclude<CropDescription, false>,
fileExtension: string
): Promise<Buffer> {
const src = ImageInfoTool.getImageData(hash).originalPath;
if (isDirectCropOptions(options)) {
const { width, height, x, y } = options;
- const croppedImageBuffer = await sharp(src)
+ let image = sharp(src)
.extract({ left: x, top: y, width, height })
- .resize({ width: resolution })
- .toBuffer();
+ .resize({ width: resolution });
+ const apply_options = format_specific_options[fileExtension];
+ if (apply_options) {
+ image = apply_options(image);
+ }
+ const croppedImageBuffer = image.toBuffer();
return croppedImageBuffer;
} else if (isSmartCropOptions(options)) {
const cropResult = await context.cachedGetSmarctopAnalysisResult({
hash: hash,
cropData: options,
});
const randomBytesString = randomBytes(16).toString("hex");
const tempDest = `${tmp_path}.${randomBytesString}.cropped.${fileExtension}`;
let image = sharp(src)
.extract({
left: cropResult.topCrop.x,
top: cropResult.topCrop.y,
width: cropResult.topCrop.width,
height: cropResult.topCrop.height,
})
.resize({ width: resolution });
const apply_options = format_specific_options[fileExtension];
if (apply_options) {
image = apply_options(image);
}
await image.toFile(tempDest);
const croppedImageBuffer = await fs.readFile(tempDest);
await fs.unlink(tempDest);
return croppedImageBuffer;
} else {
throw new Error("Invalid cropping options provided");
}
}
export {
applyCrop,
SmartCropOptions,
DirectCropOptions,
isDirectCropOptions,
isSmartCropOptions,
getSmartCropResult,
};
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Oct 11, 11:13 (12 h, 45 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
984275
Default Alt Text
(11 KB)
Attached To
Mode
rRIMAGEROUTER koa-responsive-image-router
Attached
Detach File
Event Timeline
Log In to Comment