Skip to content

Commit

Permalink
Merge pull request #130 from LeoHsiao1/dev
Browse files Browse the repository at this point in the history
Ready to release v2.11.0Dev
  • Loading branch information
LeoHsiao1 authored Dec 13, 2023
2 parents b24a99a + d26ddf6 commit 6836f8d
Show file tree
Hide file tree
Showing 12 changed files with 522 additions and 226 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/test_package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand All @@ -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: |
Expand Down
19 changes: 18 additions & 1 deletion docs/Tutorial-cn.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,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
Expand Down Expand Up @@ -216,7 +224,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)中。
Expand Down Expand Up @@ -290,3 +297,13 @@ def set_log_level(level=2)
>>> pyexiv2.set_log_level(4)
>>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) # 此时没有错误日志
```

## convert

- Exiv2 支持将某些 EXIFIPTC 标签,转换成 XMP 标签,也支持反向转换。参考:<https://github.com/Exiv2/exiv2/blob/v0.27.7/src/convert.cpp#L313>
- 示例:
```py
>>> pyexiv2.convert_exif_to_xmp({'Exif.Image.Artist': 'test-中文-', 'Exif.Image.Rating': '4'})
{'Xmp.dc.creator': ['test-中文-'], 'Xmp.xmp.Rating': '4'}
```

19 changes: 18 additions & 1 deletion docs/Tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,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
Expand Down Expand Up @@ -190,7 +198,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()
Expand Down Expand Up @@ -289,3 +297,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: <https://github.com/Exiv2/exiv2/blob/v0.27.7/src/convert.cpp#L313>
- For example:
```py
>>> pyexiv2.convert_exif_to_xmp({'Exif.Image.Artist': 'test-中文-', 'Exif.Image.Rating': '4'})
{'Xmp.dc.creator': ['test-中文-'], 'Xmp.xmp.Rating': '4'}
```
10 changes: 9 additions & 1 deletion pyexiv2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]
138 changes: 138 additions & 0 deletions pyexiv2/convert.py
Original file line number Diff line number Diff line change
@@ -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)

Loading

0 comments on commit 6836f8d

Please sign in to comment.