pdfrb 0.3.0 → 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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/Gemfile +1 -0
- data/PROPOSAL.canvas-clip.md +92 -0
- data/PROPOSAL.canvas-draw-image-matrix.md +87 -0
- data/PROPOSAL.canvas-rich-text.md +103 -0
- data/PROPOSAL.canvas-text-lines.md +77 -0
- data/PROPOSAL.canvas-transparency.md +106 -0
- data/PROPOSAL.external-font-metrics.md +129 -0
- data/PROPOSAL.font-embedding-fontfile2.md +88 -0
- data/PROPOSAL.font-resolver-ttc.md +105 -0
- data/PROPOSAL.font-resolver.md +96 -0
- data/PROPOSAL.font-subsetting.md +98 -0
- data/PROPOSAL.gradient-shadings.md +110 -0
- data/PROPOSAL.page-mediabox-setter.md +66 -0
- data/PROPOSAL.per-glyph-width.md +91 -0
- data/PROPOSAL.stringio-require.md +53 -0
- data/PROPOSAL.struct-tree-root.md +102 -0
- data/PROPOSAL.text-measurement.md +93 -0
- data/PROPOSAL.ttf-table-parsing-fix.md +112 -0
- data/RELEASE.md +48 -0
- data/lib/pdfrb/action/base.rb +21 -0
- data/lib/pdfrb/action/types.rb +178 -0
- data/lib/pdfrb/action.rb +55 -0
- data/lib/pdfrb/annotation/base.rb +79 -0
- data/lib/pdfrb/annotation/types.rb +222 -0
- data/lib/pdfrb/annotation.rb +78 -0
- data/lib/pdfrb/appearance/generator.rb +216 -0
- data/lib/pdfrb/appearance.rb +11 -0
- data/lib/pdfrb/arlington/object_definition.rb +2 -2
- data/lib/pdfrb/arlington/predicate/evaluator.rb +1 -1
- data/lib/pdfrb/arlington/predicate/functions.rb +25 -9
- data/lib/pdfrb/arlington/predicate.rb +1 -5
- data/lib/pdfrb/cli/batch.rb +18 -0
- data/lib/pdfrb/cli/debug_info.rb +18 -0
- data/lib/pdfrb/cli/files.rb +16 -0
- data/lib/pdfrb/cli/fonts.rb +16 -0
- data/lib/pdfrb/cli/form.rb +18 -0
- data/lib/pdfrb/cli/image2pdf.rb +18 -0
- data/lib/pdfrb/cli/images.rb +34 -0
- data/lib/pdfrb/cli/info.rb +20 -0
- data/lib/pdfrb/cli/inspect.rb +18 -0
- data/lib/pdfrb/cli/merge.rb +18 -0
- data/lib/pdfrb/cli/modify.rb +18 -0
- data/lib/pdfrb/cli/optimize.rb +18 -0
- data/lib/pdfrb/cli/split.rb +21 -0
- data/lib/pdfrb/cli/watermark.rb +18 -0
- data/lib/pdfrb/cli.rb +291 -7
- data/lib/pdfrb/color/color_space.rb +259 -0
- data/lib/pdfrb/color/icc_profile.rb +90 -0
- data/lib/pdfrb/color.rb +12 -0
- data/lib/pdfrb/compare/comparator.rb +244 -0
- data/lib/pdfrb/compare/report.rb +78 -0
- data/lib/pdfrb/compare.rb +42 -0
- data/lib/pdfrb/composer.rb +161 -0
- data/lib/pdfrb/conformance/pdf_a.rb +317 -56
- data/lib/pdfrb/conformance/pdf_ua.rb +186 -29
- data/lib/pdfrb/conformance/pdf_x.rb +247 -0
- data/lib/pdfrb/conformance/rule.rb +78 -0
- data/lib/pdfrb/conformance/structure_elements.rb +174 -0
- data/lib/pdfrb/conformance/verapdf_bridge.rb +92 -0
- data/lib/pdfrb/conformance.rb +7 -3
- data/lib/pdfrb/content/canvas.rb +217 -72
- data/lib/pdfrb/content/color_space.rb +157 -0
- data/lib/pdfrb/content/hidden_text_detector.rb +167 -0
- data/lib/pdfrb/content/operator.rb +21 -1
- data/lib/pdfrb/content/operators/clipping.rb +36 -0
- data/lib/pdfrb/content/operators/graphics_state_params.rb +15 -0
- data/lib/pdfrb/content/operators.rb +21 -0
- data/lib/pdfrb/content/shading/base.rb +139 -0
- data/lib/pdfrb/content/shading.rb +17 -0
- data/lib/pdfrb/content/smart_text_extractor.rb +85 -0
- data/lib/pdfrb/content/tiling_pattern.rb +78 -0
- data/lib/pdfrb/content/transformation_matrix.rb +61 -0
- data/lib/pdfrb/content.rb +7 -26
- data/lib/pdfrb/destination/fit.rb +185 -0
- data/lib/pdfrb/destination.rb +13 -0
- data/lib/pdfrb/digital_signature/cms_handler.rb +22 -0
- data/lib/pdfrb/digital_signature/signing.rb +107 -11
- data/lib/pdfrb/digital_signature/timestamp_handler.rb +45 -0
- data/lib/pdfrb/digital_signature/verification.rb +88 -9
- data/lib/pdfrb/digital_signature/verification_result.rb +14 -0
- data/lib/pdfrb/digital_signature.rb +26 -2
- data/lib/pdfrb/document/annotations.rb +120 -3
- data/lib/pdfrb/document/associated_files.rb +104 -0
- data/lib/pdfrb/document/colors.rb +95 -0
- data/lib/pdfrb/document/destinations.rb +127 -5
- data/lib/pdfrb/document/display.rb +84 -0
- data/lib/pdfrb/document/fonts.rb +417 -45
- data/lib/pdfrb/document/form.rb +338 -0
- data/lib/pdfrb/document/graphics_state.rb +91 -0
- data/lib/pdfrb/document/images.rb +4 -4
- data/lib/pdfrb/document/info.rb +102 -0
- data/lib/pdfrb/document/layers.rb +62 -0
- data/lib/pdfrb/document/metadata.rb +12 -5
- data/lib/pdfrb/document/output_intents.rb +91 -0
- data/lib/pdfrb/document/page_labels.rb +80 -0
- data/lib/pdfrb/document/pages.rb +68 -19
- data/lib/pdfrb/document/portfolio.rb +119 -0
- data/lib/pdfrb/document/shadings.rb +159 -0
- data/lib/pdfrb/document/stamps.rb +68 -0
- data/lib/pdfrb/document/structure.rb +242 -0
- data/lib/pdfrb/document.rb +73 -4
- data/lib/pdfrb/encryption/identity.rb +13 -0
- data/lib/pdfrb/encryption/password_verification.rb +53 -8
- data/lib/pdfrb/encryption/security_handler.rb +8 -5
- data/lib/pdfrb/encryption/standard_security_handler.rb +154 -81
- data/lib/pdfrb/encryption.rb +17 -0
- data/lib/pdfrb/error.rb +2 -2
- data/lib/pdfrb/filter/brotli_decode.rb +32 -0
- data/lib/pdfrb/filter/ccitt_fax_decode.rb +12 -0
- data/lib/pdfrb/filter/crypt.rb +2 -2
- data/lib/pdfrb/filter/dct_decode.rb +18 -0
- data/lib/pdfrb/filter/flate_decode.rb +4 -18
- data/lib/pdfrb/filter/jbig2_decode.rb +12 -0
- data/lib/pdfrb/filter/jpx_decode.rb +18 -0
- data/lib/pdfrb/filter/lzw_decode.rb +3 -2
- data/lib/pdfrb/filter/png_predictor.rb +94 -0
- data/lib/pdfrb/filter.rb +26 -13
- data/lib/pdfrb/font/afm_parser.rb +50 -26
- data/lib/pdfrb/font/cmap/writer.rb +9 -0
- data/lib/pdfrb/font/encoding/win_ansi_encoding.rb +17 -0
- data/lib/pdfrb/font/encoding.rb +32 -3
- data/lib/pdfrb/font/metrics_helper.rb +121 -0
- data/lib/pdfrb/font/true_type/file.rb +10 -1
- data/lib/pdfrb/font/true_type/glyf.rb +82 -0
- data/lib/pdfrb/font/true_type/kern.rb +58 -0
- data/lib/pdfrb/font/true_type/loca.rb +42 -0
- data/lib/pdfrb/font/true_type/maxp.rb +44 -0
- data/lib/pdfrb/font/true_type/name.rb +53 -0
- data/lib/pdfrb/font/true_type/post.rb +93 -0
- data/lib/pdfrb/font/true_type/subsetter.rb +3 -5
- data/lib/pdfrb/font/true_type/wrapper.rb +102 -0
- data/lib/pdfrb/font/true_type.rb +7 -0
- data/lib/pdfrb/font.rb +2 -0
- data/lib/pdfrb/font_loader/from_configuration.rb +19 -0
- data/lib/pdfrb/font_loader/from_file.rb +20 -0
- data/lib/pdfrb/font_loader/standard14.rb +20 -0
- data/lib/pdfrb/font_loader/variant_from_name.rb +36 -0
- data/lib/pdfrb/font_loader.rb +4 -0
- data/lib/pdfrb/font_resolver.rb +138 -0
- data/lib/pdfrb/image/audit.rb +94 -0
- data/lib/pdfrb/image/downsampler.rb +124 -0
- data/lib/pdfrb/image.rb +13 -0
- data/lib/pdfrb/image_loader/jpeg.rb +39 -58
- data/lib/pdfrb/image_loader/png.rb +31 -257
- data/lib/pdfrb/layout/bidi.rb +198 -0
- data/lib/pdfrb/layout/box.rb +49 -0
- data/lib/pdfrb/layout/box_fitter.rb +54 -0
- data/lib/pdfrb/layout/column_box.rb +71 -0
- data/lib/pdfrb/layout/container_box.rb +45 -0
- data/lib/pdfrb/layout/frame.rb +67 -0
- data/lib/pdfrb/layout/image_box.rb +44 -0
- data/lib/pdfrb/layout/inline_box.rb +25 -0
- data/lib/pdfrb/layout/line.rb +55 -0
- data/lib/pdfrb/layout/list_box.rb +87 -0
- data/lib/pdfrb/layout/numeric_refinements.rb +43 -0
- data/lib/pdfrb/layout/page_style.rb +65 -0
- data/lib/pdfrb/layout/style.rb +62 -0
- data/lib/pdfrb/layout/table_box.rb +66 -0
- data/lib/pdfrb/layout/text_box.rb +38 -0
- data/lib/pdfrb/layout/text_fragment.rb +45 -0
- data/lib/pdfrb/layout/text_layouter.rb +123 -0
- data/lib/pdfrb/layout.rb +28 -0
- data/lib/pdfrb/linearization/hint_stream.rb +93 -0
- data/lib/pdfrb/linearization/writer.rb +259 -0
- data/lib/pdfrb/linearization.rb +21 -0
- data/lib/pdfrb/model/cos/dictionary.rb +12 -0
- data/lib/pdfrb/model/pdf_array.rb +2 -2
- data/lib/pdfrb/model/type/action.rb +42 -0
- data/lib/pdfrb/model/type/action_go_to_3d_view.rb +14 -0
- data/lib/pdfrb/model/type/action_go_to_dp.rb +16 -0
- data/lib/pdfrb/model/type/action_go_to_r.rb +26 -0
- data/lib/pdfrb/model/type/action_goto.rb +34 -0
- data/lib/pdfrb/model/type/action_hide.rb +26 -0
- data/lib/pdfrb/model/type/action_import_data.rb +18 -0
- data/lib/pdfrb/model/type/action_java_script.rb +18 -0
- data/lib/pdfrb/model/type/action_launch.rb +22 -0
- data/lib/pdfrb/model/type/action_movie.rb +22 -0
- data/lib/pdfrb/model/type/action_named.rb +23 -0
- data/lib/pdfrb/model/type/action_nop.rb +12 -0
- data/lib/pdfrb/model/type/action_rendition.rb +28 -0
- data/lib/pdfrb/model/type/action_reset_form.rb +22 -0
- data/lib/pdfrb/model/type/action_rich_media_execute.rb +17 -0
- data/lib/pdfrb/model/type/action_set_ocg_state.rb +32 -0
- data/lib/pdfrb/model/type/action_set_state.rb +14 -0
- data/lib/pdfrb/model/type/action_sound_action.rb +23 -0
- data/lib/pdfrb/model/type/action_submit_form.rb +64 -0
- data/lib/pdfrb/model/type/action_thread.rb +23 -0
- data/lib/pdfrb/model/type/action_trans.rb +30 -0
- data/lib/pdfrb/model/type/action_uri.rb +30 -0
- data/lib/pdfrb/model/type/af_embedded_file.rb +11 -0
- data/lib/pdfrb/model/type/af_file_specification.rb +34 -0
- data/lib/pdfrb/model/type/annotation.rb +49 -10
- data/lib/pdfrb/model/type/appearance.rb +41 -0
- data/lib/pdfrb/model/type/appearance_characteristics.rb +36 -0
- data/lib/pdfrb/model/type/appearance_generator.rb +22 -0
- data/lib/pdfrb/model/type/appearance_trap_net.rb +84 -0
- data/lib/pdfrb/model/type/border_effect.rb +25 -0
- data/lib/pdfrb/model/type/border_styling.rb +38 -0
- data/lib/pdfrb/model/type/box_color_info.rb +21 -0
- data/lib/pdfrb/model/type/box_style.rb +19 -0
- data/lib/pdfrb/model/type/button.rb +17 -0
- data/lib/pdfrb/model/type/cal_gray.rb +17 -0
- data/lib/pdfrb/model/type/cal_rgb.rb +22 -0
- data/lib/pdfrb/model/type/caret_annotation.rb +21 -0
- data/lib/pdfrb/model/type/catalog.rb +99 -16
- data/lib/pdfrb/model/type/choice.rb +21 -0
- data/lib/pdfrb/model/type/cid_font.rb +52 -1
- data/lib/pdfrb/model/type/cid_font_descriptor_metrics.rb +28 -0
- data/lib/pdfrb/model/type/cid_system_info.rb +28 -0
- data/lib/pdfrb/model/type/circle_annotation.rb +20 -0
- data/lib/pdfrb/model/type/collection.rb +27 -0
- data/lib/pdfrb/model/type/collection_field.rb +24 -0
- data/lib/pdfrb/model/type/collection_item.rb +19 -0
- data/lib/pdfrb/model/type/collection_schema.rb +25 -0
- data/lib/pdfrb/model/type/collection_sort.rb +13 -0
- data/lib/pdfrb/model/type/crypt_filter.rb +58 -0
- data/lib/pdfrb/model/type/device_n.rb +37 -0
- data/lib/pdfrb/model/type/device_n_mixing_hints.rb +17 -0
- data/lib/pdfrb/model/type/device_n_process.rb +13 -0
- data/lib/pdfrb/model/type/doc_mdp_transform_parameters.rb +21 -0
- data/lib/pdfrb/model/type/document_security_store.rb +55 -0
- data/lib/pdfrb/model/type/embedded_file.rb +19 -0
- data/lib/pdfrb/model/type/embedded_file_parameter.rb +48 -0
- data/lib/pdfrb/model/type/encryption_public_key.rb +38 -0
- data/lib/pdfrb/model/type/encryption_standard.rb +68 -0
- data/lib/pdfrb/model/type/field_mdp_transform_parameters.rb +26 -0
- data/lib/pdfrb/model/type/file_attachment_annotation.rb +29 -0
- data/lib/pdfrb/model/type/file_spec_ef.rb +22 -0
- data/lib/pdfrb/model/type/file_spec_rf.rb +21 -0
- data/lib/pdfrb/model/type/file_specification.rb +43 -0
- data/lib/pdfrb/model/type/file_trailer.rb +54 -0
- data/lib/pdfrb/model/type/font.rb +73 -2
- data/lib/pdfrb/model/type/font_cid_type0.rb +17 -0
- data/lib/pdfrb/model/type/font_cid_type2.rb +17 -0
- data/lib/pdfrb/model/type/font_descriptor.rb +47 -0
- data/lib/pdfrb/model/type/font_descriptor_cid_type0.rb +15 -0
- data/lib/pdfrb/model/type/font_descriptor_cid_type2.rb +15 -0
- data/lib/pdfrb/model/type/font_encoding.rb +47 -0
- data/lib/pdfrb/model/type/font_file.rb +28 -0
- data/lib/pdfrb/model/type/font_file2.rb +32 -0
- data/lib/pdfrb/model/type/font_file3.rb +23 -0
- data/lib/pdfrb/model/type/font_multiple_master.rb +60 -0
- data/lib/pdfrb/model/type/font_true_type.rb +1 -1
- data/lib/pdfrb/model/type/font_type0.rb +1 -1
- data/lib/pdfrb/model/type/font_type1.rb +1 -1
- data/lib/pdfrb/model/type/font_type3.rb +1 -1
- data/lib/pdfrb/model/type/form_field.rb +73 -0
- data/lib/pdfrb/model/type/free_text_annotation.rb +37 -0
- data/lib/pdfrb/model/type/function.rb +42 -0
- data/lib/pdfrb/model/type/function_exponential.rb +31 -0
- data/lib/pdfrb/model/type/function_postscript.rb +25 -0
- data/lib/pdfrb/model/type/function_sampled.rb +55 -0
- data/lib/pdfrb/model/type/function_stitching.rb +33 -0
- data/lib/pdfrb/model/type/generic_appearance.rb +40 -0
- data/lib/pdfrb/model/type/graphics_state_parameter.rb +69 -0
- data/lib/pdfrb/model/type/halftone.rb +21 -0
- data/lib/pdfrb/model/type/halftone_type1.rb +17 -0
- data/lib/pdfrb/model/type/halftone_type10.rb +16 -0
- data/lib/pdfrb/model/type/halftone_type16.rb +11 -0
- data/lib/pdfrb/model/type/halftone_type5.rb +24 -0
- data/lib/pdfrb/model/type/halftone_type6.rb +16 -0
- data/lib/pdfrb/model/type/highlight_annotation.rb +11 -0
- data/lib/pdfrb/model/type/icc_based_color_space.rb +24 -0
- data/lib/pdfrb/model/type/icon_fit.rb +31 -0
- data/lib/pdfrb/model/type/indexed.rb +32 -0
- data/lib/pdfrb/model/type/info.rb +44 -0
- data/lib/pdfrb/model/type/ink_annotation.rb +29 -0
- data/lib/pdfrb/model/type/interactive_form.rb +36 -0
- data/lib/pdfrb/model/type/interior_color.rb +12 -0
- data/lib/pdfrb/model/type/jbig2_globals.rb +15 -0
- data/lib/pdfrb/model/type/lab.rb +27 -0
- data/lib/pdfrb/model/type/line_annotation.rb +44 -0
- data/lib/pdfrb/model/type/line_ending_styling.rb +39 -0
- data/lib/pdfrb/model/type/link_annotation.rb +41 -0
- data/lib/pdfrb/model/type/link_appearance.rb +9 -0
- data/lib/pdfrb/model/type/mark_information.rb +23 -0
- data/lib/pdfrb/model/type/marked_content_reference.rb +24 -0
- data/lib/pdfrb/model/type/markup_annotation.rb +36 -0
- data/lib/pdfrb/model/type/mdp_dict.rb +28 -0
- data/lib/pdfrb/model/type/measure.rb +39 -0
- data/lib/pdfrb/model/type/media_clip.rb +28 -0
- data/lib/pdfrb/model/type/media_offset.rb +106 -0
- data/lib/pdfrb/model/type/metadata.rb +27 -3
- data/lib/pdfrb/model/type/misc_helpers.rb +75 -0
- data/lib/pdfrb/model/type/movie.rb +28 -0
- data/lib/pdfrb/model/type/names.rb +51 -0
- data/lib/pdfrb/model/type/namespace.rb +27 -0
- data/lib/pdfrb/model/type/object_reference.rb +31 -0
- data/lib/pdfrb/model/type/object_stream.rb +38 -0
- data/lib/pdfrb/model/type/opt_content_ext.rb +94 -0
- data/lib/pdfrb/model/type/opt_content_page_element.rb +18 -0
- data/lib/pdfrb/model/type/optional_content_config.rb +56 -0
- data/lib/pdfrb/model/type/optional_content_group.rb +35 -10
- data/lib/pdfrb/model/type/optional_content_membership.rb +36 -2
- data/lib/pdfrb/model/type/optional_content_properties.rb +16 -1
- data/lib/pdfrb/model/type/outline.rb +40 -6
- data/lib/pdfrb/model/type/outline_item.rb +45 -0
- data/lib/pdfrb/model/type/output_intent.rb +30 -0
- data/lib/pdfrb/model/type/page.rb +127 -0
- data/lib/pdfrb/model/type/page_label.rb +68 -0
- data/lib/pdfrb/model/type/page_tree_node.rb +51 -16
- data/lib/pdfrb/model/type/page_tree_node_root.rb +15 -0
- data/lib/pdfrb/model/type/pattern.rb +16 -0
- data/lib/pdfrb/model/type/pattern_shading.rb +25 -0
- data/lib/pdfrb/model/type/pattern_tiling.rb +25 -0
- data/lib/pdfrb/model/type/polygon_annotation.rb +29 -0
- data/lib/pdfrb/model/type/polygon_polyline.rb +14 -0
- data/lib/pdfrb/model/type/polyline_annotation.rb +29 -0
- data/lib/pdfrb/model/type/popup_annotation.rb +20 -0
- data/lib/pdfrb/model/type/printer_mark_annotation.rb +13 -0
- data/lib/pdfrb/model/type/redact_annotation.rb +28 -0
- data/lib/pdfrb/model/type/rendition.rb +31 -0
- data/lib/pdfrb/model/type/resources.rb +59 -14
- data/lib/pdfrb/model/type/rich_media_activation.rb +24 -0
- data/lib/pdfrb/model/type/rich_media_animation.rb +25 -0
- data/lib/pdfrb/model/type/rich_media_annotation.rb +18 -0
- data/lib/pdfrb/model/type/rich_media_command.rb +19 -0
- data/lib/pdfrb/model/type/rich_media_configuration.rb +32 -0
- data/lib/pdfrb/model/type/rich_media_content.rb +28 -0
- data/lib/pdfrb/model/type/rich_media_cue_point.rb +18 -0
- data/lib/pdfrb/model/type/rich_media_deactivation.rb +17 -0
- data/lib/pdfrb/model/type/rich_media_instance.rb +33 -0
- data/lib/pdfrb/model/type/rich_media_presentation.rb +29 -0
- data/lib/pdfrb/model/type/rich_media_settings.rb +33 -0
- data/lib/pdfrb/model/type/rich_media_window.rb +20 -0
- data/lib/pdfrb/model/type/screen_annotation.rb +19 -0
- data/lib/pdfrb/model/type/separation.rb +25 -0
- data/lib/pdfrb/model/type/sig_field_lock.rb +25 -0
- data/lib/pdfrb/model/type/sig_field_seed_value.rb +25 -0
- data/lib/pdfrb/model/type/signature.rb +65 -0
- data/lib/pdfrb/model/type/signature_build.rb +86 -0
- data/lib/pdfrb/model/type/signature_field.rb +17 -0
- data/lib/pdfrb/model/type/software_identifier.rb +19 -0
- data/lib/pdfrb/model/type/sound.rb +38 -0
- data/lib/pdfrb/model/type/sound_annotation.rb +21 -0
- data/lib/pdfrb/model/type/square_annotation.rb +24 -0
- data/lib/pdfrb/model/type/square_circle.rb +13 -0
- data/lib/pdfrb/model/type/squiggly_annotation.rb +11 -0
- data/lib/pdfrb/model/type/stamp_annotation.rb +19 -0
- data/lib/pdfrb/model/type/strikeout_annotation.rb +11 -0
- data/lib/pdfrb/model/type/struct_elem.rb +75 -0
- data/lib/pdfrb/model/type/struct_tree_root.rb +34 -0
- data/lib/pdfrb/model/type/structure_attributes.rb +44 -0
- data/lib/pdfrb/model/type/structure_element_kid.rb +33 -0
- data/lib/pdfrb/model/type/text_annotation.rb +25 -0
- data/lib/pdfrb/model/type/text_appearance.rb +9 -0
- data/lib/pdfrb/model/type/text_field.rb +21 -0
- data/lib/pdfrb/model/type/text_markup_annotation.rb +30 -0
- data/lib/pdfrb/model/type/three_d_activation.rb +32 -0
- data/lib/pdfrb/model/type/three_d_animation_style.rb +24 -0
- data/lib/pdfrb/model/type/three_d_background.rb +28 -0
- data/lib/pdfrb/model/type/three_d_cross_section.rb +25 -0
- data/lib/pdfrb/model/type/three_d_lighting_scheme.rb +26 -0
- data/lib/pdfrb/model/type/three_d_measure.rb +20 -0
- data/lib/pdfrb/model/type/three_d_measure_3dc.rb +23 -0
- data/lib/pdfrb/model/type/three_d_measure_ad3.rb +14 -0
- data/lib/pdfrb/model/type/three_d_measure_ld3.rb +14 -0
- data/lib/pdfrb/model/type/three_d_measure_pd3.rb +20 -0
- data/lib/pdfrb/model/type/three_d_measure_rd3.rb +14 -0
- data/lib/pdfrb/model/type/three_d_node.rb +37 -0
- data/lib/pdfrb/model/type/three_d_reference.rb +21 -0
- data/lib/pdfrb/model/type/three_d_render_mode.rb +34 -0
- data/lib/pdfrb/model/type/three_d_stream.rb +48 -0
- data/lib/pdfrb/model/type/three_d_units.rb +18 -0
- data/lib/pdfrb/model/type/three_d_view.rb +40 -0
- data/lib/pdfrb/model/type/to_unicode_cmap_stream.rb +23 -0
- data/lib/pdfrb/model/type/transition.rb +68 -0
- data/lib/pdfrb/model/type/underline_annotation.rb +11 -0
- data/lib/pdfrb/model/type/ur_transform_parameters.rb +33 -0
- data/lib/pdfrb/model/type/uri_dict.rb +18 -0
- data/lib/pdfrb/model/type/url_alias.rb +19 -0
- data/lib/pdfrb/model/type/variable_text_field.rb +47 -0
- data/lib/pdfrb/model/type/viewer_preferences.rb +42 -0
- data/lib/pdfrb/model/type/watermark_annotation.rb +13 -0
- data/lib/pdfrb/model/type/widget_annotation.rb +48 -0
- data/lib/pdfrb/model/type/widget_appearance.rb +9 -0
- data/lib/pdfrb/model/type/xobject_form.rb +41 -0
- data/lib/pdfrb/model/type/xobject_image.rb +56 -0
- data/lib/pdfrb/model/type/xref_stream.rb +37 -0
- data/lib/pdfrb/model/type.rb +286 -6
- data/lib/pdfrb/serializer.rb +2 -2
- data/lib/pdfrb/source/tokenizer.rb +0 -2
- data/lib/pdfrb/task/benchmark.rb +83 -0
- data/lib/pdfrb/task/extract_text.rb +1 -1
- data/lib/pdfrb/task/generate_corpus.rb +130 -0
- data/lib/pdfrb/task/memory_profile.rb +84 -0
- data/lib/pdfrb/task/optimize.rb +97 -7
- data/lib/pdfrb/task.rb +3 -3
- data/lib/pdfrb/test_utils.rb +112 -0
- data/lib/pdfrb/validator.rb +82 -0
- data/lib/pdfrb/version.rb +1 -1
- data/lib/pdfrb/writer.rb +117 -27
- data/lib/pdfrb/xmp/packet.rb +122 -0
- data/lib/pdfrb/xmp/schemas.rb +96 -0
- data/lib/pdfrb/xmp.rb +44 -0
- data/lib/pdfrb/xref_section.rb +2 -2
- data/lib/pdfrb.rb +24 -54
- data/pdfrb.gemspec +45 -0
- data/script/apply_fixes.sh +843 -0
- data/script/make_jpeg_fixture.rb +53 -0
- data/script/make_png_fixture.rb +25 -0
- metadata +311 -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
|
data/lib/pdfrb/task/optimize.rb
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
+
canonical_oid = canonical_for[key]
|
|
75
|
+
next if canonical_oid == obj.oid
|
|
76
|
+
|
|
77
|
+
replacements[obj.oid] = canonical_oid
|
|
43
78
|
end
|
|
44
|
-
|
|
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