@canopy-iiif/app 0.9.13 → 0.9.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/build/build.js +1 -0
- package/lib/build/iiif.js +234 -12
- package/lib/build/mdx.js +13 -0
- package/lib/build/pages.js +17 -1
- package/lib/components/featured.js +6 -0
- package/lib/components/hero-slider-runtime.js +9 -2
- package/lib/iiif/thumbnail.js +262 -4
- package/package.json +2 -1
- package/ui/dist/index.mjs +294 -142
- package/ui/dist/index.mjs.map +4 -4
- package/ui/dist/server.mjs +356 -181
- package/ui/dist/server.mjs.map +4 -4
- package/ui/styles/base/_common.scss +32 -33
- package/ui/styles/base/_heading.scss +26 -11
- package/ui/styles/base/_markdown.scss +43 -0
- package/ui/styles/base/index.scss +1 -0
- package/ui/styles/components/_buttons.scss +54 -40
- package/ui/styles/components/_interstitial-hero.scss +13 -18
- package/ui/styles/components/header/_header.scss +10 -3
- package/ui/styles/components/header/_logo.scss +32 -33
- package/ui/styles/components/search/_form.scss +2 -6
- package/ui/styles/index.css +186 -144
- package/ui/theme.js +4 -4
package/lib/iiif/thumbnail.js
CHANGED
|
@@ -7,6 +7,219 @@
|
|
|
7
7
|
// Helper for resolving a representative thumbnail for an IIIF resource.
|
|
8
8
|
// Uses @iiif/helpers#createThumbnailHelper to choose an appropriate size.
|
|
9
9
|
|
|
10
|
+
function arrayify(value) {
|
|
11
|
+
if (!value) return [];
|
|
12
|
+
return Array.isArray(value) ? value : [value];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function normalizeImageServiceCandidate(candidate) {
|
|
16
|
+
if (!candidate || typeof candidate !== 'object') return null;
|
|
17
|
+
const id = candidate.id || candidate['@id'];
|
|
18
|
+
if (!id) return null;
|
|
19
|
+
const typeValue = candidate.type || candidate['@type'] || '';
|
|
20
|
+
const type = Array.isArray(typeValue) ? typeValue[0] : typeValue;
|
|
21
|
+
const profileValue = candidate.profile || candidate['@profile'];
|
|
22
|
+
const profile = Array.isArray(profileValue)
|
|
23
|
+
? profileValue.find((p) => typeof p === 'string')
|
|
24
|
+
: profileValue;
|
|
25
|
+
const preferredFormats = arrayify(candidate.preferredFormats)
|
|
26
|
+
.map((val) => String(val || '').toLowerCase())
|
|
27
|
+
.filter(Boolean);
|
|
28
|
+
const formats = arrayify(candidate.formats || candidate.supportedFormats)
|
|
29
|
+
.map((val) => String(val || '').toLowerCase())
|
|
30
|
+
.filter(Boolean);
|
|
31
|
+
const qualities = arrayify(candidate.qualities)
|
|
32
|
+
.map((val) => String(val || '').toLowerCase())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
return {
|
|
35
|
+
id: String(id),
|
|
36
|
+
type: type ? String(type) : '',
|
|
37
|
+
profile: profile ? String(profile) : '',
|
|
38
|
+
formats,
|
|
39
|
+
preferredFormats,
|
|
40
|
+
qualities,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isIiifImageService(candidate) {
|
|
45
|
+
if (!candidate) return false;
|
|
46
|
+
const {type, profile} = candidate;
|
|
47
|
+
if (type && /ImageService/i.test(type)) return true;
|
|
48
|
+
if (profile && /iiif\.io\/api\/image/i.test(profile)) return true;
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractImageService(value, seen = new Set()) {
|
|
53
|
+
if (!value || typeof value !== 'object') return null;
|
|
54
|
+
if (seen.has(value)) return null;
|
|
55
|
+
seen.add(value);
|
|
56
|
+
|
|
57
|
+
const direct = normalizeImageServiceCandidate(value);
|
|
58
|
+
if (isIiifImageService(direct)) return direct;
|
|
59
|
+
|
|
60
|
+
const branches = [
|
|
61
|
+
value.service,
|
|
62
|
+
value.services,
|
|
63
|
+
value.thumbnail,
|
|
64
|
+
value.thumbnails,
|
|
65
|
+
value.body,
|
|
66
|
+
];
|
|
67
|
+
for (const branch of branches) {
|
|
68
|
+
const list = arrayify(branch);
|
|
69
|
+
for (const entry of list) {
|
|
70
|
+
const svc = extractImageService(entry, seen);
|
|
71
|
+
if (svc) return svc;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function normalizeImagePayload(body, canvas) {
|
|
78
|
+
if (!body || typeof body !== 'object') return null;
|
|
79
|
+
const id = body.id || body['@id'];
|
|
80
|
+
const width =
|
|
81
|
+
typeof body.width === 'number'
|
|
82
|
+
? body.width
|
|
83
|
+
: typeof (canvas && canvas.width) === 'number'
|
|
84
|
+
? canvas.width
|
|
85
|
+
: undefined;
|
|
86
|
+
const height =
|
|
87
|
+
typeof body.height === 'number'
|
|
88
|
+
? body.height
|
|
89
|
+
: typeof (canvas && canvas.height) === 'number'
|
|
90
|
+
? canvas.height
|
|
91
|
+
: undefined;
|
|
92
|
+
const service = extractImageService(body);
|
|
93
|
+
return {
|
|
94
|
+
id: id ? String(id) : '',
|
|
95
|
+
width,
|
|
96
|
+
height,
|
|
97
|
+
service: service || undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function inspectCanvasForImage(canvas) {
|
|
102
|
+
if (!canvas || typeof canvas !== 'object') return null;
|
|
103
|
+
const annotationPages = arrayify(canvas.items || canvas.annotations);
|
|
104
|
+
for (const page of annotationPages) {
|
|
105
|
+
if (!page || typeof page !== 'object') continue;
|
|
106
|
+
const annotations = arrayify(page.items || page.annotations);
|
|
107
|
+
for (const annotation of annotations) {
|
|
108
|
+
if (!annotation || typeof annotation !== 'object') continue;
|
|
109
|
+
const bodies = arrayify(annotation.body);
|
|
110
|
+
for (const body of bodies) {
|
|
111
|
+
const payload = normalizeImagePayload(body, canvas);
|
|
112
|
+
if (payload && (payload.id || payload.service)) return payload;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (canvas.placeholderCanvas) {
|
|
117
|
+
const placeholderPayload = inspectCanvasForImage(canvas.placeholderCanvas);
|
|
118
|
+
if (placeholderPayload) return placeholderPayload;
|
|
119
|
+
}
|
|
120
|
+
const thumbnails = arrayify(canvas.thumbnail);
|
|
121
|
+
for (const thumb of thumbnails) {
|
|
122
|
+
const payload = normalizeImagePayload(thumb, canvas);
|
|
123
|
+
if (payload && (payload.id || payload.service)) return payload;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function findPrimaryCanvasImage(resource) {
|
|
129
|
+
try {
|
|
130
|
+
const canvases = arrayify(resource && resource.items);
|
|
131
|
+
if (!canvases.length) return null;
|
|
132
|
+
for (const canvas of canvases) {
|
|
133
|
+
const payload = inspectCanvasForImage(canvas);
|
|
134
|
+
if (payload) return payload;
|
|
135
|
+
}
|
|
136
|
+
} catch (_) {}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function findCanvasImageService(resource) {
|
|
141
|
+
const payload = findPrimaryCanvasImage(resource);
|
|
142
|
+
return payload && payload.service ? payload.service : null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function normalizeServiceBaseId(id) {
|
|
146
|
+
if (!id) return '';
|
|
147
|
+
try {
|
|
148
|
+
let cleaned = String(id).trim();
|
|
149
|
+
cleaned = cleaned.replace(/\/info\.json$/i, '');
|
|
150
|
+
cleaned = cleaned.replace(/\/$/, '');
|
|
151
|
+
return cleaned;
|
|
152
|
+
} catch (_) {
|
|
153
|
+
return String(id || '');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function selectServiceFormat(candidate) {
|
|
158
|
+
if (!candidate) return 'jpg';
|
|
159
|
+
const formats = Array.isArray(candidate.formats)
|
|
160
|
+
? candidate.formats
|
|
161
|
+
: Array.isArray(candidate.preferredFormats)
|
|
162
|
+
? candidate.preferredFormats
|
|
163
|
+
: [];
|
|
164
|
+
const prioritized = ['jpg', 'jpeg', 'png', 'webp'];
|
|
165
|
+
const lower = formats.map((f) => String(f || '').toLowerCase());
|
|
166
|
+
for (const fmt of prioritized) {
|
|
167
|
+
if (lower.includes(fmt)) return fmt === 'jpeg' ? 'jpg' : fmt;
|
|
168
|
+
}
|
|
169
|
+
return 'jpg';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function selectServiceQuality(candidate) {
|
|
173
|
+
if (!candidate) return 'default';
|
|
174
|
+
const {qualities = [], type = '', profile = ''} = candidate;
|
|
175
|
+
if (qualities.includes('default')) return 'default';
|
|
176
|
+
if (qualities.includes('native')) return 'native';
|
|
177
|
+
if (/ImageService3/i.test(type)) return 'default';
|
|
178
|
+
if (/ImageService2/i.test(type) || /ImageService1/i.test(type)) {
|
|
179
|
+
if (/level0/i.test(profile)) return 'default';
|
|
180
|
+
return 'default';
|
|
181
|
+
}
|
|
182
|
+
if (/iiif\.io\/api\/image/i.test(profile)) return 'default';
|
|
183
|
+
return 'default';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function buildIiifImageUrlFromNormalizedService(service, preferredSize = 800) {
|
|
187
|
+
if (!service || !isIiifImageService(service)) return '';
|
|
188
|
+
const baseId = normalizeServiceBaseId(service.id);
|
|
189
|
+
if (!baseId) return '';
|
|
190
|
+
const size = preferredSize && preferredSize > 0 ? preferredSize : 800;
|
|
191
|
+
const quality = selectServiceQuality(service);
|
|
192
|
+
const format = selectServiceFormat(service);
|
|
193
|
+
return `${baseId}/full/!${size},${size}/0/${quality}.${format}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildIiifImageUrlFromService(service, preferredSize = 800) {
|
|
197
|
+
const normalized = normalizeImageServiceCandidate(service);
|
|
198
|
+
if (!normalized) return '';
|
|
199
|
+
return buildIiifImageUrlFromNormalizedService(normalized, preferredSize);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function buildIiifImageSrcset(service, steps = [360, 640, 960, 1280, 1600]) {
|
|
203
|
+
const normalized = normalizeImageServiceCandidate(service);
|
|
204
|
+
if (!normalized || !isIiifImageService(normalized)) return '';
|
|
205
|
+
const uniqueSteps = Array.from(
|
|
206
|
+
new Set(
|
|
207
|
+
(Array.isArray(steps) ? steps : [])
|
|
208
|
+
.map((value) => Number(value) || 0)
|
|
209
|
+
.filter((value) => value > 0)
|
|
210
|
+
.sort((a, b) => a - b)
|
|
211
|
+
)
|
|
212
|
+
);
|
|
213
|
+
if (!uniqueSteps.length) return '';
|
|
214
|
+
const entries = uniqueSteps
|
|
215
|
+
.map((width) => {
|
|
216
|
+
const url = buildIiifImageUrlFromNormalizedService(normalized, width);
|
|
217
|
+
return url ? `${url} ${width}w` : '';
|
|
218
|
+
})
|
|
219
|
+
.filter(Boolean);
|
|
220
|
+
return entries.join(', ');
|
|
221
|
+
}
|
|
222
|
+
|
|
10
223
|
async function getRepresentativeImage(resource, preferredSize = 1200, unsafe = false) {
|
|
11
224
|
// Fast path: if resource already contains a thumbnail, return it without any helper work
|
|
12
225
|
try {
|
|
@@ -14,10 +227,26 @@ async function getRepresentativeImage(resource, preferredSize = 1200, unsafe = f
|
|
|
14
227
|
if (t) {
|
|
15
228
|
const first = Array.isArray(t) ? t[0] : t;
|
|
16
229
|
if (first && (first.id || first['@id'])) {
|
|
230
|
+
const canvasImage = findPrimaryCanvasImage(resource);
|
|
231
|
+
const service =
|
|
232
|
+
extractImageService(first) ||
|
|
233
|
+
(canvasImage && canvasImage.service) ||
|
|
234
|
+
undefined;
|
|
17
235
|
return {
|
|
18
236
|
id: String(first.id || first['@id']),
|
|
19
|
-
width:
|
|
20
|
-
|
|
237
|
+
width:
|
|
238
|
+
typeof first.width === 'number'
|
|
239
|
+
? first.width
|
|
240
|
+
: canvasImage && typeof canvasImage.width === 'number'
|
|
241
|
+
? canvasImage.width
|
|
242
|
+
: undefined,
|
|
243
|
+
height:
|
|
244
|
+
typeof first.height === 'number'
|
|
245
|
+
? first.height
|
|
246
|
+
: canvasImage && typeof canvasImage.height === 'number'
|
|
247
|
+
? canvasImage.height
|
|
248
|
+
: undefined,
|
|
249
|
+
service: service || undefined,
|
|
21
250
|
};
|
|
22
251
|
}
|
|
23
252
|
}
|
|
@@ -46,7 +275,29 @@ async function getRepresentativeImage(resource, preferredSize = 1200, unsafe = f
|
|
|
46
275
|
const id = String(result.id || result['@id'] || '');
|
|
47
276
|
const width = typeof result.width === 'number' ? result.width : undefined;
|
|
48
277
|
const height = typeof result.height === 'number' ? result.height : undefined;
|
|
49
|
-
|
|
278
|
+
const canvasImage = findPrimaryCanvasImage(resource);
|
|
279
|
+
const service =
|
|
280
|
+
extractImageService(result) ||
|
|
281
|
+
extractImageService(resource) ||
|
|
282
|
+
(canvasImage && canvasImage.service);
|
|
283
|
+
return id
|
|
284
|
+
? {
|
|
285
|
+
id,
|
|
286
|
+
width:
|
|
287
|
+
width != null
|
|
288
|
+
? width
|
|
289
|
+
: canvasImage && typeof canvasImage.width === 'number'
|
|
290
|
+
? canvasImage.width
|
|
291
|
+
: undefined,
|
|
292
|
+
height:
|
|
293
|
+
height != null
|
|
294
|
+
? height
|
|
295
|
+
: canvasImage && typeof canvasImage.height === 'number'
|
|
296
|
+
? canvasImage.height
|
|
297
|
+
: undefined,
|
|
298
|
+
service: service || undefined,
|
|
299
|
+
}
|
|
300
|
+
: null;
|
|
50
301
|
} catch (_) {
|
|
51
302
|
return null;
|
|
52
303
|
}
|
|
@@ -83,4 +334,11 @@ async function getThumbnailUrl(resource, preferredSize = 1200, unsafe = false) {
|
|
|
83
334
|
return res && res.url ? String(res.url) : '';
|
|
84
335
|
}
|
|
85
336
|
|
|
86
|
-
module.exports = {
|
|
337
|
+
module.exports = {
|
|
338
|
+
getRepresentativeImage,
|
|
339
|
+
getThumbnail,
|
|
340
|
+
getThumbnailUrl,
|
|
341
|
+
buildIiifImageUrlFromService,
|
|
342
|
+
findPrimaryCanvasImage,
|
|
343
|
+
buildIiifImageSrcset,
|
|
344
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canopy-iiif/app",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.15",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Mat Jordan <mat@northwestern.edu>",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"@mdx-js/react": "^3.1.0",
|
|
45
45
|
"charm": "^1.0.2",
|
|
46
46
|
"js-yaml": "^4.1.0",
|
|
47
|
+
"remark-gfm": "^4.0.0",
|
|
47
48
|
"react-masonry-css": "^1.0.16",
|
|
48
49
|
"sass": "^1.77.0",
|
|
49
50
|
"slugify": "^1.6.6"
|