pdfrb 0.3.0 → 0.7.1

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 (422) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/CHANGELOG.md +51 -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 +61 -0
  23. data/data/pdfrb/afm/Courier-Bold.afm +342 -342
  24. data/data/pdfrb/afm/Courier-BoldOblique.afm +342 -342
  25. data/data/pdfrb/afm/Courier-Oblique.afm +342 -342
  26. data/data/pdfrb/afm/Courier.afm +342 -342
  27. data/data/pdfrb/afm/Helvetica-Bold.afm +2827 -2827
  28. data/data/pdfrb/afm/Helvetica-BoldOblique.afm +2827 -2827
  29. data/data/pdfrb/afm/Helvetica-Oblique.afm +3051 -3051
  30. data/data/pdfrb/afm/Helvetica.afm +3051 -3051
  31. data/data/pdfrb/afm/Symbol.afm +213 -213
  32. data/data/pdfrb/afm/Times-Bold.afm +2588 -2588
  33. data/data/pdfrb/afm/Times-BoldItalic.afm +2384 -2384
  34. data/data/pdfrb/afm/Times-Italic.afm +2667 -2667
  35. data/data/pdfrb/afm/Times-Roman.afm +2419 -2419
  36. data/data/pdfrb/afm/ZapfDingbats.afm +225 -225
  37. data/lib/pdfrb/action/base.rb +21 -0
  38. data/lib/pdfrb/action/types.rb +178 -0
  39. data/lib/pdfrb/action.rb +55 -0
  40. data/lib/pdfrb/annotation/base.rb +79 -0
  41. data/lib/pdfrb/annotation/types.rb +222 -0
  42. data/lib/pdfrb/annotation.rb +78 -0
  43. data/lib/pdfrb/appearance/generator.rb +216 -0
  44. data/lib/pdfrb/appearance.rb +11 -0
  45. data/lib/pdfrb/arlington/object_definition.rb +2 -2
  46. data/lib/pdfrb/arlington/predicate/evaluator.rb +1 -1
  47. data/lib/pdfrb/arlington/predicate/functions.rb +25 -9
  48. data/lib/pdfrb/arlington/predicate.rb +1 -5
  49. data/lib/pdfrb/cli/batch.rb +18 -0
  50. data/lib/pdfrb/cli/debug_info.rb +18 -0
  51. data/lib/pdfrb/cli/files.rb +16 -0
  52. data/lib/pdfrb/cli/fonts.rb +16 -0
  53. data/lib/pdfrb/cli/form.rb +18 -0
  54. data/lib/pdfrb/cli/image2pdf.rb +18 -0
  55. data/lib/pdfrb/cli/images.rb +34 -0
  56. data/lib/pdfrb/cli/info.rb +20 -0
  57. data/lib/pdfrb/cli/inspect.rb +18 -0
  58. data/lib/pdfrb/cli/merge.rb +18 -0
  59. data/lib/pdfrb/cli/modify.rb +18 -0
  60. data/lib/pdfrb/cli/optimize.rb +18 -0
  61. data/lib/pdfrb/cli/split.rb +21 -0
  62. data/lib/pdfrb/cli/watermark.rb +18 -0
  63. data/lib/pdfrb/cli.rb +291 -7
  64. data/lib/pdfrb/color/color_space.rb +259 -0
  65. data/lib/pdfrb/color/icc_profile.rb +90 -0
  66. data/lib/pdfrb/color.rb +12 -0
  67. data/lib/pdfrb/compare/comparator.rb +244 -0
  68. data/lib/pdfrb/compare/report.rb +78 -0
  69. data/lib/pdfrb/compare.rb +42 -0
  70. data/lib/pdfrb/composer.rb +161 -0
  71. data/lib/pdfrb/conformance/pdf_a.rb +317 -56
  72. data/lib/pdfrb/conformance/pdf_ua.rb +186 -29
  73. data/lib/pdfrb/conformance/pdf_x.rb +247 -0
  74. data/lib/pdfrb/conformance/rule.rb +78 -0
  75. data/lib/pdfrb/conformance/structure_elements.rb +174 -0
  76. data/lib/pdfrb/conformance/verapdf_bridge.rb +92 -0
  77. data/lib/pdfrb/conformance.rb +7 -3
  78. data/lib/pdfrb/content/canvas.rb +354 -72
  79. data/lib/pdfrb/content/color_space.rb +157 -0
  80. data/lib/pdfrb/content/hidden_text_detector.rb +167 -0
  81. data/lib/pdfrb/content/operator.rb +21 -1
  82. data/lib/pdfrb/content/operators/clipping.rb +36 -0
  83. data/lib/pdfrb/content/operators/graphics_state_params.rb +15 -0
  84. data/lib/pdfrb/content/operators/inline_image.rb +53 -0
  85. data/lib/pdfrb/content/operators.rb +22 -0
  86. data/lib/pdfrb/content/shading/base.rb +139 -0
  87. data/lib/pdfrb/content/shading.rb +17 -0
  88. data/lib/pdfrb/content/smart_text_extractor.rb +85 -0
  89. data/lib/pdfrb/content/tiling_pattern.rb +78 -0
  90. data/lib/pdfrb/content/transformation_matrix.rb +61 -0
  91. data/lib/pdfrb/content.rb +7 -26
  92. data/lib/pdfrb/destination/fit.rb +185 -0
  93. data/lib/pdfrb/destination.rb +13 -0
  94. data/lib/pdfrb/digital_signature/cms_handler.rb +22 -0
  95. data/lib/pdfrb/digital_signature/signing.rb +107 -11
  96. data/lib/pdfrb/digital_signature/timestamp_handler.rb +45 -0
  97. data/lib/pdfrb/digital_signature/verification.rb +88 -9
  98. data/lib/pdfrb/digital_signature/verification_result.rb +14 -0
  99. data/lib/pdfrb/digital_signature.rb +26 -2
  100. data/lib/pdfrb/document/annotations.rb +120 -3
  101. data/lib/pdfrb/document/associated_files.rb +104 -0
  102. data/lib/pdfrb/document/colors.rb +95 -0
  103. data/lib/pdfrb/document/destinations.rb +127 -5
  104. data/lib/pdfrb/document/display.rb +84 -0
  105. data/lib/pdfrb/document/fonts.rb +417 -45
  106. data/lib/pdfrb/document/form.rb +338 -0
  107. data/lib/pdfrb/document/graphics_state.rb +91 -0
  108. data/lib/pdfrb/document/images.rb +4 -4
  109. data/lib/pdfrb/document/info.rb +102 -0
  110. data/lib/pdfrb/document/layers.rb +62 -0
  111. data/lib/pdfrb/document/metadata.rb +12 -5
  112. data/lib/pdfrb/document/output_intents.rb +91 -0
  113. data/lib/pdfrb/document/page_labels.rb +80 -0
  114. data/lib/pdfrb/document/pages.rb +68 -19
  115. data/lib/pdfrb/document/portfolio.rb +119 -0
  116. data/lib/pdfrb/document/shadings.rb +159 -0
  117. data/lib/pdfrb/document/stamps.rb +68 -0
  118. data/lib/pdfrb/document/structure.rb +242 -0
  119. data/lib/pdfrb/document.rb +73 -4
  120. data/lib/pdfrb/encryption/identity.rb +13 -0
  121. data/lib/pdfrb/encryption/password_verification.rb +53 -8
  122. data/lib/pdfrb/encryption/security_handler.rb +8 -5
  123. data/lib/pdfrb/encryption/standard_security_handler.rb +154 -81
  124. data/lib/pdfrb/encryption.rb +17 -0
  125. data/lib/pdfrb/error.rb +2 -2
  126. data/lib/pdfrb/filter/brotli_decode.rb +32 -0
  127. data/lib/pdfrb/filter/ccitt_fax_decode.rb +12 -0
  128. data/lib/pdfrb/filter/crypt.rb +2 -2
  129. data/lib/pdfrb/filter/dct_decode.rb +18 -0
  130. data/lib/pdfrb/filter/flate_decode.rb +4 -18
  131. data/lib/pdfrb/filter/jbig2_decode.rb +12 -0
  132. data/lib/pdfrb/filter/jpx_decode.rb +18 -0
  133. data/lib/pdfrb/filter/lzw_decode.rb +3 -2
  134. data/lib/pdfrb/filter/png_predictor.rb +94 -0
  135. data/lib/pdfrb/filter.rb +26 -13
  136. data/lib/pdfrb/font/afm_parser.rb +50 -26
  137. data/lib/pdfrb/font/cmap/writer.rb +9 -0
  138. data/lib/pdfrb/font/encoding/win_ansi_encoding.rb +17 -0
  139. data/lib/pdfrb/font/encoding.rb +32 -3
  140. data/lib/pdfrb/font/metrics_helper.rb +121 -0
  141. data/lib/pdfrb/font/true_type/file.rb +10 -1
  142. data/lib/pdfrb/font/true_type/glyf.rb +82 -0
  143. data/lib/pdfrb/font/true_type/kern.rb +58 -0
  144. data/lib/pdfrb/font/true_type/loca.rb +42 -0
  145. data/lib/pdfrb/font/true_type/maxp.rb +44 -0
  146. data/lib/pdfrb/font/true_type/name.rb +53 -0
  147. data/lib/pdfrb/font/true_type/post.rb +93 -0
  148. data/lib/pdfrb/font/true_type/subsetter.rb +3 -5
  149. data/lib/pdfrb/font/true_type/wrapper.rb +102 -0
  150. data/lib/pdfrb/font/true_type.rb +7 -0
  151. data/lib/pdfrb/font.rb +2 -0
  152. data/lib/pdfrb/font_loader/from_configuration.rb +19 -0
  153. data/lib/pdfrb/font_loader/from_file.rb +20 -0
  154. data/lib/pdfrb/font_loader/standard14.rb +20 -0
  155. data/lib/pdfrb/font_loader/variant_from_name.rb +36 -0
  156. data/lib/pdfrb/font_loader.rb +4 -0
  157. data/lib/pdfrb/font_resolver.rb +138 -0
  158. data/lib/pdfrb/image/audit.rb +94 -0
  159. data/lib/pdfrb/image/downsampler.rb +124 -0
  160. data/lib/pdfrb/image.rb +13 -0
  161. data/lib/pdfrb/image_loader/jpeg.rb +39 -58
  162. data/lib/pdfrb/image_loader/png.rb +82 -251
  163. data/lib/pdfrb/layout/bidi.rb +198 -0
  164. data/lib/pdfrb/layout/box.rb +49 -0
  165. data/lib/pdfrb/layout/box_fitter.rb +54 -0
  166. data/lib/pdfrb/layout/column_box.rb +71 -0
  167. data/lib/pdfrb/layout/container_box.rb +45 -0
  168. data/lib/pdfrb/layout/frame.rb +67 -0
  169. data/lib/pdfrb/layout/image_box.rb +44 -0
  170. data/lib/pdfrb/layout/inline_box.rb +25 -0
  171. data/lib/pdfrb/layout/line.rb +55 -0
  172. data/lib/pdfrb/layout/list_box.rb +87 -0
  173. data/lib/pdfrb/layout/numeric_refinements.rb +43 -0
  174. data/lib/pdfrb/layout/page_style.rb +65 -0
  175. data/lib/pdfrb/layout/style.rb +62 -0
  176. data/lib/pdfrb/layout/table_box.rb +66 -0
  177. data/lib/pdfrb/layout/text_box.rb +38 -0
  178. data/lib/pdfrb/layout/text_fragment.rb +45 -0
  179. data/lib/pdfrb/layout/text_layouter.rb +123 -0
  180. data/lib/pdfrb/layout.rb +28 -0
  181. data/lib/pdfrb/linearization/hint_stream.rb +93 -0
  182. data/lib/pdfrb/linearization/writer.rb +259 -0
  183. data/lib/pdfrb/linearization.rb +21 -0
  184. data/lib/pdfrb/model/cos/dictionary.rb +12 -0
  185. data/lib/pdfrb/model/pdf_array.rb +2 -2
  186. data/lib/pdfrb/model/type/action.rb +42 -0
  187. data/lib/pdfrb/model/type/action_go_to_3d_view.rb +14 -0
  188. data/lib/pdfrb/model/type/action_go_to_dp.rb +16 -0
  189. data/lib/pdfrb/model/type/action_go_to_r.rb +26 -0
  190. data/lib/pdfrb/model/type/action_goto.rb +34 -0
  191. data/lib/pdfrb/model/type/action_hide.rb +26 -0
  192. data/lib/pdfrb/model/type/action_import_data.rb +18 -0
  193. data/lib/pdfrb/model/type/action_java_script.rb +18 -0
  194. data/lib/pdfrb/model/type/action_launch.rb +22 -0
  195. data/lib/pdfrb/model/type/action_movie.rb +22 -0
  196. data/lib/pdfrb/model/type/action_named.rb +23 -0
  197. data/lib/pdfrb/model/type/action_nop.rb +12 -0
  198. data/lib/pdfrb/model/type/action_rendition.rb +28 -0
  199. data/lib/pdfrb/model/type/action_reset_form.rb +22 -0
  200. data/lib/pdfrb/model/type/action_rich_media_execute.rb +17 -0
  201. data/lib/pdfrb/model/type/action_set_ocg_state.rb +32 -0
  202. data/lib/pdfrb/model/type/action_set_state.rb +14 -0
  203. data/lib/pdfrb/model/type/action_sound_action.rb +23 -0
  204. data/lib/pdfrb/model/type/action_submit_form.rb +64 -0
  205. data/lib/pdfrb/model/type/action_thread.rb +23 -0
  206. data/lib/pdfrb/model/type/action_trans.rb +30 -0
  207. data/lib/pdfrb/model/type/action_uri.rb +30 -0
  208. data/lib/pdfrb/model/type/af_embedded_file.rb +11 -0
  209. data/lib/pdfrb/model/type/af_file_specification.rb +34 -0
  210. data/lib/pdfrb/model/type/annotation.rb +49 -10
  211. data/lib/pdfrb/model/type/appearance.rb +41 -0
  212. data/lib/pdfrb/model/type/appearance_characteristics.rb +36 -0
  213. data/lib/pdfrb/model/type/appearance_generator.rb +22 -0
  214. data/lib/pdfrb/model/type/appearance_trap_net.rb +84 -0
  215. data/lib/pdfrb/model/type/border_effect.rb +25 -0
  216. data/lib/pdfrb/model/type/border_styling.rb +38 -0
  217. data/lib/pdfrb/model/type/box_color_info.rb +21 -0
  218. data/lib/pdfrb/model/type/box_style.rb +19 -0
  219. data/lib/pdfrb/model/type/button.rb +17 -0
  220. data/lib/pdfrb/model/type/cal_gray.rb +17 -0
  221. data/lib/pdfrb/model/type/cal_rgb.rb +22 -0
  222. data/lib/pdfrb/model/type/caret_annotation.rb +21 -0
  223. data/lib/pdfrb/model/type/catalog.rb +99 -16
  224. data/lib/pdfrb/model/type/choice.rb +21 -0
  225. data/lib/pdfrb/model/type/cid_font.rb +52 -1
  226. data/lib/pdfrb/model/type/cid_font_descriptor_metrics.rb +28 -0
  227. data/lib/pdfrb/model/type/cid_system_info.rb +28 -0
  228. data/lib/pdfrb/model/type/circle_annotation.rb +20 -0
  229. data/lib/pdfrb/model/type/collection.rb +27 -0
  230. data/lib/pdfrb/model/type/collection_field.rb +24 -0
  231. data/lib/pdfrb/model/type/collection_item.rb +19 -0
  232. data/lib/pdfrb/model/type/collection_schema.rb +25 -0
  233. data/lib/pdfrb/model/type/collection_sort.rb +13 -0
  234. data/lib/pdfrb/model/type/crypt_filter.rb +58 -0
  235. data/lib/pdfrb/model/type/device_n.rb +37 -0
  236. data/lib/pdfrb/model/type/device_n_mixing_hints.rb +17 -0
  237. data/lib/pdfrb/model/type/device_n_process.rb +13 -0
  238. data/lib/pdfrb/model/type/doc_mdp_transform_parameters.rb +21 -0
  239. data/lib/pdfrb/model/type/document_security_store.rb +55 -0
  240. data/lib/pdfrb/model/type/embedded_file.rb +19 -0
  241. data/lib/pdfrb/model/type/embedded_file_parameter.rb +48 -0
  242. data/lib/pdfrb/model/type/encryption_public_key.rb +38 -0
  243. data/lib/pdfrb/model/type/encryption_standard.rb +68 -0
  244. data/lib/pdfrb/model/type/field_mdp_transform_parameters.rb +26 -0
  245. data/lib/pdfrb/model/type/file_attachment_annotation.rb +29 -0
  246. data/lib/pdfrb/model/type/file_spec_ef.rb +22 -0
  247. data/lib/pdfrb/model/type/file_spec_rf.rb +21 -0
  248. data/lib/pdfrb/model/type/file_specification.rb +43 -0
  249. data/lib/pdfrb/model/type/file_trailer.rb +54 -0
  250. data/lib/pdfrb/model/type/font.rb +73 -2
  251. data/lib/pdfrb/model/type/font_cid_type0.rb +17 -0
  252. data/lib/pdfrb/model/type/font_cid_type2.rb +17 -0
  253. data/lib/pdfrb/model/type/font_descriptor.rb +47 -0
  254. data/lib/pdfrb/model/type/font_descriptor_cid_type0.rb +15 -0
  255. data/lib/pdfrb/model/type/font_descriptor_cid_type2.rb +15 -0
  256. data/lib/pdfrb/model/type/font_encoding.rb +47 -0
  257. data/lib/pdfrb/model/type/font_file.rb +28 -0
  258. data/lib/pdfrb/model/type/font_file2.rb +32 -0
  259. data/lib/pdfrb/model/type/font_file3.rb +23 -0
  260. data/lib/pdfrb/model/type/font_multiple_master.rb +60 -0
  261. data/lib/pdfrb/model/type/font_true_type.rb +1 -1
  262. data/lib/pdfrb/model/type/font_type0.rb +1 -1
  263. data/lib/pdfrb/model/type/font_type1.rb +1 -1
  264. data/lib/pdfrb/model/type/font_type3.rb +1 -1
  265. data/lib/pdfrb/model/type/form_field.rb +73 -0
  266. data/lib/pdfrb/model/type/free_text_annotation.rb +37 -0
  267. data/lib/pdfrb/model/type/function.rb +42 -0
  268. data/lib/pdfrb/model/type/function_exponential.rb +31 -0
  269. data/lib/pdfrb/model/type/function_postscript.rb +25 -0
  270. data/lib/pdfrb/model/type/function_sampled.rb +55 -0
  271. data/lib/pdfrb/model/type/function_stitching.rb +33 -0
  272. data/lib/pdfrb/model/type/generic_appearance.rb +40 -0
  273. data/lib/pdfrb/model/type/graphics_state_parameter.rb +69 -0
  274. data/lib/pdfrb/model/type/halftone.rb +21 -0
  275. data/lib/pdfrb/model/type/halftone_type1.rb +17 -0
  276. data/lib/pdfrb/model/type/halftone_type10.rb +16 -0
  277. data/lib/pdfrb/model/type/halftone_type16.rb +11 -0
  278. data/lib/pdfrb/model/type/halftone_type5.rb +24 -0
  279. data/lib/pdfrb/model/type/halftone_type6.rb +16 -0
  280. data/lib/pdfrb/model/type/highlight_annotation.rb +11 -0
  281. data/lib/pdfrb/model/type/icc_based_color_space.rb +24 -0
  282. data/lib/pdfrb/model/type/icon_fit.rb +31 -0
  283. data/lib/pdfrb/model/type/indexed.rb +32 -0
  284. data/lib/pdfrb/model/type/info.rb +44 -0
  285. data/lib/pdfrb/model/type/ink_annotation.rb +29 -0
  286. data/lib/pdfrb/model/type/interactive_form.rb +36 -0
  287. data/lib/pdfrb/model/type/interior_color.rb +12 -0
  288. data/lib/pdfrb/model/type/jbig2_globals.rb +15 -0
  289. data/lib/pdfrb/model/type/lab.rb +27 -0
  290. data/lib/pdfrb/model/type/line_annotation.rb +44 -0
  291. data/lib/pdfrb/model/type/line_ending_styling.rb +39 -0
  292. data/lib/pdfrb/model/type/link_annotation.rb +41 -0
  293. data/lib/pdfrb/model/type/link_appearance.rb +9 -0
  294. data/lib/pdfrb/model/type/mark_information.rb +23 -0
  295. data/lib/pdfrb/model/type/marked_content_reference.rb +24 -0
  296. data/lib/pdfrb/model/type/markup_annotation.rb +36 -0
  297. data/lib/pdfrb/model/type/mdp_dict.rb +28 -0
  298. data/lib/pdfrb/model/type/measure.rb +39 -0
  299. data/lib/pdfrb/model/type/media_clip.rb +28 -0
  300. data/lib/pdfrb/model/type/media_offset.rb +106 -0
  301. data/lib/pdfrb/model/type/metadata.rb +27 -3
  302. data/lib/pdfrb/model/type/misc_helpers.rb +75 -0
  303. data/lib/pdfrb/model/type/movie.rb +28 -0
  304. data/lib/pdfrb/model/type/names.rb +51 -0
  305. data/lib/pdfrb/model/type/namespace.rb +27 -0
  306. data/lib/pdfrb/model/type/object_reference.rb +31 -0
  307. data/lib/pdfrb/model/type/object_stream.rb +38 -0
  308. data/lib/pdfrb/model/type/opt_content_ext.rb +94 -0
  309. data/lib/pdfrb/model/type/opt_content_page_element.rb +18 -0
  310. data/lib/pdfrb/model/type/optional_content_config.rb +56 -0
  311. data/lib/pdfrb/model/type/optional_content_group.rb +35 -10
  312. data/lib/pdfrb/model/type/optional_content_membership.rb +36 -2
  313. data/lib/pdfrb/model/type/optional_content_properties.rb +16 -1
  314. data/lib/pdfrb/model/type/outline.rb +40 -6
  315. data/lib/pdfrb/model/type/outline_item.rb +45 -0
  316. data/lib/pdfrb/model/type/output_intent.rb +30 -0
  317. data/lib/pdfrb/model/type/page.rb +127 -0
  318. data/lib/pdfrb/model/type/page_label.rb +68 -0
  319. data/lib/pdfrb/model/type/page_piece_info.rb +58 -0
  320. data/lib/pdfrb/model/type/page_tree_node.rb +51 -16
  321. data/lib/pdfrb/model/type/page_tree_node_root.rb +15 -0
  322. data/lib/pdfrb/model/type/pattern.rb +16 -0
  323. data/lib/pdfrb/model/type/pattern_shading.rb +25 -0
  324. data/lib/pdfrb/model/type/pattern_tiling.rb +25 -0
  325. data/lib/pdfrb/model/type/polygon_annotation.rb +29 -0
  326. data/lib/pdfrb/model/type/polygon_polyline.rb +14 -0
  327. data/lib/pdfrb/model/type/polyline_annotation.rb +29 -0
  328. data/lib/pdfrb/model/type/popup_annotation.rb +20 -0
  329. data/lib/pdfrb/model/type/printer_mark_annotation.rb +13 -0
  330. data/lib/pdfrb/model/type/redact_annotation.rb +28 -0
  331. data/lib/pdfrb/model/type/rendition.rb +31 -0
  332. data/lib/pdfrb/model/type/resources.rb +59 -14
  333. data/lib/pdfrb/model/type/rich_media_activation.rb +24 -0
  334. data/lib/pdfrb/model/type/rich_media_animation.rb +25 -0
  335. data/lib/pdfrb/model/type/rich_media_annotation.rb +18 -0
  336. data/lib/pdfrb/model/type/rich_media_command.rb +19 -0
  337. data/lib/pdfrb/model/type/rich_media_configuration.rb +32 -0
  338. data/lib/pdfrb/model/type/rich_media_content.rb +28 -0
  339. data/lib/pdfrb/model/type/rich_media_cue_point.rb +18 -0
  340. data/lib/pdfrb/model/type/rich_media_deactivation.rb +17 -0
  341. data/lib/pdfrb/model/type/rich_media_instance.rb +33 -0
  342. data/lib/pdfrb/model/type/rich_media_presentation.rb +29 -0
  343. data/lib/pdfrb/model/type/rich_media_settings.rb +33 -0
  344. data/lib/pdfrb/model/type/rich_media_window.rb +20 -0
  345. data/lib/pdfrb/model/type/screen_annotation.rb +19 -0
  346. data/lib/pdfrb/model/type/separation.rb +25 -0
  347. data/lib/pdfrb/model/type/sig_field_lock.rb +25 -0
  348. data/lib/pdfrb/model/type/sig_field_seed_value.rb +25 -0
  349. data/lib/pdfrb/model/type/signature.rb +65 -0
  350. data/lib/pdfrb/model/type/signature_build.rb +86 -0
  351. data/lib/pdfrb/model/type/signature_field.rb +17 -0
  352. data/lib/pdfrb/model/type/software_identifier.rb +19 -0
  353. data/lib/pdfrb/model/type/sound.rb +38 -0
  354. data/lib/pdfrb/model/type/sound_annotation.rb +21 -0
  355. data/lib/pdfrb/model/type/square_annotation.rb +24 -0
  356. data/lib/pdfrb/model/type/square_circle.rb +13 -0
  357. data/lib/pdfrb/model/type/squiggly_annotation.rb +11 -0
  358. data/lib/pdfrb/model/type/stamp_annotation.rb +19 -0
  359. data/lib/pdfrb/model/type/strikeout_annotation.rb +11 -0
  360. data/lib/pdfrb/model/type/struct_elem.rb +75 -0
  361. data/lib/pdfrb/model/type/struct_tree_root.rb +34 -0
  362. data/lib/pdfrb/model/type/structure_attributes.rb +44 -0
  363. data/lib/pdfrb/model/type/structure_element_kid.rb +33 -0
  364. data/lib/pdfrb/model/type/text_annotation.rb +25 -0
  365. data/lib/pdfrb/model/type/text_appearance.rb +9 -0
  366. data/lib/pdfrb/model/type/text_field.rb +21 -0
  367. data/lib/pdfrb/model/type/text_markup_annotation.rb +30 -0
  368. data/lib/pdfrb/model/type/three_d_activation.rb +32 -0
  369. data/lib/pdfrb/model/type/three_d_animation_style.rb +24 -0
  370. data/lib/pdfrb/model/type/three_d_background.rb +28 -0
  371. data/lib/pdfrb/model/type/three_d_cross_section.rb +25 -0
  372. data/lib/pdfrb/model/type/three_d_lighting_scheme.rb +26 -0
  373. data/lib/pdfrb/model/type/three_d_measure.rb +20 -0
  374. data/lib/pdfrb/model/type/three_d_measure_3dc.rb +23 -0
  375. data/lib/pdfrb/model/type/three_d_measure_ad3.rb +14 -0
  376. data/lib/pdfrb/model/type/three_d_measure_ld3.rb +14 -0
  377. data/lib/pdfrb/model/type/three_d_measure_pd3.rb +20 -0
  378. data/lib/pdfrb/model/type/three_d_measure_rd3.rb +14 -0
  379. data/lib/pdfrb/model/type/three_d_node.rb +37 -0
  380. data/lib/pdfrb/model/type/three_d_reference.rb +21 -0
  381. data/lib/pdfrb/model/type/three_d_render_mode.rb +34 -0
  382. data/lib/pdfrb/model/type/three_d_stream.rb +48 -0
  383. data/lib/pdfrb/model/type/three_d_units.rb +18 -0
  384. data/lib/pdfrb/model/type/three_d_view.rb +40 -0
  385. data/lib/pdfrb/model/type/to_unicode_cmap_stream.rb +23 -0
  386. data/lib/pdfrb/model/type/transition.rb +68 -0
  387. data/lib/pdfrb/model/type/underline_annotation.rb +11 -0
  388. data/lib/pdfrb/model/type/ur_transform_parameters.rb +33 -0
  389. data/lib/pdfrb/model/type/uri_dict.rb +18 -0
  390. data/lib/pdfrb/model/type/url_alias.rb +19 -0
  391. data/lib/pdfrb/model/type/variable_text_field.rb +47 -0
  392. data/lib/pdfrb/model/type/viewer_preferences.rb +42 -0
  393. data/lib/pdfrb/model/type/watermark_annotation.rb +13 -0
  394. data/lib/pdfrb/model/type/widget_annotation.rb +48 -0
  395. data/lib/pdfrb/model/type/widget_appearance.rb +9 -0
  396. data/lib/pdfrb/model/type/xobject_form.rb +41 -0
  397. data/lib/pdfrb/model/type/xobject_image.rb +56 -0
  398. data/lib/pdfrb/model/type/xref_stream.rb +37 -0
  399. data/lib/pdfrb/model/type.rb +289 -6
  400. data/lib/pdfrb/serializer.rb +2 -2
  401. data/lib/pdfrb/source/tokenizer.rb +0 -2
  402. data/lib/pdfrb/task/benchmark.rb +83 -0
  403. data/lib/pdfrb/task/extract_text.rb +1 -1
  404. data/lib/pdfrb/task/generate_corpus.rb +130 -0
  405. data/lib/pdfrb/task/memory_profile.rb +84 -0
  406. data/lib/pdfrb/task/optimize.rb +97 -7
  407. data/lib/pdfrb/task/regenerate_appearances.rb +98 -0
  408. data/lib/pdfrb/task.rb +4 -3
  409. data/lib/pdfrb/test_utils.rb +112 -0
  410. data/lib/pdfrb/validator.rb +82 -0
  411. data/lib/pdfrb/version.rb +1 -1
  412. data/lib/pdfrb/writer.rb +117 -27
  413. data/lib/pdfrb/xmp/packet.rb +122 -0
  414. data/lib/pdfrb/xmp/schemas.rb +96 -0
  415. data/lib/pdfrb/xmp.rb +44 -0
  416. data/lib/pdfrb/xref_section.rb +2 -2
  417. data/lib/pdfrb.rb +24 -54
  418. data/pdfrb.gemspec +45 -0
  419. data/script/apply_fixes.sh +843 -0
  420. data/script/make_jpeg_fixture.rb +53 -0
  421. data/script/make_png_fixture.rb +25 -0
  422. metadata +319 -4
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ class Document
5
+ # Catalog display controls. Sets viewer behavior: hide toolbar,
6
+ # page layout, page mode, and open-action destination.
7
+ #
8
+ # /ViewerPreferences — fine-grained viewer controls
9
+ # /PageLayout — how pages are laid out in the viewer
10
+ # /PageMode — which panels are visible on open
11
+ # /OpenAction — initial destination or action
12
+ class Display
13
+ PAGE_LAYOUTS = %i[
14
+ SinglePage OneColumn TwoColumnLeft TwoColumnRight
15
+ TwoPageLeft TwoPageRight
16
+ ].freeze
17
+
18
+ PAGE_MODES = %i[
19
+ UseNone UseOutlines UseThumbs FullScreen UseOC UseAttachments
20
+ ].freeze
21
+
22
+ NON_FULLSCREEN_PAGE_MODES = %i[UseNone UseOutlines UseThumbs UseOC].freeze
23
+
24
+ DIRECTIONS = %i[L2R R2L].freeze
25
+
26
+ attr_reader :document
27
+
28
+ def initialize(document)
29
+ @document = document
30
+ end
31
+
32
+ # @param layout [Symbol] one of PAGE_LAYOUTS.
33
+ def page_layout=(layout)
34
+ raise ArgumentError, "invalid page layout: #{layout}" unless PAGE_LAYOUTS.include?(layout)
35
+
36
+ document.catalog.value[:PageLayout] = layout
37
+ end
38
+
39
+ # @param mode [Symbol] one of PAGE_MODES.
40
+ def page_mode=(mode)
41
+ raise ArgumentError, "invalid page mode: #{mode}" unless PAGE_MODES.include?(mode)
42
+
43
+ document.catalog.value[:PageMode] = mode
44
+ end
45
+
46
+ # Set the /OpenAction destination or action.
47
+ # @param dest_or_action [Array, Hash, Pdfrb::Destination::Fit,
48
+ # Pdfrb::Model::PdfArray] the destination array or action dict.
49
+ def open_action=(dest_or_action)
50
+ document.catalog.value[:OpenAction] = dest_or_action
51
+ end
52
+
53
+ # Get or set /ViewerPreferences as a keyword-hash.
54
+ def viewer_preferences(**opts)
55
+ return read_viewer_preferences if opts.empty?
56
+
57
+ vp = document.catalog.value[:ViewerPreferences]
58
+ vp ||= {}
59
+ opts.each do |key, value|
60
+ vp[key] = value
61
+ end
62
+ document.catalog.value[:ViewerPreferences] = vp
63
+ end
64
+
65
+ # Convenience: hide toolbar and menubar, fit window.
66
+ def presentation_mode!
67
+ viewer_preferences(
68
+ HideToolbar: true,
69
+ HideMenubar: true,
70
+ FitWindow: true,
71
+ CenterWindow: true,
72
+ DisplayDocTitle: true
73
+ )
74
+ self.page_mode = :FullScreen
75
+ end
76
+
77
+ private
78
+
79
+ def read_viewer_preferences
80
+ document.catalog.value[:ViewerPreferences] || {}
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,14 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "stringio"
4
+
3
5
  module Pdfrb
