Skip to content

capturegraph.data.load.types.image #

ImageFile - Simple container for image path with EXIF metadata#

A simple dataclass that bundles an image path with its EXIF metadata.

Example
import capturegraph.data as cg

# Load EXIF directly
exif = cg.load_exif("photo.dng")
print(exif.DateTimeOriginal)  # "2024:01:15 10:30:00"

# Or wrap a path with its EXIF
img = cg.ImageFile("photo.dng")
print(img.path)                    # Path("photo.dng")
print(img.exif.DateTimeOriginal)   # "2024:01:15 10:30:00"
Notes
  • Uses exifread library for compatibility with RAW formats (DNG, CR2, NEF, etc.)
  • EXIF tag names are available without their group prefix (e.g., DateTimeOriginal instead of EXIF DateTimeOriginal)
  • Returns MissingType for non-existent tags (for safe chaining with List)

ImageFile dataclass #

An image file with its path and EXIF metadata.

A simple container that bundles a Path with its EXIF Dict. Use ImageFile() to create one from a file path.

Attributes:

Name Type Description
path Path

The path to the image file.

exif Dict

Dict of EXIF metadata with attribute-style access.

Example
img = ImageFile("photo.dng")
img.path  # PosixPath('photo.dng')
img.image_type  # 'dng'
img.exif.DateTimeOriginal  # '2024:01:15 10:30:00'

