arrolio 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rspec +2 -0
- data/.rubocop.yml +343 -0
- data/CHANGELOG.md +18 -0
- data/CLAUDE.md +154 -0
- data/Gemfile +15 -0
- data/LICENSE +24 -0
- data/README.adoc +95 -0
- data/Rakefile +21 -0
- data/TODO.complete/01-true-type-font-metrics.md +56 -0
- data/TODO.complete/02-font-embedding.md +51 -0
- data/TODO.complete/03-knuth-plass-breaker.md +42 -0
- data/TODO.complete/04-table-flowable.md +60 -0
- data/TODO.complete/05-list-flowable.md +50 -0
- data/TODO.complete/06-notes-and-examples.md +41 -0
- data/TODO.complete/07-odd-even-templates-and-page-numbering.md +63 -0
- data/TODO.complete/08-image-flowable.md +45 -0
- data/TODO.complete/09-toc-and-leaders.md +66 -0
- data/TODO.complete/10-hyperlinks-and-destinations.md +46 -0
- data/TODO.complete/11-pdf-outline-bookmarks.md +47 -0
- data/TODO.complete/12-metadata-info-dict.md +42 -0
- data/TODO.complete/13-xmp-metadata.md +43 -0
- data/TODO.complete/14-footnotes.md +43 -0
- data/TODO.complete/15-color-management.md +33 -0
- data/TODO.complete/16-pdfua-struct-tree.md +35 -0
- data/TODO.complete/17-alt-text-and-actual-text.md +28 -0
- data/TODO.complete/18-math-rendering.md +41 -0
- data/TODO.complete/19-preformatted-and-code.md +31 -0
- data/TODO.complete/20-page-break-controls.md +46 -0
- data/TODO.complete/21-svg-renderer.md +40 -0
- data/TODO.complete/22-definition-lists.md +36 -0
- data/TODO.complete/23-multi-column.md +34 -0
- data/TODO.complete/24-cover-page-fidelity.md +40 -0
- data/TODO.complete/25-i18n-and-multi-language.md +28 -0
- data/TODO.complete/26-pixel-diff-harness.md +36 -0
- data/TODO.complete/27-pdfrb-only-text-extraction.md +37 -0
- data/TODO.complete/28-restore-nested-module-style.md +41 -0
- data/TODO.complete/29-font-embedding-wiring-debug.md +72 -0
- data/TODO.complete/30-adapter-decomposition.md +76 -0
- data/TODO.complete/31-subscript-superscript-inline-runs.md +56 -0
- data/TODO.complete/32-debug-output-cleanup.md +59 -0
- data/TODO.complete/33-comprehensive-adapter-specs.md +55 -0
- data/TODO.complete/34-table-column-auto-layout.md +50 -0
- data/TODO.complete/35-image-path-resolution.md +66 -0
- data/TODO.complete/36-xsl-converter-completeness.md +40 -0
- data/TODO.complete/37-content-model-completeness.md +54 -0
- data/TODO.complete/38-pipeline-config-externalization.md +55 -0
- data/TODO.complete/39-two-pass-cross-reference-resolution.md +66 -0
- data/TODO.complete/40-vertical-writing-modes.md +63 -0
- data/TODO.complete/41-pdf-annotations-hyperlinks.md +53 -0
- data/TODO.complete/42-font-manifest-configuration.md +75 -0
- data/TODO.complete/43-shrink-to-fit-containers.md +52 -0
- data/TODO.complete/44-back-of-book-index-generation.md +51 -0
- data/TODO.complete/45-pdf-forms-acroform.md +52 -0
- data/TODO.complete/46-pdf-digital-signatures.md +47 -0
- data/TODO.complete/47-ocp-flavor-separation.md +50 -0
- data/TODO.complete/48-oiml-as-pure-configuration.md +80 -0
- data/TODO.complete/49-oiml-flowbuilder-as-configuration.md +39 -0
- data/TODO.complete/50-delete-oiml-ruby-code.md +53 -0
- data/TODO.complete/51-xsl-as-authoritative-source.md +68 -0
- data/TODO.complete/52-flavor-packaging-separation.md +49 -0
- data/TODO.complete/53-second-flavor-spike.md +74 -0
- data/TODO.complete/54-flavor-manifest-contract.md +72 -0
- data/TODO.complete/55-header-footer-style-configuration.md +57 -0
- data/TODO.complete/56-generic-adapter-mece-decomposition.md +56 -0
- data/TODO.complete/57-strict-section-typing.md +49 -0
- data/TODO.complete/58-flavor-packaging-gem-convention.md +47 -0
- data/TODO.complete/59-xsl-converter-profiles.md +76 -0
- data/TODO.complete/60-adapter-fidelity-footnotes-notes-examples.md +62 -0
- data/TODO.complete/61-error-model-and-strict-mode.md +62 -0
- data/TODO.complete/62-font-resolution-and-strict-fonts.md +66 -0
- data/TODO.complete/63-purge-hardcoded-vocabulary-from-generic-core.md +68 -0
- data/TODO.complete/64-selector-driven-adapter-specs.md +46 -0
- data/TODO.complete/66-semantic-content-types.md +171 -0
- data/TODO.complete/README.md +48 -0
- data/TODO.implementation/01-foundation.md +46 -0
- data/TODO.implementation/02-content-contract.md +47 -0
- data/TODO.implementation/03-layoutspec-contract.md +46 -0
- data/TODO.implementation/04-style-system.md +43 -0
- data/TODO.implementation/05-visitor-pattern.md +40 -0
- data/TODO.implementation/06-afm-data-and-parser.md +37 -0
- data/TODO.implementation/07-afm-metrics.md +41 -0
- data/TODO.implementation/08-truetype-tables-and-metrics.md +48 -0
- data/TODO.implementation/09-font-registry-and-glyph-measurer.md +49 -0
- data/TODO.implementation/10-break-opportunity-and-inline-run.md +47 -0
- data/TODO.implementation/11-greedy-line-breaker.md +48 -0
- data/TODO.implementation/12-knuth-plass-line-breaker.md +46 -0
- data/TODO.implementation/13-line-and-alignment.md +45 -0
- data/TODO.implementation/14-knuth-elements-and-breaker.md +56 -0
- data/TODO.implementation/15-refactor-text-layout-on-knuth.md +47 -0
- data/TODO.implementation/16-flowable-and-frame.md +52 -0
- data/TODO.implementation/17-flow-context.md +50 -0
- data/TODO.implementation/18-text-flowable.md +52 -0
- data/TODO.implementation/19-spacer-and-page-break.md +52 -0
- data/TODO.implementation/20-engine-paged.md +63 -0
- data/TODO.implementation/21-output-tree.md +56 -0
- data/TODO.implementation/22-pdf-renderer.md +77 -0
- data/TODO.implementation/23-end-to-end-smoke.md +53 -0
- data/TODO.implementation/24-page-template-and-region.md +51 -0
- data/TODO.implementation/25-page-sequence-master.md +54 -0
- data/TODO.implementation/26-two-pass-layout.md +66 -0
- data/TODO.implementation/27-table-model.md +48 -0
- data/TODO.implementation/28-table-layouts.md +53 -0
- data/TODO.implementation/29-table-renderer.md +55 -0
- data/TODO.implementation/30-table-flowable.md +53 -0
- data/TODO.implementation/31-list-model.md +45 -0
- data/TODO.implementation/32-list-flowable.md +54 -0
- data/TODO.implementation/33-image-flowable.md +59 -0
- data/TODO.implementation/34-svg-renderer.md +58 -0
- data/TODO.implementation/35-svg-as-form-xobject.md +38 -0
- data/TODO.implementation/36-inline-builder.md +50 -0
- data/TODO.implementation/37-hyperlink.md +53 -0
- data/TODO.implementation/38-sub-superscript.md +44 -0
- data/TODO.implementation/39-leader-and-field-runs.md +60 -0
- data/TODO.implementation/40-bookmark-outline.md +45 -0
- data/TODO.implementation/41-destinations.md +52 -0
- data/TODO.implementation/42-toc-builder.md +52 -0
- data/TODO.implementation/43-footnotes.md +51 -0
- data/TODO.implementation/44-multi-column.md +45 -0
- data/TODO.implementation/45-page-numbering-and-section-tracking.md +50 -0
- data/TODO.implementation/46-color-management.md +47 -0
- data/TODO.implementation/47-metadata-and-xmp.md +41 -0
- data/TODO.implementation/48-struct-tree-and-marked-content.md +49 -0
- data/TODO.implementation/49-alt-text-and-actual-text.md +43 -0
- data/TODO.implementation/50-composer-facade.md +59 -0
- data/TODO.implementation/51-style-presets.md +44 -0
- data/TODO.implementation/52-oiml-adapter-and-e2e-pipeline.md +120 -0
- data/TODO.implementation/53-oiml-layout-spec.md +121 -0
- data/TODO.implementation/54-oiml-pipeline.md +97 -0
- data/TODO.implementation/55-pdf-comparator.md +98 -0
- data/TODO.implementation/56-diff-formatters.md +95 -0
- data/TODO.implementation/57-path-locator-and-enrichment.md +74 -0
- data/TODO.implementation/58-rspec-matchers-and-harness-api.md +124 -0
- data/TODO.implementation/59-pixel-diff-mode.md +93 -0
- data/TODO.implementation/60-yard-documentation.md +43 -0
- data/TODO.implementation/61-benchmarks-and-stress-tests.md +50 -0
- data/TODO.implementation/62-validation-framework.md +107 -0
- data/TODO.implementation/63-layout-rules-profile.md +110 -0
- data/TODO.implementation/64-pdfa-and-pdfua-conformance.md +74 -0
- data/TODO.implementation/65-feature-extractor.md +64 -0
- data/TODO.implementation/66-auto-fixer.md +75 -0
- data/TODO.implementation/README.md +169 -0
- data/data/arrolio/afm/Courier-Bold.afm +342 -0
- data/data/arrolio/afm/Courier-BoldOblique.afm +342 -0
- data/data/arrolio/afm/Courier-Oblique.afm +342 -0
- data/data/arrolio/afm/Courier.afm +342 -0
- data/data/arrolio/afm/Helvetica-Bold.afm +2827 -0
- data/data/arrolio/afm/Helvetica-BoldOblique.afm +2827 -0
- data/data/arrolio/afm/Helvetica-Oblique.afm +3051 -0
- data/data/arrolio/afm/Helvetica.afm +3051 -0
- data/data/arrolio/afm/Symbol.afm +213 -0
- data/data/arrolio/afm/Times-Bold.afm +2588 -0
- data/data/arrolio/afm/Times-BoldItalic.afm +2384 -0
- data/data/arrolio/afm/Times-Italic.afm +2667 -0
- data/data/arrolio/afm/Times-Roman.afm +2419 -0
- data/data/arrolio/afm/ZapfDingbats.afm +225 -0
- data/data/arrolio/glyphlist.txt +4306 -0
- data/exe/arrolio2pdf +28 -0
- data/lib/arrolio/asset_resolver.rb +36 -0
- data/lib/arrolio/color.rb +107 -0
- data/lib/arrolio/column_set.rb +64 -0
- data/lib/arrolio/composer.rb +10 -0
- data/lib/arrolio/config_driven_pipeline.rb +144 -0
- data/lib/arrolio/content/bibliography_item.rb +42 -0
- data/lib/arrolio/content/builder.rb +108 -0
- data/lib/arrolio/content/document.rb +60 -0
- data/lib/arrolio/content/example.rb +43 -0
- data/lib/arrolio/content/figure_group.rb +39 -0
- data/lib/arrolio/content/footnote.rb +46 -0
- data/lib/arrolio/content/form_field.rb +47 -0
- data/lib/arrolio/content/formula.rb +34 -0
- data/lib/arrolio/content/heading.rb +42 -0
- data/lib/arrolio/content/hyperlink.rb +42 -0
- data/lib/arrolio/content/image.rb +36 -0
- data/lib/arrolio/content/index_entry.rb +52 -0
- data/lib/arrolio/content/inline_run.rb +51 -0
- data/lib/arrolio/content/list.rb +60 -0
- data/lib/arrolio/content/note.rb +48 -0
- data/lib/arrolio/content/page_break.rb +28 -0
- data/lib/arrolio/content/paragraph.rb +38 -0
- data/lib/arrolio/content/preformatted.rb +47 -0
- data/lib/arrolio/content/section.rb +39 -0
- data/lib/arrolio/content/table.rb +117 -0
- data/lib/arrolio/content/term_entry.rb +43 -0
- data/lib/arrolio/content.rb +27 -0
- data/lib/arrolio/data_dir.rb +28 -0
- data/lib/arrolio/engine/cross_reference_registry.rb +69 -0
- data/lib/arrolio/engine/paged.rb +186 -0
- data/lib/arrolio/engine.rb +9 -0
- data/lib/arrolio/error.rb +70 -0
- data/lib/arrolio/flavor/manifest.rb +111 -0
- data/lib/arrolio/flavor/registry.rb +54 -0
- data/lib/arrolio/flavor.rb +23 -0
- data/lib/arrolio/flow_context.rb +37 -0
- data/lib/arrolio/flowable.rb +66 -0
- data/lib/arrolio/flowables/footnote_marker_flowable.rb +28 -0
- data/lib/arrolio/flowables/heading_flowable.rb +23 -0
- data/lib/arrolio/flowables/image_flowable.rb +53 -0
- data/lib/arrolio/flowables/list_flowable.rb +117 -0
- data/lib/arrolio/flowables/note_flowable.rb +19 -0
- data/lib/arrolio/flowables/page_break.rb +15 -0
- data/lib/arrolio/flowables/page_sequence_start.rb +28 -0
- data/lib/arrolio/flowables/spacer.rb +34 -0
- data/lib/arrolio/flowables/table_flowable.rb +147 -0
- data/lib/arrolio/flowables/text_flowable.rb +95 -0
- data/lib/arrolio/flowables/toc_line_flowable.rb +35 -0
- data/lib/arrolio/flowables/two_column_block.rb +56 -0
- data/lib/arrolio/flowables.rb +19 -0
- data/lib/arrolio/font/embedder.rb +197 -0
- data/lib/arrolio/font/manifest.rb +114 -0
- data/lib/arrolio/font/resolver.rb +161 -0
- data/lib/arrolio/font/text_encoder.rb +28 -0
- data/lib/arrolio/font.rb +10 -0
- data/lib/arrolio/font_metrics/afm_metrics.rb +91 -0
- data/lib/arrolio/font_metrics/registry.rb +55 -0
- data/lib/arrolio/font_metrics/true_type_metrics.rb +95 -0
- data/lib/arrolio/font_metrics.rb +10 -0
- data/lib/arrolio/frame.rb +36 -0
- data/lib/arrolio/generic_adapter.rb +672 -0
- data/lib/arrolio/generic_flow_builder.rb +338 -0
- data/lib/arrolio/glyph_measurer.rb +71 -0
- data/lib/arrolio/harness/pdf_diff.rb +104 -0
- data/lib/arrolio/harness/pixel_diff.rb +122 -0
- data/lib/arrolio/harness/text_diff.rb +53 -0
- data/lib/arrolio/harness/text_extractor.rb +58 -0
- data/lib/arrolio/harness.rb +10 -0
- data/lib/arrolio/inline_run.rb +59 -0
- data/lib/arrolio/layout_spec/flow.rb +17 -0
- data/lib/arrolio/layout_spec/loader.rb +117 -0
- data/lib/arrolio/layout_spec/page_template.rb +127 -0
- data/lib/arrolio/layout_spec/page_template_selector.rb +58 -0
- data/lib/arrolio/layout_spec/region.rb +38 -0
- data/lib/arrolio/layout_spec/scale_length.rb +34 -0
- data/lib/arrolio/layout_spec.rb +90 -0
- data/lib/arrolio/logger.rb +54 -0
- data/lib/arrolio/output/page.rb +69 -0
- data/lib/arrolio/output/placed_box.rb +68 -0
- data/lib/arrolio/output/region.rb +32 -0
- data/lib/arrolio/output.rb +10 -0
- data/lib/arrolio/renderer/accessibility_tagger.rb +70 -0
- data/lib/arrolio/renderer/font_registry.rb +32 -0
- data/lib/arrolio/renderer/link_annotator.rb +62 -0
- data/lib/arrolio/renderer/outline_builder.rb +102 -0
- data/lib/arrolio/renderer/pdf.rb +493 -0
- data/lib/arrolio/renderer/signature_config.rb +59 -0
- data/lib/arrolio/renderer/structure_tree_builder.rb +117 -0
- data/lib/arrolio/renderer/xmp_builder.rb +92 -0
- data/lib/arrolio/renderer.rb +17 -0
- data/lib/arrolio/style/definition.rb +98 -0
- data/lib/arrolio/style/loader.rb +25 -0
- data/lib/arrolio/style/registry.rb +53 -0
- data/lib/arrolio/style.rb +9 -0
- data/lib/arrolio/table/auto_layout.rb +91 -0
- data/lib/arrolio/table.rb +9 -0
- data/lib/arrolio/text_direction.rb +81 -0
- data/lib/arrolio/text_layout/break_opportunity.rb +103 -0
- data/lib/arrolio/text_layout/greedy.rb +110 -0
- data/lib/arrolio/text_layout/knuth_plass/breaker.rb +265 -0
- data/lib/arrolio/text_layout/knuth_plass/item.rb +84 -0
- data/lib/arrolio/text_layout/knuth_plass/item_builder.rb +103 -0
- data/lib/arrolio/text_layout/knuth_plass.rb +12 -0
- data/lib/arrolio/text_layout/line.rb +72 -0
- data/lib/arrolio/text_layout.rb +11 -0
- data/lib/arrolio/toc_builder.rb +31 -0
- data/lib/arrolio/version.rb +5 -0
- data/lib/arrolio/writing_mode.rb +75 -0
- data/lib/arrolio.rb +42 -0
- data/script/smoke.rb +32 -0
- data/scripts/write_content_extras.py +227 -0
- data/scripts/xsl_profiles/standoc.yml +218 -0
- data/scripts/xsl_to_config.rb +391 -0
- metadata +394 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 2
|
|
4
|
+
depends_on: [01]
|
|
5
|
+
layer: metrics
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
TTF/OTF advance widths live in the `hmtx` table indexed by glyph ID.
|
|
13
|
+
To go Unicode → glyph ID we need the `cmap` table. To scale we need
|
|
14
|
+
`head.units_per_em`. And `hhea` / `OS/2` give ascender/descender
|
|
15
|
+
(line height).
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files under `lib/arrolio/font/true_type/`:
|
|
20
|
+
|
|
21
|
+
- `file.rb` — sfnt header + table directory; lazy table access.
|
|
22
|
+
- `head.rb` — `units_per_em`, `bbox`, `index_to_loc_format`,
|
|
23
|
+
`mac_style` (bold/italic flags).
|
|
24
|
+
- `hhea.rb` — `ascender`, `descender`, `line_gap`,
|
|
25
|
+
`number_of_hmetrics`.
|
|
26
|
+
- `hmtx.rb` — `advance_width(glyph_id)`, `lsb(glyph_id)`; handles
|
|
27
|
+
`numberOfHMetrics < numGlyphs` case.
|
|
28
|
+
- `cmap.rb` — Unicode → glyph ID; subtable selection prefers
|
|
29
|
+
`(3,12)` > `(0,*)` > `(3,1)` > `(3,0)`; supports formats 0, 4, 6, 12.
|
|
30
|
+
- `os2.rb` — `s_typo_ascender/descender/line_gap`,
|
|
31
|
+
`s_cap_height`, `sx_height`, `us_win_ascent/descent`,
|
|
32
|
+
`fs_selection` (bold/italic flags).
|
|
33
|
+
|
|
34
|
+
Under `lib/arrolio/font_metrics/`:
|
|
35
|
+
|
|
36
|
+
- `true_type_metrics.rb` — wraps `TrueType::File`; implements the
|
|
37
|
+
universal metrics interface from TODO 09.
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] Loading DejaVuSans.ttf (or similar fixture) succeeds.
|
|
42
|
+
- [ ] `ttf.head.units_per_em` matches `ttfname`/`fc-query` output.
|
|
43
|
+
- [ ] `ttf.cmap.glyph_id_for(0x41)` returns the same value FreeType
|
|
44
|
+
reports for "A".
|
|
45
|
+
- [ ] `ttf.hmtx.advance_width(0)` matches the .notdef glyph width.
|
|
46
|
+
- [ ] `TrueTypeMetrics#width_of_string` matches FreeType to within
|
|
47
|
+
0.1pt at 12pt.
|
|
48
|
+
- [ ] Specs cover cmap formats 4 and 12 (provide two test TTFs).
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 2
|
|
4
|
+
depends_on: [07, 08]
|
|
5
|
+
layer: metrics
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Engine and TextLayout need one stable interface for "ask the font about
|
|
13
|
+
glyph metrics" regardless of whether the font is AFM-backed (standard
|
|
14
|
+
Type1) or TTF-backed. Plus a high-level measurer that combines metrics
|
|
15
|
+
with PDF's `Tc`/`Tw` operator semantics.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/font_metrics/interface.rb` — module documenting the
|
|
22
|
+
contract every metrics class implements: `units_per_em`,
|
|
23
|
+
`advance_width(char)`, `width_of_string(str, font_size:)`,
|
|
24
|
+
`ascender(font_size:)`, `descender(font_size:)`,
|
|
25
|
+
`cap_height(font_size:)`, `x_height(font_size:)`,
|
|
26
|
+
`line_height(font_size:, line_spacing:)`.
|
|
27
|
+
- `lib/arrolio/font_metrics/registry.rb` — singleton; maps font name
|
|
28
|
+
to a metrics instance; memoises; supports `register_ttf(name, path)`.
|
|
29
|
+
- `lib/arrolio/glyph_measurer.rb` — high-level facade over a metrics
|
|
30
|
+
instance. Methods:
|
|
31
|
+
- `width_of_string(str, font_size:)` — direct.
|
|
32
|
+
- `width_of_run(str, font_size:, character_spacing:, word_spacing:)`
|
|
33
|
+
— adds `character_spacing * (chars - 1)` and `word_spacing * words`.
|
|
34
|
+
- `line_height(font_size:, line_spacing:)`.
|
|
35
|
+
- `space_width(font_size:)`.
|
|
36
|
+
- `available?` — false when no metrics loaded (falls back to 0.5em
|
|
37
|
+
estimate to keep layout usable).
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] `Registry["Helvetica"]` returns an `AfmMetrics` instance,
|
|
42
|
+
memoised across calls.
|
|
43
|
+
- [ ] `Registry["Unknown"]` returns `nil`.
|
|
44
|
+
- [ ] After `Registry.register_ttf("MyFont", "/path/x.ttf")`,
|
|
45
|
+
`Registry["MyFont"]` returns a `TrueTypeMetrics`.
|
|
46
|
+
- [ ] `GlyphMeasurer.new(font_name: "Helvetica").width_of_run("a b c",
|
|
47
|
+
font_size: 12, character_spacing: 1, word_spacing: 2)` matches
|
|
48
|
+
the PDF spec formula.
|
|
49
|
+
- [ ] Specs cover both AFM and TTF paths through GlyphMeasurer.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 3
|
|
4
|
+
depends_on: [04, 09]
|
|
5
|
+
layer: text
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Line breaking iterates over a list of `InlineRun`s. Each run is a
|
|
13
|
+
contiguous span of text sharing one `Style`. The line breaker must
|
|
14
|
+
find break opportunities across run boundaries (the wrap algorithm
|
|
15
|
+
cannot force a line break just because the style changed).
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/inline_run.rb` — frozen value object:
|
|
22
|
+
`text`, `style`. Methods: `empty?`, `length`, `width(measurer)`,
|
|
23
|
+
`==`, `eql?`, `hash`.
|
|
24
|
+
- `lib/arrolio/text_layout/break_opportunity.rb` — finds break
|
|
25
|
+
points in a run list. Yields `BreakOpportunity` value objects:
|
|
26
|
+
- `run_index` — index into the run list.
|
|
27
|
+
- `char_offset` — position within that run.
|
|
28
|
+
- `width_before` — cumulative width up to (and including) the break.
|
|
29
|
+
- `type` — `:start`, `:soft`, `:forced`, `:end`.
|
|
30
|
+
|
|
31
|
+
Opportunities are created at:
|
|
32
|
+
- Paragraph start (always, `:start`).
|
|
33
|
+
- After each space (`:soft`).
|
|
34
|
+
- After hyphen / em-dash / soft hyphen (`:soft`).
|
|
35
|
+
- At zero-width space (U+200B) (`:soft`, no width consumed).
|
|
36
|
+
- At newline (`:forced`).
|
|
37
|
+
- At end of paragraph (`:end`).
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] `InlineRun.new("Hello", style:).width(measurer)` returns a Float.
|
|
42
|
+
- [ ] `BreakOpportunity.each_in(runs, measurer:, font_size:)` yields
|
|
43
|
+
every opportunity in order with correct cumulative widths.
|
|
44
|
+
- [ ] A forced newline produces a `:forced` opportunity.
|
|
45
|
+
- [ ] A paragraph with no spaces yields only `:start` and `:end`.
|
|
46
|
+
- [ ] Specs cover: ASCII, multi-run with style change at space,
|
|
47
|
+
explicit newline, soft hyphen.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 3
|
|
4
|
+
depends_on: [10]
|
|
5
|
+
layer: text
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The simplest correct line breaker: first-fit word wrap. Walk runs
|
|
13
|
+
left-to-right, accumulate words, emit a line when the next word would
|
|
14
|
+
exceed the target width. FOP's default. Linear time, fast.
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
File: `lib/arrolio/text_layout/greedy.rb`.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Arrolio::TextLayout::Greedy
|
|
22
|
+
def initialize(runs, measurer:, width:, align: :left)
|
|
23
|
+
def layout -> [Line, Line, ...]
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Algorithm:
|
|
28
|
+
1. Enumerate break opportunities.
|
|
29
|
+
2. Track `line_start` index into the opportunity list.
|
|
30
|
+
3. For each subsequent opportunity, compute `line_width = opp.width_before
|
|
31
|
+
- opps[line_start].width_before`.
|
|
32
|
+
4. If `line_width > target_width` and we have at least one opportunity
|
|
33
|
+
on this line, emit a line ending at the previous opportunity; reset
|
|
34
|
+
`line_start`.
|
|
35
|
+
5. Force-break at `:forced` opportunities.
|
|
36
|
+
6. At end, emit the trailing line.
|
|
37
|
+
|
|
38
|
+
Output: list of `Line` objects (TODO 13). This TODO builds Lines
|
|
39
|
+
without alignment offsets; TODO 13 adds offsets.
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] A single short word on one line: `"Hello"` at width 500 → 1 line.
|
|
44
|
+
- [ ] Long text wraps to multiple lines; no line exceeds target width
|
|
45
|
+
except for unbreakable words.
|
|
46
|
+
- [ ] Forced newline breaks the line.
|
|
47
|
+
- [ ] Style change at a space does not produce a spurious line break.
|
|
48
|
+
- [ ] Per-line `width` is correct (matches sum of placed run widths).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 3
|
|
4
|
+
depends_on: [10]
|
|
5
|
+
layer: text
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Greedy line breaking produces uneven right margins and unbalanced
|
|
13
|
+
justify. Knuth-Plass (TeX, Pango) minimises the sum of squared
|
|
14
|
+
shrink/stretch ratios across the paragraph, producing visually
|
|
15
|
+
balanced text. Opt-in via `style.line_break: :knuth_plass`.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
File: `lib/arrolio/text_layout/knuth_plass.rb`.
|
|
20
|
+
|
|
21
|
+
Same I/O as Greedy: `(runs, measurer, width, align)` → `[Line]`.
|
|
22
|
+
|
|
23
|
+
Algorithm (simplified — TODO 14 generalises):
|
|
24
|
+
1. Enumerate break opportunities.
|
|
25
|
+
2. `best[i]` = optimal cost to reach opportunity i. `best[0] = 0`.
|
|
26
|
+
3. For each pair `(i, j)` with `i < j`:
|
|
27
|
+
- Compute `line_width = opps[j].width_before - opps[i].width_before`.
|
|
28
|
+
- If `:forced` at j, treat as zero-cost break.
|
|
29
|
+
- If `line_width > target`, allow but with very high badness.
|
|
30
|
+
- Otherwise badness = `((target - line_width) / target) ** 2 * 100`.
|
|
31
|
+
- `candidate_cost = best[i] + badness`. Keep the minimum.
|
|
32
|
+
4. Walk back from end to recover the chosen break sequence.
|
|
33
|
+
5. Skip empty segments (would produce empty trailing lines).
|
|
34
|
+
|
|
35
|
+
Knuth's full algorithm has penalties, fitness classes, and demerits.
|
|
36
|
+
This TODO uses the simplified version. TODO 14 lifts it to a general
|
|
37
|
+
`Breaker` with full Knuth semantics.
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] Single short word → 1 line.
|
|
42
|
+
- [ ] Long paragraph wraps to multiple lines without empty trailing line.
|
|
43
|
+
- [ ] For a sample paragraph, KnuthPlass produces line widths whose
|
|
44
|
+
variance is ≤ Greedy's variance on the same paragraph (i.e. more
|
|
45
|
+
balanced).
|
|
46
|
+
- [ ] Output interface is identical to Greedy (swappable).
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 3
|
|
4
|
+
depends_on: [11]
|
|
5
|
+
layer: text
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The line breakers emit lists of `Line` objects. Each `Line` carries
|
|
13
|
+
the placed runs (with x-offsets), the line width, and the alignment.
|
|
14
|
+
The renderer needs these to know exactly where to draw each run.
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
Files:
|
|
19
|
+
|
|
20
|
+
- `lib/arrolio/text_layout/line.rb` — `Line` value object:
|
|
21
|
+
- `placed_runs` — Array of `PlacedRun` (run + x_offset within line).
|
|
22
|
+
- `width` — actual content width of the line.
|
|
23
|
+
- `max_width` — target width (for alignment reference).
|
|
24
|
+
- `align` — `:left`, `:right`, `:center`, `:justify`.
|
|
25
|
+
- `x_offset` — line's left-edge offset based on alignment:
|
|
26
|
+
- left: 0
|
|
27
|
+
- right: `max_width - width`
|
|
28
|
+
- center: `(max_width - width) / 2`
|
|
29
|
+
- `justify_stretch` — extra width per word gap for justify alignment
|
|
30
|
+
(last line of a paragraph is never justified).
|
|
31
|
+
- `lib/arrolio/text_layout/placed_run.rb` — `Struct.new(:run,
|
|
32
|
+
:x_offset, keyword_init: true)`.
|
|
33
|
+
|
|
34
|
+
Both breakers (Greedy, KnuthPlass) produce `Line[]` via the same code
|
|
35
|
+
path — extract shared line-building into a private helper.
|
|
36
|
+
|
|
37
|
+
## Done-When
|
|
38
|
+
|
|
39
|
+
- [ ] `Line#x_offset` for centered alignment matches
|
|
40
|
+
`(max_width - width) / 2`.
|
|
41
|
+
- [ ] `Line#justify_stretch` is 0 for the last line in a paragraph.
|
|
42
|
+
- [ ] `Line#justify_stretch` evenly distributes slack across word gaps
|
|
43
|
+
for non-last lines.
|
|
44
|
+
- [ ] A renderer that consumes `Line#placed_runs` can place every run
|
|
45
|
+
at the correct x position.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 4
|
|
4
|
+
depends_on: [10]
|
|
5
|
+
layer: knuth
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
FOP's greatest insight: one algorithm (Knuth-Plass dynamic
|
|
13
|
+
programming) handles line breaks, page breaks, column balancing, and
|
|
14
|
+
table row distribution alike. The universal input is the Knuth element
|
|
15
|
+
triple: Box (fixed-size content), Glue (inter-content with
|
|
16
|
+
stretch/shrink), Penalty (break cost). My current pdfrb has this
|
|
17
|
+
implicit inside TextLayout only.
|
|
18
|
+
|
|
19
|
+
## Approach
|
|
20
|
+
|
|
21
|
+
Files under `lib/arrolio/knuth/`:
|
|
22
|
+
|
|
23
|
+
- `element.rb` — base class. Each element has a `width` (Float) and a
|
|
24
|
+
`flagged?` boolean (for penalty/forced breaks).
|
|
25
|
+
- `box.rb` — content with fixed size: `Box.new(width: 30.0, content_ref)`.
|
|
26
|
+
- `glue.rb` — inter-content: `Glue.new(width:, stretch:, shrink:)`.
|
|
27
|
+
- `penalty.rb` — break cost: `Penalty.new(width:, penalty:, flagged:)`.
|
|
28
|
+
Special values:
|
|
29
|
+
- `Penalty::INFINITE` (no break here)
|
|
30
|
+
- `Penalty::FORCED` (must break here)
|
|
31
|
+
|
|
32
|
+
File: `lib/arrolio/breaker.rb`.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
class Arrolio::Breaker
|
|
36
|
+
def initialize(elements, target_width:, tolerance: 1.0)
|
|
37
|
+
def break -> [BreakPoint, BreakPoint, ...]
|
|
38
|
+
end
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Algorithm (Knuth-Plass DP):
|
|
42
|
+
1. Active set starts with node 0 at total cost 0.
|
|
43
|
+
2. For each feasible break point, compute badness from preceding
|
|
44
|
+
active nodes; pick the minimum-cost predecessor.
|
|
45
|
+
3. Returns the chosen break-point sequence.
|
|
46
|
+
|
|
47
|
+
A `BreakPoint` references the element index and the predecessor.
|
|
48
|
+
|
|
49
|
+
## Done-When
|
|
50
|
+
|
|
51
|
+
- [ ] Knuth elements round-trip through serialisation.
|
|
52
|
+
- [ ] `Breaker` on a synthetic Box-Glue-Penalty sequence produces
|
|
53
|
+
optimal breaks matching a hand-computed result.
|
|
54
|
+
- [ ] `Penalty::FORCED` always breaks.
|
|
55
|
+
- [ ] `Penalty::INFINITE` never breaks.
|
|
56
|
+
- [ ] Specs cover under-full, over-full, and exact-fit cases.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 4
|
|
4
|
+
depends_on: [11, 12, 14]
|
|
5
|
+
layer: knuth
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Greedy and KnuthPlass line breakers were built independently and
|
|
13
|
+
duplicate the line-assembly logic. Once the universal `Breaker`
|
|
14
|
+
exists (TODO 14), refactor both algorithms to produce Knuth
|
|
15
|
+
element sequences from InlineRuns, then break them with the universal
|
|
16
|
+
Breaker. Same output, less code, easier to extend.
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
Files to modify:
|
|
21
|
+
|
|
22
|
+
- `lib/arrolio/text_layout/knuth_sequence_builder.rb` — new class.
|
|
23
|
+
Takes `InlineRun[]` + `GlyphMeasurer`, produces a `Knuth::Element[]`
|
|
24
|
+
sequence:
|
|
25
|
+
- Each character → `Box(width: char_width)`.
|
|
26
|
+
- Each space → `Glue(width: space_width, stretch:, shrink:)` plus a
|
|
27
|
+
`Penalty(width: 0, penalty: 0, flagged: false)` before the glue
|
|
28
|
+
(Knuth's standard space-break pattern).
|
|
29
|
+
- Each hyphenation point → `Penalty(width: hyphen_width, penalty:
|
|
30
|
+
50, flagged: true)`.
|
|
31
|
+
- Each newline → `Penalty(width: 0, penalty: -Infinity, flagged: true)`.
|
|
32
|
+
|
|
33
|
+
- `lib/arrolio/text_layout/greedy.rb` and `knuth_plass.rb` — refactor
|
|
34
|
+
to use `KnuthSequenceBuilder` + `Breaker` with different parameters:
|
|
35
|
+
- Greedy: `tolerance: Infinity` (always accept first fit).
|
|
36
|
+
- KnuthPlass: `tolerance: 1.0` (standard).
|
|
37
|
+
|
|
38
|
+
- A `LineAssembler` reconstitutes `Line[]` from the breaker's output
|
|
39
|
+
and the original run list.
|
|
40
|
+
|
|
41
|
+
## Done-When
|
|
42
|
+
|
|
43
|
+
- [ ] All TODOs 11 and 12 specs still pass after refactor.
|
|
44
|
+
- [ ] Both breakers use the same `KnuthSequenceBuilder`.
|
|
45
|
+
- [ ] Both use the same `Breaker` (different params).
|
|
46
|
+
- [ ] The shared `LineAssembler` produces identical Lines to before.
|
|
47
|
+
- [ ] Code size shrinks (no duplicate line-assembly logic).
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 5
|
|
4
|
+
depends_on: [04, 13]
|
|
5
|
+
layer: flowable
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The page-flow engine places "flowables" — paragraphs, tables, images,
|
|
13
|
+
spacers, page breaks — into frames, splitting and advancing pages as
|
|
14
|
+
needed. The Flowable base class defines the contract every flowable
|
|
15
|
+
must satisfy; Frame tracks remaining space.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/flowable.rb` — abstract base.
|
|
22
|
+
- `height(width, context)` → Float: natural height at this width.
|
|
23
|
+
- `render(canvas, x, y, width, context)` → Float: actual height
|
|
24
|
+
consumed (may differ from `height` if rendering adjusts).
|
|
25
|
+
- `splittable?` — true if the flowable supports `split`.
|
|
26
|
+
- `keep_together?` — true if it must not be split.
|
|
27
|
+
- `page_break_before?`, `page_break_after?` — bool flags from style.
|
|
28
|
+
- `split(width, remaining_height, context)` → `[head, tail]` where
|
|
29
|
+
head fits in remaining_height, tail is the rest (or nil).
|
|
30
|
+
- `do_split(width, remaining_height, context)` — subclass hook.
|
|
31
|
+
- Default `split`: if `height <= remaining_height`, `[self, nil]`;
|
|
32
|
+
if not splittable and not keep_together, render whole `[self, nil]`;
|
|
33
|
+
if keep_together, `[nil, self]`.
|
|
34
|
+
|
|
35
|
+
- `lib/arrolio/frame.rb` — rectangular region tracking consumed space.
|
|
36
|
+
- `x`, `y`, `width`, `height`, `consumed`.
|
|
37
|
+
- `remaining_height`, `full?`, `consume!(h)`, `cursor_y`, `clone_empty`.
|
|
38
|
+
- `available_width_at(y)` — for text wrap (FloatRegistry hook).
|
|
39
|
+
|
|
40
|
+
- `lib/arrolio/float_registry.rb` — tracks floated boxes for wrap;
|
|
41
|
+
`available_width_at(frame_x, full_width, y)` returns
|
|
42
|
+
`[x_start, available_width]`.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] `Flowable.new.height(100, ctx)` raises `NotImplementedError`.
|
|
47
|
+
- [ ] A subclass that returns height 50 and renders 50 actually does.
|
|
48
|
+
- [ ] `Frame.new(x: 0, y: 0, width: 100, height: 200).consume!(50)`
|
|
49
|
+
leaves `remaining_height == 150`.
|
|
50
|
+
- [ ] `FloatRegistry` with a left-floated image reduces
|
|
51
|
+
`available_width_at` on the floated y-range.
|
|
52
|
+
- [ ] Specs cover the abstract contract + 1 concrete subclass.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 5
|
|
4
|
+
depends_on: [16]
|
|
5
|
+
layer: flowable
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Every `render` call gets a `FlowContext`. It carries page-level state
|
|
13
|
+
flowables need: current page number, total page count (for "Page X of
|
|
14
|
+
Y"), resolved citation targets, bookmark targets, the float registry,
|
|
15
|
+
and a reference to the document (for emitting indirect objects like
|
|
16
|
+
hyperlink annotations).
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
File: `lib/arrolio/flow_context.rb`.
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
class Arrolio::FlowContext
|
|
24
|
+
attr_reader :document, :citations, :bookmarks, :float_registry
|
|
25
|
+
attr_accessor :page_number, :page_count
|
|
26
|
+
|
|
27
|
+
def initialize(document: nil, page_number: 1, page_count: nil)
|
|
28
|
+
...
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def citation_for(ref_id) # filled in pass 1, read in pass 2
|
|
32
|
+
def record_citation(ref_id, page_number)
|
|
33
|
+
def record_bookmark(ref_id, page_number, y)
|
|
34
|
+
def total_pages # nil until pass 1 completes
|
|
35
|
+
end
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The context is passed by reference, so mutating it during layout is
|
|
39
|
+
intentional. Pass 2 (after the document is fully laid out) reads the
|
|
40
|
+
recorded citations/bookmarks to render static content with resolved
|
|
41
|
+
field values.
|
|
42
|
+
|
|
43
|
+
## Done-When
|
|
44
|
+
|
|
45
|
+
- [ ] `FlowContext.new(page_number: 5)` exposes `page_number == 5`.
|
|
46
|
+
- [ ] `record_citation("ch1", 7)` then `citation_for("ch1") == 7`.
|
|
47
|
+
- [ ] `total_pages` returns nil until `page_count = N` is set.
|
|
48
|
+
- [ ] Document reference is propagated to children that need to emit
|
|
49
|
+
indirect objects (TODO 37 hyperlinks).
|
|
50
|
+
- [ ] Specs cover recording, lookup, two-pass resolution.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 5
|
|
4
|
+
depends_on: [13, 16, 17]
|
|
5
|
+
layer: flowable
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The fundamental flowable: a paragraph of text. Wraps an `InlineRun[]`
|
|
13
|
+
+ `Style`; uses TextLayout (Greedy or KnuthPlass) for line breaking;
|
|
14
|
+
splittable across pages.
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
File: `lib/arrolio/flowables/text_flowable.rb`.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Arrolio::Flowables::TextFlowable < Arrolio::Flowable
|
|
22
|
+
attr_reader :runs, :measurer
|
|
23
|
+
|
|
24
|
+
def initialize(text_or_runs, style:, measurer: nil)
|
|
25
|
+
def height(width, context)
|
|
26
|
+
def render(canvas, x, y, width, context)
|
|
27
|
+
def splittable?; true; end
|
|
28
|
+
def do_split(width, remaining_height, context)
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Internals:
|
|
33
|
+
- `laid_out(width)` runs Greedy or KnuthPlass based on
|
|
34
|
+
`style.line_break`; returns `Line[]`.
|
|
35
|
+
- `height(width, ctx) = lines.length * line_height`.
|
|
36
|
+
- `render(canvas, x, y, width, ctx)` walks lines; for each PlacedRun,
|
|
37
|
+
calls `canvas.text(run.text, at:, font:, size:, char_spacing:,
|
|
38
|
+
word_spacing:)` per the run's style.
|
|
39
|
+
- `do_split` allocates lines to head until `cum_height <= remaining_height`;
|
|
40
|
+
remaining lines form the tail.
|
|
41
|
+
|
|
42
|
+
`line_height = measurer.line_height(font_size:, line_spacing:)`.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] A single-line paragraph at width 500 renders exactly one line.
|
|
47
|
+
- [ ] A long paragraph splits cleanly across pages; both halves
|
|
48
|
+
render correct text.
|
|
49
|
+
- [ ] Style span (bold run inside body) preserves per-run font in
|
|
50
|
+
emitted `canvas.text` calls.
|
|
51
|
+
- [ ] Justify alignment applies per-line word spacing.
|
|
52
|
+
- [ ] Specs cover single-line, multi-line, splittable behaviour.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 5
|
|
4
|
+
depends_on: [16]
|
|
5
|
+
layer: flowable
|
|
6
|
+
est: 1d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Need two simple flowables: vertical `Spacer` (reserve empty space) and
|
|
13
|
+
`PageBreak` (force the engine to advance to the next page before
|
|
14
|
+
continuing).
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
File: `lib/arrolio/flowables/spacer.rb`.
|
|
19
|
+
|
|
20
|
+
```ruby
|
|
21
|
+
class Arrolio::Flowables::Spacer < Arrolio::Flowable
|
|
22
|
+
def initialize(amount)
|
|
23
|
+
def height(width, context) = amount
|
|
24
|
+
def render(*); amount; end
|
|
25
|
+
def splittable?; true; end
|
|
26
|
+
def do_split(width, remaining_height, context)
|
|
27
|
+
# If amount fits, return self. Else split into two spacers.
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
File: `lib/arrolio/flowables/page_break.rb`.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
class Arrolio::Flowables::PageBreak < Arrolio::Flowable
|
|
36
|
+
def height(*); 0.0; end
|
|
37
|
+
def render(*); 0.0; end
|
|
38
|
+
def page_break_after?; true; end # engine recognises this
|
|
39
|
+
end
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Engine treats `PageBreak` as a sentinel: instead of placing it, it
|
|
43
|
+
advances to a new page. Both flowables are zero-cost on render.
|
|
44
|
+
|
|
45
|
+
## Done-When
|
|
46
|
+
|
|
47
|
+
- [ ] `Spacer.new(50).height(100, ctx) == 50`.
|
|
48
|
+
- [ ] `Spacer.new(100).do_split(100, 30, ctx)` returns
|
|
49
|
+
`[Spacer(30), Spacer(70)]`.
|
|
50
|
+
- [ ] `PageBreak.new.page_break_after? == true`.
|
|
51
|
+
- [ ] Engine consumes `PageBreak` without rendering it.
|
|
52
|
+
- [ ] Specs cover both.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P0
|
|
3
|
+
phase: 5
|
|
4
|
+
depends_on: [17, 18, 19]
|
|
5
|
+
layer: flowable
|
|
6
|
+
est: 3d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
The driver that walks a Flowable list, places each into a Frame,
|
|
13
|
+
advances pages when the frame is full, honours keep-together and
|
|
14
|
+
page-break directives, and runs two passes for citation resolution.
|
|
15
|
+
This is the heart of the engine.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
File: `lib/arrolio/engine/paged.rb`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class Arrolio::Engine::Paged
|
|
23
|
+
attr_reader :layout_spec, :flowables, :context, :pages
|
|
24
|
+
|
|
25
|
+
def initialize(layout_spec:, flowables:, context: nil)
|
|
26
|
+
def layout -> [Arrolio::Output::Page]
|
|
27
|
+
end
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Algorithm:
|
|
31
|
+
1. Initialise `pending = flowables.dup`, `current_page = new_page`,
|
|
32
|
+
`current_frame = current_page.body_frame`.
|
|
33
|
+
2. Loop:
|
|
34
|
+
- Shift next flowable.
|
|
35
|
+
- `PageBreak` → new page; continue.
|
|
36
|
+
- `page_break_before?` and not first flowable → new page.
|
|
37
|
+
- `keep_together?` and `height > remaining_height` and `height <=
|
|
38
|
+
frame.height` → new page first.
|
|
39
|
+
- If frame full → new page.
|
|
40
|
+
- `render_on_page(flowable, current_page, current_frame)` →
|
|
41
|
+
`[consumed, remainder]`.
|
|
42
|
+
- `frame.consume!(consumed)`.
|
|
43
|
+
- If remainder and consumed == 0 and frame not full → new page;
|
|
44
|
+
unshift remainder.
|
|
45
|
+
- Else unshift remainder.
|
|
46
|
+
3. After all flowables placed: `context.page_count = pages.length`.
|
|
47
|
+
4. Pass 2: for each page, render its static-content regions
|
|
48
|
+
(running headers/footers) with resolved citation values.
|
|
49
|
+
|
|
50
|
+
Each page is created from the layout_spec's PageSequenceMaster
|
|
51
|
+
selection (TODO 24, 25). For now (Phase 5), use a single body
|
|
52
|
+
template.
|
|
53
|
+
|
|
54
|
+
## Done-When
|
|
55
|
+
|
|
56
|
+
- [ ] A single-paragraph document produces 1 page.
|
|
57
|
+
- [ ] 50 long paragraphs across multiple pages.
|
|
58
|
+
- [ ] `PageBreak` forces a new page.
|
|
59
|
+
- [ ] `keep_together: true` flowable that doesn't fit moves to next
|
|
60
|
+
page whole.
|
|
61
|
+
- [ ] 100-paragraph doc lays out in < 5 seconds.
|
|
62
|
+
- [ ] No infinite loops on over-large content (TODO 19's split
|
|
63
|
+
contract prevents this).
|