pixeli 0.1.9 → 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.
Files changed (204) hide show
  1. package/README.md +341 -88
  2. package/dist/cli/commands/collage/index.d.ts +2 -0
  3. package/dist/cli/commands/collage/index.js +125 -0
  4. package/dist/cli/commands/grid/index.d.ts +2 -0
  5. package/dist/cli/commands/grid/index.js +127 -0
  6. package/dist/cli/commands/masonry/index.d.ts +2 -0
  7. package/dist/cli/commands/masonry/index.js +129 -0
  8. package/dist/cli/commands/template/index.d.ts +2 -0
  9. package/dist/cli/commands/template/index.js +123 -0
  10. package/dist/cli/commands/template/presets/artGallery.d.ts +15 -0
  11. package/dist/cli/commands/template/presets/artGallery.js +15 -0
  12. package/dist/cli/commands/template/presets/dashboardShot.d.ts +15 -0
  13. package/dist/cli/commands/template/presets/dashboardShot.js +16 -0
  14. package/dist/cli/commands/template/presets/horizontalBookSpread.d.ts +15 -0
  15. package/dist/cli/commands/template/presets/horizontalBookSpread.js +13 -0
  16. package/dist/cli/commands/template/presets/instagramGrid.d.ts +15 -0
  17. package/dist/cli/commands/template/presets/instagramGrid.js +16 -0
  18. package/dist/cli/commands/template/presets/verticalBookSpread.d.ts +15 -0
  19. package/dist/cli/commands/template/presets/verticalBookSpread.js +13 -0
  20. package/dist/cli/commands/template/presets.d.ts +73 -0
  21. package/{lib/merges/collage-merge → dist/cli/commands/template}/presets.js +6 -8
  22. package/dist/cli/index.d.ts +2 -0
  23. package/dist/cli/index.js +24 -0
  24. package/dist/cli/modules/loadImages.d.ts +15 -0
  25. package/dist/cli/modules/loadImages.js +74 -0
  26. package/dist/cli/modules/progressBar.d.ts +10 -0
  27. package/dist/cli/modules/progressBar.js +34 -0
  28. package/dist/cli/schemas/collage.d.ts +29 -0
  29. package/dist/cli/schemas/collage.js +38 -0
  30. package/dist/cli/schemas/grid.d.ts +34 -0
  31. package/dist/cli/schemas/grid.js +38 -0
  32. package/dist/cli/schemas/masonry.d.ts +62 -0
  33. package/dist/cli/schemas/masonry.js +62 -0
  34. package/dist/cli/schemas/template.d.ts +31 -0
  35. package/dist/cli/schemas/template.js +49 -0
  36. package/dist/cli/utils/buildCommandFromSchema.d.ts +8 -0
  37. package/dist/cli/utils/buildCommandFromSchema.js +55 -0
  38. package/dist/cli/utils/configureCommandErrors.d.ts +2 -0
  39. package/dist/cli/utils/configureCommandErrors.js +22 -0
  40. package/dist/cli/utils/stringFormatter.d.ts +1 -0
  41. package/dist/cli/utils/stringFormatter.js +3 -0
  42. package/dist/cli/utils/toErrorMessage.d.ts +4 -0
  43. package/dist/cli/utils/toErrorMessage.js +22 -0
  44. package/dist/core/helpers.d.ts +10 -0
  45. package/dist/core/helpers.js +42 -0
  46. package/dist/core/index.d.ts +1 -0
  47. package/dist/core/index.js +1 -0
  48. package/dist/core/mergeError.d.ts +9 -0
  49. package/dist/core/mergeError.js +10 -0
  50. package/dist/core/merges/collage/index.d.ts +9 -0
  51. package/dist/core/merges/collage/index.js +32 -0
  52. package/dist/core/merges/collage/steps/calculateImageDimensions.d.ts +12 -0
  53. package/dist/core/merges/collage/steps/calculateImageDimensions.js +18 -0
  54. package/dist/core/merges/collage/steps/createComposites.d.ts +8 -0
  55. package/dist/core/merges/collage/steps/createComposites.js +58 -0
  56. package/dist/core/merges/collage/steps/resizeAndBorderImages.d.ts +12 -0
  57. package/dist/core/merges/collage/steps/resizeAndBorderImages.js +26 -0
  58. package/dist/core/merges/collage/steps/rotateImages.d.ts +7 -0
  59. package/dist/core/merges/collage/steps/rotateImages.js +9 -0
  60. package/dist/core/merges/grid/index.d.ts +12 -0
  61. package/dist/core/merges/grid/index.js +36 -0
  62. package/dist/core/merges/grid/steps/calculateCanvasDimensions.d.ts +8 -0
  63. package/dist/core/merges/grid/steps/calculateCanvasDimensions.js +18 -0
  64. package/dist/core/merges/grid/steps/calculateFontSize.d.ts +7 -0
  65. package/dist/core/merges/grid/steps/calculateFontSize.js +19 -0
  66. package/dist/core/merges/grid/steps/calculateImageDimensions.d.ts +8 -0
  67. package/dist/core/merges/grid/steps/calculateImageDimensions.js +18 -0
  68. package/dist/core/merges/grid/steps/createComposites.d.ts +10 -0
  69. package/dist/core/merges/grid/steps/createComposites.js +63 -0
  70. package/dist/core/merges/grid/steps/prepareImages.d.ts +10 -0
  71. package/dist/core/merges/grid/steps/prepareImages.js +29 -0
  72. package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.d.ts +7 -0
  73. package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.js +17 -0
  74. package/dist/core/merges/index.d.ts +3 -0
  75. package/dist/core/merges/index.js +3 -0
  76. package/dist/core/merges/masonry/index.d.ts +10 -0
  77. package/dist/core/merges/masonry/index.js +32 -0
  78. package/dist/core/merges/masonry/steps/calculateCanvasDimensions.d.ts +15 -0
  79. package/dist/core/merges/masonry/steps/calculateCanvasDimensions.js +17 -0
  80. package/dist/core/merges/masonry/steps/calculateLaneSize.d.ts +9 -0
  81. package/dist/core/merges/masonry/steps/calculateLaneSize.js +27 -0
  82. package/dist/core/merges/masonry/steps/createComposites.d.ts +25 -0
  83. package/dist/core/merges/masonry/steps/createComposites.js +108 -0
  84. package/dist/core/merges/masonry/steps/resizeImages.d.ts +7 -0
  85. package/dist/core/merges/masonry/steps/resizeImages.js +14 -0
  86. package/dist/core/merges/masonry/steps/splitIntoLanes.d.ts +17 -0
  87. package/dist/core/merges/masonry/steps/splitIntoLanes.js +78 -0
  88. package/dist/core/merges/shared-steps/applyComposites.d.ts +2 -0
  89. package/dist/core/merges/shared-steps/applyComposites.js +16 -0
  90. package/dist/core/merges/shared-steps/createCanvas.d.ts +11 -0
  91. package/dist/core/merges/shared-steps/createCanvas.js +17 -0
  92. package/dist/core/merges/shared-steps/exportCanvas.d.ts +7 -0
  93. package/dist/core/merges/shared-steps/exportCanvas.js +25 -0
  94. package/dist/core/merges/shared-steps/finalizeImagePipelines.d.ts +2 -0
  95. package/dist/core/merges/shared-steps/finalizeImagePipelines.js +9 -0
  96. package/dist/core/merges/shared-steps/loadImages.d.ts +2 -0
  97. package/dist/core/merges/shared-steps/loadImages.js +26 -0
  98. package/dist/core/merges/shared-steps/validateCaptions.d.ts +10 -0
  99. package/dist/core/merges/shared-steps/validateCaptions.js +17 -0
  100. package/dist/core/merges/template/index.d.ts +10 -0
  101. package/dist/core/merges/template/index.js +28 -0
  102. package/dist/core/merges/template/steps/calculateSlotDimensions.d.ts +9 -0
  103. package/dist/core/merges/template/steps/calculateSlotDimensions.js +12 -0
  104. package/dist/core/merges/template/steps/createComposites.d.ts +10 -0
  105. package/dist/core/merges/template/steps/createComposites.js +25 -0
  106. package/dist/core/merges/template/steps/getBlocks.d.ts +13 -0
  107. package/dist/core/merges/template/steps/getBlocks.js +28 -0
  108. package/dist/core/merges/template/types.d.ts +21 -0
  109. package/dist/core/merges/template/types.js +1 -0
  110. package/dist/core/merges/types.d.ts +102 -0
  111. package/dist/core/merges/types.js +1 -0
  112. package/dist/core/modules/messages.d.ts +32 -0
  113. package/dist/core/modules/messages.js +54 -0
  114. package/dist/core/pipeline/guards.d.ts +4 -0
  115. package/dist/core/pipeline/guards.js +23 -0
  116. package/dist/core/pipeline/mergePipeline.d.ts +60 -0
  117. package/dist/core/pipeline/mergePipeline.js +122 -0
  118. package/dist/core/schemas/collage.d.ts +26 -0
  119. package/dist/core/schemas/collage.js +17 -0
  120. package/dist/core/schemas/grid.d.ts +32 -0
  121. package/dist/core/schemas/grid.js +29 -0
  122. package/dist/core/schemas/masonry.d.ts +56 -0
  123. package/dist/core/schemas/masonry.js +43 -0
  124. package/dist/core/schemas/template.d.ts +34 -0
  125. package/dist/core/schemas/template.js +88 -0
  126. package/dist/core/utils/colors/hexToRgba.d.ts +2 -0
  127. package/dist/core/utils/colors/hexToRgba.js +25 -0
  128. package/dist/core/utils/colors/rgbaToHex.d.ts +2 -0
  129. package/dist/core/utils/colors/rgbaToHex.js +15 -0
  130. package/dist/core/utils/colors/types.d.ts +7 -0
  131. package/dist/core/utils/colors/types.js +1 -0
  132. package/dist/core/utils/fonts/getFontSize.d.ts +9 -0
  133. package/dist/core/utils/fonts/getFontSize.js +40 -0
  134. package/dist/core/utils/images/addImageBorder.d.ts +13 -0
  135. package/dist/core/utils/images/addImageBorder.js +33 -0
  136. package/dist/core/utils/images/getImageHeights.d.ts +2 -0
  137. package/dist/core/utils/images/getImageHeights.js +9 -0
  138. package/dist/core/utils/images/getImageWidths.d.ts +2 -0
  139. package/dist/core/utils/images/getImageWidths.js +9 -0
  140. package/dist/core/utils/images/getSmallestImageDimensions.d.ts +5 -0
  141. package/dist/core/utils/images/getSmallestImageDimensions.js +9 -0
  142. package/dist/core/utils/images/handleImageEdges.d.ts +13 -0
  143. package/dist/core/utils/images/handleImageEdges.js +39 -0
  144. package/dist/core/utils/images/isActualImage.d.ts +5 -0
  145. package/dist/core/utils/images/isActualImage.js +26 -0
  146. package/dist/core/utils/images/parseAspectRatio.d.ts +1 -0
  147. package/dist/core/utils/images/parseAspectRatio.js +22 -0
  148. package/dist/core/utils/images/roundImage.d.ts +9 -0
  149. package/dist/core/utils/images/roundImage.js +27 -0
  150. package/dist/core/utils/images/roundImages.d.ts +8 -0
  151. package/dist/core/utils/images/roundImages.js +19 -0
  152. package/dist/core/utils/images/scaleImage.d.ts +8 -0
  153. package/dist/core/utils/images/scaleImage.js +36 -0
  154. package/dist/core/utils/images/scaleImages.d.ts +8 -0
  155. package/dist/core/utils/images/scaleImages.js +38 -0
  156. package/dist/core/utils/math/median.d.ts +1 -0
  157. package/dist/core/utils/math/median.js +12 -0
  158. package/dist/core/utils/math/randint.d.ts +6 -0
  159. package/dist/core/utils/math/randint.js +11 -0
  160. package/dist/core/utils/math/trimmedMedian.d.ts +1 -0
  161. package/dist/core/utils/math/trimmedMedian.js +12 -0
  162. package/dist/core/utils/svg/createSvgTextBuffer.d.ts +9 -0
  163. package/dist/core/utils/svg/createSvgTextBuffer.js +21 -0
  164. package/dist/validators/aspectRatio.d.ts +2 -0
  165. package/dist/validators/aspectRatio.js +15 -0
  166. package/dist/validators/coercion.d.ts +2 -0
  167. package/dist/validators/coercion.js +12 -0
  168. package/dist/validators/format.d.ts +2 -0
  169. package/dist/validators/format.js +15 -0
  170. package/dist/validators/hexColor.d.ts +7 -0
  171. package/dist/validators/hexColor.js +27 -0
  172. package/dist/validators/index.d.ts +95 -0
  173. package/dist/validators/index.js +64 -0
  174. package/dist/validators/outputFile.d.ts +2 -0
  175. package/dist/validators/outputFile.js +7 -0
  176. package/dist/validators/path.d.ts +3 -0
  177. package/dist/validators/path.js +18 -0
  178. package/dist/validators/sharpImageInput.d.ts +3 -0
  179. package/dist/validators/sharpImageInput.js +6 -0
  180. package/dist/validators/template.d.ts +15 -0
  181. package/dist/validators/template.js +41 -0
  182. package/package.json +26 -9
  183. package/bin/pixeli.js +0 -26
  184. package/commands/merge/collage.js +0 -83
  185. package/commands/merge/grid.js +0 -71
  186. package/commands/merge/helpers/utils.js +0 -11
  187. package/commands/merge/helpers/validations.js +0 -269
  188. package/commands/merge/index.js +0 -12
  189. package/commands/merge/masonry.js +0 -72
  190. package/lib/helpers/loadImages.js +0 -94
  191. package/lib/helpers/progressBar.js +0 -20
  192. package/lib/helpers/templateValidator.js +0 -139
  193. package/lib/helpers/utils.js +0 -208
  194. package/lib/merges/collage-merge/index.js +0 -110
  195. package/lib/merges/collage-merge/presets/artGallery.js +0 -17
  196. package/lib/merges/collage-merge/presets/dashboardShot.js +0 -18
  197. package/lib/merges/collage-merge/presets/horizontalBookSpread.js +0 -15
  198. package/lib/merges/collage-merge/presets/instagramGrid.js +0 -18
  199. package/lib/merges/collage-merge/presets/verticalBookSpread.js +0 -15
  200. package/lib/merges/grid-merge/index.js +0 -152
  201. package/lib/merges/masonry-merge/horizontal.js +0 -157
  202. package/lib/merges/masonry-merge/index.js +0 -57
  203. package/lib/merges/masonry-merge/vertical.js +0 -152
  204. 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,6 @@
