jekyll_picture_tag 1.14.0 → 2.0.0pre1

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 (78) 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 +20 -0
  9. data/docs/index.md +32 -17
  10. data/docs/logo.png +0 -0
  11. data/docs/logo.svg +880 -0
  12. data/docs/users/getting_started.md +55 -0
  13. data/docs/users/installation.md +17 -38
  14. data/docs/users/liquid_tag/argument_reference/crop.md +21 -36
  15. data/docs/users/liquid_tag/examples.md +13 -25
  16. data/docs/users/liquid_tag/index.md +1 -1
  17. data/docs/users/notes/{migration.md → migration_1.md} +1 -1
  18. data/docs/users/notes/migration_2.md +99 -0
  19. data/docs/users/presets/cropping.md +21 -22
  20. data/docs/users/presets/default.md +11 -3
  21. data/docs/users/presets/examples.md +77 -45
  22. data/docs/users/presets/fallback_image.md +1 -1
  23. data/docs/users/presets/html_attributes.md +1 -1
  24. data/docs/users/presets/image_formats.md +3 -3
  25. data/docs/users/presets/image_quality.md +70 -55
  26. data/docs/users/presets/index.md +78 -42
  27. data/docs/users/presets/link_source.md +1 -1
  28. data/docs/users/presets/media_queries.md +1 -1
  29. data/docs/users/presets/nomarkdown_override.md +1 -1
  30. data/docs/users/presets/pixel_ratio_srcsets.md +1 -1
  31. data/docs/users/presets/width_height_attributes.md +1 -1
  32. data/docs/users/presets/width_srcsets.md +61 -23
  33. data/docs/users/tutorial.md +97 -0
  34. data/jekyll_picture_tag.gemspec +33 -23
  35. data/lib/jekyll_picture_tag.rb +8 -6
  36. data/lib/jekyll_picture_tag/cache.rb +64 -3
  37. data/lib/jekyll_picture_tag/defaults/global.rb +18 -0
  38. data/lib/jekyll_picture_tag/defaults/presets.rb +57 -0
  39. data/lib/jekyll_picture_tag/images.rb +1 -0
  40. data/lib/jekyll_picture_tag/images/generated_image.rb +25 -63
  41. data/lib/jekyll_picture_tag/images/image_file.rb +90 -0
  42. data/lib/jekyll_picture_tag/images/img_uri.rb +3 -12
  43. data/lib/jekyll_picture_tag/images/source_image.rb +44 -9
  44. data/lib/jekyll_picture_tag/instructions.rb +70 -6
  45. data/lib/jekyll_picture_tag/instructions/children/config.rb +128 -0
  46. data/lib/jekyll_picture_tag/instructions/children/context.rb +24 -0
  47. data/lib/jekyll_picture_tag/instructions/children/params.rb +90 -0
  48. data/lib/jekyll_picture_tag/instructions/children/parsers.rb +41 -0
  49. data/lib/jekyll_picture_tag/instructions/children/preset.rb +182 -0
  50. data/lib/jekyll_picture_tag/instructions/parents/conditional_instruction.rb +69 -0
  51. data/lib/jekyll_picture_tag/instructions/parents/env_instruction.rb +29 -0
  52. data/lib/jekyll_picture_tag/output_formats/basic.rb +5 -17
  53. data/lib/jekyll_picture_tag/parsers.rb +5 -0
  54. data/lib/jekyll_picture_tag/{instructions → parsers}/arg_splitter.rb +1 -1
  55. data/lib/jekyll_picture_tag/parsers/configuration.rb +28 -0
  56. data/lib/jekyll_picture_tag/{instructions → parsers}/html_attributes.rb +1 -1
  57. data/lib/jekyll_picture_tag/parsers/preset.rb +43 -0
  58. data/lib/jekyll_picture_tag/{instructions → parsers}/tag_parser.rb +15 -12
  59. data/lib/jekyll_picture_tag/router.rb +35 -93
  60. data/lib/jekyll_picture_tag/srcsets/basic.rb +4 -10
  61. data/lib/jekyll_picture_tag/utils.rb +10 -20
  62. data/lib/jekyll_picture_tag/version.rb +1 -1
  63. data/readme.md +2 -0
  64. metadata +124 -106
  65. data/Dockerfile +0 -9
  66. data/docs/users/notes/input_checking.md +0 -6
  67. data/docs/users/presets/strip_metadata.md +0 -13
  68. data/install_imagemagick.sh +0 -23
  69. data/jekyll-picture-tag.gemspec +0 -52
  70. data/lib/jekyll-picture-tag.rb +0 -25
  71. data/lib/jekyll_picture_tag/cache/base.rb +0 -61
  72. data/lib/jekyll_picture_tag/cache/generated.rb +0 -20
  73. data/lib/jekyll_picture_tag/cache/source.rb +0 -19
  74. data/lib/jekyll_picture_tag/defaults/global.yml +0 -13
  75. data/lib/jekyll_picture_tag/defaults/presets.yml +0 -12
  76. data/lib/jekyll_picture_tag/instructions/configuration.rb +0 -121
  77. data/lib/jekyll_picture_tag/instructions/preset.rb +0 -122
  78. data/lib/jekyll_picture_tag/instructions/set.rb +0 -75
