jekyll_picture_tag 1.10.0 → 1.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +79 -0
  3. data/docs/Gemfile +4 -2
  4. data/docs/Gemfile.lock +14 -12
  5. data/docs/_config.yml +6 -10
  6. data/docs/devs/contributing/code.md +44 -0
  7. data/docs/devs/contributing/docs.md +13 -0
  8. data/docs/devs/contributing/index.md +15 -0
  9. data/docs/devs/contributing/setup.md +13 -0
  10. data/docs/devs/contributing/testing.md +41 -0
  11. data/docs/devs/index.md +7 -0
  12. data/docs/{releases.md → devs/releases.md} +37 -13
  13. data/docs/index.md +42 -26
  14. data/docs/users/configuration/cdn.md +35 -0
  15. data/docs/users/configuration/directories.md +34 -0
  16. data/docs/users/configuration/disable.md +24 -0
  17. data/docs/users/configuration/fast_build.md +28 -0
  18. data/docs/users/configuration/ignore_missing.md +23 -0
  19. data/docs/users/configuration/index.md +29 -0
  20. data/docs/users/configuration/kramdown_fix.md +20 -0
  21. data/docs/users/configuration/relative_urls.md +15 -0
  22. data/docs/users/configuration/suppress_warnings.md +16 -0
  23. data/docs/users/index.md +7 -0
  24. data/docs/users/installation.md +52 -0
  25. data/docs/users/liquid_tag/argument_reference/alternate_images.md +18 -0
  26. data/docs/users/liquid_tag/argument_reference/attributes.md +42 -0
  27. data/docs/users/liquid_tag/argument_reference/base_image.md +12 -0
  28. data/docs/users/liquid_tag/argument_reference/crop.md +48 -0
  29. data/docs/users/liquid_tag/argument_reference/link.md +16 -0
  30. data/docs/users/liquid_tag/argument_reference/preset.md +17 -0
  31. data/docs/users/liquid_tag/argument_reference/readme.md +9 -0
  32. data/docs/users/liquid_tag/examples.md +93 -0
  33. data/docs/users/liquid_tag/index.md +31 -0
  34. data/docs/users/notes/git_lfs.md +7 -0
  35. data/docs/users/notes/github_pages.md +5 -0
  36. data/docs/users/notes/html_attributes.md +5 -0
  37. data/docs/users/notes/index.md +6 -0
  38. data/docs/users/notes/input_checking.md +6 -0
  39. data/docs/users/notes/kramdown_bug.md +41 -0
  40. data/docs/users/notes/managing_images.md +21 -0
  41. data/docs/{migration.md → users/notes/migration.md} +0 -0
  42. data/docs/users/presets/cropping.md +61 -0
  43. data/docs/users/presets/default.md +23 -0
  44. data/docs/users/presets/examples.md +79 -0
  45. data/docs/users/presets/fallback_image.md +28 -0
  46. data/docs/users/presets/html_attributes.md +26 -0
  47. data/docs/users/presets/image_formats.md +21 -0
  48. data/docs/users/presets/image_quality.md +105 -0
  49. data/docs/users/presets/index.md +101 -0
  50. data/docs/users/presets/link_source.md +16 -0
  51. data/docs/users/presets/markup_formats/fragments.md +48 -0
  52. data/docs/users/presets/markup_formats/javascript_friendly.md +57 -0
  53. data/docs/users/presets/markup_formats/readme.md +43 -0
  54. data/docs/users/presets/markup_formats/standard_html.md +25 -0
  55. data/docs/users/presets/media_queries.md +36 -0
  56. data/docs/users/presets/nomarkdown_override.md +17 -0
  57. data/docs/users/presets/pixel_ratio_srcsets.md +32 -0
  58. data/docs/users/presets/quality_width_graph.png +0 -0
  59. data/docs/users/presets/width_height_attributes.md +34 -0
  60. data/docs/users/presets/width_srcsets.md +85 -0
  61. data/jekyll_picture_tag.gemspec +4 -5
  62. data/lib/jekyll_picture_tag.rb +3 -4
  63. data/lib/jekyll_picture_tag/cache.rb +3 -0
  64. data/lib/jekyll_picture_tag/cache/base.rb +59 -0
  65. data/lib/jekyll_picture_tag/cache/generated.rb +20 -0
  66. data/lib/jekyll_picture_tag/cache/source.rb +19 -0
  67. data/lib/jekyll_picture_tag/defaults/presets.yml +1 -0
  68. data/lib/jekyll_picture_tag/images.rb +3 -0
  69. data/lib/jekyll_picture_tag/images/generated_image.rb +127 -0
  70. data/lib/jekyll_picture_tag/{img_uri.rb → images/img_uri.rb} +1 -0
  71. data/lib/jekyll_picture_tag/images/source_image.rb +103 -0
  72. data/lib/jekyll_picture_tag/instructions/arg_splitter.rb +3 -2
  73. data/lib/jekyll_picture_tag/instructions/configuration.rb +1 -1
  74. data/lib/jekyll_picture_tag/instructions/preset.rb +24 -4
  75. data/lib/jekyll_picture_tag/instructions/set.rb +5 -1
  76. data/lib/jekyll_picture_tag/output_formats/basic.rb +16 -14
  77. data/lib/jekyll_picture_tag/output_formats/img.rb +11 -0
  78. data/lib/jekyll_picture_tag/output_formats/picture.rb +22 -0
  79. data/lib/jekyll_picture_tag/router.rb +3 -2
  80. data/lib/jekyll_picture_tag/srcsets/basic.rb +10 -1
  81. data/lib/jekyll_picture_tag/utils.rb +14 -0
  82. data/lib/jekyll_picture_tag/version.rb +1 -1
  83. data/readme.md +9 -7
  84. metadata +78 -45
  85. data/docs/_layouts/directory.html +0 -32
  86. data/docs/assets/style.css +0 -31
  87. data/docs/contributing.md +0 -109
  88. data/docs/example_presets.md +0 -116
  89. data/docs/global_configuration.md +0 -173
  90. data/docs/installation.md +0 -45
  91. data/docs/notes.md +0 -91
  92. data/docs/output.md +0 -63
  93. data/docs/presets.md +0 -361
  94. data/docs/usage.md +0 -143
  95. data/lib/jekyll_picture_tag/generated_image.rb +0 -161
  96. data/lib/jekyll_picture_tag/source_image.rb +0 -87
