xmp_toolkit_ruby 0.0.2 → 0.1.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 +4 -4
- data/.clang-format +4 -0
- data/CHANGELOG.md +37 -2
- data/README.md +131 -0
- data/ext/xmp_toolkit_ruby/xmp_toolkit.cpp +117 -284
- data/ext/xmp_toolkit_ruby/xmp_toolkit.hpp +15 -26
- data/ext/xmp_toolkit_ruby/xmp_toolkit_ruby.cpp +29 -35
- data/ext/xmp_toolkit_ruby/xmp_wrapper.cpp +582 -0
- data/ext/xmp_toolkit_ruby/xmp_wrapper.hpp +35 -0
- data/lib/xmp_toolkit_ruby/namespaces.rb +1 -0
- data/lib/xmp_toolkit_ruby/version.rb +1 -1
- data/lib/xmp_toolkit_ruby/xmp_char_form.rb +45 -0
- data/lib/xmp_toolkit_ruby/xmp_file.rb +308 -0
- data/lib/xmp_toolkit_ruby/xmp_file_open_flags.rb +74 -0
- data/lib/xmp_toolkit_ruby/xmp_value.rb +16 -0
- data/lib/xmp_toolkit_ruby.rb +71 -33
- data/sig/xmp_toolkit_ruby/namespaces.rbs +119 -0
- data/sig/xmp_toolkit_ruby/xmp_char_form.rbs +17 -0
- data/sig/xmp_toolkit_ruby/xmp_file.rbs +45 -0
- data/sig/xmp_toolkit_ruby/xmp_file_format.rbs +11 -0
- data/sig/xmp_toolkit_ruby/xmp_file_handler_flags.rbs +15 -0
- data/sig/xmp_toolkit_ruby/xmp_file_open_flags.rbs +29 -0
- data/sig/xmp_toolkit_ruby/xmp_toolkit.rbs +14 -0
- data/sig/xmp_toolkit_ruby/xmp_value.rbs +19 -0
- data/sig/xmp_toolkit_ruby/xmp_wrapper.rbs +29 -0
- data/sig/xmp_toolkit_ruby.rbs +6 -1
- data/tasks/clang_format.rake +44 -0
- metadata +18 -1
@@ -0,0 +1,308 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XmpToolkitRuby
|
4
|
+
require_relative "xmp_file_open_flags"
|
5
|
+
|
6
|
+
# XmpFile provides a high-level Ruby interface for managing XMP metadata
|
7
|
+
# in files such as JPEG, TIFF, PNG, and PDF. It wraps the underlying
|
8
|
+
# Adobe XMP SDK calls, offering simplified methods to open, read, update,
|
9
|
+
# and write XMP packets, as well as to retrieve file and packet information.
|
10
|
+
#
|
11
|
+
# == Core Features
|
12
|
+
# - Open files for read or update, with optional fallback flags
|
13
|
+
# - Read raw and parsed XMP metadata
|
14
|
+
# - Update metadata by bulk XML or by individual property/schema
|
15
|
+
# - Write changes back to the file
|
16
|
+
# - Retrieve file-level info (format, handler flags, open flags)
|
17
|
+
# - Retrieve packet-level info (packet size, padding)
|
18
|
+
# - Support for localized text properties (alt-text arrays)
|
19
|
+
#
|
20
|
+
# @example Read and print XMP data:
|
21
|
+
# XmpFile.with_xmp_file("image.jpg") do |xmp|
|
22
|
+
# p xmp.meta["xmp_data"]
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# @example Update a custom property:
|
26
|
+
# XmpFile.with_xmp_file("doc.pdf", open_flags: XmpFileOpenFlags::OPEN_FOR_UPDATE) do |xmp|
|
27
|
+
# new_xml = '<x:xmpmeta xmlns:x="adobe:ns:meta/">...'</x>
|
28
|
+
# xmp.update_meta(new_xml)
|
29
|
+
# end
|
30
|
+
class XmpFile
|
31
|
+
# Path to the file on disk containing XMP metadata.
|
32
|
+
# @return [String]
|
33
|
+
attr_reader :file_path
|
34
|
+
|
35
|
+
# Flags used for the primary open operation. See XmpFileOpenFlags.
|
36
|
+
# @return [Integer]
|
37
|
+
attr_reader :open_flags
|
38
|
+
|
39
|
+
# Optional fallback flags if opening with primary flags fails.
|
40
|
+
# @return [Integer, nil]
|
41
|
+
attr_reader :fallback_flags
|
42
|
+
|
43
|
+
class << self
|
44
|
+
# Register a custom namespace URI for subsequent property operations.
|
45
|
+
#
|
46
|
+
# @param namespace [String] Full URI of the namespace, e.g. "http://ns.adobe.com/photoshop/1.0/"
|
47
|
+
# @param suggested_prefix [String] Short prefix to use in XML (e.g. "photoshop").
|
48
|
+
# @return [String] The actual prefix registered by the SDK.
|
49
|
+
# @raise [RuntimeError] if the XMP toolkit has not been initialized.
|
50
|
+
def register_namespace(namespace, suggested_prefix)
|
51
|
+
warn "XMP Toolkit not initialized; loading default plugins from \#{XmpToolkitRuby::PLUGINS_PATH}" unless XmpToolkitRuby::XmpToolkit.initialized?
|
52
|
+
XmpWrapper.register_namespace(namespace, suggested_prefix)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Open a file with XMP support, yielding a managed XmpFile instance.
|
56
|
+
# This method ensures the XMP toolkit is initialized and terminated,
|
57
|
+
# and that the file is closed and written (if modified).
|
58
|
+
#
|
59
|
+
# @param file_path [String] Path to the target file.
|
60
|
+
# @param open_flags [Integer] Bitmask from XmpFileOpenFlags (default: OPEN_FOR_READ).
|
61
|
+
# @param plugin_path [String] Directory of XMP SDK plugins (default: PLUGINS_PATH).
|
62
|
+
# @param fallback_flags [Integer, nil] Alternate flags if primary fails.
|
63
|
+
# @param auto_terminate_toolkit [Boolean] Shutdown toolkit after block (default: true).
|
64
|
+
# @yield [xmp_file] Gives an XmpFile instance for metadata operations.
|
65
|
+
# @yieldparam xmp_file [XmpFile]
|
66
|
+
# @return [void]
|
67
|
+
# @raise [IOError] if file open fails and no fallback succeeds.
|
68
|
+
def with_xmp_file(
|
69
|
+
file_path,
|
70
|
+
open_flags: XmpFileOpenFlags::OPEN_FOR_READ,
|
71
|
+
plugin_path: XmpToolkitRuby::PLUGINS_PATH,
|
72
|
+
fallback_flags: nil,
|
73
|
+
auto_terminate_toolkit: true
|
74
|
+
)
|
75
|
+
XmpToolkitRuby.check_file!(file_path,
|
76
|
+
need_to_read: true,
|
77
|
+
need_to_write: XmpFileOpenFlags.contains?(open_flags, :open_for_update))
|
78
|
+
|
79
|
+
XmpToolkitRuby::XmpToolkit.initialize_xmp(plugin_path) unless XmpToolkitRuby.sdk_initialized?
|
80
|
+
|
81
|
+
xmp_file = new(file_path,
|
82
|
+
open_flags: open_flags,
|
83
|
+
fallback_flags: fallback_flags)
|
84
|
+
xmp_file.open
|
85
|
+
yield xmp_file
|
86
|
+
ensure
|
87
|
+
xmp_file.write if xmp_file && XmpFileOpenFlags.contains?(xmp_file.open_flags, :open_for_update)
|
88
|
+
xmp_file&.close
|
89
|
+
XmpToolkitRuby::XmpToolkit.terminate if auto_terminate_toolkit && XmpToolkitRuby.sdk_initialized?
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Initialize an XmpFile for a given path.
|
94
|
+
#
|
95
|
+
# @param file_path [String,Pathname] Local file path to open.
|
96
|
+
# @param open_flags [Integer] XmpFileOpenFlags bitmask (default: OPEN_FOR_READ).
|
97
|
+
# @param fallback_flags [Integer,nil] Alternate flags on failure.
|
98
|
+
# @raise [ArgumentError] if file_path is not readable.
|
99
|
+
# @example
|
100
|
+
# XmpFile.new("photo.tif", open_flags: XmpFileOpenFlags::OPEN_FOR_UPDATE)
|
101
|
+
def initialize(file_path, open_flags: XmpFileOpenFlags::OPEN_FOR_READ, fallback_flags: nil)
|
102
|
+
@file_path = file_path.to_s
|
103
|
+
raise ArgumentError, "File path '#{@file_path}' must exist and be readable" unless File.readable?(@file_path)
|
104
|
+
|
105
|
+
@open_flags = open_flags
|
106
|
+
@fallback_flags = fallback_flags
|
107
|
+
@open = false
|
108
|
+
@xmp_wrapper = XmpWrapper.new
|
109
|
+
end
|
110
|
+
|
111
|
+
# Open the file for XMP operations.
|
112
|
+
# If initialization flags fail and fallback_flags is provided,
|
113
|
+
# attempts a second open with fallback flags.
|
114
|
+
#
|
115
|
+
# @return [void]
|
116
|
+
# @raise [IOError] if both primary and fallback open(...) fail.
|
117
|
+
# @note Emits warning if toolkit not initialized.
|
118
|
+
def open
|
119
|
+
return if open?
|
120
|
+
|
121
|
+
warn "XMP Toolkit not initialized; using default plugin path" unless XmpToolkitRuby::XmpToolkit.initialized?
|
122
|
+
|
123
|
+
begin
|
124
|
+
@xmp_wrapper.open(file_path, open_flags).tap { @open = true }
|
125
|
+
rescue IOError => e
|
126
|
+
@xmp_wrapper.close
|
127
|
+
@open = false
|
128
|
+
raise e unless fallback_flags
|
129
|
+
|
130
|
+
@xmp_wrapper.open(file_path, fallback_flags).tap { @open = true }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# @return [Boolean] Whether the file is currently open for XMP operations.
|
135
|
+
def open?
|
136
|
+
@open
|
137
|
+
end
|
138
|
+
|
139
|
+
# Retrieve a hash of file-level metadata and flags.
|
140
|
+
#
|
141
|
+
# @return [Hash{String=>Object}]
|
142
|
+
# @example
|
143
|
+
# info = xmp.file_info
|
144
|
+
# puts "Format: #{info['format']}"
|
145
|
+
def file_info
|
146
|
+
@file_info ||= begin
|
147
|
+
info = @xmp_wrapper.file_info
|
148
|
+
{
|
149
|
+
"handler_flags" => XmpToolkitRuby::XmpFileHandlerFlags.flags_for(info["handler_flags"]),
|
150
|
+
"handler_flags_orig" => info["handler_flags"],
|
151
|
+
"format" => XmpToolkitRuby::XmpFileFormat.name_for(info["format"]),
|
152
|
+
"format_orig" => info["format"],
|
153
|
+
"open_flags" => XmpToolkitRuby::XmpFileOpenFlags.flags_for(info["open_flags"]),
|
154
|
+
"open_flags_orig" => info["open_flags"]
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Retrieve low-level packet information (size, offset, padding).
|
160
|
+
#
|
161
|
+
# @return [Hash] Raw packet info as provided by the SDK.
|
162
|
+
def packet_info
|
163
|
+
@packet_info ||= @xmp_wrapper.packet_info
|
164
|
+
end
|
165
|
+
|
166
|
+
# Get parsed XMP metadata and packet boundaries.
|
167
|
+
#
|
168
|
+
# @return [Hash]
|
169
|
+
# - "begin" [String]: Packet start marker timestamp
|
170
|
+
# - "packet_id" [String]: Unique XMP packet ID
|
171
|
+
# - "xmp_data" [String]: Inner RDF/XML content
|
172
|
+
# - "xmp_data_orig" [String]: Full packet including processing instruction
|
173
|
+
# rubocop:disable Metrics/AbcSize
|
174
|
+
def meta
|
175
|
+
raw = @xmp_wrapper.meta
|
176
|
+
return {} if raw.nil? || raw.empty?
|
177
|
+
|
178
|
+
doc = Nokogiri::XML(raw)
|
179
|
+
pis = doc.xpath("//processing-instruction('xpacket')")
|
180
|
+
begin_pi = pis.detect { |pi| pi.content.start_with?("begin=") }
|
181
|
+
attrs = begin_pi.content.scan(/(\w+)="([^"]*)"/).to_h
|
182
|
+
pis.remove
|
183
|
+
|
184
|
+
{
|
185
|
+
"begin" => attrs["begin"],
|
186
|
+
"packet_id" => attrs["id"],
|
187
|
+
"xmp_data" => doc.root.to_xml,
|
188
|
+
"xmp_data_orig" => raw
|
189
|
+
}
|
190
|
+
end
|
191
|
+
|
192
|
+
# rubocop:enable Metrics/AbcSize
|
193
|
+
|
194
|
+
# Persist all pending XMP updates to the file.
|
195
|
+
#
|
196
|
+
# @raise [RuntimeError] unless file is open.
|
197
|
+
# @return [void]
|
198
|
+
def write
|
199
|
+
raise "File not open; cannot write" unless open?
|
200
|
+
|
201
|
+
@xmp_wrapper.write
|
202
|
+
end
|
203
|
+
|
204
|
+
# Bulk update XMP metadata using an RDF/XML string.
|
205
|
+
#
|
206
|
+
# @param xmp_data [String] Full RDF/XML payload or fragment
|
207
|
+
# @param mode [Symbol] :upsert (default) or :replace
|
208
|
+
# @return [void]
|
209
|
+
def update_meta(xmp_data, mode: :upsert)
|
210
|
+
open
|
211
|
+
@xmp_wrapper.update_meta(xmp_data, mode: mode)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Update a single property in the XMP schema.
|
215
|
+
#
|
216
|
+
# @param namespace [String] Schema namespace URI
|
217
|
+
# @param property [String] Qualified property name (without prefix)
|
218
|
+
# @param value [String] New value for the property
|
219
|
+
# @return [void]
|
220
|
+
def update_property(namespace, property, value)
|
221
|
+
open
|
222
|
+
@xmp_wrapper.update_property(namespace, property, value)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Retrieve the value of a simple XMP property.
|
226
|
+
#
|
227
|
+
# This will open the file (if not already open), query the underlying
|
228
|
+
# SDK for the given namespace + property, and return whatever value is stored.
|
229
|
+
#
|
230
|
+
# @param namespace [String] Namespace URI of the schema (e.g. "http://ns.adobe.com/photoshop/1.0/")
|
231
|
+
# @param property [String] Property name (without prefix), e.g. "CreatorTool"
|
232
|
+
# @return [String, nil] The value of the property, or nil if not set
|
233
|
+
# @raise [RuntimeError] if the file cannot be opened
|
234
|
+
def property(namespace, property)
|
235
|
+
open
|
236
|
+
@xmp_wrapper.property(namespace, property)
|
237
|
+
end
|
238
|
+
|
239
|
+
# Retrieve a localized (alt-text) value from an XMP array.
|
240
|
+
#
|
241
|
+
# Locates the alt-text array identified by
|
242
|
+
# `alt_text_name` in the given `schema_ns`, then returns the string
|
243
|
+
# matching the requested generic and specific language codes.
|
244
|
+
#
|
245
|
+
# @param schema_ns [String] Namespace URI of the alt-text schema
|
246
|
+
# @param alt_text_name [String] The name of the localized text array
|
247
|
+
# @param generic_lang [String] Base language code (e.g. "en")
|
248
|
+
# @param specific_lang [String] Locale variant (e.g. "en-US")
|
249
|
+
# @return [String, nil] The localized string for that locale, or nil if not found
|
250
|
+
# @raise [RuntimeError] if the file cannot be opened
|
251
|
+
def localized_property(schema_ns:, alt_text_name:, generic_lang:, specific_lang:)
|
252
|
+
open
|
253
|
+
|
254
|
+
@xmp_wrapper.localized_property(
|
255
|
+
schema_ns: schema_ns,
|
256
|
+
alt_text_name: alt_text_name,
|
257
|
+
generic_lang: generic_lang,
|
258
|
+
specific_lang: specific_lang
|
259
|
+
)
|
260
|
+
end
|
261
|
+
|
262
|
+
# Update an alternative-text (localized string) property.
|
263
|
+
#
|
264
|
+
# @param schema_ns [String] Namespace URI of the alt-text schema
|
265
|
+
# @param alt_text_name [String] Name of the alt-text array
|
266
|
+
# @param generic_lang [String] Base language (e.g. "en")
|
267
|
+
# @param specific_lang [String] Specific locale (e.g. "en-US")
|
268
|
+
# @param item_value [String] Localized string value
|
269
|
+
# @param options [Integer] Bitmask for array operations (see SDK)
|
270
|
+
# @return [void]
|
271
|
+
def update_localized_property(schema_ns:, alt_text_name:, generic_lang:, specific_lang:, item_value:, options:)
|
272
|
+
open
|
273
|
+
@xmp_wrapper.update_localized_property(
|
274
|
+
schema_ns: schema_ns,
|
275
|
+
alt_text_name: alt_text_name,
|
276
|
+
generic_lang: generic_lang,
|
277
|
+
specific_lang: specific_lang,
|
278
|
+
item_value: item_value,
|
279
|
+
options: options
|
280
|
+
)
|
281
|
+
end
|
282
|
+
|
283
|
+
# Close the file and clear internal state.
|
284
|
+
# @return [void]
|
285
|
+
def close
|
286
|
+
return unless open?
|
287
|
+
|
288
|
+
@open = false
|
289
|
+
@xmp_wrapper.close
|
290
|
+
end
|
291
|
+
|
292
|
+
private
|
293
|
+
|
294
|
+
# Internal helper to map raw handler flags to named symbols.
|
295
|
+
#
|
296
|
+
# @param handler_flags [Integer,nil]
|
297
|
+
# @return [Hash]
|
298
|
+
# @api private
|
299
|
+
def map_handler_flags(handler_flags)
|
300
|
+
return {} if handler_flags.nil?
|
301
|
+
|
302
|
+
{
|
303
|
+
"handler_flags" => XmpToolkitRuby::XmpFileHandlerFlags.flags_for(handler_flags),
|
304
|
+
"handler_flags_orig" => handler_flags
|
305
|
+
}
|
306
|
+
end
|
307
|
+
end
|
308
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XmpToolkitRuby
|
4
|
+
module XmpFileOpenFlags
|
5
|
+
OPEN_FOR_READ = 0x0000_0001 # Open for read-only access
|
6
|
+
OPEN_FOR_UPDATE = 0x0000_0002 # Open for reading and writing
|
7
|
+
OPEN_ONLY_XMP = 0x0000_0004 # Only the XMP is wanted, allows space/time optimizations
|
8
|
+
FORCE_GIVEN_HANDLER = 0x0000_0008 # Force use of the given handler (format), do not verify format
|
9
|
+
OPEN_STRICTLY = 0x0000_0010 # Strictly use only designated file handler, no fallback
|
10
|
+
OPEN_USE_SMART_HANDLER = 0x0000_0020 # Require the use of a smart handler
|
11
|
+
OPEN_USE_PACKET_SCANNING = 0x0000_0040 # Force packet scanning, do not use smart handler
|
12
|
+
OPEN_LIMITED_SCANNING = 0x0000_0080 # Only scan files "known" to need scanning
|
13
|
+
OPEN_REPAIR_FILE = 0x0000_0100 # Attempt to repair a file opened for update
|
14
|
+
OPTIMIZE_FILE_LAYOUT = 0x0000_0200 # Optimize file layout when updating
|
15
|
+
PRESERVE_PDF_STATE = 0x0000_0400 # Preserve PDF document state when updating
|
16
|
+
|
17
|
+
FLAGS = {
|
18
|
+
open_for_read: OPEN_FOR_READ,
|
19
|
+
open_for_update: OPEN_FOR_UPDATE,
|
20
|
+
open_only_xmp: OPEN_ONLY_XMP,
|
21
|
+
force_given_handler: FORCE_GIVEN_HANDLER,
|
22
|
+
open_strictly: OPEN_STRICTLY,
|
23
|
+
open_use_smart_handler: OPEN_USE_SMART_HANDLER,
|
24
|
+
open_use_packet_scanning: OPEN_USE_PACKET_SCANNING,
|
25
|
+
open_limited_scanning: OPEN_LIMITED_SCANNING,
|
26
|
+
open_repair_file: OPEN_REPAIR_FILE,
|
27
|
+
optimize_file_layout: OPTIMIZE_FILE_LAYOUT,
|
28
|
+
preserve_pdf_state: PRESERVE_PDF_STATE
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
FLAGS_BY_VALUE = FLAGS.invert.freeze
|
32
|
+
|
33
|
+
class << self
|
34
|
+
def value_for(name)
|
35
|
+
key = name.is_a?(String) ? name.to_sym : name
|
36
|
+
FLAGS[key]
|
37
|
+
end
|
38
|
+
|
39
|
+
def name_for(hex_value)
|
40
|
+
FLAGS_BY_VALUE[hex_value]
|
41
|
+
end
|
42
|
+
|
43
|
+
def flags_for(bitmask)
|
44
|
+
FLAGS.select { |_, bit| bitmask.anybits?(bit) }.keys
|
45
|
+
end
|
46
|
+
|
47
|
+
def contains?(bitmask, flag)
|
48
|
+
raise ArgumentError, "Invalid flag type: #{flag.class}" unless flag.is_a?(Symbol) || flag.is_a?(String)
|
49
|
+
|
50
|
+
bitmask & value_for(flag) != 0
|
51
|
+
end
|
52
|
+
|
53
|
+
# Takes multiple flag names (symbols or strings) or constants,
|
54
|
+
# returns combined bitmask OR-ing all.
|
55
|
+
#
|
56
|
+
# Example:
|
57
|
+
# bitmask_for(:open_for_read, :force_given_handler)
|
58
|
+
# bitmask_for(OPEN_FOR_READ, FORCE_GIVEN_HANDLER)
|
59
|
+
def bitmask_for(*args)
|
60
|
+
args.reduce(0) do |mask, flag|
|
61
|
+
val = case flag
|
62
|
+
when Symbol, String then value_for(flag)
|
63
|
+
when Integer then flag
|
64
|
+
else
|
65
|
+
raise ArgumentError, "Invalid flag type: #{flag.class}"
|
66
|
+
end
|
67
|
+
raise ArgumentError, "Unknown flag: #{flag.inspect}" unless val
|
68
|
+
|
69
|
+
mask | val
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module XmpToolkitRuby
|
4
|
+
class XmpValue
|
5
|
+
attr_reader :value, :type
|
6
|
+
|
7
|
+
TYPES = %i[string bool int int64 float date].freeze
|
8
|
+
|
9
|
+
def initialize(value, type: nil)
|
10
|
+
@value = value
|
11
|
+
@type = type.to_sym
|
12
|
+
|
13
|
+
raise ArgumentError, "Invalid type: #{type}" unless TYPES.include?(@type)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/xmp_toolkit_ruby.rb
CHANGED
@@ -5,6 +5,7 @@ require_relative "xmp_toolkit_ruby/xmp_toolkit_ruby"
|
|
5
5
|
|
6
6
|
require "nokogiri"
|
7
7
|
require "rbconfig"
|
8
|
+
require "date"
|
8
9
|
|
9
10
|
# The `XmpToolkitRuby` module serves as a Ruby interface to Adobe's XMP Toolkit,
|
10
11
|
# a native C++ library. This module allows Ruby applications to read and write
|
@@ -30,10 +31,14 @@ require "rbconfig"
|
|
30
31
|
# new_xmp_data = "<x:xmpmeta xmlns:x='adobe:ns:meta/'><rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'></rdf:RDF></x:xmpmeta>"
|
31
32
|
# XmpToolkitRuby.xmp_to_file("path/to/image.jpg", new_xmp_data, override: true)
|
32
33
|
#
|
34
|
+
# rubocop:disable Metrics/ModuleLength
|
33
35
|
module XmpToolkitRuby
|
34
36
|
require_relative "xmp_toolkit_ruby/xmp_file_format"
|
35
37
|
require_relative "xmp_toolkit_ruby/namespaces"
|
36
38
|
require_relative "xmp_toolkit_ruby/xmp_file_handler_flags"
|
39
|
+
require_relative "xmp_toolkit_ruby/xmp_file"
|
40
|
+
require_relative "xmp_toolkit_ruby/xmp_value"
|
41
|
+
require_relative "xmp_toolkit_ruby/xmp_char_form"
|
37
42
|
|
38
43
|
# The `PLUGINS_PATH` constant defines the directory where the XMP Toolkit
|
39
44
|
# should look for its plugins, particularly the PDF handler.
|
@@ -96,9 +101,17 @@ module XmpToolkitRuby
|
|
96
101
|
check_file! file_path, need_to_read: true, need_to_write: false
|
97
102
|
|
98
103
|
with_init do
|
99
|
-
|
100
|
-
|
101
|
-
|
104
|
+
XmpToolkitRuby::XmpFile.with_xmp_file(
|
105
|
+
file_path,
|
106
|
+
open_flags: XmpToolkitRuby::XmpFileOpenFlags.bitmask_for(:open_for_read, :open_use_smart_handler),
|
107
|
+
fallback_flags: XmpToolkitRuby::XmpFileOpenFlags.bitmask_for(:open_for_read, :open_use_packet_scanning)
|
108
|
+
) do |xmp_file|
|
109
|
+
file_info = xmp_file.file_info
|
110
|
+
packet_info = xmp_file.packet_info
|
111
|
+
xmp_data = xmp_file.meta
|
112
|
+
|
113
|
+
file_info.merge(packet_info).merge(xmp_data)
|
114
|
+
end
|
102
115
|
end
|
103
116
|
end
|
104
117
|
|
@@ -123,24 +136,74 @@ module XmpToolkitRuby
|
|
123
136
|
def xmp_to_file(file_path, xmp_data, override: false)
|
124
137
|
check_file! file_path, need_to_read: true, need_to_write: true
|
125
138
|
|
126
|
-
with_init
|
139
|
+
with_init do
|
140
|
+
XmpToolkitRuby::XmpFile.with_xmp_file(
|
141
|
+
file_path,
|
142
|
+
open_flags: XmpToolkitRuby::XmpFileOpenFlags.bitmask_for(:open_for_update, :open_use_smart_handler),
|
143
|
+
fallback_flags: XmpToolkitRuby::XmpFileOpenFlags.bitmask_for(:open_for_update, :open_use_packet_scanning)
|
144
|
+
) do |xmp_file|
|
145
|
+
xmp_file.update_meta xmp_data, mode: override ? :override : :upsert
|
146
|
+
|
147
|
+
file_info = xmp_file.file_info
|
148
|
+
packet_info = xmp_file.packet_info
|
149
|
+
xmp_data = xmp_file.meta
|
150
|
+
|
151
|
+
file_info.merge(packet_info).merge(xmp_data)
|
152
|
+
end
|
153
|
+
end
|
127
154
|
end
|
128
155
|
|
129
156
|
# Ensures the native XMP Toolkit is initialized before executing a block
|
130
|
-
# of code and terminated
|
157
|
+
# of code and terminated afterward. This is crucial for managing the
|
131
158
|
# lifecycle of the underlying C++ library resources.
|
132
159
|
#
|
133
160
|
# This method should wrap any calls to the native `XmpToolkitRuby::XmpToolkit` methods.
|
134
161
|
#
|
162
|
+
# @param path [String, nil] (nil) Optional path to the XMP Toolkit plugins directory.
|
163
|
+
# If `nil` or not provided, it defaults to `PLUGINS_PATH`.
|
135
164
|
# @yield The block of code to execute while the XMP Toolkit is initialized.
|
136
165
|
# @return The result of the yielded block.
|
137
|
-
def with_init(&block)
|
138
|
-
XmpToolkitRuby::XmpToolkit.initialize_xmp
|
166
|
+
def with_init(path = nil, &block)
|
167
|
+
XmpToolkitRuby::XmpToolkit.initialize_xmp(path || PLUGINS_PATH) unless XmpToolkitRuby.sdk_initialized?
|
168
|
+
|
139
169
|
block.call
|
140
170
|
ensure
|
141
171
|
XmpToolkitRuby::XmpToolkit.terminate
|
142
172
|
end
|
143
173
|
|
174
|
+
# Checks if the XMP Toolkit SDK has been initialized.
|
175
|
+
# This method is useful for ensuring that the SDK is ready for use
|
176
|
+
#
|
177
|
+
def sdk_initialized?
|
178
|
+
XmpToolkitRuby::XmpToolkit.initialized?
|
179
|
+
end
|
180
|
+
|
181
|
+
# Validates file accessibility before performing read or write operations.
|
182
|
+
#
|
183
|
+
# Checks for:
|
184
|
+
# - Nil file path.
|
185
|
+
# - File existence.
|
186
|
+
# - File readability (if `need_to_read` is true).
|
187
|
+
# - File writability (if `need_to_write` is true).
|
188
|
+
#
|
189
|
+
# @param file_path [String] The path to the file to check.
|
190
|
+
# @param need_to_read [Boolean] (true) Whether the file needs to be readable.
|
191
|
+
# @param need_to_write [Boolean] (false) Whether the file needs to be writable.
|
192
|
+
# @raise [FileNotFoundError] If any of the checks fail.
|
193
|
+
# @return [void]
|
194
|
+
# @api private
|
195
|
+
def check_file!(file_path, need_to_read: true, need_to_write: false)
|
196
|
+
if file_path.nil?
|
197
|
+
raise FileNotFoundError, "File path cannot be nil"
|
198
|
+
elsif !File.exist?(file_path)
|
199
|
+
raise FileNotFoundError, "File not found: #{file_path}"
|
200
|
+
elsif need_to_read && !File.readable?(file_path)
|
201
|
+
raise FileNotFoundError, "File exists but is not readable: #{file_path}"
|
202
|
+
elsif need_to_write && !File.writable?(file_path)
|
203
|
+
raise FileNotFoundError, "File exists but is not writable: #{file_path}"
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
144
207
|
private
|
145
208
|
|
146
209
|
# Parses raw XMP data string to extract `xpacket` processing instruction
|
@@ -207,31 +270,6 @@ module XmpToolkitRuby
|
|
207
270
|
end
|
208
271
|
|
209
272
|
# rubocop: enable Metrics/AbcSize, Metrics/MethodLength
|
210
|
-
|
211
|
-
# Validates file accessibility before performing read or write operations.
|
212
|
-
#
|
213
|
-
# Checks for:
|
214
|
-
# - Nil file path.
|
215
|
-
# - File existence.
|
216
|
-
# - File readability (if `need_to_read` is true).
|
217
|
-
# - File writability (if `need_to_write` is true).
|
218
|
-
#
|
219
|
-
# @param file_path [String] The path to the file to check.
|
220
|
-
# @param need_to_read [Boolean] (true) Whether the file needs to be readable.
|
221
|
-
# @param need_to_write [Boolean] (false) Whether the file needs to be writable.
|
222
|
-
# @raise [FileNotFoundError] If any of the checks fail.
|
223
|
-
# @return [void]
|
224
|
-
# @api private
|
225
|
-
def check_file!(file_path, need_to_read: true, need_to_write: false)
|
226
|
-
if file_path.nil?
|
227
|
-
raise FileNotFoundError, "File path cannot be nil"
|
228
|
-
elsif !File.exist?(file_path)
|
229
|
-
raise FileNotFoundError, "File not found: #{file_path}"
|
230
|
-
elsif need_to_read && !File.readable?(file_path)
|
231
|
-
raise FileNotFoundError, "File exists but is not readable: #{file_path}"
|
232
|
-
elsif need_to_write && !File.writable?(file_path)
|
233
|
-
raise FileNotFoundError, "File exists but is not writable: #{file_path}"
|
234
|
-
end
|
235
|
-
end
|
236
273
|
end
|
237
274
|
end
|
275
|
+
# rubocop: enable Metrics/ModuleLength
|
@@ -0,0 +1,119 @@
|
|
1
|
+
module XmpToolkitRuby
|
2
|
+
module Namespaces
|
3
|
+
XMP_NS_ADOBE_STOCK_PHOTO: ::String
|
4
|
+
|
5
|
+
XMP_NS_AESCART: ::String
|
6
|
+
|
7
|
+
XMP_NS_ASF: ::String
|
8
|
+
|
9
|
+
XMP_NS_BWF: ::String
|
10
|
+
|
11
|
+
XMP_NS_CAMERA_RAW: ::String
|
12
|
+
|
13
|
+
XMP_NS_CREATOR_ATOM: ::String
|
14
|
+
|
15
|
+
XMP_NS_DC: ::String
|
16
|
+
|
17
|
+
XMP_NS_DICOM: ::String
|
18
|
+
|
19
|
+
XMP_NS_DM: ::String
|
20
|
+
|
21
|
+
XMP_NS_EXIF: ::String
|
22
|
+
|
23
|
+
XMP_NS_EXIF_AUX: ::String
|
24
|
+
|
25
|
+
XMP_NS_EXIF_EX: ::String
|
26
|
+
|
27
|
+
XMP_NS_IPTC_CORE: ::String
|
28
|
+
|
29
|
+
XMP_NS_IPTC_EXT: ::String
|
30
|
+
|
31
|
+
XMP_NS_IXML: ::String
|
32
|
+
|
33
|
+
XMP_NS_JP2K: ::String
|
34
|
+
|
35
|
+
XMP_NS_JPEG: ::String
|
36
|
+
|
37
|
+
XMP_NS_PDF: ::String
|
38
|
+
|
39
|
+
XMP_NS_PDFA_EXTENSION: ::String
|
40
|
+
|
41
|
+
XMP_NS_PDFA_FIELD: ::String
|
42
|
+
|
43
|
+
XMP_NS_PDFA_ID: ::String
|
44
|
+
|
45
|
+
XMP_NS_PDFA_PROPERTY: ::String
|
46
|
+
|
47
|
+
XMP_NS_PDFA_SCHEMA: ::String
|
48
|
+
|
49
|
+
XMP_NS_PDFA_TYPE: ::String
|
50
|
+
|
51
|
+
XMP_NS_PDFUA_ID: ::String
|
52
|
+
|
53
|
+
XMP_NS_PDFX: ::String
|
54
|
+
|
55
|
+
XMP_NS_PDFX_ID: ::String
|
56
|
+
|
57
|
+
XMP_NS_PHOTOSHOP: ::String
|
58
|
+
|
59
|
+
XMP_NS_PLUS: ::String
|
60
|
+
|
61
|
+
XMP_NS_PNG: ::String
|
62
|
+
|
63
|
+
XMP_NS_PSALBUM: ::String
|
64
|
+
|
65
|
+
XMP_NS_RDF: ::String
|
66
|
+
|
67
|
+
XMP_NS_RIFFINFO: ::String
|
68
|
+
|
69
|
+
XMP_NS_SCRIPT: ::String
|
70
|
+
|
71
|
+
XMP_NS_SWF: ::String
|
72
|
+
|
73
|
+
XMP_NS_TIFF: ::String
|
74
|
+
|
75
|
+
XMP_NS_WAV: ::String
|
76
|
+
|
77
|
+
XMP_NS_XML: ::String
|
78
|
+
|
79
|
+
XMP_NS_XMP: ::String
|
80
|
+
|
81
|
+
XMP_NS_XMP_BJ: ::String
|
82
|
+
|
83
|
+
XMP_NS_XMP_DIMENSIONS: ::String
|
84
|
+
|
85
|
+
XMP_NS_XMP_FONT: ::String
|
86
|
+
|
87
|
+
XMP_NS_XMP_GRAPHICS: ::String
|
88
|
+
|
89
|
+
XMP_NS_XMP_G_IMG: ::String
|
90
|
+
|
91
|
+
XMP_NS_XMP_IDENTIFIER_QUAL: ::String
|
92
|
+
|
93
|
+
XMP_NS_XMP_IMAGE: ::String
|
94
|
+
|
95
|
+
XMP_NS_XMP_MANIFEST_ITEM: ::String
|
96
|
+
|
97
|
+
XMP_NS_XMP_MM: ::String
|
98
|
+
|
99
|
+
XMP_NS_XMP_NOTE: ::String
|
100
|
+
|
101
|
+
XMP_NS_XMP_PAGED_FILE: ::String
|
102
|
+
|
103
|
+
XMP_NS_XMP_RESOURCE_EVENT: ::String
|
104
|
+
|
105
|
+
XMP_NS_XMP_RESOURCE_REF: ::String
|
106
|
+
|
107
|
+
XMP_NS_XMP_RIGHTS: ::String
|
108
|
+
|
109
|
+
XMP_NS_XMP_ST_JOB: ::String
|
110
|
+
|
111
|
+
XMP_NS_XMP_ST_VERSION: ::String
|
112
|
+
|
113
|
+
XMP_NS_XMP_T: ::String
|
114
|
+
|
115
|
+
XMP_NS_XMP_TEXT: ::String
|
116
|
+
|
117
|
+
XMP_NS_XMP_T_PG: ::String
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module XmpToolkitRuby
|
2
|
+
module XmpCharForm
|
3
|
+
# Returns the flags for a given bitmask.
|
4
|
+
def self.flags_for: (Integer bitmask) -> Array[Symbol]
|
5
|
+
|
6
|
+
# Returns the name for a given value.
|
7
|
+
def self.name_for: (Integer value) -> String
|
8
|
+
|
9
|
+
# Returns the value for a given name.
|
10
|
+
def self.value_for: (String name) -> Integer
|
11
|
+
|
12
|
+
# Mapping from character form names to their values.
|
13
|
+
CHAR_FORM: ::Hash[String, Integer]
|
14
|
+
|
15
|
+
# Mapping from character form values to their names.
|
16
|
+
CHAR_FORM_BY_VALUE: ::Hash[Integer, String]
|
17
|
+
end
|