4
6
  class Document
5
- # Font facade. +add+ registers a font in the catalog /Resources
6
- # (so the canvas can reference it via +Tf+) and returns the
7
- # resource name to use with the canvas.
8
- #
9
- # Supports the 14 PDF standard Type1 fonts by name (Helvetica,
10
- # Times-Roman, Courier, etc.) without embedding. Future TTF/OTF
11
- # loaders (TODO 111/112) plug in via +register_loader+.
12
7
  class Fonts
13
8
  STANDARDS = %w[
14
9
  Helvetica Helvetica-Bold Helvetica-Oblique Helvetica-BoldOblique
@@ -17,78 +12,421 @@ module Pdfrb
17
12
  Symbol ZapfDingbats
18
13
  ].freeze
19
14
 
15
+ NO_ENCODING_FONTS = %w[Symbol ZapfDingbats].freeze
16
+ DEFAULT_WIDTH = 500
17
+
20
18
  attr_reader :document
21
19
 
22
20
  def initialize(document)
23
21
  @document = document
24
22
  @next_id = 1
25
- @registry = {} # name -> resource Symbol
23
+ @registry = {}
24
+ @encodings = {}
25
+ @font_dicts = {}
26
+ @afm_metrics = {}
27
+ @used_codepoints = Hash.new { |h, k| h[k] = Set.new }
28
+ @font_streams = {}
26
29
  end
