fullbleed 2.2.3__tar.gz → 2.2.5__tar.gz

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.
Files changed (184) hide show
  1. {fullbleed-2.2.3 → fullbleed-2.2.5}/Cargo.lock +1 -1
  2. {fullbleed-2.2.3 → fullbleed-2.2.5}/Cargo.toml +1 -1
  3. {fullbleed-2.2.3 → fullbleed-2.2.5}/PKG-INFO +38 -4
  4. {fullbleed-2.2.3 → fullbleed-2.2.5}/README.md +37 -3
  5. {fullbleed-2.2.3 → fullbleed-2.2.5}/pyproject.toml +1 -1
  6. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/_abi.py +57 -3
  7. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/cli.py +10 -2
  8. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/flate_native.rs +205 -71
  9. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/flowable.rs +148 -3
  10. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/font.rs +39 -0
  11. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html.rs +87 -25
  12. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_dom.rs +664 -3
  13. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/lib.rs +1756 -90
  14. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/pdf.rs +1603 -161
  15. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/python/abi_exports.rs +75 -0
  16. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/python.rs +5 -0
  17. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/style.rs +19 -1
  18. {fullbleed-2.2.3 → fullbleed-2.2.5}/COPYRIGHT +0 -0
  19. {fullbleed-2.2.3 → fullbleed-2.2.5}/FONT_LICENSE_AUDIT.json +0 -0
  20. {fullbleed-2.2.3 → fullbleed-2.2.5}/FONT_LICENSE_AUDIT.md +0 -0
  21. {fullbleed-2.2.3 → fullbleed-2.2.5}/LICENSE +0 -0
  22. {fullbleed-2.2.3 → fullbleed-2.2.5}/LICENSING.md +0 -0
  23. {fullbleed-2.2.3 → fullbleed-2.2.5}/THIRD_PARTY_LICENSES.md +0 -0
  24. {fullbleed-2.2.3 → fullbleed-2.2.5}/build.rs +0 -0
  25. {fullbleed-2.2.3 → fullbleed-2.2.5}/build_backend/fullbleed_build_backend.py +0 -0
  26. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/Cargo.lock +0 -0
  27. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/Cargo.toml +0 -0
  28. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/LICENSE +0 -0
  29. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/README.md +0 -0
  30. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/specs/fullbleed.audit_registry.v1.yaml +0 -0
  31. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/specs/section508_html_registry.v1.yaml +0 -0
  32. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/specs/wcag20aa_registry.v1.yaml +0 -0
  33. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/src/json.rs +0 -0
  34. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/src/lib.rs +0 -0
  35. {fullbleed-2.2.3 → fullbleed-2.2.5}/crates/fullbleed_audit_contract/src/sha256.rs +0 -0
  36. {fullbleed-2.2.3 → fullbleed-2.2.5}/docs/specs/fullbleed.a11y.verify.v1.schema.json +0 -0
  37. {fullbleed-2.2.3 → fullbleed-2.2.5}/docs/specs/fullbleed.audit_registry.v1.yaml +0 -0
  38. {fullbleed-2.2.3 → fullbleed-2.2.5}/docs/specs/fullbleed.pmr.v1.schema.json +0 -0
  39. {fullbleed-2.2.3 → fullbleed-2.2.5}/docs/specs/section508_html_registry.v1.yaml +0 -0
  40. {fullbleed-2.2.3 → fullbleed-2.2.5}/docs/specs/wcag20aa_registry.v1.yaml +0 -0
  41. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/__init__.py +0 -0
  42. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/__main__.py +0 -0
  43. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/accessibility/__init__.py +0 -0
  44. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/accessibility/engine.py +0 -0
  45. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/accessibility/runtime.py +0 -0
  46. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/accessibility/types.py +0 -0
  47. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/audit_prototype.py +0 -0
  48. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/audit_section508.py +0 -0
  49. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/audit_wcag.py +0 -0
  50. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/__init__.py +0 -0
  51. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/accessibility.py +0 -0
  52. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/__init__.py +0 -0
  53. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/_core.py +0 -0
  54. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/agency_letter.py +0 -0
  55. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/court_motion_form.py +0 -0
  56. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/declaration_form.py +0 -0
  57. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/instruction_sheet.py +0 -0
  58. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/investment_portfolio_report.py +0 -0
  59. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/marriage_record.py +0 -0
  60. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/profiles.py +0 -0
  61. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/recorded_plat.py +0 -0
  62. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/redaction_request_form.py +0 -0
  63. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/cav/warranty_deed.py +0 -0
  64. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/core.py +0 -0
  65. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/primitives.py +0 -0
  66. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed/ui/style.py +0 -0
  67. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/__init__.py +0 -0
  68. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/bootstrap.min.css +0 -0
  69. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/fonts/Inter-Variable.ttf +0 -0
  70. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/fonts/NotoSans-Regular.ttf +0 -0
  71. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/fonts/NotoSansMath-Regular.ttf +0 -0
  72. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/fonts/NotoSansSymbols-Regular.ttf +0 -0
  73. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  74. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_assets/icons/bootstrap-icons.svg +0 -0
  75. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/__init__.py +0 -0
  76. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/__main__.py +0 -0
  77. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/assets.py +0 -0
  78. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/cache.py +0 -0
  79. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/finalize.py +0 -0
  80. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold.py +0 -0
  81. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/__init__.py +0 -0
  82. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/.gitignore +0 -0
  83. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/COMPLIANCE.md +0 -0
  84. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/SCAFFOLDING.md +0 -0
  85. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/__init__.py +0 -0
  86. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/body.py +0 -0
  87. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/fb_ui.py +0 -0
  88. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/footer.py +0 -0
  89. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/header.py +0 -0
  90. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/primitives.py +0 -0
  91. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/styles/body.css +0 -0
  92. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/styles/footer.css +0 -0
  93. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/styles/header.css +0 -0
  94. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/components/styles/primitives.css +0 -0
  95. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/report.py +0 -0
  96. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/styles/report.css +0 -0
  97. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/init/styles/tokens.css +0 -0
  98. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/accessible/README.md +0 -0
  99. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/accessible/output/.gitignore +0 -0
  100. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/accessible/output/.gitkeep +0 -0
  101. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/accessible/report.py +0 -0
  102. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/accessible/styles/report.css +0 -0
  103. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/accessible/vendor/fonts/Inter-Variable.ttf +0 -0
  104. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/invoice/output/.gitignore +0 -0
  105. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/invoice/output/.gitkeep +0 -0
  106. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/invoice/report.py +0 -0
  107. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/invoice/templates/invoice.css +0 -0
  108. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/invoice/templates/invoice.html +0 -0
  109. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/COMPLIANCE.md +0 -0
  110. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/README.md +0 -0
  111. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/SCAFFOLDING.md +0 -0
  112. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/assets/brand-mark.svg +0 -0
  113. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/assets/reference-pattern.svg +0 -0
  114. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/__init__.py +0 -0
  115. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/appendix.py +0 -0
  116. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/architecture.py +0 -0
  117. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/cover.py +0 -0
  118. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/data_pages.py +0 -0
  119. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/fb_ui.py +0 -0
  120. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/primitives.py +0 -0
  121. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/styles/appendix.css +0 -0
  122. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/styles/architecture.css +0 -0
  123. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/styles/cover.css +0 -0
  124. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/styles/data.css +0 -0
  125. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/styles/primitives.css +0 -0
  126. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/styles/visuals.css +0 -0
  127. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/components/visuals.py +0 -0
  128. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/data/reference.json +0 -0
  129. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/output/.gitignore +0 -0
  130. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/report.py +0 -0
  131. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/styles/report.css +0 -0
  132. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/reference/styles/tokens.css +0 -0
  133. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/statement/output/.gitignore +0 -0
  134. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/statement/output/.gitkeep +0 -0
  135. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/statement/report.py +0 -0
  136. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/statement/templates/statement.css +0 -0
  137. {fullbleed-2.2.3 → fullbleed-2.2.5}/python/fullbleed_cli/scaffold_templates/new/statement/templates/statement.html +0 -0
  138. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/assets.rs +0 -0
  139. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/base64.rs +0 -0
  140. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/canvas.rs +0 -0
  141. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/css_native.rs +0 -0
  142. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/css_queries.rs +0 -0
  143. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/debug.rs +0 -0
  144. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/doc_context.rs +0 -0
  145. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/doc_template.rs +0 -0
  146. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/error.rs +0 -0
  147. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/finalize.rs +0 -0
  148. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/font_subset.rs +0 -0
  149. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/frame.rs +0 -0
  150. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/glyph_report.rs +0 -0
  151. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities.rs +0 -0
  152. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities_0.rs +0 -0
  153. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities_1.rs +0 -0
  154. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities_2.rs +0 -0
  155. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities_3.rs +0 -0
  156. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities_4.rs +0 -0
  157. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/html_entities_5.rs +0 -0
  158. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/image_native.rs +0 -0
  159. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/jit.rs +0 -0
  160. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/jpeg_native.rs +0 -0
  161. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/math.rs +0 -0
  162. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/metrics.rs +0 -0
  163. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/native_shape.rs +0 -0
  164. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/page_data.rs +0 -0
  165. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/page_template.rs +0 -0
  166. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/parallel.rs +0 -0
  167. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/pdf_encodings.rs +0 -0
  168. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/pdf_native.rs +0 -0
  169. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/pdf_raster.rs +0 -0
  170. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/pdfinspect.rs +0 -0
  171. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/perf.rs +0 -0
  172. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/plan.rs +0 -0
  173. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/python_abi.rs +0 -0
  174. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/raster.rs +0 -0
  175. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/raster_native.rs +0 -0
  176. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/sfnt.rs +0 -0
  177. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/sfnt_cff.rs +0 -0
  178. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/sfnt_outline.rs +0 -0
  179. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/spill.rs +0 -0
  180. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/svg.rs +0 -0
  181. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/text_shape.rs +0 -0
  182. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/types.rs +0 -0
  183. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/unicode_data.rs +0 -0
  184. {fullbleed-2.2.3 → fullbleed-2.2.5}/src/xml.rs +0 -0
