/src/exiv2/src/xmpsidecar.cpp
Line | Count | Source |
1 | | // SPDX-License-Identifier: GPL-2.0-or-later |
2 | | #include "xmpsidecar.hpp" |
3 | | |
4 | | #include "basicio.hpp" |
5 | | #include "config.h" |
6 | | #include "convert.hpp" |
7 | | #include "error.hpp" |
8 | | #include "futils.hpp" |
9 | | #include "image.hpp" |
10 | | #include "properties.hpp" |
11 | | #include "utils.hpp" |
12 | | #include "xmp_exiv2.hpp" |
13 | | |
14 | | #ifdef EXIV2_DEBUG_MESSAGES |
15 | | #include <iostream> |
16 | | #endif |
17 | | |
18 | | namespace { |
19 | | constexpr char xmlHeader[] = "<?xpacket begin=\"\xef\xbb\xbf\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n"; |
20 | | constexpr auto xmlHdrCnt = std::size(xmlHeader) - 1; // without the trailing 0-character |
21 | | constexpr auto xmlFooter = "<?xpacket end=\"w\"?>"; |
22 | | } // namespace |
23 | | |
24 | | // class member definitions |
25 | | namespace Exiv2 { |
26 | 378 | XmpSidecar::XmpSidecar(BasicIo::UniquePtr io, bool create) : Image(ImageType::xmp, mdXmp, std::move(io)) { |
27 | 378 | if (create && io_->open() == 0) { |
28 | 0 | IoCloser closer(*io_); |
29 | 0 | io_->write(reinterpret_cast<const byte*>(xmlHeader), xmlHdrCnt); |
30 | 0 | } |
31 | 378 | } // XmpSidecar::XmpSidecar |
32 | | |
33 | 0 | std::string XmpSidecar::mimeType() const { |
34 | 0 | return "application/rdf+xml"; |
35 | 0 | } |
36 | | |
37 | 0 | void XmpSidecar::setComment(const std::string&) { |
38 | | // not supported |
39 | 0 | throw(Error(ErrorCode::kerInvalidSettingForImage, "Image comment", "XMP")); |
40 | 0 | } |
41 | | |
42 | 378 | void XmpSidecar::readMetadata() { |
43 | | #ifdef EXIV2_DEBUG_MESSAGES |
44 | | std::cerr << "Reading XMP file " << io_->path() << "\n"; |
45 | | #endif |
46 | 378 | if (io_->open() != 0) { |
47 | 0 | throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError()); |
48 | 0 | } |
49 | 378 | IoCloser closer(*io_); |
50 | | // Ensure that this is the correct image type |
51 | 378 | if (!isXmpType(*io_, false)) { |
52 | 0 | if (io_->error() || io_->eof()) |
53 | 0 | throw Error(ErrorCode::kerFailedToReadImageData); |
54 | 0 | throw Error(ErrorCode::kerNotAnImage, "XMP"); |
55 | 0 | } |
56 | | // Read the XMP packet from the IO stream |
57 | 378 | std::string xmpPacket; |
58 | 378 | const long len = 64 * 1024; |
59 | 378 | byte buf[len]; |
60 | 834 | while (auto l = io_->read(buf, len)) { |
61 | 456 | xmpPacket.append(reinterpret_cast<char*>(buf), l); |
62 | 456 | } |
63 | 378 | if (io_->error()) |
64 | 0 | throw Error(ErrorCode::kerFailedToReadImageData); |
65 | 378 | clearMetadata(); |
66 | 378 | xmpPacket_ = std::move(xmpPacket); |
67 | 378 | if (!xmpPacket_.empty() && XmpParser::decode(xmpData_, xmpPacket_)) { |
68 | 56 | #ifndef SUPPRESS_WARNINGS |
69 | 56 | EXV_WARNING << "Failed to decode XMP metadata.\n"; |
70 | 56 | #endif |
71 | 56 | } |
72 | | |
73 | | // #1112 - store dates to deal with loss of TZ information during conversions |
74 | 378 | for (const auto& xmp : xmpData_) { |
75 | 278 | std::string key(xmp.key()); |
76 | 278 | if (Internal::contains(key, "Date")) { |
77 | 128 | dates_[key] = xmp.value().toString(); |
78 | 128 | } |
79 | 278 | } |
80 | | |
81 | 378 | copyXmpToIptc(xmpData_, iptcData_); |
82 | 378 | copyXmpToExif(xmpData_, exifData_); |
83 | 378 | } // XmpSidecar::readMetadata |
84 | | |
85 | 0 | static bool matchi(const std::string& key, const char* substr) { |
86 | 0 | return Internal::contains(Internal::lower(key), substr); |
87 | 0 | } |
88 | | |
89 | 0 | void XmpSidecar::writeMetadata() { |
90 | 0 | if (io_->open() != 0) { |
91 | 0 | throw Error(ErrorCode::kerDataSourceOpenFailed, io_->path(), strError()); |
92 | 0 | } |
93 | 0 | IoCloser closer(*io_); |
94 | |
|
95 | 0 | if (!writeXmpFromPacket()) { |
96 | | // #589 copy XMP tags |
97 | 0 | Exiv2::XmpData copy; |
98 | 0 | for (const auto& xmp : xmpData_) { |
99 | 0 | if (!matchi(xmp.key(), "exif") && !matchi(xmp.key(), "iptc")) { |
100 | 0 | copy[xmp.key()] = xmp.value(); |
101 | 0 | } |
102 | 0 | } |
103 | | |
104 | | // run the converters |
105 | 0 | copyExifToXmp(exifData_, xmpData_); |
106 | 0 | copyIptcToXmp(iptcData_, xmpData_); |
107 | | |
108 | | // #1112 - restore dates if they lost their TZ info |
109 | 0 | for (const auto& [sKey, value_orig] : dates_) { |
110 | 0 | Exiv2::XmpKey key(sKey); |
111 | 0 | if (xmpData_.findKey(key) != xmpData_.end()) { |
112 | 0 | std::string value_now(xmpData_[sKey].value().toString()); |
113 | | // std::cout << key << " -> " << value_now << " => " << value_orig << '\n'; |
114 | 0 | if (Internal::contains(value_orig, value_now.substr(0, 10))) { |
115 | 0 | xmpData_[sKey] = value_orig; |
116 | 0 | } |
117 | 0 | } |
118 | 0 | } |
119 | | |
120 | | // #589 - restore tags which were modified by the converters |
121 | 0 | for (const auto& xmp : copy) { |
122 | 0 | xmpData_[xmp.key()] = xmp.value(); |
123 | 0 | } |
124 | |
|
125 | 0 | if (XmpParser::encode(xmpPacket_, xmpData_, XmpParser::omitPacketWrapper | XmpParser::useCompactFormat) > 1) { |
126 | 0 | #ifndef SUPPRESS_WARNINGS |
127 | 0 | EXV_ERROR << "Failed to encode XMP metadata.\n"; |
128 | 0 | #endif |
129 | 0 | } |
130 | 0 | } |
131 | 0 | if (!xmpPacket_.empty()) { |
132 | 0 | if (!xmpPacket_.starts_with("<?xml")) { |
133 | 0 | xmpPacket_ = xmlHeader + xmpPacket_ + xmlFooter; |
134 | 0 | } |
135 | 0 | MemIo tempIo; |
136 | | |
137 | | // Write XMP packet |
138 | 0 | if (tempIo.write(reinterpret_cast<const byte*>(xmpPacket_.data()), xmpPacket_.size()) != xmpPacket_.size()) |
139 | 0 | throw Error(ErrorCode::kerImageWriteFailed); |
140 | 0 | if (tempIo.error()) |
141 | 0 | throw Error(ErrorCode::kerImageWriteFailed); |
142 | 0 | io_->close(); |
143 | 0 | io_->transfer(tempIo); // may throw |
144 | 0 | } |
145 | 0 | } // XmpSidecar::writeMetadata |
146 | | |
147 | | // ************************************************************************* |
148 | | // free functions |
149 | 378 | Image::UniquePtr newXmpInstance(BasicIo::UniquePtr io, bool create) { |
150 | 378 | auto image = std::make_unique<XmpSidecar>(std::move(io), create); |
151 | 378 | if (!image->good()) { |
152 | 0 | return nullptr; |
153 | 0 | } |
154 | 378 | return image; |
155 | 378 | } |
156 | | |
157 | 7.93k | bool isXmpType(BasicIo& iIo, bool advance) { |
158 | | /* |
159 | | Check if the file starts with an optional XML declaration followed by |
160 | | either an XMP header (<?xpacket ... ?>) or an <x:xmpmeta> element. |
161 | | |
162 | | In addition, in order for empty XmpSidecar objects as created by |
163 | | Exiv2 to pass the test, just an XML header is also considered ok. |
164 | | */ |
165 | 7.93k | const int32_t len = 80; |
166 | 7.93k | byte buf[len]; |
167 | 7.93k | iIo.read(buf, xmlHdrCnt + 1); |
168 | 7.93k | if (iIo.eof() && 0 == strncmp(reinterpret_cast<const char*>(buf), xmlHeader, xmlHdrCnt)) { |
169 | 3 | return true; |
170 | 3 | } |
171 | 7.92k | if (iIo.error() || iIo.eof()) { |
172 | 123 | return false; |
173 | 123 | } |
174 | 7.80k | iIo.read(buf + xmlHdrCnt + 1, len - xmlHdrCnt - 1); |
175 | 7.80k | if (iIo.error() || iIo.eof()) { |
176 | 10 | return false; |
177 | 10 | } |
178 | | // Skip leading BOM |
179 | 7.79k | int32_t start = 0; |
180 | 7.79k | if (0 == strncmp(reinterpret_cast<const char*>(buf), "\xef\xbb\xbf", 3)) { |
181 | 118 | start = 3; |
182 | 118 | } |
183 | 7.79k | bool rc = false; |
184 | 7.79k | std::string head(reinterpret_cast<const char*>(buf + start), len - start); |
185 | 7.79k | if (head.starts_with("<?xml")) { |
186 | | // Forward to the next tag |
187 | 23 | auto it = std::find(head.begin() + 5, head.end(), '<'); |
188 | 23 | if (it != head.end()) |
189 | 15 | head = head.substr(std::distance(head.begin(), it)); |
190 | 23 | } |
191 | 7.79k | if (head.starts_with("<?xpacket") || head.starts_with("<x:xmpmeta")) { |
192 | 1.13k | rc = true; |
193 | 1.13k | } |
194 | 7.79k | if (!advance || !rc) { |
195 | 7.79k | iIo.seek(-(len - start), BasicIo::cur); // Swallow the BOM |
196 | 7.79k | } |
197 | 7.79k | return rc; |
198 | 7.80k | } |
199 | | |
200 | | } // namespace Exiv2 |