jekyll_picture_tag 1.14.0 → 2.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +2 -0
  3. data/.github/workflows/code-checks.yml +2 -12
  4. data/.rubocop.yml +2 -0
  5. data/.ruby-version +1 -1
  6. data/docs/devs/contributing/code.md +11 -3
  7. data/docs/devs/contributing/testing.md +0 -11
  8. data/docs/devs/releases.md +38 -2
  9. data/docs/index.md +43 -18
  10. data/docs/logo.png +0 -0
  11. data/docs/logo.svg +880 -0
  12. data/docs/users/deployment.md +49 -0
  13. data/docs/users/getting_started.md +55 -0
  14. data/docs/users/installation.md +18 -38
  15. data/docs/users/liquid_tag/argument_reference/crop.md +21 -36
  16. data/docs/users/liquid_tag/examples.md +13 -25
  17. data/docs/users/liquid_tag/index.md +1 -1
  18. data/docs/users/notes/{migration.md → migration_1.md} +1 -1
  19. data/docs/users/notes/migration_2.md +99 -0
  20. data/docs/users/presets/cropping.md +21 -22
  21. data/docs/users/presets/default.md +10 -2
  22. data/docs/users/presets/examples.md +77 -45
  23. data/docs/users/presets/fallback_image.md +1 -1
  24. data/docs/users/presets/html_attributes.md +1 -1
  25. data/docs/users/presets/image_formats.md +3 -3
  26. data/docs/users/presets/image_quality.md +71 -56
  27. data/docs/users/presets/index.md +19 -45
  28. data/docs/users/presets/link_source.md +1 -1
  29. data/docs/users/presets/media_queries.md +1 -1
  30. data/docs/users/presets/nomarkdown_override.md +1 -1
  31. data/docs/users/presets/pixel_ratio_srcsets.md +1 -1
  32. data/docs/users/presets/width_height_attributes.md +1 -1
  33. data/docs/users/presets/width_srcsets.md +61 -23
  34. data/docs/users/presets/writing_presets.md +65 -0
  35. data/docs/users/tutorial.md +97 -0
  36. data/jekyll_picture_tag.gemspec +33 -23
  37. data/lib/jekyll_picture_tag.rb +8 -6
  38. data/lib/jekyll_picture_tag/cache.rb +64 -3
  39. data/lib/jekyll_picture_tag/defaults/global.rb +18 -0
  40. data/lib/jekyll_picture_tag/defaults/presets.rb +57 -0
  41. data/lib/jekyll_picture_tag/images.rb +1 -0
  42. data/lib/jekyll_picture_tag/images/generated_image.rb +25 -63
  43. data/lib/jekyll_picture_tag/images/image_file.rb +105 -0
  44. data/lib/jekyll_picture_tag/images/img_uri.rb +3 -12
  45. data/lib/jekyll_picture_tag/images/source_image.rb +44 -9
  46. data/lib/jekyll_picture_tag/instructions.rb +70 -6
  47. data/lib/jekyll_picture_tag/instructions/children/config.rb +128 -0
  48. data/lib/jekyll_picture_tag/instructions/children/context.rb +24 -0
  49. data/lib/jekyll_picture_tag/instructions/children/params.rb +90 -0
  50. data/lib/jekyll_picture_tag/instructions/children/parsers.rb +48 -0
  51. data/lib/jekyll_picture_tag/instructions/children/preset.rb +182 -0
  52. data/lib/jekyll_picture_tag/instructions/parents/conditional_instruction.rb +69 -0
  53. data/lib/jekyll_picture_tag/instructions/parents/env_instruction.rb +29 -0
  54. data/lib/jekyll_picture_tag/output_formats/basic.rb +5 -17
  55. data/lib/jekyll_picture_tag/parsers.rb +6 -0
  56. data/lib/jekyll_picture_tag/{instructions → parsers}/arg_splitter.rb +1 -1
  57. data/lib/jekyll_picture_tag/parsers/configuration.rb +28 -0
  58. data/lib/jekyll_picture_tag/{instructions → parsers}/html_attributes.rb +1 -1
  59. data/lib/jekyll_picture_tag/parsers/image_backend.rb +46 -0
  60. data/lib/jekyll_picture_tag/parsers/preset.rb +43 -0
  61. data/lib/jekyll_picture_tag/{instructions → parsers}/tag_parser.rb +15 -12
  62. data/lib/jekyll_picture_tag/router.rb +35 -93
  63. data/lib/jekyll_picture_tag/srcsets/basic.rb +4 -10
  64. data/lib/jekyll_picture_tag/utils.rb +10 -20
  65. data/lib/jekyll_picture_tag/version.rb +1 -1
  66. data/readme.md +38 -0
  67. metadata +126 -105
  68. data/Dockerfile +0 -9
  69. data/docs/users/notes/input_checking.md +0 -6
  70. data/docs/users/presets/strip_metadata.md +0 -13
  71. data/install_imagemagick.sh +0 -23
  72. data/jekyll-picture-tag.gemspec +0 -52
  73. data/lib/jekyll-picture-tag.rb +0 -25
  74. data/lib/jekyll_picture_tag/cache/base.rb +0 -61
  75. data/lib/jekyll_picture_tag/cache/generated.rb +0 -20
  76. data/lib/jekyll_picture_tag/cache/source.rb +0 -19
  77. data/lib/jekyll_picture_tag/defaults/global.yml +0 -13
  78. data/lib/jekyll_picture_tag/defaults/presets.yml +0 -12
  79. data/lib/jekyll_picture_tag/instructions/configuration.rb +0 -121
  80. data/lib/jekyll_picture_tag/instructions/preset.rb +0 -122
  81. data/lib/jekyll_picture_tag/instructions/set.rb +0 -75
