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.
@@ -0,0 +1,582 @@
1
+ #include "xmp_toolkit.hpp"
2
+ #include "xmp_wrapper.hpp"
3
+
4
+ #include <mutex>
5
+
6
+ static size_t xmpwrapper_memsize(const void *ptr) { return sizeof(XMPWrapper); }
7
+
8
+ static void check_wrapper_initialized(XMPWrapper *wrapper) {
9
+ if (wrapper->xmpFile == nullptr || wrapper->xmpMeta == nullptr || wrapper->xmpPacket == nullptr) {
10
+ rb_raise(rb_eRuntimeError, "XMP file or metadata not initialized or file not opened");
11
+ }
12
+ }
13
+
14
+ static void clean_wrapper(XMPWrapper *wrapper) {
15
+ if (wrapper->xmpFile) {
16
+ wrapper->xmpFile->CloseFile();
17
+ delete wrapper->xmpFile;
18
+ wrapper->xmpFile = nullptr;
19
+ }
20
+ if (wrapper->xmpMeta) {
21
+ delete wrapper->xmpMeta;
22
+ wrapper->xmpMeta = nullptr;
23
+ }
24
+ if (wrapper->xmpPacket) {
25
+ delete wrapper->xmpPacket;
26
+ wrapper->xmpPacket = nullptr;
27
+ }
28
+
29
+ wrapper->xmpMetaDataLoaded = false;
30
+ }
31
+
32
+ static void xmpwrapper_free(void *ptr) {
33
+ XMPWrapper *wrapper = static_cast<XMPWrapper *>(ptr);
34
+ if (wrapper) {
35
+ clean_wrapper(wrapper);
36
+
37
+ delete wrapper;
38
+ }
39
+ }
40
+
41
+ static const rb_data_type_t xmpwrapper_data_type = {"XMPWrapper",
42
+ {
43
+ 0,
44
+ xmpwrapper_free,
45
+ xmpwrapper_memsize,
46
+ },
47
+ 0,
48
+ 0,
49
+ RUBY_TYPED_FREE_IMMEDIATELY};
50
+
51
+ VALUE
52
+ xmpwrapper_allocate(VALUE klass) {
53
+ XMPWrapper *wrapper = new XMPWrapper();
54
+ wrapper->xmpMeta = nullptr;
55
+ wrapper->xmpFile = nullptr;
56
+ wrapper->xmpPacket = nullptr;
57
+ wrapper->xmpMetaDataLoaded = false;
58
+ return TypedData_Wrap_Struct(klass, &xmpwrapper_data_type, wrapper);
59
+ }
60
+
61
+ static void get_xmp(XMPWrapper *wrapper) {
62
+ if (wrapper->xmpMetaDataLoaded) {
63
+ return;
64
+ }
65
+
66
+ check_wrapper_initialized(wrapper);
67
+
68
+ bool ok = wrapper->xmpFile->GetXMP(wrapper->xmpMeta, 0, wrapper->xmpPacket);
69
+
70
+ if (!ok) {
71
+ clean_wrapper(wrapper);
72
+ rb_raise(rb_eRuntimeError, "Failed to get XMP metadata");
73
+ }
74
+
75
+ wrapper->xmpMetaDataLoaded = true;
76
+ }
77
+
78
+ VALUE
79
+ xmpwrapper_open_file(int argc, VALUE *argv, VALUE self) {
80
+ ensure_sdk_initialized();
81
+
82
+ XMPWrapper *wrapper;
83
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
84
+
85
+ if (wrapper->xmpFile != nullptr) {
86
+ rb_raise(rb_eRuntimeError, "File already opened");
87
+ }
88
+
89
+ VALUE rb_filename = Qnil;
90
+ VALUE rb_opts_mask = Qnil;
91
+ rb_scan_args(argc, argv, "11", &rb_filename, &rb_opts_mask);
92
+
93
+ const char *filename = StringValueCStr(rb_filename);
94
+
95
+ // Allocate native objects
96
+ wrapper->xmpMeta = new SXMPMeta();
97
+ wrapper->xmpFile = new SXMPFiles();
98
+ wrapper->xmpPacket = new XMP_PacketInfo();
99
+
100
+ XMP_OptionBits opts;
101
+
102
+ if (!NIL_P(rb_opts_mask)) {
103
+ Check_Type(rb_opts_mask, T_FIXNUM);
104
+ opts = NUM2UINT(rb_opts_mask);
105
+ } else {
106
+ opts = kXMPFiles_OpenForRead | kXMPFiles_OpenUseSmartHandler;
107
+ }
108
+
109
+ bool ok = wrapper->xmpFile->OpenFile(filename, kXMP_UnknownFile, opts);
110
+ if (!ok) {
111
+ clean_wrapper(wrapper);
112
+ rb_raise(rb_eIOError, "Failed to open file %s, try open_use_packet_scanning instead of open_use_smart_handler",
113
+ filename);
114
+ }
115
+
116
+ return Qtrue;
117
+ }
118
+
119
+ VALUE
120
+ xmp_file_info(VALUE self) {
121
+ XMPWrapper *wrapper;
122
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
123
+ check_wrapper_initialized(wrapper);
124
+
125
+ XMP_FileFormat format;
126
+ XMP_OptionBits openFlags, handlerFlags;
127
+ bool ok = wrapper->xmpFile->GetFileInfo(0, &openFlags, &format, &handlerFlags);
128
+ if (!ok) {
129
+ clean_wrapper(wrapper);
130
+ rb_raise(rb_eRuntimeError, "Failed to get file info");
131
+ return Qnil;
132
+ }
133
+
134
+ VALUE result = rb_hash_new();
135
+
136
+ rb_hash_aset(result, rb_str_new_cstr("format"), UINT2NUM(format));
137
+ rb_hash_aset(result, rb_str_new_cstr("handler_flags"), UINT2NUM(handlerFlags));
138
+ rb_hash_aset(result, rb_str_new_cstr("open_flags"), UINT2NUM(openFlags));
139
+
140
+ return result;
141
+ }
142
+
143
+ VALUE xmp_packet_info(VALUE self) {
144
+ XMPWrapper *wrapper;
145
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
146
+ check_wrapper_initialized(wrapper);
147
+
148
+ get_xmp(wrapper);
149
+
150
+ if (!wrapper->xmpMetaDataLoaded) {
151
+ rb_raise(rb_eRuntimeError, "No XMP metadata loaded");
152
+ }
153
+
154
+ VALUE result = rb_hash_new();
155
+
156
+ rb_hash_aset(result, rb_str_new_cstr("offset"), LONG2NUM(wrapper->xmpPacket->offset));
157
+ rb_hash_aset(result, rb_str_new_cstr("length"), LONG2NUM(wrapper->xmpPacket->length));
158
+ rb_hash_aset(result, rb_str_new_cstr("pad_size"), LONG2NUM(wrapper->xmpPacket->padSize));
159
+
160
+ rb_hash_aset(result, rb_str_new_cstr("char_form"), UINT2NUM(wrapper->xmpPacket->charForm));
161
+ rb_hash_aset(result, rb_str_new_cstr("writeable"), wrapper->xmpPacket->writeable ? Qtrue : Qfalse);
162
+ rb_hash_aset(result, rb_str_new_cstr("has_wrapper"), wrapper->xmpPacket->hasWrapper ? Qtrue : Qfalse);
163
+ rb_hash_aset(result, rb_str_new_cstr("pad"), UINT2NUM(wrapper->xmpPacket->pad));
164
+
165
+ return result;
166
+ }
167
+
168
+ VALUE
169
+ xmp_meta(VALUE self) {
170
+ XMPWrapper *wrapper;
171
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
172
+ check_wrapper_initialized(wrapper);
173
+
174
+ get_xmp(wrapper);
175
+
176
+ if (!wrapper->xmpMetaDataLoaded) {
177
+ rb_raise(rb_eRuntimeError, "No XMP metadata loaded");
178
+ }
179
+
180
+ std::string xmpString;
181
+ wrapper->xmpMeta->SerializeToBuffer(&xmpString);
182
+
183
+ VALUE rb_xmp_data = rb_str_new_cstr(xmpString.c_str());
184
+
185
+ return rb_xmp_data;
186
+ }
187
+
188
+ VALUE
189
+ xmpwrapper_get_property(VALUE self, VALUE rb_ns, VALUE rb_prop) {
190
+ XMPWrapper *wrapper;
191
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
192
+ check_wrapper_initialized(wrapper);
193
+
194
+ Check_Type(rb_ns, T_STRING);
195
+ Check_Type(rb_prop, T_STRING);
196
+
197
+ get_xmp(wrapper);
198
+
199
+ if (!wrapper->xmpMetaDataLoaded) {
200
+ rb_raise(rb_eRuntimeError, "No XMP metadata loaded");
201
+ }
202
+
203
+ const char *ns = StringValueCStr(rb_ns);
204
+ const char *prop = StringValueCStr(rb_prop);
205
+
206
+ std::string property_value;
207
+ XMP_OptionBits options;
208
+ bool property_exists = wrapper->xmpMeta->GetProperty(ns, prop, &property_value, &options);
209
+
210
+ VALUE result = rb_hash_new();
211
+ rb_hash_aset(result, rb_str_new_cstr("options"), UINT2NUM(options));
212
+ rb_hash_aset(result, rb_str_new_cstr("exists"), property_exists ? Qtrue : Qfalse);
213
+ rb_hash_aset(result, rb_str_new_cstr("value"), rb_str_new_cstr(property_value.c_str()));
214
+
215
+ return result;
216
+ }
217
+
218
+ VALUE
219
+ xmpwrapper_get_localized_text(int argc, VALUE *argv, VALUE self) {
220
+ XMPWrapper *wrapper;
221
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
222
+ check_wrapper_initialized(wrapper);
223
+
224
+ get_xmp(wrapper);
225
+
226
+ if (!wrapper->xmpMetaDataLoaded) {
227
+ rb_raise(rb_eRuntimeError, "No XMP metadata loaded");
228
+ }
229
+
230
+ VALUE kwargs;
231
+ rb_scan_args(argc, argv, ":", &kwargs);
232
+
233
+ // Define allowed keywords
234
+ ID kw_table[4];
235
+
236
+ kw_table[0] = rb_intern("schema_ns");
237
+ kw_table[1] = rb_intern("alt_text_name");
238
+ kw_table[2] = rb_intern("generic_lang");
239
+ kw_table[3] = rb_intern("specific_lang");
240
+
241
+ VALUE kw_values[4];
242
+ kw_values[2] = rb_str_new_cstr(""); // Default for generic_lang
243
+
244
+ rb_get_kwargs(kwargs, kw_table, 3, 1, kw_values);
245
+
246
+ VALUE schema_ns = kw_values[0];
247
+ VALUE alt_text_name = kw_values[1];
248
+ VALUE generic_lang = kw_values[2]; // Will be default if not provided
249
+ VALUE specific_lang = kw_values[3];
250
+
251
+ if (NIL_P(generic_lang)) generic_lang = rb_str_new_cstr("");
252
+
253
+ const char *c_schema_ns = StringValueCStr(schema_ns);
254
+ const char *c_alt_text_name = StringValueCStr(alt_text_name);
255
+ const char *c_generic_lang = StringValueCStr(generic_lang);
256
+ const char *c_specific_lang = StringValueCStr(specific_lang);
257
+
258
+ std::string actual_lang;
259
+ std::string item_value;
260
+ XMP_OptionBits options;
261
+
262
+ bool array_items_exists = wrapper->xmpMeta->GetLocalizedText(c_schema_ns, c_alt_text_name, c_generic_lang,
263
+ c_specific_lang, &actual_lang, &item_value, &options);
264
+
265
+ VALUE result = rb_hash_new();
266
+ rb_hash_aset(result, rb_str_new_cstr("options"), UINT2NUM(options));
267
+ rb_hash_aset(result, rb_str_new_cstr("exists"), array_items_exists ? Qtrue : Qfalse);
268
+ rb_hash_aset(result, rb_str_new_cstr("value"), rb_str_new_cstr(item_value.c_str()));
269
+ rb_hash_aset(result, rb_str_new_cstr("actual_lang"), rb_str_new_cstr(actual_lang.c_str()));
270
+
271
+ return result;
272
+ }
273
+
274
+ static XMP_DateTime datetime_to_xmp(VALUE rb_value) {
275
+ XMP_DateTime dt;
276
+
277
+ VALUE cDateTime = rb_const_get(rb_cObject, rb_intern("DateTime"));
278
+
279
+ if (!rb_obj_is_kind_of(rb_value, cDateTime)) {
280
+ rb_raise(rb_eTypeError, "expected a DateTime");
281
+ }
282
+
283
+ dt.year = NUM2INT(rb_funcall(rb_value, rb_intern("year"), 0));
284
+ dt.month = NUM2INT(rb_funcall(rb_value, rb_intern("month"), 0));
285
+ dt.day = NUM2INT(rb_funcall(rb_value, rb_intern("day"), 0));
286
+ dt.hour = NUM2INT(rb_funcall(rb_value, rb_intern("hour"), 0));
287
+ dt.minute = NUM2INT(rb_funcall(rb_value, rb_intern("minute"), 0));
288
+ dt.second = NUM2INT(rb_funcall(rb_value, rb_intern("second"), 0));
289
+
290
+ VALUE offset_r = rb_funcall(rb_value, rb_intern("offset"), 0);
291
+ VALUE num_r = rb_funcall(offset_r, rb_intern("numerator"), 0);
292
+ VALUE den_r = rb_funcall(offset_r, rb_intern("denominator"), 0);
293
+ long num = NUM2LONG(num_r);
294
+ long den = NUM2LONG(den_r);
295
+
296
+ // offset in minutes = (num/den) days * 24h * 60m
297
+ long off_minutes = (num * 24 * 60) / den;
298
+ long abs_off_minutes = llabs(off_minutes);
299
+
300
+ if (off_minutes == 0)
301
+ dt.tzSign = kXMP_TimeIsUTC;
302
+ else if (off_minutes > 0)
303
+ dt.tzSign = kXMP_TimeEastOfUTC;
304
+ else
305
+ dt.tzSign = kXMP_TimeWestOfUTC;
306
+
307
+ dt.tzHour = (XMP_Int32)(abs_off_minutes / 60);
308
+ dt.tzMinute = (XMP_Int32)(abs_off_minutes % 60);
309
+
310
+ return dt;
311
+ }
312
+
313
+ VALUE
314
+ xmpwrapper_set_meta(int argc, VALUE *argv, VALUE self) {
315
+ XMPWrapper *wrapper;
316
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
317
+ check_wrapper_initialized(wrapper);
318
+
319
+ VALUE rb_xmp_data, kwargs;
320
+ XMP_OptionBits templateFlags =
321
+ kXMPTemplate_AddNewProperties | kXMPTemplate_ReplaceExistingProperties | kXMPTemplate_IncludeInternalProperties;
322
+
323
+ rb_scan_args(argc, argv, "1:", &rb_xmp_data, &kwargs);
324
+
325
+ const char *xmpString = NULL;
326
+ if (!NIL_P(rb_xmp_data)) {
327
+ Check_Type(rb_xmp_data, T_STRING);
328
+ xmpString = StringValueCStr(rb_xmp_data);
329
+ }
330
+
331
+ ID kw_table[1];
332
+ kw_table[0] = rb_intern("mode");
333
+
334
+ VALUE kw_values[1];
335
+ kw_values[0] = rb_str_new_cstr("upsert");
336
+
337
+ rb_get_kwargs(kwargs, kw_table, 0, 1, kw_values);
338
+
339
+ VALUE rb_mode_sym = kw_values[0];
340
+
341
+ const char *mode_cstr;
342
+ if (RB_TYPE_P(rb_mode_sym, T_SYMBOL)) {
343
+ // Convert symbol to string first
344
+ VALUE mode_str = rb_sym_to_s(rb_mode_sym);
345
+ mode_cstr = StringValueCStr(mode_str);
346
+ } else {
347
+ // Already a string
348
+ mode_cstr = StringValueCStr(rb_mode_sym);
349
+ }
350
+
351
+ bool override;
352
+ if (strcmp(mode_cstr, "upsert") == 0) {
353
+ override = false;
354
+ } else if (strcmp(mode_cstr, "override") == 0) {
355
+ override = true;
356
+ } else {
357
+ rb_raise(rb_eArgError, "mode must be :upsert or :override (String or Symbol). Got '%s'", mode_cstr);
358
+ return Qnil; // unreachable, but for clarity
359
+ }
360
+
361
+ get_xmp(wrapper);
362
+
363
+ SXMPMeta newMeta;
364
+
365
+ if (xmpString != NULL) {
366
+ int i;
367
+ for (i = 0; i < (long)strlen(xmpString) - 10; i += 10) {
368
+ newMeta.ParseFromBuffer(&xmpString[i], 10, kXMP_ParseMoreBuffers);
369
+ }
370
+
371
+ newMeta.ParseFromBuffer(&xmpString[i], (XMP_StringLen)strlen(xmpString) - i);
372
+ }
373
+
374
+ XMP_DateTime dt;
375
+ SXMPUtils::CurrentDateTime(&dt);
376
+
377
+ if (xmpString != NULL) {
378
+ newMeta.SetProperty_Date(kXMP_NS_XMP, "MetadataDate", dt, 0);
379
+ }
380
+
381
+ if (override) {
382
+ wrapper->xmpMeta->Erase();
383
+ }
384
+
385
+ SXMPUtils::ApplyTemplate(wrapper->xmpMeta, newMeta, templateFlags);
386
+
387
+ return Qnil;
388
+ }
389
+
390
+ VALUE
391
+ xmpwrapper_set_property(VALUE self, VALUE rb_ns, VALUE rb_prop, VALUE rb_value) {
392
+ XMPWrapper *wrapper;
393
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
394
+ check_wrapper_initialized(wrapper);
395
+
396
+ Check_Type(rb_ns, T_STRING);
397
+ Check_Type(rb_prop, T_STRING);
398
+
399
+ get_xmp(wrapper);
400
+
401
+ if (!wrapper->xmpMetaDataLoaded) {
402
+ rb_raise(rb_eRuntimeError, "No XMP metadata loaded");
403
+ }
404
+
405
+ VALUE mXmpToolkitRuby = rb_const_get(rb_cObject, rb_intern("XmpToolkitRuby"));
406
+ VALUE cXmpValue = rb_const_get(mXmpToolkitRuby, rb_intern("XmpValue"));
407
+
408
+ const char *ns = StringValueCStr(rb_ns);
409
+ const char *prop = StringValueCStr(rb_prop);
410
+
411
+ if (rb_obj_is_kind_of(rb_value, cXmpValue)) {
412
+ VALUE rb_inner_val = rb_funcall(rb_value, rb_intern("value"), 0);
413
+ VALUE rb_type_val = rb_funcall(rb_value, rb_intern("type"), 0);
414
+
415
+ const char *type_str;
416
+ if (RB_TYPE_P(rb_type_val, T_SYMBOL)) {
417
+ VALUE mode_str = rb_sym_to_s(rb_type_val);
418
+ type_str = StringValueCStr(mode_str);
419
+ } else {
420
+ type_str = StringValueCStr(rb_type_val);
421
+ }
422
+
423
+ if (strcmp(type_str, "string") == 0) {
424
+ Check_Type(rb_inner_val, T_STRING);
425
+ wrapper->xmpMeta->SetProperty(ns, prop, StringValueCStr(rb_inner_val), 0);
426
+ return Qtrue;
427
+ } else if (strcmp(type_str, "int") == 0) {
428
+ Check_Type(rb_inner_val, T_FIXNUM);
429
+ wrapper->xmpMeta->SetProperty_Int(ns, prop, NUM2INT(rb_inner_val), 0);
430
+ return Qtrue;
431
+ } else if (strcmp(type_str, "int64") == 0) {
432
+ Check_Type(rb_inner_val, T_FIXNUM);
433
+ wrapper->xmpMeta->SetProperty_Int64(ns, prop, NUM2LL(rb_inner_val), 0);
434
+ return Qtrue;
435
+ } else if (strcmp(type_str, "float") == 0) {
436
+ Check_Type(rb_inner_val, T_FLOAT);
437
+ wrapper->xmpMeta->SetProperty_Float(ns, prop, NUM2DBL(rb_inner_val), 0);
438
+ return Qtrue;
439
+ } else if (strcmp(type_str, "bool") == 0) {
440
+ wrapper->xmpMeta->SetProperty_Bool(ns, prop, RTEST(rb_inner_val), 0);
441
+ return Qtrue;
442
+ } else if (strcmp(type_str, "date") == 0) {
443
+ wrapper->xmpMeta->SetProperty_Date(ns, prop, datetime_to_xmp(rb_inner_val), 0);
444
+ return Qtrue;
445
+ }
446
+ }
447
+
448
+ const char *val = StringValueCStr(rb_value);
449
+
450
+ try {
451
+ wrapper->xmpMeta->SetProperty(ns, prop, val, 0);
452
+ } catch (...) {
453
+ rb_raise(rb_eRuntimeError, "Failed to set XMP property");
454
+ }
455
+
456
+ return Qtrue;
457
+ }
458
+
459
+ // GetFileInfo() retrieves basic information about an opened file. to be defined
460
+
461
+ VALUE
462
+ xmpwrapper_update_localized_text(int argc, VALUE *argv, VALUE self) {
463
+ XMPWrapper *wrapper;
464
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
465
+ check_wrapper_initialized(wrapper);
466
+
467
+ get_xmp(wrapper);
468
+
469
+ if (!wrapper->xmpMetaDataLoaded) {
470
+ rb_raise(rb_eRuntimeError, "No XMP metadata loaded");
471
+ }
472
+
473
+ VALUE kwargs;
474
+ rb_scan_args(argc, argv, ":", &kwargs);
475
+
476
+ // Define allowed keywords
477
+ ID kw_table[6];
478
+
479
+ kw_table[0] = rb_intern("schema_ns");
480
+ kw_table[1] = rb_intern("alt_text_name");
481
+ kw_table[2] = rb_intern("generic_lang");
482
+ kw_table[3] = rb_intern("specific_lang");
483
+ kw_table[4] = rb_intern("item_value");
484
+ kw_table[5] = rb_intern("options");
485
+
486
+ VALUE kw_values[6];
487
+ kw_values[2] = rb_str_new_cstr(""); // Default for generic_lang
488
+ kw_values[5] = INT2NUM(0); // Default for options
489
+
490
+ // Extract keywords from kwargs hash
491
+ rb_get_kwargs(kwargs, kw_table, 4, 2, kw_values);
492
+
493
+ VALUE schema_ns = kw_values[0];
494
+ VALUE alt_text_name = kw_values[1];
495
+ VALUE generic_lang = kw_values[2]; // Will be default if not provided
496
+ VALUE specific_lang = kw_values[3];
497
+ VALUE item_value = kw_values[4];
498
+ VALUE options = kw_values[5]; // Will be default if not provided
499
+
500
+ // Provide defaults if needed
501
+ if (NIL_P(generic_lang)) generic_lang = rb_str_new_cstr("");
502
+ if (NIL_P(options)) options = INT2NUM(0);
503
+
504
+ // Convert Ruby values to C strings / types
505
+ const char *c_schema_ns = StringValueCStr(schema_ns);
506
+ const char *c_alt_text_name = StringValueCStr(alt_text_name);
507
+ const char *c_generic_lang = StringValueCStr(generic_lang);
508
+ const char *c_specific_lang = StringValueCStr(specific_lang);
509
+ const char *c_item_value = StringValueCStr(item_value);
510
+ XMP_OptionBits c_options = NUM2UINT(options);
511
+
512
+ fprintf(stderr,
513
+ "Updating localized text with schema_ns='%s', alt_text_name='%s', generic_lang='%s', specific_lang='%s', "
514
+ "item_value='%s', options=%u\n",
515
+ c_schema_ns, c_alt_text_name, c_generic_lang, c_specific_lang, c_item_value, c_options);
516
+
517
+ wrapper->xmpMeta->SetLocalizedText(c_schema_ns, c_alt_text_name, c_generic_lang, c_specific_lang,
518
+ std::string(c_item_value), c_options);
519
+
520
+ return Qtrue;
521
+ }
522
+
523
+ VALUE
524
+ register_namespace(VALUE self, VALUE rb_namespaceURI, VALUE rb_suggestedPrefix) {
525
+ const char *namespaceURI = StringValueCStr(rb_namespaceURI);
526
+ const char *suggestedPrefix = StringValueCStr(rb_suggestedPrefix);
527
+ std::string registeredPrefix;
528
+
529
+ ensure_sdk_initialized();
530
+
531
+ bool isRegistered = SXMPMeta::GetNamespacePrefix(namespaceURI, &registeredPrefix);
532
+
533
+ if (isRegistered) {
534
+ fprintf(stderr, "Namespace '%s' is already registered with prefix '%s'\n", namespaceURI, registeredPrefix.c_str());
535
+ return rb_str_new_cstr(registeredPrefix.c_str());
536
+ }
537
+
538
+ bool isSuggestedPrefix = SXMPMeta::RegisterNamespace(namespaceURI, suggestedPrefix, &registeredPrefix);
539
+
540
+ if (isSuggestedPrefix) {
541
+ return rb_suggestedPrefix;
542
+ }
543
+
544
+ return rb_str_new_cstr(registeredPrefix.c_str());
545
+ }
546
+
547
+ VALUE
548
+ write_xmp(VALUE self) {
549
+ XMPWrapper *wrapper;
550
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
551
+ check_wrapper_initialized(wrapper);
552
+
553
+ if (wrapper->xmpFile && wrapper->xmpMeta) {
554
+ try {
555
+ if (wrapper->xmpFile->CanPutXMP(*(wrapper->xmpMeta))) {
556
+ wrapper->xmpFile->PutXMP(*(wrapper->xmpMeta));
557
+ std::string newBuffer;
558
+ wrapper->xmpMeta->SerializeToBuffer(&newBuffer);
559
+ } else {
560
+ std::string newBuffer;
561
+ wrapper->xmpMeta->SerializeToBuffer(&newBuffer);
562
+ rb_raise(rb_eArgError, "Can't update XMP new Data: '%s'", newBuffer.c_str());
563
+ }
564
+ } catch (const XMP_Error &e) {
565
+ rb_raise(rb_eRuntimeError, "XMP SDK error: %s", e.GetErrMsg());
566
+ }
567
+ }
568
+
569
+ return Qtrue;
570
+ }
571
+
572
+ VALUE
573
+ xmpwrapper_close_file(VALUE self) {
574
+ XMPWrapper *wrapper;
575
+ TypedData_Get_Struct(self, XMPWrapper, &xmpwrapper_data_type, wrapper);
576
+
577
+ if (wrapper->xmpFile) {
578
+ clean_wrapper(wrapper);
579
+ }
580
+
581
+ return Qtrue;
582
+ }
@@ -0,0 +1,35 @@
1
+ #ifndef XMP_WRAPPER_HPP
2
+ #define XMP_WRAPPER_HPP
3
+
4
+ #include <mutex>
5
+
6
+ struct XMPWrapper {
7
+ SXMPMeta *xmpMeta;
8
+ SXMPFiles *xmpFile;
9
+ XMP_PacketInfo *xmpPacket;
10
+ bool xmpMetaDataLoaded;
11
+ std::mutex mutex; // Protects all mutable members
12
+ };
13
+
14
+ VALUE xmpwrapper_allocate(VALUE klass);
15
+
16
+ VALUE register_namespace(VALUE self, VALUE rb_namespaceURI, VALUE rb_suggestedPrefix);
17
+
18
+ VALUE xmpwrapper_open_file(int argc, VALUE *argv, VALUE self);
19
+
20
+ VALUE xmp_file_info(VALUE self);
21
+ VALUE xmp_packet_info(VALUE self);
22
+
23
+ VALUE xmp_meta(VALUE self);
24
+ VALUE xmpwrapper_get_property(VALUE self, VALUE rb_ns, VALUE rb_prop);
25
+ VALUE xmpwrapper_get_localized_text(int argc, VALUE *argv, VALUE self);
26
+
27
+ VALUE xmpwrapper_set_meta(int argc, VALUE *argv, VALUE self);
28
+ VALUE xmpwrapper_set_property(VALUE self, VALUE rb_ns, VALUE rb_prop, VALUE rb_value);
29
+ VALUE xmpwrapper_update_localized_text(int argc, VALUE *argv, VALUE self);
30
+
31
+ VALUE write_xmp(VALUE self);
32
+
33
+ VALUE xmpwrapper_close_file(VALUE self);
34
+
35
+ #endif
@@ -66,6 +66,7 @@ module XmpToolkitRuby
66
66
  XMP_NS_PDFA_TYPE = "http://www.aiim.org/pdfa/ns/type#"
