diff --git a/__tests__/src/lib/ThumbnailFactory.test.js b/__tests__/src/lib/ThumbnailFactory.test.js index be543fc6e39372e788f9beadf1252f4bd655ba8d..53f8daf4a1bc3b7b2f3e6a1080a9aecfa0d8b1ae 100644 --- a/__tests__/src/lib/ThumbnailFactory.test.js +++ b/__tests__/src/lib/ThumbnailFactory.test.js @@ -248,6 +248,66 @@ describe('getThumbnail', () => { }); }); +describe('picking the best format', () => { + const url = 'http://example.com'; + + it('defaults to jpg', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + profile: 'level2', + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas')) + .toMatchObject({ url: `${url}/full/,120/0/default.jpg` }); + }); + + it('uses the preferred format of the service', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + preferredFormats: ['webp'], + profile: 'level2', + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas')) + .toMatchObject({ url: `${url}/full/,120/0/default.webp` }); + }); + + it('can be filtered by application preferred formats', () => { + const myCanvas = { + ...canvas.__jsonld, + thumbnail: { + height: 100, + id: 'arbitrary-url', + service: [{ + id: url, + preferredFormats: ['webp', 'png'], + profile: 'level2', + type: 'ImageService3', + }], + width: 100, + }, + }; + expect(createSubject(myCanvas, 'Canvas', { preferredFormats: ['png', 'jpg'] })) + .toMatchObject({ url: `${url}/full/,120/0/default.png` }); + }); +}); + describe('selectBestImageSize', () => { const targetWidth = 120; const targetHeight = 120; diff --git a/src/components/IIIFThumbnail.js b/src/components/IIIFThumbnail.js index b4b044a56100026da7f408e87ec05b5161a72aad..c942f4824231739d722ab08c9682abdc3165148f 100644 --- a/src/components/IIIFThumbnail.js +++ b/src/components/IIIFThumbnail.js @@ -115,12 +115,12 @@ export class IIIFThumbnail extends Component { /** */ image() { const { - thumbnail, resource, maxHeight, maxWidth, + thumbnail, resource, maxHeight, maxWidth, thumbnailsConfig, } = this.props; if (thumbnail) return thumbnail; - const image = getThumbnail(resource, { maxHeight, maxWidth }); + const image = getThumbnail(resource, { ...thumbnailsConfig, maxHeight, maxWidth }); if (image && image.url) return image; @@ -189,6 +189,7 @@ IIIFThumbnail.propTypes = { url: PropTypes.string.isRequired, width: PropTypes.number, }), + thumbnailsConfig: PropTypes.object, // eslint-disable-line react/forbid-prop-types variant: PropTypes.oneOf(['inside', 'outside']), }; @@ -203,5 +204,6 @@ IIIFThumbnail.defaultProps = { maxWidth: null, style: {}, thumbnail: null, + thumbnailsConfig: {}, variant: null, }; diff --git a/src/config/settings.js b/src/config/settings.js index cf1115ad646b7cb66d05385b8baedcf8c1b0789a..43ec317f4b37dccf659a67a5e4fd3a67d00f7aa2 100644 --- a/src/config/settings.js +++ b/src/config/settings.js @@ -298,6 +298,9 @@ export default { // ../lib/MiradorViewer.js `windowAction` */ ], + thumbnails: { + preferredFormats: ['jpg', 'png', 'webp', 'tif'], + }, thumbnailNavigation: { defaultPosition: 'off', // Which position for the thumbnail navigation to be be displayed. Other possible values are "far-bottom" or "far-right" displaySettings: true, // Display the settings for this in WindowTopMenu diff --git a/src/containers/IIIFThumbnail.js b/src/containers/IIIFThumbnail.js index 71c95d0a4900ee5b46b177743c6952356b6e5644..c596b508cd92fee25b69b5e09e198011182363e7 100644 --- a/src/containers/IIIFThumbnail.js +++ b/src/containers/IIIFThumbnail.js @@ -1,9 +1,21 @@ import { compose } from 'redux'; +import { connect } from 'react-redux'; import { withTranslation } from 'react-i18next'; import { withStyles } from '@material-ui/core/styles'; import { withPlugins } from '../extend/withPlugins'; +import { + getConfig, +} from '../state/selectors'; import { IIIFThumbnail } from '../components/IIIFThumbnail'; +/** + * mapStateToProps - to hook up connect + * @private + */ +const mapStateToProps = (state) => ({ + thumbnailsConfig: getConfig(state).thumbnails, +}); + /** * Styles for withStyles HOC */ @@ -50,6 +62,7 @@ const styles = theme => ({ const enhance = compose( withStyles(styles), withTranslation(), + connect(mapStateToProps), withPlugins('IIIFThumbnail'), ); diff --git a/src/lib/ThumbnailFactory.js b/src/lib/ThumbnailFactory.js index 8d8d1370ac167d3860c4633820f5a3ac564656fd..67f8a5787467a7b7ce3d008508337b6690364492 100644 --- a/src/lib/ThumbnailFactory.js +++ b/src/lib/ThumbnailFactory.js @@ -197,7 +197,7 @@ class ThumbnailFactory { const region = 'full'; const quality = Utils.getImageQuality(service.getProfile()); const id = service.id.replace(/\/+$/, ''); - const format = 'jpg'; + const format = this.getFormat(service); return { height, url: [id, region, size, 0, `${quality}.${format}`].join('/'), @@ -205,6 +205,41 @@ class ThumbnailFactory { }; } + /** + * Figure out what format thumbnail to use by looking at the preferred formats + * on offer, and selecting a format shared in common with the application's + * preferred format list. + * + * Fall back to jpg, which is required to work for all IIIF services. + */ + getFormat(service) { + const { preferredFormats = [] } = this.iiifOpts; + const servicePreferredFormats = service.getProperty('preferredFormats'); + + if (!servicePreferredFormats) return 'jpg'; + + const filteredFormats = servicePreferredFormats.filter( + value => preferredFormats.includes(value), + ); + + // this is a format found in common between the preferred formats of the service + // and the application + if (filteredFormats[0]) return filteredFormats[0]; + + // IIIF Image API guarantees jpg support; if it wasn't provided by the service + // but the application is fine with it, we might as well try it. + if (!servicePreferredFormats.includes('jpg') && preferredFormats.includes('jpg')) { + return 'jpg'; + } + + // there were no formats in common, and the application didn't want jpg... so + // just trust that the IIIF service is advertising something useful? + if (servicePreferredFormats[0]) return servicePreferredFormats[0]; + + // JPG support is guaranteed by the spec, so it's a good worst-case fallback + return 'jpg'; + } + /** * Determines the content resource from which to derive a thumbnail to represent a given resource. * This method is recursive. diff --git a/src/state/selectors/manifests.js b/src/state/selectors/manifests.js index 63a726680aa8929779c2888c5cc1635b424bdbb5..e9d7c2bbcb13036f2ed27be1abb280a90750a2c7 100644 --- a/src/state/selectors/manifests.js +++ b/src/state/selectors/manifests.js @@ -234,10 +234,13 @@ export const getRights = createSelector( */ export function getManifestThumbnail(state, props) { const manifest = getManifestoInstance(state, props); + const { thumbnails = {} } = getConfig(state); if (!manifest) return undefined; - const thumbnail = getThumbnail(manifest, { maxHeight: 80, maxWidth: 120 }); + const thumbnail = getThumbnail(manifest, { + maxHeight: 80, maxWidth: 120, preferredFormats: thumbnails.preferredFormats, + }); return thumbnail && thumbnail.url; }