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,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,3 @@
1
+ export { gridMerge } from './grid/index.js';
2
+ export { masonryMerge } from './masonry/index.js';
3
+ export { templateMerge } from './template/index.js';
@@ -0,0 +1,3 @@
1
+ export { gridMerge } from './grid/index.js';
2
+ export { masonryMerge } from './masonry/index.js';
3
+ export { templateMerge } from './template/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 {};
@@ -0,0 +1,25 @@
1
+ import { MESSAGES } from '../../modules/messages.js';
2
+ import { MergeError } from '../../mergeError.js';
3
+ import { requireContextProp } from '../../pipeline/guards.js';
4
+ export const exportCanvas = async (context, options, _onProgress) => {
5
+ // Ensure canvas exists
6
+ requireContextProp(context, 'canvas');
7
+ try {
8
+ return await context.canvas.toFormat(options.format).toBuffer();
9
+ }
10
+ catch (err) {
11
+ // Assuming ALL sharp errors are instances of the Error object
12
+ const sharpError = err;
13
+ // SPECIFIC SHARP ERROR
14
+ // occurs when trying to create a buffer that exceeds the limits of the current image format
15
+ if (sharpError.message.includes('pixel limit') || sharpError.message.includes('Processed image is too large')) {
16
+ const errText = `Error: image to large for "${options.format}" format, try a format that allows larger images`;
17
+ throw new MergeError(errText, { type: 'image' });
18
+ }
19
+ // Other sharp errors
20
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, {
21
+ type: 'internal',
22
+ cause: err,
23
+ });
24
+ }
25
+ };
@@ -0,0 +1,2 @@
1
+ import type { MergeStep } from '../../pipeline/mergePipeline.js';
2
+ export declare const finalizeImagePipelines: MergeStep<any, any>;
@@ -0,0 +1,9 @@
1
+ import { requireNonEmptyArray } from '../../pipeline/guards.js';
2
+ import sharp from 'sharp';
3
+ export const finalizeImagePipelines = async (context, _options, _onProgress) => {
4
+ requireNonEmptyArray(context.images);
5
+ for (let i = 0; i < context.images.length; i++) {
6
+ const buffer = await context.images[i].toBuffer();
7
+ context.images[i] = sharp(buffer);
8
+ }
9
+ };
@@ -0,0 +1,2 @@
1
+ import type { MergeStep } from '../../pipeline/mergePipeline.js';
2
+ export declare const loadImages: MergeStep<any, any>;
@@ -0,0 +1,26 @@
1
+ import sharp from 'sharp';
2
+ import { isActualImage } from '../../utils/images/isActualImage.js';
3
+ import { MergeError } from '../../mergeError.js';
4
+ import { requireNonEmptyArray } from '../../pipeline/guards.js';
5
+ export const loadImages = async (context, _options, _onProgress) => {
6
+ // Ensure inputs are provided
7
+ requireNonEmptyArray(context.inputs, 'inputs');
8
+ const images = [];
9
+ for (let i = 0; i < context.inputs.length; i++) {
10
+ // Ensure image is valid
11
+ const input = context.inputs[i];
12
+ const { isImage, reason } = await isActualImage(input);
13
+ if (!isImage) {
14
+ throw new MergeError(`Invalid image input at index ${i}`, {
15
+ type: 'validation',
16
+ cause: reason,
17
+ });
18
+ }
19
+ images.push(sharp(input));
20
+ }
21
+ // Ensure there's at least one image
22
+ if (images.length <= 0) {
23
+ throw new MergeError('No images provided to merge', { type: 'validation' });
24
+ }
25
+ context.images = images;
26
+ };
@@ -0,0 +1,10 @@
1
+ import type { MergeStep } from '../../pipeline/mergePipeline.js';
2
+ interface Options {
3
+ caption: boolean;
4
+ captions?: string[] | undefined;
5
+ }
6
+ interface State {
7
+ areCaptionsProvided: boolean;
8
+ }
9
+ export declare const validateCaptions: MergeStep<Options, State>;
10
+ export {};
@@ -0,0 +1,17 @@
1
+ import { MergeError } from '../../mergeError.js';
2
+ export const validateCaptions = async (context, options, _onProgress) => {
3
+ // Initially set to false
4
+ context.state.areCaptionsProvided = false;
5
+ // Update context
6
+ if (areCaptionsProvided(options.caption, options.captions)) {
7
+ context.state.areCaptionsProvided = true;
8
+ context.captions = options.captions;
9
+ }
10
+ // Ensure caption length is not less than image length
11
+ if (areCaptionsProvided(options.caption, options.captions) && options.captions.length !== context.images.length) {
12
+ throw new MergeError('The same number of captions and images must be provided', { type: 'validation' });
13
+ }
14
+ };
15
+ const areCaptionsProvided = (caption, captions) => {
16
+ return caption;
17
+ };
@@ -0,0 +1,10 @@
1
+ import type { TemplateMerge } from '../types.js';
2
+ import type { Block } from './types.js';
3
+ export interface TemplateState {
4
+ slotWidth: number;
5
+ slotHeight: number;
6
+ canvasWidth: number;
7
+ canvasHeight: number;
8
+ blocks: Block[];
9
+ }
10
+ export declare const templateMerge: TemplateMerge;
@@ -0,0 +1,28 @@
1
+ import { MergePipeline } from '../../pipeline/mergePipeline.js';
2
+ import { templateSchema } from '../../schemas/template.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 { calculateCanvasDimensions } from './steps/calculateSlotDimensions.js';
8
+ import { getBlocks } from './steps/getBlocks.js';
9
+ import { createComposites } from './steps/createComposites.js';
10
+ export const templateMerge = async (imageInputs, options, onProgress) => {
11
+ const context = {
12
+ inputs: imageInputs,
13
+ captions: [],
14
+ composites: [],
15
+ images: [],
16
+ state: {},
17
+ };
18
+ const templateMergePipeline = await MergePipeline.createPipeline(templateSchema, options, context, onProgress);
19
+ templateMergePipeline
20
+ .use(loadImages)
21
+ .use(calculateCanvasDimensions)
22
+ .use(getBlocks)
23
+ .use(createCanvas)
24
+ .use(createComposites)
25
+ .use(applyComposites)
26
+ .use(exportCanvas);
27
+ return await templateMergePipeline.run();
28
+ };