# Convert to other formats
jpeg = img.save_jpeg(Path("output.jpg"))
heif = img.save_heif(Path("output.heic"))
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
@dataclass
class ImageFile:
    """An image file with its path and EXIF metadata.

    A simple container that bundles a Path with its EXIF Dict.
    Use `ImageFile()` to create one from a file path.

    Attributes:
        path: The path to the image file.
        exif: Dict of EXIF metadata with attribute-style access.

    Example:
        ```python
        img = ImageFile("photo.dng")
        img.path  # PosixPath('photo.dng')
        img.image_type  # 'dng'
        img.exif.DateTimeOriginal  # '2024:01:15 10:30:00'

        # Convert to other formats
        jpeg = img.save_jpeg(Path("output.jpg"))
        heif = img.save_heif(Path("output.heic"))
        ```
    """

    path: Path
    exif: Dict

    @property
    def image_type(self) -> str:
        """Return the image type based on file extension.

        Returns:
            One of 'dng', 'heif', 'jpeg', or 'png'.

        Example:
            ```python
            img = ImageFile("photo.dng")
            img.image_type  # 'dng'

            img2 = ImageFile("photo.heic")
            img2.image_type  # 'heif'
            ```
        """
        suffix = self.path.suffix.lower()
        if suffix == ".dng":
            return "dng"
        elif suffix in {".heic", ".heif"}:
            return "heif"
        elif suffix == ".png":
            return "png"
        elif suffix in {".jpg", ".jpeg"}:
            return "jpeg"
        else:
            return "jpeg"

    def __init__(self, path: Path | str):
        """Create an ImageFile from a path, loading its EXIF metadata.

        Args:
            path: Path to the image file.
        """
        self.path = Path(path).resolve()
        self.exif = load_exif(self.path)

    def pil_image(self, max_axis: int = None) -> Image:
        """Open and return the image as a PIL Image.

        Handles all supported formats:
        - DNG: Uses dng_to_pil adapter (rawpy with high-quality settings)
        - HEIF: Uses pillow-heif opener
        - JPEG/PNG: Uses PIL directly

        Returns:
            PIL.Image.Image: The opened image.

        Note:
            Remember to call `.close()` on the returned image when done,
            or use it as a context manager.

        Example:
            ```python
            img = ImageFile("photo.heic")
            pil = img.pil_image
            pil.thumbnail((256, 256))
            pil.save("thumb.jpg")
            pil.close()
            ```
        """
        if self.image_type == "dng":
            from capturegraph.adapters.dng_convert import dng_to_pil

            return dng_to_pil(self.path)
        elif self.image_type == "heif":
            # Register HEIF opener
            register_heif_opener()

        img = Image.open(self.path)

        if max_axis is not None:
            # Calculate new size maintaining aspect ratio
            width, height = img.size
            if width > height:
                new_width = max_axis
                new_height = int(height * max_axis / width)
            else:
                new_height = max_axis
                new_width = int(width * max_axis / height)

            # Resize using high-quality resampling
            img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

        return img

    def save_jpeg(self, path: Path, max_axis: int = None) -> "ImageFile":
        """Convert and save the image as JPEG.

        If already JPEG and no resizing is needed, copies the file without re-encoding.
        DNG files use specialized conversion; other formats use PIL.

        Args:
            path: Path for the output JPEG file.
            max_axis: If provided, resize so the largest dimension equals this value.

        Returns:
            New ImageFile pointing to the created JPEG.

        Example:
            ```python
            img = ImageFile("photo.dng")
            jpeg = img.save_jpeg(Path("output.jpg"))
            jpeg.image_type  # 'jpeg'

            # Save a smaller version for analysis
            small = img.save_jpeg(Path("thumb.jpg"), max_axis=1024)
            ```
        """
        path = Path(path).resolve()
        path.parent.mkdir(parents=True, exist_ok=True)

        if self.image_type == "jpeg" and max_axis is None:
            # Same format, no resize - just copy
            shutil.copy2(self.path, path)
        elif self.image_type == "dng":
            from capturegraph.adapters.dng_convert import dng_to_jpeg

            dng_to_jpeg(self.path, path, max_axis=max_axis)
        else:
            # HEIF, PNG, etc - use PIL
            img = self.pil_image(max_axis=max_axis)
            if img.mode in ("RGBA", "LA", "P"):
                img = img.convert("RGB")
            img.save(
                path,
                format="JPEG",
                quality=100,
                subsampling=0,
                optimize=True,
            )
            img.close()

        return ImageFile(path)

    def save_heif(self, path: Path, max_axis: int = None) -> "ImageFile":
        """Convert and save the image as HEIF.

        If already HEIF and no resizing is needed, copies the file without re-encoding.
        DNG files use specialized conversion; other formats use PIL.

        Args:
            path: Path for the output HEIF file (.heic extension).
            max_axis: If provided, resize so the largest dimension equals this value.

        Returns:
            New ImageFile pointing to the created HEIF.

        Example:
            ```python
            img = ImageFile("photo.dng")
            heif = img.save_heif(Path("output.heic"))
            heif.image_type  # 'heif'

            # Save a smaller version for analysis
            small = img.save_heif(Path("thumb.heic"), max_axis=1024)
            ```
        """
        path = Path(path).resolve()
        path.parent.mkdir(parents=True, exist_ok=True)

        if self.image_type == "heif" and max_axis is None:
            # Same format, no resize - just copy
            shutil.copy2(self.path, path)
        elif self.image_type == "dng":
            from capturegraph.adapters.dng_convert import dng_to_heif

            dng_to_heif(self.path, path, max_axis=max_axis)
        else:
            # JPEG, PNG, etc - use PIL
            img = self.pil_image(max_axis=max_axis)
            img.save(
                path,
                format="HEIF",
                quality=100,
            )
            img.close()

        return ImageFile(path)

    def save_png(self, path: Path, max_axis: int = None) -> "ImageFile":
        """Convert and save the image as PNG (lossless).

        If already PNG and no resizing is needed, copies the file without re-encoding.
        DNG files use specialized conversion; other formats use PIL.

        Args:
            path: Path for the output PNG file.
            max_axis: If provided, resize so the largest dimension equals this value.

        Returns:
            New ImageFile pointing to the created PNG.

        Example:
            ```python
            img = ImageFile("photo.dng")
            png = img.save_png(Path("output.png"))
            png.image_type  # 'png'

            # Save a smaller version for analysis
            small = img.save_png(Path("thumb.png"), max_axis=1024)
            ```
        """
        path = Path(path).resolve()
        path.parent.mkdir(parents=True, exist_ok=True)

        if self.image_type == "png" and max_axis is None:
            # Same format, no resize - just copy
            shutil.copy2(self.path, path)
        elif self.image_type == "dng":
            from capturegraph.adapters.dng_convert import dng_to_png

            dng_to_png(self.path, path, max_axis=max_axis)
        else:
            # HEIF, JPEG, etc - use PIL
            img = self.pil_image(max_axis=max_axis)
            img.save(
                path,
                format="PNG",
                optimize=True,
            )
            img.close()

        return ImageFile(path)

    def tooltip(self, max_axis: int = 64) -> str:
        """Create a base64-encoded thumbnail for Altair/Vega tooltips.

        Resizes the image so its largest dimension is `max_axis`, then
        encodes it as a base64 PNG data URL suitable for Vega tooltips.

        Supports both standard formats (JPEG, PNG, HEIC) and RAW formats
        (DNG, CR2, NEF, etc.) using rawpy.

        Args:
            max_axis: Maximum dimension (width or height) of the thumbnail.

        Returns:
            A data URL string like "data:image/png;base64,..." that can be
            used directly in Altair/Vega tooltip specifications.

        Example:
            ```python
            img = ImageFile("photo.jpg")
            img.tooltip(64)  # 'data:image/png;base64,iVBORw0KGgo...'

            # Works with RAW files too
            raw = ImageFile("photo.dng")
            raw.tooltip(64)  # 'data:image/png;base64,iVBORw0KGgo...'
            ```
        """

        with self.pil_image(max_axis=max_axis) as img:
            # Convert to RGB if necessary (for PNG encoding)
            if img.mode in ("RGBA", "LA", "P"):
                img = img.convert("RGB")

            # Encode to base64
            buffered = BytesIO()
            img.save(buffered, format="PNG")
            img_str = base64.b64encode(buffered.getvalue()).decode()

        return f"data:image/png;base64,{img_str}"

    def __repr__(self) -> str:
        return f"ImageFile({str(self.path)!r})"

