Commit Graph
150 Commits
Author SHA1 Message Date
dtourolle 202dacf350 fix(page): separate measurement context from the render canvas (S3)
add_child invalidated the canvas but left _draw bound to it, and the draw
property only rebuilt when _draw was None. Callers therefore got a context
pointing at a discarded image while page._canvas stayed None. table_layouter
reads page._canvas directly, so every image inside a table cell laid out after
any other content silently degraded to a grey [Image: WxH] placeholder.

The property now rebuilds when either half is missing. On its own that would
make layout allocate a full-page RGBA canvas per line, because layout measures
text through the page - so measurement moves to page.measurement_draw, a 1x1
scratch context that is never invalidated. Its mode matches the render canvas
so that Text's width cache does not hold two entries per word.

Children built against the scratch context are re-bound to the live canvas by
render_children, which already synchronised _draw and _canvas; that behaviour
was incidental and is now load-bearing and documented as such.

Regenerating the examples shows table images rendering as images rather than
placeholders. The empty header row in the second table of example 05 is
unrelated and pre-existing - row height ignores cell padding, so text is clipped
as padding grows - recorded as evidence under S6.
2026-08-06 22:37:24 +02:00
dtourolle 284d521125 fix(html): keep inline content inside block containers (S1)
Inline tags map to ignore_handler because they are meant to be consumed by
extract_text_content, but only paragraph_handler and heading_handler ever called
it. Every other container walked its children calling process_element, so inline
tags returned None and their text was dropped:

  <p>hello <b>world</b> again</p>      -> hello world again   (correct)
  <div>hello <b>world</b> again</div>  -> nothing at all
  <li>hello <b>world</b> again</li>    -> nothing at all
  <td>hello <b>world</b> again</td>    -> nothing at all
  <td><a href=u>link</a> text</td>     -> text   (link discarded)

div_handler ignored bare text nodes outright, so a div containing text produced
no blocks whatsoever - which for real HTML and EPUB is most of the document.
Where text did survive, in cells and list items, each text node became its own
paragraph, so "a <b>b</b> c" fragmented onto separate lines.

process_block_children now walks a container's children once, gathering runs of
inline content into a single paragraph and letting block children through to
their own handlers, preserving document order. div, li, td, th and blockquote
all delegate to it, so they gain nested blocks, links and mixed content
together. <br> ends the current run rather than being a no-op.

extract_text_content is split so the run-level logic can be reused without
building a synthetic element: extract_words_from_nodes takes the nodes directly,
and skips comments, which previously had their text extracted as content.

paragraph_handler keeps its own image-splitting path for now; folding it into
process_block_children would also fix the ordering of text around images in a
paragraph, but it carries the EPUB cover-detection behaviour and is left alone.
2026-08-06 22:31:34 +02:00
dtourolle 7bebe08432 Merge layout remediation: pagination dead-end, page padding, text alignment
Python CI / test (3.10) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
Python CI / test (3.13) (push) Has been cancelled
Three fixes from the block/table rendering audit, plus the spec covering the
remaining work.

S11: a paragraph larger than one page dead-ended the reader - the resume
position was discarded, so navigation reported no progress and the book
appeared to end mid-chapter.

S2: horizontal page padding was ignored, so text started flush against the left
border and lines broke short of the right one. Page now describes its content
box directly, and gained an origin so a page can be nested inside another
surface.

S13: ragged alignments stretched their word gaps by a varying amount per line;
justified paragraphs stretched their final line and fell a pixel or two short of
the margin. Body text now defaults to justified, configurable via
PageStyle.default_alignment.
2026-08-06 22:18:32 +02:00
dtourolle 1262be6a38 fix(text): constant word space for ragged alignment, exact justification (S13)
LeftAlignmentHandler spread each line's residual space across its word gaps,
clamped to max_spacing. A line whose residual divided to under max_spacing was
stretched flush, one that exceeded it was not, so left-aligned text was
justified sometimes, by a different amount per line - which reads as a wobbling
right edge rather than as ragged-right. Centre/right did the same, and computed
their start position from a different spacing than the one they returned, so
centred lines were not centred.

Ragged alignments now use a constant word space - the font's own space advance,
clamped to the style's bounds - and report overflow instead of tightening, so
line breaking decides what fits rather than rendering squeezing it.

