pdfrb 0.2.1 → 0.6.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.
Files changed (406) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +24 -0
  4. data/Gemfile +1 -0
  5. data/PROPOSAL.canvas-clip.md +92 -0
  6. data/PROPOSAL.canvas-draw-image-matrix.md +87 -0
  7. data/PROPOSAL.canvas-rich-text.md +103 -0
  8. data/PROPOSAL.canvas-text-lines.md +77 -0
  9. data/PROPOSAL.canvas-transparency.md +106 -0
  10. data/PROPOSAL.external-font-metrics.md +129 -0
  11. data/PROPOSAL.font-embedding-fontfile2.md +88 -0
  12. data/PROPOSAL.font-resolver-ttc.md +105 -0
  13. data/PROPOSAL.font-resolver.md +96 -0
  14. data/PROPOSAL.font-subsetting.md +98 -0
  15. data/PROPOSAL.gradient-shadings.md +110 -0
  16. data/PROPOSAL.page-mediabox-setter.md +66 -0
  17. data/PROPOSAL.per-glyph-width.md +91 -0
  18. data/PROPOSAL.stringio-require.md +53 -0
  19. data/PROPOSAL.struct-tree-root.md +102 -0
  20. data/PROPOSAL.text-measurement.md +93 -0
  21. data/PROPOSAL.ttf-table-parsing-fix.md +112 -0
  22. data/RELEASE.md +48 -0
  23. data/lib/pdfrb/action/base.rb +21 -0
  24. data/lib/pdfrb/action/types.rb +178 -0
  25. data/lib/pdfrb/action.rb +55 -0
  26. data/lib/pdfrb/annotation/base.rb +79 -0
  27. data/lib/pdfrb/annotation/types.rb +222 -0
  28. data/lib/pdfrb/annotation.rb +78 -0
  29. data/lib/pdfrb/appearance/generator.rb +216 -0
  30. data/lib/pdfrb/appearance.rb +11 -0
  31. data/lib/pdfrb/arlington/object_definition.rb +2 -2
  32. data/lib/pdfrb/arlington/predicate/evaluator.rb +1 -1
  33. data/lib/pdfrb/arlington/predicate/functions.rb +25 -9
  34. data/lib/pdfrb/arlington/predicate.rb +1 -5
  35. data/lib/pdfrb/cli/batch.rb +18 -0
  36. data/lib/pdfrb/cli/debug_info.rb +18 -0
  37. data/lib/pdfrb/cli/files.rb +16 -0
  38. data/lib/pdfrb/cli/fonts.rb +16 -0
  39. data/lib/pdfrb/cli/form.rb +18 -0
  40. data/lib/pdfrb/cli/image2pdf.rb +18 -0
  41. data/lib/pdfrb/cli/images.rb +34 -0
  42. data/lib/pdfrb/cli/info.rb +20 -0
  43. data/lib/pdfrb/cli/inspect.rb +18 -0
  44. data/lib/pdfrb/cli/merge.rb +18 -0
  45. data/lib/pdfrb/cli/modify.rb +18 -0
  46. data/lib/pdfrb/cli/optimize.rb +18 -0
  47. data/lib/pdfrb/cli/split.rb +21 -0
  48. data/lib/pdfrb/cli/watermark.rb +18 -0
  49. data/lib/pdfrb/cli.rb +312 -7
  50. data/lib/pdfrb/color/color_space.rb +259 -0
  51. data/lib/pdfrb/color/icc_profile.rb +90 -0
  52. data/lib/pdfrb/color.rb +12 -0
  53. data/lib/pdfrb/compare/comparator.rb +244 -0
  54. data/lib/pdfrb/compare/report.rb +78 -0
  55. data/lib/pdfrb/compare.rb +42 -0
  56. data/lib/pdfrb/composer.rb +161 -0
  57. data/lib/pdfrb/conformance/pdf_a.rb +317 -56
  58. data/lib/pdfrb/conformance/pdf_ua.rb +186 -29
  59. data/lib/pdfrb/conformance/pdf_x.rb +247 -0
  60. data/lib/pdfrb/conformance/rule.rb +78 -0
  61. data/lib/pdfrb/conformance/structure_elements.rb +174 -0
  62. data/lib/pdfrb/conformance/verapdf_bridge.rb +92 -0
  63. data/lib/pdfrb/conformance.rb +7 -3
  64. data/lib/pdfrb/content/canvas.rb +217 -72
  65. data/lib/pdfrb/content/color_space.rb +157 -0
  66. data/lib/pdfrb/content/hidden_text_detector.rb +167 -0
  67. data/lib/pdfrb/content/operator.rb +21 -1
  68. data/lib/pdfrb/content/operators/clipping.rb +36 -0
  69. data/lib/pdfrb/content/operators/graphics_state_params.rb +15 -0
  70. data/lib/pdfrb/content/operators.rb +21 -0
  71. data/lib/pdfrb/content/shading/base.rb +139 -0
  72. data/lib/pdfrb/content/shading.rb +17 -0
  73. data/lib/pdfrb/content/smart_text_extractor.rb +85 -0
  74. data/lib/pdfrb/content/tiling_pattern.rb +78 -0
  75. data/lib/pdfrb/content/transformation_matrix.rb +61 -0
  76. data/lib/pdfrb/content.rb +7 -26
  77. data/lib/pdfrb/destination/fit.rb +185 -0
  78. data/lib/pdfrb/destination.rb +13 -0
  79. data/lib/pdfrb/digital_signature/cms_handler.rb +22 -0
  80. data/lib/pdfrb/digital_signature/signing.rb +107 -11
  81. data/lib/pdfrb/digital_signature/timestamp_handler.rb +45 -0
  82. data/lib/pdfrb/digital_signature/verification.rb +88 -9
  83. data/lib/pdfrb/digital_signature/verification_result.rb +14 -0
  84. data/lib/pdfrb/digital_signature.rb +26 -2
  85. data/lib/pdfrb/document/annotations.rb +120 -3
  86. data/lib/pdfrb/document/associated_files.rb +104 -0
  87. data/lib/pdfrb/document/colors.rb +95 -0
  88. data/lib/pdfrb/document/destinations.rb +127 -5
  89. data/lib/pdfrb/document/display.rb +84 -0
  90. data/lib/pdfrb/document/fonts.rb +417 -45
  91. data/lib/pdfrb/document/form.rb +338 -0
  92. data/lib/pdfrb/document/form_xobject.rb +68 -0
  93. data/lib/pdfrb/document/graphics_state.rb +91 -0
  94. data/lib/pdfrb/document/images.rb +4 -4
  95. data/lib/pdfrb/document/info.rb +102 -0
  96. data/lib/pdfrb/document/layers.rb +62 -0
  97. data/lib/pdfrb/document/metadata.rb +12 -5
  98. data/lib/pdfrb/document/output_intents.rb +91 -0
  99. data/lib/pdfrb/document/page_labels.rb +80 -0
  100. data/lib/pdfrb/document/pages.rb +68 -19
  101. data/lib/pdfrb/document/portfolio.rb +119 -0
  102. data/lib/pdfrb/document/shadings.rb +159 -0
  103. data/lib/pdfrb/document/stamps.rb +68 -0
  104. data/lib/pdfrb/document/structure.rb +242 -0
  105. data/lib/pdfrb/document.rb +73 -4
  106. data/lib/pdfrb/encryption/identity.rb +13 -0
  107. data/lib/pdfrb/encryption/password_verification.rb +53 -8
  108. data/lib/pdfrb/encryption/security_handler.rb +8 -5
  109. data/lib/pdfrb/encryption/standard_security_handler.rb +154 -81
  110. data/lib/pdfrb/encryption.rb +17 -0
  111. data/lib/pdfrb/error.rb +2 -2
  112. data/lib/pdfrb/filter/brotli_decode.rb +32 -0
  113. data/lib/pdfrb/filter/ccitt_fax_decode.rb +12 -0
  114. data/lib/pdfrb/filter/crypt.rb +2 -2
  115. data/lib/pdfrb/filter/dct_decode.rb +18 -0
  116. data/lib/pdfrb/filter/flate_decode.rb +4 -18
  117. data/lib/pdfrb/filter/jbig2_decode.rb +12 -0
  118. data/lib/pdfrb/filter/jpx_decode.rb +18 -0
  119. data/lib/pdfrb/filter/lzw_decode.rb +3 -2
  120. data/lib/pdfrb/filter/png_predictor.rb +94 -0
  121. data/lib/pdfrb/filter.rb +26 -13
  122. data/lib/pdfrb/font/afm_parser.rb +50 -26
  123. data/lib/pdfrb/font/cmap/writer.rb +9 -0
  124. data/lib/pdfrb/font/encoding/win_ansi_encoding.rb +17 -0
  125. data/lib/pdfrb/font/encoding.rb +32 -3
  126. data/lib/pdfrb/font/metrics_helper.rb +121 -0
  127. data/lib/pdfrb/font/true_type/file.rb +10 -1
  128. data/lib/pdfrb/font/true_type/glyf.rb +82 -0
  129. data/lib/pdfrb/font/true_type/kern.rb +58 -0
  130. data/lib/pdfrb/font/true_type/loca.rb +42 -0
  131. data/lib/pdfrb/font/true_type/maxp.rb +44 -0
  132. data/lib/pdfrb/font/true_type/name.rb +53 -0
  133. data/lib/pdfrb/font/true_type/post.rb +93 -0
  134. data/lib/pdfrb/font/true_type/subsetter.rb +3 -5
  135. data/lib/pdfrb/font/true_type/wrapper.rb +102 -0
  136. data/lib/pdfrb/font/true_type.rb +7 -0
  137. data/lib/pdfrb/font.rb +2 -0
  138. data/lib/pdfrb/font_loader/from_configuration.rb +19 -0
  139. data/lib/pdfrb/font_loader/from_file.rb +20 -0
  140. data/lib/pdfrb/font_loader/standard14.rb +20 -0
  141. data/lib/pdfrb/font_loader/variant_from_name.rb +36 -0
  142. data/lib/pdfrb/font_loader.rb +4 -0
  143. data/lib/pdfrb/font_resolver.rb +138 -0
  144. data/lib/pdfrb/image/audit.rb +94 -0
  145. data/lib/pdfrb/image/downsampler.rb +124 -0
  146. data/lib/pdfrb/image.rb +13 -0
  147. data/lib/pdfrb/image_loader/jpeg.rb +39 -58
  148. data/lib/pdfrb/image_loader/png.rb +31 -257
  149. data/lib/pdfrb/layout/bidi.rb +198 -0
  150. data/lib/pdfrb/layout/box.rb +49 -0
  151. data/lib/pdfrb/layout/box_fitter.rb +54 -0
  152. data/lib/pdfrb/layout/column_box.rb +71 -0
  153. data/lib/pdfrb/layout/container_box.rb +45 -0
  154. data/lib/pdfrb/layout/frame.rb +67 -0
  155. data/lib/pdfrb/layout/image_box.rb +44 -0
  156. data/lib/pdfrb/layout/inline_box.rb +25 -0
  157. data/lib/pdfrb/layout/line.rb +55 -0
  158. data/lib/pdfrb/layout/list_box.rb +87 -0
  159. data/lib/pdfrb/layout/numeric_refinements.rb +43 -0
  160. data/lib/pdfrb/layout/page_style.rb +65 -0
  161. data/lib/pdfrb/layout/style.rb +62 -0
  162. data/lib/pdfrb/layout/table_box.rb +66 -0
  163. data/lib/pdfrb/layout/text_box.rb +38 -0
  164. data/lib/pdfrb/layout/text_fragment.rb +45 -0
  165. data/lib/pdfrb/layout/text_layouter.rb +123 -0
  166. data/lib/pdfrb/layout.rb +28 -0
  167. data/lib/pdfrb/linearization/hint_stream.rb +93 -0
  168. data/lib/pdfrb/linearization/writer.rb +259 -0
  169. data/lib/pdfrb/linearization.rb +21 -0
  170. data/lib/pdfrb/model/cos/dictionary.rb +12 -0
  171. data/lib/pdfrb/model/pdf_array.rb +2 -2
  172. data/lib/pdfrb/model/type/action.rb +42 -0
  173. data/lib/pdfrb/model/type/action_go_to_3d_view.rb +14 -0
  174. data/lib/pdfrb/model/type/action_go_to_dp.rb +16 -0
  175. data/lib/pdfrb/model/type/action_go_to_r.rb +26 -0
  176. data/lib/pdfrb/model/type/action_goto.rb +34 -0
  177. data/lib/pdfrb/model/type/action_hide.rb +26 -0
  178. data/lib/pdfrb/model/type/action_import_data.rb +18 -0
  179. data/lib/pdfrb/model/type/action_java_script.rb +18 -0
  180. data/lib/pdfrb/model/type/action_launch.rb +22 -0
  181. data/lib/pdfrb/model/type/action_movie.rb +22 -0
  182. data/lib/pdfrb/model/type/action_named.rb +23 -0
  183. data/lib/pdfrb/model/type/action_nop.rb +12 -0
  184. data/lib/pdfrb/model/type/action_rendition.rb +28 -0
  185. data/lib/pdfrb/model/type/action_reset_form.rb +22 -0
  186. data/lib/pdfrb/model/type/action_rich_media_execute.rb +17 -0
  187. data/lib/pdfrb/model/type/action_set_ocg_state.rb +32 -0
  188. data/lib/pdfrb/model/type/action_set_state.rb +14 -0
  189. data/lib/pdfrb/model/type/action_sound_action.rb +23 -0
  190. data/lib/pdfrb/model/type/action_submit_form.rb +64 -0
  191. data/lib/pdfrb/model/type/action_thread.rb +23 -0
  192. data/lib/pdfrb/model/type/action_trans.rb +30 -0
  193. data/lib/pdfrb/model/type/action_uri.rb +30 -0
  194. data/lib/pdfrb/model/type/af_embedded_file.rb +11 -0
  195. data/lib/pdfrb/model/type/af_file_specification.rb +34 -0
  196. data/lib/pdfrb/model/type/annotation.rb +49 -10
  197. data/lib/pdfrb/model/type/appearance.rb +41 -0
  198. data/lib/pdfrb/model/type/appearance_characteristics.rb +36 -0
  199. data/lib/pdfrb/model/type/appearance_generator.rb +22 -0
  200. data/lib/pdfrb/model/type/appearance_trap_net.rb +84 -0
  201. data/lib/pdfrb/model/type/border_effect.rb +25 -0
  202. data/lib/pdfrb/model/type/border_styling.rb +38 -0
  203. data/lib/pdfrb/model/type/box_color_info.rb +21 -0
  204. data/lib/pdfrb/model/type/box_style.rb +19 -0
  205. data/lib/pdfrb/model/type/button.rb +17 -0
  206. data/lib/pdfrb/model/type/cal_gray.rb +17 -0
  207. data/lib/pdfrb/model/type/cal_rgb.rb +22 -0
  208. data/lib/pdfrb/model/type/caret_annotation.rb +21 -0
  209. data/lib/pdfrb/model/type/catalog.rb +99 -16
  210. data/lib/pdfrb/model/type/choice.rb +21 -0
  211. data/lib/pdfrb/model/type/cid_font.rb +52 -1
  212. data/lib/pdfrb/model/type/cid_font_descriptor_metrics.rb +28 -0
  213. data/lib/pdfrb/model/type/cid_system_info.rb +28 -0
  214. data/lib/pdfrb/model/type/circle_annotation.rb +20 -0
  215. data/lib/pdfrb/model/type/collection.rb +27 -0
  216. data/lib/pdfrb/model/type/collection_field.rb +24 -0
  217. data/lib/pdfrb/model/type/collection_item.rb +19 -0
  218. data/lib/pdfrb/model/type/collection_schema.rb +25 -0
  219. data/lib/pdfrb/model/type/collection_sort.rb +13 -0
  220. data/lib/pdfrb/model/type/crypt_filter.rb +58 -0
  221. data/lib/pdfrb/model/type/device_n.rb +37 -0
  222. data/lib/pdfrb/model/type/device_n_mixing_hints.rb +17 -0
  223. data/lib/pdfrb/model/type/device_n_process.rb +13 -0
  224. data/lib/pdfrb/model/type/doc_mdp_transform_parameters.rb +21 -0
  225. data/lib/pdfrb/model/type/document_security_store.rb +55 -0
  226. data/lib/pdfrb/model/type/embedded_file.rb +19 -0
  227. data/lib/pdfrb/model/type/embedded_file_parameter.rb +48 -0
  228. data/lib/pdfrb/model/type/encryption_public_key.rb +38 -0
  229. data/lib/pdfrb/model/type/encryption_standard.rb +68 -0
  230. data/lib/pdfrb/model/type/field_mdp_transform_parameters.rb +26 -0
  231. data/lib/pdfrb/model/type/file_attachment_annotation.rb +29 -0
  232. data/lib/pdfrb/model/type/file_spec_ef.rb +22 -0
  233. data/lib/pdfrb/model/type/file_spec_rf.rb +21 -0
  234. data/lib/pdfrb/model/type/file_specification.rb +43 -0
  235. data/lib/pdfrb/model/type/file_trailer.rb +54 -0
  236. data/lib/pdfrb/model/type/font.rb +73 -2
  237. data/lib/pdfrb/model/type/font_cid_type0.rb +17 -0
  238. data/lib/pdfrb/model/type/font_cid_type2.rb +17 -0
  239. data/lib/pdfrb/model/type/font_descriptor.rb +47 -0
  240. data/lib/pdfrb/model/type/font_descriptor_cid_type0.rb +15 -0
  241. data/lib/pdfrb/model/type/font_descriptor_cid_type2.rb +15 -0
  242. data/lib/pdfrb/model/type/font_encoding.rb +47 -0
  243. data/lib/pdfrb/model/type/font_file.rb +28 -0
  244. data/lib/pdfrb/model/type/font_file2.rb +32 -0
  245. data/lib/pdfrb/model/type/font_file3.rb +23 -0
  246. data/lib/pdfrb/model/type/font_multiple_master.rb +60 -0
  247. data/lib/pdfrb/model/type/font_true_type.rb +1 -1
  248. data/lib/pdfrb/model/type/font_type0.rb +1 -1
  249. data/lib/pdfrb/model/type/font_type1.rb +1 -1
  250. data/lib/pdfrb/model/type/font_type3.rb +1 -1
  251. data/lib/pdfrb/model/type/form_field.rb +73 -0
  252. data/lib/pdfrb/model/type/free_text_annotation.rb +37 -0
  253. data/lib/pdfrb/model/type/function.rb +42 -0
  254. data/lib/pdfrb/model/type/function_exponential.rb +31 -0
  255. data/lib/pdfrb/model/type/function_postscript.rb +25 -0
  256. data/lib/pdfrb/model/type/function_sampled.rb +55 -0
  257. data/lib/pdfrb/model/type/function_stitching.rb +33 -0
  258. data/lib/pdfrb/model/type/generic_appearance.rb +40 -0
  259. data/lib/pdfrb/model/type/graphics_state_parameter.rb +69 -0
  260. data/lib/pdfrb/model/type/halftone.rb +21 -0
  261. data/lib/pdfrb/model/type/halftone_type1.rb +17 -0
  262. data/lib/pdfrb/model/type/halftone_type10.rb +16 -0
  263. data/lib/pdfrb/model/type/halftone_type16.rb +11 -0
  264. data/lib/pdfrb/model/type/halftone_type5.rb +24 -0
  265. data/lib/pdfrb/model/type/halftone_type6.rb +16 -0
  266. data/lib/pdfrb/model/type/highlight_annotation.rb +11 -0
  267. data/lib/pdfrb/model/type/icc_based_color_space.rb +24 -0
  268. data/lib/pdfrb/model/type/icon_fit.rb +31 -0
  269. data/lib/pdfrb/model/type/indexed.rb +32 -0
  270. data/lib/pdfrb/model/type/info.rb +44 -0
  271. data/lib/pdfrb/model/type/ink_annotation.rb +29 -0
  272. data/lib/pdfrb/model/type/interactive_form.rb +36 -0
  273. data/lib/pdfrb/model/type/interior_color.rb +12 -0
  274. data/lib/pdfrb/model/type/jbig2_globals.rb +15 -0
  275. data/lib/pdfrb/model/type/lab.rb +27 -0
  276. data/lib/pdfrb/model/type/line_annotation.rb +44 -0
  277. data/lib/pdfrb/model/type/line_ending_styling.rb +39 -0
  278. data/lib/pdfrb/model/type/link_annotation.rb +41 -0
  279. data/lib/pdfrb/model/type/link_appearance.rb +9 -0
  280. data/lib/pdfrb/model/type/mark_information.rb +23 -0
  281. data/lib/pdfrb/model/type/marked_content_reference.rb +24 -0
  282. data/lib/pdfrb/model/type/markup_annotation.rb +36 -0
  283. data/lib/pdfrb/model/type/mdp_dict.rb +28 -0
  284. data/lib/pdfrb/model/type/measure.rb +39 -0
  285. data/lib/pdfrb/model/type/media_clip.rb +28 -0
  286. data/lib/pdfrb/model/type/media_offset.rb +106 -0
  287. data/lib/pdfrb/model/type/metadata.rb +27 -3
  288. data/lib/pdfrb/model/type/misc_helpers.rb +75 -0
  289. data/lib/pdfrb/model/type/movie.rb +28 -0
  290. data/lib/pdfrb/model/type/names.rb +51 -0
  291. data/lib/pdfrb/model/type/namespace.rb +27 -0
  292. data/lib/pdfrb/model/type/object_reference.rb +31 -0
  293. data/lib/pdfrb/model/type/object_stream.rb +38 -0
  294. data/lib/pdfrb/model/type/opt_content_ext.rb +94 -0
  295. data/lib/pdfrb/model/type/opt_content_page_element.rb +18 -0
  296. data/lib/pdfrb/model/type/optional_content_config.rb +56 -0
  297. data/lib/pdfrb/model/type/optional_content_group.rb +35 -10
  298. data/lib/pdfrb/model/type/optional_content_membership.rb +36 -2
  299. data/lib/pdfrb/model/type/optional_content_properties.rb +16 -1
  300. data/lib/pdfrb/model/type/outline.rb +40 -6
  301. data/lib/pdfrb/model/type/outline_item.rb +45 -0
  302. data/lib/pdfrb/model/type/output_intent.rb +30 -0
  303. data/lib/pdfrb/model/type/page.rb +127 -0
  304. data/lib/pdfrb/model/type/page_label.rb +68 -0
  305. data/lib/pdfrb/model/type/page_tree_node.rb +51 -16
  306. data/lib/pdfrb/model/type/page_tree_node_root.rb +15 -0
  307. data/lib/pdfrb/model/type/pattern.rb +16 -0
  308. data/lib/pdfrb/model/type/pattern_shading.rb +25 -0
  309. data/lib/pdfrb/model/type/pattern_tiling.rb +25 -0
  310. data/lib/pdfrb/model/type/polygon_annotation.rb +29 -0
  311. data/lib/pdfrb/model/type/polygon_polyline.rb +14 -0
  312. data/lib/pdfrb/model/type/polyline_annotation.rb +29 -0
  313. data/lib/pdfrb/model/type/popup_annotation.rb +20 -0
  314. data/lib/pdfrb/model/type/printer_mark_annotation.rb +13 -0
  315. data/lib/pdfrb/model/type/redact_annotation.rb +28 -0
  316. data/lib/pdfrb/model/type/rendition.rb +31 -0
  317. data/lib/pdfrb/model/type/resources.rb +59 -14
  318. data/lib/pdfrb/model/type/rich_media_activation.rb +24 -0
  319. data/lib/pdfrb/model/type/rich_media_animation.rb +25 -0
  320. data/lib/pdfrb/model/type/rich_media_annotation.rb +18 -0
  321. data/lib/pdfrb/model/type/rich_media_command.rb +19 -0
  322. data/lib/pdfrb/model/type/rich_media_configuration.rb +32 -0
  323. data/lib/pdfrb/model/type/rich_media_content.rb +28 -0
  324. data/lib/pdfrb/model/type/rich_media_cue_point.rb +18 -0
  325. data/lib/pdfrb/model/type/rich_media_deactivation.rb +17 -0
  326. data/lib/pdfrb/model/type/rich_media_instance.rb +33 -0
  327. data/lib/pdfrb/model/type/rich_media_presentation.rb +29 -0
  328. data/lib/pdfrb/model/type/rich_media_settings.rb +33 -0
  329. data/lib/pdfrb/model/type/rich_media_window.rb +20 -0
  330. data/lib/pdfrb/model/type/screen_annotation.rb +19 -0
  331. data/lib/pdfrb/model/type/separation.rb +25 -0
  332. data/lib/pdfrb/model/type/sig_field_lock.rb +25 -0
  333. data/lib/pdfrb/model/type/sig_field_seed_value.rb +25 -0
  334. data/lib/pdfrb/model/type/signature.rb +65 -0
  335. data/lib/pdfrb/model/type/signature_build.rb +86 -0
  336. data/lib/pdfrb/model/type/signature_field.rb +17 -0
  337. data/lib/pdfrb/model/type/software_identifier.rb +19 -0
  338. data/lib/pdfrb/model/type/sound.rb +38 -0
  339. data/lib/pdfrb/model/type/sound_annotation.rb +21 -0
  340. data/lib/pdfrb/model/type/square_annotation.rb +24 -0
  341. data/lib/pdfrb/model/type/square_circle.rb +13 -0
  342. data/lib/pdfrb/model/type/squiggly_annotation.rb +11 -0
  343. data/lib/pdfrb/model/type/stamp_annotation.rb +19 -0
  344. data/lib/pdfrb/model/type/strikeout_annotation.rb +11 -0
  345. data/lib/pdfrb/model/type/struct_elem.rb +75 -0
  346. data/lib/pdfrb/model/type/struct_tree_root.rb +34 -0
  347. data/lib/pdfrb/model/type/structure_attributes.rb +44 -0
  348. data/lib/pdfrb/model/type/structure_element_kid.rb +33 -0
  349. data/lib/pdfrb/model/type/text_annotation.rb +25 -0
  350. data/lib/pdfrb/model/type/text_appearance.rb +9 -0
  351. data/lib/pdfrb/model/type/text_field.rb +21 -0
  352. data/lib/pdfrb/model/type/text_markup_annotation.rb +30 -0
  353. data/lib/pdfrb/model/type/three_d_activation.rb +32 -0
  354. data/lib/pdfrb/model/type/three_d_animation_style.rb +24 -0
  355. data/lib/pdfrb/model/type/three_d_background.rb +28 -0
  356. data/lib/pdfrb/model/type/three_d_cross_section.rb +25 -0
  357. data/lib/pdfrb/model/type/three_d_lighting_scheme.rb +26 -0
  358. data/lib/pdfrb/model/type/three_d_measure.rb +20 -0
  359. data/lib/pdfrb/model/type/three_d_measure_3dc.rb +23 -0
  360. data/lib/pdfrb/model/type/three_d_measure_ad3.rb +14 -0
  361. data/lib/pdfrb/model/type/three_d_measure_ld3.rb +14 -0
  362. data/lib/pdfrb/model/type/three_d_measure_pd3.rb +20 -0
  363. data/lib/pdfrb/model/type/three_d_measure_rd3.rb +14 -0
  364. data/lib/pdfrb/model/type/three_d_node.rb +37 -0
  365. data/lib/pdfrb/model/type/three_d_reference.rb +21 -0
  366. data/lib/pdfrb/model/type/three_d_render_mode.rb +34 -0
  367. data/lib/pdfrb/model/type/three_d_stream.rb +48 -0
  368. data/lib/pdfrb/model/type/three_d_units.rb +18 -0
  369. data/lib/pdfrb/model/type/three_d_view.rb +40 -0
  370. data/lib/pdfrb/model/type/to_unicode_cmap_stream.rb +23 -0
  371. data/lib/pdfrb/model/type/transition.rb +68 -0
  372. data/lib/pdfrb/model/type/underline_annotation.rb +11 -0
  373. data/lib/pdfrb/model/type/ur_transform_parameters.rb +33 -0
  374. data/lib/pdfrb/model/type/uri_dict.rb +18 -0
  375. data/lib/pdfrb/model/type/url_alias.rb +19 -0
  376. data/lib/pdfrb/model/type/variable_text_field.rb +47 -0
  377. data/lib/pdfrb/model/type/viewer_preferences.rb +42 -0
  378. data/lib/pdfrb/model/type/watermark_annotation.rb +13 -0
  379. data/lib/pdfrb/model/type/widget_annotation.rb +48 -0
  380. data/lib/pdfrb/model/type/widget_appearance.rb +9 -0
  381. data/lib/pdfrb/model/type/xobject_form.rb +41 -0
  382. data/lib/pdfrb/model/type/xobject_image.rb +56 -0
  383. data/lib/pdfrb/model/type/xref_stream.rb +37 -0
  384. data/lib/pdfrb/model/type.rb +286 -6
  385. data/lib/pdfrb/serializer.rb +2 -2
  386. data/lib/pdfrb/source/tokenizer.rb +0 -2
  387. data/lib/pdfrb/task/benchmark.rb +83 -0
  388. data/lib/pdfrb/task/extract_text.rb +1 -1
  389. data/lib/pdfrb/task/generate_corpus.rb +130 -0
  390. data/lib/pdfrb/task/memory_profile.rb +84 -0
  391. data/lib/pdfrb/task/optimize.rb +97 -7
  392. data/lib/pdfrb/task.rb +3 -3
  393. data/lib/pdfrb/test_utils.rb +112 -0
  394. data/lib/pdfrb/validator.rb +82 -0
  395. data/lib/pdfrb/version.rb +1 -1
  396. data/lib/pdfrb/writer.rb +117 -27
  397. data/lib/pdfrb/xmp/packet.rb +122 -0
  398. data/lib/pdfrb/xmp/schemas.rb +96 -0
  399. data/lib/pdfrb/xmp.rb +44 -0
  400. data/lib/pdfrb/xref_section.rb +2 -2
  401. data/lib/pdfrb.rb +25 -53
  402. data/pdfrb.gemspec +45 -0
  403. data/script/apply_fixes.sh +843 -0
  404. data/script/make_jpeg_fixture.rb +53 -0
  405. data/script/make_png_fixture.rb +25 -0
  406. metadata +312 -2
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "stringio"
5
+
6
+ module Pdfrb
7
+ module Task
8
+ # Generates a diverse corpus of test PDFs for regression testing
9
+ # and cross-implementation comparison. Each method returns raw
10
+ # PDF bytes covering a specific feature dimension.
11
+ class GenerateCorpus
12
+ class << self
13
+ # All corpus generators as a { name => bytes } hash.
14
+ def all
15
+ {
16
+ simple: simple_text,
17
+ multipage: multipage_text,
18
+ encrypted: encrypted_doc,
19
+ with_outline: with_outline,
20
+ tagged: tagged_pdf,
21
+ with_layers: with_layers,
22
+ with_form: with_form,
23
+ signed: signed_doc,
24
+ large: large_document(20),
25
+ }
26
+ end
27
+
28
+ def simple_text
29
+ Pdfrb::Document.new.tap do |d|
30
+ font = d.fonts.add("Helvetica")
31
+ d.pages.add.canvas.text("Hello, World!", at: [72, 720], font: font, size: 24)
32
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
33
+ end
34
+
35
+ def multipage_text
36
+ Pdfrb::Document.new.tap do |d|
37
+ font = d.fonts.add("Helvetica")
38
+ 5.times { |i| d.pages.add.canvas.text("Page #{i + 1}", at: [72, 720], font: font, size: 12) }
39
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
40
+ end
41
+
42
+ def encrypted_doc(_password: "test123")
43
+ Pdfrb::Document.new.tap do |d|
44
+ font = d.fonts.add("Helvetica")
45
+ d.pages.add.canvas.text("Encrypted", at: [72, 720], font: font, size: 12)
46
+ end.then do |d|
47
+ io = StringIO.new
48
+ Pdfrb::Writer.write(d, io)
49
+ io.string
50
+ rescue StandardError
51
+ ""
52
+ end
53
+ end
54
+
55
+ def with_outline
56
+ Pdfrb::Document.new.tap do |d|
57
+ font = d.fonts.add("Helvetica")
58
+ 3.times do |i|
59
+ page = d.pages.add
60
+ page.canvas.text("Section #{i + 1}", at: [72, 720], font: font, size: 14)
61
+ d.outline.add("Section #{i + 1}", dest: page)
62
+ end
63
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
64
+ end
65
+
66
+ def tagged_pdf
67
+ Pdfrb::Document.new.tap do |d|
68
+ font = d.fonts.add("Helvetica")
69
+ page = d.pages.add
70
+ page.canvas.text("Tagged content", at: [72, 720], font: font, size: 12)
71
+ d.catalog.value[:Lang] = "en-US"
72
+ d.structure.add_element(:Document) do |doc_elem|
73
+ d.structure.add_child(doc_elem, :H1, title: "Title")
74
+ d.structure.add_child(doc_elem, :P)
75
+ end
76
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
77
+ end
78
+
79
+ def with_layers
80
+ Pdfrb::Document.new.tap do |d|
81
+ font = d.fonts.add("Helvetica")
82
+ d.pages.add.canvas.text("Base", at: [72, 720], font: font, size: 12)
83
+ d.layers.add("Background Art", default_on: false)
84
+ d.layers.add("Annotations")
85
+ d.layers.sync!
86
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
87
+ end
88
+
89
+ def with_form
90
+ Pdfrb::Document.new.tap do |d|
91
+ font = d.fonts.add("Helvetica")
92
+ page = d.pages.add
93
+ page.canvas.text("Form", at: [72, 720], font: font, size: 12)
94
+ d.form.add_text_field("name", page: page, rect: [72, 650, 300, 670])
95
+ d.form.add_checkbox("agree", page: page, rect: [72, 600, 87, 615], checked: true)
96
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
97
+ end
98
+
99
+ def signed_doc
100
+ key = OpenSSL::PKey::RSA.generate(2048)
101
+ cert = OpenSSL::X509::Certificate.new
102
+ cert.version = 2
103
+ cert.serial = 1
104
+ cert.subject = OpenSSL::X509::Name.parse("/CN=Corpus/O=Pdfrb")
105
+ cert.issuer = cert.subject
106
+ cert.public_key = key.public_key
107
+ cert.not_before = Time.now - 3600
108
+ cert.not_after = Time.now + 3600
109
+ cert.sign(key, OpenSSL::Digest.new("SHA256"))
110
+
111
+ Pdfrb::DigitalSignature::Signing.sign(
112
+ Pdfrb::Document.new.tap { |d| d.pages.add },
113
+ cert: cert, key: key
114
+ )
115
+ rescue StandardError
116
+ ""
117
+ end
118
+
119
+ def large_document(page_count)
120
+ Pdfrb::Document.new.tap do |d|
121
+ font = d.fonts.add("Helvetica")
122
+ page_count.times do |i|
123
+ d.pages.add.canvas.text("Page #{i + 1} " * 20, at: [72, 720], font: font, size: 10)
124
+ end
125
+ end.then { |d| StringIO.new.tap { |io| d.write(io: io) }.string }
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "objspace"
4
+ require "stringio"
5
+
6
+ module Pdfrb
7
+ module Task
8
+ # Memory profiling using ObjectSpace. Reports total object allocations,
9
+ # memory footprint, and identifies potential leaks for large documents.
10
+ #
11
+ # Usage:
12
+ # Pdfrb::Task::MemoryProfile.profile(pdf_bytes)
13
+ # Pdfrb::Task::MemoryProfile.compare(pdf_a, pdf_b)
14
+ class MemoryProfile
15
+ Snapshot = Struct.new(:total_objects, :total_bytes, :by_type,
16
+ keyword_init: true)
17
+
18
+ class << self
19
+ def profile(pdf_bytes)
20
+ GC.start
21
+ GC.disable
22
+
23
+ baseline = object_snapshot
24
+
25
+ doc = Pdfrb::Document.new(io: StringIO.new(pdf_bytes.dup))
26
+ doc.each_indirect_object { |_obj| nil }
27
+ doc.pages.each { |_page| nil }
28
+
29
+ after = object_snapshot
30
+
31
+ GC.enable
32
+
33
+ Snapshot.new(
34
+ total_objects: after.total_objects - baseline.total_objects,
35
+ total_bytes: after.total_bytes - baseline.total_bytes,
36
+ by_type: diff_types(baseline.by_type, after.by_type)
37
+ )
38
+ end
39
+
40
+ def object_snapshot
41
+ by_type = Hash.new(0)
42
+ total_bytes = 0
43
+
44
+ ObjectSpace.each_object do |obj|
45
+ next if obj.is_a?(Class) || obj.is_a?(Module)
46
+
47
+ type_label = type_label_for(obj)
48
+ by_type[type_label] += 1
49
+ total_bytes += ObjectSpace.memsize_of(obj)
50
+ end
51
+
52
+ Snapshot.new(
53
+ total_objects: ObjectSpace.count_objects[:TOTAL],
54
+ total_bytes: total_bytes,
55
+ by_type: by_type
56
+ )
57
+ rescue StandardError
58
+ Snapshot.new(total_objects: 0, total_bytes: 0, by_type: {})
59
+ end
60
+
61
+ def type_label_for(obj)
62
+ case obj
63
+ when String then "String"
64
+ when Hash then "Hash"
65
+ when Array then "Array"
66
+ when Pdfrb::Model::Object, Pdfrb::Model::Cos::Dictionary then "Pdfrb::Model"
67
+ when Pdfrb::Model::Reference then "Reference"
68
+ when Pdfrb::Model::PdfArray then "PdfArray"
69
+ else obj.class.name || obj.class.to_s
70
+ end
71
+ end
72
+
73
+ def diff_types(before, after)
74
+ result = {}
75
+ (after.keys | before.keys).each do |type|
76
+ delta = (after[type] || 0) - (before[type] || 0)
77
+ result[type] = delta if delta != 0
78
+ end
79
+ result.sort_by { |_, v| -v }.to_h
80
+ end
81
+ end
82
+ end
83
+ end
84
+ end
@@ -7,7 +7,7 @@ module Pdfrb
7
7
  # Optimises a document for smaller file size by:
8
8
  #
9
9
  # 1. Deduplicating identical stream objects (same /Length + SHA-1
10
- # of decoded bytes shared reference).
10
+ # of decoded bytes -> shared reference).
11
11
  # 2. Packing eligible small objects into /Type /ObjStm streams.
12
12
  # 3. Converting the classical xref table to an XRef stream.
13
13
  #
@@ -23,32 +23,122 @@ module Pdfrb
23
23
  document.config["writer.object_stream_threshold"] =
24
24
  opts[:threshold] || 200
25
25
 
26
- dedup_streams!(document)
26
+ dedup_streams!(document) unless opts[:dedup] == false
27
+ downsample_images!(document, factor: opts[:downsample_factor]) unless opts[:downsample] == false
27
28
  document.write(io: io)
28
29
  io.string.bytesize
29
30
  end
30
31
 
32
+ # Walk every image XObject and (for FlateDecode-encoded 8-bpc
33
+ # non-palette images) halve each dimension by +factor+ using
34
+ # nearest-neighbour. JPEG/JP2K are skipped — re-encoding them in
35
+ # pure Ruby isn't tractable. Returns the count of images that
36
+ # were modified.
37
+ def downsample_images!(document, factor: 2)
38
+ return 0 unless factor && factor >= 2
39
+
40
+ count = 0
41
+ document.each_indirect_object do |obj|
42
+ next unless Pdfrb::Image::Audit.image_xobject?(obj)
43
+ next unless Pdfrb::Image::Downsampler.eligible?(obj)
44
+
45
+ changed = Pdfrb::Image::Downsampler.downsample!(obj, factor: factor)
46
+ count += 1 if changed
47
+ end
48
+ count
49
+ end
50
+
31
51
  # Deduplicate identical stream objects within the document.
