Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/linting.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.10"
python-version: "3.11"

- name: Cache dependencies
uses: actions/cache@v3
Expand All @@ -30,7 +30,7 @@ jobs:
- name: Install dependencies
run: |
pip install virtualenv
make venv reqs-install
make venv install-dev

- name: Static analysis of the code
run: |
Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,13 +40,16 @@ jobs:

- name: Install
run: |
pip install -e .
pip install virtualenv
make venv install

- name: Run in debug and color mode
run: |
source .venv/bin/activate
make test

- name: Compare image processing output
run: |
source .venv/bin/activate
make test-diff

37 changes: 23 additions & 14 deletions .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,11 @@ ignore-paths=
# Emacs file locks
ignore-patterns=^\.#

# List of module names for which member attributes should not be checked
# (useful for modules/projects where namespaces are manipulated during runtime
# and thus existing member attributes cannot be deduced by static analysis). It
# supports qualified module names, as well as Unix pattern matching.
# List of module names for which member attributes should not be checked and
# will not be imported (useful for modules/projects where namespaces are
# manipulated during runtime and thus existing member attributes cannot be
# deduced by static analysis). It supports qualified module names, as well as
# Unix pattern matching.
ignored-modules=

# Python code to execute, usually for sys.path manipulation such as
Expand All @@ -86,6 +87,10 @@ load-plugins=
# Pickle collected data for later comparisons.
persistent=yes

# Resolve imports to .pyi stubs if available. May reduce no-member messages and
# increase not-an-iterable messages.
prefer-stubs=no

# Minimum Python version to use for version dependent checks. Will default to
# the version used to run pylint.
py-version=3.7
Expand Down Expand Up @@ -190,10 +195,7 @@ good-names=i,
k,
ex,
Run,
_,
s,
fh,
EXIF
_

# Good variable names regexes, separated by a comma. If names match any regex,
# they will always be accepted
Expand Down Expand Up @@ -305,6 +307,9 @@ max-locals=25
# Maximum number of parents for a class (see R0901).
max-parents=7

# Maximum number of positional arguments for function / method.
max-positional-arguments=10

# Maximum number of public methods for a class (see R0904).
max-public-methods=20

Expand Down Expand Up @@ -429,13 +434,12 @@ disable=raw-checker-failed,
locally-disabled,
file-ignored,
suppressed-message,
useless-suppression,
deprecated-pragma,
use-symbolic-message-instead,
use-implicit-booleaness-not-comparison-to-string,
use-implicit-booleaness-not-comparison-to-zero,
consider-using-f-string,
missing-function-docstring,
consider-using-f-string,
fixme,

# Enable the message, report, category or checker with the given id(s). You can
Expand Down Expand Up @@ -474,6 +478,11 @@ max-nested-blocks=5
# printed.
never-returning-functions=sys.exit,argparse.parse_error

# Let 'consider-using-join' be raised when the separator to join on would be
# non-empty (resulting in expected fixes of the type: ``"- " + " -
# ".join(items)``)
suggest-join-with-non-empty-separator=yes


[REPORTS]

Expand All @@ -488,10 +497,10 @@ evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor
# used to format the message information. See doc for all details.
msg-template=

# Set the output format. Available formats are: text, parseable, colorized,
# json2 (improved json format), json (old json format) and msvs (visual
# studio). You can also give a reporter class, e.g.
# mypackage.mymodule.MyReporterClass.
# Set the output format. Available formats are: 'text', 'parseable',
# 'colorized', 'json2' (improved json format), 'json' (old json format), msvs
# (visual studio) and 'github' (GitHub actions). You can also give a reporter
# class, e.g. mypackage.mymodule.MyReporterClass.
#output-format=

# Tells whether to display a full report or only the messages.
Expand Down
9 changes: 8 additions & 1 deletion ChangeLog.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
EXIF.py Change Log
##################

