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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c501b48a3f6f517f1fd6906a357aba0004717c94f8b40bd20996efd696bcaff2
|
4
|
+
data.tar.gz: b29ffa367832a02a78a94641c6bf37c5bac03cda23c7db80325806bcc151d3f5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
@@ -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.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-
|
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
|