@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.
Files changed (178) hide show
  1. package/README.md +58 -0
  2. package/dist/FullGallery.svelte +51 -0
  3. package/dist/FullGallery.svelte.d.ts +17 -0
  4. package/dist/FullGallery.svelte.d.ts.map +1 -0
  5. package/dist/Gallery.svelte +30 -0
  6. package/dist/Gallery.svelte.d.ts +14 -0
  7. package/dist/Gallery.svelte.d.ts.map +1 -0
  8. package/dist/GalleryPreview.svelte +60 -0
  9. package/dist/GalleryPreview.svelte.d.ts +9 -0
  10. package/dist/GalleryPreview.svelte.d.ts.map +1 -0
  11. package/dist/HighResContext.svelte +21 -0
  12. package/dist/HighResContext.svelte.d.ts +8 -0
  13. package/dist/HighResContext.svelte.d.ts.map +1 -0
  14. package/dist/Image.svelte +171 -0
  15. package/dist/Image.svelte.d.ts +14 -0
  16. package/dist/Image.svelte.d.ts.map +1 -0
  17. package/dist/Modal.svelte +87 -0
  18. package/dist/Modal.svelte.d.ts +9 -0
  19. package/dist/Modal.svelte.d.ts.map +1 -0
  20. package/dist/ModalGallery/Carousel.svelte +76 -0
  21. package/dist/ModalGallery/Carousel.svelte.d.ts +10 -0
  22. package/dist/ModalGallery/Carousel.svelte.d.ts.map +1 -0
  23. package/dist/ModalGallery/ImageSection.svelte +156 -0
  24. package/dist/ModalGallery/ImageSection.svelte.d.ts +11 -0
  25. package/dist/ModalGallery/ImageSection.svelte.d.ts.map +1 -0
  26. package/dist/ModalGallery/ImageView.svelte +92 -0
  27. package/dist/ModalGallery/ImageView.svelte.d.ts +9 -0
  28. package/dist/ModalGallery/ImageView.svelte.d.ts.map +1 -0
  29. package/dist/ModalGallery/Spinner.svelte +71 -0
  30. package/dist/ModalGallery/Spinner.svelte.d.ts +7 -0
  31. package/dist/ModalGallery/Spinner.svelte.d.ts.map +1 -0
  32. package/dist/ModalGallery.svelte +165 -0
  33. package/dist/ModalGallery.svelte.d.ts +16 -0
  34. package/dist/ModalGallery.svelte.d.ts.map +1 -0
  35. package/dist/OpengraphMeta.svelte +125 -0
  36. package/dist/OpengraphMeta.svelte.d.ts +15 -0
  37. package/dist/OpengraphMeta.svelte.d.ts.map +1 -0
  38. package/dist/Postcard/ArtistLink.svelte +46 -0
  39. package/dist/Postcard/ArtistLink.svelte.d.ts +9 -0
  40. package/dist/Postcard/ArtistLink.svelte.d.ts.map +1 -0
  41. package/dist/Postcard/Chip.svelte +100 -0
  42. package/dist/Postcard/Chip.svelte.d.ts +12 -0
  43. package/dist/Postcard/Chip.svelte.d.ts.map +1 -0
  44. package/dist/Postcard/Description/Character.svelte +79 -0
  45. package/dist/Postcard/Description/Character.svelte.d.ts +9 -0
  46. package/dist/Postcard/Description/Character.svelte.d.ts.map +1 -0
  47. package/dist/Postcard/Description.svelte +146 -0
  48. package/dist/Postcard/Description.svelte.d.ts +13 -0
  49. package/dist/Postcard/Description.svelte.d.ts.map +1 -0
  50. package/dist/Postcard/Headline.svelte +70 -0
  51. package/dist/Postcard/Headline.svelte.d.ts +10 -0
  52. package/dist/Postcard/Headline.svelte.d.ts.map +1 -0
  53. package/dist/index.d.ts +17 -0
  54. package/dist/index.d.ts.map +1 -0
  55. package/dist/index.js +17 -0
  56. package/dist/server/artist.d.ts +7 -0
  57. package/dist/server/artist.d.ts.map +1 -0
  58. package/dist/server/artist.js +40 -0
  59. package/dist/server/character.d.ts +9 -0
  60. package/dist/server/character.d.ts.map +1 -0
  61. package/dist/server/character.js +76 -0
  62. package/dist/server/directories.d.ts +4 -0
  63. package/dist/server/directories.d.ts.map +1 -0
  64. package/dist/server/directories.js +39 -0
  65. package/dist/server/fastcache.d.ts +11 -0
  66. package/dist/server/fastcache.d.ts.map +1 -0
  67. package/dist/server/fastcache.js +43 -0
  68. package/dist/server/filter.d.ts +6 -0
  69. package/dist/server/filter.d.ts.map +1 -0
  70. package/dist/server/filter.js +53 -0
  71. package/dist/server/gallery.d.ts +16 -0
  72. package/dist/server/gallery.d.ts.map +1 -0
  73. package/dist/server/gallery.js +162 -0
  74. package/dist/server/imageprocess.d.ts +18 -0
  75. package/dist/server/imageprocess.d.ts.map +1 -0
  76. package/dist/server/imageprocess.js +243 -0
  77. package/dist/server/index.d.ts +15 -0
  78. package/dist/server/index.d.ts.map +1 -0
  79. package/dist/server/index.js +15 -0
  80. package/dist/server/models/Artist.d.ts +7 -0
  81. package/dist/server/models/Artist.d.ts.map +1 -0
  82. package/dist/server/models/Artist.js +15 -0
  83. package/dist/server/models/Character.d.ts +108 -0
  84. package/dist/server/models/Character.d.ts.map +1 -0
  85. package/dist/server/models/Character.js +21 -0
  86. package/dist/server/models/Gallery.d.ts +373 -0
  87. package/dist/server/models/Gallery.d.ts.map +1 -0
  88. package/dist/server/models/Gallery.js +60 -0
  89. package/dist/server/models/image.d.ts +64 -0
  90. package/dist/server/models/image.d.ts.map +1 -0
  91. package/dist/server/models/image.js +17 -0
  92. package/dist/server/pack.d.ts +3 -0
  93. package/dist/server/pack.d.ts.map +1 -0
  94. package/dist/server/pack.js +26 -0
  95. package/dist/server/theme/schema.d.ts +57 -0
  96. package/dist/server/theme/schema.d.ts.map +1 -0
  97. package/dist/server/theme/schema.js +217 -0
  98. package/dist/server/util.d.ts +24 -0
  99. package/dist/server/util.d.ts.map +1 -0
  100. package/dist/server/util.js +71 -0
  101. package/dist/util/art.d.ts +52 -0
  102. package/dist/util/art.d.ts.map +1 -0
  103. package/dist/util/art.js +57 -0
  104. package/dist/util/artistcontext.svelte.d.ts +8 -0
  105. package/dist/util/artistcontext.svelte.d.ts.map +1 -0
  106. package/dist/util/artistcontext.svelte.js +18 -0
  107. package/dist/util/charactercontext.svelte.d.ts +4 -0
  108. package/dist/util/charactercontext.svelte.d.ts.map +1 -0
  109. package/dist/util/charactercontext.svelte.js +11 -0
  110. package/dist/util/date.d.ts +2 -0
  111. package/dist/util/date.d.ts.map +1 -0
  112. package/dist/util/date.js +6 -0
  113. package/dist/util/index.d.ts +13 -0
  114. package/dist/util/index.d.ts.map +1 -0
  115. package/dist/util/index.js +10 -0
  116. package/dist/util/markdown.d.ts +2 -0
  117. package/dist/util/markdown.d.ts.map +1 -0
  118. package/dist/util/markdown.js +16 -0
  119. package/dist/util/phosart_config.svelte.d.ts +44 -0
  120. package/dist/util/phosart_config.svelte.d.ts.map +1 -0
  121. package/dist/util/phosart_config.svelte.js +51 -0
  122. package/dist/util/search.d.ts +3 -0
  123. package/dist/util/search.d.ts.map +1 -0
  124. package/dist/util/search.js +24 -0
  125. package/dist/util/smoothscroll.d.ts +4 -0
  126. package/dist/util/smoothscroll.d.ts.map +1 -0
  127. package/dist/util/smoothscroll.js +21 -0
  128. package/dist/util/tree.d.ts +19 -0
  129. package/dist/util/tree.d.ts.map +1 -0
  130. package/dist/util/tree.js +58 -0
  131. package/dist/util/util.d.ts +4 -0
  132. package/dist/util/util.d.ts.map +1 -0
  133. package/dist/util/util.js +22 -0
  134. package/package.json +102 -0
  135. package/src/lib/FullGallery.svelte +51 -0
  136. package/src/lib/Gallery.svelte +30 -0
  137. package/src/lib/GalleryPreview.svelte +60 -0
  138. package/src/lib/HighResContext.svelte +21 -0
  139. package/src/lib/Image.svelte +171 -0
  140. package/src/lib/Modal.svelte +87 -0
  141. package/src/lib/ModalGallery/Carousel.svelte +76 -0
  142. package/src/lib/ModalGallery/ImageSection.svelte +156 -0
  143. package/src/lib/ModalGallery/ImageView.svelte +92 -0
  144. package/src/lib/ModalGallery/Spinner.svelte +71 -0
  145. package/src/lib/ModalGallery.svelte +165 -0
  146. package/src/lib/OpengraphMeta.svelte +125 -0
  147. package/src/lib/Postcard/ArtistLink.svelte +46 -0
  148. package/src/lib/Postcard/Chip.svelte +100 -0
  149. package/src/lib/Postcard/Description/Character.svelte +79 -0
  150. package/src/lib/Postcard/Description.svelte +146 -0
  151. package/src/lib/Postcard/Headline.svelte +70 -0
  152. package/src/lib/index.ts +20 -0
  153. package/src/lib/server/artist.ts +50 -0
  154. package/src/lib/server/character.ts +113 -0
  155. package/src/lib/server/directories.ts +45 -0
  156. package/src/lib/server/fastcache.ts +66 -0
  157. package/src/lib/server/filter.ts +71 -0
  158. package/src/lib/server/gallery.ts +259 -0
  159. package/src/lib/server/imageprocess.ts +382 -0
  160. package/src/lib/server/index.ts +57 -0
  161. package/src/lib/server/models/Artist.ts +19 -0
  162. package/src/lib/server/models/Character.ts +24 -0
  163. package/src/lib/server/models/Gallery.ts +70 -0
  164. package/src/lib/server/models/image.ts +20 -0
  165. package/src/lib/server/pack.ts +31 -0
  166. package/src/lib/server/theme/schema.ts +286 -0
  167. package/src/lib/server/util.ts +102 -0
  168. package/src/lib/util/art.ts +136 -0
  169. package/src/lib/util/artistcontext.svelte.ts +25 -0
  170. package/src/lib/util/charactercontext.svelte.ts +15 -0
  171. package/src/lib/util/date.ts +7 -0
  172. package/src/lib/util/index.ts +29 -0
  173. package/src/lib/util/markdown.ts +17 -0
  174. package/src/lib/util/phosart_config.svelte.ts +101 -0
  175. package/src/lib/util/search.ts +28 -0
  176. package/src/lib/util/smoothscroll.ts +21 -0
  177. package/src/lib/util/tree.ts +75 -0
  178. 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
+ }