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,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 10
|
|
4
|
+
depends_on: [16]
|
|
5
|
+
layer: media
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Need a real ImageFlowable (not the placeholder from the FO spike).
|
|
13
|
+
It loads the image via Pdfrb, scales to target size, optionally
|
|
14
|
+
aligns (left/center/right), and is not splittable. Wraps it as a
|
|
15
|
+
Flowable that the engine places into a frame.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
File: `lib/arrolio/flowables/image_flowable.rb`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class Arrolio::Flowables::ImageFlowable < Arrolio::Flowable
|
|
23
|
+
def initialize(src, width: nil, height: nil, max_width: nil,
|
|
24
|
+
max_height: nil, align: :left)
|
|
25
|
+
def natural_size -> [w, h] # cached after first measurement
|
|
26
|
+
def height(target_width, context)
|
|
27
|
+
def render(canvas, x, y, width, context)
|
|
28
|
+
def splittable?; false; end
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Internals:
|
|
33
|
+
- On first `natural_size` call, ask Pdfrb to load the image
|
|
34
|
+
(Pdfrb::ImageLoader dispatches by format: JPEG, PNG, PDF page).
|
|
35
|
+
Cache the loaded image reference + its pixel dimensions.
|
|
36
|
+
- Scaling rules:
|
|
37
|
+
- Both `width:` and `height:` explicit → use as-is (may distort).
|
|
38
|
+
- Only `width:` → `height = width * natural_h / natural_w`.
|
|
39
|
+
- Only `height:` → symmetric.
|
|
40
|
+
- Neither, but `max_width:` / `max_height:` → scale down to fit,
|
|
41
|
+
never up.
|
|
42
|
+
- Neither → use natural size.
|
|
43
|
+
|
|
44
|
+
Render:
|
|
45
|
+
- `name = context.document.images.add(src)` (Pdfrb handles caching).
|
|
46
|
+
- `canvas.image(name, at: [x + align_offset, y], width:, height:)`.
|
|
47
|
+
|
|
48
|
+
Alignment offset: depends on `align` (:left = 0, :center = (width -
|
|
49
|
+
img_w) / 2, :right = width - img_w).
|
|
50
|
+
|
|
51
|
+
## Done-When
|
|
52
|
+
|
|
53
|
+
- [ ] An 800×600 image with `width: 200` renders at 200×150.
|
|
54
|
+
- [ ] Same image with `max_width: 1000` renders at natural size.
|
|
55
|
+
- [ ] `align: :center` horizontally centres the image.
|
|
56
|
+
- [ ] Image is not splittable; if it doesn't fit, engine advances.
|
|
57
|
+
- [ ] PNG with alpha (SMask) renders correctly.
|
|
58
|
+
- [ ] JPEG passthrough works (no re-encode).
|
|
59
|
+
- [ ] Round-trip: rendered PDF re-read has expected /XObject.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 10
|
|
4
|
+
depends_on: [22]
|
|
5
|
+
layer: media
|
|
6
|
+
est: 3d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
SVG inside documents (`<svg>...</svg>` blocks, FOP's
|
|
13
|
+
`fo:instream-foreign-object`) need a renderer. The SVG renderer
|
|
14
|
+
walks the SVG tree and emits drawing operations on a Canvas (Pdfrb
|
|
15
|
+
content stream). Supports the FOP/Batik output subset.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files under `lib/arrolio/svg/`:
|
|
20
|
+
|
|
21
|
+
- `document.rb` — wraps a parsed SVG (REXML); exposes `width`,
|
|
22
|
+
`height`, `view_box`, `root`.
|
|
23
|
+
|
|
24
|
+
- `style.rb` — resolved style from element attributes (fill, stroke,
|
|
25
|
+
stroke_width, opacity, font_*). Inherits from parent. Class
|
|
26
|
+
method `color_to_pdf(color_string)` resolves named/hex to PDF
|
|
27
|
+
Canvas colour.
|
|
28
|
+
|
|
29
|
+
- `transform_parser.rb` — `parse("translate(10,20) rotate(45)")`
|
|
30
|
+
→ list of op/args. `to_matrix(transforms)` → 6-element affine.
|
|
31
|
+
|
|
32
|
+
- `path_parser.rb` — `parse("M 10 10 L 90 10 Z")` → list of
|
|
33
|
+
`{cmd:, absolute:, args:}`. Supports M L H V C S Q T A Z.
|
|
34
|
+
|
|
35
|
+
- `element.rb` — base class with `register "name"` class method;
|
|
36
|
+
`for_element(xml, style:)` dispatcher.
|
|
37
|
+
|
|
38
|
+
- Element subclasses under `lib/arrolio/svg/element/`:
|
|
39
|
+
- `group.rb` (g) — applies transform + recurses.
|
|
40
|
+
- `path.rb` — PathState accumulator; emits move/line/curve/close.
|
|
41
|
+
- `rect.rb`, `circle.rb`, `ellipse.rb`, `line.rb`,
|
|
42
|
+
`polyline.rb`, `polygon.rb` — basic shapes.
|
|
43
|
+
- `text.rb` — single-line text.
|
|
44
|
+
- `image.rb` — embedded raster (data: URI or href).
|
|
45
|
+
|
|
46
|
+
- `renderer.rb` — walks Document, dispatches to element classes,
|
|
47
|
+
maintains transform/style stack.
|
|
48
|
+
|
|
49
|
+
## Done-When
|
|
50
|
+
|
|
51
|
+
- [ ] `<rect width="100" height="50" fill="red"/>` renders a red rectangle.
|
|
52
|
+
- [ ] `<circle cx="50" cy="50" r="20"/>` renders via Bezier approximation.
|
|
53
|
+
- [ ] `<path d="M 10 10 L 90 10 L 50 90 Z" fill="red"/>` renders triangle.
|
|
54
|
+
- [ ] `<g transform="translate(10,10)"><rect .../></g>` translates children.
|
|
55
|
+
- [ ] Nested groups compose transforms correctly.
|
|
56
|
+
- [ ] `<text x="10" y="20">Hello</text>` renders text.
|
|
57
|
+
- [ ] `<image xlink:href="data:image/png;base64,..."/>` renders PNG.
|
|
58
|
+
- [ ] Spec coverage for each element type.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 10
|
|
4
|
+
depends_on: [34]
|
|
5
|
+
layer: media
|
|
6
|
+
est: 1d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
SVG diagrams that repeat on every page (logos, watermarks) currently
|
|
13
|
+
re-render each time. Better: render once into a Form XObject, then
|
|
14
|
+
`canvas.draw_xobject(name, at:, scale:)` on every page. Cuts render
|
|
15
|
+
time and PDF size.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/svg/form_xobject_builder.rb` — takes an SVG Document,
|
|
22
|
+
renders it once into a `Pdfrb::Model::Cos::Stream` with
|
|
23
|
+
`/Type /XObject`, `/Subtype /Form`, `/BBox`. Returns the OID.
|
|
24
|
+
|
|
25
|
+
- Modify `Arrolio::SVG::Renderer` to optionally emit into a Form
|
|
26
|
+
XObject stream instead of directly into a page's canvas.
|
|
27
|
+
|
|
28
|
+
- ImageFlowable and other consumers gain an option to wrap their
|
|
29
|
+
rendering in a Form XObject when the same image is placed > N times
|
|
30
|
+
(heuristic for "this repeats").
|
|
31
|
+
|
|
32
|
+
## Done-When
|
|
33
|
+
|
|
34
|
+
- [ ] SVG rendered as Form XObject appears identical to inline render.
|
|
35
|
+
- [ ] Form XObject reused across 10 pages produces a PDF smaller than
|
|
36
|
+
rendering inline 10 times.
|
|
37
|
+
- [ ] Form XObject can be scaled/positioned per page.
|
|
38
|
+
- [ ] Round-trip: rendered PDF re-read has /XObject /Form.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 11
|
|
4
|
+
depends_on: [10]
|
|
5
|
+
layer: inline
|
|
6
|
+
est: 1d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
A paragraph is a list of InlineRuns. Authoring those runs by hand is
|
|
13
|
+
tedious. The InlineBuilder DSL lets authors compose runs declaratively:
|
|
14
|
+
`.text("Hello ")`, `.bold("world")`, `.color(:red) { .text("apple") }`.
|
|
15
|
+
Style spans (bold, italic, colour) become first-class.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/inline_builder.rb` — builder DSL on top of Style::Diff:
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
24
|
+
builder = Arrolio::InlineBuilder.new(base_style)
|
|
25
|
+
builder.text("Hello ")
|
|
26
|
+
builder.bold("world") # creates run with bold-weight style
|
|
27
|
+
builder.italic(" and italic")
|
|
28
|
+
builder.color(:red) { |b| b.text(" apple") }
|
|
29
|
+
builder.link("Click here", ref_id: "ch1")
|
|
30
|
+
runs = builder.runs # => [Run("Hello ", base), Run("world", base+bold), ...]
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- `lib/arrolio/style/diff.rb` (extended from TODO 04) — given a base
|
|
34
|
+
Definition + overrides hash, produces a new Definition. Used by
|
|
35
|
+
InlineBuilder's `bold`/`italic`/`color` helpers.
|
|
36
|
+
|
|
37
|
+
Helpers:
|
|
38
|
+
- `bold(str)` / `bold { ... }` — `font_weight: "bold"`.
|
|
39
|
+
- `italic(str)` / `italic { ... }` — `font_style: "italic"`.
|
|
40
|
+
- `color(c, str)` / `color(c) { ... }` — `fill_color: c`.
|
|
41
|
+
- `size(n, str)` / `size(n) { ... }` — `font_size: n`.
|
|
42
|
+
- `font(name, str)` — `font_name: name`.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] `InlineBuilder.new(style).text("a").bold("b").runs.length == 2`.
|
|
47
|
+
- [ ] Bold helper sets `font_weight: "bold"` on the run's style.
|
|
48
|
+
- [ ] Color helper overrides only `fill_color`, keeps other props.
|
|
49
|
+
- [ ] Nested helpers compose (`.bold { .color(:red) { .text("x") } }`).
|
|
50
|
+
- [ ] Link helper creates a HyperlinkRun (TODO 37).
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 11
|
|
4
|
+
depends_on: [36, 22]
|
|
5
|
+
layer: inline
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Hyperlinks (`<a href="...">` in HTML, `fo:basic-link` in FO) need
|
|
13
|
+
both inline rendering (the visible text) and annotation emission (a
|
|
14
|
+
PDF link annotation over the run's bounding box). The bounding box
|
|
15
|
+
isn't known until layout — link annotations are added in a
|
|
16
|
+
post-layout pass.
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
Files:
|
|
21
|
+
|
|
22
|
+
- `lib/arrolio/hyperlink.rb` — `Hyperlink < InlineRun` with
|
|
23
|
+
`destination` (URI string or internal destination name).
|
|
24
|
+
Carries an `annotation_id` that the renderer fills in after
|
|
25
|
+
placement.
|
|
26
|
+
|
|
27
|
+
- `lib/arrolio/output/link_annotation.rb` — `LinkAnnotation` value
|
|
28
|
+
object: `rect` (x, y, w, h), `destination` (URI or page ref),
|
|
29
|
+
`border_style`.
|
|
30
|
+
|
|
31
|
+
Engine integration:
|
|
32
|
+
- During TextFlowable render, track each Hyperlink run's bounding box
|
|
33
|
+
(compute from line position + run width).
|
|
34
|
+
- After all body content is placed, walk the Output tree collecting
|
|
35
|
+
Hyperlinks → emit `LinkAnnotation[]` per page.
|
|
36
|
+
- PDF renderer (TODO 22) walks LinkAnnotations → emits `/Annots`
|
|
37
|
+
array entries on each page.
|
|
38
|
+
|
|
39
|
+
Two kinds of destination:
|
|
40
|
+
- **External** (URI): `/S /URI /URI (https://...)`.
|
|
41
|
+
- **Internal** (page reference): `/S /GoTo /D [page_ref /Fit]`.
|
|
42
|
+
Requires a destination registry (TODO 41).
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] A paragraph with a hyperlink run renders the text in blue
|
|
47
|
+
underline (style default) and emits a clickable annotation.
|
|
48
|
+
- [ ] External URI links open correctly when the PDF is viewed.
|
|
49
|
+
- [ ] Internal links jump to the correct page (integration with
|
|
50
|
+
TODO 41 destinations).
|
|
51
|
+
- [ ] Multiple hyperlinks on the same page each get distinct
|
|
52
|
+
`/Annots` entries.
|
|
53
|
+
- [ ] Re-read PDF has the expected annotation structure.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 11
|
|
4
|
+
depends_on: [10]
|
|
5
|
+
layer: inline
|
|
6
|
+
est: 1d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Footnote markers, mathematical exponents, chemical formulae all need
|
|
13
|
+
sub/superscript: small text raised (or lowered) relative to the
|
|
14
|
+
baseline. The TextLayout must place these correctly without breaking
|
|
15
|
+
the line height.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Extend `Arrolio::InlineRun` (TODO 10) with two style properties:
|
|
20
|
+
- `text_rise` (Float, in points) — vertical offset from baseline.
|
|
21
|
+
Positive = superscript; negative = subscript.
|
|
22
|
+
- The existing `font_size` already supports smaller sizes.
|
|
23
|
+
|
|
24
|
+
Add helper methods to `InlineBuilder` (TODO 36):
|
|
25
|
+
- `superscript(str)` → `style.with(font_size: size * 0.7,
|
|
26
|
+
text_rise: size * 0.5)`.
|
|
27
|
+
- `subscript(str)` → `style.with(font_size: size * 0.7,
|
|
28
|
+
text_rise: -(size * 0.2))`.
|
|
29
|
+
|
|
30
|
+
TextLayout integration:
|
|
31
|
+
- When measuring a run with `text_rise`, line height calculation
|
|
32
|
+
accounts for the raised extent (extends above ascender).
|
|
33
|
+
- When rendering, emit `canvas.text(...)` with Pdfrb's `Tz` (rise)
|
|
34
|
+
parameter (PDF text-rise operator).
|
|
35
|
+
|
|
36
|
+
## Done-When
|
|
37
|
+
|
|
38
|
+
- [ ] `superscript("2")` renders smaller and raised.
|
|
39
|
+
- [ ] `subscript("n")` renders smaller and lowered.
|
|
40
|
+
- [ ] Line containing a superscript doesn't have its height reduced
|
|
41
|
+
(the rise doesn't push out of the line box; line_height is
|
|
42
|
+
generous enough).
|
|
43
|
+
- [ ] "x² + y²" renders correctly with raised 2's.
|
|
44
|
+
- [ ] "H₂O" renders with subscript 2.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 12
|
|
4
|
+
depends_on: [13, 17]
|
|
5
|
+
layer: xref
|
|
6
|
+
est: 2d
|
|
7
|
+
status: in_progress
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Two cross-reference primitives:
|
|
13
|
+
|
|
14
|
+
1. **Leader**: stretchy dot/rule that fills the space between two
|
|
15
|
+
runs on a line (for TOC entries: "Chapter 1 ........... 12").
|
|
16
|
+
2. **FieldRun**: page-number fields resolved at render time via
|
|
17
|
+
FlowContext (`page_number`, `page_number_citation`,
|
|
18
|
+
`page_count`).
|
|
19
|
+
|
|
20
|
+
Both must integrate with TextLayout: a Leader takes the residual
|
|
21
|
+
space after the line's other runs; a FieldRun produces text after
|
|
22
|
+
context lookup.
|
|
23
|
+
|
|
24
|
+
## Approach
|
|
25
|
+
|
|
26
|
+
Files:
|
|
27
|
+
|
|
28
|
+
- `lib/arrolio/leader.rb` — `Leader < InlineRun` with
|
|
29
|
+
`leader_pattern` (`:dots`, `:rule`, `:space`), `leader_length_min`,
|
|
30
|
+
`leader_length_max`. Method `fill_text(available_width, measurer)`
|
|
31
|
+
returns the dot string that fits.
|
|
32
|
+
|
|
33
|
+
- `lib/arrolio/field_run.rb` — base `FieldRun < InlineRun` with
|
|
34
|
+
`resolve(context) -> String`.
|
|
35
|
+
|
|
36
|
+
- Under `lib/arrolio/field_run/`:
|
|
37
|
+
- `page_number.rb` — `PageNumberField` returns
|
|
38
|
+
`context.page_number.to_s`.
|
|
39
|
+
- `page_number_citation.rb` — `PageNumberCitationField` returns
|
|
40
|
+
`context.citation_for(ref_id).to_s` (empty if unresolved).
|
|
41
|
+
- `page_count.rb` — `PageCountField` returns
|
|
42
|
+
`context.total_pages.to_s` (empty in pass 1).
|
|
43
|
+
|
|
44
|
+
TextLayout integration:
|
|
45
|
+
- Leaders are width=0 during line breaking (they take residual space).
|
|
46
|
+
- After all other runs on a line are placed, compute the slack;
|
|
47
|
+
Leader fills it with dots.
|
|
48
|
+
- FieldRuns are width=estimated ("000") in pass 1; in pass 2 they
|
|
49
|
+
have resolved text and re-flow if necessary (rare).
|
|
50
|
+
|
|
51
|
+
## Done-When
|
|
52
|
+
|
|
53
|
+
- [ ] TOC line `"Chapter 1" + leader + "12"` renders as
|
|
54
|
+
`"Chapter 1 ........... 12"`.
|
|
55
|
+
- [ ] `leader_pattern: :rule` renders a horizontal line.
|
|
56
|
+
- [ ] `PageNumberField` in footer renders "3" on page 3.
|
|
57
|
+
- [ ] `PageCountField` renders "7" on every page of a 7-page doc.
|
|
58
|
+
- [ ] `PageNumberCitationField` referencing a section on page 5
|
|
59
|
+
renders "5".
|
|
60
|
+
- [ ] Spec coverage for each.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 12
|
|
4
|
+
depends_on: [22, 26]
|
|
5
|
+
layer: xref
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
PDF outlines (the bookmark panel in viewers) let users navigate.
|
|
13
|
+
Each entry is a title + destination. The outline tree mirrors the
|
|
14
|
+
document's section structure. Currently Arrolio doesn't emit one.
|
|
15
|
+
|
|
16
|
+
## Approach
|
|
17
|
+
|
|
18
|
+
Files:
|
|
19
|
+
|
|
20
|
+
- `lib/arrolio/output/outline_node.rb` — value object: `title`,
|
|
21
|
+
`destination` (page_ref + viewport), `children` (Array), `parent`.
|
|
22
|
+
|
|
23
|
+
- `lib/arrolio/renderer/pdf/outline_emitter.rb` — walks the
|
|
24
|
+
OutlineNode tree, builds Pdfrb objects:
|
|
25
|
+
- `/Outlines` (root dict) on Catalog.
|
|
26
|
+
- `/Type /Outlines`, `/First`, `/Last`, `/Count`.
|
|
27
|
+
- Per outline entry: `/Title`, `/Parent`, `/Dest` (page ref + view),
|
|
28
|
+
`/First`/`/Last`/`Next`/`Prev`/`Count` for hierarchy.
|
|
29
|
+
|
|
30
|
+
Engine integration:
|
|
31
|
+
- During pass 1, when placing a flowable that has a `bookmark_target`
|
|
32
|
+
attribute, record `context.record_bookmark(ref_id, page_number, y)`.
|
|
33
|
+
- After pass 1, build the OutlineNode tree from recorded bookmarks.
|
|
34
|
+
- Pass 2 emits the outline via OutlineEmitter.
|
|
35
|
+
|
|
36
|
+
Bookmark target source: `Content::Section` carries an `id`; the
|
|
37
|
+
content adapter (or the Composer) registers these as bookmark targets.
|
|
38
|
+
|
|
39
|
+
## Done-When
|
|
40
|
+
|
|
41
|
+
- [ ] A document with 3 sections produces a 3-entry outline.
|
|
42
|
+
- [ ] Nested sections produce nested outline entries.
|
|
43
|
+
- [ ] Clicking an outline entry in a viewer jumps to the right page.
|
|
44
|
+
- [ ] Outline `/Count` reflects open/closed state default.
|
|
45
|
+
- [ ] Re-read PDF has the expected `/Outlines` structure.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 12
|
|
4
|
+
depends_on: [22, 39]
|
|
5
|
+
layer: xref
|
|
6
|
+
est: 1d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Internal hyperlinks (`PageNumberCitationField` links,
|
|
13
|
+
`Hyperlink` runs pointing to a section) need named destinations
|
|
14
|
+
that resolve to specific (page, viewport) pairs. Currently nothing
|
|
15
|
+
records where a `ref_id` lands.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/destination.rb` — `Destination = Struct.new(:ref_id,
|
|
22
|
+
:page_number, :x, :y, :zoom, keyword_init: true)`. The `ref_id`
|
|
23
|
+
is the author's identifier (e.g. "section_3"); page_number and
|
|
24
|
+
viewport are filled in during layout.
|
|
25
|
+
|
|
26
|
+
- `lib/arrolio/output/destination_registry.rb` — collects
|
|
27
|
+
`Destination` instances during pass 1; resolves by `ref_id` in
|
|
28
|
+
pass 2.
|
|
29
|
+
|
|
30
|
+
- `lib/arrolio/renderer/pdf/destination_emitter.rb` — emits:
|
|
31
|
+
- **Named destinations**: `/Names /Dests << /Name (page_ref /Fit) >>
|
|
32
|
+
>>` on Catalog (or a dedicated Names dict).
|
|
33
|
+
- **Page-relative destinations**: stored inline in the link
|
|
34
|
+
annotation's `/D` array.
|
|
35
|
+
|
|
36
|
+
Engine integration:
|
|
37
|
+
- When the engine places a flowable with a `destination_id`
|
|
38
|
+
attribute, record a Destination at the current page number and the
|
|
39
|
+
flowable's top y.
|
|
40
|
+
- Pass 2 walks the Output tree; for each Hyperlink or
|
|
41
|
+
PageNumberCitationField pointing to a `ref_id`, look up the
|
|
42
|
+
Destination.
|
|
43
|
+
|
|
44
|
+
## Done-When
|
|
45
|
+
|
|
46
|
+
- [ ] A section heading with `id: "intro"` records a Destination
|
|
47
|
+
pointing to its page.
|
|
48
|
+
- [ ] A hyperlink to "intro" jumps to the correct page in a viewer.
|
|
49
|
+
- [ ] A `PageNumberCitationField` referencing "intro" shows the
|
|
50
|
+
correct page number.
|
|
51
|
+
- [ ] Missing `ref_id` raises a clear error during pass 2.
|
|
52
|
+
- [ ] Re-read PDF has the expected `/Names /Dests` or `/Dests` dict.
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 12
|
|
4
|
+
depends_on: [39, 41]
|
|
5
|
+
layer: xref
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Authoring a TOC by hand is tedious. The TOC builder walks the
|
|
13
|
+
Content tree's sections, produces a flowable list of TOC entries —
|
|
14
|
+
each is `"section_title" + Leader + "page_number_citation"` — and
|
|
15
|
+
inserts it where the author marked a TOC placeholder.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
File: `lib/arrolio/toc_builder.rb`.
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
class Arrolio::TocBuilder
|
|
23
|
+
def initialize(content_document, levels: 1..3, style: :toc_entry)
|
|
24
|
+
def build -> [TextFlowable, TextFlowable, ...]
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Algorithm:
|
|
29
|
+
1. Walk `content_document.sections`; collect headings up to `levels`.
|
|
30
|
+
2. For each heading, build a `TextFlowable` containing:
|
|
31
|
+
- `[InlineRun.new(section.title, style: indent_style(level)),
|
|
32
|
+
Leader.new,
|
|
33
|
+
PageNumberCitationField.new(ref_id: section.id)]`
|
|
34
|
+
3. Indent per level (level 1 = no indent, level 2 = 12pt, level 3 = 24pt).
|
|
35
|
+
|
|
36
|
+
Engine integration: the author places a `TocPlaceholder` flowable in
|
|
37
|
+
the content stream. The engine replaces it with `TocBuilder.build`'s
|
|
38
|
+
output at the start of pass 1.
|
|
39
|
+
|
|
40
|
+
Two-pass effect: TOC entries need their cited page numbers, which
|
|
41
|
+
aren't known until pass 1 completes. The TOC must be laid out in
|
|
42
|
+
pass 1 with estimated widths; in pass 2 the field runs resolve and
|
|
43
|
+
the TOC text is final.
|
|
44
|
+
|
|
45
|
+
## Done-When
|
|
46
|
+
|
|
47
|
+
- [ ] A 3-section document produces a TOC with 3 entries.
|
|
48
|
+
- [ ] Each entry has the section title left, dot leader filling
|
|
49
|
+
middle, page number right.
|
|
50
|
+
- [ ] Multi-page TOC flows correctly across pages.
|
|
51
|
+
- [ ] Nested sections (level 2, 3) indent visibly.
|
|
52
|
+
- [ ] Page numbers in TOC match actual page numbers from pass 1.
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 13
|
|
4
|
+
depends_on: [20]
|
|
5
|
+
layer: page
|
|
6
|
+
est: 3d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Footnotes: inline reference number in body text, footnote body at
|
|
13
|
+
the bottom of the page where the reference appears. The footnote
|
|
14
|
+
region shrinks the body frame on that page; footnotes flow into it.
|
|
15
|
+
If a footnote doesn't fit, it continues on the next page (with the
|
|
16
|
+
body reference pointing to the original page).
|
|
17
|
+
|
|
18
|
+
## Approach
|
|
19
|
+
|
|
20
|
+
Files:
|
|
21
|
+
|
|
22
|
+
- `lib/arrolio/footnote.rb` — `Footnote = Struct.new(:id, :content,
|
|
23
|
+
:marker, keyword_init: true)`. `content` is a Flowable list.
|
|
24
|
+
|
|
25
|
+
- `lib/arrolio/output/footnote_region.rb` — a special Region placed
|
|
26
|
+
at the bottom of the body, separated by a horizontal rule.
|
|
27
|
+
|
|
28
|
+
- `lib/arrolio/engine/footnote_collector.rb` — pass-1 hook: when a
|
|
29
|
+
Footnote flowable is placed on page N, register it on page N's
|
|
30
|
+
footnote region. Body frame on page N shrinks by the footnote
|
|
31
|
+
region's height.
|
|
32
|
+
|
|
33
|
+
Engine integration:
|
|
34
|
+
- Pass 1: place body flowables; when a footnote is encountered,
|
|
35
|
+
compute its height; reserve space at the bottom of the current
|
|
36
|
+
page's body; place the footnote content there.
|
|
37
|
+
- If the footnote doesn't fit even alone on a fresh page, split it:
|
|
38
|
+
first part on the current page, continuation on next.
|
|
39
|
+
|
|
40
|
+
Marker scheme: per-document counter; numeric by default, restart
|
|
41
|
+
option per page or per section.
|
|
42
|
+
|
|
43
|
+
## Done-When
|
|
44
|
+
|
|
45
|
+
- [ ] A paragraph with one footnote renders the marker in body and
|
|
46
|
+
the footnote text at the page bottom.
|
|
47
|
+
- [ ] Footnote region is separated from body by a horizontal rule.
|
|
48
|
+
- [ ] Body frame shrinks to make room for footnotes.
|
|
49
|
+
- [ ] Long footnote splits across pages.
|
|
50
|
+
- [ ] Multiple footnotes on one page render in order.
|
|
51
|
+
- [ ] Round-trip: re-read PDF has footnotes in expected positions.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 13
|
|
4
|
+
depends_on: [20]
|
|
5
|
+
layer: page
|
|
6
|
+
est: 3d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Magazines, newspapers, and some technical docs use multi-column body
|
|
13
|
+
layout. Body content flows column-by-column; when one column fills,
|
|
14
|
+
the next column on the same page begins; only when all columns are
|
|
15
|
+
full does the engine advance to the next page.
|
|
16
|
+
|
|
17
|
+
## Approach
|
|
18
|
+
|
|
19
|
+
Files:
|
|
20
|
+
|
|
21
|
+
- `lib/arrolio/layout_spec/columns.rb` — `Columns = Struct.new(:count,
|
|
22
|
+
:gap, :balance, keyword_init: true)`. `count` = number of columns;
|
|
23
|
+
`gap` = gutter width; `balance` = bool (balance last page's columns).
|
|
24
|
+
|
|
25
|
+
- `lib/arrolio/engine/column_layout.rb` — manages N parallel Frames
|
|
26
|
+
on a page. Body flowables fill column 1 first; when column 1's
|
|
27
|
+
frame is full, advance to column 2; etc.
|
|
28
|
+
|
|
29
|
+
- `lib/arrolio/engine/column_balancer.rb` — for `balance: true` on
|
|
30
|
+
the last page, distribute content evenly across columns (uses
|
|
31
|
+
Breaker from TODO 14 with the column height as target).
|
|
32
|
+
|
|
33
|
+
PageTemplate gains a `columns:` attribute that the engine consults
|
|
34
|
+
when constructing the body frame.
|
|
35
|
+
|
|
36
|
+
## Done-When
|
|
37
|
+
|
|
38
|
+
- [ ] A 2-column layout places body content in left column first,
|
|
39
|
+
then right column on the same page.
|
|
40
|
+
- [ ] Content overflows to next page only when both columns are full.
|
|
41
|
+
- [ ] Column gap is respected.
|
|
42
|
+
- [ ] `balance: true` produces roughly equal column heights on the
|
|
43
|
+
last page.
|
|
44
|
+
- [ ] Footnotes (TODO 43) integrate: footnote region spans full
|
|
45
|
+
page width below the columns.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 13
|
|
4
|
+
depends_on: [26, 39]
|
|
5
|
+
layer: page
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Two related features:
|
|
13
|
+
|
|
14
|
+
1. **Page numbering schemes**: front matter uses roman numerals (i,
|
|
15
|
+
ii, iii); body uses arabic (1, 2, 3) restarting at 1. Currently
|
|
16
|
+
Arrolio has only one global counter.
|
|
17
|
+
2. **Section title in running header**: every body page's header
|
|
18
|
+
shows the current top-level section's title. Requires tracking
|
|
19
|
+
which section is "current" as content flows.
|
|
20
|
+
|
|
21
|
+
## Approach
|
|
22
|
+
|
|
23
|
+
Files:
|
|
24
|
+
|
|
25
|
+
- `lib/arrolio/page_numbering_scheme.rb` — value object: `format`
|
|
26
|
+
(`:arabic`, `:roman_lower`, `:roman_upper`, `:alpha_lower`,
|
|
27
|
+
`:alpha_upper`), `start` (Integer, default 1), `prefix`, `suffix`.
|
|
28
|
+
Method `render(n)` → String.
|
|
29
|
+
|
|
30
|
+
- Extend `PageSequenceMaster` (TODO 25) with `:page_numbering`
|
|
31
|
+
attribute. Each sequence can have its own scheme; restarting at
|
|
32
|
+
sequence start.
|
|
33
|
+
|
|
34
|
+
- Extend `FlowContext` with `current_section` attribute. When the
|
|
35
|
+
engine places a Section heading flowable, it updates
|
|
36
|
+
`context.current_section = section`. Static content with
|
|
37
|
+
`{ text: context.current_section.title }` then resolves correctly.
|
|
38
|
+
|
|
39
|
+
- Add `SectionTitleField < FieldRun` that returns
|
|
40
|
+
`context.current_section.title`.
|
|
41
|
+
|
|
42
|
+
## Done-When
|
|
43
|
+
|
|
44
|
+
- [ ] Front matter pages show roman numerals (i, ii, iii).
|
|
45
|
+
- [ ] Body pages show arabic numerals starting from 1.
|
|
46
|
+
- [ ] Section title in header changes as content crosses section
|
|
47
|
+
boundaries.
|
|
48
|
+
- [ ] Multi-level section: header shows "Chapter 3" while body is in
|
|
49
|
+
Chapter 3, regardless of subsection depth.
|
|
50
|
+
- [ ] Spec coverage for each scheme + section tracking.
|