2025-10-29 20:50:05 +01:00
2025-10-21 22:24:56 +02:00
2025-10-21 22:24:56 +02:00
2025-10-25 09:02:36 +02:00
2025-10-25 09:02:36 +02:00
2025-10-21 22:02:49 +02:00

pyPhotoAlbum

A Python-based desktop application for designing photo albums with an intuitive interface and professional PDF export capabilities.

Overview

pyPhotoAlbum is a photo album design tool built with PyQt6 and OpenGL, offering a powerful yet user-friendly interface for creating custom photo layouts. It supports drag-and-drop image placement, template-based designs, and high-quality PDF export.

Key Features

  • Visual Editor: OpenGL-accelerated rendering with real-time preview
  • Drag & Drop: Direct image import from file explorer
  • Template System: Create and reuse page layouts
  • Smart Layout Tools: Alignment, distribution, and sizing operations
  • Asset Management: Automatic image organization with reference counting
  • Project Files: Save/load projects in portable ZIP format (.ppz)
  • PDF Export: High-quality export with configurable DPI
  • Undo/Redo: Complete command history for all operations
  • Double-Page Spreads: Design facing pages for book-style albums

Installation

Requirements

  • Python 3.9 or higher
  • PyQt6
  • PyOpenGL
  • Pillow
  • ReportLab
  • lxml

Install from Source

# Clone the repository
git clone https://github.com/yourusername/pyPhotoAlbum.git
cd pyPhotoAlbum

# Create and activate virtual environment
python -m venv venv
source venv/bin/activate  # On Windows: venv\Scripts\activate

# Install the package
pip install -e .

Install for Development

# Install with development dependencies
pip install -e ".[dev]"

Quick Start

Running the Application

pyphotoalbum

Or run directly from source:

python pyPhotoAlbum/main.py

Basic Usage Example

from pyPhotoAlbum.project import Project, Page
from pyPhotoAlbum.page_layout import PageLayout
from pyPhotoAlbum.models import ImageData

# Create a new project
project = Project(name="My Photo Album")
project.page_size_mm = (210, 297)  # A4 size
project.working_dpi = 300

# Create a page with an image
layout = PageLayout(width=210, height=297)
image = ImageData(
    image_path="photos/vacation.jpg",
    x=10.0,
    y=10.0,
    width=190.0,
    height=140.0
)
layout.add_element(image)

# Add page to project
page = Page(layout=layout, page_number=1)
project.add_page(page)

# Save project
from pyPhotoAlbum.project_serializer import save_to_zip
success, error = save_to_zip(project, "my_album.ppz")

Architecture

Core Components

Models (models.py)

Base classes for layout elements:

# Image element with crop support
image = ImageData(
    image_path="photo.jpg",
    x=10, y=20,
    width=200, height=150,
    rotation=0,
    z_index=0,
    crop_info=(0, 0, 1, 1)  # (x_min, y_min, x_max, y_max)
)

# Text box element
textbox = TextBoxData(
    text_content="My Caption",
    font_settings={"family": "Arial", "size": 14, "color": (0, 0, 0)},
    alignment="center",
    x=10, y=180,
    width=200, height=30
)

# Placeholder for templates
placeholder = PlaceholderData(
    placeholder_type="image",
    x=10, y=10,
    width=100, height=100
)

Project Structure (project.py)

# Project contains multiple pages
project = Project(name="Album", folder_path="/path/to/project")

# Each page has a layout with elements
page = Page(layout=PageLayout(), page_number=1)
page.layout.add_element(image)

project.add_page(page)

Asset Management (asset_manager.py)

Automatic asset handling with reference counting:

# Import an image into the project
asset_path = project.asset_manager.import_asset("photo.jpg")
# Returns: "assets/photo_001.jpg" (relative path)

# Assets are automatically copied to project folder
# Reference counting tracks usage across pages
# Cleanup happens automatically when elements are deleted

Command System (commands.py)

Undo/redo support for all operations:

# Commands are automatically created for operations
from pyPhotoAlbum.commands import AddElementCommand, MoveElementCommand

# Add element (undoable)
cmd = AddElementCommand(page.layout, image, project.asset_manager)
project.history.execute(cmd)

# Move element (undoable)
cmd = MoveElementCommand(image, old_pos=(10, 10), new_pos=(20, 20))
project.history.execute(cmd)

# Undo/redo
project.history.undo()
project.history.redo()

Layout System

PageLayout (page_layout.py)

Manages elements on a page:

layout = PageLayout(width=210, height=297)  # A4 in mm

# Add multiple elements
layout.add_element(image1)
layout.add_element(image2)
layout.add_element(textbox)

# Elements are rendered in z_index order
# Serialize/deserialize for saving
data = layout.serialize()
layout2 = PageLayout()
layout2.deserialize(data)

Alignment Tools (alignment.py)