Justification kept two further defects:

  - the final line of a paragraph was stretched across the measure, so a
    three-word tail was spread edge to edge. Line now carries is_paragraph_end,
    set on the line holding the last word, and renders flush left. A paragraph
    continued on the next page is not marked, so it stays justified.

  - gaps were floored per gap with a truncated remainder, discarding the
    fractional part of both. Lines stopped one or two pixels short, differently
    each time. Distributing by cumulative rounding makes the gaps sum to the
    residual exactly; advance ends now land identically on every line.

Alignment is configurable rather than hardcoded: PageStyle.default_alignment,
defaulting to JUSTIFY for body text. text_align on abstract and concrete styles
defaults to None meaning "unspecified", so HTML without text-align inherits the
page default while explicit CSS still wins. Headings are never justified.
2026-08-06 22:18:04 +02:00
dtourolle f18cec2da8 fix(layout): honour horizontal padding and page origin (S2)
paragraph_layouter placed lines at page.border_size while sizing them to
available_width, which subtracts both paddings. Text therefore started flush
against the left border and the entire padding budget accumulated on the right,
so lines broke well short of the right border.

Page now describes its content box directly - content_origin, content_rect and
remaining_height - and the layouters use it instead of each recomputing the
geometry from border_size. The four block layouters had all been computing
remaining space as size[1] - y_offset - border_size, subtracting the border but
not the bottom padding, so every block type could be placed into the bottom
padding; remaining_height fixes that too.

Page also gains an origin, defaulting to (0, 0). That is inert for a top-level
page but lets a page be positioned inside another surface, which table cells
need in order to be laid out by the normal engine.

Golden images regenerated: content now sits inside the padding on all sides.
2026-08-06 21:11:28 +02:00
dtourolle a57da8011e fix(ereader): keep resume position when a block spans a page (S11)
render_page_forward discarded new_pos on the failure path, but a block that
only partially fitted has still advanced the position: paragraph_layouter
reports the word it stopped at, and _layout_paragraph_on_page packs it into
new_pos. Dropping it told the caller no progress was made, so navigation
dead-ended on any paragraph larger than a single page - the reader saw "end of
document" mid-book.

A 2877-word paragraph at 800x600 rendered 26 lines and reported the start
position back; it now paginates across 12 pages.

Also guard the navigation loop: EreaderManager.next_page treats no-progress as
end-of-document, which is only correct at the actual end. Anywhere else it now
logs the offending block index and skips that block, so a future layout bug
costs one block rather than the rest of the book.
2026-08-06 21:06:29 +02:00
dtourolle 583366ae1d docs: add layout remediation spec
Twelve specs covering the defects found auditing the block/table rendering
path, plus the pagination dead-end and the broken background renderer.

Each spec carries a reproduction of the defect against 2a543d0, a design with
concrete signatures, and acceptance criteria. Five design invariants are stated
up front so future changes can be rejected by reference rather than re-argued.
2026-08-06 21:03:12 +02:00
dtourolleandClaude Opus 5 e000068384 Cache word widths and glyph bitmaps to cut page render time ~3.5x
Rendering a page re-measured and re-rasterised the same words constantly: at
1404x1872 a page issued ~2800 textlength calls and ~2500 draw.text calls for
fewer than 1000 distinct (font, string) pairs. Profiling a page turn showed
FreeType glyph rendering at 57% of total time and width measurement at 38% of
layout.

Both are now cached. Measured on Crime and Punishment at 1404x1872, one page:

    layout  35ms -> 19ms
    render  84ms -> 41ms
    total  120ms -> 60ms

Eviction ranks by use count rather than recency. Word frequency in prose is
Zipfian and stationary, so the words worth keeping are the ones used most, and
unlike recency this lets a document's own frequencies be seeded up front --
see prewarm_caches(). Two details keep the policy from costing more than it
saves, since get() runs once per word drawn:

  - counting is O(1) with no reordering, because structures that reorder on
    every hit measured 3-5ms/page slower than the hit rate they bought;
  - eviction samples 8 entries and drops the least used of those, rather than
    maintaining a global order.

Aging (halving all counts periodically) is on by default. Without it a font
size change drove the hit rate to 0% on a real access trace: every key was new
and the previous size's entries held counts nothing could beat.

Both caches are bounded, since the glyph bitmaps reach ~19MB over a long
session and the target is a 512MB Pi Zero 2. Defaults are 4MB of bitmaps and
8192 widths; configure_text_caches() tunes them. Cache size barely affects
speed (2MB is within 13% of unbounded) because a miss costs only one ~44us
rasterisation, so the bound can be set for memory, not throughput.

