@ndes/iiif-images 0.0.1 → 0.1.0
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/README.md +16 -6
- package/dist/lib/iiif-images.d.ts +4 -3
- package/dist/lib/iiif-images.d.ts.map +1 -1
- package/dist/lib/iiif-images.js +67 -20
- package/dist/schema.d.ts +19 -2
- package/dist/schema.d.ts.map +1 -1
- package/dist/schema.js +10 -2
- package/package.json +7 -3
- package/dist/lib/image-service.d.ts +0 -26
- package/dist/lib/image-service.d.ts.map +0 -1
- package/dist/lib/image-service.js +0 -116
- package/dist/lib/manifest.d.ts +0 -14
- package/dist/lib/manifest.d.ts.map +0 -1
- package/dist/lib/manifest.js +0 -98
- package/dist/lib/public-url.d.ts +0 -11
- package/dist/lib/public-url.d.ts.map +0 -1
- package/dist/lib/public-url.js +0 -53
package/README.md
CHANGED
|
@@ -5,8 +5,9 @@ represents them, so a frontend can reserve the right aspect ratio and pick a siz
|
|
|
5
5
|
before the image loads.
|
|
6
6
|
|
|
7
7
|
It is a quad transform for `@lde/search-indexer`. For each work with a IIIF
|
|
8
|
-
Presentation manifest in `associatedMedia`, it reads the manifest, selects an image
|
|
9
|
-
|
|
8
|
+
Presentation manifest in `associatedMedia`, it reads the manifest, selects an image,
|
|
9
|
+
reads its image service’s `info.json` if it has one and mints a nested
|
|
10
|
+
`representativeImage`.
|
|
10
11
|
|
|
11
12
|
## Usage
|
|
12
13
|
|
|
@@ -62,6 +63,7 @@ again.
|
|
|
62
63
|
```json
|
|
63
64
|
{
|
|
64
65
|
"representativeImage": {
|
|
66
|
+
"url": "https://example.org/iiif/image/full/max/0/default.jpg",
|
|
65
67
|
"width": 1335,
|
|
66
68
|
"height": 2750,
|
|
67
69
|
"sizes": [
|
|
@@ -82,14 +84,22 @@ again.
|
|
|
82
84
|
sizes the server lists or its tiles precompute, so they are served fastest. Do not
|
|
83
85
|
rely on the array’s order.
|
|
84
86
|
- Use `service` only for deep zoom or region crops.
|
|
87
|
+
- `url` is the image as the manifest paints it, at full size.
|
|
88
|
+
- A static image, one without an image service, has only `url`, `width` and
|
|
89
|
+
`height`: no `sizes` and no `service`, because it is the only version there is
|
|
90
|
+
and cannot be resized.
|
|
85
91
|
- Without `representativeImage`, use `associatedMedia[].thumbnailUrl`, with no
|
|
86
|
-
dimensions. That is the case for a work with no manifest, no image
|
|
87
|
-
|
|
92
|
+
dimensions. That is the case for a work with no manifest, no image in its manifest
|
|
93
|
+
with an image service or a stated size, or a fetch that has not succeeded yet.
|
|
88
94
|
|
|
89
95
|
## Behaviour
|
|
90
96
|
|
|
91
|
-
- **Selection:** the first painted image in the manifest that has an image service
|
|
92
|
-
found by
|
|
97
|
+
- **Selection:** the first painted image in the manifest that has an image service
|
|
98
|
+
or whose width and height the manifest states, found by
|
|
99
|
+
[@lde/iiif-image](https://ldelements.org/reference/iiif-image). A static image’s
|
|
100
|
+
size comes from the manifest; an image service’s from its `info.json`. Where that
|
|
101
|
+
`info.json` is gone, the image counts as static, if the manifest states its size.
|
|
102
|
+
An image whose `id` is not an absolute `http(s)` URL is passed over.
|
|
93
103
|
- **Freshness:** manifest facts are refetched after 30 days, because which image
|
|
94
104
|
represents a work can change. `info.json` facts are refetched after 180 days: an
|
|
95
105
|
image’s dimensions do not change, but its server’s size URLs can.
|
|
@@ -39,9 +39,10 @@ export interface IiifImages {
|
|
|
39
39
|
check(): Promise<void>;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Enrich works with the image that represents them: its
|
|
43
|
-
*
|
|
44
|
-
* the selected image’s
|
|
42
|
+
* Enrich works with the image that represents them: its URL and dimensions, and
|
|
43
|
+
* for an image with an image service, a ladder of precomputed sizes, resolved
|
|
44
|
+
* from the work’s IIIF Presentation manifest and the selected image’s
|
|
45
|
+
* `info.json`. A static image is read from the manifest alone.
|
|
45
46
|
*
|
|
46
47
|
* A work with no manifest, or whose resolution has not succeeded, gets no
|
|
47
48
|
* `representativeImage`; consumers then fall back to the `thumbnailUrl` already
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"iiif-images.d.ts","sourceRoot":"","sources":["../../src/lib/iiif-images.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"iiif-images.d.ts","sourceRoot":"","sources":["../../src/lib/iiif-images.ts"],"names":[],"mappings":"AAWA,OAAO,EAKL,KAAK,iBAAiB,EAEvB,MAAM,eAAe,CAAC;AACvB,OAAO,KAAK,EACV,QAAQ,EAER,YAAY,EAEb,MAAM,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,IAAI,EAA6B,MAAM,cAAc,CAAC;AASpE,MAAM,WAAW,iBAAiB;IAChC;;;OAGG;IACH,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,4EAA4E;IAC5E,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACxD;AAED,kEAAkE;AAClE,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE;QAAE,QAAQ,CAAC,GAAG,EAAE,GAAG,CAAA;KAAE,CAAC;CACzC;AAED,gFAAgF;AAChF,MAAM,MAAM,mBAAmB,GAAG,CAChC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAC1B,OAAO,EAAE,iBAAiB,KACvB,aAAa,CAAC,IAAI,CAAC,CAAC;AAEzB,MAAM,WAAW,UAAU;IACzB;;;;;OAKG;IACH,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,mBAAmB,GAAG,SAAS,CAAC;IAClE,4EAA4E;IAC5E,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED;;;;;;;;;GASG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,iBAAiB,GACzB,UAAU,CAoHZ"}
|
package/dist/lib/iiif-images.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { images, readImageService, } from '@lde/iiif-image';
|
|
2
2
|
import { fetchPublic, RefusedUrlError, ResponseTooLargeError, } from '@lde/public-fetch';
|
|
3
3
|
import { createResolver, sqliteFactStore, urlHost, } from '@lde/resolver';
|
|
4
4
|
import { fieldNamed, irAlias, referenceTypeNamed } from '@lde/search/adapter';
|
|
5
5
|
import { DataFactory } from 'rdf-data-factory';
|
|
6
6
|
import { iiifImage, iiifImageService, iiifImageSize, representativeImageField, } from '../schema.js';
|
|
7
7
|
/**
|
|
8
|
-
* Enrich works with the image that represents them: its
|
|
9
|
-
*
|
|
10
|
-
* the selected image’s
|
|
8
|
+
* Enrich works with the image that represents them: its URL and dimensions, and
|
|
9
|
+
* for an image with an image service, a ladder of precomputed sizes, resolved
|
|
10
|
+
* from the work’s IIIF Presentation manifest and the selected image’s
|
|
11
|
+
* `info.json`. A static image is read from the manifest alone.
|
|
11
12
|
*
|
|
12
13
|
* A work with no manifest, or whose resolution has not succeeded, gets no
|
|
13
14
|
* `representativeImage`; consumers then fall back to the `thumbnailUrl` already
|
|
@@ -28,8 +29,7 @@ export function iiifImages(schema, options) {
|
|
|
28
29
|
host: (reference) => reference.dataset,
|
|
29
30
|
fetch: (references, { signal }) => fetchFacts(references.map(({ url }) => url), (url) => url, (document) => {
|
|
30
31
|
try {
|
|
31
|
-
|
|
32
|
-
return service;
|
|
32
|
+
return firstUsable(images(document));
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
35
|
// A document that says it is a collection, or a manifest too broken
|
|
@@ -46,10 +46,12 @@ export function iiifImages(schema, options) {
|
|
|
46
46
|
path: options.storePath,
|
|
47
47
|
table: 'manifests',
|
|
48
48
|
}),
|
|
49
|
+
// 2: the selected image rather than its service’s URL, and static images too.
|
|
50
|
+
version: '2',
|
|
49
51
|
limits: { ...limits, ttlMs: MANIFEST_TTL_MS },
|
|
50
52
|
report,
|
|
51
53
|
});
|
|
52
|
-
const
|
|
54
|
+
const services = createResolver({
|
|
53
55
|
key: (service) => service,
|
|
54
56
|
host: urlHost,
|
|
55
57
|
fetch: (services, { signal }) => fetchFacts(services, (service) => `${service}/info.json`, (info, service) => {
|
|
@@ -84,27 +86,25 @@ export function iiifImages(schema, options) {
|
|
|
84
86
|
const manifestOfWork = manifestsIn(batch, aliases);
|
|
85
87
|
const dataset = context.dataset.iri.toString();
|
|
86
88
|
const selected = await manifests.resolveAll([...manifestOfWork.values()].map((url) => ({ url, dataset })));
|
|
87
|
-
const
|
|
89
|
+
const imageOfWork = new Map();
|
|
88
90
|
for (const [work, manifest] of manifestOfWork) {
|
|
89
91
|
const resolution = selected.get(manifest);
|
|
90
92
|
if (resolution?.outcome === 'resolved') {
|
|
91
|
-
|
|
93
|
+
imageOfWork.set(work, resolution.fact);
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
|
-
const resolved = await
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (resolution?.outcome === 'resolved') {
|
|
100
|
-
yield* mint(factory.namedNode(work), resolution.fact, aliases);
|
|
96
|
+
const resolved = await services.resolveAll([...imageOfWork.values()].flatMap(({ service }) => service === undefined ? [] : [service]));
|
|
97
|
+
for (const [work, image] of imageOfWork) {
|
|
98
|
+
const representative = representativeOf(image, resolved);
|
|
99
|
+
if (representative !== undefined) {
|
|
100
|
+
yield* mint(factory.namedNode(work), representative, aliases);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
103
|
};
|
|
104
104
|
},
|
|
105
105
|
async check() {
|
|
106
106
|
await manifests.check();
|
|
107
|
-
await
|
|
107
|
+
await services.check();
|
|
108
108
|
},
|
|
109
109
|
};
|
|
110
110
|
}
|
|
@@ -128,6 +128,50 @@ const IIIF_TYPES = new Set([
|
|
|
128
128
|
]);
|
|
129
129
|
/** Answers meaning this document is not ours to have, now or on a retry. */
|
|
130
130
|
const ABSENT_STATUSES = new Set([401, 403, 404, 410]);
|
|
131
|
+
/**
|
|
132
|
+
* The first image that can represent the work: one with an image service, whose
|
|
133
|
+
* `info.json` states its dimensions, or a static image whose manifest states
|
|
134
|
+
* them. Without a size to state, a static image adds nothing to the
|
|
135
|
+
* `thumbnailUrl` consumers already have. Its URL is minted as an IRI, so an image
|
|
136
|
+
* whose `id` is not an absolute web address is passed over.
|
|
137
|
+
*/
|
|
138
|
+
function firstUsable(painted) {
|
|
139
|
+
for (const image of painted) {
|
|
140
|
+
const url = webAddress(image.url);
|
|
141
|
+
if (url !== undefined &&
|
|
142
|
+
(image.service !== undefined || staticOf(image) !== undefined)) {
|
|
143
|
+
return { ...image, url };
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The image to mint, or `undefined` while its image service is not resolved.
|
|
150
|
+
* An image service found to be gone leaves the size the manifest states, if any.
|
|
151
|
+
*/
|
|
152
|
+
function representativeOf(image, resolved) {
|
|
153
|
+
const resolution = image.service === undefined ? undefined : resolved.get(image.service);
|
|
154
|
+
if (resolution?.outcome === 'resolved') {
|
|
155
|
+
return { ...resolution.fact, url: image.url, service: resolution.fact.url };
|
|
156
|
+
}
|
|
157
|
+
return image.service === undefined || resolution?.outcome === 'absent'
|
|
158
|
+
? staticOf(image)
|
|
159
|
+
: undefined;
|
|
160
|
+
}
|
|
161
|
+
/** The image as the manifest states it, without sizes or a service. */
|
|
162
|
+
function staticOf(image) {
|
|
163
|
+
return image.width === undefined || image.height === undefined
|
|
164
|
+
? undefined
|
|
165
|
+
: { url: image.url, width: image.width, height: image.height, sizes: [] };
|
|
166
|
+
}
|
|
167
|
+
/** The URL, normalised, if it is an absolute `http(s)` one. */
|
|
168
|
+
function webAddress(url) {
|
|
169
|
+
if (!URL.canParse(url)) {
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
const { protocol, href } = new URL(url);
|
|
173
|
+
return protocol === 'http:' || protocol === 'https:' ? href : undefined;
|
|
174
|
+
}
|
|
131
175
|
function aliasesOf(rootType, schema) {
|
|
132
176
|
const media = fieldNamed(rootType, 'associatedMedia');
|
|
133
177
|
const image = fieldNamed(rootType, representativeImageField.name);
|
|
@@ -269,6 +313,7 @@ function declaresIiifType(document) {
|
|
|
269
313
|
function* mint(work, fact, aliases) {
|
|
270
314
|
const image = factory.blankNode();
|
|
271
315
|
yield triple(work, aliases.representativeImage, image);
|
|
316
|
+
yield triple(image, aliases.image.url, factory.namedNode(fact.url));
|
|
272
317
|
yield triple(image, aliases.image.width, integer(fact.width));
|
|
273
318
|
yield triple(image, aliases.image.height, integer(fact.height));
|
|
274
319
|
for (const size of fact.sizes) {
|
|
@@ -278,9 +323,11 @@ function* mint(work, fact, aliases) {
|
|
|
278
323
|
yield triple(node, aliases.size.height, integer(size.height));
|
|
279
324
|
yield triple(node, aliases.size.url, factory.namedNode(size.url));
|
|
280
325
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
326
|
+
if (fact.service !== undefined) {
|
|
327
|
+
const service = factory.blankNode();
|
|
328
|
+
yield triple(image, aliases.image.service, service);
|
|
329
|
+
yield triple(service, aliases.service.url, factory.namedNode(fact.service));
|
|
330
|
+
}
|
|
284
331
|
}
|
|
285
332
|
function triple(subject, predicate, object) {
|
|
286
333
|
return factory.quad(subject, factory.namedNode(predicate), object);
|
package/dist/schema.d.ts
CHANGED
|
@@ -35,12 +35,21 @@ export declare const iiifImageService: {
|
|
|
35
35
|
}];
|
|
36
36
|
};
|
|
37
37
|
/**
|
|
38
|
-
* The image that represents a work: the
|
|
39
|
-
* consumer can pick from without knowing Image
|
|
38
|
+
* The image that represents a work: the image the manifest paints, its full
|
|
39
|
+
* dimensions, and the renditions a consumer can pick from without knowing Image
|
|
40
|
+
* API compliance levels.
|
|
40
41
|
*/
|
|
41
42
|
export declare const iiifImage: {
|
|
42
43
|
readonly name: "IiifImage";
|
|
43
44
|
readonly fields: readonly [{
|
|
45
|
+
readonly name: "url";
|
|
46
|
+
readonly kind: "reference";
|
|
47
|
+
readonly output: true;
|
|
48
|
+
readonly path: "<https://schema.org/contentUrl>";
|
|
49
|
+
readonly ref: {
|
|
50
|
+
readonly strategy: "idOnly";
|
|
51
|
+
};
|
|
52
|
+
}, {
|
|
44
53
|
readonly name: "width";
|
|
45
54
|
readonly kind: "integer";
|
|
46
55
|
readonly output: true;
|
|
@@ -107,6 +116,14 @@ export declare const iiifImageTypes: ({
|
|
|
107
116
|
} | {
|
|
108
117
|
readonly name: "IiifImage";
|
|
109
118
|
readonly fields: readonly [{
|
|
119
|
+
readonly name: "url";
|
|
120
|
+
readonly kind: "reference";
|
|
121
|
+
readonly output: true;
|
|
122
|
+
readonly path: "<https://schema.org/contentUrl>";
|
|
123
|
+
readonly ref: {
|
|
124
|
+
readonly strategy: "idOnly";
|
|
125
|
+
};
|
|
126
|
+
}, {
|
|
110
127
|
readonly name: "width";
|
|
111
128
|
readonly kind: "integer";
|
|
112
129
|
readonly output: true;
|
package/dist/schema.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAKA,6EAA6E;AAC7E,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;CAkBK,CAAC;AAEhC,oFAAoF;AACpF,eAAO,MAAM,gBAAgB;;;;;;;;;;;CAWE,CAAC;AAEhC
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAKA,6EAA6E;AAC7E,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;CAkBK,CAAC;AAEhC,oFAAoF;AACpF,eAAO,MAAM,gBAAgB;;;;;;;;;;;CAWE,CAAC;AAEhC;;;;GAIG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCS,CAAC;AAEhC,8EAA8E;AAC9E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA+C,CAAC;AAE3E;kCACkC;AAClC,eAAO,MAAM,wBAAwB;;;;;;;;;CAML,CAAC"}
|
package/dist/schema.js
CHANGED
|
@@ -34,12 +34,20 @@ export const iiifImageService = {
|
|
|
34
34
|
],
|
|
35
35
|
};
|
|
36
36
|
/**
|
|
37
|
-
* The image that represents a work: the
|
|
38
|
-
* consumer can pick from without knowing Image
|
|
37
|
+
* The image that represents a work: the image the manifest paints, its full
|
|
38
|
+
* dimensions, and the renditions a consumer can pick from without knowing Image
|
|
39
|
+
* API compliance levels.
|
|
39
40
|
*/
|
|
40
41
|
export const iiifImage = {
|
|
41
42
|
name: 'IiifImage',
|
|
42
43
|
fields: [
|
|
44
|
+
{
|
|
45
|
+
name: 'url',
|
|
46
|
+
kind: 'reference',
|
|
47
|
+
output: true,
|
|
48
|
+
path: `<${SCHEMA}contentUrl>`,
|
|
49
|
+
ref: { strategy: 'idOnly' },
|
|
50
|
+
},
|
|
43
51
|
{ name: 'width', kind: 'integer', output: true, path: `<${SCHEMA}width>` },
|
|
44
52
|
{
|
|
45
53
|
name: 'height',
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ndes/iiif-images",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Enriches SCHEMA-AP-NDE works with the dimensions and sizes of their representative IIIF image, as a quad transform for the search indexer.",
|
|
5
|
+
"repository": {
|
|
6
|
+
"url": "git+https://github.com/netwerk-digitaal-erfgoed/stack.git",
|
|
7
|
+
"directory": "packages/iiif-images"
|
|
8
|
+
},
|
|
5
9
|
"license": "MIT",
|
|
6
10
|
"type": "module",
|
|
7
11
|
"exports": {
|
|
@@ -27,7 +31,7 @@
|
|
|
27
31
|
"!**/*.tsbuildinfo"
|
|
28
32
|
],
|
|
29
33
|
"dependencies": {
|
|
30
|
-
"@lde/iiif-image": "^0.
|
|
34
|
+
"@lde/iiif-image": "^0.2.0",
|
|
31
35
|
"@lde/public-fetch": "^0.1.0",
|
|
32
36
|
"@lde/resolver": "^0.2.1",
|
|
33
37
|
"@rdfjs/types": "^2.0.1",
|
|
@@ -50,4 +54,4 @@
|
|
|
50
54
|
"nx": {
|
|
51
55
|
"name": "iiif-images"
|
|
52
56
|
}
|
|
53
|
-
}
|
|
57
|
+
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/** What an image service’s `info.json` tells a consumer, in the consumer’s terms. */
|
|
2
|
-
export interface ImageFact {
|
|
3
|
-
readonly width: number;
|
|
4
|
-
readonly height: number;
|
|
5
|
-
/** Renditions to pick from, smallest first. */
|
|
6
|
-
readonly sizes: readonly ImageSize[];
|
|
7
|
-
/** The service itself, for consumers doing deep zoom or region crops. */
|
|
8
|
-
readonly service: {
|
|
9
|
-
readonly url: string;
|
|
10
|
-
readonly type: string;
|
|
11
|
-
readonly profile: string;
|
|
12
|
-
};
|
|
13
|
-
}
|
|
14
|
-
export interface ImageSize {
|
|
15
|
-
readonly width: number;
|
|
16
|
-
readonly height: number;
|
|
17
|
-
readonly url: string;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* An `info.json` as an {@link ImageFact}, or `undefined` where it states no
|
|
21
|
-
* dimensions. `serviceUrl` rather than the document’s own `id` names the service,
|
|
22
|
-
* because that is what the manifest resolved and what a proxied server’s
|
|
23
|
-
* self-reported `id` need not be.
|
|
24
|
-
*/
|
|
25
|
-
export declare function imageFactFrom(serviceUrl: string, info: unknown): ImageFact | undefined;
|
|
26
|
-
//# sourceMappingURL=image-service.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"image-service.d.ts","sourceRoot":"","sources":["../../src/lib/image-service.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,SAAS,SAAS,EAAE,CAAC;IACrC,yEAAyE;IACzE,QAAQ,CAAC,OAAO,EAAE;QAChB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QACtB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;KAC1B,CAAC;CACH;AAED,MAAM,WAAW,SAAS;IACxB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;CACtB;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,OAAO,GACZ,SAAS,GAAG,SAAS,CA4BvB"}
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* An `info.json` as an {@link ImageFact}, or `undefined` where it states no
|
|
3
|
-
* dimensions. `serviceUrl` rather than the document’s own `id` names the service,
|
|
4
|
-
* because that is what the manifest resolved and what a proxied server’s
|
|
5
|
-
* self-reported `id` need not be.
|
|
6
|
-
*/
|
|
7
|
-
export function imageFactFrom(serviceUrl, info) {
|
|
8
|
-
if (typeof info !== 'object' || info === null) {
|
|
9
|
-
return undefined;
|
|
10
|
-
}
|
|
11
|
-
const document = info;
|
|
12
|
-
const { width, height } = document;
|
|
13
|
-
if (!isPositive(width) || !isPositive(height)) {
|
|
14
|
-
return undefined;
|
|
15
|
-
}
|
|
16
|
-
const type = serviceTypeOf(document);
|
|
17
|
-
const profile = profileOf(document);
|
|
18
|
-
const isVersion3 = type === 'ImageService3';
|
|
19
|
-
// Image API 1.1 names its qualities differently (`native`, not `default`) and has
|
|
20
|
-
// no `sizes`, and is rare enough that its dimensions alone will do.
|
|
21
|
-
const sizes = (type === 'ImageService1' ? [] : sizesOf(document, width, height, profile))
|
|
22
|
-
.sort((left, right) => left.width - right.width)
|
|
23
|
-
.map((size) => ({
|
|
24
|
-
...size,
|
|
25
|
-
url: `${serviceUrl}/full/${isVersion3 ? `${size.width},${size.height}` : `${size.width},`}/0/default.jpg`,
|
|
26
|
-
}));
|
|
27
|
-
return {
|
|
28
|
-
width,
|
|
29
|
-
height,
|
|
30
|
-
sizes,
|
|
31
|
-
service: { url: serviceUrl, type, profile },
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/** Rungs below this width are too small to be worth a request. */
|
|
35
|
-
const MINIMUM_SYNTHESISED_WIDTH = 64;
|
|
36
|
-
function sizesOf(document, width, height, profile) {
|
|
37
|
-
const declared = Array.isArray(document.sizes)
|
|
38
|
-
? document.sizes.filter((size) => isPositive(size?.width) && isPositive(size?.height))
|
|
39
|
-
: [];
|
|
40
|
-
if (declared.length > 0) {
|
|
41
|
-
return declared.map((size) => ({ width: size.width, height: size.height }));
|
|
42
|
-
}
|
|
43
|
-
// A level-0 server only serves what it lists, so nothing can be asked of it.
|
|
44
|
-
if (/level0/.test(profile)) {
|
|
45
|
-
return [];
|
|
46
|
-
}
|
|
47
|
-
const rungs = new Map();
|
|
48
|
-
for (const scaleFactor of scaleFactorsOf(document, width)) {
|
|
49
|
-
const rung = {
|
|
50
|
-
width: Math.round(width / scaleFactor),
|
|
51
|
-
height: Math.round(height / scaleFactor),
|
|
52
|
-
};
|
|
53
|
-
if (rung.width >= MINIMUM_SYNTHESISED_WIDTH &&
|
|
54
|
-
rung.height >= 1 &&
|
|
55
|
-
isWithinMaximums(rung, document)) {
|
|
56
|
-
rungs.set(rung.width, rung);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return [...rungs.values()];
|
|
60
|
-
}
|
|
61
|
-
/** The server’s own precomputed factors where it has tiles, else powers of two. */
|
|
62
|
-
function scaleFactorsOf(document, width) {
|
|
63
|
-
const tiles = Array.isArray(document.tiles) ? document.tiles : [];
|
|
64
|
-
const declared = tiles
|
|
65
|
-
.flatMap((tile) => Array.isArray(tile?.scaleFactors) ? tile.scaleFactors : [])
|
|
66
|
-
.filter(isPositive);
|
|
67
|
-
if (declared.length > 0) {
|
|
68
|
-
return declared;
|
|
69
|
-
}
|
|
70
|
-
const factors = [];
|
|
71
|
-
for (let factor = 1; width / factor >= MINIMUM_SYNTHESISED_WIDTH; factor *= 2) {
|
|
72
|
-
factors.push(factor);
|
|
73
|
-
}
|
|
74
|
-
return factors;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Asking above a declared maximum answers 404, not 400 (Image API 3.0 § 7.3),
|
|
78
|
-
* which would read as a dead link. `maxWidth` alone also bounds the height
|
|
79
|
-
* (§ 5.3), so it is the fallback for a missing `maxHeight`.
|
|
80
|
-
*/
|
|
81
|
-
function isWithinMaximums(rung, document) {
|
|
82
|
-
const { maxWidth, maxHeight, maxArea } = document;
|
|
83
|
-
const heightLimit = isPositive(maxHeight) ? maxHeight : maxWidth;
|
|
84
|
-
return ((!isPositive(maxWidth) || rung.width <= maxWidth) &&
|
|
85
|
-
(!isPositive(heightLimit) || rung.height <= heightLimit) &&
|
|
86
|
-
(!isPositive(maxArea) || rung.width * rung.height <= maxArea));
|
|
87
|
-
}
|
|
88
|
-
function serviceTypeOf(document) {
|
|
89
|
-
// Only this family: Image API 2 documents commonly say `@type: iiif:Image`, which
|
|
90
|
-
// names what they describe and not the kind of service.
|
|
91
|
-
const type = document.type ?? document['@type'];
|
|
92
|
-
if (typeof type === 'string' && type.startsWith('ImageService')) {
|
|
93
|
-
return type;
|
|
94
|
-
}
|
|
95
|
-
const context = JSON.stringify(document['@context'] ?? '');
|
|
96
|
-
if (context.includes('image/3')) {
|
|
97
|
-
return 'ImageService3';
|
|
98
|
-
}
|
|
99
|
-
if (context.includes('image/2')) {
|
|
100
|
-
return 'ImageService2';
|
|
101
|
-
}
|
|
102
|
-
// A 1.0 document may state no context, so the profile has to answer as well.
|
|
103
|
-
return /image-api|api\/image\/1/.test(`${context} ${profileOf(document)}`)
|
|
104
|
-
? 'ImageService1'
|
|
105
|
-
: 'ImageService2';
|
|
106
|
-
}
|
|
107
|
-
/** The compliance level, which Image API 2 states first in an array. */
|
|
108
|
-
function profileOf(document) {
|
|
109
|
-
const profile = Array.isArray(document.profile)
|
|
110
|
-
? document.profile[0]
|
|
111
|
-
: document.profile;
|
|
112
|
-
return typeof profile === 'string' ? profile : '';
|
|
113
|
-
}
|
|
114
|
-
function isPositive(value) {
|
|
115
|
-
return Number.isInteger(value) && value > 0;
|
|
116
|
-
}
|
package/dist/lib/manifest.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The image service of a manifest’s first painted image that has one, in either
|
|
3
|
-
* Presentation version.
|
|
4
|
-
*
|
|
5
|
-
* A walk over the plain JSON rather than a vault: real manifests carry shapes a
|
|
6
|
-
* normalising parser rejects outright – Deventit Atlantis writes
|
|
7
|
-
* `"annotations": [null]` – and only this path matters here. Which image
|
|
8
|
-
* represents a work is a rule that lives in this one function, so a better one
|
|
9
|
-
* can replace it without touching what it feeds.
|
|
10
|
-
*/
|
|
11
|
-
export declare function selectImageService(manifest: unknown): string | undefined;
|
|
12
|
-
/** Whether a document has the canvases every manifest must have, in either version. */
|
|
13
|
-
export declare function listsCanvases(document: unknown): boolean;
|
|
14
|
-
//# sourceMappingURL=manifest.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/lib/manifest.ts"],"names":[],"mappings":"AAGA;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAcxE;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAKxD"}
|
package/dist/lib/manifest.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import { isImageService as isKnownImageService } from '@iiif/parser/image-3';
|
|
2
|
-
import { convertPresentation2 } from '@iiif/parser/presentation-2';
|
|
3
|
-
/**
|
|
4
|
-
* The image service of a manifest’s first painted image that has one, in either
|
|
5
|
-
* Presentation version.
|
|
6
|
-
*
|
|
7
|
-
* A walk over the plain JSON rather than a vault: real manifests carry shapes a
|
|
8
|
-
* normalising parser rejects outright – Deventit Atlantis writes
|
|
9
|
-
* `"annotations": [null]` – and only this path matters here. Which image
|
|
10
|
-
* represents a work is a rule that lives in this one function, so a better one
|
|
11
|
-
* can replace it without touching what it feeds.
|
|
12
|
-
*/
|
|
13
|
-
export function selectImageService(manifest) {
|
|
14
|
-
for (const canvas of arrayOf(fieldOf(upgraded(manifest), 'items'))) {
|
|
15
|
-
for (const page of arrayOf(fieldOf(canvas, 'items'))) {
|
|
16
|
-
for (const annotation of arrayOf(fieldOf(page, 'items'))) {
|
|
17
|
-
for (const body of paintedResources(fieldOf(annotation, 'body'))) {
|
|
18
|
-
const service = imageServiceOf(body);
|
|
19
|
-
if (service !== undefined) {
|
|
20
|
-
return service;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
/** Whether a document has the canvases every manifest must have, in either version. */
|
|
29
|
-
export function listsCanvases(document) {
|
|
30
|
-
return (arrayOf(fieldOf(document, 'items')).length > 0 ||
|
|
31
|
-
arrayOf(fieldOf(document, 'sequences')).length > 0);
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* The manifest in Presentation 3 terms. A Presentation 2 manifest the upgrade
|
|
35
|
-
* chokes on, such as one with `"images": [null]`, has nothing to find: retrying
|
|
36
|
-
* cannot help, and a run of them must not write off a dataset’s other manifests.
|
|
37
|
-
*/
|
|
38
|
-
function upgraded(manifest) {
|
|
39
|
-
if (fieldOf(manifest, 'sequences') === undefined) {
|
|
40
|
-
return manifest;
|
|
41
|
-
}
|
|
42
|
-
try {
|
|
43
|
-
return convertPresentation2(manifest);
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return undefined;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/** A body’s resources in order, with each `Choice` opened up and each
|
|
50
|
-
* `SpecificResource`, which is how a cropped painting is stated, unwrapped. */
|
|
51
|
-
function paintedResources(body) {
|
|
52
|
-
return arrayOf(body).flatMap((resource) => {
|
|
53
|
-
const type = fieldOf(resource, 'type');
|
|
54
|
-
if (type === 'Choice') {
|
|
55
|
-
return paintedResources(fieldOf(resource, 'items'));
|
|
56
|
-
}
|
|
57
|
-
return type === 'SpecificResource'
|
|
58
|
-
? paintedResources(fieldOf(resource, 'source'))
|
|
59
|
-
: [resource];
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
function imageServiceOf(resource) {
|
|
63
|
-
for (const service of arrayOf(fieldOf(resource, 'service'))) {
|
|
64
|
-
const id = fieldOf(service, 'id') ?? fieldOf(service, '@id');
|
|
65
|
-
if (typeof id === 'string' && isImageService(service)) {
|
|
66
|
-
return serviceBase(id);
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
/** The URL `/info.json` and sizes are appended to. Some manifests state the
|
|
72
|
-
* service with a trailing slash or as the `info.json` itself. */
|
|
73
|
-
function serviceBase(id) {
|
|
74
|
-
return id.replace(/\/info\.json$/, '').replace(/\/+$/, '');
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* The parser’s test, plus the two forms it misses: a `type` with no `profile`, and
|
|
78
|
-
* a profile merely naming a compliance level. Limb Gallery’s is the `https` form
|
|
79
|
-
* of the Stanford URL from Image API 1.1, which the parser’s list does not include.
|
|
80
|
-
*/
|
|
81
|
-
function isImageService(service) {
|
|
82
|
-
const type = fieldOf(service, 'type') ?? fieldOf(service, '@type');
|
|
83
|
-
const profile = fieldOf(service, 'profile');
|
|
84
|
-
return (isKnownImageService(service) ||
|
|
85
|
-
(typeof type === 'string' && type.startsWith('ImageService')) ||
|
|
86
|
-
(typeof profile === 'string' && /level[012]/.test(profile)));
|
|
87
|
-
}
|
|
88
|
-
function fieldOf(value, name) {
|
|
89
|
-
return typeof value === 'object' && value !== null
|
|
90
|
-
? value[name]
|
|
91
|
-
: undefined;
|
|
92
|
-
}
|
|
93
|
-
function arrayOf(value) {
|
|
94
|
-
if (value === undefined || value === null) {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
return Array.isArray(value) ? value : [value];
|
|
98
|
-
}
|
package/dist/lib/public-url.d.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether a URL taken from a publisher’s data may be fetched from inside the
|
|
3
|
-
* cluster: a web URL that does not name a private, loopback or link-local address
|
|
4
|
-
* or a cluster-internal host.
|
|
5
|
-
*
|
|
6
|
-
* This looks only at the URL, so it cannot see a public name resolving to a private
|
|
7
|
-
* address, or a redirect to one. It stops the plain case, and a cluster egress
|
|
8
|
-
* policy has to cover the rest.
|
|
9
|
-
*/
|
|
10
|
-
export declare function isPublicWebUrl(url: string): boolean;
|
|
11
|
-
//# sourceMappingURL=public-url.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"public-url.d.ts","sourceRoot":"","sources":["../../src/lib/public-url.ts"],"names":[],"mappings":"AAEA;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAoBnD"}
|
package/dist/lib/public-url.js
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { BlockList, isIP } from 'node:net';
|
|
2
|
-
/**
|
|
3
|
-
* Whether a URL taken from a publisher’s data may be fetched from inside the
|
|
4
|
-
* cluster: a web URL that does not name a private, loopback or link-local address
|
|
5
|
-
* or a cluster-internal host.
|
|
6
|
-
*
|
|
7
|
-
* This looks only at the URL, so it cannot see a public name resolving to a private
|
|
8
|
-
* address, or a redirect to one. It stops the plain case, and a cluster egress
|
|
9
|
-
* policy has to cover the rest.
|
|
10
|
-
*/
|
|
11
|
-
export function isPublicWebUrl(url) {
|
|
12
|
-
let parsed;
|
|
13
|
-
try {
|
|
14
|
-
parsed = new URL(url);
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
return false;
|
|
18
|
-
}
|
|
19
|
-
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
const host = parsed.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
23
|
-
const family = isIP(host);
|
|
24
|
-
if (family === 4) {
|
|
25
|
-
return !PRIVATE_ADDRESSES.check(host, 'ipv4');
|
|
26
|
-
}
|
|
27
|
-
if (family === 6) {
|
|
28
|
-
return !PRIVATE_ADDRESSES.check(host, 'ipv6');
|
|
29
|
-
}
|
|
30
|
-
// A single label is a Kubernetes service or a machine on the local network.
|
|
31
|
-
return host.includes('.') && !INTERNAL_SUFFIX.test(host);
|
|
32
|
-
}
|
|
33
|
-
const INTERNAL_SUFFIX = /\.(localhost|local|internal|svc)$/;
|
|
34
|
-
const PRIVATE_ADDRESSES = new BlockList();
|
|
35
|
-
for (const [network, prefix] of [
|
|
36
|
-
['0.0.0.0', 8],
|
|
37
|
-
['10.0.0.0', 8],
|
|
38
|
-
['100.64.0.0', 10],
|
|
39
|
-
['127.0.0.0', 8],
|
|
40
|
-
['169.254.0.0', 16],
|
|
41
|
-
['172.16.0.0', 12],
|
|
42
|
-
['192.168.0.0', 16],
|
|
43
|
-
]) {
|
|
44
|
-
PRIVATE_ADDRESSES.addSubnet(network, prefix, 'ipv4');
|
|
45
|
-
}
|
|
46
|
-
for (const [network, prefix] of [
|
|
47
|
-
['::', 128],
|
|
48
|
-
['::1', 128],
|
|
49
|
-
['fc00::', 7],
|
|
50
|
-
['fe80::', 10],
|
|
51
|
-
]) {
|
|
52
|
-
PRIVATE_ADDRESSES.addSubnet(network, prefix, 'ipv6');
|
|
53
|
-
}
|