3.1.0 — 2023-05-??
3.1.0 — 2025-04-25
* Add more typing definitions (#181)
* Put all test files in the repo (#208)
* use pre-commit to run black, isort, pylint, mypy (#209)
* Canon MakerNote: Allow callable to process tag value (#189) by Daan van Gorkum
* Added missing HEIC box names and handling of a TIFF header inside HEIC (#173) by Antti Ketola
* don't let debug logging trigger an exception (#196) by David Bonner
* fix for certain box names not handled, but skipping would generate valid output by Anand Mahesh
* Add DJI makernotes, extract_thumbnail parameter (#168) by Piero Toffanin
* Fix endianess bug while reading DJI makernotes, add Make tag (#169) by Piero Toffanin
* Make CI pass (#178) by Nick Dimitroff
Expand Down
31 changes: 0 additions & 31 deletions LICENSE.txt

This file was deleted.

7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@ test-diff: ## Run and compare exif dump
analyze: ## Run all static analysis tools
$(PRE_COMMIT_BIN) run --all

reqs-install: ## Install with all requirements
$(PIP_INSTALL) .[dev]
install-dev: ## Install with all development requirements
$(PIP_INSTALL) -U -e .[dev]

install: ## Install with basic requirements
$(PIP_INSTALL) -U -e .

build: ## build distribution
rm -fr ./dist
Expand Down
15 changes: 7 additions & 8 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ EXIF.py

Easy to use Python module to extract Exif metadata from digital image files.

Pure Python, lightweight, no dependencies.

Supported formats: TIFF, JPEG, PNG, Webp, HEIC


Expand All @@ -12,10 +14,6 @@ Compatibility

EXIF.py is tested and officially supported on Python 3.7 to 3.13

Starting with version ``3.0.0``, Python2 compatibility is dropped *completely* (syntax errors due to type hinting).

https://pythonclock.org/


Installation
************
Expand All @@ -36,9 +34,9 @@ Development Version

After cloning the repo, use the provided Makefile::

make venv reqs-install
make venv install-dev

Which will install a virtual environment and install development dependencies.
Which will create a virtual environment and install development dependencies.

Usage
*****
Expand Down Expand Up @@ -163,7 +161,8 @@ Pass the ``-s`` or ``--strict`` argument, or as:
Built-in Types
==============

For easier serialization and programmatic use, this option returns a dictionary with values in built-in Python types (int, float, str, bytes, list, None) instead of `IfdTag` objects.
For easier serialization and programmatic use, this option returns a dictionary with values in built-in Python types
(int, float, str, bytes, list, None) instead of `IfdTag` objects.

Pass the ``-b`` or ``--builtin`` argument, or as:

Expand Down Expand Up @@ -226,7 +225,7 @@ License

Copyright © 2002-2007 Gene Cash

Copyright © 2007-2023 Ianaré Sévi and contributors
Copyright © 2007-2025 Ianaré Sévi and contributors

A **huge** thanks to all the contributors over the years!

Expand Down
23 changes: 14 additions & 9 deletions exifread/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"""

import struct
from typing import BinaryIO, Dict, Tuple
from typing import Any, BinaryIO, Dict, Tuple

from exifread.classes import ExifHeader
from exifread.exceptions import ExifNotFound, InvalidExif
Expand Down Expand Up @@ -37,8 +37,13 @@ def _find_tiff_exif(fh: BinaryIO) -> Tuple[int, bytes]:
return offset, endian


def _find_heic_tiff(fh: BinaryIO) -> tuple:
"""In some HEIC files, the Exif offset is 0 and there is a plain TIFF header near end of the file."""
def _find_heic_tiff(fh: BinaryIO) -> Tuple[int, bytes]:
"""
Look for TIFF header in HEIC files.

In some HEIC files, the Exif offset is 0,
and yet there is a plain TIFF header near end of the file.
"""

data = fh.read(4)
if data[0:2] in [b"II", b"MM"] and data[2] == 42 and data[3] == 0:
Expand Down Expand Up @@ -126,7 +131,7 @@ def _get_xmp(fh: BinaryIO) -> bytes:
return xmp_bytes


def _determine_type(fh: BinaryIO) -> tuple:
def _determine_type(fh: BinaryIO) -> Tuple[int, bytes, int]:
# by default do not fake an EXIF beginning
fake_exif = 0

Expand Down Expand Up @@ -163,7 +168,7 @@ def process_file(
auto_seek=True,
extract_thumbnail=True,
builtin_types=False,
) -> dict:
) -> Dict[str, Any]:
"""
Process an image file (expects an open file object).

Expand All @@ -175,22 +180,22 @@ def process_file(
fh.seek(0)

try:
offset, endian, fake_exif = _determine_type(fh)
offset, endian_bytes, fake_exif = _determine_type(fh)
except ExifNotFound as err:
logger.warning(err)
return {}
except InvalidExif as err:
logger.debug(err)
return {}

endian = chr(ord_(endian[0]))
endian_str = chr(ord_(endian_bytes[0]))
# deal with the EXIF info we found
logger.debug(
"Endian format is %s (%s)", endian, ENDIAN_TYPES.get(endian, "Unknown")
"Endian format is %s (%s)", endian_str, ENDIAN_TYPES.get(endian_str, "Unknown")
)

hdr = ExifHeader(
fh, endian, offset, fake_exif, strict, debug, details, truncate_tags
fh, endian_str, offset, fake_exif, strict, debug, details, truncate_tags
)
ifd_list = hdr.list_ifd()
thumb_ifd = 0
Expand Down
6 changes: 6 additions & 0 deletions exifread/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Main entrypoint for the module."""

from exifread.cli import main

if __name__ == "__main__":
main()
6 changes: 4 additions & 2 deletions exifread/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def __init__(
file_handle: BinaryIO,
endian: str,
offset: int,
fake_exif,
fake_exif: int,
strict: bool,
debug=False,
detailed=True,
Expand Down Expand Up @@ -431,7 +431,9 @@ def extract_tiff_thumbnail(self, thumb_ifd: int) -> None:
old_offset = self.s2n(entry + 8, 4)
# start of the 4-byte pointer area in entry
ptr = i * 12 + 18
# remember strip offsets location

# remember strip offsets
strip_len = 0
if tag == 0x0111:
strip_off = ptr
strip_len = count * type_length
Expand Down
10 changes: 7 additions & 3 deletions EXIF.py → exifread/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
#
#
# Copyright (c) 2002-2007 Gene Cash
# Copyright (c) 2007-2023 Ianaré Sévi and contributors
# Copyright (c) 2007-2025 Ianaré Sévi and contributors
#
# See LICENSE.txt file for licensing information
# See ChangeLog.rst file for all contributors and changes
Expand Down Expand Up @@ -91,7 +91,7 @@ def get_args() -> argparse.Namespace:
return args


def main(args) -> None:
def run_cli(args: argparse.Namespace) -> None:
"""Extract tags based on options (args)."""

exif_log.setup_logger(args.debug, args.color)
Expand Down Expand Up @@ -159,5 +159,9 @@ def main(args) -> None:
print()


def main() -> None:
run_cli(get_args())


if __name__ == "__main__":
main(get_args())
main()
6 changes: 3 additions & 3 deletions exifread/jpeg.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Extract EXIF from JPEG files."""

from typing import BinaryIO
from typing import BinaryIO, Tuple

from exifread.exceptions import InvalidExif
from exifread.exif_log import get_logger
Expand All @@ -13,7 +13,7 @@ def _increment_base(data, base) -> int:
return ord_(data[base + 2]) * 256 + ord_(data[base + 3]) + 2


def _get_initial_base(fh: BinaryIO, data, fake_exif) -> tuple:
def _get_initial_base(fh: BinaryIO, data: bytes, fake_exif: int) -> Tuple[int, int]:
base = 2
logger.debug(
"data[2]=0x%X data[3]=0x%X data[6:10]=%s",
Expand Down Expand Up @@ -151,7 +151,7 @@ def _get_base(base: int, data: bytes) -> int:
return base


def find_jpeg_exif(fh: BinaryIO, data: bytes, fake_exif) -> tuple:
def find_jpeg_exif(fh: BinaryIO, data: bytes, fake_exif: int) -> Tuple[int, bytes, int]:
logger.debug(
"JPEG format recognized data[0:2]=0x%X%X", ord_(data[0]), ord_(data[1])
)
Expand Down
Empty file added exifread/py.typed
Empty file.
Loading