This commit is contained in:
parent
b18a780a33
commit
2a087f0c9d
656
README.md
656
README.md
@ -1,14 +1,10 @@
|
||||
# pyPhotoAlbum
|
||||
|
||||
| Badge | Description |
|
||||
|-------|-------------|
|
||||
|  | **Test Coverage** - Percentage of code covered by unit tests |
|
||||
|  | **Documentation Coverage** - Percentage of code with docstrings |
|
||||
|  | **License** - Project licensing information |
|
||||

|
||||

|
||||

|
||||
|
||||
> 📋 **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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user