pixeli 0.1.8 → 1.0.3
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 +341 -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 +1 -0
- package/dist/core/index.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 +3 -0
- package/dist/core/merges/index.js +3 -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 +102 -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/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/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,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,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
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { MESSAGES } from '../../modules/messages.js';
|
|
2
|
+
import { MergeError } from '../../mergeError.js';
|
|
3
|
+
import { requireContextProp } from '../../pipeline/guards.js';
|
|
4
|
+
export const exportCanvas = async (context, options, _onProgress) => {
|
|
5
|
+
// Ensure canvas exists
|
|
6
|
+
requireContextProp(context, 'canvas');
|
|
7
|
+
try {
|
|
8
|
+
return await context.canvas.toFormat(options.format).toBuffer();
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
// Assuming ALL sharp errors are instances of the Error object
|
|
12
|
+
const sharpError = err;
|
|
13
|
+
// SPECIFIC SHARP ERROR
|
|
14
|
+
// occurs when trying to create a buffer that exceeds the limits of the current image format
|
|
15
|
+
if (sharpError.message.includes('pixel limit') || sharpError.message.includes('Processed image is too large')) {
|
|
16
|
+
const errText = `Error: image to large for "${options.format}" format, try a format that allows larger images`;
|
|
17
|
+
throw new MergeError(errText, { type: 'image' });
|
|
18
|
+
}
|
|
19
|
+
// Other sharp errors
|
|
20
|
+
throw new MergeError(MESSAGES.ERROR.INTERNAL.message, {
|
|
21
|
+
type: 'internal',
|
|
22
|
+
cause: err,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { requireNonEmptyArray } from '../../pipeline/guards.js';
|
|
2
|
+
import sharp from 'sharp';
|
|
3
|
+
export const finalizeImagePipelines = async (context, _options, _onProgress) => {
|
|
4
|
+
requireNonEmptyArray(context.images);
|
|
5
|
+
for (let i = 0; i < context.images.length; i++) {
|
|
6
|
+
const buffer = await context.images[i].toBuffer();
|
|
7
|
+
context.images[i] = sharp(buffer);
|
|
8
|
+
}
|
|
9
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
import { isActualImage } from '../../utils/images/isActualImage.js';
|
|
3
|
+
import { MergeError } from '../../mergeError.js';
|
|
4
|
+
import { requireNonEmptyArray } from '../../pipeline/guards.js';
|
|
5
|
+
export const loadImages = async (context, _options, _onProgress) => {
|
|
6
|
+
// Ensure inputs are provided
|
|
7
|
+
requireNonEmptyArray(context.inputs, 'inputs');
|
|
8
|
+
const images = [];
|
|
9
|
+
for (let i = 0; i < context.inputs.length; i++) {
|
|
10
|
+
// Ensure image is valid
|
|
11
|
+
const input = context.inputs[i];
|
|
12
|
+
const { isImage, reason } = await isActualImage(input);
|
|
13
|
+
if (!isImage) {
|
|
14
|
+
throw new MergeError(`Invalid image input at index ${i}`, {
|
|
15
|
+
type: 'validation',
|
|
16
|
+
cause: reason,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
images.push(sharp(input));
|
|
20
|
+
}
|
|
21
|
+
// Ensure there's at least one image
|
|
22
|
+
if (images.length <= 0) {
|
|
23
|
+
throw new MergeError('No images provided to merge', { type: 'validation' });
|
|
24
|
+
}
|
|
25
|
+
context.images = images;
|
|
26
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { MergeStep } from '../../pipeline/mergePipeline.js';
|
|
2
|
+
interface Options {
|
|
3
|
+
caption: boolean;
|
|
4
|
+
captions?: string[] | undefined;
|
|
5
|
+
}
|
|
6
|
+
interface State {
|
|
7
|
+
areCaptionsProvided: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare const validateCaptions: MergeStep<Options, State>;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { MergeError } from '../../mergeError.js';
|
|
2
|
+
export const validateCaptions = async (context, options, _onProgress) => {
|
|
3
|
+
// Initially set to false
|
|
4
|
+
context.state.areCaptionsProvided = false;
|
|
5
|
+
// Update context
|
|
6
|
+
if (areCaptionsProvided(options.caption, options.captions)) {
|
|
7
|
+
context.state.areCaptionsProvided = true;
|
|
8
|
+
context.captions = options.captions;
|
|
9
|
+
}
|
|
10
|
+
// Ensure caption length is not less than image length
|
|
11
|
+
if (areCaptionsProvided(options.caption, options.captions) && options.captions.length !== context.images.length) {
|
|
12
|
+
throw new MergeError('The same number of captions and images must be provided', { type: 'validation' });
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
const areCaptionsProvided = (caption, captions) => {
|
|
16
|
+
return caption;
|
|
17
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TemplateMerge } from '../types.js';
|
|
2
|
+
import type { Block } from './types.js';
|
|
3
|
+
export interface TemplateState {
|
|
4
|
+
slotWidth: number;
|
|
5
|
+
slotHeight: number;
|
|
6
|
+
canvasWidth: number;
|
|
7
|
+
canvasHeight: number;
|
|
8
|
+
blocks: Block[];
|
|
9
|
+
}
|
|
10
|
+
export declare const templateMerge: TemplateMerge;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MergePipeline } from '../../pipeline/mergePipeline.js';
|
|
2
|
+
import { templateSchema } from '../../schemas/template.js';
|
|
3
|
+
import { loadImages } from '../shared-steps/loadImages.js';
|
|
4
|
+
import { applyComposites } from '../shared-steps/applyComposites.js';
|
|
5
|
+
import { createCanvas } from '../shared-steps/createCanvas.js';
|
|
6
|
+
import { exportCanvas } from '../shared-steps/exportCanvas.js';
|
|
7
|
+
import { calculateCanvasDimensions } from './steps/calculateSlotDimensions.js';
|
|
8
|
+
import { getBlocks } from './steps/getBlocks.js';
|
|
9
|
+
import { createComposites } from './steps/createComposites.js';
|
|
10
|
+
export const templateMerge = async (imageInputs, options, onProgress) => {
|
|
11
|
+
const context = {
|
|
12
|
+
inputs: imageInputs,
|
|
13
|
+
captions: [],
|
|
14
|
+
composites: [],
|
|
15
|
+
images: [],
|
|
16
|
+
state: {},
|
|
17
|
+
};
|
|
18
|
+
const templateMergePipeline = await MergePipeline.createPipeline(templateSchema, options, context, onProgress);
|
|
19
|
+
templateMergePipeline
|
|
20
|
+
.use(loadImages)
|
|
21
|
+
.use(calculateCanvasDimensions)
|
|
22
|
+
.use(getBlocks)
|
|
23
|
+
.use(createCanvas)
|
|
24
|
+
.use(createComposites)
|
|
25
|
+
.use(applyComposites)
|
|
26
|
+
.use(exportCanvas);
|
|
27
|
+
return await templateMergePipeline.run();
|
|
28
|
+
};
|