67
67
  XMP_NS_PDFA_FIELD = "http://www.aiim.org/pdfa/ns/field#"
68
68
  XMP_NS_PDFA_ID = "http://www.aiim.org/pdfa/ns/id/"
69
+ XMP_NS_PDFUA_ID = "http://www.aiim.org/pdfua/ns/id/"
69
70
  XMP_NS_PDFA_EXTENSION = "http://www.aiim.org/pdfa/ns/extension/"
70
71
 
71
72
  XMP_NS_PDFX = "http://ns.adobe.com/pdfx/1.3/"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module XmpToolkitRuby
4
- VERSION = "0.0.2"
4
+ VERSION = "0.1.0"
5
5
  end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module XmpToolkitRuby
4
+ module XmpCharForm
5
+ # Mapping of character format and byte-order masks (charForm enum)
6
+ CHAR_FORM = {
7
+ # Byte-order masks (do not use directly)
8
+ little_endian_mask: 0x0000_0001, # kXMP_CharLittleEndianMask
9
+ char_16bit_mask: 0x0000_0002, # kXMP_Char16BitMask
10
+ char_32bit_mask: 0x0000_0004, # kXMP_Char32BitMask
11
+
12
+ # Character format constants
13
+ char_8bit: 0x0000_0000, # kXMP_Char8Bit
14
+ char_16bit_big: 0x0000_0002, # kXMP_Char16BitBig
15
+ char_16bit_little: 0x0000_0003, # kXMP_Char16BitLittle
16
+ char_32bit_big: 0x0000_0004, # kXMP_Char32BitBig
17
+ char_32bit_little: 0x0000_0005, # kXMP_Char32BitLittle
18
+ char_unknown: 0x0000_0001 # kXMP_CharUnknown
19
+ }.freeze
20
+
21
+ # Reverse lookup by value
22
+ CHAR_FORM_BY_VALUE = CHAR_FORM.invert.freeze
23
+
24
+ class << self
25
+ # Lookup the integer value for a given charForm name (Symbol or String).
26
+ # @example XmpCharForm.value_for(:char_16bit_little) # => 3
27
+ def value_for(name)
28
+ key = name.is_a?(String) ? name.to_sym : name
29
+ CHAR_FORM[key]
30
+ end
31
+
32
+ # Lookup the charForm name for a given integer value.
33
+ # @example XmpCharForm.name_for(4) # => :char_32bit_big
34
+ def name_for(value)
35
+ CHAR_FORM_BY_VALUE[value]
36
+ end
37
+
38
+ # For a bitmask combining masks, return all matching names.
39
+ # @example XmpCharForm.flags_for(0x0003) # => [:char_16bit_mask, :little_endian_mask, :char_16bit_little]
40
+ def flags_for(bitmask)
41
+ CHAR_FORM.select { |_, v| (bitmask & v).nonzero? }.keys
42
+ end
43
+ end
44
+ end
45
+ end