kithe 2.10.0 → 2.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2442db92e7c22e24d345483d17020d6025c42e4b24b9fe2dbdf0d9ad8625309e
4
- data.tar.gz: 8d15649504ce721fd2c8b5ee1dc8a7f64b9486e64e3f4f3b5a983ce8d295e5ed
3
+ metadata.gz: c501b48a3f6f517f1fd6906a357aba0004717c94f8b40bd20996efd696bcaff2
4
+ data.tar.gz: b29ffa367832a02a78a94641c6bf37c5bac03cda23c7db80325806bcc151d3f5
5
5
  SHA512:
6
- metadata.gz: 788253f8998adb2a6cd1799b4b124d7e301737477a002c6541f7de5b33f8c6744b818d8aee40de67e3eea316ca6be3411f28a6a2c1951a2ca921416e810cfc45
7
- data.tar.gz: dac26e54710bef716c27833bf822763fc54f784a009835ee43d71fe7e39e63f315cce90dbd6b9a34af54e7ec5ab604b9bf9d749a5450bb94fdc4ebbcdcc4cbf6
6
+ metadata.gz: a1e75a34696381c7564e2f2484a991dff65a97e2d7dde3d4132ecb48eb5ba5c3967d665241dd1b4c9de82bc8a205f42238fbe2c410ad5019c878851e4e35c938
7
+ data.tar.gz: c68de2016880101aef6ed61dcf0752e62a68dc79ac9633aa9b74ad478e5daa2af1f49a94a4445d77d0e1c13a303b722eb6ec6a74affbacd59d301b7d77811dd0
@@ -0,0 +1,111 @@
1
+ module Kithe
2
+ class ExiftoolCharacterization
3
+ # Retrieve known info out of exiftool results.
4
+ #
5
+ # It can be really tricky to get this reliably from arbitrary files/cameras, there's a lot of variety
6
+ # in EXIF/XMP/etc use.
7
+ #
8
+ # We also normalize exiftool validation warnings in #exiftool_validation_warnings, they're
9
+ # kind of a pain to extract
10
+ #
11
+ # We do this right now for our use cases, in terms of what data we want, and what is actually
12
+ # found in ours. PR's welcome to generalize!
13
+ #
14
+ # In the future, we might have different result classes for different versions of exiftool or ways
15
+ # of calling it, it's best to instantiate this with:
16
+ #
17
+ # result = Kithe::ExiftoolChacterization.presenter(some_result_hash)
18
+ # result.camera_model
19
+ # result.exiftool_validation_warnings
20
+ class Result
21
+ attr_reader :result
22
+
23
+ def initialize(hash)
24
+ @result = hash || {}
25
+ end
26
+
27
+ def exiftool_version
28
+ result["ExifTool:ExifToolVersion"]
29
+ end
30
+
31
+ def exif_tool_args
32
+ result["Kithe:CliArgs"]
33
+ end
34
+
35
+ def bits_per_sample
36
+ result["EXIF:BitsPerSample"]
37
+ end
38
+
39
+ def photometric_interpretation
40
+ result["EXIF:PhotometricInterpretation"]
41
+ end
42
+
43
+ def compression
44
+ result["EXIF:Compression"]
45
+ end
46
+
47
+ def camera_make
48
+ result["EXIF:Make"]
49
+ end
50
+
51
+ def camera_model
52
+ result["EXIF:Model"]
53
+ end
54
+
55
+ def dpi
56
+ # only "dpi" if unit is inches
57
+ return nil unless result["EXIF:ResolutionUnit"] == "inches"
58
+
59
+ if result["EXIF:XResolution"] == result["EXIF:YResolution"]
60
+ result["EXIF:XResolution"]
61
+ else
62
+ # for now, we bail on complicated case
63
+ nil
64
+ end
65
+ end
66
+
67
+ def software
68
+ result["XMP:CreatorTool"]
69
+ end
70
+
71
+ def camera_lens
72
+ result["XMP:Lens"]
73
+ end
74
+
75
+ def shutter_speed
76
+ result["Composite:ShutterSpeed"]
77
+ end
78
+
79
+ def camera_iso
80
+ result["EXIF:ISO"]
81
+ end
82
+
83
+ def icc_profile_name
84
+ result["ICC_Profile:ProfileDescription"]
85
+ end
86
+
87
+ # We look in a few places, and we only return date not time because
88
+ # getting timezone info is unusual, and it's all we need right now.
89
+ #
90
+ # @return Date
91
+ def creation_date
92
+ str_date = result["EXIF:DateTimeOriginal"] || result["EXIF:DateTimeOriginal"] || result["XMP:DateCreated"]
93
+ return nil unless str_date
94
+
95
+ Date.strptime(str_date, '%Y:%m:%d')
96
+ rescue Date::Error
97
+ return nil
98
+ end
99
+
100
+ # Multiple exiftool validation warnings are annoyingly in keys `ExifTool:Warning`,
101
+ # `ExifTool:Copy1:Warning`, `ExifTool:Copy2:Warning`, etc. We provide a convenience
102
+ # method to fetch em all and return them as an array.
103
+ #
104
+ # @return Array[String]
105
+ def exiftool_validation_warnings
106
+ @exiftool_validation_warnings ||= result.slice( *result.keys.grep(/ExifTool(:Copy\d+):Warning/) ).values
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,97 @@
1
+ require 'tty/command'
2
+ require 'json'
3
+
4
+ module Kithe
5
+ # Can run an installed `exiftool` command line, and return results as a JSON hash. exiftool
6
+ # needs to be installed. This version developed against exiftool 12.60
7
+ #
8
+ # Results are extended with exact command-line arguments given to exiftool in key `Kithe:CliArgs` as
9
+ # an array of Strings.
10
+ #
11
+ # We ask exiftool to check for validation errors, and output them too, although they are
12
+ # output in exiftool json with kind of inconvenient key patterns.
13
+ #
14
+ # Results can be parsed with accompanying Kithe::Exiftool::Characterization::Result class --
15
+ # in future, if different versions or invocations of exiftool produce different hash results,
16
+ # we can provide different Results parsers, and a switching method to choose right one
17
+ # based on exiftool version and args embedded in results.
18
+ #
19
+ # In cases of errors where exiftool returns errors in hash, hash is still returned, no raise!
20
+ #
21
+ # @example
22
+ # hash = Kithe::ExiftoolCharacterization.new.call(file_path)
23
+ #
24
+ # presenter = Kithe::ExiftoolCharacterization.presenter_for(hash)
25
+ # presenter.bits_per_sample
26
+ # presenter.make
27
+ # presenter.model
28
+ # presenter.exiftool_validation_warnings # Array of strings
29
+ #
30
+ # * exiftool needs to be installed
31
+ #
32
+ # * Runs with -G0:4 so keys might look like `EXIF:BitsPerSample` or in some cases
33
+ # have a `Copy1` or `Copy2` in there, like `XMP:Copy1:Make`. The `g4` arg
34
+ # results in that `Copy1`, necessary to get multiple validation warnings
35
+ # all included, but a bit annoying when it puts in extraneous `Copy1` for singular
36
+ # results sometimes too.
37
+ class ExiftoolCharacterization
38
+ class_attribute :exiftool_command, default: "exiftool"
39
+
40
+ attr_accessor :file_path
41
+
42
+ # Returns a nice presenter object with methods that get out metadata
43
+ # we are interested in -- in the future, might return differnet classes
44
+ # for specific data, looking at `Exiftool:Version` or `Kithe:CliArgs`
45
+ # keys to switch.
46
+ #
47
+ # @param hash [Hash] a hash returned by ExiftoolCharacterization.call
48
+ # @return a presenter
49
+ def self.presenter_for(hash)
50
+ Kithe::ExiftoolCharacterization::Result.new(hash)
51
+ end
52
+
53
+ # @param file_path [String] path to a local file
54
+ # @returns Hash
55
+ def call(file_path)
56
+ cmd = TTY::Command.new(printer: :null)
57
+
58
+ exiftool_args = [
59
+ "-All", # all tags
60
+ "--File:All", # EXCEPT not "File" group tags,
61
+ "-duplicates", # include duplicate values
62
+ "-validate", # include some validation errors
63
+ "-json", # json output
64
+
65
+ # with exif group names as key prefixes eg "ICC_Profile:ProfileDescription"
66
+ # But also with weird `:Copy1`, `:Copy2` appended for multiples, which we need
67
+ # for `ExifTool:Warning` from `-validate`, to get all of them, that's what the :4 does.
68
+ #
69
+ # https://exiftool.org/forum/index.php?topic=15194.0
70
+ "-G0:4"
71
+ ]
72
+
73
+ # exiftool may return a non-zero exit for a corrupt file -- we don't want to raise,
74
+ # it's still usually returning a nice json hash with error message, just store that
75
+ # in the exiftool_result area anyway!
76
+ result = cmd.run!(
77
+ exiftool_command,
78
+ *exiftool_args,
79
+ file_path.to_s)
80
+
81
+
82
+ if result.out.blank? && result.failed?
83
+ raise ArgumentError.new("#{self.class}: #{result.err}")
84
+ end
85
+
86
+ # Returns an array of hashes
87
+ # decimal_class: String needed so exiftool version number like `12.60` doesn't
88
+ # wind up truncated to 12.6 as a ruby float!
89
+ result_hash = JSON.parse(result.out, decimal_class: String).first
90
+
91
+ # Let's add our invocation options, as a record
92
+ result_hash["Kithe:CliArgs"] = exiftool_args
93
+
94
+ result_hash
95
+ end
96
+ end
97
+ end
data/lib/kithe/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Kithe
2
- VERSION = '2.10.0'
2
+ VERSION = '2.11.0'
3
3
  end
