orator-conversion 1.0.2 → 1.0.3

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.
@@ -8,6 +8,9 @@ const libOS = require('os');
8
8
 
9
9
  const libEndpointImageJpgToPng = require('./endpoints/Endpoint-Image-JpgToPng.js');
10
10
  const libEndpointImagePngToJpg = require('./endpoints/Endpoint-Image-PngToJpg.js');
11
+ const libEndpointImageResize = require('./endpoints/Endpoint-Image-Resize.js');
12
+ const libEndpointImageRotate = require('./endpoints/Endpoint-Image-Rotate.js');
13
+ const libEndpointImageConvert = require('./endpoints/Endpoint-Image-Convert.js');
11
14
  const libEndpointPdfPageToPng = require('./endpoints/Endpoint-Pdf-PageToPng.js');
12
15
  const libEndpointPdfPageToJpg = require('./endpoints/Endpoint-Pdf-PageToJpg.js');
13
16
  const libEndpointPdfPageToPngSized = require('./endpoints/Endpoint-Pdf-PageToPng-Sized.js');
@@ -89,6 +92,9 @@ class OratorFileTranslation extends libFableServiceProviderBase
89
92
  // Register endpoint service types with fable
90
93
  this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-ImageJpgToPng', libEndpointImageJpgToPng);
91
94
  this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-ImagePngToJpg', libEndpointImagePngToJpg);
95
+ this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-ImageResize', libEndpointImageResize);
96
+ this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-ImageRotate', libEndpointImageRotate);
97
+ this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-ImageConvert', libEndpointImageConvert);
92
98
  this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-PdfPageToPng', libEndpointPdfPageToPng);
93
99
  this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-PdfPageToJpg', libEndpointPdfPageToJpg);
94
100
  this.fable.addServiceTypeIfNotExists('OratorFileTranslationEndpoint-PdfPageToPngSized', libEndpointPdfPageToPngSized);