@@ -1,3 +1,64 @@
1
- require_relative 'cache/base'
2
- require_relative 'cache/source'
3
- require_relative 'cache/generated'
1
+ require 'json'
2
+
3
+ module PictureTag
4
+ # Store expensive bits of information between text files. Originally cached
5
+ # width & heights of images in addition to digests, now just image digests.
6
+ class Cache
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.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')
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
+
60
+ def template
61
+ { digest: nil }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,18 @@
1
+ module PictureTag
2
+ # Default settings for _config.yml
3
+ DEFAULT_CONFIG = {
4
+ 'picture' => {
5
+ 'source' => '',
6
+ 'output' => 'generated',
7
+ 'suppress_warnings' => false,
8
+ 'relative_url' => true,
9
+ 'cdn_environments' => ['production'],
10
+ 'nomarkdown' => true,
11
+ 'ignore_missing_images' => false,
12
+ 'disabled' => false,
13
+ 'fast_build' => false,
14
+ 'ignore_baseurl' => false,
15
+ 'baseurl_key' => 'baseurl'
16
+ }
17
+ }.freeze
18
+ end
@@ -0,0 +1,57 @@
1
+ module PictureTag
2
+ DEFAULT_PRESET = { 'markup' => 'auto',
3
+ 'formats' => ['original'],
4
+ 'widths' => [400, 600, 800, 1000],
5
+ 'fallback_width' => 800,
6
+ 'fallback_format' => 'original',
7
+ 'noscript' => false,
8
+ 'link_source' => false,
9
+ 'quality' => 75,
10
+ 'format_quality' => { 'webp' => 50,
11
+ 'avif' => 30,
12
+ 'jp2' => 30 },
13
+ 'data_sizes' => true,
14
+ 'keep' => 'attention',
15
+ 'dimension_attributes' => false,
16
+ 'strip_metadata' => true,
17
+ 'image_options' => {
18
+ 'avif' => { 'compression' => 'av1', 'speed' => 8 }
19
+ } }.freeze
20
+
21
+ STOCK_PRESETS = {
22
+ 'jpt-webp' => { 'formats' => %w[webp original] },
23
+
24
+ 'jpt-avif' => { 'formats' => %w[avif webp original] },
25
+
26
+ 'jpt-lazy' => { 'markup' => 'data_auto',
27
+ 'noscript' => true,
28
+ 'formats' => %w[webp original],
29
+ 'attributes' => { 'parent' => 'class="lazy"' } },
30
+
31
+ 'jpt-loaded' => { 'formats' => %w[avif jp2 webp original],
32
+ 'dimension_attributes' => true },
33
+
34
+ 'jpt-direct' => { 'markup' => 'direct_url',
35
+ 'fallback_format' => 'webp',
36
+ 'fallback_width' => 600 },
37
+
38
+ 'jpt-thumbnail' => { 'base_width' => 250,
39
+ 'pixel_ratios' => [1, 1.5, 2],
40
+ 'formats' => %w[webp original],
41
+ 'fallback_width' => 250,
42
+ 'attributes' => { 'picture' => 'class="icon"' } },
43
+
44
+ 'jpt-avatar' => { 'base_width' => 100,
45
+ 'pixel_ratios' => [1, 1.5, 2],
46
+ 'fallback_width' => 100,
47
+ 'crop' => '1:1' }
48
+ }.freeze
49
+
50
+ STOCK_MEDIA_QUERIES = {
51
+ 'jpt-mobile' => 'max-width: 480px',
52
+ 'jpt-tablet' => 'max-width: 768',
53
+ 'jpt-laptop' => 'max-width: 1024px',
54
+ 'jpt-desktop' => 'max-width: 1200',
55
+ 'jpt-wide' => 'min-width: 1201'
56
+ }.freeze
57
+ end
@@ -1,3 +1,4 @@
1
+ require_relative 'images/image_file'
1
2
  require_relative 'images/generated_image'