@@ -23,6 +23,27 @@ class Shrine
23
23
  # Because getting this right required some shuffling around of where the wrapping happened, it
24
24
  # was convenient and avoided confusion to isolate wrapping in a class method that can be used
25
25
  # anywhere, and only depends on args passed in, no implicit state anywhere.
26
+ #
27
+ # ## Sharing same tempfile for processing
28
+ #
29
+ # If we have maybe multiple before_promotion hooks that all need an on-disk file,
30
+ # PLUS maybe multiple `add_metadata` hooks that need an on-desk file -- we
31
+ # ABSOLUTELY do NOT each to do a separate copy/download from possibly remote cloud source!
32
+ #
33
+ # The default shrine implementation using Down::ChunkedIO may avoid that (not necessarily
34
+ # as a contract!), BUT still makes multiple local copies, which can still be a performance issue
35
+ # for large files.
36
+ #
37
+ # The solution is the [Shrine tempfile plugin](https://shrinerb.com/docs/plugins/tempfile), which
38
+ # requires the UploadedFile to be opened *around* all uses of `Shrine.with_file`.
39
+ #
40
+ # We include some pretty kludgey code to make this a thing.
41
+ #
42
+ # To take advantage of it, you DO need to add `Shrine.plugin :tempfile` in your app, we don't
43
+ # want to force this global on apps!
44
+ #
45
+ # Note *after_promotion* hooks won't share the common tempfile, since the UploadedFile location
46
+ # has been changed from the one we opened!
26
47
  class KithePromotionCallbacks