@@ -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,90 @@
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
+ class ImageFile
6
+ def initialize(source, base)
7
+ @source = source
8
+ @base = base
9
+
10
+ build
11
+ end
12
+
13
+ private
14
+
15
+ attr_reader :source, :base
16
+
17
+ def build
18
+ notify
19
+
20
+ mkdir
21
+
22
+ image = load_image
23
+
24
+ image = process(image)
25
+
26
+ write(image)
27
+ end
28
+
29
+ # Processing pipeline
30
+ def process(image)
31
+ image = crop(image) if source.crop?
32
+
33
+ image = resize(image)
34
+
35
+ image.autorot
36
+ end
37
+
38
+ def write_opts
39
+ opts = PictureTag.preset['image_options'][@base.format] || {}
40
+
41
+ opts[:strip] = PictureTag.preset['strip_metadata']
42
+
43
+ # gifs don't accept a quality setting.
44
+ opts[:Q] = base.quality unless base.format == 'gif'
45
+
46
+ opts.transform_keys(&:to_sym)
47
+ end
48
+
49
+ def load_image
50
+ Vips::Image.new_from_file source.name
51
+ end
52
+
53
+ def write(image)
54
+ begin
55
+ image.write_to_file(base.absolute_filename, **write_opts)
56
+ rescue Vips::Error
57
+ # If vips can't handle it, fall back to imagemagick.
58
+ opts = write_opts.transform_keys do |key|
59
+ key == :Q ? :quality : key
60
+ end
61
+
62
+ image.magicksave(base.absolute_filename, **opts)
63
+ end
64
+
65
+ # Fix permissions. TODO - still necessary?
66
+ FileUtils.chmod(0o644, base.absolute_filename)
67
+ end
68
+
69
+ def notify
70
+ puts 'Generating new image file: ' + base.name
71
+ end
72
+
73
+ def resize(image)
74
+ image.resize(scale_value)
75
+ end
76
+
77
+ def crop(image)
78
+ image.smartcrop(*source.dimensions,
79
+ interesting: PictureTag.keep(@source.media_preset))
80
+ end
81
+
82
+ def scale_value
83
+ base.width.to_f / source.width
84
+ end
85
+
86
+ def mkdir
87
+ FileUtils.mkdir_p(File.dirname(base.absolute_filename))
88
+ end
89
+ end
90
+ 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
@@ -1,6 +1,70 @@
1
- require_relative './instructions/set'
2
- require_relative './instructions/configuration'
3
- require_relative './instructions/html_attributes'
4
- require_relative './instructions/preset'
5
- require_relative './instructions/tag_parser'
6
- require_relative './instructions/arg_splitter'
1
+ module PictureTag
2
+ # Instructions obtain, validate, and typecast/coerce input values. These
3
+ # inputs are either taken directly from jekyll's inputs, or handled by parsers
4
+ # first.
5
+ #
6
+ # Logic which affects only a single setting belongs in Instructions, while
7
+ # logic which affects multiple settings belongs in Parsers.
8
+ #
9
+ # Since instruction classes are so small, we define several per file in the
10
+ # instructions directory to save on boilerplate. All fall under the
11
+ # Instructions module namespace.
12
+ module Instructions
13
+ # Generic instruction, meant to be inherited. Children of this class must
14
+ # override the source method, and likely want to override valid?, coerce,
15
+ # and error_message as applicable.
16
+ class Instruction
17
+ # Memoized value of the given instruction. This is the public API.
18
+ def value
19
+ return @value if defined?(@value)
20
+
21
+ raise ArgumentError, error_message unless valid?
22
+
23
+ @value = coerced
24
+ end
25
+
26
+ private
27
+
28
+ # Source(s) of truth - where does this setting come from? Logic does not
29
+ # belong here. If information comes from muliple places, return an array
30
+ # or a hash.
31
+ def source
32
+ raise NotImplementedError
33
+ end
34
+
35
+ # Determine whether or not the input(s) are valid.
36
+ def valid?
37
+ true
38
+ end
39
+
40
+ # Convert input(s) to output.
41
+ def coerce
42
+ source
43
+ end
44
+
45
+ # Message returned if validation fails. Override this with something more
46
+ # helpful.
47
+ def error_message
48
+ "JPT - #{setting_name} received an invalid argument: #{source}"
49
+ end
50
+
51
+ def coerced
52
+ return @coerced if defined?(@coerced)
53
+
54
+ @coerced = coerce
55
+ end
56
+
57
+ def setting_name
58
+ Utils.snakeize(self.class.to_s.split('::').last)
59
+ end
60
+ end
61
+ end
62
+ end
63
+
64
+ # Load Parents
65
+ Dir[File.dirname(__FILE__) + '/instructions/parents/*.rb']
66
+ .sort.each { |file| require file }
67
+
68
+ # Load children:
69
+ Dir[File.dirname(__FILE__) + '/instructions/children/*.rb']
70
+ .sort.each { |file| require file }