27
30
 
28
- # Register a font and return the resource name (e.g. :F1).
29
- #
30
- # For the 14 standard fonts: pass the font name; no embedding
31
- # is required. For everything else, use the loader protocol
32
- # (TODO 111).
33
- def add(name_or_io, **opts)
31
+ def add(name_or_io, **)
34
32
  name = font_name_for(name_or_io)
35
33
  cached = @registry[name]
36
34
  return cached if cached
37
35
 
38
36
  resource = next_resource_name
39
- register_font(resource, name, **opts)
37
+ font_dict = register_font(resource, name, **)
38
+ @encodings[resource] = font_dict&.value&.[](:Encoding)
39
+ @font_dicts[resource] = font_dict
40
+ load_afm_metrics(resource, name)
41
+ if @pending_io_data
42
+ load_ttf_metrics(resource, @pending_io_data) unless @afm_metrics[resource]
43
+ @font_streams[resource] = @pending_io_data
44
+ @pending_io_data = nil
45
+ end
46
+ @pending_subtype = nil
47
+ if @afm_metrics[resource] && font_dict&.value&.[](:Widths)
48
+ font_dict.value[:Widths] = @afm_metrics[resource][:widths]
49
+ end
40
50
  @registry[name] = resource
41
51
  resource
42
52
  end