27
48
  def self.load_dependencies(uploader, *)
28
49
  uploader.plugin :kithe_promotion_directives
@@ -36,7 +57,20 @@ class Shrine
36
57
  # promotion_logic # sometimes `super`
37
58
  # end
38
59
  #
60
+ # This also contains some pretty kludgey code to open the underlying Shrine::UploadedFile
61
+ # *around* the before_promotion hooks AND promotion. When combined with Shrine tempfile plugin,
62
+ # this means all `before_promotion` hooks and `add_metadata` hooks can use `Shrine.with_file`
63
+ # to get the
39
64
  def self.with_promotion_callbacks(model)
65
+ # only if Shrine::UploadedFile isn't *already* open we definitely want
66
+ # to open it and keep it open -- so WITH Shrine tempfile plugin, we can have
67
+ # various hooks sharing the same tempfile instead of making multiple copies.
68
+ unless model.file_attacher.file.opened?
69
+ model.file_attacher.file.open
70
+ self_opened_file = model.file_attacher.file
71
+ end
72
+
73
+
40
74
  # If callbacks haven't been skipped, and we have a model that implements
41
75
  # callbacks, wrap yield in callbacks.
42
76
  #
@@ -44,12 +78,19 @@ class Shrine
44
78
  if ( !model.file_attacher.promotion_directives["skip_callbacks"] &&
45
79
  model &&
46
80
  model.class.respond_to?(:_promotion_callbacks) )
81
+
47
82
  model.run_callbacks(:promotion) do
48
83
  yield
49
84
  end
85
+
50
86
  else
51
87
  yield
52
88
  end
89
+ ensure
90
+ # only if we DID open the Shrine::UploadedFile ourselves, for the purpose of
91
+ # using a common tempfile with Shrine tempfile plugin,
92
+ # make sure to clean up after ourselves by closing it.
93
+ self_opened_file.close if self_opened_file && self_opened_file.opened?
53
94
  end
54
95
 
55
96
  module AttacherMethods
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kithe
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.10.0
4
+ version: 2.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jonathan Rochkind
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-08-02 00:00:00.000000000 Z
11
+ date: 2023-09-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -345,6 +345,8 @@ files:
345
345
  - README.md
346
346
  - Rakefile
347
347
  - app/assets/config/kithe_manifest.js
348
+ - app/characterization/kithe/exiftool_characterization.rb
349
+ - app/characterization/kithe/exiftool_characterization/result.rb
348
350
  - app/characterization/kithe/ffprobe_characterization.rb
349
351
  - app/derivative_transformers/kithe/ffmpeg_extract_jpg.rb
350
352
  - app/derivative_transformers/kithe/ffmpeg_transformer.rb