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,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Wmf
|
|
5
|
+
# META_ESCAPE_ENHANCED_METAFILE (Escape 0x000F): an EMF stream split across consecutive
|
|
6
|
+
# Escape records. Segment bytes stay inert; assembly concatenates them and does not
|
|
7
|
+
# parse the EMF (use Emf.parse on the result). Records are never modified.
|
|
8
|
+
#
|
|
9
|
+
# ByteCount is checked as 34 plus CurrentRecordSize. The literal MS-WMF text says
|
|
10
|
+
# "34 plus EnhancedMetafileDataSize", which cannot hold for a stream split into
|
|
11
|
+
# 8,192-byte segments; the two agree for a single segment.
|
|
12
|
+
module EmbeddedEmf
|
|
13
|
+
ESCAPE = 0x000F
|
|
14
|
+
METADATA_BYTES = 34
|
|
15
|
+
MAX_SEGMENT_BYTES = 8192
|
|
16
|
+
COMMENT_IDENTIFIER = 0x43464D57
|
|
17
|
+
COMMENT_TYPE = 1
|
|
18
|
+
|
|
19
|
+
# Raised for an invalid segment or sequence; reason is a Symbol such as
|
|
20
|
+
# :comment_identifier, :segment_too_large, :remaining_mismatch or :incomplete.
|
|
21
|
+
class Invalid < Emf::Error
|
|
22
|
+
def initialize(reason)
|
|
23
|
+
@reason = reason
|
|
24
|
+
super("invalid embedded EMF escape: #{reason}")
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
attr_reader :reason
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
Segment = Struct.new(:version, :checksum, :flags, :record_count, :current_size, :remaining_bytes, :total_size, :data,
|
|
31
|
+
keyword_init: true)
|
|
32
|
+
# checksum_valid is the one's complement of the XOR of the stream's 16-bit words
|
|
33
|
+
# compared with the first segment's Checksum; a mismatch is reported, not rejected.
|
|
34
|
+
Stream = Struct.new(:version, :checksum, :checksum_valid, :bytes, keyword_init: true)
|
|
35
|
+
# error is nil when the run assembled into stream.
|
|
36
|
+
Sequence = Struct.new(:first_record, :segment_count, :stream, :error, keyword_init: true)
|
|
37
|
+
|
|
38
|
+
module_function
|
|
39
|
+
|
|
40
|
+
def escape?(record)
|
|
41
|
+
record.is_a?(Model::Wmf::Records::Escape) && record.escape_function == ESCAPE
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Validates one segment against the record's own fields. Raises Invalid.
|
|
45
|
+
def segment(record)
|
|
46
|
+
raise Invalid, :not_embedded_emf unless escape?(record) && record.type_id & 0xFF == 0x26
|
|
47
|
+
|
|
48
|
+
data = record.data
|
|
49
|
+
raise Invalid, :transport unless record.padding.nil? != data.bytesize.odd? && data.bytesize <= 0xFFFF
|
|
50
|
+
raise Invalid, :metadata if data.bytesize < METADATA_BYTES
|
|
51
|
+
|
|
52
|
+
identifier, type, version, checksum, flags, count, current, remaining, total = data.unpack("V3vV5")
|
|
53
|
+
validate_metadata(identifier, type, flags, count, current, remaining, total)
|
|
54
|
+
raise Invalid, :metadata unless data.bytesize == METADATA_BYTES + current
|
|
55
|
+
|
|
56
|
+
Segment.new(version: version, checksum: checksum, flags: flags, record_count: count, current_size: current,
|
|
57
|
+
remaining_bytes: remaining, total_size: total, data: data.byteslice(METADATA_BYTES..))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def validate_metadata(identifier, type, flags, count, current, remaining, total)
|
|
61
|
+
raise Invalid, :comment_identifier unless identifier == COMMENT_IDENTIFIER
|
|
62
|
+
raise Invalid, :comment_type unless type == COMMENT_TYPE
|
|
63
|
+
raise Invalid, :non_zero_flags unless flags.zero?
|
|
64
|
+
raise Invalid, :zero_record_count if count.zero?
|
|
65
|
+
raise Invalid, :segment_too_large if current > MAX_SEGMENT_BYTES
|
|
66
|
+
raise Invalid, :inconsistent_sizes if current + remaining > total
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def checksum(bytes)
|
|
70
|
+
bytes = bytes.b
|
|
71
|
+
bytes += "\0".b if bytes.bytesize.odd?
|
|
72
|
+
~bytes.unpack("v*").reduce(0, :^) & 0xFFFF
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Assembles escapes that must form exactly one complete sequence, in order.
|
|
76
|
+
# Raises Invalid; max_bytes bounds the declared total before any copying.
|
|
77
|
+
def assemble(records, max_bytes: Wmf::Binary::MAX_INPUT_BYTES)
|
|
78
|
+
head = nil
|
|
79
|
+
bytes = "".b
|
|
80
|
+
seen = 0
|
|
81
|
+
complete = false
|
|
82
|
+
records.each do |record|
|
|
83
|
+
raise Invalid, :record_count_mismatch if complete
|
|
84
|
+
|
|
85
|
+
segment = segment(record)
|
|
86
|
+
head ||= segment
|
|
87
|
+
check_against(head, segment, bytes.bytesize, max_bytes)
|
|
88
|
+
bytes << segment.data
|
|
89
|
+
seen += 1
|
|
90
|
+
complete = segment.remaining_bytes.zero?
|
|
91
|
+
raise Invalid, :record_count_mismatch unless complete == (seen == head.record_count)
|
|
92
|
+
end
|
|
93
|
+
raise Invalid, :incomplete unless complete
|
|
94
|
+
|
|
95
|
+
Stream.new(version: head.version, checksum: head.checksum, checksum_valid: checksum(bytes) == head.checksum, bytes: bytes)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def check_against(head, segment, consumed, max_bytes)
|
|
99
|
+
same = segment.record_count == head.record_count && segment.total_size == head.total_size
|
|
100
|
+
raise Invalid, :sequence_mismatch unless same
|
|
101
|
+
raise Invalid, :limit_exceeded if head.total_size > max_bytes
|
|
102
|
+
raise Invalid, :remaining_mismatch unless head.total_size - consumed - segment.current_size == segment.remaining_bytes
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Groups consecutive 0x000F escapes in a record list into Sequences. A run ends when it
|
|
106
|
+
# completes, fails validation (the failing segment is included) or another record follows.
|
|
107
|
+
def sequences(records, max_bytes: Wmf::Binary::MAX_INPUT_BYTES)
|
|
108
|
+
result = []
|
|
109
|
+
index = 0
|
|
110
|
+
while index < records.length
|
|
111
|
+
(index += 1 and next) unless escape?(records[index])
|
|
112
|
+
|
|
113
|
+
stop, outcome = scan_run(records, index, max_bytes)
|
|
114
|
+
result << Sequence.new(first_record: index, segment_count: stop - index, stream: outcome.is_a?(Stream) ? outcome : nil,
|
|
115
|
+
error: outcome.is_a?(Stream) ? nil : outcome)
|
|
116
|
+
index = stop
|
|
117
|
+
end
|
|
118
|
+
result
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def scan_run(records, first, max_bytes)
|
|
122
|
+
stop = first
|
|
123
|
+
outcome = :incomplete
|
|
124
|
+
while stop < records.length && escape?(records[stop])
|
|
125
|
+
stop += 1
|
|
126
|
+
outcome = begin
|
|
127
|
+
assemble(records[first...stop], max_bytes: max_bytes)
|
|
128
|
+
rescue Invalid => e
|
|
129
|
+
e.reason
|
|
130
|
+
end
|
|
131
|
+
break unless outcome == :incomplete
|
|
132
|
+
end
|
|
133
|
+
[stop, outcome]
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Wmf
|
|
5
|
+
# Read-only views of recognized Escape subtypes. Data stays inert: nothing is
|
|
6
|
+
# executed, interpreted or decoded, and the record's raw fields remain the source
|
|
7
|
+
# of truth, so serialization is unaffected by taking a view.
|
|
8
|
+
module EscapeViews
|
|
9
|
+
# kind is the subtype name; fields holds the raw numeric fields (empty for empty subtypes).
|
|
10
|
+
Payload = Struct.new(:kind, :fields, keyword_init: true)
|
|
11
|
+
# kind names a counted subtype; data is the exact counted bytes.
|
|
12
|
+
CountedPayload = Struct.new(:kind, :data, keyword_init: true)
|
|
13
|
+
SpecialPassthrough2 = Struct.new(:reserved, :data, keyword_init: true)
|
|
14
|
+
|
|
15
|
+
EMPTY = {
|
|
16
|
+
0x0002 => :abort_doc, 0x1000 => :begin_path, 0x1010 => :close_channel, 0x0202 => :download_face,
|
|
17
|
+
0x100F => :download_header, 0x1002 => :end_path, 0x000B => :end_doc, 0x0200 => :ext_text_out,
|
|
18
|
+
0x002A => :get_device_units, 0x0100 => :get_extended_text_metrics, 0x0201 => :get_face_name,
|
|
19
|
+
0x0102 => :get_pair_kern_table, 0x000C => :get_phys_page_size, 0x000D => :get_printing_offset,
|
|
20
|
+
0x000E => :get_scaling_factor, 0x0801 => :metafile_driver, 0x0001 => :new_frame, 0x0003 => :next_band,
|
|
21
|
+
0x0026 => :postscript_ignore, 0x100E => :open_channel, 0x0C01 => :query_dib_support
|
|
22
|
+
}.freeze
|
|
23
|
+
|
|
24
|
+
# [kind, byte length, unpack format, value names] keyed by escape function.
|
|
25
|
+
FIXED = {
|
|
26
|
+
0x1001 => [:clip_to_path, 4, "v2", %i[clip_function reserved1]],
|
|
27
|
+
0x0019 => [:draw_pattern_rect, 20, "l<4v2", %i[position_x position_y size_width size_height style pattern]],
|
|
28
|
+
0x0021 => [:eps_printing, 2, "v", %i[set_eps_printing]],
|
|
29
|
+
0x1019 => [:get_ps_feature_setting, 4, "l<", %i[feature]],
|
|
30
|
+
0x0008 => [:query_esc_support, 2, "v", %i[query]],
|
|
31
|
+
0x0011 => [:set_copy_count, 2, "v", %i[copy_count]],
|
|
32
|
+
0x0015 => [:set_line_cap, 4, "l<", %i[cap]],
|
|
33
|
+
0x0016 => [:set_line_join, 4, "l<", %i[join]],
|
|
34
|
+
0x0017 => [:set_miter_limit, 4, "l<", %i[miter_limit]]
|
|
35
|
+
}.freeze
|
|
36
|
+
|
|
37
|
+
COUNTED = {
|
|
38
|
+
0x0013 => :passthrough, 0x0025 => :postscript_data, 0x1013 => :postscript_passthrough, 0x000A => :start_doc,
|
|
39
|
+
0x1017 => :check_jpeg_format, 0x1018 => :check_png_format, 0x0004 => :set_color_table
|
|
40
|
+
}.freeze
|
|
41
|
+
START_DOC_LIMIT = 260
|
|
42
|
+
SPCLPASSTHROUGH2 = 0x11D8
|
|
43
|
+
|
|
44
|
+
module_function
|
|
45
|
+
|
|
46
|
+
# Empty or fixed-size payload; nil for unknown subtypes, unexpected sizes or padding.
|
|
47
|
+
def typed_payload(record)
|
|
48
|
+
return unless dc_escape?(record) && record.padding.nil?
|
|
49
|
+
|
|
50
|
+
data = record.data
|
|
51
|
+
return (kind = EMPTY[record.escape_function]) && Payload.new(kind: kind, fields: {}) if data.empty?
|
|
52
|
+
|
|
53
|
+
kind, length, format, names = FIXED[record.escape_function]
|
|
54
|
+
return unless kind && data.bytesize == length
|
|
55
|
+
|
|
56
|
+
Payload.new(kind: kind, fields: names.zip(data.unpack(format)).to_h)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# The complete counted payload; nil for other subtypes, invalid alignment or oversized STARTDOC.
|
|
60
|
+
def typed_counted_payload(record)
|
|
61
|
+
return unless dc_escape?(record) && aligned?(record)
|
|
62
|
+
|
|
63
|
+
kind = COUNTED[record.escape_function] or return
|
|
64
|
+
return if kind == :start_doc && record.data.bytesize >= START_DOC_LIMIT
|
|
65
|
+
|
|
66
|
+
CountedPayload.new(kind: kind, data: record.data)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# SPCLPASSTHROUGH2 only when its inner size consumes the exact payload.
|
|
70
|
+
def typed_special_passthrough2_payload(record)
|
|
71
|
+
return unless dc_escape?(record) && record.escape_function == SPCLPASSTHROUGH2 && aligned?(record)
|
|
72
|
+
return unless record.data.bytesize >= 6
|
|
73
|
+
|
|
74
|
+
reserved, size = record.data.unpack("Vv")
|
|
75
|
+
return unless record.data.bytesize == 6 + size
|
|
76
|
+
|
|
77
|
+
SpecialPassthrough2.new(reserved: reserved, data: record.data.byteslice(6..))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def dc_escape?(record)
|
|
81
|
+
record.type_id & 0xFF == 0x26
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def aligned?(record)
|
|
85
|
+
record.padding.nil? != record.data.bytesize.odd? && record.data.bytesize <= 0xFFFF
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Wmf
|
|
5
|
+
# Neutral evidence for a WMF input the parser rejected, and for the header drifts and record shapes it recovers from. Both
|
|
6
|
+
# backends choose the failing check (the Ruby parser by its own raise, the native backend by its
|
|
7
|
+
# first-rejection reason) and the record offset;
|
|
8
|
+
# the declared and actual values are then read from the input bytes the same way for both.
|
|
9
|
+
#
|
|
10
|
+
# REASONS maps the stable rejection reason (codec * 100 + n, never renumbered) to its check name and the
|
|
11
|
+
# message text the contract raises (`%d` stands for one number; record texts are wrapped as
|
|
12
|
+
# "WMF <text> at offset <offset>").
|
|
13
|
+
module Evidence
|
|
14
|
+
REASONS = {
|
|
15
|
+
101 => [:input_too_large, "WMF input too large"],
|
|
16
|
+
201 => [:placeable_truncated, "truncated WMF placeable header"],
|
|
17
|
+
202 => [:placeable_reserved_nonzero, "WMF placeable reserved field must be zero"],
|
|
18
|
+
203 => [:placeable_checksum_mismatch, "WMF placeable checksum mismatch"],
|
|
19
|
+
301 => [:header_truncated, "truncated WMF standard header"],
|
|
20
|
+
302 => [:header_type_invalid, "invalid WMF Type"],
|
|
21
|
+
303 => [:header_size_invalid, "invalid WMF HeaderSize"],
|
|
22
|
+
304 => [:header_version_invalid, "invalid WMF Version"],
|
|
23
|
+
305 => [:header_file_size_invalid, "invalid WMF file Size"],
|
|
24
|
+
306 => [:placeable_handle_nonzero, "WMF on-disk placeable handle must be zero"],
|
|
25
|
+
1801 => [:max_record_mismatch, "WMF MaxRecord does not match largest record"],
|
|
26
|
+
401 => [:record_limit_exceeded, "record limit exceeded"],
|
|
27
|
+
402 => [:record_header_truncated, "truncated record header"],
|
|
28
|
+
403 => [:record_length_invalid, "invalid record length %d"],
|
|
29
|
+
405 => [:eof_missing, "missing EOF record"],
|
|
30
|
+
406 => [:eof_length_invalid, "invalid EOF length"],
|
|
31
|
+
501 => [:fixed_record_length_invalid, "invalid fixed record length"],
|
|
32
|
+
601 => [:state_record_length_invalid, "invalid state record length"],
|
|
33
|
+
701 => [:font_record_length_invalid, "invalid font record length"],
|
|
34
|
+
801 => [:object_record_length_invalid, "invalid object record length"],
|
|
35
|
+
901 => [:point_count_missing, "missing point array count"],
|
|
36
|
+
902 => [:polygon_counts_truncated, "truncated polygon counts"],
|
|
37
|
+
903 => [:point_count_invalid, "invalid point count"],
|
|
38
|
+
904 => [:point_array_length_mismatch, "point array length mismatch"],
|
|
39
|
+
1001 => [:text_record_truncated, "truncated text record"],
|
|
40
|
+
1002 => [:text_rectangle_truncated, "truncated text rectangle"],
|
|
41
|
+
1003 => [:text_length_negative, "negative text length"],
|
|
42
|
+
1004 => [:text_string_truncated, "truncated text string"],
|
|
43
|
+
1005 => [:text_spacing_truncated, "truncated text spacing array"],
|
|
44
|
+
1101 => [:palette_prefix_truncated, "truncated palette prefix"],
|
|
45
|
+
1102 => [:palette_entries_truncated, "truncated palette entries"],
|
|
46
|
+
1201 => [:region_prefix_truncated, "truncated region/clipping prefix"],
|
|
47
|
+
1202 => [:region_header_truncated, "truncated region header"],
|
|
48
|
+
1203 => [:region_header_invalid, "invalid region header"],
|
|
49
|
+
1204 => [:region_scan_truncated, "truncated region scan"],
|
|
50
|
+
1205 => [:region_scan_invalid, "invalid region scan"],
|
|
51
|
+
1206 => [:region_scan_size_invalid, "invalid region scan size or maximum"],
|
|
52
|
+
1301 => [:pattern_brush_prefix_truncated, "truncated pattern brush prefix"],
|
|
53
|
+
1302 => [:pattern_brush_extent_invalid, "invalid pattern brush extent"],
|
|
54
|
+
1303 => [:dib_brush_prefix_truncated, "truncated DIB brush prefix"],
|
|
55
|
+
1304 => [:dib_brush_color_usage_invalid, "invalid DIB brush color usage"],
|
|
56
|
+
1401 => [:bitmap_prefix_truncated, "truncated bitmap transfer prefix"],
|
|
57
|
+
1402 => [:bitmap_source_empty, "empty bitmap transfer source"],
|
|
58
|
+
1501 => [:geometry_prefix_truncated, "truncated geometry prefix"],
|
|
59
|
+
1502 => [:flood_fill_mode_invalid, "invalid extended flood fill mode"],
|
|
60
|
+
1601 => [:restore_dc_prefix_truncated, "truncated RestoreDc prefix"],
|
|
61
|
+
1602 => [:escape_prefix_truncated, "truncated Escape prefix"],
|
|
62
|
+
1603 => [:escape_data_truncated, "truncated Escape data or padding"],
|
|
63
|
+
1701 => [:object_table_full, "object table capacity exceeded"],
|
|
64
|
+
1702 => [:object_reference_invalid, "invalid object reference %d"],
|
|
65
|
+
1703 => [:object_reference_not_palette, "object reference %d is not a palette"],
|
|
66
|
+
1704 => [:object_reference_not_region, "object reference %d is not a region"],
|
|
67
|
+
1705 => [:region_reference_invalid, "invalid region reference %d"],
|
|
68
|
+
1706 => [:brush_reference_invalid, "invalid brush reference %d"],
|
|
69
|
+
1707 => [:object_reference_not_brush, "object reference %d is not a brush"]
|
|
70
|
+
}.freeze
|
|
71
|
+
|
|
72
|
+
# Recovered, not rejected: the header's Size (305) or MaxRecord (1801) disagrees with the record stream. Both
|
|
73
|
+
# backends keep the model and add a ParseError carrying this evidence to Metafile#errors (see .drift_errors).
|
|
74
|
+
DRIFT_REASONS = [305, 1801].freeze
|
|
75
|
+
DRIFT_MESSAGES = {
|
|
76
|
+
305 => "WMF header Size is %<declared>d words but the records end at %<actual>d words",
|
|
77
|
+
1801 => "WMF header MaxRecord is %<declared>d words but the largest record is %<actual>d words"
|
|
78
|
+
}.freeze
|
|
79
|
+
|
|
80
|
+
# Recovered by Wmf::Recovery, the placeable reader and Binary.validate_objects: the model is kept and the
|
|
81
|
+
# evidence is a ParseError on Metafile#errors. RETIRED_REASONS can no longer be raised at all; 701 (font), 801
|
|
82
|
+
# (object record) and 1704 (a fill or frame region naming a non-region) are still raised for the shapes that
|
|
83
|
+
# cannot be recovered. Reference reasons are 1702 (no live object) and 1704 (a non-region for a clip, paint or
|
|
84
|
+
# invert record).
|
|
85
|
+
RETIRED_REASONS = [203, 406, 1003, 1702].freeze
|
|
86
|
+
|
|
87
|
+
INPUT_REASONS = [101, 201, 202, 203, 301, 302, 303, 304, 305, 306, 1801].freeze
|
|
88
|
+
REFERENCE_REASONS = (1702..1707).to_a.freeze
|
|
89
|
+
WORD_BYTES = 2
|
|
90
|
+
|
|
91
|
+
MESSAGE_PATTERNS = REASONS.except(*DRIFT_REASONS, *RETIRED_REASONS).to_h do |reason, (_check, text)|
|
|
92
|
+
pattern = Regexp.escape(text).sub("%d", "(\\d+)")
|
|
93
|
+
[reason, INPUT_REASONS.include?(reason) ? /\A#{pattern}\z/ : /\AWMF #{pattern} at offset (\d+)\z/]
|
|
94
|
+
end.freeze
|
|
95
|
+
private_constant :MESSAGE_PATTERNS
|
|
96
|
+
|
|
97
|
+
module_function
|
|
98
|
+
|
|
99
|
+
# Evidence for a FormatError or ParseError raised by the Ruby parser, or nil when it is not a contract rejection.
|
|
100
|
+
def from_error(error, bytes)
|
|
101
|
+
reason, pattern = MESSAGE_PATTERNS.find { |_code, candidate| candidate.match?(error.message) }
|
|
102
|
+
return unless reason
|
|
103
|
+
|
|
104
|
+
detail = REFERENCE_REASONS.include?(reason) ? pattern.match(error.message)[1].to_i : nil
|
|
105
|
+
build(reason, bytes, error.respond_to?(:offset) ? error.offset : nil, detail)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# ParseErrors (with evidence) for each header field that disagrees with the record stream of `bytes`, which
|
|
109
|
+
# the parser has already accepted. Both backends call this, so their errors are identical.
|
|
110
|
+
def drift_errors(bytes)
|
|
111
|
+
DRIFT_REASONS.filter_map do |reason|
|
|
112
|
+
evidence = build(reason, bytes, nil)
|
|
113
|
+
next if evidence.declared == evidence.actual
|
|
114
|
+
|
|
115
|
+
ParseError.new(offset: evidence.offset, record_code: nil,
|
|
116
|
+
message: format(DRIFT_MESSAGES.fetch(reason), declared: evidence.declared, actual: evidence.actual))
|
|
117
|
+
.with_evidence(evidence)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The ParseError (with evidence) for a recovered `reason`. A whole-input reason (203) carries the evidence
|
|
122
|
+
# offset and no record; a record reason carries the record's offset and function.
|
|
123
|
+
def recovered_error(reason, bytes, offset, function = nil, detail = nil)
|
|
124
|
+
evidence = build(reason, bytes, offset, detail)
|
|
125
|
+
text = REASONS.fetch(reason).last.sub("%d") { detail.to_s }
|
|
126
|
+
message = INPUT_REASONS.include?(reason) ? text : "WMF #{text} at offset #{offset}"
|
|
127
|
+
ParseError.new(offset: evidence.offset, record_code: function, message: message).with_evidence(evidence)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Callback for Binary.validate_objects: appends the recovered reference errors to `errors`.
|
|
131
|
+
def reference_recovery(bytes, errors)
|
|
132
|
+
lambda do |record, reason, index|
|
|
133
|
+
errors << recovered_error(reason, bytes, record.offset, record.type_id, index)
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Recovered errors in input order (stable for equal offsets), so both backends list them identically.
|
|
138
|
+
def ordered(errors)
|
|
139
|
+
errors.each_with_index.sort_by { |error, index| [error.offset.to_i, index] }.map(&:first)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# The one evidence builder both backends use once they know the reason and (for record failures) the offset.
|
|
143
|
+
def build(reason, bytes, offset, detail = nil)
|
|
144
|
+
check = REASONS.fetch(reason).first
|
|
145
|
+
values = INPUT_REASONS.include?(reason) ? input_values(reason, bytes) : record_values(reason, bytes, offset, detail)
|
|
146
|
+
offset, declared, actual = values
|
|
147
|
+
ParseEvidence.new(check: check, offset: offset, declared: declared, actual: actual)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def input_values(reason, bytes)
|
|
151
|
+
view = View.new(bytes)
|
|
152
|
+
case reason
|
|
153
|
+
when 101 then [0, Binary::MAX_INPUT_BYTES, bytes.bytesize]
|
|
154
|
+
when 201 then [0, Binary::PLACEABLE_BYTES, bytes.bytesize]
|
|
155
|
+
when 202 then [16, 0, bytes.unpack1("V", offset: 16)]
|
|
156
|
+
when 203 then [20, bytes.unpack1("v", offset: 20), Binary.checksum(bytes.byteslice(0, 20))]
|
|
157
|
+
when 306 then [4, bytes.unpack1("v", offset: 4), 0]
|
|
158
|
+
else header_values(reason, bytes, view)
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def header_values(reason, bytes, view)
|
|
163
|
+
case reason
|
|
164
|
+
when 301 then [view.start, Binary::HEADER_BYTES, bytes.bytesize - view.start]
|
|
165
|
+
when 302 then [view.start, view.u16(0), nil]
|
|
166
|
+
when 303 then [view.start + 2, view.u16(2), 9]
|
|
167
|
+
when 304 then [view.start + 4, view.u16(4), nil]
|
|
168
|
+
when 305 then [view.start + 6, view.u32(6), view.stream_words]
|
|
169
|
+
else [view.start + 12, view.u32(12), view.largest_record_words]
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def record_values(reason, bytes, offset, detail)
|
|
174
|
+
view = View.new(bytes)
|
|
175
|
+
words = view.u32_at(offset)
|
|
176
|
+
declared, actual = record_pair(reason, view, offset, words, detail)
|
|
177
|
+
[offset, declared, actual]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def record_pair(reason, view, offset, words, detail)
|
|
181
|
+
case reason
|
|
182
|
+
when 401 then [Binary::MAX_RECORDS, Binary::MAX_RECORDS]
|
|
183
|
+
when 402 then [6, view.limit - offset]
|
|
184
|
+
when 403 then [words, (view.limit - offset) / WORD_BYTES]
|
|
185
|
+
when 405 then [view.limit, offset]
|
|
186
|
+
when 406 then [words, 3]
|
|
187
|
+
when 501, 601, 701, 801 then [words, view.required_words(reason, offset)]
|
|
188
|
+
when 1701 then [view.u16(10), nil]
|
|
189
|
+
when *REFERENCE_REASONS then [detail, view.u16(10)]
|
|
190
|
+
else [words, nil]
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Reads fields of an input that already passed the checks before the one being described.
|
|
195
|
+
class View
|
|
196
|
+
def initialize(bytes)
|
|
197
|
+
@bytes = bytes
|
|
198
|
+
@start = bytes.bytesize >= 4 && bytes.unpack1("V") == Binary::PLACEABLE_KEY ? Binary::PLACEABLE_BYTES : 0
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
attr_reader :start
|
|
202
|
+
|
|
203
|
+
def u16(offset)
|
|
204
|
+
@bytes.unpack1("v", offset: @start + offset)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def u32(offset)
|
|
208
|
+
@bytes.unpack1("V", offset: @start + offset)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def u32_at(offset)
|
|
212
|
+
@bytes.unpack1("V", offset: offset) if offset && offset + 4 <= @bytes.bytesize
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def limit
|
|
216
|
+
@bytes.bytesize
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Words from the standard header to the end of the first EOF record (or of the input without one).
|
|
220
|
+
def stream_words
|
|
221
|
+
(record_walk.last - @start) / WORD_BYTES
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def largest_record_words
|
|
225
|
+
record_walk.first
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# [largest record in words, offset after the walk]; the walk follows the parser's: it ends after the
|
|
229
|
+
# first EOF record and never reads past the input, whatever the header declares.
|
|
230
|
+
def record_walk
|
|
231
|
+
@record_walk ||= begin
|
|
232
|
+
offset = @start + Binary::HEADER_BYTES
|
|
233
|
+
largest = 0
|
|
234
|
+
while offset + 6 <= limit
|
|
235
|
+
words = u32_at(offset)
|
|
236
|
+
break if words < 3
|
|
237
|
+
|
|
238
|
+
largest = [largest, words].max
|
|
239
|
+
offset += words * WORD_BYTES
|
|
240
|
+
break if @bytes.unpack1("v", offset: offset - (words * WORD_BYTES) + 4).zero?
|
|
241
|
+
end
|
|
242
|
+
[largest, [offset, limit].min]
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def required_words(reason, offset)
|
|
247
|
+
function = @bytes.unpack1("v", offset: offset + 4) & 0xFF
|
|
248
|
+
record_class = { 501 => Binary::FIXED_RECORDS, 601 => Binary::STATE_RECORDS, 701 => nil, 801 => Binary::OBJECT_RECORDS }
|
|
249
|
+
record_class = reason == 701 ? Model::Wmf::Records::CreateFontIndirect : record_class.fetch(reason).fetch(function)
|
|
250
|
+
reason == 501 ? 3 + record_class::WIRE_FIELDS.length : 3 + (record_class::PAYLOAD_BYTES / WORD_BYTES)
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Wmf
|
|
5
|
+
module GeometryCodec
|
|
6
|
+
RECORDS = {
|
|
7
|
+
0x10 => Model::Wmf::Records::ScaleWindowExt,
|
|
8
|
+
0x12 => Model::Wmf::Records::ScaleViewportExt,
|
|
9
|
+
0x19 => Model::Wmf::Records::FloodFill,
|
|
10
|
+
0x1F => Model::Wmf::Records::SetPixel,
|
|
11
|
+
0x48 => Model::Wmf::Records::ExtFloodFill,
|
|
12
|
+
0x1D => Model::Wmf::Records::PatBlt
|
|
13
|
+
}.freeze
|
|
14
|
+
FUNCTIONS = RECORDS.invert.freeze
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
def decode(raw, function, offset)
|
|
19
|
+
klass = RECORDS.fetch(function & 0xFF)
|
|
20
|
+
prefix = 6 + klass::PAYLOAD_BYTES
|
|
21
|
+
yield "truncated geometry prefix" if raw.bytesize < prefix
|
|
22
|
+
fields = klass::WIRE_FIELDS.zip(raw.byteslice(6, klass::PAYLOAD_BYTES).unpack(klass::WIRE_FORMAT)).to_h
|
|
23
|
+
yield "invalid extended flood fill mode" if klass == Model::Wmf::Records::ExtFloodFill && fields[:mode] > 1
|
|
24
|
+
klass.new(**fields, type_id: function, offset: offset, suffix: raw.byteslice(prefix..))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def byte_size(record)
|
|
28
|
+
6 + record.class::PAYLOAD_BYTES + record.suffix.bytesize
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def encode(record)
|
|
32
|
+
function = Binary.uint16(record.type_id, "RecordFunction")
|
|
33
|
+
raise SerializeError, "WMF geometry RecordFunction mismatch" unless function & 0xFF == FUNCTIONS[record.class]
|
|
34
|
+
unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
|
|
35
|
+
raise SerializeError, "WMF geometry suffix must be a String with even length"
|
|
36
|
+
end
|
|
37
|
+
raise SerializeError, "WMF geometry too large" if byte_size(record) > Binary::MAX_INPUT_BYTES
|
|
38
|
+
|
|
39
|
+
values = record.class::WIRE_FIELDS.zip(record.class::WIRE_TYPES).map do |field, type|
|
|
40
|
+
Binary.public_send(type, record.public_send(field), field)
|
|
41
|
+
end
|
|
42
|
+
raise SerializeError, "invalid WMF extended flood fill mode" if record.is_a?(Model::Wmf::Records::ExtFloodFill) && record.mode > 1
|
|
43
|
+
|
|
44
|
+
payload = values.pack(record.class::WIRE_FORMAT) + record.suffix.b
|
|
45
|
+
[3 + (payload.bytesize / 2), function].pack("Vv") + payload
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Wmf
|
|
5
|
+
module PaletteCodec
|
|
6
|
+
RECORDS = {
|
|
7
|
+
0xF7 => Model::Wmf::Records::CreatePalette,
|
|
8
|
+
0x37 => Model::Wmf::Records::SetPalEntries,
|
|
9
|
+
0x36 => Model::Wmf::Records::AnimatePalette,
|
|
10
|
+
0x34 => Model::Wmf::Records::SelectPalette,
|
|
11
|
+
0x39 => Model::Wmf::Records::ResizePalette,
|
|
12
|
+
0x35 => Model::Wmf::Records::RealizePalette
|
|
13
|
+
}.freeze
|
|
14
|
+
FUNCTIONS = RECORDS.invert.freeze
|
|
15
|
+
ENTRY_FIELDS = %i[red green blue flags].freeze
|
|
16
|
+
|
|
17
|
+
module_function
|
|
18
|
+
|
|
19
|
+
def decode(raw, function, offset)
|
|
20
|
+
klass = RECORDS.fetch(function & 0xFF)
|
|
21
|
+
fields = { type_id: function, offset: offset }
|
|
22
|
+
prefix = 6
|
|
23
|
+
if klass <= Model::Wmf::Records::PaletteRecord
|
|
24
|
+
yield "truncated palette prefix" if raw.bytesize < 10
|
|
25
|
+
start, count = raw.byteslice(6, 4).unpack("v2")
|
|
26
|
+
prefix = 10 + (count * 4)
|
|
27
|
+
yield "truncated palette entries" if raw.bytesize < prefix
|
|
28
|
+
fields[:start] = start
|
|
29
|
+
fields[:entries] = raw.byteslice(10, count * 4).unpack("C*").each_slice(4).map do |values|
|
|
30
|
+
Model::Wmf::Records::PaletteEntry.new(**ENTRY_FIELDS.zip(values).to_h)
|
|
31
|
+
end
|
|
32
|
+
elsif klass != Model::Wmf::Records::RealizePalette
|
|
33
|
+
prefix = 8
|
|
34
|
+
yield "truncated palette prefix" if raw.bytesize < prefix
|
|
35
|
+
field = klass == Model::Wmf::Records::SelectPalette ? :object_index : :entry_count
|
|
36
|
+
fields[field] = raw.byteslice(6, 2).unpack1("v")
|
|
37
|
+
end
|
|
38
|
+
klass.new(**fields, suffix: raw.byteslice(prefix..))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def byte_size(record)
|
|
42
|
+
prefix = if record.is_a?(Model::Wmf::Records::PaletteRecord)
|
|
43
|
+
10 + (record.entries.length * 4)
|
|
44
|
+
elsif record.is_a?(Model::Wmf::Records::RealizePalette)
|
|
45
|
+
6
|
|
46
|
+
else
|
|
47
|
+
8
|
|
48
|
+
end
|
|
49
|
+
prefix + record.suffix.bytesize
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def encode(record)
|
|
53
|
+
function = Binary.uint16(record.type_id, "RecordFunction")
|
|
54
|
+
raise SerializeError, "WMF palette RecordFunction mismatch" unless function & 0xFF == FUNCTIONS[record.class]
|
|
55
|
+
unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
|
|
56
|
+
raise SerializeError, "WMF palette suffix must be a String with even length"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
payload = encode_payload(record) + record.suffix.b
|
|
60
|
+
[3 + (payload.bytesize / 2), function].pack("Vv") + payload
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def encode_payload(record)
|
|
64
|
+
case record
|
|
65
|
+
when Model::Wmf::Records::PaletteRecord
|
|
66
|
+
raise SerializeError, "WMF palette entries must be an array" unless record.entries.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
start = Binary.uint16(record.start, "start")
|
|
69
|
+
count = Binary.uint16(record.entries.length, "entry count")
|
|
70
|
+
entries = record.entries.map do |entry|
|
|
71
|
+
raise SerializeError, "WMF palette entry must be a PaletteEntry" unless entry.is_a?(Model::Wmf::Records::PaletteEntry)
|
|
72
|
+
|
|
73
|
+
ENTRY_FIELDS.map { |field| Binary.uint8(entry.public_send(field), field) }.pack("C4")
|
|
74
|
+
end
|
|
75
|
+
[start, count].pack("v2") + entries.join
|
|
76
|
+
when Model::Wmf::Records::SelectPalette
|
|
77
|
+
[Binary.uint16(record.object_index, "object_index")].pack("v")
|
|
78
|
+
when Model::Wmf::Records::ResizePalette
|
|
79
|
+
[Binary.uint16(record.entry_count, "entry_count")].pack("v")
|
|
80
|
+
else "".b
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|