@@ -4,7 +4,7 @@ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "fullbleed"
7
- version = "2.2.3"
7
+ version = "2.2.5"
8
8
  dependencies = [
9
9
  "fullbleed_audit_contract",
10
10
  "subsetter",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "fullbleed"
3
- version = "2.2.3"
3
+ version = "2.2.5"
4
4
  edition = "2024"
5
5
  rust-version = "1.85"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fullbleed
3
- Version: 2.2.3
3
+ Version: 2.2.5
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: License :: OSI Approved :: MIT License
6
6
  Classifier: Operating System :: MacOS
@@ -136,6 +136,26 @@ size-changing records render 200x faster. See
136
136
  [`docs/performance-architecture.md`](docs/performance-architecture.md) for the packed vector IR,
137
137
  typed-binding, virtual-linker, and shader roadmap.
138
138
 
139
+ Fullbleed 2.2.5 hardens the third compiler lane introduced in 2.2.4 for
140
+ size-changing records: `CompiledDocument.render_pdf_reflow_bindings(...)`. It parses and recovers
141
+ the template DOM once and compiles encountered structural flow variants into guarded fixed-point
142
+ display programs. Matching records bind directly into those programs; workers execute pre-shaped
143
+ text/TJ paint slots, cached static PDF page segments, page-local Deflate, and one ordered linker.
144
+ Explicit trusted structural slots use `data-fb-bind-html="slot"`. A value that no compiled variant
145
+ can safely place runs ordinary fixed-point layout once to add another variant.
146
+
147
+ On the independent 1,000-distinct-record case study, the 29-sample hot direct-file median was
148
+ 216.160 ms: **4,626 records/s and 8,096 pages/s**. The 168.995 ms best sample reached **5,917
149
+ records/s and 10,355 pages/s**. It produced
150
+ the exact 1,750-page 500/300/150/50 reflow distribution and verified all 24,900 markers. The
151
+ throughput-tuned 6,870,320-byte PDF was deterministic across all samples. With the compact
152
+ compression setting, compiled output was also byte-for-byte identical to ordinary rendering:
153
+ 5,298,961 bytes and SHA-256 `bb3c441313a08fb00d3bd15f23a567981f3bbbd550908e3a9fe7a07ca5d7f138`.
154
+ This is a hot compiled-variant result, not a claim for previously unseen structure. The first
155
+ variant-discovery render measured 0.509 s (3,437 pages/s), and the fully cold job including setup,
156
+ bindings, compilation, discovery, and write measured 0.588 s (2,975 pages/s). The independently
157
+ observed hot-median speedup over the ordinary path was 43.8x.
158
+
139
159
  ## Install
140
160
 
141
161
  New to Python or setting up on a fresh machine? Start with `docs/install-non-technical.md`.
@@ -147,7 +167,7 @@ python -m pip install fullbleed
147
167
  From a local wheel:
148
168
 
149
169
  ```bash
150
- python -m pip install C:\path\to\fullbleed-2.2.3-cp310-abi3-win_amd64.whl
170
+ python -m pip install C:\path\to\fullbleed-2.2.5-cp310-abi3-win_amd64.whl
151
171
  ```
152
172
 
153
173
  From a source checkout with Rust installed, no Python build package is needed:
@@ -600,7 +620,8 @@ Machine discovery:
600
620
  fullbleed capabilities --json
601
621
  ```
602
622
 
603
- Inspect the `svg` object in `fullbleed.capabilities.v1` for SVG support metadata.
623
+ Inspect the `svg` object in `fullbleed.capabilities.v1` for SVG support metadata. The `engine`
624
+ object also reports compiled-document/reflow availability and supported per-call compression modes.
604
625
  It reports the compiled `svg_raster` build feature plus a feature matrix for
605
626
  native-vector SVG, raster-fallback-required SVG, and unsupported/known-loss SVG
606
627
  features.
@@ -959,6 +980,7 @@ Module exports:
959
980
 
960
981
  - `PdfEngine`
961
982
  - `CompiledDocument`
983
+ - `CompiledFlowCompression`
962
984
  - `AssetBundle`
963
985
  - `Asset`
964
986
  - `AssetKind`
@@ -1007,13 +1029,25 @@ Module exports:
1007
1029
  | `render_pdf_batch(copies, deterministic_hash=None)` | `bytes` |
1008
1030
  | `render_pdf_bindings(bindings, deterministic_hash=None)` | `bytes` |
1009
1031
  | `render_pdf_bindings_to_file(bindings, path, deterministic_hash=None)` | `int` |
1032
+ | `render_pdf_reflow_bindings(bindings, deterministic_hash=None, *, compression="throughput")` | `bytes` |
1033
+ | `render_pdf_reflow_bindings_to_file(bindings, path, deterministic_hash=None, *, compression="throughput")` | `int` |
1010
1034
 
1011
1035
  The compiled lane runs parsing/layout once. `render_pdf_batch` virtualizes identical untagged page
1012
- content. The binding methods instead accept columnar `dict[str, list[str]]` data and emit distinct
1036
+ content. The fixed binding methods accept columnar `dict[str, list[str]]` data and emit distinct
1013
1037
  fixed-geometry text overlays per record while sharing static paint. They require exact slot keys,
1014
1038
  equal non-zero column lengths, page-local WinAnsi-compatible text, and an untagged profile; bound
1015
1039
  values do not trigger reflow.
1016
1040
 
1041
+ The reflow binding methods use the same exact-column contract but execute the compiled parsed-DOM
1042
+ program. `{{slot}}` values are literal text and may reshape, wrap, resize blocks, and change page
1043
+ counts. Empty elements can opt into trusted generated markup with
1044
+ `data-fb-bind-html="slot"`; those values replace the element's children. One DOM is reused per
1045
+ native worker and record output is linked in input order through a bounded queue. Structural slot
1046
+ values are parsed as trusted markup: sanitize untrusted input or use scalar `{{slot}}` bindings,
1047
+ which remain literal text. Choose `CompiledFlowCompression.Throughput` (default) or
1048
+ `CompiledFlowCompression.Compact` per render; modes can be mixed safely in one process. See
1049
+ [`docs/python-api.md`](docs/python-api.md) for the trust boundary and current limitations.
1050
+
1017
1051
  When `deterministic_hash` is set, engine writes PDF SHA-256 to the provided file path.
1018
1052
 
1019
1053
  `AssetBundle` methods:
@@ -99,6 +99,26 @@ size-changing records render 200x faster. See
99
99
  [`docs/performance-architecture.md`](docs/performance-architecture.md) for the packed vector IR,
100
100
  typed-binding, virtual-linker, and shader roadmap.
101
101
 
102
+ Fullbleed 2.2.5 hardens the third compiler lane introduced in 2.2.4 for
103
+ size-changing records: `CompiledDocument.render_pdf_reflow_bindings(...)`. It parses and recovers
104
+ the template DOM once and compiles encountered structural flow variants into guarded fixed-point
105
+ display programs. Matching records bind directly into those programs; workers execute pre-shaped
106
+ text/TJ paint slots, cached static PDF page segments, page-local Deflate, and one ordered linker.
107
+ Explicit trusted structural slots use `data-fb-bind-html="slot"`. A value that no compiled variant
108
+ can safely place runs ordinary fixed-point layout once to add another variant.
109
+
110
+ On the independent 1,000-distinct-record case study, the 29-sample hot direct-file median was
111
+ 216.160 ms: **4,626 records/s and 8,096 pages/s**. The 168.995 ms best sample reached **5,917
112
+ records/s and 10,355 pages/s**. It produced
113
+ the exact 1,750-page 500/300/150/50 reflow distribution and verified all 24,900 markers. The
114
+ throughput-tuned 6,870,320-byte PDF was deterministic across all samples. With the compact
115
+ compression setting, compiled output was also byte-for-byte identical to ordinary rendering:
116
+ 5,298,961 bytes and SHA-256 `bb3c441313a08fb00d3bd15f23a567981f3bbbd550908e3a9fe7a07ca5d7f138`.
117
+ This is a hot compiled-variant result, not a claim for previously unseen structure. The first
118
+ variant-discovery render measured 0.509 s (3,437 pages/s), and the fully cold job including setup,
119
+ bindings, compilation, discovery, and write measured 0.588 s (2,975 pages/s). The independently
120
+ observed hot-median speedup over the ordinary path was 43.8x.
121
+
102
122
  ## Install
103
123
 
104
124
  New to Python or setting up on a fresh machine? Start with `docs/install-non-technical.md`.
@@ -110,7 +130,7 @@ python -m pip install fullbleed
110
130
  From a local wheel:
111
131
 
112
132
  ```bash
113
- python -m pip install C:\path\to\fullbleed-2.2.3-cp310-abi3-win_amd64.whl
133
+ python -m pip install C:\path\to\fullbleed-2.2.5-cp310-abi3-win_amd64.whl
114
134
  ```
115
135
 
116
136
  From a source checkout with Rust installed, no Python build package is needed:
@@ -563,7 +583,8 @@ Machine discovery:
563
583
  fullbleed capabilities --json
564
584
  ```
565
585
 
566
- Inspect the `svg` object in `fullbleed.capabilities.v1` for SVG support metadata.
586
+ Inspect the `svg` object in `fullbleed.capabilities.v1` for SVG support metadata. The `engine`
587
+ object also reports compiled-document/reflow availability and supported per-call compression modes.
567
588
  It reports the compiled `svg_raster` build feature plus a feature matrix for
568
589
  native-vector SVG, raster-fallback-required SVG, and unsupported/known-loss SVG
569
590
  features.
@@ -922,6 +943,7 @@ Module exports:
922
943
 
923
944
  - `PdfEngine`
924
945
  - `CompiledDocument`
946
+ - `CompiledFlowCompression`
925
947
  - `AssetBundle`
926
948
  - `Asset`
927
949
  - `AssetKind`
@@ -970,13 +992,25 @@ Module exports:
970
992
  | `render_pdf_batch(copies, deterministic_hash=None)` | `bytes` |
971
993
  | `render_pdf_bindings(bindings, deterministic_hash=None)` | `bytes` |
972
994
  | `render_pdf_bindings_to_file(bindings, path, deterministic_hash=None)` | `int` |
995
+ | `render_pdf_reflow_bindings(bindings, deterministic_hash=None, *, compression="throughput")` | `bytes` |
996
+ | `render_pdf_reflow_bindings_to_file(bindings, path, deterministic_hash=None, *, compression="throughput")` | `int` |
973
997
 
974
998
  The compiled lane runs parsing/layout once. `render_pdf_batch` virtualizes identical untagged page
975
- content. The binding methods instead accept columnar `dict[str, list[str]]` data and emit distinct
999
+ content. The fixed binding methods accept columnar `dict[str, list[str]]` data and emit distinct
976
1000
  fixed-geometry text overlays per record while sharing static paint. They require exact slot keys,
977
1001
  equal non-zero column lengths, page-local WinAnsi-compatible text, and an untagged profile; bound
978
1002
  values do not trigger reflow.
979
1003
 
1004
+ The reflow binding methods use the same exact-column contract but execute the compiled parsed-DOM
1005
+ program. `{{slot}}` values are literal text and may reshape, wrap, resize blocks, and change page
1006
+ counts. Empty elements can opt into trusted generated markup with
1007
+ `data-fb-bind-html="slot"`; those values replace the element's children. One DOM is reused per
1008
+ native worker and record output is linked in input order through a bounded queue. Structural slot
1009
+ values are parsed as trusted markup: sanitize untrusted input or use scalar `{{slot}}` bindings,
1010
+ which remain literal text. Choose `CompiledFlowCompression.Throughput` (default) or
1011
+ `CompiledFlowCompression.Compact` per render; modes can be mixed safely in one process. See
1012
+ [`docs/python-api.md`](docs/python-api.md) for the trust boundary and current limitations.
1013
+
980
1014
  When `deterministic_hash` is set, engine writes PDF SHA-256 to the provided file path.
981
1015
 
982
1016
  `AssetBundle` methods:
@@ -5,7 +5,7 @@ backend-path = ["build_backend"]
5
5
 
6
6
  [project]
7
7
  name = "fullbleed"
8
- version = "2.2.3"
8
+ version = "2.2.5"
9
9
  description = "Deterministic static HTML/CSS-to-PDF renderer built in Rust"
10
10
  readme = "README.md"
11
11
  requires-python = ">=3.10"
@@ -54,6 +54,16 @@ class AssetKind(metaclass=_NativeFacadeType):
54
54
  raise TypeError("No constructor defined for AssetKind")
55
55
 
56
56
 
57
+ class CompiledFlowCompression(metaclass=_NativeFacadeType):
58
+ """Per-call size/speed policy for compiled content-reflow PDF streams."""
59
+
60
+ Throughput = "throughput"
61
+ Compact = "compact"
62
+
63
+ def __new__(cls, /, *args, **kwargs):
64
+ raise TypeError("No constructor defined for CompiledFlowCompression")
65
+
66
+
57
67
  class Asset(metaclass=_NativeFacadeType):
58
68
  __slots__ = ("_handle",)
59
69
 
@@ -161,7 +171,7 @@ class WatermarkSpec(metaclass=_NativeFacadeType):
161
171
 
162
172
 
163
173
  class CompiledDocument(metaclass=_NativeFacadeType):
164
- """Immutable fixed-point display document produced by ``PdfEngine.compile_pdf``."""
174
+ """Compiled fixed-paint and content-reflow programs from ``PdfEngine.compile_pdf``."""
165
175
 
166
176
  __slots__ = ("_handle",)
167
177
 
@@ -219,6 +229,44 @@ class CompiledDocument(metaclass=_NativeFacadeType):
219
229
  deterministic_hash,
220
230
  )
