From 30f5a8d6a1168158401f5a2388b99b6cf24b7c0f Mon Sep 17 00:00:00 2001 From: DasMoorhuhn Date: Mon, 8 Dec 2025 05:12:14 +0100 Subject: [PATCH] init push --- .gitignore | 3 ++ .python-version | 1 + README.md | 14 ++++++- examples/read_ifd_tags.py | 19 ++++++++++ pyproject.toml | 32 ++++++++++++++++ src/qifdp/__init__.py | 6 +++ src/qifdp/main.py | 78 +++++++++++++++++++++++++++++++++++++++ uv.lock | 8 ++++ 8 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 .python-version create mode 100644 examples/read_ifd_tags.py create mode 100644 pyproject.toml create mode 100644 src/qifdp/__init__.py create mode 100644 src/qifdp/main.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore index 36b13f1..a13181b 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,6 @@ cython_debug/ # PyPI configuration file .pypirc + +# ... +*.ARW diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/README.md b/README.md index 67e92fd..f03fd32 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,15 @@ # qifdp -QIFDP - Quick IFD Parser \ No newline at end of file +QIFDP - Quick IFD Parser + +Tested with: +- Sony ARW v4.0.1 + +# Build + +If you don't already have uv installed: `pip install uv` + +``` +uv build +pip install dist/qifdp-0.1.0.tar.gz +``` \ No newline at end of file diff --git a/examples/read_ifd_tags.py b/examples/read_ifd_tags.py new file mode 100644 index 0000000..ef98352 --- /dev/null +++ b/examples/read_ifd_tags.py @@ -0,0 +1,19 @@ +from qifdp import IFDTagMap +from qifdp import get_raw_ifd_tag + +file_path = "DSC00001.ARW" + +# Get the DateTimeOriginal +date = get_raw_ifd_tag(file_path=file_path) +print(date) + +# Get the camera brand name +make = get_raw_ifd_tag(file_path=file_path, tag_bytes=IFDTagMap.Make) +print(make) + +# If the buffer from 0x80000 (512k) inst enough for parsing the IFD tag, +# you can increase the buffer. Use either a hex value or an int. +# 0x80000 | 524288 +# 0x100000 | 1048576 +date = get_raw_ifd_tag(file_path=file_path, read_buffer=0x100000) +print(date) \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..712b13a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,32 @@ +[project] +name = "qifdp" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [] + +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools] +package-dir = {"" = "src"} + +[tool.setuptools.packages.find] +where = ["src"] + +[metadata] +name = "qidfp" +version = "0.1.0" +description = "A Python library to quickly parse IFD Tags" +long_description = "file:README.md" +long_description_content_type = "text/markdown" +author = "DasMoorhuhn" +author_email = "dasmoorhuhn@proton.me" +license = "GPLv3.0" +url = "https://github.com/yourusername/my-library" +classifiers = [ + "Programming Language :: Python :: 3", + "Operating System :: OS Independent", +] \ No newline at end of file diff --git a/src/qifdp/__init__.py b/src/qifdp/__init__.py new file mode 100644 index 0000000..702c535 --- /dev/null +++ b/src/qifdp/__init__.py @@ -0,0 +1,6 @@ +""" +A quick IFD parser +""" + +from .main import get_raw_ifd_tag +from .main import IFDTagMap \ No newline at end of file diff --git a/src/qifdp/main.py b/src/qifdp/main.py new file mode 100644 index 0000000..5014e42 --- /dev/null +++ b/src/qifdp/main.py @@ -0,0 +1,78 @@ +import struct + + +class IFDTagMap: + # Image tags + ImageWidth = b'\x01\x00' # 0x0100 + ImageLength = b'\x01\x01' # 0x0101 + Make = b'\x01\x0F' # 0x010F + Model = b'\x01\x10' # 0x0110 + Orientation = b'\x01\x12' # 0x0112 + XResolution = b'\x01\x1A' # 0x011A + YResolution = b'\x01\x1B' # 0x011B + ResolutionUnit = b'\x01\x28' # 0x0128 + Software = b'\x01\x31' # 0x0131 + DateTime = b'\x01\x32' # 0x0132 + Artist = b'\x01\xB0' # 0x01B0 + Copyright = b'\x82\x98' # 0x8298 + + # Exposure tags + ExposureTime = b'\x82\x9A' # 0x829A + FNumber = b'\x82\x9D' # 0x829D + ISOSpeedRatings = b'\x88\x69' # 0x8827 + ExposureBiasValue = b'\x92\x73' # 0x9273 + MeteringMode = b'\x92\x7C' # 0x927C + Flash = b'\x92\x19' # 0x9209 + FocalLength = b'\x92\x26' # 0x920A + + # Date/Time tags + DateTimeOriginal = b'\x90\x03' # 0x9003 + DateTimeDigitized = b'\x90\x04' # 0x9004 + SubsecTime = b'\x92\x00' # 0x9200 + SubsecTimeOriginal = b'\x92\x01' # 0x9201 + SubsecTimeDigitized = b'\x92\x02' # 0x9202 + + # Image dimensions and color + ColorSpace = b'\xA0\x13' # 0xA001 + PixelXDimension = b'\xA0\x02' # 0xA002 + PixelYDimension = b'\xA0\x03' # 0xA003 + + # Camera-specific / MakerNote + CameraOwnerName = b'\xC6\x0E' # 0xC60E + + +def get_raw_ifd_tag(file_path:str, read_buffer:int=0x80000, tag_bytes:bytes=IFDTagMap.DateTimeOriginal) -> str: + with open(file_path, "rb") as f: + f.seek(0, 2) + size_bytes = f.tell() + read_buffer = min(read_buffer, size_bytes) + f.seek(0) + data = f.read(read_buffer) + + # Try first LE then BE + offset = data.find(tag_bytes[::-1]) + endian = "<" + if offset == -1: + offset = data.find(tag_bytes) + endian = ">" + + if offset == -1: + raise ValueError("Tag 0x9003 not found in the file.") + + # Read type and count + type_, count = struct.unpack(endian + "HI", data[offset+2:offset+8]) + + # Read value/offset + value_or_offset = struct.unpack(endian + "I", data[offset+8:offset+12])[0] + + # If ASCII and count > 4, value_or_offset is the file offset of the string + if type_ == 2 and count > 4: + value_offset = value_or_offset + value_bytes = data[value_offset:value_offset+count] + value = value_bytes.decode('ascii') + else: + # Small values stored inline + value_bytes = data[offset+8:offset+8+count] + value = value_bytes.decode('ascii') + + return value diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..62834a1 --- /dev/null +++ b/uv.lock @@ -0,0 +1,8 @@ +version = 1 +revision = 3 +requires-python = ">=3.13" + +[[package]] +name = "qifdp" +version = "0.1.0" +source = { editable = "." }