format_parser 0.13.4 → 0.13.5
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/.travis.yml +1 -2
- data/CHANGELOG.md +3 -0
- data/Rakefile +3 -1
- data/lib/format_parser/version.rb +1 -1
- data/lib/parsers/cr2_parser.rb +17 -130
- data/spec/parsers/cr2_parser_spec.rb +18 -29
- metadata +1 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8837741d44c95c25f1b68ae19991bf6b1c1819b17f8a645d5c53a9d9ba97a8bb
|
4
|
+
data.tar.gz: 58dac9d742cf2b59bbf05ae8b810b8a6b15b144035ca625a52e4a9fbed74f93e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5be5537ea121c3eff8ec4520d049b1ff0e813f22cc4ea800ad01be7ccb823e0580ea3155d9d561b6854e1f211a98ecd23e6a8de4ef4c2a735b81a6b69a7aa780
|
7
|
+
data.tar.gz: 9b056ff9cb825a4d925a03ad3fcb245e0e93545057a1cf7c3ef778f84955b16b12175c9d8f3c0ab2e04542364c8bb1c5f65e8c0ad39ff70e199774217282a087
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,6 @@
|
|
1
|
+
## 0.13.5
|
2
|
+
* Use the same TIFF parsing flow for CR2 files as it seems we are not very reliable _yet._ The CR2 parser will need some work.
|
3
|
+
|
1
4
|
## 0.13.4
|
2
5
|
* Make sure JSON data never contains NaN, fix the test that was supposed to verify that but didn't
|
3
6
|
* Forcibly UTF-8 sanitize all EXIF data when building JSON
|
data/Rakefile
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'bundler/gem_tasks'
|
2
2
|
require 'rspec/core/rake_task'
|
3
3
|
require 'yard'
|
4
|
+
require 'rubocop/rake_task'
|
4
5
|
|
5
6
|
YARD::Rake::YardocTask.new(:doc) do |t|
|
6
7
|
# The dash has to be between the two to "divide" the source files and
|
@@ -8,5 +9,6 @@ YARD::Rake::YardocTask.new(:doc) do |t|
|
|
8
9
|
t.files = ['lib/**/*.rb', '-', 'LICENSE.txt', 'IMPLEMENTATION_DETAILS.md']
|
9
10
|
end
|
10
11
|
|
12
|
+
RuboCop::RakeTask.new
|
11
13
|
RSpec::Core::RakeTask.new(:spec)
|
12
|
-
task default: :spec
|
14
|
+
task default: [:spec, :rubocop]
|
data/lib/parsers/cr2_parser.rb
CHANGED
@@ -1,22 +1,12 @@
|
|
1
|
+
require_relative 'exif_parser'
|
2
|
+
|
1
3
|
class FormatParser::CR2Parser
|
2
4
|
include FormatParser::IOUtils
|
5
|
+
include FormatParser::EXIFParser
|
3
6
|
|
4
7
|
TIFF_HEADER = [0x49, 0x49, 0x2a, 0x00]
|
5
8
|
CR2_HEADER = [0x43, 0x52, 0x02, 0x00]
|
6
9
|
|
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
10
|
def call(io)
|
21
11
|
io = FormatParser::IOConstraint.new(io)
|
22
12
|
|
@@ -28,130 +18,27 @@ class FormatParser::CR2Parser
|
|
28
18
|
|
29
19
|
return if !magic_bytes.eql?(CR2_HEADER) || !tiff_bytes.eql?(TIFF_HEADER)
|
30
20
|
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
|
35
|
-
|
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)
|
21
|
+
# The TIFF scanner in EXIFR is plenty good enough,
|
22
|
+
# so why don't we use it? It does all the right skips
|
23
|
+
# in all the right places.
|
24
|
+
exif_data = exif_from_tiff_io(io)
|
25
|
+
return unless exif_data
|
41
26
|
|
42
|
-
|
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
|
27
|
+
w = exif_data.image_width
|
28
|
+
h = exif_data.image_length
|
56
29
|
|
57
30
|
FormatParser::Image.new(
|
58
31
|
format: :cr2,
|
59
|
-
width_px:
|
60
|
-
height_px:
|
61
|
-
|
62
|
-
|
63
|
-
|
32
|
+
width_px: w,
|
33
|
+
height_px: h,
|
34
|
+
display_width_px: exif_data.rotated? ? h : w,
|
35
|
+
display_height_px: exif_data.rotated? ? w : h,
|
36
|
+
orientation: exif_data.orientation,
|
37
|
+
intrinsics: {exif: exif_data},
|
64
38
|
)
|
65
|
-
|
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
|
39
|
+
rescue EXIFR::MalformedTIFF
|
76
40
|
nil
|
77
41
|
end
|
78
42
|
|
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
43
|
FormatParser.register_parser self, natures: :image, formats: :cr2
|
157
44
|
end
|
@@ -16,40 +16,29 @@ describe FormatParser::CR2Parser do
|
|
16
16
|
expect(parsed.height_px).to be_kind_of(Integer)
|
17
17
|
expect(parsed.height_px).to be > 0
|
18
18
|
|
19
|
-
expect(parsed.
|
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
|
19
|
+
expect(parsed.orientation).not_to be_nil
|
34
20
|
end
|
35
21
|
end
|
36
22
|
end
|
37
23
|
|
38
|
-
|
39
|
-
|
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
|
24
|
+
it 'is able to parse orientation in RAW_CANON_40D_SRAW_V103.CR2' do
|
25
|
+
file = fixtures_dir + '/CR2/RAW_CANON_40D_SRAW_V103.CR2'
|
46
26
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
27
|
+
parsed = subject.call(File.open(file, 'rb'))
|
28
|
+
|
29
|
+
expect(parsed.width_px).to eq(1936)
|
30
|
+
expect(parsed.height_px).to eq(1288)
|
31
|
+
expect(parsed.orientation).to be_kind_of(Symbol)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is able to return the orientation nil for the examples from old Canon models' do
|
35
|
+
file = fixtures_dir + '/CR2/_MG_8591.CR2'
|
36
|
+
|
37
|
+
parsed = subject.call(File.open(file, 'rb'))
|
38
|
+
|
39
|
+
expect(parsed.width_px).to eq(1536)
|
40
|
+
expect(parsed.height_px).to eq(1024)
|
41
|
+
expect(parsed.orientation).to eq(:top_left)
|
53
42
|
end
|
54
43
|
|
55
44
|
describe 'is able to return nil unless the examples are CR2' do
|