format_parser 1.5.0 → 1.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +9 -1
- data/lib/format_parser/version.rb +1 -1
- data/lib/parsers/aac_parser/adts_header_info.rb +138 -0
- data/lib/parsers/aac_parser.rb +35 -0
- data/lib/parsers/arw_parser.rb +50 -0
- data/lib/parsers/tiff_parser.rb +5 -6
- data/spec/parsers/aac_parser_spec.rb +87 -0
- data/spec/parsers/adts_header_info_spec.rb +38 -0
- data/spec/parsers/arw_parser_spec.rb +119 -0
- data/spec/parsers/tiff_parser_spec.rb +9 -15
- metadata +12 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b3cef665ae16efd68e8da952fd4656e2d9403f3899bd58839da3d8026db91f4
|
4
|
+
data.tar.gz: 7b0ec88efc2ea62f526699a4041cb3f1b3062994d2a6e0b24c2cfdf247aaf532
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 24c6379ef4fd3b5a9f061c6fc40fd8c0498ad33213684d08dd27a8b8994ba40a98bf1fa18a6d6b3b8189aa71436ec9bb394e3b8d41a8dd3ca90a5b93d0f1718a
|
7
|
+
data.tar.gz: a2d3df2c17d2559aa99f52f04624032a9243915f2a1b28a6f3626bd3b9112eb8c325b0c9a286864d25c2b4e92a44a8939448a85d1004ac5d48c2f81f747749c1
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -149,6 +149,14 @@ Unless specified otherwise in this section the fixture files are MIT licensed an
|
|
149
149
|
### MP3
|
150
150
|
- Cassy.mp3 has been produced by WeTransfer and may be used with the library for the purposes of testing
|
151
151
|
|
152
|
+
### AAC
|
153
|
+
- Originals music files: “Furious Freak” and “Galway”, Kevin MacLeod (incompetech.com), Licensed under Creative Commons: By Attribution 3.0, http://creativecommons.org/licenses/by/3.0/
|
154
|
+
- The AAC samples were converted from 'wav' format and made available [here](https://espressif-docs.readthedocs-hosted.com/projects/esp-adf/en/latest/design-guide/audio-samples.html) by Espressif Systems, as part of their audio development framework (under the ESPRESSIF MIT License).
|
155
|
+
- Files:
|
156
|
+
- ff-16b-2c-44100hz.aac
|
157
|
+
- ff-16b-1c-44100hz.aac
|
158
|
+
- gs-16b-2c-44100hz.aac
|
159
|
+
- gs-16b-1c-44100hz.aac
|
152
160
|
### FDX
|
153
161
|
- fixture.fdx was created by one of the project maintainers and is MIT licensed
|
154
162
|
|
@@ -186,7 +194,7 @@ Unless specified otherwise in this section the fixture files are MIT licensed an
|
|
186
194
|
- `IMG_9266_*.tif` and all it's variations were created by the project maintainers
|
187
195
|
|
188
196
|
### ARW
|
189
|
-
- ARW
|
197
|
+
- ARW examples are downloaded from http://www.rawsamples.ch/ and are Creative Common Licensed.
|
190
198
|
|
191
199
|
### ZIP
|
192
200
|
- The .zip fixture files have been created by the project maintainers
|
@@ -0,0 +1,138 @@
|
|
1
|
+
# This is a representation of the relevant information found in an Audio Data Transport Stream (ADTS) file header.
|
2
|
+
class FormatParser::AdtsHeaderInfo
|
3
|
+
attr_accessor :mpeg_version, :layer, :protection_absence, :profile, :mpeg4_sampling_frequency_index,
|
4
|
+
:mpeg4_channel_config, :originality, :home_usage, :frame_length, :buffer_fullness,
|
5
|
+
:aac_frames_per_adts_frame
|
6
|
+
|
7
|
+
# An ADTS header has the following format, when represented in bits:
|
8
|
+
# AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)
|
9
|
+
# The chunks represented by these letters have specific meanings, as described here:
|
10
|
+
# https://wiki.multimedia.cx/index.php/ADTS
|
11
|
+
|
12
|
+
AAC_ADTS_HEADER_BITS_CHUNK_SIZES = [
|
13
|
+
['A', 12], ['B', 1], ['C', 2], ['D', 1],
|
14
|
+
['E', 2], ['F', 4], ['G', 1], ['H', 3],
|
15
|
+
['I', 1], ['J', 1], ['K', 1], ['L', 1],
|
16
|
+
['M', 13], ['O', 11], ['P', 2], ['Q', 16]
|
17
|
+
]
|
18
|
+
MPEG4_AUDIO_OBJECT_TYPE_RANGE = 0..45
|
19
|
+
MPEG4_AUDIO_SAMPLING_FREQUENCY_RANGE = 0..14
|
20
|
+
MPEG4_AUDIO_SAMPLING_FREQUENCY_HASH = {
|
21
|
+
0 => 96000, 1 => 88200, 2 => 64000,
|
22
|
+
3 => 48000, 4 => 44100, 5 => 32000,
|
23
|
+
6 => 24000, 7 => 22050, 8 => 16000,
|
24
|
+
9 => 12000, 10 => 11025, 11 => 8000,
|
25
|
+
12 => 7350, 13 => 'Reserved', 14 => 'Reserved'
|
26
|
+
}
|
27
|
+
AAC_PROFILE_DESCRIPTION_HASH = {
|
28
|
+
0 => 'AAC_MAIN',
|
29
|
+
1 => 'AAC_LC (Low Complexity)',
|
30
|
+
2 => 'AAC_SSR (Scaleable Sampling Rate)',
|
31
|
+
3 => 'AAC_LTP (Long Term Prediction)'
|
32
|
+
}
|
33
|
+
MPEG_VERSION_HASH = { 0 => 'MPEG-4', 1 => 'MPEG-2'}
|
34
|
+
|
35
|
+
def mpeg4_sampling_frequency
|
36
|
+
if !@mpeg4_sampling_frequency_index.nil? && MPEG4_AUDIO_SAMPLING_FREQUENCY_HASH.key?(@mpeg4_sampling_frequency_index)
|
37
|
+
return MPEG4_AUDIO_SAMPLING_FREQUENCY_HASH[@mpeg4_sampling_frequency_index]
|
38
|
+
end
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def profile_description
|
43
|
+
if !@profile.nil? && AAC_PROFILE_DESCRIPTION_HASH.key?(@profile)
|
44
|
+
return AAC_PROFILE_DESCRIPTION_HASH[@profile]
|
45
|
+
end
|
46
|
+
nil
|
47
|
+
end
|
48
|
+
|
49
|
+
def mpeg_version_description
|
50
|
+
if !@mpeg_version.nil? && MPEG_VERSION_HASH.key?(@mpeg_version)
|
51
|
+
return MPEG_VERSION_HASH[@mpeg_version]
|
52
|
+
end
|
53
|
+
nil
|
54
|
+
end
|
55
|
+
|
56
|
+
def number_of_audio_channels
|
57
|
+
case @mpeg4_channel_config
|
58
|
+
when 1..6
|
59
|
+
@mpeg4_channel_config
|
60
|
+
when 7
|
61
|
+
8
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def fixed_bitrate?
|
68
|
+
# A buffer fullness value of 0x7FF (decimal: 2047) denotes a variable bitrate, for which buffer fullness isn't applicable
|
69
|
+
@buffer_fullness != 2047
|
70
|
+
end
|
71
|
+
|
72
|
+
# The frame rate - i.e. frames per second
|
73
|
+
def frame_rate
|
74
|
+
# An AAC sample uncompresses to 1024 PCM samples
|
75
|
+
mpeg4_sampling_frequency.to_f / 1024
|
76
|
+
end
|
77
|
+
|
78
|
+
# If the given bit array is a valid ADTS header, this method will parse it and return an instance of AdtsHeaderInfo.
|
79
|
+
# Will return nil if the header does not match the ADTS specifications.
|
80
|
+
def self.parse_adts_header(header_bits)
|
81
|
+
result = FormatParser::AdtsHeaderInfo.new
|
82
|
+
|
83
|
+
AAC_ADTS_HEADER_BITS_CHUNK_SIZES.each do |letter_size|
|
84
|
+
letter = letter_size[0]
|
85
|
+
chunk_size = letter_size[1]
|
86
|
+
chunk = header_bits.shift(chunk_size)
|
87
|
+
decimal_number = chunk.join.to_i(2)
|
88
|
+
|
89
|
+
# Skipping data represented by the letters G, K, L, Q, as we are not interested in those values.
|
90
|
+
case letter
|
91
|
+
when 'A'
|
92
|
+
# Syncword, all bits must be set to 1
|
93
|
+
return nil unless chunk.all? { |bit| bit == '1' }
|
94
|
+
when 'B'
|
95
|
+
# MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2
|
96
|
+
result.mpeg_version = decimal_number
|
97
|
+
when 'C'
|
98
|
+
# Layer, always set to 0
|
99
|
+
return nil unless decimal_number == 0
|
100
|
+
when 'D'
|
101
|
+
# Protection absence, set to 1 if there is no CRC and 0 if there is CRC
|
102
|
+
result.protection_absence = decimal_number == 1
|
103
|
+
when 'E'
|
104
|
+
# AAC Profile
|
105
|
+
return nil unless MPEG4_AUDIO_OBJECT_TYPE_RANGE.include?(decimal_number + 1)
|
106
|
+
result.profile = decimal_number
|
107
|
+
when 'F'
|
108
|
+
# MPEG-4 Sampling Frequency Index (15 is forbidden)
|
109
|
+
return nil unless MPEG4_AUDIO_SAMPLING_FREQUENCY_RANGE.include?(decimal_number)
|
110
|
+
result.mpeg4_sampling_frequency_index = decimal_number
|
111
|
+
when 'H'
|
112
|
+
# MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an in-band PCE (Program Config Element))
|
113
|
+
result.mpeg4_channel_config = decimal_number
|
114
|
+
when 'I'
|
115
|
+
# Originality, set to 1 to signal originality of the audio and 0 otherwise
|
116
|
+
result.originality = decimal_number == 1
|
117
|
+
when 'J'
|
118
|
+
# Home, set to 1 to signal home usage of the audio and 0 otherwise
|
119
|
+
result.home_usage = decimal_number == 1
|
120
|
+
when 'M'
|
121
|
+
# Frame length, length of the ADTS frame including headers and CRC check (protectionabsent == 1? 7: 9)
|
122
|
+
# We expect this to be higher than the header length, but we won't impose any other restrictions
|
123
|
+
header_length = result.protection_absence ? 7 : 9
|
124
|
+
return nil unless decimal_number > header_length
|
125
|
+
result.frame_length = decimal_number
|
126
|
+
when 'O'
|
127
|
+
# Buffer fullness, states the bit-reservoir per frame.
|
128
|
+
# It is merely an informative field with no clear use case defined in the specification.
|
129
|
+
result.buffer_fullness = decimal_number
|
130
|
+
when 'P'
|
131
|
+
# Number of AAC frames (RDBs (Raw Data Blocks)) in ADTS frame minus 1. For maximum compatibility always use one AAC frame per ADTS frame.
|
132
|
+
result.aac_frames_per_adts_frame = decimal_number + 1
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
result
|
137
|
+
end
|
138
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require_relative 'aac_parser/adts_header_info'
|
2
|
+
|
3
|
+
class FormatParser::AACParser
|
4
|
+
include FormatParser::IOUtils
|
5
|
+
|
6
|
+
AAC_MIME_TYPE = 'audio/aac'
|
7
|
+
|
8
|
+
def likely_match?(filename)
|
9
|
+
filename =~ /\.aac$/i
|
10
|
+
end
|
11
|
+
|
12
|
+
def call(raw_io)
|
13
|
+
io = FormatParser::IOConstraint.new(raw_io)
|
14
|
+
header = safe_read(io, 9)
|
15
|
+
header_bits = header.unpack('B*').first.split('')
|
16
|
+
|
17
|
+
header_info = FormatParser::AdtsHeaderInfo.parse_adts_header(header_bits)
|
18
|
+
return if header_info.nil?
|
19
|
+
|
20
|
+
FormatParser::Audio.new(
|
21
|
+
title: nil,
|
22
|
+
album: nil,
|
23
|
+
artist: nil,
|
24
|
+
format: :aac,
|
25
|
+
num_audio_channels: header_info.number_of_audio_channels,
|
26
|
+
audio_sample_rate_hz: header_info.mpeg4_sampling_frequency,
|
27
|
+
media_duration_seconds: nil,
|
28
|
+
media_duration_frames: nil,
|
29
|
+
intrinsics: nil,
|
30
|
+
content_type: AAC_MIME_TYPE
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
FormatParser.register_parser new, natures: :audio, formats: :aac
|
35
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative 'exif_parser'
|
2
|
+
|
3
|
+
class FormatParser::ARWParser
|
4
|
+
include FormatParser::IOUtils
|
5
|
+
include FormatParser::EXIFParser
|
6
|
+
|
7
|
+
# Standard TIFF headers
|
8
|
+
MAGIC_LE = [0x49, 0x49, 0x2A, 0x0].pack('C4')
|
9
|
+
MAGIC_BE = [0x4D, 0x4D, 0x0, 0x2A].pack('C4')
|
10
|
+
HEADER_BYTES = [MAGIC_LE, MAGIC_BE]
|
11
|
+
ARW_MIME_TYPE = 'image/x-sony-arw'
|
12
|
+
|
13
|
+
def likely_match?(filename)
|
14
|
+
filename =~ /\.arw$/i
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(io)
|
18
|
+
io = FormatParser::IOConstraint.new(io)
|
19
|
+
|
20
|
+
return unless HEADER_BYTES.include?(safe_read(io, 4))
|
21
|
+
exif_data = exif_from_tiff_io(io)
|
22
|
+
|
23
|
+
return unless valid?(exif_data)
|
24
|
+
|
25
|
+
w = exif_data.width || exif_data.pixel_x_dimension
|
26
|
+
h = exif_data.height || exif_data.pixel_y_dimension
|
27
|
+
|
28
|
+
FormatParser::Image.new(
|
29
|
+
format: :arw,
|
30
|
+
width_px: w,
|
31
|
+
height_px: h,
|
32
|
+
display_width_px: exif_data.rotated? ? h : w,
|
33
|
+
display_height_px: exif_data.rotated? ? w : h,
|
34
|
+
orientation: exif_data.orientation_sym,
|
35
|
+
intrinsics: { exif: exif_data },
|
36
|
+
content_type: ARW_MIME_TYPE,
|
37
|
+
)
|
38
|
+
rescue EXIFR::MalformedTIFF
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def valid?(exif_data)
|
43
|
+
# taken directly from tiff_parser.rb
|
44
|
+
# Similar to how exiftool determines the image type as ARW, we are implementing a check here
|
45
|
+
# https://github.com/exiftool/exiftool/blob/e969456372fbaf4b980fea8bb094d71033ac8bf7/lib/Image/ExifTool/Exif.pm#L929
|
46
|
+
exif_data.compression == 6 && exif_data.new_subfile_type == 1 && exif_data.make&.start_with?('SONY')
|
47
|
+
end
|
48
|
+
|
49
|
+
FormatParser.register_parser new, natures: :image, formats: :arw
|
50
|
+
end
|
data/lib/parsers/tiff_parser.rb
CHANGED
@@ -6,7 +6,6 @@ class FormatParser::TIFFParser
|
|
6
6
|
MAGIC_BE = [0x4D, 0x4D, 0x0, 0x2A].pack('C4')
|
7
7
|
HEADER_BYTES = [MAGIC_LE, MAGIC_BE]
|
8
8
|
TIFF_MIME_TYPE = 'image/tiff'
|
9
|
-
ARW_MIME_TYPE = 'image/x-sony-arw'
|
10
9
|
|
11
10
|
def likely_match?(filename)
|
12
11
|
filename =~ /\.tiff?$/i
|
@@ -28,20 +27,20 @@ class FormatParser::TIFFParser
|
|
28
27
|
exif_data = exif_from_tiff_io(io)
|
29
28
|
return unless exif_data
|
30
29
|
|
30
|
+
return if arw?(exif_data)
|
31
|
+
|
31
32
|
w = exif_data.width || exif_data.pixel_x_dimension
|
32
33
|
h = exif_data.height || exif_data.pixel_y_dimension
|
33
34
|
|
34
|
-
format = arw?(exif_data) ? :arw : :tif
|
35
|
-
mime_type = arw?(exif_data) ? ARW_MIME_TYPE : TIFF_MIME_TYPE
|
36
35
|
FormatParser::Image.new(
|
37
|
-
format:
|
36
|
+
format: :tif,
|
38
37
|
width_px: w,
|
39
38
|
height_px: h,
|
40
39
|
display_width_px: exif_data.rotated? ? h : w,
|
41
40
|
display_height_px: exif_data.rotated? ? w : h,
|
42
41
|
orientation: exif_data.orientation_sym,
|
43
42
|
intrinsics: {exif: exif_data},
|
44
|
-
content_type:
|
43
|
+
content_type: TIFF_MIME_TYPE,
|
45
44
|
)
|
46
45
|
rescue EXIFR::MalformedTIFF
|
47
46
|
nil
|
@@ -55,7 +54,7 @@ class FormatParser::TIFFParser
|
|
55
54
|
# Similar to how exiftool determines the image type as ARW, we are implementing a check here
|
56
55
|
# https://github.com/exiftool/exiftool/blob/e969456372fbaf4b980fea8bb094d71033ac8bf7/lib/Image/ExifTool/Exif.pm#L929
|
57
56
|
def arw?(exif_data)
|
58
|
-
exif_data.compression == 6 && exif_data.new_subfile_type == 1 && exif_data.make
|
57
|
+
exif_data.compression == 6 && exif_data.new_subfile_type == 1 && exif_data.make&.start_with?('SONY')
|
59
58
|
end
|
60
59
|
|
61
60
|
FormatParser.register_parser new, natures: :image, formats: :tif
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormatParser::AACParser do
|
4
|
+
it 'should match filenames with valid AAC extensions' do
|
5
|
+
filenames = ['audiofile', 'audio_file', 'audio-file', 'audio file', 'audio.file']
|
6
|
+
extensions = ['.aac', '.AAC', '.Aac', '.AAc', '.aAc', '.aAC', '.aaC']
|
7
|
+
filenames.each do |filename|
|
8
|
+
extensions.each do |extension|
|
9
|
+
expect(subject.likely_match?(filename + extension)).to be_truthy
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should not match filenames with invalid AAC extensions' do
|
15
|
+
extensions = ['.aa', '.ac', '.acc', '.mp3', '.ogg', '.wav', '.flac', '.m4a', '.m4b', '.m4p', '.m4r', '.3gp']
|
16
|
+
extensions.each do |extension|
|
17
|
+
expect(subject.likely_match?('audiofile' + extension)).to be_falsey
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should parse a short sample, single channel audio, 16 kb/s, 44100 HZ' do
|
22
|
+
file_path = fixtures_dir + '/AAC/gs-16b-1c-44100hz.aac'
|
23
|
+
parsed = subject.call(File.open(file_path, 'rb'))
|
24
|
+
|
25
|
+
expect(parsed).not_to be_nil
|
26
|
+
|
27
|
+
expect(parsed.nature).to eq(:audio)
|
28
|
+
expect(parsed.format).to eq(:aac)
|
29
|
+
expect(parsed.num_audio_channels).to eq(1)
|
30
|
+
expect(parsed.audio_sample_rate_hz).to eq(44100)
|
31
|
+
expect(parsed.content_type).to eq('audio/aac')
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should parse a short sample, two channel audio, 16 kb/s, 44100 HZ' do
|
35
|
+
file_path = fixtures_dir + '/AAC/gs-16b-2c-44100hz.aac'
|
36
|
+
parsed = subject.call(File.open(file_path, 'rb'))
|
37
|
+
|
38
|
+
expect(parsed).not_to be_nil
|
39
|
+
|
40
|
+
expect(parsed.nature).to eq(:audio)
|
41
|
+
expect(parsed.format).to eq(:aac)
|
42
|
+
expect(parsed.num_audio_channels).to eq(2)
|
43
|
+
expect(parsed.audio_sample_rate_hz).to eq(44100)
|
44
|
+
expect(parsed.content_type).to eq('audio/aac')
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'should parse a long sample, single channel audio, 16 kb/s, 44100 HZ' do
|
48
|
+
file_path = fixtures_dir + '/AAC/ff-16b-1c-44100hz.aac'
|
49
|
+
parsed = subject.call(File.open(file_path, 'rb'))
|
50
|
+
|
51
|
+
expect(parsed).not_to be_nil
|
52
|
+
|
53
|
+
expect(parsed.nature).to eq(:audio)
|
54
|
+
expect(parsed.format).to eq(:aac)
|
55
|
+
expect(parsed.num_audio_channels).to eq(1)
|
56
|
+
expect(parsed.audio_sample_rate_hz).to eq(44100)
|
57
|
+
expect(parsed.content_type).to eq('audio/aac')
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'should parse a long sample, two channel audio, 16 kb/s, 44100 HZ' do
|
61
|
+
file_path = fixtures_dir + '/AAC/ff-16b-2c-44100hz.aac'
|
62
|
+
parsed = subject.call(File.open(file_path, 'rb'))
|
63
|
+
|
64
|
+
expect(parsed).not_to be_nil
|
65
|
+
|
66
|
+
expect(parsed.nature).to eq(:audio)
|
67
|
+
expect(parsed.format).to eq(:aac)
|
68
|
+
expect(parsed.num_audio_channels).to eq(2)
|
69
|
+
expect(parsed.audio_sample_rate_hz).to eq(44100)
|
70
|
+
expect(parsed.content_type).to eq('audio/aac')
|
71
|
+
end
|
72
|
+
|
73
|
+
shared_examples 'invalid filetype' do |filetype, fixture_path|
|
74
|
+
it "should fail to parse #{filetype}" do
|
75
|
+
file_path = fixtures_dir + fixture_path
|
76
|
+
parsed = subject.call(File.open(file_path, 'rb'))
|
77
|
+
expect(parsed).to be_nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
include_examples 'invalid filetype', 'AIFF', '/AIFF/fixture.aiff'
|
82
|
+
include_examples 'invalid filetype', 'FLAC', '/FLAC/atc_fixture_vbr.flac'
|
83
|
+
include_examples 'invalid filetype', 'MP3', '/MP3/Cassy.mp3'
|
84
|
+
include_examples 'invalid filetype', 'MPG', '/MPG/video1.mpg'
|
85
|
+
include_examples 'invalid filetype', 'OGG', '/Ogg/hi.ogg'
|
86
|
+
include_examples 'invalid filetype', 'WAV', '/WAV/c_8kmp316.wav'
|
87
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormatParser::AdtsHeaderInfo do
|
4
|
+
shared_examples 'parsed header' do |header_bits, expected_mpeg_version_description, expected_protection_absence, expected_profile_description, expected_mpeg4_sampling_frequency, expected_mpeg4_channel_config, expected_number_of_audio_channels, expected_originality, expected_home_usage, expected_frame_length, expected_aac_frames_per_adts_frame, expected_has_fixed_bitrate|
|
5
|
+
it "extracts correct values for header #{header_bits}" do
|
6
|
+
result = FormatParser::AdtsHeaderInfo.parse_adts_header(header_bits.split(''))
|
7
|
+
expect(result).not_to be_nil
|
8
|
+
expect(result.mpeg_version_description).to eq(expected_mpeg_version_description)
|
9
|
+
expect(result.protection_absence).to eq(expected_protection_absence)
|
10
|
+
expect(result.profile_description).to eq(expected_profile_description)
|
11
|
+
expect(result.mpeg4_sampling_frequency).to eq(expected_mpeg4_sampling_frequency)
|
12
|
+
expect(result.mpeg4_channel_config).to eq(expected_mpeg4_channel_config)
|
13
|
+
expect(result.number_of_audio_channels).to eq(expected_number_of_audio_channels)
|
14
|
+
expect(result.originality).to eq(expected_originality)
|
15
|
+
expect(result.home_usage).to eq(expected_home_usage)
|
16
|
+
expect(result.frame_length).to eq(expected_frame_length)
|
17
|
+
expect(result.aac_frames_per_adts_frame).to eq(expected_aac_frames_per_adts_frame)
|
18
|
+
expect(result.fixed_bitrate?).to eq(expected_has_fixed_bitrate)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
shared_examples 'invalid header' do |failure_reason, header_bits|
|
23
|
+
it "fails on #{failure_reason} for header #{header_bits}" do
|
24
|
+
result = FormatParser::AdtsHeaderInfo.parse_adts_header(header_bits.split(''))
|
25
|
+
expect(result).to be_nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# These headers have been validated here: https://www.p23.nl/projects/aac-header/
|
30
|
+
include_examples 'parsed header', '1111111111110001010111001000000000101110011111111111110000100001', 'MPEG-4', true, 'AAC_LC (Low Complexity)', 22050, 2, 2, false, false, 371, 1, false
|
31
|
+
include_examples 'parsed header', '111111111111000101010000010000000000011110011111111111001101111000000010', 'MPEG-4', true, 'AAC_LC (Low Complexity)', 44100, 1, 1, false, false, 60, 1, false
|
32
|
+
|
33
|
+
include_examples 'invalid header', 'invalid syncword', '1111110111110001010111001000000000101110011111111111110000100001'
|
34
|
+
include_examples 'invalid header', 'invalid layer value', '1111111111110011010111001000000000101110011111111111110000100001'
|
35
|
+
include_examples 'invalid header', 'invalid sampling frequency index 15', '1111111111110001011111001000000000101110011111111111110000100001'
|
36
|
+
include_examples 'invalid header', 'zero frame length', '1111111111110001010111001000000000000000011111111111110000100001'
|
37
|
+
include_examples 'invalid header', 'random header', '101000101011010101010101111010101010101011001010101010101111000000011101'
|
38
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormatParser::ARWParser do
|
4
|
+
shared_examples 'likely_match for file' do |filename_with_extension|
|
5
|
+
it "matches '#{filename_with_extension}'" do
|
6
|
+
expect(subject.likely_match?(filename_with_extension)).to be_truthy
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
shared_examples 'no likely_match for file' do |filename_with_extension|
|
11
|
+
it "does not match '#{filename_with_extension}'" do
|
12
|
+
expect(subject.likely_match?(filename_with_extension)).to be_falsey
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe 'likely_match' do
|
17
|
+
filenames = ['raw_file', 'another raw file', 'and.another', 'one-more']
|
18
|
+
valid_extensions = ['.arw', '.Arw', '.aRw', '.arW', '.ARw', '.ArW', '.aRW', '.ARW']
|
19
|
+
invalid_extensions = ['.tiff', '.cr2', '.new', '.jpeg']
|
20
|
+
filenames.each do |filename|
|
21
|
+
valid_extensions.each do |extension|
|
22
|
+
include_examples 'likely_match for file', filename + extension
|
23
|
+
end
|
24
|
+
invalid_extensions.each do |extension|
|
25
|
+
include_examples 'no likely_match for file', filename + extension
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'parses Sony ARW fixtures as arw format file' do
|
31
|
+
expected_parsed_dimensions = {
|
32
|
+
'RAW_SONY_A100.ARW' => {
|
33
|
+
width_px: 3872,
|
34
|
+
height_px: 2592,
|
35
|
+
display_width_px: 3872,
|
36
|
+
display_height_px: 2592,
|
37
|
+
orientation: :top_left
|
38
|
+
},
|
39
|
+
'RAW_SONY_A700.ARW' => {
|
40
|
+
width_px: 4288,
|
41
|
+
height_px: 2856,
|
42
|
+
display_width_px: 4288,
|
43
|
+
display_height_px: 2856,
|
44
|
+
orientation: :top_left
|
45
|
+
},
|
46
|
+
'RAW_SONY_A900.ARW' => {
|
47
|
+
width_px: 6080,
|
48
|
+
height_px: 4048,
|
49
|
+
display_width_px: 6080,
|
50
|
+
display_height_px: 4048,
|
51
|
+
orientation: :top_left
|
52
|
+
},
|
53
|
+
# rotated 90 degree image
|
54
|
+
'RAW_SONY_DSC-RX100M2.ARW' => {
|
55
|
+
width_px: 5472,
|
56
|
+
height_px: 3648,
|
57
|
+
display_width_px: 3648,
|
58
|
+
display_height_px: 5472,
|
59
|
+
orientation: :right_top,
|
60
|
+
},
|
61
|
+
'RAW_SONY_ILCE-7RM2.ARW' => {
|
62
|
+
width_px: 7952,
|
63
|
+
height_px: 5304,
|
64
|
+
display_width_px: 7952,
|
65
|
+
display_height_px: 5304,
|
66
|
+
orientation: :top_left,
|
67
|
+
},
|
68
|
+
'RAW_SONY_NEX7.ARW' => {
|
69
|
+
width_px: 6000,
|
70
|
+
height_px: 4000,
|
71
|
+
display_width_px: 6000,
|
72
|
+
display_height_px: 4000,
|
73
|
+
orientation: :top_left,
|
74
|
+
},
|
75
|
+
'RAW_SONY_SLTA55V.ARW' => {
|
76
|
+
width_px: 4928,
|
77
|
+
height_px: 3280,
|
78
|
+
display_width_px: 4928,
|
79
|
+
display_height_px: 3280,
|
80
|
+
orientation: :top_left,
|
81
|
+
},
|
82
|
+
}
|
83
|
+
|
84
|
+
Dir.glob(fixtures_dir + '/ARW/*.ARW').each do |arw_path|
|
85
|
+
it "is able to parse #{File.basename(arw_path)}" do
|
86
|
+
expected_dimension = expected_parsed_dimensions[File.basename(arw_path)]
|
87
|
+
# error if a new .arw test file is added without specifying the expected dimensions
|
88
|
+
expect(expected_dimension).not_to be_nil
|
89
|
+
|
90
|
+
parsed = subject.call(File.open(arw_path, 'rb'))
|
91
|
+
expect(parsed).not_to be_nil
|
92
|
+
expect(parsed.nature).to eq(:image)
|
93
|
+
expect(parsed.format).to eq(:arw)
|
94
|
+
expect(parsed.intrinsics[:exif]).not_to be_nil
|
95
|
+
expect(parsed.content_type).to eq('image/x-sony-arw')
|
96
|
+
|
97
|
+
expect(parsed.width_px).to eq(expected_dimension[:width_px])
|
98
|
+
expect(parsed.height_px).to eq(expected_dimension[:height_px])
|
99
|
+
expect(parsed.display_width_px).to eq(expected_dimension[:display_width_px])
|
100
|
+
expect(parsed.display_height_px).to eq(expected_dimension[:display_height_px])
|
101
|
+
expect(parsed.orientation).to eq(expected_dimension[:orientation])
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
shared_examples 'invalid filetype' do |filetype, fixture_path|
|
106
|
+
it "should fail to parse #{filetype}" do
|
107
|
+
file_path = fixtures_dir + fixture_path
|
108
|
+
parsed = subject.call(File.open(file_path, 'rb'))
|
109
|
+
expect(parsed).to be_nil
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
include_examples 'invalid filetype', 'NEF', '/NEF/RAW_NIKON_1S2.NEF'
|
114
|
+
include_examples 'invalid filetype', 'TIFF', '/TIFF/Shinbutsureijoushuincho.tiff'
|
115
|
+
include_examples 'invalid filetype', 'JPG', '/JPEG/orient_6.jpg'
|
116
|
+
include_examples 'invalid filetype', 'PNG', '/PNG/cat.png'
|
117
|
+
include_examples 'invalid filetype', 'CR2', '/CR2/RAW_CANON_1DM2.CR2'
|
118
|
+
end
|
119
|
+
end
|
@@ -47,21 +47,6 @@ describe FormatParser::TIFFParser do
|
|
47
47
|
expect(parsed.intrinsics[:exif]).not_to be_nil
|
48
48
|
end
|
49
49
|
|
50
|
-
it 'parses Sony ARW fixture as arw format file' do
|
51
|
-
arw_path = fixtures_dir + '/ARW/RAW_SONY_ILCE-7RM2.ARW'
|
52
|
-
|
53
|
-
parsed = subject.call(File.open(arw_path, 'rb'))
|
54
|
-
|
55
|
-
expect(parsed).not_to be_nil
|
56
|
-
expect(parsed.nature).to eq(:image)
|
57
|
-
expect(parsed.format).to eq(:arw)
|
58
|
-
|
59
|
-
expect(parsed.width_px).to eq(7952)
|
60
|
-
expect(parsed.height_px).to eq(5304)
|
61
|
-
expect(parsed.intrinsics[:exif]).not_to be_nil
|
62
|
-
expect(parsed.content_type).to eq('image/x-sony-arw')
|
63
|
-
end
|
64
|
-
|
65
50
|
describe 'correctly extracts dimensions from various TIFF flavors of the same file' do
|
66
51
|
Dir.glob(fixtures_dir + '/TIFF/IMG_9266*.tif').each do |tiff_path|
|
67
52
|
it "is able to parse #{File.basename(tiff_path)}" do
|
@@ -100,4 +85,13 @@ describe FormatParser::TIFFParser do
|
|
100
85
|
end
|
101
86
|
end
|
102
87
|
end
|
88
|
+
|
89
|
+
describe 'bails out on ARW files, such as' do
|
90
|
+
Dir.glob(fixtures_dir + '/ARW/*.ARW').each do |arw_path|
|
91
|
+
it "skips #{File.basename(arw_path)}" do
|
92
|
+
parsed = subject.call(File.open(arw_path, 'rb'))
|
93
|
+
expect(parsed).to be_nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
103
97
|
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: format_parser
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Noah Berman
|
8
8
|
- Julik Tarkhanov
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2022-
|
12
|
+
date: 2022-10-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ks
|
@@ -229,7 +229,10 @@ files:
|
|
229
229
|
- lib/image.rb
|
230
230
|
- lib/io_constraint.rb
|
231
231
|
- lib/io_utils.rb
|
232
|
+
- lib/parsers/aac_parser.rb
|
233
|
+
- lib/parsers/aac_parser/adts_header_info.rb
|
232
234
|
- lib/parsers/aiff_parser.rb
|
235
|
+
- lib/parsers/arw_parser.rb
|
233
236
|
- lib/parsers/bmp_parser.rb
|
234
237
|
- lib/parsers/cr2_parser.rb
|
235
238
|
- lib/parsers/dpx_parser.rb
|
@@ -273,7 +276,10 @@ files:
|
|
273
276
|
- spec/hash_utils_spec.rb
|
274
277
|
- spec/integration/active_storage/rails_app.rb
|
275
278
|
- spec/io_utils_spec.rb
|
279
|
+
- spec/parsers/aac_parser_spec.rb
|
280
|
+
- spec/parsers/adts_header_info_spec.rb
|
276
281
|
- spec/parsers/aiff_parser_spec.rb
|
282
|
+
- spec/parsers/arw_parser_spec.rb
|
277
283
|
- spec/parsers/bmp_parser_spec.rb
|
278
284
|
- spec/parsers/cr2_parser_spec.rb
|
279
285
|
- spec/parsers/dpx_parser_spec.rb
|
@@ -306,7 +312,7 @@ licenses:
|
|
306
312
|
- MIT (Hippocratic)
|
307
313
|
metadata:
|
308
314
|
allowed_push_host: https://rubygems.org
|
309
|
-
post_install_message:
|
315
|
+
post_install_message:
|
310
316
|
rdoc_options: []
|
311
317
|
require_paths:
|
312
318
|
- lib
|
@@ -321,8 +327,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
321
327
|
- !ruby/object:Gem::Version
|
322
328
|
version: '0'
|
323
329
|
requirements: []
|
324
|
-
rubygems_version: 3.
|
325
|
-
signing_key:
|
330
|
+
rubygems_version: 3.3.7
|
331
|
+
signing_key:
|
326
332
|
specification_version: 4
|
327
333
|
summary: A library for efficient parsing of file metadata
|
328
334
|
test_files: []
|