Skip to content

Commit

Permalink
in progress
Browse files Browse the repository at this point in the history
  • Loading branch information
gestchild committed Jan 18, 2025
1 parent d6da1a3 commit e283730
Show file tree
Hide file tree
Showing 9 changed files with 362 additions and 188 deletions.
115 changes: 82 additions & 33 deletions content/webapp/components/IIIFItem/IIIFItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,39 @@ import {
ContentResource,
InternationalString,
} from '@iiif/presentation-3';
import { FunctionComponent } from 'react';
import { FunctionComponent, useEffect, useState } from 'react';
import styled from 'styled-components';

import { unavailableContentMessage } from '@weco/common/data/microcopy';
import { iiifImageTemplate } from '@weco/common/utils/convert-image-uri';
import {
ContaineredLayout,
gridSize12,
} from '@weco/common/views/components/Layout';
import Space from '@weco/common/views/components/styled/Space';
import AudioPlayer from '@weco/content/components/AudioPlayer/AudioPlayer';
import BetaMessage from '@weco/content/components/BetaMessage/BetaMessage';
import ImageViewer from '@weco/content/components/IIIFViewer/ImageViewer';
import VideoPlayer from '@weco/content/components/VideoPlayer/VideoPlayer';
import VideoTranscript from '@weco/content/components/VideoTranscript/VideoTranscript';
import { fetchCanvasOcr } from '@weco/content/services/iiif/fetch/canvasOcr';
import { transformCanvasOcr } from '@weco/content/services/iiif/transformers/canvasOcr';
import { missingAltTextMessage } from '@weco/content/services/wellcome/catalogue/works';
import {
CustomContentResource,
TransformedCanvas,
} from '@weco/content/types/manifest';
import { getLabelString } from '@weco/content/utils/iiif/v3';
import { convertRequestUriToInfoUri } from '@weco/content/utils/iiif/convert-iiif-uri';
import {
getImageServiceFromItem,
getLabelString,
} from '@weco/content/utils/iiif/v3';

const IframePdfViewer = styled(Space)`
width: 90vw;
height: 90vh;
display: block;
border: 0;
margin-top: 98px;
margin-left: auto;
margin-right: auto;
`;
Expand Down Expand Up @@ -57,19 +65,62 @@ const Choice: FunctionComponent<
return null;
};

const IIIFImage: FunctionComponent<{
index: number;
item: ItemProps['item'];
canvas: TransformedCanvas;
setImageRect?: (v: DOMRect) => void;
setImageContainerRect?: (v: DOMRect) => void;
}> = ({ index, item, canvas, setImageRect, setImageContainerRect }) => {
const [ocrText, setOcrText] = useState(missingAltTextMessage);
const imageService = getImageServiceFromItem(item);
const imageUrl = imageService?.['@id'] || '';
const infoUrl = convertRequestUriToInfoUri(imageUrl);
const urlTemplate = imageUrl ? iiifImageTemplate(imageUrl) : undefined;
useEffect(() => {
const fetchOcr = async () => {
const ocrText = await fetchCanvasOcr(canvas);
const ocrString = transformCanvasOcr(ocrText);
setOcrText(ocrString || missingAltTextMessage);
};
fetchOcr();
}, []);

if (urlTemplate) {
return (
<ImageViewer
infoUrl={infoUrl}
id={imageUrl}
width={canvas.width || 0}
height={canvas.height || 0}
index={index}
alt={ocrText}
urlTemplate={urlTemplate}
setImageRect={setImageRect}
setImageContainerRect={setImageContainerRect}
/>
);
} else {
return <img src={item.id} alt={ocrText} />;
}
};

// Some of our ContentResources can have a type of 'Audio':
// https://iiif.wellcomecollection.org/presentation/v3/b17276342
export type IIIFItemProps =
| (Omit<ContentResource, 'type'> & {
type: ContentResource['type'] | 'Audio';
})
| CustomContentResource
| ChoiceBody;
type ItemProps = {
canvas: TransformedCanvas;
// Some of our ContentResources can have a type of 'Audio':
// https://iiif.wellcomecollection.org/presentation/v3/b17276342
item:
| (Omit<ContentResource, 'type'> & {
type: ContentResource['type'] | 'Audio';
})
| CustomContentResource
| ChoiceBody;
item: IIIFItemProps;
placeholderId: string | undefined;
i: number;
exclude: (ContentResource['type'] | 'Audio' | ChoiceBody['type'])[]; // allows us to exclude certain types from being rendered
exclude: (ContentResource['type'] | 'Audio' | ChoiceBody['type'])[]; // Allows us to prevent specific types being rendered
setImageRect?: (v: DOMRect) => void;
setImageContainerRect?: (v: DOMRect) => void;
};

