format_parser 1.5.0 → 1.7.0
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 +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: []
|