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,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "yaml"
|
|
4
|
+
|
|
5
|
+
module Emf
|
|
6
|
+
module Conformance
|
|
7
|
+
# The requirement manifest: one row per rule (implemented) or per known
|
|
8
|
+
# constraint the lens cannot check yet (blocked:model, not_checkable, deferred).
|
|
9
|
+
# Rows live in manifest/<family>.yml, so adding a family means adding a file.
|
|
10
|
+
#
|
|
11
|
+
# - id: emf.restoredc.saved_dc # the registered rule id when implemented
|
|
12
|
+
# format: emf # emf | emf_plus | wmf
|
|
13
|
+
# record_types: [EMR_RESTOREDC] # [] when the rule is not tied to one record type
|
|
14
|
+
# citation: "MS-EMF 2.3.11.6"
|
|
15
|
+
# status: implemented # implemented | blocked:model | not_checkable | deferred
|
|
16
|
+
# reason: null # required unless implemented
|
|
17
|
+
#
|
|
18
|
+
# Statuses: implemented (a registered rule), blocked:model (needs a public model
|
|
19
|
+
# accessor that does not exist), not_checkable (no input can violate it, or the
|
|
20
|
+
# bytes are ignored on receipt), deferred (readable from the public model today,
|
|
21
|
+
# no rule written yet).
|
|
22
|
+
module Manifest
|
|
23
|
+
DIR = File.join(__dir__, "manifest")
|
|
24
|
+
FORMATS = %w[emf emf_plus wmf].freeze
|
|
25
|
+
STATUSES = %w[implemented blocked:model not_checkable deferred].freeze
|
|
26
|
+
KEYS = %w[id format record_types citation status reason].freeze
|
|
27
|
+
|
|
28
|
+
Row = Struct.new(:id, :format, :record_types, :citation, :status, :reason, :source, keyword_init: true) do
|
|
29
|
+
def implemented? = status == "implemented"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
module_function
|
|
33
|
+
|
|
34
|
+
# Every row of every manifest file, in file-name then file order.
|
|
35
|
+
# @param directory [String] folder of family files
|
|
36
|
+
# @return [Array<Row>] frozen rows; each carries its `source` file name
|
|
37
|
+
def rows(directory = DIR)
|
|
38
|
+
Dir[File.join(directory, "*.yml")].flat_map { |path| load_file(path) }.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Problems that make a row unusable, as strings. Empty when well formed.
|
|
42
|
+
def problems(row)
|
|
43
|
+
[
|
|
44
|
+
("id must be a non-empty string" unless row.id.is_a?(String) && !row.id.empty?),
|
|
45
|
+
("format #{row.format.inspect} is not one of #{FORMATS.join(', ')}" unless FORMATS.include?(row.format)),
|
|
46
|
+
("record_types must be an Array of record names or numeric codes" unless record_types?(row.record_types)),
|
|
47
|
+
("citation must be a non-empty string" unless row.citation.is_a?(String) && !row.citation.empty?),
|
|
48
|
+
("status #{row.status.inspect} is not one of #{STATUSES.join(', ')}" unless STATUSES.include?(row.status)),
|
|
49
|
+
reason_problem(row)
|
|
50
|
+
].compact
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def load_file(path)
|
|
54
|
+
entries = YAML.safe_load_file(path)
|
|
55
|
+
raise ArgumentError, "#{path}: expected an Array of rows" unless entries.is_a?(Array)
|
|
56
|
+
|
|
57
|
+
entries.map { |entry| row(entry, File.basename(path)) }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def row(entry, source)
|
|
61
|
+
raise ArgumentError, "#{source}: row is not a mapping: #{entry.inspect}" unless entry.is_a?(Hash)
|
|
62
|
+
|
|
63
|
+
unknown = entry.keys - KEYS
|
|
64
|
+
raise ArgumentError, "#{source}: unknown keys #{unknown.inspect} in row #{entry['id'].inspect}" unless unknown.empty?
|
|
65
|
+
|
|
66
|
+
Row.new(**entry.transform_keys(&:to_sym), source: source).freeze
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Names (EMR_RESTOREDC) or numeric record-function codes (WMF's 0x0102 as 258).
|
|
70
|
+
def record_types?(value) = value.is_a?(Array) && value.all? { |type| type.is_a?(String) || type.is_a?(Integer) }
|
|
71
|
+
|
|
72
|
+
def reason_problem(row)
|
|
73
|
+
return if row.implemented? ? row.reason.nil? : row.reason.is_a?(String) && !row.reason.strip.empty?
|
|
74
|
+
|
|
75
|
+
row.implemented? ? "an implemented row must not carry a reason" : "#{row.status} row needs a reason"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Conformance
|
|
5
|
+
# Decides severity. strict: every rule at its declared severity.
|
|
6
|
+
# tolerant: a rule that carries a `tolerant_reason` is downgraded to a warning.
|
|
7
|
+
class Profile
|
|
8
|
+
attr_reader :name
|
|
9
|
+
|
|
10
|
+
def initialize(name, downgrade:)
|
|
11
|
+
@name = name
|
|
12
|
+
@downgrade = downgrade
|
|
13
|
+
freeze
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def severity_for(rule)
|
|
17
|
+
@downgrade && rule.tolerant_reason ? :warning : rule.severity
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def waiver_for(rule)
|
|
21
|
+
rule.tolerant_reason if @downgrade
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
STRICT = new(:strict, downgrade: false)
|
|
25
|
+
TOLERANT = new(:tolerant, downgrade: true)
|
|
26
|
+
ALL = { strict: STRICT, tolerant: TOLERANT }.freeze
|
|
27
|
+
|
|
28
|
+
def self.fetch(name)
|
|
29
|
+
return name if name.is_a?(Profile)
|
|
30
|
+
|
|
31
|
+
ALL.fetch(name.to_sym) { raise ArgumentError, "unknown conformance profile: #{name.inspect} (expected strict or tolerant)" }
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Conformance
|
|
5
|
+
# EMF record type codes of MS-EMF 2.1.1 (RecordType enumeration): 1..122
|
|
6
|
+
# except 69, 107 and 117, which the enumeration does not define.
|
|
7
|
+
module RecordTypes
|
|
8
|
+
UNDEFINED = [69, 107, 117].freeze
|
|
9
|
+
DEFINED = ((1..122).to_a - UNDEFINED).freeze
|
|
10
|
+
HEADER = 1
|
|
11
|
+
EOF_RECORD = 14
|
|
12
|
+
COMMENT = 70
|
|
13
|
+
|
|
14
|
+
module_function
|
|
15
|
+
|
|
16
|
+
def defined?(type) = DEFINED.include?(type)
|
|
17
|
+
|
|
18
|
+
def name(type)
|
|
19
|
+
Emf::Emr::Binary::TypeCodes.constants.find { |c| Emf::Emr::Binary::TypeCodes.const_get(c) == type }&.to_s ||
|
|
20
|
+
format("0x%04X", type)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "monitor"
|
|
4
|
+
|
|
5
|
+
module Emf
|
|
6
|
+
module Conformance
|
|
7
|
+
# Ordered rule set. Rule files under conformance/rules/*.rb register
|
|
8
|
+
# themselves on load:
|
|
9
|
+
#
|
|
10
|
+
# Emf::Conformance::Registry.loading.register(
|
|
11
|
+
# id: "wmf.header.size", formats: [:wmf], citation: "MS-WMF 2.3.2.2",
|
|
12
|
+
# kind: :header_size, severity: :error, tolerant_reason: nil
|
|
13
|
+
# ) { |context| [{ message: "...", offset: 0 }] }
|
|
14
|
+
class Registry
|
|
15
|
+
RULES_DIR = File.join(__dir__, "rules")
|
|
16
|
+
|
|
17
|
+
include Enumerable
|
|
18
|
+
|
|
19
|
+
DEFAULT_LOCK = Monitor.new
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@rules = {}
|
|
23
|
+
@loaded_files = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def register(id:, formats:, citation:, kind:, severity: :error, tolerant_reason: nil, &check)
|
|
27
|
+
raise ArgumentError, "rule #{id} needs a check block" unless check
|
|
28
|
+
raise ArgumentError, "rule #{id} already registered" if @rules.key?(id)
|
|
29
|
+
|
|
30
|
+
@rules[id] = Rule.new(id: id, formats: Array(formats), citation: citation, kind: kind,
|
|
31
|
+
severity: severity, tolerant_reason: tolerant_reason, check: check)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def each(&) = @rules.each_value(&)
|
|
35
|
+
|
|
36
|
+
def loaded(file) = @loaded_files << File.expand_path(file)
|
|
37
|
+
|
|
38
|
+
def loaded?(file) = @loaded_files.include?(File.expand_path(file))
|
|
39
|
+
|
|
40
|
+
def [](id) = @rules[id]
|
|
41
|
+
|
|
42
|
+
def rules_for(format) = select { |rule| rule.applies_to?(format) }
|
|
43
|
+
|
|
44
|
+
# The rule's `formats.first` names its family in diagnostics (:emf, :emf_plus, :wmf).
|
|
45
|
+
# Process-wide registry. Loading it requires every rules/*.rb file, so a
|
|
46
|
+
# new family file is picked up without editing any list.
|
|
47
|
+
def self.default = @default || DEFAULT_LOCK.synchronize { @default ||= load_from(RULES_DIR) }
|
|
48
|
+
|
|
49
|
+
# A new registry holding every rule file found directly in `directory`.
|
|
50
|
+
def self.load_from(directory)
|
|
51
|
+
new.tap do |registry|
|
|
52
|
+
Thread.current[:emf_conformance_registry] = registry
|
|
53
|
+
Dir[File.join(directory, "*.rb")].each do |file|
|
|
54
|
+
load file
|
|
55
|
+
registry.loaded(file)
|
|
56
|
+
# Mark it loaded, so a later `require` of the same file does not run it a second time.
|
|
57
|
+
$LOADED_FEATURES << File.expand_path(file) if directory == RULES_DIR
|
|
58
|
+
end
|
|
59
|
+
ensure
|
|
60
|
+
Thread.current[:emf_conformance_registry] = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The registry rule files register into while they load.
|
|
65
|
+
def self.loading = Thread.current[:emf_conformance_registry] || default
|
|
66
|
+
|
|
67
|
+
# Every rule file starts with `return if Registry.loaded_elsewhere?(__FILE__)`. Inside load_from it is false and the
|
|
68
|
+
# file runs. Required directly, the default registry loads it (once) and the direct run stops before registering.
|
|
69
|
+
def self.loaded_elsewhere?(file)
|
|
70
|
+
return false if Thread.current[:emf_conformance_registry]
|
|
71
|
+
|
|
72
|
+
default.loaded?(file)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "time"
|
|
4
|
+
require "yaml"
|
|
5
|
+
|
|
6
|
+
module Emf
|
|
7
|
+
module Conformance
|
|
8
|
+
# svg_conform's ConformanceReport shape (filename, profile, tool, version,
|
|
9
|
+
# timestamp, valid, errors, warnings). Issue keys follow svg_conform's with
|
|
10
|
+
# `offset` (byte offset in the file) in place of line/column/xpath, which
|
|
11
|
+
# have no meaning in a binary format.
|
|
12
|
+
#
|
|
13
|
+
# Issue keys: `kind` is the diagnostic kind and `spec_citation` the cited section (issue #37 AC2).
|
|
14
|
+
# `category` (same value as `kind`) and `context.spec_citation` (same value as `spec_citation`) are
|
|
15
|
+
# kept for svg_conform shape compatibility and for reports already consumed.
|
|
16
|
+
#
|
|
17
|
+
# Contract (pinned by spec/emf/conformance/report_spec.rb):
|
|
18
|
+
# - `issues` keep the diagnostics' order (file offset ascending, offset-less
|
|
19
|
+
# findings first, then requirement id); `by_requirement` is ordered by id.
|
|
20
|
+
# - Every value in `to_h` is a String, Integer, true/false or nil; keys are Strings.
|
|
21
|
+
# - The only time-dependent key is "timestamp", from the `now:` given to
|
|
22
|
+
# Emf::Conformance.check. To compare two runs (for example forced backends),
|
|
23
|
+
# pass the same `now:` to both, or compare `to_h(timestamp: false)`.
|
|
24
|
+
class Report
|
|
25
|
+
TOOL = "emf"
|
|
26
|
+
|
|
27
|
+
attr_reader :filename, :profile, :timestamp, :diagnostics
|
|
28
|
+
|
|
29
|
+
def initialize(filename:, profile:, timestamp:, diagnostics:)
|
|
30
|
+
@filename = filename
|
|
31
|
+
@profile = profile
|
|
32
|
+
@timestamp = timestamp
|
|
33
|
+
@diagnostics = diagnostics.dup.freeze
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def errors = diagnostics.select(&:error?)
|
|
37
|
+
|
|
38
|
+
def warnings = diagnostics.select(&:warning?)
|
|
39
|
+
|
|
40
|
+
def valid? = errors.empty?
|
|
41
|
+
|
|
42
|
+
# @param timestamp [Boolean] false drops the "timestamp" key, giving a report
|
|
43
|
+
# that depends only on the input and profile
|
|
44
|
+
def to_h(timestamp: true)
|
|
45
|
+
head = { "filename" => filename, "profile" => profile.to_s, "tool" => TOOL, "version" => Emf::VERSION }
|
|
46
|
+
head["timestamp"] = self.timestamp.getutc.iso8601 if timestamp
|
|
47
|
+
head.merge("valid" => valid?, "errors" => summary(errors, "error"), "warnings" => summary(warnings, "warning"))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def to_yaml(*options, timestamp: true) = to_h(timestamp: timestamp).to_yaml(*options)
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def summary(list, type)
|
|
55
|
+
{ "total_count" => list.size,
|
|
56
|
+
"by_requirement" => list.map(&:requirement_id).tally.sort.to_h,
|
|
57
|
+
"issues" => list.map { |diagnostic| issue(diagnostic, type) } }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def issue(diagnostic, type)
|
|
61
|
+
{ "type" => type, "requirement_id" => diagnostic.requirement_id, "message" => diagnostic.message,
|
|
62
|
+
"element" => element(diagnostic), "attribute" => nil, "value" => nil, "offset" => diagnostic.offset,
|
|
63
|
+
"severity" => type, "kind" => diagnostic.kind.to_s, "category" => diagnostic.kind.to_s,
|
|
64
|
+
"spec_citation" => diagnostic.spec_citation, "remediation_available" => false,
|
|
65
|
+
"context" => context(diagnostic) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def element(diagnostic)
|
|
69
|
+
diagnostic.record_type && "#{diagnostic.format}:#{record_name(diagnostic)}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def record_name(diagnostic)
|
|
73
|
+
diagnostic.format == :emf ? RecordTypes.name(diagnostic.record_type) : format("0x%04X", diagnostic.record_type)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def context(diagnostic)
|
|
77
|
+
{ "spec_citation" => diagnostic.spec_citation, "format" => diagnostic.format.to_s,
|
|
78
|
+
"record_index" => diagnostic.record_index, "record_type" => diagnostic.record_type,
|
|
79
|
+
"waiver" => diagnostic.waiver }.compact
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Emf
|
|
4
|
+
module Conformance
|
|
5
|
+
# A named check. `check` is called with a Context and returns an Array of
|
|
6
|
+
# findings (Hash with :message and optional :offset, :record_index, :record_type).
|
|
7
|
+
Rule = Struct.new(:id, :formats, :citation, :kind, :severity, :tolerant_reason, :check, keyword_init: true) do
|
|
8
|
+
def applies_to?(format) = formats.include?(format)
|
|
9
|
+
|
|
10
|
+
def findings(context)
|
|
11
|
+
found = check.call(context)
|
|
12
|
+
found.is_a?(Hash) ? [found] : Array(found)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bitmap record rules (MS-EMF 2.3.1.x, MS-WMF 2.2.2.3 / 2.2.2.9). The BMI and bits
|
|
4
|
+
# bytes are read from the record's own bytes: only offsets, lengths and the fixed
|
|
5
|
+
# BITMAPINFOHEADER fields, never the pixels.
|
|
6
|
+
return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
|
|
7
|
+
|
|
8
|
+
module Emf
|
|
9
|
+
module Conformance
|
|
10
|
+
module BitmapRules
|
|
11
|
+
PALETTE_COLORS = "GDI writes PALETTEINDEX and PALETTERGB colors with 0x01 or 0x02 in the Reserved byte, and readers resolve them"
|
|
12
|
+
EMF = %i[emf emf_plus].freeze
|
|
13
|
+
Slot = Struct.new(:name, :usage, :off_bmi, :cb_bmi, :off_bits, :cb_bits)
|
|
14
|
+
SOURCE = Slot.new("source", 80, 84, 88, 92, 96)
|
|
15
|
+
DIB_SOURCE = Slot.new("source", 64, 48, 52, 56, 60)
|
|
16
|
+
MASK = Slot.new("mask", 108, 112, 116, 120, 124)
|
|
17
|
+
PLG_SOURCE = Slot.new("source", 92, 96, 100, 104, 108)
|
|
18
|
+
PLG_MASK = Slot.new("mask", 120, 124, 128, 132, 136)
|
|
19
|
+
GAMUT_INTENTS = [1, 2, 4, 8].freeze
|
|
20
|
+
V5_HEADER_SIZE = 124
|
|
21
|
+
|
|
22
|
+
# type => fixed size, bitmap slots and the offsets of the type's other checked fields.
|
|
23
|
+
LAYOUTS = {
|
|
24
|
+
76 => { name: "EMR_BITBLT", size: 100, slots: [SOURCE], rop: 40, colorrefs: [76] },
|
|
25
|
+
77 => { name: "EMR_STRETCHBLT", size: 108, slots: [SOURCE], rop: 40, colorrefs: [76] },
|
|
26
|
+
78 => { name: "EMR_MASKBLT", size: 128, slots: [SOURCE, MASK], rop4: 40, colorrefs: [76] },
|
|
27
|
+
79 => { name: "EMR_PLGBLT", size: 140, slots: [PLG_SOURCE, PLG_MASK], colorrefs: [88] },
|
|
28
|
+
80 => { name: "EMR_SETDIBITSTODEVICE", size: 76, slots: [DIB_SOURCE], scans: 72 },
|
|
29
|
+
81 => { name: "EMR_STRETCHDIBITS", size: 80, slots: [DIB_SOURCE], rop: 68 },
|
|
30
|
+
114 => { name: "EMR_ALPHABLEND", size: 108, slots: [SOURCE], blend: 40, extents: [32, 36, 100, 104], colorrefs: [76] },
|
|
31
|
+
116 => { name: "EMR_TRANSPARENTBLT", size: 108, slots: [SOURCE], colorrefs: [40, 76] }
|
|
32
|
+
}.freeze
|
|
33
|
+
|
|
34
|
+
BIT_COUNTS = [0, 1, 4, 8, 16, 24, 32].freeze
|
|
35
|
+
COMPRESSIONS = [0, 1, 2, 3, 4, 5, 0xB, 0xC, 0xD].freeze
|
|
36
|
+
RLE = [1, 2, 0xC, 0xD].freeze
|
|
37
|
+
STRIDE_BASED = [0, 3, 0xB].freeze
|
|
38
|
+
|
|
39
|
+
UNLOCATABLE = "the record Size still delimits the stream, so a bitmap that cannot be located is skipped by readers"
|
|
40
|
+
|
|
41
|
+
# id => [citation, kind, severity, tolerant_reason]
|
|
42
|
+
RULES = {
|
|
43
|
+
"emf.bitmap.record_size" => ["MS-EMF 2.3.1", :bitmap_size, :error, "the parser recovers a short bitmap record as raw bytes"],
|
|
44
|
+
"emf.bitmap.usage" => ["MS-EMF 2.1.9", :bitmap_usage, :error, nil],
|
|
45
|
+
"emf.bitmap.bmi_range" => ["MS-EMF 2.3.1", :bmi_range, :error, UNLOCATABLE],
|
|
46
|
+
"emf.bitmap.bits_range" => ["MS-EMF 2.3.1", :bits_range, :error, UNLOCATABLE],
|
|
47
|
+
"emf.bitmap.buffer_fixed_overlap" => ["MS-EMF 2.3.1", :buffer_fixed_overlap, :error, nil],
|
|
48
|
+
"emf.bitmap.buffer_overlap" => ["MS-EMF 2.3.1", :buffer_overlap, :error, nil],
|
|
49
|
+
"emf.bitmap.bmi_header_size" => ["MS-WMF 2.2.2.9", :bmi_header_size, :error, UNLOCATABLE],
|
|
50
|
+
"emf.bitmap.bmi_extent" => ["MS-WMF 2.2.2.3", :bmi_extent, :error, nil],
|
|
51
|
+
"emf.bitmap.bmi_planes" => ["MS-WMF 2.2.2.3", :bmi_planes, :error, nil],
|
|
52
|
+
"emf.bitmap.bmi_bitcount" => ["MS-WMF 2.1.1.3", :bmi_bitcount, :error, nil],
|
|
53
|
+
"emf.bitmap.bmi_compression" => ["MS-WMF 2.1.1.7", :bmi_compression, :error, nil],
|
|
54
|
+
"emf.bitmap.bits_length" => ["MS-WMF 2.2.2.9", :bits_length, :error, nil],
|
|
55
|
+
"emf.bitmap.color_table" => ["MS-WMF 2.2.2.3", :color_table, :error,
|
|
56
|
+
"readers cap an oversized ColorUsed to the colour table BitCount allows (note under ColorUsed), " \
|
|
57
|
+
"so the corrupted fixtures still decode"],
|
|
58
|
+
"emf.bitmap.color_table_length" => ["MS-WMF 2.2.2.9", :color_table_length, :warning, nil],
|
|
59
|
+
"emf.bitmap.bmi_v5_intent" => ["MS-WMF 2.2.2.5", :bmi_v5_intent, :error, nil],
|
|
60
|
+
"emf.bitmap.colorref_reserved" => ["MS-WMF 2.2.2.8", :colorref_reserved, :error, nil],
|
|
61
|
+
"emf.bitmap.colorref_palette" => ["MS-WMF 2.2.2.8", :colorref_reserved, :error, PALETTE_COLORS],
|
|
62
|
+
"emf.bitmap.mask_monochrome" => ["MS-EMF 2.3.1.2", :mask_monochrome, :error, nil],
|
|
63
|
+
"emf.bitmap.raster_operation" => ["MS-EMF 2.3.1.1", :raster_operation, :error, nil],
|
|
64
|
+
"emf.bitmap.rop4_reserved" => ["MS-EMF 2.3.1.2", :rop4_reserved, :warning, nil],
|
|
65
|
+
"emf.bitmap.blend_operation" => ["MS-EMF 2.3.1.4", :blend_operation, :error, nil],
|
|
66
|
+
"emf.bitmap.alpha_format" => ["MS-EMF 2.3.1.4", :alpha_format, :error, nil],
|
|
67
|
+
"emf.bitmap.positive_extent" => ["MS-EMF 2.3.1.4", :positive_extent, :error, nil]
|
|
68
|
+
}.freeze
|
|
69
|
+
|
|
70
|
+
# Findings of one metafile, grouped by rule id; computed once and shared by all rules.
|
|
71
|
+
CACHE = ObjectSpace::WeakMap.new
|
|
72
|
+
|
|
73
|
+
module_function
|
|
74
|
+
|
|
75
|
+
def findings(context, id)
|
|
76
|
+
# The WeakMap holds its values weakly: keep a strong reference until the lookup is done.
|
|
77
|
+
grouped = CACHE[context] || (CACHE[context] = analyze(context))
|
|
78
|
+
grouped.fetch(id, [])
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def analyze(context)
|
|
82
|
+
out = Hash.new { |hash, id| hash[id] = [] }
|
|
83
|
+
context.records.each_with_index do |record, index|
|
|
84
|
+
layout = LAYOUTS[context.record_type(record)]
|
|
85
|
+
Scan.new(record, index, layout, context.record_size(record), out).run if layout
|
|
86
|
+
end
|
|
87
|
+
out
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# BITMAPINFOHEADER / BITMAPCOREHEADER checks and the bits-length arithmetic; mixed into Scan.
|
|
91
|
+
module HeaderChecks
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def header_fields(bmi)
|
|
95
|
+
count = bmi[2] - bmi[1]
|
|
96
|
+
size = count >= 4 ? u32(bmi[1]) : nil
|
|
97
|
+
unless size && (size == 12 || size >= 40) && size <= count
|
|
98
|
+
add("emf.bitmap.bmi_header_size", "#{bmi[0]} header size #{size.inspect} is not 12 or 40+ within its #{count} bytes")
|
|
99
|
+
return
|
|
100
|
+
end
|
|
101
|
+
size == 12 ? core_header(bmi[1]) : info_header(bmi[1])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def core_header(pos)
|
|
105
|
+
fields = { planes: @bytes.unpack1("v", offset: pos + 8), bit_count: @bytes.unpack1("v", offset: pos + 10),
|
|
106
|
+
width: @bytes.unpack1("v", offset: pos + 4), height: @bytes.unpack1("v", offset: pos + 6) }
|
|
107
|
+
check_depth(fields)
|
|
108
|
+
fields.merge(compression: 0, image_size: 0, header_size: 12, color_used: 0)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def info_header(pos)
|
|
112
|
+
fields = { width: s32(pos + 4), height: s32(pos + 8), planes: @bytes.unpack1("v", offset: pos + 12),
|
|
113
|
+
bit_count: @bytes.unpack1("v", offset: pos + 14), compression: u32(pos + 16), image_size: u32(pos + 20),
|
|
114
|
+
color_used: u32(pos + 32), header_size: u32(pos) }
|
|
115
|
+
v5_intent(pos) if fields[:header_size] == V5_HEADER_SIZE
|
|
116
|
+
add("emf.bitmap.bmi_extent", "BMI Width #{fields[:width]} MUST be positive") unless fields[:width].positive?
|
|
117
|
+
add("emf.bitmap.bmi_extent", "BMI Height MUST NOT be zero") if fields[:height].zero?
|
|
118
|
+
check_depth(fields)
|
|
119
|
+
compression(fields)
|
|
120
|
+
fields
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def check_depth(fields)
|
|
124
|
+
add("emf.bitmap.bmi_planes", "BMI Planes #{fields[:planes]} MUST be 1") unless fields[:planes] == 1
|
|
125
|
+
return if BIT_COUNTS.include?(fields[:bit_count])
|
|
126
|
+
|
|
127
|
+
add("emf.bitmap.bmi_bitcount",
|
|
128
|
+
"BMI BitCount #{fields[:bit_count]} is not in the BitCount enumeration")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def compression(fields)
|
|
132
|
+
code = fields[:compression]
|
|
133
|
+
unless COMPRESSIONS.include?(code)
|
|
134
|
+
add("emf.bitmap.bmi_compression",
|
|
135
|
+
"BMI Compression #{code} is not in the Compression enumeration")
|
|
136
|
+
end
|
|
137
|
+
return unless fields[:height].negative? && RLE.include?(code)
|
|
138
|
+
|
|
139
|
+
add("emf.bitmap.bmi_compression", "top-down BMI (negative Height) MUST NOT use compression #{code}")
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Uncompressed: at least the stride formula's length; compressed: exactly the BMI ImageSize.
|
|
143
|
+
def bits_length(slot, header)
|
|
144
|
+
count = u32(slot.cb_bits)
|
|
145
|
+
return if count.zero?
|
|
146
|
+
|
|
147
|
+
if STRIDE_BASED.include?(header[:compression])
|
|
148
|
+
expected = stride_bytes(header)
|
|
149
|
+
if count < expected
|
|
150
|
+
add("emf.bitmap.bits_length",
|
|
151
|
+
"#{slot.name} cbBits #{count} is shorter than the #{expected} bytes the BMI stride needs")
|
|
152
|
+
end
|
|
153
|
+
elsif count != header[:image_size]
|
|
154
|
+
add("emf.bitmap.bits_length", "#{slot.name} cbBits #{count} MUST equal the BMI ImageSize #{header[:image_size]}")
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def v5_intent(pos)
|
|
159
|
+
intent = u32(pos + 108)
|
|
160
|
+
return if GAMUT_INTENTS.include?(intent)
|
|
161
|
+
|
|
162
|
+
add("emf.bitmap.bmi_v5_intent", "BitmapV5Header Intent #{intent} is not in the GamutMappingIntent enumeration")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# The colour table is whatever the BMI buffer holds after the header: RGBQuads, or 16-bit palette
|
|
166
|
+
# indexes under DIB_PAL_COLORS; a BI_BITFIELDS BitmapInfoHeader also keeps its three masks there.
|
|
167
|
+
def color_table(slot, bmi, header, packed)
|
|
168
|
+
size = header[:header_size]
|
|
169
|
+
usage = u32(slot.usage)
|
|
170
|
+
return if size < 40 || usage > 1 || [4, 5].include?(header[:compression]) || bmi[2] - bmi[1] < size
|
|
171
|
+
|
|
172
|
+
entry = usage.zero? ? 4 : 2
|
|
173
|
+
held = bmi[2] - bmi[1] - size
|
|
174
|
+
held -= 12 if header[:compression] == 3 && size == 40 && held >= 12
|
|
175
|
+
check_tables(slot, header, held, entry, packed)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# A packed bitmap with a ColorUsed is judged by emf.bitmap.color_table (MUST, error) on this same shortfall, so
|
|
179
|
+
# the table-length rule stays silent for it.
|
|
180
|
+
def check_tables(slot, header, held, entry, packed)
|
|
181
|
+
if packed
|
|
182
|
+
check_color_used(slot, header, held, entry)
|
|
183
|
+
return unless header[:color_used].zero?
|
|
184
|
+
end
|
|
185
|
+
check_color_table_length(slot, header, held, entry)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def table_entries(header)
|
|
189
|
+
limit = header[:bit_count] < 16 ? 1 << header[:bit_count] : nil
|
|
190
|
+
used = header[:color_used]
|
|
191
|
+
return limit.to_i if used.zero?
|
|
192
|
+
|
|
193
|
+
limit ? [used, limit].min : used
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# "In a packed bitmap, the ColorUsed value MUST be either 0 or the actual size of the color table."
|
|
197
|
+
# Only a ColorUsed that outruns the buffer is certain: real producers (test-164/165) pad the BMI buffer.
|
|
198
|
+
def check_color_used(slot, header, held, entry)
|
|
199
|
+
return if header[:color_used].zero?
|
|
200
|
+
|
|
201
|
+
want = table_entries(header)
|
|
202
|
+
return if held >= want * entry
|
|
203
|
+
|
|
204
|
+
add("emf.bitmap.color_table",
|
|
205
|
+
"#{slot.name} packed bitmap ColorUsed #{header[:color_used]} needs #{want * entry} colour table bytes, " \
|
|
206
|
+
"the BMI buffer holds only #{held} after its header")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def check_color_table_length(slot, header, held, entry)
|
|
210
|
+
return unless [1, 4, 8].include?(header[:bit_count])
|
|
211
|
+
|
|
212
|
+
need = table_entries(header) * entry
|
|
213
|
+
return if held >= need
|
|
214
|
+
|
|
215
|
+
add("emf.bitmap.color_table_length",
|
|
216
|
+
"#{slot.name} BMI holds #{held} colour table bytes, BitCount #{header[:bit_count]} / ColorUsed " \
|
|
217
|
+
"#{header[:color_used]} SHOULD give #{need}")
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def stride_bytes(header)
|
|
221
|
+
lines = header[:height].abs
|
|
222
|
+
lines = [lines, u32(@layout[:scans])].min if @layout[:scans]
|
|
223
|
+
return 0 unless header[:width].positive?
|
|
224
|
+
|
|
225
|
+
(((header[:width] * header[:planes] * header[:bit_count]) + 31) & ~31) / 8 * lines
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# One record's checks; every method adds to `out[rule id]`.
|
|
230
|
+
class Scan
|
|
231
|
+
include HeaderChecks
|
|
232
|
+
|
|
233
|
+
def initialize(record, index, layout, size, out)
|
|
234
|
+
@record = record
|
|
235
|
+
@index = index
|
|
236
|
+
@layout = layout
|
|
237
|
+
@size = size
|
|
238
|
+
@out = out
|
|
239
|
+
@bytes = record.to_binary_s
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def run
|
|
243
|
+
if @size < @layout[:size]
|
|
244
|
+
return add("emf.bitmap.record_size",
|
|
245
|
+
"#{@layout[:name]} Size #{@size} is smaller than its #{@layout[:size]}-byte fixed part")
|
|
246
|
+
end
|
|
247
|
+
return if @bytes.bytesize < @layout[:size]
|
|
248
|
+
|
|
249
|
+
ranges = @layout[:slots].flat_map { |slot| slot_checks(slot) }
|
|
250
|
+
overlaps(ranges.compact)
|
|
251
|
+
other_fields
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
private
|
|
255
|
+
|
|
256
|
+
def add(id, message)
|
|
257
|
+
@out[id] << { message: message, offset: @record.offset, record_index: @index + 1, record_type: @record.type_id.to_i }
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def u32(pos) = @bytes.unpack1("V", offset: pos)
|
|
261
|
+
def s32(pos) = @bytes.unpack1("l<", offset: pos)
|
|
262
|
+
|
|
263
|
+
def slot_checks(slot)
|
|
264
|
+
usage = u32(slot.usage)
|
|
265
|
+
add("emf.bitmap.usage", "#{@layout[:name]} #{slot.name} UsageSrc/UsageMask #{usage} is not a DIBColors value") if usage > 2
|
|
266
|
+
bmi = range(slot, "#{slot.name} BMI", slot.off_bmi, slot.cb_bmi, "emf.bitmap.bmi_range")
|
|
267
|
+
bits = range(slot, "#{slot.name} bits", slot.off_bits, slot.cb_bits, "emf.bitmap.bits_range")
|
|
268
|
+
header = bmi && header_fields(bmi)
|
|
269
|
+
header_checks(slot, bmi, bits, header) if header
|
|
270
|
+
@header ||= header if slot.name == "source"
|
|
271
|
+
[bmi, bits]
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def header_checks(slot, bmi, bits, header)
|
|
275
|
+
bits_length(slot, header)
|
|
276
|
+
color_table(slot, bmi, header, bits && bits[1] == bmi[2])
|
|
277
|
+
return unless slot.name == "mask" && header[:bit_count] != 1
|
|
278
|
+
|
|
279
|
+
add("emf.bitmap.mask_monochrome", "mask bitmap BitCount #{header[:bit_count]} is not monochrome (1)")
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
# [label, from, to] of a non-empty buffer range, or nil when empty or out of the record.
|
|
283
|
+
def range(slot, label, off_at, cb_at, id)
|
|
284
|
+
offset = u32(off_at)
|
|
285
|
+
count = u32(cb_at)
|
|
286
|
+
return if count.zero?
|
|
287
|
+
|
|
288
|
+
if offset + count > @size
|
|
289
|
+
add(id, "#{label} range #{offset}+#{count} runs past the #{@size}-byte record")
|
|
290
|
+
return
|
|
291
|
+
end
|
|
292
|
+
if offset < @layout[:size]
|
|
293
|
+
add("emf.bitmap.buffer_fixed_overlap",
|
|
294
|
+
"#{label} offset #{offset} lies inside the #{@layout[:size]}-byte fixed part")
|
|
295
|
+
end
|
|
296
|
+
[label, offset, offset + count, slot]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def overlaps(ranges)
|
|
300
|
+
reach = nil
|
|
301
|
+
ranges.sort_by { |range| range[1] }.each do |range|
|
|
302
|
+
add("emf.bitmap.buffer_overlap", "#{range[0]} overlaps #{reach[0]}") if reach && range[1] < reach[2]
|
|
303
|
+
reach = range if reach.nil? || range[2] > reach[2]
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def other_fields
|
|
308
|
+
rop = @layout[:rop]
|
|
309
|
+
if rop && u32(rop) >> 24 != 0
|
|
310
|
+
add("emf.bitmap.raster_operation",
|
|
311
|
+
"raster operation 0x#{u32(rop).to_s(16)} has bits set above the 24-bit ternary code")
|
|
312
|
+
end
|
|
313
|
+
rop4 = @layout[:rop4]
|
|
314
|
+
if rop4 && @bytes.unpack1(
|
|
315
|
+
"v", offset: rop4
|
|
316
|
+
) != 0
|
|
317
|
+
add("emf.bitmap.rop4_reserved",
|
|
318
|
+
"ROP4 reserved word 0x#{@bytes.unpack1('v', offset: rop4).to_s(16)} SHOULD be 0")
|
|
319
|
+
end
|
|
320
|
+
colorrefs
|
|
321
|
+
blend if @layout[:blend]
|
|
322
|
+
@layout[:extents]&.each do |pos|
|
|
323
|
+
add("emf.bitmap.positive_extent", "extent at offset #{pos} is #{s32(pos)}, MUST be greater than zero") unless s32(pos).positive?
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def colorrefs
|
|
328
|
+
@layout[:colorrefs]&.each do |pos|
|
|
329
|
+
reserved = @bytes.getbyte(pos + 3)
|
|
330
|
+
next if reserved.zero?
|
|
331
|
+
|
|
332
|
+
id = [1, 2].include?(reserved) ? "emf.bitmap.colorref_palette" : "emf.bitmap.colorref_reserved"
|
|
333
|
+
add(id, "ColorRef at offset #{pos} has Reserved byte #{reserved}, MUST be 0")
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
def blend
|
|
338
|
+
pos = @layout[:blend]
|
|
339
|
+
operation, _flags, _alpha, format = @bytes.byteslice(pos, 4).unpack("C4")
|
|
340
|
+
unless operation.zero?
|
|
341
|
+
add("emf.bitmap.blend_operation",
|
|
342
|
+
"BlendOperation #{operation} is not 0 (AC_SRC_OVER, the only defined value)")
|
|
343
|
+
end
|
|
344
|
+
add("emf.bitmap.alpha_format", "AlphaFormat #{format} is neither 0 nor AC_SRC_ALPHA (1)") unless [0, 1].include?(format)
|
|
345
|
+
return unless format == 1 && @header && @header[:bit_count] != 32
|
|
346
|
+
|
|
347
|
+
add("emf.bitmap.alpha_format", "AC_SRC_ALPHA needs a 32 bits-per-pixel source, BMI says #{@header[:bit_count]}")
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
BitmapRules::RULES.each do |id, (citation, kind, severity, reason)|
|
|
353
|
+
Registry.loading.register(id: id, formats: BitmapRules::EMF, citation: citation, kind: kind, severity: severity,
|
|
354
|
+
tolerant_reason: reason) { |context| BitmapRules.findings(context, id) }
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
end
|