2
3
  require_relative 'images/img_uri'
3
4
  require_relative 'images/source_image'
@@ -1,18 +1,20 @@
1
- require 'mini_magick'
1
+ require 'ruby-vips'
2
2
 
3
3
  module PictureTag
4
- # Represents a generated image file.
4
+ # Represents a generated image, but not the file itself. Its purpose is to
5
+ # make its properties available for query, and hand them off to the ImageFile
6
+ # class for generation.
5
7
  class GeneratedImage
6
- attr_reader :width, :format
8
+ attr_reader :width
7
9
 
8
- include MiniMagick
9
-
10
- def initialize(source_file:, width:, format:, crop: nil, gravity: '')
10
+ def initialize(source_file:, width:, format:)
11
11
  @source = source_file
12
12
  @width = width
13
- @format = process_format format
14
- @crop = crop
15
- @gravity = gravity
13
+ @raw_format = format
14
+ end
15
+
16
+ def format
17
+ @format ||= process_format(@raw_format)
16
18
  end
17
19
 
18
20
  def exists?
@@ -43,80 +45,40 @@ module PictureTag
43
45
 
44
46
  # Post crop
45
47
  def source_width
46
- update_cache unless cache[:width]
47
-
48
- cache[:width]
48
+ image.width
49
49
  end
50
50
 
51
51
  # Post crop
52
52
  def source_height
53
- update_cache unless cache[:height]
54
-
55
- cache[:height]
53
+ image.height
56
54
  end
57
55
 
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}")
56
+ def quality
57
+ PictureTag.quality(format, width)
63
58
  end
64
59
 
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
60
+ private
76
61
 
77
- # Hash all inputs and truncate, so we know when they change without getting too long.
62
+ # Hash all inputs and truncate, so we know when they change without getting
63
+ # too long.
78
64
  # /home/dave/my_blog/_site/generated/somefolder/myimage-100-1234abcde.jpg
79
65
  # ^^^^^^^^^
80
66
  def id
81
- @id ||= Digest::MD5.hexdigest([@source.digest, @crop, @gravity, quality].join)[0..8]
67
+ @id ||= Digest::MD5.hexdigest(settings.join)[0..8]
82
68
  end
83
69
 
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
70
+ def settings
71
+ [@source.digest, @source.crop, @source.keep, quality]
91
72
  end
92
73
 
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
74
+ def image
75
+ @image ||= Vips::Image.new_from_file @source.name
105
76
  end
106
77
 
107
78
  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