1
+ interface Randint {
2
+ (low: number, high: number): number;
3
+ (value: number): number;
4
+ }
5
+ export declare const randint: Randint;
6
+ export {};
@@ -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,2 @@
1
+ import z from 'zod';
2
+ export declare const aspectRatioValidator: z.ZodPipe<z.z.ZodCoercedString<unknown>, z.ZodTransform<number, string>>;
@@ -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,2 @@
1
+ import z from 'zod';
2
+ export declare const useNumberCoercion: (schema: z.ZodNumber) => z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodNumber>;
@@ -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,2 @@
1
+ import z from 'zod';
2
+ export declare const formatValidator: z.ZodPipe<z.ZodString, z.ZodTransform<"webp" | "gif" | "jpeg" | "jpg" | "png" | "tiff" | "avif", string>>;
@@ -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,2 @@
1
+ import z from 'zod';
2
+ export declare const outputFileValidator: z.ZodString;
@@ -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,3 @@
1
+ import z from 'zod';
2
+ export declare const filePathValidator: z.ZodString;
3
+ export declare const dirPathValidator: z.ZodString;
@@ -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,6 @@
1
+ import z from 'zod';
2
+ export const sharpImageValidation = z.union([
3
+ z.string(),
4
+ z.instanceof(Buffer), // technically redundant, but verbose
5
+ z.instanceof(Uint8Array),
6
+ ]);
@@ -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.1.9",
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": "bin/pixeli.js"
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
- "/lib",
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
- "test": "echo \"Error: no test specified\" && exit 1"
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
- "table": "^6.9.0"
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
- }