43
53
 
44
- # Look up the resource name registered for a font name.
45
- def [](name)
46
- @registry[name]
47
- end
54
+ def [](name); @registry[name]; end
48
55
 
49
- def each(&block)
56
+ def each(&)
50
57
  return enum_for(:each) unless block_given?
51
58
 
52
- @registry.each(&block)
59
+ @registry.each(&)
53
60
  self
54
61
  end
55
62
 
56
- # ---- Extension point (TODOs 110-112) ----
57
- #
58
- # A loader is an object with `call(document, name_or_io, **opts)`
59
- # that returns a Font dict + descriptor pair. Built-in loader
60
- # handles the 14 standards; future loaders add TTF/OTF/CID.
61
- class << self
62
- def loaders
63
- @loaders ||= []
63
+ def used_codepoints(resource); @used_codepoints[resource]; end
64
+ def encoding_for(resource); @encodings[resource]; end
65
+
66
+ def encode_text(text, resource)
67
+ text.to_s.each_codepoint { |cp| @used_codepoints[resource] << cp }
68
+ enc = @encodings[resource]
69
+ return text.to_s.b unless enc
70
+
71
+ Pdfrb::Font::Encoding.encode(enc, text.to_s)
72
+ end
73
+
74
+ def encodable?(text, resource); !encode_text(text, resource).include?("?"); end
75
+
76
+ def measure_text(text, font:, size:)
77
+ return 0 unless text && size
78
+
79
+ metrics = @afm_metrics[font]
80
+ return text.to_s.length * size.to_f * 0.5 unless metrics
81
+
82
+ total = text.to_s.each_char.sum do |ch|
83
+ byte = ch.bytes.first || 0
84
+ metrics[:widths][byte] || DEFAULT_WIDTH
64
85
  end
