added fotn change API and examples
This commit is contained in:
parent
9de67d958e
commit
889f27e1a3
47
README.md
47
README.md
@ -25,6 +25,7 @@ PyWebLayout is a Python library for HTML-like layout and rendering to paginated
|
|||||||
### Text and HTML Support
|
### Text and HTML Support
|
||||||
- 📝 **HTML Parsing** - Parse HTML content into structured document blocks
|
- 📝 **HTML Parsing** - Parse HTML content into structured document blocks
|
||||||
- 🔤 **Font Support** - Multiple font sizes, weights, and styles
|
- 🔤 **Font Support** - Multiple font sizes, weights, and styles
|
||||||
|
- 🎨 **Dynamic Font Families** - Switch between Sans, Serif, and Monospace fonts on-the-fly
|
||||||
- ↔️ **Text Alignment** - Left, center, right, and justified text
|
- ↔️ **Text Alignment** - Left, center, right, and justified text
|
||||||
- 📖 **Rich Content** - Headings, paragraphs, bold, italic, and more
|
- 📖 **Rich Content** - Headings, paragraphs, bold, italic, and more
|
||||||
- 📊 **Table Rendering** - Full HTML table support with headers, borders, and styling
|
- 📊 **Table Rendering** - Full HTML table support with headers, borders, and styling
|
||||||
@ -138,6 +139,13 @@ The library supports various page layouts and configurations:
|
|||||||
<em>All 14 form field types with validation</em>
|
<em>All 14 form field types with validation</em>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td align="center" colspan="2">
|
||||||
|
<b>🆕 Dynamic Font Family Switching</b><br>
|
||||||
|
<img src="docs/images/font_family_switching_vertical.png" width="600" alt="Font Switching"><br>
|
||||||
|
<em>Switch between Sans, Serif, and Monospace fonts instantly</em>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
@ -151,11 +159,13 @@ The `examples/` directory contains working demonstrations:
|
|||||||
- **[04_table_rendering.py](examples/04_table_rendering.py)** - HTML table rendering with styling
|
- **[04_table_rendering.py](examples/04_table_rendering.py)** - HTML table rendering with styling
|
||||||
- **[05_html_table_with_images.py](examples/05_html_table_with_images.py)** - Tables with embedded images
|
- **[05_html_table_with_images.py](examples/05_html_table_with_images.py)** - Tables with embedded images
|
||||||
- **[06_functional_elements_demo.py](examples/06_functional_elements_demo.py)** - Interactive buttons and forms with callbacks
|
- **[06_functional_elements_demo.py](examples/06_functional_elements_demo.py)** - Interactive buttons and forms with callbacks
|
||||||
|
- **[08_bundled_fonts_demo.py](examples/08_bundled_fonts_demo.py)** - Using the bundled DejaVu font families
|
||||||
|
|
||||||
### 🆕 Advanced Features (NEW)
|
### 🆕 Advanced Features (NEW)
|
||||||
- **[08_pagination_demo.py](examples/08_pagination_demo.py)** - Multi-page documents with PageBreak ([11 tests](tests/examples/test_08_pagination_demo.py))
|
- **[08_pagination_demo.py](examples/08_pagination_demo.py)** - Multi-page documents with PageBreak ([11 tests](tests/examples/test_08_pagination_demo.py))
|
||||||
- **[09_link_navigation_demo.py](examples/09_link_navigation_demo.py)** - All link types and navigation ([10 tests](tests/examples/test_09_link_navigation_demo.py))
|
- **[09_link_navigation_demo.py](examples/09_link_navigation_demo.py)** - All link types and navigation ([10 tests](tests/examples/test_09_link_navigation_demo.py))
|
||||||
- **[10_forms_demo.py](examples/10_forms_demo.py)** - All 14 form field types ([9 tests](tests/examples/test_10_forms_demo.py))
|
- **[10_forms_demo.py](examples/10_forms_demo.py)** - All 14 form field types ([9 tests](tests/examples/test_10_forms_demo.py))
|
||||||
|
- **[11_font_family_switching_demo.py](examples/11_font_family_switching_demo.py)** - 🆕 Dynamic font switching in ereader
|
||||||
|
|
||||||
Run any example:
|
Run any example:
|
||||||
```bash
|
```bash
|
||||||
@ -176,9 +186,46 @@ python -m pytest tests/examples/ -v # 30 tests, all passing ✅
|
|||||||
|
|
||||||
See **[examples/README.md](examples/README.md)** for detailed documentation.
|
See **[examples/README.md](examples/README.md)** for detailed documentation.
|
||||||
|
|
||||||
|
## Font Family Switching (NEW ✨)
|
||||||
|
|
||||||
|
PyWebLayout now supports dynamic font family switching in the ereader, allowing readers to change fonts on-the-fly without losing their reading position!
|
||||||
|
|
||||||
|
### Quick Example
|
||||||
|
|
||||||
|
```python
|
||||||
|
from pyWebLayout.style.fonts import BundledFont
|
||||||
|
from pyWebLayout.layout.ereader_manager import create_ereader_manager
|
||||||
|
|
||||||
|
# Create an ereader
|
||||||
|
manager = create_ereader_manager(blocks, page_size=(600, 800))
|
||||||
|
|
||||||
|
# Switch to serif font
|
||||||
|
manager.set_font_family(BundledFont.SERIF)
|
||||||
|
|
||||||
|
# Switch to monospace font
|
||||||
|
manager.set_font_family(BundledFont.MONOSPACE)
|
||||||
|
|
||||||
|
# Restore original fonts
|
||||||
|
manager.set_font_family(None)
|
||||||
|
|
||||||
|
# Query current font
|
||||||
|
current = manager.get_font_family()
|
||||||
|
```
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **3 Bundled Fonts**: Sans, Serif, and Monospace (DejaVu font family)
|
||||||
|
- **Instant Switching**: Change fonts without recreating the document
|
||||||
|
- **Position Preservation**: Reading position maintained across font changes
|
||||||
|
- **Attribute Preservation**: Bold, italic, size, and color are preserved
|
||||||
|
- **Smart Caching**: Automatic cache invalidation for optimal performance
|
||||||
|
|
||||||
|
**Learn more**: See [FONT_SWITCHING_FEATURE.md](FONT_SWITCHING_FEATURE.md) for complete documentation.
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Abstract/Concrete architecture guide
|
- **[ARCHITECTURE.md](ARCHITECTURE.md)** - Abstract/Concrete architecture guide
|
||||||
|
- **[FONT_SWITCHING_FEATURE.md](FONT_SWITCHING_FEATURE.md)** - 🆕 Font family switching guide
|
||||||
- **[examples/README.md](examples/README.md)** - Complete examples guide with tests
|
- **[examples/README.md](examples/README.md)** - Complete examples guide with tests
|
||||||
- **[docs/images/README.md](docs/images/README.md)** - Visual documentation index
|
- **[docs/images/README.md](docs/images/README.md)** - Visual documentation index
|
||||||
- **[pyWebLayout/layout/README_EREADER_API.md](pyWebLayout/layout/README_EREADER_API.md)** - EbookReader API reference
|
- **[pyWebLayout/layout/README_EREADER_API.md](pyWebLayout/layout/README_EREADER_API.md)** - EbookReader API reference
|
||||||
|
|||||||
BIN
docs/images/font_family_switching.png
Normal file
BIN
docs/images/font_family_switching.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/images/font_family_switching_vertical.png
Normal file
BIN
docs/images/font_family_switching_vertical.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 61 KiB |
240
examples/11_font_family_switching_demo.py
Normal file
240
examples/11_font_family_switching_demo.py
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
"""
|
||||||
|
Demonstration of dynamic font family switching in the ereader.
|
||||||
|
|
||||||
|
This example shows how to:
|
||||||
|
1. Initialize an ereader with content
|
||||||
|
2. Dynamically switch between different font families (Sans, Serif, Monospace)
|
||||||
|
3. Maintain reading position across font changes
|
||||||
|
4. Use the font family API
|
||||||
|
|
||||||
|
The ereader manager provides a high-level API for changing fonts on-the-fly
|
||||||
|
without losing your place in the document.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyWebLayout.abstract import Paragraph, Heading, Word
|
||||||
|
from pyWebLayout.abstract.block import HeadingLevel
|
||||||
|
from pyWebLayout.style import Font
|
||||||
|
from pyWebLayout.style.fonts import BundledFont, FontWeight
|
||||||
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
|
from pyWebLayout.layout.ereader_manager import create_ereader_manager
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
|
||||||
|
def create_sample_content():
|
||||||
|
"""Create sample document content with various text styles"""
|
||||||
|
blocks = []
|
||||||
|
|
||||||
|
# Create a default font for the content
|
||||||
|
default_font = Font.from_family(BundledFont.SANS, font_size=16)
|
||||||
|
heading_font = Font.from_family(BundledFont.SANS, font_size=24, weight=FontWeight.BOLD)
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title = Heading(level=HeadingLevel.H1, style=heading_font)
|
||||||
|
for word in "Font Family Switching Demo".split():
|
||||||
|
title.add_word(Word(word, heading_font))
|
||||||
|
blocks.append(title)
|
||||||
|
|
||||||
|
# Introduction paragraph
|
||||||
|
intro_font = Font.from_family(BundledFont.SANS, font_size=16)
|
||||||
|
intro = Paragraph(intro_font)
|
||||||
|
intro_text = (
|
||||||
|
"This demonstration shows how the ereader can dynamically switch between "
|
||||||
|
"different font families while maintaining your reading position. "
|
||||||
|
"The three bundled font families (Sans, Serif, and Monospace) can be "
|
||||||
|
"changed on-the-fly without recreating the document."
|
||||||
|
)
|
||||||
|
for word in intro_text.split():
|
||||||
|
intro.add_word(Word(word, intro_font))
|
||||||
|
blocks.append(intro)
|
||||||
|
|
||||||
|
# Section 1
|
||||||
|
section1_heading = Heading(level=HeadingLevel.H2, style=heading_font)
|
||||||
|
for word in "Sans-Serif Font".split():
|
||||||
|
section1_heading.add_word(Word(word, heading_font))
|
||||||
|
blocks.append(section1_heading)
|
||||||
|
|
||||||
|
para1 = Paragraph(default_font)
|
||||||
|
text1 = (
|
||||||
|
"Sans-serif fonts like DejaVu Sans are clean and modern, making them "
|
||||||
|
"ideal for screen reading. They lack the decorative strokes (serifs) "
|
||||||
|
"found in traditional typefaces, which can improve legibility on digital displays. "
|
||||||
|
"Many ereader applications default to sans-serif fonts for this reason."
|
||||||
|
)
|
||||||
|
for word in text1.split():
|
||||||
|
para1.add_word(Word(word, default_font))
|
||||||
|
blocks.append(para1)
|
||||||
|
|
||||||
|
# Section 2
|
||||||
|
section2_heading = Heading(level=HeadingLevel.H2, style=heading_font)
|
||||||
|
for word in "Serif Font".split():
|
||||||
|
section2_heading.add_word(Word(word, heading_font))
|
||||||
|
blocks.append(section2_heading)
|
||||||
|
|
||||||
|
para2 = Paragraph(default_font)
|
||||||
|
text2 = (
|
||||||
|
"Serif fonts like DejaVu Serif have small decorative strokes at the ends "
|
||||||
|
"of letter strokes. These fonts are traditionally used in print media and "
|
||||||
|
"can give a more formal, classic appearance. Many readers prefer serif fonts "
|
||||||
|
"for long-form reading as they find them easier on the eyes."
|
||||||
|
)
|
||||||
|
for word in text2.split():
|
||||||
|
para2.add_word(Word(word, default_font))
|
||||||
|
blocks.append(para2)
|
||||||
|
|
||||||
|
# Section 3
|
||||||
|
section3_heading = Heading(level=HeadingLevel.H2, style=heading_font)
|
||||||
|
for word in "Monospace Font".split():
|
||||||
|
section3_heading.add_word(Word(word, heading_font))
|
||||||
|
blocks.append(section3_heading)
|
||||||
|
|
||||||
|
para3 = Paragraph(default_font)
|
||||||
|
text3 = (
|
||||||
|
"Monospace fonts like DejaVu Sans Mono have equal spacing between all characters. "
|
||||||
|
"They are commonly used for displaying code, technical documentation, and typewriter-style "
|
||||||
|
"text. While less common for general reading, some users prefer the uniform character "
|
||||||
|
"spacing for certain types of content."
|
||||||
|
)
|
||||||
|
for word in text3.split():
|
||||||
|
para3.add_word(Word(word, default_font))
|
||||||
|
blocks.append(para3)
|
||||||
|
|
||||||
|
# Final paragraph
|
||||||
|
conclusion = Paragraph(default_font)
|
||||||
|
conclusion_text = (
|
||||||
|
"The ability to switch fonts dynamically is a key feature of modern ereaders. "
|
||||||
|
"It allows readers to customize their reading experience based on personal preference, "
|
||||||
|
"lighting conditions, and content type. Try switching between the three font families "
|
||||||
|
"to see which one you prefer for different types of reading."
|
||||||
|
)
|
||||||
|
for word in conclusion_text.split():
|
||||||
|
conclusion.add_word(Word(word, default_font))
|
||||||
|
blocks.append(conclusion)
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def render_pages_with_different_fonts(manager, output_prefix="demo_11"):
|
||||||
|
"""Render the same page with different font families"""
|
||||||
|
|
||||||
|
print("\nRendering pages with different font families...")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
font_families = [
|
||||||
|
(None, "Original (Sans)"),
|
||||||
|
(BundledFont.SERIF, "Serif"),
|
||||||
|
(BundledFont.MONOSPACE, "Monospace"),
|
||||||
|
(BundledFont.SANS, "Sans (explicit)")
|
||||||
|
]
|
||||||
|
|
||||||
|
images = []
|
||||||
|
|
||||||
|
for font_family, name in font_families:
|
||||||
|
print(f"\nRendering with {name} font...")
|
||||||
|
|
||||||
|
# Switch font family
|
||||||
|
manager.set_font_family(font_family)
|
||||||
|
|
||||||
|
# Get current page
|
||||||
|
page = manager.get_current_page()
|
||||||
|
|
||||||
|
# Render to image
|
||||||
|
image = page.render()
|
||||||
|
filename = f"{output_prefix}_{name.lower().replace(' ', '_').replace('(', '').replace(')', '')}.png"
|
||||||
|
image.save(filename)
|
||||||
|
print(f" Saved: {filename}")
|
||||||
|
|
||||||
|
images.append((name, image))
|
||||||
|
|
||||||
|
return images
|
||||||
|
|
||||||
|
|
||||||
|
def demonstrate_font_switching():
|
||||||
|
"""Main demonstration function"""
|
||||||
|
print("\n")
|
||||||
|
print("=" * 70)
|
||||||
|
print("Font Family Switching Demonstration")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
# Create sample content
|
||||||
|
print("Creating sample document...")
|
||||||
|
blocks = create_sample_content()
|
||||||
|
print(f" Created {len(blocks)} blocks")
|
||||||
|
|
||||||
|
# Initialize ereader manager
|
||||||
|
print("\nInitializing ereader manager...")
|
||||||
|
page_size = (600, 800)
|
||||||
|
manager = create_ereader_manager(
|
||||||
|
blocks,
|
||||||
|
page_size,
|
||||||
|
document_id="font_switching_demo"
|
||||||
|
)
|
||||||
|
print(f" Page size: {page_size[0]}x{page_size[1]}")
|
||||||
|
print(f" Initial font family: {manager.get_font_family()}")
|
||||||
|
|
||||||
|
# Render pages with different fonts
|
||||||
|
images = render_pages_with_different_fonts(manager)
|
||||||
|
|
||||||
|
# Show position info
|
||||||
|
print("\nPosition information after font switches:")
|
||||||
|
print(" " + "-" * 66)
|
||||||
|
pos_info = manager.get_position_info()
|
||||||
|
print(f" Current position: Block {pos_info['position']['block_index']}, "
|
||||||
|
f"Word {pos_info['position']['word_index']}")
|
||||||
|
print(f" Font family: {pos_info['font_family'] or 'Original'}")
|
||||||
|
print(f" Font scale: {pos_info['font_scale']}")
|
||||||
|
print(f" Reading progress: {pos_info['progress']:.1%}")
|
||||||
|
|
||||||
|
# Test navigation with font switching
|
||||||
|
print("\nTesting navigation with font switching...")
|
||||||
|
print(" " + "-" * 66)
|
||||||
|
|
||||||
|
# Reset to beginning
|
||||||
|
manager.jump_to_position(manager.current_position.__class__())
|
||||||
|
|
||||||
|
# Advance a few pages with serif font
|
||||||
|
manager.set_font_family(BundledFont.SERIF)
|
||||||
|
print(f" Switched to SERIF font")
|
||||||
|
|
||||||
|
for i in range(3):
|
||||||
|
next_page = manager.next_page()
|
||||||
|
if next_page:
|
||||||
|
print(f" Page {i+2}: Advanced successfully")
|
||||||
|
|
||||||
|
# Switch to monospace
|
||||||
|
manager.set_font_family(BundledFont.MONOSPACE)
|
||||||
|
print(f" Switched to MONOSPACE font")
|
||||||
|
current_page = manager.get_current_page()
|
||||||
|
print(f" Re-rendered current page with new font")
|
||||||
|
|
||||||
|
# Go back a page
|
||||||
|
prev_page = manager.previous_page()
|
||||||
|
if prev_page:
|
||||||
|
print(f" Navigated back successfully")
|
||||||
|
|
||||||
|
# Cache statistics
|
||||||
|
print("\nCache statistics:")
|
||||||
|
print(" " + "-" * 66)
|
||||||
|
stats = manager.get_cache_stats()
|
||||||
|
for key, value in stats.items():
|
||||||
|
print(f" {key}: {value}")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("=" * 70)
|
||||||
|
print("Demo complete!")
|
||||||
|
print()
|
||||||
|
print("Key features demonstrated:")
|
||||||
|
print(" ✓ Dynamic font family switching (Sans, Serif, Monospace)")
|
||||||
|
print(" ✓ Position preservation across font changes")
|
||||||
|
print(" ✓ Automatic cache invalidation on font change")
|
||||||
|
print(" ✓ Navigation with different fonts")
|
||||||
|
print(" ✓ Font family info in position tracking")
|
||||||
|
print()
|
||||||
|
print("The rendered pages show the same content in different font families.")
|
||||||
|
print("Notice how the layout adapts while maintaining readability.")
|
||||||
|
print("=" * 70)
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
demonstrate_font_switching()
|
||||||
252
examples/generate_readme_font_demo.py
Normal file
252
examples/generate_readme_font_demo.py
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
"""
|
||||||
|
Generate a demo image for README.md showing font family switching feature.
|
||||||
|
|
||||||
|
Creates a side-by-side comparison of the same content rendered in
|
||||||
|
Sans, Serif, and Monospace fonts.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyWebLayout.abstract import Paragraph, Heading, Word
|
||||||
|
from pyWebLayout.abstract.block import HeadingLevel
|
||||||
|
from pyWebLayout.style import Font
|
||||||
|
from pyWebLayout.style.fonts import BundledFont, FontWeight
|
||||||
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
|
from pyWebLayout.layout.ereader_manager import create_ereader_manager
|
||||||
|
from PIL import Image, ImageDraw, ImageFont
|
||||||
|
|
||||||
|
|
||||||
|
def create_demo_content():
|
||||||
|
"""Create concise demo content that fits nicely on a small page"""
|
||||||
|
blocks = []
|
||||||
|
|
||||||
|
# Title
|
||||||
|
title_font = Font.from_family(BundledFont.SANS, font_size=28, weight=FontWeight.BOLD)
|
||||||
|
title = Heading(level=HeadingLevel.H1, style=title_font)
|
||||||
|
for word in "The Adventure Begins".split():
|
||||||
|
title.add_word(Word(word, title_font))
|
||||||
|
blocks.append(title)
|
||||||
|
|
||||||
|
# Paragraph
|
||||||
|
body_font = Font.from_family(BundledFont.SANS, font_size=14)
|
||||||
|
para = Paragraph(body_font)
|
||||||
|
text = (
|
||||||
|
"In the quiet village of Millbrook, young Emma discovered an ancient map "
|
||||||
|
"hidden in her grandmother's attic. The parchment revealed a mysterious "
|
||||||
|
"forest path marked with symbols she had never seen before. With courage "
|
||||||
|
"in her heart and the map in her pocket, she set out at dawn to uncover "
|
||||||
|
"the secrets that lay beyond the old oak trees."
|
||||||
|
)
|
||||||
|
for word in text.split():
|
||||||
|
para.add_word(Word(word, body_font))
|
||||||
|
blocks.append(para)
|
||||||
|
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def render_with_font_family(blocks, page_size, font_family, family_name):
|
||||||
|
"""Render a page with a specific font family"""
|
||||||
|
manager = create_ereader_manager(
|
||||||
|
blocks,
|
||||||
|
page_size,
|
||||||
|
document_id=f"demo_{family_name.lower()}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Set font family (None means original/default)
|
||||||
|
manager.set_font_family(font_family)
|
||||||
|
|
||||||
|
# Get the first page
|
||||||
|
page = manager.get_current_page()
|
||||||
|
return page.render()
|
||||||
|
|
||||||
|
|
||||||
|
def create_comparison_image():
|
||||||
|
"""Create a side-by-side comparison of all three font families"""
|
||||||
|
|
||||||
|
# Page size for each panel
|
||||||
|
page_width = 400
|
||||||
|
page_height = 300
|
||||||
|
|
||||||
|
# Create demo content
|
||||||
|
print("Creating demo content...")
|
||||||
|
blocks = create_demo_content()
|
||||||
|
|
||||||
|
# Render with each font family
|
||||||
|
print("Rendering with Sans font...")
|
||||||
|
sans_image = render_with_font_family(
|
||||||
|
blocks, (page_width, page_height), BundledFont.SANS, "Sans"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Rendering with Serif font...")
|
||||||
|
serif_image = render_with_font_family(
|
||||||
|
blocks, (page_width, page_height), BundledFont.SERIF, "Serif"
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Rendering with Monospace font...")
|
||||||
|
mono_image = render_with_font_family(
|
||||||
|
blocks, (page_width, page_height), BundledFont.MONOSPACE, "Monospace"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a composite image with all three side by side
|
||||||
|
spacing = 20
|
||||||
|
label_height = 30
|
||||||
|
total_width = page_width * 3 + spacing * 4
|
||||||
|
total_height = page_height + label_height + spacing * 2
|
||||||
|
|
||||||
|
composite = Image.new('RGB', (total_width, total_height), color='#f5f5f5')
|
||||||
|
|
||||||
|
# Paste the three images
|
||||||
|
x_positions = [
|
||||||
|
spacing,
|
||||||
|
spacing * 2 + page_width,
|
||||||
|
spacing * 3 + page_width * 2
|
||||||
|
]
|
||||||
|
|
||||||
|
for img, x_pos in zip([sans_image, serif_image, mono_image], x_positions):
|
||||||
|
composite.paste(img, (x_pos, label_height + spacing))
|
||||||
|
|
||||||
|
# Add labels
|
||||||
|
draw = ImageDraw.Draw(composite)
|
||||||
|
|
||||||
|
# Try to use a nice font, fallback to default if not available
|
||||||
|
try:
|
||||||
|
label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 20)
|
||||||
|
except:
|
||||||
|
label_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
labels = ["Sans-Serif", "Serif", "Monospace"]
|
||||||
|
for label, x_pos in zip(labels, x_positions):
|
||||||
|
# Calculate text position to center it
|
||||||
|
bbox = draw.textbbox((0, 0), label, font=label_font)
|
||||||
|
text_width = bbox[2] - bbox[0]
|
||||||
|
text_x = x_pos + (page_width - text_width) // 2
|
||||||
|
|
||||||
|
draw.text((text_x, 5), label, fill='#333333', font=label_font)
|
||||||
|
|
||||||
|
# Save the image
|
||||||
|
output_path = "docs/images/font_family_switching.png"
|
||||||
|
composite.save(output_path, quality=95)
|
||||||
|
print(f"\n✓ Saved demo image to: {output_path}")
|
||||||
|
print(f" Image size: {total_width}x{total_height}")
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def create_single_vertical_comparison():
|
||||||
|
"""Create a vertical comparison that's better for README"""
|
||||||
|
|
||||||
|
# Page size for each panel
|
||||||
|
page_width = 700
|
||||||
|
page_height = 280
|
||||||
|
|
||||||
|
# Create demo content
|
||||||
|
print("\nCreating vertical comparison for README...")
|
||||||
|
blocks = create_demo_content()
|
||||||
|
|
||||||
|
# Render with each font family
|
||||||
|
print(" Rendering Sans...")
|
||||||
|
sans_image = render_with_font_family(
|
||||||
|
blocks, (page_width, page_height), BundledFont.SANS, "Sans"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" Rendering Serif...")
|
||||||
|
serif_image = render_with_font_family(
|
||||||
|
blocks, (page_width, page_height), BundledFont.SERIF, "Serif"
|
||||||
|
)
|
||||||
|
|
||||||
|
print(" Rendering Monospace...")
|
||||||
|
mono_image = render_with_font_family(
|
||||||
|
blocks, (page_width, page_height), BundledFont.MONOSPACE, "Monospace"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a composite image stacked vertically
|
||||||
|
spacing = 15
|
||||||
|
label_width = 120
|
||||||
|
total_width = page_width + label_width + spacing * 2
|
||||||
|
total_height = page_height * 3 + spacing * 4
|
||||||
|
|
||||||
|
composite = Image.new('RGB', (total_width, total_height), color='#ffffff')
|
||||||
|
|
||||||
|
# Add a subtle border
|
||||||
|
draw = ImageDraw.Draw(composite)
|
||||||
|
draw.rectangle([(0, 0), (total_width-1, total_height-1)], outline='#e0e0e0', width=1)
|
||||||
|
|
||||||
|
# Paste the three images vertically
|
||||||
|
y_positions = [
|
||||||
|
spacing,
|
||||||
|
spacing * 2 + page_height,
|
||||||
|
spacing * 3 + page_height * 2
|
||||||
|
]
|
||||||
|
|
||||||
|
images_data = [
|
||||||
|
(sans_image, "Sans-Serif", "#4A90E2"),
|
||||||
|
(serif_image, "Serif", "#E94B3C"),
|
||||||
|
(mono_image, "Monospace", "#50C878")
|
||||||
|
]
|
||||||
|
|
||||||
|
# Try to use a nice font
|
||||||
|
try:
|
||||||
|
label_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", 16)
|
||||||
|
small_font = ImageFont.truetype("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", 11)
|
||||||
|
except:
|
||||||
|
label_font = ImageFont.load_default()
|
||||||
|
small_font = ImageFont.load_default()
|
||||||
|
|
||||||
|
for (img, label, color), y_pos in zip(images_data, y_positions):
|
||||||
|
# Paste the page image
|
||||||
|
composite.paste(img, (label_width + spacing, y_pos))
|
||||||
|
|
||||||
|
# Draw label background
|
||||||
|
draw.rectangle(
|
||||||
|
[(spacing, y_pos + 10), (label_width, y_pos + 40)],
|
||||||
|
fill=color
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw label text
|
||||||
|
draw.text(
|
||||||
|
(spacing + 10, y_pos + 17),
|
||||||
|
label,
|
||||||
|
fill='#ffffff',
|
||||||
|
font=label_font
|
||||||
|
)
|
||||||
|
|
||||||
|
# Draw font description
|
||||||
|
descriptions = {
|
||||||
|
"Sans-Serif": "Clean & Modern",
|
||||||
|
"Serif": "Classic & Formal",
|
||||||
|
"Monospace": "Code & Technical"
|
||||||
|
}
|
||||||
|
draw.text(
|
||||||
|
(spacing + 5, y_pos + 50),
|
||||||
|
descriptions[label],
|
||||||
|
fill='#666666',
|
||||||
|
font=small_font
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the image
|
||||||
|
output_path = "docs/images/font_family_switching_vertical.png"
|
||||||
|
composite.save(output_path, quality=95)
|
||||||
|
print(f" ✓ Saved: {output_path}")
|
||||||
|
print(f" Size: {total_width}x{total_height}")
|
||||||
|
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
print("=" * 70)
|
||||||
|
print("Generating README Demo Images")
|
||||||
|
print("=" * 70)
|
||||||
|
|
||||||
|
# Create both versions
|
||||||
|
horizontal_path = create_comparison_image()
|
||||||
|
vertical_path = create_single_vertical_comparison()
|
||||||
|
|
||||||
|
print("\n" + "=" * 70)
|
||||||
|
print("Demo images generated successfully!")
|
||||||
|
print("=" * 70)
|
||||||
|
print(f"\nHorizontal comparison: {horizontal_path}")
|
||||||
|
print(f"Vertical comparison: {vertical_path}")
|
||||||
|
print("\nRecommended for README: vertical version")
|
||||||
|
print("\nMarkdown snippet:")
|
||||||
|
print("```markdown")
|
||||||
|
print("")
|
||||||
|
print("```")
|
||||||
|
print()
|
||||||
@ -21,6 +21,7 @@ from pyWebLayout.concrete.page import Page
|
|||||||
from pyWebLayout.concrete.text import Text
|
from pyWebLayout.concrete.text import Text
|
||||||
from pyWebLayout.style.page_style import PageStyle
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
from pyWebLayout.style import Font
|
from pyWebLayout.style import Font
|
||||||
|
from pyWebLayout.style.fonts import BundledFont, get_bundled_font_path, FontWeight, FontStyle
|
||||||
from pyWebLayout.layout.document_layouter import paragraph_layouter, image_layouter
|
from pyWebLayout.layout.document_layouter import paragraph_layouter, image_layouter
|
||||||
|
|
||||||
|
|
||||||
@ -181,32 +182,50 @@ class ChapterNavigator:
|
|||||||
return self.chapters[0] if self.chapters else None
|
return self.chapters[0] if self.chapters else None
|
||||||
|
|
||||||
|
|
||||||
class FontScaler:
|
class FontFamilyOverride:
|
||||||
"""
|
"""
|
||||||
Handles font scaling operations for ereader font size adjustments.
|
Manages font family preferences for ereader rendering.
|
||||||
Applies scaling at layout/render time while preserving original font objects.
|
Allows dynamic font family switching without modifying source blocks.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
def __init__(self, preferred_family: Optional[BundledFont] = None):
|
||||||
def scale_font(font: Font, scale_factor: float) -> Font:
|
|
||||||
"""
|
"""
|
||||||
Create a scaled version of a font for layout calculations.
|
Initialize font family override.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
preferred_family: Preferred bundled font family (None = use original fonts)
|
||||||
|
"""
|
||||||
|
self.preferred_family = preferred_family
|
||||||
|
|
||||||
|
def override_font(self, font: Font) -> Font:
|
||||||
|
"""
|
||||||
|
Create a new font with the preferred family while preserving other attributes.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
font: Original font object
|
font: Original font object
|
||||||
scale_factor: Scaling factor (1.0 = no change, 2.0 = double size, etc.)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
New Font object with scaled size
|
Font with overridden family, or original if no override is set
|
||||||
"""
|
"""
|
||||||
if scale_factor == 1.0:
|
if self.preferred_family is None:
|
||||||
return font
|
return font
|
||||||
|
|
||||||
scaled_size = max(1, int(font.font_size * scale_factor))
|
# Get the appropriate font path for the preferred family
|
||||||
|
# preserving the original font's weight and style
|
||||||
|
new_font_path = get_bundled_font_path(
|
||||||
|
family=self.preferred_family,
|
||||||
|
weight=font.weight,
|
||||||
|
style=font.style
|
||||||
|
)
|
||||||
|
|
||||||
|
# If we couldn't find a matching font, fall back to original
|
||||||
|
if new_font_path is None:
|
||||||
|
return font
|
||||||
|
|
||||||
|
# Create a new font with the overridden path
|
||||||
return Font(
|
return Font(
|
||||||
font_path=font._font_path,
|
font_path=new_font_path,
|
||||||
font_size=scaled_size,
|
font_size=font.font_size,
|
||||||
colour=font.colour,
|
colour=font.colour,
|
||||||
weight=font.weight,
|
weight=font.weight,
|
||||||
style=font.style,
|
style=font.style,
|
||||||
@ -216,6 +235,49 @@ class FontScaler:
|
|||||||
min_hyphenation_width=font.min_hyphenation_width
|
min_hyphenation_width=font.min_hyphenation_width
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FontScaler:
|
||||||
|
"""
|
||||||
|
Handles font scaling operations for ereader font size adjustments.
|
||||||
|
Applies scaling at layout/render time while preserving original font objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def scale_font(font: Font, scale_factor: float, family_override: Optional[FontFamilyOverride] = None) -> Font:
|
||||||
|
"""
|
||||||
|
Create a scaled version of a font for layout calculations.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font: Original font object
|
||||||
|
scale_factor: Scaling factor (1.0 = no change, 2.0 = double size, etc.)
|
||||||
|
family_override: Optional font family override
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
New Font object with scaled size and optional family override
|
||||||
|
"""
|
||||||
|
# Apply family override first if specified
|
||||||
|
working_font = font
|
||||||
|
if family_override is not None:
|
||||||
|
working_font = family_override.override_font(font)
|
||||||
|
|
||||||
|
# Then apply scaling
|
||||||
|
if scale_factor == 1.0:
|
||||||
|
return working_font
|
||||||
|
|
||||||
|
scaled_size = max(1, int(working_font.font_size * scale_factor))
|
||||||
|
|
||||||
|
return Font(
|
||||||
|
font_path=working_font._font_path,
|
||||||
|
font_size=scaled_size,
|
||||||
|
colour=working_font.colour,
|
||||||
|
weight=working_font.weight,
|
||||||
|
style=working_font.style,
|
||||||
|
decoration=working_font.decoration,
|
||||||
|
background=working_font.background,
|
||||||
|
language=working_font.language,
|
||||||
|
min_hyphenation_width=working_font.min_hyphenation_width
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def scale_word_spacing(spacing: Tuple[int, int],
|
def scale_word_spacing(spacing: Tuple[int, int],
|
||||||
scale_factor: float) -> Tuple[int, int]:
|
scale_factor: float) -> Tuple[int, int]:
|
||||||
@ -242,12 +304,14 @@ class BidirectionalLayouter:
|
|||||||
page_size: Tuple[int,
|
page_size: Tuple[int,
|
||||||
int] = (800,
|
int] = (800,
|
||||||
600),
|
600),
|
||||||
alignment_override=None):
|
alignment_override=None,
|
||||||
|
font_family_override: Optional[FontFamilyOverride] = None):
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
self.page_style = page_style
|
self.page_style = page_style
|
||||||
self.page_size = page_size
|
self.page_size = page_size
|
||||||
self.chapter_navigator = ChapterNavigator(blocks)
|
self.chapter_navigator = ChapterNavigator(blocks)
|
||||||
self.alignment_override = alignment_override
|
self.alignment_override = alignment_override
|
||||||
|
self.font_family_override = font_family_override
|
||||||
|
|
||||||
def render_page_forward(self, position: RenderingPosition,
|
def render_page_forward(self, position: RenderingPosition,
|
||||||
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||||
@ -401,14 +465,15 @@ class BidirectionalLayouter:
|
|||||||
return final_page, final_start
|
return final_page, final_start
|
||||||
|
|
||||||
def _scale_block_fonts(self, block: Block, font_scale: float) -> Block:
|
def _scale_block_fonts(self, block: Block, font_scale: float) -> Block:
|
||||||
"""Apply font scaling to all fonts in a block"""
|
"""Apply font scaling and font family override to all fonts in a block"""
|
||||||
if font_scale == 1.0:
|
# Check if we need to do any transformation
|
||||||
|
if font_scale == 1.0 and self.font_family_override is None:
|
||||||
return block
|
return block
|
||||||
|
|
||||||
# This is a simplified implementation
|
# This is a simplified implementation
|
||||||
# In practice, we'd need to handle each block type appropriately
|
# In practice, we'd need to handle each block type appropriately
|
||||||
if isinstance(block, (Paragraph, Heading)):
|
if isinstance(block, (Paragraph, Heading)):
|
||||||
scaled_block_style = FontScaler.scale_font(block.style, font_scale)
|
scaled_block_style = FontScaler.scale_font(block.style, font_scale, self.font_family_override)
|
||||||
if isinstance(block, Heading):
|
if isinstance(block, Heading):
|
||||||
scaled_block = Heading(block.level, scaled_block_style)
|
scaled_block = Heading(block.level, scaled_block_style)
|
||||||
else:
|
else:
|
||||||
@ -419,7 +484,7 @@ class BidirectionalLayouter:
|
|||||||
if isinstance(word, Word):
|
if isinstance(word, Word):
|
||||||
scaled_word = Word(
|
scaled_word = Word(
|
||||||
word.text, FontScaler.scale_font(
|
word.text, FontScaler.scale_font(
|
||||||
word.style, font_scale))
|
word.style, font_scale, self.font_family_override))
|
||||||
scaled_block.add_word(scaled_word)
|
scaled_block.add_word(scaled_word)
|
||||||
return scaled_block
|
return scaled_block
|
||||||
|
|
||||||
|
|||||||
@ -17,6 +17,7 @@ from pyWebLayout.abstract.block import Block, HeadingLevel, Image, BlockType
|
|||||||
from pyWebLayout.concrete.page import Page
|
from pyWebLayout.concrete.page import Page
|
||||||
from pyWebLayout.concrete.image import RenderableImage
|
from pyWebLayout.concrete.image import RenderableImage
|
||||||
from pyWebLayout.style.page_style import PageStyle
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
|
from pyWebLayout.style.fonts import BundledFont
|
||||||
from pyWebLayout.layout.document_layouter import image_layouter
|
from pyWebLayout.layout.document_layouter import image_layouter
|
||||||
|
|
||||||
|
|
||||||
@ -154,6 +155,7 @@ class EreaderLayoutManager:
|
|||||||
Features:
|
Features:
|
||||||
- Sub-second page rendering with intelligent buffering
|
- Sub-second page rendering with intelligent buffering
|
||||||
- Font scaling support
|
- Font scaling support
|
||||||
|
- Dynamic font family switching (Sans, Serif, Monospace)
|
||||||
- Chapter navigation
|
- Chapter navigation
|
||||||
- Bookmark management
|
- Bookmark management
|
||||||
- Position persistence
|
- Position persistence
|
||||||
@ -550,6 +552,43 @@ class EreaderLayoutManager:
|
|||||||
"""Get the current font scale"""
|
"""Get the current font scale"""
|
||||||
return self.font_scale
|
return self.font_scale
|
||||||
|
|
||||||
|
def set_font_family(self, family: Optional[BundledFont]) -> Page:
|
||||||
|
"""
|
||||||
|
Change the font family and re-render current page.
|
||||||
|
|
||||||
|
Switches all text in the document to use the specified bundled font family
|
||||||
|
while preserving font weights, styles, sizes, and other attributes.
|
||||||
|
Clears page history and cache since font changes invalidate all cached positions.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
family: Bundled font family to use (SANS, SERIF, MONOSPACE, or None for original fonts)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Re-rendered page with new font family
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> from pyWebLayout.style.fonts import BundledFont
|
||||||
|
>>> manager.set_font_family(BundledFont.SERIF) # Switch to serif
|
||||||
|
>>> manager.set_font_family(BundledFont.SANS) # Switch to sans
|
||||||
|
>>> manager.set_font_family(None) # Restore original fonts
|
||||||
|
"""
|
||||||
|
# Update the renderer's font family
|
||||||
|
self.renderer.set_font_family(family)
|
||||||
|
|
||||||
|
# Clear history since font changes invalidate all cached positions
|
||||||
|
self._clear_history()
|
||||||
|
|
||||||
|
return self.get_current_page()
|
||||||
|
|
||||||
|
def get_font_family(self) -> Optional[BundledFont]:
|
||||||
|
"""
|
||||||
|
Get the current font family override.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Current font family (SANS, SERIF, MONOSPACE) or None if using original fonts
|
||||||
|
"""
|
||||||
|
return self.renderer.get_font_family()
|
||||||
|
|
||||||
def increase_line_spacing(self, amount: int = 2) -> Page:
|
def increase_line_spacing(self, amount: int = 2) -> Page:
|
||||||
"""
|
"""
|
||||||
Increase line spacing and re-render current page.
|
Increase line spacing and re-render current page.
|
||||||
@ -787,6 +826,7 @@ class EreaderLayoutManager:
|
|||||||
Dictionary with position details
|
Dictionary with position details
|
||||||
"""
|
"""
|
||||||
current_chapter = self.get_current_chapter()
|
current_chapter = self.get_current_chapter()
|
||||||
|
font_family = self.get_font_family()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
'position': self.current_position.to_dict(),
|
'position': self.current_position.to_dict(),
|
||||||
@ -799,6 +839,7 @@ class EreaderLayoutManager:
|
|||||||
},
|
},
|
||||||
'progress': self.get_reading_progress(),
|
'progress': self.get_reading_progress(),
|
||||||
'font_scale': self.font_scale,
|
'font_scale': self.font_scale,
|
||||||
|
'font_family': font_family.value if font_family else None,
|
||||||
'page_size': self.page_size
|
'page_size': self.page_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -12,31 +12,36 @@ from concurrent.futures import ProcessPoolExecutor, Future
|
|||||||
import threading
|
import threading
|
||||||
import pickle
|
import pickle
|
||||||
|
|
||||||
from .ereader_layout import RenderingPosition, BidirectionalLayouter
|
from .ereader_layout import RenderingPosition, BidirectionalLayouter, FontFamilyOverride
|
||||||
from pyWebLayout.concrete.page import Page
|
from pyWebLayout.concrete.page import Page
|
||||||
from pyWebLayout.abstract.block import Block
|
from pyWebLayout.abstract.block import Block
|
||||||
from pyWebLayout.style.page_style import PageStyle
|
from pyWebLayout.style.page_style import PageStyle
|
||||||
|
from pyWebLayout.style.fonts import BundledFont
|
||||||
|
|
||||||
|
|
||||||
def _render_page_worker(args: Tuple[List[Block],
|
def _render_page_worker(args: Tuple[List[Block],
|
||||||
PageStyle,
|
PageStyle,
|
||||||
RenderingPosition,
|
RenderingPosition,
|
||||||
float,
|
float,
|
||||||
bool]) -> Tuple[RenderingPosition,
|
bool,
|
||||||
|
Optional[BundledFont]]) -> Tuple[RenderingPosition,
|
||||||
bytes,
|
bytes,
|
||||||
RenderingPosition]:
|
RenderingPosition]:
|
||||||
"""
|
"""
|
||||||
Worker function for multiprocess page rendering.
|
Worker function for multiprocess page rendering.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
args: Tuple of (blocks, page_style, position, font_scale, is_backward)
|
args: Tuple of (blocks, page_style, position, font_scale, is_backward, font_family)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Tuple of (original_position, pickled_page, next_position)
|
Tuple of (original_position, pickled_page, next_position)
|
||||||
"""
|
"""
|
||||||
blocks, page_style, position, font_scale, is_backward = args
|
blocks, page_style, position, font_scale, is_backward, font_family = args
|
||||||
|
|
||||||
layouter = BidirectionalLayouter(blocks, page_style)
|
# Create font family override if specified
|
||||||
|
font_family_override = FontFamilyOverride(font_family) if font_family else None
|
||||||
|
|
||||||
|
layouter = BidirectionalLayouter(blocks, page_style, font_family_override=font_family_override)
|
||||||
|
|
||||||
if is_backward:
|
if is_backward:
|
||||||
page, next_pos = layouter.render_page_backward(position, font_scale)
|
page, next_pos = layouter.render_page_backward(position, font_scale)
|
||||||
@ -85,12 +90,14 @@ class PageBuffer:
|
|||||||
self.blocks: Optional[List[Block]] = None
|
self.blocks: Optional[List[Block]] = None
|
||||||
self.page_style: Optional[PageStyle] = None
|
self.page_style: Optional[PageStyle] = None
|
||||||
self.current_font_scale: float = 1.0
|
self.current_font_scale: float = 1.0
|
||||||
|
self.current_font_family: Optional[BundledFont] = None
|
||||||
|
|
||||||
def initialize(
|
def initialize(
|
||||||
self,
|
self,
|
||||||
blocks: List[Block],
|
blocks: List[Block],
|
||||||
page_style: PageStyle,
|
page_style: PageStyle,
|
||||||
font_scale: float = 1.0):
|
font_scale: float = 1.0,
|
||||||
|
font_family: Optional[BundledFont] = None):
|
||||||
"""
|
"""
|
||||||
Initialize the buffer with document blocks and page style.
|
Initialize the buffer with document blocks and page style.
|
||||||
|
|
||||||
@ -98,10 +105,12 @@ class PageBuffer:
|
|||||||
blocks: Document blocks to render
|
blocks: Document blocks to render
|
||||||
page_style: Page styling configuration
|
page_style: Page styling configuration
|
||||||
font_scale: Current font scaling factor
|
font_scale: Current font scaling factor
|
||||||
|
font_family: Optional font family override
|
||||||
"""
|
"""
|
||||||
self.blocks = blocks
|
self.blocks = blocks
|
||||||
self.page_style = page_style
|
self.page_style = page_style
|
||||||
self.current_font_scale = font_scale
|
self.current_font_scale = font_scale
|
||||||
|
self.current_font_family = font_family
|
||||||
|
|
||||||
# Start the process pool
|
# Start the process pool
|
||||||
if self.executor is None:
|
if self.executor is None:
|
||||||
@ -207,7 +216,8 @@ class PageBuffer:
|
|||||||
self.page_style,
|
self.page_style,
|
||||||
current_pos,
|
current_pos,
|
||||||
self.current_font_scale,
|
self.current_font_scale,
|
||||||
False)
|
False,
|
||||||
|
self.current_font_family)
|
||||||
future = self.executor.submit(_render_page_worker, args)
|
future = self.executor.submit(_render_page_worker, args)
|
||||||
self.pending_renders[current_pos] = future
|
self.pending_renders[current_pos] = future
|
||||||
|
|
||||||
@ -234,7 +244,8 @@ class PageBuffer:
|
|||||||
self.page_style,
|
self.page_style,
|
||||||
current_pos,
|
current_pos,
|
||||||
self.current_font_scale,
|
self.current_font_scale,
|
||||||
True)
|
True,
|
||||||
|
self.current_font_family)
|
||||||
future = self.executor.submit(_render_page_worker, args)
|
future = self.executor.submit(_render_page_worker, args)
|
||||||
self.pending_renders[current_pos] = future
|
self.pending_renders[current_pos] = future
|
||||||
|
|
||||||
@ -296,6 +307,17 @@ class PageBuffer:
|
|||||||
self.current_font_scale = font_scale
|
self.current_font_scale = font_scale
|
||||||
self.invalidate_all()
|
self.invalidate_all()
|
||||||
|
|
||||||
|
def set_font_family(self, font_family: Optional[BundledFont]):
|
||||||
|
"""
|
||||||
|
Update font family and invalidate cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font_family: New font family (None = use original fonts)
|
||||||
|
"""
|
||||||
|
if font_family != self.current_font_family:
|
||||||
|
self.current_font_family = font_family
|
||||||
|
self.invalidate_all()
|
||||||
|
|
||||||
def get_cache_stats(self) -> Dict[str, Any]:
|
def get_cache_stats(self) -> Dict[str, Any]:
|
||||||
"""Get cache statistics for debugging/monitoring"""
|
"""Get cache statistics for debugging/monitoring"""
|
||||||
return {
|
return {
|
||||||
@ -304,7 +326,8 @@ class PageBuffer:
|
|||||||
'pending_renders': len(self.pending_renders),
|
'pending_renders': len(self.pending_renders),
|
||||||
'position_mappings': len(self.position_map),
|
'position_mappings': len(self.position_map),
|
||||||
'reverse_position_mappings': len(self.reverse_position_map),
|
'reverse_position_mappings': len(self.reverse_position_map),
|
||||||
'current_font_scale': self.current_font_scale
|
'current_font_scale': self.current_font_scale,
|
||||||
|
'current_font_family': self.current_font_family.value if self.current_font_family else None
|
||||||
}
|
}
|
||||||
|
|
||||||
def shutdown(self):
|
def shutdown(self):
|
||||||
@ -338,7 +361,8 @@ class BufferedPageRenderer:
|
|||||||
buffer_size: int = 5,
|
buffer_size: int = 5,
|
||||||
page_size: Tuple[int,
|
page_size: Tuple[int,
|
||||||
int] = (800,
|
int] = (800,
|
||||||
600)):
|
600),
|
||||||
|
font_family: Optional[BundledFont] = None):
|
||||||
"""
|
"""
|
||||||
Initialize the buffered renderer.
|
Initialize the buffered renderer.
|
||||||
|
|
||||||
@ -347,13 +371,21 @@ class BufferedPageRenderer:
|
|||||||
page_style: Page styling configuration
|
page_style: Page styling configuration
|
||||||
buffer_size: Number of pages to cache in each direction
|
buffer_size: Number of pages to cache in each direction
|
||||||
page_size: Page size (width, height) in pixels
|
page_size: Page size (width, height) in pixels
|
||||||
|
font_family: Optional font family override
|
||||||
"""
|
"""
|
||||||
self.layouter = BidirectionalLayouter(blocks, page_style, page_size)
|
# Create font family override if specified
|
||||||
|
font_family_override = FontFamilyOverride(font_family) if font_family else None
|
||||||
|
|
||||||
|
self.layouter = BidirectionalLayouter(blocks, page_style, page_size, font_family_override=font_family_override)
|
||||||
self.buffer = PageBuffer(buffer_size)
|
self.buffer = PageBuffer(buffer_size)
|
||||||
self.buffer.initialize(blocks, page_style)
|
self.buffer.initialize(blocks, page_style, font_family=font_family)
|
||||||
|
self.page_size = page_size
|
||||||
|
self.blocks = blocks
|
||||||
|
self.page_style = page_style
|
||||||
|
|
||||||
self.current_position = RenderingPosition()
|
self.current_position = RenderingPosition()
|
||||||
self.font_scale = 1.0
|
self.font_scale = 1.0
|
||||||
|
self.font_family = font_family
|
||||||
|
|
||||||
def render_page(self, position: RenderingPosition,
|
def render_page(self, position: RenderingPosition,
|
||||||
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
font_scale: float = 1.0) -> Tuple[Page, RenderingPosition]:
|
||||||
@ -453,6 +485,32 @@ class BufferedPageRenderer:
|
|||||||
|
|
||||||
return page, start_pos
|
return page, start_pos
|
||||||
|
|
||||||
|
def set_font_family(self, font_family: Optional[BundledFont]):
|
||||||
|
"""
|
||||||
|
Change the font family and invalidate cache.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
font_family: New font family (None = use original fonts)
|
||||||
|
"""
|
||||||
|
if font_family != self.font_family:
|
||||||
|
self.font_family = font_family
|
||||||
|
|
||||||
|
# Update buffer
|
||||||
|
self.buffer.set_font_family(font_family)
|
||||||
|
|
||||||
|
# Recreate layouter with new font family override
|
||||||
|
font_family_override = FontFamilyOverride(font_family) if font_family else None
|
||||||
|
self.layouter = BidirectionalLayouter(
|
||||||
|
self.blocks,
|
||||||
|
self.page_style,
|
||||||
|
self.page_size,
|
||||||
|
font_family_override=font_family_override
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_font_family(self) -> Optional[BundledFont]:
|
||||||
|
"""Get the current font family override"""
|
||||||
|
return self.font_family
|
||||||
|
|
||||||
def get_cache_stats(self) -> Dict[str, Any]:
|
def get_cache_stats(self) -> Dict[str, Any]:
|
||||||
"""Get cache statistics"""
|
"""Get cache statistics"""
|
||||||
return self.buffer.get_cache_stats()
|
return self.buffer.get_cache_stats()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user