image_type property #

Return the image type based on file extension.

Returns:

Type Description
str

One of 'dng', 'heif', 'jpeg', or 'png'.

Example
img = ImageFile("photo.dng")
img.image_type  # 'dng'

img2 = ImageFile("photo.heic")
img2.image_type  # 'heif'

__init__(path) #

Create an ImageFile from a path, loading its EXIF metadata.

Parameters:

Name Type Description Default
path Path | str

Path to the image file.

required
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def __init__(self, path: Path | str):
    """Create an ImageFile from a path, loading its EXIF metadata.

    Args:
        path: Path to the image file.
    """
    self.path = Path(path).resolve()
    self.exif = load_exif(self.path)

pil_image(max_axis=None) #

Open and return the image as a PIL Image.

Handles all supported formats: - DNG: Uses dng_to_pil adapter (rawpy with high-quality settings) - HEIF: Uses pillow-heif opener - JPEG/PNG: Uses PIL directly

Returns:

Type Description
Image

PIL.Image.Image: The opened image.

Note

Remember to call .close() on the returned image when done, or use it as a context manager.

Example
img = ImageFile("photo.heic")
pil = img.pil_image
pil.thumbnail((256, 256))
pil.save("thumb.jpg")
pil.close()
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def pil_image(self, max_axis: int = None) -> Image:
    """Open and return the image as a PIL Image.

    Handles all supported formats:
    - DNG: Uses dng_to_pil adapter (rawpy with high-quality settings)
    - HEIF: Uses pillow-heif opener
    - JPEG/PNG: Uses PIL directly

    Returns:
        PIL.Image.Image: The opened image.

    Note:
        Remember to call `.close()` on the returned image when done,
        or use it as a context manager.

    Example:
        ```python
        img = ImageFile("photo.heic")
        pil = img.pil_image
        pil.thumbnail((256, 256))
        pil.save("thumb.jpg")
        pil.close()
        ```
    """
    if self.image_type == "dng":
        from capturegraph.adapters.dng_convert import dng_to_pil

        return dng_to_pil(self.path)
    elif self.image_type == "heif":
        # Register HEIF opener
        register_heif_opener()

    img = Image.open(self.path)

    if max_axis is not None:
        # Calculate new size maintaining aspect ratio
        width, height = img.size
        if width > height:
            new_width = max_axis
            new_height = int(height * max_axis / width)
        else:
            new_height = max_axis
            new_width = int(width * max_axis / height)

        # Resize using high-quality resampling
        img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)

    return img

save_jpeg(path, max_axis=None) #

Convert and save the image as JPEG.

If already JPEG and no resizing is needed, copies the file without re-encoding. DNG files use specialized conversion; other formats use PIL.

Parameters:

Name Type Description Default
path Path

Path for the output JPEG file.

required
max_axis int

If provided, resize so the largest dimension equals this value.

None

Returns:

Type Description
ImageFile

New ImageFile pointing to the created JPEG.

Example
img = ImageFile("photo.dng")
jpeg = img.save_jpeg(Path("output.jpg"))
jpeg.image_type  # 'jpeg'

