@phosart/common 0.4.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/FullGallery.svelte +51 -0
- package/dist/FullGallery.svelte.d.ts +17 -0
- package/dist/FullGallery.svelte.d.ts.map +1 -0
- package/dist/Gallery.svelte +30 -0
- package/dist/Gallery.svelte.d.ts +14 -0
- package/dist/Gallery.svelte.d.ts.map +1 -0
- package/dist/GalleryPreview.svelte +60 -0
- package/dist/GalleryPreview.svelte.d.ts +9 -0
- package/dist/GalleryPreview.svelte.d.ts.map +1 -0
- package/dist/HighResContext.svelte +21 -0
- package/dist/HighResContext.svelte.d.ts +8 -0
- package/dist/HighResContext.svelte.d.ts.map +1 -0
- package/dist/Image.svelte +171 -0
- package/dist/Image.svelte.d.ts +14 -0
- package/dist/Image.svelte.d.ts.map +1 -0
- package/dist/Modal.svelte +87 -0
- package/dist/Modal.svelte.d.ts +9 -0
- package/dist/Modal.svelte.d.ts.map +1 -0
- package/dist/ModalGallery/Carousel.svelte +76 -0
- package/dist/ModalGallery/Carousel.svelte.d.ts +10 -0
- package/dist/ModalGallery/Carousel.svelte.d.ts.map +1 -0
- package/dist/ModalGallery/ImageSection.svelte +156 -0
- package/dist/ModalGallery/ImageSection.svelte.d.ts +11 -0
- package/dist/ModalGallery/ImageSection.svelte.d.ts.map +1 -0
- package/dist/ModalGallery/ImageView.svelte +92 -0
- package/dist/ModalGallery/ImageView.svelte.d.ts +9 -0
- package/dist/ModalGallery/ImageView.svelte.d.ts.map +1 -0
- package/dist/ModalGallery/Spinner.svelte +71 -0
- package/dist/ModalGallery/Spinner.svelte.d.ts +7 -0
- package/dist/ModalGallery/Spinner.svelte.d.ts.map +1 -0
- package/dist/ModalGallery.svelte +165 -0
- package/dist/ModalGallery.svelte.d.ts +16 -0
- package/dist/ModalGallery.svelte.d.ts.map +1 -0
- package/dist/OpengraphMeta.svelte +125 -0
- package/dist/OpengraphMeta.svelte.d.ts +15 -0
- package/dist/OpengraphMeta.svelte.d.ts.map +1 -0
- package/dist/Postcard/ArtistLink.svelte +46 -0
- package/dist/Postcard/ArtistLink.svelte.d.ts +9 -0
- package/dist/Postcard/ArtistLink.svelte.d.ts.map +1 -0
- package/dist/Postcard/Chip.svelte +100 -0
- package/dist/Postcard/Chip.svelte.d.ts +12 -0
- package/dist/Postcard/Chip.svelte.d.ts.map +1 -0
- package/dist/Postcard/Description/Character.svelte +79 -0
- package/dist/Postcard/Description/Character.svelte.d.ts +9 -0
- package/dist/Postcard/Description/Character.svelte.d.ts.map +1 -0
- package/dist/Postcard/Description.svelte +146 -0
- package/dist/Postcard/Description.svelte.d.ts +13 -0
- package/dist/Postcard/Description.svelte.d.ts.map +1 -0
- package/dist/Postcard/Headline.svelte +70 -0
- package/dist/Postcard/Headline.svelte.d.ts +10 -0
- package/dist/Postcard/Headline.svelte.d.ts.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/server/artist.d.ts +7 -0
- package/dist/server/artist.d.ts.map +1 -0
- package/dist/server/artist.js +40 -0
- package/dist/server/character.d.ts +9 -0
- package/dist/server/character.d.ts.map +1 -0
- package/dist/server/character.js +76 -0
- package/dist/server/directories.d.ts +4 -0
- package/dist/server/directories.d.ts.map +1 -0
- package/dist/server/directories.js +39 -0
- package/dist/server/fastcache.d.ts +11 -0
- package/dist/server/fastcache.d.ts.map +1 -0
- package/dist/server/fastcache.js +43 -0
- package/dist/server/filter.d.ts +6 -0
- package/dist/server/filter.d.ts.map +1 -0
- package/dist/server/filter.js +53 -0
- package/dist/server/gallery.d.ts +16 -0
- package/dist/server/gallery.d.ts.map +1 -0
- package/dist/server/gallery.js +162 -0
- package/dist/server/imageprocess.d.ts +18 -0
- package/dist/server/imageprocess.d.ts.map +1 -0
- package/dist/server/imageprocess.js +243 -0
- package/dist/server/index.d.ts +15 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +15 -0
- package/dist/server/models/Artist.d.ts +7 -0
- package/dist/server/models/Artist.d.ts.map +1 -0
- package/dist/server/models/Artist.js +15 -0
- package/dist/server/models/Character.d.ts +108 -0
- package/dist/server/models/Character.d.ts.map +1 -0
- package/dist/server/models/Character.js +21 -0
- package/dist/server/models/Gallery.d.ts +373 -0
- package/dist/server/models/Gallery.d.ts.map +1 -0
- package/dist/server/models/Gallery.js +60 -0
- package/dist/server/models/image.d.ts +64 -0
- package/dist/server/models/image.d.ts.map +1 -0
- package/dist/server/models/image.js +17 -0
- package/dist/server/pack.d.ts +3 -0
- package/dist/server/pack.d.ts.map +1 -0
- package/dist/server/pack.js +26 -0
- package/dist/server/theme/schema.d.ts +57 -0
- package/dist/server/theme/schema.d.ts.map +1 -0
- package/dist/server/theme/schema.js +217 -0
- package/dist/server/util.d.ts +24 -0
- package/dist/server/util.d.ts.map +1 -0
- package/dist/server/util.js +71 -0
- package/dist/util/art.d.ts +52 -0
- package/dist/util/art.d.ts.map +1 -0
- package/dist/util/art.js +57 -0
- package/dist/util/artistcontext.svelte.d.ts +8 -0
- package/dist/util/artistcontext.svelte.d.ts.map +1 -0
- package/dist/util/artistcontext.svelte.js +18 -0
- package/dist/util/charactercontext.svelte.d.ts +4 -0
- package/dist/util/charactercontext.svelte.d.ts.map +1 -0
- package/dist/util/charactercontext.svelte.js +11 -0
- package/dist/util/date.d.ts +2 -0
- package/dist/util/date.d.ts.map +1 -0
- package/dist/util/date.js +6 -0
- package/dist/util/index.d.ts +13 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +10 -0
- package/dist/util/markdown.d.ts +2 -0
- package/dist/util/markdown.d.ts.map +1 -0
- package/dist/util/markdown.js +16 -0
- package/dist/util/phosart_config.svelte.d.ts +44 -0
- package/dist/util/phosart_config.svelte.d.ts.map +1 -0
- package/dist/util/phosart_config.svelte.js +51 -0
- package/dist/util/search.d.ts +3 -0
- package/dist/util/search.d.ts.map +1 -0
- package/dist/util/search.js +24 -0
- package/dist/util/smoothscroll.d.ts +4 -0
- package/dist/util/smoothscroll.d.ts.map +1 -0
- package/dist/util/smoothscroll.js +21 -0
- package/dist/util/tree.d.ts +19 -0
- package/dist/util/tree.d.ts.map +1 -0
- package/dist/util/tree.js +58 -0
- package/dist/util/util.d.ts +4 -0
- package/dist/util/util.d.ts.map +1 -0
- package/dist/util/util.js +22 -0
- package/package.json +102 -0
- package/src/lib/FullGallery.svelte +51 -0
- package/src/lib/Gallery.svelte +30 -0
- package/src/lib/GalleryPreview.svelte +60 -0
- package/src/lib/HighResContext.svelte +21 -0
- package/src/lib/Image.svelte +171 -0
- package/src/lib/Modal.svelte +87 -0
- package/src/lib/ModalGallery/Carousel.svelte +76 -0
- package/src/lib/ModalGallery/ImageSection.svelte +156 -0
- package/src/lib/ModalGallery/ImageView.svelte +92 -0
- package/src/lib/ModalGallery/Spinner.svelte +71 -0
- package/src/lib/ModalGallery.svelte +165 -0
- package/src/lib/OpengraphMeta.svelte +125 -0
- package/src/lib/Postcard/ArtistLink.svelte +46 -0
- package/src/lib/Postcard/Chip.svelte +100 -0
- package/src/lib/Postcard/Description/Character.svelte +79 -0
- package/src/lib/Postcard/Description.svelte +146 -0
- package/src/lib/Postcard/Headline.svelte +70 -0
- package/src/lib/index.ts +20 -0
- package/src/lib/server/artist.ts +50 -0
- package/src/lib/server/character.ts +113 -0
- package/src/lib/server/directories.ts +45 -0
- package/src/lib/server/fastcache.ts +66 -0
- package/src/lib/server/filter.ts +71 -0
- package/src/lib/server/gallery.ts +259 -0
- package/src/lib/server/imageprocess.ts +382 -0
- package/src/lib/server/index.ts +57 -0
- package/src/lib/server/models/Artist.ts +19 -0
- package/src/lib/server/models/Character.ts +24 -0
- package/src/lib/server/models/Gallery.ts +70 -0
- package/src/lib/server/models/image.ts +20 -0
- package/src/lib/server/pack.ts +31 -0
- package/src/lib/server/theme/schema.ts +286 -0
- package/src/lib/server/util.ts +102 -0
- package/src/lib/util/art.ts +136 -0
- package/src/lib/util/artistcontext.svelte.ts +25 -0
- package/src/lib/util/charactercontext.svelte.ts +15 -0
- package/src/lib/util/date.ts +7 -0
- package/src/lib/util/index.ts +29 -0
- package/src/lib/util/markdown.ts +17 -0
- package/src/lib/util/phosart_config.svelte.ts +101 -0
- package/src/lib/util/search.ts +28 -0
- package/src/lib/util/smoothscroll.ts +21 -0
- package/src/lib/util/tree.ts +75 -0
- package/src/lib/util/util.ts +37 -0
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import sharp, { type Sharp } from 'sharp';
|
|
2
|
+
import { Logger } from 'tslog';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import type { Source } from '../util/art.ts';
|
|
6
|
+
import { getLogLevel, hashUrl } from './util.ts';
|
|
7
|
+
import type { z } from 'zod';
|
|
8
|
+
import { $PUBLIC } from './directories.ts';
|
|
9
|
+
import type { Picture } from './models/image.ts';
|
|
10
|
+
import { getFastHash, updateFastCache, type FastCache } from './fastcache.ts';
|
|
11
|
+
import sharpPhash from 'sharp-phash';
|
|
12
|
+
import { Sema } from 'async-sema';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
const ImageProcessLog = new Logger({ minLevel: getLogLevel() });
|
|
15
|
+
const sema = new Sema(os.cpus().length, { capacity: 1000 });
|
|
16
|
+
|
|
17
|
+
const phash: typeof import('sharp-phash').default =
|
|
18
|
+
sharpPhash as unknown as typeof import('sharp-phash').default;
|
|
19
|
+
|
|
20
|
+
let processedHashes = new Set<string>();
|
|
21
|
+
|
|
22
|
+
type SourceInfo = Source;
|
|
23
|
+
type ImageFormat = keyof sharp.FormatEnum;
|
|
24
|
+
|
|
25
|
+
interface SavedImage extends SourceInfo {
|
|
26
|
+
format: ImageFormat;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface Transformation {
|
|
30
|
+
format: ImageFormat;
|
|
31
|
+
width: number;
|
|
32
|
+
height?: number;
|
|
33
|
+
position?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface ProcessOptions {
|
|
37
|
+
forceKeep?: boolean;
|
|
38
|
+
position?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const LQIP_WIDTH = 64;
|
|
42
|
+
const FORMATS: Array<ImageFormat> = ['avif', 'webp'];
|
|
43
|
+
const WIDTHS = [640, 960, 1280, 1920, 3840];
|
|
44
|
+
const THUMBS = [160, 320];
|
|
45
|
+
|
|
46
|
+
export function getProcessedHashes(): ReadonlySet<string> {
|
|
47
|
+
return processedHashes;
|
|
48
|
+
}
|
|
49
|
+
export function clearProcessedHashes() {
|
|
50
|
+
processedHashes = new Set();
|
|
51
|
+
}
|
|
52
|
+
export async function getUnusedHashes(): Promise<ReadonlySet<string>> {
|
|
53
|
+
const eligible = new Set<string>();
|
|
54
|
+
const pubdir = await fs.readdir($PUBLIC(), { withFileTypes: true, recursive: false });
|
|
55
|
+
|
|
56
|
+
await Promise.all(
|
|
57
|
+
pubdir.map(async (dirent) => {
|
|
58
|
+
if (!dirent.isDirectory()) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const ents = await fs.readdir(path.resolve(path.join($PUBLIC(), dirent.name)));
|
|
62
|
+
if (!ents.includes('.keep.mark')) {
|
|
63
|
+
eligible.add(dirent.name);
|
|
64
|
+
}
|
|
65
|
+
})
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return eligible.difference(processedHashes);
|
|
69
|
+
}
|
|
70
|
+
export async function deleteHashes(unused: ReadonlySet<string>) {
|
|
71
|
+
if (unused.size === 0) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
ImageProcessLog.warn('Cleaning up generated directory:', unused.size, 'unused folders found...');
|
|
75
|
+
const respub = path.resolve($PUBLIC());
|
|
76
|
+
for (const hash of unused) {
|
|
77
|
+
const resolved = path.resolve(path.join($PUBLIC(), hash));
|
|
78
|
+
if (!resolved.startsWith(respub)) {
|
|
79
|
+
ImageProcessLog.warn(
|
|
80
|
+
'Tried to clean hash at path',
|
|
81
|
+
resolved,
|
|
82
|
+
'which is not a subdirectory of public',
|
|
83
|
+
respub,
|
|
84
|
+
'??'
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
ImageProcessLog.debug('Deleting', resolved, '...');
|
|
88
|
+
await fs.rm(resolved, { recursive: true, force: true });
|
|
89
|
+
ImageProcessLog.info('Deleted unused generated directory', resolved, '...');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
export async function cleanUnusedHashes() {
|
|
94
|
+
await deleteHashes(await getUnusedHashes());
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function getPictureDetails(h: string): Promise<z.infer<typeof Picture> | null> {
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(
|
|
100
|
+
await fs.readFile(path.join($PUBLIC(), h, 'details.json'), { encoding: 'utf-8' })
|
|
101
|
+
);
|
|
102
|
+
} catch {
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function processVideoFastcache(
|
|
108
|
+
fc: FastCache,
|
|
109
|
+
fullpath: string,
|
|
110
|
+
relpath: string,
|
|
111
|
+
name: string,
|
|
112
|
+
processOptions: ProcessOptions = {}
|
|
113
|
+
): Promise<string> {
|
|
114
|
+
const [prehash, mtime] = await getFastHash(fc, fullpath, relpath);
|
|
115
|
+
const [path, hash] = await doProcessVideo(fullpath, name, prehash, processOptions);
|
|
116
|
+
if (!prehash) {
|
|
117
|
+
await updateFastCache(fc, fullpath, relpath, hash, mtime);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return path;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function processVideo(url: string, name: string): Promise<string> {
|
|
124
|
+
return (await doProcessVideo(url, name, null))[0];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function doProcessVideo(
|
|
128
|
+
url: string,
|
|
129
|
+
name: string,
|
|
130
|
+
prehash: string | null,
|
|
131
|
+
processOptions: ProcessOptions = {}
|
|
132
|
+
): Promise<[string, string]> {
|
|
133
|
+
const h = prehash ?? (await hashUrl(url));
|
|
134
|
+
processedHashes.add(h);
|
|
135
|
+
|
|
136
|
+
name = name + path.extname(url);
|
|
137
|
+
const outputDir = path.join($PUBLIC(), h);
|
|
138
|
+
await fs.mkdir(outputDir, { recursive: true });
|
|
139
|
+
await fs.copyFile(url, path.join(outputDir, name));
|
|
140
|
+
if (processOptions?.forceKeep) {
|
|
141
|
+
await fs.writeFile(path.join(outputDir, '.keep.mark'), '');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return [`/_/${h}/${name}`, h];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function processImageFastcache(
|
|
148
|
+
fc: FastCache,
|
|
149
|
+
fullpath: string,
|
|
150
|
+
relpath: string,
|
|
151
|
+
processOptions: ProcessOptions = {}
|
|
152
|
+
): Promise<z.infer<typeof Picture>> {
|
|
153
|
+
const [prehash, mtime] = await getFastHash(fc, fullpath, relpath);
|
|
154
|
+
const [image, hash] = await doProcessImage(fullpath, prehash, processOptions);
|
|
155
|
+
if (!prehash) {
|
|
156
|
+
await updateFastCache(fc, fullpath, relpath, hash, mtime);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return image;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function processImage(
|
|
163
|
+
url: string,
|
|
164
|
+
processOptions: ProcessOptions = {}
|
|
165
|
+
): Promise<z.infer<typeof Picture>> {
|
|
166
|
+
return (await doProcessImage(url, null, processOptions))[0];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function doProcessImage(
|
|
170
|
+
url: string,
|
|
171
|
+
prehash: string | null,
|
|
172
|
+
processOptions: ProcessOptions = {}
|
|
173
|
+
): Promise<[z.infer<typeof Picture>, string]> {
|
|
174
|
+
const h = prehash ?? (await hashUrl(url));
|
|
175
|
+
processedHashes.add(h);
|
|
176
|
+
ImageProcessLog.silly('[IMAGE] Got image with hash', h);
|
|
177
|
+
const cached = await getPictureDetails(h);
|
|
178
|
+
if (cached) {
|
|
179
|
+
ImageProcessLog.debug('[IMAGE] Found cached details for', url, h);
|
|
180
|
+
return [cached, h];
|
|
181
|
+
}
|
|
182
|
+
const tok = await sema.acquire();
|
|
183
|
+
try {
|
|
184
|
+
ImageProcessLog.debug('[IMAGE] Starting to process', url);
|
|
185
|
+
const image = sharp(url, { animated: true });
|
|
186
|
+
const details = await _doProcessImage(url, image, h, processOptions);
|
|
187
|
+
ImageProcessLog.info('[IMAGE] Finished processing', url, details);
|
|
188
|
+
return [details, h];
|
|
189
|
+
} finally {
|
|
190
|
+
sema.release(tok);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function removeDuplicates(images: SavedImage[]): SavedImage[] {
|
|
195
|
+
for (let i = images.length - 1; i >= 0; i--) {
|
|
196
|
+
const me = images[i];
|
|
197
|
+
if (
|
|
198
|
+
images.findIndex(
|
|
199
|
+
(v) => v.format === me.format && v.h === me.h && v.w === me.w && v.src === me.src
|
|
200
|
+
) !== i
|
|
201
|
+
) {
|
|
202
|
+
images.splice(i, 1);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return images;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function _doProcessImage(
|
|
209
|
+
url: string,
|
|
210
|
+
image: Sharp,
|
|
211
|
+
hash: string,
|
|
212
|
+
processOptions: ProcessOptions = {}
|
|
213
|
+
): Promise<z.infer<typeof Picture>> {
|
|
214
|
+
const meta = await image.metadata();
|
|
215
|
+
const fullTransformations: Transformation[] =
|
|
216
|
+
meta.format === 'gif'
|
|
217
|
+
? [
|
|
218
|
+
{ format: 'gif' as ImageFormat, width: meta.width! },
|
|
219
|
+
{ format: 'webp', width: meta.width! }
|
|
220
|
+
]
|
|
221
|
+
: FORMATS.flatMap((format) =>
|
|
222
|
+
WIDTHS.flatMap(
|
|
223
|
+
(width) =>
|
|
224
|
+
({ format, width, position: processOptions?.position }) satisfies Transformation
|
|
225
|
+
)
|
|
226
|
+
);
|
|
227
|
+
const thumbnailTransformations: Transformation[] = (
|
|
228
|
+
meta.format === 'gif' ? ['gif' as ImageFormat, 'webp' as ImageFormat] : FORMATS
|
|
229
|
+
).flatMap((format) =>
|
|
230
|
+
THUMBS.flatMap(
|
|
231
|
+
(width) =>
|
|
232
|
+
({
|
|
233
|
+
format,
|
|
234
|
+
width,
|
|
235
|
+
height: width,
|
|
236
|
+
position: processOptions?.position
|
|
237
|
+
}) satisfies Transformation
|
|
238
|
+
)
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const fulls = Promise.all(
|
|
242
|
+
fullTransformations.map((tf) =>
|
|
243
|
+
doSaveImage(url, hash, doTransformImage(image, tf), tf, meta.pages ?? 1)
|
|
244
|
+
)
|
|
245
|
+
).then(removeDuplicates);
|
|
246
|
+
const thumbs = Promise.all(
|
|
247
|
+
thumbnailTransformations.map((tf) =>
|
|
248
|
+
doSaveImage(url, hash, doTransformImage(image, tf), tf, meta.pages ?? 1)
|
|
249
|
+
)
|
|
250
|
+
).then(removeDuplicates);
|
|
251
|
+
const phPromise = phash(url);
|
|
252
|
+
const fullLqip = doLQIP(image, {
|
|
253
|
+
format: 'webp',
|
|
254
|
+
width: LQIP_WIDTH,
|
|
255
|
+
position: processOptions?.position
|
|
256
|
+
});
|
|
257
|
+
const thumbLqip = doLQIP(image, {
|
|
258
|
+
format: 'webp',
|
|
259
|
+
width: LQIP_WIDTH,
|
|
260
|
+
height: LQIP_WIDTH,
|
|
261
|
+
position: processOptions?.position
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
const data = {
|
|
265
|
+
phash: await phPromise,
|
|
266
|
+
full: {
|
|
267
|
+
sha256: hash,
|
|
268
|
+
lqip: await fullLqip,
|
|
269
|
+
sources: (await fulls).reduce<Record<string, SourceInfo[]>>(
|
|
270
|
+
(all, cur) => ({
|
|
271
|
+
...all,
|
|
272
|
+
[cur.format]: [...(all[cur.format] ?? []), cur]
|
|
273
|
+
}),
|
|
274
|
+
{}
|
|
275
|
+
),
|
|
276
|
+
fallback: (await fulls).reduce<SourceInfo>(
|
|
277
|
+
(best, cur) => (cur.format === 'webp' && cur.w > best.w ? cur : best),
|
|
278
|
+
(await fulls)[0]
|
|
279
|
+
)
|
|
280
|
+
},
|
|
281
|
+
thumbnail: {
|
|
282
|
+
sha256: hash,
|
|
283
|
+
lqip: await thumbLqip,
|
|
284
|
+
sources: (await thumbs).reduce<Record<string, SourceInfo[]>>(
|
|
285
|
+
(all, cur) => ({
|
|
286
|
+
...all,
|
|
287
|
+
[cur.format]: [...(all[cur.format] ?? []), cur]
|
|
288
|
+
}),
|
|
289
|
+
{}
|
|
290
|
+
),
|
|
291
|
+
fallback: (await thumbs).reduce<SourceInfo>(
|
|
292
|
+
(best, cur) => (cur.format === 'webp' && cur.w > best.w ? cur : best),
|
|
293
|
+
(await thumbs)[0]
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
await fs.writeFile(path.join($PUBLIC(), hash, 'details.json'), JSON.stringify(data, null, 4), {
|
|
299
|
+
encoding: 'utf-8'
|
|
300
|
+
});
|
|
301
|
+
if (processOptions?.forceKeep) {
|
|
302
|
+
await fs.writeFile(path.join($PUBLIC(), hash, '.keep.mark'), '');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
ImageProcessLog.info('[IMAGE] Processed image ', hash);
|
|
306
|
+
return data;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function doSaveImage(
|
|
310
|
+
url: string,
|
|
311
|
+
hash: string,
|
|
312
|
+
image: Sharp,
|
|
313
|
+
tf: Transformation,
|
|
314
|
+
pages: number
|
|
315
|
+
): Promise<SavedImage> {
|
|
316
|
+
const base = path.join($PUBLIC(), hash);
|
|
317
|
+
await fs.mkdir(base, { recursive: true });
|
|
318
|
+
ImageProcessLog.silly('[IMAGE] Starting transformation of', url, 'to', tf);
|
|
319
|
+
return await new Promise<SavedImage>((resolve, reject) =>
|
|
320
|
+
image.toBuffer(async (err, buf, info) => {
|
|
321
|
+
if (err) return void reject(err);
|
|
322
|
+
|
|
323
|
+
if (info.format === 'heif') info.format = 'avif';
|
|
324
|
+
|
|
325
|
+
const name = `${info.width}x${info.height / pages}.${info.format}`;
|
|
326
|
+
const output = path.join(base, name);
|
|
327
|
+
|
|
328
|
+
await fs.writeFile(output, buf);
|
|
329
|
+
|
|
330
|
+
ImageProcessLog.debug('[IMAGE] Saved new image', output, 'for transformation of', url, tf);
|
|
331
|
+
resolve({
|
|
332
|
+
src: `/_/${hash}/${name}`,
|
|
333
|
+
w: info.width,
|
|
334
|
+
h: info.height / pages,
|
|
335
|
+
format: info.format as ImageFormat
|
|
336
|
+
});
|
|
337
|
+
})
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function doTransformImage(
|
|
342
|
+
image: Sharp,
|
|
343
|
+
{ format, width, height = -1, position = 'north' }: Transformation
|
|
344
|
+
): Sharp {
|
|
345
|
+
return image
|
|
346
|
+
.clone()
|
|
347
|
+
.resize({
|
|
348
|
+
width: width,
|
|
349
|
+
height: height === -1 ? undefined : height,
|
|
350
|
+
position,
|
|
351
|
+
withoutEnlargement: true
|
|
352
|
+
})
|
|
353
|
+
.toFormat(format);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function doLQIP(
|
|
357
|
+
image: Sharp,
|
|
358
|
+
{ width, height = -1, position = 'north' }: Transformation
|
|
359
|
+
): Promise<SavedImage> {
|
|
360
|
+
return await new Promise((resolve, reject) =>
|
|
361
|
+
image
|
|
362
|
+
.clone()
|
|
363
|
+
.resize({
|
|
364
|
+
width: width,
|
|
365
|
+
height: height === -1 ? undefined : height,
|
|
366
|
+
position,
|
|
367
|
+
withoutEnlargement: true
|
|
368
|
+
})
|
|
369
|
+
.toFormat('webp', { quality: 20 })
|
|
370
|
+
.toBuffer((err, buffer, info) => {
|
|
371
|
+
if (err) return void reject(err);
|
|
372
|
+
|
|
373
|
+
const dataUrl = `data:image/webp;base64,${buffer.toString('base64')}`;
|
|
374
|
+
resolve({
|
|
375
|
+
format: 'webp',
|
|
376
|
+
w: info.width,
|
|
377
|
+
h: info.height,
|
|
378
|
+
src: dataUrl
|
|
379
|
+
});
|
|
380
|
+
})
|
|
381
|
+
);
|
|
382
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
// Reexport server-only modules here
|
|
2
|
+
export { type ArtistCache, artists, getAllArtists } from './artist.ts';
|
|
3
|
+
export {
|
|
4
|
+
type CharacterCache,
|
|
5
|
+
characters,
|
|
6
|
+
type RawCharacterCache,
|
|
7
|
+
rawCharacters,
|
|
8
|
+
getAllCharacters
|
|
9
|
+
} from './character.ts';
|
|
10
|
+
export { $DATA as $ART, $PUBLIC, $ROOT } from './directories.ts';
|
|
11
|
+
export { filter } from './filter.ts';
|
|
12
|
+
export {
|
|
13
|
+
type GalleryCache,
|
|
14
|
+
type RawGalleryCache,
|
|
15
|
+
galleries,
|
|
16
|
+
rawGalleries,
|
|
17
|
+
allPieces,
|
|
18
|
+
getPieceBySlug
|
|
19
|
+
} from './gallery.ts';
|
|
20
|
+
export {
|
|
21
|
+
processImage,
|
|
22
|
+
processVideo,
|
|
23
|
+
processImageFastcache,
|
|
24
|
+
processVideoFastcache,
|
|
25
|
+
clearProcessedHashes,
|
|
26
|
+
cleanUnusedHashes
|
|
27
|
+
} from './imageprocess.ts';
|
|
28
|
+
|
|
29
|
+
export { Artist } from './models/Artist.ts';
|
|
30
|
+
export { FullCharacter, RawCharacter } from './models/Character.ts';
|
|
31
|
+
export {
|
|
32
|
+
BaseArtPiece,
|
|
33
|
+
BaseGallery,
|
|
34
|
+
FullArtPiece,
|
|
35
|
+
FullGallery,
|
|
36
|
+
RawGallery,
|
|
37
|
+
Video,
|
|
38
|
+
BaseArtist,
|
|
39
|
+
ExtendedGallery
|
|
40
|
+
} from './models/Gallery.ts';
|
|
41
|
+
export { Image, Picture, Source } from './models/image.ts';
|
|
42
|
+
export { clearCache } from './util.ts';
|
|
43
|
+
export { getFastHash, readFastCache, flushFastCache, updateFastCache } from './fastcache.ts';
|
|
44
|
+
|
|
45
|
+
export {
|
|
46
|
+
readThemeConfig,
|
|
47
|
+
readThemeSchema,
|
|
48
|
+
type BuiltinSettings,
|
|
49
|
+
type SettingsFor,
|
|
50
|
+
type ThemeSettingsSchema,
|
|
51
|
+
ZThemeSettingsSchema,
|
|
52
|
+
builtinSettings,
|
|
53
|
+
validateSchema,
|
|
54
|
+
writeThemeConfig
|
|
55
|
+
} from './theme/schema.ts';
|
|
56
|
+
|
|
57
|
+
export { readPack, writePack } from './pack.ts';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const Artist = z.object({
|
|
4
|
+
name: z.string(),
|
|
5
|
+
links: z.record(
|
|
6
|
+
z.string(),
|
|
7
|
+
z.union([
|
|
8
|
+
z.literal('twitter'),
|
|
9
|
+
z.literal('facebook'),
|
|
10
|
+
z.literal('instagram'),
|
|
11
|
+
z.literal('tumblr'),
|
|
12
|
+
z.literal('toyhouse'),
|
|
13
|
+
z.literal('website'),
|
|
14
|
+
z.literal('linktree'),
|
|
15
|
+
z.string()
|
|
16
|
+
])
|
|
17
|
+
),
|
|
18
|
+
type: z.literal('Artist').default('Artist')
|
|
19
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Picture } from './image.ts';
|
|
3
|
+
|
|
4
|
+
const AltPicture = Picture.and(z.object({ alt: z.string() }));
|
|
5
|
+
const BaseAltPicture = z.object({
|
|
6
|
+
image: z.string(),
|
|
7
|
+
alt: z.string()
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function Character<T extends z.ZodTypeAny>(imageType: T) {
|
|
11
|
+
return z.object({
|
|
12
|
+
name: z.string(),
|
|
13
|
+
pronouns: z.string(),
|
|
14
|
+
thumbnail: imageType.optional(),
|
|
15
|
+
picture: imageType,
|
|
16
|
+
description: z.string(),
|
|
17
|
+
short_description: z.string().optional(),
|
|
18
|
+
index: z.number(),
|
|
19
|
+
type: z.literal('Character').default('Character')
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const RawCharacter = Character(BaseAltPicture);
|
|
24
|
+
export const FullCharacter = Character(AltPicture);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Picture } from './image.ts';
|
|
3
|
+
|
|
4
|
+
export const ExtendedGallery = z.object({
|
|
5
|
+
$extends: z.array(z.string())
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export const Video = z.object({
|
|
9
|
+
full: z.string(),
|
|
10
|
+
thumb: z.string()
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const BaseArtist = z.union([
|
|
14
|
+
z.string(),
|
|
15
|
+
z.object({ name: z.string(), anonymous: z.coerce.boolean() })
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function ArtPiece<T extends z.ZodTypeAny>(imageType: T) {
|
|
19
|
+
return z.object({
|
|
20
|
+
id: z.string().optional(),
|
|
21
|
+
name: z.string(),
|
|
22
|
+
artist: z.union([BaseArtist, z.array(BaseArtist)]).optional(),
|
|
23
|
+
date: z.coerce.date(),
|
|
24
|
+
image: imageType,
|
|
25
|
+
characters: z
|
|
26
|
+
.array(
|
|
27
|
+
z.union([
|
|
28
|
+
z.string(),
|
|
29
|
+
z.object({
|
|
30
|
+
from: z.string(),
|
|
31
|
+
name: z.string()
|
|
32
|
+
})
|
|
33
|
+
])
|
|
34
|
+
)
|
|
35
|
+
.default([]),
|
|
36
|
+
position: z.string().optional(),
|
|
37
|
+
tags: z.array(z.string()).default([]),
|
|
38
|
+
alt: z.string(),
|
|
39
|
+
description: z.string().optional(),
|
|
40
|
+
alts: z
|
|
41
|
+
.array(
|
|
42
|
+
z.object({
|
|
43
|
+
name: z.string(),
|
|
44
|
+
image: imageType,
|
|
45
|
+
alt: z.string(),
|
|
46
|
+
description: z.string().optional(),
|
|
47
|
+
video: Video.optional(),
|
|
48
|
+
deindexed: z.boolean().optional(),
|
|
49
|
+
nsfw: z.boolean().optional()
|
|
50
|
+
})
|
|
51
|
+
)
|
|
52
|
+
.optional(),
|
|
53
|
+
video: Video.optional(),
|
|
54
|
+
slug: z.string(),
|
|
55
|
+
deindexed: z.boolean().optional(),
|
|
56
|
+
nsfw: z.boolean().optional()
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function Gallery<T extends z.ZodTypeAny>(imageType: T) {
|
|
61
|
+
return z.object({
|
|
62
|
+
pieces: z.array(ArtPiece(imageType))
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const BaseGallery = Gallery(z.string());
|
|
67
|
+
export const RawGallery = z.union([ExtendedGallery, BaseGallery]);
|
|
68
|
+
export const FullGallery = Gallery(Picture);
|
|
69
|
+
export const BaseArtPiece = ArtPiece(z.string());
|
|
70
|
+
export const FullArtPiece = ArtPiece(Picture);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
|
|
3
|
+
export const Source = z.object({
|
|
4
|
+
src: z.string(),
|
|
5
|
+
w: z.number(),
|
|
6
|
+
h: z.number()
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
export const Image = z.object({
|
|
10
|
+
sources: z.record(z.string(), z.array(Source)),
|
|
11
|
+
fallback: Source,
|
|
12
|
+
lqip: Source,
|
|
13
|
+
sha256: z.string()
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const Picture = z.object({
|
|
17
|
+
full: Image,
|
|
18
|
+
thumbnail: Image,
|
|
19
|
+
phash: z.string()
|
|
20
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { PackrStream, UnpackrStream } from 'msgpackr';
|
|
2
|
+
import { randomInt } from 'node:crypto';
|
|
3
|
+
import { createReadStream, createWriteStream } from 'node:fs';
|
|
4
|
+
import { rename } from 'node:fs/promises';
|
|
5
|
+
import { pipeline } from 'node:stream/promises';
|
|
6
|
+
import { createGunzip, createGzip } from 'node:zlib';
|
|
7
|
+
|
|
8
|
+
export async function writePack<T>(path: string, data: T) {
|
|
9
|
+
const tmp = path + '.tmp.' + Date.now() + '.' + randomInt(16777216);
|
|
10
|
+
const ws = createWriteStream(tmp);
|
|
11
|
+
const gz = createGzip({ level: 9 });
|
|
12
|
+
const packr = new PackrStream();
|
|
13
|
+
const p = pipeline(packr, gz, ws);
|
|
14
|
+
packr.end(data);
|
|
15
|
+
|
|
16
|
+
await p;
|
|
17
|
+
|
|
18
|
+
await rename(tmp, path);
|
|
19
|
+
}
|
|
20
|
+
export async function readPack<T>(path: string): Promise<T> {
|
|
21
|
+
const gunzip = createGunzip();
|
|
22
|
+
const unpackr = new UnpackrStream();
|
|
23
|
+
const rs = createReadStream(path);
|
|
24
|
+
const data = new Promise<T>((resolve) => {
|
|
25
|
+
unpackr.once('data', resolve);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
await pipeline(rs, gunzip, unpackr);
|
|
29
|
+
|
|
30
|
+
return await data;
|
|
31
|
+
}
|