jekyll_picture_tag 1.14.0 → 2.0.3

Sign up to get free protection for your applications and to get access to all the features.
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