• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

samvera / node-iiif / 619f18b3-f505-46b3-95a3-4596a879db55

22 Nov 2024 06:17PM UTC coverage: 97.451% (-0.2%) from 97.683%
619f18b3-f505-46b3-95a3-4596a879db55

push

circleci

web-flow
Merge pull request #36 from samvera/30-flexible-path-template

Add more flexibility to `pathPrefix` constructor option

139 of 150 branches covered (92.67%)

Branch coverage included in aggregate %.

17 of 17 new or added lines in 1 file covered. (100.0%)

1 existing line in 1 file now uncovered.

358 of 360 relevant lines covered (99.44%)

109.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.2
/src/processor.js
1
const debug = require('debug')('iiif-processor:main');
5✔
2
const mime = require('mime-types');
5✔
3
const path = require('path');
5✔
4
const sharp = require('sharp');
5✔
5
const { Operations } = require('./transform');
5✔
6
const IIIFError = require('./error');
5✔
7
const IIIFVersions = require('./versions');
5✔
8

9
const defaultpathPrefix = '/iiif/{{version}}/';
5✔
10

11
function getIiifVersion (url, template) {
12
  const { origin, pathname } = new URL(url);
103✔
13
  const templateMatcher = template.replace(/\{\{version\}\}/, '(?<iiifVersion>2|3)');
103✔
14
  const pathMatcher = `^(?<prefix>${templateMatcher})(?<request>.+)$`;
103✔
15
  const re = new RegExp(pathMatcher);
103✔
16
  const parsed = re.exec(pathname);
103✔
17
  if (parsed) {
103✔
18
    parsed.groups.prefix = origin + parsed.groups.prefix;
101✔
19
    return { ...parsed.groups };
101✔
20
  } else {
21
    throw new IIIFError('Invalid IIIF path');
2✔
22
  }
23
};
24

