jekyll_picture_tag 1.9.0 → 1.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -2,6 +2,10 @@
2
2
  ---
3
3
  # Release History
4
4
 
5
+ * 1.10.0 May 11, 2020
6
+ * **Image Cropping support!** access the power of ImageMagick's `crop` function.
7
+ * Don't issue a warning when `default` preset is not found.
8
+ * Documentation improvements
5
9
  * 1.9.0 Feb 2, 2020
6
10
  * Add `fast_build` global setting
7
11
  * Add `disabled` global setting
@@ -1,11 +1,12 @@
1
1
  ---
2
2
  ---
3
+
3
4
  # How to use the Liquid Tag:
4
5
 
5
6
  ## Format:
6
7
 
7
8
  {% raw %}
8
- `{% picture [preset] (base image) [alternate images] [attributes] %}`
9
+ `{% picture [preset] (image) [crop] [alternate images & crops] [attributes] %}`
9
10
  {% endraw %}
10
11
 
11
12
  The only required argument is the base image. Line breaks and extra spaces are fine, and you can
@@ -16,50 +17,79 @@ use liquid variables anywhere.
16
17
  {% raw %}
17
18
  `{% picture example.jpg %}`
18
19
 
19
- `{% picture thumbnail example.jpg --alt Example Image %}`
20
+ `{% picture thumbnail example.jpg 1:1 --alt Example Image %}`
20
21
 
21
- `{% picture example.jpg --picture class="attribute-demo" %}`
22
+ `{% picture example.jpg 16:9 north --picture class="attribute-demo" %}`
22
23
 
23
24
  `{% picture blog_index {{ post.image }} --link {{ post.url }} %}`
24
25
 
25
26
  `{% picture "some example.jpg" mobile: other\ example.jpg %}`
26
27
 
27
28
  ```md
28
- {% picture
29
- hero
30
- example.jpg
31
- tablet: example_cropped.jpg
32
- mobile: example_cropped_more.jpg
33
- --alt Happy Puppy
34
- --picture class="hero"
29
+ {% picture
30
+ hero
31
+ example.jpg 16:9 east
32
+ tablet: example_cropped.jpg 3:2 east
33
+ mobile: example_cropped_more.jpg 1:1-50+0 east
34
+ --alt Happy Puppy
35
+ --picture class="hero"
35
36
  --link /
36
37
  %}
37
38
  ```
39
+
38
40
  {% endraw %}
39
41
 
40
42
  ## Argument reference
41
43
 
42
44
  Given in order:
43
45
 
44
- * **Preset**
46
+ - **Preset**
45
47
 
46
48
  Select a [markup preset]({{ site.baseurl }}/presets#markup-presets), or omit to use the `default` preset. Presets
47
49
  are collections of settings that determine nearly everything about JPT's output, from the image
48
50
  formats used to the exact format your markup will take.
49
51
 
50
- * **Base Image** (Required)
52
+ - **Base Image** (Required)
51
53
 
52
54
  Can be any raster image (as long as you have the required ImageMagick delegate). Relative to
53
55
  jekyll's root directory, or the `source` [setting]({{ site.baseurl }}/global_configuration) if you've configured it.
54
56
 
55
- For filenames with spaces, either use double quotes (`"my image.jpg"`) or a backslash (`my\
56
- image.jpg`).
57
+ For filenames with spaces, either use double quotes (`"my image.jpg"`) or a backslash (`my\ image.jpg`).
58
+
59
+ - **Crop**
60
+
61
+ **Check the [ installation guide ](installation) before using this feature.**
62
+
63
+ Crop an image to a given aspect ratio or size. This argument is given as a `geometry` and
64
+ (optionally) a `gravity`, which can appear in either order and are thin wrappers around
65
+ ImageMagick's [geometry](http://www.imagemagick.org/script/command-line-processing.php#geometry)
66
+ and [gravity](http://www.imagemagick.org/script/command-line-options.php#gravity) settings. The
67
+ values given here will override the preset settings (if present), can be given after every image,
68
+ and apply only to the preceding image.
69
+
70
+ Geometry can take many forms, but most likely you'll want to set an aspect ratio-- given in the
71
+ standard `width:height` ratio such as `3:2`. Gravity sets which portion of the image to keep, and
72
+ is given in compass directions (`north`, `southeast`, etc) or `center` (default). Cropping happens
73
+ before resizing; the preset `widths` setting is a post-crop value.
57
74
 
58
- * **Alternate images**
75
+ If you'd like more fine-grained control, this can be offset by appending `+|-x` and (optionally)
76
+ `y` pixel values to the _geometry_ (not the gravity!). Example: `1:1+400 west` means "Crop to a
77
+ 1:1 aspect ratio, starting 400 pixels from the left side.", and `north 3:2+0+100` means "Crop to
78
+ 3:2, starting 100 pixels from the top." These can get a bit persnickety; there's nothing to stop
79
+ you from running off the side of the image. Pay attention.
59
80
 
60
- *Format:* `(media query preset): (filename) (...)`
81
+ For detailed documentation, see ImageMagick's
82
+ [crop](http://www.imagemagick.org/script/command-line-options.php#crop) tool.
61
83
 
62
- *Example:* `tablet: img_cropped.jpg mobile: img_cropped_more.jpg`
84
+ _Note:_ If you do a lot of trial and error with these, it's a good idea to manually delete your
85
+ generated images folder more often as each change will build a new set of images without removing
86
+ the old ones.
87
+
88
+ - **Alternate images**
89
+
90
+ _Format:_ `(media query preset): (filename) (...)`
91
+
92
+ _Example:_ `tablet: img_cropped.jpg mobile: img_cropped_more.jpg`
63
93
 
64
94
  Optionally specify any number of alternate base images for given [screen
65
95
  sizes]({{ site.baseurl }}/presets/#media-presets) (specified in `_data/picture.yml`). This is called [art
@@ -70,44 +100,44 @@ Given in order:
70
100
  be provided to the browser in reverse order, and it will select the first one with an applicable
71
101
  media query.
72
102
 
73
- * **Attributes**
103
+ - **Attributes**
74
104
 
75
105
  Optionally specify any number of HTML attributes, or an href target. These will be added to any
76
106
  attributes you've set in a preset.
77
107
 
78
- * **`--link`**
108
+ - **`--link`**
79
109
 
80
- *Format:* `--link (some url)`
110
+ _Format:_ `--link (some url)`
81
111
 
82
- *Examples*: `--link https://example.com`, `--link /blog/some_post/`
112
+ _Examples_: `--link https://example.com`, `--link /blog/some_post/`
83
113
 
84
- Wrap the image in an anchor tag, with the `href` attribute set to whatever value you give it.
85
- This will override automatic source image linking, if you have enabled it.
114
+ Wrap the image in an anchor tag, with the `href` attribute set to whatever value you give it.
115
+ This will override automatic source image linking, if you have enabled it.
86
116
 
87
- **Note**: If you get either mangled HTML or extra {::nomarkdown} tags when using this, read
88
- [here]({{ site.baseurl }}/notes).
117
+ **Note**: If you get either mangled HTML or extra {::nomarkdown} tags when using this, read
118
+ [here]({{ site.baseurl }}/notes).
89
119
 
90
- * **`--alt`**
91
-
92
- *Format:* `--alt (alt text)`
120
+ - **`--alt`**
93
121
 
94
- *Example:* `--alt Here is my alt text!`
122
+ _Format:_ `--alt (alt text)`
123
+
124
+ _Example:_ `--alt Here is my alt text!`
95
125
 
96
126
  Convenience shortcut for `--img alt="..."`
97
-
98
- * **`--(element)`**
99
127
 
100
- *Format:* `--(picture|img|source|a|parent) (Standard HTML attributes)`
128
+ - **`--(element)`**
129
+
130
+ _Format:_ `--(picture|img|source|a|parent) (Standard HTML attributes)`
101
131
 
102
- *Example:* `--img class="awesome-fade-in" id="coolio" --a data-awesomeness="11"`
132
+ _Example:_ `--img class="awesome-fade-in" id="coolio" --a data-awesomeness="11"`
103
133
 
104
134
  Apply attributes to a given HTML element. Your options are:
105
135
 
106
- * `picture`
107
- * `img`
108
- * `source`
109
- * `a` (anchor tag)
110
- * `parent`
136
+ - `picture`
137
+ - `img`
138
+ - `source`
139
+ - `a` (anchor tag)
140
+ - `parent`
111
141
 
112
142
  `--parent` will be applied to the `<picture>` if present, otherwise the `<img>`; useful when
113
143
  using an `auto` output format.
@@ -38,9 +38,10 @@ Gem::Specification.new do |spec|
38
38
  spec.add_development_dependency 'solargraph'
39
39
 
40
40
  spec.add_dependency 'addressable', '~> 2.6'
41
+ spec.add_dependency 'base32', '~> 0.3'
41
42
  spec.add_dependency 'mime-types', '~> 3'
42
43
  spec.add_dependency 'mini_magick', '~> 4'
43
- spec.add_dependency 'objective_elements', '~> 1.1.1'
44
+ spec.add_dependency 'objective_elements', '~> 1.1.2'
44
45
 
45
46
  spec.add_runtime_dependency 'jekyll', '< 5'
46
47
  end
@@ -7,3 +7,4 @@ noscript: false
7
7
  link_source: false
8
8
  quality: 75
9
9
  data_sizes: true
10
+ gravity: center
@@ -1,4 +1,5 @@
1
1
  require 'mini_magick'
2
+ require 'base32'
2
3
 
3
4
  module PictureTag
4
5
  # Represents a generated image file.
@@ -6,10 +7,12 @@ module PictureTag
6
7
  attr_reader :width, :format
7
8
  include MiniMagick
8
9
 
9
- def initialize(source_file:, width:, format:)
10
+ def initialize(source_file:, width:, format:, crop: nil, gravity: '')
10
11
  @source = source_file
11
12
  @width = width
12
13
  @format = process_format format
14
+ @crop = crop
15
+ @gravity = gravity
13
16
  end
14
17
 
15
18
  def exists?
@@ -38,6 +41,10 @@ module PictureTag
38
41
  ImgURI.new(name).to_s
39
42
  end
40
43
 
44
+ def cropped_source_width
45
+ image.width
46
+ end
47
+
41
48
  private
42
49
 
43
50
  # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
@@ -57,7 +64,19 @@ module PictureTag
57
64
  # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
58
65
  # ^^^^
59
66
  def name_right
60
- '.' + @format
67
+ crop_code + '.' + @format
68
+ end
69
+
70
+ # Hash the crop settings, so we can detect when they change. We use a
71
+ # base32 encoding scheme to pack more information into fewer characters,
72
+ # without dealing with various filesystem naming limitations that would crop
73
+ # up using base64 (such as NTFS being case insensitive).
74
+ def crop_code
75
+ return '' unless @crop
76
+
77
+ Base32.encode(
78
+ Digest::MD5.hexdigest(@crop + @gravity)
79
+ )[0..2]
61
80
  end
62
81
 
63
82
  # Used for the fast build option: look for a file which matches everything
@@ -77,15 +96,12 @@ module PictureTag
77
96
 
78
97
  # Returns a list of images which are probably correct.
79
98
  def dest_glob
80
- Dir.glob File.join(PictureTag.dest_dir, name_left + '?' * 6 + name_right)
81
- end
99
+ query = File.join(
100
+ PictureTag.dest_dir,
101
+ name_left + '?' * 6 + name_right
102
+ )
82
103
 
83
- def image
84
- @image ||= Image.open(@source.name)
85
- end
86
-
87
- def dest_dir
88
- File.dirname absolute_filename
104
+ Dir.glob query
89
105
  end
90
106
 
91
107
  def generate_image
@@ -94,10 +110,26 @@ module PictureTag
94
110
  write_image
95
111
  end
96
112
 
113
+ def image
114
+ @image ||= open_image
115
+ end
116
+
117
+ def open_image
118
+ image_base = Image.open(@source.name)
119
+ image_base.combine_options do |i|
120
+ i.auto_orient
121
+ if @crop
122
+ i.gravity @gravity
123
+ i.crop @crop
124
+ end
125
+ end
126
+
127
+ image_base
128
+ end
129
+
97
130
  def process_image
98
131
  image.combine_options do |i|
99
132
  i.resize "#{@width}x"
100
- i.auto_orient
101
133
  i.strip
102
134
  end
103
135
 
@@ -106,16 +138,16 @@ module PictureTag
106
138
  end
107
139
 
108
140
  def write_image
109
- check_dest_dir
141
+ # Make sure destination directory exists:
142
+ FileUtils.mkdir_p(dest_dir) unless Dir.exist?(dest_dir)
110
143
 
111
144
  image.write absolute_filename
112
145
 
113
146
  FileUtils.chmod(0o644, absolute_filename)
114
147
  end
115
148
 
116
- # Make sure destination directory exists
117
- def check_dest_dir
118
- FileUtils.mkdir_p(dest_dir) unless Dir.exist?(dest_dir)
149
+ def dest_dir
150
+ File.dirname absolute_filename
119
151
  end
120
152
 
121
153
  def process_format(format)
@@ -3,3 +3,4 @@ require_relative './instructions/configuration'
3
3
  require_relative './instructions/html_attributes'
4
4
  require_relative './instructions/preset'
5
5
  require_relative './instructions/tag_parser'
6
+ require_relative './instructions/arg_splitter'
@@ -0,0 +1,68 @@
1
+ module PictureTag
2
+ module Instructions
3
+ # This class takes in the arguments passed to the liquid tag, and splits it
4
+ # up into 'words' (correctly handling quotes and backslash escapes.)
5
+ #
6
+ # To handle quotes and backslash escaping, we have to parse the string by
7
+ # characters to break it up correctly. I'm sure there's a library to do
8
+ # this, but it's not that much code honestly. If this starts getting big,
9
+ # we'll pull in a new dependency.
10
+ #
11
+ class ArgSplitter
12
+ attr_reader :words
13
+
14
+ def initialize(raw_params)
15
+ @words = []
16
+ @word = ''
17
+ @in_quotes = false
18
+ @escaped = false
19
+
20
+ raw_params.each_char { |c| handle_char(c) }
21
+
22
+ add_word # We have to explicitly add the last one.
23
+ end
24
+
25
+ private
26
+
27
+ def handle_char(char)
28
+ # last character was a backslash:
29
+ if @escaped
30
+ close_escape char
31
+
32
+ # char is a backslash or a quote:
33
+ elsif char.match?(/["\\]/)
34
+ handle_special char
35
+
36
+ # Character isn't whitespace, or it's inside double quotes:
37
+ elsif @in_quotes || char.match(/\S/)
38
+ @word << char
39
+
40
+ # Character is whitespace outside of double quotes:
41
+ else
42
+ add_word
43
+ end
44
+ end
45
+
46
+ def add_word
47
+ return if @word.empty?
48
+
49
+ @words << @word
50
+ @word = ''
51
+ end
52
+
53
+ def handle_special(char)
54
+ if char == '\\'
55
+ @escaped = true
56
+ elsif char == '"'
57
+ @in_quotes = !@in_quotes
58
+ @word << char
59
+ end
60
+ end
61
+
62
+ def close_escape(char)
63
+ @word << char
64
+ @escaped = false
65
+ end
66
+ end
67
+ end
68
+ end
@@ -12,13 +12,6 @@ module PictureTag
12
12
  @content[key]
13
13
  end
14
14
 
15
- # Returns the set of widths to use for a given media query.
16
- def widths(media)
17
- width_hash = self['media_widths'] || {}
18
- width_hash.default = self['widths']
19
- width_hash[media]
20
- end
21
-
22
15
  def formats
23
16
  @content['formats']
24
17
  end
@@ -41,15 +34,38 @@ module PictureTag
41
34
  end
42
35
  end
43
36
 
37
+ # Image widths to generate for a given media query.
38
+ def widths(media = nil)
39
+ setting_lookup('widths', 'media', media)
40
+ end
41
+
42
+ # Image quality setting, possibly dependent on format.
44
43
  def quality(format = nil)
45
- qualities = @content['format_quality'] || {}
46
- qualities.default = @content['quality']
44
+ setting_lookup('quality', 'format', format)
45
+ end
46
+
47
+ # Gravity setting (for imagemagick cropping)
48
+ def gravity(media = nil)
49
+ setting_lookup('gravity', 'media', media)
50
+ end
47
51
 
48
- qualities[format]
52
+ # Crop value
53
+ def crop(media = nil)
54
+ setting_lookup('crop', 'media', media)
49
55
  end
50
56
 
51
57
  private
52
58
 
59
+ # Return arbitrary setting values, taking their defaults into account.
60
+ # Ex: quality can be set for all image formats, or individually per
61
+ # format. Per-format settings should override the general setting.
62
+ def setting_lookup(setting, prefix, lookup)
63
+ media_values = @content[prefix + '_' + setting] || {}
64
+ media_values.default = @content[setting]
65
+
66
+ media_values[lookup]
67
+ end
68
+
53
69
  def build_preset
54
70
  # The _data/picture.yml file is optional.
55
71
  picture_data_file = grab_data_file
@@ -70,10 +86,14 @@ module PictureTag
70
86
  end
71
87
 
72
88
  def no_preset
73
- Utils.warning(
74
- " Preset \"#{@name}\" not found in #{PictureTag.config['data_dir']}/"\
75
- + 'picture.yml under markup_presets key. Using default values.'
76
- )
89
+ unless @name == 'default'
90
+ Utils.warning(
91
+ <<~HEREDOC
92
+ Preset "#{@name}" not found in {PictureTag.config['data_dir']}/picture.yml
93
+ under markup_presets key. Using default values."
94
+ HEREDOC
95
+ )
96
+ end
77
97
 
78
98
  {}
79
99
  end