221
231
 
232
+ def render_pdf_reflow_bindings(
233
+ self,
234
+ /,
235
+ bindings,
236
+ deterministic_hash=None,
237
+ *,
238
+ compression=CompiledFlowCompression.Throughput,
239
+ ):
240
+ """Render columnar text bindings with content-driven layout and pagination."""
241
+ _require_string(compression, "compression")
242
+ return _call(
243
+ self._handle,
244
+ "CompiledDocument.render_pdf_reflow_bindings",
245
+ bindings,
246
+ deterministic_hash,
247
+ compression,
248
+ )
249
+
250
+ def render_pdf_reflow_bindings_to_file(
251
+ self,
252
+ /,
253
+ bindings,
254
+ path,
255
+ deterministic_hash=None,
256
+ *,
257
+ compression=CompiledFlowCompression.Throughput,
258
+ ):
259
+ """Stream a compiled content-reflow batch directly to ``path``."""
260
+ _require_string(compression, "compression")
261
+ return _call(
262
+ self._handle,
263
+ "CompiledDocument.render_pdf_reflow_bindings_to_file",
264
+ bindings,
265
+ path,
266
+ deterministic_hash,
267
+ compression,
268
+ )
269
+
222
270
 
223
271
  class PdfEngine(metaclass=_NativeFacadeType):