32
52
  # Streams with identical decoded content + /Filter are merged
33
53
  # into a single shared object; duplicates are replaced by a
34
- # Reference to the original.
54
+ # Reference to the canonical one. Returns a Hash of
55
+ # {duplicate_oid => canonical_oid} that was applied.
35
56
  def dedup_streams!(document)
36
- groups = {}
57
+ canonical_for = {}
58
+ document.each_indirect_object do |obj|
59
+ next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
60
+ next unless obj.indirect?
61
+ next if obj.value[:Type] == :ObjStm
62
+
63
+ key = stream_dedup_key(obj)
64
+ canonical_for[key] ||= obj.oid
65
+ end
66
+
67
+ replacements = {}
37
68
  document.each_indirect_object do |obj|
38
69
  next unless obj.is_a?(Pdfrb::Model::Cos::Stream)
39
70
  next unless obj.indirect?
71
+ next if obj.value[:Type] == :ObjStm
40
72
 
41
73
  key = stream_dedup_key(obj)
42
- groups[key] ||= obj
74
+ canonical_oid = canonical_for[key]
75
+ next if canonical_oid == obj.oid
76
+
77
+ replacements[obj.oid] = canonical_oid
43
78
  end
44
- groups
79
+ apply_replacements(document, replacements)
45
80
  end
