jekyll_picture_tag 1.13.0 → 2.0.2

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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +4 -0
  3. data/.github/workflows/code-checks.yml +33 -0
  4. data/.gitignore +3 -0
  5. data/.rubocop.yml +29 -76
  6. data/.ruby-version +1 -1
  7. data/docs/.envrc +2 -0
  8. data/docs/devs/contributing/code.md +14 -4
  9. data/docs/devs/contributing/docs.md +24 -6
  10. data/docs/devs/contributing/setup.md +21 -1
  11. data/docs/devs/contributing/testing.md +19 -37
  12. data/docs/devs/releases.md +45 -4
  13. data/docs/index.md +43 -18
  14. data/docs/logo.png +0 -0
  15. data/docs/logo.svg +880 -0
  16. data/docs/users/configuration/disable.md +1 -1
  17. data/docs/users/configuration/ignore_missing.md +1 -1
  18. data/docs/users/configuration/kramdown_fix.md +1 -1
  19. data/docs/users/configuration/suppress_warnings.md +1 -1
  20. data/docs/users/configuration/urls.md +69 -0
  21. data/docs/users/deployment.md +49 -0
  22. data/docs/users/getting_started.md +55 -0
  23. data/docs/users/installation.md +18 -38
  24. data/docs/users/liquid_tag/argument_reference/crop.md +21 -36
  25. data/docs/users/liquid_tag/examples.md +13 -25
  26. data/docs/users/liquid_tag/index.md +1 -1
  27. data/docs/users/notes/{migration.md → migration_1.md} +1 -1
  28. data/docs/users/notes/migration_2.md +99 -0
  29. data/docs/users/presets/cropping.md +21 -22
  30. data/docs/users/presets/default.md +11 -2
  31. data/docs/users/presets/examples.md +77 -45
  32. data/docs/users/presets/fallback_image.md +1 -1
  33. data/docs/users/presets/html_attributes.md +1 -1
  34. data/docs/users/presets/image_formats.md +3 -3
  35. data/docs/users/presets/image_quality.md +71 -56
  36. data/docs/users/presets/index.md +19 -45
  37. data/docs/users/presets/link_source.md +1 -1
  38. data/docs/users/presets/media_queries.md +1 -1
  39. data/docs/users/presets/nomarkdown_override.md +1 -1
  40. data/docs/users/presets/pixel_ratio_srcsets.md +1 -1
  41. data/docs/users/presets/width_height_attributes.md +1 -1
  42. data/docs/users/presets/width_srcsets.md +61 -23
  43. data/docs/users/presets/writing_presets.md +65 -0
  44. data/docs/users/tutorial.md +97 -0
  45. data/jekyll_picture_tag.gemspec +38 -23
  46. data/lib/jekyll_picture_tag.rb +8 -5
  47. data/lib/jekyll_picture_tag/cache.rb +64 -3
  48. data/lib/jekyll_picture_tag/defaults/global.rb +18 -0
  49. data/lib/jekyll_picture_tag/defaults/presets.rb +57 -0
  50. data/lib/jekyll_picture_tag/images.rb +1 -0
  51. data/lib/jekyll_picture_tag/images/generated_image.rb +25 -60
  52. data/lib/jekyll_picture_tag/images/image_file.rb +105 -0
  53. data/lib/jekyll_picture_tag/images/img_uri.rb +3 -10
  54. data/lib/jekyll_picture_tag/images/source_image.rb +44 -9
  55. data/lib/jekyll_picture_tag/instructions.rb +70 -6
  56. data/lib/jekyll_picture_tag/instructions/children/config.rb +128 -0
  57. data/lib/jekyll_picture_tag/instructions/children/context.rb +24 -0
  58. data/lib/jekyll_picture_tag/instructions/children/params.rb +90 -0
  59. data/lib/jekyll_picture_tag/instructions/children/parsers.rb +48 -0
  60. data/lib/jekyll_picture_tag/instructions/children/preset.rb +182 -0
  61. data/lib/jekyll_picture_tag/instructions/parents/conditional_instruction.rb +69 -0
  62. data/lib/jekyll_picture_tag/instructions/parents/env_instruction.rb +29 -0
  63. data/lib/jekyll_picture_tag/output_formats/basic.rb +5 -17
  64. data/lib/jekyll_picture_tag/parsers.rb +6 -0
  65. data/lib/jekyll_picture_tag/{instructions → parsers}/arg_splitter.rb +1 -1
  66. data/lib/jekyll_picture_tag/parsers/configuration.rb +28 -0
  67. data/lib/jekyll_picture_tag/{instructions → parsers}/html_attributes.rb +1 -1
  68. data/lib/jekyll_picture_tag/parsers/image_backend.rb +33 -0
  69. data/lib/jekyll_picture_tag/parsers/preset.rb +43 -0
  70. data/lib/jekyll_picture_tag/{instructions → parsers}/tag_parser.rb +15 -12
  71. data/lib/jekyll_picture_tag/router.rb +35 -93
  72. data/lib/jekyll_picture_tag/srcsets/basic.rb +4 -10
  73. data/lib/jekyll_picture_tag/utils.rb +10 -20
  74. data/lib/jekyll_picture_tag/version.rb +1 -1
  75. data/readme.md +48 -9
  76. metadata +161 -80
  77. data/.travis.yml +0 -8
  78. data/Dockerfile +0 -9
  79. data/docs/users/configuration/cdn.md +0 -35
  80. data/docs/users/configuration/relative_urls.md +0 -15
  81. data/docs/users/notes/input_checking.md +0 -6
  82. data/jekyll-picture-tag.gemspec +0 -52
  83. data/lib/jekyll-picture-tag.rb +0 -25
  84. data/lib/jekyll_picture_tag/cache/base.rb +0 -59
  85. data/lib/jekyll_picture_tag/cache/generated.rb +0 -20
  86. data/lib/jekyll_picture_tag/cache/source.rb +0 -19
  87. data/lib/jekyll_picture_tag/defaults/global.yml +0 -11
  88. data/lib/jekyll_picture_tag/defaults/presets.yml +0 -11
  89. data/lib/jekyll_picture_tag/instructions/configuration.rb +0 -121
  90. data/lib/jekyll_picture_tag/instructions/preset.rb +0 -122
  91. data/lib/jekyll_picture_tag/instructions/set.rb +0 -75
