Skip to content
53 changes: 35 additions & 18 deletions __tests__/CanvasDownloadLinks.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ function createWrapper(props) {
canvasId="abc123"
canvasLabel="My Canvas Label"
classes={{}}
infoResponse={{}}
restrictDownloadOnSizeDefinition={false}
viewType="single"
windowId="wid123"
Expand Down Expand Up @@ -40,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 @@ -51,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 @@ -60,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: /mirador-dl-plugin\.whole_image {"width":4000,"height":1000}/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 @@ -129,8 +136,7 @@ describe('CanvasDownloadLinks', () => {
canvas,
infoResponse: {
json: {
width: 4000,
height: 1000,
...infoResponse.json,
sizes: [{ width: 400, height: 100 }],
},
},
Expand All @@ -146,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 @@ -161,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: /mirador-dl-plugin\.whole_image {"width":4000,"height":1000}/i });
const link2 = screen.getByRole('link', { name: /mirador-dl-plugin\.whole_image {"width":2000,"height":500}/i });
Expand All @@ -175,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: /mirador-dl-plugin\.whole_image {"width":4000,"height":1000}/i });
expect(link).toBeInTheDocument();
Expand All @@ -184,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: /mirador-dl-plugin\.whole_image {"width":4000,"height":1000}/i });
expect(link1).toHaveAttribute('href', 'http://example.com/iiif/abc123/full/full/0/default.jpg?download=true');
Expand All @@ -197,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', () => {
const smallCanvas = { ...canvas, getWidth: () => 999 };
createWrapper({ canvas: smallCanvas });
const smallInfoResponse = {
json: {
...infoResponse.json,
width: 999,
},
};
createWrapper({ canvas: smallCanvas, infoResponse: smallInfoResponse });