// This component will be useful for the IIIFViewer if we want to make that render video, audio, pdfs and Born Digital files in addition to images.
Expand All @@ -81,6 +132,8 @@ const IIIFItem: FunctionComponent<ItemProps> = ({
placeholderId,
i,
exclude,
setImageRect,
setImageContainerRect,
}) => {
switch (true) {
case item.type === 'Choice' && !exclude.includes('Choice'):
Expand All @@ -93,6 +146,8 @@ const IIIFItem: FunctionComponent<ItemProps> = ({
i={i}
RenderItem={IIIFItem}
exclude={exclude}
setImageRect={setImageRect}
setImageContainerRect={setImageContainerRect}
/>
);
case ((item.type === 'Sound' && !exclude.includes('Sound')) ||
Expand All @@ -106,14 +161,14 @@ const IIIFItem: FunctionComponent<ItemProps> = ({
);
case item.type === 'Video' && !exclude.includes('Video'):
return (
<>
<div className="video">
<VideoPlayer
video={item}
placeholderId={placeholderId}
video={item}
showDownloadOptions={true}
/>
<VideoTranscript supplementing={canvas.supplementing} />
</>
</div>
);
case item.type === 'Text' && !exclude.includes('Text'):
if ('label' in item) {
Expand All @@ -122,33 +177,27 @@ const IIIFItem: FunctionComponent<ItemProps> = ({
: '';
return (
<IframePdfViewer
$v={{
size: 'l',
properties: ['margin-bottom'],
}}
as="iframe"
title={`PDF: ${itemLabel}`}
src={item.id}
/>
);
} else {
return (
<IframePdfViewer
$v={{
size: 'l',
properties: ['margin-bottom'],
}}
as="iframe"
title="PDF"
src={item.id}
/>
);
return <IframePdfViewer as="iframe" title="PDF" src={item.id} />;
}
case item.type === 'Image' && !exclude.includes('Image'):
return <p>Image goes here</p>; // This will be needed if this Item component is to be used to render an Image in the IIIFViewer
return (
<IIIFImage
index={i}
item={item}
canvas={canvas}
setImageRect={setImageRect}
setImageContainerRect={setImageContainerRect}
/>
);
default: // There are other types we don't do anything with at present, e.g. Dataset
if (exclude.length === 0) {
// If we have exclusions, we don't want to fall back to the BetaMessage
if (!exclude.includes(item.type)) {
// If the item hasn't been purposefully excluded then we should show a message
return (
<ContaineredLayout gridSizes={gridSize12()}>
<Space $v={{ size: 'l', properties: ['margin-bottom'] }}>
Expand Down
10 changes: 5 additions & 5 deletions content/webapp/components/IIIFViewer/IIIFViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import styled from 'styled-components';

import { DigitalLocation } from '@weco/common/model/catalogue';
import { useToggles } from '@weco/common/server-data/Context';
import { iiifImageTemplate } from '@weco/common/utils/convert-image-uri';
import { AppContext } from '@weco/common/views/components/AppContext/AppContext';
import LL from '@weco/common/views/components/styled/LL';
Expand Down Expand Up @@ -218,6 +219,7 @@ const IIIFViewer: FunctionComponent<IIIFViewerProps> = ({
shouldScrollToCanvas = true,
query = '',
} = fromQuery(router.query);
const { extendedViewer } = useToggles();
const [gridVisible, setGridVisible] = useState(false);
const { isFullSupportBrowser } = useContext(AppContext);
const viewerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -349,8 +351,6 @@ const IIIFViewer: FunctionComponent<IIIFViewerProps> = ({
index={0}
alt={work?.description || work?.title || ''}
urlTemplate={urlTemplate}
setImageRect={() => undefined}
setImageContainerRect={() => undefined}
/>
)}

Expand All @@ -359,9 +359,9 @@ const IIIFViewer: FunctionComponent<IIIFViewerProps> = ({
)}

{/* If we hide the MainViewer when resizing the browser, it will then rerender with the correct canvas displayed */}
{hasImageService && !isResizing && isFullSupportBrowser && (
<MainViewer />
)}
{(hasImageService || extendedViewer) &&
!isResizing &&
isFullSupportBrowser && <MainViewer />}
</DelayVisibility>
</Main>
{showZoomed && isFullSupportBrowser && (
Expand Down
94 changes: 51 additions & 43 deletions content/webapp/components/IIIFViewer/IIIFViewerImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { forwardRef, useState } from 'react';
import styled from 'styled-components';

import { convertIiifImageUri } from '@weco/common/utils/convert-image-uri';
import LL from '@weco/common/views/components/styled/LL';
import { convertRequestUriToInfoUri } from '@weco/content/utils/iiif/convert-iiif-uri';
async function getImageMax(url: string): Promise<number> {
try {
Expand Down Expand Up @@ -61,50 +62,57 @@ const IIIFViewerImage = (
ref
) => {
const [tryLoadingSmallerImg, setTryLoadingSmallerImg] = useState(true);
const [hasLoaded, setHasLoaded] = useState(false);
return (
<Image
data-testid={index !== undefined ? `image-${index}` : null}
ref={ref}
tabIndex={tabIndex}
lang={lang}
width={width}
height={height}
className="image"
$zoomOnClick={zoomOnClick}
$highlightImage={highlightImage}
onLoad={loadHandler}
onClick={clickHandler}
onKeyDown={({ key, keyCode }) => {
if (key === 'Enter' || keyCode === 13) {
clickHandler && clickHandler();
}
}}
onError={async ({ currentTarget }) => {
// Hack/workaround
// If the image fails to load it may be because of a size limit,
// see: https://wellcome.slack.com/archives/CBT40CMKQ/p1691050149722109,
// so first off we try a smaller image
if (tryLoadingSmallerImg) {
setTryLoadingSmallerImg(false); // prevent looping if image fails to load again
// we need to know the max size of the longest side first
const imageMax = await getImageMax(currentTarget.src);
const isPortrait = Boolean(height && height > width);
const newSrc = isPortrait
? convertIiifImageUri(currentTarget.src, imageMax, true)
: convertIiifImageUri(currentTarget.src, imageMax);
currentTarget.src = newSrc;
currentTarget.removeAttribute('srcset');
currentTarget.removeAttribute('sizes');
} else {
// If the image still fails to load, we check to see if it's because the authorisation cookie is missing/no longer valid
errorHandler && errorHandler();
}
}}
src={src}
srcSet={srcSet}
sizes={sizes}
alt={alt}
/>
<>
{!hasLoaded && <LL $lighten={true} />}
<Image
data-testid={index !== undefined ? `image-${index}` : null}
ref={ref}
tabIndex={tabIndex}
lang={lang}
width={width}
height={height}
className="image"
$zoomOnClick={zoomOnClick}
$highlightImage={highlightImage}
onLoad={() => {
loadHandler && loadHandler();
setHasLoaded(true);
}}
onClick={clickHandler}
onKeyDown={({ key, keyCode }) => {
if (key === 'Enter' || keyCode === 13) {
clickHandler && clickHandler();
}
}}
onError={async ({ currentTarget }) => {
// Hack/workaround
// If the image fails to load it may be because of a size limit,
// see: https://wellcome.slack.com/archives/CBT40CMKQ/p1691050149722109,
// so first off we try a smaller image
if (tryLoadingSmallerImg) {
setTryLoadingSmallerImg(false); // prevent looping if image fails to load again
// we need to know the max size of the longest side first
const imageMax = await getImageMax(currentTarget.src);
const isPortrait = Boolean(height && height > width);
const newSrc = isPortrait
? convertIiifImageUri(currentTarget.src, imageMax, true)
: convertIiifImageUri(currentTarget.src, imageMax);
currentTarget.src = newSrc;
currentTarget.removeAttribute('srcset');
currentTarget.removeAttribute('sizes');
} else {
// If the image still fails to load, we check to see if it's because the authorisation cookie is missing/no longer valid
errorHandler && errorHandler();
}
}}
src={src}
srcSet={srcSet}
sizes={sizes}
alt={alt}
/>
</>
);
};

Expand Down
17 changes: 9 additions & 8 deletions content/webapp/components/IIIFViewer/ImageViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ type ImageViewerProps = {
urlTemplate: (v: IIIFUriProps) => string;
loadHandler?: () => void;
index: number;
setImageRect: (v: DOMRect) => void;
setImageContainerRect: (v: DOMRect) => void;
setImageRect?: (v: DOMRect) => void;
setImageContainerRect?: (v: DOMRect) => void;
};

const ImageViewer: FunctionComponent<ImageViewerProps> = ({
Expand All @@ -73,11 +73,11 @@ const ImageViewer: FunctionComponent<ImageViewerProps> = ({
rotatedImages,
transformedManifest,
} = useContext(ItemViewerContext);
const imageViewer = useRef<HTMLDivElement>(null);
const imageWrapperRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLDivElement>(null);
const isOnScreen = useOnScreen({
root: mainAreaRef?.current || undefined,
ref: imageViewer || undefined,
ref: imageWrapperRef || undefined,
threshold: [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9],
});
const [imageSrc, setImageSrc] = useState(urlTemplate({ size: '640,' }));
Expand All @@ -100,12 +100,13 @@ const ImageViewer: FunctionComponent<ImageViewerProps> = ({

function updateImagePosition() {
const imageRect = imageRef?.current?.getBoundingClientRect();
const imageContainerRect = imageViewer?.current?.getBoundingClientRect();
const imageContainerRect =
imageWrapperRef?.current?.getBoundingClientRect();
if (imageRect) {
setImageRect(imageRect);
setImageRect && setImageRect(imageRect);
}
if (imageContainerRect) {
setImageContainerRect(imageContainerRect);
setImageContainerRect && setImageContainerRect(imageContainerRect);
}
}

Expand Down Expand Up @@ -163,7 +164,7 @@ const ImageViewer: FunctionComponent<ImageViewerProps> = ({
}, []);

return (
<ImageWrapper onLoad={loadHandler} ref={imageViewer}>
<ImageWrapper onLoad={loadHandler} ref={imageWrapperRef}>
<IIIFViewerImage
index={index}
ref={imageRef}
Expand Down
Loading

0 comments on commit e283730

Please sign in to comment.