Precise element positioning:

from pyPhotoAlbum.alignment import AlignmentManager

# Align multiple elements to the left
changes = AlignmentManager.align_left(selected_elements)
for element, new_position in changes:
    element.position = new_position

# Distribute elements evenly
changes = AlignmentManager.distribute_horizontally(selected_elements)

# Make elements the same size
changes = AlignmentManager.make_same_size(selected_elements)
for element, new_position, new_size in changes:
    element.position = new_position
    element.size = new_size

Template System

Creating Templates

from pyPhotoAlbum.template_manager import TemplateManager, Template

manager = TemplateManager()

# Create template from existing page
template = manager.create_template_from_page(
    page=current_page,
    name="My Grid Layout",
    description="2x2 photo grid"
)

# Save template
manager.save_template(template)

Using Templates

# List available templates
templates = manager.list_templates()
# Returns: ["Grid_2x2", "Single_Large", "My Grid Layout", ...]

# Create new page from template
new_page = manager.create_page_from_template(
    template_name="Grid_2x2",
    target_page_size=(210, 297),
    page_number=5
)

# Apply template to existing page
manager.apply_template_to_page(
    template=template,
    target_page=existing_page,
    mode="replace",  # or "reflow"
    scaling="proportional"  # or "stretch" or "center"
)

Project Serialization

Save/Load Projects

Projects are saved as ZIP archives (.ppz) containing:

  • project.json - Project metadata and structure
  • assets/ - All referenced images
from pyPhotoAlbum.project_serializer import save_to_zip, load_from_zip

# Save project
success, error = save_to_zip(project, "album.ppz")
if not success:
    print(f"Error saving: {error}")

# Load project
loaded_project, error = load_from_zip("album.ppz")
if loaded_project:
    print(f"Loaded: {loaded_project.name}")
else:
    print(f"Error loading: {error}")

# Get project info without loading
from pyPhotoAlbum.project_serializer import get_project_info
info = get_project_info("album.ppz")
print(f"Name: {info['name']}, Pages: {info['page_count']}")

Project Structure

{
    "name": "My Album",
    "serialization_version": "1.0",
    "page_size_mm": [210, 297],
    "working_dpi": 300,
    "export_dpi": 300,
    "pages": [
        {
            "page_number": 1,
            "is_double_spread": false,
            "layout": {
                "width": 210,
                "height": 297,
                "elements": [...]
            }
        }
    ]
}

PDF Export

from pyPhotoAlbum.pdf_exporter import PDFExporter

# Create exporter
exporter = PDFExporter(project, export_dpi=300)

# Export with progress callback
def progress_callback(current, total):
    print(f"Exporting page {current}/{total}")

success, errors = exporter.export(
    output_path="album.pdf",
    progress_callback=progress_callback
)

if success:
    print("PDF exported successfully")
else:
    print(f"Errors: {errors}")

Testing

Run Tests

# Run all tests
pytest

# Run with coverage
pytest --cov=pyPhotoAlbum --cov-report=html

# Run specific test file
pytest tests/test_models.py

# Run with verbose output
pytest -v

Test Structure

tests/
├── __init__.py
├── conftest.py              # Shared fixtures
├── test_models.py           # Model serialization tests
├── test_project.py          # Project and page tests
├── test_project_serialization.py  # Save/load tests
├── test_page_renderer.py    # Rendering tests
└── test_pdf_export.py       # PDF export tests

Example Test Cases

From tests/test_models.py:

def test_image_serialization():
    """Test ImageData serialization"""
    img = ImageData(
        image_path="test.jpg",
        x=15.0, y=25.0,
        width=180.0, height=120.0,
        rotation=30.0,
        z_index=3
    )
    
    # Serialize
    data = img.serialize()
    assert data["type"] == "image"
    assert data["position"] == (15.0, 25.0)
    
    # Deserialize
    img2 = ImageData()
    img2.deserialize(data)
    assert img2.position == img.position

From tests/test_project_serialization.py:

def test_save_and_load_project(tmp_path):
    """Test complete save/load cycle"""
    # Create project with pages
    project = Project(name="Test")
    page = Page(layout=PageLayout(), page_number=1)
    project.add_page(page)
    
    # Save
    zip_path = tmp_path / "project.ppz"
    success, error = save_to_zip(project, str(zip_path))
    assert success is True
    
    # Load
    loaded, error = load_from_zip(str(zip_path))
    assert loaded.name == "Test"
    assert len(loaded.pages) == 1

Project Structure

