triiiceratops 0.16.2 → 0.16.4
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.
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
let viewer: any | undefined = $state.raw();
|
|
16
16
|
let OSD: any | undefined = $state();
|
|
17
17
|
|
|
18
|
+
const IIIF_LEVEL0_V2_HTTPS = 'https://iiif.io/api/image/2/level0.json';
|
|
19
|
+
const IIIF_LEVEL0_V2_HTTP = 'http://iiif.io/api/image/2/level0.json';
|
|
20
|
+
|
|
18
21
|
// Track OSD state changes for reactivity
|
|
19
22
|
let osdVersion = $state(0);
|
|
20
23
|
// Track last opened tile source to prevent unnecessary resets
|
|
@@ -169,6 +172,7 @@
|
|
|
169
172
|
if (!mounted) return;
|
|
170
173
|
|
|
171
174
|
OSD = osdModule.default || osdModule;
|
|
175
|
+
patchIiifLevel0ProfileCompatibility(OSD);
|
|
172
176
|
|
|
173
177
|
// Initialize OpenSeadragon viewer
|
|
174
178
|
viewer = OSD({
|
|
@@ -184,6 +188,9 @@
|
|
|
184
188
|
animationTime: 0.5,
|
|
185
189
|
springStiffness: 7.0,
|
|
186
190
|
zoomPerClick: 2.0,
|
|
191
|
+
// Lower threshold prevents blanking on sparse/level-0 pyramids
|
|
192
|
+
// when zooming far out. Consumers can override via config.
|
|
193
|
+
minPixelRatio: 0.1,
|
|
187
194
|
// Consumer-provided OSD overrides
|
|
188
195
|
...(viewerState.config?.openSeadragonConfig ?? {}),
|
|
189
196
|
// Enable double-click to zoom, but keep clickToZoom disabled for Annotorious
|
|
@@ -193,6 +200,20 @@
|
|
|
193
200
|
},
|
|
194
201
|
});
|
|
195
202
|
|
|
203
|
+
viewer.addHandler('open', () => {
|
|
204
|
+
const overrides = viewerState.config?.openSeadragonConfig ?? {};
|
|
205
|
+
if (overrides.minZoomLevel !== undefined) return;
|
|
206
|
+
|
|
207
|
+
// Prevent zooming into the "no tiles rendered" range seen on
|
|
208
|
+
// some level-0 services when zoomed far past home view.
|
|
209
|
+
const homeZoom = viewer.viewport.getHomeZoom();
|
|
210
|
+
viewer.viewport.minZoomLevel = homeZoom * 0.5;
|
|
211
|
+
|
|
212
|
+
if (overrides.minPixelRatio === undefined) {
|
|
213
|
+
viewer.minPixelRatio = 0.1;
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
|
|
196
217
|
// Notify plugins that OSD is ready
|
|
197
218
|
viewerState.notifyOSDReady(viewer);
|
|
198
219
|
})();
|
|
@@ -239,6 +260,43 @@
|
|
|
239
260
|
}
|
|
240
261
|
});
|
|
241
262
|
|
|
263
|
+
function normalizeIiifLevel0Profile<T>(source: T): T {
|
|
264
|
+
if (!source || typeof source !== 'object') return source;
|
|
265
|
+
|
|
266
|
+
const obj = source as any;
|
|
267
|
+
const profile = obj.profile;
|
|
268
|
+
|
|
269
|
+
if (typeof profile === 'string' && profile === IIIF_LEVEL0_V2_HTTPS) {
|
|
270
|
+
obj.profile = IIIF_LEVEL0_V2_HTTP;
|
|
271
|
+
} else if (Array.isArray(profile) && profile.length > 0) {
|
|
272
|
+
const first = profile[0];
|
|
273
|
+
if (typeof first === 'string' && first === IIIF_LEVEL0_V2_HTTPS) {
|
|
274
|
+
obj.profile = [IIIF_LEVEL0_V2_HTTP, ...profile.slice(1)];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return source;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function patchIiifLevel0ProfileCompatibility(osd: any) {
|
|
282
|
+
if (
|
|
283
|
+
!osd?.IIIFTileSource?.prototype ||
|
|
284
|
+
osd.__triiiceratopsIiifLevel0Patched
|
|
285
|
+
)
|
|
286
|
+
return;
|
|
287
|
+
|
|
288
|
+
const proto = osd.IIIFTileSource.prototype;
|
|
289
|
+
const originalConfigure = proto.configure;
|
|
290
|
+
if (typeof originalConfigure !== 'function') return;
|
|
291
|
+
|
|
292
|
+
proto.configure = function (data: any, url: string, postData: any) {
|
|
293
|
+
const configured = originalConfigure.call(this, data, url, postData);
|
|
294
|
+
return normalizeIiifLevel0Profile(configured);
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
osd.__triiiceratopsIiifLevel0Patched = true;
|
|
298
|
+
}
|
|
299
|
+
|
|
242
300
|
// Pre-fetch info.json URLs to detect 401 auth errors before passing to OSD
|
|
243
301
|
async function resolveTileSources(
|
|
244
302
|
sources: any[],
|
|
@@ -248,18 +306,25 @@
|
|
|
248
306
|
> {
|
|
249
307
|
const resolved = await Promise.all(
|
|
250
308
|
sources.map(async (source) => {
|
|
251
|
-
// Only
|
|
252
|
-
|
|
309
|
+
// Only probe string sources (info.json URLs); preserve string tile
|
|
310
|
+
// sources so OSD follows its normal source-loading path.
|
|
311
|
+
if (typeof source !== 'string')
|
|
312
|
+
return normalizeIiifLevel0Profile(source);
|
|
253
313
|
try {
|
|
254
314
|
const response = await fetch(source);
|
|
255
315
|
if (response.status === 401) {
|
|
256
316
|
return { __authError: true };
|
|
257
317
|
}
|
|
258
|
-
if (
|
|
259
|
-
//
|
|
260
|
-
|
|
318
|
+
if (response.ok) {
|
|
319
|
+
// Keep probing for auth, but preserve source URL so OSD
|
|
320
|
+
// continues using IIIF tile source behavior.
|
|
321
|
+
try {
|
|
322
|
+
normalizeIiifLevel0Profile(await response.json());
|
|
323
|
+
} catch {
|
|
324
|
+
// Not JSON or malformed response; let OSD handle source URL.
|
|
325
|
+
}
|
|
261
326
|
}
|
|
262
|
-
return
|
|
327
|
+
return source;
|
|
263
328
|
} catch {
|
|
264
329
|
// Network errors: pass through and let OSD handle it
|
|
265
330
|
return source;
|
|
@@ -309,107 +374,178 @@
|
|
|
309
374
|
|
|
310
375
|
// Capture stateKey for staleness guard
|
|
311
376
|
const capturedKey = stateKey;
|
|
377
|
+
const overrides = viewerState.config?.openSeadragonConfig ?? {};
|
|
312
378
|
|
|
313
379
|
if (mode === 'continuous') {
|
|
314
|
-
|
|
380
|
+
// Continuous mode can include mixed-size canvases; keep tile culling
|
|
381
|
+
// thresholds low so smaller images don't disappear before full-strip view.
|
|
382
|
+
if (overrides.minPixelRatio === undefined) {
|
|
383
|
+
viewer.minPixelRatio = 0.01;
|
|
384
|
+
}
|
|
385
|
+
if (overrides.minZoomImageRatio === undefined) {
|
|
386
|
+
viewer.minZoomImageRatio = 0.1;
|
|
387
|
+
}
|
|
315
388
|
|
|
316
|
-
|
|
389
|
+
resolveTileSources(sources).then((result) => {
|
|
390
|
+
// Staleness guard: if tile sources changed while we were fetching, discard
|
|
391
|
+
if (capturedKey !== lastTileSourceStr) return;
|
|
317
392
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (isVertical) {
|
|
323
|
-
const yPos = index * (1 + gap);
|
|
324
|
-
y = isBTT ? -yPos : yPos;
|
|
325
|
-
} else {
|
|
326
|
-
const xPos = index * (1 + gap);
|
|
327
|
-
x = isRTL ? -xPos : xPos;
|
|
393
|
+
if (!result.ok) {
|
|
394
|
+
viewerState.tileSourceError = result.error;
|
|
395
|
+
viewer.close();
|
|
396
|
+
return;
|
|
328
397
|
}
|
|
329
|
-
return { tileSource: source, x, y, width: 1.0 };
|
|
330
|
-
});
|
|
331
398
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
const INITIAL_WINDOW = 3; // canvases on each side of current
|
|
335
|
-
const currentIndex = viewerState.currentCanvasIndex;
|
|
336
|
-
const startIdx = Math.max(0, currentIndex - INITIAL_WINDOW);
|
|
337
|
-
const endIdx = Math.min(
|
|
338
|
-
allPositions.length,
|
|
339
|
-
currentIndex + INITIAL_WINDOW + 1,
|
|
340
|
-
);
|
|
399
|
+
viewerState.tileSourceError = null;
|
|
400
|
+
const resolvedSources = result.resolved;
|
|
341
401
|
|
|
342
|
-
|
|
343
|
-
viewer.open(initialSpread);
|
|
402
|
+
const gap = 0.025;
|
|
344
403
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
404
|
+
// Build position info for all sources
|
|
405
|
+
const allPositions = resolvedSources.map((source, index) => {
|
|
406
|
+
let x = 0;
|
|
407
|
+
let y = 0;
|
|
408
|
+
if (isVertical) {
|
|
409
|
+
const yPos = index * (1 + gap);
|
|
410
|
+
y = isBTT ? -yPos : yPos;
|
|
411
|
+
} else {
|
|
412
|
+
const xPos = index * (1 + gap);
|
|
413
|
+
x = isRTL ? -xPos : xPos;
|
|
414
|
+
}
|
|
415
|
+
return { tileSource: source, x, y, width: 1.0 };
|
|
416
|
+
});
|
|
352
417
|
|
|
353
|
-
//
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const batch = remaining.slice(
|
|
374
|
-
batchIdx * BATCH_SIZE,
|
|
375
|
-
(batchIdx + 1) * BATCH_SIZE,
|
|
376
|
-
);
|
|
377
|
-
if (batch.length === 0) {
|
|
378
|
-
// All loaded — now set zoom constraints based on full world bounds
|
|
379
|
-
const worldBounds = viewer.world.getHomeBounds();
|
|
380
|
-
const viewportBounds = viewer.viewport.getBounds();
|
|
381
|
-
const minZoom =
|
|
382
|
-
(Math.min(
|
|
383
|
-
viewportBounds.width / worldBounds.width,
|
|
384
|
-
viewportBounds.height / worldBounds.height,
|
|
385
|
-
) *
|
|
386
|
-
viewer.viewport.getZoom()) *
|
|
387
|
-
0.8;
|
|
388
|
-
viewer.viewport.minZoomLevel = minZoom;
|
|
389
|
-
viewer.viewport.visibilityRatio = 1;
|
|
390
|
-
return;
|
|
418
|
+
// Only open a window of canvases around the active one for fast initial load.
|
|
419
|
+
// The rest are added progressively after the viewer opens.
|
|
420
|
+
const INITIAL_WINDOW = 3; // canvases on each side of current
|
|
421
|
+
const currentIndex = viewerState.currentCanvasIndex;
|
|
422
|
+
const startIdx = Math.max(0, currentIndex - INITIAL_WINDOW);
|
|
423
|
+
const endIdx = Math.min(
|
|
424
|
+
allPositions.length,
|
|
425
|
+
currentIndex + INITIAL_WINDOW + 1,
|
|
426
|
+
);
|
|
427
|
+
|
|
428
|
+
const initialSpread = allPositions.slice(startIdx, endIdx);
|
|
429
|
+
viewer.open(initialSpread);
|
|
430
|
+
|
|
431
|
+
viewer.addOnceHandler('open', () => {
|
|
432
|
+
// Zoom to the active canvas
|
|
433
|
+
const itemIdx = currentIndex - startIdx;
|
|
434
|
+
const item = viewer.world.getItemAt(itemIdx);
|
|
435
|
+
if (item) {
|
|
436
|
+
viewer.viewport.fitBounds(item.getBounds(), true);
|
|
391
437
|
}
|
|
392
438
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
439
|
+
// Progressively add remaining canvases in batches
|
|
440
|
+
const remaining = [
|
|
441
|
+
...allPositions
|
|
442
|
+
.slice(0, startIdx)
|
|
443
|
+
.map((pos, i) => ({ ...pos, originalIndex: i })),
|
|
444
|
+
...allPositions
|
|
445
|
+
.slice(endIdx)
|
|
446
|
+
.map((pos, i) => ({
|
|
447
|
+
...pos,
|
|
448
|
+
originalIndex: endIdx + i,
|
|
449
|
+
})),
|
|
450
|
+
];
|
|
451
|
+
|
|
452
|
+
const BATCH_SIZE = 5;
|
|
453
|
+
let batchIdx = 0;
|
|
454
|
+
|
|
455
|
+
function addNextBatch() {
|
|
456
|
+
// Staleness guard
|
|
457
|
+
if (capturedKey !== lastTileSourceStr) return;
|
|
458
|
+
|
|
459
|
+
const batch = remaining.slice(
|
|
460
|
+
batchIdx * BATCH_SIZE,
|
|
461
|
+
(batchIdx + 1) * BATCH_SIZE,
|
|
462
|
+
);
|
|
463
|
+
if (batch.length === 0) {
|
|
464
|
+
// In continuous mode, allow a bit beyond home zoom so
|
|
465
|
+
// strips centered near one end can still include all canvases.
|
|
466
|
+
if (overrides.minZoomLevel === undefined) {
|
|
467
|
+
viewer.viewport.minZoomLevel =
|
|
468
|
+
viewer.viewport.getHomeZoom() + 0.1;
|
|
469
|
+
}
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
for (const pos of batch) {
|
|
474
|
+
viewer.addTiledImage({
|
|
475
|
+
tileSource: pos.tileSource,
|
|
476
|
+
x: pos.x,
|
|
477
|
+
y: pos.y,
|
|
478
|
+
width: pos.width,
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
batchIdx++;
|
|
482
|
+
setTimeout(addNextBatch, 100);
|
|
400
483
|
}
|
|
401
|
-
batchIdx++;
|
|
402
|
-
setTimeout(addNextBatch, 100);
|
|
403
|
-
}
|
|
404
484
|
|
|
405
|
-
|
|
406
|
-
|
|
485
|
+
// Start adding remaining canvases after a short delay
|
|
486
|
+
setTimeout(addNextBatch, 200);
|
|
487
|
+
});
|
|
407
488
|
});
|
|
408
489
|
|
|
409
490
|
return;
|
|
410
491
|
}
|
|
411
492
|
|
|
412
|
-
//
|
|
493
|
+
// In paged/individual modes, clear the current image immediately so
|
|
494
|
+
// users don't see stale content while tile sources are being prepared.
|
|
495
|
+
viewer.close();
|
|
496
|
+
viewerState.tileSourceError = null;
|
|
497
|
+
|
|
498
|
+
// Restore less aggressive defaults outside continuous mode unless user-overridden.
|
|
499
|
+
if (overrides.minPixelRatio === undefined) {
|
|
500
|
+
viewer.minPixelRatio = 0.1;
|
|
501
|
+
}
|
|
502
|
+
if (overrides.minZoomImageRatio === undefined) {
|
|
503
|
+
viewer.minZoomImageRatio = 0.9;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const immediateSources = sources.map((source) =>
|
|
507
|
+
typeof source === 'string'
|
|
508
|
+
? source
|
|
509
|
+
: normalizeIiifLevel0Profile(source),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
// Open immediately for perceived responsiveness.
|
|
513
|
+
if (mode === 'paged' && immediateSources.length === 2) {
|
|
514
|
+
const gap = 0.025;
|
|
515
|
+
const offset = 1 + gap;
|
|
516
|
+
|
|
517
|
+
// Two pages.
|
|
518
|
+
// If LTR: [0] at 0, [1] at 1.025
|
|
519
|
+
// If RTL: [0] at 1.025, [1] at 0
|
|
520
|
+
const firstX = isPagedRTL ? offset : 0;
|
|
521
|
+
const secondX = isPagedRTL ? 0 : offset;
|
|
522
|
+
|
|
523
|
+
const spread = [
|
|
524
|
+
{
|
|
525
|
+
tileSource: immediateSources[0],
|
|
526
|
+
x: firstX,
|
|
527
|
+
y: 0,
|
|
528
|
+
width: 1.0,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
tileSource: immediateSources[1],
|
|
532
|
+
x: secondX,
|
|
533
|
+
y: 0,
|
|
534
|
+
width: 1.0,
|
|
535
|
+
},
|
|
536
|
+
];
|
|
537
|
+
viewer.open(spread);
|
|
538
|
+
} else {
|
|
539
|
+
// Individuals or single paged or fallback
|
|
540
|
+
viewer.open(
|
|
541
|
+
immediateSources.length === 1
|
|
542
|
+
? immediateSources[0]
|
|
543
|
+
: immediateSources,
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Pre-fetch info.json URLs in the background to detect auth errors
|
|
548
|
+
// without delaying image display.
|
|
413
549
|
resolveTileSources(sources).then((result) => {
|
|
414
550
|
// Staleness guard: if tile sources changed while we were fetching, discard
|
|
415
551
|
if (capturedKey !== lastTileSourceStr) return;
|
|
@@ -423,42 +559,6 @@
|
|
|
423
559
|
|
|
424
560
|
// Clear any previous error
|
|
425
561
|
viewerState.tileSourceError = null;
|
|
426
|
-
|
|
427
|
-
const resolvedSources = result.resolved;
|
|
428
|
-
|
|
429
|
-
if (mode === 'paged' && resolvedSources.length === 2) {
|
|
430
|
-
const gap = 0.025;
|
|
431
|
-
const offset = 1 + gap;
|
|
432
|
-
|
|
433
|
-
// Two pages.
|
|
434
|
-
// If LTR: [0] at 0, [1] at 1.025
|
|
435
|
-
// If RTL: [0] at 1.025, [1] at 0
|
|
436
|
-
const firstX = isPagedRTL ? offset : 0;
|
|
437
|
-
const secondX = isPagedRTL ? 0 : offset;
|
|
438
|
-
|
|
439
|
-
const spread = [
|
|
440
|
-
{
|
|
441
|
-
tileSource: resolvedSources[0],
|
|
442
|
-
x: firstX,
|
|
443
|
-
y: 0,
|
|
444
|
-
width: 1.0,
|
|
445
|
-
},
|
|
446
|
-
{
|
|
447
|
-
tileSource: resolvedSources[1],
|
|
448
|
-
x: secondX,
|
|
449
|
-
y: 0,
|
|
450
|
-
width: 1.0,
|
|
451
|
-
},
|
|
452
|
-
];
|
|
453
|
-
viewer.open(spread);
|
|
454
|
-
} else {
|
|
455
|
-
// Individuals or single paged or fallback
|
|
456
|
-
viewer.open(
|
|
457
|
-
resolvedSources.length === 1
|
|
458
|
-
? resolvedSources[0]
|
|
459
|
-
: resolvedSources,
|
|
460
|
-
);
|
|
461
|
-
}
|
|
462
562
|
});
|
|
463
563
|
});
|
|
464
564
|
|
|
@@ -610,10 +610,9 @@
|
|
|
610
610
|
{manifestData.error}
|
|
611
611
|
</div>
|
|
612
612
|
{:else if tileSources}
|
|
613
|
-
<OSDViewer {tileSources} viewerState={internalViewerState} />
|
|
614
613
|
{#if internalViewerState.tileSourceError}
|
|
615
614
|
<div
|
|
616
|
-
class="absolute inset-0 z-[5] flex items-center justify-center pointer-events-none overflow-hidden"
|
|
615
|
+
class="w-full h-full absolute inset-0 z-[5] flex items-center justify-center pointer-events-none overflow-hidden"
|
|
617
616
|
role="alert"
|
|
618
617
|
>
|
|
619
618
|
{#if currentCanvasThumbnail}
|
|
@@ -643,6 +642,8 @@
|
|
|
643
642
|
</p>
|
|
644
643
|
</div>
|
|
645
644
|
</div>
|
|
645
|
+
{:else}
|
|
646
|
+
<OSDViewer {tileSources} viewerState={internalViewerState} />
|
|
646
647
|
{/if}
|
|
647
648
|
{:else if manifestData && !manifestData.isFetching && !tileSources}
|
|
648
649
|
<div
|