Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions common/src/gallery/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,9 @@ export const GalleryDataSchema = z.object({
.object({
size: z.number().optional(),
edge: z.enum(['auto', 'width', 'height']).optional(),
format: z.enum(['avif', 'webp', 'jpeg']).optional(),
quality: z.number().optional(),
effort: z.number().optional(),
})
.optional(),
metadata: GalleryMetadataSchema,
Expand Down
40 changes: 37 additions & 3 deletions common/src/theme/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
import { z } from 'zod';

/** Supported output formats for generated thumbnails */
export const THUMBNAIL_FORMATS = ['avif', 'webp', 'jpeg'] as const;

/** TypeScript type for a thumbnail output format */
export type ThumbnailFormat = (typeof THUMBNAIL_FORMATS)[number];

/** Zod schema for thumbnail configuration */
export const ThumbnailConfigSchema = z.object({
size: z.number().min(50).max(4000).optional(),
edge: z.enum(['auto', 'width', 'height']).optional(),
format: z.enum(THUMBNAIL_FORMATS).optional(),
quality: z.number().min(1).max(100).optional(),
effort: z.number().min(0).max(9).optional(),
});

/** Zod schema for theme configuration file (themeConfig.json) */
Expand All @@ -17,12 +26,31 @@ export type ThumbnailConfig = z.infer<typeof ThumbnailConfigSchema>;
/** TypeScript type for theme configuration */
export type ThemeConfig = z.infer<typeof ThemeConfigSchema>;

/** Fully resolved thumbnail configuration with size, edge and format always present */
export interface ResolvedThumbnailConfig {
size: number;
edge: 'auto' | 'width' | 'height';
format: ThumbnailFormat;
quality?: number;
effort?: number;
}

/** Default thumbnail configuration values */
export const DEFAULT_THUMBNAIL_CONFIG: Required<ThumbnailConfig> = {
export const DEFAULT_THUMBNAIL_CONFIG: ResolvedThumbnailConfig = {
size: 300,
edge: 'auto',
format: 'avif',
};

/**
* Returns the file extension (without a leading dot) used for thumbnails of the given format.
* @param format - The thumbnail output format
* @returns The file extension to use for generated thumbnails
*/
export function getThumbnailExtension(format: ThumbnailFormat): string {
return format === 'jpeg' ? 'jpg' : format;
}

/**
* Extracts thumbnail config from gallery data.
* @param gallery - The gallery data object
Expand All @@ -32,6 +60,9 @@ export function extractThumbnailConfigFromGallery(gallery: { thumbnails?: Thumbn
return {
size: gallery.thumbnails?.size,
edge: gallery.thumbnails?.edge,
format: gallery.thumbnails?.format,
quality: gallery.thumbnails?.quality,
effort: gallery.thumbnails?.effort,
};
}

Expand All @@ -45,15 +76,18 @@ export function extractThumbnailConfigFromGallery(gallery: { thumbnails?: Thumbn
* @param cliConfig - Config from CLI flags (optional)
* @param galleryConfig - Config from gallery.json (optional)
* @param themeConfig - Config from themeConfig.json (optional)
* @returns Merged thumbnail configuration with all values resolved
* @returns Merged thumbnail configuration with all required values resolved
*/
export function mergeThumbnailConfig(
cliConfig?: ThumbnailConfig,
galleryConfig?: ThumbnailConfig,
themeConfig?: ThumbnailConfig,
): Required<ThumbnailConfig> {
): ResolvedThumbnailConfig {
return {
size: cliConfig?.size ?? galleryConfig?.size ?? themeConfig?.size ?? DEFAULT_THUMBNAIL_CONFIG.size,
edge: cliConfig?.edge ?? galleryConfig?.edge ?? themeConfig?.edge ?? DEFAULT_THUMBNAIL_CONFIG.edge,
format: cliConfig?.format ?? galleryConfig?.format ?? themeConfig?.format ?? DEFAULT_THUMBNAIL_CONFIG.format,
quality: cliConfig?.quality ?? galleryConfig?.quality ?? themeConfig?.quality,
effort: cliConfig?.effort ?? galleryConfig?.effort ?? themeConfig?.effort,
};
}
4 changes: 3 additions & 1 deletion common/src/theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
export type { ResolvedGalleryData, ResolvedHero, ResolvedImage, ResolvedSection, ResolvedSubGallery } from './types';

// Theme config
export type { ThemeConfig, ThumbnailConfig } from './config';
export type { ResolvedThumbnailConfig, ThemeConfig, ThumbnailConfig, ThumbnailFormat } from './config';
export {
DEFAULT_THUMBNAIL_CONFIG,
extractThumbnailConfigFromGallery,
getThumbnailExtension,
mergeThumbnailConfig,
THUMBNAIL_FORMATS,
ThemeConfigSchema,
ThumbnailConfigSchema,
} from './config';
Expand Down
11 changes: 9 additions & 2 deletions common/src/theme/paths.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import path from 'node:path';

import { getThumbnailExtension, type ThumbnailFormat } from './config';

/**
* Normalizes resource paths to be relative to the gallery root directory.
*
Expand Down Expand Up @@ -56,11 +58,16 @@ export function getPhotoPath(filename: string, mediaBaseUrl?: string, url?: stri
*
* @param headerImageFilename - The filename of the subgallery header image
* @param resolvedSubgalleryPath - The resolved subgallery path relative to the gallery root
* @param format - The thumbnail output format used to derive the file extension (defaults to 'avif')
* @returns The normalized path relative to the gallery root directory
*/
export function getSubgalleryThumbnailPath(headerImageFilename: string, resolvedSubgalleryPath?: string): string {
export function getSubgalleryThumbnailPath(
headerImageFilename: string,
resolvedSubgalleryPath?: string,
format: ThumbnailFormat = 'avif',
): string {
const basename = path.basename(headerImageFilename, path.extname(headerImageFilename));
const thumbnailFilename = `${basename}.avif`;
const thumbnailFilename = `${basename}.${getThumbnailExtension(format)}`;
const subgalleryFolder = resolvedSubgalleryPath || path.basename(path.dirname(headerImageFilename));

return path.join(subgalleryFolder, 'gallery', 'images', thumbnailFilename);
Expand Down
25 changes: 17 additions & 8 deletions common/src/theme/resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import path from 'node:path';

import { extractThumbnailConfigFromGallery, mergeThumbnailConfig, type ThumbnailConfig } from './config';
import {
extractThumbnailConfigFromGallery,
mergeThumbnailConfig,
type ThumbnailConfig,
type ThumbnailFormat,
} from './config';
import { LANDSCAPE_SIZES, PORTRAIT_SIZES } from './constants';
import { renderMarkdown } from './markdown';
import { buildHeroSrcset, getPhotoPath, getRelativePath, getSubgalleryThumbnailPath, getThumbnailPath } from './paths';
Expand Down Expand Up @@ -60,13 +65,17 @@ async function resolveSection(
/**
* Resolve a sub-gallery with computed thumbnail path and optional resolved path.
*/
function resolveSubGallery(subGallery: SubGallery, galleryJsonPath?: string): ResolvedSubGallery {
function resolveSubGallery(
subGallery: SubGallery,
galleryJsonPath?: string,
thumbnailFormat?: ThumbnailFormat,
): ResolvedSubGallery {
const resolvedPath = galleryJsonPath ? getRelativePath(subGallery.path, galleryJsonPath) : undefined;
return {
title: subGallery.title,
headerImage: subGallery.headerImage,
path: subGallery.path,
thumbnailPath: getSubgalleryThumbnailPath(subGallery.headerImage, resolvedPath),
thumbnailPath: getSubgalleryThumbnailPath(subGallery.headerImage, resolvedPath, thumbnailFormat),
resolvedPath,
};
}
Expand Down Expand Up @@ -180,17 +189,17 @@ export async function resolveGalleryData(
gallery.sections.map((section) => resolveSection(section, mediaBaseUrl, thumbsBaseUrl)),
);

// Merge thumbnail config: CLI > gallery.json > themeConfig > defaults
const galleryThumbnailConfig = extractThumbnailConfigFromGallery(gallery);
const thumbnails = mergeThumbnailConfig(cliConfig, galleryThumbnailConfig, themeConfig);

const resolvedSubGalleries = subGalleries?.galleries?.length
? {
title: subGalleries.title,
galleries: subGalleries.galleries.map((sg) => resolveSubGallery(sg, galleryJsonPath)),
galleries: subGalleries.galleries.map((sg) => resolveSubGallery(sg, galleryJsonPath, thumbnails.format)),
Comment thread
haltakov marked this conversation as resolved.
}
: undefined;

// Merge thumbnail config: CLI > gallery.json > themeConfig > defaults
const galleryThumbnailConfig = extractThumbnailConfigFromGallery(gallery);
const thumbnails = mergeThumbnailConfig(cliConfig, galleryThumbnailConfig, themeConfig);

return {
title: gallery.title,
url: gallery.url,
Expand Down
4 changes: 2 additions & 2 deletions common/src/theme/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GalleryMetadata, HeaderImageVariants } from '../gallery';
import type { ThumbnailConfig } from './config';
import type { ResolvedThumbnailConfig } from './config';

/** Resolved hero data with all paths computed and markdown parsed */
export interface ResolvedHero {
Expand Down Expand Up @@ -73,7 +73,7 @@ export interface ResolvedGalleryData {
* Thumbnail configuration with dimension and edge settings.
* Themes should use this for display sizing (e.g., row-height for modern theme).
*/
thumbnails?: Required<ThumbnailConfig>;
thumbnails?: ResolvedThumbnailConfig;
/** Custom CSS variable overrides from gallery.json, passed through for theme injection. */
customStyles?: Record<string, string>;
}
2 changes: 2 additions & 0 deletions gallery/jest.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ module.exports = {
'^@simple-photo-gallery/common$': '<rootDir>/../common/src/gallery.ts',
'^@simple-photo-gallery/common/theme$': '<rootDir>/../common/src/theme/index.ts',
'^@simple-photo-gallery/common/theme/config$': '<rootDir>/../common/src/theme/config.ts',
// Use the CJS-compatible UMD build of marked, since Jest cannot parse its ESM build
'^marked$': '<rootDir>/../node_modules/marked/lib/marked.umd.js',
},
transform: {
'^.+\\.tsx?$': [
Expand Down
8 changes: 7 additions & 1 deletion gallery/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { init } from './modules/init';
import { telemetry } from './modules/telemetry';
import { TelemetryService } from './modules/telemetry/service';
import { thumbnails } from './modules/thumbnails';
import { parseTelemetryOption } from './utils';
import { parseTelemetryOption, parseThumbnailFormat } from './utils';
import { checkForUpdates, displayUpdateNotification, waitForUpdateCheck } from './utils/version';

import packageJson from '../package.json' with { type: 'json' };
Expand Down Expand Up @@ -174,6 +174,9 @@ program
.option('-r, --recursive', 'Scan subdirectories recursively', false)
.option('--thumbnail-size <pixels>', 'Override thumbnail size in pixels', Number.parseInt)
.option('--thumbnail-edge <mode>', 'Override how thumbnail size is applied: auto, width, or height')
.option('--thumbnail-format <format>', 'Override thumbnail output format: avif, webp, or jpeg', parseThumbnailFormat)
.option('--thumbnail-quality <number>', 'Override thumbnail output quality (1-100)', Number.parseInt)
.option('--thumbnail-effort <number>', 'Override thumbnail encoder effort (higher is slower but smaller)', Number.parseInt)
.action(withCommandContext((options, ui) => thumbnails(options, ui)));

program
Expand All @@ -191,6 +194,9 @@ program
)
.option('--thumbnail-size <pixels>', 'Override thumbnail size in pixels', Number.parseInt)
.option('--thumbnail-edge <mode>', 'Override how thumbnail size is applied: auto, width, or height')
.option('--thumbnail-format <format>', 'Override thumbnail output format: avif, webp, or jpeg', parseThumbnailFormat)
.option('--thumbnail-quality <number>', 'Override thumbnail output quality (1-100)', Number.parseInt)
.option('--thumbnail-effort <number>', 'Override thumbnail encoder effort (higher is slower but smaller)', Number.parseInt)
.action(withCommandContext((options, ui) => build(options, ui)));

program
Expand Down
19 changes: 6 additions & 13 deletions gallery/src/modules/build/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
hasOldHeaderImages,
} from './utils';

import { findGalleries } from '../../utils';
import { buildCliThumbnailConfig, findGalleries } from '../../utils';
import { parseGalleryJson } from '../../utils/gallery';
import { scanDirectory } from '../init';
import { processGalleryThumbnails } from '../thumbnails';
Expand Down Expand Up @@ -225,17 +225,13 @@ async function buildGallery(

// If thumbnail settings are provided via CLI, update the gallery.json file if needed
if (cliThumbnailConfig) {
const needsUpdate =
(cliThumbnailConfig.size !== undefined && galleryData.thumbnails?.size !== cliThumbnailConfig.size) ||
(cliThumbnailConfig.edge !== undefined && galleryData.thumbnails?.edge !== cliThumbnailConfig.edge);
const currentThumbnails = galleryData.thumbnails ?? {};
const cliKeys = Object.keys(cliThumbnailConfig) as (keyof typeof cliThumbnailConfig)[];
const needsUpdate = cliKeys.some((key) => currentThumbnails[key] !== cliThumbnailConfig[key]);

if (needsUpdate) {
ui.debug('Updating gallery.json with thumbnail settings');
galleryData.thumbnails = {
...galleryData.thumbnails,
...(cliThumbnailConfig.size !== undefined && { size: cliThumbnailConfig.size }),
...(cliThumbnailConfig.edge !== undefined && { edge: cliThumbnailConfig.edge }),
};
galleryData.thumbnails = { ...currentThumbnails, ...cliThumbnailConfig };
fs.writeFileSync(galleryJsonPath, JSON.stringify(galleryData, null, 2));
}
}
Expand Down Expand Up @@ -335,10 +331,7 @@ export async function build(options: BuildOptions, ui: ConsolaInstance): Promise
}

// Create CLI thumbnail config from options (only include values that were provided)
const cliThumbnailConfig: ThumbnailConfig | undefined =
options.thumbnailSize !== undefined || options.thumbnailEdge !== undefined
? { size: options.thumbnailSize, edge: options.thumbnailEdge }
: undefined;
const cliThumbnailConfig = buildCliThumbnailConfig(options);

// Process each gallery directory
let totalGalleries = 0;
Expand Down
8 changes: 8 additions & 0 deletions gallery/src/modules/build/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ThumbnailFormat } from '@simple-photo-gallery/common/theme';

/** Options for building gallery HTML output */
export interface BuildOptions {
/** Path to the directory containing the gallery */
Expand All @@ -18,4 +20,10 @@ export interface BuildOptions {
thumbnailSize?: number;
/** Override how thumbnail size should be applied: 'auto', 'width', or 'height' */
thumbnailEdge?: 'auto' | 'width' | 'height';
/** Override thumbnail output format */
thumbnailFormat?: ThumbnailFormat;
/** Override thumbnail output quality (1-100) */
thumbnailQuality?: number;
/** Override thumbnail encoder effort */
thumbnailEffort?: number;
}
Loading
Loading