format_parser 0.22.1 → 0.24.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: 05470fef0edec68c427b9680cf94a88a8f3286e2279b2daf13e87679324eb061
4
- data.tar.gz: f65ed1d390e4e70bd34a3b01b5d54d39e4076d18ebf15fc20196c2746c86122f
3
+ metadata.gz: b680c35747790df90503edc5cb7ddda0d8929ab47c945fe6c13f5fa4319ba747
4
+ data.tar.gz: e8cf15c34f0422a99f3e09b30ab7ca50d54c17f2933762414323f34f06100260
5
5
  SHA512:
6
- metadata.gz: e6e9e2d1fbfe6e813d1e0d9d2c102ce3f031fd37395cc75807ca7b527b1a2fc4a9a3c41baaf9e2cdaa601063cded92ef446ce913f2210f3d82e063126873d54d
7
- data.tar.gz: 86e166b2ac754fe5d7e42e471bcaa70cd54ae0aa22da3e0f85a6223213c2bd5a284f2b53416d03656dc291898837636678dbfba2e32b179a562ea7c8b05ef0bf
6
+ metadata.gz: 658e3d789e95c20b7a30e4a4c0cdb37fc186b0b696d8bfa1fd7b7b500bb375dfc0dc6873f908e20c45e871ac3831231adad3589457073bdf13697c79677b450f
7
+ data.tar.gz: 669af5c03327b8e048cee8401ac340b77b5bb990d08b4786c402451a092fdbea1d4a106a6404a6ac711baed2171eced775b37e5e5979e130fea2e833380e7598
@@ -1,3 +1,23 @@
1
+ ## 0.24.2
2
+ * Update gem id3tag to 0.14.0 to fix MP3 issues
3
+
4
+ ## 0.24.1
5
+ * Fix MP3 frames reading to jump correctly to the next bytes
6
+
7
+ ## 0.24.0
8
+ * The TIFF parser will now return :arw as format for Sony ARW files insted of :tif so that the caller can decide whether it
9
+ wants to deal with RAW processing or not
10
+
11
+ ## 0.23.1
12
+ * Updated gem exifr to fix problems related to jpeg files from Olympos microscopes, which often have bad thumbnail data
13
+
14
+ ## 0.23.0
15
+ * Add ActiveStorage analyzer which can analyze ActiveStorage blobs. Enable it by setting
16
+ `config.active_storage.analyzers.prepend FormatParser::ActiveStorage::BlobAnalyzer`
17
+ * Ignore empty ID3 tags and do not allow them to overwrite others
18
+ * Update the id3tag dependency so that we can fallback to UTF8 instead of raising an error when parsing
19
+ MP3 files
20
+
1
21
  ## 0.22.1
2
22
  * Fix Zip parser to not raise error for invalid zip files, with an invalid central directory
3
23
 
@@ -31,8 +31,8 @@ Gem::Specification.new do |spec|
31
31
  spec.require_paths = ['lib']
32
32
 
33
33
  spec.add_dependency 'ks', '~> 0.0'
34
- spec.add_dependency 'exifr', '~> 1', '>= 1.3.4'
35
- spec.add_dependency 'id3tag', '~> 0.10', '>= 0.10.1'
34
+ spec.add_dependency 'exifr', '~> 1', '>= 1.3.7'
35
+ spec.add_dependency 'id3tag', '~> 0.14'
36
36
  spec.add_dependency 'faraday', '~> 0.13'
37
37
  spec.add_dependency 'measurometer', '~> 1'
38
38
 