pyPhotoAlbum/
├── __init__.py
├── main.py                 # Application entry point
├── models.py               # Data models (ImageData, TextBoxData, etc.)
├── project.py              # Project and Page classes
├── page_layout.py          # Page layout management
├── page_renderer.py        # OpenGL rendering
├── gl_widget.py            # Main OpenGL widget
├── project_serializer.py   # Save/load functionality
├── asset_manager.py        # Asset handling
├── commands.py             # Undo/redo system
├── template_manager.py     # Template system
├── pdf_exporter.py         # PDF export
├── alignment.py            # Alignment tools
├── snapping.py             # Snapping system
├── decorators.py           # UI decorators
├── ribbon_widget.py        # Ribbon interface
├── ribbon_builder.py       # Ribbon configuration
├── mixins/                 # Operation mixins
│   ├── base.py
│   └── operations/
│       ├── element_ops.py
│       ├── page_ops.py
│       ├── file_ops.py
│       ├── view_ops.py
│       ├── edit_ops.py
│       ├── template_ops.py
│       ├── alignment_ops.py
│       ├── distribution_ops.py
│       └── size_ops.py
└── templates/              # Built-in templates
    ├── Grid_2x2.json
    └── Single_Large.json

tests/                      # Unit tests
examples/                   # Usage examples

Development

Code Style

The project uses:

  • Black for code formatting (line length: 120)
  • Flake8 for linting
  • MyPy for type checking
# Format code
black pyPhotoAlbum tests

# Run linter
flake8 pyPhotoAlbum tests

# Type checking
mypy pyPhotoAlbum

Continuous Integration

GitHub Actions / Gitea Actions workflows:

  • Run tests on Python 3.9, 3.10, 3.11
  • Check code quality with linters
  • Generate coverage reports

Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Write tests for new features
  4. Ensure all tests pass
  5. Submit a pull request

Usage Examples

See the examples/ directory for complete working examples:

  • basic_usage.py - Creating projects and adding images
  • template_example.py - Working with templates
  • generate_screenshots.py - Creating documentation screenshots

Run examples:

cd examples
python basic_usage.py

API Reference

Key Classes

BaseLayoutElement (Abstract)

Base class for all layout elements.

Methods:

  • render() - Render element using OpenGL
  • serialize() -> Dict - Convert to dictionary
  • deserialize(data: Dict) - Load from dictionary

Attributes:

  • position: Tuple[float, float] - (x, y) in mm
  • size: Tuple[float, float] - (width, height) in mm
  • rotation: float - Rotation angle in degrees
  • z_index: int - Layer order

ImageData

Image element with crop support.

Constructor:

ImageData(
    image_path: str = "",
    crop_info: Tuple = (0, 0, 1, 1),
    x: float = 0,
    y: float = 0,
    width: float = 100,
    height: float = 100,
    rotation: float = 0,
    z_index: int = 0
)

TextBoxData

Text element with formatting.

Constructor:

TextBoxData(
    text_content: str = "",
    font_settings: Dict = None,
    alignment: str = "left",
    x: float = 0,
    y: float = 0,
    width: float = 100,
    height: float = 100
)

Font Settings:

{
    "family": "Arial",
    "size": 12,
    "color": (0, 0, 0)  # RGB tuple
}

Project

Main project container.

Methods:

  • add_page(page: Page) - Add page to project
  • remove_page(page: Page) - Remove page
  • serialize() -> Dict - Save to dictionary
  • deserialize(data: Dict) - Load from dictionary

Attributes:

  • name: str - Project name
  • pages: List[Page] - List of pages
  • page_size_mm: Tuple[float, float] - Page dimensions
  • working_dpi: int - Display DPI
  • export_dpi: int - Export DPI
  • asset_manager: AssetManager - Asset handler
  • history: CommandHistory - Undo/redo history

Page

Single page in project.

Constructor:

Page(
    layout: PageLayout = None,
    page_number: int = 1,
    is_double_spread: bool = False
)

PageLayout

Element container for a page.

Methods:

  • add_element(element: BaseLayoutElement) - Add element
  • remove_element(element: BaseLayoutElement) - Remove element
  • render(dpi: int) - Render all elements

Attributes:

  • elements: List[BaseLayoutElement] - Page elements
  • width: float - Page width in mm
  • height: float - Page height in mm

Keyboard Shortcuts

  • Ctrl+Z - Undo
  • Ctrl+Y - Redo
  • Ctrl+S - Save project
  • Ctrl+O - Open project
  • Ctrl+N - New project
  • Ctrl+E - Export to PDF
  • Delete - Delete selected element
  • Arrow Keys - Move selected element
  • Shift+Arrow Keys - Resize selected element
  • Ctrl+D - Duplicate selected element

License

This project is licensed under the MIT License.

  • Documentation: [Link to docs]
  • Issue Tracker: [Link to issues]
  • Changelog: [Link to changelog]

Acknowledgments

Built with:

  • PyQt6 for the GUI framework
  • OpenGL for hardware-accelerated rendering
  • ReportLab for PDF generation
  • Pillow for image processing
Description
No description provided
Readme 5.5 MiB
Languages
Python 97.9%
Shell 2.1%