jekyll_picture_tag 1.10.1 → 1.14.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.github/workflows/code-checks.yml +43 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +30 -0
  6. data/.ruby-version +1 -1
  7. data/docs/.envrc +2 -0
  8. data/docs/Gemfile +4 -2
  9. data/docs/Gemfile.lock +14 -12
  10. data/docs/_config.yml +6 -10
  11. data/docs/devs/contributing/code.md +46 -0
  12. data/docs/devs/contributing/docs.md +31 -0
  13. data/docs/devs/contributing/index.md +15 -0
  14. data/docs/devs/contributing/setup.md +33 -0
  15. data/docs/devs/contributing/testing.md +34 -0
  16. data/docs/devs/index.md +7 -0
  17. data/docs/{releases.md → devs/releases.md} +44 -15
  18. data/docs/index.md +42 -26
  19. data/docs/users/configuration/directories.md +34 -0
  20. data/docs/users/configuration/disable.md +24 -0
  21. data/docs/users/configuration/fast_build.md +28 -0
  22. data/docs/users/configuration/ignore_missing.md +23 -0
  23. data/docs/users/configuration/index.md +29 -0
  24. data/docs/users/configuration/kramdown_fix.md +20 -0
  25. data/docs/users/configuration/suppress_warnings.md +16 -0
  26. data/docs/users/configuration/urls.md +69 -0
  27. data/docs/users/index.md +7 -0
  28. data/docs/users/installation.md +52 -0
  29. data/docs/users/liquid_tag/argument_reference/alternate_images.md +18 -0
  30. data/docs/users/liquid_tag/argument_reference/attributes.md +42 -0
  31. data/docs/users/liquid_tag/argument_reference/base_image.md +12 -0
  32. data/docs/users/liquid_tag/argument_reference/crop.md +48 -0
  33. data/docs/users/liquid_tag/argument_reference/link.md +16 -0
  34. data/docs/users/liquid_tag/argument_reference/preset.md +17 -0
  35. data/docs/users/liquid_tag/argument_reference/readme.md +9 -0
  36. data/docs/users/liquid_tag/examples.md +93 -0
  37. data/docs/users/liquid_tag/index.md +31 -0
  38. data/docs/users/notes/git_lfs.md +7 -0
  39. data/docs/users/notes/github_pages.md +5 -0
  40. data/docs/users/notes/html_attributes.md +5 -0
  41. data/docs/users/notes/index.md +6 -0
  42. data/docs/users/notes/input_checking.md +6 -0
  43. data/docs/users/notes/kramdown_bug.md +41 -0
  44. data/docs/users/notes/managing_images.md +21 -0
  45. data/docs/{migration.md → users/notes/migration.md} +0 -0
  46. data/docs/users/presets/cropping.md +61 -0
  47. data/docs/users/presets/default.md +24 -0
  48. data/docs/users/presets/examples.md +79 -0
  49. data/docs/users/presets/fallback_image.md +28 -0
  50. data/docs/users/presets/html_attributes.md +26 -0
  51. data/docs/users/presets/image_formats.md +21 -0
  52. data/docs/users/presets/image_quality.md +105 -0
  53. data/docs/users/presets/index.md +101 -0
  54. data/docs/users/presets/link_source.md +16 -0
  55. data/docs/users/presets/markup_formats/fragments.md +48 -0
  56. data/docs/users/presets/markup_formats/javascript_friendly.md +57 -0
  57. data/docs/users/presets/markup_formats/readme.md +43 -0
  58. data/docs/users/presets/markup_formats/standard_html.md +25 -0
  59. data/docs/users/presets/media_queries.md +36 -0
  60. data/docs/users/presets/nomarkdown_override.md +17 -0
  61. data/docs/users/presets/pixel_ratio_srcsets.md +32 -0
  62. data/docs/users/presets/quality_width_graph.png +0 -0
  63. data/docs/users/presets/strip_metadata.md +13 -0
  64. data/docs/users/presets/width_height_attributes.md +34 -0
  65. data/docs/users/presets/width_srcsets.md +85 -0
  66. data/install_imagemagick.sh +23 -0
  67. data/jekyll_picture_tag.gemspec +10 -6
  68. data/lib/jekyll_picture_tag.rb +5 -5
  69. data/lib/jekyll_picture_tag/cache.rb +3 -0
  70. data/lib/jekyll_picture_tag/cache/base.rb +61 -0
  71. data/lib/jekyll_picture_tag/cache/generated.rb +20 -0
  72. data/lib/jekyll_picture_tag/cache/source.rb +19 -0
  73. data/lib/jekyll_picture_tag/defaults/global.yml +2 -0
  74. data/lib/jekyll_picture_tag/defaults/presets.yml +2 -0
  75. data/lib/jekyll_picture_tag/images.rb +3 -0
  76. data/lib/jekyll_picture_tag/images/generated_image.rb +130 -0
  77. data/lib/jekyll_picture_tag/{img_uri.rb → images/img_uri.rb} +4 -1
  78. data/lib/jekyll_picture_tag/images/source_image.rb +103 -0
  79. data/lib/jekyll_picture_tag/instructions/arg_splitter.rb +3 -2
  80. data/lib/jekyll_picture_tag/instructions/preset.rb +24 -4
  81. data/lib/jekyll_picture_tag/instructions/set.rb +5 -1
  82. data/lib/jekyll_picture_tag/output_formats/basic.rb +16 -14
  83. data/lib/jekyll_picture_tag/output_formats/img.rb +11 -0
  84. data/lib/jekyll_picture_tag/output_formats/picture.rb +22 -0
  85. data/lib/jekyll_picture_tag/router.rb +3 -2
  86. data/lib/jekyll_picture_tag/srcsets/basic.rb +10 -1
  87. data/lib/jekyll_picture_tag/utils.rb +14 -0
  88. data/lib/jekyll_picture_tag/version.rb +1 -1
  89. data/readme.md +13 -10
  90. metadata +139 -46
  91. data/.travis.yml +0 -8
  92. data/docs/_layouts/directory.html +0 -32
  93. data/docs/assets/style.css +0 -31
  94. data/docs/contributing.md +0 -109
  95. data/docs/example_presets.md +0 -116
  96. data/docs/global_configuration.md +0 -173
  97. data/docs/installation.md +0 -45
  98. data/docs/notes.md +0 -91
  99. data/docs/output.md +0 -63
  100. data/docs/presets.md +0 -361
  101. data/docs/usage.md +0 -143
  102. data/lib/jekyll_picture_tag/generated_image.rb +0 -161
  103. data/lib/jekyll_picture_tag/source_image.rb +0 -87
