@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 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
- service, reads that service’s `info.json` and mints a nested `representativeImage`.
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 service in its
87
- manifest, or a fetch that has not succeeded yet.
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 [@lde/iiif-image](https://ldelements.org/reference/iiif-image).
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 dimensions and a ladder
43
- * of precomputed sizes, resolved from the work’s IIIF Presentation manifest and
44
- * the selected image’s `info.json`.
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":"AAUA,OAAO,EAIL,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;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,MAAM,EAAE,YAAY,EACpB,OAAO,EAAE,iBAAiB,GACzB,UAAU,CAiHZ"}
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"}
@@ -1,13 +1,14 @@
1
- import { imageServices, readImageService, } from '@lde/iiif-image';
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 dimensions and a ladder
9
- * of precomputed sizes, resolved from the work’s IIIF Presentation manifest and
10
- * the selected image’s `info.json`.
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
- const [service] = imageServices(document);
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 images = createResolver({
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 serviceOfWork = new Map();
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
- serviceOfWork.set(work, resolution.fact);
93
+ imageOfWork.set(work, resolution.fact);
92
94
  }
93
95
  }
94
- const resolved = await images.resolveAll([
95
- ...serviceOfWork.values(),
96
- ]);
97
- for (const [work, service] of serviceOfWork) {
98
- const resolution = resolved.get(service);
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 images.check();
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
- const service = factory.blankNode();
282
- yield triple(image, aliases.image.service, service);
283
- yield triple(service, aliases.service.url, factory.namedNode(fact.url));
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 full dimensions, and the renditions a
39
- * consumer can pick from without knowing Image API compliance levels.
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;
@@ -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;;;GAGG;AACH,eAAO,MAAM,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA0BS,CAAC;AAEhC,8EAA8E;AAC9E,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;IAA+C,CAAC;AAE3E;kCACkC;AAClC,eAAO,MAAM,wBAAwB;;;;;;;;;CAML,CAAC"}
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 full dimensions, and the renditions a
38
- * consumer can pick from without knowing Image API compliance levels.
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.1",
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.1.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
- }
@@ -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"}
@@ -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
- }
@@ -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"}
@@ -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
- }