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,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 {};
@@ -0,0 +1,29 @@
1
+ import { scaleImage } from '../../../utils/images/scaleImage.js';
2
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
3
+ import { handleImageEdges } from '../../../utils/images/handleImageEdges.js';
4
+ export const prepareImages = async (context, options, _onProgress) => {
5
+ requireState(context, 'imageWidth');
6
+ requireState(context, 'imageHeight');
7
+ requireNonEmptyArray(context.images, 'images');
8
+ // Get values from context and options
9
+ const width = context.state.imageWidth;
10
+ const height = context.state.imageHeight;
11
+ const cornerRadius = options.cornerRadius;
12
+ for (let i = 0; i < context.images.length; i++) {
13
+ const image = context.images[i];
14
+ // Resize image
15
+ const resizedImage = await scaleImage(image, { width, height });
16
+ // Handle borders and corner rounding
17
+ const borderedImage = await handleImageEdges(resizedImage, {
18
+ imageWidth: width,
19
+ imageHeight: height,
20
+ borderWidth: options.borderWidth,
21
+ borderHeight: options.borderWidth,
22
+ borderColor: options.borderColor,
23
+ cornerRadius,
24
+ finalizePipeline: true,
25
+ });
26
+ // Update context
27
+ context.images[i] = borderedImage;
28
+ }
29
+ };
@@ -0,0 +1,7 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { GridState } from '../index.js';
3
+ interface Options {
4
+ shuffle: boolean;
5
+ }
6
+ export declare const shuffleImagesAndCaptions: MergeStep<Options, GridState>;
7
+ export {};
@@ -0,0 +1,17 @@
1
+ import { shuffleArray, shuffleTogether } from '../../../helpers.js';
2
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
3
+ export const shuffleImagesAndCaptions = async (context, options, _onProgress) => {
4
+ requireState(context, 'areCaptionsProvided');
5
+ requireNonEmptyArray(context.images, 'images');
6
+ // If captions are given and shuffle is true
7
+ if (context.state.areCaptionsProvided && options.shuffle) {
8
+ const [shuffledImages, shuffledCaptions] = shuffleTogether(context.images, context.captions);
9
+ context.images = shuffledImages;
10
+ context.captions = shuffledCaptions;
11
+ }
12
+ // If there are no captions but shuffle is true
13
+ else if (!context.state.areCaptionsProvided && options.shuffle) {
14
+ const shuffledImages = shuffleArray(context.images);
15
+ context.images = shuffledImages;
16
+ }
17
+ };
@@ -0,0 +1,5 @@
1
+ export { gridMerge } from './grid/index.js';
2
+ export { masonryMerge } from './masonry/index.js';
3
+ export { templateMerge } from './template/index.js';
4
+ export { collageMerge } from './collage/index.js';
5
+ export type { GridMergeOptions, MasonryMergeOptions, TemplateMergeOptions, CollageMergeOptions } from './types.js';
@@ -0,0 +1,4 @@
1
+ export { gridMerge } from './grid/index.js';
2
+ export { masonryMerge } from './masonry/index.js';
3
+ export { templateMerge } from './template/index.js';
4
+ export { collageMerge } from './collage/index.js';
@@ -0,0 +1,10 @@
1
+ import type { MasonryMerge } from '../types.js';
2
+ import type sharp from 'sharp';
3
+ export interface MasonryState {
4
+ rowHeight: number;
5
+ columnWidth: number;
6
+ canvasWidth: number;
7
+ canvasHeight: number;
8
+ lanes: sharp.Sharp[][];
9
+ }
10
+ export declare const masonryMerge: MasonryMerge;
@@ -0,0 +1,32 @@
1
+ import { MergePipeline } from '../../pipeline/mergePipeline.js';
2
+ import { masonrySchema } from '../../schemas/masonry.js';
3
+ import { loadImages } from '../shared-steps/loadImages.js';
4
+ import { createCanvas } from '../shared-steps/createCanvas.js';
5
+ import { applyComposites } from '../shared-steps/applyComposites.js';
6
+ import { exportCanvas } from '../shared-steps/exportCanvas.js';
7
+ import { calculateLaneSize } from './steps/calculateLaneSize.js';
8
+ import { resizeImages } from './steps/resizeImages.js';
9
+ import { splitIntoLanes } from './steps/splitIntoLanes.js';
10
+ import { calculateCanvasDimensions } from './steps/calculateCanvasDimensions.js';
11
+ import { createComposites } from './steps/createComposites.js';
12
+ export const masonryMerge = async (imageInputs, options, onProgress) => {
13
+ const context = {
14
+ inputs: imageInputs,
15
+ captions: [],
16
+ composites: [],
17
+ images: [],
18
+ state: {},
19
+ };
20
+ const masonryMergePipeline = await MergePipeline.createPipeline(masonrySchema, options, context, onProgress);
21
+ masonryMergePipeline
22
+ .use(loadImages)
23
+ .use(calculateLaneSize)
24
+ .use(resizeImages)
25
+ .use(splitIntoLanes)
26
+ .use(calculateCanvasDimensions)
27
+ .use(createCanvas)
28
+ .use(createComposites)
29
+ .use(applyComposites)
30
+ .use(exportCanvas);
31
+ return await masonryMergePipeline.run();
32
+ };
@@ -0,0 +1,15 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { MasonryState } from '../index.js';
3
+ interface HorizontalOptions {
4
+ flow: 'horizontal';
5
+ gap: number;
6
+ canvasWidth: number;
7
+ }
8
+ interface VerticalOptions {
9
+ flow: 'vertical';
10
+ gap: number;
11
+ canvasHeight: number;
12
+ }
13
+ type Options = HorizontalOptions | VerticalOptions;
14
+ export declare const calculateCanvasDimensions: MergeStep<Options, MasonryState>;
15
+ export {};
@@ -0,0 +1,17 @@
1
+ import { requireState } from '../../../pipeline/guards.js';
2
+ export const calculateCanvasDimensions = async (context, options, _onProgress) => {
3
+ // Mandatory regardless of flow
4
+ requireState(context, 'lanes');
5
+ // Put both canvas width and height in context state
6
+ const totalLanes = context.state.lanes.length;
7
+ if (options.flow === 'horizontal') {
8
+ requireState(context, 'rowHeight');
9
+ context.state.canvasWidth = options.canvasWidth;
10
+ context.state.canvasHeight = totalLanes * context.state.rowHeight + (totalLanes + 1) * options.gap;
11
+ }
12
+ else {
13
+ requireState(context, 'columnWidth');
14
+ context.state.canvasHeight = options.canvasHeight;
15
+ context.state.canvasWidth = totalLanes * context.state.columnWidth + (totalLanes + 1) * options.gap;
16
+ }
17
+ };
@@ -0,0 +1,9 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { MasonryState } from '../index.js';
3
+ interface Options {
4
+ rowHeight?: number | undefined;
5
+ columnWidth?: number | undefined;
6
+ flow: 'horizontal' | 'vertical';
7
+ }
8
+ export declare const calculateLaneSize: MergeStep<Options, MasonryState>;
9
+ export {};
@@ -0,0 +1,27 @@
1
+ import { trimmedMedian } from '../../../utils/math/trimmedMedian.js';
2
+ import { getImageWidths } from '../../../utils/images/getImageWidths.js';
3
+ import { getImageHeights } from '../../../utils/images/getImageHeights.js';
4
+ import { MESSAGES } from '../../../modules/messages.js';
5
+ import { MergeError } from '../../../mergeError.js';
6
+ import { requireNonEmptyArray } from '../../../pipeline/guards.js';
7
+ export const calculateLaneSize = async (context, options, _onProgress) => {
8
+ requireNonEmptyArray(context.images, 'images');
9
+ if (options.flow === 'horizontal') {
10
+ // Calculate rowHeight
11
+ const rowHeight = options.rowHeight || trimmedMedian(await getImageHeights(context.images));
12
+ if (rowHeight === null) {
13
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
14
+ }
15
+ // Set rowHeight
16
+ context.state.rowHeight = rowHeight;
17
+ }
18
+ else {
19
+ // Calculate columnWidth
20
+ const columnWidth = options.columnWidth || trimmedMedian(await getImageWidths(context.images));
21
+ if (columnWidth === null) {
22
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: 'trimmedMedian failed' });
23
+ }
24
+ // Set columnWidth
25
+ context.state.columnWidth = columnWidth;
26
+ }
27
+ };
@@ -0,0 +1,25 @@
1
+ import type { MasonryState } from '../index.js';
2
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
3
+ import type { RGBA } from '../../../utils/colors/types.js';
4
+ interface BaseOptions {
5
+ flow: 'horizontal' | 'vertical';
6
+ gap: number;
7
+ cornerRadius: number;
8
+ borderWidth: number;
9
+ borderColor: RGBA;
10
+ }
11
+ interface HorizontalOptions extends BaseOptions {
12
+ flow: 'horizontal';
13
+ gap: number;
14
+ canvasWidth: number;
15
+ hAlign: 'justified' | 'left' | 'center' | 'right';
16
+ }
17
+ interface VerticalOptions extends BaseOptions {
18
+ flow: 'vertical';
19
+ gap: number;
20
+ canvasHeight: number;
21
+ vAlign: 'justified' | 'top' | 'middle' | 'bottom';
22
+ }
23
+ type Options = HorizontalOptions | VerticalOptions;
24
+ export declare const createComposites: MergeStep<Options, MasonryState>;
25
+ export {};
@@ -0,0 +1,108 @@
1
+ import sharp from 'sharp';
2
+ import { requireState } from '../../../pipeline/guards.js';
3
+ import { handleImageEdges } from '../../../utils/images/handleImageEdges.js';
4
+ // |----------------------|
5
+ // |----------------------|
6
+ // | ROW OFFSET FUNCTION |
7
+ // |----------------------|
8
+ // |----------------------|
9
+ const computeOffset = async (flow, lane, canvasSize, gap, alignment) => {
10
+ // Calculate total row width
11
+ let totalLaneLength = gap * (lane.length + 1);
12
+ for (const im of lane) {
13
+ const meta = await im.metadata();
14
+ totalLaneLength += flow === 'horizontal' ? meta.width : meta.height;
15
+ }
16
+ // Get x offset
17
+ switch (alignment) {
18
+ case 'justified':
19
+ case 'left':
20
+ case 'top':
21
+ return gap;
22
+ case 'right':
23
+ case 'bottom':
24
+ return canvasSize - totalLaneLength + gap;
25
+ case 'middle':
26
+ case 'center':
27
+ const canvasGap = gap * 2;
28
+ return Math.floor((canvasSize + canvasGap - totalLaneLength) / 2);
29
+ }
30
+ };
31
+ const horizontalAxis = {
32
+ flow: 'horizontal',
33
+ getPrimary: (meta) => meta.width,
34
+ getCross: (meta) => meta.height,
35
+ crop: (meta, overflow) => ({
36
+ width: meta.width - overflow,
37
+ height: meta.height,
38
+ fit: 'cover',
39
+ }),
40
+ };
41
+ const verticalAxis = {
42
+ flow: 'vertical',
43
+ getPrimary: (meta) => meta.height,
44
+ getCross: (meta) => meta.width,
45
+ crop: (meta, overflow) => ({
46
+ width: meta.width,
47
+ height: meta.height - overflow,
48
+ fit: 'cover',
49
+ }),
50
+ };
51
+ export const createComposites = async (context, options, onProgress) => {
52
+ // Require needed states
53
+ requireState(context, 'lanes');
54
+ options.flow === 'horizontal' ? requireState(context, 'rowHeight') : requireState(context, 'columnWidth');
55
+ // Initialize flow-dependent values
56
+ const laneCrossSize = options.flow === 'horizontal' ? context.state.rowHeight : context.state.columnWidth;
57
+ const axis = options.flow === 'horizontal' ? horizontalAxis : verticalAxis;
58
+ const alignment = options.flow === 'horizontal' ? options.hAlign : options.vAlign;
59
+ const primaryCanvasSize = options.flow === 'horizontal' ? options.canvasWidth : options.canvasHeight;
60
+ // Define initial variables
61
+ const composites = [];
62
+ let primaryCursor = options.gap;
63
+ let crossCursor = options.gap;
64
+ for (const lane of context.state.lanes) {
65
+ // Get the lanes vertical/horizontal offset
66
+ let primary = await computeOffset(options.flow, lane, primaryCanvasSize, options.gap, alignment);
67
+ let cross = crossCursor;
68
+ for (const im of lane) {
69
+ let finalizedImage = im;
70
+ let meta = await im.metadata();
71
+ // Update x/y position
72
+ primaryCursor += axis.getPrimary(meta) + options.gap;
73
+ // Handle image overflows for justified layouts
74
+ if (primaryCursor >= primaryCanvasSize) {
75
+ const overflow = primaryCursor - primaryCanvasSize;
76
+ const buffer = await im.resize(axis.crop(meta, overflow)).toBuffer();
77
+ finalizedImage = sharp(buffer);
78
+ meta = await finalizedImage.metadata();
79
+ }
80
+ // Handle borders and corner rounding
81
+ const borderedImage = await handleImageEdges(finalizedImage, {
82
+ imageWidth: meta.width,
83
+ imageHeight: meta.height,
84
+ borderWidth: options.borderWidth,
85
+ borderHeight: options.borderWidth,
86
+ borderColor: options.borderColor,
87
+ cornerRadius: options.cornerRadius,
88
+ });
89
+ // Create the composite
90
+ composites.push({
91
+ input: await borderedImage.toBuffer(),
92
+ left: options.flow === 'horizontal' ? primary : cross,
93
+ top: options.flow === 'horizontal' ? cross : primary,
94
+ });
95
+ primary += axis.getPrimary(meta) + options.gap;
96
+ // Update progress
97
+ if (onProgress) {
98
+ context.progressInfo.phase = 'Merging images';
99
+ context.progressInfo.completed += 1;
100
+ onProgress({ ...context.progressInfo });
101
+ }
102
+ }
103
+ primaryCursor = options.gap;
104
+ crossCursor += laneCrossSize + options.gap;
105
+ }
106
+ // Update context.composites
107
+ context.composites = composites;
108
+ };
@@ -0,0 +1,7 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { MasonryState } from '../index.js';
3
+ interface Options {
4
+ flow: 'horizontal' | 'vertical';
5
+ }
6
+ export declare const resizeImages: MergeStep<Options, MasonryState>;
7
+ export {};
@@ -0,0 +1,14 @@
1
+ import { requireNonEmptyArray, requireState } from '../../../pipeline/guards.js';
2
+ import { scaleImage } from '../../../utils/images/scaleImage.js';
3
+ export const resizeImages = async (context, options, _onProgress) => {
4
+ requireNonEmptyArray(context.images, 'images');
5
+ // Require either rowHeight or columnWidth
6
+ options.flow === 'horizontal' ? requireState(context, 'rowHeight') : requireState(context, 'columnWidth');
7
+ // Rescale images to match rowHeight or columnWidth
8
+ const scaleOptions = options.flow === 'horizontal'
9
+ ? { height: context.state.rowHeight, finalizePipeline: true }
10
+ : { width: context.state.columnWidth, finalizePipeline: true };
11
+ for (let i = 0; i < context.images.length; i++) {
12
+ context.images[i] = await scaleImage(context.images[i], scaleOptions);
13
+ }
14
+ };
@@ -0,0 +1,17 @@
1
+ import type { MergeStep } from '../../../pipeline/mergePipeline.js';
2
+ import type { MasonryState } from '../index.js';
3
+ interface HorizontalOptions {
4
+ flow: 'horizontal';
5
+ gap: number;
6
+ canvasWidth: number;
7
+ hAlign: 'justified' | 'left' | 'center' | 'right';
8
+ }
9
+ interface VerticalOptions {
10
+ flow: 'vertical';
11
+ gap: number;
12
+ canvasHeight: number;
13
+ vAlign: 'justified' | 'top' | 'middle' | 'bottom';
14
+ }
15
+ type Options = HorizontalOptions | VerticalOptions;
16
+ export declare const splitIntoLanes: MergeStep<Options, MasonryState>;
17
+ export {};
@@ -0,0 +1,78 @@
1
+ import { requireNonEmptyArray } from '../../../pipeline/guards.js';
2
+ export const splitIntoLanes = async (context, options, _onProgress) => {
3
+ requireNonEmptyArray(context.images, 'images');
4
+ // Split into lanes
5
+ const lanes = options.flow === 'horizontal'
6
+ ? await splitIntoRows(context.images, options.canvasWidth, options.gap, options.hAlign)
7
+ : await splitIntoColumns(context.images, options.canvasHeight, options.gap, options.vAlign);
8
+ // Assign lanes
9
+ context.state.lanes = lanes;
10
+ };
11
+ const splitIntoRows = async (images, canvasWidth, gap, hAlign) => {
12
+ const rows = [];
13
+ let currentRow = [];
14
+ let currentWidth = gap; // initial leading gap
15
+ for (const im of images) {
16
+ const meta = await im.metadata();
17
+ const nextWidth = currentWidth + meta.width + gap;
18
+ if (hAlign === 'justified') {
19
+ // Greedy: always push image, fix overflow later
20
+ currentRow.push(im);
21
+ currentWidth = nextWidth;
22
+ if (currentWidth + gap >= canvasWidth) {
23
+ rows.push(currentRow.slice());
24
+ currentRow.length = 0;
25
+ currentWidth = gap;
26
+ }
27
+ }
28
+ else {
29
+ // Non-greedy: break BEFORE adding image that doesn't fit
30
+ if (nextWidth > canvasWidth && currentRow.length > 0) {
31
+ rows.push(currentRow.slice());
32
+ currentRow = [];
33
+ currentWidth = gap;
34
+ }
35
+ // Add the image (may be first in a new row)
36
+ currentRow.push(im);
37
+ currentWidth += meta.width + gap;
38
+ }
39
+ }
40
+ if (currentRow.length > 0) {
41
+ rows.push(currentRow);
42
+ }
43
+ return rows;
44
+ };
45
+ const splitIntoColumns = async (images, canvasHeight, gap, vAlign) => {
46
+ const cols = [];
47
+ const currentCol = [];
48
+ let currentHeight = gap;
49
+ for (const im of images) {
50
+ const meta = await im.metadata();
51
+ let nextHeight = currentHeight + meta.height + gap;
52
+ if (vAlign === 'justified') {
53
+ // Greedy: always push image, fix overflow later
54
+ currentCol.push(im);
55
+ currentHeight = nextHeight;
56
+ if (currentHeight + gap >= canvasHeight) {
57
+ cols.push(currentCol.slice());
58
+ currentCol.length = 0;
59
+ currentHeight = gap;
60
+ }
61
+ }
62
+ else {
63
+ // Non-greedy: break BEFORE adding image that doesn't fit
64
+ if (nextHeight > canvasHeight && currentCol.length > 0) {
65
+ cols.push(currentCol.slice());
66
+ currentCol.length = 0;
67
+ currentHeight = gap;
68
+ }
69
+ // Add the image (may be first in a new column)
70
+ currentCol.push(im);
71
+ currentHeight += meta.height + gap;
72
+ }
73
+ }
74
+ if (currentCol.length > 0) {
75
+ cols.push(currentCol);
76
+ }
77
+ return cols;
78
+ };
@@ -0,0 +1,2 @@
1
+ import type { MergeStep } from '../../pipeline/mergePipeline.js';
2
+ export declare const applyComposites: MergeStep<any, any>;
@@ -0,0 +1,16 @@
1
+ import { MESSAGES } from '../../modules/messages.js';
2
+ import { MergeError } from '../../mergeError.js';
3
+ import { requireContextProp } from '../../pipeline/guards.js';
4
+ export const applyComposites = async (context, _options, _onProgress) => {
5
+ requireContextProp(context, 'canvas');
6
+ // Create final grid
7
+ try {
8
+ context.canvas = context.canvas.composite(context.composites);
9
+ }
10
+ catch (err) {
11
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, {
12
+ type: 'internal',
13
+ cause: err,
14
+ });
15
+ }
16
+ };
@@ -0,0 +1,11 @@
1
+ import type { MergeStep } from '../../pipeline/mergePipeline.js';
2
+ import type { RGBA } from '../../utils/colors/types.js';
3
+ interface Options {
4
+ canvasColor: RGBA;
5
+ }
6
+ interface State {
7
+ canvasWidth: number;
8
+ canvasHeight: number;
9
+ }
10
+ export declare const createCanvas: MergeStep<Options, State>;
11
+ export {};
@@ -0,0 +1,17 @@
1
+ import { requireState } from '../../pipeline/guards.js';
2
+ import sharp from 'sharp';
3
+ export const createCanvas = async (context, options, _onProgress) => {
4
+ requireState(context, 'canvasWidth');
5
+ requireState(context, 'canvasHeight');
6
+ // Create canvas
7
+ const canvas = sharp({
8
+ limitInputPixels: false,
9
+ create: {
10
+ width: context.state.canvasWidth,
11
+ height: context.state.canvasHeight,
12
+ channels: 4,
13
+ background: options.canvasColor,
14
+ },
15
+ });
16
+ context.canvas = canvas;
17
+ };
@@ -0,0 +1,7 @@
1
+ import type { SupportedOutputFormat } from '../../helpers.js';
2
+ import type { MergeStep } from '../../pipeline/mergePipeline.js';
3
+ interface Options {
4
+ format: SupportedOutputFormat;
5
+ }
6
+ export declare const exportCanvas: MergeStep<Options, any>;
7
+ export {};