25
class Processor {
26
  constructor (url, streamResolver, opts = {}) {
77✔
27
    const { prefix, iiifVersion, request } = getIiifVersion(url, opts.pathPrefix || defaultpathPrefix);
103✔
28

29
    if (typeof streamResolver !== 'function') {
101✔
30
      throw new IIIFError('streamResolver option must be specified');
2✔
31
    }
32

33
    if (opts.max?.height && !opts.max?.width) {
99✔
34
      throw new IIIFError('maxHeight cannot be specified without maxWidth');
1✔
35
    };
36

37
    const defaults = {
98✔
38
      dimensionFunction: this.defaultDimensionFunction,
39
      density: null
40
    };
41

42
    this
98✔
43
      .setOpts({ ...defaults, iiifVersion, ...opts, prefix, request })
44
      .initialize(streamResolver);
45
  }
46

47
  setOpts (opts) {
48
    this.errorClass = IIIFError;
98✔
49
    this.dimensionFunction = opts.dimensionFunction;
98✔
50
    this.max = { ...opts.max };
98✔
51
    this.includeMetadata = !!opts.includeMetadata;
98✔
52
    this.density = opts.density;
98✔
53
    this.baseUrl = opts.prefix;
98✔
54
    this.sharpOptions = { ...opts.sharpOptions };
98✔
55
    this.version = opts.iiifVersion;
98✔
56
    this.request = opts.request;
98✔
57

58
    return this;
98✔
59
  }
60

61
  initialize (streamResolver) {
62
    this.Implementation = IIIFVersions[this.version];
98✔
63
    if (!this.Implementation) {
98!
UNCOV
64
      throw new IIIFError(`No implementation found for IIIF Image API v${this.version}`);
×
65
    }
66

67
    const params = this.Implementation.Calculator.parsePath(this.request);
98✔
68
    debug('Parsed URL: %j', params);
96✔
69
    Object.assign(this, params);
96✔
70
    this.streamResolver = streamResolver;
96✔
71

72
    if (this.quality && this.format) {
96✔
73
      this.filename = [this.quality, this.format].join('.');
91✔
74
    } else if (this.info) {
5!
75
      this.filename = 'info.json';
5✔
76
    }
77
    return this;
96✔
78
  }
79

80
  async withStream ({ id, baseUrl }, callback) {
81
    debug('Requesting stream for %s', id);
132✔
82
    if (this.streamResolver.length === 2) {
132✔
83
      return await this.streamResolver({ id, baseUrl }, callback);
4✔
84
    } else {
85
      const stream = await this.streamResolver({ id, baseUrl });
128✔
86
      return await callback(stream);
128✔
87
    }
88
  }
89

90
  async defaultDimensionFunction ({ id, baseUrl }) {
91
    const result = [];
71✔
92
    let page = 0;
71✔
93
    const target = sharp({ limitInputPixels: false, page });
71✔
94

95
    return await this.withStream({ id, baseUrl }, async (stream) => {
71✔
96
      stream.pipe(target);
71✔
97
      const { width, height, pages } = await target.metadata();
71✔
98
      result.push({ width, height });
69✔
99
      for (page += 1; page < pages; page++) {
69✔
100
        const scale = 1 / 2 ** page;
207✔
101
        result.push({ width: Math.floor(width * scale), height: Math.floor(height * scale) });
207✔
102
      }
103
      return result;
69✔
104
    });
105
  }
106

107
  async dimensions () {
108
    const fallback = this.dimensionFunction !== this.defaultDimensionFunction;
73✔
109

110
    if (!this.sizeInfo) {
73!
111
      debug('Attempting to use dimensionFunction to retrieve dimensions for %j', this.id);
73✔
112
      const params = { id: this.id, baseUrl: this.baseUrl };
73✔
113
      let dims = await this.dimensionFunction(params);
73✔
114
      if (fallback && !dims) {
71✔
115
        const warning =
116
          'Unable to get dimensions for %s using custom function. Falling back to sharp.metadata().';
2✔
117
        debug(warning, this.id);
2✔
118
        console.warn(warning, this.id);
2✔
119
        dims = await this.defaultDimensionFunction(params);
2✔
120
      }
121
      if (!Array.isArray(dims)) dims = [dims];
71✔
122
      this.sizeInfo = dims;
71✔
123
    }
124
    return this.sizeInfo;
71✔
125
  }
126

127
  async infoJson () {
128
    const [dim] = await this.dimensions();
4✔
129
    const sizes = [];
4✔
130
    for (let size = [dim.width, dim.height]; size.every((x) => x >= 64); size = size.map((x) => Math.floor(x / 2))) {
32✔
131
      sizes.push({ width: size[0], height: size[1] });
12✔
132
    }
133

134
    const uri = new URL(this.baseUrl);
4✔
135
    uri.pathname = path.join(uri.pathname, this.id);
4✔
136
    const id = uri.toString();
4✔
137
    const doc = this.Implementation.infoDoc({ id, ...dim, sizes, max: this.max });
4✔
138
    for (const prop in doc) {
4✔
139
      if (doc[prop] === null || doc[prop] === undefined) delete doc[prop];
46✔
140
    }
141

142
    // Serialize sets as arrays
143
    const body = JSON.stringify(doc, (_key, value) =>
4✔
144
      value?.constructor === Set ? [...value] : value
215✔
145
    );
146
    return { contentType: 'application/json', body };
4✔
147
  }
148

149
  operations (dim) {
150
    const { sharpOptions: sharp, max } = this;
85✔
151
    return new Operations(this.version, dim, { sharp, max })
85✔
152
      .region(this.region)
153
      .size(this.size)
154
      .rotation(this.rotation)
155
      .quality(this.quality)
156
      .format(this.format, this.density)
157
      .withMetadata(this.includeMetadata);
158
  }
159

160
  async iiifImage () {
161
    const dim = await this.dimensions();
67✔
162
    const operations = this.operations(dim);
65✔
163
    const pipeline = await operations.pipeline();
61✔
164

165
    const result = await this.withStream({ id: this.id, baseUrl: this.baseUrl }, async (stream) => {
61✔
166
      debug('piping stream to pipeline');
61✔
167
      const transformed = await stream.pipe(pipeline);
61✔
168
      debug('converting to buffer');
61✔
169
      return await transformed.toBuffer();
61✔
170
    });
171
    debug('returning %d bytes', result.length);
59✔
172
    debug('baseUrl', this.baseUrl);
59✔
173

174
    const canonicalUrl = new URL(path.join(this.id, operations.canonicalPath()), this.baseUrl);
59✔
175
    return {
59✔
176
      canonicalLink: canonicalUrl.toString(),
177
      profileLink: this.Implementation.profileLink,
178
      contentType: mime.lookup(this.format),
179
      body: result
180
    };
181
  }
182

183
  async execute () {
184
    if (this.filename === 'info.json') {
71✔
185
      return await this.infoJson();
4✔
186
    } else {
187
      return await this.iiifImage();
67✔
188
    }
189
  }
190
}
191

192
module.exports = Processor;
5✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc