Skip to content

Commit 7af306e

Browse files
committed
By default images are now prerendered to serve request much faster
1 parent 5ed9b7e commit 7af306e

26 files changed

+1763
-227
lines changed

ImageConverter.d.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/// <reference types="node" />
2+
import { ImageStorage } from "./ImageStorage";
3+
export type ImageInfo = {
4+
imageData: Buffer;
5+
format: string;
6+
mimeType: string;
7+
};
8+
export type PrerenderOptions = {
9+
/** Subdirectories to prerender (relative to srcDir) [Defaults to '.' to render all whole srcDir] */
10+
subDirs?: string | string[];
11+
/** Factor to scale images by (multiplier applied to width and height for each step) [Defaults to 1.5] */
12+
scaleFactor?: number;
13+
/** If subdirectories should be recursively prerendered too (Defaults to true) */
14+
recursive?: boolean;
15+
/** List of relative path prefixes starting inside srcDir that should not be optimized.
16+
* Subdirectories starting with underscore '_' won't be optimized.
17+
* Defaults to ["./favicons"].
18+
*/
19+
exlucePrefixes?: string[];
20+
/** If true, the start and end of the pre-rendering process will be logged (because takes up high hardware utilization).
21+
* Defaults to true.
22+
*/
23+
message?: boolean;
24+
};
25+
export type ImageConverterOptions = {
26+
/** Minimum pixel width of image to generate / cache.
27+
* Defaults to 32.
28+
*/
29+
minImageWidth?: number;
30+
/** Maxmimum pixel width of image to generate / cache.
31+
* Defaults to 4096 (4K).
32+
*/
33+
maxImageWidth?: number;
34+
};
35+
export declare class ImageConverter {
36+
readonly minImageWidth: number;
37+
readonly maxImageWidth: number;
38+
constructor(options?: ImageConverterOptions);
39+
/**
40+
* Loads an image from the given file path, resizes it to the given width and converts it to the given format.
41+
* @param filePath Path to image file.
42+
* @param width Width in pixels of available space for image.
43+
* @param format Format of returned image.
44+
* @returns Promise that resolves to an object containing the image data, format and mime type.
45+
*/
46+
convertImage(filePath: string, width: number, format: 'avif' | 'webp' | 'jpg' | 'png' | 'gif' | 'heif' | 'tiff' | 'tif'): Promise<ImageInfo>;
47+
private _collectImagesToConvert;
48+
private _prerender;
49+
/**
50+
* Pre-renders images in the given directory in all sizes and formats,
51+
* and stores them in the given storage.
52+
* @param storage Storage to store images in.
53+
* @param srcDir Path to directory that contains the images.
54+
* @param options Options for pre-rendering.
55+
*/
56+
prerender(storage: ImageStorage, srcDir: string, options?: PrerenderOptions): Promise<void>;
57+
}

ImageConverter.js

Lines changed: 288 additions & 0 deletions
Large diffs are not rendered by default.

ImageRequestHandler.d.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { NextFunction, Request, Response } from "express";
2+
import { ImageConverterOptions } from "./ImageConverter";
3+
export type ImageRequestHandlerOptions = ImageConverterOptions & {
4+
/** Path relative to project root where images are located (subdirectories starting with underscore '_' won't be optimized) */
5+
srcDir: string;
6+
/** URI prefix */
7+
uriPrefix: string;
8+
/** Factor to scale images by (multiplier applied to width and height for each step) [Defaults to 1.5] */
9+
scaleFactor?: number;
10+
/** Pulblic HTTP cache time in seconds.
11+
* Can be undefined or null to disable public HTTP caching.
12+
* Defaults to 3600 (1 hour).
13+
*/
14+
httpCacheTime?: number | null;
15+
/** Path to directory relative to project root where images should be cached.
16+
* Leave empty to disable caching.
17+
* Defaults to "./cache"
18+
*/
19+
cacheDir?: string;
20+
/** Maximum cache size in MB.
21+
* Defaults to 1000 MB.
22+
*/
23+
cacheSize?: number;
24+
/** If true, images will be pre-rendered on startup
25+
* which can take a while and uses more storage but will speed up requests.
26+
* (subdirectories starting with underscore '_' won't be optimized)
27+
* Defaults to true.
28+
*/
29+
prerender?: boolean;
30+
/** Path relative to project root where pre-rendered images should be stored.
31+
* If empty, images will be pre-rendered in place where original image is locaded (uses a subdirectory per original image).
32+
* Defaults to "./.prerendered"
33+
*/
34+
prerenderOutputDir?: string;
35+
/** If true, the start and end of the pre-rendering process will be logged (because takes up high hardware utilization).
36+
* Defaults to true.
37+
*/
38+
prerenderMessage?: boolean;
39+
/** List of path prefixes starting inside srcDir that should not be optimized.
40+
* Subdirectories starting with underscore '_' won't be optimized.
41+
* Defaults to ["./favicons"].
42+
*/
43+
exludePrefix?: string[];
44+
};
45+
/**
46+
* Returns a request handler (req, res, next) => void that will serve optimized images.
47+
* It supports different search parameters:
48+
* - w: width in pixels of available space for image (default: 1024)
49+
* - f: format of returned image (jpg, png, webp, etc.)
50+
* @param options Options for image optimization
51+
* @returns Request handler that can be passed e.g. to express
52+
*/
53+
export declare function ImageRequestHandler(options: ImageRequestHandlerOptions): (req: Request, res: Response, next: NextFunction) => Promise<void>;

