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.
- package/contributing.md +177 -0
- package/dist/image-utils.d.ts +71 -0
- package/dist/image-utils.js +238 -0
- package/dist/imageutils.d.ts +71 -0
- package/dist/imageutils.js +234 -0
- package/dist/pics.d.ts +1 -0
- package/dist/pics.js +11 -0
- package/image-utils.ts +271 -0
- package/license.txt +21 -0
- package/package.json +25 -0
- package/pics.ts +8 -0
- package/readme.md +38 -0
- package/tsconfig.json +23 -0
package/contributing.md
ADDED
|
@@ -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
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
|
+
}
|