EreaderLayoutManager.prewarm_caches() counts the book's word frequencies and
preloads the most common ones, seeding each with its document frequency. This
moves that rasterisation to open time and cut misses by 27%, for ~15% faster
page turns at a one-off ~300ms cost. It is opt-in; nothing calls it yet.

Rendering is no longer bit-identical. PIL positions text at sub-pixel offsets,
so the cache buckets that phase, defaulting to 2 buckets per axis. Total ink
per page is unchanged and the mean pixel difference is 3.6/255 -- a fifth of
one step of a 16-level e-ink panel. subpixel_steps=4 halves that if wanted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-06 20:49:32 +02:00
dtourolle 2a543d0319 reduced redundant code
Python CI / test (3.10) (push) Successful in 2m23s
Python CI / test (3.12) (push) Successful in 2m15s
Python CI / test (3.13) (push) Successful in 2m11s
2025-11-11 21:19:41 +01:00
dtourolle 23d3278b50 update docs and gitignore
Python CI / test (3.10) (push) Successful in 2m23s
Python CI / test (3.12) (push) Successful in 2m13s
Python CI / test (3.13) (push) Successful in 2m10s
2025-11-11 18:19:49 +01:00
dtourolle 3bcd1bffb5 Tables now use ""dynamic page" allowing the contents to be anything that can be rendered ion a page.
Python CI / test (3.10) (push) Successful in 2m22s
Python CI / test (3.12) (push) Successful in 2m13s
Python CI / test (3.13) (push) Successful in 2m9s
2025-11-11 18:10:47 +01:00
dtourolle 889f27e1a3 added fotn change API and examples
Python CI / test (3.10) (push) Successful in 2m18s
Python CI / test (3.12) (push) Successful in 2m8s
Python CI / test (3.13) (push) Successful in 2m6s
2025-11-11 12:44:18 +01:00
dtourolle 9de67d958e cell height now dynamic in tables
Python CI / test (3.10) (push) Successful in 2m16s
Python CI / test (3.12) (push) Successful in 2m8s
Python CI / test (3.13) (push) Successful in 2m2s
2025-11-10 22:06:05 +01:00
dtourolle 41dc904755 fixed issue where last word was counted for spacing
Python CI / test (3.10) (push) Successful in 2m17s
Python CI / test (3.12) (push) Successful in 2m6s
Python CI / test (3.13) (push) Successful in 2m1s
2025-11-10 15:22:18 +01:00
dtourolle 8e720d4037 fix table in cell wrapping
Python CI / test (3.10) (push) Successful in 2m17s
Python CI / test (3.13) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
2025-11-10 15:15:03 +01:00
dtourolle 5afad2ca07 added line wrapping to table 2025-11-10 14:33:30 +01:00
dtourolle 303179865d tests for author names and metadata extraction
Python CI / test (3.10) (push) Successful in 2m16s
Python CI / test (3.12) (push) Successful in 2m7s
Python CI / test (3.13) (push) Successful in 2m2s
2025-11-10 13:54:36 +01:00
dtourolle fb52178cc6 fix for regresssion in fw/bw navigation
Python CI / test (3.10) (push) Successful in 2m15s
Python CI / test (3.12) (push) Successful in 2m7s
Python CI / test (3.13) (push) Successful in 2m0s
2025-11-10 13:25:04 +01:00
dtourolle 890a0e768b fix for reverse redering on restart
Python CI / test (3.10) (push) Successful in 2m10s
Python CI / test (3.12) (push) Successful in 2m2s
Python CI / test (3.13) (push) Successful in 1m56s
2025-11-10 13:17:20 +01:00
dtourolle a8e459bce5 fixed issue with cover and image rendering
Python CI / test (3.10) (push) Successful in 2m10s
Python CI / test (3.12) (push) Successful in 2m3s
Python CI / test (3.13) (push) Successful in 1m57s
2025-11-10 13:06:21 +01:00
dtourolle 9fb6792e10 fix missing images in paras
Python CI / test (3.10) (push) Successful in 2m6s
Python CI / test (3.12) (push) Successful in 1m58s
Python CI / test (3.13) (push) Successful in 1m51s
2025-11-09 22:25:23 +01:00
dtourolle 40c1b913ec doc updates
Python CI / test (3.10) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
Python CI / test (3.13) (push) Has been cancelled
2025-11-09 22:06:42 +01:00
dtourolle cc34c79495 more examples
Python CI / test (3.10) (push) Successful in 2m6s
Python CI / test (3.12) (push) Successful in 1m57s
Python CI / test (3.13) (push) Successful in 1m52s
2025-11-09 21:40:59 +01:00
dtourolle 12ebddaa79 more examples 2025-11-09 21:40:50 +01:00
dtourolle 2b14517344 adding more fonts
Python CI / test (3.10) (push) Successful in 2m2s
Python CI / test (3.12) (push) Successful in 1m52s
Python CI / test (3.13) (push) Successful in 1m47s
2025-11-09 21:17:26 +01:00
dtourolle 849ba2f60f Added press state, fixed font registry
Python CI / test (3.10) (push) Successful in 2m2s
Python CI / test (3.12) (push) Successful in 1m52s
Python CI / test (3.13) (push) Successful in 1m47s
2025-11-09 17:45:53 +01:00
dtourolle 9ae8ddddca fixed example
Python CI / test (3.10) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
Python CI / test (3.13) (push) Has been cancelled
2025-11-09 17:21:17 +01:00
dtourolle 50b9aa5431 fixed issue with bounding box height being wrong
Python CI / test (3.10) (push) Has been cancelled
Python CI / test (3.12) (push) Has been cancelled
Python CI / test (3.13) (push) Has been cancelled
2025-11-09 17:10:50 +01:00
dtourolle 56c2c21021 fix backwards rendering
Python CI / test (3.10) (push) Successful in 7m36s
Python CI / test (3.12) (push) Successful in 7m19s
Python CI / test (3.13) (push) Successful in 7m5s
2025-11-09 15:56:00 +01:00
dtourolle 73700baf87 CI fix
Python CI / test (3.10) (push) Successful in 7m50s
Python CI / test (3.12) (push) Successful in 8m34s
Python CI / test (3.13) (push) Successful in 8m9s
2025-11-09 09:16:10 +01:00
dtourolle 8b833eef0b more fstring fixes
Python CI / test (push) Successful in 6m46s
2025-11-09 00:15:25 +01:00
dtourolle 78745c4e29 more fstring fixes 2025-11-09 00:15:07 +01:00
dtourolle 10612fefae undoing more autoflake8 damage
Python CI / test (push) Failing after 43s
2025-11-09 00:11:48 +01:00
dtourolle ce7293824e fixing unterminated fsting
Python CI / test (push) Failing after 46s
2025-11-09 00:09:04 +01:00
dtourolle 8dce1569c0 fixed typo
Python CI / test (push) Failing after 46s
2025-11-09 00:05:14 +01:00
dtourolle f070121e5c bumpy version
Python CI / test (push) Failing after 45s
2025-11-08 23:59:53 +01:00
dtourolle 4c99282aef setup cfg file
Python CI / test (push) Failing after 46s
2025-11-08 23:46:28 +01:00
dtourolle 781a9b6c08 auto flake and corrections 2025-11-08 23:46:15 +01:00
dtourolle 1ea870eef5 yet more tests 2025-11-08 19:52:19 +01:00
dtourolle 00314c9b4f ADDITIOANL TEST
Python CI / test (push) Successful in 6m49s
2025-11-08 19:39:10 +01:00
dtourolle af18b1794a update readme 2025-11-08 19:25:32 +01:00
dtourolle 13d20c28c5 Api for changing sizes dynamically
Python CI / test (push) Successful in 6m54s
2025-11-08 18:22:58 +01:00
dtourolle f8baf155e9 ereader manager tests 2025-11-08 12:59:57 +01:00
dtourolle b65c35d96d Add missing tests
Python CI / test (push) Successful in 6m40s
2025-11-08 12:43:39 +01:00
dtourolle 5c0b22569a fix tests 2025-11-08 12:42:17 +01:00
dtourolle 39622c7dd7 integration of functional elements
Python CI / test (push) Successful in 6m46s
2025-11-08 10:17:01 +01:00
dtourolle ea93681aaf refactoring the mixin system
Python CI / test (push) Successful in 6m46s
2025-11-08 08:08:02 +01:00
dtourolle 49d4e551f8 Some clean up and added interactable images.
Python CI / test (push) Successful in 6m34s
2025-11-07 22:56:35 +01:00
dtourolle 15305011dc moved gestures to application
Python CI / test (push) Successful in 6m36s
2025-11-07 22:18:24 +01:00
dtourolle 9bc9c96e14 clean up examples
Python CI / test (push) Successful in 6m37s
2025-11-07 21:35:43 +01:00