# Save a smaller version for analysis
small = img.save_jpeg(Path("thumb.jpg"), max_axis=1024)
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def save_jpeg(self, path: Path, max_axis: int = None) -> "ImageFile":
    """Convert and save the image as JPEG.

    If already JPEG and no resizing is needed, copies the file without re-encoding.
    DNG files use specialized conversion; other formats use PIL.

    Args:
        path: Path for the output JPEG file.
        max_axis: If provided, resize so the largest dimension equals this value.

    Returns:
        New ImageFile pointing to the created JPEG.

    Example:
        ```python
        img = ImageFile("photo.dng")
        jpeg = img.save_jpeg(Path("output.jpg"))
        jpeg.image_type  # 'jpeg'

        # Save a smaller version for analysis
        small = img.save_jpeg(Path("thumb.jpg"), max_axis=1024)
        ```
    """
    path = Path(path).resolve()
    path.parent.mkdir(parents=True, exist_ok=True)

    if self.image_type == "jpeg" and max_axis is None:
        # Same format, no resize - just copy
        shutil.copy2(self.path, path)
    elif self.image_type == "dng":
        from capturegraph.adapters.dng_convert import dng_to_jpeg

        dng_to_jpeg(self.path, path, max_axis=max_axis)
    else:
        # HEIF, PNG, etc - use PIL
        img = self.pil_image(max_axis=max_axis)
        if img.mode in ("RGBA", "LA", "P"):
            img = img.convert("RGB")
        img.save(
            path,
            format="JPEG",
            quality=100,
            subsampling=0,
            optimize=True,
        )
        img.close()

    return ImageFile(path)

save_heif(path, max_axis=None) #

Convert and save the image as HEIF.

If already HEIF and no resizing is needed, copies the file without re-encoding. DNG files use specialized conversion; other formats use PIL.

Parameters:

Name Type Description Default
path Path

Path for the output HEIF file (.heic extension).

required
max_axis int

If provided, resize so the largest dimension equals this value.

None

Returns:

Type Description
ImageFile

New ImageFile pointing to the created HEIF.

Example
img = ImageFile("photo.dng")
heif = img.save_heif(Path("output.heic"))
heif.image_type  # 'heif'

# Save a smaller version for analysis
small = img.save_heif(Path("thumb.heic"), max_axis=1024)
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def save_heif(self, path: Path, max_axis: int = None) -> "ImageFile":
    """Convert and save the image as HEIF.

    If already HEIF and no resizing is needed, copies the file without re-encoding.
    DNG files use specialized conversion; other formats use PIL.

    Args:
        path: Path for the output HEIF file (.heic extension).
        max_axis: If provided, resize so the largest dimension equals this value.

    Returns:
        New ImageFile pointing to the created HEIF.

    Example:
        ```python
        img = ImageFile("photo.dng")
        heif = img.save_heif(Path("output.heic"))
        heif.image_type  # 'heif'

        # Save a smaller version for analysis
        small = img.save_heif(Path("thumb.heic"), max_axis=1024)
        ```
    """
    path = Path(path).resolve()
    path.parent.mkdir(parents=True, exist_ok=True)

    if self.image_type == "heif" and max_axis is None:
        # Same format, no resize - just copy
        shutil.copy2(self.path, path)
    elif self.image_type == "dng":
        from capturegraph.adapters.dng_convert import dng_to_heif

        dng_to_heif(self.path, path, max_axis=max_axis)
    else:
        # JPEG, PNG, etc - use PIL
        img = self.pil_image(max_axis=max_axis)
        img.save(
            path,
            format="HEIF",
            quality=100,
        )
        img.close()

    return ImageFile(path)

save_png(path, max_axis=None) #

Convert and save the image as PNG (lossless).

If already PNG and no resizing is needed, copies the file without re-encoding. DNG files use specialized conversion; other formats use PIL.

Parameters:

Name Type Description Default
path Path

Path for the output PNG file.

required
max_axis int

If provided, resize so the largest dimension equals this value.

None

Returns:

Type Description
ImageFile

New ImageFile pointing to the created PNG.

Example
img = ImageFile("photo.dng")
png = img.save_png(Path("output.png"))
png.image_type  # 'png'