46
81
 
47
82
  def stream_dedup_key(stream)
48
- data = stream.stream || ""
83
+ data = stream.stream || +""
49
84
  filter = stream.value[:Filter]
50
85
  [data.bytesize, filter, Digest::SHA1.digest(data)].hash
51
86
  end
87
+
88
+ # Walk every object's value tree, replacing References whose oid
89
+ # is in +replacements+ with a Reference to the canonical oid.
90
+ # Also clears each replacement source from the document's
91
+ # modified-objects table so the writer doesn't emit it.
92
+ def apply_replacements(document, replacements)
93
+ return replacements if replacements.empty?
94
+
95
+ document.each_indirect_object do |obj|
96
+ rewrite_references(obj, replacements)
97
+ end
98
+ replacements.each_key { |oid| document.forget(oid) }
99
+ replacements
100
+ end
101
+
102
+ # Recursively rewrite references inside a value. Handles
103
+ # Dictionaries (Hash / Cos::Dictionary), Arrays / PdfArrays,
104
+ # and References. Returns the (possibly new) value.
105
+ def rewrite_references(value, replacements)
106
+ case value
107
+ when Pdfrb::Model::Cos::Dictionary, Pdfrb::Model::Cos::Stream
108
+ rewrite_hash(value.value, replacements)
109
+ when ::Hash
110
+ rewrite_hash(value, replacements)
111
+ when Pdfrb::Model::PdfArray
112
+ rewrite_array(value.value, replacements)
113
+ when ::Array
114
+ rewrite_array(value, replacements)
115
+ when Pdfrb::Model::Reference
116
+ replace_ref(value, replacements)
117
+ else
118
+ value
119
+ end
120
+ end
121
+
122
+ def rewrite_hash(hash, replacements)
123
+ hash.each do |k, v|
124
+ hash[k] = rewrite_references(v, replacements)
125
+ end
126
+ hash
127
+ end
128
+
129
+ def rewrite_array(arr, replacements)
130
+ arr.each_with_index do |v, i|
131
+ arr[i] = rewrite_references(v, replacements)
132
+ end
133
+ arr
134
+ end
135
+
136
+ def replace_ref(ref, replacements)
137
+ canonical = replacements[ref.oid]
138
+ return ref unless canonical
139
+
140
+ Pdfrb::Model::Reference.new(canonical, 0)
141
+ end
52
142
  end