ImageRequestHandler.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"use strict";
2+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4+
return new (P || (P = Promise))(function (resolve, reject) {
5+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8+
step((generator = generator.apply(thisArg, _arguments || [])).next());
9+
});
10+
};
11+
var __generator = (this && this.__generator) || function (thisArg, body) {
12+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
13+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
14+
function verb(n) { return function (v) { return step([n, v]); }; }
15+
function step(op) {
16+
if (f) throw new TypeError("Generator is already executing.");
17+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
18+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
19+
if (y = 0, t) op = [op[0] & 2, t.value];
20+
switch (op[0]) {
21+
case 0: case 1: t = op; break;
22+
case 4: _.label++; return { value: op[1], done: false };
23+
case 5: _.label++; y = op[1]; op = [0]; continue;
24+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
25+
default:
26+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
27+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
28+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
29+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
30+
if (t[2]) _.ops.pop();
31+
_.trys.pop(); continue;
32+
}
33+
op = body.call(thisArg, _);
34+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
35+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
36+
}
37+
};
38+
var __importDefault = (this && this.__importDefault) || function (mod) {
39+
return (mod && mod.__esModule) ? mod : { "default": mod };
40+
};
41+
Object.defineProperty(exports, "__esModule", { value: true });
42+
exports.ImageRequestHandler = void 0;
43+
var path_1 = __importDefault(require("path"));
44+
var lup_root_1 = require("lup-root");
45+
var ImageConverter_1 = require("./ImageConverter");
46+
var index_1 = require("./index");
47+
var ImageStorage_1 = require("./ImageStorage");
48+
/**
49+
* Returns a request handler (req, res, next) => void that will serve optimized images.
50+
* It supports different search parameters:
51+
* - w: width in pixels of available space for image (default: 1024)
52+
* - f: format of returned image (jpg, png, webp, etc.)
53+
* @param options Options for image optimization
54+
* @returns Request handler that can be passed e.g. to express
55+
*/
56+
function ImageRequestHandler(options) {
57+
var _this = this;
58+
var scaleFactor = options.scaleFactor || index_1.OptimizerSettings.DEFAULT_SCALE_FACTOR;
59+
scaleFactor = scaleFactor > 1.0 ? scaleFactor : ((scaleFactor == 1.0 || scaleFactor == 0.0) ? 1.5 : 1.0 / scaleFactor); // same as in OptimizedImage
60+
var httpCacheSec = options.httpCacheTime != null ? options.httpCacheTime || index_1.OptimizerSettings.DEFAULT_HTTP_CACHE_SEC : null;
61+
var optimizer = new ImageConverter_1.ImageConverter(options);
62+
// storage for caching images
63+
var cacheStorage = options.cacheDir !== '' ? new ImageStorage_1.ImageCacheStorage({
64+
cacheDir: options.cacheDir,
65+
cacheSize: options.cacheSize,
66+
}) : null;
67+
// storage for pre-rendered images
68+
var prerenderStorage = !options.prerender ? null : (options.prerenderOutputDir !== '' ? new ImageStorage_1.ImageDirectoryStorage({
69+
dirPath: options.prerenderOutputDir || index_1.OptimizerSettings.DEFAULT_PREDENDER_OUTPUT_DIRECTORY,
70+
}) : new ImageStorage_1.ImageInPlaceStorage());
71+
// prerender images
72+
if (prerenderStorage) {
73+
optimizer.prerender(prerenderStorage, options.srcDir, {
74+
recursive: options.prerender,
75+
scaleFactor: scaleFactor,
76+
message: options.prerenderMessage,
77+
});
78+
}
79+
return function (req, res, next) { return __awaiter(_this, void 0, void 0, function () {
80+
var fileName, searchIdx, filePath, width, idx, format, imageInfo;
81+
return __generator(this, function (_a) {
82+
switch (_a.label) {
83+
case 0:
84+
if (!req.query.f || !req.query.w) {
85+
next();
86+
return [2 /*return*/];
87+
}
88+
fileName = req.url.substring((options.uriPrefix || '').length + 1);
89+
searchIdx = fileName.indexOf('?');
90+
fileName = searchIdx >= 0 ? fileName.substring(0, searchIdx) : fileName;
91+
filePath = path_1.default.resolve(lup_root_1.ROOT, options.srcDir, fileName);
92+
width = parseInt(req.query.w || '', 10) || 1024;
93+
idx = fileName.lastIndexOf(".");
94+
format = (req.query.f.length > 0) ? req.query.f :
95+
((idx >= 0 && idx < fileName.length - 1) ? fileName.substring(idx + 1) : 'jpg');
96+
if (!index_1.OptimizerSettings.FILE_EXTENSION_TO_MIME_TYPE[format]) {
97+
next();
98+
return [2 /*return*/];
99+
}
100+
imageInfo = null;
101+
if (!prerenderStorage) return [3 /*break*/, 2];
102+
return [4 /*yield*/, prerenderStorage.getImage(filePath, width, format)];
103+
case 1:
104+
imageInfo = _a.sent();
105+
_a.label = 2;
106+
case 2:
107+
if (!(!imageInfo && cacheStorage)) return [3 /*break*/, 4];
108+
return [4 /*yield*/, cacheStorage.getImage(filePath, width, format)];
109+
case 3:
110+
imageInfo = _a.sent();
111+
_a.label = 4;
112+
case 4:
113+
if (!!imageInfo) return [3 /*break*/, 6];
114+
return [4 /*yield*/, optimizer.convertImage(filePath, width, format)];
115+
case 5:
116+
imageInfo = _a.sent();
117+
if (imageInfo && cacheStorage)
118+
cacheStorage.putImage(filePath, width, format, imageInfo.imageData);
119+
_a.label = 6;
120+
case 6:
121+
// send image
122+
if (imageInfo) {
123+
if (httpCacheSec && httpCacheSec > 0)
124+
res.set('Cache-control', 'public, max-age=' + httpCacheSec);
125+
res.set('Content-type', imageInfo.mimeType);
126+
res.send(imageInfo.imageData);
127+
}
128+
else {
129+
next();
130+
}
131+
return [2 /*return*/];
132+
}
133+
});
134+
}); };
135+
}
136+
exports.ImageRequestHandler = ImageRequestHandler;
137+
;

