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,36 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
export const scaleImage = async (image, { width, height, finalizePipeline = false }) => {
|
|
3
|
+
// Ensure either width or height is provided
|
|
4
|
+
if (width == undefined && height === undefined) {
|
|
5
|
+
throw new Error('You must provide either width or height.');
|
|
6
|
+
}
|
|
7
|
+
let targetWidth, targetHeight;
|
|
8
|
+
const meta = await image.metadata();
|
|
9
|
+
// Set target width and height using size factor
|
|
10
|
+
if (width !== undefined && height !== undefined) {
|
|
11
|
+
targetWidth = width;
|
|
12
|
+
targetHeight = height;
|
|
13
|
+
}
|
|
14
|
+
else if (width !== undefined) {
|
|
15
|
+
const f = width / meta.width;
|
|
16
|
+
targetWidth = width;
|
|
17
|
+
targetHeight = Math.floor(meta.height * f);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const f = height / meta.height;
|
|
21
|
+
targetHeight = height;
|
|
22
|
+
targetWidth = Math.floor(meta.width * f);
|
|
23
|
+
}
|
|
24
|
+
// Resize the image
|
|
25
|
+
const resizedImage = image.resize(targetWidth, targetHeight);
|
|
26
|
+
// Only finalize changes in the image pipeline if needed
|
|
27
|
+
if (finalizePipeline) {
|
|
28
|
+
// Use jpg format if possible for less memory usage
|
|
29
|
+
const formatPipe = meta.channels === 4 ? resizedImage.toFormat('png') : resizedImage.toFormat('jpg');
|
|
30
|
+
const buffer = await formatPipe.toBuffer();
|
|
31
|
+
return sharp(buffer);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return resizedImage;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
interface ScaleImagesOptions {
|
|
3
|
+
width?: number;
|
|
4
|
+
height?: number;
|
|
5
|
+
finalizePipeline?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare const scaleImages: (images: sharp.Sharp[], { width, height, finalizePipeline }?: ScaleImagesOptions) => Promise<sharp.Sharp[]>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import sharp from 'sharp';
|
|
2
|
+
export const scaleImages = async (images, { width, height, finalizePipeline = false } = {}) => {
|
|
3
|
+
// Ensure either width or height is provided
|
|
4
|
+
if (width == undefined && height === undefined) {
|
|
5
|
+
throw new Error('You must provide either width or height.');
|
|
6
|
+
}
|
|
7
|
+
// Return scaled images
|
|
8
|
+
const scaledImages = await Promise.all(images.map(async (image) => {
|
|
9
|
+
const meta = await image.metadata();
|
|
10
|
+
let targetWidth, targetHeight;
|
|
11
|
+
if (width !== undefined && height !== undefined) {
|
|
12
|
+
targetWidth = width;
|
|
13
|
+
targetHeight = height;
|
|
14
|
+
}
|
|
15
|
+
else if (width !== undefined) {
|
|
16
|
+
const f = width / meta.width;
|
|
17
|
+
targetWidth = width;
|
|
18
|
+
targetHeight = Math.floor(meta.height * f);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
const f = height / meta.height;
|
|
22
|
+
targetHeight = height;
|
|
23
|
+
targetWidth = Math.floor(meta.width * f);
|
|
24
|
+
}
|
|
25
|
+
const newImage = image.resize(targetWidth, targetHeight);
|
|
26
|
+
// Only finalize changes in the image pipeline if needed
|
|
27
|
+
if (finalizePipeline) {
|
|
28
|
+
// Use jpg format if possible for less memory usage
|
|
29
|
+
const formatPipe = meta.channels === 4 ? newImage.toFormat('png') : newImage.toFormat('jpg');
|
|
30
|
+
const buffer = await formatPipe.toBuffer();
|
|
31
|
+
return sharp(buffer);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
return newImage;
|
|
35
|
+
}
|
|
36
|
+
}));
|
|
37
|
+
return scaledImages;
|
|
38
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function median(values: readonly number[]): number | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function median(values) {
|
|
2
|
+
if (values.length === 0)
|
|
3
|
+
return null;
|
|
4
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
5
|
+
const mid = Math.floor(sorted.length / 2);
|
|
6
|
+
if (sorted.length % 2 === 1) {
|
|
7
|
+
return sorted[mid] ?? null;
|
|
8
|
+
}
|
|
9
|
+
const a = sorted[mid - 1];
|
|
10
|
+
const b = sorted[mid];
|
|
11
|
+
return a !== undefined && b !== undefined ? (a + b) / 2 : null;
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export const randint = (lowOrValue, high) => {
|
|
2
|
+
if (high === undefined) {
|
|
3
|
+
return Math.round(Math.random() * lowOrValue);
|
|
4
|
+
}
|
|
5
|
+
let low = lowOrValue;
|
|
6
|
+
if (high > low) {
|
|
7
|
+
[low, high] = [high, low];
|
|
8
|
+
}
|
|
9
|
+
const range = high - low;
|
|
10
|
+
return Math.round(low + Math.random() * range);
|
|
11
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const trimmedMedian: (values: number[], trimRatio?: number) => number | null;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { median } from './median.js';
|
|
2
|
+
export const trimmedMedian = (values, trimRatio = 0.1) => {
|
|
3
|
+
if (values.length === 0)
|
|
4
|
+
return null;
|
|
5
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
6
|
+
const trim = Math.floor(sorted.length * trimRatio);
|
|
7
|
+
// Prevent trimming everything
|
|
8
|
+
if (trim * 2 >= sorted.length) {
|
|
9
|
+
return median(sorted);
|
|
10
|
+
}
|
|
11
|
+
return median(sorted.slice(trim, sorted.length - trim));
|
|
12
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface CreateSvgTextBufferOptions {
|
|
2
|
+
text: string;
|
|
3
|
+
maxWidth: number;
|
|
4
|
+
maxHeight: number;
|
|
5
|
+
fontSize: number;
|
|
6
|
+
fill?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare const createSvgTextBuffer: ({ text, maxWidth, maxHeight, fontSize, fill }: CreateSvgTextBufferOptions) => Buffer<ArrayBuffer>;
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { escapeXML } from '../../helpers.js';
|
|
2
|
+
export const createSvgTextBuffer = ({ text, maxWidth, maxHeight, fontSize, fill = '#000000' }) => {
|
|
3
|
+
// Width and viewport are assigned to this svg
|
|
4
|
+
const svg = `
|
|
5
|
+
<svg xmlns="http://www.w3.org/2000/svg"
|
|
6
|
+
width="${maxWidth}" height="${maxHeight}"
|
|
7
|
+
viewBox="0 0 ${maxWidth} ${maxHeight}">
|
|
8
|
+
<text
|
|
9
|
+
x="${maxWidth / 2}"
|
|
10
|
+
y="${maxHeight / 2}"
|
|
11
|
+
font-size="${fontSize}"
|
|
12
|
+
font-family="sans-serif"
|
|
13
|
+
fill="${fill}"
|
|
14
|
+
text-anchor="middle"
|
|
15
|
+
dominant-baseline="middle">
|
|
16
|
+
${escapeXML(text)}
|
|
17
|
+
</text>
|
|
18
|
+
</svg>
|
|
19
|
+
`;
|
|
20
|
+
return Buffer.from(svg);
|
|
21
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { parseAspectRatio } from '../core/utils/images/parseAspectRatio.js';
|
|
3
|
+
export const aspectRatioValidator = z.coerce.string().transform((ratio, ctx) => {
|
|
4
|
+
const result = parseAspectRatio(ratio);
|
|
5
|
+
// Validate aspect ratio
|
|
6
|
+
if (!result) {
|
|
7
|
+
ctx.addIssue({
|
|
8
|
+
code: 'custom',
|
|
9
|
+
message: 'Invalid aspect ratio: Examples of valid ratios include 16/9, 2:3, 1x2, 1.77.',
|
|
10
|
+
input: ratio,
|
|
11
|
+
});
|
|
12
|
+
return z.NEVER;
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export const useNumberCoercion = (schema) => {
|
|
3
|
+
return z.preprocess((val) => {
|
|
4
|
+
if (typeof val === 'number')
|
|
5
|
+
return val;
|
|
6
|
+
if (typeof val === 'string' && val.trim() !== '') {
|
|
7
|
+
const n = Number(val);
|
|
8
|
+
return Number.isNaN(n) ? val : n;
|
|
9
|
+
}
|
|
10
|
+
return val;
|
|
11
|
+
}, schema);
|
|
12
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { isSupportedOutputImage, SUPPORTED_OUTPUT_FORMATS } from '../core/helpers.js';
|
|
3
|
+
export const formatValidator = z.string().transform((extname, ctx) => {
|
|
4
|
+
if (isSupportedOutputImage(extname)) {
|
|
5
|
+
return extname;
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
ctx.addIssue({
|
|
9
|
+
code: 'custom',
|
|
10
|
+
path: ['format'],
|
|
11
|
+
message: `Invalid format type. Valid output formats include: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}`,
|
|
12
|
+
});
|
|
13
|
+
return z.NEVER;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const hexColorValidator: z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<import("../core/utils/colors/types.js").RGBA, string>>, z.ZodObject<{
|
|
3
|
+
r: z.ZodNumber;
|
|
4
|
+
g: z.ZodNumber;
|
|
5
|
+
b: z.ZodNumber;
|
|
6
|
+
alpha: z.ZodNumber;
|
|
7
|
+
}, z.z.core.$strip>]>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { isValidHexColor } from '../core/helpers.js';
|
|
3
|
+
import { hexToRgba } from '../core/utils/colors/hexToRgba.js';
|
|
4
|
+
export const hexColorValidator = z.union([
|
|
5
|
+
z.string().transform((color, ctx) => {
|
|
6
|
+
// Handle transparency
|
|
7
|
+
if (color === 'transparent') {
|
|
8
|
+
return hexToRgba('#00000000');
|
|
9
|
+
}
|
|
10
|
+
// Handle hex values
|
|
11
|
+
if (!isValidHexColor(color)) {
|
|
12
|
+
ctx.addIssue({
|
|
13
|
+
code: 'custom',
|
|
14
|
+
message: "Invalid color: must be 'transparent', #rgb, #rrggbb, #rrggbbaa, or object of {r, g, b, a}.",
|
|
15
|
+
input: color,
|
|
16
|
+
});
|
|
17
|
+
return z.NEVER;
|
|
18
|
+
}
|
|
19
|
+
return hexToRgba(color);
|
|
20
|
+
}),
|
|
21
|
+
z.object({
|
|
22
|
+
r: z.number().int().min(0).max(255),
|
|
23
|
+
g: z.number().int().min(0).max(255),
|
|
24
|
+
b: z.number().int().min(0).max(255),
|
|
25
|
+
alpha: z.number().min(0).max(1),
|
|
26
|
+
}, "Invalid color: must be 'transparent', #rgb, #rrggbb, #rrggbbaa, or object of {r, g, b, a}."),
|
|
27
|
+
], "Invalid color: must be 'transparent', #rgb, #rrggbb, #rrggbbaa, or object of {r, g, b, a}.");
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const VALIDATORS: {
|
|
3
|
+
files: z.ZodArray<z.ZodString>;
|
|
4
|
+
dir: z.ZodString;
|
|
5
|
+
output: z.ZodString;
|
|
6
|
+
format: z.ZodPipe<z.ZodString, z.ZodTransform<"webp" | "gif" | "jpeg" | "jpg" | "png" | "tiff" | "avif", string>>;
|
|
7
|
+
cliTemplate: z.ZodString;
|
|
8
|
+
imageInputs: z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>, z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>]>>;
|
|
9
|
+
captions: z.ZodArray<z.ZodString>;
|
|
10
|
+
caption: z.ZodBoolean;
|
|
11
|
+
recursive: z.ZodBoolean;
|
|
12
|
+
shuffle: z.ZodBoolean;
|
|
13
|
+
canvasColor: z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<import("../core/utils/colors/types.js").RGBA, string>>, z.ZodObject<{
|
|
14
|
+
r: z.ZodNumber;
|
|
15
|
+
g: z.ZodNumber;
|
|
16
|
+
b: z.ZodNumber;
|
|
17
|
+
alpha: z.ZodNumber;
|
|
18
|
+
}, z.z.core.$strip>]>;
|
|
19
|
+
captionColor: z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<import("../core/utils/colors/types.js").RGBA, string>>, z.ZodObject<{
|
|
20
|
+
r: z.ZodNumber;
|
|
21
|
+
g: z.ZodNumber;
|
|
22
|
+
b: z.ZodNumber;
|
|
23
|
+
alpha: z.ZodNumber;
|
|
24
|
+
}, z.z.core.$strip>]>;
|
|
25
|
+
borderColor: z.ZodUnion<readonly [z.ZodPipe<z.ZodString, z.ZodTransform<import("../core/utils/colors/types.js").RGBA, string>>, z.ZodObject<{
|
|
26
|
+
r: z.ZodNumber;
|
|
27
|
+
g: z.ZodNumber;
|
|
28
|
+
b: z.ZodNumber;
|
|
29
|
+
alpha: z.ZodNumber;
|
|
30
|
+
}, z.z.core.$strip>]>;
|
|
31
|
+
cornerRadius: z.ZodNumber;
|
|
32
|
+
gap: z.ZodNumber;
|
|
33
|
+
imageWidth: z.ZodNumber;
|
|
34
|
+
columns: z.ZodNumber;
|
|
35
|
+
maxCaptionSize: z.ZodNumber;
|
|
36
|
+
rowHeight: z.ZodNumber;
|
|
37
|
+
columnWidth: z.ZodNumber;
|
|
38
|
+
canvasWidth: z.ZodNumber;
|
|
39
|
+
canvasHeight: z.ZodNumber;
|
|
40
|
+
overlapPercentage: z.ZodNumber;
|
|
41
|
+
rotationRange: z.ZodNumber;
|
|
42
|
+
imageWidthVariance: z.ZodNumber;
|
|
43
|
+
borderWidth: z.ZodNumber;
|
|
44
|
+
cliCornerRadius: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
45
|
+
cliGap: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
46
|
+
cliImageWidth: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
47
|
+
cliColumns: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
48
|
+
cliMaxCaptionSize: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
49
|
+
cliRowHeight: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
50
|
+
cliColumnWidth: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
51
|
+
cliCanvasWidth: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
52
|
+
cliCanvasHeight: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
53
|
+
cliOverlapPercentage: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
54
|
+
cliRotationRange: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
55
|
+
cliImageWidthVariance: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
56
|
+
cliBorderWidth: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
|
|
57
|
+
flow: z.ZodEnum<{
|
|
58
|
+
horizontal: "horizontal";
|
|
59
|
+
vertical: "vertical";
|
|
60
|
+
}>;
|
|
61
|
+
hAlign: z.ZodEnum<{
|
|
62
|
+
left: "left";
|
|
63
|
+
center: "center";
|
|
64
|
+
right: "right";
|
|
65
|
+
justified: "justified";
|
|
66
|
+
}>;
|
|
67
|
+
vAlign: z.ZodEnum<{
|
|
68
|
+
justified: "justified";
|
|
69
|
+
top: "top";
|
|
70
|
+
middle: "middle";
|
|
71
|
+
bottom: "bottom";
|
|
72
|
+
}>;
|
|
73
|
+
preset: z.ZodEnum<{
|
|
74
|
+
"instagram-grid": "instagram-grid";
|
|
75
|
+
"dashboard-shot": "dashboard-shot";
|
|
76
|
+
"horizontal-book-spread": "horizontal-book-spread";
|
|
77
|
+
"vertical-book-spread": "vertical-book-spread";
|
|
78
|
+
"art-gallery": "art-gallery";
|
|
79
|
+
}>;
|
|
80
|
+
template: z.ZodObject<{
|
|
81
|
+
canvas: z.ZodObject<{
|
|
82
|
+
width: z.ZodNumber;
|
|
83
|
+
height: z.ZodNumber;
|
|
84
|
+
columns: z.ZodNumber;
|
|
85
|
+
rows: z.ZodNumber;
|
|
86
|
+
}, z.z.core.$strip>;
|
|
87
|
+
slots: z.ZodArray<z.ZodObject<{
|
|
88
|
+
col: z.ZodNumber;
|
|
89
|
+
row: z.ZodNumber;
|
|
90
|
+
colSpan: z.ZodNumber;
|
|
91
|
+
rowSpan: z.ZodNumber;
|
|
92
|
+
}, z.z.core.$strip>>;
|
|
93
|
+
}, z.z.core.$strip>;
|
|
94
|
+
aspectRatio: z.ZodPipe<z.z.ZodCoercedString<unknown>, z.ZodTransform<number, string>>;
|
|
95
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
import { hexColorValidator } from './hexColor.js';
|
|
3
|
+
import { aspectRatioValidator } from './aspectRatio.js';
|
|
4
|
+
import { sharpImageValidation } from './sharpImageInput.js';
|
|
5
|
+
import { dirPathValidator, filePathValidator } from './path.js';
|
|
6
|
+
import { outputFileValidator } from './outputFile.js';
|
|
7
|
+
import { useNumberCoercion } from './coercion.js';
|
|
8
|
+
import { formatValidator } from './format.js';
|
|
9
|
+
import { templateValidator } from './template.js';
|
|
10
|
+
export const VALIDATORS = {
|
|
11
|
+
// Inputs and outputs
|
|
12
|
+
files: z.array(filePathValidator),
|
|
13
|
+
dir: dirPathValidator,
|
|
14
|
+
output: outputFileValidator,
|
|
15
|
+
format: formatValidator,
|
|
16
|
+
cliTemplate: filePathValidator,
|
|
17
|
+
imageInputs: z.array(sharpImageValidation),
|
|
18
|
+
// Strings
|
|
19
|
+
captions: z.array(z.string()),
|
|
20
|
+
// Flags
|
|
21
|
+
caption: z.boolean(),
|
|
22
|
+
recursive: z.boolean(),
|
|
23
|
+
shuffle: z.boolean(),
|
|
24
|
+
// Colors
|
|
25
|
+
canvasColor: hexColorValidator,
|
|
26
|
+
captionColor: hexColorValidator,
|
|
27
|
+
borderColor: hexColorValidator,
|
|
28
|
+
// Numbers
|
|
29
|
+
cornerRadius: z.number().int().gte(0),
|
|
30
|
+
gap: z.number().gte(0).int(),
|
|
31
|
+
imageWidth: z.number().gt(0).int(),
|
|
32
|
+
columns: z.number().gt(0).int(),
|
|
33
|
+
maxCaptionSize: z.number().gt(0).int(),
|
|
34
|
+
rowHeight: z.number().gt(0).int(),
|
|
35
|
+
columnWidth: z.number().gt(0).int(),
|
|
36
|
+
canvasWidth: z.number().gt(0).int(),
|
|
37
|
+
canvasHeight: z.number().gt(0).int(),
|
|
38
|
+
overlapPercentage: z.number().gte(0).lte(100).int(),
|
|
39
|
+
rotationRange: z.number().gte(0).lte(360).int(),
|
|
40
|
+
imageWidthVariance: z.number().gte(0).int(),
|
|
41
|
+
borderWidth: z.number().gte(0).int(),
|
|
42
|
+
// Coerced Numbers
|
|
43
|
+
cliCornerRadius: useNumberCoercion(z.number().int().gte(0)),
|
|
44
|
+
cliGap: useNumberCoercion(z.number().gte(0).int()),
|
|
45
|
+
cliImageWidth: useNumberCoercion(z.number().gt(0).int()),
|
|
46
|
+
cliColumns: useNumberCoercion(z.number().gt(0).int()),
|
|
47
|
+
cliMaxCaptionSize: useNumberCoercion(z.number().gt(0).int()),
|
|
48
|
+
cliRowHeight: useNumberCoercion(z.number().gt(0).int()),
|
|
49
|
+
cliColumnWidth: useNumberCoercion(z.number().gt(0).int()),
|
|
50
|
+
cliCanvasWidth: useNumberCoercion(z.number().gt(0).int()),
|
|
51
|
+
cliCanvasHeight: useNumberCoercion(z.number().gt(0).int()),
|
|
52
|
+
cliOverlapPercentage: useNumberCoercion(z.number().gte(0).lte(100).int()),
|
|
53
|
+
cliRotationRange: useNumberCoercion(z.number().gte(0).lte(360).int()),
|
|
54
|
+
cliImageWidthVariance: useNumberCoercion(z.number().gte(0).int()),
|
|
55
|
+
cliBorderWidth: useNumberCoercion(z.number().gte(0).int()),
|
|
56
|
+
// Enumerations
|
|
57
|
+
flow: z.enum(['horizontal', 'vertical']),
|
|
58
|
+
hAlign: z.enum(['left', 'center', 'right', 'justified']),
|
|
59
|
+
vAlign: z.enum(['top', 'middle', 'bottom', 'justified']),
|
|
60
|
+
preset: z.enum(['instagram-grid', 'dashboard-shot', 'horizontal-book-spread', 'vertical-book-spread', 'art-gallery']),
|
|
61
|
+
// Misc
|
|
62
|
+
template: templateValidator,
|
|
63
|
+
aspectRatio: aspectRatioValidator,
|
|
64
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
import { SUPPORTED_OUTPUT_FORMATS } from '../core/helpers.js';
|
|
4
|
+
export const outputFileValidator = z.string().refine((outputPath) => {
|
|
5
|
+
const extension = path.extname(outputPath).replace('.', '');
|
|
6
|
+
return SUPPORTED_OUTPUT_FORMATS.includes(extension);
|
|
7
|
+
}, '--output image format is invalid');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import z from 'zod';
|
|
3
|
+
export const filePathValidator = z.string().refine(async (path) => {
|
|
4
|
+
try {
|
|
5
|
+
return (await stat(path)).isFile();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}, { error: 'File path does not exist or is invalid.' });
|
|
11
|
+
export const dirPathValidator = z.string().refine(async (path) => {
|
|
12
|
+
try {
|
|
13
|
+
return (await stat(path)).isDirectory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}, { error: 'Directory path does not exist or is invalid.', abort: true });
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const sharpImageValidation: z.ZodUnion<readonly [z.ZodString, z.ZodCustom<Buffer<ArrayBufferLike>, Buffer<ArrayBufferLike>>, z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>]>;
|
|
3
|
+
export type SharpImageInput = z.infer<typeof sharpImageValidation>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export declare const templateValidator: z.ZodObject<{
|
|
3
|
+
canvas: z.ZodObject<{
|
|
4
|
+
width: z.ZodNumber;
|
|
5
|
+
height: z.ZodNumber;
|
|
6
|
+
columns: z.ZodNumber;
|
|
7
|
+
rows: z.ZodNumber;
|
|
8
|
+
}, z.z.core.$strip>;
|
|
9
|
+
slots: z.ZodArray<z.ZodObject<{
|
|
10
|
+
col: z.ZodNumber;
|
|
11
|
+
row: z.ZodNumber;
|
|
12
|
+
colSpan: z.ZodNumber;
|
|
13
|
+
rowSpan: z.ZodNumber;
|
|
14
|
+
}, z.z.core.$strip>>;
|
|
15
|
+
}, z.z.core.$strip>;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import z from 'zod';
|
|
2
|
+
export const templateValidator = z
|
|
3
|
+
.object({
|
|
4
|
+
canvas: z.object({
|
|
5
|
+
width: z.number().int().positive(),
|
|
6
|
+
height: z.number().int().positive(),
|
|
7
|
+
columns: z.number().int().positive(),
|
|
8
|
+
rows: z.number().int().positive(),
|
|
9
|
+
}),
|
|
10
|
+
slots: z.array(z.object({
|
|
11
|
+
col: z.number().int().positive(),
|
|
12
|
+
row: z.number().int().positive(),
|
|
13
|
+
colSpan: z.number().int().positive(),
|
|
14
|
+
rowSpan: z.number().int().positive(),
|
|
15
|
+
})),
|
|
16
|
+
})
|
|
17
|
+
.superRefine((opts, ctx) => {
|
|
18
|
+
// Ensure slots do not overlap
|
|
19
|
+
const result = validateSlotOverlaps(opts.slots);
|
|
20
|
+
if (!result.success) {
|
|
21
|
+
const [i, j] = result.overlaps;
|
|
22
|
+
ctx.addIssue({ code: 'custom', message: `slot ${i} overlaps with slot ${j}` });
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
const validateSlotOverlaps = (slots) => {
|
|
26
|
+
for (let i = 0; i < slots.length; i++) {
|
|
27
|
+
const A = slots[i];
|
|
28
|
+
const A_right = A.col + A.colSpan - 1;
|
|
29
|
+
const A_bottom = A.row + A.rowSpan - 1;
|
|
30
|
+
for (let j = i + 1; j < slots.length; j++) {
|
|
31
|
+
const B = slots[j];
|
|
32
|
+
const B_right = B.col + B.colSpan - 1;
|
|
33
|
+
const B_bottom = B.row + B.rowSpan - 1;
|
|
34
|
+
const overlap = A.col <= B_right && A_right >= B.col && A.row <= B_bottom && A_bottom >= B.row;
|
|
35
|
+
if (overlap) {
|
|
36
|
+
return { success: false, overlaps: [i, j] };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { success: true };
|
|
41
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pixeli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "A lightweight command-line tool for merging multiple images into customizable grid layouts.",
|
|
5
5
|
"homepage": "https://github.com/pakdad-mousavi/pixeli#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -11,16 +11,21 @@
|
|
|
11
11
|
"url": "git+https://github.com/pakdad-mousavi/pixeli.git"
|
|
12
12
|
},
|
|
13
13
|
"bin": {
|
|
14
|
-
"pixeli": "
|
|
14
|
+
"pixeli": "./dist/cli/index.js"
|
|
15
15
|
},
|
|
16
16
|
"license": "MIT",
|
|
17
17
|
"author": "Pakdad Mousavi",
|
|
18
18
|
"type": "module",
|
|
19
|
-
"main": "index.js",
|
|
19
|
+
"main": "./dist/core/index.js",
|
|
20
|
+
"types": "./dist/core/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": "./dist/core/index.js",
|
|
24
|
+
"types": "./dist/core/index.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
20
27
|
"files": [
|
|
21
|
-
"
|
|
22
|
-
"/bin",
|
|
23
|
-
"/commands"
|
|
28
|
+
"dist"
|
|
24
29
|
],
|
|
25
30
|
"keywords": [
|
|
26
31
|
"cli",
|
|
@@ -30,14 +35,26 @@
|
|
|
30
35
|
"image-merge"
|
|
31
36
|
],
|
|
32
37
|
"scripts": {
|
|
33
|
-
"
|
|
38
|
+
"dev": "tsc --watch",
|
|
39
|
+
"build": "tsc",
|
|
40
|
+
"test": "vitest",
|
|
41
|
+
"test:watch": "vitest --watch",
|
|
42
|
+
"test:coverage": "vitest run --coverage"
|
|
34
43
|
},
|
|
35
44
|
"dependencies": {
|
|
36
|
-
"ajv": "^8.17.1",
|
|
37
45
|
"chalk": "^5.6.2",
|
|
38
46
|
"cli-progress": "^3.12.0",
|
|
39
47
|
"commander": "^14.0.2",
|
|
40
48
|
"sharp": "^0.34.5",
|
|
41
|
-
"
|
|
49
|
+
"zod": "^4.1.13"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/cli-progress": "^3.11.6",
|
|
53
|
+
"@types/node": "^25.0.0",
|
|
54
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
55
|
+
"prettier": "^3.8.0",
|
|
56
|
+
"tsx": "^4.21.0",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"vitest": "^3.2.4"
|
|
42
59
|
}
|
|
43
60
|
}
|
package/bin/pixeli.js
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { Command } from 'commander';
|
|
4
|
-
import mergeCommand from '../commands/merge/index.js';
|
|
5
|
-
import { configureCommandErrors, handleError } from '../lib/helpers/utils.js';
|
|
6
|
-
|
|
7
|
-
const program = new Command();
|
|
8
|
-
|
|
9
|
-
// Define program
|
|
10
|
-
program
|
|
11
|
-
.name('pixeli')
|
|
12
|
-
.description('A lightweight command-line tool for merging multiple images into customizable grid layouts.')
|
|
13
|
-
.version('1.0.0');
|
|
14
|
-
|
|
15
|
-
// Add subcommands
|
|
16
|
-
program.addCommand(mergeCommand);
|
|
17
|
-
|
|
18
|
-
// Configure errors for all subcommands
|
|
19
|
-
configureCommandErrors(program);
|
|
20
|
-
|
|
21
|
-
// Parse arguments
|
|
22
|
-
try {
|
|
23
|
-
program.parse();
|
|
24
|
-
} catch (e) {
|
|
25
|
-
handleError(e);
|
|
26
|
-
}
|