From d6af39a8727b7c862844d63d55e230d89d9d44ac Mon Sep 17 00:00:00 2001 From: Jian Weihang Date: Mon, 11 Sep 2017 10:38:46 +0800 Subject: [PATCH] feat: make Exif::Data#new accept either String or IO instance It makes passing image data from memory possible. BREAKING CHANGE: Passing String to Data#new is no longer treated as a file path, instead, it loads as binary data. Therefore, `Data.new(path)` has to be changed to `Data.new(File.open(path))` or `Data.new(IO.read(path))`. --- ext/exif/data.c | 27 +++++-- test/test_exif.rb | 175 ++++++++++++++++++++++++++-------------------- 2 files changed, 119 insertions(+), 83 deletions(-) diff --git a/ext/exif/data.c b/ext/exif/data.c index 98b7094..2bfaa7e 100644 --- a/ext/exif/data.c +++ b/ext/exif/data.c @@ -3,6 +3,7 @@ #include "ruby.h" #include +#include #include #include @@ -13,7 +14,7 @@ VALUE rb_cData; static const char* attrs[] = {"aperture_value", "artist", "battery_level", "bits_per_sample", "brightness_value", "cfa_pattern", "cfa_repeat_pattern_dim", "color_space", "components_configuration", "compressed_bits_per_pixel", "compression", "contrast", "copyright", "custom_rendered", "date_time", "date_time_digitized", "date_time_original", "device_setting_description", "digital_zoom_ratio", "document_name", "exif_ifd_pointer", "exif_version", "exposure_bias_value", "exposure_index", "exposure_mode", "exposure_program", "exposure_time", "file_source", "fill_order", "flash", "flash_energy", "flash_pix_version", "fnumber", "focal_length", "focal_length_in_35mm_film", "focal_plane_resolution_unit", "focal_plane_x_resolution", "focal_plane_y_resolution", "gain_control", "gamma", "gps_altitude", "gps_altitude_ref", "gps_area_information", "gps_date_stamp", "gps_dest_bearing", "gps_dest_bearing_ref", "gps_dest_distance", "gps_dest_distance_ref", "gps_dest_latitude", "gps_dest_latitude_ref", "gps_dest_longitude", "gps_dest_longitude_ref", "gps_differential", "gps_dop", "gps_img_direction", "gps_img_direction_ref", "gps_info_ifd_pointer", "gps_latitude", "gps_latitude_ref", "gps_longitude", "gps_longitude_ref", "gps_map_datum", "gps_measure_mode", "gps_processing_method", "gps_satellites", "gps_speed", "gps_speed_ref", "gps_status", "gps_time_stamp", "gps_track", "gps_track_ref", "gps_version_id", "image_description", "image_length", "image_resources", "image_unique_id", "image_width", "inter_color_profile", "interoperability_ifd_pointer", "interoperability_index", "interoperability_version", "iptc_naa", "iso_speed_ratings", "jpeg_interchange_format", "jpeg_interchange_format_length", "jpeg_proc", "light_source", "make", "maker_note", "max_aperture_value", "metering_mode", "model", "new_cfa_pattern", "new_subfile_type", "oecf", "orientation", "padding", "photometric_interpretation", "pixel_x_dimension", "pixel_y_dimension", "planar_configuration", "primary_chromaticities", "print_image_matching", "reference_black_white", "related_image_file_format", "related_image_length", "related_image_width", "related_sound_file", "resolution_unit", "rows_per_strip", "samples_per_pixel", "saturation", "scene_capture_type", "scene_type", "sensing_method", "sharpness", "shutter_speed_value", "software", "spatial_frequency_response", "spectral_sensitivity", "strip_byte_counts", "strip_offsets", "sub_ifds", "sub_sec_time", "sub_sec_time_digitized", "sub_sec_time_original", "subject_area", "subject_distance", "subject_distance_range", "subject_location", "tiff_ep_standard_id", "time_zone_offset", "transfer_function", "transfer_range", "user_comment", "white_balance", "white_point", "x_resolution", "xml_packet", "xp_author", "xp_comment", "xp_keywords", "xp_subject", "xp_title", "y_resolution", "ycbcr_coefficients", "ycbcr_positioning", "ycbcr_sub_sampling"}; -static VALUE new(VALUE self, VALUE file_path); +static VALUE new(VALUE self, VALUE input); static VALUE dump(VALUE self); static void each_content(ExifContent *ec, void *user_data); static void each_entry(ExifEntry *, void *user_data); @@ -30,16 +31,28 @@ void init_data(){ rb_define_method(rb_cData, "dump", dump, 0); } -VALUE new(VALUE self, VALUE file_path){ - Check_Type(file_path, T_STRING); - +VALUE new(VALUE self, VALUE input){ ExifData* ed; - VALUE rb_data; + VALUE read_data; + + switch (TYPE(input)) { + case T_STRING: + read_data = input; + break; + case T_FILE: + read_data = rb_funcall(input, rb_intern("read"), 0); + break; + default: + rb_raise(rb_eTypeError, "wrong argument type %s (expected String or IO)", rb_obj_classname(input)); + } - ed = exif_data_new_from_file(StringValueCStr(file_path)); + ExifLoader *loader = exif_loader_new(); + exif_loader_write(loader, (unsigned char*)RSTRING_PTR(read_data), RSTRING_LEN(read_data)); + ed = exif_loader_get_data(loader); + exif_loader_unref (loader); if(!ed) rb_raise(rb_eNotReadble, "File not readable or no EXIF data in file."); - rb_data = Data_Wrap_Struct(self, NULL, exif_data_free, ed); + VALUE rb_data = Data_Wrap_Struct(self, NULL, exif_data_free, ed); exif_data_foreach_content(ed, each_content, &rb_data); return rb_data; } diff --git a/test/test_exif.rb b/test/test_exif.rb index 2b4574f..5e48363 100644 --- a/test/test_exif.rb +++ b/test/test_exif.rb @@ -4,96 +4,119 @@ require 'exif' class TestExif < Minitest::Test - def test_byte - assert_equal 0, data.gps_altitude_ref - assert_equal [2, 2, 0, 0], data.gps_version_id - end + module Shared + def test_byte + assert_equal 0, data.gps_altitude_ref + assert_equal [2, 2, 0, 0], data.gps_version_id + end - def test_short - assert_equal 4000, data.image_width - assert_equal 2670, data.image_length - assert_equal [8, 8, 8], data.bits_per_sample - assert_equal 2, data.photometric_interpretation - assert_equal 1, data.orientation - assert_equal 3, data.samples_per_pixel - assert_equal 2, data.resolution_unit - assert_equal 6, data.compression - assert_equal 3, data.exposure_program - assert_equal 250, data.iso_speed_ratings - assert_equal 2, data.metering_mode - assert_equal 9, data.light_source - assert_equal 16, data.flash - assert_equal 1, data.color_space - assert_equal 3, data.focal_plane_resolution_unit - assert_equal 2, data.sensing_method - assert_equal 0, data.custom_rendered - assert_equal 0, data.exposure_mode - assert_equal 1, data.white_balance - assert_equal 70, data.focal_length_in_35mm_film - assert_equal 0, data.scene_capture_type - assert_equal 0, data.gain_control - assert_equal 0, data.contrast - assert_equal 0, data.saturation - assert_equal 0, data.sharpness - assert_equal 0, data.subject_distance_range - end + def test_short + assert_equal 4000, data.image_width + assert_equal 2670, data.image_length + assert_equal [8, 8, 8], data.bits_per_sample + assert_equal 2, data.photometric_interpretation + assert_equal 1, data.orientation + assert_equal 3, data.samples_per_pixel + assert_equal 2, data.resolution_unit + assert_equal 6, data.compression + assert_equal 3, data.exposure_program + assert_equal 250, data.iso_speed_ratings + assert_equal 2, data.metering_mode + assert_equal 9, data.light_source + assert_equal 16, data.flash + assert_equal 1, data.color_space + assert_equal 3, data.focal_plane_resolution_unit + assert_equal 2, data.sensing_method + assert_equal 0, data.custom_rendered + assert_equal 0, data.exposure_mode + assert_equal 1, data.white_balance + assert_equal 70, data.focal_length_in_35mm_film + assert_equal 0, data.scene_capture_type + assert_equal 0, data.gain_control + assert_equal 0, data.contrast + assert_equal 0, data.saturation + assert_equal 0, data.sharpness + assert_equal 0, data.subject_distance_range + end - def test_long - assert_equal 4000, data.pixel_x_dimension - assert_equal 2670, data.pixel_y_dimension - end + def test_long + assert_equal 4000, data.pixel_x_dimension + assert_equal 2670, data.pixel_y_dimension + end - def test_ascii - assert_equal 'NIKON CORPORATION', data.make - assert_equal 'NIKON D600', data.model - assert_equal 'Adobe Photoshop CS6 (Macintosh)', data.software - assert_equal '2013:12:08 21:14:11', data.date_time - assert_equal '2013:09:10 16:31:21', data.date_time_original - assert_equal '2013:09:10 16:31:21', data.date_time_digitized - assert_equal '90', data.sub_sec_time_original - assert_equal '90', data.sub_sec_time_digitized - assert_equal 'N', data.gps_latitude_ref - assert_equal 'E', data.gps_longitude_ref - assert_equal 'WGS-84', data.gps_map_datum - assert_equal '2013:09:10', data.gps_date_stamp - end + def test_ascii + assert_equal 'NIKON CORPORATION', data.make + assert_equal 'NIKON D600', data.model + assert_equal 'Adobe Photoshop CS6 (Macintosh)', data.software + assert_equal '2013:12:08 21:14:11', data.date_time + assert_equal '2013:09:10 16:31:21', data.date_time_original + assert_equal '2013:09:10 16:31:21', data.date_time_digitized + assert_equal '90', data.sub_sec_time_original + assert_equal '90', data.sub_sec_time_digitized + assert_equal 'N', data.gps_latitude_ref + assert_equal 'E', data.gps_longitude_ref + assert_equal 'WGS-84', data.gps_map_datum + assert_equal '2013:09:10', data.gps_date_stamp + end - def test_rational - # assert_equal Rational(72, 1), data.x_resolution - # assert_equal Rational(72, 1), data.y_resolution - assert_equal Rational(1, 125), data.exposure_time - assert_equal Rational(8, 1), data.fnumber - assert_equal Rational(870723, 125000), data.shutter_speed_value - assert_equal Rational(6, 1), data.aperture_value - assert_equal Rational(-1, 3), data.exposure_bias_value - assert_equal Rational(3, 1), data.max_aperture_value - assert_equal Rational(70, 1), data.focal_length - assert_equal Rational(54886891, 32768), data.focal_plane_x_resolution - assert_equal Rational(54886891, 32768), data.focal_plane_y_resolution - assert_equal Rational(1, 1), data.digital_zoom_ratio - assert_equal [Rational(24, 1), Rational(106817, 10000), Rational(0, 1)], data.gps_latitude - assert_equal [Rational(121, 1), Rational(76869, 2500), Rational(0, 1)], data.gps_longitude - assert_equal Rational(332, 1), data.gps_altitude - assert_equal [Rational(8, 1), Rational(4, 1), Rational(25, 1)], data.gps_time_stamp + def test_rational + # assert_equal Rational(72, 1), data.x_resolution + # assert_equal Rational(72, 1), data.y_resolution + assert_equal Rational(1, 125), data.exposure_time + assert_equal Rational(8, 1), data.fnumber + assert_equal Rational(870723, 125000), data.shutter_speed_value + assert_equal Rational(6, 1), data.aperture_value + assert_equal Rational(-1, 3), data.exposure_bias_value + assert_equal Rational(3, 1), data.max_aperture_value + assert_equal Rational(70, 1), data.focal_length + assert_equal Rational(54886891, 32768), data.focal_plane_x_resolution + assert_equal Rational(54886891, 32768), data.focal_plane_y_resolution + assert_equal Rational(1, 1), data.digital_zoom_ratio + assert_equal [Rational(24, 1), Rational(106817, 10000), Rational(0, 1)], data.gps_latitude + assert_equal [Rational(121, 1), Rational(76869, 2500), Rational(0, 1)], data.gps_longitude + assert_equal Rational(332, 1), data.gps_altitude + assert_equal [Rational(8, 1), Rational(4, 1), Rational(25, 1)], data.gps_time_stamp + end + + def test_double + # TODO + end + + def test_float + # TODO + end + + def data + raise NotImplementedError + end end - def test_double - # TODO + class TestLoadFromString < Minitest::Test + include Shared + + def data + @data ||= Exif::Data.new(IO.read(File.expand_path('../sample.jpg', __FILE__), mode: 'rb')) + end end - def test_float - # TODO + class TestLoadFromIO < Minitest::Test + include Shared + + def data + @data ||= Exif::Data.new(File.open(File.expand_path('../sample.jpg', __FILE__))) + end end def test_not_readble + assert_raises(Exif::NotReadble) { Exif::Data.new 'not readable' } assert_raises Exif::NotReadble do - Exif::Data.new(File.expand_path('../not_readable.jpg', __FILE__)) + Exif::Data.new(File.open(File.expand_path('../not_readable.jpg', __FILE__))) end end - private - - def data - @data ||= Exif::Data.new(File.expand_path('../sample.jpg', __FILE__)) + def test_type_error + assert_raises TypeError do + Exif::Data.new(123) + end end end