pixeli 0.1.8 → 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.
- package/README.md +341 -88
- package/dist/cli/commands/collage/index.d.ts +2 -0
- package/dist/cli/commands/collage/index.js +125 -0
- package/dist/cli/commands/grid/index.d.ts +2 -0
- package/dist/cli/commands/grid/index.js +127 -0
- package/dist/cli/commands/masonry/index.d.ts +2 -0
- package/dist/cli/commands/masonry/index.js +129 -0
- package/dist/cli/commands/template/index.d.ts +2 -0
- package/dist/cli/commands/template/index.js +123 -0
- package/dist/cli/commands/template/presets/artGallery.d.ts +15 -0
- package/dist/cli/commands/template/presets/artGallery.js +15 -0
- package/dist/cli/commands/template/presets/dashboardShot.d.ts +15 -0
- package/dist/cli/commands/template/presets/dashboardShot.js +16 -0
- package/dist/cli/commands/template/presets/horizontalBookSpread.d.ts +15 -0
- package/dist/cli/commands/template/presets/horizontalBookSpread.js +13 -0
- package/dist/cli/commands/template/presets/instagramGrid.d.ts +15 -0
- package/dist/cli/commands/template/presets/instagramGrid.js +16 -0
- package/dist/cli/commands/template/presets/verticalBookSpread.d.ts +15 -0
- package/dist/cli/commands/template/presets/verticalBookSpread.js +13 -0
- package/dist/cli/commands/template/presets.d.ts +73 -0
- package/{lib/merges/collage-merge → dist/cli/commands/template}/presets.js +6 -8
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +24 -0
- package/dist/cli/modules/loadImages.d.ts +15 -0
- package/dist/cli/modules/loadImages.js +74 -0
- package/dist/cli/modules/progressBar.d.ts +10 -0
- package/dist/cli/modules/progressBar.js +34 -0
- package/dist/cli/schemas/collage.d.ts +29 -0
- package/dist/cli/schemas/collage.js +38 -0
- package/dist/cli/schemas/grid.d.ts +34 -0
- package/dist/cli/schemas/grid.js +38 -0
- package/dist/cli/schemas/masonry.d.ts +62 -0
- package/dist/cli/schemas/masonry.js +62 -0
- package/dist/cli/schemas/template.d.ts +31 -0
- package/dist/cli/schemas/template.js +49 -0
- package/dist/cli/utils/buildCommandFromSchema.d.ts +8 -0
- package/dist/cli/utils/buildCommandFromSchema.js +55 -0
- package/dist/cli/utils/configureCommandErrors.d.ts +2 -0
- package/dist/cli/utils/configureCommandErrors.js +22 -0
- package/dist/cli/utils/stringFormatter.d.ts +1 -0
- package/dist/cli/utils/stringFormatter.js +3 -0
- package/dist/cli/utils/toErrorMessage.d.ts +4 -0
- package/dist/cli/utils/toErrorMessage.js +22 -0
- package/dist/core/helpers.d.ts +10 -0
- package/dist/core/helpers.js +42 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/mergeError.d.ts +9 -0
- package/dist/core/mergeError.js +10 -0
- package/dist/core/merges/collage/index.d.ts +9 -0
- package/dist/core/merges/collage/index.js +32 -0
- package/dist/core/merges/collage/steps/calculateImageDimensions.d.ts +12 -0
- package/dist/core/merges/collage/steps/calculateImageDimensions.js +18 -0
- package/dist/core/merges/collage/steps/createComposites.d.ts +8 -0
- package/dist/core/merges/collage/steps/createComposites.js +58 -0
- package/dist/core/merges/collage/steps/resizeAndBorderImages.d.ts +12 -0
- package/dist/core/merges/collage/steps/resizeAndBorderImages.js +26 -0
- package/dist/core/merges/collage/steps/rotateImages.d.ts +7 -0
- package/dist/core/merges/collage/steps/rotateImages.js +9 -0
- package/dist/core/merges/grid/index.d.ts +12 -0
- package/dist/core/merges/grid/index.js +36 -0
- package/dist/core/merges/grid/steps/calculateCanvasDimensions.d.ts +8 -0
- package/dist/core/merges/grid/steps/calculateCanvasDimensions.js +18 -0
- package/dist/core/merges/grid/steps/calculateFontSize.d.ts +7 -0
- package/dist/core/merges/grid/steps/calculateFontSize.js +19 -0
- package/dist/core/merges/grid/steps/calculateImageDimensions.d.ts +8 -0
- package/dist/core/merges/grid/steps/calculateImageDimensions.js +18 -0
- package/dist/core/merges/grid/steps/createComposites.d.ts +10 -0
- package/dist/core/merges/grid/steps/createComposites.js +63 -0
- package/dist/core/merges/grid/steps/prepareImages.d.ts +10 -0
- package/dist/core/merges/grid/steps/prepareImages.js +29 -0
- package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.d.ts +7 -0
- package/dist/core/merges/grid/steps/shuffleImagesAndCaptions.js +17 -0
- package/dist/core/merges/index.d.ts +3 -0
- package/dist/core/merges/index.js +3 -0
- package/dist/core/merges/masonry/index.d.ts +10 -0
- package/dist/core/merges/masonry/index.js +32 -0
- package/dist/core/merges/masonry/steps/calculateCanvasDimensions.d.ts +15 -0
- package/dist/core/merges/masonry/steps/calculateCanvasDimensions.js +17 -0
- package/dist/core/merges/masonry/steps/calculateLaneSize.d.ts +9 -0
- package/dist/core/merges/masonry/steps/calculateLaneSize.js +27 -0
- package/dist/core/merges/masonry/steps/createComposites.d.ts +25 -0
- package/dist/core/merges/masonry/steps/createComposites.js +108 -0
- package/dist/core/merges/masonry/steps/resizeImages.d.ts +7 -0
- package/dist/core/merges/masonry/steps/resizeImages.js +14 -0
- package/dist/core/merges/masonry/steps/splitIntoLanes.d.ts +17 -0
- package/dist/core/merges/masonry/steps/splitIntoLanes.js +78 -0
- package/dist/core/merges/shared-steps/applyComposites.d.ts +2 -0
- package/dist/core/merges/shared-steps/applyComposites.js +16 -0
- package/dist/core/merges/shared-steps/createCanvas.d.ts +11 -0
- package/dist/core/merges/shared-steps/createCanvas.js +17 -0
- package/dist/core/merges/shared-steps/exportCanvas.d.ts +7 -0
- package/dist/core/merges/shared-steps/exportCanvas.js +25 -0
- package/dist/core/merges/shared-steps/finalizeImagePipelines.d.ts +2 -0
- package/dist/core/merges/shared-steps/finalizeImagePipelines.js +9 -0
- package/dist/core/merges/shared-steps/loadImages.d.ts +2 -0
- package/dist/core/merges/shared-steps/loadImages.js +26 -0
- package/dist/core/merges/shared-steps/validateCaptions.d.ts +10 -0
- package/dist/core/merges/shared-steps/validateCaptions.js +17 -0
- package/dist/core/merges/template/index.d.ts +10 -0
- package/dist/core/merges/template/index.js +28 -0
- package/dist/core/merges/template/steps/calculateSlotDimensions.d.ts +9 -0
- package/dist/core/merges/template/steps/calculateSlotDimensions.js +12 -0
- package/dist/core/merges/template/steps/createComposites.d.ts +10 -0
- package/dist/core/merges/template/steps/createComposites.js +25 -0
- package/dist/core/merges/template/steps/getBlocks.d.ts +13 -0
- package/dist/core/merges/template/steps/getBlocks.js +28 -0
- package/dist/core/merges/template/types.d.ts +21 -0
- package/dist/core/merges/template/types.js +1 -0
- package/dist/core/merges/types.d.ts +102 -0
- package/dist/core/merges/types.js +1 -0
- package/dist/core/modules/messages.d.ts +32 -0
- package/dist/core/modules/messages.js +54 -0
- package/dist/core/pipeline/guards.d.ts +4 -0
- package/dist/core/pipeline/guards.js +23 -0
- package/dist/core/pipeline/mergePipeline.d.ts +60 -0
- package/dist/core/pipeline/mergePipeline.js +122 -0
- package/dist/core/schemas/collage.d.ts +26 -0
- package/dist/core/schemas/collage.js +17 -0
- package/dist/core/schemas/grid.d.ts +32 -0
- package/dist/core/schemas/grid.js +29 -0
- package/dist/core/schemas/masonry.d.ts +56 -0
- package/dist/core/schemas/masonry.js +43 -0
- package/dist/core/schemas/template.d.ts +34 -0
- package/dist/core/schemas/template.js +88 -0
- package/dist/core/utils/colors/hexToRgba.d.ts +2 -0
- package/dist/core/utils/colors/hexToRgba.js +25 -0
- package/dist/core/utils/colors/rgbaToHex.d.ts +2 -0
- package/dist/core/utils/colors/rgbaToHex.js +15 -0
- package/dist/core/utils/colors/types.d.ts +7 -0
- package/dist/core/utils/colors/types.js +1 -0
- package/dist/core/utils/fonts/getFontSize.d.ts +9 -0
- package/dist/core/utils/fonts/getFontSize.js +40 -0
- package/dist/core/utils/images/addImageBorder.d.ts +13 -0
- package/dist/core/utils/images/addImageBorder.js +33 -0
- package/dist/core/utils/images/getImageHeights.d.ts +2 -0
- package/dist/core/utils/images/getImageHeights.js +9 -0
- package/dist/core/utils/images/getImageWidths.d.ts +2 -0
- package/dist/core/utils/images/getImageWidths.js +9 -0
- package/dist/core/utils/images/getSmallestImageDimensions.d.ts +5 -0
- package/dist/core/utils/images/getSmallestImageDimensions.js +9 -0
- package/dist/core/utils/images/handleImageEdges.d.ts +13 -0
- package/dist/core/utils/images/handleImageEdges.js +39 -0
- package/dist/core/utils/images/isActualImage.d.ts +5 -0
- package/dist/core/utils/images/isActualImage.js +26 -0
- package/dist/core/utils/images/parseAspectRatio.d.ts +1 -0
- package/dist/core/utils/images/parseAspectRatio.js +22 -0
- package/dist/core/utils/images/roundImage.d.ts +9 -0
- package/dist/core/utils/images/roundImage.js +27 -0
- package/dist/core/utils/images/roundImages.d.ts +8 -0
- package/dist/core/utils/images/roundImages.js +19 -0
- package/dist/core/utils/images/scaleImage.d.ts +8 -0
- package/dist/core/utils/images/scaleImage.js +36 -0
- package/dist/core/utils/images/scaleImages.d.ts +8 -0
- package/dist/core/utils/images/scaleImages.js +38 -0
- package/dist/core/utils/math/median.d.ts +1 -0
- package/dist/core/utils/math/median.js +12 -0
- package/dist/core/utils/math/randint.d.ts +6 -0
- package/dist/core/utils/math/randint.js +11 -0
- package/dist/core/utils/math/trimmedMedian.d.ts +1 -0
- package/dist/core/utils/math/trimmedMedian.js +12 -0
- package/dist/core/utils/svg/createSvgTextBuffer.d.ts +9 -0
- package/dist/core/utils/svg/createSvgTextBuffer.js +21 -0
- package/dist/validators/aspectRatio.d.ts +2 -0
- package/dist/validators/aspectRatio.js +15 -0
- package/dist/validators/coercion.d.ts +2 -0
- package/dist/validators/coercion.js +12 -0
- package/dist/validators/format.d.ts +2 -0
- package/dist/validators/format.js +15 -0
- package/dist/validators/hexColor.d.ts +7 -0
- package/dist/validators/hexColor.js +27 -0
- package/dist/validators/index.d.ts +95 -0
- package/dist/validators/index.js +64 -0
- package/dist/validators/outputFile.d.ts +2 -0
- package/dist/validators/outputFile.js +7 -0
- package/dist/validators/path.d.ts +3 -0
- package/dist/validators/path.js +18 -0
- package/dist/validators/sharpImageInput.d.ts +3 -0
- package/dist/validators/sharpImageInput.js +6 -0
- package/dist/validators/template.d.ts +15 -0
- package/dist/validators/template.js +41 -0
- package/package.json +26 -9
- package/bin/pixeli.js +0 -26
- package/commands/merge/collage.js +0 -83
- package/commands/merge/grid.js +0 -71
- package/commands/merge/helpers/utils.js +0 -11
- package/commands/merge/helpers/validations.js +0 -269
- package/commands/merge/index.js +0 -12
- package/commands/merge/masonry.js +0 -72
- package/lib/helpers/loadImages.js +0 -94
- package/lib/helpers/progressBar.js +0 -20
- package/lib/helpers/templateValidator.js +0 -139
- package/lib/helpers/utils.js +0 -208
- package/lib/merges/collage-merge/index.js +0 -110
- package/lib/merges/collage-merge/presets/artGallery.js +0 -17
- package/lib/merges/collage-merge/presets/dashboardShot.js +0 -18
- package/lib/merges/collage-merge/presets/horizontalBookSpread.js +0 -15
- package/lib/merges/collage-merge/presets/instagramGrid.js +0 -18
- package/lib/merges/collage-merge/presets/verticalBookSpread.js +0 -15
- package/lib/merges/grid-merge/index.js +0 -152
- package/lib/merges/masonry-merge/horizontal.js +0 -157
- package/lib/merges/masonry-merge/index.js +0 -57
- package/lib/merges/masonry-merge/vertical.js +0 -152
- package/lib/merges/merge-utils.js +0 -176
|
@@ -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
|
-
};
|
package/lib/helpers/utils.js
DELETED
|
@@ -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
|
-
};
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
canvas: {
|
|
3
|
-
width: 1200,
|
|
4
|
-
height: 1600,
|
|
5
|
-
columns: 3,
|
|
6
|
-
rows: 6,
|
|
7
|
-
gap: 12,
|
|
8
|
-
background: '#000',
|
|
9
|
-
},
|
|
10
|
-
slots: [
|
|
11
|
-
{ col: 1, row: 1, colSpan: 2, rowSpan: 2 },
|
|
12
|
-
{ col: 3, row: 1, colSpan: 1, rowSpan: 1 },
|
|
13
|
-
{ col: 3, row: 2, colSpan: 1, rowSpan: 1 },
|
|
14
|
-
{ col: 1, row: 3, colSpan: 1, rowSpan: 2 },
|
|
15
|
-
{ col: 2, row: 3, colSpan: 2, rowSpan: 2 },
|
|
16
|
-
{ col: 1, row: 5, colSpan: 3, rowSpan: 2 },
|
|
17
|
-
],
|
|
18
|
-
};
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
export default {
|
|
2
|
-
canvas: {
|
|
3
|
-
width: 1400,
|
|
4
|
-
height: 2100,
|
|
5
|
-
columns: 2,
|
|
6
|
-
rows: 3,
|
|
7
|
-
gap: 20,
|
|
8
|
-
background: '#000',
|
|
9
|
-
},
|
|
10
|
-
slots: [
|
|
11
|
-
{ col: 1, row: 1, colSpan: 2, rowSpan: 1 },
|
|
12
|
-
{ col: 1, row: 2, colSpan: 1, rowSpan: 2 },
|
|
13
|
-
{ col: 2, row: 2, colSpan: 1, rowSpan: 2 },
|
|
14
|
-
],
|
|
15
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import sharp from 'sharp';
|
|
3
|
-
import { getSmallestImageDimensions, getFontSize, createSvgTextBuffer, roundImages } from '../merge-utils.js';
|
|
4
|
-
import { progressBar, WRITING_TO_FILE_PERCENTAGE } from '../../helpers/progressBar.js';
|
|
5
|
-
|
|
6
|
-
export const gridMerge = async (files, images, validatedParams) => {
|
|
7
|
-
// Destructure params
|
|
8
|
-
const { aspectRatio, imageWidth, columns, gap, cornerRadius, canvasColor, caption, captionColor, maxCaptionSize } = validatedParams;
|
|
9
|
-
|
|
10
|
-
// Calculate width if needed, and height from aspect ratio
|
|
11
|
-
const width = imageWidth || (await getSmallestImageDimensions(images)).smallestWidth;
|
|
12
|
-
const height = Math.floor(width / aspectRatio);
|
|
13
|
-
|
|
14
|
-
// resize images to match width and height
|
|
15
|
-
const resizedImages = images.map((image) => {
|
|
16
|
-
return image.resize({
|
|
17
|
-
width,
|
|
18
|
-
height,
|
|
19
|
-
fit: 'cover',
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Round images if needed
|
|
24
|
-
const roundedImages = await roundImages(resizedImages, { width, height, cornerRadius });
|
|
25
|
-
|
|
26
|
-
// Get filenames if needed
|
|
27
|
-
let filenames = null;
|
|
28
|
-
if (caption) {
|
|
29
|
-
filenames = files.map((file) => path.basename(file));
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Lay images in a grid
|
|
33
|
-
const gridParams = {
|
|
34
|
-
images: roundedImages,
|
|
35
|
-
width,
|
|
36
|
-
height,
|
|
37
|
-
columns,
|
|
38
|
-
gap,
|
|
39
|
-
canvasColor,
|
|
40
|
-
filenames,
|
|
41
|
-
caption,
|
|
42
|
-
captionColor,
|
|
43
|
-
maxCaptionSize,
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
return await layImagesInGrid(gridParams);
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const layImagesInGrid = async (opts) => {
|
|
50
|
-
// Destructure params
|
|
51
|
-
const { images, width, height, columns, gap, canvasColor, filenames, caption, captionColor, maxCaptionSize } = opts;
|
|
52
|
-
|
|
53
|
-
// Use 5% of images.length for writing to file
|
|
54
|
-
const fileWriteAmount = Math.ceil(images.length * WRITING_TO_FILE_PERCENTAGE);
|
|
55
|
-
progressBar.start(images.length + fileWriteAmount, 0, {
|
|
56
|
-
stage: 'Merging images',
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// Set constant
|
|
60
|
-
const CAPTION_HEIGHT_TO_CANVAS_WIDTH_RATIO = 0.04;
|
|
61
|
-
|
|
62
|
-
// Calculate number of rows
|
|
63
|
-
const rows = Math.ceil(images.length / columns);
|
|
64
|
-
|
|
65
|
-
// Calculate canvas width and caption height
|
|
66
|
-
const canvasWidth = width * columns + (columns + 1) * gap;
|
|
67
|
-
const captionHeight = Math.floor(canvasWidth * CAPTION_HEIGHT_TO_CANVAS_WIDTH_RATIO);
|
|
68
|
-
|
|
69
|
-
// Calculate canvas height
|
|
70
|
-
const minimumCanvasHeight = height * rows + (rows + 1) * gap;
|
|
71
|
-
const canvasHeight = caption ? minimumCanvasHeight + rows * captionHeight : minimumCanvasHeight;
|
|
72
|
-
|
|
73
|
-
// Calculate font size if needed
|
|
74
|
-
let fontSize = null;
|
|
75
|
-
if (caption) {
|
|
76
|
-
const longestFilename = filenames.reduce((longest, current) => {
|
|
77
|
-
return current.length > longest.length ? current : longest;
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
fontSize = await getFontSize({
|
|
81
|
-
text: longestFilename,
|
|
82
|
-
maxWidth: width,
|
|
83
|
-
maxHeight: captionHeight,
|
|
84
|
-
initialFontSize: maxCaptionSize,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Create canvas
|
|
89
|
-
const canvas = sharp({
|
|
90
|
-
limitInputPixels: false,
|
|
91
|
-
create: {
|
|
92
|
-
width: canvasWidth,
|
|
93
|
-
height: canvasHeight,
|
|
94
|
-
channels: 4,
|
|
95
|
-
background: canvasColor,
|
|
96
|
-
},
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
// Collect composites
|
|
100
|
-
const composites = [];
|
|
101
|
-
|
|
102
|
-
let x = gap;
|
|
103
|
-
let y = gap;
|
|
104
|
-
|
|
105
|
-
for (let row = 0; row < rows; row++) {
|
|
106
|
-
for (let col = 0; col < columns; col++) {
|
|
107
|
-
const index = row * columns + col;
|
|
108
|
-
if (index >= images.length) break;
|
|
109
|
-
|
|
110
|
-
const image = images[index];
|
|
111
|
-
|
|
112
|
-
composites.push({
|
|
113
|
-
input: await image.toBuffer(),
|
|
114
|
-
left: x,
|
|
115
|
-
top: y,
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
// Add caption if required
|
|
119
|
-
if (caption) {
|
|
120
|
-
// Create text
|
|
121
|
-
const svgBuffer = createSvgTextBuffer({
|
|
122
|
-
text: filenames[index],
|
|
123
|
-
maxWidth: width,
|
|
124
|
-
maxHeight: captionHeight,
|
|
125
|
-
fontSize,
|
|
126
|
-
fill: captionColor,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Add text to composites
|
|
130
|
-
composites.push({
|
|
131
|
-
input: svgBuffer,
|
|
132
|
-
left: x,
|
|
133
|
-
top: y + height,
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// Update coordinates
|
|
138
|
-
x += width + gap;
|
|
139
|
-
|
|
140
|
-
// Update progress bar
|
|
141
|
-
progressBar.increment();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// Update coordinates
|
|
145
|
-
y += caption ? height + gap + captionHeight : height + gap;
|
|
146
|
-
x = gap;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// Create final grid
|
|
150
|
-
canvas.composite(composites);
|
|
151
|
-
return canvas;
|
|
152
|
-
};
|