Skip to content

Commit f9eed2f

Browse files
authored
Improvements and bug fixes related to resizing with format conversion (#103)
* simplifying the format conversion approach * allowing to set quality level for the JpegOptim optimizer * fixing issue with incorrect file extension after format conversion * adding new test cases * adding error handler for child processes to help investigate issues with binaries * asking Travis to install missing dependency * switching test helper methods to async style
1 parent 2221f83 commit f9eed2f

File tree

9 files changed

+111
-76
lines changed

9 files changed

+111
-76
lines changed

.travis.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
language: node_js
22
node_js:
33
- 4
4+
addons:
5+
apt:
6+
packages:
7+
- libjpeg62
48
after_success:
59
- './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls'

lib/ImageData.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const path = require("path");
44
const imageType = require("image-type");
55

66
class ImageType {
7+
78
/**
89
* Gets real image type from ImageData
910
*
@@ -158,8 +159,8 @@ class ImageData {
158159
combineWithDirectory(directory, filePrefix, fileSuffix) {
159160
const prefix = filePrefix || "";
160161
const suffix = fileSuffix || "";
161-
const fileName = this.baseName.substr(0, this.baseName.lastIndexOf('.'));
162-
const extension = this.baseName.substr(this.baseName.lastIndexOf('.'));
162+
const fileName = path.parse(this.baseName).name;
163+
const extension = "." + this.type.ext;
163164
if ( directory != null ) {
164165
// ./X , ../X , . , ..
165166
if ( directory.match(/^\.\.?\//) || directory.match(/^\.\.?$/) ) {

lib/ImageReducer.js

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22

33
const ImageData = require("./ImageData");
44
const Mozjpeg = require("./optimizer/Mozjpeg");
5+
const JpegOptim = require("./optimizer/JpegOptim");
56
const Pngquant = require("./optimizer/Pngquant");
67
const Gifsicle = require("./optimizer/Gifsicle");
78
const ReadableStream = require("./ReadableImageStream");
89
const StreamChain = require("./StreamChain");
9-
// const JpegOptim = require("./optimizer/JpegOptim");
1010

1111
class ImageReducer {
1212

@@ -59,37 +59,23 @@ class ImageReducer {
5959
console.log("Reducing to: " + (this.option.directory || "in-place"));
6060

6161
const streams = [];
62-
63-
let outputType;
64-
65-
if ( type == "gif" ) {
66-
outputType = type;
67-
} else if ( "quality" in this.option ) {
68-
outputType = "jpg"; // force JPEG when quality given
69-
} else if ( "format" in this.option ) {
70-
outputType = this.option.format;
71-
} else {
72-
outputType = type;
73-
}
74-
75-
switch ( outputType ) {
62+
switch ( type ) {
7663
case "png":
7764
streams.push(new Pngquant());
7865
break;
7966
case "jpg":
8067
case "jpeg":
81-
streams.push(new Mozjpeg(this.option.quality));
82-
// switch JPEG optimizer
83-
// if ( this.option.jpegOptimizer === "jpegoptim" ) { // using jpegoptim
84-
// streams.push(new JpegOptim());
85-
// } else { // using mozjpeg
86-
// }
68+
if ( this.option.jpegOptimizer === "jpegoptim" ) { // using jpegoptim
69+
streams.push( new JpegOptim( this.option.quality ) );
70+
} else { // using mozjpeg
71+
streams.push( new Mozjpeg( this.option.quality ) );
72+
}
8773
break;
8874
case "gif":
8975
streams.push(new Gifsicle());
9076
break;
9177
default:
92-
throw new Error("Unexcepted output type: " + outputType);
78+
throw new Error("Unexpected output type: " + type);
9379
}
9480

9581
return streams;

lib/ImageResizer.js

Lines changed: 13 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,6 @@ class ImageResizer {
4141
if ( "background" in this.options ) {
4242
img = img.background(this.options.background).flatten();
4343
}
44-
if ( "quality" in this.options ) {
45-
img = img.quality(this.options.quality);
46-
}
4744
if ( "crop" in this.options ) {
4845
var cropArgs = this.options.crop.match(cropSpec);
4946
const cropWidth = cropArgs[1];
@@ -53,45 +50,25 @@ class ImageResizer {
5350
const cropPercent = cropArgs[5];
5451
img = img.crop(cropWidth, cropHeight, cropX, cropY, cropPercent === "%");
5552
}
53+
if( "format" in this.options ) {
54+
img = img.setFormat(this.options.format);
55+
}
5656

57-
img.toBuffer(this.detectFormat(image), (err, buffer) => {
57+
img.toBuffer((err, buffer) => {
5858
if (err) {
59-
return reject(err);
59+
reject(err);
60+
} else {
61+
resolve(new ImageData(
62+
image.fileName,
63+
image.bucketName,
64+
buffer,
65+
image.headers,
66+
acl
67+
));
6068
}
61-
resolve(new ImageData(
62-
image.fileName,
63-
image.bucketName,
64-
buffer,
65-
image.headers,
66-
acl
67-
));
6869
});
6970
});
7071
}
71-
72-
/**
73-
* Format detection
74-
*
75-
* @protected
76-
* @param ImageData image
77-
* @return string
78-
* @throws Error
79-
*/
80-
detectFormat(image) {
81-
// Does options exists?
82-
if ( "format" in this.options ) {
83-
return this.options.format.toUpperCase();
84-
}
85-
86-
// Detect from MimeType
87-
switch (image.type.mime) {
88-
case "image/gif": return "GIF";
89-
case "image/jpeg": return "JPG";
90-
case "image/png": return "PNG";
91-
default:
92-
throw new Error("Unexpected MimeType: " + image.type.mime);
93-
}
94-
}
9572
}
9673

9774
module.exports = ImageResizer;

lib/optimizer/JpegOptim.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,19 @@ class Jpegoptim extends Optimizer {
88
*
99
* @constructor
1010
* @extends Optimizer
11+
* @param Number|undefined quality
1112
*/
12-
constructor() {
13+
constructor(quality) {
1314
super();
1415

1516
this.command = this.findBin("jpegoptim");
1617
this.args = ["--stdin", "-s", "--all-progressive", "--stdout"];
18+
19+
// determine quality if supplied
20+
if ( quality ) {
21+
this.args.unshift(quality);
22+
this.args.unshift("-m");
23+
}
1724
}
1825
}
1926

lib/optimizer/Optimizer.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,11 @@ class Optimizer {
2121
* @return ChildProcess
2222
*/
2323
spawnProcess() {
24-
return spawn(this.command, this.args);
24+
const process = spawn(this.command, this.args);
25+
process.on('err', (err) => {
26+
console.log(`Child process ended with error code ${err}`);
27+
});
28+
return process
2529
}
2630

2731
/**

test/e2e-jpeg-jpegoptim.js

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const setting = JSON.parse(fs.readFileSync(sourceFile));
1515
let processor;
1616
let images;
1717

18-
test.before(() => {
18+
test.before(async t => {
1919
sinon.stub(S3, "getObject", () => {
2020
return fsP.readFile(`${__dirname}/fixture/fixture.jpg`).then(data => {
2121
return new ImageData(
@@ -25,23 +25,23 @@ test.before(() => {
2525
);
2626
});
2727
});
28-
images = [];
2928
sinon.stub(S3, "putObject", (image) => {
3029
images.push(image);
31-
return new Promise((resolve) => resolve(image));
30+
return Promise.resolve(image);
3231
});
3332
});
3433

35-
test.after(() => {
34+
test.after(async t => {
3635
S3.getObject.restore();
3736
S3.putObject.restore();
3837
});
3938

40-
test.beforeEach(() => {
39+
test.beforeEach(async t => {
4140
processor = new ImageProcessor(setting.Records[0].s3, {
4241
done: () => {},
4342
fail: () => {}
4443
});
44+
images = [];
4545
});
4646

4747
test("Reduce JPEG with no configuration", async t => {
@@ -80,3 +80,18 @@ test("Reduce JPEG with bucket/directory configuration", async t => {
8080
t.true(image.data.length > 0);
8181
t.true(image.data.length < fixture.length);
8282
});
83+
84+
85+
test("Reduce JPEG with quality", async t => {
86+
await processor.run(new Config({
87+
"reduce": {
88+
"quality": 90,
89+
"jpegOptimizer": "jpegoptim"
90+
}
91+
}));
92+
t.is(images.length, 1);
93+
const image = images.shift();
94+
const fixture = await fsP.readFile(`${__dirname}/fixture/fixture.jpg`);
95+
t.true(image.data.length > 0);
96+
t.true(image.data.length < fixture.length);
97+
});

test/e2e-jpeg.js

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const setting = JSON.parse(fs.readFileSync(sourceFile));
1515
let processor;
1616
let images;
1717

18-
test.before(() => {
18+
test.before(async t => {
1919
sinon.stub(S3, "getObject", () => {
2020
return fsP.readFile(`${__dirname}/fixture/fixture.jpg`).then(data => {
2121
return new ImageData(
@@ -25,23 +25,23 @@ test.before(() => {
2525
);
2626
});
2727
});
28-
images = [];
2928
sinon.stub(S3, "putObject", (image) => {
3029
images.push(image);
3130
return Promise.resolve(image);
3231
});
3332
});
3433

35-
test.after(() => {
34+
test.after(async t => {
3635
S3.getObject.restore();
3736
S3.putObject.restore();
3837
});
3938

40-
test.beforeEach(() => {
39+
test.beforeEach(async t => {
4140
processor = new ImageProcessor(setting.Records[0].s3, {
4241
done: () => {},
4342
fail: () => {}
4443
});
44+
images = [];
4545
});
4646

4747
test("Reduce JPEG with no configuration", async t => {
@@ -78,3 +78,44 @@ test("Reduce JPEG with bucket/directory configuration", async t => {
7878
t.true(image.data.length > 0);
7979
t.true(image.data.length < fixture.length);
8080
});
81+
82+
test("Resize JPEG with quality", async t => {
83+
await processor.run(new Config({
84+
"resizes": [
85+
{
86+
"size": 100,
87+
"quality": 90
88+
}
89+
]
90+
}));
91+
t.is(images.length, 1);
92+
const image = images.shift();
93+
const fixture = await fsP.readFile(`${__dirname}/fixture/fixture.jpg`);
94+
t.is(image.fileName, "HappyFace.jpg");
95+
t.true(image.data.length > 0);
96+
t.true(image.data.length < fixture.length);
97+
});
98+
99+
test("Resize JPEG with format", async t => {
100+
await processor.run(new Config({
101+
"resizes": [
102+
{
103+
"size": 100,
104+
"format": "png"
105+
},
106+
{
107+
"size": 100,
108+
"format": "gif"
109+
}
110+
]
111+
}));
112+
t.is(images.length, 2);
113+
114+
const pngImage = images.shift();
115+
t.is(pngImage.fileName, "HappyFace.png");
116+
t.true(pngImage.data.length > 0);
117+
118+
const gifImage = images.shift();
119+
t.is(gifImage.fileName, "HappyFace.gif");
120+
t.true(gifImage.data.length > 0);
121+
});

test/e2e-png.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const setting = JSON.parse(fs.readFileSync(sourceFile));
1515
let processor;
1616
let images;
1717

18-
test.before(() => {
18+
test.before(async t => {
1919
sinon.stub(S3, "getObject", () => {
2020
return fsP.readFile(`${__dirname}/fixture/fixture.png`).then(data => {
2121
return new ImageData(
@@ -25,23 +25,23 @@ test.before(() => {
2525
);
2626
});
2727
});
28-
images = [];
2928
sinon.stub(S3, "putObject", (image) => {
3029
images.push(image);
31-
return new Promise((resolve) => resolve(image));
30+
return Promise.resolve(image);
3231
});
3332
});
3433

35-
test.after(() => {
34+
test.after(async t => {
3635
S3.getObject.restore();
3736
S3.putObject.restore();
3837
});
3938

40-
test.beforeEach(() => {
39+
test.beforeEach(async t => {
4140
processor = new ImageProcessor(setting.Records[0].s3, {
4241
done: () => {},
4342
fail: () => {}
4443
});
44+
images = [];
4545
});
4646

4747
test("Reduce PNG with no configuration", async t => {

0 commit comments

Comments
 (0)