Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Output placeholder image as data URI #16

Merged
merged 12 commits into from
Nov 14, 2016
37 changes: 29 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ const responsiveImage = require('responsive?sizes[]=100,sizes[]=200,sizes[]=300!
// responsiveImage.images => [{height: 50, path: '2fefae46cb857bc750fa5e5eed4a0cde-100.jpg', width: 100}, {height: 100, path: '2fefae46cb857bc750fa5e5eed4a0cde-200.jpg', width: 200}, {height: 150, path: '2fefae46cb857bc750fa5e5eed4a0cde-300.jpg', width: 300}]
// responsiveImage.src => '2fefae46cb857bc750fa5e5eed4a0cde-100.jpg'
// responsiveImage.toString() => '2fefae46cb857bc750fa5e5eed4a0cde-100.jpg'
React.render(<img srcSet={responsiveImage.srcSet} src={responsiveImage.src} />, el);
ReactDOM.render(<img srcSet={responsiveImage.srcSet} src={responsiveImage.src} />, el);

// Or you can just use it as props, `srcSet` and `src` will be set properly
React.render(<img {...responsiveImage} />, el);
ReactDOM.render(<img {...responsiveImage} />, el);
```

Or use it in CSS (only the first resized image will be used, if you use multiple `sizes`):
Expand All @@ -38,13 +38,32 @@ Or use it in CSS (only the first resized image will be used, if you use multiple
}
```

```js
// Outputs placeholder image as a data URI, and three images with 100, 200, and 300px widths
const responsiveImage = require('responsive?placeholder=true&sizes[]=100,sizes[]=200,sizes[]=300!myImage.jpg');
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe an example using import as well?

Copy link
Collaborator Author

@alex-ketch alex-ketch Oct 9, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the syntax for using query configuration with import? Is it the same?
Otherwise I'd rather keep this consistent with the rest of the examples, and create them with the Webpack v2 documentation update.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes sounds reasonable :)


// responsiveImage.placeholder => 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAIBAQE…'
ReactDOM.render(
<div style={{
height: responsiveImage.height,
width: responsiveImage.width,
backgroundSize: 'cover',
backgroundImage: 'url("' + responsiveImage.placeholder + '")'
}}>
<img src={responsiveImage.src} srcSet={responsiveImage.srcSet} />
</div>, el);
```


### Options

- `sizes: array`: specify all widths you want to use; if a specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up). You may also declare a default `sizes` array in `responsiveLoader` in your `webpack.config.js`.
- `size: integer`: specify one width you want to use; if the specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up)
- `quality: integer`: JPEG compression quality; defaults to `95`
- `ext: string`: either `png`, `jpg`, or `gif`; use to convert to another format; defaults to original file's extension
- `background: hex`: Background fill when converting transparent to opaque images; defaults to `0xFFFFFFFF` (note: make sure this is a valid hex number)
- `sizes: array` — specify all widths you want to use; if a specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up). You may also declare a default `sizes` array in `responsiveLoader` in your `webpack.config.js`.
- `size: integer` — specify one width you want to use; if the specified size exceeds the original image's width, the latter will be used (i.e. images won't be scaled up)
- `quality: integer` — JPEG compression quality; defaults to `95`
- `ext: string` — either `png`, `jpg`, or `gif`; use to convert to another format; defaults to original file's extension
- `background: hex` — Background fill when converting transparent to opaque images; defaults to `0xFFFFFFFF` (note: make sure this is a valid hex number)
- `placeholder: bool` — A true or false value to specify wether to output a placeholder image as a data URI. (Defaults to `false`)
- `placeholderSize: integer` — A number value specifying the width of the placeholder image, if enabled with the option above. (Defaults to `40`)


