format_parser 0.3.0 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/README.md +67 -23
- data/format_parser.gemspec +0 -8
- data/lib/format_parser.rb +58 -21
- data/lib/format_parser/version.rb +1 -1
- data/lib/parsers/aiff_parser.rb +1 -5
- data/lib/parsers/cr2_parser.rb +157 -0
- data/lib/parsers/dpx_parser.rb +1 -5
- data/lib/parsers/fdx_parser.rb +2 -5
- data/lib/parsers/gif_parser.rb +1 -5
- data/lib/parsers/jpeg_parser.rb +1 -5
- data/lib/parsers/moov_parser.rb +1 -5
- data/lib/parsers/mp3_parser.rb +1 -5
- data/lib/parsers/png_parser.rb +1 -5
- data/lib/parsers/psd_parser.rb +1 -4
- data/lib/parsers/tiff_parser.rb +10 -6
- data/lib/parsers/wav_parser.rb +1 -5
- data/spec/format_parser_spec.rb +43 -0
- data/spec/{aiff_parser_spec.rb → parsers/aiff_parser_spec.rb} +2 -2
- data/spec/parsers/cr2_parser_spec.rb +63 -0
- data/spec/parsers/exif_parser_spec.rb +0 -11
- data/spec/parsers/tiff_parser_spec.rb +9 -0
- metadata +7 -11
- data/lib/parsers/dsl.rb +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9d0319cf9897c4d253b9b2202ef4d35477cc2d31
|
4
|
+
data.tar.gz: 4ed1a85defea0ae296abe5e553e739e0d555d6e6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cf6c8801dad23ebab67e116d3d3e1da1a718198049d4c039b8abc4a6001b060c32300f69f73a7f35bf708566fbd9856c0d67b1e03300d7f84ad61b57c2a98d08
|
7
|
+
data.tar.gz: d3aae7f141dbd7431f93555b483845579437cc936c7fe4b09a729c782d3ac7b7001e5df7ef27cea7b4fe4eec2b534148b74eec5f69f9dc5dfe563ea38c82c61b
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -12,7 +12,7 @@ and [dimensions,](https://github.com/sstephenson/dimensions) borrowing from them
|
|
12
12
|
|
13
13
|
## Currently supported filetypes:
|
14
14
|
|
15
|
-
`TIFF, PSD, PNG, MP3, JPEG, GIF, DPX, AIFF, WAV, FDX, MOV, MP4`
|
15
|
+
`TIFF, CR2, PSD, PNG, MP3, JPEG, GIF, DPX, AIFF, WAV, FDX, MOV, MP4`
|
16
16
|
|
17
17
|
...with [more](https://github.com/WeTransfer/format_parser/issues?q=is%3Aissue+is%3Aopen+label%3Aformats) on the way!
|
18
18
|
|
@@ -43,31 +43,68 @@ FormatParser.parse(File.open("myimage", "rb"), natures: [:video, :image], format
|
|
43
43
|
|
44
44
|
## Creating your own parsers
|
45
45
|
|
46
|
-
In order to create new parsers,
|
46
|
+
In order to create new parsers, you have to write a method or a Proc that accepts an IO and performs the
|
47
|
+
parsing, and then returns the metadata for the file (if it could recover any) or `nil` if it couldn't. All files pass
|
48
|
+
through all parsers by default, so if you are dealing with a file that is not "your" format - return `nil` from
|
49
|
+
your method or `break` your Proc as early as possible. A blank `return` works fine too.
|
47
50
|
|
48
|
-
|
49
|
-
2) Instances of the new parser class needs to respond `natures` and `formats` accessor methods, both returning an array of symbols. A simple DSL is provided to avoid writing those accessors.
|
50
|
-
3) The class needs to register itself as a parser.
|
51
|
+
The IO will at the minimum support the subset of the IO API defined in `IOConstraint`
|
51
52
|
|
53
|
+
Strictly, a parser should be one of the two things:
|
54
|
+
|
55
|
+
1) An object that can be `call()`-ed itself, with an argument that conforms to `IOConstraint`
|
56
|
+
2) An object that responds to `new` and returns something that can be `call()`-ed with the same convention.
|
57
|
+
|
58
|
+
The second opton is useful for parsers that are stateful and non-reentrant. FormatParser is made to be used in
|
59
|
+
threaded environments, and if you use instance variables you need your parser to be isolated from it's siblings in
|
60
|
+
other threads - therefore you can pass a Class on registration to have your parser instantiated for each `call()`,
|
61
|
+
anew.
|
62
|
+
|
63
|
+
Your parser has to be registered using `FormatParser.register_parser` with the information on the formats
|
64
|
+
and file natures it provides.
|
52
65
|
|
53
66
|
Down below you can find a basic parser implementation:
|
54
67
|
|
55
68
|
```ruby
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
69
|
+
MyParser = ->(io) {
|
70
|
+
# ... do some parsing with `io`
|
71
|
+
magic_bytes = io.read(4)
|
72
|
+
break if magic_bytes != 'XBMP'
|
73
|
+
# ... more parsing code
|
74
|
+
# ...and return the FileInformation::Image object with the metadata.
|
75
|
+
FormatParser::Image.new(
|
76
|
+
width_px: parsed_width,
|
77
|
+
height_px: parsed_height,
|
78
|
+
)
|
79
|
+
}
|
80
|
+
|
81
|
+
# Register the parser with the module, so that it will be applied to any
|
82
|
+
# document given to `FormatParser.parse()`. The supported natures are currently
|
83
|
+
# - :audio
|
84
|
+
# - :document
|
85
|
+
# - :image
|
86
|
+
# - :video
|
87
|
+
FormatParser.register_parser MyParser, natures: :image, formats: :bmp
|
88
|
+
```
|
89
|
+
|
90
|
+
If you are using a class, this is the skeleton to use:
|
91
|
+
|
92
|
+
```ruby
|
93
|
+
class MyParser
|
94
|
+
def call(io)
|
95
|
+
# ... do some parsing with `io`
|
96
|
+
magic_bytes = io.read(4)
|
97
|
+
return unless magic_bytes != 'XBMP'
|
98
|
+
# ... more parsing code
|
99
|
+
# ...and return the FileInformation::Image object with the metadata.
|
100
|
+
FormatParser::Image.new(
|
101
|
+
width_px: parsed_width,
|
102
|
+
height_px: parsed_height,
|
103
|
+
)
|
68
104
|
end
|
69
105
|
|
70
|
-
FormatParser.
|
106
|
+
FormatParser.register_parser self, natures: :image, formats: :bmp
|
107
|
+
end
|
71
108
|
```
|
72
109
|
|
73
110
|
## Design rationale
|
@@ -75,13 +112,15 @@ class BasicParser
|
|
75
112
|
We need to recover metadata from various file types, and we need to do so satisfying the following constraints:
|
76
113
|
|
77
114
|
* The data in those files can be malicious and/or incomplete, so we need to be failsafe
|
78
|
-
* The data will be fetched from a remote location, so we want to
|
79
|
-
|
80
|
-
fact that we rely on AWS, and data transfer is much cheaper than per-request fees.
|
115
|
+
* The data will be fetched from a remote location (S3), so we want to obtain it with as few HTTP requests as possible
|
116
|
+
* ...and with the amount of data fetched being small - the number of HTTP requests being of greater concern
|
81
117
|
* The data can be recognized ambiguously and match more than one format definition (like TIFF sections of camera RAW)
|
118
|
+
* The information necessary is a small subset of the overall metadata available in the file.
|
82
119
|
* The number of supported formats is only ever going to increase, not decrease
|
83
120
|
* The library is likely to be used in multiple consumer applications
|
84
|
-
* The
|
121
|
+
* The library is likely to be used in multithreading environments
|
122
|
+
|
123
|
+
## Deliberate design choices
|
85
124
|
|
86
125
|
Therefore we adapt the following approaches:
|
87
126
|
|
@@ -93,7 +132,9 @@ Therefore we adapt the following approaches:
|
|
93
132
|
* A caching system that allows us to ideally fetch once, and only once, and as little as possible - but still accomodate formats
|
94
133
|
that have the important information at the end of the file or might need information from the middle of the file
|
95
134
|
* Minimal dependencies, and if dependencies are to be used they should be very stable and low-level
|
96
|
-
* Where possible, use small subsets of full-feature format parsers since we only care about a small subset of the data
|
135
|
+
* Where possible, use small subsets of full-feature format parsers since we only care about a small subset of the data.
|
136
|
+
* When a choice arises between using a dependency or writing a small parser, write the small parser since less code
|
137
|
+
is easier to verify and test, and we likely don't care about all the metadata anyway
|
97
138
|
* Avoid using C libraries which are likely to contain buffer overflows/underflows - we stay memory safe
|
98
139
|
|
99
140
|
## Fixture Sources
|
@@ -117,3 +158,6 @@ Unless specified otherwise in this section the fixture files are MIT licensed an
|
|
117
158
|
### MOOV
|
118
159
|
- bmff.mp4 is borrowed from the [bmff](https://github.com/zuku/bmff) project
|
119
160
|
- Test_Circular MOV files were created by one of the project maintainers and are MIT licensed
|
161
|
+
|
162
|
+
### CR2
|
163
|
+
- CR2 examples are downloaded from http://www.rawsamples.ch/ and are Creative Common Licensed.
|
data/format_parser.gemspec
CHANGED
@@ -15,14 +15,6 @@ Gem::Specification.new do |spec|
|
|
15
15
|
minimum amount of data possible."
|
16
16
|
spec.homepage = 'https://github.com/WeTransfer/format_parser'
|
17
17
|
spec.license = 'MIT'
|
18
|
-
# Alert people to a change in the gem's interface, will remove in a subsequent version
|
19
|
-
spec.post_install_message = %q{
|
20
|
-
-----------------------------------------------------------------------------
|
21
|
-
| ALERT: format_parser **v0.3.0** introduces changes to the gem's interface.|
|
22
|
-
| See https://github.com/WeTransfer/format_parser#basic-usage |
|
23
|
-
| for up-to-date usage instructions. Thank you for using format_parser! :) |
|
24
|
-
-----------------------------------------------------------------------------
|
25
|
-
}
|
26
18
|
# to allow pushing to a single host or delete this section to allow pushing to any host.
|
27
19
|
if spec.respond_to?(:metadata)
|
28
20
|
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
data/lib/format_parser.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
module FormatParser
|
2
|
+
require 'set'
|
2
3
|
require_relative 'image'
|
3
4
|
require_relative 'audio'
|
4
5
|
require_relative 'document'
|
@@ -8,21 +9,37 @@ module FormatParser
|
|
8
9
|
require_relative 'remote_io'
|
9
10
|
require_relative 'io_constraint'
|
10
11
|
require_relative 'care'
|
11
|
-
require_relative 'parsers/dsl'
|
12
12
|
|
13
13
|
PARSER_MUX = Mutex.new
|
14
|
+
MAX_BYTES = 512 * 1024
|
15
|
+
MAX_READS = 64 * 1024
|
16
|
+
MAX_SEEKS = 64 * 1024
|
14
17
|
|
15
|
-
def self.
|
18
|
+
def self.register_parser(callable_or_responding_to_new, formats:, natures:)
|
19
|
+
parser_provided_formats = Array(formats)
|
20
|
+
parser_provided_natures = Array(natures)
|
16
21
|
PARSER_MUX.synchronize do
|
17
|
-
@parsers ||=
|
18
|
-
@parsers <<
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
@
|
25
|
-
|
22
|
+
@parsers ||= Set.new
|
23
|
+
@parsers << callable_or_responding_to_new
|
24
|
+
@parsers_per_nature ||= {}
|
25
|
+
parser_provided_natures.each do |provided_nature|
|
26
|
+
@parsers_per_nature[provided_nature] ||= Set.new
|
27
|
+
@parsers_per_nature[provided_nature] << callable_or_responding_to_new
|
28
|
+
end
|
29
|
+
@parsers_per_format ||= {}
|
30
|
+
parser_provided_formats.each do |provided_format|
|
31
|
+
@parsers_per_format[provided_format] ||= Set.new
|
32
|
+
@parsers_per_format[provided_format] << callable_or_responding_to_new
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.deregister_parser(callable_or_responding_to_new)
|
38
|
+
# Used only in tests
|
39
|
+
PARSER_MUX.synchronize do
|
40
|
+
(@parsers || []).delete(callable_or_responding_to_new)
|
41
|
+
(@parsers_per_nature || {}).values.map { |e| e.delete(callable_or_responding_to_new) }
|
42
|
+
(@parsers_per_format || {}).values.map { |e| e.delete(callable_or_responding_to_new) }
|
26
43
|
end
|
27
44
|
end
|
28
45
|
|
@@ -41,7 +58,7 @@ module FormatParser
|
|
41
58
|
end
|
42
59
|
|
43
60
|
# Return all by default
|
44
|
-
def self.parse(io, natures: @
|
61
|
+
def self.parse(io, natures: @parsers_per_nature.keys, formats: @parsers_per_format.keys, results: :first)
|
45
62
|
# If the cache is preconfigured do not apply an extra layer. It is going
|
46
63
|
# to be preconfigured when using parse_http.
|
47
64
|
io = Care::IOWrapper.new(io) unless io.is_a?(Care::IOWrapper)
|
@@ -60,11 +77,13 @@ module FormatParser
|
|
60
77
|
# Always instantiate parsers fresh for each input, since they might
|
61
78
|
# contain instance variables which otherwise would have to be reset
|
62
79
|
# between invocations, and would complicate threading situations
|
63
|
-
|
80
|
+
parsers = parsers_for(natures, formats)
|
81
|
+
|
82
|
+
results = parsers.lazy.map do |parser|
|
64
83
|
# We need to rewind for each parser, anew
|
65
84
|
io.seek(0)
|
66
85
|
# Limit how many operations the parser can perform
|
67
|
-
limited_io = ReadLimiter.new(io, max_bytes:
|
86
|
+
limited_io = ReadLimiter.new(io, max_bytes: MAX_BYTES, max_reads: MAX_READS, max_seeks: MAX_SEEKS)
|
68
87
|
begin
|
69
88
|
parser.call(limited_io)
|
70
89
|
rescue IOUtils::InvalidRead
|
@@ -78,16 +97,34 @@ module FormatParser
|
|
78
97
|
end.reject(&:nil?).take(amount)
|
79
98
|
|
80
99
|
return results.first if amount == 1
|
81
|
-
# Convert the results from a lazy enumerator to an
|
100
|
+
# Convert the results from a lazy enumerator to an Array.
|
82
101
|
results.to_a
|
83
102
|
end
|
84
103
|
|
85
|
-
def self.parsers_for(
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
104
|
+
def self.parsers_for(desired_natures, desired_formats)
|
105
|
+
assemble_parser_set = ->(hash_of_sets, keys_of_interest) {
|
106
|
+
hash_of_sets.values_at(*keys_of_interest).compact.inject(&:+) || Set.new
|
107
|
+
}
|
108
|
+
|
109
|
+
fitting_by_natures = assemble_parser_set[@parsers_per_nature, desired_natures]
|
110
|
+
fitting_by_formats = assemble_parser_set[@parsers_per_format, desired_formats]
|
111
|
+
factories = fitting_by_natures & fitting_by_formats
|
112
|
+
|
113
|
+
if factories.empty?
|
114
|
+
raise ArgumentError, "No parsers provide both natures #{desired_natures.inspect} and formats #{desired_formats.inspect}"
|
115
|
+
end
|
116
|
+
|
117
|
+
factories.map { |callable_or_class| instantiate_parser(callable_or_class) }
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.instantiate_parser(callable_or_responding_to_new)
|
121
|
+
if callable_or_responding_to_new.respond_to?(:call)
|
122
|
+
callable_or_responding_to_new
|
123
|
+
elsif callable_or_responding_to_new.respond_to?(:new)
|
124
|
+
callable_or_responding_to_new.new
|
125
|
+
else
|
126
|
+
raise ArgumentError, 'A parser should be either a class with an instance method #call or a Proc'
|
127
|
+
end
|
91
128
|
end
|
92
129
|
|
93
130
|
Dir.glob(__dir__ + '/parsers/*.rb').sort.each do |parser_file|
|
data/lib/parsers/aiff_parser.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
class FormatParser::AIFFParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
3
|
|
5
4
|
# Known chunk types we can omit when parsing,
|
6
5
|
# grossly lifted from http://www.muratnkonar.com/aiff/
|
@@ -19,9 +18,6 @@ class FormatParser::AIFFParser
|
|
19
18
|
'ANNO',
|
20
19
|
]
|
21
20
|
|
22
|
-
natures :audio
|
23
|
-
formats :aiff
|
24
|
-
|
25
21
|
def call(io)
|
26
22
|
io = FormatParser::IOConstraint.new(io)
|
27
23
|
form_chunk_type, chunk_size = safe_read(io, 8).unpack('a4N')
|
@@ -84,5 +80,5 @@ class FormatParser::AIFFParser
|
|
84
80
|
(sign == '1' ? -1.0 : 1.0) * (fraction.to_f / ((1 << 63) - 1)) * (2**exponent)
|
85
81
|
end
|
86
82
|
|
87
|
-
FormatParser.
|
83
|
+
FormatParser.register_parser self, natures: :audio, formats: :aiff
|
88
84
|
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
class FormatParser::CR2Parser
|
2
|
+
include FormatParser::IOUtils
|
3
|
+
|
4
|
+
TIFF_HEADER = [0x49, 0x49, 0x2a, 0x00]
|
5
|
+
CR2_HEADER = [0x43, 0x52, 0x02, 0x00]
|
6
|
+
|
7
|
+
PREVIEW_ORIENTATION_TAG = 0x0112
|
8
|
+
PREVIEW_RESOLUTION_TAG = 0x011a
|
9
|
+
PREVIEW_IMAGE_OFFSET_TAG = 0x0111
|
10
|
+
PREVIEW_IMAGE_BYTE_COUNT_TAG = 0x0117
|
11
|
+
EXIF_OFFSET_TAG = 0x8769
|
12
|
+
MAKERNOTE_OFFSET_TAG = 0x927c
|
13
|
+
AFINFO_TAG = 0x0012
|
14
|
+
AFINFO2_TAG = 0x0026
|
15
|
+
CAMERA_MODEL_TAG = 0x0110
|
16
|
+
SHOOT_DATE_TAG = 0x0132
|
17
|
+
EXPOSURE_TAG = 0x829a
|
18
|
+
APERTURE_TAG = 0x829d
|
19
|
+
|
20
|
+
def call(io)
|
21
|
+
io = FormatParser::IOConstraint.new(io)
|
22
|
+
|
23
|
+
tiff_header = safe_read(io, 8)
|
24
|
+
|
25
|
+
# Check whether it's a CR2 file
|
26
|
+
tiff_bytes = tiff_header[0..3].bytes
|
27
|
+
magic_bytes = safe_read(io, 4).unpack('C4')
|
28
|
+
|
29
|
+
return if !magic_bytes.eql?(CR2_HEADER) || !tiff_bytes.eql?(TIFF_HEADER)
|
30
|
+
|
31
|
+
# Offset to IFD #0 where the preview image data is located
|
32
|
+
# For more information about CR2 format,
|
33
|
+
# see http://lclevy.free.fr/cr2/
|
34
|
+
# and https://github.com/lclevy/libcraw2/blob/master/docs/cr2_poster.pdf
|
35
|
+
if0_offset = parse_sequence_to_int tiff_header[4..7]
|
36
|
+
|
37
|
+
parse_ifd_0(io, if0_offset)
|
38
|
+
set_orientation(io, if0_offset)
|
39
|
+
|
40
|
+
exif_offset = parse_ifd(io, if0_offset, EXIF_OFFSET_TAG)
|
41
|
+
|
42
|
+
set_photo_info(io, exif_offset[0])
|
43
|
+
|
44
|
+
makernote_offset = parse_ifd(io, exif_offset[0], MAKERNOTE_OFFSET_TAG)
|
45
|
+
|
46
|
+
# Old Canon models have CanonAFInfo tags
|
47
|
+
# Newer models have CanonAFInfo2 tags instead
|
48
|
+
# See https://sno.phy.queensu.ca/~phil/exiftool/TagNames/Canon.html
|
49
|
+
af_info = parse_ifd(io, makernote_offset[0], AFINFO2_TAG)
|
50
|
+
unless af_info.nil?
|
51
|
+
parse_dimensions(io, af_info[0], af_info[1], 8, 10)
|
52
|
+
else
|
53
|
+
af_info = parse_ifd(io, makernote_offset[0], AFINFO_TAG)
|
54
|
+
parse_dimensions(io, af_info[0], af_info[1], 4, 6)
|
55
|
+
end
|
56
|
+
|
57
|
+
FormatParser::Image.new(
|
58
|
+
format: :cr2,
|
59
|
+
width_px: @width,
|
60
|
+
height_px: @height,
|
61
|
+
orientation: @orientation,
|
62
|
+
image_orientation: @image_orientation,
|
63
|
+
intrinsics: intrinsics
|
64
|
+
)
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
|
69
|
+
def parse_ifd(io, offset, searched_tag)
|
70
|
+
io.seek(offset)
|
71
|
+
entries_count = parse_sequence_to_int safe_read(io, 2)
|
72
|
+
entries_count.times do
|
73
|
+
ifd = ifd_entry safe_read(io, 12)
|
74
|
+
return [ifd[:value], ifd[:length], ifd[:type]].map { |b| parse_sequence_to_int b } if ifd[:tag] == [searched_tag].pack('v')
|
75
|
+
end
|
76
|
+
nil
|
77
|
+
end
|
78
|
+
|
79
|
+
def ifd_entry(binary)
|
80
|
+
{ tag: binary[0..1], type: binary[2..3], length: binary[4..7], value: binary[8..11] }
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_sequence_to_int(sequence)
|
84
|
+
sequence.reverse.unpack('H*').join.hex
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_dimensions(io, offset, length, w_offset, h_offset)
|
88
|
+
io.seek(offset)
|
89
|
+
items = safe_read(io, length)
|
90
|
+
@width = parse_sequence_to_int items[w_offset..w_offset + 1]
|
91
|
+
@height = parse_sequence_to_int items[h_offset..h_offset + 1]
|
92
|
+
end
|
93
|
+
|
94
|
+
def parse_ifd_0(io, offset)
|
95
|
+
resolution_offset = parse_ifd(io, offset, PREVIEW_RESOLUTION_TAG)
|
96
|
+
resolution_data = read_data(io, resolution_offset[0], resolution_offset[1] * 8, resolution_offset[2])
|
97
|
+
@resolution = resolution_data[0] / resolution_data[1]
|
98
|
+
|
99
|
+
@preview_offset = parse_ifd(io, offset, PREVIEW_IMAGE_OFFSET_TAG).first
|
100
|
+
@preview_byte_count = parse_ifd(io, offset, PREVIEW_IMAGE_BYTE_COUNT_TAG).first
|
101
|
+
|
102
|
+
model_offset = parse_ifd(io, offset, CAMERA_MODEL_TAG)
|
103
|
+
@model = read_data(io, model_offset[0], model_offset[1], model_offset[2])
|
104
|
+
|
105
|
+
shoot_date_offset = parse_ifd(io, offset, SHOOT_DATE_TAG)
|
106
|
+
@shoot_date = read_data(io, shoot_date_offset[0], shoot_date_offset[1], shoot_date_offset[2])
|
107
|
+
end
|
108
|
+
|
109
|
+
def set_orientation(io, offset)
|
110
|
+
orient = parse_ifd(io, offset, PREVIEW_ORIENTATION_TAG).first
|
111
|
+
# Some old models do not have orientation info in TIFF headers
|
112
|
+
return if orient > 8
|
113
|
+
# EXIF orientation is an one based index
|
114
|
+
# http://sylvana.net/jpegcrop/exif_orientation.html
|
115
|
+
@orientation = FormatParser::EXIFParser::ORIENTATIONS[orient - 1]
|
116
|
+
@image_orientation = orient
|
117
|
+
end
|
118
|
+
|
119
|
+
def set_photo_info(io, offset)
|
120
|
+
# Type for exposure, aperture and resolution is unsigned rational
|
121
|
+
# Unsigned rational = 2x unsigned long (4 bytes)
|
122
|
+
exposure_offset = parse_ifd(io, offset, EXPOSURE_TAG)
|
123
|
+
exposure_data = read_data(io, exposure_offset[0], exposure_offset[1] * 8, exposure_offset[2])
|
124
|
+
@exposure = "#{exposure_data[0]}/#{exposure_data[1]}"
|
125
|
+
|
126
|
+
aperture_offset = parse_ifd(io, offset, APERTURE_TAG)
|
127
|
+
aperture_data = read_data(io, aperture_offset[0], aperture_offset[1] * 8, aperture_offset[2])
|
128
|
+
@aperture = "f#{aperture_data[0] / aperture_data[1].to_f}"
|
129
|
+
end
|
130
|
+
|
131
|
+
def read_data(io, offset, length, type)
|
132
|
+
io.seek(offset)
|
133
|
+
data = io.read(length)
|
134
|
+
case type
|
135
|
+
when 5
|
136
|
+
n = parse_sequence_to_int data[0..3]
|
137
|
+
d = parse_sequence_to_int data[4..7]
|
138
|
+
[n, d]
|
139
|
+
else
|
140
|
+
data
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
def intrinsics
|
145
|
+
{
|
146
|
+
camera_model: @model,
|
147
|
+
shoot_date: @shoot_date,
|
148
|
+
exposure: @exposure,
|
149
|
+
aperture: @aperture,
|
150
|
+
resolution: @resolution,
|
151
|
+
preview_offset: @preview_offset,
|
152
|
+
preview_length: @preview_byte_count
|
153
|
+
}
|
154
|
+
end
|
155
|
+
|
156
|
+
FormatParser.register_parser self, natures: :image, formats: :cr2
|
157
|
+
end
|
data/lib/parsers/dpx_parser.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
class FormatParser::DPXParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
|
-
|
5
|
-
natures :image
|
6
|
-
formats :dpx
|
7
3
|
|
8
4
|
FILE_INFO = [
|
9
5
|
# :x4, # magic bytes SDPX, we read them anyway so not in the pattern
|
@@ -145,5 +141,5 @@ class FormatParser::DPXParser
|
|
145
141
|
)
|
146
142
|
end
|
147
143
|
|
148
|
-
FormatParser.
|
144
|
+
FormatParser.register_parser self, natures: :image, formats: :dpx
|
149
145
|
end
|
data/lib/parsers/fdx_parser.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
class FormatParser::FDXParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
|
-
|
5
|
-
formats :fdx
|
6
|
-
natures :document
|
7
3
|
|
8
4
|
def call(io)
|
9
5
|
return unless xml_check(io)
|
@@ -29,5 +25,6 @@ class FormatParser::FDXParser
|
|
29
25
|
return
|
30
26
|
end
|
31
27
|
end
|
32
|
-
|
28
|
+
|
29
|
+
FormatParser.register_parser self, natures: :document, formats: :fdx
|
33
30
|
end
|
data/lib/parsers/gif_parser.rb
CHANGED
@@ -1,13 +1,9 @@
|
|
1
1
|
class FormatParser::GIFParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
3
|
|
5
4
|
HEADERS = ['GIF87a', 'GIF89a'].map(&:b)
|
6
5
|
NETSCAPE_AND_AUTHENTICATION_CODE = 'NETSCAPE2.0'
|
7
6
|
|
8
|
-
natures :image
|
9
|
-
formats :gif
|
10
|
-
|
11
7
|
def call(io)
|
12
8
|
io = FormatParser::IOConstraint.new(io)
|
13
9
|
header = safe_read(io, 6)
|
@@ -48,5 +44,5 @@ class FormatParser::GIFParser
|
|
48
44
|
)
|
49
45
|
end
|
50
46
|
|
51
|
-
FormatParser.
|
47
|
+
FormatParser.register_parser self, natures: :image, formats: :gif
|
52
48
|
end
|
data/lib/parsers/jpeg_parser.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
class FormatParser::JPEGParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
3
|
|
5
4
|
class InvalidStructure < StandardError
|
6
5
|
end
|
@@ -11,9 +10,6 @@ class FormatParser::JPEGParser
|
|
11
10
|
SOS_MARKER = 0xDA # start of stream
|
12
11
|
APP1_MARKER = 0xE1 # maybe EXIF
|
13
12
|
|
14
|
-
natures :image
|
15
|
-
formats :jpg
|
16
|
-
|
17
13
|
def call(io)
|
18
14
|
@buf = FormatParser::IOConstraint.new(io)
|
19
15
|
@width = nil
|
@@ -110,5 +106,5 @@ class FormatParser::JPEGParser
|
|
110
106
|
safe_skip(@buf, length)
|
111
107
|
end
|
112
108
|
|
113
|
-
FormatParser.
|
109
|
+
FormatParser.register_parser self, natures: :image, formats: :jpg
|
114
110
|
end
|
data/lib/parsers/moov_parser.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
class FormatParser::MOOVParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
3
|
require_relative 'moov_parser/decoder'
|
5
4
|
|
6
5
|
# Maps values of the "ftyp" atom to something
|
@@ -12,9 +11,6 @@ class FormatParser::MOOVParser
|
|
12
11
|
'm4a ' => :m4a,
|
13
12
|
}
|
14
13
|
|
15
|
-
natures :video
|
16
|
-
formats *FTYP_MAP.values
|
17
|
-
|
18
14
|
# It is currently not documented and not particularly well-tested,
|
19
15
|
# so not considered a public API for now
|
20
16
|
private_constant :Decoder
|
@@ -80,5 +76,5 @@ class FormatParser::MOOVParser
|
|
80
76
|
maybe_atom_size >= minimum_ftyp_atom_size && maybe_ftyp_atom_signature == 'ftyp'
|
81
77
|
end
|
82
78
|
|
83
|
-
FormatParser.
|
79
|
+
FormatParser.register_parser self, natures: :video, formats: FTYP_MAP.values
|
84
80
|
end
|
data/lib/parsers/mp3_parser.rb
CHANGED
@@ -23,10 +23,6 @@ class FormatParser::MP3Parser
|
|
23
23
|
# Default frame size for mp3
|
24
24
|
SAMPLES_PER_FRAME = 1152
|
25
25
|
|
26
|
-
include FormatParser::DSL
|
27
|
-
natures :audio
|
28
|
-
formats :mp3
|
29
|
-
|
30
26
|
def call(io)
|
31
27
|
# Read the last 128 bytes which might contain ID3v1
|
32
28
|
id3_v1 = ID3V1.attempt_id3_v1_extraction(io)
|
@@ -235,5 +231,5 @@ class FormatParser::MP3Parser
|
|
235
231
|
raise InvalidDeepFetch, "Could not retrieve #{keys.inspect} from #{from.inspect}"
|
236
232
|
end
|
237
233
|
|
238
|
-
FormatParser.
|
234
|
+
FormatParser.register_parser self, natures: :audio, formats: :mp3
|
239
235
|
end
|
data/lib/parsers/png_parser.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
class FormatParser::PNGParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
|
-
|
5
|
-
natures :image
|
6
|
-
formats :png
|
7
3
|
|
8
4
|
PNG_HEADER_BYTES = [137, 80, 78, 71, 13, 10, 26, 10].pack('C*')
|
9
5
|
COLOR_TYPES = {
|
@@ -74,5 +70,5 @@ class FormatParser::PNGParser
|
|
74
70
|
)
|
75
71
|
end
|
76
72
|
|
77
|
-
FormatParser.
|
73
|
+
FormatParser.register_parser self, natures: :image, formats: :png
|
78
74
|
end
|
data/lib/parsers/psd_parser.rb
CHANGED
@@ -1,10 +1,7 @@
|
|
1
1
|
class FormatParser::PSDParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
3
|
|
5
4
|
PSD_HEADER = [0x38, 0x42, 0x50, 0x53]
|
6
|
-
natures :image
|
7
|
-
formats :psd
|
8
5
|
|
9
6
|
def call(io)
|
10
7
|
io = FormatParser::IOConstraint.new(io)
|
@@ -22,5 +19,5 @@ class FormatParser::PSDParser
|
|
22
19
|
)
|
23
20
|
end
|
24
21
|
|
25
|
-
FormatParser.
|
22
|
+
FormatParser.register_parser self, natures: :image, formats: :psd
|
26
23
|
end
|
data/lib/parsers/tiff_parser.rb
CHANGED
@@ -1,20 +1,17 @@
|
|
1
1
|
class FormatParser::TIFFParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
3
|
|
5
4
|
LITTLE_ENDIAN_TIFF_HEADER_BYTES = [0x49, 0x49, 0x2A, 0x0]
|
6
5
|
BIG_ENDIAN_TIFF_HEADER_BYTES = [0x4D, 0x4D, 0x0, 0x2A]
|
7
6
|
WIDTH_TAG = 0x100
|
8
7
|
HEIGHT_TAG = 0x101
|
9
8
|
|
10
|
-
natures :image
|
11
|
-
formats :tif
|
12
|
-
|
13
9
|
def call(io)
|
14
10
|
io = FormatParser::IOConstraint.new(io)
|
15
11
|
magic_bytes = safe_read(io, 4).unpack('C4')
|
16
12
|
endianness = scan_tiff_endianness(magic_bytes)
|
17
|
-
return
|
13
|
+
return if !endianness || cr2_check(io)
|
14
|
+
|
18
15
|
w, h = read_tiff_by_endianness(io, endianness)
|
19
16
|
scanner = FormatParser::EXIFParser.new(:tiff, io)
|
20
17
|
scanner.scan_image_exif
|
@@ -57,11 +54,18 @@ class FormatParser::TIFFParser
|
|
57
54
|
end
|
58
55
|
|
59
56
|
def read_tiff_by_endianness(io, endianness)
|
57
|
+
io.seek(4)
|
60
58
|
offset = safe_read(io, 4).unpack(endianness.upcase)[0]
|
61
59
|
io.seek(offset)
|
62
60
|
scan_ifd(io, offset, endianness)
|
63
61
|
[@width, @height]
|
64
62
|
end
|
65
63
|
|
66
|
-
|
64
|
+
def cr2_check(io)
|
65
|
+
io.seek(8)
|
66
|
+
cr2_check_bytes = safe_read(io, 2)
|
67
|
+
cr2_check_bytes == 'CR'
|
68
|
+
end
|
69
|
+
|
70
|
+
FormatParser.register_parser self, natures: :image, formats: :tif
|
67
71
|
end
|
data/lib/parsers/wav_parser.rb
CHANGED
@@ -1,9 +1,5 @@
|
|
1
1
|
class FormatParser::WAVParser
|
2
2
|
include FormatParser::IOUtils
|
3
|
-
include FormatParser::DSL
|
4
|
-
|
5
|
-
natures :audio
|
6
|
-
formats :wav
|
7
3
|
|
8
4
|
def call(io)
|
9
5
|
# Read the RIFF header. Chunk descriptor should be RIFF, the size should
|
@@ -99,5 +95,5 @@ class FormatParser::WAVParser
|
|
99
95
|
)
|
100
96
|
end
|
101
97
|
|
102
|
-
FormatParser.
|
98
|
+
FormatParser.register_parser self, natures: :audio, formats: :wav
|
103
99
|
end
|
data/spec/format_parser_spec.rb
CHANGED
@@ -58,4 +58,47 @@ describe FormatParser do
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
end
|
61
|
+
|
62
|
+
describe 'parsers_for' do
|
63
|
+
it 'raises on an invalid request' do
|
64
|
+
expect {
|
65
|
+
FormatParser.parsers_for([:image], [:fdx])
|
66
|
+
}.to raise_error(/No parsers provide/)
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'returns an intersection of all parsers supplying natures and formats requested' do
|
70
|
+
image_parsers = FormatParser.parsers_for([:image], [:tif, :jpg])
|
71
|
+
expect(image_parsers.length).to eq(2)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'omits parsers not matching formats' do
|
75
|
+
image_parsers = FormatParser.parsers_for([:image, :audio], [:tif, :jpg])
|
76
|
+
expect(image_parsers.length).to eq(2)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'omits parsers not matching nature' do
|
80
|
+
image_parsers = FormatParser.parsers_for([:image], [:tif, :jpg, :aiff, :mp3])
|
81
|
+
expect(image_parsers.length).to eq(2)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe 'parser registration and deregistration with the module' do
|
86
|
+
it 'registers a parser for a certain nature and format' do
|
87
|
+
some_parser = ->(_io) { 'I parse EXRs! Whee!' }
|
88
|
+
|
89
|
+
expect {
|
90
|
+
FormatParser.parsers_for([:image], [:exr])
|
91
|
+
}.to raise_error(/No parsers provide/)
|
92
|
+
|
93
|
+
FormatParser.register_parser some_parser, natures: :image, formats: :exr
|
94
|
+
|
95
|
+
image_parsers = FormatParser.parsers_for([:image], [:exr])
|
96
|
+
expect(image_parsers).not_to be_empty
|
97
|
+
|
98
|
+
FormatParser.deregister_parser some_parser
|
99
|
+
expect {
|
100
|
+
FormatParser.parsers_for([:image], [:exr])
|
101
|
+
}.to raise_error(/No parsers provide/)
|
102
|
+
end
|
103
|
+
end
|
61
104
|
end
|
@@ -2,7 +2,7 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe FormatParser::AIFFParser do
|
4
4
|
it 'parses an AIFF sample file' do
|
5
|
-
parse_result = subject.call(File.open(__dir__ + '
|
5
|
+
parse_result = subject.call(File.open(__dir__ + '/../fixtures/AIFF/fixture.aiff', 'rb'))
|
6
6
|
|
7
7
|
expect(parse_result.nature).to eq(:audio)
|
8
8
|
expect(parse_result.format).to eq(:aiff)
|
@@ -13,7 +13,7 @@ describe FormatParser::AIFFParser do
|
|
13
13
|
end
|
14
14
|
|
15
15
|
it 'parses a Logic Pro created AIFF sample file having a COMT chunk before a COMM chunk' do
|
16
|
-
parse_result = subject.call(File.open(__dir__ + '
|
16
|
+
parse_result = subject.call(File.open(__dir__ + '/../fixtures/AIFF/fixture-logic-aiff.aif', 'rb'))
|
17
17
|
|
18
18
|
expect(parse_result.nature).to eq(:audio)
|
19
19
|
expect(parse_result.format).to eq(:aiff)
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe FormatParser::CR2Parser do
|
4
|
+
describe 'is able to parse CR2 files' do
|
5
|
+
Dir.glob(fixtures_dir + '/CR2/*.CR2').each do |cr2_path|
|
6
|
+
it "is able to parse #{File.basename(cr2_path)}" do
|
7
|
+
parsed = subject.call(File.open(cr2_path, 'rb'))
|
8
|
+
|
9
|
+
expect(parsed).not_to be_nil
|
10
|
+
expect(parsed.nature).to eq(:image)
|
11
|
+
expect(parsed.format).to eq(:cr2)
|
12
|
+
|
13
|
+
expect(parsed.width_px).to be_kind_of(Integer)
|
14
|
+
expect(parsed.width_px).to be > 0
|
15
|
+
|
16
|
+
expect(parsed.height_px).to be_kind_of(Integer)
|
17
|
+
expect(parsed.height_px).to be > 0
|
18
|
+
|
19
|
+
expect(parsed.intrinsics).not_to be_nil
|
20
|
+
expect(parsed.intrinsics[:camera_model]).to be_kind_of(String)
|
21
|
+
expect(parsed.intrinsics[:camera_model]).to match(/Canon \w+/)
|
22
|
+
expect(parsed.intrinsics[:shoot_date]).to be_kind_of(String)
|
23
|
+
expect(parsed.intrinsics[:shoot_date]).to match(/\d{4}:\d{2}:\d{2} \d{2}:\d{2}:\d{2}/)
|
24
|
+
expect(parsed.intrinsics[:exposure]).to be_kind_of(String)
|
25
|
+
expect(parsed.intrinsics[:exposure]).to match(/1\/[0-9]+/)
|
26
|
+
expect(parsed.intrinsics[:aperture]).to be_kind_of(String)
|
27
|
+
expect(parsed.intrinsics[:aperture]).to match(/f[0-9]+\.[0-9]/)
|
28
|
+
expect(parsed.intrinsics[:resolution]).to be_kind_of(Integer)
|
29
|
+
expect(parsed.intrinsics[:resolution]).to be > 0
|
30
|
+
expect(parsed.intrinsics[:preview_offset]).to be_kind_of(Integer)
|
31
|
+
expect(parsed.intrinsics[:preview_offset]).to be > 0
|
32
|
+
expect(parsed.intrinsics[:preview_length]).to be_kind_of(Integer)
|
33
|
+
expect(parsed.intrinsics[:preview_length]).to be > 0
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe 'is able to parse orientation info in the examples' do
|
39
|
+
it 'is able to parse orientation in RAW_CANON_40D_SRAW_V103.CR2' do
|
40
|
+
file = fixtures_dir + '/CR2/RAW_CANON_40D_SRAW_V103.CR2'
|
41
|
+
parsed = subject.call(File.open(file, 'rb'))
|
42
|
+
expect(parsed.orientation).to be_kind_of(Symbol)
|
43
|
+
expect(parsed.image_orientation).to be_kind_of(Integer)
|
44
|
+
expect(parsed.image_orientation).to be > 0
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'is able to return the orientation nil for the examples from old Canon models' do
|
48
|
+
file = fixtures_dir + '/CR2/_MG_8591.CR2'
|
49
|
+
parsed = subject.call(File.open(file, 'rb'))
|
50
|
+
expect(parsed.orientation).to be_nil
|
51
|
+
expect(parsed.image_orientation).to be_nil
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe 'is able to return nil unless the examples are CR2' do
|
56
|
+
Dir.glob(fixtures_dir + '/TIFF/*.tif').each do |tiff_path|
|
57
|
+
it "should return nil for #{File.basename(tiff_path)}" do
|
58
|
+
parsed = subject.call(File.open(tiff_path, 'rb'))
|
59
|
+
expect(parsed).to be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -1,17 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe FormatParser::EXIFParser do
|
4
|
-
# ORIENTATIONS = [
|
5
|
-
# :top_left,
|
6
|
-
# :top_right,
|
7
|
-
# :bottom_right,
|
8
|
-
# :bottom_left,
|
9
|
-
# :left_top,
|
10
|
-
# :right_top,
|
11
|
-
# :right_bottom,
|
12
|
-
# :left_bottom
|
13
|
-
# ]
|
14
|
-
|
15
4
|
describe 'is able to correctly parse orientation for all the JPEG EXIF examples from FastImage' do
|
16
5
|
Dir.glob(fixtures_dir + '/exif-orientation-testimages/jpg/*.jpg').each do |jpeg_path|
|
17
6
|
filename = File.basename(jpeg_path)
|
@@ -33,4 +33,13 @@ describe FormatParser::TIFFParser do
|
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
+
|
37
|
+
describe 'is able to return nil when parsing CR2 examples' do
|
38
|
+
Dir.glob(fixtures_dir + '/CR2/*.CR2').each do |cr2_path|
|
39
|
+
it "is able to return nil when parsing #{File.basename(cr2_path)}" do
|
40
|
+
parsed = subject.call(File.open(cr2_path, 'rb'))
|
41
|
+
expect(parsed).to be_nil
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
36
45
|
end
|
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.3.
|
4
|
+
version: 0.3.1
|
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: 2018-
|
12
|
+
date: 2018-02-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: ks
|
@@ -168,8 +168,8 @@ files:
|
|
168
168
|
- lib/io_constraint.rb
|
169
169
|
- lib/io_utils.rb
|
170
170
|
- lib/parsers/aiff_parser.rb
|
171
|
+
- lib/parsers/cr2_parser.rb
|
171
172
|
- lib/parsers/dpx_parser.rb
|
172
|
-
- lib/parsers/dsl.rb
|
173
173
|
- lib/parsers/exif_parser.rb
|
174
174
|
- lib/parsers/fdx_parser.rb
|
175
175
|
- lib/parsers/gif_parser.rb
|
@@ -186,11 +186,12 @@ files:
|
|
186
186
|
- lib/read_limiter.rb
|
187
187
|
- lib/remote_io.rb
|
188
188
|
- lib/video.rb
|
189
|
-
- spec/aiff_parser_spec.rb
|
190
189
|
- spec/care_spec.rb
|
191
190
|
- spec/file_information_spec.rb
|
192
191
|
- spec/format_parser_spec.rb
|
193
192
|
- spec/io_utils_spec.rb
|
193
|
+
- spec/parsers/aiff_parser_spec.rb
|
194
|
+
- spec/parsers/cr2_parser_spec.rb
|
194
195
|
- spec/parsers/dpx_parser_spec.rb
|
195
196
|
- spec/parsers/exif_parser_spec.rb
|
196
197
|
- spec/parsers/fdx_parser_spec.rb
|
@@ -211,12 +212,7 @@ licenses:
|
|
211
212
|
- MIT
|
212
213
|
metadata:
|
213
214
|
allowed_push_host: https://rubygems.org
|
214
|
-
post_install_message:
|
215
|
-
\ | ALERT: format_parser **v0.3.0** introduces changes to the gem's interface.|\n
|
216
|
-
\ | See https://github.com/WeTransfer/format_parser#basic-usage |\n
|
217
|
-
\ | for up-to-date usage instructions. Thank you for using format_parser! :) |\n
|
218
|
-
\ -----------------------------------------------------------------------------\n
|
219
|
-
\ "
|
215
|
+
post_install_message:
|
220
216
|
rdoc_options: []
|
221
217
|
require_paths:
|
222
218
|
- lib
|
@@ -232,7 +228,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
232
228
|
version: '0'
|
233
229
|
requirements: []
|
234
230
|
rubyforge_project:
|
235
|
-
rubygems_version: 2.
|
231
|
+
rubygems_version: 2.6.13
|
236
232
|
signing_key:
|
237
233
|
specification_version: 4
|
238
234
|
summary: A library for efficient parsing of file metadata
|
data/lib/parsers/dsl.rb
DELETED
@@ -1,29 +0,0 @@
|
|
1
|
-
module FormatParser
|
2
|
-
# Small DSL to avoid repetitive code while defining a new parsers. Also, it can be leveraged by
|
3
|
-
# third parties to define their own parsers.
|
4
|
-
module DSL
|
5
|
-
def self.included(base)
|
6
|
-
base.extend(ClassMethods)
|
7
|
-
end
|
8
|
-
|
9
|
-
module ClassMethods
|
10
|
-
def formats(*registred_formats)
|
11
|
-
__define(:formats, registred_formats)
|
12
|
-
end
|
13
|
-
|
14
|
-
def natures(*registred_natures)
|
15
|
-
__define(:natures, registred_natures)
|
16
|
-
end
|
17
|
-
|
18
|
-
private
|
19
|
-
|
20
|
-
def __define(name, value)
|
21
|
-
throw ArgumentError('empty array') if value.empty?
|
22
|
-
throw ArgumentError('requires array of symbols') if value.any? { |s| !s.is_a?(Symbol) }
|
23
|
-
define_method(name) do
|
24
|
-
value
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
28
|
-
end
|
29
|
-
end
|