/*! * @license * Copyright Squiz Australia Pty Ltd. All Rights Reserved. */ import type { SquizImageType } from './SquizImage'; import type { SquizLinkType } from './SquizLink'; import type { MatrixAssetLinkType } from '../resolvableTypes/MatrixAsset'; /** * Minimal Matrix asset shape used for mapping to Squiz primitives. * Compatible with MANIFEST_MODELS.v1.MatrixAssetSchema and API response shapes. */ export interface MatrixAssetLike { url: string; type: string; attributes: Record; /** Optional; when present, varieties (for image types) may be read from it */ additional?: unknown; } const MATRIX_IMAGE_TYPES = /^image$|^image_variety$/; /** * Maps a Matrix asset and link reference to a SquizLink shape. * Mirrors the logic in content-api MatrixResolutionService.mapMatrixAssetToSquizLink. */ export function mapMatrixAssetToSquizLink( asset: MatrixAssetLike, matrixLinkAsset: MatrixAssetLinkType['__shape__'], ): SquizLinkType['__shape__'] { const text = (typeof matrixLinkAsset.text === 'string' && matrixLinkAsset.text) || (asset.attributes?.name as string) || ''; return { text, url: asset.url ?? '', target: matrixLinkAsset.target, }; } /** * Maps a Matrix image asset to a SquizImage shape. * Mirrors the logic in content-api MatrixResolutionService.mapMatrixAssetToSquizImage. */ export function mapMatrixAssetToSquizImage(asset: MatrixAssetLike): SquizImageType['__shape__'] { if (!MATRIX_IMAGE_TYPES.test(asset.type)) { throw new Error(`Matrix asset is not an image: ${asset.type}`); } const variations = (asset.additional as { varieties?: MatrixAssetLike[] })?.varieties ?? []; const isVariety = asset.type === 'image_variety'; return { name: asset.attributes?.title as string, alt: asset.attributes?.alt as string | undefined, caption: asset.attributes?.caption as string | undefined, imageVariations: { original: mapMatrixAssetToSquizImageVariation(asset, isVariety), aspectRatios: variations.map((v) => mapMatrixAssetToSquizImageVariation(v as MatrixAssetLike, true)), }, }; } function mapMatrixAssetToSquizImageVariation( matrixAsset: MatrixAssetLike, isVariety: boolean, ): SquizImageType['__shape__']['imageVariations']['original'] { const prefix = isVariety ? 'variety_' : ''; const attrs = matrixAsset.attributes ?? {}; const width = attrs[`${prefix}width`] as number; const height = attrs[`${prefix}height`] as number; const mimeType = getMimeTypeFromUrl(matrixAsset.url); return { url: matrixAsset.url, width, height, byteSize: attrs[`${prefix}size`] as number, mimeType, aspectRatio: isVariety ? calculateAspectRatio(width, height) : '4:3', sha1Hash: '', }; } function getMimeTypeFromUrl(url: string): string { const ext = url.includes('.') ? url.split('.').pop()?.replace(/\?.*/, '')?.toLowerCase() : ''; const mimeMap: Record = { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', gif: 'image/gif', webp: 'image/webp', svg: 'image/svg+xml', }; return (ext && mimeMap[ext]) || 'image/jpeg'; } function calculateAspectRatio(width: number, height: number): string { if (!width || !height) return '4:3'; const gcd = (a: number, b: number): number => (b === 0 ? a : gcd(b, a % b)); const d = gcd(width, height); const sx = width / d; const sy = height / d; const ratio = width / height; const standardRatios = [ { label: '16:9', value: 16 / 9 }, { label: '3:2', value: 3 / 2 }, { label: '4:3', value: 4 / 3 }, { label: '1:1', value: 1 }, ]; const cmp = width >= height ? ratio : 1 / ratio; let bestLabel = ''; let bestDiff = Infinity; for (const { label, value } of standardRatios) { const diff = Math.abs(cmp - value); if (diff < bestDiff) { bestDiff = diff; bestLabel = label; } } return bestDiff <= 0.2 ? bestLabel : `${sx}:${sy}`; }