pixeli 0.1.9 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/README.md +362 -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 +2 -0
  47. package/dist/core/index.js +2 -0
  48. package/dist/core/jobs/batchRunner.d.ts +44 -0
  49. package/dist/core/jobs/batchRunner.js +90 -0
  50. package/dist/core/jobs/types.d.ts +10 -0
  51. package/dist/core/jobs/types.js +1 -0
  52. package/dist/core/mergeError.d.ts +9 -0
  53. package/dist/core/mergeError.js +10 -0
  54. package/dist/core/merges/collage/index.d.ts +9 -0
  55. package/dist/core/merges/collage/index.js +32 -0
  56. package/dist/core/merges/collage/steps/calculateImageDimensions.d.ts +12 -0
  57. package/dist/core/merges/collage/steps/calculateImageDimensions.js +18 -0
  58. package/dist/core/merges/collage/steps/createComposites.d.ts +8 -0
  59. package/dist/core/merges/collage/steps/createComposites.js +58 -0
  60. package/dist/core/merges/collage/steps/resizeAndBorderImages.d.ts +12 -0
  61. package/dist/core/merges/collage/steps/resizeAndBorderImages.js +26 -0
  62. package/dist/core/merges/collage/steps/rotateImages.d.ts +7 -0
  63. package/dist/core/merges/collage/steps/rotateImages.js +9 -0
  64. package/dist/core/merges/grid/index.d.ts +12 -0
  65. package/dist/core/merges/grid/index.js +36 -0
  66. package/dist/core/merges/grid/steps/calculateCanvasDimensions.d.ts +8 -0
  67. package/dist/core/merges/grid/steps/calculateCanvasDimensions.js +18 -0
  68. package/dist/core/merges/grid/steps/calculateFontSize.d.ts +7 -0
  69. package/dist/core/merges/grid/steps/calculateFontSize.js +19 -0
  70. package/dist/core/merges/grid/steps/calculateImageDimensions.d.ts +8 -0
  71. package/dist/core/merges/grid/steps/calculateImageDimensions.js +18 -0
  72. package/dist/core/merges/grid/steps/createComposites.d.ts +10 -0
  73. package/dist/core/merges/grid/steps/createComposites.js +63 -0
  74. package/dist/core/merges/grid/steps/prepareImages.d.ts +10 -0
  75. package/dist/core/merges/grid/steps/prepareImages.js +29 -0
  76. package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.d.ts +7 -0
  77. package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.js +17 -0
  78. package/dist/core/merges/index.d.ts +5 -0
  79. package/dist/core/merges/index.js +4 -0
  80. package/dist/core/merges/masonry/index.d.ts +10 -0
  81. package/dist/core/merges/masonry/index.js +32 -0
  82. package/dist/core/merges/masonry/steps/calculateCanvasDimensions.d.ts +15 -0
  83. package/dist/core/merges/masonry/steps/calculateCanvasDimensions.js +17 -0
  84. package/dist/core/merges/masonry/steps/calculateLaneSize.d.ts +9 -0
  85. package/dist/core/merges/masonry/steps/calculateLaneSize.js +27 -0
  86. package/dist/core/merges/masonry/steps/createComposites.d.ts +25 -0
  87. package/dist/core/merges/masonry/steps/createComposites.js +108 -0
  88. package/dist/core/merges/masonry/steps/resizeImages.d.ts +7 -0
  89. package/dist/core/merges/masonry/steps/resizeImages.js +14 -0
  90. package/dist/core/merges/masonry/steps/splitIntoLanes.d.ts +17 -0
  91. package/dist/core/merges/masonry/steps/splitIntoLanes.js +78 -0
  92. package/dist/core/merges/shared-steps/applyComposites.d.ts +2 -0
  93. package/dist/core/merges/shared-steps/applyComposites.js +16 -0
  94. package/dist/core/merges/shared-steps/createCanvas.d.ts +11 -0
  95. package/dist/core/merges/shared-steps/createCanvas.js +17 -0
  96. package/dist/core/merges/shared-steps/exportCanvas.d.ts +7 -0
  97. package/dist/core/merges/shared-steps/exportCanvas.js +25 -0
  98. package/dist/core/merges/shared-steps/finalizeImagePipelines.d.ts +2 -0
  99. package/dist/core/merges/shared-steps/finalizeImagePipelines.js +9 -0
  100. package/dist/core/merges/shared-steps/loadImages.d.ts +2 -0
  101. package/dist/core/merges/shared-steps/loadImages.js +26 -0
  102. package/dist/core/merges/shared-steps/validateCaptions.d.ts +10 -0
  103. package/dist/core/merges/shared-steps/validateCaptions.js +17 -0
  104. package/dist/core/merges/template/index.d.ts +10 -0
  105. package/dist/core/merges/template/index.js +28 -0
  106. package/dist/core/merges/template/steps/calculateSlotDimensions.d.ts +9 -0
  107. package/dist/core/merges/template/steps/calculateSlotDimensions.js +12 -0
  108. package/dist/core/merges/template/steps/createComposites.d.ts +10 -0
  109. package/dist/core/merges/template/steps/createComposites.js +25 -0
  110. package/dist/core/merges/template/steps/getBlocks.d.ts +13 -0
  111. package/dist/core/merges/template/steps/getBlocks.js +28 -0
  112. package/dist/core/merges/template/types.d.ts +21 -0
  113. package/dist/core/merges/template/types.js +1 -0
  114. package/dist/core/merges/types.d.ts +123 -0
  115. package/dist/core/merges/types.js +1 -0
  116. package/dist/core/modules/messages.d.ts +32 -0
  117. package/dist/core/modules/messages.js +54 -0
  118. package/dist/core/modules/typedEventEmitter.d.ts +7 -0
  119. package/dist/core/modules/typedEventEmitter.js +9 -0
  120. package/dist/core/pipeline/guards.d.ts +4 -0
  121. package/dist/core/pipeline/guards.js +23 -0
  122. package/dist/core/pipeline/mergePipeline.d.ts +60 -0
  123. package/dist/core/pipeline/mergePipeline.js +122 -0
  124. package/dist/core/schemas/collage.d.ts +26 -0
  125. package/dist/core/schemas/collage.js +17 -0
  126. package/dist/core/schemas/grid.d.ts +32 -0
  127. package/dist/core/schemas/grid.js +29 -0
  128. package/dist/core/schemas/masonry.d.ts +56 -0
  129. package/dist/core/schemas/masonry.js +43 -0
  130. package/dist/core/schemas/mergeJob.d.ts +11 -0
  131. package/dist/core/schemas/mergeJob.js +6 -0
  132. package/dist/core/schemas/template.d.ts +34 -0
  133. package/dist/core/schemas/template.js +88 -0
  134. package/dist/core/utils/colors/hexToRgba.d.ts +2 -0
  135. package/dist/core/utils/colors/hexToRgba.js +25 -0
  136. package/dist/core/utils/colors/rgbaToHex.d.ts +2 -0
  137. package/dist/core/utils/colors/rgbaToHex.js +15 -0
  138. package/dist/core/utils/colors/types.d.ts +7 -0
  139. package/dist/core/utils/colors/types.js +1 -0
  140. package/dist/core/utils/fonts/getFontSize.d.ts +9 -0
  141. package/dist/core/utils/fonts/getFontSize.js +40 -0
  142. package/dist/core/utils/images/addImageBorder.d.ts +13 -0
  143. package/dist/core/utils/images/addImageBorder.js +33 -0
  144. package/dist/core/utils/images/getImageHeights.d.ts +2 -0
  145. package/dist/core/utils/images/getImageHeights.js +9 -0
  146. package/dist/core/utils/images/getImageWidths.d.ts +2 -0
  147. package/dist/core/utils/images/getImageWidths.js +9 -0
  148. package/dist/core/utils/images/getSmallestImageDimensions.d.ts +5 -0
  149. package/dist/core/utils/images/getSmallestImageDimensions.js +9 -0
  150. package/dist/core/utils/images/handleImageEdges.d.ts +13 -0
  151. package/dist/core/utils/images/handleImageEdges.js +39 -0
  152. package/dist/core/utils/images/isActualImage.d.ts +5 -0
  153. package/dist/core/utils/images/isActualImage.js +26 -0
  154. package/dist/core/utils/images/parseAspectRatio.d.ts +1 -0
  155. package/dist/core/utils/images/parseAspectRatio.js +22 -0
  156. package/dist/core/utils/images/roundImage.d.ts +9 -0
  157. package/dist/core/utils/images/roundImage.js +27 -0
  158. package/dist/core/utils/images/roundImages.d.ts +8 -0
  159. package/dist/core/utils/images/roundImages.js +19 -0
  160. package/dist/core/utils/images/scaleImage.d.ts +8 -0
  161. package/dist/core/utils/images/scaleImage.js +36 -0
  162. package/dist/core/utils/images/scaleImages.d.ts +8 -0
  163. package/dist/core/utils/images/scaleImages.js +38 -0
  164. package/dist/core/utils/math/median.d.ts +1 -0
  165. package/dist/core/utils/math/median.js +12 -0
  166. package/dist/core/utils/math/randint.d.ts +6 -0
  167. package/dist/core/utils/math/randint.js +11 -0
  168. package/dist/core/utils/math/trimmedMedian.d.ts +1 -0
  169. package/dist/core/utils/math/trimmedMedian.js +12 -0
  170. package/dist/core/utils/svg/createSvgTextBuffer.d.ts +9 -0
  171. package/dist/core/utils/svg/createSvgTextBuffer.js +21 -0
  172. package/dist/validators/aspectRatio.d.ts +2 -0
  173. package/dist/validators/aspectRatio.js +15 -0
  174. package/dist/validators/coercion.d.ts +2 -0
  175. package/dist/validators/coercion.js +12 -0
  176. package/dist/validators/format.d.ts +2 -0
  177. package/dist/validators/format.js +15 -0
  178. package/dist/validators/hexColor.d.ts +7 -0
  179. package/dist/validators/hexColor.js +27 -0
  180. package/dist/validators/index.d.ts +95 -0
  181. package/dist/validators/index.js +64 -0
  182. package/dist/validators/outputFile.d.ts +2 -0
  183. package/dist/validators/outputFile.js +7 -0
  184. package/dist/validators/path.d.ts +3 -0
  185. package/dist/validators/path.js +18 -0
  186. package/dist/validators/sharpImageInput.d.ts +3 -0
  187. package/dist/validators/sharpImageInput.js +6 -0
  188. package/dist/validators/template.d.ts +15 -0
  189. package/dist/validators/template.js +41 -0
  190. package/package.json +26 -9
  191. package/bin/pixeli.js +0 -26
  192. package/commands/merge/collage.js +0 -83
  193. package/commands/merge/grid.js +0 -71
  194. package/commands/merge/helpers/utils.js +0 -11
  195. package/commands/merge/helpers/validations.js +0 -269
  196. package/commands/merge/index.js +0 -12
  197. package/commands/merge/masonry.js +0 -72
  198. package/lib/helpers/loadImages.js +0 -94
  199. package/lib/helpers/progressBar.js +0 -20
  200. package/lib/helpers/templateValidator.js +0 -139
  201. package/lib/helpers/utils.js +0 -208
  202. package/lib/merges/collage-merge/index.js +0 -110
  203. package/lib/merges/collage-merge/presets/artGallery.js +0 -17
  204. package/lib/merges/collage-merge/presets/dashboardShot.js +0 -18
  205. package/lib/merges/collage-merge/presets/horizontalBookSpread.js +0 -15
  206. package/lib/merges/collage-merge/presets/instagramGrid.js +0 -18
  207. package/lib/merges/collage-merge/presets/verticalBookSpread.js +0 -15
  208. package/lib/merges/grid-merge/index.js +0 -152
  209. package/lib/merges/masonry-merge/horizontal.js +0 -157
  210. package/lib/merges/masonry-merge/index.js +0 -57
  211. package/lib/merges/masonry-merge/vertical.js +0 -152
  212. package/lib/merges/merge-utils.js +0 -176
