Skip to content

Commit

Permalink
Merge pull request #192 from recogito/develop
Browse files Browse the repository at this point in the history
Develop to main
  • Loading branch information
lwjameson authored Jun 7, 2024
2 parents 1f42797 + d849d15 commit b1f75d9
Show file tree
Hide file tree
Showing 5 changed files with 212 additions and 109 deletions.
99 changes: 51 additions & 48 deletions src/apps/annotation-image/IIIF/useIIIF.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { useEffect, useState } from 'react';
import { Utils, Manifest, Resource, Sequence } from 'manifesto.js';
import type { DocumentInTaggedContext } from 'src/Types';
import type { DocumentWithContext } from 'src/Types';
import { supabase } from '@backend/supabaseBrowserClient';

type ManifestType = 'PRESENTATION' | 'IMAGE';

const CANTALOUPE_PATH: string | undefined = import.meta.env.PUBLIC_IIIF_CANTALOUPE_PATH;
const CANTALOUPE_PATH: string | undefined = import.meta.env
.PUBLIC_IIIF_CANTALOUPE_PATH;

// Performs a simple sanity check
const isSupported = (manifest: Manifest) => {
Expand All @@ -20,14 +21,13 @@ const isSupported = (manifest: Manifest) => {
return false;

return true;
}
};

/**
/**
* TODO add additional checks on the document metadata, to ensure
* we are dealing with a IIIF (image or presentation) manifest.
*/
export const useIIIF = (document: DocumentInTaggedContext) => {

export const useIIIF = (document: DocumentWithContext) => {
const [sequence, setSequence] = useState<Sequence | undefined>();

const [currentImage, setCurrentImage] = useState<string | undefined>();
Expand All @@ -38,17 +38,19 @@ export const useIIIF = (document: DocumentInTaggedContext) => {

const [manifestType, setManifestType] = useState<ManifestType | undefined>();

const images = sequence ? sequence.getCanvases().reduce<Resource[]>((images, canvas) => {
return [...images, ...canvas.getImages().map(i => i.getResource())];
}, []) : [];
const images = sequence
? sequence.getCanvases().reduce<Resource[]>((images, canvas) => {
return [...images, ...canvas.getImages().map((i) => i.getResource())];
}, [])
: [];

useEffect(() => {
const isUploadedFile = document.content_type?.startsWith('image/');

const url = isUploadedFile
// Locally uploaded image - for now, assume this is served via built-in IIIF
// TODO how to construct the right IIIF URL?
? `${CANTALOUPE_PATH}/${document.id}/info.json`
? // Locally uploaded image - for now, assume this is served via built-in IIIF
// TODO how to construct the right IIIF URL?
`${CANTALOUPE_PATH}/${document.id}/info.json`
: document.meta_data?.url;

if (!url) {
Expand All @@ -66,39 +68,41 @@ export const useIIIF = (document: DocumentInTaggedContext) => {
// Shouldn't really happen at this point
console.error(error);
} else {
setAuthToken(data.session?.access_token)
setAuthToken(data.session?.access_token);
setCurrentImage(url);
setManifestType('IMAGE');
}
});

} else {
// Remote image API manifest
setCurrentImage(url);
setManifestType('IMAGE');
}
}
} else {
Utils.loadManifest(url).then(data => {
const manifest = Utils.parseManifest(data) as Manifest;

if (isSupported(manifest)) {
const sequence = manifest.getSequences()[0];

const firstImage = getImageManifestURL(sequence.getCanvases()[0].getImages()[0].getResource());

setSequence(sequence);
setCurrentImage(firstImage);
setManifestType('PRESENTATION');
} else {
console.log('unsupported manifest');

setManifestError(`Unsupported IIIF manifest: ${url}`)
}
}).catch(error => {
console.error('Error loading manifest', error);
});
}
Utils.loadManifest(url)
.then((data) => {
const manifest = Utils.parseManifest(data) as Manifest;

if (isSupported(manifest)) {
const sequence = manifest.getSequences()[0];

const firstImage = getImageManifestURL(
sequence.getCanvases()[0].getImages()[0].getResource()
);

setSequence(sequence);
setCurrentImage(firstImage);
setManifestType('PRESENTATION');
} else {
console.log('unsupported manifest');

setManifestError(`Unsupported IIIF manifest: ${url}`);
}
})
.catch((error) => {
console.error('Error loading manifest', error);
});
}
}, [document]);

// Helpers
Expand All @@ -109,22 +113,22 @@ export const useIIIF = (document: DocumentInTaggedContext) => {
const next = () => {
if (!currentImage || images.length === 0) return;

const idx = images.findIndex(resource => resource.id === currentImage);
const nextIdx = Math.min(idx + 1, images.length -1);
const idx = images.findIndex((resource) => resource.id === currentImage);
const nextIdx = Math.min(idx + 1, images.length - 1);

setCurrentImage(images[nextIdx].id);
}
};

const previous = () => {
if (!currentImage || images.length === 0) return;

const idx = images.findIndex(resource => resource.id === currentImage);
const idx = images.findIndex((resource) => resource.id === currentImage);
const nextIdx = Math.max(0, idx - 1);

setCurrentImage(images[nextIdx].id);
}
};

return {
return {
authToken,
currentImage,
isPresentationManifest,
Expand All @@ -133,10 +137,9 @@ export const useIIIF = (document: DocumentInTaggedContext) => {
next,
previous,
setCurrentImage,
sequence
sequence,
};
};

}

export const getImageManifestURL = (image: Resource) => `${image.getServices()[0].id}/info.json`;