86
+ total * size.to_f / 1000.0
87
+ end
88
+
89
+ def text_width(text, _resource, size:)
90
+ return 0 unless text && size
91
+
92
+ res = @afm_metrics[_resource] ? _resource : (@registry[_resource.to_s] || _resource)
93
+ metrics = @afm_metrics[res]
94
+ return text.to_s.length * size.to_f * 0.5 unless metrics
95
+
96
+ upem = metrics[:units_per_em] || 1000
97
+ total = text.to_s.each_char.sum do |ch|
98
+ byte = ch.bytes.first || 0
99
+ metrics[:widths][byte] || DEFAULT_WIDTH
100
+ end
101
+ total * size.to_f / upem.to_f
102
+ end
103
+
104
+ def glyph_width(resource, codepoint)
105
+ metrics = @afm_metrics[resource]
106
+ return DEFAULT_WIDTH unless metrics
107
+
108
+ cp = codepoint.is_a?(String) ? (codepoint.each_codepoint.first || 0) : codepoint.to_i
109
+ return 0 if cp > 255
110
+
111
+ metrics[:widths][cp] || 0
112
+ end
113
+
114
+ def glyph_widths(resource, codepoints)
115
+ cps = codepoints.is_a?(String) ? codepoints.each_codepoint.to_a : codepoints
116
+ cps.map { |cp| glyph_width(resource, cp) }
117
+ end
118
+
119
+ def metrics_for(resource); @afm_metrics[resource]; end
120
+
121
+ def valid_font_data?(data)
122
+ return false unless data.is_a?(String) && data.bytesize >= 4
123
+
124
+ magic = data.byteslice(0, 4)
125
+ ["ttcf".b, "\x00\x01\x00\x00".b, "OTTO".b, "true".b, "typ1".b].include?(magic)
126
+ end
127
+
128
+ def embedded?(resource)
129
+ dict = @font_dicts[resource]
130
+ return false unless dict
131
+
132
+ desc = dict.value[:FontDescriptor]
133
+ return false unless desc
134
+
135
+ desc = document.object(desc) if desc.is_a?(Pdfrb::Model::Reference)
136
+ desc&.value&.key?(:FontFile2)
137
+ end
65
138
 
