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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a6b9d925571ed9f332f97df29a5e1f04aae7154d2c9887712cfd077f237b69eb
|
4
|
+
data.tar.gz: 8a5fa5d1acd62524ec9178eecb04cc4db281bdb24d98da1e470cfdf37414f689
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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.
|
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-
|
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:
|
19
|
+
version: '6.0'
|
20
20
|
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '7.
|
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:
|
29
|
+
version: '6.0'
|
30
30
|
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '7.
|
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:
|
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:
|
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.
|
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.
|