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

samvera / node-iiif / 51847fda-a900-452b-a861-1f0740800a77

pending completion
51847fda-a900-452b-a861-1f0740800a77

Pull #29

circleci

mbklein
Implementation of IIIF Image API v3.0.0
Pull Request #29: Implementation of IIIF Image API v3.0.0

131 of 140 branches covered (93.57%)

Branch coverage included in aggregate %.

225 of 225 new or added lines in 11 files covered. (100.0%)

355 of 355 relevant lines covered (100.0%)

105.27 hits per line

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

98.47
/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 fixupSlashes = (path, leaveOne) => {
5✔
10
  const replacement = leaveOne ? '/' : '';
103✔
11
  return path?.replace(/^\/*/, replacement).replace(/\/*$/, replacement);
103✔
12
};
13

14
class Processor {
15
  constructor (version, url, streamResolver, opts = {}) {
73✔
16
    if (typeof streamResolver !== 'function') {
98✔
17
      throw new IIIFError('streamResolver option must be specified');
2✔
18
    }
19

20
    if (opts.max?.height && !opts.max?.width) {
96✔
21
      throw new IIIFError('maxHeight cannot be specified without maxWidth');
1✔
22
    };
23

24
    const defaults = {
95✔
25
      pathPrefix: `/iiif/${version}/`,
26
      dimensionFunction: this.defaultDimensionFunction,
27
      density: null
28
    };
29

30
    this
95✔
31
      .setOpts({ ...defaults, ...opts })
32
      .initialize(version, url, streamResolver);
33
  }
34

35
  setOpts (opts) {
36
    this.errorClass = IIIFError;
95✔
37
    this.dimensionFunction = opts.dimensionFunction;
95✔
38
    this.max = { ...opts.max };
95✔
39
    this.includeMetadata = !!opts.includeMetadata;
95✔
40
    this.density = opts.density;
95✔
41
    this.pathPrefix = fixupSlashes(opts.pathPrefix, true);
95✔
42
    this.sharpOptions = { ...opts.sharpOptions };
95✔
43

44
    return this;
95✔
45
  }
46

47
  parseUrl (url) {
48
    const parser = new RegExp(`(?<baseUrl>https?://[^/]+${this.pathPrefix})(?<path>.+)$`);
95✔
49
    const { baseUrl, path } = parser.exec(url).groups;
95✔
50
    const result = this.Implementation.Calculator.parsePath(path);
95✔
51
    result.baseUrl = baseUrl;
93✔
52

53
    return result;
93✔
54
  }
55

56
  initialize (version, url, streamResolver) {
57
    this.version = version;
95✔
58
    this.Implementation = IIIFVersions[this.version];
95✔
59

60
    const params = this.parseUrl(url);
95✔
61
    debug('Parsed URL: %j', params);
93✔
62
    Object.assign(this, params);
93✔
63
    this.streamResolver = streamResolver;
93✔
64

65
    if (this.quality && this.format) {
93✔
66
      this.filename = [this.quality, this.format].join('.');
89✔
67
    } else if (this.info) {
4!
68
      this.filename = 'info.json';
4✔
69
    }
70
    return this;
93✔
71
  }
72

73
  async withStream ({ id, baseUrl }, callback) {
74
    debug('Requesting stream for %s', id);
132✔
75
    if (this.streamResolver.length === 2) {
132✔
76
      return await this.streamResolver({ id, baseUrl }, callback);
4✔
77
    } else {
78
      const stream = await this.streamResolver({ id, baseUrl });
128✔
79
      return await callback(stream);
128✔
80
    }
81
  }
82

83
  async defaultDimensionFunction ({ id, baseUrl }) {
84
    const result = [];
71✔
85
    let page = 0;
71✔
86
    const target = sharp({ page });
71✔
87

88
    return await this.withStream({ id, baseUrl }, async (stream) => {
71✔
89
      stream.pipe(target);
71✔
90
      const { width, height, pages } = await target.metadata();
71✔
91
      result.push({ width, height });
69✔
92
      for (page += 1; page < pages; page++) {
69✔
93
        const scale = 1 / 2 ** page;
207✔
94
        result.push({ width: Math.floor(width * scale), height: Math.floor(height * scale) });
207✔
95
      }
96
      return result;
69✔
97
    });
98
  }
99

100
  async dimensions () {
101
    const fallback = this.dimensionFunction !== this.defaultDimensionFunction;
73✔
102

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

120
  async infoJson () {
121
    const [dim] = await this.dimensions();
4✔
122
    const sizes = [];
4✔
123
    for (let size = [dim.width, dim.height]; size.every((x) => x >= 64); size = size.map((x) => Math.floor(x / 2))) {
32✔
124
      sizes.push({ width: size[0], height: size[1] });
12✔
125
    }
126

127
    const id = [fixupSlashes(this.baseUrl), fixupSlashes(this.id)].join('/');
4✔
128
    const doc = this.Implementation.infoDoc({ id, ...dim, sizes, max: this.max });
4✔
129
    for (const prop in doc) {
4✔
130
      if (doc[prop] === null || doc[prop] === undefined) delete doc[prop];
46✔
131
    }
132

133
    // Serialize sets as arrays
134
    const body = JSON.stringify(doc, (_key, value) =>
4✔
135
      value?.constructor === Set ? [...value] : value
215✔
136
    );
137
    return { contentType: 'application/json', body };
4✔
138
  }
139

140
  operations (dim) {
141
    const { sharpOptions: sharp, max } = this;
83✔
142
    return new Operations(this.version, dim, { sharp, max })
83✔
143
      .region(this.region)
144
      .size(this.size)
145
      .rotation(this.rotation)
146
      .quality(this.quality)
147
      .format(this.format, this.density)
148
      .withMetadata(this.includeMetadata);
149
  }
150

151
  async iiifImage () {
152
    const dim = await this.dimensions();
67✔
153
    const operations = this.operations(dim);
65✔
154
    const pipeline = await operations.pipeline();
61✔
155

156
    const result = await this.withStream({ id: this.id, baseUrl: this.baseUrl }, async (stream) => {
61✔
157
      debug('piping stream to pipeline');
61✔
158
      const transformed = await stream.pipe(pipeline);
61✔
159
      debug('converting to buffer');
61✔
160
      return await transformed.toBuffer();
61✔
161
    });
162
    debug('returning %d bytes', result.length);
59✔
163
    debug('baseUrl', this.baseUrl);
59✔
164

165
    const canonicalUrl = new URL(path.join(this.id, operations.canonicalPath()), this.baseUrl);
59✔
166
    return {
59✔
167
      canonicalLink: canonicalUrl.toString(),
168
      profileLink: this.Implementation.profileLink,
169
      contentType: mime.lookup(this.format),
170
      body: result
171
    };
172
  }
173

174
  async execute () {
175
    if (this.filename === 'info.json') {
71✔
176
      return await this.infoJson();
4✔
177
    } else {
178
      return await this.iiifImage();
67✔
179
    }
180
  }
181
}
182

183
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

© 2025 Coveralls, Inc