66
- def register_loader(loader)
67
- loaders.unshift(loader)
139
+ def subset_fonts!
140
+ @font_streams.each do |resource, data|
141
+ next unless valid_font_data?(data)
142
+
143
+ codepoints = @used_codepoints[resource]
144
+ next if codepoints.empty?
145
+
146
+ begin
147
+ ttf = Pdfrb::Font::TrueType::File.new(data)
148
+ subsetter = Pdfrb::Font::TrueType::Subsetter.new(ttf)
149
+ subset = subsetter.subset(codepoints.to_a)
150
+ dict = @font_dicts[resource]
151
+ next unless dict
152
+
153
+ desc_ref = dict.value[:FontDescriptor]
154
+ next unless desc_ref
155
+
156
+ desc = desc_ref.is_a?(Pdfrb::Model::Reference) ? document.object(desc_ref) : desc_ref
157
+ next unless desc
158
+
159
+ fd_stream = document.add({ Length: subset.bytesize }, type: Pdfrb::Model::Cos::Stream)
160
+ fd_stream.stream = subset
161
+ desc.value[:FontFile2] = Pdfrb::Model::Reference.new(fd_stream.oid, fd_stream.gen)
162
+ rescue StandardError
163
+ next
164
+ end
68
165
  end
69
166
  end
70
167
 
168
+ class << self
169
+ def loaders; @loaders ||= []; end
170
+ def register_loader(loader); loaders.unshift(loader); end
171
+ end
172
+
71
173
  register_loader ->(doc, name, **opts) {
72
174
  next nil unless STANDARDS.include?(name.to_s)
73
- next nil unless opts[:embedded].nil? # standards never embed
175
+ next nil unless opts[:embedded].nil?
74
176
 
75
- doc.add(
76
- { Type: :Font, Subtype: :Type1, BaseFont: name.to_sym },
77
- type: Pdfrb::Model::Type::FontType1
78
- )
177
+ widths = Array.new(256, DEFAULT_WIDTH)
178
+ tu_stream = build_tounicode(doc)
179
+ tu_ref = Pdfrb::Model::Reference.new(tu_stream.oid, tu_stream.gen)
180
+ fd = doc.add({
181
+ Type: :FontDescriptor, FontName: name.to_sym, Flags: 32,
182
+ FontBBox: [0, 0, 1000, 1000], ItalicAngle: 0,
183
+ Ascent: 800, Descent: -200, CapHeight: 700, StemV: 80
184
+ }, type: Pdfrb::Model::Cos::Dictionary)
185
+ fd_ref = Pdfrb::Model::Reference.new(fd.oid, fd.gen)
186
+ font_hash = {
187
+ Type: :Font, Subtype: :Type1, BaseFont: name.to_sym,
188
+ FirstChar: 0, LastChar: 255, Widths: widths,
189
+ FontDescriptor: fd_ref, ToUnicode: tu_ref
190
+ }
191
+ font_hash[:Encoding] = :WinAnsiEncoding unless NO_ENCODING_FONTS.include?(name.to_s)
192
+ doc.add(font_hash, type: Pdfrb::Model::Type::FontType1)
79
193
  }
80
194
 
195
+ def self.build_tounicode(doc)
196
+ cmap_body = build_tounicode_cmap
197
+ stream = doc.add({ Length: cmap_body.bytesize }, type: Pdfrb::Model::Cos::Stream)
198
+ stream.stream = cmap_body
199
+ stream
200
+ end
201
+
202
+ def self.build_tounicode_cmap
203
+ lines = []
204
+ lines << "/CIDInit /ProcSet findresource begin"
205
+ lines << "12 dict begin"
206
+ lines << "begincmap"
207
+ lines << "/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def"
208
+ lines << "/CMapName /Adobe-Identity-UCS def"
209
+ lines << "/CMapType 2 def"
210
+ lines << "1 begincodespacerange"
211
+ lines << "<00> <FF>"
212
+ lines << "endcodespacerange"
213
+ table = Pdfrb::Font::Encoding::WinAnsiEncoding::TABLE
214
+ pairs = []
215
+ table.each_with_index { |cp, byte| pairs << format("<%02X> <%04X>", byte, cp) if cp }
216
+ pairs.each_slice(100) do |chunk|
217
+ lines << "#{chunk.length} beginbfchar"
218
+ lines.concat(chunk)
219
+ lines << "endbfchar"
220
+ end
221
+ lines << "endcmap"
222
+ lines << "CMapName currentdict /CMap defineresource pop"
223
+ lines << "end"
224
+ lines << "end"
225
+ "#{lines.join("\n")}\n"
226
+ end
227
+
81
228
  private
