pixeli 1.0.3 → 1.0.5

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 CHANGED
@@ -51,6 +51,7 @@ The tool currently supports four main layout modes: ***Grid***, ***Masonry*** (h
51
51
  * [All Supported Input Formats](#all-supported-input-formats)
52
52
  * [All Supported Output Formats](#all-supported-output-formats)
53
53
  * [Pixel Limits](#pixel-limits)
54
+ * [Error Handling](#error-handling)
54
55
  * [Colors and Transparency](#colors-and-transparency)
55
56
  * [CLI](#cli)
56
57
  * [Library](#library)
@@ -471,6 +472,26 @@ Generating extremely large images significantly reduces speed, and may also lead
471
472
  | **BMP** | `.bmp` | **32,767×32,767 or ~2,147,483,647×2,147,483,647 px** | Depends on version/fields. |
472
473
  | **TIFF** | `.tif`, `.tiff` | **4,294,967,295 × 4,294,967,295 px** (theoretical) | Very large; may be limited by software or memory. |
473
474
 
475
+ ### Error Handling
476
+ All errors produced by this library are instances of the `MergeError` class, which extends the `Error` class.
477
+
478
+ In addition to the inherited `message` property, the `MergeError` class also has a `context` object parameter attached to it, where there is a `type` and an optional `cause` property. Consider the following:
479
+
480
+ ```typescript
481
+ throw new MergeError("Some error has occurred", {
482
+ type: 'internal',
483
+ cause: 'helper "trimmedMedian" failed'
484
+ });
485
+ ```
486
+
487
+ The `cause` property is only used when an internal error has occurred, meaning that there is something wrong with the API.
488
+
489
+ The `type` property can be one of the following:
490
+ - `validation`: One of the options provided to the merge function is incorrect or is required but not provided.
491
+ - `image`: An invalid, corrupt, or empty image input list has been provided.
492
+ - `internal`: An error with the interal library. Should not occur.
493
+
494
+
474
495
  ### Colors and Transparency
475
496
 
476
497
  #### CLI
@@ -1 +1,2 @@
1
1
  export * from './merges/index.js';
2
+ export { BatchRunner } from './jobs/batchRunner.js';
@@ -1 +1,2 @@
1
1
  export * from './merges/index.js';
2
+ export { BatchRunner } from './jobs/batchRunner.js';
@@ -0,0 +1,44 @@
1
+ import { MergeError } from '../mergeError.js';
2
+ import { TypedEventEmitter } from '../modules/typedEventEmitter.js';
3
+ import type { BatchOptions, MergeJob } from './types.js';
4
+ type MergeResult = {
5
+ index: number;
6
+ success: true;
7
+ buffer: Buffer;
8
+ } | {
9
+ index: number;
10
+ success: false;
11
+ error: MergeError;
12
+ };
13
+ export interface BatchEvents {
14
+ start: {
15
+ totalJobs: number;
16
+ };
17
+ 'job:start': {
18
+ job: MergeJob;
19
+ index: number;
20
+ };
21
+ 'job:complete': {
22
+ job: MergeJob;
23
+ index: number;
24
+ buffer: Buffer;
25
+ };
26
+ 'job:error': {
27
+ job: MergeJob;
28
+ index: number;
29
+ error: MergeError;
30
+ };
31
+ complete: {
32
+ results: MergeResult[];
33
+ };
34
+ }
35
+ export declare class BatchRunner extends TypedEventEmitter<BatchEvents> {
36
+ private jobs;
37
+ private results;
38
+ private validatedJobs;
39
+ constructor(jobs: MergeJob[]);
40
+ run(options?: BatchOptions): Promise<MergeResult[]>;
41
+ private runJob;
42
+ private validateJob;
43
+ }
44
+ export {};
@@ -0,0 +1,90 @@
1
+ // Merges
2
+ import * as mergeFunctions from '../merges/index.js';
3
+ // Errors
4
+ import { MergeError } from '../mergeError.js';
5
+ import { MESSAGES } from '../modules/messages.js';
6
+ // Other
7
+ import { TypedEventEmitter } from '../modules/typedEventEmitter.js';
8
+ import { mergeJobSchema } from '../schemas/mergeJob.js';
9
+ export class BatchRunner extends TypedEventEmitter {
10
+ jobs;
11
+ results = [];
12
+ validatedJobs = [];
13
+ constructor(jobs) {
14
+ super();
15
+ this.jobs = jobs;
16
+ // Ensure all jobs are valid
17
+ for (const job of jobs) {
18
+ const validatedJob = this.validateJob(job);
19
+ this.validatedJobs.push(validatedJob);
20
+ }
21
+ }
22
+ async run(options = {}) {
23
+ const { stopOnError = false } = options;
24
+ // Run jobs
25
+ for (let i = 0; i < this.validatedJobs.length; i++) {
26
+ const job = this.validatedJobs[i];
27
+ try {
28
+ // Emit job start event
29
+ this.emit('job:start', {
30
+ index: i,
31
+ job: this.jobs[i],
32
+ });
33
+ // Run job
34
+ const buffer = await this.runJob(job);
35
+ // Emit job completion event
36
+ this.emit('job:complete', {
37
+ index: i,
38
+ job: this.jobs[i],
39
+ buffer,
40
+ });
41
+ this.results.push({ index: i, success: true, buffer: buffer });
42
+ }
43
+ catch (error) {
44
+ // Emit job error event
45
+ this.emit('job:error', {
46
+ index: i,
47
+ job: this.jobs[i],
48
+ error: error,
49
+ });
50
+ // Throw error if needed
51
+ if (stopOnError)
52
+ throw error;
53
+ // Store error for later
54
+ this.results.push({ index: i, success: false, error: error });
55
+ }
56
+ }
57
+ // Emit batch job completion event
58
+ this.emit('complete', {
59
+ results: this.results,
60
+ });
61
+ return this.results;
62
+ }
63
+ async runJob(job) {
64
+ // Run job and return resulting buffer
65
+ switch (job.type) {
66
+ case 'grid':
67
+ return mergeFunctions.gridMerge(job.inputs, job.options);
68
+ case 'masonry':
69
+ return mergeFunctions.masonryMerge(job.inputs, job.options);
70
+ case 'collage':
71
+ return mergeFunctions.collageMerge(job.inputs, job.options);
72
+ case 'template':
73
+ return mergeFunctions.templateMerge(job.inputs, job.options);
74
+ }
75
+ }
76
+ validateJob(job) {
77
+ // Validate job format
78
+ const { success, data, error } = mergeJobSchema.safeParse(job);
79
+ if (!success) {
80
+ const path = error.issues[0]?.path;
81
+ const err = error.issues[0]?.message;
82
+ if (!path || !err) {
83
+ throw new MergeError(MESSAGES.ERROR.INTERNAL.message, { type: 'internal', cause: error });
84
+ }
85
+ const errorText = path.length > 0 ? `Invalid value at ${path.join('/')}: ${err}` : `Error: ${err}`;
86
+ throw new MergeError(errorText, { type: 'validation' });
87
+ }
88
+ return data;
89
+ }
90
+ }
@@ -0,0 +1,10 @@
1
+ import type sharp from 'sharp';
2
+ import type { MergeTypeOptions } from '../merges/types.js';
3
+ export type MergeJob = {
4
+ /** File paths to all of the images to load. */
5
+ inputs: sharp.SharpInput[];
6
+ } & MergeTypeOptions;
7
+ export interface BatchOptions {
8
+ /** Throws a `MergeError` instantly and stops the process if true. */
9
+ stopOnError?: boolean;
10
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -1,3 +1,5 @@
1
1
  export { gridMerge } from './grid/index.js';
2
2
  export { masonryMerge } from './masonry/index.js';
3
3
  export { templateMerge } from './template/index.js';
4
+ export { collageMerge } from './collage/index.js';
5
+ export type { GridMergeOptions, MasonryMergeOptions, TemplateMergeOptions, CollageMergeOptions } from './types.js';
@@ -1,3 +1,4 @@
1
1
  export { gridMerge } from './grid/index.js';
2
2
  export { masonryMerge } from './masonry/index.js';
3
3
  export { templateMerge } from './template/index.js';
4
+ export { collageMerge } from './collage/index.js';
@@ -72,10 +72,10 @@ export interface MasonryMergeOptions extends BaseMergeOptions {
72
72
  /** The vertical alignment of each column. Only applied in vertical layouts. */
73
73
  vAlign?: 'top' | 'middle' | 'bottom' | 'justified';
74
74
  }
75
- interface TemplateMergeOptions extends BaseMergeOptions {
75
+ export interface TemplateMergeOptions extends BaseMergeOptions {
76
76
  template: Template;
77
77
  }
78
- interface CollageMergeOptions extends Omit<BaseMergeOptions, 'gap'> {
78
+ export interface CollageMergeOptions extends Omit<BaseMergeOptions, 'gap'> {
79
79
  /** Width of each image cell in pixels, uses the median image width if undefined. */
80
80
  imageWidth?: number | undefined;
81
81
  /** The aspect ratio of each image. Used to calculate image height based on given width.
@@ -95,6 +95,27 @@ interface CollageMergeOptions extends Omit<BaseMergeOptions, 'gap'> {
95
95
  * For example, a value of `10` will result in a random degree being picked from `-10` to `+10` degrees. */
96
96
  rotationRange?: number;
97
97
  }
98
+ export type MergeTypeOptions = {
99
+ /** Use the grid layout engine. */
100
+ type: 'grid';
101
+ /** Options specific to grid merges. */
102
+ options: GridMergeOptions;
103
+ } | {
104
+ /** Use the masonry layout engine. */
105
+ type: 'masonry';
106
+ /** Options specific to masonry merges. */
107
+ options: MasonryMergeOptions;
108
+ } | {
109
+ /** Use the template-based merge engine. */
110
+ type: 'template';
111
+ /** Options specific to template merges. */
112
+ options: TemplateMergeOptions;
113
+ } | {
114
+ /** Use the free-form collage merge engine. */
115
+ type: 'collage';
116
+ /** Options specific to collage merges. */
117
+ options: CollageMergeOptions;
118
+ };
98
119
  export type GridMerge = MergeCommand<GridMergeOptions>;
99
120
  export type MasonryMerge = MergeCommand<MasonryMergeOptions>;
100
121
  export type TemplateMerge = MergeCommand<TemplateMergeOptions>;
@@ -0,0 +1,7 @@
1
+ import EventEmitter from 'node:events';
2
+ type EventKey<T> = keyof T & string;
3
+ export declare class TypedEventEmitter<TEvents> extends EventEmitter {
4
+ on<K extends EventKey<TEvents>>(event: K, listener: (payload: TEvents[K]) => void): this;
5
+ emit<K extends EventKey<TEvents>>(event: K, payload: TEvents[K]): boolean;
6
+ }
7
+ export {};
@@ -0,0 +1,9 @@
1
+ import EventEmitter from 'node:events';
2
+ export class TypedEventEmitter extends EventEmitter {
3
+ on(event, listener) {
4
+ return super.on(event, listener);
5
+ }
6
+ emit(event, payload) {
7
+ return super.emit(event, payload);
8
+ }
9
+ }
@@ -0,0 +1,11 @@
1
+ import z from 'zod';
2
+ export declare const mergeJobSchema: z.ZodObject<{
3
+ type: z.ZodEnum<{
4
+ template: "template";
5
+ grid: "grid";
6
+ masonry: "masonry";
7
+ collage: "collage";
8
+ }>;
9
+ inputs: z.ZodArray<z.ZodString>;
10
+ options: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodAny>>;
11
+ }, z.z.core.$strict>;
@@ -0,0 +1,6 @@
1
+ import z from 'zod';
2
+ export const mergeJobSchema = z.strictObject({
3
+ type: z.enum(['grid', 'masonry', 'collage', 'template']),
4
+ inputs: z.array(z.string()).min(1),
5
+ options: z.record(z.string(), z.any()).optional(),
6
+ });
@@ -4,4 +4,4 @@ import { SUPPORTED_OUTPUT_FORMATS } from '../core/helpers.js';
4
4
  export const outputFileValidator = z.string().refine((outputPath) => {
5
5
  const extension = path.extname(outputPath).replace('.', '');
6
6
  return SUPPORTED_OUTPUT_FORMATS.includes(extension);
7
- }, '--output image format is invalid');
7
+ }, 'output image format is invalid');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixeli",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "description": "A lightweight command-line tool for merging multiple images into customizable grid layouts.",
5
5
  "homepage": "https://github.com/pakdad-mousavi/pixeli#readme",
6
6
  "bugs": {
@@ -56,5 +56,10 @@
56
56
  "tsx": "^4.21.0",
57
57
  "typescript": "^5.9.3",
58
58
  "vitest": "^3.2.4"
59
+ },
60
+ "allowScripts": {
61
+ "esbuild@0.27.2": true,
62
+ "fsevents@2.3.3": true,
63
+ "sharp@0.34.5": true
59
64
  }
60
65
  }