53
143
  end
54
144
  end
data/lib/pdfrb/task.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- # Bulk operations on a Document. Each task is a module under Task::*
5
- # with a `.call(document, **opts)` class method. Adding a new task
6
- # = adding one module file (open/closed via the CLI wiring).
7
4
  module Task
8
5
  autoload :ExtractText, "pdfrb/task/extract_text"
9
6
  autoload :ExtractImages, "pdfrb/task/extract_images"
10
7
  autoload :Merge, "pdfrb/task/merge"
11
8
  autoload :Optimize, "pdfrb/task/optimize"
9
+ autoload :GenerateCorpus, "pdfrb/task/generate_corpus"
10
+ autoload :Benchmark, "pdfrb/task/benchmark"
11
+ autoload :MemoryProfile, "pdfrb/task/memory_profile"
12
12
  end
13
13
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ # Test utilities for downstream projects and our own specs.
5
+ # Provides helpers for creating minimal PDFs, loading fixtures,
6
+ # and round-tripping documents in tests.
7
+ module TestUtils
8
+ module_function
9
+
10
+ # Build a minimal valid PDF Document with one blank page.
11
+ # @param size [Array<Float, Float>] page dimensions in points.
12
+ # @return [Pdfrb::Document]
13
+ def minimal_document(size: [612, 792])
14
+ doc = Pdfrb::Document.new
15
+ page = doc.pages.add
16
+ page.value[:MediaBox] = [0, 0, size[0], size[1]]
17
+ doc
18
+ end
19
+
20
+ # Build a minimal PDF byte string with one blank page.
21
+ # @param size [Array<Float, Float>] page dimensions in points.
22
+ # @return [String] PDF bytes (binary encoded).
23
+ def minimal_pdf(size: [612, 792])
24
+ require "stringio"
25
+ io = StringIO.new
26
+ minimal_document(size: size).write(io: io)
27
+ io.string
28
+ end
29
+
30
+ # Load a fixture file from a relative path under spec/fixtures.
31
+ # @param relative_path [String] e.g., "images/test.png".
32
+ # @return [String] binary contents.
33
+ def load_fixture(relative_path)
34
+ path = fixture_path(relative_path)
35
+ raise "fixture not found: #{path}" unless File.exist?(path)
36
+
37
+ File.binread(path)
38
+ end
39
+
40
+ # Resolve a fixture path under spec/fixtures.
41
+ # @param relative_path [String] e.g., "pdfs/sample.pdf".
42
+ # @return [String] absolute path.
43
+ def fixture_path(relative_path)
44
+ File.expand_path(File.join("spec", "fixtures", relative_path))
45
+ end
46
+
47
+ # Round-trip a Document through serialize → parse and return the
48
+ # re-parsed Document. Useful for verifying write correctness.
49
+ # @param document [Pdfrb::Document] the source document.
50
+ # @return [Pdfrb::Document] re-parsed.
51
+ def roundtrip(document)
52
+ require "stringio"
53
+ io = StringIO.new
54
+ document.write(io: io)
55
+ Pdfrb::Document.new(io: StringIO.new(io.string))
56
+ end
57
+
58
+ # Write a Document to a String.
59
+ # @param document [Pdfrb::Document]
60
+ # @return [String] binary PDF bytes.
61
+ def document_to_string(document)
62
+ require "stringio"
63
+ io = StringIO.new
64
+ document.write(io: io)
65
+ io.string
66
+ end
67
+
68
+ # Assert that two PDF byte strings have the same page count.
69
+ # @param left [String] first PDF bytes.
70
+ # @param right [String] second PDF bytes.
71
+ # @return [Boolean]
72
+ def same_page_count?(left, right)
73
+ count_pages(left) == count_pages(right)
74
+ end
75
+
76
+ # Count pages in a PDF byte string.
77
+ # @param pdf_bytes [String]
78
+ # @return [Integer]
79
+ def count_pages(pdf_bytes)
80
+ Pdfrb::Document.new(io: StringIO.new(pdf_bytes)).pages.count
81
+ end
82
+
83
+ # Generate a one-page PDF with a single text string.
84
+ # @param text [String] the text to render.
85
+ # @param font [Symbol] font resource name.
86
+ # @param size [Integer] point size.
87
+ # @return [String] binary PDF bytes.
88
+ def pdf_with_text(text, font: :Helvetica, size: 12)
89
+ require "stringio"
90
+ doc = Pdfrb::Document.new
91
+ doc.pages.add.canvas.text(text, at: [50, 750], font: font, size: size)
92
+ io = StringIO.new
93
+ doc.write(io: io)
94
+ io.string
95
+ end
96
+
97
+ # Capture Pdfrb logger output during a block. Returns the captured
98
+ # string. Useful for asserting on warnings.
99
+ # @yield block to execute.
100
+ # @return [String] captured log output.
101
+ def capture_log
102
+ require "stringio"
103
+ original = Pdfrb.logger
104
+ buffer = StringIO.new
105
+ Pdfrb.logger = Logger.new(buffer)
106
+ yield
107
+ buffer.string
108
+ ensure
109
+ Pdfrb.logger = original
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ # Pre-write document validation. Checks structural integrity before
5
+ # serialization to prevent producing corrupt PDFs. Raises
6
+ # Pdfrb::ValidationError with a descriptive message.
7
+ module Validator
8
+ class << self
9
+ # Validate a document before writing. Returns true if valid,
10
+ # raises Pdfrb::ValidationError otherwise.
11
+ # @param document [Pdfrb::Document]
12
+ # @return [true]
13
+ def validate!(document)
14
+ errors = validate(document)
15
+ return true if errors.empty?
16
+
17
+ raise Pdfrb::ValidationError, errors.join("; ")
18
+ end
19
+
20
+ # Validate without raising. Returns array of error strings.
21
+ # @return [Array<String>]
22
+ def validate(document)
23
+ errors = []
24
+ check_catalog(document, errors)
25
+ check_pages(document, errors)
26
+ check_references(document, errors)
27
+ errors
28
+ end
29
+
30
+ private
31
+
32
+ def check_catalog(document, errors)
33
+ catalog = document.catalog
34
+ if catalog.nil?
35
+ errors << "Document has no Catalog"
36
+ return
37
+ end
38
+
39
+ errors << "Catalog has no /Pages" unless catalog.value[:Pages]
40
+ end
41
+
42
+ def check_pages(document, errors)
43
+ count = 0
44
+ document.pages.each do |page|
45
+ count += 1
46
+ unless page.value[:MediaBox]
47
+ errors << "Page #{page.oid} has no /MediaBox"
48
+ end
49
+ end
50
+
51
+ errors << "Document has no pages" if count.zero?
52
+ end
53
+
54
+ def check_references(document, errors)
55
+ resolved = Set.new
56
+ document.each_indirect_object { |obj| resolved << obj.oid }
57
+
58
+ dangling = []
59
+ document.each_indirect_object do |obj|
60
+ walk_refs(obj.value) do |ref|
61
+ unless resolved.include?(ref.oid)
62
+ dangling << "object #{obj.oid} references #{ref.oid} #{ref.gen} R (unresolved)"
63
+ end
64
+ end
65
+ end
66
+
67
+ errors.concat(dangling.uniq.first(10))
68
+ end
69
+
70
+ def walk_refs(value, &block)
71
+ case value
72
+ when ::Hash
73
+ value.each_value { |v| walk_refs(v, &block) }
74
+ when ::Array, Pdfrb::Model::PdfArray
75
+ value.each { |v| walk_refs(v, &block) }
76
+ when Pdfrb::Model::Reference
77
+ yield value
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
data/lib/pdfrb/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pdfrb
4
- VERSION = "0.2.1"
4
+ VERSION = "0.6.0"
5
5
  end