const links = screen.getAllByRole('link');
expect(links).toHaveLength(2); // Should only show full-size version and link to PDF.
Expand Down
29 changes: 23 additions & 6 deletions __tests__/MiradorDownloadDialog.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function createWrapper(props) {
closeDialog={() => {}}
containerId="container-123"
infoResponse={() => ({})}
manifest={{ getSequences: () => [] }}
manifest={{ getRenderings: () => undefined, getSequences: () => [] }}
open
t={(k) => k}
viewType="single"
Expand Down Expand Up @@ -58,22 +58,39 @@ describe('Dialog', () => {
describe('ManifestDownloadLinks', () => {
it('does not render when there are no manifest renderings', () => {
createWrapper();
const manifestLinks = screen.queryByText('ManifestDownloadLinks');
expect(manifestLinks).not.toBeInTheDocument();
const manifestLinksHeading = screen.queryByText('Other download options');
expect(manifestLinksHeading).not.toBeInTheDocument();
});

it('renders when the manifest contains renderings', () => {
const rendering = { id: '', getLabel: () => ({ getValue: () => 'ManifestDownloadLinks' }), getFormat: () => {} };
it('renders when the default sequence contains renderings', () => {
const rendering = { id: '', getLabel: () => ({ getValue: () => 'Rendering from sequence' }), getFormat: () => {} };
createWrapper({
manifest: {
getRenderings: () => undefined,
getSequences: () => [
{
getRenderings: () => [rendering],
},
],
},
});
const manifestLinks = screen.queryByText('ManifestDownloadLinks');
const manifestLinksHeading = screen.queryByText('mirador-dl-plugin.other_download');
expect(manifestLinksHeading).toBeInTheDocument();
const manifestLinks = screen.queryByText('Rendering from sequence');
expect(manifestLinks).toBeInTheDocument();
});

it('renders when the manifest contains renderings', () => {
const rendering = { id: '', getLabel: () => ({ getValue: () => 'Rendering from manifest' }), getFormat: () => {} };
createWrapper({
manifest: {
getRenderings: () => [rendering],
getSequences: () => undefined,
},
});
const manifestLinksHeading = screen.queryByText('mirador-dl-plugin.other_download');
expect(manifestLinksHeading).toBeInTheDocument();
const manifestLinks = screen.queryByText('Rendering from manifest');
expect(manifestLinks).toBeInTheDocument();
});
});
Expand Down
123 changes: 86 additions & 37 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 @@ -22,48 +23,71 @@ export default class CanvasDownloadLinks extends Component {
}

fullImageLabel() {
const { canvas, t } = this.props;
return t('mirador-dl-plugin.whole_image', { width: canvas.getWidth(), height: canvas.getHeight() });
const { infoResponse, t } = this.props;
const imageInfo = infoResponse && infoResponse.json;
return imageInfo && t('mirador-dl-plugin.whole_image', { width: imageInfo.width, height: imageInfo.height });
}

smallImageLabel() {
const { canvas, t } = this.props;
const height = Math.floor((1000 * canvas.getHeight()) / canvas.getWidth());
const { infoResponse, t } = this.props;
const imageInfo = infoResponse && infoResponse.json;
const height = Math.floor((1000 * imageInfo.height) / imageInfo.width);

return t('mirador-dl-plugin.whole_image', { width: 1000, height });
}

nonTiledLabel(image) {
const { t } = this.props;
const width = image.getProperty('width');
const height = image.getProperty('height');
const label = image.getProperty('label');
if (width && height) {
return t('mirador-dl-plugin.whole_image', { width, height });
}
return t('mirador-dl-plugin.whole_image_labeled', { label: label || image.id });
}

zoomedImageUrl() {
const { canvas } = 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}/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 } = this.props;

return `${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 } = this.props;
const { infoResponse } = this.props;
const imageInfo = infoResponse && infoResponse.json;
return imageInfo && `${createCanonicalImageUrl(imageInfo, 'full', imageInfo.width, imageInfo.height)}?download=true`;
}

return `${canvas
.getCanonicalImageUri()
.replace(/\/full\/.*\/0\//, '/full/full/0/')}?download=true`;
nonTiledImagesForCanvas() {
const { canvas, nonTiledResources } = this.props;
if (!nonTiledResources || nonTiledResources.length === 0) {
return [];
}
return nonTiledResources.filter((res) => (
(res.getProperty('type') === 'Image' || res.getProperty('type') === 'dctypes:Image' || res.getProperty('format')?.startsWith('image/'))
&& canvas.imageResources.find(r => r.id === res.id)
));
}

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 @@ -127,24 +151,27 @@ export default class CanvasDownloadLinks extends Component {
}

fullImageLink() {
return (
<ListItem disableGutters divider key={this.fullImageUrl()}>
<Link
href={this.fullImageUrl()}
rel="noopener noreferrer"
target="_blank"
variant="body1"
>
{this.fullImageLabel()}
</Link>
</ListItem>
);
return this.fullImageUrl()
? (
<ListItem disableGutters divider key={this.fullImageUrl()}>
<Link
href={this.fullImageUrl()}
rel="noopener noreferrer"
target="_blank"
variant="body1"
>
{this.fullImageLabel()}
</Link>
</ListItem>
)
: '';
}

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 @@ -176,6 +203,21 @@ export default class CanvasDownloadLinks extends Component {
));
}

nonTiledImageLinks() {
return this.nonTiledImagesForCanvas().map((image) => (
<ListItem disableGutters divider key={image.id}>
<Link
href={`${image.id}?download=true`}
rel="noopener noreferrer"
target="_blank"
variant="body1"
>
{this.nonTiledLabel(image)}
</Link>
</ListItem>
));
}

/**
* Returns the rendered component
*/
Expand Down Expand Up @@ -206,6 +248,7 @@ export default class CanvasDownloadLinks extends Component {
this.thousandPixelWideLink(),
]}
{this.definedSizes().length > 0 && this.linksForDefinedSizes()}
{this.nonTiledImageLinks()}
{canvas.getRenderings().map((rendering) => (
<RenderingDownloadLink rendering={rendering} key={rendering.id} />
))}
Expand All @@ -222,6 +265,9 @@ CanvasDownloadLinks.propTypes = {
getHeight: PropTypes.func.isRequired,
getRenderings: PropTypes.func.isRequired,
getWidth: PropTypes.func.isRequired,
imageResources: PropTypes.arrayOf(
PropTypes.shape({ id: PropTypes.string }),
),
}).isRequired,
canvasLabel: PropTypes.string.isRequired, // canvasLabel is passed because we need access to redux
infoResponse: PropTypes.shape({
Expand All @@ -233,6 +279,9 @@ CanvasDownloadLinks.propTypes = {
width: PropTypes.number,
}),
}).isRequired,
nonTiledResources: PropTypes.arrayOf(
PropTypes.shape({ id: PropTypes.string, format: PropTypes.string }),
).isRequired,
restrictDownloadOnSizeDefinition: PropTypes.bool.isRequired,
t: PropTypes.func.isRequired,
viewType: PropTypes.string.isRequired,
Expand Down
Loading