# Save a smaller version for analysis
small = img.save_png(Path("thumb.png"), max_axis=1024)
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def save_png(self, path: Path, max_axis: int = None) -> "ImageFile":
    """Convert and save the image as PNG (lossless).

    If already PNG and no resizing is needed, copies the file without re-encoding.
    DNG files use specialized conversion; other formats use PIL.

    Args:
        path: Path for the output PNG file.
        max_axis: If provided, resize so the largest dimension equals this value.

    Returns:
        New ImageFile pointing to the created PNG.

    Example:
        ```python
        img = ImageFile("photo.dng")
        png = img.save_png(Path("output.png"))
        png.image_type  # 'png'

        # Save a smaller version for analysis
        small = img.save_png(Path("thumb.png"), max_axis=1024)
        ```
    """
    path = Path(path).resolve()
    path.parent.mkdir(parents=True, exist_ok=True)

    if self.image_type == "png" and max_axis is None:
        # Same format, no resize - just copy
        shutil.copy2(self.path, path)
    elif self.image_type == "dng":
        from capturegraph.adapters.dng_convert import dng_to_png

        dng_to_png(self.path, path, max_axis=max_axis)
    else:
        # HEIF, JPEG, etc - use PIL
        img = self.pil_image(max_axis=max_axis)
        img.save(
            path,
            format="PNG",
            optimize=True,
        )
        img.close()

    return ImageFile(path)

tooltip(max_axis=64) #

Create a base64-encoded thumbnail for Altair/Vega tooltips.

Resizes the image so its largest dimension is max_axis, then encodes it as a base64 PNG data URL suitable for Vega tooltips.

Supports both standard formats (JPEG, PNG, HEIC) and RAW formats (DNG, CR2, NEF, etc.) using rawpy.

Parameters:

Name Type Description Default
max_axis int

Maximum dimension (width or height) of the thumbnail.

64

Returns:

Type Description
str

A data URL string like "data:image/png;base64,..." that can be

str

used directly in Altair/Vega tooltip specifications.

Example
img = ImageFile("photo.jpg")
img.tooltip(64)  # 'data:image/png;base64,iVBORw0KGgo...'

# Works with RAW files too
raw = ImageFile("photo.dng")
raw.tooltip(64)  # 'data:image/png;base64,iVBORw0KGgo...'
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def tooltip(self, max_axis: int = 64) -> str:
    """Create a base64-encoded thumbnail for Altair/Vega tooltips.

    Resizes the image so its largest dimension is `max_axis`, then
    encodes it as a base64 PNG data URL suitable for Vega tooltips.

    Supports both standard formats (JPEG, PNG, HEIC) and RAW formats
    (DNG, CR2, NEF, etc.) using rawpy.

    Args:
        max_axis: Maximum dimension (width or height) of the thumbnail.

    Returns:
        A data URL string like "data:image/png;base64,..." that can be
        used directly in Altair/Vega tooltip specifications.

    Example:
        ```python
        img = ImageFile("photo.jpg")
        img.tooltip(64)  # 'data:image/png;base64,iVBORw0KGgo...'

        # Works with RAW files too
        raw = ImageFile("photo.dng")
        raw.tooltip(64)  # 'data:image/png;base64,iVBORw0KGgo...'
        ```
    """

    with self.pil_image(max_axis=max_axis) as img:
        # Convert to RGB if necessary (for PNG encoding)
        if img.mode in ("RGBA", "LA", "P"):
            img = img.convert("RGB")

        # Encode to base64
        buffered = BytesIO()
        img.save(buffered, format="PNG")
        img_str = base64.b64encode(buffered.getvalue()).decode()

    return f"data:image/png;base64,{img_str}"

load_exif(path) #

Load EXIF metadata from an image file.

Parameters:

Name Type Description Default
path Path | str

Path to the image file.

required

Returns:

Type Description
Dict

Dict with EXIF tags accessible by short name (e.g., DateTimeOriginal)

Dict

or full name (e.g., EXIF DateTimeOriginal).

Example
exif = load_exif("photo.dng")
exif.DateTimeOriginal  # '2024:01:15 10:30:00'
exif.FocalLength       # '35'
Source code in capturegraph-lib/capturegraph/data/load/types/image.py
def load_exif(path: Path | str) -> Dict:
    """Load EXIF metadata from an image file.

    Args:
        path: Path to the image file.

    Returns:
        Dict with EXIF tags accessible by short name (e.g., `DateTimeOriginal`)
        or full name (e.g., `EXIF DateTimeOriginal`).

    Example:
        ```python
        exif = load_exif("photo.dng")
        exif.DateTimeOriginal  # '2024:01:15 10:30:00'
        exif.FocalLength       # '35'
        ```
    """
    result = Dict()

    with open(path, "rb") as f:
        raw_tags = exifread.process_file(f, details=False)

    for full_name, value in raw_tags.items():
        path = full_name.split(" ")
        while path[0] == "EXIF":
            path = path[1:]

        dest = result
        for p in path[:-1]:
            dest = dest.setdefault(p, Dict())

        dest[path[-1]] = _exif_value(value)

    return result