79
+ return if @source.missing
117
80
 
118
- def quality
119
- PictureTag.quality(@format, @width)
81
+ ImageFile.new(@source, self)
120
82
  end
121
83
 
122
84
  def process_format(format)
@@ -0,0 +1,105 @@
1
+ module PictureTag
2
+ # Basically a wrapper class for vips. Handles image operations.
3
+ # Vips returns new images for Crop, resize, and autorotate operations.
4
+ # Quality, metadata stripping, and format are applied on write.
5
+ #
6
+ # This deserves to be two classes and a factory, one for normal vips save and
7
+ # one for magicksave. This is illustrated by the fact that stubbing backend
8
+ # determination logic for its unit tests would basically require
9
+ # re-implementing it completely.
10
+ #
11
+ # I'm planning to implement standalone imagemagick as an alternative to vips,
12
+ # so when I add that I'll also do that refactoring. For now it works fine and
13
+ # it's not too bloated.
14
+ class ImageFile
15
+ def initialize(source, base)
16
+ @source = source
17
+ @base = base
18
+
19
+ build
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :source, :base
25
+
26
+ def build
27
+ notify
28
+
29
+ mkdir
30
+
31
+ image = load_image
32
+
33
+ image = process(image)
34
+
35
+ write(image)
36
+ end
37
+
38
+ # Processing pipeline
39
+ def process(image)
40
+ image = crop(image) if source.crop?
41
+
42
+ image = resize(image)
43
+
44
+ image.autorot
45
+ end
46
+
47
+ def handler
48
+ PictureTag.backend.handler_for(@base.format)
49
+ end
50
+
51
+ def quality_key
52
+ handler == :vips ? :Q : :quality
53
+ end
54
+
55
+ def write_opts
56
+ opts = PictureTag.preset['image_options'][@base.format] || {}
57
+
58
+ opts[:strip] = PictureTag.preset['strip_metadata']
59
+
60
+ # gifs don't accept a quality setting, and PNGs don't on older versions of
61
+ # vips. Since it's not remarkably useful anyway, we'll ignore them.
62
+ opts[quality_key] = base.quality unless %w[gif png].include? base.format
63
+
64
+ opts.transform_keys(&:to_sym)
65
+ end
66
+
67
+ def load_image
68
+ Vips::Image.new_from_file source.name
69
+ end
70
+
71
+ def write(image)
72
+ case handler
73
+ when :vips
74
+ image.write_to_file(base.absolute_filename, **write_opts)
75
+ # If vips can't handle it, fall back to imagemagick.
76
+ when :magick
77
+ image.magicksave(base.absolute_filename, **write_opts)
78
+ end
79
+
80
+ # Fix permissions. TODO - still necessary?
81
+ FileUtils.chmod(0o644, base.absolute_filename)
82
+ end
83
+
84
+ def notify
85
+ puts 'Generating new image file: ' + base.name
86
+ end
87
+
88
+ def resize(image)
89
+ image.resize(scale_value)
90
+ end
91
+
92
+ def crop(image)
93
+ image.smartcrop(*source.dimensions,
94
+ interesting: PictureTag.keep(@source.media_preset))
95
+ end
96
+
97
+ def scale_value
98
+ base.width.to_f / source.width
99
+ end
100
+
101
+ def mkdir
102
+ FileUtils.mkdir_p(File.dirname(base.absolute_filename))
103
+ end
104
+ end
105
+ end
@@ -18,7 +18,7 @@ module PictureTag
18
18
  # | domain | baseurl | directory | filename
19
19
  def to_s
20
20
  Addressable::URI.escape(
21
- File.join(domain, baseurl, directory, @filename)
21
+ File.join(domain, PictureTag.baseurl, directory, @filename)
22
22
  )
23
23
  end
24
24
 
@@ -29,23 +29,14 @@ module PictureTag
29
29
  # | domain | baseurl | j-p-t output dir | filename
30
30
  def domain
31
31
  if PictureTag.cdn?
