animepic-utils 0.0.1

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.
@@ -0,0 +1,177 @@
1
+ # Typescript Style Guide
2
+
3
+ If you want to contribute to my repository, I ask that you follow my code styling. If something doesn't appear in this guide then I have no specific preference
4
+ for it (but this may get updated in the future as needed).
5
+
6
+ **1. Naming Scheme** \
7
+ Variables and functions use `camelCase`. Classes and interfaces use `PascalCase`.
8
+
9
+ ```ts
10
+ // x Bad
11
+ class aClass {}
12
+ const Method = () => {}
13
+ let Hi = 1
14
+ // ✓ Good
15
+ class AClass {}
16
+ const method = () => {}
17
+ let hi = 1
18
+ ```
19
+
20
+ **2. Braces** \
21
+ Always put the function brace on the same line.
22
+
23
+ ```ts
24
+ // x Bad
25
+ const a = () =>
26
+ {
27
+ }
28
+ // ✓ Good
29
+ const a = () => {
30
+ }
31
+ ```
32
+
33
+ **3. No Semicolons** \
34
+ Do not use semicolons, unless when needed to avoid a syntax error. They are ugly.
35
+
36
+ ```ts
37
+ // x Bad
38
+ let a = 5;
39
+ // ✓ Good
40
+ let a = 5
41
+ ```
42
+
43
+ **4. Double quote strings** \
44
+ Use double quotes for string literals, not single quotes.
45
+
46
+ ```ts
47
+ // x Bad
48
+ let a = 'hello'
49
+ // ✓ Good
50
+ let a = "hello"
51
+ ```
52
+
53
+ **5. No spaces on imports** \
54
+ No spaces when importing classes.
55
+
56
+ ```ts
57
+ // x Bad
58
+ import { Test } from "test"
59
+ // ✓ Good
60
+ import {Test} from "test"
61
+ ```
62
+
63
+ **6. Use arrow functions** \
64
+ Always use arrow functions to avoid having to bind this.
65
+
66
+ ```ts
67
+ // x Bad
68
+ async function func(str: string) {}
69
+ // ✓ Good
70
+ const func = async (str: string) => {}
71
+ ```
72
+
73
+ **7. No var** \
74
+ Do not use var when declaring variables.
75
+
76
+ ```ts
77
+ // x Bad
78
+ var a = 1
79
+ // ✓ Good
80
+ let a = 1
81
+ const b = 2
82
+ ```
83
+
84
+ **8. No double equals** \
85
+ Do not use the double equals/not equals.
86
+
87
+ ```ts
88
+ // x Bad
89
+ if (a == 1)
90
+ if (a != 1)
91
+ // ✓ Good
92
+ if (a === 1)
93
+ if (a !== 1)
94
+ ```
95
+
96
+ **9. Do not fill with comments** \
97
+ Do not fill the code with excessive comments. A documentation comment for the function is fine. If you have
98
+ to comment every other line, you are making bad code.
99
+
100
+ ```ts
101
+ // x Bad
102
+ // set a to 1
103
+ let a = 1
104
+ // ✓ Good
105
+ /**
106
+ * Gets a user from the api
107
+ */
108
+ public getUser = async () => {}
109
+ ```
110
+
111
+ **10. Use implicit return types** \
112
+ Let typescript infer the return type whenever possible. This helps when refactoring, since changes to the function will always update to the correct return type.
113
+
114
+ ```ts
115
+ // x Bad
116
+ public func = async (str: string): Promise<string> => {}
117
+ // ✓ Good
118
+ public func = async (str: string) => {}
119
+ ```
120
+
121
+ **11. Interface vs type** \
122
+ Interface should be used for large object-like types. Type is used for simpler union types or when generics are needed.
123
+
124
+ ```ts
125
+ // Interface
126
+ interface User {
127
+ name: string
128
+ birthday: string
129
+ }
130
+ // Type
131
+ type Theme = "light" | "dark"
132
+ ```
133
+
134
+ **12. Minimize any usage** \
135
+ Typed code minimizes bugs. Therefore you should reduce the usage of any type as much as possible, although sometimes it is
136
+ unavoidable.
137
+
138
+ ```ts
139
+ // x Bad
140
+ let x = [] as any
141
+ // ✓ Good
142
+ let x = [] as string[]
143
+ ```
144
+
145
+ **13. Use async/await** \
146
+ Use async/await. Avoid nested callbacks hell. You can convert a callback to async/await like this:
147
+
148
+ ```ts
149
+ await new Promise<void>((resolve) => {
150
+ callback((result) => {
151
+ resolve()
152
+ })
153
+ }
154
+ ```
155
+
156
+ **14. Use array methods for simple logic** \
157
+ Prefer array methods like map and filter for simple logic over a for loop. For complex logic, you may
158
+ use a for loop instead.
159
+
160
+ ```ts
161
+ // x Bad
162
+ for (let i = 0; i < a.length; i++) {
163
+ a[i] += 5
164
+ }
165
+ // ✓ Good
166
+ a = a.map(x => x + 5)
167
+ ```
168
+
169
+ **15. Use index signature over record type** \
170
+ Prefer index signature over Record.
171
+
172
+ ```ts
173
+ // x Bad
174
+ let x = {} as Record<string, number>
175
+ // ✓ Good
176
+ let x = {} as {[key: string]: number}
177
+ ```
@@ -0,0 +1,71 @@
1
+ import sharp from "sharp";
2
+ import { Waifu2xOptions } from "waifu2x";
3
+ type Formats = "jpg" | "png" | "webp" | "avif" | "jxl";
4
+ type FormatOptionMap = {
5
+ jpg: sharp.JpegOptions;
6
+ png: sharp.PngOptions;
7
+ webp: sharp.WebpOptions;
8
+ avif: sharp.AvifOptions;
9
+ jxl: sharp.JxlOptions;
10
+ };
11
+ export default class ImageUtils {
12
+ /**
13
+ * Fixes incorrect image extensions. It must be correct to preview on mac.
14
+ */
15
+ static fixFileExtensions: (folder: string) => Promise<void>;
16
+ /**
17
+ * Copies images to the destination (unchanged)
18
+ */
19
+ static copyImages: (sourceFolder: string, destFolder: string) => void;
20
+ /**
21
+ * Moves images to the destination
22
+ */
23
+ static moveImages: (sourceFolder: string, destFolder: string) => void;
24
+ /**
25
+ * Resizes image down to a maximum width/height.
26
+ */
27
+ static resizeImage: (filepath: string, maxSize?: number | {
28
+ maxWidth: number;
29
+ maxHeight: number;
30
+ }) => Promise<string>;
31
+ /**
32
+ * Transparent image check.
33
+ */
34
+ static isTransparent: (filepath: string) => Promise<boolean>;
35
+ /**
36
+ * Converts image to the specified format. Default is jpg for non-transparent and webp for transparent.
37
+ */
38
+ static convertImage: <T extends Formats>(filepath: string, format?: T, formatOptions?: FormatOptionMap[T], transparentFormat?: T, transparentFormatOptions?: FormatOptionMap[T]) => Promise<string>;
39
+ /**
40
+ * Upscale image. Optionally copies unprocessed files (due to an error) to a folder.
41
+ */
42
+ static upscaleImage: <T extends Formats>(src: string, destFolder: string, options?: Waifu2xOptions, unprocessedFolder?: boolean) => Promise<string>;
43
+ /**
44
+ * Processes an image folder with a custom chain of operations.
45
+ */
46
+ static processImages: <T extends Formats>(folder: string, ...operations: Array<(file: string) => Promise<string>>) => Promise<void>;
47
+ /**
48
+ * Shorthand process images with only a resize.
49
+ */
50
+ static resizeImages: (folder: string, maxSize?: number | {
51
+ maxWidth: number;
52
+ maxHeight: number;
53
+ }) => Promise<void>;
54
+ /**
55
+ * Shorthand process images with only a conversion.
56
+ */
57
+ static convertImages: <T extends Formats>(folder: string, format?: T, formatOptions?: FormatOptionMap[T], transparentFormat?: T, transparentFormatOptions?: FormatOptionMap[T]) => Promise<void>;
58
+ /**
59
+ * Shorthand process images with only a upscale.
60
+ */
61
+ static upscaleImages: <T extends Formats>(sourceFolder: string, destFolder: string, options?: Waifu2xOptions, unprocessedFolder?: boolean) => Promise<void>;
62
+ /**
63
+ * Splits up a folder into more manageable chunks.
64
+ */
65
+ static splitFolder: (folder: string, maxAmount?: number) => void;
66
+ /**
67
+ * Processes an image folder to be suitable to upload to moepictures.
68
+ */
69
+ static moepicsProcess: (folder: string) => Promise<void>;
70
+ }
71
+ export {};
@@ -0,0 +1,238 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const sharp_1 = __importDefault(require("sharp"));
9
+ const waifu2x_1 = __importDefault(require("waifu2x"));
10
+ class ImageUtils {
11
+ /**
12
+ * Fixes incorrect image extensions. It must be correct to preview on mac.
13
+ */
14
+ static fixFileExtensions = async (folder) => {
15
+ const files = fs_1.default.readdirSync(folder).filter((f) => f !== ".DS_Store");
16
+ for (const file of files) {
17
+ let filepath = path_1.default.join(folder, file);
18
+ if (fs_1.default.lstatSync(filepath).isDirectory())
19
+ continue;
20
+ const buffer = fs_1.default.readFileSync(filepath);
21
+ const meta = await (0, sharp_1.default)(buffer, { limitInputPixels: false }).metadata();
22
+ let ext = meta.format.replace("jpeg", "jpg");
23
+ let newFile = `${path_1.default.basename(file, path_1.default.extname(file))}.${ext}`;
24
+ let newFilePath = path_1.default.join(folder, newFile);
25
+ fs_1.default.renameSync(filepath, newFilePath);
26
+ }
27
+ };
28
+ /**
29
+ * Copies images to the destination (unchanged)
30
+ */
31
+ static copyImages = (sourceFolder, destFolder) => {
32
+ const files = fs_1.default.readdirSync(sourceFolder).filter((f) => f !== ".DS_Store");
33
+ for (const file of files) {
34
+ let src = path_1.default.join(sourceFolder, file);
35
+ if (fs_1.default.lstatSync(src).isDirectory())
36
+ continue;
37
+ let dest = path_1.default.join(destFolder, file);
38
+ fs_1.default.copyFileSync(src, dest);
39
+ }
40
+ };
41
+ /**
42
+ * Moves images to the destination
43
+ */
44
+ static moveImages = (sourceFolder, destFolder) => {
45
+ const files = fs_1.default.readdirSync(sourceFolder).filter((f) => f !== ".DS_Store");
46
+ for (const file of files) {
47
+ let src = path_1.default.join(sourceFolder, file);
48
+ if (fs_1.default.lstatSync(src).isDirectory())
49
+ continue;
50
+ let dest = path_1.default.join(destFolder, file);
51
+ fs_1.default.renameSync(src, dest);
52
+ }
53
+ };
54
+ /**
55
+ * Resizes image down to a maximum width/height.
56
+ */
57
+ static resizeImage = async (filepath, maxSize = 2000) => {
58
+ let maxWidth = typeof maxSize === "number" ? maxSize : maxSize.maxWidth;
59
+ let maxHeight = typeof maxSize === "number" ? maxSize : maxSize.maxHeight;
60
+ let buffer = new Uint8Array(fs_1.default.readFileSync(filepath));
61
+ const dim = await (0, sharp_1.default)(buffer).metadata();
62
+ if (dim.width > maxWidth || dim.height > maxHeight) {
63
+ buffer = await (0, sharp_1.default)(buffer)
64
+ .resize(maxWidth, maxHeight, { fit: "inside" })
65
+ .toBuffer().then((b) => new Uint8Array(b));
66
+ fs_1.default.writeFileSync(filepath, buffer);
67
+ }
68
+ return filepath;
69
+ };
70
+ /**
71
+ * Transparent image check.
72
+ */
73
+ static isTransparent = async (filepath) => {
74
+ const image = (0, sharp_1.default)(filepath);
75
+ const metadata = await image.metadata();
76
+ if (!metadata.hasAlpha)
77
+ return false;
78
+ const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
79
+ for (let i = 3; i < data.length; i += info.channels) {
80
+ if (data[i] === 0)
81
+ return true;
82
+ }
83
+ return false;
84
+ };
85
+ /**
86
+ * Converts image to the specified format. Default is jpg for non-transparent and webp for transparent.
87
+ */
88
+ static convertImage = async (filepath, format, formatOptions, transparentFormat, transparentFormatOptions) => {
89
+ let buffer = fs_1.default.readFileSync(filepath);
90
+ let newBuffer = null;
91
+ let targetFormat = format;
92
+ let targetOptions = formatOptions;
93
+ if (await this.isTransparent(filepath)) {
94
+ if (transparentFormat) {
95
+ targetFormat = transparentFormat;
96
+ targetOptions = transparentFormatOptions;
97
+ }
98
+ else if (!format) {
99
+ targetFormat = "webp";
100
+ targetOptions = undefined;
101
+ }
102
+ }
103
+ if (!targetFormat)
104
+ targetFormat = "jpg";
105
+ switch (targetFormat) {
106
+ case "jpg":
107
+ newBuffer = await (0, sharp_1.default)(buffer).jpeg(targetOptions ?? { quality: 95, optimiseScans: true }).toBuffer();
108
+ break;
109
+ case "png":
110
+ newBuffer = await (0, sharp_1.default)(buffer).png(targetOptions ?? { compressionLevel: 7 }).toBuffer();
111
+ break;
112
+ case "webp":
113
+ newBuffer = await (0, sharp_1.default)(buffer).webp(targetOptions ?? { quality: 90 }).toBuffer();
114
+ break;
115
+ case "avif":
116
+ newBuffer = await (0, sharp_1.default)(buffer).avif(targetOptions ?? { quality: 80, effort: 2 }).toBuffer();
117
+ break;
118
+ case "jxl":
119
+ newBuffer = await (0, sharp_1.default)(buffer).jxl(targetOptions ?? { quality: 90, effort: 4 }).toBuffer();
120
+ break;
121
+ default:
122
+ newBuffer = buffer;
123
+ }
124
+ let newFile = `${path_1.default.basename(filepath, path_1.default.extname(filepath))}.${targetFormat}`;
125
+ const newFilePath = path_1.default.join(path_1.default.dirname(filepath), newFile);
126
+ fs_1.default.writeFileSync(filepath, newBuffer);
127
+ fs_1.default.renameSync(filepath, newFilePath);
128
+ return newFilePath;
129
+ };
130
+ /**
131
+ * Upscale image. Optionally copies unprocessed files (due to an error) to a folder.
132
+ */
133
+ static upscaleImage = async (src, destFolder, options, unprocessedFolder = true) => {
134
+ let dest = path_1.default.join(destFolder, path_1.default.basename(src));
135
+ let target = src;
136
+ let isWebp = path_1.default.extname(src) === ".webp";
137
+ let isAvif = path_1.default.extname(src) === ".avif";
138
+ let isJxl = path_1.default.extname(src) === ".jxl";
139
+ if (isWebp || isAvif || isJxl) {
140
+ fs_1.default.copyFileSync(src, dest);
141
+ target = await this.convertImage(dest, "png");
142
+ }
143
+ let result = await waifu2x_1.default.upscaleImage(target, destFolder, options ?? { rename: "", upscaler: "real-cugan", scale: 4 });
144
+ if (isWebp) {
145
+ await this.convertImage(result, "webp");
146
+ }
147
+ else if (isAvif) {
148
+ await this.convertImage(result, "avif");
149
+ }
150
+ else if (isJxl) {
151
+ await this.convertImage(result, "jxl");
152
+ }
153
+ if (!fs_1.default.existsSync(dest) && unprocessedFolder) {
154
+ let unprocfolder = path_1.default.join(path_1.default.dirname(destFolder), "unprocessed");
155
+ if (!fs_1.default.existsSync(unprocfolder))
156
+ fs_1.default.mkdirSync(unprocfolder);
157
+ fs_1.default.copyFileSync(src, path_1.default.join(unprocfolder, path_1.default.basename(src)));
158
+ }
159
+ return dest;
160
+ };
161
+ /**
162
+ * Processes an image folder with a custom chain of operations.
163
+ */
164
+ static processImages = async (folder, ...operations) => {
165
+ const files = fs_1.default.readdirSync(folder).filter((f) => f !== ".DS_Store");
166
+ let i = 1;
167
+ for (const file of files) {
168
+ console.log(`${i}/${files.length} -> ${file}`);
169
+ let src = path_1.default.join(folder, file);
170
+ if (fs_1.default.lstatSync(src).isDirectory())
171
+ continue;
172
+ for (const operation of operations) {
173
+ src = await operation(src);
174
+ }
175
+ i++;
176
+ }
177
+ };
178
+ /**
179
+ * Shorthand process images with only a resize.
180
+ */
181
+ static resizeImages = (folder, maxSize = 2000) => {
182
+ return this.processImages(folder, async (file) => this.resizeImage(file, maxSize));
183
+ };
184
+ /**
185
+ * Shorthand process images with only a conversion.
186
+ */
187
+ static convertImages = (folder, format = "jpg", formatOptions, transparentFormat = "webp", transparentFormatOptions) => {
188
+ return this.processImages(folder, async (file) => this.convertImage(file, format, formatOptions, transparentFormat, transparentFormatOptions));
189
+ };
190
+ /**
191
+ * Shorthand process images with only a upscale.
192
+ */
193
+ static upscaleImages = (sourceFolder, destFolder, options, unprocessedFolder = true) => {
194
+ return this.processImages(sourceFolder, async (file) => this.upscaleImage(file, destFolder, options, unprocessedFolder));
195
+ };
196
+ /**
197
+ * Splits up a folder into more manageable chunks.
198
+ */
199
+ static splitFolder = (folder, maxAmount = 300) => {
200
+ const files = fs_1.default.readdirSync(folder).filter((f) => f !== ".DS_Store")
201
+ .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare);
202
+ const chunks = Math.ceil(files.length / maxAmount);
203
+ for (let i = 0; i < chunks; i++) {
204
+ const chunkFiles = files.slice(i * maxAmount, (i + 1) * maxAmount);
205
+ const chunkFolder = path_1.default.join(folder, `${i + 1}`);
206
+ if (!fs_1.default.existsSync(chunkFolder))
207
+ fs_1.default.mkdirSync(chunkFolder);
208
+ for (const file of chunkFiles) {
209
+ const src = path_1.default.join(folder, file);
210
+ const dest = path_1.default.join(chunkFolder, file);
211
+ fs_1.default.renameSync(src, dest);
212
+ }
213
+ }
214
+ };
215
+ /**
216
+ * Processes an image folder to be suitable to upload to moepictures.
217
+ */
218
+ static moepicsProcess = async (folder) => {
219
+ const original = path_1.default.join(folder, "original");
220
+ const compressed = path_1.default.join(folder, "compressed");
221
+ const upscaled = path_1.default.join(folder, "upscaled");
222
+ if (!fs_1.default.existsSync(original))
223
+ fs_1.default.mkdirSync(original);
224
+ if (!fs_1.default.existsSync(compressed))
225
+ fs_1.default.mkdirSync(compressed);
226
+ if (!fs_1.default.existsSync(upscaled))
227
+ fs_1.default.mkdirSync(upscaled);
228
+ this.moveImages(folder, original);
229
+ this.copyImages(original, compressed);
230
+ console.log("Compressing images...");
231
+ await this.processImages(compressed, async (file) => this.resizeImage(file), async (file) => this.convertImage(file));
232
+ console.log("Upscaling images...");
233
+ await this.processImages(compressed, async (file) => this.upscaleImage(file, upscaled), async (file) => this.convertImage(file, "avif"));
234
+ this.splitFolder(compressed);
235
+ this.splitFolder(upscaled);
236
+ };
237
+ }
238
+ exports.default = ImageUtils;
@@ -0,0 +1,71 @@
1
+ import sharp from "sharp";
2
+ import { Waifu2xOptions } from "waifu2x";
3
+ type Formats = "jpg" | "png" | "webp" | "avif" | "jxl";
4
+ type FormatOptionMap = {
5
+ jpg: sharp.JpegOptions;
6
+ png: sharp.PngOptions;
7
+ webp: sharp.WebpOptions;
8
+ avif: sharp.AvifOptions;
9
+ jxl: sharp.JxlOptions;
10
+ };
11
+ export default class ImageUtils {
12
+ /**
13
+ * Fixes incorrect image extensions. It must be correct to preview on mac.
14
+ */
15
+ static fixFileExtensions: (folder: string) => Promise<void>;
16
+ /**
17
+ * Copies images to the destination (unchanged)
18
+ */
19
+ static copyImages: (sourceFolder: string, destFolder: string) => void;
20
+ /**
21
+ * Moves images to the destination
22
+ */
23
+ static moveImages: (sourceFolder: string, destFolder: string) => void;
24
+ /**
25
+ * Resizes image down to a maximum width/height.
26
+ */
27
+ static resizeImage: (filepath: string, maxSize?: number | {
28
+ maxWidth: number;
29
+ maxHeight: number;
30
+ }) => Promise<string>;
31
+ /**
32
+ * Transparent image check.
33
+ */
34
+ static isTransparent: (filepath: string) => Promise<boolean>;
35
+ /**
36
+ * Converts image to the specified format. Default is jpg for non-transparent and webp for transparent.
37
+ */
38
+ static convertImage: <T extends Formats>(filepath: string, format?: T, formatOptions?: FormatOptionMap[T], transparentFormat?: T, transparentFormatOptions?: FormatOptionMap[T]) => Promise<string>;
39
+ /**
40
+ * Upscale image. Optionally copies unprocessed files (due to an error) to a folder.
41
+ */
42
+ static upscaleImage: <T extends Formats>(src: string, destFolder: string, options?: Waifu2xOptions, unprocessedFolder?: boolean) => Promise<string>;
43
+ /**
44
+ * Processes an image folder with a custom chain of operations.
45
+ */
46
+ static processImages: <T extends Formats>(folder: string, ...operations: Array<(file: string) => Promise<string>>) => Promise<void>;
47
+ /**
48
+ * Shorthand process images with only a resize.
49
+ */
50
+ static resizeImages: (folder: string, maxSize?: number | {
51
+ maxWidth: number;
52
+ maxHeight: number;
53
+ }) => Promise<void>;
54
+ /**
55
+ * Shorthand process images with only a conversion.
56
+ */
57
+ static convertImages: <T extends Formats>(folder: string, format?: T, formatOptions?: FormatOptionMap[T], transparentFormat?: T, transparentFormatOptions?: FormatOptionMap[T]) => Promise<void>;
58
+ /**
59
+ * Shorthand process images with only a upscale.
60
+ */
61
+ static upscaleImages: <T extends Formats>(sourceFolder: string, destFolder: string, options?: Waifu2xOptions, unprocessedFolder?: boolean) => Promise<void>;
62
+ /**
63
+ * Splits up a folder into more manageable chunks.
64
+ */
65
+ static splitFolder: (folder: string, maxAmount?: number) => void;
66
+ /**
67
+ * Processes an image folder to be suitable to upload to moepictures.
68
+ */
69
+ static moepicsProcess: (folder: string) => Promise<void>;
70
+ }
71
+ export {};
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const fs_1 = __importDefault(require("fs"));
7
+ const path_1 = __importDefault(require("path"));
8
+ const sharp_1 = __importDefault(require("sharp"));
9
+ const waifu2x_1 = __importDefault(require("waifu2x"));
10
+ class ImageUtils {
11
+ /**
12
+ * Fixes incorrect image extensions. It must be correct to preview on mac.
13
+ */
14
+ static fixFileExtensions = async (folder) => {
15
+ const files = fs_1.default.readdirSync(folder).filter((f) => f !== ".DS_Store");
16
+ for (const file of files) {
17
+ let filepath = path_1.default.join(folder, file);
18
+ if (fs_1.default.lstatSync(filepath).isDirectory())
19
+ continue;
20
+ const buffer = fs_1.default.readFileSync(filepath);
21
+ const meta = await (0, sharp_1.default)(buffer, { limitInputPixels: false }).metadata();
22
+ let ext = meta.format.replace("jpeg", "jpg");
23
+ let newFile = `${path_1.default.basename(file, path_1.default.extname(file))}.${ext}`;
24
+ let newFilePath = path_1.default.join(folder, newFile);
25
+ fs_1.default.renameSync(filepath, newFilePath);
26
+ }
27
+ };
28
+ /**
29
+ * Copies images to the destination (unchanged)
30
+ */
31
+ static copyImages = (sourceFolder, destFolder) => {
32
+ const files = fs_1.default.readdirSync(sourceFolder).filter((f) => f !== ".DS_Store");
33
+ for (const file of files) {
34
+ let src = path_1.default.join(sourceFolder, file);
35
+ if (fs_1.default.lstatSync(src).isDirectory())
36
+ continue;
37
+ let dest = path_1.default.join(destFolder, file);
38
+ fs_1.default.copyFileSync(src, dest);
39
+ }
40
+ };
41
+ /**
42
+ * Moves images to the destination
43
+ */
44
+ static moveImages = (sourceFolder, destFolder) => {
45
+ const files = fs_1.default.readdirSync(sourceFolder).filter((f) => f !== ".DS_Store");
46
+ for (const file of files) {
47
+ let src = path_1.default.join(sourceFolder, file);
48
+ if (fs_1.default.lstatSync(src).isDirectory())
49
+ continue;
50
+ let dest = path_1.default.join(destFolder, file);
51
+ fs_1.default.renameSync(src, dest);
52
+ }
53
+ };
54
+ /**
55
+ * Resizes image down to a maximum width/height.
56
+ */
57
+ static resizeImage = async (filepath, maxSize = 2000) => {
58
+ let maxWidth = typeof maxSize === "number" ? maxSize : maxSize.maxWidth;
59
+ let maxHeight = typeof maxSize === "number" ? maxSize : maxSize.maxHeight;
60
+ let buffer = new Uint8Array(fs_1.default.readFileSync(filepath));
61
+ const dim = await (0, sharp_1.default)(buffer).metadata();
62
+ if (dim.width > maxWidth || dim.height > maxHeight) {
63
+ buffer = await (0, sharp_1.default)(buffer)
64
+ .resize(maxWidth, maxHeight, { fit: "inside" })
65
+ .toBuffer().then((b) => new Uint8Array(b));
66
+ fs_1.default.writeFileSync(filepath, buffer);
67
+ }
68
+ return filepath;
69
+ };
70
+ /**
71
+ * Transparent image check.
72
+ */
73
+ static isTransparent = async (filepath) => {
74
+ const image = (0, sharp_1.default)(filepath);
75
+ const metadata = await image.metadata();
76
+ if (!metadata.hasAlpha)
77
+ return false;
78
+ const { data, info } = await image.ensureAlpha().raw().toBuffer({ resolveWithObject: true });
79
+ for (let i = 3; i < data.length; i += info.channels) {
80
+ if (data[i] === 0)
81
+ return true;
82
+ }
83
+ return false;
84
+ };
85
+ /**
86
+ * Converts image to the specified format. Default is jpg for non-transparent and webp for transparent.
87
+ */
88
+ static convertImage = async (filepath, format, formatOptions, transparentFormat, transparentFormatOptions) => {
89
+ let buffer = fs_1.default.readFileSync(filepath);
90
+ let newBuffer = null;
91
+ let targetFormat = format;
92
+ let targetOptions = formatOptions;
93
+ if (await this.isTransparent(filepath)) {
94
+ if (transparentFormat) {
95
+ targetFormat = transparentFormat;
96
+ targetOptions = transparentFormatOptions;
97
+ }
98
+ else if (!format) {
99
+ targetFormat = "webp";
100
+ targetOptions = undefined;
101
+ }
102
+ }
103
+ if (!targetFormat)
104
+ targetFormat = "jpg";
105
+ switch (targetFormat) {
106
+ case "jpg":
107
+ newBuffer = await (0, sharp_1.default)(buffer).jpeg(targetOptions ?? { quality: 95, optimiseScans: true }).toBuffer();
108
+ break;
109
+ case "png":
110
+ newBuffer = await (0, sharp_1.default)(buffer).png(targetOptions ?? { compressionLevel: 7 }).toBuffer();
111
+ break;
112
+ case "webp":
113
+ newBuffer = await (0, sharp_1.default)(buffer).webp(targetOptions ?? { quality: 90 }).toBuffer();
114
+ break;
115
+ case "avif":
116
+ newBuffer = await (0, sharp_1.default)(buffer).avif(targetOptions ?? { quality: 80, effort: 2 }).toBuffer();
117
+ break;
118
+ case "jxl":
119
+ newBuffer = await (0, sharp_1.default)(buffer).jxl(targetOptions ?? { quality: 90, effort: 4 }).toBuffer();
120
+ break;
121
+ default:
122
+ newBuffer = buffer;
123
+ }
124
+ let newFile = `${path_1.default.basename(filepath, path_1.default.extname(filepath))}.${targetFormat}`;
125
+ const newFilePath = path_1.default.join(path_1.default.dirname(filepath), newFile);
126
+ fs_1.default.writeFileSync(filepath, newBuffer);
127
+ fs_1.default.renameSync(filepath, newFilePath);
128
+ return newFilePath;
129
+ };
130
+ /**
131
+ * Upscale image. Optionally copies unprocessed files (due to an error) to a folder.
132
+ */
133
+ static upscaleImage = async (src, destFolder, options, unprocessedFolder = true) => {
134
+ let dest = path_1.default.join(destFolder, path_1.default.basename(src));
135
+ let target = src;
136
+ let isWebp = path_1.default.extname(src) === ".webp";
137
+ let isAvif = path_1.default.extname(src) === ".avif";
138
+ if (isWebp || isAvif) {
139
+ fs_1.default.copyFileSync(src, dest);
140
+ target = await this.convertImage(dest, "png");
141
+ }
142
+ let result = await waifu2x_1.default.upscaleImage(target, destFolder, options ?? { rename: "", upscaler: "real-cugan", scale: 4 });
143
+ if (isWebp) {
144
+ await this.convertImage(result, "webp");
145
+ }
146
+ else if (isAvif) {
147
+ await this.convertImage(result, "avif");
148
+ }
149
+ if (!fs_1.default.existsSync(dest) && unprocessedFolder) {
150
+ let unprocfolder = path_1.default.join(path_1.default.dirname(destFolder), "unprocessed");
151
+ if (!fs_1.default.existsSync(unprocfolder))
152
+ fs_1.default.mkdirSync(unprocfolder);
153
+ fs_1.default.copyFileSync(src, path_1.default.join(unprocfolder, path_1.default.basename(src)));
154
+ }
155
+ return dest;
156
+ };
157
+ /**
158
+ * Processes an image folder with a custom chain of operations.
159
+ */
160
+ static processImages = async (folder, ...operations) => {
161
+ const files = fs_1.default.readdirSync(folder).filter((f) => f !== ".DS_Store");
162
+ let i = 1;
163
+ for (const file of files) {
164
+ console.log(`${i}/${files.length} -> ${file}`);
165
+ let src = path_1.default.join(folder, file);
166
+ if (fs_1.default.lstatSync(src).isDirectory())
167
+ continue;
168
+ for (const operation of operations) {
169
+ src = await operation(src);
170
+ }
171
+ i++;
172
+ }
173
+ };
174
+ /**
175
+ * Shorthand process images with only a resize.
176
+ */
177
+ static resizeImages = (folder, maxSize = 2000) => {
178
+ return this.processImages(folder, async (file) => this.resizeImage(file, maxSize));
179
+ };
180
+ /**
181
+ * Shorthand process images with only a conversion.
182
+ */
183
+ static convertImages = (folder, format = "jpg", formatOptions, transparentFormat = "webp", transparentFormatOptions) => {
184
+ return this.processImages(folder, async (file) => this.convertImage(file, format, formatOptions, transparentFormat, transparentFormatOptions));
185
+ };
186
+ /**
187
+ * Shorthand process images with only a upscale.
188
+ */
189
+ static upscaleImages = (sourceFolder, destFolder, options, unprocessedFolder = true) => {
190
+ return this.processImages(sourceFolder, async (file) => this.upscaleImage(file, destFolder, options, unprocessedFolder));
191
+ };
192
+ /**
193
+ * Splits up a folder into more manageable chunks.
194
+ */
195
+ static splitFolder = (folder, maxAmount = 300) => {
196
+ const files = fs_1.default.readdirSync(folder).filter((f) => f !== ".DS_Store")
197
+ .sort(new Intl.Collator(undefined, { numeric: true, sensitivity: "base" }).compare);
198
+ const chunks = Math.ceil(files.length / maxAmount);
199
+ for (let i = 0; i < chunks; i++) {
200
+ const chunkFiles = files.slice(i * maxAmount, (i + 1) * maxAmount);
201
+ const chunkFolder = path_1.default.join(folder, `${i + 1}`);
202
+ if (!fs_1.default.existsSync(chunkFolder))
203
+ fs_1.default.mkdirSync(chunkFolder);
204
+ for (const file of chunkFiles) {
205
+ const src = path_1.default.join(folder, file);
206
+ const dest = path_1.default.join(chunkFolder, file);
207
+ fs_1.default.renameSync(src, dest);
208
+ }
209
+ }
210
+ };
211
+ /**
212
+ * Processes an image folder to be suitable to upload to moepictures.
213
+ */
214
+ static moepicsProcess = async (folder) => {
215
+ const original = path_1.default.join(folder, "original");
216
+ const compressed = path_1.default.join(folder, "compressed");
217
+ const upscaled = path_1.default.join(folder, "upscaled");
218
+ if (!fs_1.default.existsSync(original))
219
+ fs_1.default.mkdirSync(original);
220
+ if (!fs_1.default.existsSync(compressed))
221
+ fs_1.default.mkdirSync(compressed);
222
+ if (!fs_1.default.existsSync(upscaled))
223
+ fs_1.default.mkdirSync(upscaled);
224
+ this.moveImages(folder, original);
225
+ this.copyImages(original, compressed);
226
+ console.log("Compressing images...");
227
+ await this.processImages(compressed, async (file) => this.resizeImage(file), async (file) => this.convertImage(file));
228
+ console.log("Upscaling images...");
229
+ await this.processImages(compressed, async (file) => this.upscaleImage(file, upscaled), async (file) => this.convertImage(file, "avif"));
230
+ this.splitFolder(compressed);
231
+ this.splitFolder(upscaled);
232
+ };
233
+ }
234
+ exports.default = ImageUtils;
package/dist/pics.d.ts ADDED
@@ -0,0 +1 @@
1
+ import "dotenv/config";
package/dist/pics.js ADDED
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ require("dotenv/config");
7
+ const image_utils_1 = __importDefault(require("./image-utils"));
8
+ const start = async () => {
9
+ await image_utils_1.default.splitFolder(process.env.FOLDER);
10
+ };
11
+ start();
package/image-utils.ts ADDED
@@ -0,0 +1,271 @@
1
+ import fs from "fs"
2
+ import path from "path"
3
+ import sharp from "sharp"
4
+ import waifu2x, {Waifu2xOptions} from "waifu2x"
5
+
6
+ type Formats = "jpg" | "png" | "webp" | "avif" | "jxl"
7
+
8
+ type FormatOptionMap = {
9
+ jpg: sharp.JpegOptions
10
+ png: sharp.PngOptions
11
+ webp: sharp.WebpOptions
12
+ avif: sharp.AvifOptions
13
+ jxl: sharp.JxlOptions
14
+ }
15
+
16
+ export default class ImageUtils {
17
+ /**
18
+ * Fixes incorrect image extensions. It must be correct to preview on mac.
19
+ */
20
+ public static fixFileExtensions = async (folder: string) => {
21
+ const files = fs.readdirSync(folder).filter((f) => f !== ".DS_Store")
22
+ for (const file of files) {
23
+ let filepath = path.join(folder, file)
24
+ if (fs.lstatSync(filepath).isDirectory()) continue
25
+ const buffer = fs.readFileSync(filepath)
26
+ const meta = await sharp(buffer, {limitInputPixels: false}).metadata()
27
+ let ext = meta.format.replace("jpeg", "jpg")
28
+ let newFile = `${path.basename(file, path.extname(file))}.${ext}`
29
+ let newFilePath = path.join(folder, newFile)
30
+ fs.renameSync(filepath, newFilePath)
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Copies images to the destination (unchanged)
36
+ */
37
+ public static copyImages = (sourceFolder: string, destFolder: string) => {
38
+ const files = fs.readdirSync(sourceFolder).filter((f) => f !== ".DS_Store")
39
+ for (const file of files) {
40
+ let src = path.join(sourceFolder, file)
41
+ if (fs.lstatSync(src).isDirectory()) continue
42
+ let dest = path.join(destFolder, file)
43
+ fs.copyFileSync(src, dest)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Moves images to the destination
49
+ */
50
+ public static moveImages = (sourceFolder: string, destFolder: string) => {
51
+ const files = fs.readdirSync(sourceFolder).filter((f) => f !== ".DS_Store")
52
+ for (const file of files) {
53
+ let src = path.join(sourceFolder, file)
54
+ if (fs.lstatSync(src).isDirectory()) continue
55
+ let dest = path.join(destFolder, file)
56
+ fs.renameSync(src, dest)
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Resizes image down to a maximum width/height.
62
+ */
63
+ public static resizeImage = async (filepath: string, maxSize: number | {maxWidth: number, maxHeight: number} = 2000) => {
64
+ let maxWidth = typeof maxSize === "number" ? maxSize : maxSize.maxWidth
65
+ let maxHeight = typeof maxSize === "number" ? maxSize : maxSize.maxHeight
66
+ let buffer = new Uint8Array(fs.readFileSync(filepath))
67
+ const dim = await sharp(buffer).metadata()
68
+ if (dim.width > maxWidth || dim.height > maxHeight) {
69
+ buffer = await sharp(buffer)
70
+ .resize(maxWidth, maxHeight, {fit: "inside"})
71
+ .toBuffer().then((b) => new Uint8Array(b))
72
+ fs.writeFileSync(filepath, buffer)
73
+ }
74
+ return filepath
75
+ }
76
+
77
+ /**
78
+ * Transparent image check.
79
+ */
80
+ public static isTransparent = async (filepath: string) => {
81
+ const image = sharp(filepath)
82
+ const metadata = await image.metadata()
83
+ if (!metadata.hasAlpha) return false
84
+
85
+ const {data, info} = await image.ensureAlpha().raw().toBuffer({resolveWithObject: true})
86
+
87
+ for (let i = 3; i < data.length; i += info.channels) {
88
+ if (data[i] === 0) return true
89
+ }
90
+
91
+ return false
92
+ }
93
+
94
+ /**
95
+ * Converts image to the specified format. Default is jpg for non-transparent and webp for transparent.
96
+ */
97
+ public static convertImage = async <T extends Formats>(filepath: string, format?: T, formatOptions?: FormatOptionMap[T],
98
+ transparentFormat?: T, transparentFormatOptions?: FormatOptionMap[T]) => {
99
+ let buffer = fs.readFileSync(filepath)
100
+ let newBuffer = null as unknown as Buffer
101
+
102
+ let targetFormat = format
103
+ let targetOptions = formatOptions
104
+ if (await this.isTransparent(filepath)) {
105
+ if (transparentFormat) {
106
+ targetFormat = transparentFormat
107
+ targetOptions = transparentFormatOptions
108
+ } else if (!format) {
109
+ targetFormat = "webp" as T
110
+ targetOptions = undefined
111
+ }
112
+ }
113
+ if (!targetFormat) targetFormat = "jpg" as T
114
+
115
+ switch(targetFormat) {
116
+ case "jpg":
117
+ newBuffer = await sharp(buffer).jpeg(targetOptions ?? {quality: 95, optimiseScans: true}).toBuffer()
118
+ break
119
+ case "png":
120
+ newBuffer = await sharp(buffer).png(targetOptions ?? {compressionLevel: 7}).toBuffer()
121
+ break
122
+ case "webp":
123
+ newBuffer = await sharp(buffer).webp(targetOptions ?? {quality: 90}).toBuffer()
124
+ break
125
+ case "avif":
126
+ newBuffer = await sharp(buffer).avif(targetOptions ?? {quality: 80, effort: 2}).toBuffer()
127
+ break
128
+ case "jxl":
129
+ newBuffer = await sharp(buffer).jxl(targetOptions ?? {quality: 90, effort: 4}).toBuffer()
130
+ break
131
+ default:
132
+ newBuffer = buffer
133
+ }
134
+
135
+ let newFile = `${path.basename(filepath, path.extname(filepath))}.${targetFormat}`
136
+ const newFilePath = path.join(path.dirname(filepath), newFile)
137
+ fs.writeFileSync(filepath, newBuffer)
138
+ fs.renameSync(filepath, newFilePath)
139
+ return newFilePath
140
+ }
141
+
142
+ /**
143
+ * Upscale image. Optionally copies unprocessed files (due to an error) to a folder.
144
+ */
145
+ public static upscaleImage = async <T extends Formats>(src: string, destFolder: string,
146
+ options?: Waifu2xOptions, unprocessedFolder: boolean = true) => {
147
+ let dest = path.join(destFolder, path.basename(src))
148
+
149
+ let target = src
150
+ let isWebp = path.extname(src) === ".webp"
151
+ let isAvif = path.extname(src) === ".avif"
152
+ let isJxl = path.extname(src) === ".jxl"
153
+ if (isWebp || isAvif || isJxl) {
154
+ fs.copyFileSync(src, dest)
155
+ target = await this.convertImage(dest, "png")
156
+ }
157
+
158
+ let result = await waifu2x.upscaleImage(target, destFolder, options ?? {rename: "", upscaler: "real-cugan", scale: 4})
159
+ if (isWebp) {
160
+ await this.convertImage(result, "webp")
161
+ } else if (isAvif) {
162
+ await this.convertImage(result, "avif")
163
+ } else if (isJxl) {
164
+ await this.convertImage(result, "jxl")
165
+ }
166
+
167
+ if (!fs.existsSync(dest) && unprocessedFolder) {
168
+ let unprocfolder = path.join(path.dirname(destFolder), "unprocessed")
169
+ if (!fs.existsSync(unprocfolder)) fs.mkdirSync(unprocfolder)
170
+ fs.copyFileSync(src, path.join(unprocfolder, path.basename(src)))
171
+ }
172
+ return dest
173
+ }
174
+
175
+ /**
176
+ * Processes an image folder with a custom chain of operations.
177
+ */
178
+ public static processImages = async <T extends Formats>(folder: string,
179
+ ...operations: Array<(file: string) => Promise<string>>) => {
180
+ const files = fs.readdirSync(folder).filter((f) => f !== ".DS_Store")
181
+ let i = 1
182
+ for (const file of files) {
183
+ console.log(`${i}/${files.length} -> ${file}`)
184
+ let src = path.join(folder, file)
185
+ if (fs.lstatSync(src).isDirectory()) continue
186
+ for (const operation of operations) {
187
+ src = await operation(src)
188
+ }
189
+ i++
190
+ }
191
+ }
192
+
193
+ /**
194
+ * Shorthand process images with only a resize.
195
+ */
196
+ public static resizeImages = (folder: string, maxSize: number | {maxWidth: number, maxHeight: number} = 2000) => {
197
+ return this.processImages(folder,
198
+ async (file) => this.resizeImage(file, maxSize)
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Shorthand process images with only a conversion.
204
+ */
205
+ public static convertImages = <T extends Formats>(folder: string, format = "jpg" as T, formatOptions?: FormatOptionMap[T],
206
+ transparentFormat = "webp" as T, transparentFormatOptions?: FormatOptionMap[T]) => {
207
+ return this.processImages(folder,
208
+ async (file) => this.convertImage(file, format, formatOptions, transparentFormat, transparentFormatOptions)
209
+ )
210
+ }
211
+
212
+ /**
213
+ * Shorthand process images with only a upscale.
214
+ */
215
+ public static upscaleImages = <T extends Formats>(sourceFolder: string, destFolder: string,
216
+ options?: Waifu2xOptions, unprocessedFolder: boolean = true) => {
217
+ return this.processImages(sourceFolder,
218
+ async (file) => this.upscaleImage(file, destFolder, options, unprocessedFolder)
219
+ )
220
+ }
221
+
222
+ /**
223
+ * Splits up a folder into more manageable chunks.
224
+ */
225
+ public static splitFolder = (folder: string, maxAmount: number = 300) => {
226
+ const files = fs.readdirSync(folder).filter((f) => f !== ".DS_Store")
227
+ .sort(new Intl.Collator(undefined, {numeric: true, sensitivity: "base"}).compare)
228
+
229
+ const chunks = Math.ceil(files.length / maxAmount)
230
+
231
+ for (let i = 0; i < chunks; i++) {
232
+ const chunkFiles = files.slice(i * maxAmount, (i + 1) * maxAmount)
233
+ const chunkFolder = path.join(folder, `${i + 1}`)
234
+ if (!fs.existsSync(chunkFolder)) fs.mkdirSync(chunkFolder)
235
+
236
+ for (const file of chunkFiles) {
237
+ const src = path.join(folder, file)
238
+ const dest = path.join(chunkFolder, file)
239
+ fs.renameSync(src, dest)
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Processes an image folder to be suitable to upload to moepictures.
246
+ */
247
+ public static moepicsProcess = async (folder: string) => {
248
+ const original = path.join(folder, "original")
249
+ const compressed = path.join(folder, "compressed")
250
+ const upscaled = path.join(folder, "upscaled")
251
+ if (!fs.existsSync(original)) fs.mkdirSync(original)
252
+ if (!fs.existsSync(compressed)) fs.mkdirSync(compressed)
253
+ if (!fs.existsSync(upscaled)) fs.mkdirSync(upscaled)
254
+
255
+ this.moveImages(folder, original)
256
+ this.copyImages(original, compressed)
257
+ console.log("Compressing images...")
258
+ await this.processImages(compressed,
259
+ async (file) => this.resizeImage(file),
260
+ async (file) => this.convertImage(file)
261
+ )
262
+ console.log("Upscaling images...")
263
+ await this.processImages(compressed,
264
+ async (file) => this.upscaleImage(file, upscaled),
265
+ async (file) => this.convertImage(file, "avif")
266
+ )
267
+
268
+ this.splitFolder(compressed)
269
+ this.splitFolder(upscaled)
270
+ }
271
+ }
package/license.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Moebytes
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/package.json ADDED
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "animepic-utils",
3
+ "version": "0.0.1",
4
+ "description": "Node.js utils for processing anime images",
5
+ "main": "dist/image-utils.js",
6
+ "types": "dist/image-utils.d.ts",
7
+ "scripts": {
8
+ "start": "tsc && node dist/pics.js",
9
+ "clean": "del-cli ./dist",
10
+ "prepublishOnly": "tsc"
11
+ },
12
+ "keywords": ["anime", "image", "utils"],
13
+ "author": "Moebytes",
14
+ "license": "MIT",
15
+ "devDependencies": {
16
+ "@types/node": "^24.7.2",
17
+ "del-cli": "^7.0.0",
18
+ "dotenv": "^17.2.3",
19
+ "typescript": "^5.9.3"
20
+ },
21
+ "dependencies": {
22
+ "sharp": "^0.34.4",
23
+ "waifu2x": "^1.5.1"
24
+ }
25
+ }
package/pics.ts ADDED
@@ -0,0 +1,8 @@
1
+ import "dotenv/config"
2
+ import imageUtils from "./image-utils"
3
+
4
+ const start = async () => {
5
+ await imageUtils.splitFolder(process.env.FOLDER!)
6
+ }
7
+
8
+ start()
package/readme.md ADDED
@@ -0,0 +1,38 @@
1
+ ## Animepic Utils
2
+
3
+ Some utilities for processing anime images. (However I guess it'll work for any images).
4
+
5
+ The primary function is `processImages` that accepts a folder of images, and then a variable
6
+ amount of processing functions that will be applied to every image in the folder. The processing functions
7
+ should take the current file parameter and return the path to the output, this is then fed back as the
8
+ "file" argument to the next function in the chain. See below for an example of using it.
9
+
10
+ ```ts
11
+ import imageUtils from "animepic-utils"
12
+
13
+ await imageUtils.processImages(folder,
14
+ async (file) => this.resizeImage(file),
15
+ async (file) => this.convertImage(file),
16
+ async (file) => this.upscaleImage(file, upscaledFolder)
17
+ )
18
+ ```
19
+
20
+ We already have functions for resizing, conversion, and upscaling, but you can continue to write your
21
+ own if you wish. Apart from image processing, there is the `fixFileExtensions` function that will correct
22
+ the file extensions of all the images in a folder. On macOS, the extensions have to be correct in order for
23
+ the file to preview in finder, so a png cannot have the jpg extension.
24
+
25
+ ```ts
26
+ import imageUtils from "animepic-utils"
27
+
28
+ await imageUtils.fixFileExtensions(folder)
29
+ ```
30
+
31
+ Lastly, the function `moepicsProcess` will takes a folder of anime images and will generate the compressed
32
+ and upscaled versions that are suitable to upload to my website, https://moepictures.moe.
33
+
34
+ ```ts
35
+ import imageUtils from "animepic-utils"
36
+
37
+ await imageUtils.moepicsProcess(folder)
38
+ ```
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "outDir": "dist",
4
+ "declaration": true,
5
+ "noImplicitAny": false,
6
+ "moduleResolution": "node",
7
+ "target": "es2023",
8
+ "module": "commonjs",
9
+ "lib": ["es2023", "dom"],
10
+ "allowJs": true,
11
+ "allowSyntheticDefaultImports": true,
12
+ "jsx": "react-jsx",
13
+ "esModuleInterop": true,
14
+ "strict": true,
15
+ "strictNullChecks": true,
16
+ "skipLibCheck": true,
17
+ "resolveJsonModule": true
18
+ },
19
+ "exclude": [
20
+ "node_modules",
21
+ "dist"
22
+ ]
23
+ }