@@ -0,0 +1,19 @@
1
+ module PictureTag
2
+ module Cache
3
+ # Caches source image details, so we can skip expensive operations whenever
4
+ # possible.
5
+ class Source
6
+ include Base
7
+
8
+ private
9
+
10
+ def template
11
+ { digest: nil, width: nil, height: nil }
12
+ end
13
+
14
+ def cache_dir
15
+ 'source'
16
+ end
17
+ end
18
+ end
19
+ end
@@ -8,3 +8,4 @@ link_source: false
8
8
  quality: 75
9
9
  data_sizes: true
10
10
  gravity: center
11
+ dimension_attributes: false
@@ -0,0 +1,3 @@
1
+ require_relative 'images/generated_image'
2
+ require_relative 'images/img_uri'
3
+ require_relative 'images/source_image'
@@ -0,0 +1,127 @@
1
+ require 'mini_magick'
2
+
3
+ module PictureTag
4
+ # Represents a generated image file.
5
+ class GeneratedImage
6
+ attr_reader :width, :format
7
+
8
+ include MiniMagick
9
+
10
+ def initialize(source_file:, width:, format:, crop: nil, gravity: '')
11
+ @source = source_file
12
+ @width = width
13
+ @format = process_format format
14
+ @crop = crop
15
+ @gravity = gravity
16
+ end
17
+
18
+ def exists?
19
+ File.exist?(absolute_filename)
20
+ end
21
+
22
+ def generate
23
+ generate_image unless @source.missing || exists?
24
+ end
25
+
26
+ # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
27
+ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
28
+ def absolute_filename
29
+ @absolute_filename ||= File.join(PictureTag.dest_dir, name)
30
+ end
31
+
32
+ # /home/dave/my_blog/_site/generated/somefolder/myimage-100-123abc.jpg
33
+ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34
+ def name
35
+ @name ||= "#{@source.base_name}-#{@width}-#{id}.#{@format}"
36
+ end
37
+
38
+ # https://example.com/assets/somefolder/myimage-100-123abc.jpg
39
+ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
40
+ def uri
41
+ ImgURI.new(name).to_s
42
+ end
43
+
44
+ # Post crop
45
+ def source_width
46
+ update_cache unless cache[:width]
47
+
48
+ cache[:width]
49
+ end
50
+
51
+ # Post crop
52
+ def source_height
53
+ update_cache unless cache[:height]
54
+
55
+ cache[:height]
56
+ end
57
+
58
+ private
59
+
60
+ # We exclude width and format from the cache name, since it isn't specific to them.
61
+ def cache
62
+ @cache ||= Cache::Generated.new("#{@source.base_name}-#{id}")
63
+ end
64
+
65
+ def update_cache
66
+ return if @source.missing
67
+
68
+ # Ensure it's generated:
69
+ image
70
+
71
+ cache[:width] = @source_dimensions[:width]
72
+ cache[:height] = @source_dimensions[:height]
73
+
74
+ cache.write
75
+ end
76
+
77
+ # Hash all inputs and truncate, so we know when they change without getting too long.
78
+ # /home/dave/my_blog/_site/generated/somefolder/myimage-100-1234abcde.jpg
79
+ # ^^^^^^^^^
80
+ def id
81
+ @id ||= Digest::MD5.hexdigest([@source.digest, @crop, @gravity, quality].join)[0..8]
82
+ end
83
+
84
+ # Post crop, before resizing and reformatting
85
+ def image
86
+ @image ||= open_image
87
+ end
88
+
89
+ def open_image
90
+ image_base = Image.open(@source.name)
91
+ image_base.combine_options do |i|
92
+ i.auto_orient
93
+ if @crop
94
+ i.gravity @gravity
95
+ i.crop @crop
96
+ end
97
+ end
98
+
99
+ @source_dimensions = { width: image_base.width, height: image_base.height }
100
+
101
+ image_base
102
+ end
103
+
104
+ def generate_image
105
+ puts 'Generating new image file: ' + name
106
+
107
+ image.format(@format, 0, { resize: "#{@width}x", quality: quality })
108
+ FileUtils.mkdir_p(File.dirname(absolute_filename))
109
+
110
+ image.write absolute_filename
111
+
112
+ FileUtils.chmod(0o644, absolute_filename)
113
+ end
114
+
115
+ def quality
116
+ PictureTag.quality(@format, @width)
117
+ end
118
+
119
+ def process_format(format)
120
+ if format.casecmp('original').zero?
121
+ @source.ext
122
+ else
123
+ format.downcase
124
+ end
125
+ end
126
+ end
127
+ end
@@ -8,6 +8,7 @@ module PictureTag
8
8
  # image. Call to_s on it to get the link.
