diff --git a/README.md b/README.md index 8a841cf..1cd5e0d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,10 @@ # pyPhotoAlbum -| Badge | Description | -|-------|-------------| -| ![Test Coverage](https://gitea.tourolle.paris/dtourolle/pyPhotoAlbum/raw/branch/badges/cov_info/coverage.svg) | **Test Coverage** - Percentage of code covered by unit tests | -| ![Documentation Coverage](https://gitea.tourolle.paris/dtourolle/pyPhotoAlbum/raw/branch/badges/cov_info/coverage-docs.svg) | **Documentation Coverage** - Percentage of code with docstrings | -| ![License](https://img.shields.io/badge/license-MIT-blue.svg) | **License** - Project licensing information | +![Test Coverage](https://gitea.tourolle.paris/dtourolle/pyPhotoAlbum/raw/branch/badges/cov_info/coverage.svg) +![Documentation Coverage](https://gitea.tourolle.paris/dtourolle/pyPhotoAlbum/raw/branch/badges/cov_info/coverage-docs.svg) +![License](https://img.shields.io/badge/license-MIT-blue.svg) -> 📋 **Note**: Badges show results from the commit referenced in the URLs. Red "error" badges indicate build failures for that specific step. - -A Python-based desktop application for designing photo albums with an intuitive interface and professional PDF export capabilities. +A desktop application for designing and creating professional photo albums with an intuitive drag-and-drop interface and high-quality PDF export. ## Overview @@ -39,19 +35,8 @@ cd pyPhotoAlbum ./install.sh ``` -**For GNOME users:** See [GNOME_QUICKSTART.md](GNOME_QUICKSTART.md) for desktop integration. - **For detailed instructions:** See [INSTALLATION.md](INSTALLATION.md) -### Requirements - -- Python 3.9 or higher -- PyQt6 -- PyOpenGL -- Pillow -- ReportLab -- lxml - ### Distribution Packages **Fedora (RPM):** @@ -67,25 +52,12 @@ makepkg -si See [INSTALLATION.md](INSTALLATION.md) for complete instructions. -### Install for Development - -```bash -# Clone repository -git clone https://gitea.tourolle.paris/dtourolle/pyPhotoAlbum.git -cd pyPhotoAlbum - -# Create virtual environment -python -m venv venv -source venv/bin/activate - -# Install with development dependencies -pip install -e ".[dev]" -``` - ## Quick Start ### Running the Application +After installation, launch pyPhotoAlbum: + ```bash pyphotoalbum ``` @@ -96,600 +68,44 @@ Or run directly from source: python pyPhotoAlbum/main.py ``` -### Basic Usage Example +### Basic Workflow -```python -from pyPhotoAlbum.project import Project, Page -from pyPhotoAlbum.page_layout import PageLayout -from pyPhotoAlbum.models import ImageData +1. **Create a New Project** - Choose your page size (A4, Letter, etc.) and DPI +2. **Add Pages** - Start with blank pages or use templates +3. **Add Images** - Drag and drop images from your file browser onto pages +4. **Arrange & Edit** - Move, resize, rotate, and crop images to your liking +5. **Save Your Work** - Projects are saved as .ppz files (ZIP archives) +6. **Export to PDF** - Generate high-quality PDFs ready for printing -# Create a new project -project = Project(name="My Photo Album") -project.page_size_mm = (210, 297) # A4 size -project.working_dpi = 300 +## Using Templates -# 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) +pyPhotoAlbum includes a template system to help you quickly create consistent layouts: -# Add page to project -page = Page(layout=layout, page_number=1) -project.add_page(page) +- **Built-in Templates**: Grid layouts, single large image, and more +- **Custom Templates**: Save your favorite layouts as templates +- **Flexible Application**: Apply templates to new or existing pages -# Save project -from pyPhotoAlbum.project_serializer import save_to_zip -success, error = save_to_zip(project, "my_album.ppz") -``` +## Architecture Highlights -## Architecture +pyPhotoAlbum is built with clean, maintainable design patterns: -### GLWidget Mixin Architecture +### Mixin-Based Composition -The main OpenGL widget uses a **mixin-based architecture** for maintainability and testability. The monolithic 1,368-line `gl_widget.py` has been refactored into 9 focused mixins averaging 89 lines each: +The main OpenGL widget is composed of **12 specialized mixins** instead of one monolithic class: +- Each mixin handles a single responsibility (viewport, rendering, selection, etc.) +- Average ~90 lines per mixin for maintainability +- Easy to test in isolation with comprehensive unit tests +- Clean separation of concerns throughout the codebase -```python -class GLWidget( - ViewportMixin, # Zoom & pan state - RenderingMixin, # OpenGL rendering - AssetDropMixin, # Drag-and-drop - PageNavigationMixin, # Page detection - ImagePanMixin, # Image cropping - ElementManipulationMixin, # Resize & rotate - ElementSelectionMixin, # Hit detection - MouseInteractionMixin, # Event routing - UndoableInteractionMixin, # Undo/redo - QOpenGLWidget # Qt base class -): - """Clean orchestration with minimal boilerplate""" -``` +### Declarative UI with Decorators -**Benefits:** -- Each mixin has a single, clear responsibility -- 89 comprehensive unit tests with 69-97% coverage per mixin -- Easy to test in isolation with mock dependencies -- Clear separation of concerns -- Maintainable codebase (average 89 lines per mixin) +The ribbon interface is **auto-generated from decorator metadata**: +- `@ribbon_action` - Automatically creates ribbon buttons from method metadata +- `@undoable_operation` - Automatically captures state for undo/redo +- `@dialog_action` - Separates dialog presentation from business logic +- No manual UI wiring required - just add decorators to your methods -See [REFACTORING_COMPLETE.md](REFACTORING_COMPLETE.md) for details on the refactoring process. - -### Core Components - -#### Models (`models.py`) - -Base classes for layout elements: - -```python -# 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`) - -```python -# 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: - -```python -# 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: - -```python -# 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: - -```python -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: - -```python -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 - -```python -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 - -```python -# 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 - -```python -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 -try: - loaded_project = load_from_zip("album.ppz") - print(f"Loaded: {loaded_project.name}") -except Exception as error: - 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 - -```python -{ - "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 - -```python -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 - -```bash -# 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 -├── test_gl_widget_fixtures.py # Shared GL widget test fixtures -├── test_viewport_mixin.py # Viewport mixin tests -├── test_element_selection_mixin.py # Selection mixin tests -├── test_element_manipulation_mixin.py # Manipulation mixin tests -├── test_image_pan_mixin.py # Image pan mixin tests -├── test_page_navigation_mixin.py # Page navigation mixin tests -└── test_asset_drop_mixin.py # Asset drop mixin tests -``` - -### Example Test Cases - -From `tests/test_models.py`: - -```python -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`: - -```python -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 = 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 (mixin orchestration) -├── 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/ # Mixin architecture -│ ├── __init__.py -│ ├── base.py # Base mixin class -│ ├── viewport.py # Zoom and pan management -│ ├── rendering.py # OpenGL rendering pipeline -│ ├── asset_drop.py # Drag-and-drop functionality -│ ├── page_navigation.py # Page detection and ghost pages -│ ├── image_pan.py # Image cropping within frames -│ ├── element_manipulation.py # Resize and rotate -│ ├── element_selection.py # Hit detection and selection -│ ├── mouse_interaction.py # Mouse event coordination -│ ├── interaction_undo.py # Undo/redo integration -│ └── operations/ # Operation mixins -│ ├── 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 (312 tests, 29% coverage) -examples/ # Usage examples -``` - -## Development - -### Code Style - -The project uses: -- **Black** for code formatting (line length: 120) -- **Flake8** for linting -- **MyPy** for type checking - -```bash -# 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: - -```bash -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:** -```python -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:** -```python -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:** -```python -{ - "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:** -```python -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 +This approach keeps UI concerns separate from business logic and makes the codebase easier to maintain and extend. ## Keyboard Shortcuts @@ -708,12 +124,6 @@ Element container for a page. This project is licensed under the MIT License. -## Links - -- Documentation: [Link to docs] -- Issue Tracker: [Link to issues] -- Changelog: [Link to changelog] - ## Acknowledgments Built with: