dynamic_image 3.1.1 → 3.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f098d53169629e3e9958e1cd67d320c3950d87ef9480451201c95fc74aeb0673
4
- data.tar.gz: 86b1de10949a440da1a2f7ec732a8e8ef883f481950facb5d1bac597dd1f3392
3
+ metadata.gz: cce4ab2b6835bbc3e5299f0c9513ada8a30a2db2b46865b975d20492febb8656
4
+ data.tar.gz: 172bf1404ef850c29b15d2f3cb88bc41fd4bb7274d0a10e1bfc41d0d6f1b8c55
5
5
  SHA512:
6
- metadata.gz: 2a4bb11b4f2e08f64498f09524f308414aab7007b721aaa15c9d3fcdaad58efa97810bc5597fd32315b88e73c8db53408b6a141dc5289966d88caa46effd675d
7
- data.tar.gz: 95d13e86e911f99ec93222ed2f6b79efba44c54a6407a6e859bc2623f1d2eb020e05301260827d8862a5ac6c26f064b56c6812e6901cb68172f7a688cd9eb492
6
+ metadata.gz: 917691711e8027af32a176f95d2ad9ccc1621cf8b3141c0e734f086357ace2d6cd2ea0e5217c07ef7ada018a5baeddad6f509efd8b0f4919eb6d2c8e1c4ceafb
7
+ data.tar.gz: ab5d71c3d0508b4819e93489dfaee8ad5772177b3c1cf76174ac16ebfd4d459070a0b1c8fe2b1c2bc3d20f3f9d648c48ecf07c4e3920ce690dc492b721692d9e
data/README.md CHANGED
@@ -285,7 +285,7 @@ media queries.
285
285
  ## Formats
286
286
 
287
287
  Supported formats are JPEG, PNG, GIF, WebP, JPEG XL and TIFF.
288
- BMP, HEIC and AVIF can also be uploaded, but aren't supported for output.
288
+ HEIC and AVIF can also be uploaded, but aren't supported for output.
289
289
 
290
290
  The preferred format lists are configurable, sorted most preferred format
291
291
  first:
@@ -4,7 +4,7 @@ module DynamicImage
4
4
  # = DynamicImage Backfill
5
5
  #
6
6
  # Fills in the metadata columns for records stored before those columns existed. The values are read back from
7
- # the stored file.
7
+ # the stored file, except where the format guarantees them.
8
8
  #
9
9
  # Records are written with +update_columns+, so no callbacks run and +updated_at+ is left alone.
10
10
  # {DynamicImage::Model#to_param} fingerprints on +updated_at+, and touching it would invalidate every image URL
@@ -17,20 +17,24 @@ module DynamicImage
17
17
  # The columns this fills in.
18
18
  COLUMNS = %i[frame_count alpha].freeze
19
19
 
20
- # @return [Class] the model being backfilled
21
- attr_reader :model
22
-
23
20
  # @return [Integer] records written
24
21
  attr_reader :updated
25
22
 
26
23
  # @return [Integer] records left alone, unreadable or missing
27
24
  attr_reader :skipped
28
25
 
26
+ # @return [Integer] records filled in from the format, without a read
27
+ attr_reader :inferred
28
+
29
29
  # @param model [Class] a model including {DynamicImage::Model}
30
- def initialize(model)
30
+ # @param concurrency [Integer] how many files to read at a time. Reads are network bound; the metadata itself
31
+ # comes off the header. Set to 1 to read serially.
32
+ def initialize(model, concurrency: 4)
31
33
  @model = model
32
34
  @updated = 0
33
35
  @skipped = 0
36
+ @inferred = 0
37
+ @concurrency = [concurrency.to_i, 1].max
34
38
  end
35
39
 
36
40
  # The records with a column still unset.
@@ -40,22 +44,42 @@ module DynamicImage
40
44
  COLUMNS.map { |column| model.where(column => nil) }.reduce(:or)
41
45
  end
42
46
 
43
- # Reads metadata for every pending record and fills the columns in.
47
+ # Fills in what the format settles, then reads the rest.
44
48
  #
45
- # @yieldparam record [DynamicImage::Model] each record, after it has been processed
49
+ # @yieldparam record [DynamicImage::Model] each record, after it has been read
46
50
  # @return [self]
47
51
  # @raise [ArgumentError] if the table doesn't have the columns yet
48
52
  def run
49
53
  ensure_columns
50
- pending.find_each do |record|
51
- process(record)
52
- yield(record) if block_given?
54
+ infer_from_format
55
+ pending.find_in_batches(batch_size: concurrency) do |batch|
56
+ read(batch).each do |record, values|
57
+ write(record, values)
58
+ yield(record) if block_given?
59
+ end
53
60
  end
54
61
  self
55
62
  end
56
63
 
57
64
  private
58
65
 
66
+ attr_reader :model, :concurrency
67
+
68
+ # The formats whose values follow from the format itself. A format that holds neither animation nor an alpha
69
+ # channel has one frame and no transparency, whatever the file contains.
70
+ def inferable_formats
71
+ DynamicImage::Format.formats.reject { |f| f.animated? || f.alpha? }
72
+ end
73
+
74
+ # Written with update_all, so no files are read, no callbacks run and updated_at is left alone.
75
+ def infer_from_format
76
+ inferable_formats.each do |format|
77
+ @inferred += pending.where(content_type: format.content_types)
78
+ .update_all(frame_count: 1, alpha: false)
79
+ end
80
+ @inferred
81
+ end
82
+
59
83
  def ensure_columns
60
84
  missing = COLUMNS.reject { |c| model.column_names.include?(c.to_s) }
61
85
  return if missing.empty?
@@ -65,20 +89,30 @@ module DynamicImage
65
89
  "Run bin/rails generate dynamic_image:upgrade #{model.name}"
66
90
  end
67
91
 
68
- def process(record)
69
- record.with_data_file { |path| apply(record, path) }
92
+ # Reads run in threads and touch no database connection. The writes happen on the calling thread, so the
93
+ # counters stay consistent.
94
+ def read(records)
95
+ return records.map { |record| [record, values_for(record)] } if concurrency == 1
96
+
97
+ records.map { |record| Thread.new { [record, values_for(record)] } }
98
+ .map(&:value)
99
+ end
100
+
101
+ # Metadata reads lazily, so the values have to be resolved before the file goes out of scope.
102
+ def values_for(record)
103
+ record.with_data_file do |path|
104
+ metadata = DynamicImage::Metadata.new(path)
105
+ [metadata.frame_count, metadata.alpha?] if metadata.valid?
106
+ end
70
107
  rescue Dis::Errors::NotFoundError
71
- @skipped += 1
108
+ nil
72
109
  end
73
110
 
74
- # Metadata reads lazily, so the values have to be resolved before the file goes out of scope. Written with
75
- # update_columns so that no callbacks run.
76
- def apply(record, path)
77
- metadata = DynamicImage::Metadata.new(path)
78
- return @skipped += 1 unless metadata.valid?
111
+ # Written with update_columns so that no callbacks run.
112
+ def write(record, values)
113
+ return @skipped += 1 if values.nil?
79
114
 
80
- record.update_columns(frame_count: metadata.frame_count,
81
- alpha: metadata.alpha?)
115
+ record.update_columns(frame_count: values.first, alpha: values.last)
82
116
  @updated += 1
83
117
  end
84
118
  end
@@ -32,7 +32,7 @@ module DynamicImage
32
32
  PROCESSED_FORMATS = %i[gif jpeg jpg jxl png tiff webp].freeze
33
33
 
34
34
  # Additional formats +original+ and +download+ will serve.
35
- STORED_FORMATS = (PROCESSED_FORMATS + %i[avif bmp heic]).freeze
35
+ STORED_FORMATS = (PROCESSED_FORMATS + %i[avif heic]).freeze
36
36
 
37
37
  included do
38
38
  before_action :verify_signed_params
@@ -19,7 +19,6 @@ module DynamicImage
19
19
 
20
20
  initializer "dynamic_image.mime_types" do
21
21
  Mime::Type.register "image/avif", :avif
22
- Mime::Type.register "image/bmp", :bmp
23
22
  Mime::Type.register "image/gif", :gif
24
23
  Mime::Type.register "image/heic", :heic, %w[image/heif]
25
24
  Mime::Type.register "image/jpeg", :jpg
@@ -191,13 +191,6 @@ module DynamicImage
191
191
  signature: ->(bytes) { iso_brands(bytes).intersect?(%w[avif avis]) }
192
192
  )
193
193
 
194
- register(
195
- "BMP",
196
- content_type: %w[image/bmp],
197
- extension: %w[.bmp],
198
- magic_bytes: ["\x42\x4d"]
199
- )
200
-
201
194
  register(
202
195
  "GIF",
203
196
  animated: true,
@@ -74,7 +74,7 @@ module DynamicImage
74
74
  end
75
75
 
76
76
  def animated?
77
- frame_count.to_i > 1
77
+ record.animated?
78
78
  end
79
79
 
80
80
  def alpha?
@@ -71,13 +71,17 @@ module DynamicImage
71
71
  before_validation :read_image_metadata, if: :data_changed?
72
72
  end
73
73
 
74
- # Returns true if the image holds more than one frame.
74
+ # Returns true if the image holds more than one frame in a format that renders them.
75
75
  #
76
- # Images stored before +frame_count+ existed have none, and are taken to be still.
76
+ # Images stored before +frame_count+ existed have none, and are taken to be still. A multi-page document in a
77
+ # format that isn't animated, such as TIFF, is also still: {DynamicImage::ImageReader} loads only its first
78
+ # page.
77
79
  #
78
80
  # @return [Boolean]
79
81
  def animated?
80
- has_attribute?(:frame_count) && frame_count.to_i > 1
82
+ return false unless has_attribute?(:frame_count) && frame_count.to_i > 1
83
+
84
+ DynamicImage::Format.content_type(content_type)&.animated? || false
81
85
  end
82
86
 
83
87
  # Returns the alt text for the image, or nil if none has been set.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DynamicImage
4
- VERSION = "3.1.1"
4
+ VERSION = "3.1.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynamic_image
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.1
4
+ version: 3.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Inge Jørgensen