9
9
  class ImgURI
10
10
  attr_reader :filename, :source_image
11
+
11
12
  def initialize(filename, source_image: false)
12
13
  @source_image = source_image
13
14
  @filename = filename
@@ -0,0 +1,103 @@
1
+ module PictureTag
2
+ # Handles a given source image file and its properties. Provides a speed
3
+ # advantage by storing expensive file reads and writes in instance variables,
4
+ # to be reused by many different generated images.
5
+ class SourceImage
6
+ attr_reader :shortname, :missing, :media_preset
7
+
8
+ include MiniMagick
9
+
10
+ def initialize(relative_filename, media_preset = nil)
11
+ # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
12
+ # ^^^^^^^^^^^^^^^^^^^^^^
13
+ @shortname = relative_filename
14
+ @media_preset = media_preset
15
+
16
+ @missing = missing?
17
+ check_cache
18
+ end
19
+
20
+ def digest
21
+ @digest ||= cache[:digest] || ''
22
+ end
23
+
24
+ def width
25
+ @width ||= cache[:width] || 999_999
26
+ end
27
+
28
+ def height
29
+ @height ||= cache[:height] || 999_999
30
+ end
31
+
32
+ # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
33
+ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
34
+ def name
35
+ @name ||= File.join(PictureTag.source_dir, @shortname)
36
+ end
37
+
38
+ # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
39
+ # ^^^^^^^^^^^^^^^^^^
40
+ def base_name
41
+ @shortname.delete_suffix File.extname(@shortname)
42
+ end
43
+
44
+ # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
45
+ # ^^^
46
+ def ext
47
+ @ext ||= File.extname(name)[1..-1].downcase
48
+ end
49
+
50
+ private
51
+
52
+ def cache
53
+ @cache ||= Cache::Source.new(@shortname)
54
+ end
55
+
56
+ def missing?
57
+ if File.exist? name
58
+ false
59
+
60
+ elsif PictureTag.continue_on_missing?
61
+ Utils.warning(missing_image_warning)
62
+ true
63
+
64
+ else
65
+ raise ArgumentError, missing_image_error
66
+ end
67
+ end
68
+
69
+ def check_cache
70
+ return if @missing
71
+ return if cache[:digest] && PictureTag.fast_build?
72
+
73
+ update_cache if source_digest != cache[:digest]
74
+ end
75
+
76
+ def update_cache
77
+ cache[:digest] = source_digest
78
+ cache[:width] = image.width
79
+ cache[:height] = image.height
80
+
81
+ cache.write
82
+ end
83
+
84
+ def image
85
+ @image ||= Image.open(name)
86
+ end
87
+
88
+ def source_digest
89
+ @source_digest ||= Digest::MD5.hexdigest(File.read(name))
90
+ end
91
+
92
+ def missing_image_warning
93
+ "JPT Could not find #{name}. Your site will have broken images. Continuing."
94
+ end
95
+
96
+ def missing_image_error
97
+ <<~HEREDOC
98
+ Could not find #{name}. You can force the build to continue anyway by
99
+ setting "picture: ignore_missing_images: true" in "_config.yml".
100
+ HEREDOC
101
+ end
102
+ end
103
+ end
@@ -51,9 +51,10 @@ module PictureTag
51
51
  end
