Skip to content
This repository was archived by the owner on Jul 26, 2025. It is now read-only.

Commit 0e8410a

Browse files
authored
feat: add support for URL and recursive option in write methods (#284)
1 parent 6423b27 commit 0e8410a

File tree

2 files changed

+100
-23
lines changed

2 files changed

+100
-23
lines changed

src/save/__tests__/write.test.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { existsSync } from 'node:fs';
22
import path from 'node:path';
3+
import { pathToFileURL } from 'node:url';
34

45
import { write, writeSync } from '..';
5-
import { read } from '../..';
6+
import { read, readSync } from '../..';
67

78
let tmpDir: string;
89
beforeEach(() => {
@@ -40,36 +41,37 @@ test('async write image to disk (jpeg)', async () => {
4041
expect(imgRead.colorModel).toBe('RGBA');
4142
});
4243

43-
test('write image to disk', async () => {
44+
test('sync write image to disk', () => {
4445
const img = testUtils.load('opencv/test.png');
4546
const destination = path.join(tmpDir, 'image.png');
4647
writeSync(destination, img);
4748
expect(existsSync(destination)).toBe(true);
48-
const imgRead = await read(destination);
49+
const imgRead = readSync(destination);
4950
expect(imgRead).toMatchImage(img);
5051
});
5152

52-
test('write image to disk (jpeg)', async () => {
53+
test('sync write image to disk (jpeg)', () => {
5354
const img = testUtils.load('opencv/test.png');
5455
const destination = path.join(tmpDir, 'image.jpeg');
5556
writeSync(destination, img);
5657
expect(existsSync(destination)).toBe(true);
57-
const imgRead = await read(destination);
58+
const imgRead = readSync(destination);
5859
expect(imgRead.width).toBe(img.width);
5960
expect(imgRead.colorModel).toBe('RGBA');
6061
});
6162

62-
test('write mask image to disk', async () => {
63+
test('sync write mask image to disk', () => {
6364
let img = testUtils.load('opencv/test.png');
6465
img = img.convertColor('GREY');
6566
let mask = img.threshold();
6667
let maskImage = mask.convertColor('GREY');
6768
const destination = path.join(tmpDir, 'image.png');
6869
writeSync(destination, mask);
6970
expect(existsSync(destination)).toBe(true);
70-
const imgRead = await read(destination);
71+
const imgRead = readSync(destination);
7172
expect(imgRead).toMatchImage(maskImage);
7273
});
74+
7375
test('async write mask image to disk', async () => {
7476
let img = testUtils.load('opencv/test.png');
7577
img = img.convertColor('GREY');
@@ -82,6 +84,51 @@ test('async write mask image to disk', async () => {
8284
expect(imgRead).toMatchImage(maskImage);
8385
});
8486

87+
test('async write with URL', async () => {
88+
const img = testUtils.load('opencv/test.png');
89+
const destination = pathToFileURL(path.join(tmpDir, 'image.png'));
90+
await write(destination, img);
91+
expect(existsSync(destination)).toBe(true);
92+
const imgRead = await read(destination);
93+
expect(imgRead).toMatchImage(img);
94+
});
95+
96+
test('sync write with URL', () => {
97+
const img = testUtils.load('opencv/test.png');
98+
const destination = pathToFileURL(path.join(tmpDir, 'image.png'));
99+
writeSync(destination, img);
100+
expect(existsSync(destination)).toBe(true);
101+
const imgRead = readSync(destination);
102+
expect(imgRead).toMatchImage(img);
103+
});
104+
105+
test('async write with recursive option', async () => {
106+
const img = testUtils.load('opencv/test.png');
107+
const destination = path.join(tmpDir, 'subdir/123', 'image.png');
108+
await write(destination, img, { recursive: true });
109+
expect(existsSync(destination)).toBe(true);
110+
const imgRead = await read(destination);
111+
expect(imgRead).toMatchImage(img);
112+
});
113+
114+
test('sync write with recursive option', () => {
115+
const img = testUtils.load('opencv/test.png');
116+
const destination = path.join(tmpDir, 'subdir/123', 'image.png');
117+
writeSync(destination, img, { recursive: true });
118+
expect(existsSync(destination)).toBe(true);
119+
const imgRead = readSync(destination);
120+
expect(imgRead).toMatchImage(img);
121+
});
122+
123+
test('unknown format error', () => {
124+
const img = testUtils.load('opencv/test.png');
125+
const destination = path.join(tmpDir, 'image.png');
126+
// @ts-expect-error test invalid format
127+
expect(() => writeSync(destination, img, { format: 'foo' })).toThrow(
128+
/unknown format: foo/,
129+
);
130+
});
131+
85132
test('image extension error', async () => {
86133
const img = testUtils.load('opencv/test.png');
87134
const destination = path.join(tmpDir, 'image.tiff');

src/save/write.ts

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import fs from 'node:fs';
2-
import path from 'node:path';
2+
import { dirname, extname } from 'node:path';
3+
import { fileURLToPath } from 'node:url';
34

45
import { Mask, Image } from '..';
56

@@ -10,40 +11,55 @@ import {
1011
EncodeOptionsJpeg,
1112
} from './encode';
1213

14+
export interface WriteOptions {
15+
/**
16+
* If true, attempts to create all the missing directories recursively.
17+
*/
18+
recursive?: boolean;
19+
}
20+
21+
export type WriteOptionsPng = WriteOptions & EncodeOptionsPng;
22+
export type WriteOptionsJpeg = WriteOptions & EncodeOptionsJpeg;
23+
1324
/**
1425
* Write an image to the disk.
1526
* The file format is determined automatically from the file's extension.
1627
* If the extension is not supported, an error will be thrown.
1728
*
18-
* @param path - Path where the image should be written.
29+
* @param path - Path or file URL where the image should be written.
1930
* @param image - Image to save.
31+
* @param options - Write options.
2032
*/
21-
export async function write(path: string, image: Image | Mask): Promise<void>;
33+
export async function write(
34+
path: string | URL,
35+
image: Image | Mask,
36+
options?: WriteOptions,
37+
): Promise<void>;
2238
/**
2339
* Write an image to the disk as PNG.
2440
* When the `png` format is specified, the file's extension doesn't matter.
2541
*
26-
* @param path - Path where the image should be written.
42+
* @param path - Path or file URL where the image should be written.
2743
* @param image - Image to save.
2844
* @param options - Encode options for png images.
2945
*/
3046
export async function write(
31-
path: string,
47+
path: string | URL,
3248
image: Image | Mask,
33-
options: EncodeOptionsPng,
49+
options: WriteOptionsPng,
3450
): Promise<void>;
3551
/**
3652
* Write an image to the disk as JPEG.
3753
* When the `jpeg` format is specified, the file's extension doesn't matter.
3854
*
39-
* @param path - Path where the image should be written.
55+
* @param path - Path or file URL where the image should be written.
4056
* @param image - Image to save.
4157
* @param options - Encode options for jpeg images.
4258
*/
4359
export async function write(
44-
path: string,
60+
path: string | URL,
4561
image: Image | Mask,
46-
options: EncodeOptionsJpeg,
62+
options: WriteOptionsJpeg,
4763
): Promise<void>;
4864
/**
4965
* Asynchronously write an image to the disk.
@@ -53,14 +69,21 @@ export async function write(
5369
* @param options - Encode options.
5470
*/
5571
export async function write(
56-
path: string,
72+
path: string | URL,
5773
image: Image | Mask,
58-
options?: EncodeOptionsPng | EncodeOptionsJpeg,
74+
options?: WriteOptionsPng | WriteOptionsJpeg | WriteOptions,
5975
): Promise<void> {
76+
if (typeof path !== 'string') {
77+
path = fileURLToPath(path);
78+
}
6079
if (image instanceof Mask) {
6180
image = image.convertColor('GREY');
6281
}
6382
const toWrite = getDataToWrite(path, image, options);
83+
if (options?.recursive) {
84+
const dir = dirname(path);
85+
await fs.promises.mkdir(dir, { recursive: true });
86+
}
6487
await fs.promises.writeFile(path, toWrite);
6588
}
6689

@@ -72,14 +95,21 @@ export async function write(
7295
* @param options - Encode options.
7396
*/
7497
export function writeSync(
75-
path: string,
98+
path: string | URL,
7699
image: Image | Mask,
77-
options?: EncodeOptionsPng | EncodeOptionsJpeg,
100+
options?: WriteOptionsPng | WriteOptionsJpeg | WriteOptions,
78101
): void {
102+
if (typeof path !== 'string') {
103+
path = fileURLToPath(path);
104+
}
79105
if (image instanceof Mask) {
80106
image = image.convertColor('GREY');
81107
}
82108
const toWrite = getDataToWrite(path, image, options);
109+
if (options?.recursive) {
110+
const dir = dirname(path);
111+
fs.mkdirSync(dir, { recursive: true });
112+
}
83113
fs.writeFileSync(path, toWrite);
84114
}
85115

@@ -94,11 +124,11 @@ export function writeSync(
94124
function getDataToWrite(
95125
destinationPath: string,
96126
image: Image,
97-
options?: EncodeOptionsPng | EncodeOptionsJpeg,
127+
options?: WriteOptionsPng | WriteOptionsJpeg | WriteOptions,
98128
): Uint8Array {
99129
let format: ImageFormat;
100-
if (options === undefined) {
101-
const extension = path.extname(destinationPath).slice(1).toLowerCase();
130+
if (!options || !('format' in options)) {
131+
const extension = extname(destinationPath).slice(1).toLowerCase();
102132
if (extension === 'png') {
103133
format = 'png';
104134
return encode(image, { format });

0 commit comments

Comments
 (0)