@@ -0,0 +1,35 @@
1
+ require_relative 'blob_io'
2
+
3
+ # An analyzer class that can be hooked to ActiveStorage, in order to enable
4
+ # FormatParser to do the blob analysis instead of ActiveStorage builtin-analyzers.
5
+ # Invoked if properly integrated in Rails initializer.
6
+
7
+ module FormatParser
8
+ module ActiveStorage
9
+ class BlobAnalyzer
10
+ # Format parser is able to handle a lot of format so by default it will accept all files
11
+ #
12
+ # @return [Boolean, true] always return true
13
+ def self.accept?(_blob)
14
+ true
15
+ end
16
+
17
+ def initialize(blob)
18
+ @blob = blob
19
+ end
20
+
21
+ # @return [Hash] file metadatas
22
+ def metadata
23
+ io = BlobIO.new(@blob)
24
+ parsed_file = FormatParser.parse(io)
25
+
26
+ if parsed_file
27
+ # We symbolize keys because of existing output hash format of ImageAnalyzer
28
+ parsed_file.as_json.symbolize_keys
29
+ else
30
+ logger.info "Skipping file analysis because FormatParser doesn't support the file"
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,51 @@
1
+ # Acts as a proxy to turn ActiveStorage file into IO object
2
+
3
+ module FormatParser
4
+ module ActiveStorage
5
+ class BlobIO
6
+ # @param blob[ActiveStorage::Blob] the file with linked service
7
+ # @return [BlobIO]
8
+ def initialize(blob)
9
+ @blob = blob
10
+ @service = blob.service
11
+ @pos = 0
12
+ end
13
+
14
+ # Emulates IO#read, but requires the number of bytes to read.
15
+ # Rely on `ActiveStorage::Service.download_chunk` of each hosting type (local, S3, Azure, etc)
16
+ #
17
+ # @param n_bytes[Integer] how many bytes to read
18
+ # @return [String] the read bytes
19
+ def read(n_bytes)
20
+ # HTTP ranges are exclusive.
21
+ http_range = (@pos..(@pos + n_bytes - 1))
22
+ body = @service.download_chunk(@blob.key, http_range)
23
+ @pos += body.bytesize
24
+ body.force_encoding(Encoding::ASCII_8BIT)
25
+ end
26
+
27
+ # Emulates IO#seek
28
+ #
29
+ # @param [Integer] offset size
30
+ # @return [Integer] always return 0, `seek` only mutates `pos` attribute
31
+ def seek(offset)
32
+ @pos = offset
33
+ 0
34
+ end
35
+
36
+ # Emulates IO#size.
37
+ #
38
+ # @return [Integer] the size of the blob size from ActiveStorage
39
+ def size
40
+ @blob.byte_size
41
+ end
42
+
43
+ # Emulates IO#pos
44
+ #
45
+ # @return [Integer] the current offset (in bytes) of the io
46
+ def pos
47
+ @pos
48
+ end
49
+ end
50
+ end
51
+ end
@@ -18,6 +18,7 @@ module FormatParser
18
18
  require_relative 'remote_io'
19
19
  require_relative 'io_constraint'
20
20
  require_relative 'care'
21
+ require_relative 'active_storage/blob_analyzer'
21
22
 
22
23
  # Define Measurometer in the internal namespace as well
23
24
  # so that we stay compatible for the applications that use it
@@ -1,3 +1,3 @@
1
1
  module FormatParser
2
- VERSION = '0.22.1'
2
+ VERSION = '0.24.2'
3
3
  end
@@ -20,13 +20,14 @@ class FormatParser::MP3Parser
20
20
 
21
21
  # We limit the number of MPEG frames we scan
22
22
  # to obtain our duration estimation
23
- MAX_FRAMES_TO_SCAN = 128
23
+ MAX_FRAMES_TO_SCAN = 500
24
24
 
25
25
  # Default frame size for mp3
26
26
  SAMPLES_PER_FRAME = 1152
27
27
 
28
28
  # For some edge cases
29
29
  ZIP_LOCAL_ENTRY_SIGNATURE = "PK\x03\x04\x14\x00".b
30
+ PNG_HEADER_BYTES = [137, 80, 78, 71, 13, 10, 26, 10].pack('C*')
30
31
 
31
32
  # Wraps the Tag object returned by ID3Tag in such
32
33
  # a way that a usable JSON representation gets
@@ -44,7 +45,7 @@ class FormatParser::MP3Parser
44
45
  tag = __getobj__
45
46
  MEMBERS.each_with_object({}) do |k, h|
46
47
  value = tag.public_send(k)
47
- h[k] = value if value
48
+ h[k] = value if value && !value.empty?
48
49
  end
49
50
  end
50
51
  end
@@ -60,8 +61,12 @@ class FormatParser::MP3Parser
60
61
  # To avoid having that happen, we check for the PKZIP signature -
61
62
  # local entry header signature - at the very start of the file.
62
63
  # If the file is too small safe_read will fail too and the parser
