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,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 @@
1
+ export * from './merges/index.js';
@@ -0,0 +1 @@
1
+ export * from './merges/index.js';
@@ -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 {};
@@ -0,0 +1,19 @@
1
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
2
+ import { getFontSize } from '../../../utils/fonts/getFontSize.js';
3
+ export const calculateFontSize = async (context, options, _onProgress) => {
4
+ requireState(context, 'imageWidth');
5
+ requireState(context, 'captionHeight');
6
+ if (!context.state.areCaptionsProvided) {
7
+ return;
8
+ }
9
+ requireNonEmptyArray(context.captions, 'captions');
10
+ const longestCaption = context.captions.reduce((longest, current) => {
11
+ return current.length > longest.length ? current : longest;
12
+ });
13
+ context.state.fontSize = await getFontSize({
14
+ text: longestCaption,
15
+ maxWidth: context.state.imageWidth,
16
+ maxHeight: context.state.captionHeight,
17
+ initialFontSize: options.maxCaptionSize,
18
+ });
19
+ };
@@ -0,0 +1,8 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { GridState } from '../index.js';
3
+ interface Options {
4
+ imageWidth?: number | undefined;
5
+ aspectRatio: number;
6
+ }
7
+ export declare const calculateImageDimensions: MergeStep<Options, GridState>;
8
+ export {};
@@ -0,0 +1,18 @@
1
+ import { MESSAGES } from '../../../modules/messages.js';
2
+ import { MergeError } from '../../../mergeError.js';
3
+ import { getImageWidths } from '../../../utils/images/getImageWidths.js';
4
+ import { trimmedMedian } from '../../../utils/math/trimmedMedian.js';
5
+ import { requireNonEmptyArray } from '../../../pipeline/guards.js';
6
+ export const calculateImageDimensions = async (context, options, _onProgress) => {
7
+ requireNonEmptyArray(context.images, 'images');
8
+ // Calculate image width
9
+ const width = options.imageWidth || trimmedMedian(await getImageWidths(context.images));
10
+ if (width === null) {
11
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
12
+ }
13
+ // Calculate image height
14
+ const height = Math.floor(width / options.aspectRatio);
15
+ // Assign to context
16
+ context.state.imageWidth = width;
17
+ context.state.imageHeight = height;
18
+ };
@@ -0,0 +1,10 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { GridState } from '../index.js';
3
+ import type { RGBA } from '../../../utils/colors/types.js';
4
+ interface Options {
5
+ columns: number;
6
+ gap: number;
7
+ captionColor: RGBA;
8
+ }
9
+ export declare const createComposites: MergeStep<Options, GridState>;
10
+ export {};
@@ -0,0 +1,63 @@
1
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
2
+ import sharp from 'sharp';
3
+ import { createSvgTextBuffer } from '../../../utils/svg/createSvgTextBuffer.js';
4
+ import { rgbaToHex } from '../../../utils/colors/rgbaToHex.js';
5
+ export const createComposites = async (context, options, onProgress) => {
6
+ requireState(context, 'areCaptionsProvided');
7
+ requireState(context, 'captionHeight');
8
+ requireState(context, 'imageWidth');
9
+ requireState(context, 'imageHeight');
10
+ requireState(context, 'rows');
11
+ requireNonEmptyArray(context.images, 'images');
12
+ // Only needed when there are captions
13
+ if (context.state.areCaptionsProvided) {
14
+ requireState(context, 'fontSize');
15
+ }
16
+ const composites = [];
17
+ let x = options.gap;
18
+ let y = options.gap;
19
+ for (let row = 0; row < context.state.rows; row++) {
20
+ for (let col = 0; col < options.columns; col++) {
21
+ const index = row * options.columns + col;
22
+ if (index >= context.images.length)
23
+ break;
24
+ const image = context.images[index];
25
+ composites.push({
26
+ input: await image.toBuffer(),
27
+ left: x,
28
+ top: y,
29
+ });
30
+ // Add caption if required
31
+ if (context.state.areCaptionsProvided) {
32
+ // Create text
33
+ const svgBuffer = createSvgTextBuffer({
34
+ text: context.captions[index],
35
+ maxWidth: context.state.imageWidth,
36
+ maxHeight: context.state.captionHeight,
37
+ fontSize: context.state.fontSize,
38
+ fill: rgbaToHex(options.captionColor),
39
+ });
40
+ // Add text to composites
41
+ composites.push({
42
+ input: svgBuffer,
43
+ left: x,
44
+ top: y + context.state.imageHeight,
45
+ });
46
+ }
47
+ // Update coordinates
48
+ x += context.state.imageWidth + options.gap;
49
+ // Call onProgress
50
+ if (onProgress) {
51
+ context.progressInfo.completed += 1;
52
+ context.progressInfo.phase = 'Merging images';
53
+ onProgress({ ...context.progressInfo });
54
+ }
55
+ }
56
+ // Update coordinates
57
+ y += context.state.areCaptionsProvided
58
+ ? context.state.imageHeight + options.gap + context.state.captionHeight
59
+ : context.state.imageHeight + options.gap;
60
+ x = options.gap;
61
+ }
62
+ context.composites = composites;
63
+ };
@@ -0,0 +1,10 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { GridState } from '../index.js';
3
+ import type { RGBA } from '../../../utils/colors/types.js';
4
+ interface Options {
5
+ cornerRadius: number;
6
+ borderWidth: number;
7
+ borderColor: RGBA;
8
+ }
9
+ export declare const prepareImages: MergeStep<Options, GridState>;
10
+ export {};