| An array of all the resized images. Each image is an **Image Object**. |
102 | | `original` | Object | An **Image Object** containing information about the original image. |
103 | | `*` | String | Any other attributes will be passed to the template verbatim as strings (see below). |
104 |
105 | Any other attributes that are given to the `responsive_image` or `responsive_image_block` tags will be available in the template. For example the following tag will provide an `{{ alt }}` variable to the template:
106 |
107 | ```twig
108 | {% responsive_image path: assets/my-file.jpg alt: "A description of the image" %}
109 | ```
110 |
111 | #### Image Objects
112 |
113 | Image objects (like `original` and each object in `resized`) contain the following properties:
114 |
115 | | Variable | Type | Description |
116 | |-------------|---------|----------------------------------------------------------------------------------------------|
117 | | `path` | String | The path to the image. |
118 | | `width` | Integer | The width of the image. |
119 | | `height` | Integer | The height of the image. |
120 | | `basename` | String | Basename of the file (`assets/some-file.jpg` => `some-file.jpg`). |
121 | | `dirname` | String | Directory of the file relative to `base_path` (`assets/sub/dir/some-file.jpg` => `sub/dir`). |
122 | | `filename` | String | Basename without the extension (`assets/some-file.jpg` => `some-file`). |
123 | | `extension` | String | Extension of the file (`assets/some-file.jpg` => `jpg`). |
124 |
125 | ## All configuration options
126 |
127 | A full list of all of the available configuration options is below.
128 |
129 | ```yaml
130 | responsive_image:
131 | # [Required]
132 | # Path to the image template.
133 | template: _includes/responsive-image.html
134 |
135 | # [Optional, Default: 85]
136 | # Quality to use when resizing images.
137 | default_quality: 90
138 |
139 | # [Optional, Default: []]
140 | # An array of resize configuration objects. Each object must contain at least
141 | # a `width` value.
142 | sizes:
143 | - width: 480 # [Required] How wide the resized image will be.
144 | quality: 80 # [Optional] Overrides default_quality for this size.
145 | - width: 800
146 | - width: 1400
147 | quality: 90
148 |
149 | # [Optional, Default: false]
150 | # Rotate resized images depending on their EXIF rotation attribute. Useful for
151 | # working with JPGs directly from digital cameras and smartphones
152 | auto_rotate: false
153 |
154 | # [Optional, Default: false]
155 | # Strip EXIF and other JPEG profiles. Helps to minimize JPEG size and win friends
156 | # at Google PageSpeed.
157 | strip: false
158 |
159 | # [Optional, Default: assets]
160 | # The base directory where assets are stored. This is used to determine the
161 | # `dirname` value in `output_path_format` below.
162 | base_path: assets
163 |
164 | # [Optional, Default: assets/resized/%{filename}-%{width}x%{height}.%{extension}]
165 | # The template used when generating filenames for resized images. Must be a
166 | # relative path.
167 | #
168 | # Parameters available are:
169 | # %{dirname} Directory of the file relative to `base_path` (assets/sub/dir/some-file.jpg => sub/dir)
170 | # %{basename} Basename of the file (assets/some-file.jpg => some-file.jpg)
171 | # %{filename} Basename without the extension (assets/some-file.jpg => some-file)
172 | # %{extension} Extension of the file (assets/some-file.jpg => jpg)
173 | # %{width} Width of the resized image
174 | # %{height} Height of the resized image
175 | #
176 | output_path_format: assets/resized/%{width}/%{basename}
177 |
178 | # [Optional, Default: true]
179 | # Whether or not to save the generated assets into the source folder.
180 | save_to_source: false
181 |
182 | # [Optional, Default: false]
183 | # Cache the result of {% responsive_image %} and {% responsive_image_block %}
184 | # tags. See the "Caching" section of the README for more information.
185 | cache: false
186 |
187 | # [Optional, Default: []]
188 | # By default, only images referenced by the responsive_image and responsive_image_block
189 | # tags are resized. Here you can set a list of paths or path globs to resize other
190 | # images. This is useful for resizing images which will be referenced from stylesheets.
191 | extra_images:
192 | - assets/foo/bar.png
193 | - assets/bgs/*.png
194 | - assets/avatars/*.{jpeg,jpg}
195 | ```
196 |
197 | ## Troubleshooting
198 |
199 | ### Error: Can't install RMagick
200 |
201 | `jekyll-responsive-image` uses `rmagick` which is currently incompatible with ImageMagick 7. If you get an error like:
202 |
203 | ```
204 | Can't install RMagick 2.16.0. Can't find MagickWand.h
205 | ```
206 |
207 | Then you will need to install ImageMagick 6. If you are using Homebrew on Mac OS, this can be done with the following commands:
208 |
209 | ```
210 | $ brew uninstall imagemagick
211 | $ brew install imagemagick@6 && brew link imagemagick@6 --force
212 | ```
213 |
214 | ## Caching
215 |
216 | You may be able to speed up the build of large sites by enabling render caching. This is usually only effective when the same image is used many times, for example a header image that is rendered in every post.
217 |
218 | The recommended way to enable caching is on an image-by-image basis, by adding `cache: true` to the tag:
219 |
220 | ```twig
221 | {% responsive_image path: 'assets/my-file.jpg' cache: true %}
222 |
223 | {% responsive_image_block %}
224 | path: assets/my-file.jpg
225 | cache: true
226 | {% endresponsive_image_block %}
227 | ```
228 |
229 | You can also enable it for all images by adding `cache: true` to your `_config.yml`:
230 |
231 | ```yaml
232 | responsive_image:
233 | cache: true
234 | template: _includes/responsive-image.html
235 | sizes:
236 | - ...
237 | ```
238 |
239 | ## Development
240 |
241 | If you'd like to contribute to this repository, here's how you can set it up for development:
242 |
243 | 1. Fork this repository
244 | 2. Clone the fork to your local machine
245 | 3. Install [ImageMagick](http://www.imagemagick.org/) (if you haven't already)
246 | 4. Run `bundle install`
247 | 5. Run the tests like this: `cucumber`
248 |
249 | If you'd like your Jekyll project to use your local fork directly, you can add the `:path` parameter to your gem command in the project's Gemfile:
250 |
251 | ```ruby
252 | gem 'jekyll-responsive-image', :path => "/your/local/path/to/jekyll-responsive-image"
253 | ```
254 |
255 | If you'd like your changes to be considered for the original repository, simply submit a pull request after you've made your changes. Please make sure all tests pass.
256 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 |
3 | begin
4 | Bundler.setup(:default, :development)
5 | rescue Bundler::BundlerError => e
6 | $stderr.puts e.message
7 | $stderr.puts "Run `bundle install` to install missing gems"
8 | exit e.status_code
9 | end
10 |
11 | require 'rake'
12 | require 'jekyll-responsive-image/version'
13 | require 'cucumber/rake/task'
14 | require 'coveralls/rake/task'
15 |
16 | Cucumber::Rake::Task.new(:features)
17 |
18 | Coveralls::RakeTask.new
19 | task :features_with_coveralls => [:features, 'coveralls:push']
20 |
21 | task :default => [:features]
22 |
23 | task :release do |t|
24 | system "gem build jekyll-responsive-image.gemspec"
25 | system "git tag v#{Jekyll::ResponsiveImage::VERSION} -a -m 'Tagged release of jekyll-responsive-image-#{Jekyll::ResponsiveImage::VERSION}.gem'"
26 | system "git push --tags"
27 | system "gem push jekyll-responsive-image-#{Jekyll::ResponsiveImage::VERSION}.gem"
28 | end
29 |
--------------------------------------------------------------------------------
/compositor.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wildlyinaccurate/jekyll-responsive-image",
3 | "version": "0.1.4",
4 | "libraries": {
5 | "xv": "^1.1.21"
6 | },
7 | "title": "",
8 | "branch": "master",
9 | "style": {
10 | "name": "Default",
11 | "componentSet": {
12 | "nav": "nav/BasicNav",
13 | "header": "header/BannerHeader",
14 | "article": "article/BasicArticle",
15 | "footer": "footer/BasicFooter"
16 | },
17 | "fontFamily": "-apple-system, BlinkMacSystemFont, sans-serif",
18 | "fontWeight": 400,
19 | "bold": 600,
20 | "lineHeight": 1.5,
21 | "typeScale": [
22 | 72,
23 | 48,
24 | 24,
25 | 20,
26 | 16,
27 | 14,
28 | 12
29 | ],
30 | "monospace": "Menlo, monospace",
31 | "heading": {
32 | "fontFamily": null,
33 | "fontStyle": null,
34 | "fontWeight": 600,
35 | "lineHeight": 1.25,
36 | "textTransform": null,
37 | "letterSpacing": null,
38 | "h0": {},
39 | "h1": {},
40 | "h2": {},
41 | "h3": {},
42 | "h4": {},
43 | "h5": {},
44 | "h6": {}
45 | },
46 | "alternativeText": {},
47 | "space": [
48 | 0,
49 | 8,
50 | 16,
51 | 32,
52 | 48,
53 | 64,
54 | 96
55 | ],
56 | "layout": {
57 | "maxWidth": 1024,
58 | "centered": false
59 | },
60 | "colors": {
61 | "text": "#111",
62 | "background": "#fff",
63 | "primary": "#08e",
64 | "secondary": "#059",
65 | "highlight": "#e08",
66 | "border": "#ddd",
67 | "muted": "#eee"
68 | },
69 | "border": {
70 | "width": 1,
71 | "radius": 2
72 | },
73 | "link": {},
74 | "button": {
75 | "hover": {
76 | "boxShadow": "inset 0 0 0 999px rgba(0, 0, 0, .125)"
77 | }
78 | },
79 | "input": {},
80 | "body": {
81 | "margin": 0
82 | },
83 | "breakpoints": {
84 | "xs": "@media screen and (max-width:40em)",
85 | "sm": "@media screen and (min-width:40em)",
86 | "md": "@media screen and (min-width:52em)",
87 | "lg": "@media screen and (min-width:64em)"
88 | }
89 | },
90 | "content": [
91 | {
92 | "component": "nav",
93 | "links": [
94 | {
95 | "href": "https://github.com/wildlyinaccurate/jekyll-responsive-image",
96 | "text": "GitHub"
97 | }
98 | ]
99 | },
100 | {
101 | "component": "header",
102 | "heading": "jekyll-responsive-image",
103 | "subhead": "An unopinionated Jekyll plugin for generating and using responsive images",
104 | "children": [
105 | {
106 | "component": "ui/TweetButton",
107 | "text": "jekyll-responsive-image: An unopinionated Jekyll plugin for generating and using responsive images",
108 | "url": ""
109 | },
110 | {
111 | "component": "ui/GithubButton",
112 | "user": "wildlyinaccurate",
113 | "repo": "jekyll-responsive-image"
114 | }
115 | ]
116 | },
117 | {
118 | "component": "article",
119 | "metadata": {
120 | "source": "github.readme"
121 | },
122 | "html": "Jekyll Responsive Images \nJekyll Responsive Images is a Jekyll plugin and utility for automatically resizing images. Its intended use is for sites which want to display responsive images using something like srcset
or Imager.js .
\n \n \n
\nInstallation \nFirst, install the gem:
\n$ gem install jekyll-responsive_image Then you can either add it to the gems
section of your _config.yml
:
\ngems: [jekyll-responsive-image]Or you can copy the contents of responsive_image.rb
into your _plugins
directory.
\nConfiguration \nAn example configuration is below.
\nresponsive_image: \n \n \n template: _includes/responsive-image.html\n\n \n \n default_quality: 90 \n\n \n \n \n sizes: \n - width: 480 \n quality: 80 \n - width: 800 \n - width: 1400 \n quality: 90 \n\n \n \n \n base_path: assets\n\n \n \n \n \n \n \n \n \n \n \n \n \n output_path_format: assets/resized/%{width}/%{basename}\n\n \n \n \n extra_images: \n - assets/foo/bar.png\n - assets/bgs/*.png\n - assets/avatars/*.{jpeg,jpg}Usage \nReplace your images with the responsive_image
tag, specifying the path to the image in the path
attribute.
\n{% responsive_image path: assets/my-file.jpg %} You can override the template on a per-image basis by specifying the template
attribute.
\n{% responsive_image path: assets/my-file.jpg template: _includes/another-template.html %} Any extra attributes will be passed straight to the template as variables.
\n{% responsive_image path: assets/image.jpg alt: "Lorem ipsum..." title: "Lorem ipsum..." %} Liquid variables as attributes \nYou can use Liquid variables as attributes with the responsive_image_block
tag. This tag works in exactly the same way as the responsive_image
tag, but is implemented as a block tag to allow for more complex logic.
\n\nImportant! The attributes in the responsive_image_block
tag are parsed as YAML, so whitespace and indentation are significant!
\n \n{% assign path = 'assets/test.png' %} \n {% assign alt = 'Lorem ipsum...' %} \n\n {% responsive_image_block %} \n path: {{ path }} \n alt: {{ alt }} \n {% if title %} \n title: {{ title }} \n {% endif %} \n {% endresponsive_image_block %} Template \nYou will need to create a template in order to use the responsive_image
tag. Below are some sample templates to get you started.
\nResponsive images with srcset
\n{% capture srcset %} \n {% for i in resized %} \n / {{ i.path }} {{ i.width }} w,\n {% endfor %} \n {% endcapture %} \n\n<img src ="/ {{ path }} " alt =" {{ alt }} " srcset =" {{ srcset | strip_newlines }} / {{ original.path }} {{ original.width }} w" >Responsive images with <picture>
\n<picture > \n {% for i in resized %} \n <source media ="(min-width: {{ i.width }} px)" srcset ="/ {{ i.path }} " > \n {% endfor %} \n <img src ="/ {{ path }} " > \n</picture > Responsive images using Imager.js \n\nNote: This template assumes an output_path_format
of assets/resized/%{width}/%{basename}
\n \n{% assign smallest = resized | sort: 'width' | first %} \n\n<div class ="responsive-image" > \n <img class ="responsive-image__placeholder" src ="/ {{ smallest.path }} " > \n <div class ="responsive-image__delayed" data-src ="/assets/resized/{width}/ {{ original.basename }} " ></div > \n</div > \n\n<script > \n new Imager('.responsive-image__delayed' , {\n availableWidths: [ {{ resized | map: 'width' | join: ', ' }} ]\n onImagesReplaced: function ( ) {\n $('.responsive-image__placeholder' ).hide();\n }\n });\n </script > Template Variables \nThe following variables are available in the template:
\n\n\n\nVariable \nType \nDescription \n \n \n\n\npath
\nString \nThe path of the unmodified image. This is always the same as the path
attribute passed to the tag. \n \n\nresized
\nArray \nAn array of all the resized images. Each image is an Image Object . \n \n\noriginal
\nObject \nAn Image Object containing information about the original image. \n \n\n*
\nString \nAny other attributes will be passed to the template verbatim as strings. \n \n \n
\nImage Objects \nImage objects (like original
and each object in resized
) contain the following properties:
\n\n\n\nVariable \nType \nDescription \n \n \n\n\npath
\nString \nThe path to the image. \n \n\nwidth
\nInteger \nThe width of the image. \n \n\nheight
\nInteger \nThe height of the image. \n \n\nbasename
\nString \nBasename of the file (assets/some-file.jpg
=> some-file.jpg
). \n \n\ndirname
\nString \nDirectory of the file relative to base_path
(assets/sub/dir/some-file.jpg
=> sub/dir
). \n \n\nfilename
\nString \nBasename without the extension (assets/some-file.jpg
=> some-file
). \n \n\nextension
\nString \nExtension of the file (assets/some-file.jpg
=> jpg
). \n \n \n
\n"
123 | },
124 | {
125 | "component": "footer",
126 | "links": [
127 | {
128 | "href": "https://github.com/wildlyinaccurate/jekyll-responsive-image",
129 | "text": "GitHub"
130 | },
131 | {
132 | "href": "https://github.com/wildlyinaccurate",
133 | "text": "wildlyinaccurate"
134 | }
135 | ]
136 | }
137 | ]
138 | }
139 |
--------------------------------------------------------------------------------
/features/extra-image-generation.feature:
--------------------------------------------------------------------------------
1 | Feature: Extra image generation
2 | Scenario: Specifying a single image and glob patterns
3 | Given I have a responsive_image configuration with:
4 | """
5 | sizes:
6 | - width: 100
7 | extra_images:
8 | - assets/everybody-loves-jalapeño-pineapple-cornbread.png
9 | - assets/*.jpeg
10 | """
11 | And I have a file "index.html" with "Hello, world!"
12 | When I run Jekyll
13 | Then the image "assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should have the dimensions "100x50"
14 | And the image "assets/resized/progressive-100x50.jpeg" should have the dimensions "100x50"
15 | And the file "_site/assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should exist
16 | And the file "_site/assets/resized/progressive-100x50.jpeg" should exist
17 |
18 | Scenario: Specifying a recursive glob pattern
19 | Given I have a responsive_image configuration with:
20 | """
21 | sizes:
22 | - width: 100
23 | extra_images:
24 | - assets/**/*
25 | """
26 | And I have a file "index.html" with "Hello, world!"
27 | When I run Jekyll
28 | Then the image "assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should have the dimensions "100x50"
29 | And the image "assets/resized/exif-rotation-100x50.jpeg" should have the dimensions "100x50"
30 | And the image "assets/resized/progressive-100x50.jpeg" should have the dimensions "100x50"
31 | And the image "assets/resized/test-100x50.png" should have the dimensions "100x50"
32 | And the file "_site/assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should exist
33 | And the file "_site/assets/resized/exif-rotation-100x50.jpeg" should exist
34 | And the file "_site/assets/resized/progressive-100x50.jpeg" should exist
35 | And the file "_site/assets/resized/test-100x50.png" should exist
36 |
37 | Scenario: Honouring Jekyll 'source' configuration
38 | Given I have copied my site to "sub-dir/my-site-copy"
39 | And I have a configuration with:
40 | """
41 | source: sub-dir/my-site-copy
42 | responsive_image:
43 | sizes:
44 | - width: 100
45 | extra_images:
46 | - assets/*.png
47 | """
48 | And I have a file "index.html" with "Hello, world!"
49 | When I run Jekyll
50 | Then the image "sub-dir/my-site-copy/assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should have the dimensions "100x50"
51 | And the file "_site/assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should exist
52 | And the file "_site/assets/resized/progressive-100x50.jpeg" should not exist
53 |
--------------------------------------------------------------------------------
/features/image-generation.feature:
--------------------------------------------------------------------------------
1 | Feature: Responsive image generation
2 | Scenario: Resizing images
3 | Given I have a responsive_image configuration with:
4 | """
5 | template: _includes/responsive-image.html
6 | sizes:
7 | - width: 100
8 | """
9 | And I have a file "index.html" with "{% responsive_image path: assets/everybody-loves-jalapeño-pineapple-cornbread.png alt: Foobar %}"
10 | When I run Jekyll
11 | Then the image "assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should have the dimensions "100x50"
12 | And the file "_site/assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100x50.png" should exist
13 |
14 | Scenario: Handling subdirectories
15 | Given I have a responsive_image configuration with:
16 | """
17 | template: _includes/responsive-image.html
18 | output_path_format: assets/resized/%{dirname}/%{filename}-%{width}.%{extension}
19 | sizes:
20 | - width: 100
21 | """
22 | And I have a file "index.html" with:
23 | """
24 | {% responsive_image path: assets/everybody-loves-jalapeño-pineapple-cornbread.png %}
25 | {% responsive_image path: assets/subdir/test.png %}
26 | {% responsive_image path: assets/everybody-loves-jalapeño-pineapple-cornbread.png cache: true %}
27 | """
28 | When I run Jekyll
29 | Then the file "assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100.png" should exist
30 | And the file "_site/assets/resized/everybody-loves-jalapeño-pineapple-cornbread-100.png" should exist
31 | And the file "assets/resized/subdir/test-100.png" should exist
32 | And the file "_site/assets/resized/subdir/test-100.png" should exist
33 |
34 | Scenario: Honouring source image interlace mode
35 | Given I have a responsive_image configuration with:
36 | """
37 | template: _includes/responsive-image.html
38 | sizes:
39 | - width: 100
40 | """
41 | And I have a file "index.html" with "{% responsive_image path: assets/progressive.jpeg %}"
42 | When I run Jekyll
43 | Then the image "assets/resized/progressive-100x50.jpeg" should be interlaced
44 |
45 | Scenario: Honouring Jekyll 'source' configuration
46 | Given I have copied my site to "my-site-copy/src"
47 | And I have a configuration with:
48 | """
49 | source: my-site-copy/src
50 | responsive_image:
51 | base_path: assets
52 | template: _includes/responsive-image.html
53 | output_path_format: assets/resized/%{dirname}/%{width}/%{basename}
54 | sizes:
55 | - width: 100
56 | """
57 | And I have a file "my-site-copy/src/index.html" with "{% responsive_image path: assets/subdir/test.png %}"
58 | When I run Jekyll
59 | Then the image "my-site-copy/src/assets/resized/subdir/100/test.png" should have the dimensions "100x50"
60 | And the file "_site/assets/resized/subdir/100/test.png" should exist
61 |
62 | Scenario: Images can be auto-rotated based on EXIF rotation
63 | Given I have a responsive_image configuration with:
64 | """
65 | template: _includes/responsive-image.html
66 | sizes:
67 | - width: 100
68 | auto_rotate: true
69 | """
70 | And I have a file "index.html" with "{% responsive_image path: assets/exif-rotation.jpeg %}"
71 | When I run Jekyll
72 | Then the file "_site/assets/resized/exif-rotation-100x200.jpeg" should exist
73 |
74 | Scenario: Images aren't auto-rotated by default
75 | Given I have a responsive_image configuration with:
76 | """
77 | template: _includes/responsive-image.html
78 | sizes:
79 | - width: 100
80 | """
81 | And I have a file "index.html" with:
82 | """
83 | {% responsive_image path: assets/exif-rotation.jpeg %}
84 | {% responsive_image path: assets/progressive.jpeg %}
85 | """
86 | When I run Jekyll
87 | Then the file "_site/assets/resized/exif-rotation-100x50.jpeg" should exist
88 | Then the file "_site/assets/resized/progressive-100x50.jpeg" should exist
89 |
90 | Scenario: Images should not be stripped of EXIF info by default
91 | Given I have a responsive_image configuration with:
92 | """
93 | template: _includes/responsive-image.html
94 | sizes:
95 | - width: 100
96 | """
97 | And I have a file "index.html" with:
98 | """
99 | {% responsive_image path: assets/exif-rotation.jpeg %}
100 | """
101 | When I run Jekyll
102 | Then the file "_site/assets/resized/exif-rotation-100x50.jpeg" should exist
103 | Then the image "_site/assets/resized/exif-rotation-100x50.jpeg" should have an EXIF orientation
104 |
105 | Scenario: Images can be stripped of EXIF info
106 | Given I have a responsive_image configuration with:
107 | """
108 | template: _includes/responsive-image.html
109 | sizes:
110 | - width: 100
111 | strip: true
112 | """
113 | And I have a file "index.html" with:
114 | """
115 | {% responsive_image path: assets/exif-rotation.jpeg %}
116 | """
117 | When I run Jekyll
118 | Then the file "_site/assets/resized/exif-rotation-100x50.jpeg" should exist
119 | Then the image "_site/assets/resized/exif-rotation-100x50.jpeg" should have no EXIF orientation
120 |
--------------------------------------------------------------------------------
/features/image-hashes.feature:
--------------------------------------------------------------------------------
1 | Feature: Image hashes inside responsive image templates
2 | Scenario: Using the {% responsive_image %} tag
3 | Given I have copied my site to "my-site-copy/src"
4 | And I have a configuration with:
5 | """
6 | source: my-site-copy/src
7 | responsive_image:
8 | template: _includes/hash.html
9 | output_path_format: assets/resized/%{dirname}/%{width}/%{basename}
10 | sizes:
11 | - width: 100
12 | """
13 | And I have a file "my-site-copy/src/index.html" with "{% responsive_image path: assets/subdir/test.png %}"
14 | When I run Jekyll
15 | Then the file "_site/index.html" should contain:
16 | """
17 | path: assets/subdir/test.png
18 | width: 300
19 | height: 150
20 | basename: test.png
21 | dirname: subdir
22 | filename: test
23 | extension: png
24 |
25 | path: assets/resized/subdir/100/test.png
26 | width: 100
27 | height: 50
28 | basename: test.png
29 | dirname: resized/subdir/100
30 | filename: test
31 | extension: png
32 | """
33 |
--------------------------------------------------------------------------------
/features/plugin.feature:
--------------------------------------------------------------------------------
1 | Feature: General plugin usage
2 | Scenario: No config at all
3 | Given I have no configuration
4 | When I run Jekyll
5 | Then there should be no errors
6 |
7 | Scenario: Config with empty responsive_image block
8 | Given I have a responsive_image configuration with:
9 | """
10 | """
11 | When I run Jekyll
12 | Then there should be no errors
13 |
--------------------------------------------------------------------------------
/features/responsive-image-block.feature:
--------------------------------------------------------------------------------
1 | Feature: Jekyll responsive_image_block tag
2 | Scenario: Simple image tag
3 | Given I have a responsive_image configuration with "template" set to "_includes/responsive-image.html"
4 | And I have a file "index.html" with:
5 | """
6 | {% assign path = 'assets/everybody-loves-jalapeño-pineapple-cornbread.png' %}
7 | {% responsive_image_block %}
8 | path: {{ path }}
9 | title: Magic rainbow adventure!
10 | alt: Lorem ipsum
11 | {% endresponsive_image_block %}
12 | """
13 | When I run Jekyll
14 | Then I should see " " in "_site/index.html"
45 |
46 | Scenario: More complex logic in the block tag
47 | Given I have a responsive_image configuration with "template" set to "_includes/responsive-image.html"
48 | And I have a file "index.html" with:
49 | """
50 | {% assign path = 'assets/everybody-loves-jalapeño-pineapple-cornbread.png' %}
51 | {% assign alt = 'Lorem ipsum' %}
52 | {% responsive_image_block %}
53 | path: {{ path }}
54 |
55 | {% if another_alt %}
56 | alt: {{ another_alt }}
57 | {% else %}
58 | alt: {{ alt }}
59 | {% endif %}
60 | {% endresponsive_image_block %}
61 | """
62 | When I run Jekyll
63 | Then I should see " " in "_site/index.html"
18 |
19 | Scenario: Adding custom attributes
20 | Given I have a responsive_image configuration with "template" set to "_includes/responsive-image.html"
21 | And I have a file "index.html" with "{% responsive_image path: assets/everybody-loves-jalapeño-pineapple-cornbread.png alt: 'Foobar bazbar' title: 'Lorem Ipsum' %}"
22 | When I run Jekyll
23 | Then I should see "
2 |
--------------------------------------------------------------------------------
/features/test-site/_includes/custom-template.html:
--------------------------------------------------------------------------------
1 | [{{ resized | map: 'width' | join: ', ' }}]
2 |
--------------------------------------------------------------------------------
/features/test-site/_includes/hash.html:
--------------------------------------------------------------------------------
1 | path: {{ original.path }}
2 | width: {{ original.width }}
3 | height: {{ original.height }}
4 | basename: {{ original.basename }}
5 | dirname: {{ original.dirname }}
6 | filename: {{ original.filename }}
7 | extension: {{ original.extension }}
8 | {% for i in resized %}
9 | path: {{ i.path }}
10 | width: {{ i.width }}
11 | height: {{ i.height }}
12 | basename: {{ i.basename }}
13 | dirname: {{ i.dirname }}
14 | filename: {{ i.filename }}
15 | extension: {{ i.extension }}
16 | {% endfor %}
17 |
--------------------------------------------------------------------------------
/features/test-site/_includes/responsive-image.html:
--------------------------------------------------------------------------------
1 | {% assign largest = resized | sort: 'width' | last %}
2 |
3 |
4 |
--------------------------------------------------------------------------------
/features/test-site/assets/everybody-loves-jalapeño-pineapple-cornbread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wildlyinaccurate/jekyll-responsive-image/dda6d748c8b070b37900d9e0b528d11701808f76/features/test-site/assets/everybody-loves-jalapeño-pineapple-cornbread.png
--------------------------------------------------------------------------------
/features/test-site/assets/exif-rotation.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wildlyinaccurate/jekyll-responsive-image/dda6d748c8b070b37900d9e0b528d11701808f76/features/test-site/assets/exif-rotation.jpeg
--------------------------------------------------------------------------------
/features/test-site/assets/progressive.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wildlyinaccurate/jekyll-responsive-image/dda6d748c8b070b37900d9e0b528d11701808f76/features/test-site/assets/progressive.jpeg
--------------------------------------------------------------------------------
/features/test-site/assets/subdir/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wildlyinaccurate/jekyll-responsive-image/dda6d748c8b070b37900d9e0b528d11701808f76/features/test-site/assets/subdir/test.png
--------------------------------------------------------------------------------
/jekyll-responsive-image.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | lib = File.expand_path('../lib/', __FILE__)
3 | $:.unshift lib unless $:.include?(lib)
4 |
5 | require 'jekyll-responsive-image/version'
6 |
7 | Gem::Specification.new do |spec|
8 | spec.name = 'jekyll-responsive-image'
9 | spec.version = Jekyll::ResponsiveImage::VERSION
10 | spec.authors = ['Joseph Wynn']
11 | spec.email = ['joseph@wildlyinaccurate.com']
12 | spec.summary = 'Responsive image management for Jekyll'
13 | spec.homepage = 'https://github.com/wildlyinaccurate/jekyll-responsive-image'
14 | spec.licenses = ['MIT']
15 | spec.description = %q{
16 | Highly configurable Jekyll plugin for managing responsive images. Automatically
17 | resizes images and provides a Liquid template tag for loading the images with
18 | picture, img srcset, Imager.js, etc.
19 | }
20 |
21 | spec.files = `git ls-files -z lib/`.split("\u0000")
22 | spec.executables = []
23 | spec.require_paths = ['lib']
24 |
25 | spec.add_runtime_dependency 'jekyll', ['>= 2.0', "< 5.0"]
26 | spec.add_runtime_dependency 'rmagick', ['>= 2.0', '< 5.0']
27 | end
28 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image.rb:
--------------------------------------------------------------------------------
1 | require 'fileutils'
2 | require 'yaml'
3 |
4 | require 'jekyll'
5 | require 'rmagick'
6 |
7 | require 'jekyll-responsive-image/version'
8 | require 'jekyll-responsive-image/config'
9 | require 'jekyll-responsive-image/utils'
10 | require 'jekyll-responsive-image/render_cache'
11 | require 'jekyll-responsive-image/image_processor'
12 | require 'jekyll-responsive-image/resize_handler'
13 | require 'jekyll-responsive-image/renderer'
14 | require 'jekyll-responsive-image/tag'
15 | require 'jekyll-responsive-image/block'
16 | require 'jekyll-responsive-image/extra_image_generator'
17 |
18 | Liquid::Template.register_tag('responsive_image', Jekyll::ResponsiveImage::Tag)
19 | Liquid::Template.register_tag('responsive_image_block', Jekyll::ResponsiveImage::Block)
20 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/block.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class Block < Liquid::Block
4 | include Jekyll::ResponsiveImage::Utils
5 |
6 | def render(context)
7 | content = super
8 |
9 | if content.include?("\t")
10 | content = content.lines.map {|line| line.gsub(/\G[\t ]/, " ")}.join("\n")
11 | end
12 |
13 | attributes = YAML.load(content)
14 | Renderer.new(context.registers[:site], attributes).render_responsive_image
15 | end
16 | end
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/config.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class Config
4 | DEFAULTS = {
5 | 'default_quality' => 85,
6 | 'base_path' => 'assets',
7 | 'output_path_format' => 'assets/resized/%{filename}-%{width}x%{height}.%{extension}',
8 | 'sizes' => [],
9 | 'extra_images' => [],
10 | 'auto_rotate' => false,
11 | 'save_to_source' => true,
12 | 'cache' => false,
13 | 'strip' => false
14 | }
15 |
16 | def initialize(site)
17 | @site = site
18 | end
19 |
20 | def valid_config(config)
21 | config.has_key?('responsive_image') && config['responsive_image'].is_a?(Hash)
22 | end
23 |
24 | def to_h
25 | config = {}
26 |
27 | if valid_config(@site.config)
28 | config = @site.config['responsive_image']
29 | end
30 |
31 |
32 | DEFAULTS.merge(config)
33 | .merge(site_source: @site.source, site_dest: @site.dest)
34 | end
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/extra_image_generator.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class ExtraImageGenerator < Jekyll::Generator
4 | include Jekyll::ResponsiveImage::Utils
5 | include FileTest
6 |
7 | def generate(site)
8 | config = Config.new(site).to_h
9 | site_source = Pathname.new(site.source)
10 |
11 | config['extra_images'].each do |pathspec|
12 | Dir.glob(site.in_source_dir(pathspec)) do |image_path|
13 | if FileTest.file?(image_path)
14 | path = Pathname.new(image_path)
15 | relative_image_path = path.relative_path_from(site_source)
16 |
17 | result = ImageProcessor.process(relative_image_path, config)
18 | result[:resized].each { |image| keep_resized_image!(site, image) }
19 | end
20 | end
21 | end
22 | end
23 | end
24 | end
25 | end
26 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/image_processor.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class ImageProcessor
4 | include ResponsiveImage::Utils
5 |
6 | def process(image_path, config)
7 | absolute_image_path = File.expand_path(image_path.to_s, config[:site_source])
8 |
9 | Jekyll.logger.warn "Invalid image path specified: #{image_path.inspect}" unless File.file?(absolute_image_path)
10 |
11 | resize_handler = ResizeHandler.new(absolute_image_path, config)
12 |
13 | {
14 | original: image_hash(config, image_path, resize_handler.original_image.columns, resize_handler.original_image.rows),
15 | resized: resize_handler.resize_image,
16 | }
17 | end
18 |
19 | def self.process(image_path, config)
20 | self.new.process(image_path, config)
21 | end
22 | end
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/render_cache.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class RenderCache
4 | attr_accessor :store
5 |
6 | class << self
7 | def store
8 | @store ||= {}
9 | end
10 |
11 | def get(key)
12 | store[key]
13 | end
14 |
15 | def set(key, val)
16 | store[key] = val
17 | end
18 | end
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/renderer.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class Renderer
4 | include Jekyll::ResponsiveImage::Utils
5 |
6 | def initialize(site, attributes)
7 | @site = site
8 | @attributes = attributes
9 | end
10 |
11 | def render_responsive_image
12 | config = Config.new(@site).to_h
13 | use_cache = config['cache'] || @attributes['cache']
14 | cache_key = @attributes.to_s
15 | result = use_cache ? RenderCache.get(cache_key) : nil
16 |
17 | if result.nil?
18 | image = ImageProcessor.process(@attributes['path'], config)
19 | @attributes['original'] = image[:original]
20 | @attributes['resized'] = image[:resized]
21 |
22 | @attributes['resized'].each { |resized| keep_resized_image!(@site, resized) }
23 |
24 | image_template = @site.in_source_dir(@attributes['template'] || config['template'])
25 | partial = File.read(image_template)
26 | template = Liquid::Template.parse(partial)
27 |
28 | info = {
29 | registers: { site: @site }
30 | }
31 |
32 | result = template.render!(@attributes.merge(@site.site_payload), info)
33 |
34 | RenderCache.set(cache_key, result)
35 | end
36 |
37 | result
38 | end
39 | end
40 | end
41 | end
42 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/resize_handler.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class ResizeHandler
4 | include ResponsiveImage::Utils
5 |
6 | attr_reader :original_image
7 |
8 | def initialize(original_image_absolute_path, config)
9 | @config = config
10 |
11 | @original_image_absolute_path = original_image_absolute_path
12 |
13 | if @config['auto_rotate']
14 | load_full_image
15 | @original_image.auto_orient!
16 | else
17 | load_image_properties_only
18 | end
19 | end
20 |
21 | def resize_image
22 | resized = []
23 |
24 | @config['sizes'].each do |size|
25 | width = size['width']
26 | ratio = width.to_f / @original_image.columns.to_f
27 | height = (@original_image.rows.to_f * ratio).round
28 |
29 | next unless needs_resizing?(width)
30 |
31 | image_path = @original_image.filename.force_encoding(Encoding::UTF_8)
32 | filepath = format_output_path(@config['output_path_format'], @config, image_path, width, height)
33 | resized.push(image_hash(@config, filepath, width, height))
34 |
35 | site_source_filepath = File.expand_path(filepath, @config[:site_source])
36 | site_dest_filepath = File.expand_path(filepath, @config[:site_dest])
37 |
38 | if @config['save_to_source']
39 | target_filepath = site_source_filepath
40 | else
41 | target_filepath = site_dest_filepath
42 | end
43 |
44 | # Don't resize images more than once
45 | next if File.exist?(target_filepath)
46 |
47 | ensure_output_dir_exists!(target_filepath)
48 | ensure_output_dir_exists!(site_dest_filepath)
49 |
50 | Jekyll.logger.info "Generating #{target_filepath}"
51 |
52 | load_full_image unless @original_image_pixels_loaded
53 |
54 | if @config['strip']
55 | @original_image.strip!
56 | end
57 |
58 | i = @original_image.scale(ratio)
59 |
60 | quality = size['quality'] || @config['default_quality']
61 |
62 | i.write(target_filepath) do |f|
63 | f.interlace = i.interlace
64 | f.quality = quality
65 | end
66 |
67 | if @config['save_to_source']
68 | # Ensure the generated file is copied to the _site directory
69 | Jekyll.logger.info "Copying resized image to #{site_dest_filepath}"
70 | FileUtils.copy_file(site_source_filepath, site_dest_filepath)
71 | end
72 |
73 | i.destroy!
74 | end
75 |
76 | @original_image.destroy!
77 |
78 | resized
79 | end
80 |
81 | def format_output_path(format, config, image_path, width, height)
82 | params = symbolize_keys(image_hash(config, image_path, width, height))
83 |
84 | Pathname.new(format % params).cleanpath.to_s
85 | end
86 |
87 | def needs_resizing?(width)
88 | @original_image.columns > width
89 | end
90 |
91 | def load_full_image
92 | @original_image = Magick::Image::read(@original_image_absolute_path).first
93 | @original_image_pixels_loaded = true
94 | end
95 |
96 | def load_image_properties_only
97 | @original_image = Magick::Image::ping(@original_image_absolute_path).first
98 | @original_image_pixels_loaded = false
99 | end
100 |
101 | def ensure_output_dir_exists!(path)
102 | dir = File.dirname(path)
103 |
104 | unless Dir.exist?(dir)
105 | Jekyll.logger.info "Creating output directory #{dir}"
106 | FileUtils.mkdir_p(dir)
107 | end
108 | end
109 | end
110 | end
111 | end
112 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/tag.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | class Tag < Liquid::Tag
4 | def initialize(tag_name, markup, tokens)
5 | super
6 |
7 | @attributes = {}
8 |
9 | markup.scan(::Liquid::TagAttributes) do |key, value|
10 | # Strip quotes from around attribute values
11 | @attributes[key] = value.gsub(/^['"]|['"]$/, '')
12 | end
13 | end
14 |
15 | def render(context)
16 | Renderer.new(context.registers[:site], @attributes).render_responsive_image
17 | end
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/utils.rb:
--------------------------------------------------------------------------------
1 | require 'pathname'
2 |
3 | module Jekyll
4 | module ResponsiveImage
5 | module Utils
6 | def keep_resized_image!(site, image)
7 | keep_dir = File.dirname(image['path'])
8 | site.config['keep_files'] << keep_dir unless site.config['keep_files'].include?(keep_dir)
9 | end
10 |
11 | def symbolize_keys(hash)
12 | result = {}
13 | hash.each_key do |key|
14 | result[key.to_sym] = hash[key]
15 | end
16 | result
17 | end
18 |
19 | # Build a hash containing image information
20 | def image_hash(config, image_path, width, height)
21 | {
22 | 'path' => image_path,
23 | 'dirname' => relative_dirname(config, image_path),
24 | 'basename' => File.basename(image_path),
25 | 'filename' => File.basename(image_path, '.*'),
26 | 'extension' => File.extname(image_path).delete('.'),
27 | 'width' => width,
28 | 'height' => height,
29 | }
30 | end
31 |
32 | def relative_dirname(config, image_path)
33 | path = Pathname.new(File.expand_path(image_path, config[:site_source]))
34 | base = Pathname.new(File.expand_path(config['base_path'], config[:site_source]))
35 |
36 | path.relative_path_from(base).dirname.to_s.delete('.')
37 | end
38 | end
39 | end
40 | end
41 |
--------------------------------------------------------------------------------
/lib/jekyll-responsive-image/version.rb:
--------------------------------------------------------------------------------
1 | module Jekyll
2 | module ResponsiveImage
3 | VERSION = '1.6.0'.freeze
4 | end
5 | end
6 |
--------------------------------------------------------------------------------
/sample-templates/imager-js.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Render your responsive images using Imager.js (https://github.com/BBC-News/Imager.js/), with the smallest resized image used as a fallback.
3 |
4 | Usage:
5 |
6 | {% responsive_image path: assets/image.jpg alt: "A description of the image" %}
7 |
8 | (P.S. You can safely delete this comment block)
9 | {% endcomment %}
10 |
11 | {% assign smallest = resized | sort: 'width' | first %}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
26 |
--------------------------------------------------------------------------------
/sample-templates/picture.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Render your responsive images using , with the original asset used as a fallback. Note: If your original assets are not web-friendly (e.g. they are very large), you can use a resized image as the fallback instead. See the srcset-resized-fallback.html template for how to do this.
3 |
4 | Usage:
5 |
6 | {% responsive_image path: assets/image.jpg alt: "A description of the image" %}
7 |
8 | (P.S. You can safely delete this comment block)
9 | {% endcomment %}
10 |
11 |
12 | {% for i in resized %}
13 |
14 | {% endfor %}
15 |
16 |
17 |
--------------------------------------------------------------------------------
/sample-templates/srcset-resized-fallback.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Render your responsive images using , with the largest resized image used as a fallback.
3 |
4 | Usage:
5 |
6 | {% responsive_image path: assets/image.jpg alt: "A description of the image" %}
7 |
8 | (P.S. You can safely delete this comment block)
9 | {% endcomment %}
10 |
11 | {% assign largest = resized | sort: 'width' | last %}
12 | {% capture srcset %}
13 | {% for i in resized %}
14 | /{{ i.path }} {{ i.width }}w,
15 | {% endfor %}
16 | {% endcapture %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/sample-templates/srcset.html:
--------------------------------------------------------------------------------
1 | {% comment %}
2 | Render your responsive images using , with the original asset used as a fallback. Note: If your original assets are not web-friendly (e.g. they are very large), you might prefer to use the srcset-resized-fallback.html template.
3 |
4 | Usage:
5 |
6 | {% responsive_image path: assets/image.jpg alt: "A description of the image" %}
7 |
8 | (P.S. You can safely delete this comment block)
9 | {% endcomment %}
10 |
11 | {% capture srcset %}
12 | {% for i in resized %}
13 | /{{ i.path }} {{ i.width }}w,
14 | {% endfor %}
15 | {% endcapture %}
16 |
17 |
18 |
--------------------------------------------------------------------------------