@@ -100,6 +106,9 @@ class OratorFileTranslation extends libFableServiceProviderBase
100
106
  [
101
107
  'OratorFileTranslationEndpoint-ImageJpgToPng',
102
108
  'OratorFileTranslationEndpoint-ImagePngToJpg',
109
+ 'OratorFileTranslationEndpoint-ImageResize',
110
+ 'OratorFileTranslationEndpoint-ImageRotate',
111
+ 'OratorFileTranslationEndpoint-ImageConvert',
103
112
  'OratorFileTranslationEndpoint-PdfPageToPng',
104
113
  'OratorFileTranslationEndpoint-PdfPageToJpg',
105
114
  'OratorFileTranslationEndpoint-PdfPageToPngSized',
@@ -0,0 +1,33 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ const libConversionCore = require('../Conversion-Core.js');
4
+
5
+ class EndpointImageConvert extends libFableServiceProviderBase
6
+ {
7
+ constructor(pFable, pOptions, pServiceHash)
8
+ {
9
+ super(pFable, pOptions, pServiceHash);
10
+
11
+ this.serviceType = 'OratorFileTranslationEndpoint-ImageConvert';
12
+
13
+ this.converterPath = 'image/convert/:Format';
14
+ }
15
+
16
+ convert(pInputBuffer, pRequest, fCallback)
17
+ {
18
+ let tmpParams = pRequest.params || {};
19
+ let tmpQuery = pRequest.query || {};
20
+
21
+ let tmpCore = new libConversionCore();
22
+
23
+ let tmpOptions =
24
+ {
25
+ Format: tmpParams.Format,
26
+ Quality: tmpQuery.Quality ? parseInt(tmpQuery.Quality, 10) : undefined
27
+ };
28
+
29
+ tmpCore.imageConvert(pInputBuffer, tmpOptions, fCallback);
30
+ }
31
+ }
32
+
33
+ module.exports = EndpointImageConvert;
@@ -0,0 +1,49 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+ const libURL = require('url');
3
+
4
+ const libConversionCore = require('../Conversion-Core.js');
5
+
6
+ class EndpointImageResize extends libFableServiceProviderBase
7
+ {
8
+ constructor(pFable, pOptions, pServiceHash)
9
+ {
10
+ super(pFable, pOptions, pServiceHash);
11
+
12
+ this.serviceType = 'OratorFileTranslationEndpoint-ImageResize';
13
+
14
+ this.converterPath = 'image/resize';
15
+ }
16
+
17
+ convert(pInputBuffer, pRequest, fCallback)
18
+ {
19
+ let tmpFileTranslation = this.options.FileTranslation;
20
+
21
+ let tmpCore = new libConversionCore({
22
+ MaxFileSize: tmpFileTranslation ? tmpFileTranslation.MaxFileSize : undefined
23
+ });
24
+
25
+ let tmpParams = pRequest.params || {};
26
+ // Parse query string from the URL if pRequest.query is not available
27
+ let tmpQuery = pRequest.query || {};
28
+ if (Object.keys(tmpQuery).length === 0 && pRequest.url)
29
+ {
30
+ let tmpParsed = libURL.parse(pRequest.url, true);
31
+ tmpQuery = tmpParsed.query || {};
32
+ }
33
+
34
+ let tmpOptions =
35
+ {
36
+ Width: tmpQuery.Width || tmpParams.Width,
37
+ Height: tmpQuery.Height || tmpParams.Height,
38
+ Format: tmpQuery.Format || tmpParams.Format,
39
+ Quality: tmpQuery.Quality || tmpParams.Quality,
40
+ Fit: tmpQuery.Fit || tmpParams.Fit,
41
+ Position: tmpQuery.Position || tmpParams.Position,
42
+ AutoOrient: tmpQuery.AutoOrient !== 'false'
43
+ };
44
+
45
+ tmpCore.imageResize(pInputBuffer, tmpOptions, fCallback);
46
+ }
47
+ }
48
+
49
+ module.exports = EndpointImageResize;
@@ -0,0 +1,34 @@
1
+ const libFableServiceProviderBase = require('fable-serviceproviderbase');
2
+
3
+ const libConversionCore = require('../Conversion-Core.js');
4
+
5
+ class EndpointImageRotate extends libFableServiceProviderBase
6
+ {
7
+ constructor(pFable, pOptions, pServiceHash)
8
+ {
9
+ super(pFable, pOptions, pServiceHash);
10
+
11
+ this.serviceType = 'OratorFileTranslationEndpoint-ImageRotate';
12
+
13
+ this.converterPath = 'image/rotate/:Angle';
14
+ }
15
+
16
+ convert(pInputBuffer, pRequest, fCallback)
17
+ {
18
+ let tmpCore = new libConversionCore();
19
+
20
+ let tmpParams = pRequest.params || {};
21
+ let tmpQuery = pRequest.query || {};
22
+
23
+ let tmpOptions =
24
+ {
25
+ Angle: tmpParams.Angle,
26
+ Flip: (tmpQuery.Flip === 'true' || tmpQuery.Flip === '1'),
27
+ Flop: (tmpQuery.Flop === 'true' || tmpQuery.Flop === '1')
28
+ };
29
+
30
+ tmpCore.imageRotate(pInputBuffer, tmpOptions, fCallback);
31
+ }
32
+ }
33
+
34
+ module.exports = EndpointImageRotate;
@@ -0,0 +1,442 @@
1
+ /**
2
+ * Unit tests for Conversion-Core — Image, Video, Audio operations
3
+ *
4
+ * @license MIT
5
+ *
6
+ * @author Steven Velozo <steven@velozo.com>
7
+ */
8
+
9
+ const Chai = require("chai");
10
+ const Expect = Chai.expect;
11
+
12
+ const libSharp = require('sharp');
13
+
14
+ const libConversionCore = require('../source/Conversion-Core.js');
15
+
16
+ /**
17
+ * Generate a minimal JPEG test image buffer (10x20 red).
18
+ */
19
+ function createTestJpegBuffer(fCallback)
20
+ {
21
+ libSharp(
22
+ {
23
+ create:
24
+ {
25
+ width: 10,
26
+ height: 20,
27
+ channels: 3,
28
+ background: { r: 255, g: 0, b: 0 }
29
+ }
30
+ }).jpeg().toBuffer().then(
31
+ (pBuffer) => { return fCallback(null, pBuffer); },
32
+ (pError) => { return fCallback(pError); });
33
+ }
34
+
35
+ /**
36
+ * Generate a minimal PNG test image buffer.
37
+ */
38
+ function createTestPngBuffer(fCallback)
39
+ {
40
+ libSharp(
41
+ {
42
+ create:
43
+ {
44
+ width: 10,
45
+ height: 20,
46
+ channels: 4,
47
+ background: { r: 0, g: 0, b: 255, alpha: 1 }
48
+ }
49
+ }).png().toBuffer().then(
50
+ (pBuffer) => { return fCallback(null, pBuffer); },
51
+ (pError) => { return fCallback(pError); });
52
+ }
53
+
54
+ suite
55
+ (
56
+ 'Conversion Core',
57
+ () =>
58
+ {
59
+ suite
60
+ (
61
+ 'Image Rotate',
62
+ () =>
63
+ {
64
+ test
65
+ (
66
+ 'should rotate an image 90 degrees and swap dimensions',
67
+ (fDone) =>
68
+ {
69
+ createTestJpegBuffer(
70
+ (pError, pJpegBuffer) =>
71
+ {
72
+ Expect(pError).to.equal(null);
73
+
74
+ let tmpCore = new libConversionCore();
75
+
76
+ tmpCore.imageRotate(pJpegBuffer,
77
+ { Angle: 90 },
78
+ (pRotateError, pOutputBuffer, pContentType) =>
79
+ {
80
+ Expect(pRotateError).to.equal(null);
81
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
82
+ Expect(pOutputBuffer.length).to.be.greaterThan(0);
83
+ Expect(pContentType).to.equal('image/jpeg');
84
+
85
+ // Verify dimensions are swapped (10x20 → 20x10)
86
+ libSharp(pOutputBuffer).metadata().then(
87
+ (pMetadata) =>
88
+ {
89
+ Expect(pMetadata.width).to.equal(20);
90
+ Expect(pMetadata.height).to.equal(10);
91
+ return fDone();
92
+ }).catch(
93
+ (pMetaError) => { return fDone(pMetaError); });
94
+ });
95
+ });
96
+ }
97
+ );
98
+
99
+ test
100
+ (
101
+ 'should flip an image without error',
102
+ (fDone) =>
103
+ {
104
+ createTestJpegBuffer(
105
+ (pError, pJpegBuffer) =>
106
+ {
107
+ Expect(pError).to.equal(null);
108
+
109
+ let tmpCore = new libConversionCore();
110
+
111
+ tmpCore.imageRotate(pJpegBuffer,
112
+ { Flip: true },
113
+ (pRotateError, pOutputBuffer, pContentType) =>
114
+ {
115
+ Expect(pRotateError).to.equal(null);
116
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
117
+ Expect(pOutputBuffer.length).to.be.greaterThan(0);
118
+ return fDone();
119
+ });
120
+ });
121
+ }
122
+ );
123
+
124
+ test
125
+ (
126
+ 'should flop an image without error',
127
+ (fDone) =>
128
+ {
129
+ createTestJpegBuffer(
130
+ (pError, pJpegBuffer) =>
131
+ {
132
+ Expect(pError).to.equal(null);
133
+
134
+ let tmpCore = new libConversionCore();
135
+
136
+ tmpCore.imageRotate(pJpegBuffer,
137
+ { Flop: true },
138
+ (pRotateError, pOutputBuffer, pContentType) =>
139
+ {
140
+ Expect(pRotateError).to.equal(null);
141
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
142
+ Expect(pOutputBuffer.length).to.be.greaterThan(0);
143
+ return fDone();
144
+ });
145
+ });
146
+ }
147
+ );
148
+ }
149
+ );
150
+
151
+ suite
152
+ (
153
+ 'Image Convert',
154
+ () =>
155
+ {
156
+ test
157
+ (
158
+ 'should convert JPEG to PNG',
159
+ (fDone) =>
160
+ {
161
+ createTestJpegBuffer(
162
+ (pError, pJpegBuffer) =>
163
+ {
164
+ Expect(pError).to.equal(null);
165
+
166
+ let tmpCore = new libConversionCore();
167
+
168
+ tmpCore.imageConvert(pJpegBuffer,
169
+ { Format: 'png' },
170
+ (pConvertError, pOutputBuffer, pContentType) =>
171
+ {
172
+ Expect(pConvertError).to.equal(null);
173
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
174
+ Expect(pContentType).to.equal('image/png');
175
+
176
+ // Verify PNG magic bytes
177
+ Expect(pOutputBuffer[0]).to.equal(137);
178
+ Expect(pOutputBuffer[1]).to.equal(80);
179
+ Expect(pOutputBuffer[2]).to.equal(78);
180
+ Expect(pOutputBuffer[3]).to.equal(71);
181
+
182
+ return fDone();
183
+ });
184
+ });
185
+ }
186
+ );
187
+
188
+ test
189
+ (
190
+ 'should convert PNG to WebP',
191
+ (fDone) =>
192
+ {
193
+ createTestPngBuffer(
194
+ (pError, pPngBuffer) =>
195
+ {
196
+ Expect(pError).to.equal(null);
197
+
198
+ let tmpCore = new libConversionCore();
199
+
200
+ tmpCore.imageConvert(pPngBuffer,
201
+ { Format: 'webp' },
202
+ (pConvertError, pOutputBuffer, pContentType) =>
203
+ {
204
+ Expect(pConvertError).to.equal(null);
205
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
206
+ Expect(pContentType).to.equal('image/webp');
207
+ Expect(pOutputBuffer.length).to.be.greaterThan(0);
208
+
209
+ return fDone();
210
+ });
211
+ });
212
+ }
213
+ );
214
+
215
+ test
216
+ (
217
+ 'should convert PNG to JPEG with quality option',
218
+ (fDone) =>
219
+ {
220
+ createTestPngBuffer(
221
+ (pError, pPngBuffer) =>
222
+ {
223
+ Expect(pError).to.equal(null);
224
+
225
+ let tmpCore = new libConversionCore();
226
+
227
+ tmpCore.imageConvert(pPngBuffer,
228
+ { Format: 'jpeg', Quality: 50 },
229
+ (pConvertError, pOutputBuffer, pContentType) =>
230
+ {
231
+ Expect(pConvertError).to.equal(null);
232
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
233
+ Expect(pContentType).to.equal('image/jpeg');
234
+
235
+ // Verify JPEG magic bytes
236
+ Expect(pOutputBuffer[0]).to.equal(0xFF);
237
+ Expect(pOutputBuffer[1]).to.equal(0xD8);
238
+ Expect(pOutputBuffer[2]).to.equal(0xFF);
239
+
240
+ return fDone();
241
+ });
242
+ });
243
+ }
244
+ );
245
+ }
246
+ );
247
+
248
+ suite
249
+ (
250
+ 'Enhanced Image Resize',
251
+ () =>
252
+ {
253
+ test
254
+ (
255
+ 'should resize with contain fit mode',
256
+ (fDone) =>
257
+ {
258
+ createTestJpegBuffer(
259
+ (pError, pJpegBuffer) =>
260
+ {
261
+ Expect(pError).to.equal(null);
262
+
263
+ let tmpCore = new libConversionCore();
264
+
265
+ // 10x20 image, resize to fit within 5x5 with "inside" mode
266
+ tmpCore.imageResize(pJpegBuffer,
267
+ { Width: 5, Height: 5, Fit: 'inside' },
268
+ (pResizeError, pOutputBuffer, pContentType) =>
269
+ {
270
+ Expect(pResizeError).to.equal(null);
271
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
272
+
273
+ libSharp(pOutputBuffer).metadata().then(
274
+ (pMetadata) =>
275
+ {
276
+ // With 'inside' fit, the image should fit within 5x5
277
+ // The original is 10x20 (portrait), so height is dominant
278
+ // Scaled to fit: width=2 or 3, height=5
279
+ Expect(pMetadata.width).to.be.at.most(5);
280
+ Expect(pMetadata.height).to.be.at.most(5);
281
+ return fDone();
282
+ }).catch(
283
+ (pMetaError) => { return fDone(pMetaError); });
284
+ });
285
+ });
286
+ }
287
+ );
288
+
289
+ test
290
+ (
291
+ 'should support avif output format',
292
+ (fDone) =>
293
+ {
294
+ createTestJpegBuffer(
295
+ (pError, pJpegBuffer) =>
296
+ {
297
+ Expect(pError).to.equal(null);
298
+
299
+ let tmpCore = new libConversionCore();
300
+
301
+ tmpCore.imageResize(pJpegBuffer,
302
+ { Width: 5, Format: 'avif' },
303
+ (pResizeError, pOutputBuffer, pContentType) =>
304
+ {
305
+ Expect(pResizeError).to.equal(null);
306
+ Expect(pOutputBuffer).to.be.an.instanceOf(Buffer);
307
+ Expect(pContentType).to.equal('image/avif');
308
+ Expect(pOutputBuffer.length).to.be.greaterThan(0);
309
+
310
+ return fDone();
311
+ });
312
+ });
313
+ }
314
+ );
315
+ }
316
+ );
317
+
318
+ suite
319
+ (
320
+ 'Tool Availability Checks',
321
+ () =>
322
+ {
323
+ test
324
+ (
325
+ 'checkFfmpeg should return a result',
326
+ (fDone) =>
327
+ {
328
+ let tmpCore = new libConversionCore();
329
+
330
+ tmpCore.checkFfmpeg(
331
+ (pError, pAvailable) =>
332
+ {
333
+ // We don't require ffmpeg to be installed;
334
+ // just verify the check completes without throwing
335
+ Expect(typeof pAvailable).to.equal('boolean');
336
+ return fDone();
337
+ });
338
+ }
339
+ );
340
+
341
+ test
342
+ (
343
+ 'checkFfprobe should return a result',
344
+ (fDone) =>
345
+ {
346
+ let tmpCore = new libConversionCore();
347
+
348
+ tmpCore.checkFfprobe(
349
+ (pError, pAvailable) =>
350
+ {
351
+ Expect(typeof pAvailable).to.equal('boolean');
352
+ return fDone();
353
+ });
354
+ }
355
+ );
356
+
357
+ test
358
+ (
359
+ 'checkFfmpeg with invalid path should return not available',
360
+ (fDone) =>
361
+ {
362
+ let tmpCore = new libConversionCore({ FfmpegPath: '/nonexistent/ffmpeg' });
363
+
364
+ tmpCore.checkFfmpeg(
365
+ (pError, pAvailable) =>
366
+ {
367
+ Expect(pError).to.not.equal(null);
368
+ Expect(pAvailable).to.equal(false);
369
+ return fDone();
370
+ });
371
+ }
372
+ );
373
+
374
+ test
375
+ (
376
+ 'checkFfprobe with invalid path should return not available',
377
+ (fDone) =>
378
+ {
379
+ let tmpCore = new libConversionCore({ FfprobePath: '/nonexistent/ffprobe' });
380
+
381
+ tmpCore.checkFfprobe(
382
+ (pError, pAvailable) =>
383
+ {
384
+ Expect(pError).to.not.equal(null);
385
+ Expect(pAvailable).to.equal(false);
386
+ return fDone();
387
+ });
388
+ }
389
+ );
390
+ }
391
+ );
392
+
393
+ suite
394
+ (
395
+ 'Media Probe',
396
+ () =>
397
+ {
398
+ test
399
+ (
400
+ 'should return an error when no file path is provided',
401
+ (fDone) =>
402
+ {
403
+ let tmpCore = new libConversionCore();
404
+
405
+ tmpCore.mediaProbe(null,
406
+ (pError, pMetadata) =>
407
+ {
408
+ Expect(pError).to.not.equal(null);
409
+ Expect(pError.message).to.include('No file path');
410
+ return fDone();
411
+ });
412
+ }
413
+ );
414
+ }
415
+ );
416
+
417
+ suite
418
+ (
419
+ 'Audio Extract Segment',
420
+ () =>
421
+ {
422
+ test
423
+ (
424
+ 'should require a Duration parameter',
425
+ (fDone) =>
426
+ {
427
+ let tmpCore = new libConversionCore();
428
+
429
+ tmpCore.audioExtractSegment('/tmp/input.mp3', '/tmp/output.mp3',
430
+ { Start: '0' },
431
+ (pError, pResultPath) =>
432
+ {
433
+ Expect(pError).to.not.equal(null);
434
+ Expect(pError.message).to.include('Duration is required');
435
+ return fDone();
436
+ });
437
+ }
438
+ );
439
+ }
440
+ );
441
+ }
442
+ );
@@ -79,6 +79,12 @@ suite
79
79
  Expect(tmpFileTranslation.converters['image/jpg-to-png']).to.be.a('function');
80
80
  Expect(tmpFileTranslation.converters).to.have.a.property('image/png-to-jpg');
81
81
  Expect(tmpFileTranslation.converters['image/png-to-jpg']).to.be.a('function');
82
+ Expect(tmpFileTranslation.converters).to.have.a.property('image/resize');
83
+ Expect(tmpFileTranslation.converters['image/resize']).to.be.a('function');
84
+ Expect(tmpFileTranslation.converters).to.have.a.property('image/rotate/:Angle');
85
+ Expect(tmpFileTranslation.converters['image/rotate/:Angle']).to.be.a('function');
86
+ Expect(tmpFileTranslation.converters).to.have.a.property('image/convert/:Format');
87
+ Expect(tmpFileTranslation.converters['image/convert/:Format']).to.be.a('function');
82
88
  Expect(tmpFileTranslation.converters).to.have.a.property('pdf-to-page-png/:Page');
83
89
  Expect(tmpFileTranslation.converters['pdf-to-page-png/:Page']).to.be.a('function');
84
90
  Expect(tmpFileTranslation.converters).to.have.a.property('pdf-to-page-jpg/:Page');
@@ -270,13 +276,13 @@ suite
270
276
  let tmpOrator = tmpFable.serviceManager.instantiateServiceProvider('Orator', {});
271
277
  let tmpFileTranslation = tmpFable.serviceManager.instantiateServiceProvider('OratorFileTranslation', {});
272
278
 
273
- // Should have 6 default converters (jpg-to-png, png-to-jpg, pdf-to-page-png, pdf-to-page-jpg, pdf-to-page-png sized, pdf-to-page-jpg sized)
274
- Expect(Object.keys(tmpFileTranslation.converters).length).to.equal(6);
279
+ // Should have 9 default converters (jpg-to-png, png-to-jpg, resize, rotate, convert, pdf-to-page-png, pdf-to-page-jpg, pdf-to-page-png sized, pdf-to-page-jpg sized)
280
+ Expect(Object.keys(tmpFileTranslation.converters).length).to.equal(9);
275
281
 
276
282
  tmpFileTranslation.addConverter('document/txt-to-html', (pInput, pReq, fCb) => { fCb(null, pInput, 'text/html'); });
277
283
 
278
- // Should now have 7
279
- Expect(Object.keys(tmpFileTranslation.converters).length).to.equal(7);
284
+ // Should now have 10
285
+ Expect(Object.keys(tmpFileTranslation.converters).length).to.equal(10);
280
286
 
281
287
  return fDone();
282
288
  }