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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b7aee0acc492647a8dba0533d35958329a0a3778b46d57788c351de5aeadfaa6
4
- data.tar.gz: 17a5b39e71708f9d4a6ce25a24e03451fe65bf929f2a46c8d57ac4c320c9c7ff
3
+ metadata.gz: 79c3338c7bfd4d7b6634b4a07791b21f970c2039e06522b3d465fdf83a114f3c
4
+ data.tar.gz: 96cba607570ff7fb55c712855bfd6b9696e94149bfc8f94b4b425d5cba474eed
5
5
  SHA512:
6
- metadata.gz: cdc48bafd443561d8e7b920dfa8a0c50b47f7704ec4e6bcf20c8a7258f65cb8329f8e66956e1aa38099646d26407369a78b18355b0af7b848885090168e67303
7
- data.tar.gz: 88658e87bbae1cf9e2f2d43a4a3c73772994c44ee847c3b5523b99e8568630e1b321976d2f8e67088c557231e586ceb88ca20fa7cce88c4f9b32430bdd7a3c84
6
+ metadata.gz: 705b287f7a933014e54080e6b2668201b4b2815fa1b8cb3d7dc53d13bf717e909627caa29be9fab739b8449b3f7271e9f5d96536c9193165362f94d9938d7eaf
7
+ data.tar.gz: 925a360cd3f4c733e632020d53fa8251837a62c60a2e3d560bdee3f698d67fe7e6a0174a5b9649499909d382dc905f5af26d67b3dc561274b3da1475e2d90171
data/.clang-format ADDED
@@ -0,0 +1,4 @@
1
+ BasedOnStyle: Google
2
+ SortIncludes: false
3
+ IndentWidth: 2
4
+ ColumnLimit: 120
data/CHANGELOG.md CHANGED
@@ -7,10 +7,42 @@ All notable changes to this project will be documented in this file.
7
7
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
8
8
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
9
9
 
10
- [unreleased]: https://github.com///compare/v0.0.1..HEAD
10
+ [unreleased]: https://github.com///compare/v0.0.2..HEAD
11
11
 
12
12
  <!-- generated by git-cliff end -->
13
13
 
14
+ ## [0.1.0] - 2025-06-11
15
+
16
+ ### ๐Ÿงช Tests
17
+
18
+ - Add XMP data extraction tests for various files
19
+
20
+ ### ๐Ÿ”„ Changed
21
+
22
+ - Added Multiple new Classes and Methods for XMP Metadata Handling
23
+
24
+ ### ๐Ÿš€ Added
25
+
26
+ - Add rbs signatures
27
+ - Add localized property retrieval method
28
+ - Add property retrieval method for XMP metadata
29
+ - Enhance XMP file handling with fallback options
30
+ - Enhance XMP file handling and error messaging
31
+ - Add update_meta method for XMP metadata
32
+ - Add metadata retrieval method
33
+ - Add character format mapping and lookup methods
34
+ - Add file_info and packet_info methods
35
+ - Add support for multiple XMP value types
36
+ - Enhance XMP file handling with fine-grained control
37
+ - Enable single property updates in XMP files
38
+ - Enhance SDK initialization with optional path
39
+ - Enhance SDK initialization and termination
40
+
41
+ ### ๐Ÿ“ Docs
42
+
43
+ - Add docs for simple property and localized property getters
44
+ - Enhance XmpFile class with detailed documentation
45
+
14
46
  ## [0.0.2] - 2025-06-06
15
47
 
16
48
  ### ๐Ÿ› Fixed
@@ -41,5 +73,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
41
73
 
42
74
  - Initial release
43
75
 