@@ -5,9 +5,12 @@ require_relative 'jekyll_picture_tag/cache'
5
5
  require_relative 'jekyll_picture_tag/images'
6
6
  require_relative 'jekyll_picture_tag/instructions'
7
7
  require_relative 'jekyll_picture_tag/output_formats'
8
+ require_relative 'jekyll_picture_tag/parsers'
8
9
  require_relative 'jekyll_picture_tag/router'
9
10
  require_relative 'jekyll_picture_tag/srcsets'
10
11
  require_relative 'jekyll_picture_tag/utils'
12
+ require_relative 'jekyll_picture_tag/defaults/presets'
13
+ require_relative 'jekyll_picture_tag/defaults/global'
11
14
 
12
15
  # Title: Jekyll Picture Tag
13
16
  # Authors: Rob Wierzbowski : @robwierzbowski
@@ -60,7 +63,9 @@ module PictureTag
60
63
  def render(context)
61
64
  setup(context)
62
65
 
63
- if PictureTag.disabled?
66
+ if PictureTag.disabled? || PictureTag.raw_params.empty?
67
+ Utils.warning 'You have called JPT without any arguments.'
68
+
64
69
  ''
65
70
  else
66
71
  PictureTag.output_class.new.to_s
@@ -70,11 +75,9 @@ module PictureTag
70
75
  private
71
76
 
72
77
  def setup(context)
78
+ PictureTag.clear_instructions
73
79
  PictureTag.context = context
74
-
75
- # Now that we have both the tag parameters and the context object, we can
76
- # build our instruction set.
77
- PictureTag.instructions = Instructions::Set.new(@raw_params)
80
+ PictureTag.raw_params = @raw_params
78
81
 
79
82
  # We need to explicitly prevent jekyll from overwriting our generated
80
83
  # image files:
@@ -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,77 +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
- # Post crop, before resizing and reformatting
85
- def image
86
- @image ||= open_image
70
+ def settings
71
+ [@source.digest, @source.crop, @source.keep, quality]
87
72
  end
88
73
 
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
74
+ def image
75
+ @image ||= Vips::Image.new_from_file @source.name
102
76
  end
103
77
 
104
78
  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
79
+ return if @source.missing
114
80
 
115
- def quality
116
- PictureTag.quality(@format, @width)
81
+ ImageFile.new(@source, self)
117
82
  end
118
83
 
119
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,21 +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
- PictureTag.config['baseurl'] || ''
45
- end
46
-
47
40
  # https://example.com/my-base-path/assets/generated-images/image.jpg
48
41
  # ^^^^^^^^^^^^^^^^^^^^^^^^
49
42
  # | domain | baseurl | directory | filename