kithe 2.10.0 → 2.12.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2442db92e7c22e24d345483d17020d6025c42e4b24b9fe2dbdf0d9ad8625309e
4
- data.tar.gz: 8d15649504ce721fd2c8b5ee1dc8a7f64b9486e64e3f4f3b5a983ce8d295e5ed
3
+ metadata.gz: a6b9d925571ed9f332f97df29a5e1f04aae7154d2c9887712cfd077f237b69eb
4
+ data.tar.gz: 8a5fa5d1acd62524ec9178eecb04cc4db281bdb24d98da1e470cfdf37414f689
5
5
  SHA512:
6
- metadata.gz: 788253f8998adb2a6cd1799b4b124d7e301737477a002c6541f7de5b33f8c6744b818d8aee40de67e3eea316ca6be3411f28a6a2c1951a2ca921416e810cfc45
7
- data.tar.gz: dac26e54710bef716c27833bf822763fc54f784a009835ee43d71fe7e39e63f315cce90dbd6b9a34af54e7ec5ab604b9bf9d749a5450bb94fdc4ebbcdcc4cbf6
6
+ metadata.gz: 9b302eac83111e962398ab69ac13b6dd9a49f68b774a75615e1fbf892c2c60903c2ca640ed7c0d8a85c828c871d51772f9117247aed5d3a18f42425adc39b073
7
+ data.tar.gz: 69876d680490a3989f29091b4ab06aca3671a094ea8ad6a4c2f2cf58cb6eb2882bcdf49724a8397dd72783636d61e6b72f184b846213739276bcc7e412808d87
@@ -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.12.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.12.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-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -16,34 +16,34 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 5.2.1
19
+ version: '6.0'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '7.1'
22
+ version: '7.2'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
26
26
  requirements:
27
27
  - - ">="
28
28
  - !ruby/object:Gem::Version
29
- version: 5.2.1
29
+ version: '6.0'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '7.1'
32
+ version: '7.2'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: attr_json
35
35
  requirement: !ruby/object:Gem::Requirement
36
36
  requirements:
37
- - - "<"
37
+ - - "~>"
38
38
  - !ruby/object:Gem::Version
39
- version: 3.0.0
39
+ version: '2.0'
40
40
  type: :runtime
41
41
  prerelease: false
42
42
  version_requirements: !ruby/object:Gem::Requirement
43
43
  requirements:
44
- - - "<"
44
+ - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 3.0.0
46
+ version: '2.0'
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: simple_form
49
49
  requirement: !ruby/object:Gem::Requirement
@@ -334,6 +334,20 @@ dependencies:
334
334
  - - "<"
335
335
  - !ruby/object:Gem::Version
336
336
  version: '2'
337
+ - !ruby/object:Gem::Dependency
338
+ name: rspec-rails
339
+ requirement: !ruby/object:Gem::Requirement
340
+ requirements:
341
+ - - ">="
342
+ - !ruby/object:Gem::Version
343
+ version: '0'
344
+ type: :development
345
+ prerelease: false
346
+ version_requirements: !ruby/object:Gem::Requirement
347
+ requirements:
348
+ - - ">="
349
+ - !ruby/object:Gem::Version
350
+ version: '0'
337
351
  description:
338
352
  email:
339
353
  - jrochkind@sciencehistory.org
@@ -345,6 +359,8 @@ files:
345
359
  - README.md
346
360
  - Rakefile
347
361
  - app/assets/config/kithe_manifest.js
362
+ - app/characterization/kithe/exiftool_characterization.rb
363
+ - app/characterization/kithe/exiftool_characterization/result.rb
348
364
  - app/characterization/kithe/ffprobe_characterization.rb
349
365
  - app/derivative_transformers/kithe/ffmpeg_extract_jpg.rb
350
366
  - app/derivative_transformers/kithe/ffmpeg_transformer.rb
@@ -426,7 +442,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
426
442
  - !ruby/object:Gem::Version
427
443
  version: '0'
428
444
  requirements: []
429
- rubygems_version: 3.3.26
445
+ rubygems_version: 3.4.21
430
446
  signing_key:
431
447
  specification_version: 4
432
448
  summary: Shareable tools/components for building a digital collections app in Rails.