82
229
 
230
+ def load_afm_metrics(resource, name)
231
+ return unless STANDARDS.include?(name.to_s)
232
+
233
+ afm_path = Pdfrb::DataDir.resolve("afm", "#{name}.afm")
234
+ return unless File.exist?(afm_path)
235
+
236
+ begin
237
+ parser = Pdfrb::Font::AFMParser.from_file(afm_path)
238
+ widths = build_winansi_widths(parser)
239
+ @afm_metrics[resource] = {
240
+ widths: widths,
241
+ ascent: parser.ascender || 800,
242
+ descent: parser.descender || -200,
243
+ cap_height: parser.cap_height || 700,
244
+ bbox: parser.bbox || [0, 0, 1000, 1000],
245
+ units_per_em: 1000,
246
+ }
247
+ rescue StandardError
248
+ nil
249
+ end
250
+ end
251
+
252
+ def load_ttf_metrics(resource, data)
253
+ return unless valid_font_data?(data)
254
+
255
+ begin
256
+ ttf = Pdfrb::Font::TrueType::File.new(data)
257
+ widths = Array.new(256, DEFAULT_WIDTH)
258
+
259
+ table = Pdfrb::Font::Encoding::WinAnsiEncoding::TABLE
260
+ table.each_with_index do |cp, byte|
261
+ next unless cp
262
+
263
+ begin
264
+ glyph_id = ttf.cmap.glyph_id_for(cp)
265
+ next unless glyph_id&.positive?
266
+
267
+ w = ttf.hmtx.advance_width(glyph_id)
268
+ widths[byte] = w if w&.positive?
269
+ rescue StandardError
270
+ next
271
+ end
272
+ end
273
+
274
+ head = ttf.head
275
+ hhea = ttf.hhea
276
+ @afm_metrics[resource] = {
277
+ widths: widths,
278
+ ascent: hhea.ascender || 800,
279
+ descent: hhea.descender || -200,
280
+ cap_height: nil,
281
+ bbox: head.bbox || [0, 0, 1000, 1000],
282
+ units_per_em: head.units_per_em || 1000,
283
+ }
284
+ rescue StandardError
285
+ nil
286
+ end
287
+ end
288
+
289
+ def build_winansi_widths(parser)
290
+ widths = Array.new(256, DEFAULT_WIDTH)
291
+ (0..127).each do |byte|
292
+ metric = parser.char_metrics[byte]
293
+ widths[byte] = metric[:width].to_i if metric && metric[:width]
294
+ end
295
+ table = Pdfrb::Font::Encoding::WinAnsiEncoding::TABLE
296
+ (128..255).each do |byte|
297
+ cp = table[byte]
298
+ next unless cp
299
+
300
+ glyph_name = unicode_to_glyph_name(cp)
301
+ next unless glyph_name
302
+
303
+ metric = parser.char_metrics[glyph_name]
304
+ widths[byte] = metric[:width].to_i if metric && metric[:width]
305
+ end
306
+ widths
307
+ end
308
+
309
+ def unicode_to_glyph_name(cp)
310
+ return cp.chr if cp < 128
311
+
312
+ { 0x20AC => "Euro", 0x201A => "quotesinglbase", 0x0192 => "florin", 0x201E => "quotedblbase",
313
+ 0x2026 => "ellipsis", 0x2020 => "dagger", 0x2021 => "daggerdbl", 0x02C6 => "circumflex",
314
+ 0x2030 => "perthousand", 0x0160 => "Scaron", 0x2039 => "guilsinglleft", 0x0152 => "OE",
315
+ 0x017D => "Zcaron", 0x2018 => "quoteleft", 0x2019 => "quoteright", 0x201C => "quotedblleft",
316
+ 0x201D => "quotedblright", 0x2022 => "bullet", 0x2013 => "endash", 0x2014 => "emdash",
317
+ 0x02DC => "tilde", 0x2122 => "trademark", 0x0161 => "scaron", 0x203A => "guilsinglright",
318
+ 0x0153 => "oe", 0x017E => "zcaron", 0x0178 => "Ydieresis", 0x00A0 => "space",
319
+ 0x00A1 => "exclamdown", 0x00A2 => "cent", 0x00A3 => "sterling", 0x00A4 => "currency",
320
+ 0x00A5 => "yen", 0x00A6 => "brokenbar", 0x00A7 => "section", 0x00A8 => "dieresis",
321
+ 0x00A9 => "copyright", 0x00AA => "ordfeminine", 0x00AB => "guillemotleft",
322
+ 0x00AC => "logicalnot", 0x00AD => "hyphen", 0x00AE => "registered", 0x00AF => "macron",
323
+ 0x00B0 => "degree", 0x00B1 => "plusminus", 0x00B2 => "twosuperior", 0x00B3 => "threesuperior",
324
+ 0x00B4 => "acute", 0x00B5 => "mu", 0x00B6 => "paragraph", 0x00B7 => "periodcentered",
325
+ 0x00B8 => "cedilla", 0x00B9 => "onesuperior", 0x00BA => "ordmasculine",
326
+ 0x00BB => "guillemotright", 0x00BC => "onequarter", 0x00BD => "onehalf",
327
+ 0x00BE => "threequarters", 0x00BF => "questiondown", 0x00C0 => "Agrave",
328
+ 0x00C1 => "Aacute", 0x00C2 => "Acircumflex", 0x00C3 => "Atilde", 0x00C4 => "Adieresis",
329
+ 0x00C5 => "Aring", 0x00C6 => "AE", 0x00C7 => "Ccedilla", 0x00C8 => "Egrave",
330
+ 0x00C9 => "Eacute", 0x00CA => "Ecircumflex", 0x00CB => "Edieresis", 0x00CC => "Igrave",
331
+ 0x00CD => "Iacute", 0x00CE => "Icircumflex", 0x00CF => "Idieresis", 0x00D0 => "Eth",
332
+ 0x00D1 => "Ntilde", 0x00D2 => "Ograve", 0x00D3 => "Oacute", 0x00D4 => "Ocircumflex",
333
+ 0x00D5 => "Otilde", 0x00D6 => "Odieresis", 0x00D7 => "multiply", 0x00D8 => "Oslash",
334
+ 0x00D9 => "Ugrave", 0x00DA => "Uacute", 0x00DB => "Ucircumflex", 0x00DC => "Udieresis",
335
+ 0x00DD => "Yacute", 0x00DE => "Thorn", 0x00DF => "germandbls", 0x00E0 => "agrave",
336
+ 0x00E1 => "aacute", 0x00E2 => "acircumflex", 0x00E3 => "atilde", 0x00E4 => "adieresis",
337
+ 0x00E5 => "aring", 0x00E6 => "ae", 0x00E7 => "ccedilla", 0x00E8 => "egrave",
338
+ 0x00E9 => "eacute", 0x00EA => "ecircumflex", 0x00EB => "edieresis", 0x00EC => "igrave",
339
+ 0x00ED => "iacute", 0x00EE => "icircumflex", 0x00EF => "idieresis", 0x00F0 => "eth",
340
+ 0x00F1 => "ntilde", 0x00F2 => "ograve", 0x00F3 => "oacute", 0x00F4 => "ocircumflex",
341
+ 0x00F5 => "otilde", 0x00F6 => "odieresis", 0x00F7 => "divide", 0x00F8 => "oslash",
342
+ 0x00F9 => "ugrave", 0x00FA => "uacute", 0x00FB => "ucircumflex", 0x00FC => "udieresis",
343
+ 0x00FD => "yacute", 0x00FE => "thorn", 0x00FF => "ydieresis" }[cp]
344
+ end
345
+
83
346
  def font_name_for(name_or_io)
