pixeli 0.1.9 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +362 -88
- package/dist/cli/commands/collage/index.d.ts +2 -0
- package/dist/cli/commands/collage/index.js +125 -0
- package/dist/cli/commands/grid/index.d.ts +2 -0
- package/dist/cli/commands/grid/index.js +127 -0
- package/dist/cli/commands/masonry/index.d.ts +2 -0
- package/dist/cli/commands/masonry/index.js +129 -0
- package/dist/cli/commands/template/index.d.ts +2 -0
- package/dist/cli/commands/template/index.js +123 -0
- package/dist/cli/commands/template/presets/artGallery.d.ts +15 -0
- package/dist/cli/commands/template/presets/artGallery.js +15 -0
- package/dist/cli/commands/template/presets/dashboardShot.d.ts +15 -0
- package/dist/cli/commands/template/presets/dashboardShot.js +16 -0
- package/dist/cli/commands/template/presets/horizontalBookSpread.d.ts +15 -0
- package/dist/cli/commands/template/presets/horizontalBookSpread.js +13 -0
- package/dist/cli/commands/template/presets/instagramGrid.d.ts +15 -0
- package/dist/cli/commands/template/presets/instagramGrid.js +16 -0
- package/dist/cli/commands/template/presets/verticalBookSpread.d.ts +15 -0
- package/dist/cli/commands/template/presets/verticalBookSpread.js +13 -0
- package/dist/cli/commands/template/presets.d.ts +73 -0
- package/{lib/merges/collage-merge → dist/cli/commands/template}/presets.js +6 -8
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +24 -0
- package/dist/cli/modules/loadImages.d.ts +15 -0
- package/dist/cli/modules/loadImages.js +74 -0
- package/dist/cli/modules/progressBar.d.ts +10 -0
- package/dist/cli/modules/progressBar.js +34 -0
- package/dist/cli/schemas/collage.d.ts +29 -0
- package/dist/cli/schemas/collage.js +38 -0
- package/dist/cli/schemas/grid.d.ts +34 -0
- package/dist/cli/schemas/grid.js +38 -0
- package/dist/cli/schemas/masonry.d.ts +62 -0
- package/dist/cli/schemas/masonry.js +62 -0
- package/dist/cli/schemas/template.d.ts +31 -0
- package/dist/cli/schemas/template.js +49 -0
- package/dist/cli/utils/buildCommandFromSchema.d.ts +8 -0
- package/dist/cli/utils/buildCommandFromSchema.js +55 -0
- package/dist/cli/utils/configureCommandErrors.d.ts +2 -0
- package/dist/cli/utils/configureCommandErrors.js +22 -0
- package/dist/cli/utils/stringFormatter.d.ts +1 -0
- package/dist/cli/utils/stringFormatter.js +3 -0
- package/dist/cli/utils/toErrorMessage.d.ts +4 -0
- package/dist/cli/utils/toErrorMessage.js +22 -0
- package/dist/core/helpers.d.ts +10 -0
- package/dist/core/helpers.js +42 -0
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +2 -0
- package/dist/core/jobs/batchRunner.d.ts +44 -0
- package/dist/core/jobs/batchRunner.js +90 -0
- package/dist/core/jobs/types.d.ts +10 -0
- package/dist/core/jobs/types.js +1 -0
- package/dist/core/mergeError.d.ts +9 -0
- package/dist/core/mergeError.js +10 -0
- package/dist/core/merges/collage/index.d.ts +9 -0
- package/dist/core/merges/collage/index.js +32 -0
- package/dist/core/merges/collage/steps/calculateImageDimensions.d.ts +12 -0
- package/dist/core/merges/collage/steps/calculateImageDimensions.js +18 -0
- package/dist/core/merges/collage/steps/createComposites.d.ts +8 -0
- package/dist/core/merges/collage/steps/createComposites.js +58 -0
- package/dist/core/merges/collage/steps/resizeAndBorderImages.d.ts +12 -0
- package/dist/core/merges/collage/steps/resizeAndBorderImages.js +26 -0
- package/dist/core/merges/collage/steps/rotateImages.d.ts +7 -0
- package/dist/core/merges/collage/steps/rotateImages.js +9 -0
- package/dist/core/merges/grid/index.d.ts +12 -0
- package/dist/core/merges/grid/index.js +36 -0
- package/dist/core/merges/grid/steps/calculateCanvasDimensions.d.ts +8 -0
- package/dist/core/merges/grid/steps/calculateCanvasDimensions.js +18 -0
- package/dist/core/merges/grid/steps/calculateFontSize.d.ts +7 -0
- package/dist/core/merges/grid/steps/calculateFontSize.js +19 -0
- package/dist/core/merges/grid/steps/calculateImageDimensions.d.ts +8 -0
- package/dist/core/merges/grid/steps/calculateImageDimensions.js +18 -0
- package/dist/core/merges/grid/steps/createComposites.d.ts +10 -0
- package/dist/core/merges/grid/steps/createComposites.js +63 -0
- package/dist/core/merges/grid/steps/prepareImages.d.ts +10 -0
- package/dist/core/merges/grid/steps/prepareImages.js +29 -0
- package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.d.ts +7 -0
- package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.js +17 -0
- package/dist/core/merges/index.d.ts +5 -0
- package/dist/core/merges/index.js +4 -0
- package/dist/core/merges/masonry/index.d.ts +10 -0
- package/dist/core/merges/masonry/index.js +32 -0
- package/dist/core/merges/masonry/steps/calculateCanvasDimensions.d.ts +15 -0
- package/dist/core/merges/masonry/steps/calculateCanvasDimensions.js +17 -0
- package/dist/core/merges/masonry/steps/calculateLaneSize.d.ts +9 -0
- package/dist/core/merges/masonry/steps/calculateLaneSize.js +27 -0
- package/dist/core/merges/masonry/steps/createComposites.d.ts +25 -0
- package/dist/core/merges/masonry/steps/createComposites.js +108 -0
- package/dist/core/merges/masonry/steps/resizeImages.d.ts +7 -0
- package/dist/core/merges/masonry/steps/resizeImages.js +14 -0
- package/dist/core/merges/masonry/steps/splitIntoLanes.d.ts +17 -0
- package/dist/core/merges/masonry/steps/splitIntoLanes.js +78 -0
- package/dist/core/merges/shared-steps/applyComposites.d.ts +2 -0
- package/dist/core/merges/shared-steps/applyComposites.js +16 -0
- package/dist/core/merges/shared-steps/createCanvas.d.ts +11 -0
- package/dist/core/merges/shared-steps/createCanvas.js +17 -0
- package/dist/core/merges/shared-steps/exportCanvas.d.ts +7 -0
- package/dist/core/merges/shared-steps/exportCanvas.js +25 -0
- package/dist/core/merges/shared-steps/finalizeImagePipelines.d.ts +2 -0
- package/dist/core/merges/shared-steps/finalizeImagePipelines.js +9 -0
- package/dist/core/merges/shared-steps/loadImages.d.ts +2 -0
- package/dist/core/merges/shared-steps/loadImages.js +26 -0
- package/dist/core/merges/shared-steps/validateCaptions.d.ts +10 -0
- package/dist/core/merges/shared-steps/validateCaptions.js +17 -0
- package/dist/core/merges/template/index.d.ts +10 -0
- package/dist/core/merges/template/index.js +28 -0
- package/dist/core/merges/template/steps/calculateSlotDimensions.d.ts +9 -0
- package/dist/core/merges/template/steps/calculateSlotDimensions.js +12 -0
- package/dist/core/merges/template/steps/createComposites.d.ts +10 -0
- package/dist/core/merges/template/steps/createComposites.js +25 -0
- package/dist/core/merges/template/steps/getBlocks.d.ts +13 -0
- package/dist/core/merges/template/steps/getBlocks.js +28 -0
- package/dist/core/merges/template/types.d.ts +21 -0
- package/dist/core/merges/template/types.js +1 -0
- package/dist/core/merges/types.d.ts +123 -0
- package/dist/core/merges/types.js +1 -0
- package/dist/core/modules/messages.d.ts +32 -0
- package/dist/core/modules/messages.js +54 -0
- package/dist/core/modules/typedEventEmitter.d.ts +7 -0
- package/dist/core/modules/typedEventEmitter.js +9 -0
- package/dist/core/pipeline/guards.d.ts +4 -0
- package/dist/core/pipeline/guards.js +23 -0
- package/dist/core/pipeline/mergePipeline.d.ts +60 -0
- package/dist/core/pipeline/mergePipeline.js +122 -0
- package/dist/core/schemas/collage.d.ts +26 -0
- package/dist/core/schemas/collage.js +17 -0
- package/dist/core/schemas/grid.d.ts +32 -0
- package/dist/core/schemas/grid.js +29 -0
- package/dist/core/schemas/masonry.d.ts +56 -0
- package/dist/core/schemas/masonry.js +43 -0
- package/dist/core/schemas/mergeJob.d.ts +11 -0
- package/dist/core/schemas/mergeJob.js +6 -0
- package/dist/core/schemas/template.d.ts +34 -0
- package/dist/core/schemas/template.js +88 -0
- package/dist/core/utils/colors/hexToRgba.d.ts +2 -0
- package/dist/core/utils/colors/hexToRgba.js +25 -0
- package/dist/core/utils/colors/rgbaToHex.d.ts +2 -0
- package/dist/core/utils/colors/rgbaToHex.js +15 -0
- package/dist/core/utils/colors/types.d.ts +7 -0
- package/dist/core/utils/colors/types.js +1 -0
- package/dist/core/utils/fonts/getFontSize.d.ts +9 -0
- package/dist/core/utils/fonts/getFontSize.js +40 -0
- package/dist/core/utils/images/addImageBorder.d.ts +13 -0
- package/dist/core/utils/images/addImageBorder.js +33 -0
- package/dist/core/utils/images/getImageHeights.d.ts +2 -0
- package/dist/core/utils/images/getImageHeights.js +9 -0
- package/dist/core/utils/images/getImageWidths.d.ts +2 -0
- package/dist/core/utils/images/getImageWidths.js +9 -0
- package/dist/core/utils/images/getSmallestImageDimensions.d.ts +5 -0
- package/dist/core/utils/images/getSmallestImageDimensions.js +9 -0
- package/dist/core/utils/images/handleImageEdges.d.ts +13 -0
- package/dist/core/utils/images/handleImageEdges.js +39 -0
- package/dist/core/utils/images/isActualImage.d.ts +5 -0
- package/dist/core/utils/images/isActualImage.js +26 -0
- package/dist/core/utils/images/parseAspectRatio.d.ts +1 -0
- package/dist/core/utils/images/parseAspectRatio.js +22 -0
- package/dist/core/utils/images/roundImage.d.ts +9 -0
- package/dist/core/utils/images/roundImage.js +27 -0
- package/dist/core/utils/images/roundImages.d.ts +8 -0
- package/dist/core/utils/images/roundImages.js +19 -0
- package/dist/core/utils/images/scaleImage.d.ts +8 -0
- package/dist/core/utils/images/scaleImage.js +36 -0
- package/dist/core/utils/images/scaleImages.d.ts +8 -0
- package/dist/core/utils/images/scaleImages.js +38 -0
- package/dist/core/utils/math/median.d.ts +1 -0
- package/dist/core/utils/math/median.js +12 -0
- package/dist/core/utils/math/randint.d.ts +6 -0
- package/dist/core/utils/math/randint.js +11 -0
- package/dist/core/utils/math/trimmedMedian.d.ts +1 -0
- package/dist/core/utils/math/trimmedMedian.js +12 -0
- package/dist/core/utils/svg/createSvgTextBuffer.d.ts +9 -0
- package/dist/core/utils/svg/createSvgTextBuffer.js +21 -0
- package/dist/validators/aspectRatio.d.ts +2 -0
- package/dist/validators/aspectRatio.js +15 -0
- package/dist/validators/coercion.d.ts +2 -0
- package/dist/validators/coercion.js +12 -0
- package/dist/validators/format.d.ts +2 -0
- package/dist/validators/format.js +15 -0
- package/dist/validators/hexColor.d.ts +7 -0
- package/dist/validators/hexColor.js +27 -0
- package/dist/validators/index.d.ts +95 -0
- package/dist/validators/index.js +64 -0
- package/dist/validators/outputFile.d.ts +2 -0
- package/dist/validators/outputFile.js +7 -0
- package/dist/validators/path.d.ts +3 -0
- package/dist/validators/path.js +18 -0
- package/dist/validators/sharpImageInput.d.ts +3 -0
- package/dist/validators/sharpImageInput.js +6 -0
- package/dist/validators/template.d.ts +15 -0
- package/dist/validators/template.js +41 -0
- package/package.json +26 -9
- package/bin/pixeli.js +0 -26
- package/commands/merge/collage.js +0 -83
- package/commands/merge/grid.js +0 -71
- package/commands/merge/helpers/utils.js +0 -11
- package/commands/merge/helpers/validations.js +0 -269
- package/commands/merge/index.js +0 -12
- package/commands/merge/masonry.js +0 -72
- package/lib/helpers/loadImages.js +0 -94
- package/lib/helpers/progressBar.js +0 -20
- package/lib/helpers/templateValidator.js +0 -139
- package/lib/helpers/utils.js +0 -208
- package/lib/merges/collage-merge/index.js +0 -110
- package/lib/merges/collage-merge/presets/artGallery.js +0 -17
- package/lib/merges/collage-merge/presets/dashboardShot.js +0 -18
- package/lib/merges/collage-merge/presets/horizontalBookSpread.js +0 -15
- package/lib/merges/collage-merge/presets/instagramGrid.js +0 -18
- package/lib/merges/collage-merge/presets/verticalBookSpread.js +0 -15
- package/lib/merges/grid-merge/index.js +0 -152
- package/lib/merges/masonry-merge/horizontal.js +0 -157
- package/lib/merges/masonry-merge/index.js +0 -57
- package/lib/merges/masonry-merge/vertical.js +0 -152
- package/lib/merges/merge-utils.js +0 -176
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
|
|
2
|
+
import { getFontSize } from '../../../utils/fonts/getFontSize.js';
|
|
3
|
+
export const calculateFontSize = async (context, options, _onProgress) => {
|
|
4
|
+
requireState(context, 'imageWidth');
|
|
5
|
+
requireState(context, 'captionHeight');
|
|
6
|
+
if (!context.state.areCaptionsProvided) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
requireNonEmptyArray(context.captions, 'captions');
|
|
10
|
+
const longestCaption = context.captions.reduce((longest, current) => {
|
|
11
|
+
return current.length > longest.length ? current : longest;
|
|
12
|
+
});
|
|
13
|
+
context.state.fontSize = await getFontSize({
|
|
14
|
+
text: longestCaption,
|
|
15
|
+
maxWidth: context.state.imageWidth,
|
|
16
|
+
maxHeight: context.state.captionHeight,
|
|
17
|
+
initialFontSize: options.maxCaptionSize,
|
|
18
|
+
});
|
|
19
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { GridState } from '../index.js';
|
|
3
|
+
interface Options {
|
|
4
|
+
imageWidth?: number | undefined;
|
|
5
|
+
aspectRatio: number;
|
|
6
|
+
}
|
|
7
|
+
export declare const calculateImageDimensions: MergeStep<Options, GridState>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { MESSAGES } from '../../../modules/messages.js';
|
|
2
|
+
import { MergeError } from '../../../mergeError.js';
|
|
3
|
+
import { getImageWidths } from '../../../utils/images/getImageWidths.js';
|
|
4
|
+
import { trimmedMedian } from '../../../utils/math/trimmedMedian.js';
|
|
5
|
+
import { requireNonEmptyArray } from '../../../pipeline/guards.js';
|
|
6
|
+
export const calculateImageDimensions = async (context, options, _onProgress) => {
|
|
7
|
+
requireNonEmptyArray(context.images, 'images');
|
|
8
|
+
// Calculate image width
|
|
9
|
+
const width = options.imageWidth || trimmedMedian(await getImageWidths(context.images));
|
|
10
|
+
if (width === null) {
|
|
11
|
+
throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
|
|
12
|
+
}
|
|
13
|
+
// Calculate image height
|
|
14
|
+
const height = Math.floor(width / options.aspectRatio);
|
|
15
|
+
// Assign to context
|
|
16
|
+
context.state.imageWidth = width;
|
|
17
|
+
context.state.imageHeight = height;
|
|
18
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { GridState } from '../index.js';
|
|
3
|
+
import type { RGBA } from '../../../utils/colors/types.js';
|
|
4
|
+
interface Options {
|
|
5
|
+
columns: number;
|
|
6
|
+
gap: number;
|
|
7
|
+
captionColor: RGBA;
|
|
8
|
+
}
|
|
9
|
+
export declare const createComposites: MergeStep<Options, GridState>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
import { createSvgTextBuffer } from '../../../utils/svg/createSvgTextBuffer.js';
|
|
4
|
+
import { rgbaToHex } from '../../../utils/colors/rgbaToHex.js';
|
|
5
|
+
export const createComposites = async (context, options, onProgress) => {
|
|
6
|
+
requireState(context, 'areCaptionsProvided');
|
|
7
|
+
requireState(context, 'captionHeight');
|
|
8
|
+
requireState(context, 'imageWidth');
|
|
9
|
+
requireState(context, 'imageHeight');
|
|
10
|
+
requireState(context, 'rows');
|
|
11
|
+
requireNonEmptyArray(context.images, 'images');
|
|
12
|
+
// Only needed when there are captions
|
|
13
|
+
if (context.state.areCaptionsProvided) {
|
|
14
|
+
requireState(context, 'fontSize');
|
|
15
|
+
}
|
|
16
|
+
const composites = [];
|
|
17
|
+
let x = options.gap;
|
|
18
|
+
let y = options.gap;
|
|
19
|
+
for (let row = 0; row < context.state.rows; row++) {
|
|
20
|
+
for (let col = 0; col < options.columns; col++) {
|
|
21
|
+
const index = row * options.columns + col;
|
|
22
|
+
if (index >= context.images.length)
|
|
23
|
+
break;
|
|
24
|
+
const image = context.images[index];
|
|
25
|
+
composites.push({
|
|
26
|
+
input: await image.toBuffer(),
|
|
27
|
+
left: x,
|
|
28
|
+
top: y,
|
|
29
|
+
});
|
|
30
|
+
// Add caption if required
|
|
31
|
+
if (context.state.areCaptionsProvided) {
|
|
32
|
+
// Create text
|
|
33
|
+
const svgBuffer = createSvgTextBuffer({
|
|
34
|
+
text: context.captions[index],
|
|
35
|
+
maxWidth: context.state.imageWidth,
|
|
36
|
+
maxHeight: context.state.captionHeight,
|
|
37
|
+
fontSize: context.state.fontSize,
|
|
38
|
+
fill: rgbaToHex(options.captionColor),
|
|
39
|
+
});
|
|
40
|
+
// Add text to composites
|
|
41
|
+
composites.push({
|
|
42
|
+
input: svgBuffer,
|
|
43
|
+
left: x,
|
|
44
|
+
top: y + context.state.imageHeight,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// Update coordinates
|
|
48
|
+
x += context.state.imageWidth + options.gap;
|
|
49
|
+
// Call onProgress
|
|
50
|
+
if (onProgress) {
|
|
51
|
+
context.progressInfo.completed += 1;
|
|
52
|
+
context.progressInfo.phase = 'Merging images';
|
|
53
|
+
onProgress({ ...context.progressInfo });
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Update coordinates
|
|
57
|
+
y += context.state.areCaptionsProvided
|
|
58
|
+
? context.state.imageHeight + options.gap + context.state.captionHeight
|
|
59
|
+
: context.state.imageHeight + options.gap;
|
|
60
|
+
x = options.gap;
|
|
61
|
+
}
|
|
62
|
+
context.composites = composites;
|
|
63
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { GridState } from '../index.js';
|
|
3
|
+
import type { RGBA } from '../../../utils/colors/types.js';
|
|
4
|
+
interface Options {
|
|
5
|
+
cornerRadius: number;
|
|
6
|
+
borderWidth: number;
|
|
7
|
+
borderColor: RGBA;
|
|
8
|
+
}
|
|
9
|
+
export declare const prepareImages: MergeStep<Options, GridState>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { scaleImage } from '../../../utils/images/scaleImage.js';
|
|
2
|
+
import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
|
|
3
|
+
import { handleImageEdges } from '../../../utils/images/handleImageEdges.js';
|
|
4
|
+
export const prepareImages = async (context, options, _onProgress) => {
|
|
5
|
+
requireState(context, 'imageWidth');
|
|
6
|
+
requireState(context, 'imageHeight');
|
|
7
|
+
requireNonEmptyArray(context.images, 'images');
|
|
8
|
+
// Get values from context and options
|
|
9
|
+
const width = context.state.imageWidth;
|
|
10
|
+
const height = context.state.imageHeight;
|
|
11
|
+
const cornerRadius = options.cornerRadius;
|
|
12
|
+
for (let i = 0; i < context.images.length; i++) {
|
|
13
|
+
const image = context.images[i];
|
|
14
|
+
// Resize image
|
|
15
|
+
const resizedImage = await scaleImage(image, { width, height });
|
|
16
|
+
// Handle borders and corner rounding
|
|
17
|
+
const borderedImage = await handleImageEdges(resizedImage, {
|
|
18
|
+
imageWidth: width,
|
|
19
|
+
imageHeight: height,
|
|
20
|
+
borderWidth: options.borderWidth,
|
|
21
|
+
borderHeight: options.borderWidth,
|
|
22
|
+
borderColor: options.borderColor,
|
|
23
|
+
cornerRadius,
|
|
24
|
+
finalizePipeline: true,
|
|
25
|
+
});
|
|
26
|
+
// Update context
|
|
27
|
+
context.images[i] = borderedImage;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { shuffleArray, shuffleTogether } from '../../../helpers.js';
|
|
2
|
+
import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
|
|
3
|
+
export const shuffleImagesAndCaptions = async (context, options, _onProgress) => {
|
|
4
|
+
requireState(context, 'areCaptionsProvided');
|
|
5
|
+
requireNonEmptyArray(context.images, 'images');
|
|
6
|
+
// If captions are given and shuffle is true
|
|
7
|
+
if (context.state.areCaptionsProvided && options.shuffle) {
|
|
8
|
+
const [shuffledImages, shuffledCaptions] = shuffleTogether(context.images, context.captions);
|
|
9
|
+
context.images = shuffledImages;
|
|
10
|
+
context.captions = shuffledCaptions;
|
|
11
|
+
}
|
|
12
|
+
// If there are no captions but shuffle is true
|
|
13
|
+
else if (!context.state.areCaptionsProvided && options.shuffle) {
|
|
14
|
+
const shuffledImages = shuffleArray(context.images);
|
|
15
|
+
context.images = shuffledImages;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { gridMerge } from './grid/index.js';
|
|
2
|
+
export { masonryMerge } from './masonry/index.js';
|
|
3
|
+
export { templateMerge } from './template/index.js';
|
|
4
|
+
export { collageMerge } from './collage/index.js';
|
|
5
|
+
export type { GridMergeOptions, MasonryMergeOptions, TemplateMergeOptions, CollageMergeOptions } from './types.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MasonryMerge } from '../types.js';
|
|
2
|
+
import type sharp from 'sharp';
|
|
3
|
+
export interface MasonryState {
|
|
4
|
+
rowHeight: number;
|
|
5
|
+
columnWidth: number;
|
|
6
|
+
canvasWidth: number;
|
|
7
|
+
canvasHeight: number;
|
|
8
|
+
lanes: sharp.Sharp[][];
|
|
9
|
+
}
|
|
10
|
+
export declare const masonryMerge: MasonryMerge;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { MergePipeline } from '../../pipeline/mergePipeline.js';
|
|
2
|
+
import { masonrySchema } from '../../schemas/masonry.js';
|
|
3
|
+
import { loadImages } from '../shared-steps/loadImages.js';
|
|
4
|
+
import { createCanvas } from '../shared-steps/createCanvas.js';
|
|
5
|
+
import { applyComposites } from '../shared-steps/applyComposites.js';
|
|
6
|
+
import { exportCanvas } from '../shared-steps/exportCanvas.js';
|
|
7
|
+
import { calculateLaneSize } from './steps/calculateLaneSize.js';
|
|
8
|
+
import { resizeImages } from './steps/resizeImages.js';
|
|
9
|
+
import { splitIntoLanes } from './steps/splitIntoLanes.js';
|
|
10
|
+
import { calculateCanvasDimensions } from './steps/calculateCanvasDimensions.js';
|
|
11
|
+
import { createComposites } from './steps/createComposites.js';
|
|
12
|
+
export const masonryMerge = async (imageInputs, options, onProgress) => {
|
|
13
|
+
const context = {
|
|
14
|
+
inputs: imageInputs,
|
|
15
|
+
captions: [],
|
|
16
|
+
composites: [],
|
|
17
|
+
images: [],
|
|
18
|
+
state: {},
|
|
19
|
+
};
|
|
20
|
+
const masonryMergePipeline = await MergePipeline.createPipeline(masonrySchema, options, context, onProgress);
|
|
21
|
+
masonryMergePipeline
|
|
22
|
+
.use(loadImages)
|
|
23
|
+
.use(calculateLaneSize)
|
|
24
|
+
.use(resizeImages)
|
|
25
|
+
.use(splitIntoLanes)
|
|
26
|
+
.use(calculateCanvasDimensions)
|
|
27
|
+
.use(createCanvas)
|
|
28
|
+
.use(createComposites)
|
|
29
|
+
.use(applyComposites)
|
|
30
|
+
.use(exportCanvas);
|
|
31
|
+
return await masonryMergePipeline.run();
|
|
32
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { MasonryState } from '../index.js';
|
|
3
|
+
interface HorizontalOptions {
|
|
4
|
+
flow: 'horizontal';
|
|
5
|
+
gap: number;
|
|
6
|
+
canvasWidth: number;
|
|
7
|
+
}
|
|
8
|
+
interface VerticalOptions {
|
|
9
|
+
flow: 'vertical';
|
|
10
|
+
gap: number;
|
|
11
|
+
canvasHeight: number;
|
|
12
|
+
}
|
|
13
|
+
type Options = HorizontalOptions | VerticalOptions;
|
|
14
|
+
export declare const calculateCanvasDimensions: MergeStep<Options, MasonryState>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { requireState } from '../../../pipeline/guards.js';
|
|
2
|
+
export const calculateCanvasDimensions = async (context, options, _onProgress) => {
|
|
3
|
+
// Mandatory regardless of flow
|
|
4
|
+
requireState(context, 'lanes');
|
|
5
|
+
// Put both canvas width and height in context state
|
|
6
|
+
const totalLanes = context.state.lanes.length;
|
|
7
|
+
if (options.flow === 'horizontal') {
|
|
8
|
+
requireState(context, 'rowHeight');
|
|
9
|
+
context.state.canvasWidth = options.canvasWidth;
|
|
10
|
+
context.state.canvasHeight = totalLanes * context.state.rowHeight + (totalLanes + 1) * options.gap;
|
|
11
|
+
}
|
|
12
|
+
else {
|
|
13
|
+
requireState(context, 'columnWidth');
|
|
14
|
+
context.state.canvasHeight = options.canvasHeight;
|
|
15
|
+
context.state.canvasWidth = totalLanes * context.state.columnWidth + (totalLanes + 1) * options.gap;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { MasonryState } from '../index.js';
|
|
3
|
+
interface Options {
|
|
4
|
+
rowHeight?: number | undefined;
|
|
5
|
+
columnWidth?: number | undefined;
|
|
6
|
+
flow: 'horizontal' | 'vertical';
|
|
7
|
+
}
|
|
8
|
+
export declare const calculateLaneSize: MergeStep<Options, MasonryState>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { trimmedMedian } from '../../../utils/math/trimmedMedian.js';
|
|
2
|
+
import { getImageWidths } from '../../../utils/images/getImageWidths.js';
|
|
3
|
+
import { getImageHeights } from '../../../utils/images/getImageHeights.js';
|
|
4
|
+
import { MESSAGES } from '../../../modules/messages.js';
|
|
5
|
+
import { MergeError } from '../../../mergeError.js';
|
|
6
|
+
import { requireNonEmptyArray } from '../../../pipeline/guards.js';
|
|
7
|
+
export const calculateLaneSize = async (context, options, _onProgress) => {
|
|
8
|
+
requireNonEmptyArray(context.images, 'images');
|
|
9
|
+
if (options.flow === 'horizontal') {
|
|
10
|
+
// Calculate rowHeight
|
|
11
|
+
const rowHeight = options.rowHeight || trimmedMedian(await getImageHeights(context.images));
|
|
12
|
+
if (rowHeight === null) {
|
|
13
|
+
throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
|
|
14
|
+
}
|
|
15
|
+
// Set rowHeight
|
|
16
|
+
context.state.rowHeight = rowHeight;
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
// Calculate columnWidth
|
|
20
|
+
const columnWidth = options.columnWidth || trimmedMedian(await getImageWidths(context.images));
|
|
21
|
+
if (columnWidth === null) {
|
|
22
|
+
throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
|
|
23
|
+
}
|
|
24
|
+
// Set columnWidth
|
|
25
|
+
context.state.columnWidth = columnWidth;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MasonryState } from '../index.js';
|
|
2
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
3
|
+
import type { RGBA } from '../../../utils/colors/types.js';
|
|
4
|
+
interface BaseOptions {
|
|
5
|
+
flow: 'horizontal' | 'vertical';
|
|
6
|
+
gap: number;
|
|
7
|
+
cornerRadius: number;
|
|
8
|
+
borderWidth: number;
|
|
9
|
+
borderColor: RGBA;
|
|
10
|
+
}
|
|
11
|
+
interface HorizontalOptions extends BaseOptions {
|
|
12
|
+
flow: 'horizontal';
|
|
13
|
+
gap: number;
|
|
14
|
+
canvasWidth: number;
|
|
15
|
+
hAlign: 'justified' | 'left' | 'center' | 'right';
|
|
16
|
+
}
|
|
17
|
+
interface VerticalOptions extends BaseOptions {
|
|
18
|
+
flow: 'vertical';
|
|
19
|
+
gap: number;
|
|
20
|
+
canvasHeight: number;
|
|
21
|
+
vAlign: 'justified' | 'top' | 'middle' | 'bottom';
|
|
22
|
+
}
|
|
23
|
+
type Options = HorizontalOptions | VerticalOptions;
|
|
24
|
+
export declare const createComposites: MergeStep<Options, MasonryState>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { requireState } from '../../../pipeline/guards.js';
|
|
3
|
+
import { handleImageEdges } from '../../../utils/images/handleImageEdges.js';
|
|
4
|
+
// |----------------------|
|
|
5
|
+
// |----------------------|
|
|
6
|
+
// | ROW OFFSET FUNCTION |
|
|
7
|
+
// |----------------------|
|
|
8
|
+
// |----------------------|
|
|
9
|
+
const computeOffset = async (flow, lane, canvasSize, gap, alignment) => {
|
|
10
|
+
// Calculate total row width
|
|
11
|
+
let totalLaneLength = gap * (lane.length + 1);
|
|
12
|
+
for (const im of lane) {
|
|
13
|
+
const meta = await im.metadata();
|
|
14
|
+
totalLaneLength += flow === 'horizontal' ? meta.width : meta.height;
|
|
15
|
+
}
|
|
16
|
+
// Get x offset
|
|
17
|
+
switch (alignment) {
|
|
18
|
+
case 'justified':
|
|
19
|
+
case 'left':
|
|
20
|
+
case 'top':
|
|
21
|
+
return gap;
|
|
22
|
+
case 'right':
|
|
23
|
+
case 'bottom':
|
|
24
|
+
return canvasSize - totalLaneLength + gap;
|
|
25
|
+
case 'middle':
|
|
26
|
+
case 'center':
|
|
27
|
+
const canvasGap = gap * 2;
|
|
28
|
+
return Math.floor((canvasSize + canvasGap - totalLaneLength) / 2);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
const horizontalAxis = {
|
|
32
|
+
flow: 'horizontal',
|
|
33
|
+
getPrimary: (meta) => meta.width,
|
|
34
|
+
getCross: (meta) => meta.height,
|
|
35
|
+
crop: (meta, overflow) => ({
|
|
36
|
+
width: meta.width - overflow,
|
|
37
|
+
height: meta.height,
|
|
38
|
+
fit: 'cover',
|
|
39
|
+
}),
|
|
40
|
+
};
|
|
41
|
+
const verticalAxis = {
|
|
42
|
+
flow: 'vertical',
|
|
43
|
+
getPrimary: (meta) => meta.height,
|
|
44
|
+
getCross: (meta) => meta.width,
|
|
45
|
+
crop: (meta, overflow) => ({
|
|
46
|
+
width: meta.width,
|
|
47
|
+
height: meta.height - overflow,
|
|
48
|
+
fit: 'cover',
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
export const createComposites = async (context, options, onProgress) => {
|
|
52
|
+
// Require needed states
|
|
53
|
+
requireState(context, 'lanes');
|
|
54
|
+
options.flow === 'horizontal' ? requireState(context, 'rowHeight') : requireState(context, 'columnWidth');
|
|
55
|
+
// Initialize flow-dependent values
|
|
56
|
+
const laneCrossSize = options.flow === 'horizontal' ? context.state.rowHeight : context.state.columnWidth;
|
|
57
|
+
const axis = options.flow === 'horizontal' ? horizontalAxis : verticalAxis;
|
|
58
|
+
const alignment = options.flow === 'horizontal' ? options.hAlign : options.vAlign;
|
|
59
|
+
const primaryCanvasSize = options.flow === 'horizontal' ? options.canvasWidth : options.canvasHeight;
|
|
60
|
+
// Define initial variables
|
|
61
|
+
const composites = [];
|
|
62
|
+
let primaryCursor = options.gap;
|
|
63
|
+
let crossCursor = options.gap;
|
|
64
|
+
for (const lane of context.state.lanes) {
|
|
65
|
+
// Get the lanes vertical/horizontal offset
|
|
66
|
+
let primary = await computeOffset(options.flow, lane, primaryCanvasSize, options.gap, alignment);
|
|
67
|
+
let cross = crossCursor;
|
|
68
|
+
for (const im of lane) {
|
|
69
|
+
let finalizedImage = im;
|
|
70
|
+
let meta = await im.metadata();
|
|
71
|
+
// Update x/y position
|
|
72
|
+
primaryCursor += axis.getPrimary(meta) + options.gap;
|
|
73
|
+
// Handle image overflows for justified layouts
|
|
74
|
+
if (primaryCursor >= primaryCanvasSize) {
|
|
75
|
+
const overflow = primaryCursor - primaryCanvasSize;
|
|
76
|
+
const buffer = await im.resize(axis.crop(meta, overflow)).toBuffer();
|
|
77
|
+
finalizedImage = sharp(buffer);
|
|
78
|
+
meta = await finalizedImage.metadata();
|
|
79
|
+
}
|
|
80
|
+
// Handle borders and corner rounding
|
|
81
|
+
const borderedImage = await handleImageEdges(finalizedImage, {
|
|
82
|
+
imageWidth: meta.width,
|
|
83
|
+
imageHeight: meta.height,
|
|
84
|
+
borderWidth: options.borderWidth,
|
|
85
|
+
borderHeight: options.borderWidth,
|
|
86
|
+
borderColor: options.borderColor,
|
|
87
|
+
cornerRadius: options.cornerRadius,
|
|
88
|
+
});
|
|
89
|
+
// Create the composite
|
|
90
|
+
composites.push({
|
|
91
|
+
input: await borderedImage.toBuffer(),
|
|
92
|
+
left: options.flow === 'horizontal' ? primary : cross,
|
|
93
|
+
top: options.flow === 'horizontal' ? cross : primary,
|
|
94
|
+
});
|
|
95
|
+
primary += axis.getPrimary(meta) + options.gap;
|
|
96
|
+
// Update progress
|
|
97
|
+
if (onProgress) {
|
|
98
|
+
context.progressInfo.phase = 'Merging images';
|
|
99
|
+
context.progressInfo.completed += 1;
|
|
100
|
+
onProgress({ ...context.progressInfo });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
primaryCursor = options.gap;
|
|
104
|
+
crossCursor += laneCrossSize + options.gap;
|
|
105
|
+
}
|
|
106
|
+
// Update context.composites
|
|
107
|
+
context.composites = composites;
|
|
108
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
|
|
2
|
+
import { scaleImage } from '../../../utils/images/scaleImage.js';
|
|
3
|
+
export const resizeImages = async (context, options, _onProgress) => {
|
|
4
|
+
requireNonEmptyArray(context.images, 'images');
|
|
5
|
+
// Require either rowHeight or columnWidth
|
|
6
|
+
options.flow === 'horizontal' ? requireState(context, 'rowHeight') : requireState(context, 'columnWidth');
|
|
7
|
+
// Rescale images to match rowHeight or columnWidth
|
|
8
|
+
const scaleOptions = options.flow === 'horizontal'
|
|
9
|
+
? { height: context.state.rowHeight, finalizePipeline: true }
|
|
10
|
+
: { width: context.state.columnWidth, finalizePipeline: true };
|
|
11
|
+
for (let i = 0; i < context.images.length; i++) {
|
|
12
|
+
context.images[i] = await scaleImage(context.images[i], scaleOptions);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { MergeStep } from '../../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { MasonryState } from '../index.js';
|
|
3
|
+
interface HorizontalOptions {
|
|
4
|
+
flow: 'horizontal';
|
|
5
|
+
gap: number;
|
|
6
|
+
canvasWidth: number;
|
|
7
|
+
hAlign: 'justified' | 'left' | 'center' | 'right';
|
|
8
|
+
}
|
|
9
|
+
interface VerticalOptions {
|
|
10
|
+
flow: 'vertical';
|
|
11
|
+
gap: number;
|
|
12
|
+
canvasHeight: number;
|
|
13
|
+
vAlign: 'justified' | 'top' | 'middle' | 'bottom';
|
|
14
|
+
}
|
|
15
|
+
type Options = HorizontalOptions | VerticalOptions;
|
|
16
|
+
export declare const splitIntoLanes: MergeStep<Options, MasonryState>;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { requireNonEmptyArray } from '../../../pipeline/guards.js';
|
|
2
|
+
export const splitIntoLanes = async (context, options, _onProgress) => {
|
|
3
|
+
requireNonEmptyArray(context.images, 'images');
|
|
4
|
+
// Split into lanes
|
|
5
|
+
const lanes = options.flow === 'horizontal'
|
|
6
|
+
? await splitIntoRows(context.images, options.canvasWidth, options.gap, options.hAlign)
|
|
7
|
+
: await splitIntoColumns(context.images, options.canvasHeight, options.gap, options.vAlign);
|
|
8
|
+
// Assign lanes
|
|
9
|
+
context.state.lanes = lanes;
|
|
10
|
+
};
|
|
11
|
+
const splitIntoRows = async (images, canvasWidth, gap, hAlign) => {
|
|
12
|
+
const rows = [];
|
|
13
|
+
let currentRow = [];
|
|
14
|
+
let currentWidth = gap; // initial leading gap
|
|
15
|
+
for (const im of images) {
|
|
16
|
+
const meta = await im.metadata();
|
|
17
|
+
const nextWidth = currentWidth + meta.width + gap;
|
|
18
|
+
if (hAlign === 'justified') {
|
|
19
|
+
// Greedy: always push image, fix overflow later
|
|
20
|
+
currentRow.push(im);
|
|
21
|
+
currentWidth = nextWidth;
|
|
22
|
+
if (currentWidth + gap >= canvasWidth) {
|
|
23
|
+
rows.push(currentRow.slice());
|
|
24
|
+
currentRow.length = 0;
|
|
25
|
+
currentWidth = gap;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
// Non-greedy: break BEFORE adding image that doesn't fit
|
|
30
|
+
if (nextWidth > canvasWidth && currentRow.length > 0) {
|
|
31
|
+
rows.push(currentRow.slice());
|
|
32
|
+
currentRow = [];
|
|
33
|
+
currentWidth = gap;
|
|
34
|
+
}
|
|
35
|
+
// Add the image (may be first in a new row)
|
|
36
|
+
currentRow.push(im);
|
|
37
|
+
currentWidth += meta.width + gap;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (currentRow.length > 0) {
|
|
41
|
+
rows.push(currentRow);
|
|
42
|
+
}
|
|
43
|
+
return rows;
|
|
44
|
+
};
|
|
45
|
+
const splitIntoColumns = async (images, canvasHeight, gap, vAlign) => {
|
|
46
|
+
const cols = [];
|
|
47
|
+
const currentCol = [];
|
|
48
|
+
let currentHeight = gap;
|
|
49
|
+
for (const im of images) {
|
|
50
|
+
const meta = await im.metadata();
|
|
51
|
+
let nextHeight = currentHeight + meta.height + gap;
|
|
52
|
+
if (vAlign === 'justified') {
|
|
53
|
+
// Greedy: always push image, fix overflow later
|
|
54
|
+
currentCol.push(im);
|
|
55
|
+
currentHeight = nextHeight;
|
|
56
|
+
if (currentHeight + gap >= canvasHeight) {
|
|
57
|
+
cols.push(currentCol.slice());
|
|
58
|
+
currentCol.length = 0;
|
|
59
|
+
currentHeight = gap;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Non-greedy: break BEFORE adding image that doesn't fit
|
|
64
|
+
if (nextHeight > canvasHeight && currentCol.length > 0) {
|
|
65
|
+
cols.push(currentCol.slice());
|
|
66
|
+
currentCol.length = 0;
|
|
67
|
+
currentHeight = gap;
|
|
68
|
+
}
|
|
69
|
+
// Add the image (may be first in a new column)
|
|
70
|
+
currentCol.push(im);
|
|
71
|
+
currentHeight += meta.height + gap;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (currentCol.length > 0) {
|
|
75
|
+
cols.push(currentCol);
|
|
76
|
+
}
|
|
77
|
+
return cols;
|
|
78
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { MESSAGES } from '../../modules/messages.js';
|
|
2
|
+
import { MergeError } from '../../mergeError.js';
|
|
3
|
+
import { requireContextProp } from '../../pipeline/guards.js';
|
|
4
|
+
export const applyComposites = async (context, _options, _onProgress) => {
|
|
5
|
+
requireContextProp(context, 'canvas');
|
|
6
|
+
// Create final grid
|
|
7
|
+
try {
|
|
8
|
+
context.canvas = context.canvas.composite(context.composites);
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
throw new MergeError(MESSAGES.ERROR.INTERNAL.message, {
|
|
12
|
+
type: 'internal',
|
|
13
|
+
cause: err,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { MergeStep } from '../../pipeline/mergePipeline.js';
|
|
2
|
+
import type { RGBA } from '../../utils/colors/types.js';
|
|
3
|
+
interface Options {
|
|
4
|
+
canvasColor: RGBA;
|
|
5
|
+
}
|
|
6
|
+
interface State {
|
|
7
|
+
canvasWidth: number;
|
|
8
|
+
canvasHeight: number;
|
|
9
|
+
}
|
|
10
|
+
export declare const createCanvas: MergeStep<Options, State>;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { requireState } from '../../pipeline/guards.js';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
export const createCanvas = async (context, options, _onProgress) => {
|
|
4
|
+
requireState(context, 'canvasWidth');
|
|
5
|
+
requireState(context, 'canvasHeight');
|
|
6
|
+
// Create canvas
|
|
7
|
+
const canvas = sharp({
|
|
8
|
+
limitInputPixels: false,
|
|
9
|
+
create: {
|
|
10
|
+
width: context.state.canvasWidth,
|
|
11
|
+
height: context.state.canvasHeight,
|
|
12
|
+
channels: 4,
|
|
13
|
+
background: options.canvasColor,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
context.canvas = canvas;
|
|
17
|
+
};
|