@@ -0,0 +1,55 @@
1
+ import z from 'zod';
2
+ import { Argument, Command, Option } from 'commander';
3
+ const getDefault = (schema) => {
4
+ try {
5
+ return schema.parse(undefined);
6
+ }
7
+ catch {
8
+ return undefined;
9
+ }
10
+ };
11
+ export const buildCommandFromSchema = (name, description, schema, args, options) => {
12
+ // Initialize command
13
+ const command = new Command(name).description(description);
14
+ // Initialize the shape
15
+ let shape = {};
16
+ if (schema instanceof z.ZodUnion) {
17
+ for (const option of schema.options) {
18
+ if (option instanceof z.ZodObject) {
19
+ shape = { ...shape, ...option.shape };
20
+ }
21
+ }
22
+ }
23
+ else {
24
+ shape = schema.shape;
25
+ }
26
+ // Go through schema
27
+ for (const [key, fieldSchema] of Object.entries(shape)) {
28
+ const argConfig = args?.[key];
29
+ const optionConfig = options?.[key];
30
+ let optionOrArgument;
31
+ if (optionConfig !== undefined) {
32
+ optionOrArgument = new Option(optionConfig.flags, optionConfig.description);
33
+ }
34
+ else if (argConfig !== undefined) {
35
+ optionOrArgument = new Argument(argConfig.flags, argConfig.description);
36
+ }
37
+ else
38
+ continue;
39
+ // 1. Try to get enum choices as a set of strings
40
+ const choices = fieldSchema?.options;
41
+ if (choices) {
42
+ optionOrArgument.choices(choices);
43
+ }
44
+ // 2. Try to get any default value set
45
+ const def = getDefault(fieldSchema);
46
+ if (def !== undefined) {
47
+ optionOrArgument.default(def);
48
+ }
49
+ // 3. Add option/argument to command
50
+ optionConfig !== undefined
51
+ ? command.addOption(optionOrArgument)
52
+ : command.addArgument(optionOrArgument);
53
+ }
54
+ return command;
55
+ };
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare const configureCommandErrors: (cmd: Command) => void;
@@ -0,0 +1,22 @@
1
+ import { MessageRenderer } from '../../core/modules/messages.js';
2
+ import chalk from 'chalk';
3
+ export const configureCommandErrors = (cmd) => {
4
+ // Configure error message for the current command
5
+ cmd.configureOutput({
6
+ writeErr: (str) => {
7
+ if (str.includes('error:')) {
8
+ const errorMessage = {
9
+ message: str.trim().replace('error', 'Error'),
10
+ chalk: chalk.red,
11
+ };
12
+ const error = new MessageRenderer(errorMessage);
13
+ return error.render();
14
+ }
15
+ process.stderr.write(str);
16
+ },
17
+ });
18
+ // Recursively configure error messages for all subcommands
19
+ for (const subCmd of cmd.commands) {
20
+ configureCommandErrors(subCmd);
21
+ }
22
+ };
@@ -0,0 +1 @@
1
+ export declare const formatString: (str: string, ...args: (string | number)[]) => string;
@@ -0,0 +1,3 @@
1
+ export const formatString = (str, ...args) => {
2
+ return str.replace(/{(\d+)}/g, (m, i) => (args[Number(i)] !== undefined ? String(args[Number(i)]) : m));
3
+ };
@@ -0,0 +1,4 @@
1
+ export declare const toErrorMessage: (err: unknown) => {
2
+ message: string;
3
+ chalk: import("chalk").ChalkInstance;
4
+ };
@@ -0,0 +1,22 @@
1
+ import z from 'zod';
2
+ import chalk from 'chalk';
3
+ import { MESSAGES } from '../../core/modules/messages.js';
4
+ import { MergeError } from '../../core/mergeError.js';
5
+ export const toErrorMessage = (err) => {
6
+ let errorMessage = MESSAGES.ERROR.INTERNAL;
7
+ // Schema errors
8
+ if (err instanceof z.ZodError) {
9
+ errorMessage = {
10
+ message: z.prettifyError(err),
11
+ chalk: chalk.red,
12
+ };
13
+ }
14
+ // Merge errors
15
+ if (err instanceof MergeError) {
16
+ errorMessage = {
17
+ message: err.message,
18
+ chalk: chalk.red,
19
+ };
20
+ }
21
+ return errorMessage;
22
+ };
@@ -0,0 +1,10 @@
1
+ export declare const SUPPORTED_INPUT_FORMATS: readonly ["webp", "gif", "jpeg", "jpg", "png", "tiff", "avif", "svg"];
2
+ export type SupportedInputFormat = (typeof SUPPORTED_INPUT_FORMATS)[number];
3
+ export declare const SUPPORTED_OUTPUT_FORMATS: readonly ["webp", "gif", "jpeg", "jpg", "png", "tiff", "avif"];
4
+ export type SupportedOutputFormat = (typeof SUPPORTED_OUTPUT_FORMATS)[number];
5
+ export declare const escapeXML: (str: string) => string;
6
+ export declare const shuffleTogether: <T, U>(a: T[], b: U[]) => [T[], U[]];
7
+ export declare const shuffleArray: <T>(array: T[]) => T[];
8
+ export declare const isSupportedInputImage: (extname: string) => extname is SupportedInputFormat;
9
+ export declare const isSupportedOutputImage: (extname: string) => extname is SupportedOutputFormat;
10
+ export declare const isValidHexColor: (hex: string) => boolean;
@@ -0,0 +1,42 @@
1
+ export const SUPPORTED_INPUT_FORMATS = ['webp', 'gif', 'jpeg', 'jpg', 'png', 'tiff', 'avif', 'svg'];
2
+ export const SUPPORTED_OUTPUT_FORMATS = ['webp', 'gif', 'jpeg', 'jpg', 'png', 'tiff', 'avif'];
3
+ export const escapeXML = (str) => {
4
+ return str.replace(/[<>&'"]/g, (c) => ({
5
+ '<': '&lt;',
6
+ '>': '&gt;',
7
+ '&': '&amp;',
8
+ '"': '&quot;',
9
+ "'": '&apos;',
10
+ }[c]));
11
+ };
12
+ export const shuffleTogether = (a, b) => {
13
+ if (a.length !== b.length) {
14
+ throw new Error('Arrays must have the same length');
15
+ }
16
+ const indices = [...a.keys()];
17
+ shuffleArray(indices);
18
+ return [indices.map((i) => a[i]), indices.map((i) => b[i])];
19
+ };
20
+ export const shuffleArray = (array) => {
21
+ let currentIndex = array.length;
22
+ let randomIndex;
23
+ // While there remain elements to shuffle.
24
+ while (currentIndex !== 0) {
25
+ // Pick a remaining element.
26
+ randomIndex = Math.floor(Math.random() * currentIndex);
27
+ currentIndex--;
28
+ // And swap it with the current element.
29
+ [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
30
+ }
31
+ return array;
32
+ };
33
+ export const isSupportedInputImage = (extname) => {
34
+ return SUPPORTED_INPUT_FORMATS.includes(extname);
35
+ };
36
+ export const isSupportedOutputImage = (extname) => {
37
+ return SUPPORTED_OUTPUT_FORMATS.includes(extname);
38
+ };
39
+ export const isValidHexColor = (hex) => {
40
+ const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
41
+ return hexRegex.test(hex);
42
+ };
@@ -0,0 +1,2 @@
1
+ export * from './merges/index.js';
2
+ export { BatchRunner } from './jobs/batchRunner.js';
@@ -0,0 +1,2 @@
1
+ export * from './merges/index.js';
2
+ export { BatchRunner } from './jobs/batchRunner.js';
@@ -0,0 +1,44 @@
1
+ import { MergeError } from '../mergeError.js';
2
+ import { TypedEventEmitter } from '../modules/typedEventEmitter.js';
3
+ import type { BatchOptions, MergeJob } from './types.js';
4
+ type MergeResult = {
5
+ index: number;
6
+ success: true;
7
+ buffer: Buffer;
8
+ } | {
9
+ index: number;
10
+ success: false;
11
+ error: MergeError;
12
+ };
13
+ export interface BatchEvents {
14
+ start: {
15
+ totalJobs: number;
16
+ };
17
+ 'job:start': {
18
+ job: MergeJob;
19
+ index: number;
20
+ };
21
+ 'job:complete': {
22
+ job: MergeJob;
23
+ index: number;
24
+ buffer: Buffer;
25
+ };
26
+ 'job:error': {
27
+ job: MergeJob;
28
+ index: number;
29
+ error: MergeError;
30
+ };
31
+ complete: {
32
+ results: MergeResult[];
33
+ };
34
+ }
35
+ export declare class BatchRunner extends TypedEventEmitter<BatchEvents> {
36
+ private jobs;
37
+ private results;
38
+ private validatedJobs;
39
+ constructor(jobs: MergeJob[]);
40
+ run(options?: BatchOptions): Promise<MergeResult[]>;
41
+ private runJob;
42
+ private validateJob;
43
+ }
44
+ export {};
@@ -0,0 +1,90 @@
1
+ // Merges
2
+ import * as mergeFunctions from '../merges/index.js';
3
+ // Errors
4
+ import { MergeError } from '../mergeError.js';
5
+ import { MESSAGES } from '../modules/messages.js';
6
+ // Other
7
+ import { TypedEventEmitter } from '../modules/typedEventEmitter.js';
8
+ import { mergeJobSchema } from '../schemas/mergeJob.js';
9
+ export class BatchRunner extends TypedEventEmitter {
10
+ jobs;
11
+ results = [];
12
+ validatedJobs = [];
13
+ constructor(jobs) {
14
+ super();
15
+ this.jobs = jobs;
16
+ // Ensure all jobs are valid
17
+ for (const job of jobs) {
18
+ const validatedJob = this.validateJob(job);
19
+ this.validatedJobs.push(validatedJob);
20
+ }
21
+ }
22
+ async run(options = {}) {
23
+ const { stopOnError = false } = options;
24
+ // Run jobs
25
+ for (let i = 0; i < this.validatedJobs.length; i++) {
26
+ const job = this.validatedJobs[i];
27
+ try {
28
+ // Emit job start event
29
+ this.emit('job:start', {
30
+ index: i,
31
+ job: this.jobs[i],
32
+ });
33
+ // Run job
34
+ const buffer = await this.runJob(job);
35
+ // Emit job completion event
36
+ this.emit('job:complete', {
37
+ index: i,
38
+ job: this.jobs[i],
39
+ buffer,
40
+ });
41
+ this.results.push({ index: i, success: true, buffer: buffer });
42
+ }
43
+ catch (error) {
44
+ // Emit job error event
45
+ this.emit('job:error', {
46
+ index: i,
47
+ job: this.jobs[i],
48
+ error: error,
49
+ });
50
+ // Throw error if needed
51
+ if (stopOnError)
52
+ throw error;
53
+ // Store error for later
54
+ this.results.push({ index: i, success: false, error: error });
55
+ }
56
+ }
57
+ // Emit batch job completion event
58
+ this.emit('complete', {
59
+ results: this.results,
60
+ });
61
+ return this.results;
62
+ }
63
+ async runJob(job) {
64
+ // Run job and return resulting buffer
65
+ switch (job.type) {
66
+ case 'grid':
67
+ return mergeFunctions.gridMerge(job.inputs, job.options);
68
+ case 'masonry':
69
+ return mergeFunctions.masonryMerge(job.inputs, job.options);
70
+ case 'collage':
71
+ return mergeFunctions.collageMerge(job.inputs, job.options);
72
+ case 'template':
73
+ return mergeFunctions.templateMerge(job.inputs, job.options);
74
+ }
75
+ }
76
+ validateJob(job) {
77
+ // Validate job format
78
+ const { success, data, error } = mergeJobSchema.safeParse(job);
79
+ if (!success) {
80
+ const path = error.issues[0]?.path;
81
+ const err = error.issues[0]?.message;
82
+ if (!path || !err) {
83
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: error });
84
+ }
85
+ const errorText = path.length > 0 ? `Invalid value at ${path.join('/')}: ${err}` : `Error: ${err}`;
86
+ throw new MergeError(errorText, { type: 'validation' });
87
+ }
88
+ return data;
89
+ }
90
+ }
@@ -0,0 +1,10 @@
1
+ import type sharp from 'sharp';
2
+ import type { MergeTypeOptions } from '../merges/types.js';
3
+ export type MergeJob = {
4
+ /** File paths to all of the images to load. */
5
+ inputs: sharp.SharpInput[];
6
+ } & MergeTypeOptions;
7
+ export interface BatchOptions {
8
+ /** Throws a `MergeError` instantly and stops the process if true. */
9
+ stopOnError?: boolean;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ interface Context {
2
+ type: 'validation' | 'image' | 'internal';
3
+ cause?: any;
4
+ }
5
+ export declare class MergeError extends Error {
6
+ readonly context: Context;
7
+ constructor(message: string, context: Context);
8
+ }
9
+ export {};
@@ -0,0 +1,10 @@
1
+ export class MergeError extends Error {
2
+ context;
3
+ constructor(message, context) {
4
+ super(message);
5
+ this.name = 'MergeError';
6
+ this.context = context;
7
+ // Fix prototype chain
8
+ Object.setPrototypeOf(this, new.target.prototype);
9
+ }
10
+ }
@@ -0,0 +1,9 @@
1
+ import type { CollageMerge } from '../types.js';
2
+ export interface CollageState {
3
+ imageWidth: number;
4
+ imageHeight: number;
5
+ canvasWidth: number;
6
+ canvasHeight: number;
7
+ rows: number;
8
+ }
9
+ export declare const collageMerge: CollageMerge;
@@ -0,0 +1,32 @@
1
+ import { collageSchema } from '../../schemas/collage.js';
2
+ import { MergePipeline } from '../../pipeline/mergePipeline.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 { finalizeImagePipelines } from '../shared-steps/finalizeImagePipelines.js';
8
+ import { calculateImageDimensions } from './steps/calculateImageDimensions.js';
9
+ import { resizeAndBorderImages } from './steps/resizeAndBorderImages.js';
10
+ import { rotateImages } from './steps/rotateImages.js';
11
+ import { createComposites } from './steps/createComposites.js';
12
+ export const collageMerge = async (imageInputs, options, onProgress) => {
13
+ const context = {
14
+ inputs: imageInputs,
15
+ captions: [],
16
+ composites: [],
17
+ images: [],
18
+ state: {},
19
+ };
20
+ const collageMergePipeline = await MergePipeline.createPipeline(collageSchema, options, context, onProgress);
21
+ collageMergePipeline
22
+ .use(loadImages)
23
+ .use(calculateImageDimensions)
24
+ .use(resizeAndBorderImages)
25
+ .use(rotateImages)
26
+ .use(finalizeImagePipelines)
27
+ .use(createComposites)
28
+ .use(createCanvas)
29
+ .use(applyComposites)
30
+ .use(exportCanvas);
31
+ return await collageMergePipeline.run();
32
+ };
@@ -0,0 +1,12 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { CollageState } from '../index.js';
3
+ interface Options {
4
+ imageWidth?: number | undefined;
5
+ aspectRatio: number;
6
+ imageWidthVariance: number;
7
+ gap: number;
8
+ columns: number;
9
+ overlapPercentage: number;
10
+ }
11
+ export declare const calculateImageDimensions: MergeStep<Options, CollageState>;
12
+ export {};
@@ -0,0 +1,18 @@
1
+ import { requireNonEmptyArray } from '../../../pipeline/guards.js';
2
+ import { MESSAGES } from '../../../modules/messages.js';
3
+ import { MergeError } from '../../../mergeError.js';
4
+ import { getImageWidths } from '../../../utils/images/getImageWidths.js';
5
+ import { trimmedMedian } from '../../../utils/math/trimmedMedian.js';
6
+ export const calculateImageDimensions = async (context, options, _onProgress) => {
7
+ requireNonEmptyArray(context.images, 'images');
8
+ // Calculate image width
9
+ const width = options.imageWidth || trimmedMedian(await getImageWidths(context.images));
10
+ if (width === null) {
11
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
12
+ }
13
+ const height = Math.floor(width / options.aspectRatio);
14
+ const rows = Math.ceil(context.images.length / options.columns);
15
+ context.state.imageWidth = width;
16
+ context.state.imageHeight = height;
17
+ context.state.rows = rows;
18
+ };
@@ -0,0 +1,8 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { CollageState } from '../index.js';
3
+ interface Options {
4
+ overlapPercentage: number;
5
+ columns: number;
6
+ }
7
+ export declare const createComposites: MergeStep<Options, CollageState>;
8
+ export {};
@@ -0,0 +1,58 @@
1
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
2
+ import { shuffleArray } from '../../../helpers.js';
3
+ export const createComposites = async (context, options, onProgress) => {
4
+ requireState(context, 'rows');
5
+ requireNonEmptyArray(context.images, 'images');
6
+ // Used to track canvas bounds
7
+ let minX = Infinity;
8
+ let minY = Infinity;
9
+ let maxX = -Infinity;
10
+ let maxY = -Infinity;
11
+ for (let i = 0; i < context.state.rows; i++) {
12
+ for (let j = 0; j < options.columns; j++) {
13
+ const index = i * options.columns + j;
14
+ if (index >= context.images.length) {
15
+ break;
16
+ }
17
+ const image = context.images[index];
18
+ const meta = await image.metadata();
19
+ // Calculate overlap
20
+ const widthOverlap = context.state.imageWidth * (options.overlapPercentage / 100);
21
+ const heightOverlap = context.state.imageHeight * (options.overlapPercentage / 100);
22
+ // Calculate image center
23
+ const cx = context.state.imageWidth * i - widthOverlap * i + context.state.imageWidth / 2;
24
+ const cy = context.state.imageHeight * j - heightOverlap * j + context.state.imageHeight / 2;
25
+ // Re-center rotated bitmap on original image center
26
+ const top = Math.round(cx - meta.width / 2);
27
+ const left = Math.round(cy - meta.height / 2);
28
+ // Add to composites
29
+ context.composites.push({
30
+ input: await image.toBuffer(),
31
+ left,
32
+ top,
33
+ });
34
+ // Update bounds
35
+ minX = Math.min(minX, left);
36
+ minY = Math.min(minY, top);
37
+ maxX = Math.max(maxX, left + meta.width);
38
+ maxY = Math.max(maxY, top + meta.height);
39
+ if (onProgress) {
40
+ context.progressInfo.completed += 1;
41
+ context.progressInfo.phase = 'Merging images';
42
+ onProgress({ ...context.progressInfo });
43
+ }
44
+ }
45
+ }
46
+ // Offset all coords to avoid negatives
47
+ const offsetX = -minX;
48
+ const offsetY = -minY;
49
+ for (const c of context.composites) {
50
+ c.left += offsetX;
51
+ c.top += offsetY;
52
+ }
53
+ // Update composites
54
+ context.composites = shuffleArray(context.composites);
55
+ // Update canvas width and height
56
+ context.state.canvasWidth = Math.ceil(maxX - minX);
57
+ context.state.canvasHeight = Math.ceil(maxY - minY);
58
+ };
@@ -0,0 +1,12 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { CollageState } from '../index.js';
3
+ import type { RGBA } from '../../../utils/colors/types.js';
4
+ interface Options {
5
+ imageWidthVariance: number;
6
+ aspectRatio: number;
7
+ borderWidth: number;
8
+ cornerRadius: number;
9
+ borderColor: RGBA;
10
+ }
11
+ export declare const resizeAndBorderImages: MergeStep<Options, CollageState>;
12
+ export {};
@@ -0,0 +1,26 @@
1
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
2
+ import { randint } from '../../../utils/math/randint.js';
3
+ import { scaleImage } from '../../../utils/images/scaleImage.js';
4
+ import { handleImageEdges } from '../../../utils/images/handleImageEdges.js';
5
+ export const resizeAndBorderImages = async (context, options, _onProgress) => {
6
+ requireState(context, 'imageWidth');
7
+ requireState(context, 'imageHeight');
8
+ requireNonEmptyArray(context.images, 'images');
9
+ for (let i = 0; i < context.images.length; i++) {
10
+ const varianceWidth = randint(-options.imageWidthVariance, options.imageWidthVariance);
11
+ const width = context.state.imageWidth + varianceWidth;
12
+ const varianceHeight = Math.floor(varianceWidth / options.aspectRatio);
13
+ const height = context.state.imageHeight + varianceHeight;
14
+ const resizedImage = await scaleImage(context.images[i], { width, height });
15
+ const borderedImage = await handleImageEdges(resizedImage, {
16
+ imageWidth: width,
17
+ imageHeight: height,
18
+ borderWidth: options.borderWidth,
19
+ borderHeight: options.borderWidth,
20
+ borderColor: options.borderColor,
21
+ cornerRadius: options.cornerRadius,
22
+ finalizePipeline: true,
23
+ });
24
+ context.images[i] = borderedImage;
25
+ }
26
+ };
@@ -0,0 +1,7 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { CollageState } from '../index.js';
3
+ interface Options {
4
+ rotationRange: number;
5
+ }
6
+ export declare const rotateImages: MergeStep<Options, CollageState>;
7
+ export {};
@@ -0,0 +1,9 @@
1
+ import { requireNonEmptyArray } from '../../../pipeline/guards.js';
2
+ import { randint } from '../../../utils/math/randint.js';
3
+ export const rotateImages = async (context, options, _onProgress) => {
4
+ requireNonEmptyArray(context.images, 'images');
5
+ for (let i = 0; i < context.images.length; i++) {
6
+ const angle = randint(-options.rotationRange, options.rotationRange);
7
+ context.images[i] = context.images[i].toFormat('png').rotate(angle, { background: { r: 0, g: 0, b: 0, alpha: 0 } });
8
+ }
9
+ };
@@ -0,0 +1,12 @@
1
+ import type { GridMerge } from '../types.js';
2
+ export interface GridState {
3
+ areCaptionsProvided: boolean;
4
+ imageWidth: number;
5
+ imageHeight: number;
6
+ canvasWidth: number;
7
+ canvasHeight: number;
8
+ captionHeight: number;
9
+ fontSize: number;
10
+ rows: number;
11
+ }
12
+ export declare const gridMerge: GridMerge;
@@ -0,0 +1,36 @@
1
+ import { gridSchema } from '../../schemas/grid.js';
2
+ import { MergePipeline } from '../../pipeline/mergePipeline.js';
3
+ import { loadImages } from '../shared-steps/loadImages.js';
4
+ import { validateCaptions } from '../shared-steps/validateCaptions.js';
5
+ import { applyComposites } from '../shared-steps/applyComposites.js';
6
+ import { createCanvas } from '../shared-steps/createCanvas.js';
7
+ import { exportCanvas } from '../shared-steps/exportCanvas.js';
8
+ import { shuffleImagesAndCaptions } from './steps/shuffleImagesAndCaptions.js';
9
+ import { calculateImageDimensions } from './steps/calculateImageDimensions.js';
10
+ import { prepareImages } from './steps/prepareImages.js';
11
+ import { calculateCanvasDimensions } from './steps/calculateCanvasDimensions.js';
12
+ import { calculateFontSize } from './steps/calculateFontSize.js';
13
+ import { createComposites } from './steps/createComposites.js';
14
+ export const gridMerge = async (imageInputs, options, onProgress) => {
15
+ const context = {
16
+ inputs: imageInputs,
17
+ captions: [],
18
+ composites: [],
19
+ images: [],
20
+ state: {},
21
+ };
22
+ const gridMergePipeline = await MergePipeline.createPipeline(gridSchema, options, context, onProgress);
23
+ gridMergePipeline
24
+ .use(loadImages)
25
+ .use(validateCaptions)
26
+ .use(shuffleImagesAndCaptions)
27
+ .use(calculateImageDimensions)
28
+ .use(prepareImages)
29
+ .use(calculateCanvasDimensions)
30
+ .use(calculateFontSize)
31
+ .use(createCanvas)
32
+ .use(createComposites)
33
+ .use(applyComposites)
34
+ .use(exportCanvas);
35
+ return await gridMergePipeline.run();
36
+ };
@@ -0,0 +1,8 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { GridState } from '../index.js';
3
+ interface Options {
4
+ columns: number;
5
+ gap: number;
6
+ }
7
+ export declare const calculateCanvasDimensions: MergeStep<Options, GridState>;
8
+ export {};
@@ -0,0 +1,18 @@
1
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
2
+ export const calculateCanvasDimensions = async (context, options, _onProgress) => {
3
+ requireState(context, 'imageWidth');
4
+ requireState(context, 'imageHeight');
5
+ requireState(context, 'areCaptionsProvided');
6
+ requireNonEmptyArray(context.images, 'images');
7
+ const CAPTION_HEIGHT_TO_CANVAS_WIDTH_RATIO = 0.04;
8
+ const rows = Math.ceil(context.images.length / options.columns);
9
+ const canvasWidth = context.state.imageWidth * options.columns + (options.columns + 1) * options.gap;
10
+ const captionHeight = Math.floor(canvasWidth * CAPTION_HEIGHT_TO_CANVAS_WIDTH_RATIO);
11
+ const minimumCanvasHeight = context.state.imageHeight * rows + (rows + 1) * options.gap;
12
+ const canvasHeight = context.state.areCaptionsProvided ? minimumCanvasHeight + rows * captionHeight : minimumCanvasHeight;
13
+ // Assign values to state
14
+ context.state.rows = rows;
15
+ context.state.canvasWidth = canvasWidth;
16
+ context.state.canvasHeight = canvasHeight;
17
+ context.state.captionHeight = captionHeight;
18
+ };
@@ -0,0 +1,7 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { GridState } from '../index.js';
3
+ interface Options {
4
+ maxCaptionSize: number;
5
+ }
6
+ export declare const calculateFontSize: MergeStep<Options, GridState>;
7
+ export {};