Skip to content

Commit 321e892

Browse files
committed
Get rid of manifesto's canonical URI creation method, improve API compliance
TODOs - tests for added IIIF image functions - CanvasDownloadLinks with different API versions
1 parent 3e57cf4 commit 321e892

File tree

4 files changed

+183
-74
lines changed

4 files changed

+183
-74
lines changed

__tests__/CanvasDownloadLinks.test.js

Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ function createWrapper(props) {
1212
canvasId="abc123"
1313
canvasLabel="My Canvas Label"
1414
classes={{}}
15-
isVersion3={false}
16-
infoResponse={{}}
1715
restrictDownloadOnSizeDefinition={false}
1816
viewType="single"
1917
windowId="wid123"
@@ -41,6 +39,18 @@ describe('CanvasDownloadLinks', () => {
4139
],
4240
};
4341

42+
const infoResponse = {
43+
json: {
44+
'@context': 'http://iiif.io/api/image/2/context.json',
45+
'@id': 'http://example.com/iiif/abc123/',
46+
width: 4000,
47+
height: 1000,
48+
profile: [
49+
'http://iiif.io/api/image/2/level1.json',
50+
],
51+
},
52+
};
53+
4454
let currentBoundsSpy;
4555

4656
beforeEach(() => {
@@ -52,7 +62,7 @@ describe('CanvasDownloadLinks', () => {
5262
});
5363

5464
it('renders the canvas label as an h3 heading', () => {
55-
createWrapper({ canvas });
65+
createWrapper({ canvas, infoResponse });
5666

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

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

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

7181
describe('Zoomed Region Links', () => {
72-
const infoResponse = {
73-
json: { width: 4000, height: 1000 },
74-
};
75-
7682
it('does not render a zoom link when viewer is zoomed out to full image', () => {
7783
currentBoundsSpy.mockImplementation(() => ({
7884
x: 0, y: 0, width: 6000, height: 1000,
@@ -130,8 +136,7 @@ describe('CanvasDownloadLinks', () => {
130136
canvas,
131137
infoResponse: {
132138
json: {
133-
width: 4000,
134-
height: 1000,
139+
...infoResponse.json,
135140
sizes: [{ width: 400, height: 100 }],
136141
},
137142
},
@@ -147,11 +152,16 @@ describe('CanvasDownloadLinks', () => {
147152
});
148153

149154
describe('When Defined Sizes Are Present in infoResponse', () => {
150-
const sizes = [
151-
{ width: 4000, height: 1000 },
152-
{ width: 2000, height: 500 },
153-
{ width: 1000, height: 250 },
154-
];
155+
const infoResponseWithSizes = {
156+
json: {
157+
...infoResponse.json,
158+
sizes: [
159+
{ width: 4000, height: 1000 },
160+
{ width: 2000, height: 500 },
161+
{ width: 1000, height: 250 },
162+
],
163+
},
164+
};
155165

156166
const viewport = {
157167
getBounds: () => ({
@@ -162,7 +172,7 @@ describe('CanvasDownloadLinks', () => {
162172
current: { viewport },
163173
});
164174
it('renders download links for all specified sizes in the dialog', () => {
165-
createWrapper({ canvas, infoResponse: { json: { sizes } } });
175+
createWrapper({ canvas, infoResponse: infoResponseWithSizes });
166176

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

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

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

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

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

203219
const links = screen.getAllByRole('link');
204220
expect(links).toHaveLength(2); // Should only show full-size version and link to PDF.

src/CanvasDownloadLinks.js

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import Link from '@mui/material/Link';
77
import List from '@mui/material/List';
88
import ListItem from '@mui/material/ListItem';
99
import RenderingDownloadLink from './RenderingDownloadLink';
10+
import { calculateHeightForWidth, createCanonicalImageUrl } from './iiifImageFunctions';
1011

1112
/**
1213
* CanvasDownloadLinks ~
@@ -20,51 +21,50 @@ export default class CanvasDownloadLinks extends Component {
2021
}
2122

2223
fullImageLabel() {
23-
const { canvas } = this.props;
24-
25-
return `Whole image (${canvas.getWidth()} x ${canvas.getHeight()}px)`;
24+
const { infoResponse } = this.props;
25+
const imageInfo = infoResponse && infoResponse.json;
26+
return imageInfo && `Whole image (${imageInfo.width} x ${imageInfo.height}px)`;
2627
}
2728

2829
smallImageLabel() {
29-
const { canvas } = this.props;
30+
const { infoResponse } = this.props;
31+
const imageInfo = infoResponse && infoResponse.json;
3032

3133
return `Whole image (1000 x ${Math.floor(
32-
(1000 * canvas.getHeight()) / canvas.getWidth(),
34+
(1000 * imageInfo.height) / imageInfo.width,
3335
)}px)`;
3436
}
3537

3638
zoomedImageUrl() {
37-
const { canvas, isVersion3 } = this.props;
39+
const { infoResponse } = this.props;
40+
const imageInfo = infoResponse && infoResponse.json;
3841
const bounds = this.currentBounds();
39-
const boundsUrl = canvas
40-
.getCanonicalImageUri()
41-
.replace(
42-
/\/full\/.*\/0\//,
43-
`/${bounds.x},${bounds.y},${bounds.width},${bounds.height}/${isVersion3 ? `${bounds.width},${bounds.height}` : 'full'}/0/`,
44-
);
45-
46-
return `${boundsUrl}?download=true`;
42+
const boundsUrl = createCanonicalImageUrl(
43+
imageInfo,
44+
`${bounds.x},${bounds.y},${bounds.width},${bounds.height}`,
45+
bounds.width,
46+
bounds.height,
47+
);
48+
return imageInfo && `${boundsUrl}?download=true`;
4749
}
4850

4951
imageUrlForSize(size) {
50-
const { canvas, isVersion3 } = this.props;
51-
52-
return isVersion3 ? `${canvas.getCanonicalImageUri().replace(/\/full\/.*\/0\//, `/full/${size.width},${size.height}/0/`)}?download=true`
53-
: `${canvas.getCanonicalImageUri(size.width)}?download=true`;
52+
const { infoResponse } = this.props;
53+
const imageInfo = infoResponse && infoResponse.json;
54+
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', size.width, size.height)}?download=true`;
5455
}
5556

5657
fullImageUrl() {
57-
const { canvas, isVersion3 } = this.props;
58-
59-
return `${canvas
60-
.getCanonicalImageUri()
61-
.replace(/\/full\/.*\/0\//, `/full/${isVersion3 ? 'max' : 'full'}/0/`)}?download=true`;
58+
const { infoResponse } = this.props;
59+
const imageInfo = infoResponse && infoResponse.json;
60+
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', imageInfo.width, imageInfo.height)}?download=true`;
6261
}
6362

6463
thousandPixelWideImage() {
65-
const { canvas } = this.props;
66-
67-
return `${canvas.getCanonicalImageUri('1000')}?download=true`;
64+
const { infoResponse } = this.props;
65+
const imageInfo = infoResponse && infoResponse.json;
66+
const height = calculateHeightForWidth(imageInfo, 1000);
67+
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', 1000, height)}?download=true`;
6868
}
6969

7070
osdViewport() {
@@ -143,9 +143,10 @@ export default class CanvasDownloadLinks extends Component {
143143
}
144144

145145
thousandPixelWideLink() {
146-
const { canvas } = this.props;
146+
const { infoResponse } = this.props;
147+
const imageInfo = infoResponse && infoResponse.json;
147148

148-
if (canvas.getWidth() < 1000) return '';
149+
if (!imageInfo || imageInfo.width < 1000) return '';
149150

150151
return (
151152
<ListItem disableGutters divider key={this.thousandPixelWideImage()}>
@@ -233,7 +234,6 @@ CanvasDownloadLinks.propTypes = {
233234
width: PropTypes.number,
234235
}),
235236
}).isRequired,
236-
isVersion3: PropTypes.bool.isRequired,
237237
restrictDownloadOnSizeDefinition: PropTypes.bool.isRequired,
238238
viewType: PropTypes.string.isRequired,
239239
windowId: PropTypes.string.isRequired,

src/MiradorDownloadDialog.js

Lines changed: 13 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -83,32 +83,19 @@ export class MiradorDownloadDialog extends Component {
8383
<Typography variant="h2" component="span">Download</Typography>
8484
</DialogTitle>
8585
<ScrollIndicatedDialogContent>
86-
{canvases.map((canvas) => {
87-
const imageInfo = infoResponse(canvas.id);
88-
const context = imageInfo.json && imageInfo.json['@context'];
89-
let contextArray;
90-
if (Array.isArray(context)) {
91-
contextArray = context;
92-
} else if (typeof context === 'string') {
93-
contextArray = [context];
94-
}
95-
const isVersion3 = contextArray && contextArray.indexOf('http://iiif.io/api/image/3/context.json') > -1;
96-
97-
return (
98-
<CanvasDownloadLinks
99-
canvas={canvas}
100-
canvasLabel={canvasLabel(canvas.id)}
101-
isVersion3={isVersion3}
102-
infoResponse={infoResponse(canvas.id)}
103-
restrictDownloadOnSizeDefinition={
104-
restrictDownloadOnSizeDefinition
105-
}
106-
key={canvas.id}
107-
viewType={viewType}
108-
windowId={windowId}
109-
/>
110-
);
111-
})}
86+
{canvases.map((canvas) => (
87+
<CanvasDownloadLinks
88+
canvas={canvas}
89+
canvasLabel={canvasLabel(canvas.id)}
90+
infoResponse={infoResponse(canvas.id)}
91+
restrictDownloadOnSizeDefinition={
92+
restrictDownloadOnSizeDefinition
93+
}
94+
key={canvas.id}
95+
viewType={viewType}
96+
windowId={windowId}
97+
/>
98+
))}
11299
{this.renderings().length > 0 && (
113100
<ManifestDownloadLinks
114101
renderings={this.renderings()}

src/iiifImageFunctions.js

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
function getArrayFromInfoResponse(imageInfo, key) {
2+
const value = imageInfo && imageInfo[key];
3+
let valueArray;
4+
if (Array.isArray(value)) {
5+
valueArray = value;
6+
} else if (typeof value === 'string') {
7+
valueArray = [value];
8+
}
9+
return valueArray;
10+
}
11+
12+
export function getComplianceLevel(imageInfo) {
13+
const profile = getArrayFromInfoResponse(imageInfo, 'profile');
14+
switch (profile && profile[0]) {
15+
case 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level0':
16+
case 'http://iiif.io/api/image/1/level0.json':
17+
case 'http://iiif.io/api/image/2/level0.json':
18+
case 'level0':
19+
return 0;
20+
case 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level1':
21+
case 'http://iiif.io/api/image/1/level1.json':
22+
case 'http://iiif.io/api/image/2/level1.json':
23+
case 'level1':
24+
return 1;
25+
case 'http://library.stanford.edu/iiif/image-api/1.1/compliance.html#level2':
26+
case 'http://iiif.io/api/image/1/level2.json':
27+
case 'http://iiif.io/api/image/2/level2.json':
28+
case 'level2':
29+
return 2;
30+
default:
31+
return undefined;
32+
}
33+
}
34+
35+
export function getImageApiVersion(imageInfo) {
36+
const context = getArrayFromInfoResponse(imageInfo, '@context');
37+
if (!context) {
38+
return undefined;
39+
}
40+
if (context.indexOf('http://iiif.io/api/image/3/context.json') > -1) {
41+
return 3;
42+
}
43+
if (context.indexOf('http://iiif.io/api/image/2/context.json') > -1) {
44+
return 2;
45+
}
46+
if (context.indexOf('http://iiif.io/api/image/1/context.json') > -1
47+
|| context.indexOf('http://library.stanford.edu/iiif/image-api/1.1/context.json') > -1) {
48+
return 1;
49+
}
50+
return undefined;
51+
}
52+
53+
function supportsAdditonalFeature(imageInfo, feature) {
54+
const version = getImageApiVersion(imageInfo);
55+
switch (version) {
56+
case 2: {
57+
const profile = getArrayFromInfoResponse(imageInfo, 'profile');
58+
return profile
59+
&& profile.length > 1
60+
&& profile[1].supports
61+
&& profile[1].supports.indexOf(feature) > -1;
62+
}
63+
case 3:
64+
return imageInfo.extraFeatures && imageInfo.extraFeatures.indexOf(feature) > -1;
65+
default:
66+
return false;
67+
}
68+
}
69+
70+
function supportsArbitrarySizeInCanonicalForm(imageInfo) {
71+
const level = getComplianceLevel(imageInfo);
72+
const version = getImageApiVersion(imageInfo);
73+
// everything but undefined or 0 is fine
74+
if (!!level
75+
|| (version < 3 && supportsAdditonalFeature(imageInfo, 'sizeByW'))
76+
|| (version === 3 && supportsAdditonalFeature(imageInfo, 'sizeByWh'))) {
77+
return true;
78+
}
79+
return false;
80+
}
81+
82+
export function calculateHeightForWidth(imageInfo, width) {
83+
if (!imageInfo) {
84+
return undefined;
85+
}
86+
if (imageInfo.width === width) {
87+
return imageInfo.width;
88+
}
89+
return Math.floor((imageInfo.height * width) / imageInfo.width);
90+
}
91+
92+
export function createCanonicalImageUrl(imageInfo, region, width, height) {
93+
const version = getImageApiVersion(imageInfo);
94+
let baseUri = imageInfo['@id'] || imageInfo.id;
95+
baseUri = baseUri && baseUri.replace(/\/$/, '');
96+
let size = `${width},${version === 3 ? height : ''}`;
97+
const quality = version === 1 ? 'native' : 'default';
98+
if (version < 3 && imageInfo.width === width && imageInfo.height === height) {
99+
size = 'full';
100+
}
101+
if (!supportsArbitrarySizeInCanonicalForm(imageInfo)) {
102+
// TODO check if requested size is available for level 0, return undefined otherwise
103+
}
104+
// TODO check if size exceeds maximum width / height / area
105+
return `${baseUri}/${region}/${size}/0/${quality}.jpg`;
106+
}

0 commit comments

Comments
 (0)