52
52
 
53
53
  def handle_special(char)
54
- if char == '\\'
54
+ case char
55
+ when '\\'
55
56
  @escaped = true
56
- elsif char == '"'
57
+ when '"'
57
58
  @in_quotes = !@in_quotes
58
59
  @word << char
59
60
  end
@@ -30,7 +30,7 @@ module PictureTag
30
30
  # source_dest is the jekyll-picture-tag destination directory. (generated
31
31
  # file location setting.)
32
32
  def dest_dir
33
- File.join PictureTag.site.dest, pconfig['output']
33
+ File.join PictureTag.site.config['destination'], pconfig['output']
34
34
  end
35
35
 
36
36
  def nomarkdown?
@@ -3,6 +3,7 @@ module PictureTag
3
3
  # Handles the specific tag image set to construct.
4
4
  class Preset
5
5
  attr_reader :name
6
+
6
7
  def initialize(name)
7
8
  @name = name
8
9
  @content = build_preset
@@ -39,9 +40,24 @@ module PictureTag
39
40
  setting_lookup('widths', 'media', media)
40
41
  end
41
42
 
42
- # Image quality setting, possibly dependent on format.
43
- def quality(format = nil)
44
- setting_lookup('quality', 'format', format)
43
+ # Image quality setting. Surprisingly complicated; can depend on both format and width.
44
+ def quality(format = nil, width = nil)
45
+ setting = setting_lookup('quality', 'format', format)
46
+
47
+ return setting unless setting.is_a? Hash
48
+
49
+ parse_quality_hash(setting, width)
50
+ end
51
+
52
+ def parse_quality_hash(points, width)
53
+ # The points can be given in any order.
54
+ low, high = *points.keys.map(&:to_i).sort
55
+
56
+ case width
57
+ when 0..low then points[low]
58
+ when low..high then Utils.interpolate(points.keys, points.values, width)
59
+ when high..999_999 then points[high]
60
+ end
45
61
  end
46
62
 
47
63
  # Gravity setting (for imagemagick cropping)
@@ -80,9 +96,13 @@ module PictureTag
80
96
  end
81
97
 
82
98
  def grab_data_file
99
+ search_data('presets') || search_data('markup_presets') || no_preset
100
+ end
101
+
102
+ def search_data(key)
83
103
  PictureTag.site
