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,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P1
|
|
3
|
+
phase: 19
|
|
4
|
+
depends_on: [62]
|
|
5
|
+
layer: conformance
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
A baseline ruleset for "did Arrolio itself produce a clean layout?"
|
|
13
|
+
This is the **render-time self-check**: catch Arrolio's own
|
|
14
|
+
regressions, not the source document's. Includes rules every
|
|
15
|
+
production publisher expects:
|
|
16
|
+
|
|
17
|
+
- No orphan headings (heading at very bottom of page).
|
|
18
|
+
- No widow/orphan lines (single line of paragraph alone on a page).
|
|
19
|
+
- Every page has the running header.
|
|
20
|
+
- Tables don't break mid-row.
|
|
21
|
+
- List items don't break mid-item.
|
|
22
|
+
- Footnote and reference on the same page.
|
|
23
|
+
- TOC entries resolve (no broken citations).
|
|
24
|
+
- Page numbering is contiguous.
|
|
25
|
+
|
|
26
|
+
These rules complement semantic-diff (TODO 55): diff catches
|
|
27
|
+
"output changed", layout-rules catch "output is wrong even if it
|
|
28
|
+
matches yesterday's output".
|
|
29
|
+
|
|
30
|
+
## Approach
|
|
31
|
+
|
|
32
|
+
File: `lib/arrolio/conformance/profiles/layout_rules.rb`.
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
module Arrolio::Conformance::Profiles
|
|
36
|
+
module LayoutRules
|
|
37
|
+
def self.profile
|
|
38
|
+
Arrolio::Conformance::Profile.new(
|
|
39
|
+
name: "layout-baseline",
|
|
40
|
+
version: "1.0.0",
|
|
41
|
+
spec_reference: "Arrolio baseline layout expectations",
|
|
42
|
+
rules: [
|
|
43
|
+
# Headings
|
|
44
|
+
Rule.new(id: "no-orphan-heading",
|
|
45
|
+
applies_to: :flowable,
|
|
46
|
+
severity: :warning,
|
|
47
|
+
test: ->(t, ctx) { !(t.heading? && t.last_on_page?) }),
|
|
48
|
+
Rule.new(id: "heading-keep-with-next",
|
|
49
|
+
applies_to: :flowable,
|
|
50
|
+
severity: :error,
|
|
51
|
+
test: ->(t, ctx) { !t.heading? || t.kept_with_next? }),
|
|
52
|
+
|
|
53
|
+
# Paragraphs
|
|
54
|
+
Rule.new(id: "no-widow",
|
|
55
|
+
applies_to: :paragraph,
|
|
56
|
+
severity: :warning,
|
|
57
|
+
test: ->(t, ctx) { t.line_count > 1 || t.first_on_page? }),
|
|
58
|
+
|
|
59
|
+
# Tables
|
|
60
|
+
Rule.new(id: "table-no-mid-row-break",
|
|
61
|
+
applies_to: :table_row,
|
|
62
|
+
severity: :error,
|
|
63
|
+
test: ->(t, ctx) { !t.split_mid_row? }),
|
|
64
|
+
|
|
65
|
+
# Pagination
|
|
66
|
+
Rule.new(id: "page-numbering-contiguous",
|
|
67
|
+
applies_to: :document,
|
|
68
|
+
severity: :error,
|
|
69
|
+
test: ->(t, ctx) { t.page_numbers_contiguous? }),
|
|
70
|
+
Rule.new(id: "every-page-has-footer",
|
|
71
|
+
applies_to: :page,
|
|
72
|
+
severity: :warning,
|
|
73
|
+
test: ->(t, ctx) { t.region(:after).placed_boxes.any? }),
|
|
74
|
+
|
|
75
|
+
# Cross-references
|
|
76
|
+
Rule.new(id: "citations-resolved",
|
|
77
|
+
applies_to: :field_run,
|
|
78
|
+
severity: :error,
|
|
79
|
+
test: ->(t, ctx) { !t.citation? || ctx.citation_for(t.ref_id) }),
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Plus an OIML-specific extension profile that adds rules particular
|
|
88
|
+
to OIML house style (e.g. "every clause has a number", "running
|
|
89
|
+
header shows current top-level section title").
|
|
90
|
+
|
|
91
|
+
### Engine integration
|
|
92
|
+
|
|
93
|
+
Engine emits an `Output::Page[]` with enough metadata for the rules
|
|
94
|
+
to evaluate: `heading?`, `last_on_page?`, `kept_with_next?`,
|
|
95
|
+
`line_count`, `first_on_page?`, `split_mid_row?`,
|
|
96
|
+
`page_numbers_contiguous?`, etc.
|
|
97
|
+
|
|
98
|
+
Run the layout-rules profile automatically after layout (configurable).
|
|
99
|
+
Failed `:error` rules raise `LayoutError`; `:warning` rules log.
|
|
100
|
+
|
|
101
|
+
## Done-When
|
|
102
|
+
|
|
103
|
+
- [ ] LayoutRules.profile has at least 8 rules covering the
|
|
104
|
+
categories above.
|
|
105
|
+
- [ ] A document with an orphan heading fails `no-orphan-heading`.
|
|
106
|
+
- [ ] A document with a mid-row table break fails
|
|
107
|
+
`table-no-mid-row-break`.
|
|
108
|
+
- [ ] A document with broken citations fails `citations-resolved`.
|
|
109
|
+
- [ ] Engine integration: rules run automatically unless disabled.
|
|
110
|
+
- [ ] OIML extension profile applies on top of baseline.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 19
|
|
4
|
+
depends_on: [62, 48]
|
|
5
|
+
layer: conformance
|
|
6
|
+
est: 5d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
veraPDF ships rule profiles for ISO 19005 (PDF/A) and ISO 14289
|
|
13
|
+
(PDF/UA). Arrolio should be able to validate its own output against
|
|
14
|
+
these standards — without requiring veraPDF itself.
|
|
15
|
+
|
|
16
|
+
This is a pragmatic subset, not full veraPDF parity. Goal: catch
|
|
17
|
+
the common 80% of conformance failures before users hit them, then
|
|
18
|
+
let veraPDF do the authoritative check.
|
|
19
|
+
|
|
20
|
+
## Approach
|
|
21
|
+
|
|
22
|
+
Files under `lib/arrolio/conformance/profiles/`:
|
|
23
|
+
|
|
24
|
+
- `pdfa_1b.rb` — PDF/A-1b baseline rules:
|
|
25
|
+
- `/MarkInfo` not required (Level B).
|
|
26
|
+
- All fonts embedded (no standard-14 references).
|
|
27
|
+
- No unembedded JPG2000 in /JBIG2Decode without /DecodeParms.
|
|
28
|
+
- No JavaScript actions.
|
|
29
|
+
- No encrypted streams.
|
|
30
|
+
- `/ColorSpace` does not reference /DeviceN or /DevRGB without ICC.
|
|
31
|
+
|
|
32
|
+
- `pdfa_2b.rb` — PDF/A-2b additions:
|
|
33
|
+
- JPEG2000 allowed (with restrictions).
|
|
34
|
+
- Transparency allowed (with restrictions).
|
|
35
|
+
- Object-level XMP metadata.
|
|
36
|
+
|
|
37
|
+
- `pdfua_1.rb` — PDF/UA-1 rules:
|
|
38
|
+
- `/MarkInfo /true` on Catalog.
|
|
39
|
+
- `/StructTreeRoot` present.
|
|
40
|
+
- Every page has `/Tabs /S` (structured reading order).
|
|
41
|
+
- Every image has `/Alt` text.
|
|
42
|
+
- Every heading maps to the right `<Hn>` element.
|
|
43
|
+
- No nested marked-content sequences beyond depth limit.
|
|
44
|
+
- Font has `/ToUnicode` CMap (text extractable).
|
|
45
|
+
|
|
46
|
+
- `pdfua_2.rb` — PDF/UA-2 (when standard is final).
|
|
47
|
+
|
|
48
|
+
Each profile is a Ruby module with `.profile` returning a
|
|
49
|
+
`Conformance::Profile`. Rules are written in the same DSL as TODO 63.
|
|
50
|
+
|
|
51
|
+
### veraPDF cross-validation
|
|
52
|
+
|
|
53
|
+
- A spec under `spec/conformance/veraPDF_cross_check_spec.rb` runs
|
|
54
|
+
both Arrolio's profiles and veraPDF (via shell) on the same
|
|
55
|
+
fixture PDFs; expected disagreements are catalogued.
|
|
56
|
+
- Rake task `rake conformance:cross_check` runs the suite.
|
|
57
|
+
|
|
58
|
+
## Approach for partial-coverage
|
|
59
|
+
|
|
60
|
+
Be honest in the docs: "Arrolio catches N% of PDF/A-1b rules.
|
|
61
|
+
Run veraPDF for authoritative validation." List the rules NOT
|
|
62
|
+
implemented so users know what's missing.
|
|
63
|
+
|
|
64
|
+
## Done-When
|
|
65
|
+
|
|
66
|
+
- [ ] `pdfa_1b` profile covers ≥ 30 of the most common rules.
|
|
67
|
+
- [ ] `pdfua_1` profile covers ≥ 20 of the most common rules.
|
|
68
|
+
- [ ] An Arrolio-rendered PDF (without fonts embedded) fails
|
|
69
|
+
`pdfa_1b` with a useful error.
|
|
70
|
+
- [ ] An Arrolio-rendered PDF (without StructTree) fails
|
|
71
|
+
`pdfua_1`.
|
|
72
|
+
- [ ] veraPDF cross-check spec runs (when veraPDF is installed) and
|
|
73
|
+
documents the disagreements.
|
|
74
|
+
- [ ] README documents the partial-coverage scope clearly.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 19
|
|
4
|
+
depends_on: [22]
|
|
5
|
+
layer: conformance
|
|
6
|
+
est: 2d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Modeled on `veraPDF/features/`, Arrolio needs a feature extractor
|
|
13
|
+
that walks a PDF (ours or a third-party's) and produces a structured
|
|
14
|
+
feature report — fonts used, images embedded, colour spaces, page
|
|
15
|
+
sizes, annotations, signatures, metadata. Useful for:
|
|
16
|
+
|
|
17
|
+
- Debugging: "why does this PDF render wrong on viewer X?"
|
|
18
|
+
- Conformance pre-checks before running full validation.
|
|
19
|
+
- Diff inputs (compare features between two PDFs as a quick scan).
|
|
20
|
+
- Documentation: "what does this PDF contain?"
|
|
21
|
+
|
|
22
|
+
## Approach
|
|
23
|
+
|
|
24
|
+
Files under `lib/arrolio/conformance/features/`:
|
|
25
|
+
|
|
26
|
+
- `feature_extractor.rb` — top-level orchestrator; walks a
|
|
27
|
+
`Pdfrb::Document`, dispatches per object type, accumulates
|
|
28
|
+
`FeatureReport`.
|
|
29
|
+
|
|
30
|
+
- `feature_report.rb` — frozen value object: pages, fonts, images,
|
|
31
|
+
colour_spaces, annotations, signatures, metadata, embedded_files,
|
|
32
|
+
outlines, acroform_fields.
|
|
33
|
+
|
|
34
|
+
- Per-category extractors:
|
|
35
|
+
- `font_features.rb` — base_font, subtype, embedded?, encoding,
|
|
36
|
+
to_unicode?, num_glyphs.
|
|
37
|
+
- `image_features.rb` — width, height, bits_per_component, color_space,
|
|
38
|
+
filter, embedded?, smask?.
|
|
39
|
+
- `color_space_features.rb` — name, type (DeviceRGB/CMYK/ICC/etc.),
|
|
40
|
+
icc_profile?.
|
|
41
|
+
- `annotation_features.rb` — subtype, rect, action.
|
|
42
|
+
- `page_features.rb` — mediabox, rotate, contents_size,
|
|
43
|
+
annotation_count.
|
|
44
|
+
- `metadata_features.rb` — info dict fields, xmp_packet?.
|
|
45
|
+
|
|
46
|
+
- `feature_serializer.rb` — `to_yaml`, `to_json`, `to_html_table`.
|
|
47
|
+
|
|
48
|
+
### Diff integration
|
|
49
|
+
|
|
50
|
+
A `feature_diff` mode that compares two FeatureReports and emits a
|
|
51
|
+
summary ("font Helvetica added", "image /Im3 missing in v2"). This
|
|
52
|
+
is a fast pre-check before the full semantic comparator (TODO 55).
|
|
53
|
+
|
|
54
|
+
## Done-When
|
|
55
|
+
|
|
56
|
+
- [ ] FeatureExtractor produces a complete FeatureReport on a
|
|
57
|
+
fixture PDF.
|
|
58
|
+
- [ ] All category extractors run without error on FOP-produced PDFs.
|
|
59
|
+
- [ ] YAML/JSON/HTML serialisation works.
|
|
60
|
+
- [ ] A simple "feature_diff" highlights added/removed/changed
|
|
61
|
+
features between two PDFs.
|
|
62
|
+
- [ ] Round-trip: extract from rendered PDF shows expected font
|
|
63
|
+
list, image count, page count.
|
|
64
|
+
- [ ] Spec coverage for each category extractor.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
priority: P2
|
|
3
|
+
phase: 19
|
|
4
|
+
depends_on: [62, 64]
|
|
5
|
+
layer: conformance
|
|
6
|
+
est: 3d
|
|
7
|
+
status: pending
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Problem
|
|
11
|
+
|
|
12
|
+
Modeled on `veraPDF/metadata/fixer/`, Arrolio needs an auto-fixer
|
|
13
|
+
that takes the failed assertions from a conformance profile and
|
|
14
|
+
applies known-safe fixes:
|
|
15
|
+
|
|
16
|
+
- Missing `/CreationDate` → fill in `Time.now`.
|
|
17
|
+
- Missing `/Producer` → `"Arrolio #{Arrolio::VERSION} + Pdfrb"`.
|
|
18
|
+
- Missing `/MarkInfo /true` → add (when StructTree exists).
|
|
19
|
+
- Missing `/Tabs /S` on pages → add.
|
|
20
|
+
- Missing font `/ToUnicode` → generate from font encoding (best effort).
|
|
21
|
+
- XMP packet missing → emit one from `/Info` fields.
|
|
22
|
+
|
|
23
|
+
The fixer is **opt-in**: it never modifies without being asked. It
|
|
24
|
+
also reports what it changed.
|
|
25
|
+
|
|
26
|
+
## Approach
|
|
27
|
+
|
|
28
|
+
Files under `lib/arrolio/conformance/fixer/`:
|
|
29
|
+
|
|
30
|
+
- `base_fixer.rb` — abstract; takes a `Pdfrb::Document` + a
|
|
31
|
+
`ValidationResult`, returns a `FixResult`.
|
|
32
|
+
|
|
33
|
+
- `fix_result.rb` — value object: applied_fixes (`Array<AppliedFix>`),
|
|
34
|
+
skipped_fixes (`Array<SkippedFix>`), unchanged.
|
|
35
|
+
|
|
36
|
+
- `applied_fix.rb` — value object: rule_id, fix_kind, before, after,
|
|
37
|
+
location.
|
|
38
|
+
|
|
39
|
+
- Per-rule fixers (one file each, loaded only when the rule failed):
|
|
40
|
+
- `creation_date_fixer.rb`
|
|
41
|
+
- `producer_fixer.rb`
|
|
42
|
+
- `mark_info_fixer.rb`
|
|
43
|
+
- `tabs_fixer.rb`
|
|
44
|
+
- `to_unicode_fixer.rb`
|
|
45
|
+
- `xmp_packet_fixer.rb`
|
|
46
|
+
|
|
47
|
+
Each fixer is **conservative**: if it can't be sure the fix is
|
|
48
|
+
correct, it skips (records the reason) rather than guessing.
|
|
49
|
+
|
|
50
|
+
### Pipeline integration
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
# After render:
|
|
54
|
+
doc = render_to_pdfrb_doc(content, layout_spec)
|
|
55
|
+
report = Arrolio::Conformance::Validator.(
|
|
56
|
+
Arrolio::Conformance::Profiles::Pdfa1b.profile,
|
|
57
|
+
doc
|
|
58
|
+
)
|
|
59
|
+
fixed_doc, fix_result = Arrolio::Conformance::Fixer.(doc, report)
|
|
60
|
+
fixed_doc.write(io: output_io)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`Fixer` is opt-in via a render option `fix_conformance: true`.
|
|
64
|
+
|
|
65
|
+
## Done-When
|
|
66
|
+
|
|
67
|
+
- [ ] A PDF missing `/Producer` gets it added.
|
|
68
|
+
- [ ] A PDF missing `/CreationDate` gets it added.
|
|
69
|
+
- [ ] A PDF missing `/MarkInfo /true` (but with StructTree) gets it
|
|
70
|
+
added.
|
|
71
|
+
- [ ] Each applied fix is recorded with before/after evidence.
|
|
72
|
+
- [ ] Fixer skips fixes it's not sure about (no guessing).
|
|
73
|
+
- [ ] Round-trip: fixed PDF re-validated passes the rules it failed
|
|
74
|
+
before.
|
|
75
|
+
- [ ] `render(fix_conformance: true)` flag wires it in.
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# TODO.implementation — Arrolio Work Breakdown
|
|
2
|
+
|
|
3
|
+
Arrolio is the **Ruby-only alternative to Apache FOP / XSL-FO**. It takes
|
|
4
|
+
a typed content tree plus a typed layout spec, lays out pages, and renders
|
|
5
|
+
to PDF via the sibling `pdfrb` gem. No XML input. No FO intermediate.
|
|
6
|
+
The Ruby model is the source of truth.
|
|
7
|
+
|
|
8
|
+
This directory tracks all implementation work as one file per TODO,
|
|
9
|
+
named `{NN}-{slug}.md`.
|
|
10
|
+
|
|
11
|
+
## Architectural layering (recap)
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
Content Tree (Ruby, typed, medium-independent)
|
|
15
|
+
↓
|
|
16
|
+
LayoutSpec (Ruby, typed: PageTemplate, Region, Style, Flow)
|
|
17
|
+
↓
|
|
18
|
+
Engine::Paged (Knuth-Plass breaking, frame threading)
|
|
19
|
+
↓
|
|
20
|
+
Output::Page[] (laid-out pages with placed boxes)
|
|
21
|
+
↓
|
|
22
|
+
Renderer::Pdf (walks Output tree, emits bytes via Pdfrb)
|
|
23
|
+
↓
|
|
24
|
+
PDF bytes
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Each layer is independently testable. Output::Page[] is the contract
|
|
28
|
+
between Engine and Renderer.
|
|
29
|
+
|
|
30
|
+
## Primary end-to-end correctness: three subsystems
|
|
31
|
+
|
|
32
|
+
Arrolio is validated by three complementary subsystems, all
|
|
33
|
+
exercised against real OIML documents:
|
|
34
|
+
|
|
35
|
+
1. **OIML E2E pipeline** (TODOs 52–54) — parse real OIML XML via
|
|
36
|
+
`metanorma-document`, apply an OIML LayoutSpec, render to PDF via
|
|
37
|
+
Pdfrb. Proves the full pipeline works end-to-end.
|
|
38
|
+
|
|
39
|
+
2. **PDF diff subsystem** (TODOs 55–59), modeled on
|
|
40
|
+
`~/src/lutaml/canon/` — semantic comparator with dimensions
|
|
41
|
+
(text, font, position, colour, image, vector, structure),
|
|
42
|
+
normative/informative classifier, multiple formatters, RSpec
|
|
43
|
+
matchers, optional pixel diff. Catches "output changed in a way
|
|
44
|
+
that matters".
|
|
45
|
+
|
|
46
|
+
3. **Conformance subsystem** (TODOs 62–66), modeled on
|
|
47
|
+
`~/src/external/veraPDF-library/` — rule-based validator with
|
|
48
|
+
profiles for layout baseline (TODO 63), PDF/A (TODO 64), and
|
|
49
|
+
PDF/UA (TODO 64); feature extractor for debugging (TODO 65);
|
|
50
|
+
auto-fixer for known-safe repairs (TODO 66). Catches "output is
|
|
51
|
+
wrong even when it matches yesterday's output".
|
|
52
|
+
|
|
53
|
+
## Critical path (P0)
|
|
54
|
+
|
|
55
|
+
To reach **minimum viable PDF** (a one-page document with text), in order:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
01 → 02 → 06 → 07 → 09 → 10 → 11 → 13
|
|
59
|
+
↓
|
|
60
|
+
21 → 22 → 23 (output) ←── 20 (engine) ←── 16-19 (flowables)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
After that, page templates (Phase 7) and tables (Phase 8) deliver
|
|
64
|
+
FOP-equivalent capability. Then the OIML E2E pipeline (Phase 17)
|
|
65
|
+
proves the engine on real-world documents, and the diff subsystem
|
|
66
|
+
makes correctness verifiable per-commit.
|
|
67
|
+
|
|
68
|
+
## Phase index
|
|
69
|
+
|
|
70
|
+
| Phase | Range | Layer | Outcome |
|
|
71
|
+
|---|---|---|---|
|
|
72
|
+
| 1 | 01–05 | foundation | Skeleton, content contract, layout spec, style system, visitors |
|
|
73
|
+
| 2 | 06–09 | metrics | AFM + TrueType glyph metrics + Registry + GlyphMeasurer |
|
|
74
|
+
| 3 | 10–13 | text | Break opportunities, Greedy + Knuth-Plass, Line + alignment |
|
|
75
|
+
| 4 | 14–15 | knuth | Universal Knuth elements + Breaker (refactor of Phase 3) |
|
|
76
|
+
| 5 | 16–20 | flowable | Flowable hierarchy + Frame + Engine::Paged driver |
|
|
77
|
+
| 6 | 21–23 | output | Output::Page[] + Renderer::Pdf + end-to-end smoke |
|
|
78
|
+
| 7 | 24–26 | template | PageTemplate, PageSequenceMaster, two-pass citations |
|
|
79
|
+
| 8 | 27–30 | table | Table model + Fixed/Auto layouts + TableFlowable |
|
|
80
|
+
| 9 | 31–32 | list | List model + ListFlowable (proper label/body columns) |
|
|
81
|
+
| 10 | 33–35 | media | Image loading via Pdfrb + SVG renderer + Form XObjects |
|
|
82
|
+
| 11 | 36–38 | inline | InlineBuilder + Hyperlink + Sub/superscript |
|
|
83
|
+
| 12 | 39–42 | xref | Leader + FieldRuns + Bookmark/outline + Destinations + TOC |
|
|
84
|
+
| 13 | 43–45 | page | Footnotes + Multi-column + Page numbering + Section tracking |
|
|
85
|
+
| 14 | 46–47 | color | Color management + XMP/Info metadata write |
|
|
86
|
+
| 15 | 48–49 | a11y | PDF/UA StructTree + marked content + alt text |
|
|
87
|
+
| 16 | 50–51 | composer | High-level Composer facade + style presets |
|
|
88
|
+
| 17 | **52–59** | **harness/diff** | **OIML E2E pipeline + canon-style diff subsystem** |
|
|
89
|
+
| 18 | 60–61 | polish | YARD docs + performance/stress tests |
|
|
90
|
+
| 19 | **62–66** | **conformance** | **veraPDF-style validator + profiles + feature extractor + fixer** |
|
|
91
|
+
|
|
92
|
+
**Total: 66 TODOs.**
|
|
93
|
+
|
|
94
|
+
## Phase 17 in detail — OIML E2E + diff subsystem
|
|
95
|
+
|
|
96
|
+
| TODO | What it builds |
|
|
97
|
+
|---|---|
|
|
98
|
+
| 52 | OIML adapter (metanorma-document → Arrolio content) + fixture corpus |
|
|
99
|
+
| 53 | OIML LayoutSpec — Ruby encoding of `oiml.xsl`'s visual intent |
|
|
100
|
+
| 54 | Pipeline (`Arrolio::Oiml::Pipeline.(xml, io:)`) + CLI + sanity spec |
|
|
101
|
+
| 55 | **PDF comparator** — semantic tree diff with dimensions, normative/informative classifier, profiles |
|
|
102
|
+
| 56 | **Diff formatters** — summary, pretty, by-page, by-element, JSON, HTML, JUnit |
|
|
103
|
+
| 57 | **Path locator + enrichment** — canonical paths, char-level diffs, source positions |
|
|
104
|
+
| 58 | **RSpec matchers + Harness API** — `match_pdf`, `be_equivalent_to_pdf`, `Arrolio::Harness.diff` |
|
|
105
|
+
| 59 | **Pixel diff mode** — optional raster-based diff for visual regressions |
|
|
106
|
+
|
|
107
|
+
## Phase 19 in detail — Conformance & validation (veraPDF-inspired)
|
|
108
|
+
|
|
109
|
+
| TODO | What it builds |
|
|
110
|
+
|---|---|
|
|
111
|
+
| 62 | Validation framework — Rule, Profile, TestAssertion, ValidationResult, Validator, Location |
|
|
112
|
+
| 63 | Layout baseline rules — no orphan headings, no widow/orphans, contiguous page numbering |
|
|
113
|
+
| 64 | PDF/A + PDF/UA conformance profiles — pragmatic rule subset; cross-check vs veraPDF |
|
|
114
|
+
| 65 | Feature extractor — fonts/images/colour/annotations dump for debugging |
|
|
115
|
+
| 66 | Auto-fixer — applies known-safe fixes (CreationDate, Producer, MarkInfo, XMP) |
|
|
116
|
+
|
|
117
|
+
After TODO 66 lands, Arrolio can self-validate its own output against
|
|
118
|
+
PDF/A and PDF/UA rules — a veraPDF in Ruby for documents Arrolio
|
|
119
|
+
produces.
|
|
120
|
+
|
|
121
|
+
## Front-matter shape
|
|
122
|
+
|
|
123
|
+
```yaml
|
|
124
|
+
---
|
|
125
|
+
priority: P0 # P0 blocks MVP; P1 = FOP parity; P2 = production; P3 = polish
|
|
126
|
+
phase: 1
|
|
127
|
+
depends_on: [] # TODO numbers that must be done first
|
|
128
|
+
layer: foundation
|
|
129
|
+
est: 2d # 1d = single focused session; 5d = multi-week module
|
|
130
|
+
status: pending # pending | in_progress | done
|
|
131
|
+
---
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## What Arrolio deliberately does NOT do
|
|
135
|
+
|
|
136
|
+
- Parse XML input (FO or otherwise). **Loom's** job.
|
|
137
|
+
- Author DSLs (YAML, custom syntax). **Loom's** job.
|
|
138
|
+
- Compile to multiple targets (CSS, FO, IDML). **Loom's** job.
|
|
139
|
+
- Speak PDF byte format directly. **Pdfrb's** job.
|
|
140
|
+
- Own the semantic content model. **metanorma-document's** job —
|
|
141
|
+
Arrolio defines a content contract, metanorma-document fills it.
|
|
142
|
+
- Replace veraPDF authoritatively for PDF/A. **veraPDF's** job.
|
|
143
|
+
Arrolio's conformance profiles (Phase 19) are a pragmatic
|
|
144
|
+
pre-check; veraPDF remains authoritative.
|
|
145
|
+
|
|
146
|
+
If a TODO crosses these boundaries, it belongs in another gem.
|
|
147
|
+
|
|
148
|
+
## Verification
|
|
149
|
+
|
|
150
|
+
Every TODO carries Done-When criteria. The global verification is:
|
|
151
|
+
|
|
152
|
+
- `bundle exec rake` — runs rspec + rubocop; must be green after every TODO.
|
|
153
|
+
- After TODO 23 (end-to-end smoke): a single-paragraph PDF renders correctly.
|
|
154
|
+
- After TODO 30 (TableFlowable): an OIML-style multi-page doc with tables
|
|
155
|
+
renders.
|
|
156
|
+
- After TODO 54 (OIML pipeline): real OIML XML renders to PDF.
|
|
157
|
+
- After TODO 58 (RSpec matchers): every commit validates against FOP
|
|
158
|
+
references with `be_equivalent_to_pdf`.
|
|
159
|
+
- After TODO 64 (PDF/A + PDF/UA): Arrolio's output passes the
|
|
160
|
+
common-subset conformance rules.
|
|
161
|
+
|
|
162
|
+
## Status legend
|
|
163
|
+
|
|
164
|
+
- `pending` — not started.
|
|
165
|
+
- `in_progress` — actively being worked.
|
|
166
|
+
- `done` — complete; specs pass; ready for review.
|
|
167
|
+
|
|
168
|
+
(Initial seed: all 66 TODOs are `pending`. TODO 01 is partially done —
|
|
169
|
+
the gem skeleton already exists.)
|