63
- # will terminate here.
64
- return if safe_read(io, 6) == ZIP_LOCAL_ENTRY_SIGNATURE
64
+ # will terminate here. Same with PNGs. In the future
65
+ # we should implement "confidence" for MP3 as of all our formats
66
+ # it is by far the most lax.
67
+ header = safe_read(io, 8)
68
+ return if header.start_with?(ZIP_LOCAL_ENTRY_SIGNATURE)
69
+ return if header.start_with?(PNG_HEADER_BYTES)
65
70
 
66
71
  # Read all the ID3 tags (or at least attempt to)
67
72
  io.seek(0)
@@ -81,7 +86,7 @@ class FormatParser::MP3Parser
81
86
 
82
87
  first_frame = initial_frames.first
83
88
 
84
- id3tags_hash = blend_id3_tags_into_hash(*tags)
89
+ id3tags_hash = with_id3tag_local_configs { blend_id3_tags_into_hash(*tags) }
85
90
 
86
91
  file_info = FormatParser::Audio.new(
87
92
  format: :mp3,
@@ -131,27 +136,28 @@ class FormatParser::MP3Parser
131
136
  # if you have a minute. https://pypi.python.org/pypi/tinytag
132
137
  def parse_mpeg_frames(io)
133
138
  mpeg_frames = []
139
+ bytes_to_read = 4
134
140
 
135
141
  MAX_FRAMES_TO_SCAN.times do |frame_i|
136
142
  # Read through until we can latch onto the 11 sync bits. Read in 4-byte
137
143
  # increments to save on read() calls
138
- data = io.read(4)
144
+ data = io.read(bytes_to_read)
139
145
 
140
146
  # If we are at EOF - stop iterating
141
- break unless data && data.bytesize == 4
147
+ break unless data && data.bytesize == bytes_to_read
142
148
 
143
149
  # Look for the sync pattern. It can be either the last byte being 0xFF,
144
150
  # or any of the 2 bytes in sequence being 0xFF and > 0xF0.
145
151
  four_bytes = data.unpack('C4')
146
152
  seek_jmp = sync_bytes_offset_in_4_byte_seq(four_bytes)
147
153
  if seek_jmp > 0
148
- io.seek(io.pos + seek_jmp)
154
+ io.seek(io.pos - bytes_to_read + seek_jmp)
149
155
  next
150
156
  end
151
157
 
152
158
  # Once we are past that stage we have latched onto a sync frame header
153
159
  sync, conf, bitrate_freq, rest = four_bytes
154
- frame_detail = parse_mpeg_frame_header(io.pos - 4, sync, conf, bitrate_freq, rest)
160
+ frame_detail = parse_mpeg_frame_header(io.pos - bytes_to_read, sync, conf, bitrate_freq, rest)
155
161
  mpeg_frames << frame_detail
156
162
 
157
163
  # There might be a xing header in the first frame that contains
@@ -166,7 +172,7 @@ class FormatParser::MP3Parser
166
172
  end
167
173
  end
168
174
  if frame_detail.frame_length > 1 # jump over current frame body
169
- io.seek(io.pos + frame_detail.frame_length - 4)
175
+ io.seek(io.pos + frame_detail.frame_length - bytes_to_read)
170
176
  end
171
177
  end
172
178
  [nil, mpeg_frames]
@@ -293,5 +299,14 @@ class FormatParser::MP3Parser
293
299
  attrs
294
300
  end
295
301
 
302
+ def with_id3tag_local_configs
303
+ ID3Tag.local_configuration do |c|
304
+ c.string_encode_options = { invalid: :replace, undef: :replace }
305
+ c.source_encoding_fallback = Encoding::UTF_8
306
+
307
+ yield
308
+ end
309
+ end
310
+
296
311
  FormatParser.register_parser new, natures: :audio, formats: :mp3, priority: 99
297
312
  end
@@ -26,7 +26,7 @@ class FormatParser::TIFFParser
26
26
  h = exif_data.height || exif_data.pixel_y_dimension
27
27
 
28
28
  FormatParser::Image.new(
29
- format: :tif,
29
+ format: arw?(exif_data) ? :arw : :tif, # Specify format as arw for Sony ARW format images, else tif
30
30
  width_px: w,
31
31
  height_px: h,
32
32
  display_width_px: exif_data.rotated? ? h : w,
@@ -43,5 +43,11 @@ class FormatParser::TIFFParser
43
43
  safe_read(io, 2) == 'CR'
44
44
  end
45
45
 
46
+ # Similar to how exiftool determines the image type as ARW, we are implementing a check here
47
+ # https://github.com/exiftool/exiftool/blob/e969456372fbaf4b980fea8bb094d71033ac8bf7/lib/Image/ExifTool/Exif.pm#L929
48
+ def arw?(exif_data)
49
+ exif_data.compression == 6 && exif_data.new_subfile_type == 1 && exif_data.make == 'SONY'
50
+ end
51
+
46
52
  FormatParser.register_parser new, natures: :image, formats: :tif
47
53
  end
@@ -0,0 +1,36 @@
1
+ require 'spec_helper'
2
+
3
+ describe FormatParser::ActiveStorage::BlobIO do
4
+ let(:blob_service) { double }
5
+ let(:blob) { double(key: 'blob_key', service: blob_service, byte_size: 43000) }
6
+ let(:io) { described_class.new(blob) }
7
+ let(:fixture_path) { fixtures_dir + '/test.png' }
8
+
9
+ it_behaves_like 'an IO object compatible with IOConstraint'
10
+
11
+ describe '#read' do
12
+ it 'reads io using download_chunk from ActiveStorage#Service' do
13
+ allow(blob_service).to receive(:download_chunk) { 'a' }
14
+
15
+ expect(io.read(1)).to eq('a')
16
+ end
17
+
18
+ it 'updates #pos on read' do
19
+ allow(blob_service).to receive(:download_chunk) { 'a' }
20
+
21
+ expect { io.read(1) }.to change { io.pos }.from(0).to(1)
22
+ end
23
+ end
24
+
25
+ describe '#seek' do
26
+ it 'updates @pos' do
27
+ expect { io.seek(10) }.to change { io.pos }.from(0).to(10)
28
+ end
29
+ end
30
+
31
+ describe '#size' do
32
+ it 'returns the size of the blob byte_size' do
33
+ expect(io.size).to eq(blob.byte_size)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+
3
+ def skip_reason
4
+ if RUBY_ENGINE == 'jruby'
5
+ 'Skipping because JRuby have randon failing issue'
6
+ elsif RUBY_VERSION.to_f < 2.5
7
+ 'Skipping because Rails testing script use Rails 6, who does not support Ruby bellow 2.5'
8
+ else
9
+ false
10
+ end
11
+ end
12
+
13
+ describe 'Rails app with ActiveStorage and format-parser', skip: skip_reason do
14
+ describe 'local hosting with ActiveStorage disk adapter' do
15
+ it 'parse local file with format_parser' do
16
+ clean_env do
17
+ cmd = 'ruby spec/integration/active_storage/rails_app.rb'
18
+ cmd_status = ruby_script_runner(cmd)
19
+ expect(cmd_status[:stdout].last).to match(/1 runs, 3 assertions, 0 failures, 0 errors, 0 skips/)
20
+ expect(cmd_status[:exitstatus]).to eq(0)
21
+ end
22
+ end
23
+ end
24
+
25
+ def ruby_script_runner(cmd)
26
+ require 'open3'
27
+ cmd_status = { stdout: [], exitstatus: nil }
28
+ Open3.popen2(cmd) do |_stdin, stdout, wait_thr|
29
+ frame_stdout do
30
+ while line = stdout.gets
31
+ puts "| #{line}"
32
+ cmd_status[:stdout] << line
33
+ end
34
+ end
35
+ cmd_status[:exitstatus] = wait_thr.value.exitstatus
36
+ end
37
+ cmd_status
38
+ end
39
+
40
+ def frame_stdout
41
+ puts
42
+ puts '-' * 50
43
+ yield
44
+ puts '-' * 50
45
+ end
46
+
47
+ def clean_env
48
+ if Bundler.respond_to?(:with_unbundled_env)
49
+ Bundler.with_unbundled_env do
50
+ yield
51
+ end
52
+ else
53
+ Bundler.with_clean_env do
54
+ yield
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,72 @@
1
+ require 'bundler/inline'
2
+
3
+ gemfile(true) do
4
+ source 'https://rubygems.org'
5
+
6
+ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
7
+
8
+ gem 'rails', '6.0.3'
9
+ gem 'sqlite3'
10
+ gem 'format_parser', path: './'
11
+ end
12
+
13
+ require 'active_record/railtie'
14
+ require 'active_storage/engine'
15
+ require 'tmpdir'
16
+
17
+ class TestApp < Rails::Application
18
+ config.root = __dir__
19
+ config.hosts << 'example.org'
20
+ config.eager_load = false
21
+ config.session_store :cookie_store, key: 'cookie_store_key'
22
+ secrets.secret_key_base = 'secret_key_base'
23
+
24
+ config.logger = Logger.new('/dev/null')
25
+
26
+ config.active_storage.service = :local
27
+ config.active_storage.service_configurations = {
28
+ local: {
29
+ root: Dir.tmpdir,
30
+ service: 'Disk'
31
+ }
32
+ }
33
+
34
+ config.active_storage.analyzers.prepend FormatParser::ActiveStorage::BlobAnalyzer
35
+ end
36
+
37
+ ENV['DATABASE_URL'] = 'sqlite3::memory:'
38
+
39
+ Rails.application.initialize!
40
+
41
+ require ActiveStorage::Engine.root.join('db/migrate/20170806125915_create_active_storage_tables.rb').to_s
42
+
43
+ ActiveRecord::Schema.define do
44
+ CreateActiveStorageTables.new.change
45
+
46
+ create_table :users, force: true
47
+ end
48
+
49
+ class User < ActiveRecord::Base
50
+ has_one_attached :profile_picture
51
+ end
52
+
53
+ require 'minitest/autorun'
54
+ require 'open-uri'
55
+
56
+ describe User do
57
+ describe "profile_picture's metadatas" do
58
+ it 'parse metadatas with format_parser' do
59
+ user = User.create
60
+ user.profile_picture.attach(
61
+ filename: 'cat.png',
62
+ io: URI.open('https://freesvg.org/img/1416155153.png')
63
+ )
64
+
65
+ user.profile_picture.analyze
66
+
67
+ _(user.profile_picture.metadata[:width_px]).must_equal 500
68
+ _(user.profile_picture.metadata[:height_px]).must_equal 296
69
+ _(user.profile_picture.metadata[:color_mode]).must_equal 'rgba'
70
+ end
71
+ end
72
+ end
@@ -15,6 +15,12 @@ describe FormatParser::MP3Parser do
15
15
  expect(parsed.media_duration_seconds).to be_within(0.1).of(0.836)
16
16
  end
17
17
 
18
+ it 'does not misdetect a PNG' do
19
+ fpath = fixtures_dir + '/PNG/anim.png'
20
+ parsed = subject.call(File.open(fpath, 'rb'))
21
+ expect(parsed).to be_nil
22
+ end
23
+
18
24
  describe 'title/artist/album attributes' do
19
25
  let(:parsed) { subject.call(File.open(fpath, 'rb')) }
20
26
 
@@ -37,6 +43,14 @@ describe FormatParser::MP3Parser do
37
43
  expect(parsed.album).to be_nil
38
44
  end
39
45
  end
46
+
47
+ context 'when has an empty tag' do
48
+ let(:fpath) { fixtures_dir + '/MP3/id3v2_with_empty_tag.mp3' }
49
+
50
+ it 'ignores the empty tags' do
51
+ expect(parsed.intrinsics[:genre]).to eq('Rock')
52
+ end
53
+ end
40
54
  end
41
55
 
42
56
  it 'decodes and estimates duration for a CBR MP3' do
@@ -71,6 +85,25 @@ describe FormatParser::MP3Parser do
71
85
  expect(prepped.pos).to eq(3145738)
72
86
  end
73
87
 
88
+ it 'does not raise error when a tag frame has unsupported encoding' do
89
+ fpath = fixtures_dir + '/MP3/id3v2_frame_with_invalid_encoding.mp3'
90
+
91
+ parsed = subject.call(File.open(fpath, 'rb'))
92
+
93
+ expect(parsed.nature). to eq(:audio)
94
+ expect(parsed.album).to eq('wetransfer')
95
+ expect(parsed.artist).to eq('wetransfer')
96
+ expect(parsed.title).to eq('test')
97
+ end
98
+
99
+ it 'reads the mpeg frames correctly' do
100
+ fpath = fixtures_dir + '/MP3/test_read_frames.mp3'
101
+
102
+ parsed = subject.call(File.open(fpath, 'rb'))
103
+
104
+ expect(parsed.audio_sample_rate_hz). to eq(48000)
105
+ end
106
+
74
107
  it 'parses the Cassy MP3' do
75
108
  fpath = fixtures_dir + '/MP3/Cassy.mp3'
76
109
  parsed = subject.call(File.open(fpath, 'rb'))
@@ -47,12 +47,15 @@ describe FormatParser::TIFFParser do
47
47
  expect(parsed.intrinsics[:exif]).not_to be_nil
48
48
  end
49
49
 
50
- it 'correctly extracts dimensions for a Sony ARW fixture' do
50
+ it 'parses Sony ARW fixture as arw format file' do
51
51
  arw_path = fixtures_dir + '/ARW/RAW_SONY_ILCE-7RM2.ARW'
52
52
 
53
53
  parsed = subject.call(File.open(arw_path, 'rb'))
54
54
 
55
55
  expect(parsed).not_to be_nil
56
+ expect(parsed.nature).to eq(:image)
57
+ expect(parsed.format).to eq(:arw)
58
+
56
59
  expect(parsed.width_px).to eq(7952)
57
60
  expect(parsed.height_px).to eq(5304)
58
61
  expect(parsed.intrinsics[:exif]).not_to be_nil
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: format_parser
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.22.1
4
+ version: 0.24.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Noah Berman
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2020-08-26 00:00:00.000000000 Z
12
+ date: 2020-09-18 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: ks
@@ -34,7 +34,7 @@ dependencies:
34
34
  version: '1'
35
35
  - - ">="
36
36
  - !ruby/object:Gem::Version
37
- version: 1.3.4
37
+ version: 1.3.7
38
38
  type: :runtime
39
39
  prerelease: false
40
40
  version_requirements: !ruby/object:Gem::Requirement
@@ -44,27 +44,21 @@ dependencies:
44
44
  version: '1'
45
45
  - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: 1.3.4
47
+ version: 1.3.7
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: id3tag
50
50
  requirement: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: '0.10'
55
- - - ">="
56
- - !ruby/object:Gem::Version
57
- version: 0.10.1
54
+ version: '0.14'
58
55
  type: :runtime
59
56
  prerelease: false
60
57
  version_requirements: !ruby/object:Gem::Requirement
61
58
  requirements:
62
59
  - - "~>"
63
60
  - !ruby/object:Gem::Version
64
- version: '0.10'
65
- - - ">="
66
- - !ruby/object:Gem::Version
67
- version: 0.10.1
61
+ version: '0.14'
68
62
  - !ruby/object:Gem::Dependency
69
63
  name: faraday
70
64
  requirement: !ruby/object:Gem::Requirement
@@ -202,6 +196,8 @@ files:
202
196
  - Rakefile
203
197
  - exe/format_parser_inspect
204
198
  - format_parser.gemspec
199
+ - lib/active_storage/blob_analyzer.rb
200
+ - lib/active_storage/blob_io.rb
205
201
  - lib/archive.rb
206
202
  - lib/attributes_json.rb
207
203
  - lib/audio.rb
@@ -241,6 +237,8 @@ files:
241
237
  - lib/read_limits_config.rb
242
238
  - lib/remote_io.rb
243
239
  - lib/video.rb
240
+ - spec/active_storage/blob_io_spec.rb
241
+ - spec/active_storage/rails_app_spec.rb
244
242
  - spec/attributes_json_spec.rb
245
243
  - spec/care_spec.rb
246
244
  - spec/esoteric_formats_spec.rb
@@ -248,6 +246,7 @@ files:
248
246
  - spec/format_parser_inspect_spec.rb
249
247
  - spec/format_parser_spec.rb
250
248
  - spec/hash_utils_spec.rb
249
+ - spec/integration/active_storage/rails_app.rb
251
250
  - spec/io_utils_spec.rb
252
251
  - spec/parsers/aiff_parser_spec.rb
253
252
  - spec/parsers/bmp_parser_spec.rb