export const getImageManifestURL = (image: Resource) =>
`${image.getServices()[0].id}/info.json`;
15 changes: 8 additions & 7 deletions src/components/DocumentCard/DocumentCardThumbnail.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
.doc-card-thumbnail-spinner {
width: 200px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}

.document-card-image-container {
width: 200px;
Expand All @@ -14,3 +7,11 @@
.document-card-image-container img {
object-fit: cover;
}

.document-card-image-container .spinner-container {
width: 200px;
height: 200px;
display: flex;
align-items: center;
justify-content: center;
}
64 changes: 10 additions & 54 deletions src/components/DocumentCard/DocumentCardThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,19 @@
import React, { useState } from 'react';
import type { Document } from 'src/Types';
import { ContentTypeIcon } from '@components/DocumentCard/ContentTypeIcon';
import './DocumentCardThumbnail.css';
import { Spinner } from '@components/Spinner';

const CANTALOUPE_PATH: string | undefined = import.meta.env
.PUBLIC_IIIF_CANTALOUPE_PATH;
import { IIIFPresentationThumbnail } from './IIIFPresentationThumbnail';
import { IIIFImageThumbnail } from './IIIFImageThumbnail';

export interface DocumentCardThumbnailProps {

document: Document;

}

export const DocumentCardThumbnail = (props: DocumentCardThumbnailProps) => {
const [loading, setLoading] = useState(true);

const [url, setUrl] = useState<string | null>();

const { document } = props;

const loadManifestThumbnail = (manifestURL: string) => {
fetch(manifestURL)
.then((response) => response.json())
.then((data) => {
setLoading(false);
// Look for thumbnail
if (data.thumbnail) {
setUrl(data.thumbnail['@id']);
} else {
setUrl(undefined);
}
});
};

if (document.content_type === 'text/plain') {
return (
<div className='document-card-image-container'>
Expand All @@ -51,41 +33,15 @@ export const DocumentCardThumbnail = (props: DocumentCardThumbnailProps) => {
</div>
);
} else if (document.meta_data?.protocol === 'IIIF_PRESENTATION') {
if (loading && !url) {
loadManifestThumbnail(document.meta_data.url);
}
return (
<div className='document-card-image-container'>
{loading ? (
<div>
<Spinner className='search-icon spinner' size={14} />
</div>
) : (
<img
src={!loading && url ? url : '/img/iiif-manifest-document.png'}
height={200}
width={200}
/>
)}
</div>
<IIIFPresentationThumbnail
manifestURL={document.meta_data.url} />
);
} else if (document.content_type?.startsWith('image/')) {
const url = `${CANTALOUPE_PATH}/${document.id}/square/max/0/default.jpg`;
} else if (document.content_type?.startsWith('image/') || document.meta_data?.protocol === 'IIIF_IMAGE') {
return (
<div className='document-card-image-container'>
<img src={url} height={200} width={200} />
</div>
);
} else if (document.meta_data?.protocol === 'IIIF_IMAGE') {
const url = document.meta_data.url.replace(
'/info.json',
'/square/max/0/default.jpg'
);
return (
<div className='document-card-image-container'>
<img src={url} height={200} width={200} />
</div>
);
<IIIFImageThumbnail
document={document} />
)
} else {
return (
<div className='document-card-body'>
Expand Down
98 changes: 98 additions & 0 deletions src/components/DocumentCard/IIIFImageThumbnail.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useEffect, useState } from 'react';
import { supabase } from '@backend/supabaseBrowserClient';
import type { Document } from 'src/Types';
import { Spinner } from '@components/Spinner';

const CANTALOUPE_PATH: string | undefined = import.meta.env.PUBLIC_IIIF_CANTALOUPE_PATH;

interface IIIFImageThumbnailProps {

document: Document;

}

export const IIIFImageThumbnail = (props: IIIFImageThumbnailProps) => {

const { document } = props;

const [authToken, setAuthToken] = useState<string | undefined>();

const [thumbnailBlob, setThumbnailBlob] = useState<string | undefined>();

const isUploadedFile = document.content_type?.startsWith('image/');

const imageManifest = isUploadedFile
? `${CANTALOUPE_PATH}/${document.id}/info.json`
: document.meta_data?.url;

const thumbnailURL = imageManifest?.replace('/info.json', '/square/max/0/default.jpg');

useEffect(() => {
// Standard image tag
if (!isUploadedFile || !thumbnailURL) return;

supabase.auth.getSession().then(({ error, data }) => {
if (error) {
// Shouldn't really happen at this point
console.error(error);
} else {
setAuthToken(data.session?.access_token);
}
});
}, []);

useEffect(() => {
if (!authToken || !thumbnailURL) return;

fetch(thumbnailURL, {
headers: {
'Authorization': `Bearer ${authToken}`
}
}).then(res => {
if (!res.ok) {
console.error('Failed thumbnail download', res);
} else {
res.blob().then(blob => {
const objectURL = URL.createObjectURL(blob);
setThumbnailBlob(objectURL);
});
}
}).catch(error => {
console.error('Failed thumbnail download', error);
});
}, [authToken]);

useEffect(() => {
// Cleanup: free resources properly on unmount
if (!thumbnailBlob) return;

return () => {
URL.revokeObjectURL(thumbnailBlob);
}
}, [thumbnailBlob]);

return isUploadedFile ? (
<div className='document-card-image-container'>
{!thumbnailBlob ? (
<div className="spinner-container">
<Spinner className='search-icon spinner' size={14} />
</div>
) : (
<img
src={thumbnailBlob}
height={200}
width={200}
/>
)}
</div>
) : (
<div className='document-card-image-container'>
<img
src={thumbnailURL}
height={200}
width={200}
/>
</div>
);

}
Loading

0 comments on commit b1f75d9

Please sign in to comment.