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
@@ -1,72 +0,0 @@
1
- import { Command } from 'commander';
2
- import chalk from 'chalk';
3
- import {
4
- cliConfirm,
5
- displayInfoMessage,
6
- displaySuccessMessage,
7
- displayWarningMessage,
8
- handleError,
9
- writeImage,
10
- } from '../../lib/helpers/utils.js';
11
- import { addSharedOptions } from './helpers/utils.js';
12
- import { validateMasonryOptions, validateSharedOptions } from './helpers/validations.js';
13
- import { loadImages } from '../../lib/helpers/loadImages.js';
14
- import { masonryMerge } from '../../lib/merges/masonry-merge/index.js';
15
-
16
- const masonryCommand = new Command('masonry');
17
-
18
- masonryCommand
19
- .description("Use a ragged-grid layout, preserves images' aspect ratios")
20
- .option('--rh, --row-height <px>', 'The height of each row, defaults to the smallest image height', null)
21
- .option('--cw, --column-width <px>', 'The width of each column, defaults to the smallest image width', null)
22
- .option('--cvw, --canvas-width <px>', 'The width of the canvas', null)
23
- .option('--cvh, --canvas-height <px>', 'The height of the canvas', null)
24
- .option('-f, --flow <horizontal|vertical>', 'The flow of the masonry layout', 'horizontal')
25
- .option('--ha, --h-align <left|center|right|justified>', 'Horizontal alignment of the grid (for horizontal flows)', null)
26
- .option('--va, --v-align <top|middle|bottom|justified>', 'Vertical alignment of the grid (for vertical flows)', null)
27
- .action((files, opts) => {
28
- main(files, opts);
29
- });
30
-
31
- const main = async (files, opts) => {
32
- try {
33
- // Collect and validate parameters
34
- const params = { files, ...opts };
35
- const sharedOptions = await validateSharedOptions(params);
36
- const masonryOptions = validateMasonryOptions(sharedOptions, opts);
37
- const validatedParams = { ...sharedOptions, ...masonryOptions };
38
-
39
- // Load images, create grid, and write grid on disk
40
- generateAndSaveGrid(validatedParams);
41
-
42
- // Output success message
43
- } catch (e) {
44
- handleError(e);
45
- }
46
- };
47
-
48
- const generateAndSaveGrid = async (validatedParams) => {
49
- const { images, ignoredFiles } = await loadImages(validatedParams);
50
-
51
- // Display warnings if needed
52
- if (ignoredFiles.length) {
53
- displayWarningMessage('\nThese files will be ignored due to unsupported formats:');
54
- for (const file of ignoredFiles) {
55
- displayInfoMessage(file);
56
- }
57
-
58
- const confirmation = await cliConfirm('\nAre you sure you want to continue?');
59
- if (!confirmation) return;
60
- }
61
-
62
- const grid = await masonryMerge(images, validatedParams);
63
- const success = await writeImage(grid, validatedParams.output);
64
-
65
- // Display success message
66
- if (success) {
67
- displaySuccessMessage(`\nImage has been created successfully: ${chalk.bold(validatedParams.output)}\n`);
68
- }
69
- };
70
-
71
- addSharedOptions(masonryCommand);
72
- export default masonryCommand;
@@ -1,94 +0,0 @@
1
- import sharp from 'sharp';
2
- import fs from 'node:fs/promises';
3
- import path from 'node:path';
4
- import { isSupportedInputImage, shuffleTogether } from './utils.js';
5
-
6
- const MAX_RECURSION_DEPTH = 10;
7
-
8
- export const loadImages = async ({ files, dir, recursive, shuffle, count }) => {
9
- let ignoredFiles = [];
10
- let filepaths = files;
11
- let images = [];
12
-
13
- if (files && files.length) {
14
- // Load directly from provided file list
15
- images = await loadFromFiles(files, count);
16
- } else {
17
- // Get all files from directory
18
- const { skippedFiles, paths } = await getFilesFromDirectory(dir, recursive);
19
- filepaths = paths;
20
- ignoredFiles = skippedFiles;
21
- images = await loadFromFiles(filepaths, count);
22
- }
23
-
24
- // Ensure filepaths and images match
25
- if (images.length !== filepaths.length) {
26
- filepaths = filepaths.slice(0, images.length);
27
- }
28
-
29
- // Optional: shuffle filepaths and images together
30
- if (shuffle) {
31
- [filepaths, images] = shuffleTogether(filepaths, images);
32
- }
33
-
34
- return { images, files: filepaths, ignoredFiles };
35
- };
36
-
37
- const loadFromFiles = async (files, count) => {
38
- const images = [];
39
- const total = count || files.length;
40
- for (let i = 0; i < total; i++) {
41
- // End the loop if count is higher than number of available files
42
- if (i >= files.length) break;
43
-
44
- // Load images
45
- const filepath = files[i];
46
- let image;
47
-
48
- if (filepath.endsWith('.svg')) {
49
- const svgBuffer = await fs.readFile(filepath);
50
- image = sharp(svgBuffer);
51
- } else {
52
- image = sharp(filepath);
53
- }
54
-
55
- images.push(image);
56
- }
57
-
58
- return images;
59
- };
60
-
61
- const getFilesFromDirectory = async (dir, recursive, depth = 0) => {
62
- // Use to collect warnings
63
- const skippedFiles = [];
64
-
65
- // Ensure recursiveness ends at the max recursion depth
66
- if (depth >= MAX_RECURSION_DEPTH) return { paths: [], skippedFiles: [] };
67
-
68
- // Get entries
69
- const entries = await fs.readdir(dir, { withFileTypes: true });
70
- const files = [];
71
-
72
- for (const entry of entries) {
73
- const file = path.join(entry.parentPath, entry.name);
74
-
75
- // If the entry is a valid image file, add it to the list
76
- if (entry.isFile() && isSupportedInputImage(entry.name)) {
77
- files.push(file);
78
- }
79
- // If it is an invalid file format, add to skipped files
80
- else if (entry.isFile() && !isSupportedInputImage(entry.name) && entry.name !== '.DS_Store') {
81
- skippedFiles.push(entry.name);
82
- }
83
- // If it's a directory AND the recursive option is true,
84
- // recursively get all the files
85
- else if (recursive && entry.isDirectory() && !entry.isSymbolicLink()) {
86
- const dirpath = path.join(entry.parentPath, entry.name);
87
- const dirObj = await getFilesFromDirectory(dirpath, recursive, depth + 1);
88
- files.push(...dirObj?.paths);
89
- skippedFiles.push(...dirObj.skippedFiles);
90
- }
91
- }
92
-
93
- return { paths: files, skippedFiles };
94
- };
@@ -1,20 +0,0 @@
1
- import { SingleBar } from 'cli-progress';
2
- import chalk from 'chalk';
3
-
4
- const title = chalk.gray('Creating Image:');
5
- const bar = chalk.blue('{bar}');
6
- const percentage = chalk.yellow('{percentage}%');
7
- const eta = chalk.blue('ETA: ') + chalk.yellow('{eta_formatted}');
8
- const stage = chalk.gray('{stage}...');
9
- const divider = chalk.blue('|');
10
-
11
- export const WRITING_TO_FILE_PERCENTAGE = 0.05;
12
-
13
- export const progressBar = new SingleBar({
14
- format: `${title} ${divider}${bar}${divider} ${percentage} ${divider} ${eta} ${divider} ${stage} `,
15
- barCompleteChar: '\u2588',
16
- barIncompleteChar: '\u2591',
17
- stopOnComplete: true,
18
- barsize: 40,
19
- etaBuffer: 50,
20
- });
@@ -1,139 +0,0 @@
1
- import Ajv from 'ajv';
2
- import { isValidHexadecimal } from './utils.js';
3
-
4
- // Define entire template schema
5
- const TEMPLATE_SCHEMA = {
6
- type: 'object',
7
- additionalProperties: false,
8
- required: ['canvas', 'slots'],
9
- properties: {
10
- canvas: {
11
- type: 'object',
12
- additionalProperties: false,
13
- required: ['width', 'height', 'columns', 'rows'],
14
- properties: {
15
- width: { type: 'number', minimum: 1, multipleOf: 1 },
16
- height: { type: 'number', minimum: 1, multipleOf: 1 },
17
- columns: { type: 'number', minimum: 1, multipleOf: 1 },
18
- rows: { type: 'number', minimum: 1, multipleOf: 1 },
19
- gap: { type: 'number', minimum: 0, multipleOf: 1 },
20
- background: { type: 'string' },
21
- },
22
- },
23
-
24
- slots: {
25
- type: 'array',
26
- items: {
27
- type: 'object',
28
- additionalProperties: false,
29
- required: ['col', 'row', 'colSpan', 'rowSpan'],
30
- properties: {
31
- col: { type: 'number', minimum: 1, multipleOf: 1 },
32
- row: { type: 'number', minimum: 1, multipleOf: 1 },
33
- colSpan: { type: 'number', minimum: 1, multipleOf: 1 },
34
- rowSpan: { type: 'number', minimum: 1, multipleOf: 1 },
35
- borderRadius: { type: 'number', minimum: 0, multipleOf: 1 },
36
- },
37
- },
38
- },
39
- },
40
- };
41
-
42
- const ajv = new Ajv({ allErrors: true });
43
-
44
- const validate = ajv.compile(TEMPLATE_SCHEMA);
45
-
46
- export const validateTemplate = (json) => {
47
- const valid = validate(json);
48
-
49
- // Handle schema validation
50
- if (!valid) {
51
- const path = validate.errors[0].instancePath.slice(1).replaceAll('/', '.');
52
- const message = validate.errors[0].message;
53
- throw new Error(`${path} ${message}.`);
54
- }
55
-
56
- // Handle canvas background color
57
- if (json.canvas.background !== 'transparent' && !isValidHexadecimal(json.canvas.background)) {
58
- throw new Error(`Canvas color must be a valid hex value or "transparent".`);
59
- }
60
- json.canvas.background = json.canvas.background === 'transparent' ? { r: 0, g: 0, b: 0, alpha: 0 } : json.canvas.background;
61
-
62
- // Ensure canvas is wide enough for at least a single 1px column
63
- if (json.canvas.width <= json.canvas.gap * 2) {
64
- throw new Error(`Canvas width must be greater than ${json.canvas.gap * 2}.`);
65
- }
66
-
67
- // Ensure canvas is long enough for at least a single 1px row
68
- if (json.canvas.height <= json.canvas.gap * 2) {
69
- throw new Error(`Canvas height must be greater than ${json.canvas.gap * 2}.`);
70
- }
71
-
72
- // Calculate column width and row height
73
- const workableCanvasWidth = json.canvas.width - json.canvas.gap * (json.canvas.columns + 1);
74
- const workableCanvasHeight = json.canvas.height - json.canvas.gap * (json.canvas.rows + 1);
75
- const columnWidth = Math.floor(workableCanvasWidth / json.canvas.columns);
76
- const rowHeight = Math.floor(workableCanvasHeight / json.canvas.rows);
77
-
78
- // Ensure columns are thick enough
79
- if (columnWidth <= 0) {
80
- throw new Error(`Columns are too thin. Increase canvas width, reduce gap, or reduce number of columns.`);
81
- }
82
-
83
- // Ensure rows are thick enough
84
- if (rowHeight <= 0) {
85
- throw new Error(`Rows are too thin. Increase canvas height or reduce number of rows.`);
86
- }
87
-
88
- // For each slot...
89
- for (let i = 0; i < json.slots.length; i++) {
90
- const slot = json.slots[i];
91
-
92
- // Ensure slot is placed inside given canvas columns
93
- if (slot.col > json.canvas.columns) {
94
- throw new Error(`json.slots[${i}].col must be between 1 and ${json.canvas.columns}.`);
95
- }
96
-
97
- // Ensure slot is placed inside given canvas rows
98
- if (slot.row > json.canvas.rows) {
99
- throw new Error(`json.slots[${i}].row must be between 1 and ${json.canvas.rows}.`);
100
- }
101
-
102
- // Ensure slot spans within given canvas columns
103
- if (slot.col + slot.colSpan - 1 > json.canvas.columns) {
104
- throw new Error(`json.slots[${i}] spans past the right edge of the grid (col + colSpan exceeds columns).`);
105
- }
106
-
107
- // Ensure slot spans within given canvas rows
108
- if (slot.row + slot.rowSpan - 1 > json.canvas.rows) {
109
- throw new Error(`json.slots[${i}] spans past the bottom edge of the grid (row + rowSpan exceeds rows).`);
110
- }
111
- }
112
-
113
- // Ensure no slots overlap
114
- validateSlotOverlaps(json.slots);
115
-
116
- return json;
117
- };
118
-
119
- const validateSlotOverlaps = (slots) => {
120
- for (let i = 0; i < slots.length; i++) {
121
- const A = slots[i];
122
-
123
- const A_right = A.col + A.colSpan - 1;
124
- const A_bottom = A.row + A.rowSpan - 1;
125
-
126
- for (let j = i + 1; j < slots.length; j++) {
127
- const B = slots[j];
128
-
129
- const B_right = B.col + B.colSpan - 1;
130
- const B_bottom = B.row + B.rowSpan - 1;
131
-
132
- const overlap = A.col <= B_right && A_right >= B.col && A.row <= B_bottom && A_bottom >= B.row;
133
-
134
- if (overlap) {
135
- throw new Error(`Slot ${i} overlaps with slot ${j}.`);
136
- }
137
- }
138
- }
139
- };
@@ -1,208 +0,0 @@
1
- import readline from 'node:readline';
2
- import path from 'node:path';
3
- import chalk from 'chalk';
4
- import { progressBar } from './progressBar.js';
5
-
6
- export const SUPPORTED_INPUT_FORMATS = ['.webp', '.gif', '.jpeg', '.jpg', '.png', '.tiff', '.avif', '.svg'];
7
- export const SUPPORTED_OUTPUT_FORMATS = ['.webp', '.gif', '.jpeg', '.jpg', '.png', '.tiff', '.avif'];
8
-
9
- // Message class used for logging
10
- export class Message {
11
- constructor(text, type) {
12
- this.text = text;
13
- this.type = type;
14
-
15
- switch (this.type) {
16
- case 'error':
17
- this.message = chalk.bold.red('Error: ') + chalk.red(this.text);
18
- break;
19
- case 'warning':
20
- this.message = chalk.yellow(this.text);
21
- break;
22
- case 'success':
23
- this.message = chalk.blue(this.text);
24
- break;
25
- case 'neutral':
26
- this.message = chalk.gray(this.text);
27
- break;
28
- default:
29
- this.message = chalk.gray(this.text);
30
- break;
31
- }
32
- }
33
- }
34
-
35
- export const isValidHexadecimal = (str) => {
36
- const hexRegex = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;
37
- return hexRegex.test(str);
38
- };
39
-
40
- export const parseAspectRatio = (input) => {
41
- // return ratio straight away if its just a number
42
- const ratio = Number(input);
43
- if (ratio) {
44
- return ratio;
45
- }
46
-
47
- const ratioRegex = /^\s*(\d+)\s*(\/|:|x)\s*(\d+)\s*$/i;
48
- const match = input.match(ratioRegex);
49
-
50
- // not parsable
51
- if (!match) {
52
- return null;
53
- }
54
-
55
- const width = parseInt(match[1], 10);
56
- const height = parseInt(match[3], 10);
57
-
58
- return width / height;
59
- };
60
-
61
- export const handleError = (error) => {
62
- const m = new Message(error.message, 'error');
63
- console.log(m.message);
64
- };
65
-
66
- export const configureCommandErrors = (cmd) => {
67
- // Configure error message for the current command
68
- cmd.configureOutput({
69
- writeErr: (str) => {
70
- if (str.includes('error:')) {
71
- const err = new Message(str.replace('error: ', ''), 'error');
72
- return process.stderr.write(err.message);
73
- }
74
-
75
- process.stderr.write(str);
76
- },
77
- });
78
-
79
- // Recursively configure error messages for all subcommands
80
- for (const subCmd of cmd.commands) {
81
- configureCommandErrors(subCmd);
82
- }
83
- };
84
-
85
- export const displayInfoMessage = (message) => {
86
- const m = new Message(message, 'neutral');
87
- console.log(m.message);
88
- };
89
-
90
- export const displayWarningMessage = (message) => {
91
- const m = new Message(message, 'warning');
92
- console.log(m.message);
93
- };
94
-
95
- export const displaySuccessMessage = (message) => {
96
- const m = new Message(message, 'success');
97
- console.log(m.message);
98
- };
99
-
100
- export const cliConfirm = (message) => {
101
- const rl = readline.createInterface({
102
- input: process.stdin,
103
- output: process.stdout,
104
- });
105
-
106
- return new Promise((resolve) => {
107
- rl.question(chalk.yellow(`${message} (Y/n) `), (value) => {
108
- const cleanedValue = value.toLowerCase().trim();
109
- if (cleanedValue === 'y' || !cleanedValue.length) resolve(true);
110
- else resolve(false);
111
-
112
- console.log();
113
- rl.close();
114
- });
115
- });
116
- };
117
-
118
- export const isSupportedInputImage = (filename) => {
119
- for (const supportedFormat of SUPPORTED_INPUT_FORMATS) {
120
- if (filename.endsWith(supportedFormat)) {
121
- return true;
122
- }
123
- }
124
- return false;
125
- };
126
-
127
- export const isSupportedOutputImage = (filename) => {
128
- for (const supportedFormat of SUPPORTED_OUTPUT_FORMATS) {
129
- if (filename.endsWith(supportedFormat)) {
130
- return true;
131
- }
132
- }
133
- return false;
134
- };
135
-
136
- export const writeImage = async (image, output) => {
137
- // Define file size limits
138
- const LIMITS = {
139
- png: 2_147_483_647,
140
- jpg: 65_535,
141
- jpeg: 65_535,
142
- avif: 65_535,
143
- webp: 16_383,
144
- };
145
-
146
- // Update progress bar stage
147
- progressBar.update({ stage: 'Writing to file' });
148
-
149
- try {
150
- // Get image width and height
151
- const { width, height } = await image.metadata();
152
- const format = path.extname(output).replaceAll('.', '');
153
-
154
- // Ensure image can be encoded in the respective format
155
- const formatLimit = LIMITS[format];
156
- if (width > formatLimit || height > formatLimit) {
157
- throw new Error(`image is too large for ${format} format.`);
158
- }
159
-
160
- // Write to file
161
- await image.toFile(output);
162
- } catch (e) {
163
- // Complete the progress bar
164
- progressBar.update(progressBar.getTotal());
165
-
166
- // Handle any errors
167
- const m = new Message('Failed to write image on disk: ' + e.message, 'error');
168
- console.log('\n' + m.message);
169
-
170
- return false;
171
- }
172
-
173
- // Complete the progress bar
174
- progressBar.update(progressBar.getTotal());
175
-
176
- return true;
177
- };
178
-
179
- export const shuffleArray = (array) => {
180
- let currentIndex = array.length;
181
- let randomIndex;
182
-
183
- // While there remain elements to shuffle.
184
- while (currentIndex !== 0) {
185
- // Pick a remaining element.
186
- randomIndex = Math.floor(Math.random() * currentIndex);
187
- currentIndex--;
188
-
189
- // And swap it with the current element.
190
- [array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
191
- }
192
-
193
- return array;
194
- };
195
-
196
- export const shuffleTogether = (a, b) => {
197
- // 1. Build array of indices
198
- const indices = [...a.keys()];
199
-
200
- // 2. Shuffle the indices using your Fisher-Yates
201
- shuffleArray(indices);
202
-
203
- // 3. Apply same permutation to both arrays
204
- const aShuffled = indices.map((i) => a[i]);
205
- const bShuffled = indices.map((i) => b[i]);
206
-
207
- return [aShuffled, bShuffled];
208
- };
@@ -1,110 +0,0 @@
1
- import sharp from 'sharp';
2
- import { progressBar, WRITING_TO_FILE_PERCENTAGE } from '../../helpers/progressBar.js';
3
- import { roundImages } from '../merge-utils.js';
4
-
5
- export const collageMerge = async (images, validatedParams) => {
6
- const { template, cornerRadius } = validatedParams;
7
-
8
- // Set up progress bar
9
- const total = Math.min(images.length, template.slots.length) * 2;
10
- const totalFileWrite = Math.ceil(total * WRITING_TO_FILE_PERCENTAGE);
11
- progressBar.start(total + totalFileWrite, 0, {
12
- stage: 'Calculating dimensions',
13
- });
14
-
15
- // Calculate each column's width and height
16
- const workableCanvasWidth = template.canvas.width - template.canvas.gap * (template.canvas.columns + 1);
17
- const workableCanvasHeight = template.canvas.height - template.canvas.gap * (template.canvas.rows + 1);
18
- const columnWidth = workableCanvasWidth / template.canvas.columns;
19
- const rowHeight = workableCanvasHeight / template.canvas.rows;
20
-
21
- // Each block has its resized image with its respective slot coordinates
22
- progressBar.update({ stage: 'Resizing images' });
23
- const blocks = await getBlocks({
24
- slots: template.slots,
25
- images,
26
- gap: template.canvas.gap,
27
- columnWidth,
28
- rowHeight,
29
- cornerRadius,
30
- });
31
-
32
- // Lay blocks
33
- progressBar.update({ stage: 'Merging images' });
34
- const collage = layBlocks({
35
- canvasOptions: template.canvas,
36
- blocks,
37
- columnWidth,
38
- rowHeight,
39
- });
40
-
41
- return collage;
42
- };
43
-
44
- const getBlocks = async ({ slots, images, gap, columnWidth, rowHeight, cornerRadius }) => {
45
- const blocks = [];
46
-
47
- for (let i = 0; i < slots.length && i < images.length; i++) {
48
- const slot = slots[i];
49
- const image = images[i];
50
- let imageBuffer;
51
-
52
- // Calculate image width and height
53
- const width = slot.colSpan * columnWidth + (slot.colSpan - 1) * gap;
54
- const height = slot.rowSpan * rowHeight + (slot.rowSpan - 1) * gap;
55
-
56
- // Resize image respectively
57
- const resizedImage = image.resize({ width: Math.floor(width), height: Math.floor(height) });
58
-
59
- // Round corners of images if needed
60
- if (cornerRadius > 0) {
61
- const roundingOptions = {
62
- width: Math.floor(width),
63
- height: Math.floor(height),
64
- cornerRadius,
65
- };
66
-
67
- const roundedImage = (await roundImages([resizedImage], roundingOptions))[0];
68
- imageBuffer = await roundedImage.toBuffer();
69
- } else {
70
- imageBuffer = await resizedImage.toBuffer();
71
- }
72
-
73
- blocks.push({ imageBuffer, col: slot.col, row: slot.row });
74
- progressBar.increment();
75
- }
76
-
77
- return blocks;
78
- };
79
-
80
- const layBlocks = ({ canvasOptions, blocks, columnWidth, rowHeight }) => {
81
- // Create canvas
82
- const canvas = sharp({
83
- limitInputPixels: false,
84
- create: {
85
- background: canvasOptions.background,
86
- channels: 4,
87
- width: canvasOptions.width,
88
- height: canvasOptions.height,
89
- },
90
- });
91
-
92
- const composites = [];
93
-
94
- // Collect composites
95
- for (const block of blocks) {
96
- const x = (block.col - 1) * columnWidth + block.col * canvasOptions.gap;
97
- const y = (block.row - 1) * rowHeight + block.row * canvasOptions.gap;
98
-
99
- composites.push({
100
- input: block.imageBuffer,
101
- left: Math.floor(x),
102
- top: Math.floor(y),
103
- });
104
-
105
- progressBar.increment();
106
- }
107
-
108
- canvas.composite(composites);
109
- return canvas;
110
- };
@@ -1,17 +0,0 @@
1
- export default {
2
- canvas: {
3
- width: 2000,
4
- height: 1500,
5
- columns: 5,
6
- rows: 5,
7
- gap: 20,
8
- background: '#000',
9
- },
10
- slots: [
11
- { col: 1, row: 1, colSpan: 3, rowSpan: 3 },
12
- { col: 4, row: 1, colSpan: 2, rowSpan: 2 },
13
- { col: 4, row: 3, colSpan: 2, rowSpan: 1 },
14
- { col: 1, row: 4, colSpan: 2, rowSpan: 2 },
15
- { col: 3, row: 4, colSpan: 3, rowSpan: 2 },
16
- ],
17
- };
@@ -1,18 +0,0 @@
1
- export default {
2
- canvas: {
3
- width: 1800,
4
- height: 1200,
5
- columns: 6,
6
- rows: 4,
7
- gap: 10,
8
- background: '#000',
9
- },
10
- slots: [
11
- { col: 1, row: 1, colSpan: 3, rowSpan: 2 },
12
- { col: 4, row: 1, colSpan: 3, rowSpan: 1 },
13
- { col: 4, row: 2, colSpan: 3, rowSpan: 1 },
14
- { col: 1, row: 3, colSpan: 2, rowSpan: 2 },
15
- { col: 3, row: 3, colSpan: 2, rowSpan: 2 },
16
- { col: 5, row: 3, colSpan: 2, rowSpan: 2 },
17
- ],
18
- };
@@ -1,15 +0,0 @@
1
- export default {
2
- canvas: {
3
- width: 2400,
4
- height: 1400,
5
- columns: 8,
6
- rows: 3,
7
- gap: 16,
8
- background: '#000',
9
- },
10
- slots: [
11
- { col: 1, row: 1, colSpan: 3, rowSpan: 3 },
12
- { col: 4, row: 1, colSpan: 5, rowSpan: 1 },
13
- { col: 4, row: 2, colSpan: 5, rowSpan: 2 },
14
- ],
15
- };