mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-26 21:51:46 +00:00
Display images in details panel fromm gedzip files
This commit is contained in:
@@ -4,10 +4,16 @@ import {
|
||||
findRelationshipPath,
|
||||
getAncestors,
|
||||
getDescendants,
|
||||
getFileName,
|
||||
getImageFileEntry,
|
||||
getName,
|
||||
getNonImageFileEntry,
|
||||
idToFamMap,
|
||||
idToIndiMap,
|
||||
isBrowserLoadable,
|
||||
isImageFile,
|
||||
normalizeGedcom,
|
||||
resolveFileUrl,
|
||||
} from './gedcom_util';
|
||||
|
||||
describe('normalizeGedcom()', () => {
|
||||
@@ -188,3 +194,122 @@ describe('Relationship algorithms', () => {
|
||||
expect(descendants).toContain('I3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Media Resolution and Utilities', () => {
|
||||
describe('isImageFile()', () => {
|
||||
it('returns true for common images', () => {
|
||||
expect(isImageFile('test.jpg')).toBe(true);
|
||||
expect(isImageFile('test.png')).toBe(true);
|
||||
expect(isImageFile('test.gif')).toBe(true);
|
||||
expect(isImageFile('test.webp')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for non-images', () => {
|
||||
expect(isImageFile('test.pdf')).toBe(false);
|
||||
expect(isImageFile('test.txt')).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores query parameters and hashes', () => {
|
||||
expect(isImageFile('test.jpg?version=123')).toBe(true);
|
||||
expect(isImageFile('test.png#anchor')).toBe(true);
|
||||
expect(isImageFile('test.webp?a=1&b=2#h')).toBe(true);
|
||||
expect(isImageFile('test.pdf?img=test.jpg')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('isBrowserLoadable()', () => {
|
||||
it('returns true for browser loadable protocols', () => {
|
||||
expect(isBrowserLoadable('http://example.com/a.jpg')).toBe(true);
|
||||
expect(isBrowserLoadable('https://example.com/a.jpg')).toBe(true);
|
||||
expect(isBrowserLoadable('blob:http://localhost:3000/uuid')).toBe(true);
|
||||
expect(isBrowserLoadable('data:image/png;base64,abc')).toBe(true);
|
||||
expect(isBrowserLoadable('//example.com/a.jpg')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for relative local paths', () => {
|
||||
expect(isBrowserLoadable('photos/a.jpg')).toBe(false);
|
||||
expect(isBrowserLoadable('C:\\Users\\a.jpg')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileName()', () => {
|
||||
it('prefers TITL and FORM if present', () => {
|
||||
const entry = {
|
||||
level: 2,
|
||||
pointer: '',
|
||||
tag: 'FILE',
|
||||
data: 'photos/ignored.jpg',
|
||||
tree: [
|
||||
{level: 3, pointer: '', tag: 'TITL', data: 'myphoto', tree: []},
|
||||
{level: 3, pointer: '', tag: 'FORM', data: 'png', tree: []},
|
||||
],
|
||||
};
|
||||
expect(getFileName(entry)).toBe('myphoto.png');
|
||||
});
|
||||
|
||||
it('falls back to data path name if TITL/FORM is missing', () => {
|
||||
const entry = {
|
||||
level: 2,
|
||||
pointer: '',
|
||||
tag: 'FILE',
|
||||
data: 'photos/realname.jpg?width=100',
|
||||
tree: [],
|
||||
};
|
||||
expect(getFileName(entry)).toBe('realname.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveFileUrl()', () => {
|
||||
it('passes through browser loadable URLs', () => {
|
||||
expect(resolveFileUrl('https://example.com/img.jpg')).toBe(
|
||||
'https://example.com/img.jpg',
|
||||
);
|
||||
expect(resolveFileUrl('blob:uuid')).toBe('blob:uuid');
|
||||
});
|
||||
|
||||
it('matches path case-insensitively from images map', () => {
|
||||
const images = new Map([['photos/img.jpg', 'blob:resolved']]);
|
||||
expect(resolveFileUrl('photos/img.jpg', images)).toBe('blob:resolved');
|
||||
expect(resolveFileUrl('PHOTOS\\IMG.JPG', images)).toBe('blob:resolved');
|
||||
});
|
||||
|
||||
it('does not match base filename from images map', () => {
|
||||
const images = new Map([['img.jpg', 'blob:resolved-base']]);
|
||||
expect(resolveFileUrl('photos/IMG.JPG', images)).toBe('photos/IMG.JPG');
|
||||
});
|
||||
|
||||
it('falls back to normalized path if no match found', () => {
|
||||
expect(resolveFileUrl('photos\\img.jpg')).toBe('photos/img.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('findFileEntries() via getters', () => {
|
||||
const objectEntry = {
|
||||
level: 1,
|
||||
pointer: '@O1@',
|
||||
tag: 'OBJE',
|
||||
data: '',
|
||||
tree: [
|
||||
{level: 2, pointer: '', tag: 'FILE', data: 'photos/a.jpg', tree: []},
|
||||
{level: 2, pointer: '', tag: 'FILE', data: 'documents/b.pdf', tree: []},
|
||||
{
|
||||
level: 2,
|
||||
pointer: '',
|
||||
tag: 'FILE',
|
||||
data: 'https://example.com/c.png',
|
||||
tree: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('extracts images including relative and web URLs', () => {
|
||||
const image = getImageFileEntry(objectEntry);
|
||||
expect(image?.data).toBe('photos/a.jpg');
|
||||
});
|
||||
|
||||
it('extracts non-images', () => {
|
||||
const nonImage = getNonImageFileEntry(objectEntry);
|
||||
expect(nonImage?.data).toBe('documents/b.pdf');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface GedcomData {
|
||||
export interface TopolaData {
|
||||
chartData: JsonGedcomData;
|
||||
gedcom: GedcomData;
|
||||
images?: Map<string, string>;
|
||||
}
|
||||
|
||||
export interface Source {
|
||||
@@ -204,11 +205,12 @@ export function normalizeGedcom(gedcom: JsonGedcomData): JsonGedcomData {
|
||||
return sortSpouses(sortChildren(gedcom));
|
||||
}
|
||||
|
||||
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif'];
|
||||
const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.webp'];
|
||||
|
||||
/** Returns true if the given file name has a known image extension. */
|
||||
export function isImageFile(fileName: string): boolean {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
const cleanName = fileName.split(/[?#]/)[0];
|
||||
const lowerName = cleanName.toLowerCase();
|
||||
return IMAGE_EXTENSIONS.some((ext) => lowerName.endsWith(ext));
|
||||
}
|
||||
|
||||
@@ -216,22 +218,41 @@ export function isImageFile(fileName: string): boolean {
|
||||
* Removes images that are not HTTP links or do not have known image extensions.
|
||||
* Does not modify the input object.
|
||||
*/
|
||||
export function isBrowserLoadable(url: string): boolean {
|
||||
return /^(https?:|blob:|data:|\/\/)/i.test(url);
|
||||
}
|
||||
|
||||
export function resolveFileUrl(
|
||||
url: string,
|
||||
images?: Map<string, string>,
|
||||
): string {
|
||||
if (isBrowserLoadable(url)) {
|
||||
return url;
|
||||
}
|
||||
const normalizedUrl = url.replace(/\\/g, '/');
|
||||
if (images instanceof Map) {
|
||||
const lowercasePath = normalizedUrl.toLowerCase();
|
||||
const mappedUrl = images.get(lowercasePath);
|
||||
if (mappedUrl) {
|
||||
return mappedUrl;
|
||||
}
|
||||
}
|
||||
return normalizedUrl;
|
||||
}
|
||||
|
||||
function filterImage(indi: JsonIndi, images: Map<string, string>): JsonIndi {
|
||||
if (!indi.images || indi.images.length === 0) {
|
||||
return indi;
|
||||
}
|
||||
const newImages: JsonImage[] = [];
|
||||
indi.images.forEach((image) => {
|
||||
const filePath = image.url.replaceAll('\\', '/');
|
||||
const fileName = filePath.split('/').pop() || '';
|
||||
const fileUrl = images.get(filePath);
|
||||
const nameUrl = images.get(fileName);
|
||||
if (fileUrl) {
|
||||
newImages.push({url: fileUrl, title: image.title});
|
||||
} else if (nameUrl) {
|
||||
newImages.push({url: nameUrl, title: image.title});
|
||||
} else if (image.url.startsWith('http') && isImageFile(image.url)) {
|
||||
newImages.push(image);
|
||||
const resolvedUrl = resolveFileUrl(image.url, images);
|
||||
const normalizedUrl = image.url.replace(/\\/g, '/');
|
||||
if (
|
||||
resolvedUrl !== normalizedUrl ||
|
||||
(isBrowserLoadable(resolvedUrl) && isImageFile(resolvedUrl))
|
||||
) {
|
||||
newImages.push({url: resolvedUrl, title: image.title});
|
||||
}
|
||||
});
|
||||
return Object.assign({}, indi, {images: newImages});
|
||||
@@ -276,6 +297,7 @@ export function convertGedcom(
|
||||
return {
|
||||
chartData: filterImages(normalizeGedcom(json), images),
|
||||
gedcom: prepareGedcom(entries),
|
||||
images,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -309,7 +331,17 @@ export function getFileName(fileEntry: GedcomEntry): string | undefined {
|
||||
(entry) => entry.tag === 'FORM',
|
||||
)?.data;
|
||||
|
||||
return fileTitle && fileExtension && fileTitle + '.' + fileExtension;
|
||||
if (fileTitle && fileExtension) {
|
||||
return fileTitle + '.' + fileExtension;
|
||||
}
|
||||
|
||||
if (fileEntry && fileEntry.data) {
|
||||
const path = fileEntry.data.replace(/\\/g, '/');
|
||||
const cleanPath = path.split(/[?#]/)[0];
|
||||
return cleanPath.split('/').pop();
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function findFileEntry(
|
||||
@@ -317,8 +349,7 @@ function findFileEntry(
|
||||
predicate: (entry: GedcomEntry) => boolean,
|
||||
): GedcomEntry | undefined {
|
||||
return objectEntry.tree.find(
|
||||
(entry) =>
|
||||
entry.tag === 'FILE' && entry.data.startsWith('http') && predicate(entry),
|
||||
(entry) => entry.tag === 'FILE' && entry.data && predicate(entry),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user