32
- PictureTag.pconfig['cdn_url']
33
- elsif PictureTag.pconfig['relative_url']
32
+ PictureTag.cdn_url
33
+ elsif PictureTag.relative_url
34
34
  ''
35
35
  else
36
36
  PictureTag.config['url'] || ''
37
37
  end
38
38
  end
39
39
 
40
- # https://example.com/my-base-path/assets/generated-images/image.jpg
41
- # ^^^^^^^^^^^^^
42
- # | domain | baseurl | directory | filename
43
- def baseurl
44
- return '' if PictureTag.pconfig['ignore_baseurl']
45
-
46
- PictureTag.config[PictureTag.pconfig['baseurl_key']] || ''
47
- end
48
-
49
40
  # https://example.com/my-base-path/assets/generated-images/image.jpg
50
41
  # ^^^^^^^^^^^^^^^^^^^^^^^^
51
42
  # | domain | baseurl | directory | filename
@@ -5,7 +5,7 @@ module PictureTag
5
5
  class SourceImage
6
6
  attr_reader :shortname, :missing, :media_preset
7
7
 
8
- include MiniMagick
8
+ # include MiniMagick
9
9
 
10
10
  def initialize(relative_filename, media_preset = nil)
11
11
  # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
@@ -21,12 +21,38 @@ module PictureTag
21
21
  @digest ||= cache[:digest] || ''
22
22
  end
23
23
 
24
+ def crop
25
+ PictureTag.crop(media_preset)
26
+ end
27
+
28
+ def crop?
29
+ !crop.nil?
30
+ end
31
+
32
+ def keep
33
+ PictureTag.keep(media_preset)
34
+ end
35
+
36
+ def dimensions
37
+ [width, height]
38
+ end
39
+
24
40
  def width
25
- @width ||= cache[:width] || 999_999
41
+ return raw_width unless crop?
42
+
43
+ [raw_width, (raw_height * cropped_aspect)].min.round
26
44
  end
27
45
 
28
46
  def height
29
- @height ||= cache[:height] || 999_999
47
+ return raw_height unless crop?
48
+
49
+ [raw_height, (raw_width / cropped_aspect)].min.round
50
+ end
51
+
52
+ def cropped_aspect
53
+ return Utils.aspect_float(raw_width, raw_height) unless crop?
54
+
55
+ Utils.aspect_float(*crop.split(':').map(&:to_f))
30
56
  end
31
57
 
32
58
  # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
@@ -44,13 +70,23 @@ module PictureTag
44
70
  # /home/dave/my_blog/assets/images/somefolder/myimage.jpg
45
71
  # ^^^
46
72
  def ext
47
- @ext ||= File.extname(name)[1..-1].downcase
73
+ @ext ||= File.extname(name)[1..].downcase
48
74
  end
49
75
 
50
76
  private
51
77
 
78
+ # pre-crop
79
+ def raw_width
80
+ @raw_width ||= @missing ? 999_999 : image.width
81
+ end
82
+
83
+ # pre-crop
84
+ def raw_height
85
+ @raw_height ||= @missing ? 999_999 : image.height
86
+ end
87
+
52
88
  def cache
53
- @cache ||= Cache::Source.new(@shortname)
89
+ @cache ||= Cache.new(@shortname)
54
90
  end
55
91
 
56
92
  def missing?
@@ -75,14 +111,12 @@ module PictureTag
75
111
 
76
112
  def update_cache
77
113
  cache[:digest] = source_digest
78
- cache[:width] = image.width
79
- cache[:height] = image.height
80
114
 
81
115
  cache.write
82
116
  end
83
117
 
84
118
  def image
85
- @image ||= Image.open(name)
119
+ @image ||= Vips::Image.new_from_file(name)
86
120
  end
87
121
 
88
122
  def source_digest
@@ -90,7 +124,8 @@ module PictureTag
90
124
  end
91
125
 
92
126
  def missing_image_warning
93
- "JPT Could not find #{name}. Your site will have broken images. Continuing."
127
+ "JPT Could not find #{name}. " \
128
+ 'Your site will have broken images. Continuing.'
94
129
  end
95
130
 
96
131
  def missing_image_error