### Examples
Expand All @@ -62,7 +81,9 @@ module.exports = {
]}
},
responsiveLoader: {
sizes: [300, 600, 1200, 2000]
sizes: [300, 600, 1200, 2000],
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webpack 2 doesn't allow this, options should be passed to the plugin afaik.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't really worked with v2 much and will leave updating documentation to someone more knowledgable. We would need to show examples for both versions, as webpack 2 isn't officially out yet.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically this, but yeah it's probably for another PR: https://webpack.js.org/how-to/upgrade-from-webpack-1/#loaderoptionplugin-context

placeholder: true,
placeholderSize: 50
}
}
```
Expand Down
40 changes: 37 additions & 3 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ module.exports = function loader(content) {
const sizes = query.sizes || query.size || options.sizes || [Number.MAX_SAFE_INTEGER];
const name = query.name || options.name || '[hash]-[width].';
const outputContext = query.context || options.context || '';
const outputPlaceholder = query.placeholder || query.placeholder !== false && options.placeholder || false;
const placeholderSize = query.placeholderSize || options.placeholderSize || 40;
// JPEG compression
const quality = parseInt(query.quality, 10) || options.quality || 95;
// Useful when converting from PNG to JPG
Expand Down Expand Up @@ -87,14 +89,46 @@ module.exports = function loader(content) {
}
});

if (outputPlaceholder) {
q.defer(function generatePlaceholder(queueCallback) {
img
.clone()
.resize(placeholderSize, jimp.AUTO)
.quality(quality)
.background(background)
.getBuffer(mime, function resizeCallback(queueErr, buf) {
if (err) {
return queueCallback(queueErr);
}

const placeholder = buf.toString('base64');
return queueCallback(null, JSON.stringify('data:' + (mime ? mime + ';' : '') + 'base64,' + placeholder));
});
});
}

return q.awaitAll((queueErr, files) => {
'use strict'; // eslint-disable-line
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is 'use strict' necessary?

Copy link
Collaborator Author

@alex-ketch alex-ketch Oct 18, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe I was getting the following error in older versions of node, complaining about use of const and let, which is why I added it.

SyntaxError: Block-scoped declarations (let, const, function, class) not yet supported outside strict mode

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But not with node >= 4, no? If we want to support older versions (I don't really 😁 ), we should add tests for those too in travis.yml.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thought so too, but it was failing with v4 and v5, only passed with v6.
Let me look into this a bit more...

let placeholder;
if (outputPlaceholder) {
placeholder = files.pop();
}

const srcset = files.map(f => f.src).join('+","+');

const images = files.map(f => '{path:' + f.path + ',width:' + f.width + ',height:' + f.height + '}').join(',');

const firstImagePath = files[0].path;

loaderCallback(null, 'module.exports = {srcSet:' + srcset + ',images:[' + images + '],src:' + firstImagePath + ',toString:function(){return ' + firstImagePath + '}};');
const firstImage = files[0];

loaderCallback(null, 'module.exports = {' +
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't width and height missing here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, still too jet lagged. I've added the original image dimensions to the output.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be the dimensions of the first resized image to align it with src and toString()?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that would make sense, wouldn't it 😅
Will update

'srcSet:' + srcset + ',' +
'images:[' + images + '],' +
'src:' + firstImage.path + ',' +
'toString:function(){return ' + firstImage.path + '},' +
'placeholder: ' + placeholder + ',' +
'width:' + firstImage.width + ',' +
'height:' + firstImage.height +
'};');
});
});
};
Expand Down
13 changes: 13 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,16 @@ test('output should be relative to context', t => {
t.equal(multi.toString(), 'foobar/test/9a7bdeb1304946f1fcd4a691409f286a-500.jpg');
t.end();
});

test('with placeholder image', t => {
const output = require('../index?placeholder=true!./cat-1000.jpg');
t.equal(output.placeholder, 'data:image/jpeg;base64,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');
t.end();
});

test('output first resized image height & width', t => {
const output = require('../index?size=500!./cat-1000.jpg');
t.equal(output.height, 450);
t.equal(output.width, 500);
t.end();
});