Skip to content

Commit

Permalink
Get rid of manifesto's canonical URI creation method, improve API com…
Browse files Browse the repository at this point in the history
…pliance

TODOs
- tests for added IIIF image functions
- CanvasDownloadLinks with different API versions
  • Loading branch information
lutzhelm committed Nov 22, 2024
1 parent 3e57cf4 commit 321e892
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 74 deletions.
54 changes: 35 additions & 19 deletions __tests__/CanvasDownloadLinks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ function createWrapper(props) {
canvasId="abc123"
canvasLabel="My Canvas Label"
classes={{}}
isVersion3={false}
infoResponse={{}}
restrictDownloadOnSizeDefinition={false}
viewType="single"
windowId="wid123"
Expand Down Expand Up @@ -41,6 +39,18 @@ describe('CanvasDownloadLinks', () => {
],
};

const infoResponse = {
json: {
'@context': 'http://iiif.io/api/image/2/context.json',
'@id': 'http://example.com/iiif/abc123/',
width: 4000,
height: 1000,
profile: [
'http://iiif.io/api/image/2/level1.json',
],
},
};

let currentBoundsSpy;

beforeEach(() => {
Expand All @@ -52,7 +62,7 @@ describe('CanvasDownloadLinks', () => {
});

it('renders the canvas label as an h3 heading', () => {
createWrapper({ canvas });
createWrapper({ canvas, infoResponse });

const headingElement = screen.getByText('My Canvas Label');
expect(headingElement).toBeInTheDocument();
Expand All @@ -61,18 +71,14 @@ describe('CanvasDownloadLinks', () => {

describe('Canvas Renderings', () => {
it('includes a canvas-level rendering as a download link', () => {
createWrapper({ canvas });
createWrapper({ canvas, infoResponse });

const downloadLink = screen.getByRole('link', { name: /Whole image \(4000 x 1000px\)/i });
expect(downloadLink).toBeInTheDocument();
});
});

describe('Zoomed Region Links', () => {
const infoResponse = {
json: { width: 4000, height: 1000 },
};

it('does not render a zoom link when viewer is zoomed out to full image', () => {
currentBoundsSpy.mockImplementation(() => ({
x: 0, y: 0, width: 6000, height: 1000,
Expand Down Expand Up @@ -130,8 +136,7 @@ describe('CanvasDownloadLinks', () => {
canvas,
infoResponse: {
json: {
width: 4000,
height: 1000,
...infoResponse.json,
sizes: [{ width: 400, height: 100 }],
},
},
Expand All @@ -147,11 +152,16 @@ describe('CanvasDownloadLinks', () => {
});

describe('When Defined Sizes Are Present in infoResponse', () => {
const sizes = [
{ width: 4000, height: 1000 },
{ width: 2000, height: 500 },
{ width: 1000, height: 250 },
];
const infoResponseWithSizes = {
json: {
...infoResponse.json,
sizes: [
{ width: 4000, height: 1000 },
{ width: 2000, height: 500 },
{ width: 1000, height: 250 },
],
},
};

const viewport = {
getBounds: () => ({
Expand All @@ -162,7 +172,7 @@ describe('CanvasDownloadLinks', () => {
current: { viewport },
});
it('renders download links for all specified sizes in the dialog', () => {
createWrapper({ canvas, infoResponse: { json: { sizes } } });
createWrapper({ canvas, infoResponse: infoResponseWithSizes });

const link1 = screen.getByRole('link', { name: /Whole image \(4000 x 1000px\)/i });
const link2 = screen.getByRole('link', { name: /Whole image \(2000 x 500px\)/i });
Expand All @@ -176,7 +186,7 @@ describe('CanvasDownloadLinks', () => {

describe('When No Sizes Are Defined in infoResponse', () => {
it('renders a single link to the full-size image', () => {
createWrapper({ canvas });
createWrapper({ canvas, infoResponse });

const link = screen.getByRole('link', { name: /Whole image \(4000 x 1000px\)/i });
expect(link).toBeInTheDocument();
Expand All @@ -185,7 +195,7 @@ describe('CanvasDownloadLinks', () => {

describe('For Images Wider Than 1000px', () => {
it('renders links for both full-size and 1000px wide versions', () => {
createWrapper({ canvas });
createWrapper({ canvas, infoResponse });

const link1 = screen.getByRole('link', { name: /Whole image \(4000 x 1000px\)/i });
expect(link1).toHaveAttribute('href', 'http://example.com/iiif/abc123/full/full/0/default.jpg?download=true');
Expand All @@ -198,7 +208,13 @@ describe('CanvasDownloadLinks', () => {
describe('For Images Less Than 1000px Wide', () => {
it('does not render a smaller version link if image is under 1000px wide', () => {
canvas.getWidth = () => 999;
createWrapper({ canvas });
const smallInfoResponse = {
json: {
...infoResponse.json,
width: 999,
},
};
createWrapper({ canvas, infoResponse: smallInfoResponse });

const links = screen.getAllByRole('link');
expect(links).toHaveLength(2); // Should only show full-size version and link to PDF.
Expand Down
58 changes: 29 additions & 29 deletions src/CanvasDownloadLinks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Link from '@mui/material/Link';
import List from '@mui/material/List';
import ListItem from '@mui/material/ListItem';
import RenderingDownloadLink from './RenderingDownloadLink';
import { calculateHeightForWidth, createCanonicalImageUrl } from './iiifImageFunctions';

/**
* CanvasDownloadLinks ~
Expand All @@ -20,51 +21,50 @@ export default class CanvasDownloadLinks extends Component {
}

fullImageLabel() {
const { canvas } = this.props;

return `Whole image (${canvas.getWidth()} x ${canvas.getHeight()}px)`;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;
return imageInfo && `Whole image (${imageInfo.width} x ${imageInfo.height}px)`;
}

smallImageLabel() {
const { canvas } = this.props;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;

return `Whole image (1000 x ${Math.floor(
(1000 * canvas.getHeight()) / canvas.getWidth(),
(1000 * imageInfo.height) / imageInfo.width,
)}px)`;
}

zoomedImageUrl() {
const { canvas, isVersion3 } = this.props;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;
const bounds = this.currentBounds();
const boundsUrl = canvas
.getCanonicalImageUri()
.replace(
/\/full\/.*\/0\//,
`/${bounds.x},${bounds.y},${bounds.width},${bounds.height}/${isVersion3 ? `${bounds.width},${bounds.height}` : 'full'}/0/`,
);

return `${boundsUrl}?download=true`;
const boundsUrl = createCanonicalImageUrl(
imageInfo,
`${bounds.x},${bounds.y},${bounds.width},${bounds.height}`,
bounds.width,
bounds.height,
);
return imageInfo && `${boundsUrl}?download=true`;
}

imageUrlForSize(size) {
const { canvas, isVersion3 } = this.props;

return isVersion3 ? `${canvas.getCanonicalImageUri().replace(/\/full\/.*\/0\//, `/full/${size.width},${size.height}/0/`)}?download=true`
: `${canvas.getCanonicalImageUri(size.width)}?download=true`;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', size.width, size.height)}?download=true`;
}

fullImageUrl() {
const { canvas, isVersion3 } = this.props;

return `${canvas
.getCanonicalImageUri()
.replace(/\/full\/.*\/0\//, `/full/${isVersion3 ? 'max' : 'full'}/0/`)}?download=true`;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', imageInfo.width, imageInfo.height)}?download=true`;
}

thousandPixelWideImage() {
const { canvas } = this.props;

return `${canvas.getCanonicalImageUri('1000')}?download=true`;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;
const height = calculateHeightForWidth(imageInfo, 1000);
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', 1000, height)}?download=true`;
}

osdViewport() {
Expand Down Expand Up @@ -143,9 +143,10 @@ export default class CanvasDownloadLinks extends Component {
}

thousandPixelWideLink() {
const { canvas } = this.props;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;

if (canvas.getWidth() < 1000) return '';
if (!imageInfo || imageInfo.width < 1000) return '';

return (
<ListItem disableGutters divider key={this.thousandPixelWideImage()}>
Expand Down Expand Up @@ -233,7 +234,6 @@ CanvasDownloadLinks.propTypes = {
width: PropTypes.number,
}),
}).isRequired,
isVersion3: PropTypes.bool.isRequired,
restrictDownloadOnSizeDefinition: PropTypes.bool.isRequired,
viewType: PropTypes.string.isRequired,
windowId: PropTypes.string.isRequired,
Expand Down
39 changes: 13 additions & 26 deletions src/MiradorDownloadDialog.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,19 @@ export class MiradorDownloadDialog extends Component {
<Typography variant="h2" component="span">Download</Typography>
</DialogTitle>
<ScrollIndicatedDialogContent>
{canvases.map((canvas) => {
const imageInfo = infoResponse(canvas.id);
const context = imageInfo.json && imageInfo.json['@context'];
let contextArray;
if (Array.isArray(context)) {
contextArray = context;
} else if (typeof context === 'string') {
contextArray = [context];
}
const isVersion3 = contextArray && contextArray.indexOf('http://iiif.io/api/image/3/context.json') > -1;

return (
<CanvasDownloadLinks
canvas={canvas}
canvasLabel={canvasLabel(canvas.id)}
isVersion3={isVersion3}
infoResponse={infoResponse(canvas.id)}
restrictDownloadOnSizeDefinition={
restrictDownloadOnSizeDefinition
}
key={canvas.id}
viewType={viewType}
windowId={windowId}
/>
);
})}
{canvases.map((canvas) => (
<CanvasDownloadLinks
canvas={canvas}
canvasLabel={canvasLabel(canvas.id)}
infoResponse={infoResponse(canvas.id)}
restrictDownloadOnSizeDefinition={
restrictDownloadOnSizeDefinition
}
key={canvas.id}
viewType={viewType}
windowId={windowId}
/>
))}
{this.renderings().length > 0 && (
<ManifestDownloadLinks
renderings={this.renderings()}
Expand Down
106 changes: 106 additions & 0 deletions src/iiifImageFunctions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
function getArrayFromInfoResponse(imageInfo, key) {
const value = imageInfo && imageInfo[key];
let valueArray;
if (Array.isArray(value)) {
valueArray = value;
} else if (typeof value === 'string') {
valueArray = [value];
}
return valueArray;
}

export function getComplianceLevel(imageInfo) {
const profile = getArrayFromInfoResponse(imageInfo, 'profile');
switch (profile && profile[0]) {
case 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0':
case 'http://iiif.io/api/image/1/level0.json':
case 'http://iiif.io/api/image/2/level0.json':
case 'level0':
return 0;
case 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level1':
case 'http://iiif.io/api/image/1/level1.json':
case 'http://iiif.io/api/image/2/level1.json':
case 'level1':
return 1;
case 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2':
case 'http://iiif.io/api/image/1/level2.json':
case 'http://iiif.io/api/image/2/level2.json':
case 'level2':
return 2;
default:
return undefined;
}
}

export function getImageApiVersion(imageInfo) {
const context = getArrayFromInfoResponse(imageInfo, '@context');
if (!context) {
return undefined;
}
if (context.indexOf('http://iiif.io/api/image/3/context.json') > -1) {
return 3;
}
if (context.indexOf('http://iiif.io/api/image/2/context.json') > -1) {
return 2;
}
if (context.indexOf('http://iiif.io/api/image/1/context.json') > -1
|| context.indexOf('http://library.stanford.edu/iiif/image-api/1.1/context.json') > -1) {
return 1;
}
return undefined;
}

function supportsAdditonalFeature(imageInfo, feature) {
const version = getImageApiVersion(imageInfo);
switch (version) {
case 2: {
const profile = getArrayFromInfoResponse(imageInfo, 'profile');
return profile
&& profile.length > 1
&& profile[1].supports
&& profile[1].supports.indexOf(feature) > -1;
}
case 3:
return imageInfo.extraFeatures && imageInfo.extraFeatures.indexOf(feature) > -1;
default:
return false;
}
}

function supportsArbitrarySizeInCanonicalForm(imageInfo) {
const level = getComplianceLevel(imageInfo);
const version = getImageApiVersion(imageInfo);
// everything but undefined or 0 is fine
if (!!level
|| (version < 3 && supportsAdditonalFeature(imageInfo, 'sizeByW'))
|| (version === 3 && supportsAdditonalFeature(imageInfo, 'sizeByWh'))) {
return true;
}
return false;
}

export function calculateHeightForWidth(imageInfo, width) {
if (!imageInfo) {
return undefined;
}
if (imageInfo.width === width) {
return imageInfo.width;
}
return Math.floor((imageInfo.height * width) / imageInfo.width);
}

export function createCanonicalImageUrl(imageInfo, region, width, height) {
const version = getImageApiVersion(imageInfo);
let baseUri = imageInfo['@id'] || imageInfo.id;
baseUri = baseUri && baseUri.replace(/\/$/, '');
let size = `${width},${version === 3 ? height : ''}`;
const quality = version === 1 ? 'native' : 'default';
if (version < 3 && imageInfo.width === width && imageInfo.height === height) {
size = 'full';
}
if (!supportsArbitrarySizeInCanonicalForm(imageInfo)) {
// TODO check if requested size is available for level 0, return undefined otherwise
}
// TODO check if size exceeds maximum width / height / area
return `${baseUri}/${region}/${size}/0/${quality}.jpg`;
}

0 comments on commit 321e892

Please sign in to comment.