emf 0.1.2 → 0.2.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/README.adoc +224 -19
- data/exe/emf +12 -0
- data/lib/emf/backend.rb +60 -0
- data/lib/emf/binary/class_field_accessors.rb +49 -0
- data/lib/emf/binary/float32_le.rb +54 -0
- data/lib/emf/binary/types.rb +40 -6
- data/lib/emf/conformance/context.rb +36 -0
- data/lib/emf/conformance/diagnostic.rb +18 -0
- data/lib/emf/conformance/manifest/emf_bitmap_misc.yml +622 -0
- data/lib/emf/conformance/manifest/emf_control_buffers.yml +61 -0
- data/lib/emf/conformance/manifest/emf_core.yml +117 -0
- data/lib/emf/conformance/manifest/emf_corpus_rules.yml +21 -0
- data/lib/emf/conformance/manifest/emf_objects_geometry.yml +229 -0
- data/lib/emf/conformance/manifest/emf_plus.yml +357 -0
- data/lib/emf/conformance/manifest/emf_plus_core.yml +33 -0
- data/lib/emf/conformance/manifest/emf_state_path.yml +403 -0
- data/lib/emf/conformance/manifest/emf_text.yml +97 -0
- data/lib/emf/conformance/manifest/wmf.yml +874 -0
- data/lib/emf/conformance/manifest.rb +79 -0
- data/lib/emf/conformance/profile.rb +35 -0
- data/lib/emf/conformance/record_types.rb +24 -0
- data/lib/emf/conformance/registry.rb +76 -0
- data/lib/emf/conformance/report.rb +83 -0
- data/lib/emf/conformance/rule.rb +16 -0
- data/lib/emf/conformance/rules/emf_bitmap.rb +357 -0
- data/lib/emf/conformance/rules/emf_comment_escape.rb +309 -0
- data/lib/emf/conformance/rules/emf_control_buffers.rb +137 -0
- data/lib/emf/conformance/rules/emf_corpus_named.rb +83 -0
- data/lib/emf/conformance/rules/emf_framing.rb +94 -0
- data/lib/emf/conformance/rules/emf_geometry.rb +282 -0
- data/lib/emf/conformance/rules/emf_header.rb +114 -0
- data/lib/emf/conformance/rules/emf_objects.rb +291 -0
- data/lib/emf/conformance/rules/emf_parse_recovery.rb +97 -0
- data/lib/emf/conformance/rules/emf_path_clip.rb +165 -0
- data/lib/emf/conformance/rules/emf_plus.rb +75 -0
- data/lib/emf/conformance/rules/emf_plus_common.rb +58 -0
- data/lib/emf/conformance/rules/emf_plus_drawing.rb +78 -0
- data/lib/emf/conformance/rules/emf_plus_effects.rb +104 -0
- data/lib/emf/conformance/rules/emf_plus_fields.rb +169 -0
- data/lib/emf/conformance/rules/emf_plus_fields_b.rb +102 -0
- data/lib/emf/conformance/rules/emf_plus_framing.rb +172 -0
- data/lib/emf/conformance/rules/emf_plus_objects.rb +97 -0
- data/lib/emf/conformance/rules/emf_plus_payloads.rb +320 -0
- data/lib/emf/conformance/rules/emf_plus_properties.rb +96 -0
- data/lib/emf/conformance/rules/emf_plus_state.rb +77 -0
- data/lib/emf/conformance/rules/emf_state.rb +244 -0
- data/lib/emf/conformance/rules/emf_text.rb +221 -0
- data/lib/emf/conformance/rules/emf_transform.rb +85 -0
- data/lib/emf/conformance/rules/wmf_buffers.rb +259 -0
- data/lib/emf/conformance/rules/wmf_framing.rb +146 -0
- data/lib/emf/conformance/rules/wmf_layout_records.rb +112 -0
- data/lib/emf/conformance/rules/wmf_objects.rb +168 -0
- data/lib/emf/conformance/rules/wmf_records.rb +191 -0
- data/lib/emf/conformance/rules/wmf_rejections.rb +184 -0
- data/lib/emf/conformance/runner.rb +61 -0
- data/lib/emf/conformance/ternary_raster_operations.rb +52 -0
- data/lib/emf/conformance.rb +31 -0
- data/lib/emf/emf_plus/effects/blur.rb +24 -0
- data/lib/emf/emf_plus/effects/brightness_contrast.rb +22 -0
- data/lib/emf/emf_plus/effects/color_balance.rb +24 -0
- data/lib/emf/emf_plus/effects/color_curve.rb +24 -0
- data/lib/emf/emf_plus/effects/color_lookup_table.rb +26 -0
- data/lib/emf/emf_plus/effects/color_matrix.rb +23 -0
- data/lib/emf/emf_plus/effects/effect.rb +50 -0
- data/lib/emf/emf_plus/effects/hue_saturation_lightness.rb +24 -0
- data/lib/emf/emf_plus/effects/levels.rb +24 -0
- data/lib/emf/emf_plus/effects/opaque.rb +19 -0
- data/lib/emf/emf_plus/effects/red_eye_correction.rb +35 -0
- data/lib/emf/emf_plus/effects/sharpen.rb +24 -0
- data/lib/emf/emf_plus/effects/tint.rb +22 -0
- data/lib/emf/emf_plus/effects.rb +45 -0
- data/lib/emf/emf_plus/frame.rb +41 -0
- data/lib/emf/emf_plus/links/effect_link.rb +16 -0
- data/lib/emf/emf_plus/links/get_dc_span.rb +18 -0
- data/lib/emf/emf_plus/links/link_error.rb +15 -0
- data/lib/emf/emf_plus/links.rb +77 -0
- data/lib/emf/emf_plus/object_chain.rb +108 -0
- data/lib/emf/emf_plus/object_table.rb +182 -0
- data/lib/emf/emf_plus/objects/brush.rb +296 -0
- data/lib/emf/emf_plus/objects/custom_line_cap.rb +106 -0
- data/lib/emf/emf_plus/objects/font.rb +47 -0
- data/lib/emf/emf_plus/objects/image.rb +68 -0
- data/lib/emf/emf_plus/objects/image_attributes.rb +28 -0
- data/lib/emf/emf_plus/objects/opaque.rb +19 -0
- data/lib/emf/emf_plus/objects/parts.rb +103 -0
- data/lib/emf/emf_plus/objects/path.rb +21 -0
- data/lib/emf/emf_plus/objects/pen.rb +132 -0
- data/lib/emf/emf_plus/objects/region.rb +39 -0
- data/lib/emf/emf_plus/objects/string_format.rb +55 -0
- data/lib/emf/emf_plus/objects/typed.rb +50 -0
- data/lib/emf/emf_plus/objects.rb +46 -0
- data/lib/emf/emf_plus/parser.rb +64 -2
- data/lib/emf/emf_plus/records/base.rb +107 -0
- data/lib/emf/emf_plus/records/begin_container.rb +31 -0
- data/lib/emf/emf_plus/records/begin_container_no_params.rb +20 -0
- data/lib/emf/emf_plus/records/body.rb +16 -0
- data/lib/emf/emf_plus/records/clear.rb +22 -0
- data/lib/emf/emf_plus/records/clip_transform_flags.rb +26 -0
- data/lib/emf/emf_plus/records/comment.rb +21 -0
- data/lib/emf/emf_plus/records/draw_arc.rb +33 -0
- data/lib/emf/emf_plus/records/draw_beziers.rb +31 -0
- data/lib/emf/emf_plus/records/draw_closed_curve.rb +34 -0
- data/lib/emf/emf_plus/records/draw_curve.rb +38 -0
- data/lib/emf/emf_plus/records/draw_driver_string.rb +61 -0
- data/lib/emf/emf_plus/records/draw_ellipse.rb +28 -0
- data/lib/emf/emf_plus/records/draw_image.rb +42 -0
- data/lib/emf/emf_plus/records/draw_image_points.rb +62 -0
- data/lib/emf/emf_plus/records/draw_lines.rb +34 -0
- data/lib/emf/emf_plus/records/draw_path.rb +23 -0
- data/lib/emf/emf_plus/records/draw_pie.rb +33 -0
- data/lib/emf/emf_plus/records/draw_rects.rb +30 -0
- data/lib/emf/emf_plus/records/draw_string.rb +41 -0
- data/lib/emf/emf_plus/records/end_container.rb +20 -0
- data/lib/emf/emf_plus/records/end_of_file.rb +17 -0
- data/lib/emf/emf_plus/records/fill_brush.rb +21 -0
- data/lib/emf/emf_plus/records/fill_closed_curve.rb +38 -0
- data/lib/emf/emf_plus/records/fill_ellipse.rb +29 -0
- data/lib/emf/emf_plus/records/fill_path.rb +23 -0
- data/lib/emf/emf_plus/records/fill_pie.rb +34 -0
- data/lib/emf/emf_plus/records/fill_polygon.rb +32 -0
- data/lib/emf/emf_plus/records/fill_rects.rb +31 -0
- data/lib/emf/emf_plus/records/fill_region.rb +23 -0
- data/lib/emf/emf_plus/records/get_dc.rb +18 -0
- data/lib/emf/emf_plus/records/header.rb +35 -0
- data/lib/emf/emf_plus/records/multiply_world_transform.rb +24 -0
- data/lib/emf/emf_plus/records/object.rb +32 -0
- data/lib/emf/emf_plus/records/offset_clip.rb +24 -0
- data/lib/emf/emf_plus/records/primitives/argb.rb +20 -0
- data/lib/emf/emf_plus/records/primitives/bitmap.rb +32 -0
- data/lib/emf/emf_plus/records/primitives/bitmap_data.rb +21 -0
- data/lib/emf/emf_plus/records/primitives/blend_colors.rb +22 -0
- data/lib/emf/emf_plus/records/primitives/blend_factors.rb +21 -0
- data/lib/emf/emf_plus/records/primitives/bounded_array.rb +38 -0
- data/lib/emf/emf_plus/records/primitives/byte_reader.rb +25 -0
- data/lib/emf/emf_plus/records/primitives/character_range.rb +20 -0
- data/lib/emf/emf_plus/records/primitives/compressed_image.rb +17 -0
- data/lib/emf/emf_plus/records/primitives/focus_scale_data.rb +20 -0
- data/lib/emf/emf_plus/records/primitives/graphics_version.rb +21 -0
- data/lib/emf/emf_plus/records/primitives/language_identifier.rb +22 -0
- data/lib/emf/emf_plus/records/primitives/metafile.rb +19 -0
- data/lib/emf/emf_plus/records/primitives/palette.rb +21 -0
- data/lib/emf/emf_plus/records/primitives/path.rb +89 -0
- data/lib/emf/emf_plus/records/primitives/path_point_type.rb +21 -0
- data/lib/emf/emf_plus/records/primitives/path_point_type_rle.rb +23 -0
- data/lib/emf/emf_plus/records/primitives/point.rb +18 -0
- data/lib/emf/emf_plus/records/primitives/point_array.rb +42 -0
- data/lib/emf/emf_plus/records/primitives/point_f.rb +19 -0
- data/lib/emf/emf_plus/records/primitives/point_r.rb +19 -0
- data/lib/emf/emf_plus/records/primitives/rect.rb +20 -0
- data/lib/emf/emf_plus/records/primitives/rect_array.rb +24 -0
- data/lib/emf/emf_plus/records/primitives/rect_f.rb +21 -0
- data/lib/emf/emf_plus/records/primitives/region_node.rb +68 -0
- data/lib/emf/emf_plus/records/primitives/region_node_child_nodes.rb +22 -0
- data/lib/emf/emf_plus/records/primitives/region_node_path.rb +37 -0
- data/lib/emf/emf_plus/records/primitives/relative_coordinate.rb +32 -0
- data/lib/emf/emf_plus/records/primitives/string_format_data.rb +23 -0
- data/lib/emf/emf_plus/records/primitives/transform_matrix.rb +23 -0
- data/lib/emf/emf_plus/records/primitives.rb +41 -0
- data/lib/emf/emf_plus/records/reset_clip.rb +17 -0
- data/lib/emf/emf_plus/records/reset_world_transform.rb +17 -0
- data/lib/emf/emf_plus/records/restore.rb +20 -0
- data/lib/emf/emf_plus/records/rotate_world_transform.rb +24 -0
- data/lib/emf/emf_plus/records/save.rb +20 -0
- data/lib/emf/emf_plus/records/scale_world_transform.rb +26 -0
- data/lib/emf/emf_plus/records/serializable_object.rb +32 -0
- data/lib/emf/emf_plus/records/set_anti_alias_mode.rb +25 -0
- data/lib/emf/emf_plus/records/set_clip_path.rb +20 -0
- data/lib/emf/emf_plus/records/set_clip_rect.rb +24 -0
- data/lib/emf/emf_plus/records/set_clip_region.rb +20 -0
- data/lib/emf/emf_plus/records/set_compositing_mode.rb +21 -0
- data/lib/emf/emf_plus/records/set_compositing_quality.rb +21 -0
- data/lib/emf/emf_plus/records/set_interpolation_mode.rb +21 -0
- data/lib/emf/emf_plus/records/set_page_transform.rb +25 -0
- data/lib/emf/emf_plus/records/set_pixel_offset_mode.rb +21 -0
- data/lib/emf/emf_plus/records/set_rendering_origin.rb +22 -0
- data/lib/emf/emf_plus/records/set_text_contrast.rb +24 -0
- data/lib/emf/emf_plus/records/set_text_rendering_hint.rb +21 -0
- data/lib/emf/emf_plus/records/set_ts_clip.rb +40 -0
- data/lib/emf/emf_plus/records/set_ts_graphics.rb +88 -0
- data/lib/emf/emf_plus/records/set_world_transform.rb +22 -0
- data/lib/emf/emf_plus/records/shape_flags.rb +23 -0
- data/lib/emf/emf_plus/records/translate_world_transform.rb +26 -0
- data/lib/emf/emf_plus/records/unknown.rb +20 -0
- data/lib/emf/emf_plus/records.rb +107 -0
- data/lib/emf/emf_plus/serializer.rb +9 -2
- data/lib/emf/emf_plus/stream/conformance.rb +68 -0
- data/lib/emf/emf_plus/stream/editor.rb +21 -0
- data/lib/emf/emf_plus/stream/emf_gap.rb +20 -0
- data/lib/emf/emf_plus/stream/link_index.rb +30 -0
- data/lib/emf/emf_plus/stream/rebuild.rb +47 -0
- data/lib/emf/emf_plus/stream/record_handle.rb +26 -0
- data/lib/emf/emf_plus/stream/segment.rb +70 -0
- data/lib/emf/emf_plus/stream.rb +153 -0
- data/lib/emf/emf_plus.rb +9 -0
- data/lib/emf/emr/binary/dib_header.rb +142 -0
- data/lib/emf/emr/binary/ext_text_out_fields.rb +43 -0
- data/lib/emf/emr/binary/extended_pen_buffers.rb +23 -0
- data/lib/emf/emr/binary/fast_fill.rb +1164 -0
- data/lib/emf/emr/binary/font.rb +159 -0
- data/lib/emf/emr/binary/lazy_utf16_array.rb +82 -0
- data/lib/emf/emr/binary/pack_plan.rb +144 -0
- data/lib/emf/emr/binary/pack_plan_check.rb +75 -0
- data/lib/emf/emr/binary/pack_plan_exact.rb +130 -0
- data/lib/emf/emr/binary/pack_plan_generate.rb +116 -0
- data/lib/emf/emr/binary/pack_plan_values.rb +92 -0
- data/lib/emf/emr/binary/palette_body.rb +19 -0
- data/lib/emf/emr/binary/palette_entry.rb +18 -0
- data/lib/emf/emr/binary/record.rb +33 -0
- data/lib/emf/emr/binary/records/alpha_blend.rb +43 -0
- data/lib/emf/emr/binary/records/angle_arc.rb +2 -2
- data/lib/emf/emr/binary/records/bit_blt.rb +14 -0
- data/lib/emf/emr/binary/records/color_correct_palette.rb +55 -1
- data/lib/emf/emr/binary/records/colormatch_to_target_w.rb +76 -1
- data/lib/emf/emr/binary/records/create_colorspace.rb +67 -1
- data/lib/emf/emr/binary/records/create_colorspace_w.rb +90 -1
- data/lib/emf/emr/binary/records/create_dib_pattern_brush_pt.rb +14 -0
- data/lib/emf/emr/binary/records/create_font_indirect_w.rb +6 -2
- data/lib/emf/emr/binary/records/create_mono_brush.rb +14 -0
- data/lib/emf/emr/binary/records/create_palette.rb +19 -1
- data/lib/emf/emr/binary/records/delete_colorspace.rb +22 -1
- data/lib/emf/emr/binary/records/draw_escape.rb +45 -1
- data/lib/emf/emr/binary/records/ellipse.rb +1 -1
- data/lib/emf/emr/binary/records/eof.rb +9 -0
- data/lib/emf/emr/binary/records/ext_create_pen.rb +80 -1
- data/lib/emf/emr/binary/records/ext_escape.rb +45 -1
- data/lib/emf/emr/binary/records/ext_flood_fill.rb +9 -2
- data/lib/emf/emr/binary/records/ext_select_clip_rgn.rb +9 -1
- data/lib/emf/emr/binary/records/ext_text_out_a.rb +16 -3
- data/lib/emf/emr/binary/records/ext_text_out_w.rb +14 -3
- data/lib/emf/emr/binary/records/fill_rgn.rb +11 -2
- data/lib/emf/emr/binary/records/force_ufi_mapping.rb +38 -1
- data/lib/emf/emr/binary/records/frame_rgn.rb +12 -2
- data/lib/emf/emr/binary/records/gls_bounded_record.rb +61 -1
- data/lib/emf/emr/binary/records/gls_record.rb +42 -1
- data/lib/emf/emr/binary/records/gradient_fill.rb +7 -0
- data/lib/emf/emr/binary/records/invert_rgn.rb +11 -3
- data/lib/emf/emr/binary/records/mask_blt.rb +66 -0
- data/lib/emf/emr/binary/records/named_escape.rb +78 -1
- data/lib/emf/emr/binary/records/paint_rgn.rb +11 -3
- data/lib/emf/emr/binary/records/pixel_format.rb +76 -1
- data/lib/emf/emr/binary/records/plg_blt.rb +66 -0
- data/lib/emf/emr/binary/records/poly_textout_a.rb +128 -1
- data/lib/emf/emr/binary/records/poly_textout_w.rb +128 -1
- data/lib/emf/emr/binary/records/raw.rb +3 -0
- data/lib/emf/emr/binary/records/realize_palette.rb +1 -0
- data/lib/emf/emr/binary/records/rectangle.rb +1 -1
- data/lib/emf/emr/binary/records/resize_palette.rb +16 -2
- data/lib/emf/emr/binary/records/round_rect.rb +2 -2
- data/lib/emf/emr/binary/records/select_palette.rb +1 -0
- data/lib/emf/emr/binary/records/set_color_adjustment.rb +136 -0
- data/lib/emf/emr/binary/records/set_colorspace.rb +22 -1
- data/lib/emf/emr/binary/records/set_dibits_to_device.rb +43 -0
- data/lib/emf/emr/binary/records/set_icm_profile_a.rb +71 -1
- data/lib/emf/emr/binary/records/set_icm_profile_w.rb +71 -1
- data/lib/emf/emr/binary/records/set_linked_ufis.rb +49 -1
- data/lib/emf/emr/binary/records/set_miter_limit.rb +1 -1
- data/lib/emf/emr/binary/records/set_palette_entries.rb +20 -2
- data/lib/emf/emr/binary/records/set_text_justification.rb +8 -2
- data/lib/emf/emr/binary/records/small_text_out.rb +55 -4
- data/lib/emf/emr/binary/records/stretch_blt.rb +14 -0
- data/lib/emf/emr/binary/records/stretch_dibits.rb +14 -0
- data/lib/emf/emr/binary/records/transparent_blt.rb +43 -0
- data/lib/emf/emr/binary/records.rb +4 -2
- data/lib/emf/emr/binary/region_data.rb +33 -0
- data/lib/emf/emr/binary/state_record_body.rb +33 -0
- data/lib/emf/emr/binary/thin_shell.rb +226 -0
- data/lib/emf/emr/binary/value_view/layout.rb +101 -0
- data/lib/emf/emr/binary/value_view/reader_source.rb +88 -0
- data/lib/emf/emr/binary/value_view.rb +173 -0
- data/lib/emf/emr/binary/variable_buffers.rb +87 -0
- data/lib/emf/emr/binary.rb +4 -0
- data/lib/emf/emr/filled_record.rb +77 -0
- data/lib/emf/emr/parser.rb +239 -50
- data/lib/emf/emr/serializer.rb +4 -5
- data/lib/emf/emr.rb +1 -0
- data/lib/emf/error.rb +24 -1
- data/lib/emf/model/diagnostic.rb +41 -0
- data/lib/emf/model/emr/header.rb +40 -1
- data/lib/emf/model/emr/records.rb +142 -6
- data/lib/emf/model/emr/wire_values.rb +43 -0
- data/lib/emf/model/emr.rb +1 -0
- data/lib/emf/model/geometry/color.rb +19 -1
- data/lib/emf/model/metafile.rb +22 -2
- data/lib/emf/model/wmf/bitmap_records.rb +155 -0
- data/lib/emf/model/wmf/brush_records.rb +45 -0
- data/lib/emf/model/wmf/dc_escape_records.rb +67 -0
- data/lib/emf/model/wmf/font_record.rb +40 -0
- data/lib/emf/model/wmf/geometry_records.rb +107 -0
- data/lib/emf/model/wmf/header.rb +36 -0
- data/lib/emf/model/wmf/object_records.rb +71 -0
- data/lib/emf/model/wmf/palette_records.rb +68 -0
- data/lib/emf/model/wmf/records.rb +247 -0
- data/lib/emf/model/wmf/region_records.rb +112 -0
- data/lib/emf/model/wmf/state_records.rb +188 -0
- data/lib/emf/model/wmf/text_records.rb +36 -0
- data/lib/emf/model/wmf.rb +11 -0
- data/lib/emf/model.rb +2 -0
- data/lib/emf/native/bitmap_fields.rb +58 -0
- data/lib/emf/native/diagnostic_contract.rb +39 -0
- data/lib/emf/native/diagnostics.rb +81 -0
- data/lib/emf/native/emf_plus_loader.rb +35 -0
- data/lib/emf/native/emf_plus_snapshot.rb +96 -0
- data/lib/emf/native/emf_plus_source.rb +20 -0
- data/lib/emf/native/emf_plus_stream.rb +150 -0
- data/lib/emf/native/fields.rb +369 -0
- data/lib/emf/native/font_fields.rb +87 -0
- data/lib/emf/native/gradient_fields.rb +23 -0
- data/lib/emf/native/header.rb +35 -0
- data/lib/emf/native/header_errors.rb +30 -0
- data/lib/emf/native/hydrator.rb +151 -0
- data/lib/emf/native/lazy_wire.rb +100 -0
- data/lib/emf/native/legacy_state_fields.rb +22 -0
- data/lib/emf/native/library_path.rb +68 -0
- data/lib/emf/native/loader.rb +120 -0
- data/lib/emf/native/palette_fields.rb +45 -0
- data/lib/emf/native/poly_text_fields.rb +116 -0
- data/lib/emf/native/resource_failure.rb +28 -0
- data/lib/emf/native/small_text_fields.rb +27 -0
- data/lib/emf/native/snapshot.rb +102 -0
- data/lib/emf/native/wmf_bitmap_fields.rb +58 -0
- data/lib/emf/native/wmf_brush_fields.rb +62 -0
- data/lib/emf/native/wmf_fields.rb +117 -0
- data/lib/emf/native/wmf_hydrator.rb +143 -0
- data/lib/emf/native/wmf_loading.rb +49 -0
- data/lib/emf/native/wmf_palette_fields.rb +60 -0
- data/lib/emf/native/wmf_region_fields.rb +92 -0
- data/lib/emf/native/wmf_rejection.rb +84 -0
- data/lib/emf/native/wmf_remaining_fields.rb +70 -0
- data/lib/emf/native/wmf_snapshot.rb +51 -0
- data/lib/emf/native/wmf_text_fields.rb +56 -0
- data/lib/emf/native.rb +61 -0
- data/lib/emf/parse_evidence.rb +57 -0
- data/lib/emf/resource_budget/bitmap.rb +66 -0
- data/lib/emf/resource_budget/gradient.rb +30 -0
- data/lib/emf/resource_budget/poly_text.rb +85 -0
- data/lib/emf/resource_budget.rb +382 -0
- data/lib/emf/resource_limits/emf_records.rb +130 -0
- data/lib/emf/resource_limits.rb +58 -0
- data/lib/emf/version.rb +1 -1
- data/lib/emf/visitors/dump.rb +4 -0
- data/lib/emf/visitors/stats.rb +6 -5
- data/lib/emf/wmf/binary.rb +150 -1
- data/lib/emf/wmf/bitmap_codec.rb +118 -0
- data/lib/emf/wmf/brush_codec.rb +102 -0
- data/lib/emf/wmf/dc_escape_codec.rb +75 -0
- data/lib/emf/wmf/dib.rb +187 -0
- data/lib/emf/wmf/embedded_emf.rb +137 -0
- data/lib/emf/wmf/escape_views.rb +89 -0
- data/lib/emf/wmf/evidence.rb +255 -0
- data/lib/emf/wmf/geometry_codec.rb +49 -0
- data/lib/emf/wmf/palette_codec.rb +85 -0
- data/lib/emf/wmf/parser.rb +147 -2
- data/lib/emf/wmf/recovery.rb +95 -0
- data/lib/emf/wmf/region_codec.rb +151 -0
- data/lib/emf/wmf/serializer.rb +164 -2
- data/lib/emf/wmf/text_codec.rb +123 -0
- data/lib/emf/wmf.rb +7 -0
- data/lib/emf.rb +13 -9
- metadata +287 -10
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Emr
|
|
5
|
+
module Binary
|
|
6
|
+
# Copy of an instantiated BinData struct tree. BinData builds each lazy
|
|
7
|
+
# field by cloning a prototype, which dominates `new` plus first access
|
|
8
|
+
# for a record; cloning the finished tree gives the same objects (same
|
|
9
|
+
# classes, params, nil values, parent links) at a fraction of the cost.
|
|
10
|
+
# Relies on BinData::Struct#@field_objs and Base#@parent, as
|
|
11
|
+
# Emf::Binary::ClassFieldAccessors already does for @field_objs.
|
|
12
|
+
module WireCopy
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# Drops a cached BinData lazy evaluator so a template copy does not
|
|
16
|
+
# evaluate parameters against the template.
|
|
17
|
+
def forget_evaluator(object)
|
|
18
|
+
return unless object.instance_variable_defined?(:@lazy_evaluator)
|
|
19
|
+
|
|
20
|
+
object.send(:remove_instance_variable, :@lazy_evaluator)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def call(template, parent)
|
|
24
|
+
copy = template.dup
|
|
25
|
+
copy.instance_variable_set(:@parent, parent)
|
|
26
|
+
objs = template.instance_variable_get(:@field_objs).map do |field|
|
|
27
|
+
field.respond_to?(:field_names) ? call(field, copy) : leaf(field, copy)
|
|
28
|
+
end
|
|
29
|
+
copy.instance_variable_set(:@field_objs, objs)
|
|
30
|
+
copy
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Field name to position in @field_objs.
|
|
34
|
+
def slots(sample)
|
|
35
|
+
fields = sample.send(:get_parameter, :fields)
|
|
36
|
+
fields.each_with_index.with_object({}) do |(field, index), slots|
|
|
37
|
+
slots[field.name_as_sym] = index if field.name_as_sym
|
|
38
|
+
end.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# A copy of the template with each given field assigned, as BinData's
|
|
42
|
+
# constructor does after instantiating the fields one by one. BinData
|
|
43
|
+
# skips nil values in a constructor hash, so does this.
|
|
44
|
+
def build(template, slots, attrs)
|
|
45
|
+
return unless attrs.each_key.all? { |name| slots.key?(name) }
|
|
46
|
+
|
|
47
|
+
wire = call(template, nil)
|
|
48
|
+
objs = wire.instance_variable_get(:@field_objs)
|
|
49
|
+
attrs.each { |name, value| assign(objs[slots[name]], value) unless value.nil? }
|
|
50
|
+
wire
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
STRUCT_SLOTS = {}.compare_by_identity
|
|
54
|
+
private_constant :STRUCT_SLOTS
|
|
55
|
+
|
|
56
|
+
# Same result as `target.assign(value)` on a fresh (clear) target. A
|
|
57
|
+
# nested struct takes a symbol-keyed Hash of its own fields or a struct
|
|
58
|
+
# of exactly its class leaf by leaf, skipping BinData's clear and
|
|
59
|
+
# stringified-hash round trip; any other value goes through BinData.
|
|
60
|
+
def assign(target, value)
|
|
61
|
+
return target.assign(value) unless target.respond_to?(:field_names)
|
|
62
|
+
|
|
63
|
+
slots = (STRUCT_SLOTS[target.class] ||= slots(target))
|
|
64
|
+
if value.instance_of?(Hash) && value.each_key.all? { |name| slots.key?(name) }
|
|
65
|
+
objs = target.instance_variable_get(:@field_objs)
|
|
66
|
+
value.each { |name, leaf| assign(objs[slots[name]], leaf) unless leaf.nil? }
|
|
67
|
+
elsif value.instance_of?(target.class)
|
|
68
|
+
objs = target.instance_variable_get(:@field_objs)
|
|
69
|
+
slots.each_value { |index| assign(objs[index], source_field(value, index)) }
|
|
70
|
+
else
|
|
71
|
+
target.assign(value)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def source_field(struct, index)
|
|
76
|
+
struct.send(:instantiate_obj_at, index)
|
|
77
|
+
struct.instance_variable_get(:@field_objs)[index]
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# BinData extends each array with its initial_length behavior; dup
|
|
81
|
+
# would drop that singleton module, clone keeps it.
|
|
82
|
+
def leaf(leaf, parent)
|
|
83
|
+
copy = leaf.is_a?(BinData::Array) ? leaf.clone : leaf.dup
|
|
84
|
+
copy.instance_variable_set(:@parent, parent)
|
|
85
|
+
copy
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Source generation for the lazy fields of a FastFill's wires (see Record):
|
|
90
|
+
# how to copy one field from the template and read its leaves from the
|
|
91
|
+
# record's bytes.
|
|
92
|
+
module LazySource
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
# The template's struct alone: no field object is copied, so each
|
|
96
|
+
# slot of its @field_objs is nil until `lazy_field` makes it.
|
|
97
|
+
def shell_lines(template)
|
|
98
|
+
@templates << template
|
|
99
|
+
["wire = TEMPLATES[#{@templates.size - 1}].dup", "wire.instance_variable_set(:@parent, nil)",
|
|
100
|
+
"wire.instance_variable_set(:@field_objs, Array.new(#{template.instance_variable_get(:@field_objs).size}))"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# One `when` per top-level field: a copy of its subtree with the
|
|
104
|
+
# values of the leaves beneath it, read from the record's bytes,
|
|
105
|
+
# assigned.
|
|
106
|
+
def lazy_cases(objs, slots, leaves, reads)
|
|
107
|
+
slots.map do |name, index|
|
|
108
|
+
field = objs[index]
|
|
109
|
+
first = leaves.index { |path, _| path.first == name }
|
|
110
|
+
lines = field.respond_to?(:field_names) ? copy_lines(field, "f", "wire") : leaf_lines(field, "f", "wire")
|
|
111
|
+
lines += lazy_assigns(name, leaves, reads, first)
|
|
112
|
+
"when #{index}\n #{lines.join("\n ")}\n f"
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def lazy_assigns(name, leaves, reads, first)
|
|
117
|
+
return ["fill_unvalued(f, data, base)"] unless first
|
|
118
|
+
|
|
119
|
+
leaves.each_with_index.filter_map do |(path, leaf), position|
|
|
120
|
+
next unless path.first == name
|
|
121
|
+
|
|
122
|
+
verb = leaf.name == "Emf::Binary::Float32Le" ? "assign_bits" : "assign"
|
|
123
|
+
directive, at = reads[position]
|
|
124
|
+
"#{path.drop(1).inject('f') { |expr, step| "#{expr}.#{step}" }}.#{verb}(data.unpack1(\"#{directive}\", offset: base + #{at}))"
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Where each leaf of a fixed layout sits in the record's bytes, and the
|
|
130
|
+
# shell that reads them on demand.
|
|
131
|
+
module LeafReads
|
|
132
|
+
WIDTHS = { "V" => 4, "l<" => 4, "C" => 1 }.freeze
|
|
133
|
+
private_constant :WIDTHS
|
|
134
|
+
|
|
135
|
+
# `build` without a wire: a shell over the bytes PackPlan packs from
|
|
136
|
+
# the attributes, n_size as given. nil when there is no verified
|
|
137
|
+
# plan, the attributes are not ones it takes, or fields cannot be
|
|
138
|
+
# made lazily; the caller then builds.
|
|
139
|
+
def build_packed(attrs)
|
|
140
|
+
bytes = pack_plan&.pack(attrs, size: false) or return
|
|
141
|
+
bytes = bytes.b unless bytes.encoding == Encoding::BINARY
|
|
142
|
+
return eager(copy_shell, bytes, 0) unless @lazy_ok
|
|
143
|
+
|
|
144
|
+
@wire_class.allocate.thin(bytes, 0, self, @shell_params, @shell_names, true)
|
|
145
|
+
rescue StandardError
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
# [field count, params, field names] a thin shell shares with the template.
|
|
152
|
+
def shell_identity(sample)
|
|
153
|
+
[sample.instance_variable_get(:@field_objs).size, sample.instance_variable_get(:@params),
|
|
154
|
+
sample.instance_variable_get(:@field_names)]
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# A shell whose fields are made, from the record's bytes, as they are
|
|
158
|
+
# first used.
|
|
159
|
+
def lazy_wire(data, base)
|
|
160
|
+
return eager(copy_shell, data, base) unless @lazy_ok
|
|
161
|
+
|
|
162
|
+
@wire_class.allocate.thin(data, base, self, @shell_params, @shell_names, nil)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The same record with every field made now: what lazy fill is
|
|
166
|
+
# checked against, and the fallback when the check fails.
|
|
167
|
+
def eager(wire, data, base)
|
|
168
|
+
objs = wire.instance_variable_get(:@field_objs)
|
|
169
|
+
objs.each_index { |index| objs[index] = lazy_field(index, wire, data, base) }
|
|
170
|
+
wire
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# [directive, byte offset in the record] of each leaf, in leaf order;
|
|
174
|
+
# the layout must be gap-free up to the end of the fixed part.
|
|
175
|
+
def leaf_reads(leaves, directives)
|
|
176
|
+
at = 0
|
|
177
|
+
reads = leaves.map do |_, field|
|
|
178
|
+
directive = directives.fetch(field.name)
|
|
179
|
+
[directive, at].tap { at += WIDTHS.fetch(directive) }
|
|
180
|
+
end
|
|
181
|
+
raise SelfCheck::Unverified, "#{@wire_class}: leaves do not tile the record" unless at == @size
|
|
182
|
+
|
|
183
|
+
reads
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# The self-check of FlatPoints, run once per point array filler.
|
|
188
|
+
module FlatPointsCheck
|
|
189
|
+
# The BinData::Array API the flat-points holder was written
|
|
190
|
+
# against (bindata 2.5.1): every reader of its element list goes
|
|
191
|
+
# through `elements`, `clear?` or `assign`.
|
|
192
|
+
ARRAY_METHODS = %i[<< [] []= assign at clear? concat debug_name_of do_num_bytes do_write each empty?
|
|
193
|
+
find_index find_index_of first index initialize_instance initialize_shared_instance
|
|
194
|
+
insert last length offset_of push size slice snapshot to_ary unshift].freeze
|
|
195
|
+
ARRAY_PRIVATE_METHODS = %i[append_new_element elements extend_array new_element slice_index slice_range
|
|
196
|
+
slice_start_length sum_num_bytes_below_index
|
|
197
|
+
sum_num_bytes_for_all_elements].freeze
|
|
198
|
+
private_constant :ARRAY_METHODS, :ARRAY_PRIVATE_METHODS
|
|
199
|
+
|
|
200
|
+
# Reads of a held array, one fresh pair (held, BinData-built) per
|
|
201
|
+
# scenario; each returns what it observed.
|
|
202
|
+
FLAT_SCENARIOS = [
|
|
203
|
+
->(_wire, array, _write) { [array.size, array.first.snapshot, array.last.snapshot, array.to_a.map(&:snapshot)] },
|
|
204
|
+
->(_wire, array, _write) { [array.map { |point| point.x.value + point.y.value }, array[1].x.value] },
|
|
205
|
+
->(wire, array, _write) { [array.num_bytes, wire.num_bytes, array.empty?, array.clear?] },
|
|
206
|
+
->(wire, _array, _write) { [wire.snapshot] },
|
|
207
|
+
lambda { |wire, array, write|
|
|
208
|
+
array << { x: 9, y: -9 }
|
|
209
|
+
[array.size, array.last.snapshot, write.call(wire)]
|
|
210
|
+
},
|
|
211
|
+
lambda { |wire, array, write|
|
|
212
|
+
array.assign([])
|
|
213
|
+
[array.size, write.call(wire)]
|
|
214
|
+
},
|
|
215
|
+
lambda { |_wire, array, _write|
|
|
216
|
+
array.assign([{ x: 1, y: 2 }])
|
|
217
|
+
[array.size, array.first.snapshot, array.clear?]
|
|
218
|
+
},
|
|
219
|
+
->(_wire, array, _write) { [array.to_binary_s, array.clear?, array.snapshot] }
|
|
220
|
+
].freeze
|
|
221
|
+
private_constant :FLAT_SCENARIOS
|
|
222
|
+
|
|
223
|
+
# True when this BinData still behaves as the flat-points holder
|
|
224
|
+
# assumes: the Array API is the one it was written against and a
|
|
225
|
+
# parsed record that holds its points flat reads, writes and
|
|
226
|
+
# edits like the same record built by BinData. Any difference, or
|
|
227
|
+
# an error, makes parsed and built records use eager points.
|
|
228
|
+
def verify_flat_points
|
|
229
|
+
return false unless BinData::Array.instance_methods(false).sort == ARRAY_METHODS.sort &&
|
|
230
|
+
BinData::Array.private_instance_methods(false).sort == ARRAY_PRIVATE_METHODS.sort
|
|
231
|
+
|
|
232
|
+
bytes = bindata_bytes(verification_record)
|
|
233
|
+
write = method(:bindata_bytes)
|
|
234
|
+
FLAT_SCENARIOS.all? do |scenario|
|
|
235
|
+
held = call(bytes).first
|
|
236
|
+
reference = @wire_class.read(bytes)
|
|
237
|
+
held.public_send(array_name).respond_to?(:hold_flat_points) &&
|
|
238
|
+
scenario.call(held, held.public_send(array_name), write) ==
|
|
239
|
+
scenario.call(reference, reference.public_send(array_name), write)
|
|
240
|
+
end
|
|
241
|
+
rescue StandardError
|
|
242
|
+
false
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# Source of a FastFill's generated `build_attrs`, same result as
|
|
247
|
+
# WireCopy.build: every known key is fetched into a local first (an
|
|
248
|
+
# unknown key makes the count disagree and returns nil before any
|
|
249
|
+
# copy), then each non-nil value is assigned straight into the copy.
|
|
250
|
+
# A nested struct given as the same struct class is copied leaf by
|
|
251
|
+
# leaf inline; any other value goes through WireCopy.assign.
|
|
252
|
+
class BuildSource
|
|
253
|
+
include LazySource
|
|
254
|
+
|
|
255
|
+
ABSENT = Object.new.freeze
|
|
256
|
+
|
|
257
|
+
def initialize(template, slots, leaves = [], reads = [])
|
|
258
|
+
@struct_classes = []
|
|
259
|
+
@templates = []
|
|
260
|
+
@copy = copy_lines(template, "wire", "nil")
|
|
261
|
+
objs = template.instance_variable_get(:@field_objs)
|
|
262
|
+
@shell = shell_lines(template)
|
|
263
|
+
@lazy = lazy_cases(objs, slots, leaves, reads)
|
|
264
|
+
@fetches = []
|
|
265
|
+
@counts = []
|
|
266
|
+
@assigns = []
|
|
267
|
+
slots.each_with_index do |(name, index), position|
|
|
268
|
+
value = "v#{position}"
|
|
269
|
+
@fetches << "#{value} = attrs.fetch(:#{name}, ABSENT)"
|
|
270
|
+
@counts << "seen += 1 unless ABSENT.equal?(#{value})"
|
|
271
|
+
@assigns << assign(objs[index], index, value)
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# A module holding `build_attrs` and the constants it refers to.
|
|
276
|
+
def to_module
|
|
277
|
+
source = to_s
|
|
278
|
+
classes = @struct_classes
|
|
279
|
+
Module.new.tap do |mod|
|
|
280
|
+
mod.const_set(:STRUCTS, classes.freeze)
|
|
281
|
+
mod.const_set(:ABSENT, ABSENT)
|
|
282
|
+
mod.const_set(:TEMPLATES, @templates.freeze)
|
|
283
|
+
mod.module_eval(source, __FILE__, __LINE__)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def to_s
|
|
288
|
+
<<~RUBY
|
|
289
|
+
def copy_template
|
|
290
|
+
#{@copy.join("\n ")}
|
|
291
|
+
wire
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def copy_shell
|
|
295
|
+
#{@shell.join("\n ")}
|
|
296
|
+
wire
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# The field at `index` of a shell, copied from the template and
|
|
300
|
+
# given the value read from `data`, the record starting at `base`.
|
|
301
|
+
def lazy_field(index, wire, data, base)
|
|
302
|
+
case index
|
|
303
|
+
#{@lazy.join("\n ")}
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def build_attrs(attrs)
|
|
308
|
+
#{@fetches.join("\n ")}
|
|
309
|
+
seen = 0
|
|
310
|
+
#{@counts.join("\n ")}
|
|
311
|
+
return unless seen == attrs.size
|
|
312
|
+
#{@copy.join("\n ")}
|
|
313
|
+
objs = wire.instance_variable_get(:@field_objs)
|
|
314
|
+
#{@assigns.join("\n ")}
|
|
315
|
+
wire
|
|
316
|
+
end
|
|
317
|
+
RUBY
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
# Straight-line twin of WireCopy.call: dup of every struct and leaf of
|
|
323
|
+
# the template, parents pointed into the copy, field lists rebuilt.
|
|
324
|
+
def copy_lines(struct, var, parent)
|
|
325
|
+
@templates << struct
|
|
326
|
+
lines = ["#{var} = TEMPLATES[#{@templates.size - 1}].dup", "#{var}.instance_variable_set(:@parent, #{parent})"]
|
|
327
|
+
children = struct.instance_variable_get(:@field_objs).each_with_index.map do |field, index|
|
|
328
|
+
child = "#{var}_#{index}"
|
|
329
|
+
lines.concat(field.respond_to?(:field_names) ? copy_lines(field, child, var) : leaf_lines(field, child, var))
|
|
330
|
+
child
|
|
331
|
+
end
|
|
332
|
+
lines << "#{var}.instance_variable_set(:@field_objs, [#{children.join(', ')}])"
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def leaf_lines(field, var, parent)
|
|
336
|
+
@templates << field
|
|
337
|
+
copier = field.is_a?(BinData::Array) ? "clone" : "dup"
|
|
338
|
+
["#{var} = TEMPLATES[#{@templates.size - 1}].#{copier}", "#{var}.instance_variable_set(:@parent, #{parent})"]
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def assign(field, index, value)
|
|
342
|
+
present = "unless #{value}.nil? || ABSENT.equal?(#{value})"
|
|
343
|
+
return "objs[#{index}].assign(#{value}) #{present}" unless field.respond_to?(:field_names)
|
|
344
|
+
|
|
345
|
+
@struct_classes << field.class
|
|
346
|
+
klass = "STRUCTS[#{@struct_classes.size - 1}]"
|
|
347
|
+
<<~RUBY.chomp
|
|
348
|
+
#{present}
|
|
349
|
+
if #{value}.instance_of?(#{klass})
|
|
350
|
+
t1 = objs[#{index}].instance_variable_get(:@field_objs)
|
|
351
|
+
#{struct_copy(field, 1, value).join("\n ")}
|
|
352
|
+
else
|
|
353
|
+
Emf::Emr::Binary::WireCopy.assign(objs[#{index}], #{value})
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
RUBY
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Leaf-by-leaf assignments from `source` into the target field list `t<depth>`.
|
|
360
|
+
def struct_copy(struct, depth, source)
|
|
361
|
+
fields = struct.instance_variable_get(:@field_objs)
|
|
362
|
+
WireCopy.slots(struct).flat_map do |name, index|
|
|
363
|
+
field = fields[index]
|
|
364
|
+
if field.respond_to?(:field_names)
|
|
365
|
+
["t#{depth + 1} = t#{depth}[#{index}].instance_variable_get(:@field_objs)",
|
|
366
|
+
*struct_copy(field, depth + 1, "#{source}.#{name}")]
|
|
367
|
+
else
|
|
368
|
+
["t#{depth}[#{index}].assign(#{source}.#{name})"]
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Self-check for a FastFill, run once when the filler is built. Proves on
|
|
375
|
+
# the installed BinData that a template copy is a faithful, independent
|
|
376
|
+
# twin of a fresh instance and that the generated reader and writer agree
|
|
377
|
+
# with BinData's own on a record with distinct leaf values.
|
|
378
|
+
module SelfCheck
|
|
379
|
+
# Each compares a lazy record with the read one after the same use.
|
|
380
|
+
LAZY_SCENARIOS = [
|
|
381
|
+
->(lazy, ref, write, _) { lazy.snapshot == ref.snapshot && write.call(lazy) == write.call(ref) },
|
|
382
|
+
lambda { |lazy, ref, write, leaves|
|
|
383
|
+
path = leaves.last.first
|
|
384
|
+
last = ->(wire) { path.inject(wire) { |obj, name| obj.public_send(name) } }
|
|
385
|
+
last.call(lazy).value == last.call(ref).value && lazy.snapshot == ref.snapshot &&
|
|
386
|
+
write.call(lazy) == write.call(ref)
|
|
387
|
+
},
|
|
388
|
+
lambda { |lazy, ref, write, leaves|
|
|
389
|
+
[lazy, ref].each { |wire| leaves.first.first.inject(wire) { |o, n| o.public_send(n) }.assign(0x2A) }
|
|
390
|
+
lazy.snapshot == ref.snapshot && write.call(lazy) == write.call(ref)
|
|
391
|
+
},
|
|
392
|
+
lambda { |lazy, _ref, write, _|
|
|
393
|
+
lazy.assign(lazy.class.new)
|
|
394
|
+
fresh = lazy.class.new
|
|
395
|
+
lazy.snapshot == fresh.snapshot && write.call(lazy) == write.call(fresh)
|
|
396
|
+
},
|
|
397
|
+
lambda { |lazy, _ref, _write, _|
|
|
398
|
+
was_clear = lazy.clear?
|
|
399
|
+
lazy.clear
|
|
400
|
+
was_clear == false && lazy.snapshot == lazy.class.new.snapshot
|
|
401
|
+
}
|
|
402
|
+
].freeze
|
|
403
|
+
private_constant :LAZY_SCENARIOS
|
|
404
|
+
|
|
405
|
+
# Raised when BinData's internals do not behave as WireCopy assumes.
|
|
406
|
+
class Unverified < StandardError; end
|
|
407
|
+
|
|
408
|
+
BINDATA_WRITER = BinData::Base.instance_method(:to_binary_s)
|
|
409
|
+
private_constant :BINDATA_WRITER
|
|
410
|
+
|
|
411
|
+
def self.included(base)
|
|
412
|
+
base.extend(ClassMethods)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Class-level entry point.
|
|
416
|
+
module ClassMethods
|
|
417
|
+
# A verified filler for the class, or nil when the class cannot be
|
|
418
|
+
# filled safely: construction or the self-check raised or disagreed
|
|
419
|
+
# with plain BinData. Callers treat nil as "use BinData".
|
|
420
|
+
def guarded(klass)
|
|
421
|
+
filler = new(klass)
|
|
422
|
+
filler.verify!
|
|
423
|
+
filler
|
|
424
|
+
rescue StandardError
|
|
425
|
+
nil
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Raises Unverified (or whatever the internals raise) on any difference.
|
|
430
|
+
def verify!
|
|
431
|
+
fresh = @wire_class.new
|
|
432
|
+
verify_template(fresh)
|
|
433
|
+
verify_independent
|
|
434
|
+
sample = verification_record
|
|
435
|
+
bytes = bindata_bytes(sample) + verification_tail
|
|
436
|
+
verify_read(bytes)
|
|
437
|
+
verify_write(sample)
|
|
438
|
+
verify_build
|
|
439
|
+
@lazy_ok = verify_lazy(bytes)
|
|
440
|
+
self
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
private
|
|
444
|
+
|
|
445
|
+
# BinData::Struct as the lazy shell assumes (bindata 2.5.1): fields
|
|
446
|
+
# are reached only through instantiate_obj_at / instantiate_all_objs,
|
|
447
|
+
# and clear / clear? are the only readers of @field_objs besides.
|
|
448
|
+
STRUCT_METHODS = %i[[] []= assign clear clear? debug_name_of do_num_bytes do_read do_write each_pair
|
|
449
|
+
field_names has_key? initialize_instance initialize_shared_instance key?
|
|
450
|
+
offset_of snapshot].freeze
|
|
451
|
+
STRUCT_PRIVATE_METHODS = %i[as_stringified_hash assign_fields base_field_name define_field_accessors
|
|
452
|
+
define_field_accessors_for find_index_of find_obj_for_name include_obj?
|
|
453
|
+
include_obj_for_io? instantiate_all_objs instantiate_obj_at
|
|
454
|
+
sum_num_bytes_below_index sum_num_bytes_for_all_fields].freeze
|
|
455
|
+
private_constant :STRUCT_METHODS, :STRUCT_PRIVATE_METHODS
|
|
456
|
+
|
|
457
|
+
# True when a lazily filled record reads, writes and edits like the
|
|
458
|
+
# BinData-read one; false (eager fill) on any difference or error.
|
|
459
|
+
def verify_lazy(bytes)
|
|
460
|
+
return false unless BinData::Struct.instance_methods(false).sort == STRUCT_METHODS.sort &&
|
|
461
|
+
BinData::Struct.private_instance_methods(false).sort == STRUCT_PRIVATE_METHODS.sort
|
|
462
|
+
|
|
463
|
+
data = bytes.b.freeze
|
|
464
|
+
LAZY_SCENARIOS.all? do |scenario|
|
|
465
|
+
lazy = lazy_wire(data, 0)
|
|
466
|
+
reference = @wire_class.read(data)
|
|
467
|
+
lazy.instance_variable_get(:@lazy_filler) &&
|
|
468
|
+
scenario.call(lazy, reference, method(:bindata_bytes), @leaves) == true
|
|
469
|
+
end
|
|
470
|
+
rescue StandardError
|
|
471
|
+
false
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def unverified!(what)
|
|
475
|
+
raise Unverified, "#{@wire_class}: #{what}"
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
def bindata_bytes(wire)
|
|
479
|
+
BINDATA_WRITER.bind_call(wire)
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def verify_template(fresh)
|
|
483
|
+
[WireCopy.call(@template, nil), copy_template].each { |copy| verify_copy_of(copy, fresh) }
|
|
484
|
+
unverified!("slots do not cover the field list") unless @slots.keys == fresh.field_names
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def verify_copy_of(copy, fresh)
|
|
488
|
+
same = copy.instance_of?(@wire_class) && copy.snapshot == fresh.snapshot &&
|
|
489
|
+
copy.num_bytes == fresh.num_bytes && bindata_bytes(copy) == bindata_bytes(fresh)
|
|
490
|
+
unverified!("template copy differs from a fresh instance") unless same
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
# No object of a copy is shared with the template, and every parent
|
|
494
|
+
# link points into the copy.
|
|
495
|
+
def verify_independent
|
|
496
|
+
[-> { WireCopy.call(@template, nil) }, -> { copy_template }].each { |copier| verify_independent_copy(copier) }
|
|
497
|
+
end
|
|
498
|
+
|
|
499
|
+
def verify_independent_copy(copier)
|
|
500
|
+
copy = copier.call
|
|
501
|
+
shared = tree_objects(@template).map(&:object_id) & tree_objects(copy).map(&:object_id)
|
|
502
|
+
unverified!("copy shares objects with the template") unless shared.empty?
|
|
503
|
+
unverified!("copy parent links are wrong") unless parents_consistent?(copy)
|
|
504
|
+
edit_leaf(copy)
|
|
505
|
+
unverified!("editing a copy changed the template") unless copier.call.snapshot == @wire_class.new.snapshot
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
def tree_objects(struct)
|
|
509
|
+
objs = struct.instance_variable_get(:@field_objs)
|
|
510
|
+
raise Unverified, "no field objects" unless objs.is_a?(::Array)
|
|
511
|
+
|
|
512
|
+
[struct] + objs.flat_map { |obj| obj.respond_to?(:field_names) ? tree_objects(obj) : [obj] }
|
|
513
|
+
end
|
|
514
|
+
|
|
515
|
+
def parents_consistent?(struct)
|
|
516
|
+
struct.instance_variable_get(:@field_objs).all? do |obj|
|
|
517
|
+
obj.instance_variable_get(:@parent).equal?(struct) &&
|
|
518
|
+
(!obj.respond_to?(:field_names) || parents_consistent?(obj))
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
def edit_leaf(wire)
|
|
523
|
+
path, field = @leaves.first
|
|
524
|
+
target = path.inject(wire) { |obj, name| obj.public_send(name) }
|
|
525
|
+
float?(field) ? target.assign_bits(1) : target.assign(1)
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
# A record with a distinct small value in every leaf.
|
|
529
|
+
def verification_record
|
|
530
|
+
wire = @wire_class.new
|
|
531
|
+
@leaves.each_with_index do |(path, field), index|
|
|
532
|
+
target = path.inject(wire) { |obj, name| obj.public_send(name) }
|
|
533
|
+
float?(field) ? target.assign_bits(index + 1) : target.assign(index + 1)
|
|
534
|
+
end
|
|
535
|
+
wire
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def verification_tail
|
|
539
|
+
"\x07\x08".b
|
|
540
|
+
end
|
|
541
|
+
|
|
542
|
+
def verify_read(bytes)
|
|
543
|
+
wire, rest = call(bytes)
|
|
544
|
+
expected = @wire_class.read(bytes)
|
|
545
|
+
unverified!("call differs from read") unless wire.snapshot == expected.snapshot &&
|
|
546
|
+
bindata_bytes(wire) == bindata_bytes(expected)
|
|
547
|
+
unverified!("call kept the wrong suffix") unless rest == verification_tail
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def verify_write(sample)
|
|
551
|
+
packed = pack(sample)
|
|
552
|
+
return if packed.nil? || packed == bindata_bytes(sample)
|
|
553
|
+
|
|
554
|
+
unverified!("pack differs from BinData's writer")
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Keyword construction with the first scalar field set.
|
|
558
|
+
def verify_build
|
|
559
|
+
name = @slots.each_key.find { |slot| @leaves.any? { |path, _| path == [slot] } }
|
|
560
|
+
return unless name
|
|
561
|
+
|
|
562
|
+
built = build(name => 1)
|
|
563
|
+
unverified!("build refused a known field") unless built
|
|
564
|
+
expected = @wire_class.new({ name => 1 })
|
|
565
|
+
same = built.snapshot == expected.snapshot && bindata_bytes(built) == bindata_bytes(expected)
|
|
566
|
+
unverified!("build differs from BinData's constructor") unless same
|
|
567
|
+
verify_build_copy
|
|
568
|
+
verify_build_structs
|
|
569
|
+
unverified!("build accepted an unknown key") if build(name => 1, no_such_field_: 1)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# The generated copy is a fresh, independent twin of the template.
|
|
573
|
+
def verify_build_copy
|
|
574
|
+
copy = build({})
|
|
575
|
+
unless copy.instance_of?(@wire_class) && copy.snapshot == @wire_class.new.snapshot
|
|
576
|
+
unverified!("build copy differs from a fresh instance")
|
|
577
|
+
end
|
|
578
|
+
shared = tree_objects(@template).map(&:object_id) & tree_objects(copy).map(&:object_id)
|
|
579
|
+
unverified!("build copy shares objects with the template") unless shared.empty? && parents_consistent?(copy)
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
# Every nested struct field, given as a distinct-valued struct of its
|
|
583
|
+
# own class, builds the same record as BinData's constructor.
|
|
584
|
+
def verify_build_structs
|
|
585
|
+
sample = verification_record
|
|
586
|
+
@slots.each_key do |slot|
|
|
587
|
+
next unless @template.public_send(slot).respond_to?(:field_names)
|
|
588
|
+
|
|
589
|
+
given = sample.public_send(slot)
|
|
590
|
+
built = build(slot => given)
|
|
591
|
+
expected = @wire_class.new({ slot => given })
|
|
592
|
+
same = built && built.snapshot == expected.snapshot && bindata_bytes(built) == bindata_bytes(expected)
|
|
593
|
+
unverified!("build of #{slot} differs from BinData's constructor") unless same
|
|
594
|
+
end
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
|
|
598
|
+
# Eager fill of fixed-layout records: one String#unpack for the whole
|
|
599
|
+
# record, then leaf assignment into a fresh instance of the same wire
|
|
600
|
+
# class. The unpack format and leaf paths are derived once per class
|
|
601
|
+
# from its field list, so the result is indistinguishable from `read`.
|
|
602
|
+
class FastFill
|
|
603
|
+
DIRECTIVES = {
|
|
604
|
+
"BinData::Uint32le" => "V",
|
|
605
|
+
"BinData::Int32le" => "l<",
|
|
606
|
+
"BinData::Uint8" => "C",
|
|
607
|
+
"Emf::Binary::Float32Le" => "V"
|
|
608
|
+
}.freeze
|
|
609
|
+
FLOAT_LEAF = "Emf::Binary::Float32Le"
|
|
610
|
+
EMPTY_SUFFIX = "".b.freeze
|
|
611
|
+
private_constant :DIRECTIVES, :FLOAT_LEAF, :EMPTY_SUFFIX
|
|
612
|
+
|
|
613
|
+
include SelfCheck
|
|
614
|
+
include LeafReads
|
|
615
|
+
|
|
616
|
+
def self.for(type_id)
|
|
617
|
+
BY_TYPE[type_id]
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def self.for_class(wire_class)
|
|
621
|
+
BY_CLASS[wire_class] || POINT_ARRAY_BY_CLASS[wire_class] || TEXT_BY_CLASS[wire_class]
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
attr_reader :size, :wire_class, :template, :field_count
|
|
625
|
+
|
|
626
|
+
# The PackPlan for the class (bytes straight from attributes), or nil
|
|
627
|
+
# when no verified plan exists.
|
|
628
|
+
def pack_plan = defined?(@pack_plan) ? @pack_plan : (@pack_plan = PackPlan.build(self))
|
|
629
|
+
|
|
630
|
+
def initialize(klass)
|
|
631
|
+
@wire_class = klass
|
|
632
|
+
@lazy_ok = true
|
|
633
|
+
sample = klass.new
|
|
634
|
+
@size = sample.num_bytes
|
|
635
|
+
@leaves = leaves = leaf_paths(sample, [])
|
|
636
|
+
# leaf_paths touched every field, so the sample is fully instantiated.
|
|
637
|
+
@template = sample
|
|
638
|
+
@field_count, @shell_params, @shell_names = shell_identity(sample)
|
|
639
|
+
@format = leaves.map { |_, field| DIRECTIVES.fetch(field.name) }.join
|
|
640
|
+
@leaf_reads = leaf_reads(leaves, DIRECTIVES)
|
|
641
|
+
# Float layouts are not packed: measured on test-000, BinData writes
|
|
642
|
+
# SETWORLDTRANSFORM in 15 objects against 18+ for a value pack, and
|
|
643
|
+
# Float32Le must write its retained NaN bits itself.
|
|
644
|
+
@pack_format = @format unless leaves.any? { |_, field| float?(field) }
|
|
645
|
+
@slots = WireCopy.slots(sample)
|
|
646
|
+
generate_call(leaves)
|
|
647
|
+
generate_pack(leaves)
|
|
648
|
+
extend(BuildSource.new(@template, @slots, leaves, @leaf_reads).to_module)
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Keyword construction, `wire_class.new(**attrs)`; nil when a key is
|
|
652
|
+
# not a field, so the caller falls back to BinData. Generated per
|
|
653
|
+
# class by generate_build; subclasses override `build` and call super.
|
|
654
|
+
def build(attrs) = build_attrs(attrs)
|
|
655
|
+
|
|
656
|
+
# [wire, suffix] for one record's bytes. The wire reads its fields
|
|
657
|
+
# from a private frozen view of them when they are first used.
|
|
658
|
+
def call(record_bytes)
|
|
659
|
+
data = record_bytes.b.freeze
|
|
660
|
+
call_at(data, 0, data.bytesize)
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# The same for the n_size-byte record at `offset` of the frozen
|
|
664
|
+
# binary `data`, which the wire keeps and reads fields from.
|
|
665
|
+
def call_at(data, offset, n_size)
|
|
666
|
+
[lazy_wire(data, offset), suffix(data, offset, n_size)]
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
# The wire `call_at` answers for a record that is exactly the layout
|
|
670
|
+
# (no suffix), without the [wire, suffix] pair; nil for any other
|
|
671
|
+
# n_size, which `call_at` reads.
|
|
672
|
+
def exact_wire(data, offset, n_size) = (lazy_wire(data, offset) if n_size == @size)
|
|
673
|
+
|
|
674
|
+
# The bytes a wire of the record at `base` of `data` is.
|
|
675
|
+
def record_size(_data, _base) = @size
|
|
676
|
+
|
|
677
|
+
# Whether `call_at` reads the n_size-byte record at `offset`: always
|
|
678
|
+
# for a fixed layout.
|
|
679
|
+
def fits?(_data, _offset, _n_size) = true
|
|
680
|
+
|
|
681
|
+
# Called by a lazy wire's field copy for a field `call` has no
|
|
682
|
+
# unpacked leaf value for (subclasses with such a field override it).
|
|
683
|
+
def fill_unvalued(_field, _data, _base); end
|
|
684
|
+
|
|
685
|
+
private
|
|
686
|
+
|
|
687
|
+
def float?(field)
|
|
688
|
+
field.name == FLOAT_LEAF
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Bytes of the record at `offset` of `data` past the `consumed` ones.
|
|
692
|
+
def suffix(data, offset, n_size, consumed = @size)
|
|
693
|
+
return EMPTY_SUFFIX if n_size == consumed
|
|
694
|
+
|
|
695
|
+
data.byteslice(offset + consumed, n_size - consumed).b.freeze
|
|
696
|
+
end
|
|
697
|
+
|
|
698
|
+
def leaf_paths(record, prefix)
|
|
699
|
+
record.field_names.flat_map do |name|
|
|
700
|
+
field = record.public_send(name)
|
|
701
|
+
if field.respond_to?(:field_names)
|
|
702
|
+
leaf_paths(field, prefix + [name])
|
|
703
|
+
else
|
|
704
|
+
[[prefix + [name], field.class]]
|
|
705
|
+
end
|
|
706
|
+
end
|
|
707
|
+
end
|
|
708
|
+
|
|
709
|
+
# One specialized `call` per wire class: the record is unpacked into
|
|
710
|
+
# locals and each leaf is assigned through the accessors BinData
|
|
711
|
+
# already resolved for the class, with no per-leaf dispatch loop.
|
|
712
|
+
def generate_call(_leaves); end
|
|
713
|
+
|
|
714
|
+
def generate_pack(leaves)
|
|
715
|
+
if @pack_format
|
|
716
|
+
reads = leaves.map { |path, _| "#{accessor_chain(path)}.value" }
|
|
717
|
+
body = "[#{reads.join(', ')}].pack(@pack_format)"
|
|
718
|
+
else
|
|
719
|
+
body = "nil"
|
|
720
|
+
end
|
|
721
|
+
define_generated(:pack, "def pack(wire)\n #{body}\nend\n")
|
|
722
|
+
end
|
|
723
|
+
|
|
724
|
+
def accessor_chain(path)
|
|
725
|
+
path.inject("wire") { |expr, name| "#{expr}.#{name}" }
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def define_generated(name, source)
|
|
729
|
+
singleton_class.class_eval(source, __FILE__, __LINE__)
|
|
730
|
+
singleton_class.send(:public, name)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
ENABLED_TYPES = [
|
|
734
|
+
TypeCodes::SELECTOBJECT, TypeCodes::MOVETOEX, TypeCodes::LINETO,
|
|
735
|
+
TypeCodes::DELETEOBJECT, TypeCodes::CREATEBRUSHINDIRECT,
|
|
736
|
+
TypeCodes::SETBKCOLOR, TypeCodes::SETPIXELV, TypeCodes::CREATEPEN,
|
|
737
|
+
TypeCodes::SETBKMODE, TypeCodes::SETTEXTCOLOR, TypeCodes::SETTEXTALIGN,
|
|
738
|
+
TypeCodes::ELLIPSE, TypeCodes::SETVIEWPORTORGEX, TypeCodes::RESTOREDC,
|
|
739
|
+
TypeCodes::SETWORLDTRANSFORM, TypeCodes::MODIFYWORLDTRANSFORM,
|
|
740
|
+
TypeCodes::SETMAPMODE, TypeCodes::SETWINDOWEXTEX, TypeCodes::SETWINDOWORGEX,
|
|
741
|
+
TypeCodes::SETVIEWPORTEXTEX, TypeCodes::SETPOLYFILLMODE, TypeCodes::SETROP2,
|
|
742
|
+
TypeCodes::SETSTRETCHBLTMODE, TypeCodes::SETBRUSHORGEX, TypeCodes::SAVEDC,
|
|
743
|
+
TypeCodes::BEGINPATH, TypeCodes::ENDPATH, TypeCodes::CLOSEFIGURE,
|
|
744
|
+
TypeCodes::FILLPATH, TypeCodes::STROKEANDFILLPATH, TypeCodes::STROKEPATH,
|
|
745
|
+
TypeCodes::RECTANGLE, TypeCodes::INTERSECTCLIPRECT, TypeCodes::EXCLUDECLIPRECT,
|
|
746
|
+
TypeCodes::SETMITERLIMIT, TypeCodes::SETICMMODE, TypeCodes::SETLAYOUT
|
|
747
|
+
].freeze
|
|
748
|
+
BY_TYPE = ENABLED_TYPES.to_h { |type_id| [type_id, guarded(Records.lookup(type_id))] }.compact.freeze
|
|
749
|
+
BY_CLASS = BY_TYPE.values.to_h { |filler| [filler.wire_class, filler] }.freeze
|
|
750
|
+
private_constant :ENABLED_TYPES, :BY_TYPE, :BY_CLASS
|
|
751
|
+
end
|
|
752
|
+
|
|
753
|
+
class FastFill
|
|
754
|
+
# Extended onto the template's array (clones keep it). A built
|
|
755
|
+
# record holds its coordinates as one flat list of integers; the
|
|
756
|
+
# BinData points are made the first time anything reads the array's
|
|
757
|
+
# elements, so a record built and serialized untouched never pays
|
|
758
|
+
# for them. Every reader of @elements in BinData::Array goes through
|
|
759
|
+
# `elements`, `clear?` or `assign`, which are the methods covered here.
|
|
760
|
+
module FlatPoints
|
|
761
|
+
def hold_flat_points(flat, filler)
|
|
762
|
+
@elements = nil
|
|
763
|
+
@flat_points = flat
|
|
764
|
+
@point_filler = filler
|
|
765
|
+
end
|
|
766
|
+
|
|
767
|
+
def initialize_instance
|
|
768
|
+
@flat_points = @point_filler = nil
|
|
769
|
+
super
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def clear?
|
|
773
|
+
@flat_points ? false : super
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
# Makes the points, so the array holds no filler (Marshal cannot
|
|
777
|
+
# dump generated code).
|
|
778
|
+
def settle_points
|
|
779
|
+
materialize_points if @flat_points
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def assign(array)
|
|
783
|
+
return if equal?(array)
|
|
784
|
+
|
|
785
|
+
@flat_points = @point_filler = nil
|
|
786
|
+
super
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# The coordinates of the given points as one list of Integers, the
|
|
790
|
+
# values `assign` would store; nil when any is not an Integer or
|
|
791
|
+
# there are no points, leaving those to the element-by-element copy.
|
|
792
|
+
def self.coords_of(points)
|
|
793
|
+
return if points.empty?
|
|
794
|
+
|
|
795
|
+
flat = Array.new(points.size * 2)
|
|
796
|
+
points.each_with_index do |source, index|
|
|
797
|
+
x, y = source.is_a?(Hash) ? [source[:x], source[:y]] : [source.x, source.y]
|
|
798
|
+
flat[index * 2] = x.respond_to?(:snapshot) ? x.snapshot : x
|
|
799
|
+
flat[(index * 2) + 1] = y.respond_to?(:snapshot) ? y.snapshot : y
|
|
800
|
+
end
|
|
801
|
+
return unless flat.all?(Integer)
|
|
802
|
+
|
|
803
|
+
flat
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
private
|
|
807
|
+
|
|
808
|
+
def elements
|
|
809
|
+
materialize_points if @flat_points
|
|
810
|
+
super
|
|
811
|
+
end
|
|
812
|
+
|
|
813
|
+
# Build the full element list before publishing it, so a concurrent
|
|
814
|
+
# reader sees either the flat list or complete elements.
|
|
815
|
+
def materialize_points
|
|
816
|
+
list = @point_filler.points_from(self, @flat_points)
|
|
817
|
+
@elements = list
|
|
818
|
+
@flat_points = @point_filler = nil
|
|
819
|
+
end
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
# Builds BinData points from the filler's point template (`@point`
|
|
823
|
+
# and its x and y leaves, set up by PointArrayFill).
|
|
824
|
+
module PointCopying
|
|
825
|
+
# Points as BinData elements, built from one flat coordinate list.
|
|
826
|
+
def points_from(array, flat)
|
|
827
|
+
Array.new(flat.size / 2) do |index|
|
|
828
|
+
point, x, y = point_copy(array)
|
|
829
|
+
x.assign(flat[index * 2])
|
|
830
|
+
y.assign(flat[(index * 2) + 1])
|
|
831
|
+
point
|
|
832
|
+
end
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
private
|
|
836
|
+
|
|
837
|
+
def point_copy(array)
|
|
838
|
+
point = @point.dup
|
|
839
|
+
x = @x_leaf.dup
|
|
840
|
+
y = @y_leaf.dup
|
|
841
|
+
point.instance_variable_set(:@parent, array)
|
|
842
|
+
x.instance_variable_set(:@parent, point)
|
|
843
|
+
y.instance_variable_set(:@parent, point)
|
|
844
|
+
point.instance_variable_set(:@field_objs, [x, y])
|
|
845
|
+
[point, x, y]
|
|
846
|
+
end
|
|
847
|
+
|
|
848
|
+
def copy_point(array, source)
|
|
849
|
+
point, x, y = point_copy(array)
|
|
850
|
+
x.assign(source.is_a?(Hash) ? source[:x] : source.x)
|
|
851
|
+
y.assign(source.is_a?(Hash) ? source[:y] : source.y)
|
|
852
|
+
point
|
|
853
|
+
end
|
|
854
|
+
end
|
|
855
|
+
|
|
856
|
+
# Header plus one counted array of PointL (32-bit) or PointS (16-bit)
|
|
857
|
+
# points. The array field, its count field, the point class, the
|
|
858
|
+
# unpack directive and the coordinate range are all read from the
|
|
859
|
+
# record template, so one class serves every such record. The header is unpacked and assigned like a fixed layout; the points
|
|
860
|
+
# come from one `unpack` and are built by copying a PointL template,
|
|
861
|
+
# so the array holds the same BinData elements a read produces.
|
|
862
|
+
# `call` returns nil when the count overruns the bytes, and `pack`
|
|
863
|
+
# when a coordinate is not an int32, leaving those to BinData.
|
|
864
|
+
class PointArrayFill < FastFill
|
|
865
|
+
include PointCopying
|
|
866
|
+
include FlatPointsCheck
|
|
867
|
+
|
|
868
|
+
LAYOUTS = {
|
|
869
|
+
"BinData::Int32le" => { directive: "l<", bytes: 8, range: (-(2**31))..((2**31) - 1) },
|
|
870
|
+
"BinData::Int16le" => { directive: "s<", bytes: 4, range: (-(2**15))..((2**15) - 1) }
|
|
871
|
+
}.freeze
|
|
872
|
+
private_constant :LAYOUTS
|
|
873
|
+
|
|
874
|
+
def initialize(klass)
|
|
875
|
+
super
|
|
876
|
+
array = @template.public_send(array_name)
|
|
877
|
+
@point = array.send(:new_element)
|
|
878
|
+
@point_class = @point.class
|
|
879
|
+
@directive, @point_bytes, @range = LAYOUTS.fetch(@point.x.class.name).values_at(:directive, :bytes, :range)
|
|
880
|
+
@x_leaf = @point.x
|
|
881
|
+
@y_leaf = @point.y
|
|
882
|
+
# The sample's num_bytes built the array's (empty) element list and
|
|
883
|
+
# cached evaluators that read the template's cptl; copies share
|
|
884
|
+
# neither.
|
|
885
|
+
array.instance_variable_set(:@elements, nil)
|
|
886
|
+
[@template, array, @point, @x_leaf, @y_leaf].each { |object| WireCopy.forget_evaluator(object) }
|
|
887
|
+
array.extend(FlatPoints)
|
|
888
|
+
@pack_points_format = "#{@format}#{@directive}*"
|
|
889
|
+
@hold_flat = true
|
|
890
|
+
@hold_flat = verify_flat_points
|
|
891
|
+
end
|
|
892
|
+
|
|
893
|
+
# `nil` when the count overruns the n_size bytes of the record.
|
|
894
|
+
def call_at(data, offset, n_size)
|
|
895
|
+
consumed = @size + (data.unpack1("V", offset: offset + @count_offset) * @point_bytes)
|
|
896
|
+
return if n_size < consumed
|
|
897
|
+
|
|
898
|
+
[lazy_wire(data, offset), suffix(data, offset, n_size, consumed)]
|
|
899
|
+
end
|
|
900
|
+
|
|
901
|
+
def fits?(data, offset, n_size)
|
|
902
|
+
record_size(data, offset) <= n_size
|
|
903
|
+
end
|
|
904
|
+
|
|
905
|
+
def record_size(data, base)
|
|
906
|
+
@size + (data.unpack1("V", offset: base + @count_offset) * @point_bytes)
|
|
907
|
+
end
|
|
908
|
+
|
|
909
|
+
# The array field of a lazy wire: its points, held flat or built.
|
|
910
|
+
def fill_unvalued(array, data, base)
|
|
911
|
+
count = data.unpack1("V", offset: base + @count_offset)
|
|
912
|
+
flat = data.unpack("#{@directive}#{count * 2}", offset: base + @size)
|
|
913
|
+
if @hold_flat
|
|
914
|
+
array.hold_flat_points(flat, self)
|
|
915
|
+
else
|
|
916
|
+
array.instance_variable_set(:@elements, points_from(array, flat))
|
|
917
|
+
end
|
|
918
|
+
end
|
|
919
|
+
|
|
920
|
+
# Keyword construction; the points are built from template copies
|
|
921
|
+
# when every element is a PointL or an {x:, y:} hash, else BinData's.
|
|
922
|
+
def build(attrs)
|
|
923
|
+
points = attrs[array_name]
|
|
924
|
+
return super unless points.is_a?(::Array) && points.all? { |source| fast_point?(source) }
|
|
925
|
+
|
|
926
|
+
wire = super(attrs.except(array_name))
|
|
927
|
+
return unless wire
|
|
928
|
+
|
|
929
|
+
array = wire.public_send(array_name)
|
|
930
|
+
flat = FlatPoints.coords_of(points)
|
|
931
|
+
if flat && @hold_flat
|
|
932
|
+
array.hold_flat_points(flat, self)
|
|
933
|
+
else
|
|
934
|
+
array.instance_variable_set(:@elements, points.map { |source| copy_point(array, source) })
|
|
935
|
+
end
|
|
936
|
+
wire
|
|
937
|
+
end
|
|
938
|
+
|
|
939
|
+
# [point class, coordinate directive, low, high, bytes per coordinate, count field]
|
|
940
|
+
def points_layout
|
|
941
|
+
[@point_class, @directive, @range.begin, @range.end, @point_bytes / 2, count_name]
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
def pack(wire)
|
|
945
|
+
array = wire.public_send(array_name)
|
|
946
|
+
coords = array.instance_variable_get(:@flat_points) || collect_coords(array)
|
|
947
|
+
return unless coords.all?(Integer) && coords.all?(@range)
|
|
948
|
+
|
|
949
|
+
header_values(wire).concat(coords).pack(@pack_points_format)
|
|
950
|
+
end
|
|
951
|
+
|
|
952
|
+
private
|
|
953
|
+
|
|
954
|
+
def collect_coords(array)
|
|
955
|
+
array.each_with_object([]) { |point, coords| coords << point.x.value << point.y.value }
|
|
956
|
+
end
|
|
957
|
+
|
|
958
|
+
# Two points, so the counted array is exercised by the self-check.
|
|
959
|
+
def verification_record
|
|
960
|
+
wire = super
|
|
961
|
+
wire.public_send(count_name).assign(2)
|
|
962
|
+
wire.public_send(array_name).assign([{ x: 3, y: 4 }, { x: -5, y: 6 }])
|
|
963
|
+
wire
|
|
964
|
+
end
|
|
965
|
+
|
|
966
|
+
# The record's one counted array field.
|
|
967
|
+
def array_name
|
|
968
|
+
@array_name ||= @template.field_names.find { |name| @template.public_send(name).is_a?(BinData::Array) }
|
|
969
|
+
end
|
|
970
|
+
|
|
971
|
+
def count_name
|
|
972
|
+
@template.public_send(array_name).send(:get_parameter, :initial_length)
|
|
973
|
+
end
|
|
974
|
+
|
|
975
|
+
def leaf_paths(record, prefix)
|
|
976
|
+
super.reject { |_, field| field <= BinData::Array }
|
|
977
|
+
end
|
|
978
|
+
|
|
979
|
+
def fast_point?(point)
|
|
980
|
+
point.instance_of?(@point_class) ||
|
|
981
|
+
(point.instance_of?(Hash) && point.size == 2 && point.key?(:x) && point.key?(:y))
|
|
982
|
+
end
|
|
983
|
+
|
|
984
|
+
def generate_call(leaves)
|
|
985
|
+
count = leaves.index { |path, _| path == [count_name] }
|
|
986
|
+
@count_offset = @leaf_reads.fetch(count).last
|
|
987
|
+
end
|
|
988
|
+
|
|
989
|
+
def generate_pack(leaves)
|
|
990
|
+
reads = leaves.map { |path, _| "#{accessor_chain(path)}.value" }
|
|
991
|
+
define_generated(:header_values, "def header_values(wire)\n [#{reads.join(', ')}]\nend\n")
|
|
992
|
+
end
|
|
993
|
+
end
|
|
994
|
+
|
|
995
|
+
POINT_ARRAY_TYPES = [
|
|
996
|
+
TypeCodes::POLYGON, TypeCodes::POLYLINE, TypeCodes::POLYBEZIER, TypeCodes::POLYBEZIERTO,
|
|
997
|
+
TypeCodes::POLYLINETO, TypeCodes::POLYGON16, TypeCodes::POLYLINE16,
|
|
998
|
+
TypeCodes::POLYBEZIER16, TypeCodes::POLYBEZIERTO16, TypeCodes::POLYLINETO16
|
|
999
|
+
].freeze
|
|
1000
|
+
POINT_ARRAY_BY_TYPE = POINT_ARRAY_TYPES.to_h do |type_id|
|
|
1001
|
+
[type_id, PointArrayFill.guarded(Records.lookup(type_id))]
|
|
1002
|
+
end.compact.freeze
|
|
1003
|
+
POINT_ARRAY_BY_CLASS = POINT_ARRAY_BY_TYPE.values.to_h { |filler| [filler.wire_class, filler] }.freeze
|
|
1004
|
+
private_constant :POINT_ARRAY_TYPES, :POINT_ARRAY_BY_TYPE, :POINT_ARRAY_BY_CLASS
|
|
1005
|
+
|
|
1006
|
+
# The filler for a header plus point array record, which parses only
|
|
1007
|
+
# when its count fits the bytes.
|
|
1008
|
+
def self.for_points(type_id)
|
|
1009
|
+
POINT_ARRAY_BY_TYPE[type_id]
|
|
1010
|
+
end
|
|
1011
|
+
end
|
|
1012
|
+
|
|
1013
|
+
class FastFill
|
|
1014
|
+
# EMR_EXTTEXTOUTA/W: a fixed header, an optional clip rectangle (absent
|
|
1015
|
+
# when f_options has ETO_NO_RECT, 0x100) and `rest :trailing`, which
|
|
1016
|
+
# takes every remaining byte, so a parse never leaves a suffix. The
|
|
1017
|
+
# option bits are read from the bytes to pick the with-rect or
|
|
1018
|
+
# without-rect layout; both unpack the header in one call and copy
|
|
1019
|
+
# one template, the rect leaves just staying at their defaults when
|
|
1020
|
+
# absent, as BinData leaves them. `call` returns nil when the bytes
|
|
1021
|
+
# are too short for the layout they select, and `pack` when a scale is
|
|
1022
|
+
# NaN (Float32Le keeps NaN bits) or trailing is not a String, leaving
|
|
1023
|
+
# those to BinData.
|
|
1024
|
+
class TextFill < FastFill
|
|
1025
|
+
NO_RECT = 0x100
|
|
1026
|
+
OPTIONS_OFFSET = 52
|
|
1027
|
+
FLOAT_DIRECTIVE = "e"
|
|
1028
|
+
private_constant :NO_RECT, :OPTIONS_OFFSET, :FLOAT_DIRECTIVE
|
|
1029
|
+
|
|
1030
|
+
def initialize(klass)
|
|
1031
|
+
super
|
|
1032
|
+
@rect_size = @size
|
|
1033
|
+
@size = @rect_size - rect_bytes
|
|
1034
|
+
forget_evaluators(@template)
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
def call_at(data, offset, n_size)
|
|
1038
|
+
call(data.byteslice(offset, n_size))
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
# A text record's layout varies with its options, so `call_at` reads it.
|
|
1042
|
+
def exact_wire(_data, _offset, _n_size); end
|
|
1043
|
+
|
|
1044
|
+
def call(record_bytes)
|
|
1045
|
+
return if record_bytes.bytesize < @size
|
|
1046
|
+
|
|
1047
|
+
if record_bytes.unpack1("V", offset: OPTIONS_OFFSET).anybits?(NO_RECT)
|
|
1048
|
+
read_plain(record_bytes)
|
|
1049
|
+
elsif record_bytes.bytesize >= @rect_size
|
|
1050
|
+
read_rect(record_bytes)
|
|
1051
|
+
end
|
|
1052
|
+
end
|
|
1053
|
+
|
|
1054
|
+
def pack(wire)
|
|
1055
|
+
options = wire.f_options.value
|
|
1056
|
+
trailing = wire.trailing.value
|
|
1057
|
+
return unless options.is_a?(Integer) && trailing.is_a?(::String)
|
|
1058
|
+
return unless [wire.ex_scale.value, wire.ey_scale.value].all? { |v| v.is_a?(Numeric) && !(v.is_a?(Float) && v.nan?) }
|
|
1059
|
+
|
|
1060
|
+
if options.anybits?(NO_RECT)
|
|
1061
|
+
[*plain_values(wire), trailing].pack("#{@plain_pack_format}a*")
|
|
1062
|
+
else
|
|
1063
|
+
[*rect_values(wire), trailing].pack("#{@rect_pack_format}a*")
|
|
1064
|
+
end
|
|
1065
|
+
end
|
|
1066
|
+
|
|
1067
|
+
private
|
|
1068
|
+
|
|
1069
|
+
def rect_bytes
|
|
1070
|
+
@template.rcl.num_bytes
|
|
1071
|
+
end
|
|
1072
|
+
|
|
1073
|
+
def forget_evaluators(object)
|
|
1074
|
+
WireCopy.forget_evaluator(object)
|
|
1075
|
+
object.instance_variable_get(:@field_objs).each do |field|
|
|
1076
|
+
field.respond_to?(:field_names) ? forget_evaluators(field) : WireCopy.forget_evaluator(field)
|
|
1077
|
+
end
|
|
1078
|
+
end
|
|
1079
|
+
|
|
1080
|
+
def leaf_paths(record, prefix)
|
|
1081
|
+
super.reject { |path, _| path == [:trailing] }
|
|
1082
|
+
end
|
|
1083
|
+
|
|
1084
|
+
# Distinct trailing bytes, so the rest field is exercised.
|
|
1085
|
+
def verification_record
|
|
1086
|
+
wire = super
|
|
1087
|
+
wire.trailing.assign("abc".b)
|
|
1088
|
+
wire
|
|
1089
|
+
end
|
|
1090
|
+
|
|
1091
|
+
def verification_tail
|
|
1092
|
+
"".b
|
|
1093
|
+
end
|
|
1094
|
+
|
|
1095
|
+
# The base self-check covers the with-rect layout; this repeats the
|
|
1096
|
+
# read and write comparison with ETO_NO_RECT set, where BinData
|
|
1097
|
+
# skips the rect.
|
|
1098
|
+
def verify_write(sample)
|
|
1099
|
+
super
|
|
1100
|
+
sample.f_options.assign(NO_RECT)
|
|
1101
|
+
bytes = bindata_bytes(sample)
|
|
1102
|
+
unverified!("no-rect layout is not shorter") unless bytes.bytesize == @size + 3
|
|
1103
|
+
wire, rest = call(bytes)
|
|
1104
|
+
expected = @wire_class.read(bytes)
|
|
1105
|
+
same = wire.snapshot == expected.snapshot && bindata_bytes(wire) == bytes && rest.empty?
|
|
1106
|
+
unverified!("no-rect call differs from read") unless same
|
|
1107
|
+
unverified!("no-rect pack differs from BinData's writer") unless pack(sample) == bytes
|
|
1108
|
+
end
|
|
1109
|
+
|
|
1110
|
+
def generate_call(leaves)
|
|
1111
|
+
plain = leaves.reject { |path, _| path.first == :rcl }
|
|
1112
|
+
@rect_format = @format
|
|
1113
|
+
@plain_format = plain.map { |_, field| DIRECTIVES.fetch(field.name) }.join
|
|
1114
|
+
@rect_pack_format = pack_format(leaves)
|
|
1115
|
+
@plain_pack_format = pack_format(plain)
|
|
1116
|
+
define_reader(:read_rect, leaves, @rect_format, :@rect_size)
|
|
1117
|
+
define_reader(:read_plain, plain, @plain_format, :@size)
|
|
1118
|
+
define_values(:rect_values, leaves)
|
|
1119
|
+
define_values(:plain_values, plain)
|
|
1120
|
+
end
|
|
1121
|
+
|
|
1122
|
+
def generate_pack(_leaves); end
|
|
1123
|
+
|
|
1124
|
+
# Scales are packed as floats, everything else as in the read format.
|
|
1125
|
+
def pack_format(leaves)
|
|
1126
|
+
leaves.map { |_, field| float?(field) ? FLOAT_DIRECTIVE : DIRECTIVES.fetch(field.name) }.join
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
def define_reader(name, leaves, format, size)
|
|
1130
|
+
locals = leaves.each_index.map { |i| "v#{i}" }
|
|
1131
|
+
lines = leaves.each_with_index.map do |(path, field), i|
|
|
1132
|
+
"#{accessor_chain(path)}.#{float?(field) ? 'assign_bits' : 'assign'}(v#{i})"
|
|
1133
|
+
end
|
|
1134
|
+
define_generated(name, <<~RUBY)
|
|
1135
|
+
def #{name}(record_bytes)
|
|
1136
|
+
#{locals.join(', ')} = record_bytes.unpack("#{format}")
|
|
1137
|
+
wire = copy_template
|
|
1138
|
+
#{lines.join("\n ")}
|
|
1139
|
+
wire.trailing.assign(record_bytes.byteslice(#{size}, record_bytes.bytesize - #{size}))
|
|
1140
|
+
[wire, EMPTY_SUFFIX]
|
|
1141
|
+
end
|
|
1142
|
+
RUBY
|
|
1143
|
+
end
|
|
1144
|
+
|
|
1145
|
+
# Pack-ready values; scale leaves are read as floats, not bits.
|
|
1146
|
+
def define_values(name, leaves)
|
|
1147
|
+
reads = leaves.map { |path, _| "#{accessor_chain(path)}.value" }
|
|
1148
|
+
define_generated(name, "def #{name}(wire)\n [#{reads.join(', ')}]\nend\n")
|
|
1149
|
+
end
|
|
1150
|
+
end
|
|
1151
|
+
|
|
1152
|
+
TEXT_TYPES = [TypeCodes::EXTTEXTOUTA, TypeCodes::EXTTEXTOUTW].freeze
|
|
1153
|
+
TEXT_BY_TYPE = TEXT_TYPES.to_h { |type_id| [type_id, TextFill.guarded(Records.lookup(type_id))] }.compact.freeze
|
|
1154
|
+
TEXT_BY_CLASS = TEXT_BY_TYPE.values.to_h { |filler| [filler.wire_class, filler] }.freeze
|
|
1155
|
+
private_constant :TEXT_TYPES, :TEXT_BY_TYPE, :TEXT_BY_CLASS
|
|
1156
|
+
|
|
1157
|
+
# The filler for the text-out records, whose layout depends on f_options.
|
|
1158
|
+
def self.for_text(type_id)
|
|
1159
|
+
TEXT_BY_TYPE[type_id]
|
|
1160
|
+
end
|
|
1161
|
+
end
|
|
1162
|
+
end
|
|
1163
|
+
end
|
|
1164
|
+
end
|