diff --git a/.github/workflows/test_pip.yml b/.github/workflows/test_pip.yml index 4376c42..3ff8c13 100644 --- a/.github/workflows/test_pip.yml +++ b/.github/workflows/test_pip.yml @@ -8,9 +8,10 @@ jobs: Test_run: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-18.04, macOS-latest, windows-2019] - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - name: git pull uses: actions/checkout@v1 diff --git a/.github/workflows/test_run.yml b/.github/workflows/test_run.yml index ef6158f..2b3a285 100644 --- a/.github/workflows/test_run.yml +++ b/.github/workflows/test_run.yml @@ -6,9 +6,10 @@ jobs: Test_run: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-18.04, macOS-latest, windows-2019] - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.5, 3.6, 3.7, 3.8, 3.9] steps: - name: git pull uses: actions/checkout@v1 diff --git a/README.md b/README.md index 3100b87..7274cb7 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Read/Write metadata of digital image, including [EXIF](https://en.wikipedia.org/ ## Features - Base on C++ API of [Exiv2](https://www.exiv2.org/index.html) and invoke it through [pybind11](https://github.com/pybind/pybind11). -- Supports running on Linux, MacOS and Windows, with Python3(64bit, including `3.5` `3.6` `3.7` `3.8`). +- Supports running on Linux, MacOS and Windows, with Python3(64bit, including `3.5` `3.6` `3.7` `3.8` `3.9`). If you want to run pyexiv2 on another platform, please compile it yourself. See [lib](https://github.com/LeoHsiao1/pyexiv2/blob/master/pyexiv2/lib/README.md) - [Supports various metadata](https://www.exiv2.org/metadata.html) - [Supports various image formats](https://dev.exiv2.org/projects/exiv2/wiki/Supported_image_formats) diff --git a/docs/Tutorial-cn.md b/docs/Tutorial-cn.md index 7a502c9..7f88a16 100644 --- a/docs/Tutorial-cn.md +++ b/docs/Tutorial-cn.md @@ -13,9 +13,9 @@ class Image: def read_xmp(self, encoding='utf-8') -> dict def read_raw_xmp(self, encoding='utf-8') -> str - def modify_exif(self, dict_, encoding='utf-8') - def modify_iptc(self, dict_, encoding='utf-8') - def modify_xmp(self, dict_, encoding='utf-8') + def modify_exif(self, data: dict, encoding='utf-8') + def modify_iptc(self, data: dict, encoding='utf-8') + def modify_xmp(self, data: dict, encoding='utf-8') def clear_exif(self) def clear_iptc(self) @@ -27,22 +27,22 @@ class ImageData(Image): def __init__(self, data: bytes) def get_bytes(self) -> bytes -set_log_level() +set_log_level(level=2) ``` ## 类 Image - 类 `Image` 用于根据文件路径打开图片。例如: ```py - >>> from pyexiv2 import Image - >>> img = Image(r'.\pyexiv2\tests\1.jpg') + >>> import pyexiv2 + >>> img = pyexiv2.Image(r'.\pyexiv2\tests\1.jpg') >>> data = img.read_exif() >>> img.close() ``` - 当你处理完图片之后,请记得调用 `img.close()` ,以释放用于存储图片数据的内存。不调用该方法会导致内存泄漏,但不会锁定文件描述符。 - 通过 `with` 关键字打开图片时,它会自动关闭图片。例如: ```py - with Image(r'.\pyexiv2\tests\1.jpg') as img: + with pyexiv2.Image(r'.\pyexiv2\tests\1.jpg') as img: ims.read_exif() ``` @@ -50,8 +50,6 @@ set_log_level() - 示例: ```py - >>> from pyexiv2 import Image - >>> img = Image(r'.\pyexiv2\tests\1.jpg') >>> img.read_exif() {'Exif.Image.DateTime': '2019:06:23 19:45:17', 'Exif.Image.Artist': 'TEST', 'Exif.Image.Rating': '4', ...} >>> img.read_iptc() @@ -63,12 +61,12 @@ set_log_level() - pyexiv2 支持包含 Unicode 字符的图片路径、元数据。大部分函数都有一个默认参数:`encoding='utf-8'`。 如果你因为图片路径、元数据包含非 ASCII 码字符而遇到错误,请尝试更换编码。例如: ```python - img = Image(path, encoding='utf-8') - img = Image(path, encoding='GBK') - img = Image(path, encoding='ISO-8859-1') + img = pyexiv2.Image(path, encoding='utf-8') + img = pyexiv2.Image(path, encoding='GBK') + img = pyexiv2.Image(path, encoding='ISO-8859-1') ``` 另一个例子:中国地区的 Windows 电脑通常用 GBK 编码文件路径,因此它们不能被 utf-8 解码。 -- 使用`Image.read_*()`是安全的。这些方法永远不会影响图片文件。(md5不变) +- 使用 `Image.read_*()` 是安全的。这些方法永远不会影响图片文件(md5不变)。 - 如果 XMP 元数据包含 `\v` 或 `\f`,它将被空格 ` ` 代替。 - 元数据的读取速度与元数据的数量成反比,不管图片的大小如何。 @@ -76,7 +74,6 @@ set_log_level() - 示例: ```py - >>> img = Image(r'.\pyexiv2\tests\1.jpg') >>> # 准备要修改的XMP数据 >>> dict1 = {'Xmp.xmp.CreateDate': '2019-06-23T19:45:17.834', # 这将覆盖该标签的原始值,如果不存在该标签则将其添加 ... 'Xmp.xmp.Rating': ''} # 赋值一个空字符串会删除该标签 @@ -124,20 +121,20 @@ set_log_level() - 读取的示例: ```py with open(r'.\pyexiv2\tests\1.jpg', 'rb') as f: - with ImageData(f.read()) as img: + with pyexiv2.ImageData(f.read()) as img: data = img.read_exif() ``` - 修改的示例: ```py with open(r'.\pyexiv2\tests\1.jpg', 'rb+') as f: - with ImageData(f.read()) as img: + with pyexiv2.ImageData(f.read()) as img: changes = {'Iptc.Application2.ObjectName': 'test'} img.modify_iptc(changes) f.seek(0) # 获取图片的字节数据并保存到文件中 f.write(img.get_bytes()) f.seek(0) - with ImageData(f.read()) as img: + with pyexiv2.ImageData(f.read()) as img: result = img.read_iptc() ``` @@ -171,8 +168,6 @@ set_log_level() - `error` 日志会被转换成异常并抛出,其它日志则会被打印到 stdout 。 - 调用函数 `pyexiv2.set_log_level()` 可以设置处理日志的级别。例如: ```py - >>> import pyexiv2 - >>> img = pyexiv2.Image(r'.\pyexiv2\tests\1.jpg') >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) RuntimeError: XMP Toolkit error 102: Indexing applied to non-array Failed to encode XMP metadata. diff --git a/docs/Tutorial.md b/docs/Tutorial.md index 9222b5e..6b9239f 100644 --- a/docs/Tutorial.md +++ b/docs/Tutorial.md @@ -13,9 +13,9 @@ class Image: def read_xmp(self, encoding='utf-8') -> dict def read_raw_xmp(self, encoding='utf-8') -> str - def modify_exif(self, dict_, encoding='utf-8') - def modify_iptc(self, dict_, encoding='utf-8') - def modify_xmp(self, dict_, encoding='utf-8') + def modify_exif(self, data: dict, encoding='utf-8') + def modify_iptc(self, data: dict, encoding='utf-8') + def modify_xmp(self, data: dict, encoding='utf-8') def clear_exif(self) def clear_iptc(self) @@ -27,22 +27,22 @@ class ImageData(Image): def __init__(self, data: bytes) def get_bytes(self) -> bytes -set_log_level() +set_log_level(level=2) ``` ## class Image - Class `Image` is used to open an image based on the file path. For example: ```py - >>> from pyexiv2 import Image - >>> img = Image(r'.\pyexiv2\tests\1.jpg') + >>> import pyexiv2 + >>> img = pyexiv2.Image(r'.\pyexiv2\tests\1.jpg') >>> data = img.read_exif() >>> img.close() ``` - When you're done with the image, remember to call `img.close()` to free the memory for storing image data. Not calling this method causes a memory leak, but it doesn't lock the file descriptor. - Opening an image by keyword `with` will close the image automatically. For example: ```py - with Image(r'.\pyexiv2\tests\1.jpg') as img: + with pyexiv2.Image(r'.\pyexiv2\tests\1.jpg') as img: data = ims.read_exif() ``` @@ -50,7 +50,6 @@ set_log_level() - Sample: ```py - >>> img = Image(r'.\pyexiv2\tests\1.jpg') >>> img.read_exif() {'Exif.Image.DateTime': '2019:06:23 19:45:17', 'Exif.Image.Artist': 'TEST', 'Exif.Image.Rating': '4', ...} >>> img.read_iptc() @@ -67,7 +66,7 @@ set_log_level() img = Image(path, encoding='ISO-8859-1') ``` Another example: Windows computers in China usually encoded file paths by GBK, so they cannot be decoded by utf-8. -- It is safe to use `Image.read_*()`. These methods never affect image files. (md5 unchanged) +- It is safe to use `Image.read_*()`. These methods never affect image files (md5 unchanged). - If the XMP metadata contains `\v` or `\f`, it will be replaced with space ` `. - The speed of reading metadata is inversely proportional to the amount of metadata, regardless of the size of the image. @@ -75,7 +74,6 @@ set_log_level() - Sample: ```py - >>> img = Image(r'.\pyexiv2\tests\1.jpg') >>> # Prepare the XMP data you want to modify >>> dict1 = {'Xmp.xmp.CreateDate': '2019-06-23T19:45:17.834', # This will overwrite its original value, or add it if it doesn't exist ... 'Xmp.xmp.Rating': ''} # Set an empty str explicitly to delete the datum @@ -123,20 +121,20 @@ set_log_level() - Example of reading: ```py with open(r'.\pyexiv2\tests\1.jpg', 'rb') as f: - with ImageData(f.read()) as img: + with pyexiv2.ImageData(f.read()) as img: data = img.read_exif() ``` - Example of modifing: ```py with open(r'.\pyexiv2\tests\1.jpg', 'rb+') as f: - with ImageData(f.read()) as img: + with pyexiv2.ImageData(f.read()) as img: changes = {'Iptc.Application2.ObjectName': 'test'} img.modify_iptc(changes) f.seek(0) # Get the bytes data of the image and save it to the file f.write(img.get_bytes()) f.seek(0) - with ImageData(f.read()) as img: + with pyexiv2.ImageData(f.read()) as img: result = img.read_iptc() ``` @@ -170,13 +168,10 @@ set_log_level() - The `error` log will be converted to an exception and thrown. Other logs will be printed to stdout. - Call the function `pyexiv2.set_log_level()` to set the level of handling logs. For example: ```py - >>> import pyexiv2 - >>> img = pyexiv2.Image(r'.\pyexiv2\tests\1.jpg') - >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) + >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) # An error that was not caught is displayed RuntimeError: XMP Toolkit error 102: Indexing applied to non-array Failed to encode XMP metadata. >>> pyexiv2.set_log_level(4) - >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) - >>> img.close() + >>> img.modify_xmp({'Xmp.xmpMM.History': 'type="Seq"'}) # No error displayed ``` diff --git a/pyexiv2/core.py b/pyexiv2/core.py index 553a9fb..c9841b7 100644 --- a/pyexiv2/core.py +++ b/pyexiv2/core.py @@ -25,9 +25,9 @@ def close(self): """ Free the memory for storing image data. """ self.img.close_image() - # Disable all members + # Disable all methods and properties def closed_warning(): - raise RuntimeError('Do not operate on the closed image.') + raise RuntimeError('The image has been closed, so it is not allowed to operate.') for attr in dir(self): if not attr.startswith('__'): if callable(getattr(self, attr)): diff --git a/pyexiv2/lib/README.md b/pyexiv2/lib/README.md index d2b3e3a..90beccb 100644 --- a/pyexiv2/lib/README.md +++ b/pyexiv2/lib/README.md @@ -23,65 +23,66 @@ - The current release version of Exiv2 is `0.27.2`. - It is not necessary to compile all versions of exiv2api.cpp and save them to the git repository, except when release a new version. -## Compile steps on Darwin +## Compile steps on Linux 1. Download the release version of Exiv2, unpack it. - - Darwin : + - Linux64 : - For example: ```sh - cd /Users/leo/Documents/ - curl -O https://www.exiv2.org/builds/exiv2-0.27.2-Darwin.tar.gz - tar -zxvf exiv2-0.27.2-Darwin.tar.gz + cd /root/ + curl -O https://www.exiv2.org/builds/exiv2-0.27.2-Linux64.tar.gz + tar -zxvf exiv2-0.27.2-Linux64.tar.gz ``` 2. Prepare the environment: ```sh - EXIV2_DIR=/Users/leo/Documents/exiv2-0.27.2-Darwin - LIB_DIR=/Users/leo/Documents/pyexiv2/pyexiv2/lib/ - cp ${EXIV2_DIR}/lib/libexiv2.0.27.2.dylib ${LIB_DIR}/libexiv2.dylib + EXIV2_DIR=/root/exiv2-0.27.2-Linux64 # According to your download location + LIB_DIR=/root/pyexiv2/pyexiv2/lib/ + cp ${EXIV2_DIR}/lib/libexiv2.so.0.27.2 $LIB_DIR/libexiv2.so ``` 3. Set up the python interpreter. For example: ```sh py_version=8 + # docker run -it --rm --name python3.$py_version -e "py_version=$py_version" -e "EXIV2_DIR=$EXIV2_DIR" -e "LIB_DIR=$LIB_DIR" -v /root:/root python:3.$py_version-buster sh python3.$py_version -m pip install pybind11 ``` 4. Compile: ```sh cd $LIB_DIR - g++ exiv2api.cpp -o py3${py_version}-darwin/exiv2api.so -O3 -Wall -std=c++11 -shared -fPIC `python3.$py_version -m pybind11 --includes` -I ${EXIV2_DIR}/include -L ${EXIV2_DIR}/lib -l exiv2 -undefined dynamic_lookup + g++ exiv2api.cpp -o py3${py_version}-linux/exiv2api.so -O3 -Wall -std=c++11 -shared -fPIC `python3.$py_version -m pybind11 --includes` -I ${EXIV2_DIR}/include -L ${EXIV2_DIR}/lib -l exiv2 ``` -## Compile steps on Linux +## Compile steps on Darwin 1. Download the release version of Exiv2, unpack it. - - Linux64 : + - Darwin : - For example: ```sh - cd /root/pyexiv2/ - curl -O https://www.exiv2.org/builds/exiv2-0.27.2-Linux64.tar.gz - tar -zxvf exiv2-0.27.2-Linux64.tar.gz + cd /Users/leo/Documents/ + curl -O https://www.exiv2.org/builds/exiv2-0.27.2-Darwin.tar.gz + tar -zxvf exiv2-0.27.2-Darwin.tar.gz ``` 2. Prepare the environment: ```sh - EXIV2_DIR=/root/pyexiv2/exiv2-0.27.2-Linux64 # According to your download location - LIB_DIR=/root/pyexiv2/pyexiv2/lib/ - cp ${EXIV2_DIR}/lib/libexiv2.so.0.27.2 $LIB_DIR/libexiv2.so + EXIV2_DIR=/Users/leo/Documents/exiv2-0.27.2-Darwin + LIB_DIR=/Users/leo/Documents/pyexiv2/pyexiv2/lib/ + cp ${EXIV2_DIR}/lib/libexiv2.0.27.2.dylib ${EXIV2_DIR}/lib/libexiv2.dylib + cp ${EXIV2_DIR}/lib/libexiv2.0.27.2.dylib ${LIB_DIR}/libexiv2.dylib ``` 3. Set up the python interpreter. For example: ```sh py_version=8 - docker run -it --rm --name python3.$py_version -e "py_version=$py_version" -e "EXIV2_DIR=$EXIV2_DIR" -e "LIB_DIR=$LIB_DIR" -v /root/pyexiv2:/root/pyexiv2 python:3.$py_version bash python3.$py_version -m pip install pybind11 ``` 4. Compile: ```sh cd $LIB_DIR - g++ exiv2api.cpp -o py3${py_version}-linux/exiv2api.so -O3 -Wall -std=c++11 -shared -fPIC `python3.$py_version -m pybind11 --includes` -I ${EXIV2_DIR}/include -L ${EXIV2_DIR}/lib -l exiv2 + g++ exiv2api.cpp -o py3${py_version}-darwin/exiv2api.so -O3 -Wall -std=c++11 -shared -fPIC `python3.$py_version -m pybind11 --includes` -I ${EXIV2_DIR}/include -L ${EXIV2_DIR}/lib -l exiv2 -undefined dynamic_lookup ``` ## Compile steps on Windows @@ -102,7 +103,8 @@ 4. Compile: ```batch set py_version=8 - cl /MD /LD exiv2api.cpp /EHsc -I %EXIV2_DIR%\include -I C:\Users\Leo\AppData\Local\Programs\Python\Python3%py_version%\include /link %EXIV2_DIR%\lib\exiv2.lib C:\Users\Leo\AppData\Local\Programs\Python\Python3%py_version%\libs\python3%py_version%.lib /OUT:py3%py_version%-win\exiv2api.pyd + set py_home=C:\Users\Leo\AppData\Local\Programs\Python\Python3%py_version% + cl /MD /LD exiv2api.cpp /EHsc -I %EXIV2_DIR%\include -I %py_home%\include -I %py_home%\Lib\site-packages\pybind11\include /link %EXIV2_DIR%\lib\exiv2.lib %py_home%\libs\python3%py_version%.lib /OUT:py3%py_version%-win\exiv2api.pyd del exiv2api.exp exiv2api.obj exiv2api.lib ``` - Modify the path here according to your installation location. + - Modify the path here according to your installation location. diff --git a/pyexiv2/lib/__init__.py b/pyexiv2/lib/__init__.py index 5446283..8949696 100644 --- a/pyexiv2/lib/__init__.py +++ b/pyexiv2/lib/__init__.py @@ -4,12 +4,14 @@ import platform -# Check the Python interpreter if platform.architecture()[0] != '64bit': raise RuntimeError('pyexiv2 can only run on 64-bit python3 interpreter.') + +# Check the Python interpreter py_version = platform.python_version()[:3] -if py_version not in ['3.5', '3.6', '3.7', '3.8']: - raise RuntimeError('pyexiv2 only supports these Python versions: 3.5, 3.6, 3.7, 3.8 , but your version is {} .'.format(py_version)) +expected_py_version = ['3.5', '3.6', '3.7', '3.8', '3.9'] +if py_version not in expected_py_version: + raise RuntimeError('pyexiv2 only supports these Python versions: {} . But your version is {} .'.format(expected_py_version, py_version)) lib_dir = os.path.dirname(__file__) diff --git a/pyexiv2/lib/py39-darwin/__init__.py b/pyexiv2/lib/py39-darwin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyexiv2/lib/py39-darwin/exiv2api.so b/pyexiv2/lib/py39-darwin/exiv2api.so new file mode 100644 index 0000000..1112045 Binary files /dev/null and b/pyexiv2/lib/py39-darwin/exiv2api.so differ diff --git a/pyexiv2/lib/py39-linux/__init__.py b/pyexiv2/lib/py39-linux/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyexiv2/lib/py39-linux/exiv2api.so b/pyexiv2/lib/py39-linux/exiv2api.so new file mode 100644 index 0000000..8add36f Binary files /dev/null and b/pyexiv2/lib/py39-linux/exiv2api.so differ diff --git a/pyexiv2/lib/py39-win/__init__.py b/pyexiv2/lib/py39-win/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyexiv2/lib/py39-win/exiv2api.pyd b/pyexiv2/lib/py39-win/exiv2api.pyd new file mode 100644 index 0000000..532fff3 Binary files /dev/null and b/pyexiv2/lib/py39-win/exiv2api.pyd differ diff --git a/setup.py b/setup.py index ee71883..248607e 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ setuptools.setup( name='pyexiv2', - version='2.3.1', + version='2.3.2', author='LeoHsiao', author_email='leohsiao@foxmail.com', description='Read/Write metadata of digital image, including EXIF, IPTC, XMP.', @@ -29,6 +29,7 @@ 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', ], )