84
347
  case name_or_io
85
- when Symbol, String then name_or_io.to_s
348
+ when Symbol
349
+ name_or_io.to_s
350
+ when String
351
+ if File.file?(name_or_io)
352
+ @pending_io_data = File.binread(name_or_io)
353
+ @pending_subtype = true_type_subtype(@pending_io_data)
354
+ subset_tag = subset_tag_for(@pending_io_data)
355
+ base = postscript_name_for(@pending_io_data) ||
356
+ File.basename(name_or_io, ".*")
357
+ "#{subset_tag}+#{base}"
358
+ else
359
+ name_or_io.to_s
360
+ end
361
+ when IO, StringIO
362
+ @pending_io_data = name_or_io.read
363
+ @pending_subtype = true_type_subtype(@pending_io_data)
364
+ unless valid_font_data?(@pending_io_data)
365
+ Pdfrb.logger&.warn("Font data does not look like a valid TTF/OTF")
366
+ end
367
+ subset_tag = subset_tag_for(@pending_io_data)
368
+ base = postscript_name_for(@pending_io_data) ||
369
+ "EmbeddedFont#{@pending_io_data.bytesize}"
370
+ "#{subset_tag}+#{base}"
86
371
  else
87
- raise ArgumentError,
88
- "font name must be a String or Symbol; TTF/OTF IO loading lands in TODO 111"
372
+ raise ArgumentError, "font name must be a String, Symbol, or IO"
89
373
  end
90
374
  end
91
375
 
376
+ # Extract the PostScript name (name table nameID 6) from raw
377
+ # TTF/OTF bytes. Returns nil if the data isn't parseable or the
378
+ # name record is missing. Used to derive a meaningful BaseFont
379
+ # name (e.g. "MinionPro-Regular") instead of falling back to a
380
+ # size-derived placeholder.
381
+ def postscript_name_for(font_bytes)
382
+ return nil unless font_bytes && font_bytes.bytesize >= 12
383
+ return nil unless valid_font_data?(font_bytes)
384
+
385
+ ttf = Pdfrb::Font::TrueType::File.new(font_bytes)
386
+ name = ttf.name_table_parsed
387
+ return nil unless name
388
+
389
+ sanitize_ps_name(name.ps_name)
390
+ end
391
+
392
+ # PDF /BaseFont names must be a Name token: no whitespace, no
393
+ # delimiter chars (()[<>/{%]). Trim and substitute.
394
+ def sanitize_ps_name(name)
395
+ return nil if name.nil? || name.empty?
396
+
397
+ name.to_s.gsub(/[\s()\[\]<>{}\/%]+/, "-")
398
+ end
399
+
400
+ # Generate a deterministic 6-letter subset tag from the font bytes
401
+ # so re-embedding the same font produces the same prefix. Format
402
+ # is six uppercase ASCII letters derived from the SHA-1 of the
403
+ # first 1 KB of the font file (per PDF spec s9.6.4).
404
+ def subset_tag_for(font_bytes)
405
+ require "digest"
406
+ digest = Digest::SHA1.digest(font_bytes.byteslice(0, 1024) || "")
407
+ digest.byteslice(0, 6).bytes.each_with_object(+"") do |b, s|
408
+ s << ((b % 26) + 65).chr
409
+ end
410
+ end
411
+
412
+ def true_type_subtype(data)
413
+ magic = data&.byteslice(0, 4)
414
+ # Per PDF spec s9.6.2 + s9.9: magic distinguishes the two
415
+ # outline formats. "OTTO" means OpenType with CFF outlines,
416
+ # which must be embedded via /FontFile3 + /Subtype /OpenType
417
+ # and treated as /Subtype /Type1 (CFF) — NOT as /Subtype
418
+ # /TrueType, which was the bug (Issue #62).
419
+ return :TrueType if magic == "\x00\x01\x00\x00".b
420
+ return :TrueType if magic == "true".b
421
+ return :Type1 if magic == "OTTO".b
422
+
423
+ nil
424
+ end
425
+
426
+ def cff_font?(data)
427
+ true_type_subtype(data) == :Type1
428
+ end
429
+
92
430
  def next_resource_name
93
431
  sym = :"F#{@next_id}"
94
432
  @next_id += 1
@@ -97,16 +435,50 @@ module Pdfrb
97
435
 
98
436
  def register_font(resource, name, **opts)
99
437
  loader = self.class.loaders.find { |l| l.call(document, name, **opts) }
100
- font_dict = loader ? loader.call(document, name, **opts) :
101
- default_font(name)
438
+ font_dict = loader ? loader.call(document, name, **opts) : default_font(name)
102
439
  attach_to_resources(resource, font_dict)
440
+ font_dict
103
441
  end
104
442
 
105
443
  def default_font(name)
106
- document.add(
107
- { Type: :Font, Subtype: :Type1, BaseFont: name.to_sym },
108
- type: Pdfrb::Model::Type::FontType1
109
- )
444
+ tu_stream = self.class.build_tounicode(document)
445
+ tu_ref = Pdfrb::Model::Reference.new(tu_stream.oid, tu_stream.gen)
446
+
447
+ fd = document.add({
448
+ Type: :FontDescriptor, FontName: name.to_sym, Flags: 32,
449
+ FontBBox: [0, 0, 1000, 1000], ItalicAngle: 0,
450
+ Ascent: 800, Descent: -200, CapHeight: 700, StemV: 80
451
+ }, type: Pdfrb::Model::Cos::Dictionary)
452
+ fd_ref = Pdfrb::Model::Reference.new(fd.oid, fd.gen)
453
+
454
+ if @pending_io_data && valid_font_data?(@pending_io_data)
455
+ font_file = document.add(
456
+ { Length: @pending_io_data.bytesize },
457
+ type: Pdfrb::Model::Cos::Stream
458
+ )
459
+ font_file.stream = @pending_io_data
460
+ # Per PDF spec s9.9: TrueType outlines → FontFile2; CFF (OTF)
461
+ # outlines → FontFile3 with /Subtype /OpenType. Issue #62
462
+ # was embedding OTF/CFF as TrueType via FontFile2.
463
+ if cff_font?(@pending_io_data)
464
+ font_file.value[:Subtype] = :OpenType
465
+ font_file.value[:Length1] = @pending_io_data.bytesize
466
+ fd.value[:FontFile3] =
467
+ Pdfrb::Model::Reference.new(font_file.oid, font_file.gen)
468
+ else
469
+ font_file.value[:Length1] = @pending_io_data.bytesize
470
+ fd.value[:FontFile2] =
471
+ Pdfrb::Model::Reference.new(font_file.oid, font_file.gen)
472
+ end
473
+ end
474
+
475
+ subtype = @pending_subtype || :Type1
476
+ document.add({
477
+ Type: :Font, Subtype: subtype, BaseFont: name.to_sym,
478
+ Encoding: :WinAnsiEncoding, FirstChar: 0, LastChar: 255,
479
+ Widths: Array.new(256, DEFAULT_WIDTH),
480
+ FontDescriptor: fd_ref, ToUnicode: tu_ref
481
+ }, type: subtype == :TrueType ? Pdfrb::Model::Type::FontTrueType : Pdfrb::Model::Type::FontType1)
110
482
  end
111
483
 
112
484
  def attach_to_resources(resource, font_dict)