44
- - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.0.2..HEAD)
76
+ ## Changelog
77
+
78
+ - [unreleased](https://github.com/dieter-medium/bidi2pdf/compare/v0.1.0..HEAD)
79
+ - [0.1.0](https://github.com/dieter-medium/bidi2pdf/compare/v0.0.2..v0.1.0)
45
80
  - [0.0.2](https://github.com/dieter-medium/bidi2pdf/compare/v0.0.1..v0.0.2)
data/README.md CHANGED
@@ -214,6 +214,32 @@ XmpToolkitRuby.xmp_to_file(
214
214
  new_xmp,
215
215
  override: true # Set to false to upsert/merge instead of replacing
216
216
  )
217
+
218
+ # fine-grained control
219
+
220
+ # if you want to take full control over the SDK lifecycle
221
+ # you can use the XmpToolkitRuby::XmpToolkit module directly:
222
+ XmpToolkitRuby::XmpToolkit.initialize_xmp(XmpToolkitRuby::PLUGINS_PATH)
223
+ # in this case you can call here
224
+ XmpToolkitRuby::XmpFile.register_namespace XmpToolkitRuby::Namespaces::XMP_NS_PDFUA_ID, "pdfua"
225
+
226
+ XmpToolkitRuby::XmpFile.with_xmp_file(filename, open_flags: XmpToolkitRuby::XmpFileOpenFlags.bitmask_for(:open_for_update, :open_use_smart_handler, auto_terminate_toolkit: false)) do |xmp_file|
227
+ # in case the lifecycle is managed by with_xmp_file
228
+ XmpToolkitRuby::XmpFile.register_namespace XmpToolkitRuby::Namespaces::XMP_NS_PDFUA_ID, "pdfua"
229
+
230
+ xmp_file.update_localized_property schema_ns: XmpToolkitRuby::Namespaces::XMP_NS_DC,
231
+ alt_text_name: "title",
232
+ generic_lang: "en",
233
+ specific_lang: "en-US",
234
+ item_value: "Hello world",
235
+ options: 0
236
+
237
+ xmp_file.update_property XmpToolkitRuby::Namespaces::XMP_NS_PDFUA_ID, "part", "1"
238
+ end
239
+
240
+ # if auto_terminate_toolkit is false, you must call
241
+ XmpToolkitRuby::XmpToolkit.terminate
242
+ # to clean up resources
217
243
  ```
218
244
 
219
245
  If you built the XMP Toolkit yourself or store the plugins elsewhere,
@@ -227,6 +253,109 @@ export XMP_TOOLKIT_PLUGINS_PATH=/path/to/XMP-Toolkit-SDK/XMPFilesPlugins/PDF_Han
227
253
  Run your Ruby code or CLI commands in the same shell so this variable is
228
254
  visible.
229
255
 
256
+ #### Fine-grained Control with `XmpFile`
257
+
258
+ The `XmpToolkitRuby::XmpFile` class exposes low-level access to XMP metadata files, allowing you to:
259
+
260
+ - Register new or custom namespaces to extend XMP property support
261
+ - Open files with precise control over file open modes and handlers
262
+ - Update localized properties (e.g., multi-language text with alternatives)
263
+ - Update simple properties in specified namespaces
264
+ - Use blocks to ensure file handles are properly closed and changes saved
265
+
266
+ In order to work properly you need to ensure the XMP Toolkit is initialized:
267
+
268
+ ```ruby
269
+ XmpToolkitRuby::XmpToolkit.initialize_xmp(XmpToolkitRuby::PLUGINS_PATH)
270
+ ```
271
+
272
+ Alternatively, you can use the `with_xmp_file` method which automatically initializes and terminates the toolkit.
273
+
274
+ ##### Registering a Namespace
275
+
276
+ Before working with properties in custom or less-common namespaces, you need to register them with a prefix:
277
+
278
+ ```ruby
279
+ XmpToolkitRuby::XmpFile.register_namespace XmpToolkitRuby::Namespaces::XMP_NS_PDFUA_ID, "pdfua"
280
+ ```
281
+
282
+ This allows the SDK to recognize and correctly handle properties within that namespace.
283
+
284
+ ##### Opening a File with Custom Flags
285
+
286
+ You can open an XMP file with specific flags to control behavior:
287
+
288
+ - `open_for_update`: Open the file for modifying metadata
289
+ - `open_use_smart_handler`: Use smart handlers for better metadata processing
290
+
291
+ Flags are combined via the `bitmask_for` helper method:
292
+
293
+ ```ruby
294
+ flags = XmpToolkitRuby::XmpFileOpenFlags.bitmask_for(:open_for_update, :open_use_smart_handler)
295
+ ```
296
+
297
+ Open the file in a block to ensure safe usage:
298
+
299
+ ```ruby
300
+ XmpToolkitRuby::XmpFile.with_xmp_file(filename, open_flags: flags) do |xmp_file|
301
+ # work with xmp_file inside this block
302
+ end
303
+ ```
304
+
305
+ ##### Updating a Localized Property
306
+
307
+ Localized properties support multiple language alternatives. Use `update_localized_property` with options:
308
+
309
+ - `schema_ns`: Namespace URI for the schema (e.g., Dublin Core)
310
+ - `alt_text_name`: The property name that holds alternative texts
311
+ - `generic_lang`: Language tag for the generic language (e.g., `"en"`)
312
+ - `specific_lang`: Specific locale variant (e.g., `"en-US"`)
313
+ - `item_value`: The actual text value to set
314
+ - `options`: Optional flags (typically 0)
315
+
316
+ Example:
317
+
318
+ ```ruby
319
+ xmp_file.update_localized_property(
320
+ schema_ns: XmpToolkitRuby::Namespaces::XMP_NS_DC,
321
+ alt_text_name: "title",
322
+ generic_lang: "en",
323
+ specific_lang: "en-US",
324
+ item_value: "Hello world",
325
+ options: 0
326
+ )
327
+ ```
328
+
329
+ This sets or updates the localized `"title"` property in English (US).
330
+
331
+ ##### Updating a Simple Property
332
+
333
+ For simpler property updates without localization, use `update_property`:
334
+
335
+ ```ruby
336
+ xmp_file.update_property(
337
+ XmpToolkitRuby::Namespaces::XMP_NS_PDFUA_ID,
338
+ "part",
339
+ "1"
340
+ )
341
+ ```
342
+
343
+ This updates the `"part"` property within the PDF/UA ID namespace.
344
+
345
+ ---
346
+
347
+ ##### Summary
348
+
349
+ The fine-grained control API empowers you to work precisely with metadata:
350
+
351
+ - Register custom namespaces for extended support
352
+ - Open files with control over update modes and handlers
353
+ - Modify localized text properties with language variants
354
+ - Update simple XMP properties by namespace and name
355
+
356
+ Using these methods allows sophisticated metadata management, crucial for professional publishing, digital asset
357
+ management, or compliance workflows.
358
+
230
359
  ---
231
360
 
232
361
  ### CLI
@@ -285,3 +414,5 @@ Bug reports and pull requests are welcome on [GitHub](https://github.com/DieterS
285
414
  ## License
286
415
 
287
416
  This project is open source, licensed under the [MIT License](https://opensource.org/licenses/MIT).
417
+
418
+ ---
@@ -1,321 +1,154 @@
1
1
  #include "xmp_toolkit.hpp"
2
2
 
3
+ #include <mutex>
3
4
 
5
+ static std::mutex sdk_init_mutex;
6
+ static bool sdk_initialized = false;
7
+ static bool terminate_registered = false;
4
8
 
5
- bool xmp_meta_error_callback(
6
- void * clientContext,
7
- XMP_ErrorSeverity severity,
8
- XMP_Int32 cause,
9
- XMP_StringPtr message
10
- ) {
11
- const char * sevStr = (severity == kXMPErrSev_Recoverable) ? "RECOVERABLE"
12
- : (severity == kXMPErrSev_OperationFatal) ? "FATAL OPERATION"
13
- : (severity == kXMPErrSev_FileFatal) ? "FATAL FILE"
14
- : (severity == kXMPErrSev_ProcessFatal) ? "FATAL PROCESS"
15
- : "UNKNOWN";
16
- std::cerr << "[TXMPMeta " << sevStr << "] "
17
- << "Code=0x" << std::hex << cause << std::dec
18
- << " Msg=\"" << (message ? message : "(no detail)") << "\"\n";
19
-
20
- // If it's a recoverable error, return true so XMP can try to continue;
21
- // otherwise return false to force an exception back to the caller.
22
- return (severity == kXMPErrSev_Recoverable);
9
+ static void terminate_sdk_internal() {
10
+ std::lock_guard<std::mutex> guard(sdk_init_mutex);
11
+ if (sdk_initialized) {
12
+ SXMPFiles::Terminate();
13
+ SXMPMeta::Terminate();
14
+ sdk_initialized = false;
15
+ }
23
16
  }
24
17
 
25
- bool xmp_file_error_callback(
26
- void * clientContext,
27
- XMP_StringPtr filePath,
28
- XMP_ErrorSeverity severity,
29
- XMP_Int32 cause,
30
- XMP_StringPtr message){
31
-
32
- const char * sevStr = (severity == kXMPErrSev_Recoverable) ? "RECOVERABLE"
33
- : (severity == kXMPErrSev_OperationFatal) ? "FATAL OPERATION"
34
- : (severity == kXMPErrSev_FileFatal) ? "FATAL FILE"
35
- : (severity == kXMPErrSev_ProcessFatal) ? "FATAL PROCESS"
36
- : "UNKNOWN";
18
+ // Register termination at Ruby exit
19
+ static void register_terminate_at_exit() {
20
+ if (terminate_registered) {
21
+ return; // Already registered
22
+ }
37
23
 
38
- std::cerr << "[TXMPFiles " << sevStr << "] "
39
- << "file=\"" << (filePath ? filePath : "(null)") << "\" "
40
- << "cause=0x" << std::hex << cause << std::dec << "\n"
41
- << " msg=\"" << (message ? message : "(no detail)") << "\"\n";
24
+ terminate_registered = true;
42
25
 
43
- // Only attempt recovery if the error is marked recoverable
44
- return (severity == kXMPErrSev_Recoverable);
26
+ rb_set_end_proc([](VALUE) { terminate_sdk_internal(); }, Qnil);
45
27
  }
46
28
 
47
- VALUE write_xmp_to_file(int argc, VALUE* argv, VALUE self){
48
- VALUE rb_filename, rb_xmp_data, rb_mode_sym;
49
- XMP_OptionBits templateFlags = 0;
29
+ static void ensure_sdk_initialized(const char *path) {
30
+ std::lock_guard<std::mutex> guard(sdk_init_mutex);
50
31
 
51
- rb_scan_args(argc, argv, "3", &rb_filename, &rb_xmp_data, &rb_mode_sym);
32
+ if (sdk_initialized) {
33
+ return;
34
+ }
52
35
 
53
- Check_Type(rb_filename, T_STRING);
54
- const char* xmpString = NULL;
55
- if (!NIL_P(rb_xmp_data)) {
56
- Check_Type(rb_xmp_data, T_STRING);
57
- xmpString = StringValueCStr(rb_xmp_data);
36
+ try {
37
+ if (!SXMPMeta::Initialize()) {
38
+ rb_raise(rb_eRuntimeError, "Failed to initialize XMP Toolkit metadata");
39
+ return;
58
40
  }
59
41
 
60
- const char* mode_cstr;
61
- if (RB_TYPE_P(rb_mode_sym, T_SYMBOL)) {
62
- // Convert symbol to string first
63
- VALUE mode_str = rb_sym_to_s(rb_mode_sym);
64
- mode_cstr = StringValueCStr(mode_str);
65
- } else {
66
- // Already a string
67
- mode_cstr = StringValueCStr(rb_mode_sym);
68
- }
69
- bool override;
42
+ sdk_initialized = true;
43
+ register_terminate_at_exit();
44
+
45
+ XMP_OptionBits options = 0;
46
+ options |= kXMPFiles_ServerMode;
70
47
 
71
- if (strcmp(mode_cstr, "upsert") == 0) {
72
- templateFlags = kXMPTemplate_AddNewProperties
73
- | kXMPTemplate_ReplaceExistingProperties
74
- | kXMPTemplate_IncludeInternalProperties;
75
- override = false;
76
- } else if (strcmp(mode_cstr, "override") == 0) {
77
- override = true;
48
+ if (path) {
49
+ if (!SXMPFiles::Initialize(options, path)) {
50
+ rb_raise(rb_eRuntimeError, "Failed to initialize XMP Files with plugin path");
51
+ return;
52
+ }
78
53
  } else {
79
- rb_raise(rb_eArgError, "mode must be :upsert or :override (String or Symbol). Got '%s'", mode_cstr);
80
- return Qnil; // unreachable, but for clarity
54
+ if (!SXMPFiles::Initialize(options)) {
55
+ rb_raise(rb_eRuntimeError, "Failed to initialize XMP Files without plugin path");
56
+ return;
57
+ }
81
58
  }
82
59
 
60
+ return;
61
+ } catch (const XMP_Error &e) {
62
+ rb_raise(rb_eRuntimeError, "XMP Error during initialization: %s", e.GetErrMsg());
63
+ return;
64
+ } catch (const std::exception &e) {
65
+ rb_raise(rb_eRuntimeError, "C++ exception during initialization: %s", e.what());
66
+ return;
67
+ } catch (...) {
68
+ rb_raise(rb_eRuntimeError, "Unknown error during XMP initialization");
69
+ return;
70
+ }
71
+ }
83
72
 
84
- const char* fileName = StringValueCStr(rb_filename);
85
-
86
- bool ok;
87
- SXMPFiles xmpFile;
88
- SXMPMeta newMeta;
89
- SXMPMeta currentMeta;
90
- XMP_PacketInfo xmpPacket;
73
+ VALUE
74
+ is_sdk_initialized(VALUE self) {
75
+ std::lock_guard<std::mutex> guard(sdk_init_mutex);
76
+ return sdk_initialized ? Qtrue : Qfalse;
77
+ }
91
78
 
92
- if (xmpString != NULL) {
93
- int i;
94
- for (i = 0; i < (long)strlen(xmpString) - 10; i += 10){
95
- newMeta.ParseFromBuffer(&xmpString[i], 10, kXMP_ParseMoreBuffers);
96
- }
79
+ void ensure_sdk_initialized() {
80
+ // Look up XmpToolkitRuby::PLUGINS_PATH if defined
81
+ VALUE xmp_module = rb_const_get(rb_cObject, rb_intern("XmpToolkitRuby"));
82
+ if (rb_const_defined(xmp_module, rb_intern("PLUGINS_PATH"))) {
83
+ VALUE plugins_path = rb_const_get(xmp_module, rb_intern("PLUGINS_PATH"));
97
84
 
98
- newMeta.ParseFromBuffer(&xmpString[i], (XMP_StringLen)strlen(xmpString) - i);
85
+ if (TYPE(plugins_path) == T_STRING) {
86
+ const char *path = StringValueCStr(plugins_path);
87
+ ensure_sdk_initialized(path);
88
+ return;
99
89
  }
90
+ }
100
91
 
101
- try {
102
- XMP_OptionBits opts = kXMPFiles_OpenForUpdate | kXMPFiles_OpenUseSmartHandler;
103
- ok = xmpFile.OpenFile(fileName, kXMP_UnknownFile, opts);
104
- if (!ok) {
105
- xmpFile.CloseFile();
106
-
107
- opts = kXMPFiles_OpenForUpdate | kXMPFiles_OpenUsePacketScanning;
108
- ok = xmpFile.OpenFile( fileName, kXMP_UnknownFile, opts );
109
- if ( ! ok ) {
110
- // Neither smart handler nor packet scanning worked
111
- xmpFile.CloseFile();
112
- return Qnil;
113
- }
114
- }
115
-
116
- fprintf(stderr, "Opened file %s for update\n", fileName);
117
-
118
- ok = xmpFile.GetXMP(&currentMeta, 0, &xmpPacket);
119
- if (!ok) {
120
- xmpFile.CloseFile();
121
- return Qnil;
122
- }
123
-
124
- XMP_DateTime dt;
125
- SXMPUtils::CurrentDateTime(&dt);
126
- std::string nowStr;
127
- SXMPUtils::ConvertFromDate(dt, &nowStr);
128
-
129
- if (xmpString != NULL){
130
- newMeta.SetProperty(kXMP_NS_XMP, "MetadataDate", nowStr.c_str());
131
- }
132
-
133
- std::string newBuffer;
134
- if (override) {
135
-
136
- if (xmpFile.CanPutXMP(newMeta) ) {
137
- xmpFile.PutXMP(newMeta);
138
- } else {
139
- xmpFile.CloseFile();
140
-
141
- newMeta.SerializeToBuffer(&newBuffer);
142
-
143
- rb_raise(rb_eArgError, "Can't update XMP new Data: '%s'", newBuffer.c_str());
144
-
145
- return Qnil;
146
- }
147
- } else {
148
- SXMPUtils::ApplyTemplate(
149
- &currentMeta,
150
- newMeta,
151
- templateFlags
152
- );
153
-
154
- if (xmpFile.CanPutXMP(currentMeta) ) {
155
- xmpFile.PutXMP(currentMeta);
156
- } else {
157
- xmpFile.CloseFile();
158
-
159
- currentMeta.SerializeToBuffer(&newBuffer);
160
-
161
- rb_raise(rb_eArgError, "Can't update XMP new Data: '%s'", newBuffer.c_str());
162
-
163
- return Qnil;
164
- }
165
- }
166
-
167
-
168
-
92
+ ensure_sdk_initialized(nullptr);
169
93
 
170
- xmpFile.CloseFile();
171
- }
172
- catch (XMP_Error& e) {
173
- xmpFile.CloseFile();
174
- rb_raise(rb_eRuntimeError, "XMP Error: %s", e.GetErrMsg());
175
- return Qnil;
176
- }
177
- catch (...) {
178
- xmpFile.CloseFile();
179
- rb_raise(rb_eRuntimeError, "Unknown error processing XMP file");
180
- return Qnil;
181
- }
182
-
183
- return Qnil;
94
+ return;
184
95
  }
185
96
 
186
- VALUE get_xmp_from_file(VALUE self, VALUE rb_filename)
187
- {
188
- Check_Type(rb_filename, T_STRING);
189
- const char* fileName = StringValueCStr(rb_filename);
190
-
191
- bool ok;
192
- SXMPMeta xmpMeta;
193
- SXMPFiles xmpFile;
194
- XMP_FileFormat format;
195
- XMP_OptionBits openFlags, handlerFlags;
196
- XMP_PacketInfo xmpPacket;
197
-
198
- try {
199
- XMP_OptionBits opts = kXMPFiles_OpenForRead | kXMPFiles_OpenUseSmartHandler;
200
- ok = xmpFile.OpenFile(fileName, kXMP_UnknownFile, opts);
201
- if (!ok) {
202
- xmpFile.CloseFile();
203
-
204
- opts = kXMPFiles_OpenForRead | kXMPFiles_OpenUsePacketScanning;
205
- ok = xmpFile.OpenFile( fileName, kXMP_UnknownFile, opts );
206
- if ( ! ok ) {
207
- // Neither smart handler nor packet scanning worked
208
- xmpFile.CloseFile();
209
- return Qnil;
210
- }
211
- }
212
-
213
- ok = xmpFile.GetFileInfo(0, &openFlags, &format, &handlerFlags);
214
- if (!ok) {
215
- xmpFile.CloseFile();
216
- return Qnil;
217
- }
218
-
219
- ok = xmpFile.GetXMP(&xmpMeta, 0, &xmpPacket);
220
- if (!ok) {
221
- xmpFile.CloseFile();
222
- return Qnil;
223
- }
224
-
225
- VALUE result = rb_hash_new();
226
-
227
- rb_hash_aset(result,
228
- rb_str_new_cstr("format"),
229
- UINT2NUM(format));
230
- rb_hash_aset(result,
231
- rb_str_new_cstr("handler_flags"),
232
- UINT2NUM(handlerFlags));
233
- rb_hash_aset(result,
234
- rb_str_new_cstr("offset"),
235
- LONG2NUM(xmpPacket.offset));
236
- rb_hash_aset(result,
237
- rb_str_new_cstr("length"),
238
- LONG2NUM(xmpPacket.length));
239
-
240
- std::string xmpString;
241
- xmpMeta.SerializeToBuffer(&xmpString);
242
- rb_hash_aset(result,
243
- rb_str_new_cstr("xmp_data"),
244
- rb_str_new_cstr(xmpString.c_str()));
97
+ bool xmp_meta_error_callback(void *clientContext, XMP_ErrorSeverity severity, XMP_Int32 cause, XMP_StringPtr message) {
98
+ const char *sevStr = (severity == kXMPErrSev_Recoverable) ? "RECOVERABLE"
99
+ : (severity == kXMPErrSev_OperationFatal) ? "FATAL OPERATION"
100
+ : (severity == kXMPErrSev_FileFatal) ? "FATAL FILE"
101
+ : (severity == kXMPErrSev_ProcessFatal) ? "FATAL PROCESS"
102
+ : "UNKNOWN";
103
+ std::cerr << "[TXMPMeta " << sevStr << "] "
104
+ << "Code=0x" << std::hex << cause << std::dec << " Msg=\"" << (message ? message : "(no detail)")
105
+ << "\"\n";
106
+
107
+ // If it's a recoverable error, return true so XMP can try to continue;
108
+ // otherwise return false to force an exception back to the caller.
109
+ return (severity == kXMPErrSev_Recoverable);
110
+ }
245
111
 
246
- xmpFile.CloseFile();
247
- return result;
248
- }
249
- catch (XMP_Error& e) {
250
- xmpFile.CloseFile();
251
- rb_raise(rb_eRuntimeError, "XMP Error: %s", e.GetErrMsg());
252
- return Qnil;
253
- }
254
- catch (...) {
255
- xmpFile.CloseFile();
256
- rb_raise(rb_eRuntimeError, "Unknown error processing XMP file");
257
- return Qnil;
258
- }
112
+ bool xmp_file_error_callback(void *clientContext, XMP_StringPtr filePath, XMP_ErrorSeverity severity, XMP_Int32 cause,
113
+ XMP_StringPtr message) {
114
+ const char *sevStr = (severity == kXMPErrSev_Recoverable) ? "RECOVERABLE"
115
+ : (severity == kXMPErrSev_OperationFatal) ? "FATAL OPERATION"
116
+ : (severity == kXMPErrSev_FileFatal) ? "FATAL FILE"
117
+ : (severity == kXMPErrSev_ProcessFatal) ? "FATAL PROCESS"
118
+ : "UNKNOWN";
119
+
120
+ std::cerr << "[TXMPFiles " << sevStr << "] "
121
+ << "file=\"" << (filePath ? filePath : "(null)") << "\" "
122
+ << "cause=0x" << std::hex << cause << std::dec << "\n"
123
+ << " msg=\"" << (message ? message : "(no detail)") << "\"\n";
124
+
125
+ // Only attempt recovery if the error is marked recoverable
126
+ return (severity == kXMPErrSev_Recoverable);
259
127
  }
260
128
 
261
- // xmp_initialize(self, filename)
129
+ // xmp_initialize(self)
262
130
  // Initialize the XMP Toolkit and SXMPFiles with an optional PLUGINS_PATH
263
- VALUE xmp_initialize(VALUE self)
264
- {
265
- try {
266
- if (!SXMPMeta::Initialize()) {
267
- rb_raise(rb_eRuntimeError, "Failed to initialize XMP Toolkit metadata");
268
- return Qnil;
269
- }
131
+ VALUE
132
+ xmp_initialize(int argc, VALUE *argv, VALUE self) {
133
+ VALUE rb_path_arg = Qnil;
270
134
 
271
- // Look up XmpToolkitRuby::PLUGINS_PATH if defined
272
- VALUE xmp_module = rb_const_get(rb_cObject, rb_intern("XmpToolkitRuby"));
273
- if (rb_const_defined(xmp_module, rb_intern("PLUGINS_PATH"))) {
274
- VALUE plugins_path = rb_const_get(xmp_module, rb_intern("PLUGINS_PATH"));
135
+ rb_scan_args(argc, argv, "01", &rb_path_arg);
275
136
 
276
- if (TYPE(plugins_path) == T_STRING) {
277
- const char* path = StringValueCStr(plugins_path);
278
- XMP_OptionBits options = 0;
279
- options |= kXMPFiles_ServerMode;
137
+ if (rb_path_arg != Qnil) {
138
+ Check_Type(rb_path_arg, T_STRING);
139
+ const char *path = StringValueCStr(rb_path_arg);
280
140
 
281
- if (!SXMPFiles::Initialize(options, path)) {
282
- rb_raise(rb_eRuntimeError, "Failed to initialize XMP Files with plugin path");
283
- return Qnil;
284
- }
285
- }
286
- else {
287
- if (!SXMPFiles::Initialize()) {
288
- rb_raise(rb_eRuntimeError, "Failed to initialize XMP Files without plugin path");
289
- return Qnil;
290
- }
291
- }
292
- }
293
- else {
294
- if (!SXMPFiles::Initialize()) {
295
- rb_raise(rb_eRuntimeError, "Failed to initialize XMP Files (PLUGINS_PATH not defined)");
296
- return Qnil;
297
- }
298
- }
141
+ ensure_sdk_initialized(path);
299
142
 
300
- return Qnil;
301
- }
302
- catch (const XMP_Error& e) {
303
- rb_raise(rb_eRuntimeError, "XMP Error during initialization: %s", e.GetErrMsg());
304
- return Qnil;
305
- }
306
- catch (const std::exception& e) {
307
- rb_raise(rb_eRuntimeError, "C++ exception during initialization: %s", e.what());
308
- return Qnil;
309
- }
310
- catch (...) {
311
- rb_raise(rb_eRuntimeError, "Unknown error during XMP initialization");
312
- return Qnil;
313
- }
143
+ return Qnil;
144
+ }
145
+
146
+ ensure_sdk_initialized();
147
+ return Qnil;
314
148
  }
315
149
 
316
- VALUE xmp_terminate(VALUE self)
317
- {
318
- SXMPFiles::Terminate();
319
- SXMPMeta::Terminate();
320
- return Qnil;
150
+ VALUE
151
+ xmp_terminate(VALUE self) {
152
+ terminate_sdk_internal();
153
+ return Qnil;
321
154
  }