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,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, ®isteredPrefix);
|
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, ®isteredPrefix);
|
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/"
|
@@ -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
|