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,97 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Parse-recovery errors (Metafile#errors) and unparseable input become
|
|
4
|
+
# diagnostics. The record count and offsets come from the ParseError itself.
|
|
5
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
6
|
+
|
|
7
|
+
module Emf
|
|
8
|
+
module Conformance
|
|
9
|
+
module ParseRecoveryRules
|
|
10
|
+
EMF = %i[emf emf_plus].freeze
|
|
11
|
+
RECOVERED = "the parser recovered and kept the bytes (as a raw record or trailing data), so the file still opens"
|
|
12
|
+
|
|
13
|
+
# Recovered ParseErrors (Metafile#errors) carry no #evidence on either backend (measured 2026-10-22:
|
|
14
|
+
# Emf::ParseEvidence is attached only to the FormatError/ParseError raised for an input that yields no
|
|
15
|
+
# metafile, and none of its check symbols describes a recovered record). Their rule id is therefore
|
|
16
|
+
# chosen from the message. When recovered errors start carrying evidence, classify from
|
|
17
|
+
# error.evidence.check first and keep this table as the fallback for errors without evidence;
|
|
18
|
+
# spec/emf/conformance/parse_recovery_spec.rb fails the day that happens.
|
|
19
|
+
#
|
|
20
|
+
# message pattern => [rule id, kind]
|
|
21
|
+
CLASSES = [
|
|
22
|
+
[/header/, "emf.record.truncated_header", :truncated_header],
|
|
23
|
+
[/nSize \d+ < 8/, "emf.record.size_minimum", :size_minimum],
|
|
24
|
+
[/truncated record/, "emf.record.size_bounds", :size_bounds],
|
|
25
|
+
[/declares more entries than nSize allows/, "emf.record.array_exceeds_size", :array_exceeds_size]
|
|
26
|
+
].freeze
|
|
27
|
+
OTHER = ["emf.record.undecodable", :undecodable].freeze
|
|
28
|
+
|
|
29
|
+
module_function
|
|
30
|
+
|
|
31
|
+
def classify(error) = CLASSES.find { |pattern, *| pattern.match?(error.message) }&.drop(1) || OTHER
|
|
32
|
+
|
|
33
|
+
# Text rules that name the same too-large count or too-small record the parser recovered as Raw; where one
|
|
34
|
+
# fires on the record, the recovery diagnostic would only repeat it.
|
|
35
|
+
TEXT_OWNERS = %w[emf.exttextout.string_bounds emf.exttextout.dx_bounds].freeze
|
|
36
|
+
|
|
37
|
+
def findings(context, id)
|
|
38
|
+
owned = id == "emf.record.array_exceeds_size" ? text_owned_offsets(context) : []
|
|
39
|
+
context.metafile.errors.filter_map do |error|
|
|
40
|
+
next unless classify(error).first == id
|
|
41
|
+
next if owned.include?(error.offset)
|
|
42
|
+
|
|
43
|
+
{ message: error.message, offset: error.offset, record_type: error.record_code }
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def text_owned_offsets(context)
|
|
48
|
+
(TEXT_OWNERS.flat_map { |owner| TextRules.findings(context, owner) } + CorpusNamedRules.exttextout_fixed_part(context))
|
|
49
|
+
.map { |finding| finding[:offset] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def unparseable(context)
|
|
53
|
+
failure = context.parse_failure
|
|
54
|
+
return [] if WmfRejectionRules.reason_for(failure) # a named wmf.reject.rNNNN finding already explains it
|
|
55
|
+
return [] if wmf_header_fault(context.bytes) # wmf.header.unrecognised cites MS-WMF instead
|
|
56
|
+
|
|
57
|
+
[{ message: "input could not be parsed: #{failure.message}", offset: 0 }]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
WMF_TYPES = [1, 2].freeze
|
|
61
|
+
WMF_VERSIONS = [0x0100, 0x0300].freeze
|
|
62
|
+
|
|
63
|
+
# The detector accepts a standard WMF only when Type is 1 or 2 and HeaderSize is 9, so a META_HEADER with
|
|
64
|
+
# exactly one of those wrong (and a valid Version) never reaches the WMF parser. Name that field here.
|
|
65
|
+
def wmf_header_fault(bytes)
|
|
66
|
+
return unless bytes && bytes.bytesize >= 18
|
|
67
|
+
|
|
68
|
+
type, header_size, version = bytes.unpack("vvv")
|
|
69
|
+
return unless WMF_VERSIONS.include?(version)
|
|
70
|
+
return "HeaderSize #{header_size} is not 9" if WMF_TYPES.include?(type) && header_size != 9
|
|
71
|
+
|
|
72
|
+
"Type #{type} is not 1 (memory) or 2 (disk)" if header_size == 9 && !WMF_TYPES.include?(type)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def wmf_header(context)
|
|
76
|
+
fault = wmf_header_fault(context.bytes)
|
|
77
|
+
fault ? [{ message: "WMF META_HEADER #{fault}, so the input could not be parsed", offset: 0 }] : []
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
registry = Registry.loading
|
|
82
|
+
citations = { "emf.record.truncated_header" => "MS-EMF 2.3", "emf.record.size_minimum" => "MS-EMF 2.3",
|
|
83
|
+
"emf.record.size_bounds" => "MS-EMF 2.3", "emf.record.array_exceeds_size" => "MS-EMF 2.3",
|
|
84
|
+
"emf.record.undecodable" => "MS-EMF 2.3" }
|
|
85
|
+
(ParseRecoveryRules::CLASSES.map { |_, id, kind| [id, kind] } << ParseRecoveryRules::OTHER).each do |id, kind|
|
|
86
|
+
registry.register(id: id, formats: ParseRecoveryRules::EMF, citation: citations.fetch(id), kind: kind,
|
|
87
|
+
tolerant_reason: ParseRecoveryRules::RECOVERED) do |context|
|
|
88
|
+
ParseRecoveryRules.findings(context, id)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
registry.register(id: "emf.parse.unparseable", formats: [:unparseable], citation: "MS-EMF 2.3.4.2",
|
|
93
|
+
kind: :unparseable) { |context| ParseRecoveryRules.unparseable(context) }
|
|
94
|
+
registry.register(id: "wmf.header.unrecognised", formats: [:unparseable], citation: "MS-WMF 2.3.2.2",
|
|
95
|
+
kind: :unparseable) { |context| ParseRecoveryRules.wmf_header(context) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Path bracket (MS-EMF 2.3.10) and clipping (2.3.2) record rules, plus the
|
|
4
|
+
# RESTOREDC depth sequence rule (2.3.11.6).
|
|
5
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
6
|
+
|
|
7
|
+
module Emf
|
|
8
|
+
module Conformance
|
|
9
|
+
module PathClipRules
|
|
10
|
+
EMF = %i[emf emf_plus].freeze
|
|
11
|
+
BEGIN_PATH = 59
|
|
12
|
+
END_PATH = 60
|
|
13
|
+
CLOSE_FIGURE = 61
|
|
14
|
+
ABORT_PATH = 68
|
|
15
|
+
SAVE_DC = 33
|
|
16
|
+
RESTORE_DC = 34
|
|
17
|
+
PATH_TYPES = [BEGIN_PATH, END_PATH, CLOSE_FIGURE, 65, 66, ABORT_PATH].freeze # 65 FLATTENPATH, 66 WIDENPATH
|
|
18
|
+
SELECT_CLIP_PATH = 67
|
|
19
|
+
EXT_SELECT_CLIP_RGN = 75
|
|
20
|
+
# Clipping records have no stated Size; their fixed fields occupy: type => size.
|
|
21
|
+
CLIP_LAYOUT = { 26 => 16, 28 => 8, 29 => 24, 30 => 24, SELECT_CLIP_PATH => 12 }.freeze
|
|
22
|
+
REGION_MODES = (1..5)
|
|
23
|
+
SHIFTED = "a RESTOREDC below the saved depth leaves the rest of the stream readable; bad_corrupted_2014-12-02-215339.emf " \
|
|
24
|
+
"(tolerant-valid by owner decision) reaches it after a record boundary shift"
|
|
25
|
+
|
|
26
|
+
module_function
|
|
27
|
+
|
|
28
|
+
def each_wire(context, types)
|
|
29
|
+
context.records.each_with_index do |record, index|
|
|
30
|
+
next unless types.include?(context.record_type(record))
|
|
31
|
+
|
|
32
|
+
wire = record.wire
|
|
33
|
+
yield(record, index, wire) if wire.is_a?(Emf::Emr::Binary::Record)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def at(record, index, message)
|
|
38
|
+
{ message: message, offset: record.offset, record_index: index + 1, record_type: record.type_id.to_i }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def path_size(context)
|
|
42
|
+
found = []
|
|
43
|
+
each_wire(context, PATH_TYPES) do |record, index, _wire|
|
|
44
|
+
size = context.record_size(record)
|
|
45
|
+
next if size == 8
|
|
46
|
+
|
|
47
|
+
found << at(record, index,
|
|
48
|
+
"#{RecordTypes.name(context.record_type(record))} Size is #{size}, path bracket records are 0x00000008")
|
|
49
|
+
end
|
|
50
|
+
found
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def clip_layout_size(context)
|
|
54
|
+
found = []
|
|
55
|
+
each_wire(context, CLIP_LAYOUT.keys) do |record, index, _wire|
|
|
56
|
+
type = context.record_type(record)
|
|
57
|
+
size = context.record_size(record)
|
|
58
|
+
next if size == CLIP_LAYOUT.fetch(type)
|
|
59
|
+
|
|
60
|
+
found << at(record, index, "#{RecordTypes.name(type)} Size is #{size}, its fixed fields occupy #{CLIP_LAYOUT.fetch(type)}")
|
|
61
|
+
end
|
|
62
|
+
found
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def region_mode(context, type, field, name)
|
|
66
|
+
found = []
|
|
67
|
+
each_wire(context, [type]) do |record, index, wire|
|
|
68
|
+
mode = wire.public_send(field).to_i
|
|
69
|
+
unless REGION_MODES.cover?(mode)
|
|
70
|
+
found << at(record, index,
|
|
71
|
+
"#{name} RegionMode #{mode} is not in the RegionMode enumeration (RGN_AND..RGN_COPY, 1..5)")
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
found
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def select_clip_path_mode(context) = region_mode(context, SELECT_CLIP_PATH, :region_mode, "EMR_SELECTCLIPPATH")
|
|
78
|
+
|
|
79
|
+
def ext_select_clip_rgn_mode(context) = region_mode(context, EXT_SELECT_CLIP_RGN, :dw_mode, "EMR_EXTSELECTCLIPRGN")
|
|
80
|
+
|
|
81
|
+
# Walks the records once, tracking whether path bracket construction is open.
|
|
82
|
+
def bracket_findings(context)
|
|
83
|
+
open_at = nil
|
|
84
|
+
found = []
|
|
85
|
+
context.records.each_with_index do |record, index|
|
|
86
|
+
type = context.record_type(record)
|
|
87
|
+
found.concat(bracket_step(type, record, index, open_at))
|
|
88
|
+
open_at = bracket_open(type, record, index, open_at)
|
|
89
|
+
end
|
|
90
|
+
found << [:unclosed, at(*open_at, "EMR_BEGINPATH opens a path bracket that no EMR_ENDPATH or EMR_ABORTPATH closes")] if open_at
|
|
91
|
+
found
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def bracket_open(type, record, index, open_at)
|
|
95
|
+
case type
|
|
96
|
+
when BEGIN_PATH then open_at || [record, index]
|
|
97
|
+
when END_PATH, ABORT_PATH then nil
|
|
98
|
+
else open_at
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def bracket_step(type, record, index, open_at)
|
|
103
|
+
if type == BEGIN_PATH && open_at
|
|
104
|
+
[[:nested, at(record, index, "EMR_BEGINPATH while a path bracket is open (opened at record #{open_at[1] + 1})")]]
|
|
105
|
+
elsif type == END_PATH && !open_at
|
|
106
|
+
[[:unopened, at(record, index, "EMR_ENDPATH closes a path bracket but none is open")]]
|
|
107
|
+
elsif type == CLOSE_FIGURE && !open_at
|
|
108
|
+
[[:figure, at(record, index, "EMR_CLOSEFIGURE outside path bracket construction")]]
|
|
109
|
+
else
|
|
110
|
+
[]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def bracket(context, kind) = bracket_findings(context).filter_map { |k, finding| finding if k == kind }
|
|
115
|
+
|
|
116
|
+
# Saved-state depth: SAVEDC pushes, RESTOREDC with a negative SavedDC of -n pops n.
|
|
117
|
+
def restore_depth(context)
|
|
118
|
+
depth = 0
|
|
119
|
+
found = []
|
|
120
|
+
context.records.each_with_index do |record, index|
|
|
121
|
+
case context.record_type(record)
|
|
122
|
+
when SAVE_DC then depth += 1
|
|
123
|
+
when RESTORE_DC
|
|
124
|
+
next unless record.wire.respond_to?(:saved_dc)
|
|
125
|
+
|
|
126
|
+
saved = record.wire.saved_dc.to_i
|
|
127
|
+
next unless saved.negative?
|
|
128
|
+
|
|
129
|
+
if -saved > depth
|
|
130
|
+
found << at(record, index, "EMR_RESTOREDC SavedDC #{saved} restores #{-saved} states deep but only #{depth} are saved")
|
|
131
|
+
depth = 0
|
|
132
|
+
else
|
|
133
|
+
depth += saved
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
found
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
registry = Registry.loading
|
|
142
|
+
pc = PathClipRules::EMF
|
|
143
|
+
{
|
|
144
|
+
"emf.path.fixed_size" => [:path_size, "MS-EMF 2.3.10", :fixed_size, :error],
|
|
145
|
+
"emf.clip.layout_size" => [:clip_layout_size, "MS-EMF 2.3.2", :layout_size, :warning],
|
|
146
|
+
"emf.clip.select_clip_path_mode" => [:select_clip_path_mode, "MS-EMF 2.3.2.5", :region_mode, :error],
|
|
147
|
+
"emf.clip.ext_select_clip_rgn_mode" => [:ext_select_clip_rgn_mode, "MS-EMF 2.3.2.2", :region_mode, :error],
|
|
148
|
+
"emf.dc.restore_depth" => [:restore_depth, "MS-EMF 2.3.11.6", :restore_depth, :error, PathClipRules::SHIFTED]
|
|
149
|
+
}.each do |id, (method, citation, kind, severity, reason)|
|
|
150
|
+
registry.register(id: id, formats: pc, citation: citation, kind: kind, severity: severity, tolerant_reason: reason) do |c|
|
|
151
|
+
PathClipRules.public_send(method, c)
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
{
|
|
155
|
+
"emf.path.begin_nested" => %i[nested error],
|
|
156
|
+
"emf.path.unclosed" => %i[unclosed error],
|
|
157
|
+
"emf.path.end_unopened" => %i[unopened warning],
|
|
158
|
+
"emf.path.closefigure_outside" => %i[figure warning]
|
|
159
|
+
}.each do |id, (kind, severity)|
|
|
160
|
+
registry.register(id: id, formats: pc, citation: "MS-EMF 2.3.10", kind: kind, severity: severity) do |c|
|
|
161
|
+
PathClipRules.bracket(c, kind)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# EMF+ adapter: maps the existing Emf::EmfPlus::Stream::Conformance diagnostics
|
|
4
|
+
# (and the stream's other framing errors) onto lens rules. Offsets are the file
|
|
5
|
+
# offset of the EMR_COMMENT that carries the EMF+ record.
|
|
6
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
7
|
+
|
|
8
|
+
module Emf
|
|
9
|
+
module Conformance
|
|
10
|
+
module EmfPlusRules
|
|
11
|
+
KINDS = {
|
|
12
|
+
reserved_type: ["emfplus.record.reserved_type", "MS-EMFPLUS 2.1.1.1"],
|
|
13
|
+
stroke_fill_path: ["emfplus.record.stroke_fill_path", "MS-EMFPLUS 2.1.1.1"],
|
|
14
|
+
unknown_type: ["emfplus.record.unknown_type", "MS-EMFPLUS 2.1.1.1"],
|
|
15
|
+
header_not_after_emf_header: ["emfplus.header.placement", "MS-EMFPLUS 2.3.3.3"]
|
|
16
|
+
}.freeze
|
|
17
|
+
TOLERATED = "the records stay readable as unknown records with their bytes intact; corpus files carry them"
|
|
18
|
+
STREAM_ID = "emfplus.stream.framing"
|
|
19
|
+
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def findings(context, id)
|
|
23
|
+
stream = context.metafile.emf_plus_stream
|
|
24
|
+
return [] unless stream
|
|
25
|
+
|
|
26
|
+
undecodable = id == STREAM_ID ? undecodable_types(context) : []
|
|
27
|
+
stream.errors.filter_map do |error|
|
|
28
|
+
next unless rule_id(error) == id
|
|
29
|
+
next if owned_elsewhere?(error, undecodable)
|
|
30
|
+
|
|
31
|
+
finding(context, error)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# A record body that does not decode is emfplus.record.body_bounds, and a missing or non-adjacent effect is
|
|
36
|
+
# emfplus.object.effect_missing; both are located at the record, so the stream-wide copy is dropped.
|
|
37
|
+
def owned_elsewhere?(error, undecodable)
|
|
38
|
+
case error
|
|
39
|
+
when Emf::EmfPlus::Links::LinkError then %i[missing_effect non_adjacent_effect].include?(error.reason)
|
|
40
|
+
when Emf::ParseError then undecodable.include?(error.record_code) && error.message.match?(/\Amalformed \w+ body at offset/)
|
|
41
|
+
else false
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def undecodable_types(context)
|
|
46
|
+
EmfPlusSupport.records(context).filter_map do |located|
|
|
47
|
+
located.record.type unless EmfPlusSupport.typed?(located.record) || !Emf::EmfPlus::Records.registry.key?(located.record.type)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def rule_id(error)
|
|
52
|
+
error.is_a?(Emf::EmfPlus::Stream::Conformance::Diagnostic) ? KINDS.fetch(error.kind).first : STREAM_ID
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def finding(context, error)
|
|
56
|
+
return { message: error.message, offset: nil } unless
|
|
57
|
+
error.is_a?(Emf::EmfPlus::Stream::Conformance::Diagnostic)
|
|
58
|
+
|
|
59
|
+
record = context.records[error.comment_index]
|
|
60
|
+
{ message: error.message, offset: record&.offset, record_index: error.comment_index + 1, record_type: error.record_type }
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
registry = Registry.loading
|
|
65
|
+
EmfPlusRules::KINDS.each do |kind, (id, citation)|
|
|
66
|
+
registry.register(id: id, formats: [:emf_plus], citation: citation, kind: kind,
|
|
67
|
+
tolerant_reason: EmfPlusRules::TOLERATED) { |context| EmfPlusRules.findings(context, id) }
|
|
68
|
+
end
|
|
69
|
+
registry.register(id: EmfPlusRules::STREAM_ID, formats: [:emf_plus], citation: "MS-EMFPLUS 2.3",
|
|
70
|
+
kind: :stream_framing,
|
|
71
|
+
tolerant_reason: "the stream keeps unframed bytes verbatim, so nothing is dropped") do |context|
|
|
72
|
+
EmfPlusRules.findings(context, EmfPlusRules::STREAM_ID)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
4
|
+
|
|
5
|
+
return if defined?(Emf::Conformance::EmfPlusSupport)
|
|
6
|
+
|
|
7
|
+
# Shared helper for the emf_plus_*.rb rule files (no rules of its own). It walks
|
|
8
|
+
# the public EMF+ stream and locates each record in the file the way
|
|
9
|
+
# emf_plus.rb does: by the file offset of the EMR_COMMENT that carries it, taken from
|
|
10
|
+
# the handle's comment_offset (nil on a rebuilt stream, then the comment record's own offset).
|
|
11
|
+
module Emf
|
|
12
|
+
module Conformance
|
|
13
|
+
module EmfPlusSupport
|
|
14
|
+
Located = Struct.new(:record, :handle, :comment_index, :comment, keyword_init: true)
|
|
15
|
+
|
|
16
|
+
module_function
|
|
17
|
+
|
|
18
|
+
# Every EMF+ record of the metafile, in stream order.
|
|
19
|
+
def records(context)
|
|
20
|
+
stream = context.metafile.emf_plus_stream
|
|
21
|
+
return [] unless stream
|
|
22
|
+
|
|
23
|
+
stream.each_record.map do |handle|
|
|
24
|
+
index = stream.segments.fetch(handle.segment_index).record_index
|
|
25
|
+
Located.new(record: handle.record, handle: handle, comment_index: index, comment: context.records[index])
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def finding(located, message)
|
|
30
|
+
{ message: "#{message} (EMF+ record #{format('0x%04X', located.record.type)} at offset #{located.handle.offset} " \
|
|
31
|
+
"of EMR_COMMENT_EMFPLUS record #{located.comment_index + 1})",
|
|
32
|
+
offset: located.handle.comment_offset || located.comment&.offset, record_index: located.comment_index + 1,
|
|
33
|
+
record_type: located.record.type }
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A decoded record (not Records::Unknown).
|
|
37
|
+
def typed?(record) = record.is_a?(Emf::EmfPlus::Records::Base)
|
|
38
|
+
|
|
39
|
+
def size_of(record) = typed?(record) ? record.size : record.frame.size
|
|
40
|
+
|
|
41
|
+
def data_size_of(record) = typed?(record) ? record.data_size : record.frame.data_size
|
|
42
|
+
|
|
43
|
+
def type_name(record)
|
|
44
|
+
klass = Emf::EmfPlus::Records.registry[record.type]
|
|
45
|
+
klass ? klass.name.split("::").last : format("0x%04X", record.type)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Registers a rule whose check runs per EMF+ record. The block gets the
|
|
49
|
+
# Located record and returns nil, a message, or an Array of messages.
|
|
50
|
+
def per_record(id:, citation:, kind:, severity: :error, tolerant_reason: nil, &check)
|
|
51
|
+
Registry.loading.register(id: id, formats: [:emf_plus], citation: citation, kind: kind, severity: severity,
|
|
52
|
+
tolerant_reason: tolerant_reason) do |context|
|
|
53
|
+
records(context).flat_map { |located| Array(yield(located)).map { |message| finding(located, message) } }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "emf_plus_common"
|
|
6
|
+
|
|
7
|
+
# EMF+ drawing geometry rules over the public decoded records: minimum point and
|
|
8
|
+
# rectangle counts, the image source unit. Counts that run past the record are
|
|
9
|
+
# emfplus.record.body_bounds (the body does not decode at all).
|
|
10
|
+
module Emf
|
|
11
|
+
module Conformance
|
|
12
|
+
module EmfPlusDrawing
|
|
13
|
+
Records = Emf::EmfPlus::Records
|
|
14
|
+
Support = EmfPlusSupport
|
|
15
|
+
|
|
16
|
+
# record class => [minimum, noun, exact?] from the MS-EMFPLUS 2.3.4 Count descriptions.
|
|
17
|
+
COUNTS = {
|
|
18
|
+
Records::DrawBeziers => [4, "points", false], Records::DrawClosedCurve => [3, "points", false],
|
|
19
|
+
Records::DrawCurve => [2, "points", false], Records::DrawLines => [2, "points", false],
|
|
20
|
+
Records::FillClosedCurve => [3, "points", false], Records::FillPolygon => [3, "points", false],
|
|
21
|
+
Records::DrawImagePoints => [3, "points", true],
|
|
22
|
+
Records::DrawRects => [1, "rectangles", false], Records::FillRects => [1, "rectangles", false]
|
|
23
|
+
}.freeze
|
|
24
|
+
UNIT_PIXEL = 2
|
|
25
|
+
# record class => { compressed? => [Size, DataSize] } from the MS-EMFPLUS 2.3.4.7 and 2.3.4.16 Size and DataSize tables.
|
|
26
|
+
ELLIPSE_SIZES = {
|
|
27
|
+
Records::DrawEllipse => { true => [0x14, 0x08], false => [0x1C, 0x10] },
|
|
28
|
+
Records::FillEllipse => { true => [0x18, 0x0C], false => [0x20, 0x14] }
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def counts(located)
|
|
34
|
+
record = located.record
|
|
35
|
+
minimum, noun, exact = COUNTS.find { |klass, _| record.is_a?(klass) }&.last
|
|
36
|
+
return unless minimum
|
|
37
|
+
|
|
38
|
+
count = record.count
|
|
39
|
+
return if exact ? count == minimum : count >= minimum
|
|
40
|
+
|
|
41
|
+
"#{Support.type_name(record)} has #{count} #{noun}; #{exact ? 'exactly' : 'at least'} #{minimum} MUST be specified"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def ellipse_size(located)
|
|
45
|
+
record = located.record
|
|
46
|
+
sizes = ELLIPSE_SIZES.find { |klass, _| record.is_a?(klass) }&.last
|
|
47
|
+
return unless sizes
|
|
48
|
+
|
|
49
|
+
size, data_size = sizes.fetch(record.compressed?)
|
|
50
|
+
problems = []
|
|
51
|
+
problems << "Size is #{record.size}, MUST be 0x#{size.to_s(16).rjust(8, '0')}" unless record.size == size
|
|
52
|
+
problems << "DataSize is #{record.data_size}, MUST be 0x#{data_size.to_s(16).rjust(8, '0')}" unless record.data_size == data_size
|
|
53
|
+
return if problems.empty?
|
|
54
|
+
|
|
55
|
+
"#{Support.type_name(record)} with the C bit #{record.compressed? ? 'set' : 'clear'}: #{problems.join(' and ')}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def source_unit(located)
|
|
59
|
+
record = located.record
|
|
60
|
+
return unless record.is_a?(Records::DrawImage) || record.is_a?(Records::DrawImagePoints)
|
|
61
|
+
return if record.src_unit == UNIT_PIXEL
|
|
62
|
+
|
|
63
|
+
"#{Support.type_name(record)} SrcUnit is #{record.src_unit}, MUST be UnitTypePixel (2)"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
EmfPlusSupport.per_record(id: "emfplus.draw.count", citation: "MS-EMFPLUS 2.3.4", kind: :draw_count) do |located|
|
|
68
|
+
EmfPlusDrawing.counts(located)
|
|
69
|
+
end
|
|
70
|
+
EmfPlusSupport.per_record(id: "emfplus.draw.ellipse_size", citation: "MS-EMFPLUS 2.3.4", kind: :draw_size) do |located|
|
|
71
|
+
EmfPlusDrawing.ellipse_size(located)
|
|
72
|
+
end
|
|
73
|
+
EmfPlusSupport.per_record(id: "emfplus.draw.image_source_unit", citation: "MS-EMFPLUS 2.3.4", kind: :draw_source_unit,
|
|
74
|
+
tolerant_reason: "a real producer writes SrcUnit 0 (spec/fixtures/emf/test-038.emf)") do |located|
|
|
75
|
+
EmfPlusDrawing.source_unit(located)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
4
|
+
|
|
5
|
+
require_relative "emf_plus_common"
|
|
6
|
+
|
|
7
|
+
# EMF+ image rules (reading EmfPlusPayloads.completed_object at run time) over the decoded effect parameter blocks (MS-EMFPLUS 2.2.3) and
|
|
8
|
+
# the pixel data of a bitmap (2.2.2.2, 2.2.2.3). Only a "MUST be in the range" /
|
|
9
|
+
# "MUST be one of" clause is an error. A field whose text gives meaningful ranges
|
|
10
|
+
# in "SHOULD" terms (ColorCurve AdjustmentIntensity, ColorMatrix[4][4] = 1.0,
|
|
11
|
+
# SharpenEffect Radius, which has no stated range) is not checked.
|
|
12
|
+
module Emf
|
|
13
|
+
module Conformance
|
|
14
|
+
module EmfPlusEffects
|
|
15
|
+
Effects = Emf::EmfPlus::Effects
|
|
16
|
+
Records = Emf::EmfPlus::Records
|
|
17
|
+
Objects = Emf::EmfPlus::Objects
|
|
18
|
+
Support = EmfPlusSupport
|
|
19
|
+
|
|
20
|
+
CURVE_ADJUSTMENTS = (0..7) # CurveAdjustments (2.1.1.7), AdjustExposure..AdjustBlackSaturation
|
|
21
|
+
CURVE_CHANNELS = (0..3) # CurveChannel (2.1.1.8)
|
|
22
|
+
BITS_PER_PIXEL_SHIFT = 8
|
|
23
|
+
|
|
24
|
+
# effect class => [[reader, allowed range, label], ...]; each range is a MUST (or a defined value table) of 2.2.3.x.
|
|
25
|
+
RANGES = {
|
|
26
|
+
Effects::Blur => [[:blur_radius, 0.0..255.0, "BlurRadius"], [:expand_edge, 0..1, "ExpandEdge"]],
|
|
27
|
+
Effects::BrightnessContrast => [[:brightness_level, -255..255, "BrightnessLevel"],
|
|
28
|
+
[:contrast_level, -100..100, "ContrastLevel"]],
|
|
29
|
+
Effects::ColorBalance => [[:cyan_red, -100..100, "CyanRed"], [:magenta_green, -100..100, "MagentaGreen"],
|
|
30
|
+
[:yellow_blue, -100..100, "YellowBlue"]],
|
|
31
|
+
Effects::ColorCurve => [[:curve_adjustment, CURVE_ADJUSTMENTS, "CurveAdjustment"],
|
|
32
|
+
[:curve_channel, CURVE_CHANNELS, "CurveChannel"]],
|
|
33
|
+
Effects::HueSaturationLightness => [[:hue_level, -180..180, "HueLevel"],
|
|
34
|
+
[:saturation_level, -100..100, "SaturationLevel"],
|
|
35
|
+
[:lightness_level, -100..100, "LightnessLevel"]],
|
|
36
|
+
Effects::Levels => [[:highlight, 0..100, "Highlight"], [:mid_tone, -100..100, "MidTone"],
|
|
37
|
+
[:shadow, 0..100, "Shadow"]],
|
|
38
|
+
Effects::Sharpen => [[:amount, 0.0..100.0, "Amount"]],
|
|
39
|
+
Effects::Tint => [[:hue, -180..180, "Hue"], [:amount, -100..100, "Amount"]]
|
|
40
|
+
}.freeze
|
|
41
|
+
|
|
42
|
+
module_function
|
|
43
|
+
|
|
44
|
+
def effect_parameters(located)
|
|
45
|
+
record = located.record
|
|
46
|
+
return unless record.is_a?(Records::SerializableObject) && Effects.lookup(record.object_guid)
|
|
47
|
+
|
|
48
|
+
effect = record.effect
|
|
49
|
+
problems = RANGES.find { |klass, _| effect.is_a?(klass) }&.last.to_a.filter_map do |reader, range, label|
|
|
50
|
+
value = effect.public_send(reader)
|
|
51
|
+
"#{label} #{value} is outside #{range.first}..#{range.last}" unless range.cover?(value)
|
|
52
|
+
end
|
|
53
|
+
problems.concat(color_matrix_problems(effect)) if effect.is_a?(Effects::ColorMatrix)
|
|
54
|
+
problems.map { |problem| "#{effect.class.name.split('::').last} effect (#{record.object_guid}) #{problem}" }
|
|
55
|
+
rescue Emf::ParseError
|
|
56
|
+
nil # reported by emfplus.object.serializable_buffer
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Matrix_4_0 .. Matrix_4_3 MUST be 0.0 (2.2.3.6).
|
|
60
|
+
def color_matrix_problems(effect)
|
|
61
|
+
effect.matrix[4].first(4).each_with_index.filter_map do |value, column|
|
|
62
|
+
"Matrix[4][#{column}] is #{value}, MUST be 0.0" unless value.zero?
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Decoded bitmaps of the object this record completes.
|
|
67
|
+
def pixel_data(stream, located)
|
|
68
|
+
logical = EmfPlusPayloads.completed_object(stream, located)
|
|
69
|
+
return unless logical&.object_type == Objects::IMAGE
|
|
70
|
+
|
|
71
|
+
bitmap = logical.body.bitmap
|
|
72
|
+
return unless bitmap&.bitmap_type&.zero?
|
|
73
|
+
|
|
74
|
+
pixel_problems(bitmap).map { |problem| "EmfPlusImage (object ID #{logical.emf_object_id}) bitmap #{problem}" }
|
|
75
|
+
rescue Emf::ParseError
|
|
76
|
+
nil # reported by emfplus.object.payload
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Stride is bytes per pixel times Width (2.2.2.2), so a scan-line is at least ceil(Width * bits / 8)
|
|
80
|
+
# bytes; PixelData holds Height scan-lines of Stride bytes (2.2.2.3).
|
|
81
|
+
def pixel_problems(bitmap)
|
|
82
|
+
bits = (bitmap.pixel_format >> BITS_PER_PIXEL_SHIFT) & 0xFF
|
|
83
|
+
row = ((bitmap.width * bits) + 7) / 8
|
|
84
|
+
problems = []
|
|
85
|
+
if bitmap.stride < row
|
|
86
|
+
problems << "Stride #{bitmap.stride} is smaller than the #{row} bytes of a #{bitmap.width}-pixel scan-line " \
|
|
87
|
+
"at #{bits} bits per pixel"
|
|
88
|
+
end
|
|
89
|
+
needed = bitmap.stride.abs * bitmap.height.abs
|
|
90
|
+
have = bitmap.bitmap_data.pixel_data.to_s.bytesize
|
|
91
|
+
problems << "PixelData holds #{have} bytes, Stride #{bitmap.stride} * Height #{bitmap.height} needs #{needed}" if have < needed
|
|
92
|
+
problems
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
EmfPlusSupport.per_record(id: "emfplus.object.effect_parameters", citation: "MS-EMFPLUS 2.2.3",
|
|
97
|
+
kind: :effect_parameters) { |located| EmfPlusEffects.effect_parameters(located) }
|
|
98
|
+
|
|
99
|
+
Registry.loading.register(id: "emfplus.image.pixel_data", formats: [:emf_plus], citation: "MS-EMFPLUS 2.2.2.3",
|
|
100
|
+
kind: :pixel_data, tolerant_reason: nil) do |context|
|
|
101
|
+
EmfPlusPayloads.per_object(context) { |stream, located| EmfPlusEffects.pixel_data(stream, located) }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|