zone5 0.0.0 → 1.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.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +258 -0
  3. package/dist/cli/index.js +33 -0
  4. package/dist/cli/index2.js +238 -0
  5. package/dist/cli/index3.js +53 -0
  6. package/dist/cli/templates/.zone5.toml +4 -0
  7. package/dist/cli/templates/app.css +3 -0
  8. package/dist/cli/templates/layout.svelte +15 -0
  9. package/dist/cli/templates/layout.ts +1 -0
  10. package/dist/components/Zone5.svelte +153 -0
  11. package/dist/components/Zone5.svelte.d.ts +12 -0
  12. package/dist/components/Zone5Img.svelte +103 -0
  13. package/dist/components/Zone5Img.svelte.d.ts +10 -0
  14. package/dist/components/Zone5Lightbox.svelte +131 -0
  15. package/dist/components/Zone5Lightbox.svelte.d.ts +12 -0
  16. package/dist/components/Zone5Provider.svelte +68 -0
  17. package/dist/components/Zone5Provider.svelte.d.ts +9 -0
  18. package/dist/components/atoms/Button.svelte +40 -0
  19. package/dist/components/atoms/Button.svelte.d.ts +13 -0
  20. package/dist/components/atoms/CloseButton.svelte +18 -0
  21. package/dist/components/atoms/CloseButton.svelte.d.ts +9 -0
  22. package/dist/components/atoms/NextButton.svelte +19 -0
  23. package/dist/components/atoms/NextButton.svelte.d.ts +10 -0
  24. package/dist/components/atoms/PrevButton.svelte +19 -0
  25. package/dist/components/atoms/PrevButton.svelte.d.ts +10 -0
  26. package/dist/components/atoms/index.d.ts +4 -0
  27. package/dist/components/atoms/index.js +5 -0
  28. package/dist/components/constants.d.ts +17 -0
  29. package/dist/components/constants.js +17 -0
  30. package/dist/components/index.d.ts +6 -0
  31. package/dist/components/index.js +7 -0
  32. package/dist/components/portal.d.ts +4 -0
  33. package/dist/components/portal.js +26 -0
  34. package/dist/components/types.d.ts +7 -0
  35. package/dist/components/types.js +1 -0
  36. package/dist/config.d.ts +51 -0
  37. package/dist/config.js +56 -0
  38. package/dist/index.d.ts +6 -0
  39. package/dist/index.js +4 -0
  40. package/dist/module.d.ts +19 -0
  41. package/dist/processor/blurhash.d.ts +7 -0
  42. package/dist/processor/blurhash.js +37 -0
  43. package/dist/processor/color.d.ts +5 -0
  44. package/dist/processor/color.js +32 -0
  45. package/dist/processor/config.d.ts +12 -0
  46. package/dist/processor/config.js +9 -0
  47. package/dist/processor/exif/converters.d.ts +7 -0
  48. package/dist/processor/exif/converters.js +38 -0
  49. package/dist/processor/exif/defaults.d.ts +17 -0
  50. package/dist/processor/exif/defaults.js +17 -0
  51. package/dist/processor/exif/exif.d.ts +34 -0
  52. package/dist/processor/exif/exif.js +43 -0
  53. package/dist/processor/exif/index.d.ts +1 -0
  54. package/dist/processor/exif/index.js +1 -0
  55. package/dist/processor/exif/types.d.ts +4 -0
  56. package/dist/processor/exif/types.js +1 -0
  57. package/dist/processor/file.d.ts +3 -0
  58. package/dist/processor/file.js +28 -0
  59. package/dist/processor/index.d.ts +27 -0
  60. package/dist/processor/index.js +70 -0
  61. package/dist/processor/variants.d.ts +13 -0
  62. package/dist/processor/variants.js +83 -0
  63. package/dist/remark.d.ts +5 -0
  64. package/dist/remark.js +268 -0
  65. package/dist/stores/index.d.ts +1 -0
  66. package/dist/stores/index.js +1 -0
  67. package/dist/stores/registry.svelte.d.ts +22 -0
  68. package/dist/stores/registry.svelte.js +100 -0
  69. package/dist/test-setup.d.ts +9 -0
  70. package/dist/test-setup.js +25 -0
  71. package/dist/vite.d.ts +2 -0
  72. package/dist/vite.js +158 -0
  73. package/package.json +144 -5
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Default column breakpoints for waterfall mode.
3
+ * Maps viewport width (in pixels) to number of columns.
4
+ */
5
+ export const DEFAULT_COLUMN_BREAKPOINTS = {
6
+ 640: 2, // sm: 2 columns
7
+ 768: 3, // md: 3 columns
8
+ 1024: 4, // lg: 4 columns
9
+ };
10
+ /**
11
+ * Height for single image in wall mode (Tailwind class)
12
+ */
13
+ export const SINGLE_IMAGE_HEIGHT_CLASS = 'h-96';
14
+ /**
15
+ * Height for multiple images in wall mode (Tailwind class)
16
+ */
17
+ export const WALL_IMAGE_HEIGHT_CLASS = 'h-96';
@@ -0,0 +1,6 @@
1
+ export { default as Zone5 } from './Zone5.svelte';
2
+ export { default as Zone5Provider, useImageRegistry } from './Zone5Provider.svelte';
3
+ export { default as Zone5Img } from './Zone5Img.svelte';
4
+ export { default as Zone5Lightbox } from './Zone5Lightbox.svelte';
5
+ export type * from './types.js';
6
+ export * from './portal.js';
@@ -0,0 +1,7 @@
1
+ // Component exports
2
+ export { default as Zone5 } from './Zone5.svelte';
3
+ export { default as Zone5Provider, useImageRegistry } from './Zone5Provider.svelte';
4
+ export { default as Zone5Img } from './Zone5Img.svelte';
5
+ export { default as Zone5Lightbox } from './Zone5Lightbox.svelte';
6
+ // Export portal utilities
7
+ export * from './portal.js';
@@ -0,0 +1,4 @@
1
+ import type { Action } from 'svelte/action';
2
+ type targetSelector = HTMLElement | string | undefined;
3
+ declare const portal: Action<HTMLElement, targetSelector>;
4
+ export default portal;
@@ -0,0 +1,26 @@
1
+ const resolveTarget = (target = 'body') => {
2
+ if (typeof target === 'string') {
3
+ const element = document.querySelector(target);
4
+ if (element === null) {
5
+ throw new Error(`portal: no element found matching css selector: "${target}"`);
6
+ }
7
+ return element;
8
+ }
9
+ return target;
10
+ };
11
+ const moveToTarget = (node, target) => {
12
+ resolveTarget(target).appendChild(node);
13
+ };
14
+ const removeFromParent = (node) => {
15
+ if (node.parentNode) {
16
+ node.parentNode.removeChild(node);
17
+ }
18
+ };
19
+ const portal = (node, target) => {
20
+ moveToTarget(node, resolveTarget(target));
21
+ return {
22
+ update: (target) => moveToTarget(node, resolveTarget(target)),
23
+ destroy: () => removeFromParent(node),
24
+ };
25
+ };
26
+ export default portal;
@@ -0,0 +1,7 @@
1
+ import type { ItemFeature } from '../processor';
2
+ export interface ImageData extends ItemFeature {
3
+ properties: ItemFeature['properties'] & {
4
+ alt: string;
5
+ title?: string;
6
+ };
7
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,51 @@
1
+ import { z } from 'zod';
2
+ declare const BaseConfigSchema: z.ZodObject<{
3
+ root: z.ZodDefault<z.ZodString>;
4
+ cache: z.ZodDefault<z.ZodString>;
5
+ namespace: z.ZodDefault<z.ZodString>;
6
+ }, z.core.$strip>;
7
+ export type BaseConfigType = z.infer<typeof BaseConfigSchema>;
8
+ declare const ConfigSchema: z.ZodObject<{
9
+ base: z.ZodPrefault<z.ZodObject<{
10
+ root: z.ZodDefault<z.ZodString>;
11
+ cache: z.ZodDefault<z.ZodString>;
12
+ namespace: z.ZodDefault<z.ZodString>;
13
+ }, z.core.$strip>>;
14
+ processor: z.ZodPrefault<z.ZodPrefault<z.ZodObject<{
15
+ resize_kernel: z.ZodDefault<z.ZodEnum<{
16
+ [x: string]: any;
17
+ }>>;
18
+ resize_gamma: z.ZodDefault<z.ZodNumber>;
19
+ variants: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
20
+ }, z.core.$strip>>>;
21
+ }, z.core.$strip>;
22
+ export type ConfigType = z.infer<typeof ConfigSchema>;
23
+ export declare const walkUntilFound: (path: string) => Promise<string | undefined>;
24
+ export declare const load: (configDir?: string | undefined) => Promise<{
25
+ base: {
26
+ root: string;
27
+ cache: string;
28
+ namespace: string;
29
+ };
30
+ processor: {
31
+ resize_kernel: any;
32
+ resize_gamma: number;
33
+ variants: number[];
34
+ };
35
+ } | {
36
+ src: string;
37
+ base: {
38
+ root: string;
39
+ cache: string;
40
+ namespace: string;
41
+ };
42
+ processor: {
43
+ resize_kernel: any;
44
+ resize_gamma: number;
45
+ variants: number[];
46
+ };
47
+ }>;
48
+ export declare const toToml: (config: ConfigType & {
49
+ src?: string;
50
+ }) => string;
51
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,56 @@
1
+ import { access } from 'node:fs/promises';
2
+ import { join, parse, relative, resolve } from 'node:path';
3
+ import { cwd } from 'node:process';
4
+ import { stringify } from 'smol-toml';
5
+ import { z } from 'zod';
6
+ import { loadConfig } from 'zod-config';
7
+ import { tomlAdapter } from 'zod-config/toml-adapter';
8
+ import { ProcessorConfigSchema } from './processor/config.js';
9
+ const BaseConfigSchema = z.object({
10
+ root: z.string().default(cwd()),
11
+ cache: z.string().default(join(cwd(), '.zone5')),
12
+ namespace: z.string().default('@zone5'),
13
+ });
14
+ const ConfigSchema = z.object({
15
+ base: BaseConfigSchema.prefault({}),
16
+ processor: ProcessorConfigSchema.prefault({}),
17
+ });
18
+ export const walkUntilFound = async (path) => {
19
+ try {
20
+ await access(path);
21
+ return path;
22
+ }
23
+ catch {
24
+ const parsed = parse(path);
25
+ if (parsed.dir != '/') {
26
+ const next = join(parsed.dir, '..', parsed.base);
27
+ return await walkUntilFound(next);
28
+ }
29
+ }
30
+ return undefined;
31
+ };
32
+ export const load = async (configDir = undefined) => {
33
+ const tomlFile = await walkUntilFound(resolve(configDir || cwd(), '.zone5.toml'));
34
+ if (tomlFile) {
35
+ const config = await loadConfig({
36
+ schema: ConfigSchema,
37
+ adapters: tomlAdapter({ path: tomlFile }),
38
+ });
39
+ // TODO: Define z.Path type which the adapter resolves?
40
+ const tomlFileDir = parse(tomlFile).dir;
41
+ config.base.root = resolve(tomlFileDir, config.base.root);
42
+ config.base.cache = resolve(tomlFileDir, config.base.cache);
43
+ return { ...config, src: tomlFile };
44
+ }
45
+ return ConfigSchema.parse({});
46
+ };
47
+ export const toToml = (config) => {
48
+ const data = ConfigSchema.parse(config);
49
+ // output relative paths
50
+ if (config.src) {
51
+ const srcDir = parse(config.src).dir;
52
+ data.base.root = relative(srcDir, data.base.root);
53
+ data.base.cache = relative(srcDir, data.base.cache);
54
+ }
55
+ return stringify(data);
56
+ };
@@ -0,0 +1,6 @@
1
+ export { load } from './config.js';
2
+ export type { BaseConfigType, ConfigType } from './config.js';
3
+ export { default, default as processor } from './processor/index.js';
4
+ export type * from './processor/index.js';
5
+ export type * from './components/types.js';
6
+ export type * from './module.js';
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ // Main exports
2
+ export { load } from './config.js';
3
+ // Re-export processor
4
+ export { default, default as processor } from './processor/index.js';
@@ -0,0 +1,19 @@
1
+ // Augment mdast types
2
+ import type { SvelteComponent } from 'svast';
3
+
4
+ declare module 'mdast' {
5
+ interface RootContentMap {
6
+ raw: {
7
+ type: 'raw';
8
+ value: string;
9
+ };
10
+ svelteComponent: SvelteComponent;
11
+ }
12
+ }
13
+
14
+ declare module '*?z5' {
15
+ import type { ItemFeature } from './processor';
16
+
17
+ const data: ItemFeature;
18
+ export default data;
19
+ }
@@ -0,0 +1,7 @@
1
+ export interface BlurhashOptions {
2
+ componentX?: number;
3
+ componentY?: number;
4
+ width?: number;
5
+ height?: number;
6
+ }
7
+ export declare function generateBlurhash(imagePath: string, options?: BlurhashOptions): Promise<string>;
@@ -0,0 +1,37 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { encode } from 'blurhash';
3
+ import sharp from 'sharp';
4
+ const tracer = trace.getTracer('zone5-processor-blurhash');
5
+ export async function generateBlurhash(imagePath, options = {}) {
6
+ return tracer.startActiveSpan('zone5.generateBlurhash', async (span) => {
7
+ try {
8
+ const { componentX = 4, componentY = 4, width = 100, height = 100 } = options;
9
+ span.setAttributes({
10
+ 'zone5.imagePath': imagePath,
11
+ 'zone5.componentX': componentX,
12
+ 'zone5.componentY': componentY,
13
+ 'zone5.width': width,
14
+ 'zone5.height': height,
15
+ });
16
+ const { data, info } = await sharp(imagePath)
17
+ .resize(width, height, { fit: 'inside' })
18
+ .ensureAlpha()
19
+ .raw()
20
+ .toBuffer({ resolveWithObject: true });
21
+ const blurhash = encode(new Uint8ClampedArray(data), info.width, info.height, componentX, componentY);
22
+ span.setStatus({ code: SpanStatusCode.OK });
23
+ return blurhash;
24
+ }
25
+ catch (error) {
26
+ span.setStatus({
27
+ code: SpanStatusCode.ERROR,
28
+ message: error instanceof Error ? error.message : String(error),
29
+ });
30
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
31
+ throw new Error(`Failed to generate blurhash for ${imagePath}: ${error}`);
32
+ }
33
+ finally {
34
+ span.end();
35
+ }
36
+ });
37
+ }
@@ -0,0 +1,5 @@
1
+ export interface DominantColor {
2
+ hex: string;
3
+ isDark: boolean;
4
+ }
5
+ export declare function getDominantColors(imagePath: string): Promise<DominantColor>;
@@ -0,0 +1,32 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { getAverageColor } from 'fast-average-color-node';
3
+ const tracer = trace.getTracer('zone5-processor-color');
4
+ export async function getDominantColors(imagePath) {
5
+ return tracer.startActiveSpan('zone5.getDominantColors', async (span) => {
6
+ try {
7
+ span.setAttribute('zone5.imagePath', imagePath);
8
+ const result = await getAverageColor(imagePath, {});
9
+ const color = {
10
+ hex: result.hex,
11
+ isDark: result.isDark,
12
+ };
13
+ span.setAttributes({
14
+ 'zone5.color.hex': color.hex,
15
+ 'zone5.color.isDark': color.isDark,
16
+ });
17
+ span.setStatus({ code: SpanStatusCode.OK });
18
+ return color;
19
+ }
20
+ catch (error) {
21
+ span.setStatus({
22
+ code: SpanStatusCode.ERROR,
23
+ message: error instanceof Error ? error.message : String(error),
24
+ });
25
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
26
+ throw new Error(`Failed to extract dominant colors from ${imagePath}: ${error}`);
27
+ }
28
+ finally {
29
+ span.end();
30
+ }
31
+ });
32
+ }
@@ -0,0 +1,12 @@
1
+ import z from 'zod';
2
+ export declare const ProcessorConfigSchema: z.ZodPrefault<z.ZodObject<{
3
+ resize_kernel: z.ZodDefault<z.ZodEnum<{
4
+ [x: string]: any;
5
+ }>>;
6
+ resize_gamma: z.ZodDefault<z.ZodNumber>;
7
+ variants: z.ZodDefault<z.ZodArray<z.ZodNumber>>;
8
+ }, z.core.$strip>>;
9
+ export type ProcessorConfig = z.infer<typeof ProcessorConfigSchema> & {
10
+ clear?: boolean;
11
+ forceOverwrite?: boolean;
12
+ };
@@ -0,0 +1,9 @@
1
+ import sharp from 'sharp';
2
+ import z from 'zod';
3
+ export const ProcessorConfigSchema = z
4
+ .object({
5
+ resize_kernel: z.enum(Object.values(sharp.kernel)).default(sharp.kernel.mks2021),
6
+ resize_gamma: z.number().min(1.0).max(3.0).default(2.2),
7
+ variants: z.array(z.number().int().min(1)).default([640, 768, 1280, 1920, 2560]),
8
+ })
9
+ .prefault({});
@@ -0,0 +1,7 @@
1
+ import { type Dayjs } from 'dayjs';
2
+ import type { GeojsonPoint } from './types.js';
3
+ export declare const makeDate: (value: Date, offset?: string) => Dayjs | undefined;
4
+ export declare const makeRational: (value: unknown, round?: number) => [number, number] | undefined;
5
+ export declare const gpsToGeoJson: (exifData: {
6
+ [key: string]: unknown;
7
+ }) => GeojsonPoint | null;
@@ -0,0 +1,38 @@
1
+ import dayjs, {} from 'dayjs';
2
+ import customParseFormat from 'dayjs/plugin/customParseFormat.js';
3
+ import utc from 'dayjs/plugin/utc.js';
4
+ dayjs.extend(customParseFormat);
5
+ dayjs.extend(utc);
6
+ export const makeDate = (value, offset) => {
7
+ if (value instanceof Date && !isNaN(value.valueOf())) {
8
+ const date = dayjs(value);
9
+ if (offset) {
10
+ const [offset_hours_string, offset_minutes_string] = offset.split(':');
11
+ const offset_minutes = parseInt(offset_hours_string) * 60 + parseInt(offset_minutes_string);
12
+ return date.utcOffset(offset_minutes, true);
13
+ }
14
+ return date;
15
+ }
16
+ };
17
+ export const makeRational = (value, round) => {
18
+ if (typeof value === 'number') {
19
+ if (value < 1) {
20
+ return [1, 1 / value];
21
+ }
22
+ return [round === undefined ? value : +value.toFixed(round), 1];
23
+ }
24
+ };
25
+ export const gpsToGeoJson = (exifData) => {
26
+ const latitude = exifData.latitude, longitude = exifData.longitude, altitude = exifData.GPSAltitude;
27
+ if (typeof longitude === 'number' && typeof latitude === 'number') {
28
+ let coordinates = [longitude, latitude];
29
+ if (typeof altitude === 'number') {
30
+ coordinates = [...coordinates, altitude];
31
+ }
32
+ return {
33
+ type: 'Point',
34
+ coordinates,
35
+ };
36
+ }
37
+ return null;
38
+ };
@@ -0,0 +1,17 @@
1
+ export declare const DEFAULT_MAP_MAKE: {
2
+ Apple: string;
3
+ Canon: string;
4
+ 'NIKON CORPORATION': string;
5
+ 'RICOH IMAGING COMPANY, LTD.': string;
6
+ };
7
+ export declare const DEFAULT_MAP_MODEL: {
8
+ 'Canon EOS M6 Mark II': string;
9
+ 'NIKON Z6_3': string;
10
+ 'iPhone 15 Pro Max': string;
11
+ 'RICOH GR IIIx': string;
12
+ };
13
+ export declare const DEFAULT_LENS_MAP: {
14
+ 'EF-M22mm f/2 STM': string;
15
+ 'iPhone 15 Pro Max back camera 6.765mm f/1.78': string;
16
+ 'NIKKOR Z 40mm f/2': string;
17
+ };
@@ -0,0 +1,17 @@
1
+ export const DEFAULT_MAP_MAKE = {
2
+ Apple: 'Apple',
3
+ Canon: 'Canon',
4
+ 'NIKON CORPORATION': 'Nikon',
5
+ 'RICOH IMAGING COMPANY, LTD.': 'Ricoh',
6
+ };
7
+ export const DEFAULT_MAP_MODEL = {
8
+ 'Canon EOS M6 Mark II': 'Canon EOS M6 MkII',
9
+ 'NIKON Z6_3': 'Nikon Z6 III',
10
+ 'iPhone 15 Pro Max': 'Apple iPhone 15 Pro Max',
11
+ 'RICOH GR IIIx': 'Ricoh GR IIIx',
12
+ };
13
+ export const DEFAULT_LENS_MAP = {
14
+ 'EF-M22mm f/2 STM': 'Canon EF-M 22mm f/2 STM',
15
+ 'iPhone 15 Pro Max back camera 6.765mm f/1.78': 'iPhone 15 Pro Max 6.765mm f/1.78',
16
+ 'NIKKOR Z 40mm f/2': 'Nikkor Z 40mm f/2',
17
+ };
@@ -0,0 +1,34 @@
1
+ import type { GeojsonPoint } from './types.js';
2
+ type Rational = [number, number];
3
+ export interface ExifItem {
4
+ type: 'Feature';
5
+ geometry: GeojsonPoint | null;
6
+ properties: {
7
+ make?: string;
8
+ model?: string;
9
+ dateTime?: string;
10
+ artist?: string;
11
+ copyright?: string;
12
+ exposureTime?: Rational;
13
+ fNumber?: Rational;
14
+ iso?: number;
15
+ focalLength?: Rational;
16
+ lens?: string;
17
+ };
18
+ }
19
+ interface Options {
20
+ make_map?: {
21
+ [key: string]: string;
22
+ };
23
+ model_map?: {
24
+ [key: string]: string;
25
+ };
26
+ lens_map?: {
27
+ [key: string]: string;
28
+ };
29
+ warn_on_unknown_make?: boolean;
30
+ warn_on_unknown_model?: boolean;
31
+ warn_on_unknown_lens?: boolean;
32
+ }
33
+ export declare function exifFromFilePath(filePath: string, options?: Options): Promise<ExifItem>;
34
+ export {};
@@ -0,0 +1,43 @@
1
+ import exifr from 'exifr';
2
+ import { gpsToGeoJson, makeDate, makeRational } from './converters.js';
3
+ import { DEFAULT_LENS_MAP, DEFAULT_MAP_MAKE, DEFAULT_MAP_MODEL } from './defaults.js';
4
+ const findInMap = (value, map, warn = true) => {
5
+ if (value === undefined)
6
+ return undefined;
7
+ const result = map[value];
8
+ if (!result) {
9
+ if (warn) {
10
+ console.warn(`Zone5: item not in map: ${value}`);
11
+ }
12
+ return value;
13
+ }
14
+ return result;
15
+ };
16
+ export async function exifFromFilePath(filePath, options = {}) {
17
+ const { make_map = DEFAULT_MAP_MAKE, model_map = DEFAULT_MAP_MODEL, lens_map = DEFAULT_LENS_MAP, warn_on_unknown_make = true, warn_on_unknown_model = true, warn_on_unknown_lens = true, } = options;
18
+ const exifData = await exifr.parse(filePath, {});
19
+ if (!exifData || Object.keys(exifData).length === 0) {
20
+ return {
21
+ type: 'Feature',
22
+ geometry: null,
23
+ properties: {},
24
+ };
25
+ }
26
+ const mappedData = {
27
+ type: 'Feature',
28
+ geometry: gpsToGeoJson(exifData) || null,
29
+ properties: {
30
+ make: findInMap(exifData.Make || exifData['271'], make_map || {}, warn_on_unknown_make),
31
+ model: findInMap(exifData.Model || exifData['272'], model_map || {}, warn_on_unknown_model),
32
+ dateTime: makeDate(exifData.DateTimeOriginal, exifData.OffsetTimeOriginal)?.format(),
33
+ artist: exifData.Artist,
34
+ copyright: exifData.Copyright,
35
+ exposureTime: makeRational(exifData.ExposureTime),
36
+ fNumber: makeRational(exifData.FNumber, 1),
37
+ iso: exifData.ISO,
38
+ focalLength: makeRational(exifData.FocalLength, 1),
39
+ lens: findInMap(exifData.LensModel, lens_map || {}, warn_on_unknown_lens),
40
+ },
41
+ };
42
+ return mappedData;
43
+ }
@@ -0,0 +1 @@
1
+ export { exifFromFilePath as default } from './exif.js';
@@ -0,0 +1 @@
1
+ export { exifFromFilePath as default } from './exif.js';
@@ -0,0 +1,4 @@
1
+ export interface GeojsonPoint {
2
+ type: 'Point';
3
+ coordinates: [number, number] | [number, number, number];
4
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ export declare const sourceFileHash: (sourceBaseDir: string, sourceFile: string) => string;
2
+ export declare const ensureDirectoryExists: (dirPath: string) => Promise<void>;
3
+ export declare const fileExists: (filePath: string) => Promise<boolean>;
@@ -0,0 +1,28 @@
1
+ import { createHash } from 'crypto';
2
+ import { access, mkdir } from 'fs/promises';
3
+ import { relative } from 'path';
4
+ export const sourceFileHash = (sourceBaseDir, sourceFile) => {
5
+ // Calculate relative path from source base dir
6
+ const relativePath = relative(sourceBaseDir, sourceFile);
7
+ // Generate SHAKE256 hash with length 8
8
+ const hash = createHash('shake256', { outputLength: 8 });
9
+ hash.update(relativePath);
10
+ return hash.digest('hex');
11
+ };
12
+ export const ensureDirectoryExists = async (dirPath) => {
13
+ try {
14
+ await access(dirPath);
15
+ }
16
+ catch {
17
+ await mkdir(dirPath, { recursive: true });
18
+ }
19
+ };
20
+ export const fileExists = async (filePath) => {
21
+ try {
22
+ await access(filePath);
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ };
@@ -0,0 +1,27 @@
1
+ import type { BaseConfigType } from '../config.js';
2
+ import { type DominantColor } from './color.js';
3
+ import type { ProcessorConfig } from './config.js';
4
+ import type { ExifItem } from './exif/exif.js';
5
+ import type { GeojsonPoint } from './exif/types.js';
6
+ export interface ItemFeature {
7
+ type: 'Feature';
8
+ id: string;
9
+ geometry: GeojsonPoint | null;
10
+ properties: ExifItem['properties'] & {
11
+ aspectRatio: number;
12
+ blurhash: string;
13
+ averageColor: DominantColor;
14
+ };
15
+ assets: {
16
+ href: string;
17
+ width: number;
18
+ }[];
19
+ }
20
+ declare const processor: (options: {
21
+ base: BaseConfigType;
22
+ processor: ProcessorConfig;
23
+ sourceFile: string;
24
+ clear?: boolean;
25
+ forceOverwrite?: boolean;
26
+ }) => Promise<string>;
27
+ export default processor;
@@ -0,0 +1,70 @@
1
+ import { SpanStatusCode, trace } from '@opentelemetry/api';
2
+ import { writeFile } from 'fs/promises';
3
+ import { join, parse, relative } from 'path';
4
+ import sharp from 'sharp';
5
+ import { generateBlurhash } from './blurhash.js';
6
+ import { getDominantColors } from './color.js';
7
+ import exifFromFilePath from './exif/index.js';
8
+ import { fileExists, sourceFileHash } from './file.js';
9
+ import { generateImageVariants } from './variants.js';
10
+ const tracer = trace.getTracer('zone5-processor');
11
+ const processor = async (options) => {
12
+ return tracer.startActiveSpan('zone5.processor', async (span) => {
13
+ try {
14
+ const { base, sourceFile, clear = false, forceOverwrite = false } = options;
15
+ const { name: fileBasename } = parse(sourceFile);
16
+ const sourceHash = sourceFileHash(base.root, sourceFile);
17
+ const featureFile = join(base.cache, `${fileBasename}-${sourceHash}`, 'index.json');
18
+ span.setAttributes({
19
+ 'zone5.sourceFile': sourceFile,
20
+ 'zone5.fileBasename': fileBasename,
21
+ 'zone5.sourceHash': sourceHash,
22
+ 'zone5.clear': clear,
23
+ 'zone5.forceOverwrite': forceOverwrite,
24
+ });
25
+ if (!(await fileExists(featureFile)) || clear || forceOverwrite) {
26
+ const [exifFeature, blurhash, averageColor, variants, metadata] = await Promise.all([
27
+ exifFromFilePath(sourceFile),
28
+ generateBlurhash(sourceFile),
29
+ getDominantColors(sourceFile),
30
+ generateImageVariants(options),
31
+ sharp(sourceFile).metadata(),
32
+ ]);
33
+ const feature = {
34
+ type: 'Feature',
35
+ geometry: exifFeature.geometry,
36
+ id: sourceHash,
37
+ properties: {
38
+ ...exifFeature.properties,
39
+ aspectRatio: metadata.width / metadata.height,
40
+ blurhash,
41
+ averageColor,
42
+ },
43
+ assets: variants.map((variant) => ({
44
+ href: relative(base.cache, variant.path),
45
+ width: variant.width,
46
+ })),
47
+ };
48
+ await writeFile(featureFile, JSON.stringify(feature));
49
+ span.setAttribute('zone5.variantsCount', variants.length);
50
+ }
51
+ else {
52
+ span.setAttribute('zone5.cached', true);
53
+ }
54
+ span.setStatus({ code: SpanStatusCode.OK });
55
+ return featureFile;
56
+ }
57
+ catch (error) {
58
+ span.setStatus({
59
+ code: SpanStatusCode.ERROR,
60
+ message: error instanceof Error ? error.message : String(error),
61
+ });
62
+ span.recordException(error instanceof Error ? error : new Error(String(error)));
63
+ throw error;
64
+ }
65
+ finally {
66
+ span.end();
67
+ }
68
+ });
69
+ };
70
+ export default processor;
@@ -0,0 +1,13 @@
1
+ import type { BaseConfigType } from '../config.js';
2
+ import type { ProcessorConfig } from './config.js';
3
+ export interface GeneratedVariant {
4
+ width: number;
5
+ path: string;
6
+ }
7
+ export declare function generateImageVariants(options: {
8
+ base: BaseConfigType;
9
+ processor: ProcessorConfig;
10
+ sourceFile: string;
11
+ clear?: boolean;
12
+ forceOverwrite?: boolean;
13
+ }): Promise<GeneratedVariant[]>;