zone5 1.0.1 → 1.2.0
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/dist/components/Zone5.svelte +10 -4
- package/dist/components/Zone5Img.svelte +22 -4
- package/dist/components/Zone5Provider.svelte +74 -25
- package/dist/config.d.ts +3 -3
- package/dist/processor/config.d.ts +3 -5
- package/dist/processor/config.js +8 -1
- package/dist/processor/index.d.ts +1 -1
- package/dist/processor/index.js +8 -3
- package/dist/processor/variants.d.ts +1 -2
- package/dist/processor/variants.js +12 -9
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from 'svelte';
|
|
2
|
+
import { onMount, untrack } from 'svelte';
|
|
3
3
|
|
|
4
4
|
import Img from './Zone5Img.svelte';
|
|
5
5
|
import { useImageRegistry } from './Zone5Provider.svelte';
|
|
@@ -29,9 +29,15 @@
|
|
|
29
29
|
|
|
30
30
|
// Register images with the global image registry
|
|
31
31
|
$effect(() => {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
// Track images to re-run when they change
|
|
33
|
+
const imagesToRegister = images;
|
|
34
|
+
|
|
35
|
+
// Use untrack to prevent the store update from creating a circular dependency
|
|
36
|
+
untrack(() => {
|
|
37
|
+
if (imageStore) {
|
|
38
|
+
imageStore.register(componentId, imagesToRegister);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
// Cleanup on component unmount
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { untrack } from 'svelte';
|
|
2
3
|
import type { Action } from 'svelte/action';
|
|
3
4
|
|
|
4
5
|
import type { ImageData } from './types';
|
|
@@ -26,13 +27,30 @@
|
|
|
26
27
|
});
|
|
27
28
|
|
|
28
29
|
let img: HTMLImageElement;
|
|
29
|
-
let loaded = $state(
|
|
30
|
+
let loaded = $state(true);
|
|
31
|
+
|
|
30
32
|
$effect(() => {
|
|
31
|
-
|
|
33
|
+
// Track `sizes` to rerun on change of `image` prop
|
|
34
|
+
void sizes;
|
|
35
|
+
// Track `img` element, in case effect runs before img is set
|
|
36
|
+
const imgEl = img;
|
|
37
|
+
|
|
38
|
+
if (!imgEl) return;
|
|
39
|
+
if (imgEl.complete) {
|
|
40
|
+
untrack(() => {
|
|
41
|
+
loaded = true;
|
|
42
|
+
});
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
untrack(() => {
|
|
47
|
+
loaded = false;
|
|
48
|
+
});
|
|
32
49
|
const handleLoad = () => (loaded = true);
|
|
33
|
-
|
|
50
|
+
imgEl.addEventListener('load', handleLoad);
|
|
51
|
+
|
|
34
52
|
return () => {
|
|
35
|
-
|
|
53
|
+
imgEl.removeEventListener('load', handleLoad);
|
|
36
54
|
};
|
|
37
55
|
});
|
|
38
56
|
|
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
<script lang="ts" module>
|
|
2
2
|
import { getContext, setContext } from 'svelte';
|
|
3
3
|
|
|
4
|
-
const key =
|
|
4
|
+
const key = 'Zone5 provider';
|
|
5
5
|
export const useImageRegistry = () => getContext<Registry>(key);
|
|
6
6
|
</script>
|
|
7
7
|
|
|
8
8
|
<script lang="ts">
|
|
9
9
|
import type { Snippet } from 'svelte';
|
|
10
|
-
import {
|
|
10
|
+
import { untrack } from 'svelte';
|
|
11
11
|
|
|
12
|
-
import {
|
|
12
|
+
import { browser } from '$app/environment';
|
|
13
|
+
import { beforeNavigate, goto } from '$app/navigation';
|
|
14
|
+
import { page } from '$app/state';
|
|
13
15
|
|
|
14
16
|
import { type Registry, registry } from '../stores';
|
|
15
17
|
import Zone5Lightbox from './Zone5Lightbox.svelte';
|
|
@@ -18,39 +20,86 @@
|
|
|
18
20
|
|
|
19
21
|
setContext<Registry>(key, registry);
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
const getZ5FromUrl = () => (browser ? page.url.searchParams.get('z5') : null);
|
|
24
|
+
const setZ5InUrl = (value: string | null) => {
|
|
25
|
+
if (!browser) return;
|
|
26
|
+
const url = new URL(page.url);
|
|
27
|
+
if (value) {
|
|
28
|
+
url.searchParams.set('z5', value);
|
|
29
|
+
} else {
|
|
30
|
+
url.searchParams.delete('z5');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// eslint-disable-next-line svelte/no-navigation-without-resolve -- staying on same page, just updating query params
|
|
34
|
+
goto(`${url.pathname}${url.search}`, { replaceState: true, noScroll: true, keepFocus: true });
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
let optimisticForce = $state(getZ5FromUrl() !== null);
|
|
38
|
+
|
|
39
|
+
let isUpdatingFromUrl = false;
|
|
40
|
+
let isUpdatingUrl = false;
|
|
41
|
+
let initialSyncComplete = false;
|
|
26
42
|
|
|
27
|
-
// clear registry when navigating away
|
|
28
43
|
beforeNavigate((navigation) => {
|
|
29
|
-
if (navigation.from?.route.id
|
|
44
|
+
if (navigation.from?.route.id === navigation.to?.route.id) return;
|
|
30
45
|
registry.clear();
|
|
31
46
|
});
|
|
32
47
|
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
// registry → URL
|
|
49
|
+
$effect(() => {
|
|
50
|
+
const current = $registry.current;
|
|
51
|
+
const images = $registry.images;
|
|
52
|
+
|
|
53
|
+
if (isUpdatingFromUrl) return;
|
|
54
|
+
|
|
55
|
+
const currentZ5 = untrack(() => getZ5FromUrl());
|
|
56
|
+
const newZ5 = current?.id ?? null;
|
|
57
|
+
|
|
58
|
+
if (currentZ5 !== newZ5) {
|
|
59
|
+
// Don't clear the URL z5 param during initial load (waiting for all images to register)
|
|
60
|
+
if (newZ5 === null && currentZ5 !== null && !initialSyncComplete) {
|
|
61
|
+
const urlImageExists = images.some((img) => img.id === currentZ5);
|
|
62
|
+
if (urlImageExists) {
|
|
63
|
+
// Don't clear URL yet, let URL→registry sync handle it
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
41
66
|
}
|
|
67
|
+
|
|
68
|
+
isUpdatingUrl = true;
|
|
69
|
+
setZ5InUrl(newZ5);
|
|
70
|
+
queueMicrotask(() => {
|
|
71
|
+
isUpdatingUrl = false;
|
|
72
|
+
});
|
|
42
73
|
}
|
|
43
74
|
});
|
|
44
75
|
|
|
45
|
-
//
|
|
46
|
-
$effect(
|
|
47
|
-
const z5 =
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
// URL → registry
|
|
77
|
+
$effect(() => {
|
|
78
|
+
const z5 = getZ5FromUrl();
|
|
79
|
+
const images = $registry.images;
|
|
80
|
+
|
|
81
|
+
if (isUpdatingUrl || images.length === 0) return;
|
|
82
|
+
|
|
83
|
+
if (z5) {
|
|
84
|
+
isUpdatingFromUrl = true;
|
|
85
|
+
const found = registry.findCurrent(z5);
|
|
86
|
+
if (found) {
|
|
51
87
|
optimisticForce = false;
|
|
52
|
-
$params.z5 = null;
|
|
53
88
|
}
|
|
89
|
+
initialSyncComplete = true;
|
|
90
|
+
queueMicrotask(() => {
|
|
91
|
+
isUpdatingFromUrl = false;
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
const currentId = untrack(() => $registry.current?.id);
|
|
95
|
+
if (currentId) {
|
|
96
|
+
isUpdatingFromUrl = true;
|
|
97
|
+
registry.clearCurrent();
|
|
98
|
+
queueMicrotask(() => {
|
|
99
|
+
isUpdatingFromUrl = false;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
initialSyncComplete = true;
|
|
54
103
|
}
|
|
55
104
|
});
|
|
56
105
|
</script>
|
package/dist/config.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
15
15
|
resize_kernel: z.ZodDefault<z.ZodEnum<{
|
|
16
16
|
[x: string]: any;
|
|
17
17
|
}>>;
|
|
18
|
-
resize_gamma: z.
|
|
18
|
+
resize_gamma: z.ZodOptional<z.ZodNumber>;
|
|
19
19
|
variants: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
20
20
|
}, z.core.$strip>>>;
|
|
21
21
|
}, z.core.$strip>;
|
|
@@ -29,8 +29,8 @@ export declare const load: (configDir?: string | undefined) => Promise<{
|
|
|
29
29
|
};
|
|
30
30
|
processor: {
|
|
31
31
|
resize_kernel: any;
|
|
32
|
-
resize_gamma: number;
|
|
33
32
|
variants: number[];
|
|
33
|
+
resize_gamma?: number | undefined;
|
|
34
34
|
};
|
|
35
35
|
} | {
|
|
36
36
|
src: string;
|
|
@@ -41,8 +41,8 @@ export declare const load: (configDir?: string | undefined) => Promise<{
|
|
|
41
41
|
};
|
|
42
42
|
processor: {
|
|
43
43
|
resize_kernel: any;
|
|
44
|
-
resize_gamma: number;
|
|
45
44
|
variants: number[];
|
|
45
|
+
resize_gamma?: number | undefined;
|
|
46
46
|
};
|
|
47
47
|
}>;
|
|
48
48
|
export declare const toToml: (config: ConfigType & {
|
|
@@ -3,10 +3,8 @@ export declare const ProcessorConfigSchema: z.ZodPrefault<z.ZodObject<{
|
|
|
3
3
|
resize_kernel: z.ZodDefault<z.ZodEnum<{
|
|
4
4
|
[x: string]: any;
|
|
5
5
|
}>>;
|
|
6
|
-
resize_gamma: z.
|
|
6
|
+
resize_gamma: z.ZodOptional<z.ZodNumber>;
|
|
7
7
|
variants: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
|
|
8
8
|
}, z.core.$strip>>;
|
|
9
|
-
export type ProcessorConfig = z.infer<typeof ProcessorConfigSchema
|
|
10
|
-
|
|
11
|
-
forceOverwrite?: boolean;
|
|
12
|
-
};
|
|
9
|
+
export type ProcessorConfig = z.infer<typeof ProcessorConfigSchema>;
|
|
10
|
+
export declare const configHash: (config: ProcessorConfig) => string;
|
package/dist/processor/config.js
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
1
2
|
import sharp from 'sharp';
|
|
2
3
|
import z from 'zod';
|
|
3
4
|
export const ProcessorConfigSchema = z
|
|
4
5
|
.object({
|
|
5
6
|
resize_kernel: z.enum(Object.values(sharp.kernel)).default(sharp.kernel.mks2021),
|
|
6
|
-
resize_gamma: z.number().min(1.0).max(3.0).
|
|
7
|
+
resize_gamma: z.number().min(1.0).max(3.0).optional(),
|
|
7
8
|
variants: z.array(z.number().int().min(1)).default([640, 768, 1280, 1920, 2560]),
|
|
8
9
|
})
|
|
9
10
|
.prefault({});
|
|
11
|
+
export const configHash = (config) => {
|
|
12
|
+
// Generate SHAKE256 hash with length 4 for config
|
|
13
|
+
const hash = createHash('shake256', { outputLength: 4 });
|
|
14
|
+
hash.update(JSON.stringify(config));
|
|
15
|
+
return hash.digest('hex');
|
|
16
|
+
};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { BaseConfigType } from '../config.js';
|
|
2
2
|
import { type DominantColor } from './color.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type ProcessorConfig } from './config.js';
|
|
4
4
|
import type { ExifItem } from './exif/exif.js';
|
|
5
5
|
import type { GeojsonPoint } from './exif/types.js';
|
|
6
6
|
export interface ItemFeature {
|
package/dist/processor/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, parse, relative } from 'path';
|
|
|
4
4
|
import sharp from 'sharp';
|
|
5
5
|
import { generateBlurhash } from './blurhash.js';
|
|
6
6
|
import { getDominantColors } from './color.js';
|
|
7
|
+
import { configHash } from './config.js';
|
|
7
8
|
import exifFromFilePath from './exif/index.js';
|
|
8
9
|
import { fileExists, sourceFileHash } from './file.js';
|
|
9
10
|
import { generateImageVariants } from './variants.js';
|
|
@@ -11,14 +12,18 @@ const tracer = trace.getTracer('zone5-processor');
|
|
|
11
12
|
const processor = async (options) => {
|
|
12
13
|
return tracer.startActiveSpan('zone5.processor', async (span) => {
|
|
13
14
|
try {
|
|
14
|
-
const { base, sourceFile, clear = false, forceOverwrite = false } = options;
|
|
15
|
+
const { base, processor: processorConfig, sourceFile, clear = false, forceOverwrite = false } = options;
|
|
15
16
|
const { name: fileBasename } = parse(sourceFile);
|
|
16
17
|
const sourceHash = sourceFileHash(base.root, sourceFile);
|
|
17
|
-
const
|
|
18
|
+
const procConfigHash = configHash(processorConfig);
|
|
19
|
+
const cacheDir = join(base.cache, `${procConfigHash}-${fileBasename}-${sourceHash}`);
|
|
20
|
+
const featureFile = join(cacheDir, 'index.json');
|
|
18
21
|
span.setAttributes({
|
|
19
22
|
'zone5.sourceFile': sourceFile,
|
|
20
23
|
'zone5.fileBasename': fileBasename,
|
|
21
24
|
'zone5.sourceHash': sourceHash,
|
|
25
|
+
'zone5.configHash': procConfigHash,
|
|
26
|
+
'zone5.cacheDir': cacheDir,
|
|
22
27
|
'zone5.clear': clear,
|
|
23
28
|
'zone5.forceOverwrite': forceOverwrite,
|
|
24
29
|
});
|
|
@@ -27,7 +32,7 @@ const processor = async (options) => {
|
|
|
27
32
|
exifFromFilePath(sourceFile),
|
|
28
33
|
generateBlurhash(sourceFile),
|
|
29
34
|
getDominantColors(sourceFile),
|
|
30
|
-
generateImageVariants(
|
|
35
|
+
generateImageVariants({ sourceFile, processor: processorConfig, cacheDir, clear, forceOverwrite }),
|
|
31
36
|
sharp(sourceFile).metadata(),
|
|
32
37
|
]);
|
|
33
38
|
const feature = {
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import type { BaseConfigType } from '../config.js';
|
|
2
1
|
import type { ProcessorConfig } from './config.js';
|
|
3
2
|
export interface GeneratedVariant {
|
|
4
3
|
width: number;
|
|
5
4
|
path: string;
|
|
6
5
|
}
|
|
7
6
|
export declare function generateImageVariants(options: {
|
|
8
|
-
base: BaseConfigType;
|
|
9
7
|
processor: ProcessorConfig;
|
|
10
8
|
sourceFile: string;
|
|
9
|
+
cacheDir: string;
|
|
11
10
|
clear?: boolean;
|
|
12
11
|
forceOverwrite?: boolean;
|
|
13
12
|
}): Promise<GeneratedVariant[]>;
|
|
@@ -2,7 +2,7 @@ import { SpanStatusCode, trace } from '@opentelemetry/api';
|
|
|
2
2
|
import { rm } from 'fs/promises';
|
|
3
3
|
import { join, parse } from 'path';
|
|
4
4
|
import sharp from 'sharp';
|
|
5
|
-
import { ensureDirectoryExists, fileExists
|
|
5
|
+
import { ensureDirectoryExists, fileExists } from './file.js';
|
|
6
6
|
const tracer = trace.getTracer('zone5-processor-variants');
|
|
7
7
|
const addDebugText = async (img, width, height) => {
|
|
8
8
|
const svg = `<svg height="100" width="300">
|
|
@@ -13,7 +13,7 @@ const addDebugText = async (img, width, height) => {
|
|
|
13
13
|
export async function generateImageVariants(options) {
|
|
14
14
|
return tracer.startActiveSpan('zone5.generateImageVariants', async (span) => {
|
|
15
15
|
try {
|
|
16
|
-
const {
|
|
16
|
+
const { processor, sourceFile, cacheDir, clear = false, forceOverwrite = false } = options;
|
|
17
17
|
// Parse file path components
|
|
18
18
|
const { name: fileBasename, ext: fileExtension } = parse(sourceFile);
|
|
19
19
|
// Get source image metadata to check dimensions
|
|
@@ -25,26 +25,29 @@ export async function generateImageVariants(options) {
|
|
|
25
25
|
'zone5.sourceFile': sourceFile,
|
|
26
26
|
'zone5.sourceWidth': sourceWidth,
|
|
27
27
|
'zone5.validWidthsCount': validWidths.length,
|
|
28
|
+
'zone5.cacheDir': cacheDir,
|
|
28
29
|
'zone5.clear': clear,
|
|
29
30
|
'zone5.forceOverwrite': forceOverwrite,
|
|
30
31
|
});
|
|
31
|
-
//
|
|
32
|
-
const sourceHash = sourceFileHash(base.root, sourceFile);
|
|
33
|
-
const cacheSubDir = join(base.cache, `${fileBasename}-${sourceHash}`);
|
|
32
|
+
// Prepare cache directory
|
|
34
33
|
if (clear) {
|
|
35
|
-
await rm(
|
|
34
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
36
35
|
}
|
|
37
|
-
await ensureDirectoryExists(
|
|
36
|
+
await ensureDirectoryExists(cacheDir);
|
|
38
37
|
// Generate variants for each valid width
|
|
39
38
|
const variants = [];
|
|
40
39
|
let generatedCount = 0;
|
|
41
40
|
for (const width of validWidths) {
|
|
42
41
|
const variantFilename = `${fileBasename}-${width}${fileExtension}`;
|
|
43
|
-
const variantPath = join(
|
|
42
|
+
const variantPath = join(cacheDir, variantFilename);
|
|
44
43
|
// Check if variant already exists and should be overwritten
|
|
45
44
|
const variantExists = await fileExists(variantPath);
|
|
46
45
|
if (!variantExists || forceOverwrite) {
|
|
47
|
-
let img = sharp(sourceFile)
|
|
46
|
+
let img = sharp(sourceFile);
|
|
47
|
+
if (processor.resize_gamma) {
|
|
48
|
+
img = img.gamma(processor.resize_gamma);
|
|
49
|
+
}
|
|
50
|
+
img = img.resize(width, null, {
|
|
48
51
|
fit: 'inside',
|
|
49
52
|
kernel: processor.resize_kernel,
|
|
50
53
|
});
|