@@ -0,0 +1,61 @@
1
+ require 'json'
2
+
3
+ module PictureTag
4
+ module Cache
5
+ # Basic image information cache functionality
6
+ module Base
7
+ def initialize(base_name)
8
+ @base_name = base_name
9
+ end
10
+
11
+ def [](key)
12
+ data[key]
13
+ end
14
+
15
+ def []=(key, value)
16
+ raise ArgumentError unless template.keys.include? key
17
+
18
+ data[key] = value
19
+ end
20
+
21
+ # Call after updating data.
22
+ def write
23
+ return if PictureTag.site.config['disable_disk_cache']
24
+
25
+ FileUtils.mkdir_p(File.join(base_directory, sub_directory))
26
+
27
+ File.open(filename, 'w+') do |f|
28
+ f.write JSON.generate(data)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def data
35
+ @data ||= if File.exist?(filename)
36
+ JSON.parse(File.read(filename)).transform_keys(&:to_sym)
37
+ else
38
+ template
39
+ end
40
+ end
41
+
42
+ # /home/dave/my_blog/.jekyll-cache/jpt/(cache_dir)/assets/myimage.jpg.json
43
+ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44
+ def base_directory
45
+ File.join(PictureTag.site.cache_dir, 'jpt', cache_dir)
46
+ end
47
+
48
+ # /home/dave/my_blog/.jekyll-cache/jpt/(cache_dir)/assets/myimage.jpg.json
49
+ # ^^^^^^^^
50
+ def sub_directory
51
+ File.dirname(@base_name)
52
+ end
53
+
54
+ # /home/dave/my_blog/.jekyll-cache/jpt/somefolder/myimage.jpg.json
55
+ # ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
56
+ def filename
57
+ File.join(base_directory, @base_name + '.json')
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,20 @@
1
+ module PictureTag
2
+ module Cache
3
+ # Caches generated image details, so we can skip expensive operations whenever
4
+ # possible.
5
+ # Stored width and height are values for the source image, after cropping.
6
+ class Generated
7
+ include Base
8
+
9
+ private
10
+
11
+ def cache_dir
12
+ 'generated'
13
+ end
14
+
15
+ def template
16
+ { width: nil, height: nil }
17
+ end
18
+ end
19
+ end
20
+ end
@@ -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
@@ -9,3 +9,5 @@ picture:
9
9
  ignore_missing_images: false
10
10
  disabled: false
11
11
  fast_build: false
12
+ ignore_baseurl: false
13
+ baseurl_key: 'baseurl'
@@ -8,3 +8,5 @@ link_source: false
8
8
  quality: 75
9
9
  data_sizes: true
10
10
  gravity: center
11
+ dimension_attributes: false
12
+ strip_metadata: true
@@ -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,130 @@
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
+ def image
85
+ return @image if defined? @image
86
+
87
+ # Post crop, before resizing and reformatting
88
+ @source_dimensions = { width: image_base.width, height: image_base.height }
89
+
90
+ @image = image_base
91
+ end
92
+
93
+ def image_base
94
+ @image_base ||= Image.open(@source.name).combine_options do |i|
95
+ if PictureTag.preset['strip_metadata']
96
+ i.auto_orient
97
+ i.strip
98
+ end
99
+
100
+ if @crop
101
+ i.gravity @gravity
102
+ i.crop @crop
103
+ end
104
+ end
105
+ end
106
+
107
+ def generate_image
108
+ puts 'Generating new image file: ' + name
109
+
110
+ image.format(@format, 0, { resize: "#{@width}x", quality: quality })
111
+ FileUtils.mkdir_p(File.dirname(absolute_filename))
112
+
113
+ image.write absolute_filename
114
+
115
+ FileUtils.chmod(0o644, absolute_filename)
116
+ end
117
+
118
+ def quality
119
+ PictureTag.quality(@format, @width)
120
+ end
121
+
122
+ def process_format(format)
123
+ if format.casecmp('original').zero?
124
+ @source.ext
125
+ else
126
+ format.downcase
127
+ end
128
+ end
129
+ end
130
+ 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
@@ -40,7 +41,9 @@ module PictureTag
40
41
  # ^^^^^^^^^^^^^
41
42
  # | domain | baseurl | directory | filename
42
43
  def baseurl
43
- PictureTag.config['baseurl'] || ''
44
+ return '' if PictureTag.pconfig['ignore_baseurl']
45
+
46
+ PictureTag.config[PictureTag.pconfig['baseurl_key']] || ''
44
47
  end
45
48
 
46
49
  # https://example.com/my-base-path/assets/generated-images/image.jpg
@@ -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
@@ -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