diff --git a/src/chart/photo.test.ts b/src/chart/photo.test.ts index 8e0eb20..9ca2c7b 100644 --- a/src/chart/photo.test.ts +++ b/src/chart/photo.test.ts @@ -17,6 +17,19 @@ const indi = (m: MediaLink[], restrictions: string[] = []) => const on = {allowRemote: true, hideRestricted: true}; describe('photoOf', () => { + it('skips scanned documents: only a photo or an image of unknown kind', () => { + const scan = (medium: string) => + media('doc.jpg', { + files: [{path: 'doc.jpg', medium, translations: []}], + }); + const t = tree({'doc.jpg': 'blob:doc', 'me.jpg': 'blob:me'}); + expect(photoOf(indi([scan('OTHER'), media('me.jpg')]), t, on)?.src).toBe( + 'blob:me', + ); + expect(photoOf(indi([scan('CARD')]), t, on)).toBeUndefined(); + expect(photoOf(indi([scan('PHOTO')]), t, on)?.src).toBe('blob:doc'); + }); + it('takes the first image that can be shown', () => { const t = tree({'photos/anna.jpg': 'blob:anna'}); const p = photoOf( diff --git a/src/chart/photo.ts b/src/chart/photo.ts index 802255f..70097e7 100644 --- a/src/chart/photo.ts +++ b/src/chart/photo.ts @@ -2,7 +2,7 @@ // that can be shown, cropped as GEDCOM 7.0 CROP says. import type {Crop, IndiView, LoadedTree, MediaLink} from '../gedcom/types'; -import {isImageFile, mediaUrl} from '../load'; +import {isPortraitFile, mediaUrl} from '../load'; export interface Photo { src: string; @@ -16,7 +16,7 @@ const isRestricted = (r: readonly string[]) => /** * The person's portrait: the first OBJE (the most preferred one in GEDCOM) - * whose first file is an image we may show — bundled with the tree, or a + * whose first file is a photo (not a scanned document) we may show — bundled with the tree, or a * linked http(s) image when `allowRemote` is on. Restricted media are * skipped when `hideRestricted` is on. */ @@ -45,7 +45,7 @@ function usable( ): Photo | undefined { if (hideRestricted && isRestricted(media.restrictions)) return undefined; const file = media.files[0]; - if (!file || !isImageFile(file)) return undefined; + if (!file || !isPortraitFile(file)) return undefined; const src = mediaUrl(tree.media, file.path); if (!src) return undefined; if (/^https?:/i.test(src) && !allowRemote) return undefined; diff --git a/src/load/index.ts b/src/load/index.ts index a4dd222..05ed971 100644 --- a/src/load/index.ts +++ b/src/load/index.ts @@ -11,6 +11,7 @@ export {looksLikeZip, openGedzip} from './zip'; export { decodeFilePath, isImageFile, + isPortraitFile, lookupMedia, mediaUrl, safeMediaUrl, diff --git a/src/model/mediaUrl.ts b/src/model/mediaUrl.ts index 6a0a33e..2540ec0 100644 --- a/src/model/mediaUrl.ts +++ b/src/model/mediaUrl.ts @@ -80,3 +80,16 @@ export function mediaUrl(media: MediaMap, path: string): string | undefined { export function isImageFile(file: {path: string; mime?: string}): boolean { return (file.mime ?? guessMime(file.path))?.startsWith('image/') ?? false; } + +/** + * True when the file may stand for the person as a portrait: an image that + * is a photo, or of unknown medium. A scanned document, card or tombstone + * (MEDI other than PHOTO) is an image but not a portrait. + */ +export function isPortraitFile(file: { + path: string; + mime?: string; + medium?: string; +}): boolean { + return isImageFile(file) && (!file.medium || file.medium === 'PHOTO'); +} diff --git a/src/ui/media.test.ts b/src/ui/media.test.ts index 30cb834..a3c86bf 100644 --- a/src/ui/media.test.ts +++ b/src/ui/media.test.ts @@ -63,3 +63,27 @@ describe('titleYear', () => { expect(titleYear('Портрет')).toBeUndefined(); }); }); + +describe('the portrait', () => { + const docTree = treeOf( + doc7( + '0 @I1@ INDI', + '1 OBJE @O1@', + '1 OBJE @O2@', + '0 @O1@ OBJE', + '1 FILE https://images.example.test/O1.jpg', + '2 FORM image/jpeg', + '3 MEDI OTHER', + '2 TITL Свидетельство о смерти', + ...obje('O2', 'Портрет'), + ), + ); + + it('is a photo, never a scanned document', () => { + const entries = mediaEntries(docTree, docTree.indis.get('I1')!, { + hide: true, + allowRemote: true, + }); + expect(portraitOf(entries)?.entry.title).toBe('Портрет'); + }); +}); diff --git a/src/ui/media.ts b/src/ui/media.ts index d4d396b..22600c2 100644 --- a/src/ui/media.ts +++ b/src/ui/media.ts @@ -10,7 +10,7 @@ import type { MediaLink, } from '../gedcom/types'; import {yearOf} from '../model/bounds'; -import {isImageFile, mediaUrl} from '../load'; +import {isImageFile, isPortraitFile, mediaUrl} from '../load'; import {cropBox} from '../chart/photo'; export interface MediaEntry { @@ -151,7 +151,8 @@ export function blockedHosts(entries: MediaEntry[]): string[] { ]; } -/** The portrait: the first image that can be shown, and its place. */ +/** The portrait: the first photo that can be shown (a scanned document + * is not one), and its place. */ export function portraitOf( entries: MediaEntry[], ): {entry: MediaEntry; index: number} | undefined { @@ -159,7 +160,12 @@ export function portraitOf( // years put it. let index = -1; entries.forEach((e, i) => { - if (e.image && e.src && (index === -1 || e.order < entries[index]!.order)) + if ( + e.image && + isPortraitFile(e.file) && + e.src && + (index === -1 || e.order < entries[index]!.order) + ) index = i; }); return index === -1 ? undefined : {entry: entries[index]!, index};