diff --git a/.github/workflows/test_package.yml b/.github/workflows/test_package.yml index 8160552..043b3fa 100644 --- a/.github/workflows/test_package.yml +++ b/.github/workflows/test_package.yml @@ -24,7 +24,7 @@ jobs: python-version: ${{ matrix.python_version }} - name: Install dependencies run: | - python -m pip install pyexiv2==2.9.0 + python -m pip install pyexiv2==2.11.0 python -m pip install pytest psutil - name: Test run: | @@ -50,7 +50,7 @@ jobs: python-version: ${{ matrix.python_version }} - name: Install dependencies run: | - python -m pip install pyexiv2==2.9.0 + python -m pip install pyexiv2==2.11.0 python -m pip install pytest psutil - name: Test run: | diff --git a/docs/Tutorial-cn.md b/docs/Tutorial-cn.md index 370f7ef..1759538 100644 --- a/docs/Tutorial-cn.md +++ b/docs/Tutorial-cn.md @@ -112,6 +112,14 @@ class ImageData(Image): def registerNs(namespace: str, prefix: str) def enableBMFF(enable=True) def set_log_level(level=2) + +def convert_exif_to_xmp(data: dict, encoding='utf-8') -> dict +def convert_iptc_to_xmp(data: dict, encoding='utf-8') -> dict +def convert_xmp_to_exif(data: dict, encoding='utf-8') -> dict +def convert_xmp_to_iptc(data: dict, encoding='utf-8') -> dict + +__version__ = '...' +__exiv2_version__ = '...' ``` ## Class Image @@ -238,7 +246,6 @@ def set_log_level(level=2) - `img.read_icc()`、`img.modify_icc()`、`img.clear_icc()` 用于访问图片里的 [ICC profile](https://en.wikipedia.org/wiki/ICC_profile) 。 - ### thumbnail - EXIF 标准允许在 JPEG 图片中嵌入缩略图,通常存储在 APP1 标签(FFE1)中。 @@ -315,3 +322,13 @@ def set_log_level(level=2) >>> pyexiv2.set_log_level(4) >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) # 此时没有错误日志 ``` + +## convert + +- Exiv2 支持将某些 EXIF 或 IPTC 标签,转换成 XMP 标签,也支持反向转换。参考: +- 示例: + ```py + >>> pyexiv2.convert_exif_to_xmp({'Exif.Image.Artist': 'test-中文-', 'Exif.Image.Rating': '4'}) + {'Xmp.dc.creator': ['test-中文-'], 'Xmp.xmp.Rating': '4'} + ``` + diff --git a/docs/Tutorial.md b/docs/Tutorial.md index c506ba5..1736581 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -112,6 +112,14 @@ class ImageData(Image): def registerNs(namespace: str, prefix: str) def enableBMFF(enable=True) def set_log_level(level=2) + +def convert_exif_to_xmp(data: dict, encoding='utf-8') -> dict +def convert_iptc_to_xmp(data: dict, encoding='utf-8') -> dict +def convert_xmp_to_exif(data: dict, encoding='utf-8') -> dict +def convert_xmp_to_iptc(data: dict, encoding='utf-8') -> dict + +__version__ = '...' +__exiv2_version__ = '...' ``` ## class Image @@ -212,7 +220,7 @@ def set_log_level(level=2) - `img.read_comment()`、`img.modify_comment()`、`img.clear_comment()` are used to access JPEG COM (Comment) segment in the image, which does not belong to EXIF, IPTC or XMP metadata. - [related issue](https://github.com/Exiv2/exiv2/issues/1445) -- Sample: +- For example: ```py >>> img.modify_comment('Hello World! \n你好!\n') >>> img.read_comment() @@ -314,3 +322,12 @@ def set_log_level(level=2) >>> pyexiv2.set_log_level(4) >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) # No error reported ``` + +## convert + +- Exiv2 supports converting some EXIF or IPTC tags to XMP tags, and also supports reverse conversion. Reference: +- For example: + ```py + >>> pyexiv2.convert_exif_to_xmp({'Exif.Image.Artist': 'test-中文-', 'Exif.Image.Rating': '4'}) + {'Xmp.dc.creator': ['test-中文-'], 'Xmp.xmp.Rating': '4'} + ``` diff --git a/pyexiv2/__init__.py b/pyexiv2/__init__.py index 791712e..2118fa1 100644 --- a/pyexiv2/__init__.py +++ b/pyexiv2/__init__.py @@ -6,16 +6,24 @@ from .core import * -__version__ = '2.9.0' +__version__ = '2.11.0' __exiv2_version__ = exiv2api.version() __all__ = [ '__version__', '__exiv2_version__', + + # core.py 'Image', 'ImageData', 'registerNs', 'enableBMFF', 'set_log_level', + + # convert.py + 'convert_exif_to_xmp', + 'convert_iptc_to_xmp', + 'convert_xmp_to_exif', + 'convert_xmp_to_iptc', ] diff --git a/pyexiv2/convert.py b/pyexiv2/convert.py new file mode 100644 index 0000000..d8c5cdb --- /dev/null +++ b/pyexiv2/convert.py @@ -0,0 +1,138 @@ +import re + +from .lib import exiv2api + + +# These tags are used by Windows and encoded in UCS2-LE. +# pyexiv2 will automatically convert encoding formats when reading and writing them. +EXIF_TAGS_ENCODED_IN_UCS2 = [ + 'Exif.Image.XPTitle', + 'Exif.Image.XPComment', + 'Exif.Image.XPAuthor', + 'Exif.Image.XPKeywords', + 'Exif.Image.XPSubject', +] + +# These tags can be written repeatedly, so there may be multiple values. +# pyexiv2 will convert their values to a list of strings. +IPTC_TAGS_REPEATABLE = [ + 'Iptc.Envelope.Destination', + 'Iptc.Envelope.ProductId', + 'Iptc.Application2.ObjectAttribute', + 'Iptc.Application2.Subject', + 'Iptc.Application2.SuppCategory', + 'Iptc.Application2.Keywords', + 'Iptc.Application2.LocationCode', + 'Iptc.Application2.LocationName', + 'Iptc.Application2.ReferenceService', + 'Iptc.Application2.ReferenceDate', + 'Iptc.Application2.ReferenceNumber', + 'Iptc.Application2.Byline', + 'Iptc.Application2.BylineTitle', + 'Iptc.Application2.Contact', + 'Iptc.Application2.Writer', +] + + +def _parse(table: list, encoding='utf-8') -> dict: + """ Parse the metadata from a text table into a dict. """ + data = {} + for line in table: + tag, value, typeName = [field.decode(encoding) for field in line] + if typeName in ['XmpBag', 'XmpSeq']: + value = value.split(', ') + elif typeName in ['XmpText']: + # Handle nested array structures in XML. Refer to https://exiv2.org/manpage.html#set_xmp_struct + if value in ['type="Bag"', 'type="Seq"']: + value = [''] + elif typeName in ['LangAlt']: + # Refer to https://exiv2.org/manpage.html#langalt_values + if 'lang=' in value: + fields = re.split(r', (lang="\S+") ', ', ' + value)[1:] + value = {language: content for language, content in zip(fields[0::2], fields[1::2])} + + # Convert the values to a list of strings if the tag has multiple values + pre_value = data.get(tag) + if pre_value == None: + data[tag] = value + elif isinstance(pre_value, str): + data[tag] = [pre_value, value] + elif isinstance(pre_value, list): + data[tag].append(value) + + return data + + +def _dumps(data: dict) -> list: + """ Convert the metadata from a dict into a text table. """ + table = [] + for tag, value in data.items(): + tag = str(tag) + if value == None: + typeName = '_delete' + value = '' + elif isinstance(value, (list, tuple)): + typeName = 'array' + value = list(value) + elif isinstance(value, dict): + typeName = 'string' + value = ', '.join(['{} {}'.format(k,v) for k,v in value.items()]) + else: + typeName = 'string' + value = str(value) + line = [tag, value, typeName] + table.append(line) + return table + + +def decode_ucs2(text: str) -> str: + """ + Convert text from UCS2 encoding to UTF8 encoding. + For example: + >>> decode_ucs2('116 0 101 0 115 0 116 0') + 'test' + """ + hex_str = ''.join(['{:02x}'.format(int(i)) for i in text.split()]) + return bytes.fromhex(hex_str).decode('utf-16le') + + +def encode_ucs2(text: str) -> str: + """ + Convert text from UTF8 encoding to UCS2 encoding. + For example: + >>> encode_ucs2('test') + '116 0 101 0 115 0 116 0' + """ + hex_str = text.encode('utf-16le').hex() + int_list = [int(''.join(i), base=16) for i in zip(*[iter(hex_str)] * 2)] + return ' '.join([str(i) for i in int_list]) + + +def convert_exif_to_xmp(data: dict, encoding='utf-8') -> dict: + """ Input EXIF metadata, convert to XMP metadata and return. It works like executing modify_exif() then read_xmp(). """ + data = data.copy() + for tag in EXIF_TAGS_ENCODED_IN_UCS2: + value = data.get(tag) + if value: + data[tag] = encode_ucs2(value) + converted_data = exiv2api.convert_exif_to_xmp(_dumps(data), encoding) + return _parse(converted_data, encoding) + + +def convert_iptc_to_xmp(data: dict, encoding='utf-8') -> dict: + """ Input IPTC metadata, convert to XMP metadata and return. It works like executing modify_iptc() then read_xmp(). """ + converted_data = exiv2api.convert_iptc_to_xmp(_dumps(data), encoding) + return _parse(converted_data, encoding) + + +def convert_xmp_to_exif(data: dict, encoding='utf-8') -> dict: + """ Input XMP metadata, convert to EXIF metadata and return. It works like executing modify_xmp() then read_exif(). """ + converted_data = exiv2api.convert_xmp_to_exif(_dumps(data), encoding) + return _parse(converted_data, encoding) + + +def convert_xmp_to_iptc(data: dict, encoding='utf-8') -> dict: + """ Input XMP metadata, convert to IPTC metadata and return. It works like executing modify_xmp() then read_iptc(). """ + converted_data = exiv2api.convert_xmp_to_iptc(_dumps(data), encoding) + return _parse(converted_data, encoding) + diff --git a/pyexiv2/core.py b/pyexiv2/core.py index 25fb565..8990893 100644 --- a/pyexiv2/core.py +++ b/pyexiv2/core.py @@ -1,7 +1,6 @@ -import re - -from . import reference from .lib import exiv2api +from .convert import * +from .convert import _parse, _dumps class Image: @@ -49,29 +48,24 @@ def get_access_mode(self) -> dict: return dic def read_exif(self, encoding='utf-8') -> dict: - data = self._parse(self.img.read_exif(), encoding) - - # Decode some tags - for tag in reference.EXIF_TAGS_ENCODED_IN_UCS2: + data = _parse(self.img.read_exif(), encoding) + for tag in EXIF_TAGS_ENCODED_IN_UCS2: value = data.get(tag) if value: - data[tag] = self._decode_ucs2(value) - + data[tag] = decode_ucs2(value) return data def read_iptc(self, encoding='utf-8') -> dict: - data = self._parse(self.img.read_iptc(), encoding) - - # For repeatable tags, even if they do not have multiple values, their values are converted to list type. - for tag in reference.IPTC_TAGS_REPEATABLE: + data = _parse(self.img.read_iptc(), encoding) + # For repeatable tags, the value is converted to list type even if there are no multiple values. + for tag in IPTC_TAGS_REPEATABLE: value = data.get(tag) if isinstance(value, str): data[tag] = [value] - return data def read_xmp(self, encoding='utf-8') -> dict: - return self._parse(self.img.read_xmp(), encoding) + return _parse(self.img.read_xmp(), encoding) def read_raw_xmp(self, encoding='utf-8') -> str: return self.img.read_raw_xmp().decode(encoding) @@ -86,19 +80,18 @@ def read_thumbnail(self) -> bytes: return self.img.read_thumbnail() def modify_exif(self, data: dict, encoding='utf-8'): - data = data.copy() - for tag in reference.EXIF_TAGS_ENCODED_IN_UCS2: + data = data.copy() # Avoid modifying the original data + for tag in EXIF_TAGS_ENCODED_IN_UCS2: value = data.get(tag) if value: - data[tag] = self._encode_ucs2(value) - - self.img.modify_exif(self._dumps(data), encoding) + data[tag] = encode_ucs2(value) + self.img.modify_exif(_dumps(data), encoding) def modify_iptc(self, data: dict, encoding='utf-8'): - self.img.modify_iptc(self._dumps(data), encoding) + self.img.modify_iptc(_dumps(data), encoding) def modify_xmp(self, data: dict, encoding='utf-8'): - self.img.modify_xmp(self._dumps(data), encoding) + self.img.modify_xmp(_dumps(data), encoding) def modify_raw_xmp(self, data: str, encoding='utf-8'): self.img.modify_raw_xmp(data, encoding) @@ -116,76 +109,6 @@ def modify_thumbnail(self, data: bytes): raise TypeError('The thumbnail should be of bytes type.') return self.img.modify_thumbnail(data, len(data)) - def _parse(self, table: list, encoding='utf-8') -> dict: - """ Parse the metadata from a text table into a dict. """ - data = {} - for line in table: - tag, value, typeName = [field.decode(encoding) for field in line] - if typeName in ['XmpBag', 'XmpSeq']: - value = value.split(', ') - elif typeName in ['XmpText']: - # Handle nested array structures in XML. Refer to https://exiv2.org/manpage.html#set_xmp_struct - if value in ['type="Bag"', 'type="Seq"']: - value = [''] - elif typeName in ['LangAlt']: - # Refer to https://exiv2.org/manpage.html#langalt_values - if 'lang=' in value: - fields = re.split(r', (lang="\S+") ', ', ' + value)[1:] - value = {language: content for language, content in zip(fields[0::2], fields[1::2])} - - # Convert the values to a list of strings if the tag has multiple values - pre_value = data.get(tag) - if pre_value == None: - data[tag] = value - elif isinstance(pre_value, str): - data[tag] = [pre_value, value] - elif isinstance(pre_value, list): - data[tag].append(value) - - return data - - def _dumps(self, data: dict) -> list: - """ Convert the metadata from a dict into a text table. """ - table = [] - for tag, value in data.items(): - tag = str(tag) - if value == None: - typeName = '_delete' - value = '' - elif isinstance(value, (list, tuple)): - typeName = 'array' - value = list(value) - elif isinstance(value, dict): - typeName = 'string' - value = ', '.join(['{} {}'.format(k,v) for k,v in value.items()]) - else: - typeName = 'string' - value = str(value) - line = [tag, value, typeName] - table.append(line) - return table - - def _decode_ucs2(self, text): - """ - Convert text from UCS2 encoding to UTF8 encoding. - For example: - >>> img._decode_ucs2('116 0 101 0 115 0 116 0') - 'test' - """ - hex_str = ''.join(['{:02x}'.format(int(i)) for i in text.split()]) - return bytes.fromhex(hex_str).decode('utf-16le') - - def _encode_ucs2(self, text): - """ - Convert text from UTF8 encoding to UCS2 encoding. - For example: - >>> img._encode_ucs2('test') - '116 0 101 0 115 0 116 0' - """ - hex_str = text.encode('utf-16le').hex() - int_list = [int(''.join(i), base=16) for i in zip(*[iter(hex_str)] * 2)] - return ' '.join([str(i) for i in int_list]) - def clear_exif(self): self.img.clear_exif() @@ -234,10 +157,12 @@ def registerNs(namespace: str, prefix: str): """ return exiv2api.registerNs(namespace, prefix) + def enableBMFF(enable=True): """ Enable or disable reading BMFF images. Return True on success. """ return exiv2api.enableBMFF(enable) + def set_log_level(level=2): """ Set the level of handling logs. There are five levels of handling logs: diff --git a/pyexiv2/lib/exiv2api.cpp b/pyexiv2/lib/exiv2api.cpp index 334562b..3be5fbb 100644 --- a/pyexiv2/lib/exiv2api.cpp +++ b/pyexiv2/lib/exiv2api.cpp @@ -32,7 +32,7 @@ void logHandler(int level, const char *msg) } /* The error log should be checked at the end of each operation. - If there is a C++ error, it is converted to a Python exception. */ + If a C++ error occurs, convert it to a Python exception. */ void check_error_log() { std::string str = error_log.str(); @@ -227,7 +227,6 @@ class Image{ void modify_exif(py::list table, py::str encoding) { - // Create an empty container for storing data Exiv2::ExifData &exifData = (*img)->exifData(); // Iterate the input table. each line contains a key and a value @@ -255,8 +254,9 @@ class Image{ else if (typeName == "string") exifData[key] = value; } - (*img)->setExifData(exifData); - (*img)->writeMetadata(); // Save the metadata from memory to disk + + (*img)->setExifData(exifData); // Save metadata to images in memory + (*img)->writeMetadata(); // Save metadata from memory to disk check_error_log(); } @@ -269,13 +269,11 @@ class Image{ line.append(field); std::string key = py::bytes(line[0].attr("encode")(encoding)); std::string typeName = py::bytes(line[2].attr("encode")(encoding)); - Exiv2::IptcData::iterator key_pos = iptcData.findKey(Exiv2::IptcKey(key)); while (key_pos != iptcData.end()){ // Use the while loop because the iptc key may repeat iptcData.erase(key_pos); key_pos = iptcData.findKey(Exiv2::IptcKey(key)); } - if (typeName == "_delete") continue; else if (typeName == "string") @@ -307,11 +305,9 @@ class Image{ line.append(field); std::string key = py::bytes(line[0].attr("encode")(encoding)); std::string typeName = py::bytes(line[2].attr("encode")(encoding)); - Exiv2::XmpData::iterator key_pos = xmpData.findKey(Exiv2::XmpKey(key)); if (key_pos != xmpData.end()) xmpData.erase(key_pos); - if (typeName == "_delete") continue; else if (typeName == "string") @@ -412,6 +408,157 @@ class Image{ }; +py::object convert_exif_to_xmp(py::list table, py::str encoding) +{ + Exiv2::ExifData exifData; + Exiv2::XmpData xmpData; + + // Write metadata, which works like modify_exif() + for (auto _line : table){ + py::list line; + for (auto field : _line) + line.append(field); + std::string key = py::bytes(line[0].attr("encode")(encoding)); + std::string value = py::bytes(line[1].attr("encode")(encoding)); + std::string typeName = py::bytes(line[2].attr("encode")(encoding)); + Exiv2::ExifData::iterator key_pos = exifData.findKey(Exiv2::ExifKey(key)); + if (key_pos != exifData.end()) + exifData.erase(key_pos); + + if (typeName == "_delete") + continue; + else if (typeName == "string") + exifData[key] = value; + } + + // Convert and read metadata, which works like read_xmp() + Exiv2::copyExifToXmp(exifData, xmpData); + Exiv2::XmpData::iterator i = xmpData.begin(); + Exiv2::XmpData::iterator end = xmpData.end(); + read_block; +} + +py::object convert_iptc_to_xmp(py::list table, py::str encoding) +{ + Exiv2::IptcData iptcData; + Exiv2::XmpData xmpData; + + // Write metadata, which works like modify_iptc() + for (auto _line : table){ + py::list line; + for (auto field : _line) + line.append(field); + std::string key = py::bytes(line[0].attr("encode")(encoding)); + std::string typeName = py::bytes(line[2].attr("encode")(encoding)); + Exiv2::IptcData::iterator key_pos = iptcData.findKey(Exiv2::IptcKey(key)); + while (key_pos != iptcData.end()){ + iptcData.erase(key_pos); + key_pos = iptcData.findKey(Exiv2::IptcKey(key)); + } + if (typeName == "_delete") + continue; + else if (typeName == "string") + { + std::string value = py::bytes(line[1].attr("encode")(encoding)); + iptcData[key] = value; + } + else if (typeName == "array") + { + Exiv2::Value::AutoPtr value = Exiv2::Value::create(Exiv2::string); + for (auto item: line[1]){ + std::string item_str = py::bytes(py::str(item).attr("encode")(encoding)); + value->read(item_str); + iptcData.add(Exiv2::IptcKey(key), value.get()); + } + } + } + + // Convert and read metadata, which works like read_xmp() + Exiv2::copyIptcToXmp(iptcData, xmpData); + Exiv2::XmpData::iterator i = xmpData.begin(); + Exiv2::XmpData::iterator end = xmpData.end(); + read_block; +} + +py::object convert_xmp_to_exif(py::list table, py::str encoding) +{ + Exiv2::XmpData xmpData; + Exiv2::ExifData exifData; + + // Write metadata, which works like modify_xmp() + for (auto _line : table){ + py::list line; + for (auto field : _line) + line.append(field); + std::string key = py::bytes(line[0].attr("encode")(encoding)); + std::string typeName = py::bytes(line[2].attr("encode")(encoding)); + Exiv2::XmpData::iterator key_pos = xmpData.findKey(Exiv2::XmpKey(key)); + if (key_pos != xmpData.end()) + xmpData.erase(key_pos); + if (typeName == "_delete") + continue; + else if (typeName == "string") + { + std::string value = py::bytes(line[1].attr("encode")(encoding)); + xmpData[key] = value; + } + else if (typeName == "array") + { + Exiv2::Value::AutoPtr value = Exiv2::Value::create(Exiv2::xmpSeq); + for (auto item: line[1]){ + std::string item_str = py::bytes(py::str(item).attr("encode")(encoding)); + value->read(item_str); + } + xmpData.add(Exiv2::XmpKey(key), value.get()); + } + } + + // Convert and read metadata, which works like read_exif() + Exiv2::copyXmpToExif(xmpData, exifData); + Exiv2::ExifData::iterator i = exifData.begin(); + Exiv2::ExifData::iterator end = exifData.end(); + read_block; +} + +py::object convert_xmp_to_iptc(py::list table, py::str encoding) +{ + Exiv2::XmpData xmpData; + Exiv2::IptcData iptcData; + + // Write metadata, which works like modify_xmp() + for (auto _line : table){ + py::list line; + for (auto field : _line) + line.append(field); + std::string key = py::bytes(line[0].attr("encode")(encoding)); + std::string typeName = py::bytes(line[2].attr("encode")(encoding)); + Exiv2::XmpData::iterator key_pos = xmpData.findKey(Exiv2::XmpKey(key)); + if (key_pos != xmpData.end()) + xmpData.erase(key_pos); + if (typeName == "_delete") + continue; + else if (typeName == "string") + { + std::string value = py::bytes(line[1].attr("encode")(encoding)); + xmpData[key] = value; + } + else if (typeName == "array") + { + Exiv2::Value::AutoPtr value = Exiv2::Value::create(Exiv2::xmpSeq); + for (auto item: line[1]){ + std::string item_str = py::bytes(py::str(item).attr("encode")(encoding)); + value->read(item_str); + } + xmpData.add(Exiv2::XmpKey(key), value.get()); + } + } + + // Convert and read metadata, which works like read_iptc() + Exiv2::copyXmpToIptc(xmpData, iptcData); + Exiv2::IptcData::iterator i = iptcData.begin(); + Exiv2::IptcData::iterator end = iptcData.end(); + read_block; +} // Declare the API that needs to be mapped, to convert this CPP file into a Python module. PYBIND11_MODULE(exiv2api, m) @@ -457,4 +604,8 @@ PYBIND11_MODULE(exiv2api, m) .def("clear_comment" , &Image::clear_comment) .def("clear_icc" , &Image::clear_icc) .def("clear_thumbnail" , &Image::clear_thumbnail); + m.def("convert_exif_to_xmp" , &convert_exif_to_xmp); + m.def("convert_iptc_to_xmp" , &convert_iptc_to_xmp); + m.def("convert_xmp_to_exif" , &convert_xmp_to_exif); + m.def("convert_xmp_to_iptc" , &convert_xmp_to_iptc); } diff --git a/pyexiv2/tests/data/data.py b/pyexiv2/tests/data/data.py index cdc816e..eedebc2 100644 --- a/pyexiv2/tests/data/data.py +++ b/pyexiv2/tests/data/data.py @@ -3,97 +3,102 @@ MIME_TYPE = 'image/jpeg' -ACCESS_MODE = {'exif' : 'read+write', - 'iptc' : 'read+write', - 'xmp' : 'read+write', - 'comment': 'read+write'} +ACCESS_MODE = { + 'exif' : 'read+write', + 'iptc' : 'read+write', + 'xmp' : 'read+write', + 'comment': 'read+write', + } -EXIF = {'Exif.Image.ImageDescription' : 'test-中文-', - 'Exif.Image.Make' : 'test-中文-', - 'Exif.Image.Model' : 'test-中文-', - 'Exif.Image.Orientation' : '1', - 'Exif.Image.DateTime' : '2019:08:12 19:44:04', - 'Exif.Image.Artist' : 'test-中文-', - 'Exif.Image.Rating' : '4', - 'Exif.Image.RatingPercent' : '75', - 'Exif.Image.Copyright' : 'test-中文-', - 'Exif.Image.ExifTag' : '2446', - 'Exif.Photo.ExposureProgram' : '1', - 'Exif.Photo.ExifVersion' : '48 50 50 49', - 'Exif.Photo.DateTimeOriginal' : '2019:08:12 19:44:04', - 'Exif.Photo.DateTimeDigitized' : '2019:08:12 19:44:04', - 'Exif.Photo.LightSource' : '1', - 'Exif.Photo.SubSecTime' : '18', - 'Exif.Photo.SubSecTimeOriginal' : '18', - 'Exif.Photo.SubSecTimeDigitized': '176', - 'Exif.Photo.ColorSpace' : '65535', - 'Exif.Photo.WhiteBalance' : '0', - 'Exif.Photo.Contrast' : '0', - 'Exif.Photo.Saturation' : '0', - 'Exif.Photo.Sharpness' : '0', - 'Exif.Photo.0xea1c' : '28 234 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0', - 'Exif.Image.XPTitle' : 'test-中文-\x00', - 'Exif.Image.XPComment' : 'test-中文-\x00', - 'Exif.Image.XPAuthor' : 'test-中文-\x00', - 'Exif.Image.XPKeywords' : 'test-中文-\x00', - 'Exif.Image.XPSubject' : 'test-中文-\x00', - 'Exif.Image.0xea1c' : '28 234 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0', - 'Exif.Thumbnail.Compression' : '6', - 'Exif.Thumbnail.JPEGInterchangeFormat' : '4762', - 'Exif.Thumbnail.JPEGInterchangeFormatLength': '6969' - } +EXIF = { + 'Exif.Image.ImageDescription' : 'test-中文-', + 'Exif.Image.Make' : 'test-中文-', + 'Exif.Image.Model' : 'test-中文-', + 'Exif.Image.Orientation' : '1', + 'Exif.Image.DateTime' : '2019:08:12 19:44:04', + 'Exif.Image.Artist' : 'test-中文-', + 'Exif.Image.Rating' : '4', + 'Exif.Image.RatingPercent' : '75', + 'Exif.Image.Copyright' : 'test-中文-', + 'Exif.Image.ExifTag' : '2446', + 'Exif.Photo.ExposureProgram' : '1', + 'Exif.Photo.ExifVersion' : '48 50 50 49', + 'Exif.Photo.DateTimeOriginal' : '2019:08:12 19:44:04', + 'Exif.Photo.DateTimeDigitized' : '2019:08:12 19:44:04', + 'Exif.Photo.LightSource' : '1', + 'Exif.Photo.SubSecTime' : '18', + 'Exif.Photo.SubSecTimeOriginal' : '18', + 'Exif.Photo.SubSecTimeDigitized': '176', + 'Exif.Photo.ColorSpace' : '65535', + 'Exif.Photo.WhiteBalance' : '0', + 'Exif.Photo.Contrast' : '0', + 'Exif.Photo.Saturation' : '0', + 'Exif.Photo.Sharpness' : '0', + 'Exif.Photo.0xea1c' : '28 234 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0', + 'Exif.Image.XPTitle' : 'test-中文-\x00', + 'Exif.Image.XPComment' : 'test-中文-\x00', + 'Exif.Image.XPAuthor' : 'test-中文-\x00', + 'Exif.Image.XPKeywords' : 'test-中文-\x00', + 'Exif.Image.XPSubject' : 'test-中文-\x00', + 'Exif.Image.0xea1c' : '28 234 0 0 0 8 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0', + 'Exif.Thumbnail.Compression' : '6', + 'Exif.Thumbnail.JPEGInterchangeFormat' : '4762', + 'Exif.Thumbnail.JPEGInterchangeFormatLength': '6969', + } -IPTC = {'Iptc.Envelope.CharacterSet' : '\x1b%G', - 'Iptc.Application2.RecordVersion': '4', - 'Iptc.Application2.ObjectName' : 'test-中文-', - 'Iptc.Application2.Keywords' : ['tag1', 'tag2', 'tag3'], - 'Iptc.Application2.DateCreated' : '2019-08-12', - 'Iptc.Application2.TimeCreated' : '19:44:04+00:00', - 'Iptc.Application2.Byline' : ['test-中文-'], - 'Iptc.Application2.Copyright' : 'test-中文-', - 'Iptc.Application2.Caption' : 'test-中文-', - } +IPTC = { + 'Iptc.Envelope.CharacterSet' : '\x1b%G', + 'Iptc.Application2.RecordVersion': '4', + 'Iptc.Application2.ObjectName' : 'test-中文-', + 'Iptc.Application2.Keywords' : ['tag1', 'tag2', 'tag3'], + 'Iptc.Application2.DateCreated' : '2019-08-12', + 'Iptc.Application2.TimeCreated' : '19:44:04+00:00', + 'Iptc.Application2.Byline' : ['test-中文-'], + 'Iptc.Application2.Copyright' : 'test-中文-', + 'Iptc.Application2.Caption' : 'test-中文-', + } -XMP = {'Xmp.dc.format' : 'image/jpeg', - 'Xmp.dc.title' : {'lang="x-default"': 'test-中文-', 'lang="de-DE"': 'Hallo, Welt'}, - 'Xmp.dc.subject' : ['tag1', 'tag2', 'tag3'], - 'Xmp.dc.creator' : ['test-中文-'], - 'Xmp.dc.rights' : {'lang="x-default"': 'test-中文-'}, - 'Xmp.dc.description' : {'lang="x-default"': 'test-中文-'}, - 'Xmp.xmp.Rating' : '4', - 'Xmp.xmp.CreateDate' : '2019-08-12T19:44:04.176', - 'Xmp.xmp.ModifyDate' : '2019-08-12T19:44:04.18', - 'Xmp.xmp.MetadataDate' : '2020-04-06T00:55:07+08:00', - 'Xmp.MicrosoftPhoto.Rating' : '75', - 'Xmp.MicrosoftPhoto.DateAcquired' : '2019-08-12T19:44:08.151', - 'Xmp.MicrosoftPhoto.LensModel' : 'test-中文-', - 'Xmp.MicrosoftPhoto.LensManufacturer' : 'test-中文-', - 'Xmp.MicrosoftPhoto.FlashModel' : 'test-中文-', - 'Xmp.MicrosoftPhoto.FlashManufacturer' : 'test-中文-', - 'Xmp.MicrosoftPhoto.CameraSerialNumber' : 'test-中文-', - 'Xmp.MicrosoftPhoto.LastKeywordXMP' : ['test-中文-'], - 'Xmp.xmpMM.InstanceID' : 'xmp.iid:14282d1f-7831-7043-ad77-1e10959ecd50', - 'Xmp.xmpMM.DocumentID' : 'ECE1099AF3406874FAA7B01CBB5C6F71', - 'Xmp.xmpMM.OriginalDocumentID' : 'ECE1099AF3406874FAA7B01CBB5C6F71', - 'Xmp.xmpMM.History' : [''], - 'Xmp.xmpMM.History[1]' : 'type="Struct"', - 'Xmp.xmpMM.History[1]/stEvt:action' : 'saved', - 'Xmp.xmpMM.History[1]/stEvt:instanceID' : 'xmp.iid:8f83ee32-1163-7b40-b31b-deab20789cf4', - 'Xmp.xmpMM.History[1]/stEvt:when' : '2019-08-19T19:45:55+08:00', - 'Xmp.xmpMM.History[1]/stEvt:softwareAgent' : 'Adobe Photoshop Camera Raw 10.0', - 'Xmp.xmpMM.History[1]/stEvt:changed' : '/metadata', - 'Xmp.xmpMM.History[2]' : 'type="Struct"', - 'Xmp.xmpMM.History[2]/stEvt:action' : 'saved', - 'Xmp.xmpMM.History[2]/stEvt:instanceID' : 'xmp.iid:14282d1f-7831-7043-ad77-1e10959ecd50', - 'Xmp.xmpMM.History[2]/stEvt:when' : '2020-04-06T00:55:07+08:00', - 'Xmp.xmpMM.History[2]/stEvt:softwareAgent' : 'Adobe Photoshop Camera Raw (Windows)', - 'Xmp.xmpMM.History[2]/stEvt:changed' : '/metadata', - 'Xmp.photoshop.DateCreated' : '2019-08-12T19:44:04Z', - 'Xmp.iptc.CreatorContactInfo' : 'type="Struct"', - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork' : '123456', - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiEmailWork': '123456@gmail.com', - 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiUrlWork' : 'www.123456.com' - } +XMP = { + 'Xmp.dc.format' : 'image/jpeg', + 'Xmp.dc.title' : {'lang="x-default"': 'test-中文-', 'lang="de-DE"': 'Hallo, Welt'}, + 'Xmp.dc.subject' : ['tag1', 'tag2', 'tag3'], + 'Xmp.dc.creator' : ['test-中文-'], + 'Xmp.dc.rights' : {'lang="x-default"': 'test-中文-'}, + 'Xmp.dc.description' : {'lang="x-default"': 'test-中文-'}, + 'Xmp.xmp.Rating' : '4', + 'Xmp.xmp.CreateDate' : '2019-08-12T19:44:04.176', + 'Xmp.xmp.ModifyDate' : '2019-08-12T19:44:04.18', + 'Xmp.xmp.MetadataDate' : '2020-04-06T00:55:07+08:00', + 'Xmp.MicrosoftPhoto.Rating' : '75', + 'Xmp.MicrosoftPhoto.DateAcquired' : '2019-08-12T19:44:08.151', + 'Xmp.MicrosoftPhoto.LensModel' : 'test-中文-', + 'Xmp.MicrosoftPhoto.LensManufacturer' : 'test-中文-', + 'Xmp.MicrosoftPhoto.FlashModel' : 'test-中文-', + 'Xmp.MicrosoftPhoto.FlashManufacturer' : 'test-中文-', + 'Xmp.MicrosoftPhoto.CameraSerialNumber' : 'test-中文-', + 'Xmp.MicrosoftPhoto.LastKeywordXMP' : ['test-中文-'], + 'Xmp.xmpMM.InstanceID' : 'xmp.iid:14282d1f-7831-7043-ad77-1e10959ecd50', + 'Xmp.xmpMM.DocumentID' : 'ECE1099AF3406874FAA7B01CBB5C6F71', + 'Xmp.xmpMM.OriginalDocumentID' : 'ECE1099AF3406874FAA7B01CBB5C6F71', + 'Xmp.xmpMM.History' : [''], + 'Xmp.xmpMM.History[1]' : 'type="Struct"', + 'Xmp.xmpMM.History[1]/stEvt:action' : 'saved', + 'Xmp.xmpMM.History[1]/stEvt:instanceID' : 'xmp.iid:8f83ee32-1163-7b40-b31b-deab20789cf4', + 'Xmp.xmpMM.History[1]/stEvt:when' : '2019-08-19T19:45:55+08:00', + 'Xmp.xmpMM.History[1]/stEvt:softwareAgent' : 'Adobe Photoshop Camera Raw 10.0', + 'Xmp.xmpMM.History[1]/stEvt:changed' : '/metadata', + 'Xmp.xmpMM.History[2]' : 'type="Struct"', + 'Xmp.xmpMM.History[2]/stEvt:action' : 'saved', + 'Xmp.xmpMM.History[2]/stEvt:instanceID' : 'xmp.iid:14282d1f-7831-7043-ad77-1e10959ecd50', + 'Xmp.xmpMM.History[2]/stEvt:when' : '2020-04-06T00:55:07+08:00', + 'Xmp.xmpMM.History[2]/stEvt:softwareAgent' : 'Adobe Photoshop Camera Raw (Windows)', + 'Xmp.xmpMM.History[2]/stEvt:changed' : '/metadata', + 'Xmp.photoshop.DateCreated' : '2019-08-12T19:44:04Z', + 'Xmp.iptc.CreatorContactInfo' : 'type="Struct"', + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiTelWork' : '123456', + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiEmailWork': '123456@gmail.com', + 'Xmp.iptc.CreatorContactInfo/Iptc4xmpCore:CiUrlWork' : 'www.123456.com', + } RAW_XMP = """ tag1 tag2 tag3 test-中文- test-中文-, lang="de-DE" Hallo, Welt test-中文- test-中文- test-中文- diff --git a/pyexiv2/tests/test_func.py b/pyexiv2/tests/test_func.py index 5d255dc..ac2eafc 100644 --- a/pyexiv2/tests/test_func.py +++ b/pyexiv2/tests/test_func.py @@ -1,6 +1,7 @@ from .base import * +# First, test the most basic functionality: can it call exiv2 def test_version(): try: from .base import __exiv2_version__ @@ -9,6 +10,7 @@ def test_version(): ENV.skip_test = True raise + def test_open_img_by_path(): try: img = Image(ENV.test_img) @@ -240,10 +242,10 @@ def test_enableBMFF(): with Image(ENV.heic_img) as img: pass + def test_log_level(): with pytest.raises(RuntimeError): ENV.img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) set_log_level(4) ENV.img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) set_log_level(2) # recover the log level - diff --git a/pyexiv2/tests/test_func_on_convert.py b/pyexiv2/tests/test_func_on_convert.py new file mode 100644 index 0000000..3875b7b --- /dev/null +++ b/pyexiv2/tests/test_func_on_convert.py @@ -0,0 +1,40 @@ +from .base import * + + +EXIF = { + 'Exif.Image.Artist': 'test-中文-', + 'Exif.Image.Rating': '4', + } +XMP_CONVERTED_FROM_EXIF = { + 'Xmp.dc.creator': ['test-中文-'], + 'Xmp.xmp.Rating': '4', + } +IPTC = { + 'Iptc.Envelope.CharacterSet' : '\x1b%G', + 'Iptc.Application2.ObjectName' : 'test-中文-', + 'Iptc.Application2.Keywords' : ['tag1', 'tag2', 'tag3'], + } +XMP_CONVERTED_FROM_IPTC = { + 'Xmp.dc.title' : {'lang="x-default"': 'test-中文-'}, + 'Xmp.dc.subject': ['tag1', 'tag2', 'tag3'], + } + + +def test_convert_exif_to_xmp(): + result = convert_exif_to_xmp(EXIF) + diff_dict(XMP_CONVERTED_FROM_EXIF, result) + + +def test_convert_iptc_to_xmp(): + result = convert_iptc_to_xmp(IPTC) + diff_dict(XMP_CONVERTED_FROM_IPTC, result) + + +def test_convert_xmp_to_exif(): + result = convert_xmp_to_exif(XMP_CONVERTED_FROM_EXIF) + diff_dict(EXIF, result) + + +def test_convert_xmp_to_iptc(): + result = convert_xmp_to_iptc(XMP_CONVERTED_FROM_IPTC) + diff_dict(IPTC, result) diff --git a/pyexiv2/tests/test_perf.py b/pyexiv2/tests/test_perf.py index fc86343..b780d73 100644 --- a/pyexiv2/tests/test_perf.py +++ b/pyexiv2/tests/test_perf.py @@ -1,36 +1,44 @@ from .base import * -from . import test_func +from . import test_func, test_func_on_convert def test_memory_leak_when_reading(): - p = psutil.Process(os.getpid()) - m0 = p.memory_info().rss - for _ in range(1000): + process = psutil.Process(os.getpid()) + # memory_init = process.memory_info().rss + for i in range(100): + if i == 1: + memory_1 = process.memory_info().rss test_func.test_read_exif() test_func.test_read_iptc() test_func.test_read_xmp() test_func.test_read_raw_xmp() test_func.test_read_comment() test_func.test_read_icc() - m1 = p.memory_info().rss - delta = (m1 - m0) / 1024 / 1024 - assert delta < 1, 'Memory grew by {}MB, possibly due to the memory leak.'.format(delta) - # If img.close() hasn't been called, the memory can increase by more than 100MB. + test_func_on_convert.test_convert_exif_to_xmp() + test_func_on_convert.test_convert_iptc_to_xmp() + test_func_on_convert.test_convert_xmp_to_exif() + test_func_on_convert.test_convert_xmp_to_iptc() + memory_end = process.memory_info().rss + delta = (memory_end - memory_1) / 1024 + assert delta < 100, 'Memory grew by {}KB, a memory leak may have occurred.'.format(delta) + # If img.close() hasn't been called, the memory can increase by more than 10MB. check_img_md5() def test_memory_leak_when_writing(): - p = psutil.Process(os.getpid()) - m0 = p.memory_info().rss - for _ in range(1000): + process = psutil.Process(os.getpid()) + # memory_init = process.memory_info().rss + for i in range(100): + if i == 1: + memory_1 = process.memory_info().rss test_func.test_modify_exif() test_func.test_modify_iptc() test_func.test_modify_xmp() test_func.test_modify_comment() test_func.test_modify_icc() - m1 = p.memory_info().rss - delta = (m1 - m0) / 1024 / 1024 - assert delta < 1, 'Memory grew by {}MB, possibly due to the memory leak.'.format(delta) + memory_end = process.memory_info().rss + delta = (memory_end - memory_1) / 1024 + assert delta < 100, 'Memory grew by {}KB, a memory leak may have occurred.'.format(delta) def test_stack_overflow(): @@ -70,8 +78,8 @@ def test_transmit_various_characters(): def _test_thread_safe(): """ - Test whether pyexiv can successfully run multiple threads. - TODO: Could not catch the exception from the child thread. + Test whether pyexiv2 can successfully run multiple threads. + TODO: pyexiv2 is not thread-safe because in exiv2api.cpp, check_error_log() reads and writes to a global variable. """ import multiprocessing pool = multiprocessing.Pool(3) @@ -79,18 +87,3 @@ def _test_thread_safe(): pool.apply_async(test_memory_leak_when_reading, ()) pool.close() pool.join() - - -def _test_recovery_exif(): - """ - Test whether pyexiv2 can delete metadata and recover it completely. - TODO: complete it - """ - original_dict = ENV.img.read_exif() - ENV.img.clear_exif() - ENV.img.modify_exif(original_dict) - new_dict = ENV.img.read_exif() - for key in original_dict.keys(): - for key in original_dict.keys(): - assert original_dict[key] == new_dict.get(key), "{} didn't recover".format(key) - check_img_md5() diff --git a/setup.py b/setup.py index 7c22d1f..d870e03 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,7 @@ setuptools.setup( name='pyexiv2', - version='2.9.0', # need to set the variable in 'pyexiv2/__init__.py' + version='2.11.0', # need to set the variable in 'pyexiv2/__init__.py' author='LeoHsiao', author_email='leohsiao@foxmail.com', description='Read/Write metadata(including EXIF, IPTC, XMP), comment and ICC Profile embedded in digital images.',