• 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

97.09
/src/transform.js
1
const Sharp = require('sharp');
5✔
2
const debug = require('debug')('iiif-processor:transform');
5✔
3
const IIIFVersions = require('./versions');
5✔
4

5
const ExtractAttributes = [
5✔
6
  'topOffsetPre',
7
  'leftOffsetPre',
8
  'widthPre',
9
  'heightPre'
10
];
11

12
const SCALE_PRECISION = 10000000;
5✔
13

14
class Operations {
15
  #pages;
16
  #pipeline;
17

18
  constructor (version, dims, opts) {
19
    const { sharp, ...rest } = opts;
83✔
20
    const Implementation = IIIFVersions[version];
83✔
21
    this.calculator = new Implementation.Calculator(dims[0], rest);
83✔
22

23
    this.#pages = dims
83✔
24
      .map((dim, page) => {
25
        return { ...dim, page };
278✔
26
      })
27
      .sort((a, b) => (b.width * b.height) - (a.width * a.height));
195✔
28
    this.#pipeline = Sharp({ limitInputPixels: false, ...sharp });
83✔
29
  }
30

31
  region (v) {
32
    this.calculator.region(v);
83✔
33
    const { region } = this.info();
81✔
34
    this.#pipeline = this.#pipeline.extract(region);
81✔
35

36
    const ifPositive = (a, b) => (a > 0 ? a : b);
162!
37
    this.calculator.dims.width = ifPositive(
81✔
38
      this.#pipeline.options.widthPre,
39
      this.calculator.dims.width
40
    );
41
    this.calculator.dims.height = ifPositive(
81✔
42
      this.#pipeline.options.heightPre,
43
      this.calculator.dims.height
44
    );
45

46
    return this;
81✔
47
  }
48

49
  size (v) {
50
    this.calculator.size(v);
81✔
51
    const { size } = this.info();
79✔
52
    this.#pipeline = this.#pipeline.resize(size);
79✔
53
    return this;
79✔
54
  }
55

56
  rotation (v) {
57
    this.calculator.rotation(v);
79✔
58
    const { flop, degree } = this.info().rotation;
79✔
59
    if (flop) {
79✔
60
      this.#pipeline = this.#pipeline.flop();
2✔
61
    }
62
    this.#pipeline = this.#pipeline.rotate(degree);
79✔
63
    return this;
79✔
64
  }
65

66
  quality (v) {
67
    this.calculator.quality(v);
79✔
68
    const { quality } = this.info();
79✔
69
    if (quality === 'color' || quality === 'default') {
79✔
70
      // do nothing
71
    } else if (quality === 'gray') {
4✔
72
      this.#pipeline = this.#pipeline.grayscale();
2✔
73
    } else if (quality === 'bitonal') {
2!
74
      this.#pipeline = this.#pipeline.threshold();
2✔
75
    }
76
    return this;
79✔
77
  }
78

79
  format (v, density) {
80
    this.calculator.format(v, density);
79✔
81

82
    const { format } = this.info();
79✔
83

84
    let pipelineFormat;
85
    const pipelineOptions = {};
79✔
86

87
    switch (format.type) {
79✔
88
      case 'jpeg':
89
        pipelineFormat = 'jpg';
2✔
90
        break;
2✔
91
      case 'tif':
92
        pipelineFormat = 'tiff';
10✔
93
        if (format.density) {
10✔
94
          pipelineOptions.xres = format.density / 25.4;
2✔
95
          pipelineOptions.yres = format.density / 25.4;
2✔
96
        }
97
        break;
10✔
98
      default:
99
        pipelineFormat = format.type;
67✔
100
    }
101
    this.#pipeline = this.#pipeline.toFormat(pipelineFormat, pipelineOptions);
79✔
102
    if (format.density) {
79✔
103
      this.#pipeline = this.#pipeline.withMetadata({ density: format.density });
6✔
104
    }
105
    return this;
79✔
106
  }
107

108
  info () {
109
    return this.calculator.info();
476✔
110
  }
111

112
  canonicalPath () {
113
    return this.calculator.canonicalPath();
59✔
114
  }
115

116

117
  #setPage () {
118
    if (this.#pipeline.options.input.page) return this;
79!
119

120
    const { fullSize } = this.info();
79✔
121
    const { page } = this.#pages.find((_candidate, index) => {
79✔
122
      const next = this.#pages[index + 1];
103✔
123
      debug('comparing candidate %j to target %j', next, fullSize);
103✔
124
      return !next || (next.width < fullSize.width && next.height < fullSize.height);
103✔
125
    });
126

127
    const resolution = this.#pages[page];
79✔
128
    debug('Using page %d (%j) as source', page, resolution);
79✔
129
    this.#pipeline.options.input.page = page;
79✔
130

131
    const newScale = Math.round(resolution.width / this.#pages[0].width * SCALE_PRECISION) / SCALE_PRECISION;
79✔
132
    for (const attr of ExtractAttributes) {
79✔
133
      if (this.#pipeline.options[attr] > 0) {
316✔
134
        const newValue = Math.round(this.#pipeline.options[attr] * newScale);
212✔
135
        debug('Scaling %s from %f to %f', attr, this.#pipeline.options[attr], newValue);
212✔
136
        this.#pipeline.options[attr] = newValue;
212✔
137
      }
138
    }
139

140
    return this;
79✔
141
  }
142

143
  withMetadata (v) {
144
    if (v) this.#pipeline = this.#pipeline.withMetadata();
79✔
145
    return this;
79✔
146
  }
147

148
  pipeline () {
149
    return this.#setPage().#pipeline;
79✔
150
  }
151
}
152

153
module.exports = { Operations };
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