ImageStorage.d.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/// <reference types="node" />
2+
import { ImageInfo } from ".";
3+
export interface ImageStorage {
4+
/**
5+
* Returns the last modified time of the file.
6+
* @param filePath File path to original image.
7+
* @return Promise that resolves to last modified time or null if file could not be found.
8+
*/
9+
getLastModified(filePath: string, width: number, format: string): Promise<number | null>;
10+
/**
11+
* Loads an image from storage.
12+
* @param filePath File path to original image.
13+
* @param width Width to scale image to.
14+
* @param format Format to convert image to.
15+
* @return Promise that resolves to image info or null if image with specified parameters could not be found.
16+
*/
17+
getImage(filePath: string, width: number, format: string): Promise<ImageInfo | null>;
18+
/**
19+
* Stores an image in storage.
20+
* @param filePath File path to original image.
21+
* @param width Width to scale image to.
22+
* @param format Format to convert image to.
23+
* @param imageData Image data to store.
24+
* @return Promise that resolves when image has been stored.
25+
*/
26+
putImage(filePath: string, width: number, format: string, imageData: Buffer): Promise<void>;
27+
}
28+
export type ImageCacheStorageProps = {
29+
/** Path relative to project root where scaled images should be stored.
30+
* If null scaled images won't be cached.
31+
* Defaults to './cache'
32+
*/
33+
cacheDir?: string | null;
34+
/**
35+
* Maximum size of the cache directory in MB.
36+
* If max size is reached and a new image is about to be stored, the longest unused image will be deleted.
37+
* Defaults to 1000 (1GB), zero or negative numbers disable the cache size limit.
38+
*/
39+
cacheSize?: number;
40+
};
41+
/** Creates a directory with limited size containing the latest used formats. */
42+
export declare class ImageCacheStorage implements ImageStorage {
43+
private readonly cacheDir;
44+
private readonly cacheSize;
45+
constructor(options: ImageCacheStorageProps);
46+
/**
47+
* Manually enforces cache cleanup.
48+
* In particular, this is useful if cache was manipulated manually.
49+
* By default this is called automatically when a new image is stored in the cache.
50+
*/
51+
private cleanUpCache;
52+
private getCacheFilePath;
53+
getLastModified(filePath: string, width: number, format: string): Promise<number | null>;
54+
getImage(filePath: string, width: number, format: string): Promise<ImageInfo | null>;
55+
putImage(filePath: string, width: number, format: string, imageData: Buffer): Promise<void>;
56+
}
57+
export type ImageDirectoryStorageProps = {
58+
/** Path relative to project root where scaled images should be stored.*/
59+
dirPath: string;
60+
};
61+
export declare class ImageDirectoryStorage implements ImageStorage {
62+
private dirPath;
63+
constructor(options: ImageDirectoryStorageProps);
64+
private getStoreFilePath;
65+
getLastModified(filePath: string, width: number, format: string): Promise<number | null>;
66+
getImage(filePath: string, width: number, format: string): Promise<ImageInfo | null>;
67+
putImage(filePath: string, width: number, format: string, imageData: Buffer): Promise<void>;
68+
}
69+
/** For each image creates a subdirectory in place that will contain all the different formats. */
70+
export declare class ImageInPlaceStorage implements ImageStorage {
71+
private _getStorageFilePath;
72+
getLastModified(filePath: string, width: number, format: string): Promise<number | null>;
73+
getImage(filePath: string, width: number, format: string): Promise<ImageInfo | null>;
74+
putImage(filePath: string, width: number, format: string, imageData: Buffer): Promise<void>;
75+
}

0 commit comments

Comments
 (0)