iptcr 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 801173393ff24fc468de4a0c0842f03f28058b4b
4
+ data.tar.gz: fceac79394ba9e6823b39228652e440a69a78415
5
+ SHA512:
6
+ metadata.gz: 038c87adeebe6b3047abfb4304145b2fca3344ccef31de92ff9ab1f93d68b5ca42a32199280a02bf8689fde9d464de544126a2f6ff384f97515c91b67c704e02
7
+ data.tar.gz: 612b8924a4456eb48d252be3f097dd10b75d5acb7d5b6943133691ea927178878eeb41ce72719c734fc9455f36f119c9b0fcceb2c9eeab9195b4e45b969354e8
checksums.yaml.gz.sig ADDED
Binary file
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Samuel Cochran
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ # IPTCR
2
+
3
+ [![Build Status](https://travis-ci.org/sj26/iptcr?branch=master)](https://travis-ci.org/sj26/iptcr)
4
+
5
+ IPTC Reader in Ruby. Parse IPTC data extracted from an image into rich data types and respecting string encodings.
6
+
7
+ ## Usage
8
+
9
+ Use something like imagemagick to extract the IPTC, then read it with this class:
10
+
11
+ ```ruby
12
+ require "iptcr"
13
+ raw_iptc = `convert images/ian.jpg iptc:-`
14
+ iptc = IPTCR.parse(iptc)
15
+ iptc["ObjectName"] # => "Ian"
16
+ iptc.to_hash # => {"ObjectName" => "Ian", "ColorSequence" => 32, ...}
17
+ ```
18
+
19
+ ## Thanks
20
+
21
+ Inspired by [ExifTool][exiftool].
22
+
23
+ [exiftool]: http://www.sno.phy.queensu.ca/~phil/exiftool/
24
+
25
+ ## License
26
+
27
+ MIT license, see [LICENSE](LICENSE).
data/lib/iptcr/iptc.rb ADDED
@@ -0,0 +1,221 @@
1
+ require "iptcr"
2
+ require "iptcr/records"
3
+
4
+ module IPTCR
5
+ class Malformed < RuntimeError; end
6
+
7
+ # # IPTC
8
+ #
9
+ # ## Notes
10
+ #
11
+ # IPTC metadata is a series of records. Records contain datasets. Each
12
+ # dataset contains a data field, which is a particular value for that
13
+ # dataset. Repeated datasets represent mulitple fields for the same dataset,
14
+ # like for list data.
15
+ #
16
+ # ## References
17
+ #
18
+ # * https://www.iptc.org/std/photometadata/2008/specification/IPTC-PhotoMetadata-2008.pdf
19
+ # * https://www.iptc.org/std/IIM/4.1/specification/IIMV4.1.pdf
20
+ # * https://en.wikipedia.org/wiki/ISO/IEC_2022#ISO.2FIEC_2022_character_sets
21
+ class IPTC
22
+ attr_reader :fields
23
+
24
+ # IPTC IIM specifics the default encoding is ISO646/4873, which is roughly ASCII
25
+ DEFAULT_ENCODING = Encoding::ASCII
26
+
27
+ def initialize(io, length:)
28
+ @fields = []
29
+ read_fields(io, length: length)
30
+ end
31
+
32
+ def encoding
33
+ @encoding || DEFAULT_ENCODING
34
+ end
35
+
36
+ def [](name)
37
+ field_values[name]
38
+ end
39
+
40
+ def to_hash
41
+ field_values
42
+ end
43
+
44
+ private
45
+
46
+ def read_fields(io, length:)
47
+ unless length.nil?
48
+ start = io.tell
49
+ finish = start + length
50
+ end
51
+
52
+ until io.eof? || (!length.nil? && io.tell >= finish)
53
+ read_field(io)
54
+ end
55
+ end
56
+
57
+ MARKER = 0x1c
58
+
59
+ def read_field(io)
60
+ marker = io.getbyte
61
+ unless marker == MARKER
62
+ raise Malformed, "Expected marker byte, got: #{marker}"
63
+ end
64
+
65
+ record_number = io.getbyte
66
+
67
+ dataset_number = io.getbyte
68
+
69
+ # Length can be a variable integer
70
+ length = io.read(2).unpack("S>").first
71
+ if length & 0x8000 == 0x8000
72
+ size = length & 0x7fff
73
+ length = 0
74
+ size.times do
75
+ length = (length << 8) + io.readbyte
76
+ end
77
+ end
78
+
79
+ start = io.tell
80
+ finish = start + length
81
+
82
+ field = Field.new(record_number: record_number, dataset_number: dataset_number, data: io.read(length), encoding: encoding)
83
+ @fields << field
84
+
85
+ # IPTC specifies that records must be ordered. This dataset is in the
86
+ # first possible record, so later records (like the main application
87
+ # record) should all receive this encoding.
88
+ #
89
+ # There is a list of encodings here:
90
+ # https://en.wikipedia.org/wiki/ISO/IEC_2022#ISO.2FIEC_2022_character_sets
91
+ #
92
+ # Ruby doesn't support a bunch of them, so we really only support UTF-8
93
+ # and fall back to ASCII.
94
+ if field.dataset? and field.dataset_name == "CodedCharacterSet"
95
+ case field.value
96
+ when "\x1b%G"
97
+ @encoding = Encoding::UTF_8
98
+ else
99
+ EXIFR.logger.warn { "IPTC: Unknown codec character set: #{field.value.inspect}" }
100
+ end
101
+ end
102
+
103
+ unless io.tell == finish
104
+ io.seek(finish)
105
+ end
106
+ end
107
+
108
+ def field_values
109
+ @field_values ||= @fields.each_with_object({}) do |field, index|
110
+ if field.dataset?
111
+ if field.dataset[:list]
112
+ (index[field.dataset_name] ||= []) << field.value
113
+ else
114
+ index[field.dataset_name] = field.value
115
+ end
116
+ end
117
+ end
118
+ end
119
+
120
+ class Field
121
+ def initialize(record_number:, dataset_number:, data:, encoding:)
122
+ @record_number = record_number
123
+ @dataset_number = dataset_number
124
+ @data = data
125
+ @encoding = encoding
126
+ end
127
+
128
+ attr_reader :record_number
129
+
130
+ def record?
131
+ RECORDS.has_key? record_number
132
+ end
133
+
134
+ def record
135
+ RECORDS[record_number]
136
+ end
137
+
138
+ def record_name
139
+ if record?
140
+ record[:name]
141
+ end
142
+ end
143
+
144
+ attr_reader :dataset_number, :data
145
+
146
+ def dataset?
147
+ if record?
148
+ record[:datasets] && record[:datasets].has_key?(dataset_number)
149
+ end
150
+ end
151
+
152
+ def dataset
153
+ if dataset?
154
+ record[:datasets][dataset_number]
155
+ end
156
+ end
157
+
158
+ def dataset_name
159
+ if dataset?
160
+ dataset[:name]
161
+ end
162
+ end
163
+
164
+ def dataset_type
165
+ if dataset?
166
+ dataset[:type]
167
+ end
168
+ end
169
+
170
+ attr_reader :data
171
+
172
+ attr_reader :encoding
173
+
174
+ def value
175
+ case dataset_type
176
+ when "string"
177
+ # Record number 1 is always the default encoding
178
+ if record_number == 1
179
+ @data.force_encoding(DEFAULT_ENCODING)
180
+ # Records 2-6 and 8 respect tagged encoding
181
+ elsif (2..6).include?(record_number) || record_number == 8
182
+ @data.force_encoding(encoding)
183
+ # Other behaviour is undefined
184
+ else
185
+ @data
186
+ end
187
+ when "digits"
188
+ @data
189
+ when "int8u"
190
+ @data.unpack("C").first
191
+ when "int16u"
192
+ @data.unpack("S").first
193
+ when "int32u"
194
+ @data.unpack("L").first
195
+ else
196
+ @data
197
+ end
198
+ end
199
+
200
+ def inspect
201
+ "#<%s:0x%014x record=%s dataset=%s %p>" % [self.class, object_id, inspect_record, inspect_dataset, value]
202
+ end
203
+
204
+ private def inspect_record
205
+ if record?
206
+ record_name
207
+ else
208
+ "unknown:0x%x" % [record_number]
209
+ end
210
+ end
211
+
212
+ private def inspect_dataset
213
+ if dataset?
214
+ dataset_name
215
+ else
216
+ "unknown:0x%x" % [dataset_number]
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,819 @@
1
+ # Descriptions of records and their datasets.
2
+ # Copied from ExifTool
3
+ IPTCR::RECORDS = {
4
+ 0x01 => {
5
+ name: 'IPTCEnvelope',
6
+ datasets: {
7
+ 0x00 => {
8
+ name: 'EnvelopeRecordVersion',
9
+ type: 'int16u',
10
+ },
11
+ 0x05 => {
12
+ name: 'Destination',
13
+ list: true,
14
+ type: 'string',
15
+ size: (0..1024),
16
+ },
17
+ 0x14 => {
18
+ name: 'FileFormat',
19
+ type: 'int16u',
20
+ },
21
+ 0x16 => {
22
+ name: 'FileVersion',
23
+ type: 'int16u',
24
+ },
25
+ 0x1e => {
26
+ name: 'ServiceIdentifier',
27
+ type: 'string',
28
+ size: (0..10),
29
+ },
30
+ 0x28 => {
31
+ name: 'EnvelopeNumber',
32
+ type: 'digits',
33
+ size: 8,
34
+ },
35
+ 0x32 => {
36
+ name: 'ProductID',
37
+ list: true,
38
+ type: 'string',
39
+ size: (0..32),
40
+ },
41
+ 0x3c => {
42
+ name: 'EnvelopePriority',
43
+ type: 'digits',
44
+ size: 1,
45
+ print_conv: {
46
+ 0x00 => '0 (reserved)',
47
+ 0x01 => '1 (most urgent)',
48
+ 0x02 => 2,
49
+ 0x03 => 3,
50
+ 0x04 => 4,
51
+ 0x05 => '5 (normal urgency)',
52
+ 0x06 => 6,
53
+ 0x07 => 7,
54
+ 0x08 => '8 (least urgent)',
55
+ 0x09 => '9 (user-defined priority)',
56
+ },
57
+ },
58
+ 0x46 => {
59
+ name: 'DateSent',
60
+ type: 'digits',
61
+ size: 8,
62
+ shift: 'Time',
63
+ value_conv: 'Image::ExifTool::Exif::ExifDate($val)',
64
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcDate($val)',
65
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
66
+ },
67
+ 0x50 => {
68
+ name: 'TimeSent',
69
+ type: 'string',
70
+ size: 11,
71
+ shift: 'Time',
72
+ value_conv: 'Image::ExifTool::Exif::ExifTime($val)',
73
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcTime($val)',
74
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
75
+ },
76
+ 0x5a => {
77
+ name: 'CodedCharacterSet',
78
+ notes: %{
79
+ values are entered in the form "ESC X Y[, ...]". The escape sequence for
80
+ UTF-8 character coding is "ESC % G", but this is displayed as "UTF8" for
81
+ convenience. Either string may be used when writing. The value of this tag
82
+ affects the decoding of string values in the Application and NewsPhoto
83
+ records. This tag is marked as "unsafe" to prevent it from being copied by
84
+ default in a group operation because existing tags in the destination image
85
+ may use a different encoding. When creating a new IPTC record from scratch,
86
+ it is suggested that this be set to "UTF8" if special characters are a
87
+ possibility
88
+ },
89
+ protected: 1,
90
+ type: 'string',
91
+ size: (0..32),
92
+ # convert ISO 2022 escape sequences to a more readable format
93
+ print_conv: 'PrintCodedCharset',
94
+ print_conv_inv: 'PrintInvCodedCharset',
95
+ },
96
+ 0x64 => {
97
+ name: 'UniqueObjectName',
98
+ type: 'string',
99
+ size: (14..80),
100
+ },
101
+ 0x78 => {
102
+ name: 'ARMIdentifier',
103
+ type: 'int16u',
104
+ },
105
+ 0x7a => {
106
+ name: 'ARMVersion',
107
+ type: 'int16u',
108
+ },
109
+ },
110
+ },
111
+ 0x02 => {
112
+ name: 'IPTCApplication',
113
+ datasets: {
114
+ 0x00 => {
115
+ name: 'ApplicationRecordVersion',
116
+ type: 'int16u',
117
+ mandatory: 1,
118
+ },
119
+ 0x03 => {
120
+ name: 'ObjectTypeReference',
121
+ type: 'string',
122
+ size: (3..67),
123
+ },
124
+ 0x04 => {
125
+ name: 'ObjectAttributeReference',
126
+ list: true,
127
+ type: 'string',
128
+ size: (4..68),
129
+ },
130
+ 0x05 => {
131
+ name: 'ObjectName',
132
+ type: 'string',
133
+ size: (0..64),
134
+ },
135
+ 0x07 => {
136
+ name: 'EditStatus',
137
+ type: 'string',
138
+ size: (0..64),
139
+ },
140
+ 0x08 => {
141
+ name: 'EditorialUpdate',
142
+ type: 'digits',
143
+ size: 2,
144
+ print_conv: {
145
+ '01' => 'Additional language',
146
+ },
147
+ },
148
+ 0x0a => {
149
+ name: 'Urgency',
150
+ type: 'digits',
151
+ size: 1,
152
+ print_conv: {
153
+ 0x00 => '0 (reserved)',
154
+ 0x01 => '1 (most urgent)',
155
+ 0x02 => 2,
156
+ 0x03 => 3,
157
+ 0x04 => 4,
158
+ 0x05 => '5 (normal urgency)',
159
+ 0x06 => 6,
160
+ 0x07 => 7,
161
+ 0x08 => '8 (least urgent)',
162
+ 0x09 => '9 (user-defined priority)',
163
+ },
164
+ },
165
+ 0x0c => {
166
+ name: 'SubjectReference',
167
+ list: true,
168
+ type: 'string',
169
+ size: (13..236),
170
+ },
171
+ 0x0f => {
172
+ name: 'Category',
173
+ type: 'string',
174
+ size: (0..3),
175
+ },
176
+ 0x14 => {
177
+ name: 'SupplementalCategories',
178
+ list: true,
179
+ type: 'string',
180
+ size: (0..32),
181
+ },
182
+ 0x16 => {
183
+ name: 'FixtureIdentifier',
184
+ type: 'string',
185
+ size: (0..32),
186
+ },
187
+ 0x19 => {
188
+ name: 'Keywords',
189
+ list: true,
190
+ type: 'string',
191
+ size: (0..64),
192
+ },
193
+ 0x1a => {
194
+ name: 'ContentLocationCode',
195
+ list: true,
196
+ type: 'string',
197
+ size: 3,
198
+ },
199
+ 0x1b => {
200
+ name: 'ContentLocationName',
201
+ list: true,
202
+ type: 'string',
203
+ size: (0..64),
204
+ },
205
+ 0x1e => {
206
+ name: 'ReleaseDate',
207
+ type: 'digits',
208
+ size: 8,
209
+ shift: 'Time',
210
+ value_conv: 'Image::ExifTool::Exif::ExifDate($val)',
211
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcDate($val)',
212
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
213
+ },
214
+ 0x23 => {
215
+ name: 'ReleaseTime',
216
+ type: 'string',
217
+ size: 11,
218
+ shift: 'Time',
219
+ value_conv: 'Image::ExifTool::Exif::ExifTime($val)',
220
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcTime($val)',
221
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
222
+ },
223
+ 0x25 => {
224
+ name: 'ExpirationDate',
225
+ type: 'digits',
226
+ size: 8,
227
+ shift: 'Time',
228
+ value_conv: 'Image::ExifTool::Exif::ExifDate($val)',
229
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcDate($val)',
230
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
231
+ },
232
+ 0x26 => {
233
+ name: 'ExpirationTime',
234
+ type: 'string',
235
+ size: 11,
236
+ shift: 'Time',
237
+ value_conv: 'Image::ExifTool::Exif::ExifTime($val)',
238
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcTime($val)',
239
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
240
+ },
241
+ 0x28 => {
242
+ name: 'SpecialInstructions',
243
+ type: 'string',
244
+ size: (0..256),
245
+ },
246
+ 0x2a => {
247
+ name: 'ActionAdvised',
248
+ type: 'digits',
249
+ size: 2,
250
+ print_conv: {
251
+ '' => '',
252
+ '01' => 'Object Kill',
253
+ '02' => 'Object Replace',
254
+ '03' => 'Object Append',
255
+ '04' => 'Object Reference',
256
+ },
257
+ },
258
+ 0x2d => {
259
+ name: 'ReferenceService',
260
+ list: true,
261
+ type: 'string',
262
+ size: (0..10),
263
+ },
264
+ 0x2f => {
265
+ name: 'ReferenceDate',
266
+ list: true,
267
+ type: 'digits',
268
+ size: 8,
269
+ shift: 'Time',
270
+ value_conv: 'Image::ExifTool::Exif::ExifDate($val)',
271
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcDate($val)',
272
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
273
+ },
274
+ 0x32 => {
275
+ name: 'ReferenceNumber',
276
+ list: true,
277
+ type: 'digits',
278
+ size: 8,
279
+ },
280
+ 0x37 => {
281
+ name: 'DateCreated',
282
+ type: 'digits',
283
+ size: 8,
284
+ shift: 'Time',
285
+ value_conv: 'Image::ExifTool::Exif::ExifDate($val)',
286
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcDate($val)',
287
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
288
+ },
289
+ 0x3c => {
290
+ name: 'TimeCreated',
291
+ type: 'string',
292
+ size: 11,
293
+ shift: 'Time',
294
+ value_conv: 'Image::ExifTool::Exif::ExifTime($val)',
295
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcTime($val)',
296
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
297
+ },
298
+ 0x3e => {
299
+ name: 'DigitalCreationDate',
300
+ type: 'digits',
301
+ size: 8,
302
+ shift: 'Time',
303
+ value_conv: 'Image::ExifTool::Exif::ExifDate($val)',
304
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcDate($val)',
305
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
306
+ },
307
+ 0x3f => {
308
+ name: 'DigitalCreationTime',
309
+ type: 'string',
310
+ size: 11,
311
+ shift: 'Time',
312
+ value_conv: 'Image::ExifTool::Exif::ExifTime($val)',
313
+ value_conv_inv: 'Image::ExifTool::IPTC::IptcTime($val)',
314
+ print_conv_inv: 'Image::ExifTool::IPTC::InverseDateOrTime($val)',
315
+ },
316
+ 0x41 => {
317
+ name: 'OriginatingProgram',
318
+ type: 'string',
319
+ size: (0..32),
320
+ },
321
+ 0x46 => {
322
+ name: 'ProgramVersion',
323
+ type: 'string',
324
+ size: (0..10),
325
+ },
326
+ 0x4b => {
327
+ name: 'ObjectCycle',
328
+ type: 'string',
329
+ size: 1,
330
+ print_conv: {
331
+ 'a' => 'Morning',
332
+ 'p' => 'Evening',
333
+ 'b' => 'Both Morning and Evening',
334
+ },
335
+ },
336
+ 0x50 => {
337
+ name: 'By-line',
338
+ list: true,
339
+ type: 'string',
340
+ size: (0..32),
341
+ },
342
+ 0x55 => {
343
+ name: 'By-lineTitle',
344
+ list: true,
345
+ type: 'string',
346
+ size: (0..32),
347
+ },
348
+ 0x5a => {
349
+ name: 'City',
350
+ type: 'string',
351
+ size: (0..32),
352
+ },
353
+ 0x5c => {
354
+ name: 'Sub-location',
355
+ type: 'string',
356
+ size: (0..32),
357
+ },
358
+ 0x5f => {
359
+ name: 'Province-State',
360
+ type: 'string',
361
+ size: (0..32),
362
+ },
363
+ 0x64 => {
364
+ name: 'Country-PrimaryLocationCode',
365
+ type: 'string',
366
+ size: 3,
367
+ },
368
+ 0x65 => {
369
+ name: 'Country-PrimaryLocationName',
370
+ type: 'string',
371
+ size: (0..64),
372
+ },
373
+ 0x67 => {
374
+ name: 'OriginalTransmissionReference',
375
+ type: 'string',
376
+ size: (0..32),
377
+ },
378
+ 0x69 => {
379
+ name: 'Headline',
380
+ type: 'string',
381
+ size: (0..256),
382
+ },
383
+ 0x6e => {
384
+ name: 'Credit',
385
+ type: 'string',
386
+ size: (0..32),
387
+ },
388
+ 0x73 => {
389
+ name: 'Source',
390
+ type: 'string',
391
+ size: (0..32),
392
+ },
393
+ 0x74 => {
394
+ name: 'CopyrightNotice',
395
+ type: 'string',
396
+ size: (0..128),
397
+ },
398
+ 0x76 => {
399
+ name: 'Contact',
400
+ list: true,
401
+ type: 'string',
402
+ size: (0..128),
403
+ },
404
+ 0x78 => {
405
+ name: 'Caption-Abstract',
406
+ type: 'string',
407
+ size: (0..2000),
408
+ },
409
+ 0x79 => {
410
+ name: 'LocalCaption',
411
+ type: 'string',
412
+ size: (0..256), # (guess)
413
+ notes: %{
414
+ I haven't found a reference for the format of tags 121, 184-188 and
415
+ 225-232, so I have just make them writable as strings with
416
+ reasonable length. Beware that if this is wrong, other utilities
417
+ won't be able to read these tags as written by ExifTool
418
+ },
419
+ },
420
+ 0x7a => {
421
+ name: 'Writer-Editor',
422
+ list: true,
423
+ type: 'string',
424
+ size: (0..32),
425
+ },
426
+ 0x7d => {
427
+ name: 'RasterizedCaption',
428
+ type: 'undef',
429
+ size: 7360,
430
+ binary: 1,
431
+ },
432
+ 0x82 => {
433
+ name: 'ImageType',
434
+ type: 'string',
435
+ size: 2,
436
+ },
437
+ 0x83 => {
438
+ name: 'ImageOrientation',
439
+ type: 'string',
440
+ size: 1,
441
+ print_conv: {
442
+ 'P' => 'Portrait',
443
+ 'L' => 'Landscape',
444
+ 'S' => 'Square',
445
+ },
446
+ },
447
+ 0x87 => {
448
+ name: 'LanguageIdentifier',
449
+ type: 'string',
450
+ size: (2..3),
451
+ },
452
+ 0x96 => {
453
+ name: 'AudioType',
454
+ type: 'string',
455
+ size: 2,
456
+ print_conv: {
457
+ '1A' => 'Mono Actuality',
458
+ '2A' => 'Stereo Actuality',
459
+ '1C' => 'Mono Question and Answer Session',
460
+ '2C' => 'Stereo Question and Answer Session',
461
+ '1M' => 'Mono Music',
462
+ '2M' => 'Stereo Music',
463
+ '1Q' => 'Mono Response to a Question',
464
+ '2Q' => 'Stereo Response to a Question',
465
+ '1R' => 'Mono Raw Sound',
466
+ '2R' => 'Stereo Raw Sound',
467
+ '1S' => 'Mono Scener',
468
+ '2S' => 'Stereo Scener',
469
+ '0T' => 'Text Only',
470
+ '1V' => 'Mono Voicer',
471
+ '2V' => 'Stereo Voicer',
472
+ '1W' => 'Mono Wrap',
473
+ '2W' => 'Stereo Wrap',
474
+ },
475
+ },
476
+ 0x97 => {
477
+ name: 'AudioSamplingRate',
478
+ type: 'digits',
479
+ size: 6,
480
+ },
481
+ 0x98 => {
482
+ name: 'AudioSamplingResolution',
483
+ type: 'digits',
484
+ size: 2,
485
+ },
486
+ 0x99 => {
487
+ name: 'AudioDuration',
488
+ type: 'digits',
489
+ size: 6,
490
+ },
491
+ 0x9a => {
492
+ name: 'AudioOutcue',
493
+ type: 'string',
494
+ size: (0..64),
495
+ },
496
+ 0xb8 => {
497
+ name: 'JobID',
498
+ type: 'string',
499
+ size: (0..64), # (guess)
500
+ },
501
+ 0xb9 => {
502
+ name: 'MasterDocumentID',
503
+ type: 'string',
504
+ size: (0..256), # (guess)
505
+ },
506
+ 0xba => {
507
+ name: 'ShortDocumentID',
508
+ type: 'string',
509
+ size: (0..64), # (guess)
510
+ },
511
+ 0xbb => {
512
+ name: 'UniqueDocumentID',
513
+ type: 'string',
514
+ size: (0..128), # (guess)
515
+ },
516
+ 0xbc => {
517
+ name: 'OwnerID',
518
+ type: 'string',
519
+ size: (0..128), # (guess)
520
+ },
521
+ 0xc8 => {
522
+ name: 'ObjectPreviewFileFormat',
523
+ type: 'int16u',
524
+ print_conv: "fileFormat",
525
+ },
526
+ 0xc9 => {
527
+ name: 'ObjectPreviewFileVersion',
528
+ type: 'int16u',
529
+ },
530
+ 0xca => {
531
+ name: 'ObjectPreviewData',
532
+ type: 'undef',
533
+ size: (0..256000),
534
+ binary: 1,
535
+ },
536
+ 0xdd => {
537
+ name: 'Prefs',
538
+ type: 'string',
539
+ size: (0..64),
540
+ notes: 'PhotoMechanic preferences',
541
+ print_conv: %{
542
+ $val =~ s[\s*(\d+):\s*(\d+):\s*(\d+):\s*(\S*)]
543
+ [Tagged:$1, ColorClass:$2, Rating:$3, FrameNum:$4];
544
+ return $val;
545
+ },
546
+ print_conv_inv: %{
547
+ $val =~ s[Tagged:\s*(\d+).*ColorClass:\s*(\d+).*Rating:\s*(\d+).*FrameNum:\s*(\S*)]
548
+ [$1:$2:$3:$4]is;
549
+ return $val;
550
+ },
551
+ },
552
+ 0xe1 => {
553
+ name: 'ClassifyState',
554
+ type: 'string',
555
+ size: (0..64), # (guess)
556
+ },
557
+ 0xe4 => {
558
+ name: 'SimilarityIndex',
559
+ type: 'string',
560
+ size: (0..32), # (guess)
561
+ },
562
+ 0xe6 => {
563
+ name: 'DocumentNotes',
564
+ type: 'string',
565
+ size: (0..1024), # (guess)
566
+ },
567
+ 0xe7 => {
568
+ name: 'DocumentHistory',
569
+ type: 'string',
570
+ size: (0..256), # (guess)
571
+ },
572
+ 0xe8 => {
573
+ name: 'ExifCameraInfo',
574
+ type: 'string',
575
+ size: (0..4096), # (guess)
576
+ },
577
+ 0xff => { #PH
578
+ name: 'CatalogSets',
579
+ list: 1,
580
+ type: 'string',
581
+ size: (0..256), # (guess)
582
+ notes: 'written by iView MediaPro',
583
+ },
584
+ },
585
+ },
586
+ 0x03 => {
587
+ name: 'IPTCNewsPhoto',
588
+ datasets: {
589
+ 0x00 => {
590
+ name: 'NewsPhotoVersion',
591
+ type: 'int16u',
592
+ mandatory: 1,
593
+ },
594
+ 0x0a => {
595
+ name: 'IPTCPictureNumber',
596
+ type: 'string',
597
+ size: 16,
598
+ notes: '4 numbers: 1-Manufacturer ID, 2-Equipment ID, 3-Date, 4-Sequence',
599
+ print_conv: 'Image::ExifTool::IPTC::ConvertPictureNumber($val)',
600
+ print_conv_inv: 'Image::ExifTool::IPTC::InvConvertPictureNumber($val)',
601
+ },
602
+ 0x14 => {
603
+ name: 'IPTCImageWidth',
604
+ type: 'int16u',
605
+ },
606
+ 0x1e => {
607
+ name: 'IPTCImageHeight',
608
+ type: 'int16u',
609
+ },
610
+ 0x28 => {
611
+ name: 'IPTCPixelWidth',
612
+ type: 'int16u',
613
+ },
614
+ 0x32 => {
615
+ name: 'IPTCPixelHeight',
616
+ type: 'int16u',
617
+ },
618
+ 0x37 => {
619
+ name: 'SupplementalType',
620
+ type: 'int8u',
621
+ print_conv: {
622
+ 0x00 => 'Main Image',
623
+ 0x01 => 'Reduced Resolution Image',
624
+ 0x02 => 'Logo',
625
+ 0x03 => 'Rasterized Caption',
626
+ },
627
+ },
628
+ 0x3c => {
629
+ name: 'ColorRepresentation',
630
+ type: 'int16u',
631
+ print_hex: 1,
632
+ print_conv: {
633
+ 0x000 => 'No Image, Single Frame',
634
+ 0x100 => 'Monochrome, Single Frame',
635
+ 0x300 => '3 Components, Single Frame',
636
+ 0x301 => '3 Components, Frame Sequential in Multiple Objects',
637
+ 0x302 => '3 Components, Frame Sequential in One Object',
638
+ 0x303 => '3 Components, Line Sequential',
639
+ 0x304 => '3 Components, Pixel Sequential',
640
+ 0x305 => '3 Components, Special Interleaving',
641
+ 0x400 => '4 Components, Single Frame',
642
+ 0x401 => '4 Components, Frame Sequential in Multiple Objects',
643
+ 0x402 => '4 Components, Frame Sequential in One Object',
644
+ 0x403 => '4 Components, Line Sequential',
645
+ 0x404 => '4 Components, Pixel Sequential',
646
+ 0x405 => '4 Components, Special Interleaving',
647
+ },
648
+ },
649
+ 0x40 => {
650
+ name: 'InterchangeColorSpace',
651
+ type: 'int8u',
652
+ print_conv: {
653
+ 0x01 => 'X,Y,Z CIE',
654
+ 0x02 => 'RGB SMPTE',
655
+ 0x03 => 'Y,U,V (K) (D65)',
656
+ 0x04 => 'RGB Device Dependent',
657
+ 0x05 => 'CMY (K) Device Dependent',
658
+ 0x06 => 'Lab (K) CIE',
659
+ 0x07 => 'YCbCr',
660
+ 0x08 => 'sRGB',
661
+ },
662
+ },
663
+ 0x41 => {
664
+ name: 'ColorSequence',
665
+ type: 'int8u',
666
+ },
667
+ 0x42 => {
668
+ name: 'ICC_Profile',
669
+ binary: 1,
670
+ },
671
+ 0x46 => {
672
+ name: 'ColorCalibrationMatrix',
673
+ binary: 1,
674
+ },
675
+ 0x50 => {
676
+ name: 'LookupTable',
677
+ binary: 1,
678
+ },
679
+ 0x54 => {
680
+ name: 'NumIndexEntries',
681
+ type: 'int16u',
682
+ },
683
+ 0x55 => {
684
+ name: 'ColorPalette',
685
+ binary: 1,
686
+ },
687
+ 0x56 => {
688
+ name: 'IPTCBitsPerSample',
689
+ type: 'int8u',
690
+ },
691
+ 0x5a => {
692
+ name: 'SampleStructure',
693
+ type: 'int8u',
694
+ print_conv: {
695
+ 0x00 => 'OrthogonalConstangSampling',
696
+ 0x01 => 'Orthogonal4-2-2Sampling',
697
+ 0x02 => 'CompressionDependent',
698
+ },
699
+ },
700
+ 0x64 => {
701
+ name: 'ScanningDirection',
702
+ type: 'int8u',
703
+ print_conv: {
704
+ 0x00 => 'L-R, Top-Bottom',
705
+ 0x01 => 'R-L, Top-Bottom',
706
+ 0x02 => 'L-R, Bottom-Top',
707
+ 0x03 => 'R-L, Bottom-Top',
708
+ 0x04 => 'Top-Bottom, L-R',
709
+ 0x05 => 'Bottom-Top, L-R',
710
+ 0x06 => 'Top-Bottom, R-L',
711
+ 0x07 => 'Bottom-Top, R-L',
712
+ },
713
+ },
714
+ 0x66 => {
715
+ name: 'IPTCImageRotation',
716
+ type: 'int8u',
717
+ print_conv: {
718
+ 0x00 => 0,
719
+ 0x01 => 90,
720
+ 0x02 => 180,
721
+ 0x03 => 270,
722
+ },
723
+ },
724
+ 0x6e => {
725
+ name: 'DataCompressionMethod',
726
+ type: 'int32u',
727
+ },
728
+ 0x78 => {
729
+ name: 'QuantizationMethod',
730
+ type: 'int8u',
731
+ print_conv: {
732
+ 0x00 => 'Linear Reflectance/Transmittance',
733
+ 0x01 => 'Linear Density',
734
+ 0x02 => 'IPTC Ref B',
735
+ 0x03 => 'Linear Dot Percent',
736
+ 0x04 => 'AP Domestic Analogue',
737
+ 0x05 => 'Compression Method Specific',
738
+ 0x06 => 'Color Space Specific',
739
+ 0x07 => 'Gamma Compensated',
740
+ },
741
+ },
742
+ 0x7d => {
743
+ name: 'EndPoints',
744
+ binary: 1,
745
+ },
746
+ 0x82 => {
747
+ name: 'ExcursionTolerance',
748
+ type: 'int8u',
749
+ print_conv: {
750
+ 0x00 => 'Not Allowed',
751
+ 0x01 => 'Allowed',
752
+ },
753
+ },
754
+ 0x87 => {
755
+ name: 'BitsPerComponent',
756
+ type: 'int8u',
757
+ },
758
+ 0x8c => {
759
+ name: 'MaximumDensityRange',
760
+ type: 'int16u',
761
+ },
762
+ 0x91 => {
763
+ name: 'GammaCompensatedValue',
764
+ type: 'int16u',
765
+ },
766
+ },
767
+ },
768
+ 0x07 => {
769
+ # Record 7 -- Pre-object Data
770
+ name: 'IPTCPreObjectData',
771
+ datasets: {
772
+ 0x0a => {
773
+ name: 'SizeMode',
774
+ type: 'int8u',
775
+ print_conv: {
776
+ 0x00 => 'Size Not Known',
777
+ 0x01 => 'Size Known',
778
+ },
779
+ },
780
+ 0x14 => {
781
+ name: 'MaxSubfileSize',
782
+ type: 'int32u',
783
+ },
784
+ 0x5a => {
785
+ name: 'ObjectSizeAnnounced',
786
+ type: 'int32u',
787
+ },
788
+ 0x5f => {
789
+ name: 'MaximumObjectSize',
790
+ type: 'int32u',
791
+ },
792
+ },
793
+ },
794
+ 0x08 => {
795
+ # Record 8 -- ObjectData
796
+ name: 'IPTCObjectData',
797
+ datasets: {
798
+ 0x0a => {
799
+ name: 'SubFile',
800
+ list: true,
801
+ binary: 1,
802
+ },
803
+ },
804
+ },
805
+ 0x09 => {
806
+ # Record 9 -- PostObjectData
807
+ name: 'IPTCPostObjectData',
808
+ datasets: {
809
+ 0x0a => {
810
+ name: 'ConfirmedObjectSize',
811
+ type: 'int32u',
812
+ },
813
+ },
814
+ },
815
+ 0xf0 => {
816
+ # Record 240 -- FotoStation proprietary data (ref PH)
817
+ name: 'IPTCFotoStation',
818
+ },
819
+ }
@@ -0,0 +1,3 @@
1
+ module IPTCR
2
+ VERSION = "0.1.0"
3
+ end
data/lib/iptcr.rb ADDED
@@ -0,0 +1,15 @@
1
+ require "iptcr/version"
2
+
3
+ module IPTCR
4
+ def self.parse(value, length: nil)
5
+ if value.is_a? String
6
+ require "stringio"
7
+ length ||= value.bytesize
8
+ IPTC.new(StringIO.new(value), length: length)
9
+ else
10
+ IPTC.new(value, length: length)
11
+ end
12
+ end
13
+
14
+ autoload :IPTC, "iptcr/iptc"
15
+ end
data/test/iptc_test.rb ADDED
@@ -0,0 +1,91 @@
1
+ require "test_helper"
2
+
3
+ class IPTCTest < Minitest::Test
4
+ def test_iptc_string
5
+ iptc = IPTCR.parse("\x1c\x00\x00\x00\x05Hello".b)
6
+ assert iptc.is_a? IPTCR::IPTC
7
+ assert_equal 1, iptc.fields.size
8
+ assert_equal "Hello".b, iptc.fields.first.value
9
+ end
10
+
11
+ def test_iptc_string_respects_length
12
+ iptc = IPTCR.parse("\x1c\x00\x00\x00\x05Hello\x1c\x00\x00\x07\x00Goodbye".b, length: 10)
13
+ assert iptc.is_a? IPTCR::IPTC
14
+ assert_equal 1, iptc.fields.size
15
+ assert_equal "Hello".b, iptc.fields.first.value
16
+ end
17
+
18
+ def test_iptc_io
19
+ iptc = IPTCR.parse(StringIO.new("\x1c\x00\x00\x00\x05Hello"))
20
+ assert iptc.is_a? IPTCR::IPTC
21
+ assert_equal 1, iptc.fields.size
22
+ assert_equal "Hello".b, iptc.fields.first.value
23
+ end
24
+
25
+ def test_iptc_io_respects_length
26
+ iptc = IPTCR.parse(StringIO.new("\x1c\x00\x00\x00\x05Hello\x1c\x00\x00\x07\x00Goodbye".b), length: 10)
27
+ assert iptc.is_a? IPTCR::IPTC
28
+ assert_equal 1, iptc.fields.size
29
+ assert_equal "Hello".b, iptc.fields.first.value
30
+ end
31
+
32
+ def test_bad_marker
33
+ assert_raises(IPTCR::Malformed) do
34
+ IPTCR.parse("\x1d\x00\x00\x00\x05Hello".b)
35
+ end
36
+
37
+ assert_raises(IPTCR::Malformed) do
38
+ IPTCR.parse("\x1c\x00\x00\x00\x05Hello\x1d".b)
39
+ end
40
+ end
41
+
42
+ def test_iptc_unknown_record
43
+ iptc = IPTCR.parse("\x1c\x00\x00\x00\x05Hello".b)
44
+ assert_equal(1, iptc.fields.size)
45
+ refute iptc.fields.first.record?
46
+ refute iptc.fields.first.dataset?
47
+ assert_equal "Hello".b, iptc.fields.first.value
48
+ end
49
+
50
+ def test_iptc_unknown_dataset
51
+ iptc = IPTCR.parse("\x1c\x01\x01\x00\x05Hello".b)
52
+ assert_equal(1, iptc.fields.size)
53
+ assert iptc.fields.first.record?
54
+ refute iptc.fields.first.dataset?
55
+ assert_equal "Hello".b, iptc.fields.first.value
56
+ end
57
+
58
+ def test_iptc_int8u
59
+ iptc = IPTCR.parse("\x1c\x03\x37\x00\x02\x01".b)
60
+ assert_equal({"SupplementalType" => 1}, iptc.to_hash)
61
+ end
62
+
63
+ def test_iptc_int16u
64
+ iptc = IPTCR.parse("\x1c\x01\x00\x00\x02\x01\x02".b)
65
+ assert_equal({"EnvelopeRecordVersion" => 513}, iptc.to_hash)
66
+ end
67
+
68
+ def test_iptc_int32u
69
+ iptc = IPTCR.parse("\x1c\x03\x6e\x00\x04\x01\x02\x04\x08".b)
70
+ assert_equal({"DataCompressionMethod" => 134480385}, iptc.to_hash)
71
+ end
72
+
73
+ def test_iptc_string
74
+ iptc = IPTCR.parse("\x1c\x01\x1e\x00\x05Hello".b)
75
+ assert_equal({"ServiceIdentifier" => "Hello".b}, iptc.to_hash)
76
+ end
77
+
78
+ def test_iptc_encoded_string
79
+ iptc = IPTCR.parse("\x1c\x01\x5a\x00\x03\x1b\x25\x47\x1c\x02\x05\x00\x03\xE2\x98\x83".b)
80
+ assert_equal(2, iptc.fields.size)
81
+ assert_equal("\e%G".b, iptc["CodedCharacterSet"])
82
+ assert_equal(Encoding::UTF_8, iptc["ObjectName"].encoding)
83
+ assert_equal("☃".force_encoding("UTF-8"), iptc["ObjectName"])
84
+ end
85
+
86
+ def test_iptc_string_list
87
+ iptc = IPTCR.parse("\x1c\x01\x05\x00\x05Hello\x1c\x01\x05\x00\x05World".b)
88
+ assert_equal(2, iptc.fields.size)
89
+ assert_equal(["Hello", "World"], iptc["Destination"])
90
+ end
91
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'iptcr'
3
+
4
+ require 'minitest/autorun'
data.tar.gz.sig ADDED
@@ -0,0 +1,2 @@
1
+ W���SA~(��<,�%u���D�E5��N����ݡu�
2
+ w��V�D�E��Rk���8㾢wP�R6��X���s�oww�ZWK��mu��DQ�.D�1"L=S�)�5r�Ә��(�=��#2A���{S�e���K�FGl&7�;�+�9M��7�ь�6�o��G�wV�b��ld\�+w8�Y�*�y�J-/��6�v�R��>�|��������1���9����{�<���*��z� �
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: iptcr
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Samuel Cochran
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDKDCCAhCgAwIBAgIBAzANBgkqhkiG9w0BAQUFADA6MQ0wCwYDVQQDDARzajI2
14
+ MRQwEgYKCZImiZPyLGQBGRYEc2oyNjETMBEGCgmSJomT8ixkARkWA2NvbTAeFw0x
15
+ NTAzMTcyMjUwMjZaFw0xNjAzMTYyMjUwMjZaMDoxDTALBgNVBAMMBHNqMjYxFDAS
16
+ BgoJkiaJk/IsZAEZFgRzajI2MRMwEQYKCZImiZPyLGQBGRYDY29tMIIBIjANBgkq
17
+ hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr60Eo/ttCk8GMTMFiPr3GoYMIMFvLak
18
+ xSmTk9YGCB6UiEePB4THSSA5w6IPyeaCF/nWkDp3/BAam0eZMWG1IzYQB23TqIM0
19
+ 1xzcNRvFsn0aQoQ00k+sj+G83j3T5OOV5OZIlu8xAChMkQmiPd1NXc6uFv+Iacz7
20
+ kj+CMsI9YUFdNoU09QY0b+u+Rb6wDYdpyvN60YC30h0h1MeYbvYZJx/iZK4XY5zu
21
+ 4O/FL2ChjL2CPCpLZW55ShYyrzphWJwLOJe+FJ/ZBl6YXwrzQM9HKnt4titSNvyU
22
+ KzE3L63A3PZvExzLrN9u09kuWLLJfXB2sGOlw3n9t72rJiuBr3/OQQIDAQABozkw
23
+ NzAJBgNVHRMEAjAAMAsGA1UdDwQEAwIEsDAdBgNVHQ4EFgQU99dfRjEKFyczTeIz
24
+ m3ZsDWrNC80wDQYJKoZIhvcNAQEFBQADggEBAFxKLjiLkMLkUmdpsAzJad/t7Jo/
25
+ CGby/3n0WSXPBeZJfsnSdJ2qtG7iy/xqYDc1RjpKgX0RlMgeQRSE3ZDL/HZzBKDF
26
+ azaTgG9Zk1Quu59/79Z0Sltq07Z/IeccFl5j9M+1YS8VY2mOPi9g03OoOSRmhsMS
27
+ wpEF+zvJ0ESS5OPjtp6Sk4q1QYc0aVIthEznuVNMW6CPpTNhMAOFMaTC5AXCzJ3Q
28
+ 52G9HuhbVSTgE/I10H9qZBOE3qdP8ka/Fk0PUrux/DuUanNZgSKJokrQvRA4H9Au
29
+ WpPA7HJYV6msWQiukoBEhfQ2l6Fl2HUwntvX3MCcFNHeJJ5ETERp9alo88E=
30
+ -----END CERTIFICATE-----
31
+ date: 2015-12-18 00:00:00.000000000 Z
32
+ dependencies:
33
+ - !ruby/object:Gem::Dependency
34
+ name: bundler
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '1.10'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.10'
47
+ - !ruby/object:Gem::Dependency
48
+ name: rake
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '10.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '10.0'
61
+ - !ruby/object:Gem::Dependency
62
+ name: minitest
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ description: Parse IPTC data extracted from an image into rich data types and respecting
76
+ string encodings
77
+ email: sj26@sj26.com
78
+ executables: []
79
+ extensions: []
80
+ extra_rdoc_files: []
81
+ files:
82
+ - LICENSE
83
+ - README.md
84
+ - lib/iptcr.rb
85
+ - lib/iptcr/iptc.rb
86
+ - lib/iptcr/records.rb
87
+ - lib/iptcr/version.rb
88
+ - test/iptc_test.rb
89
+ - test/test_helper.rb
90
+ homepage: https://github.com/sj26/iptcr
91
+ licenses:
92
+ - MIT
93
+ metadata: {}
94
+ post_install_message:
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '2.1'
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubyforge_project:
110
+ rubygems_version: 2.4.5.1
111
+ signing_key:
112
+ specification_version: 4
113
+ summary: Parse IPTC data
114
+ test_files:
115
+ - test/iptc_test.rb
116
+ - test/test_helper.rb
117
+ has_rdoc:
metadata.gz.sig ADDED
@@ -0,0 +1 @@
1
+ ���KL������Z�[hя��`�ѵ���t�qx›#