@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.
@@ -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: typeof first.width === 'number' ? first.width : undefined,
20
- height: typeof first.height === 'number' ? first.height : undefined,
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
- return id ? { id, width, height } : null;
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 = { getRepresentativeImage, getThumbnail, getThumbnailUrl };
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.13",
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"