224
272
  __slots__ = ("_handle",)
@@ -558,7 +606,9 @@ class PdfEngine(metaclass=_NativeFacadeType):
558
606
  )
559
607
 
560
608
  def render_pdf(self, /, html, css, deterministic_hash=None):
561
- return _call(self._handle, "PdfEngine.render_pdf", html, css, deterministic_hash)
609
+ return _call(
610
+ self._handle, "PdfEngine.render_pdf", html, css, deterministic_hash
611
+ )
562
612
 
563
613
  def compile_pdf(self, /, html, css):
564
614
  handle = _call(self._handle, "PdfEngine.compile_pdf", html, css)
@@ -726,7 +776,9 @@ class PdfEngine(metaclass=_NativeFacadeType):
726
776
  deterministic_hash,
727
777
  )
728
778
 
729
- def render_pdf_batch_to_file(self, /, html_list, css, path, deterministic_hash=None):
779
+ def render_pdf_batch_to_file(
780
+ self, /, html_list, css, path, deterministic_hash=None
781
+ ):
730
782
  return _call(
731
783
  self._handle,
732
784
  "PdfEngine.render_pdf_batch_to_file",
@@ -876,6 +928,7 @@ def audit_sparse_page_visual_pair(source_png_path, render_png_path):
876
928
  __all__ = [
877
929
  "PdfEngine",
878
930
  "CompiledDocument",
931
+ "CompiledFlowCompression",
879
932
  "AssetKind",
880
933
  "Asset",
881
934
  "AssetBundle",
@@ -904,6 +957,7 @@ __all__ = [
904
957
  _CLASS_NAMES = {
905
958
  "PdfEngine",
906
959
  "CompiledDocument",
960
+ "CompiledFlowCompression",
907
961
  "AssetKind",
908
962
  "Asset",
909
963
  "AssetBundle",
@@ -2793,6 +2793,8 @@ def cmd_capabilities(args):
2793
2793
  build_features = {}
2794
2794
  svg_raster_available = bool(build_features.get("svg_raster", False))
2795
2795
  pdf_engine = getattr(fullbleed, "PdfEngine", None)
2796
+ compiled_document = getattr(fullbleed, "CompiledDocument", None)
2797
+ compression_modes = list(build_features.get("compiled_flow_compression_modes", []))
2796
2798
 
2797
2799
  commands = [
2798
2800
  "render",
@@ -2828,6 +2830,14 @@ def cmd_capabilities(args):
2828
2830
  "--repro-check",
2829
2831
  ],
2830
2832
  "engine": {
2833
+ "compiled_document": bool(pdf_engine and hasattr(pdf_engine, "compile_pdf")),
2834
+ "compiled_reflow_bindings": bool(
2835
+ build_features.get("compiled_reflow", False)
2836
+ and pdf_engine
2837
+ and compiled_document
2838
+ and hasattr(compiled_document, "render_pdf_reflow_bindings")
2839
+ ),
2840
+ "compiled_flow_compression_modes": compression_modes,
2831
2841
  "batch_render": bool(pdf_engine and hasattr(pdf_engine, "render_pdf_batch")),
2832
2842
  "batch_render_parallel": bool(
2833
2843
  pdf_engine and hasattr(pdf_engine, "render_pdf_batch_parallel")
@@ -3677,5 +3687,3 @@ if __name__ == "__main__":
3677
3687
  raise SystemExit(main())
3678
3688
 
3679
3689
 
3680
-
3681
-
@@ -1,11 +1,15 @@
1
+ use std::cell::RefCell;
1
2
  const ADLER_BASE: u32 = 65_521;
2
3
  const DEFAULT_ADLER_CHUNK: usize = 1 << 20;
4
+ const ADLER_NMAX: usize = 5_552;
3
5
 
4
6
  const LZ77_CHUNK_BYTES: usize = 128 * 1024;
5
7
  const MIN_MATCH: usize = 3;
6
8
  const MAX_MATCH: usize = 258;
7
9
  const MAX_DISTANCE: usize = 32 * 1024;
8
10
  const MAX_CHAIN_STEPS: usize = 64;
11
+ const DEFAULT_COMPILED_FLOW_CHAIN_STEPS: usize = 4;
12
+ const COMPILED_FLOW_THROUGHPUT_MIN_BYTES: usize = 4 * 1024;
9
13
  const HASH_BITS: usize = 15;
10
14
  const HASH_SIZE: usize = 1 << HASH_BITS;
11
15
 
@@ -41,19 +45,19 @@ impl AdlerPartial {
41
45
  }
42
46
 
43
47
  fn for_bytes(data: &[u8]) -> Self {
44
- let mut a: u32 = 1;
45
- let mut b: u32 = 0;
46
- for &byte in data {
47
- a += byte as u32;
48
- if a >= ADLER_BASE {
49
- a -= ADLER_BASE;
48
+ let mut a = 1_u64;
49
+ let mut b = 0_u64;
50
+ for chunk in data.chunks(ADLER_NMAX) {
51
+ for &byte in chunk {
52
+ a += u64::from(byte);
53
+ b += a;
50
54
  }
51
- b += a;
52
- b %= ADLER_BASE;
55
+ a %= u64::from(ADLER_BASE);
56
+ b %= u64::from(ADLER_BASE);
53
57
  }
54
58
  Self {
55
- a,
56
- b,
59
+ a: a as u32,
60
+ b: b as u32,
57
61
  len: data.len(),
58
62
  }
59
63
  }
@@ -166,87 +170,155 @@ fn hash3(data: &[u8], i: usize) -> usize {
166
170
 
167
171
  fn match_len(data: &[u8], a: usize, b: usize, max_len: usize) -> usize {
168
172
  let mut l = 0usize;
173
+ while l + 8 <= max_len {
174
+ let left = u64::from_le_bytes(
175
+ data[a + l..a + l + 8]
176
+ .try_into()
177
+ .expect("eight-byte match window"),
178
+ );
179
+ let right = u64::from_le_bytes(
180
+ data[b + l..b + l + 8]
181
+ .try_into()
182
+ .expect("eight-byte match window"),
183
+ );
184
+ let difference = left ^ right;
185
+ if difference != 0 {
186
+ return l + (difference.trailing_zeros() as usize >> 3);
187
+ }
188
+ l += 8;
189
+ }
169
190
  while l < max_len && data[a + l] == data[b + l] {
170
191
  l += 1;
171
192
  }
172
193
  l
173
194
  }
174
195
 
175
- fn plan_lz77_chunk(data: &[u8]) -> ChunkPlan {
176
- let n = data.len();
177
- if n == 0 {
178
- return ChunkPlan { tokens: Vec::new() };
196
+ struct Lz77Workspace {
197
+ head: Vec<i32>,
198
+ head_epoch: Vec<u32>,
199
+ prev: Vec<i32>,
200
+ epoch: u32,
201
+ }
202
+
203
+ impl Lz77Workspace {
204
+ fn new() -> Self {
205
+ Self {
206
+ head: vec![-1; HASH_SIZE],
207
+ head_epoch: vec![0; HASH_SIZE],
208
+ prev: Vec::new(),
209
+ epoch: 0,
210
+ }
179
211
  }
180
212
 
181
- let mut head = vec![-1_i32; HASH_SIZE];
182
- let mut prev = vec![-1_i32; n];
183
- let mut tokens = Vec::with_capacity(n / 2);
213
+ fn begin_page(&mut self, len: usize) -> u32 {
214
+ self.epoch = self.epoch.wrapping_add(1);
215
+ if self.epoch == 0 {
216
+ self.head_epoch.fill(0);
217
+ self.epoch = 1;
218
+ }
219
+ if self.prev.len() < len {
220
+ self.prev.resize(len, -1);
221
+ }
222
+ self.epoch
223
+ }
184
224
 
185
- let mut i = 0usize;
186
- while i < n {
187
- if i + MIN_MATCH > n {
188
- tokens.push(Token::Literal(data[i]));
189
- i += 1;
190
- continue;
225
+ fn plan(&mut self, data: &[u8], max_chain_steps: usize) -> ChunkPlan {
226
+ let n = data.len();
227
+ if n == 0 {
228
+ return ChunkPlan { tokens: Vec::new() };
191
229
  }
230
+ let epoch = self.begin_page(n);
231
+ let mut tokens = Vec::with_capacity(n / 2);
232
+
233
+ let mut i = 0usize;
234
+ while i < n {
235
+ if i + MIN_MATCH > n {
236
+ tokens.push(Token::Literal(data[i]));
237
+ i += 1;
238
+ continue;
239
+ }
192
240
 
193
- let h = hash3(data, i);
194
- let mut cand = head[h];
195
- prev[i] = cand;
196
- head[h] = i as i32;
241
+ let h = hash3(data, i);
242
+ let mut cand = if self.head_epoch[h] == epoch {
243
+ self.head[h]
244
+ } else {
245
+ -1
246
+ };
247
+ self.prev[i] = cand;
248
+ self.head[h] = i as i32;
249
+ self.head_epoch[h] = epoch;
250
+
251
+ let mut best_len = 0usize;
252
+ let mut best_dist = 0usize;
253
+ let mut steps = 0usize;
254
+
255
+ while cand >= 0 && steps < max_chain_steps {
256
+ let c = cand as usize;
257
+ let dist = i - c;
258
+ if dist > MAX_DISTANCE {
259
+ break;
260
+ }
197
261
 
198
- let mut best_len = 0usize;
199
- let mut best_dist = 0usize;
200
- let mut steps = 0usize;
262
+ if data[c] == data[i] && data[c + 1] == data[i + 1] && data[c + 2] == data[i + 2] {
263
+ let max_len = MAX_MATCH.min(n - i);
264
+ let len = match_len(data, c, i, max_len);
265
+ if len >= MIN_MATCH && (len > best_len || (len == best_len && dist < best_dist))
266
+ {
267
+ best_len = len;
268
+ best_dist = dist;
269
+ if best_len == MAX_MATCH {
270
+ break;
271
+ }
272
+ }
273
+ }
201
274
 
202
- while cand >= 0 && steps < MAX_CHAIN_STEPS {
203
- let c = cand as usize;
204
- let dist = i - c;
205
- if dist > MAX_DISTANCE {
206
- break;
275
+ cand = self.prev[c];
276
+ steps += 1;
207
277
  }
208
278
 
209
- if data[c] == data[i] && data[c + 1] == data[i + 1] && data[c + 2] == data[i + 2] {
210
- let max_len = MAX_MATCH.min(n - i);
211
- let len = match_len(data, c, i, max_len);
212
- if len >= MIN_MATCH && (len > best_len || (len == best_len && dist < best_dist)) {
213
- best_len = len;
214
- best_dist = dist;
215
- if best_len == MAX_MATCH {
216
- break;
279
+ if best_len >= MIN_MATCH {
280
+ tokens.push(Token::Match {
281
+ len: best_len as u16,
282
+ dist: best_dist as u16,
283
+ });
284
+
285
+ let end = (i + best_len).min(n);
286
+ let mut j = i + 1;
287
+ while j < end {
288
+ if j + MIN_MATCH <= n {
289
+ let hj = hash3(data, j);
290
+ self.prev[j] = if self.head_epoch[hj] == epoch {
291
+ self.head[hj]
292
+ } else {
293
+ -1
294
+ };
295
+ self.head[hj] = j as i32;
296
+ self.head_epoch[hj] = epoch;
217
297
  }
298
+ j += 1;
218
299
  }
219
- }
220
300
 
221
- cand = prev[c];
222
- steps += 1;
223
- }
224
-
225
- if best_len >= MIN_MATCH {
226
- tokens.push(Token::Match {
227
- len: best_len as u16,
228
- dist: best_dist as u16,
229
- });
230
-
231
- let end = (i + best_len).min(n);
232
- let mut j = i + 1;
233
- while j < end {
234
- if j + MIN_MATCH <= n {
235
- let hj = hash3(data, j);
236
- prev[j] = head[hj];
237
- head[hj] = j as i32;
238
- }
239
- j += 1;
301
+ i += best_len;
302
+ } else {
303
+ tokens.push(Token::Literal(data[i]));
304
+ i += 1;
240
305
  }
241
-
242
- i += best_len;
243
- } else {
244
- tokens.push(Token::Literal(data[i]));
245
- i += 1;
246
306
  }
307
+
308
+ ChunkPlan { tokens }
247
309
  }
310
+ }
248
311
 
249
- ChunkPlan { tokens }
312
+ thread_local! {
313
+ static LZ77_WORKSPACE: RefCell<Lz77Workspace> = RefCell::new(Lz77Workspace::new());
314
+ }
315
+
316
+ fn plan_lz77_chunk_with_chain(data: &[u8], max_chain_steps: usize) -> ChunkPlan {
317
+ LZ77_WORKSPACE.with(|workspace| {
318
+ workspace
319
+ .borrow_mut()
320
+ .plan(data, max_chain_steps.clamp(1, MAX_CHAIN_STEPS))
321
+ })
250
322
  }
251
323
 
252
324
  fn reverse_bits(mut value: u16, len: u8) -> u16 {
@@ -348,10 +420,31 @@ fn estimate_deflate_capacity(input_len: usize) -> usize {
348
420
  }
349
421
 
350
422
  pub(crate) fn zlib_deflate_parallel(data: &[u8]) -> Vec<u8> {
423
+ zlib_deflate_with_chain(data, MAX_CHAIN_STEPS)
424
+ }
425
+
426
+ pub(crate) fn zlib_deflate_compiled_flow(
427
+ data: &[u8],
428
+ compression: crate::pdf::CompiledFlowCompression,
429
+ ) -> Vec<u8> {
430
+ let steps = match compression {
431
+ crate::pdf::CompiledFlowCompression::Throughput => DEFAULT_COMPILED_FLOW_CHAIN_STEPS,
432
+ crate::pdf::CompiledFlowCompression::Compact => MAX_CHAIN_STEPS,
433
+ };
434
+ let steps = if data.len() < COMPILED_FLOW_THROUGHPUT_MIN_BYTES {
435
+ MAX_CHAIN_STEPS
436
+ } else {
437
+ steps
438
+ };
439
+ zlib_deflate_with_chain(data, steps)
440
+ }
441
+
442
+ fn zlib_deflate_with_chain(data: &[u8], max_chain_steps: usize) -> Vec<u8> {
351
443
  let ranges = chunk_ranges(data.len(), LZ77_CHUNK_BYTES);
352
444
 
353
- let plans =
354
- crate::parallel::map_ordered(&ranges, |(start, end)| plan_lz77_chunk(&data[*start..*end]));
445
+ let plans = crate::parallel::map_ordered(&ranges, |(start, end)| {
446
+ plan_lz77_chunk_with_chain(&data[*start..*end], max_chain_steps)
447
+ });
355
448
 
356
449
  let adler = adler32_parallel(data, DEFAULT_ADLER_CHUNK);
357
450
 
@@ -812,6 +905,47 @@ mod tests {
812
905
  );
813
906
  }
814
907
 
908
+ #[test]
909
+ fn zlib_compiled_flow_chain_levels_roundtrip_deterministically() {
910
+ let src = b"BT /F1 10 Tf 72 720 Td [(REFLOW-000001)] TJ ET\n".repeat(2_000);
911
+ for steps in [1, 4, 8, 16, 32, 64] {
912
+ let a = zlib_deflate_with_chain(&src, steps);
913
+ let b = zlib_deflate_with_chain(&src, steps);
914
+ assert_eq!(a, b, "deterministic at {steps} chain steps");
915
+ assert_eq!(
916
+ zlib_inflate(&a, src.len()).expect("native inflate"),
917
+ src,
918
+ "roundtrip at {steps} chain steps",
919
+ );
920
+ }
921
+ }
922
+
923
+ #[test]
924
+ fn compiled_flow_compression_modes_are_per_call_and_deterministic() {
925
+ use crate::pdf::CompiledFlowCompression::{Compact, Throughput};
926
+
927
+ let src = (0..8_000)
928
+ .map(|index| format!("BT /F1 10 Tf 72 720 Td [(RECORD-{index:05})] TJ ET\n"))
929
+ .collect::<String>()
930
+ .into_bytes();
931
+ let throughput = zlib_deflate_compiled_flow(&src, Throughput);
932
+ let compact = zlib_deflate_compiled_flow(&src, Compact);
933
+ assert_eq!(throughput, zlib_deflate_compiled_flow(&src, Throughput));
934
+ assert_eq!(compact, zlib_deflate_compiled_flow(&src, Compact));
935
+ assert_eq!(
936
+ zlib_inflate(&throughput, src.len()).expect("throughput roundtrip"),
937
+ src
938
+ );
939
+ assert_eq!(
940
+ zlib_inflate(&compact, src.len()).expect("compact roundtrip"),
941
+ src
942
+ );
943
+ assert!(
944
+ compact.len() <= throughput.len(),
945
+ "compact search should not produce a larger representative stream"
946
+ );
947
+ }
948
+
815
949
  #[test]
816
950
  fn zlib_lz77_roundtrip_empty() {
817
951
  let src: Vec<u8> = Vec::new();