84
104
  .data
85
- .dig('picture', 'markup_presets', @name) || no_preset
105
+ .dig('picture', key, @name)
86
106
  end
87
107
 
88
108
  def no_preset
@@ -27,7 +27,7 @@ module PictureTag
27
27
  # These are our Media Query presets. It's really just a hash, and there
28
28
  # are no default values, so extracting this to its own class is overkill.
29
29
  def media_presets
30
- PictureTag.site.data.dig('picture', 'media_presets') || {}
30
+ search_data('media_queries') || search_data('media_presets') || {}
31
31
  end
32
32
 
33
33
  def source_images
@@ -52,6 +52,10 @@ module PictureTag
52
52
 
53
53
  private
54
54
 
55
+ def search_data(key)
56
+ PictureTag.site.data.dig('picture', key)
57
+ end
58
+
55
59
  def build_source_images
56
60
  source_names = params.source_names
57
61
  media_presets = params.media_presets
@@ -6,6 +6,10 @@ module PictureTag
6
6
  class Basic
7
7
  include ObjectiveElements
8
8
 
9
+ def to_s
10
+ wrap(base_markup).to_s
11
+ end
12
+
9
13
  # Used for both the fallback image, and for the complete markup.
10
14
  def build_base_img
11
15
  img = SingleTag.new 'img'
@@ -23,10 +27,6 @@ module PictureTag
23
27
  img
24
28
  end
25
29
 
26
- def to_s
27
- wrap(base_markup).to_s
28
- end
29
-
30
30
  private
31
31
 
32
32
  # Handles various wrappers around basic markup
@@ -73,24 +73,26 @@ module PictureTag
73
73
  def build_fallback_image
74
74
  return fallback_candidate if fallback_candidate.exists?
75
75
 
76
- build_new_fallback_image
77
- end
78
-
79
- def fallback_candidate
80
- @fallback_candidate ||= GeneratedImage.new(
76
+ image = GeneratedImage.new(
81
77
  source_file: PictureTag.source_images.first,
82
78
  format: PictureTag.fallback_format,
83
- width: PictureTag.fallback_width,
79
+ width: checked_fallback_width,
84
80
  crop: PictureTag.crop,
85
81
  gravity: PictureTag.gravity
86
82
  )
83
+
84
+ image.generate
85
+
86
+ image
87
87
  end
88
88
 
89
- def build_new_fallback_image
90
- GeneratedImage.new(
89
+ # It's only a candidate, because we don't know if the fallback width
90
+ # setting is larger than the source file.
91
+ def fallback_candidate
92
+ @fallback_candidate ||= GeneratedImage.new(
91
93
  source_file: PictureTag.source_images.first,
92
94
  format: PictureTag.fallback_format,
93
- width: checked_fallback_width,
95
+ width: PictureTag.fallback_width,
94
96
  crop: PictureTag.crop,
95
97
  gravity: PictureTag.gravity
96
98
  )
@@ -119,7 +121,7 @@ module PictureTag
119
121
 
120
122
  def source_width
121
123
  if PictureTag.crop
122
- fallback_candidate.cropped_source_width
124
+ fallback_candidate.source_width
123
125
  else
124
126
  source.width
125
127
  end
@@ -3,6 +3,8 @@ module PictureTag
3
3
  # Represents a bare <img> tag with a srcset attribute.
4
4
  # Used when <picture> is unnecessary.
5
5
  class Img < Basic
6
+ private
7
+
6
8
  def srcset
7
9
  @srcset ||= build_srcset(
8
10
  PictureTag.source_images.first, PictureTag.formats.first
@@ -17,8 +19,17 @@ module PictureTag
17
19
 
18
20
  img.attributes << PictureTag.html_attributes['parent']
19
21
 
22
+ add_dimensions(img, srcset)
23
+
20
24
  img
21
25
  end
26
+
27
+ def add_dimensions(img, srcset)
28
+ return unless PictureTag.preset['dimension_attributes']
29
+
30
+ img.width = srcset.width_attribute
31
+ img.height = srcset.height_attribute
32
+ end
22
33
  end
23
34
  end
24
35
  end