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.
Files changed (360) hide show
  1. checksums.yaml +4 -4
  2. data/README.adoc +224 -19
  3. data/exe/emf +12 -0
  4. data/lib/emf/backend.rb +60 -0
  5. data/lib/emf/binary/class_field_accessors.rb +49 -0
  6. data/lib/emf/binary/float32_le.rb +54 -0
  7. data/lib/emf/binary/types.rb +40 -6
  8. data/lib/emf/conformance/context.rb +36 -0
  9. data/lib/emf/conformance/diagnostic.rb +18 -0
  10. data/lib/emf/conformance/manifest/emf_bitmap_misc.yml +622 -0
  11. data/lib/emf/conformance/manifest/emf_control_buffers.yml +61 -0
  12. data/lib/emf/conformance/manifest/emf_core.yml +117 -0
  13. data/lib/emf/conformance/manifest/emf_corpus_rules.yml +21 -0
  14. data/lib/emf/conformance/manifest/emf_objects_geometry.yml +229 -0
  15. data/lib/emf/conformance/manifest/emf_plus.yml +357 -0
  16. data/lib/emf/conformance/manifest/emf_plus_core.yml +33 -0
  17. data/lib/emf/conformance/manifest/emf_state_path.yml +403 -0
  18. data/lib/emf/conformance/manifest/emf_text.yml +97 -0
  19. data/lib/emf/conformance/manifest/wmf.yml +874 -0
  20. data/lib/emf/conformance/manifest.rb +79 -0
  21. data/lib/emf/conformance/profile.rb +35 -0
  22. data/lib/emf/conformance/record_types.rb +24 -0
  23. data/lib/emf/conformance/registry.rb +76 -0
  24. data/lib/emf/conformance/report.rb +83 -0
  25. data/lib/emf/conformance/rule.rb +16 -0
  26. data/lib/emf/conformance/rules/emf_bitmap.rb +357 -0
  27. data/lib/emf/conformance/rules/emf_comment_escape.rb +309 -0
  28. data/lib/emf/conformance/rules/emf_control_buffers.rb +137 -0
  29. data/lib/emf/conformance/rules/emf_corpus_named.rb +83 -0
  30. data/lib/emf/conformance/rules/emf_framing.rb +94 -0
  31. data/lib/emf/conformance/rules/emf_geometry.rb +282 -0
  32. data/lib/emf/conformance/rules/emf_header.rb +114 -0
  33. data/lib/emf/conformance/rules/emf_objects.rb +291 -0
  34. data/lib/emf/conformance/rules/emf_parse_recovery.rb +97 -0
  35. data/lib/emf/conformance/rules/emf_path_clip.rb +165 -0
  36. data/lib/emf/conformance/rules/emf_plus.rb +75 -0
  37. data/lib/emf/conformance/rules/emf_plus_common.rb +58 -0
  38. data/lib/emf/conformance/rules/emf_plus_drawing.rb +78 -0
  39. data/lib/emf/conformance/rules/emf_plus_effects.rb +104 -0
  40. data/lib/emf/conformance/rules/emf_plus_fields.rb +169 -0
  41. data/lib/emf/conformance/rules/emf_plus_fields_b.rb +102 -0
  42. data/lib/emf/conformance/rules/emf_plus_framing.rb +172 -0
  43. data/lib/emf/conformance/rules/emf_plus_objects.rb +97 -0
  44. data/lib/emf/conformance/rules/emf_plus_payloads.rb +320 -0
  45. data/lib/emf/conformance/rules/emf_plus_properties.rb +96 -0
  46. data/lib/emf/conformance/rules/emf_plus_state.rb +77 -0
  47. data/lib/emf/conformance/rules/emf_state.rb +244 -0
  48. data/lib/emf/conformance/rules/emf_text.rb +221 -0
  49. data/lib/emf/conformance/rules/emf_transform.rb +85 -0
  50. data/lib/emf/conformance/rules/wmf_buffers.rb +259 -0
  51. data/lib/emf/conformance/rules/wmf_framing.rb +146 -0
  52. data/lib/emf/conformance/rules/wmf_layout_records.rb +112 -0
  53. data/lib/emf/conformance/rules/wmf_objects.rb +168 -0
  54. data/lib/emf/conformance/rules/wmf_records.rb +191 -0
  55. data/lib/emf/conformance/rules/wmf_rejections.rb +184 -0
  56. data/lib/emf/conformance/runner.rb +61 -0
  57. data/lib/emf/conformance/ternary_raster_operations.rb +52 -0
  58. data/lib/emf/conformance.rb +31 -0
  59. data/lib/emf/emf_plus/effects/blur.rb +24 -0
  60. data/lib/emf/emf_plus/effects/brightness_contrast.rb +22 -0
  61. data/lib/emf/emf_plus/effects/color_balance.rb +24 -0
  62. data/lib/emf/emf_plus/effects/color_curve.rb +24 -0
  63. data/lib/emf/emf_plus/effects/color_lookup_table.rb +26 -0
  64. data/lib/emf/emf_plus/effects/color_matrix.rb +23 -0
  65. data/lib/emf/emf_plus/effects/effect.rb +50 -0
  66. data/lib/emf/emf_plus/effects/hue_saturation_lightness.rb +24 -0
  67. data/lib/emf/emf_plus/effects/levels.rb +24 -0
  68. data/lib/emf/emf_plus/effects/opaque.rb +19 -0
  69. data/lib/emf/emf_plus/effects/red_eye_correction.rb +35 -0
  70. data/lib/emf/emf_plus/effects/sharpen.rb +24 -0
  71. data/lib/emf/emf_plus/effects/tint.rb +22 -0
  72. data/lib/emf/emf_plus/effects.rb +45 -0
  73. data/lib/emf/emf_plus/frame.rb +41 -0
  74. data/lib/emf/emf_plus/links/effect_link.rb +16 -0
  75. data/lib/emf/emf_plus/links/get_dc_span.rb +18 -0
  76. data/lib/emf/emf_plus/links/link_error.rb +15 -0
  77. data/lib/emf/emf_plus/links.rb +77 -0
  78. data/lib/emf/emf_plus/object_chain.rb +108 -0
  79. data/lib/emf/emf_plus/object_table.rb +182 -0
  80. data/lib/emf/emf_plus/objects/brush.rb +296 -0
  81. data/lib/emf/emf_plus/objects/custom_line_cap.rb +106 -0
  82. data/lib/emf/emf_plus/objects/font.rb +47 -0
  83. data/lib/emf/emf_plus/objects/image.rb +68 -0
  84. data/lib/emf/emf_plus/objects/image_attributes.rb +28 -0
  85. data/lib/emf/emf_plus/objects/opaque.rb +19 -0
  86. data/lib/emf/emf_plus/objects/parts.rb +103 -0
  87. data/lib/emf/emf_plus/objects/path.rb +21 -0
  88. data/lib/emf/emf_plus/objects/pen.rb +132 -0
  89. data/lib/emf/emf_plus/objects/region.rb +39 -0
  90. data/lib/emf/emf_plus/objects/string_format.rb +55 -0
  91. data/lib/emf/emf_plus/objects/typed.rb +50 -0
  92. data/lib/emf/emf_plus/objects.rb +46 -0
  93. data/lib/emf/emf_plus/parser.rb +64 -2
  94. data/lib/emf/emf_plus/records/base.rb +107 -0
  95. data/lib/emf/emf_plus/records/begin_container.rb +31 -0
  96. data/lib/emf/emf_plus/records/begin_container_no_params.rb +20 -0
  97. data/lib/emf/emf_plus/records/body.rb +16 -0
  98. data/lib/emf/emf_plus/records/clear.rb +22 -0
  99. data/lib/emf/emf_plus/records/clip_transform_flags.rb +26 -0
  100. data/lib/emf/emf_plus/records/comment.rb +21 -0
  101. data/lib/emf/emf_plus/records/draw_arc.rb +33 -0
  102. data/lib/emf/emf_plus/records/draw_beziers.rb +31 -0
  103. data/lib/emf/emf_plus/records/draw_closed_curve.rb +34 -0
  104. data/lib/emf/emf_plus/records/draw_curve.rb +38 -0
  105. data/lib/emf/emf_plus/records/draw_driver_string.rb +61 -0
  106. data/lib/emf/emf_plus/records/draw_ellipse.rb +28 -0
  107. data/lib/emf/emf_plus/records/draw_image.rb +42 -0
  108. data/lib/emf/emf_plus/records/draw_image_points.rb +62 -0
  109. data/lib/emf/emf_plus/records/draw_lines.rb +34 -0
  110. data/lib/emf/emf_plus/records/draw_path.rb +23 -0
  111. data/lib/emf/emf_plus/records/draw_pie.rb +33 -0
  112. data/lib/emf/emf_plus/records/draw_rects.rb +30 -0
  113. data/lib/emf/emf_plus/records/draw_string.rb +41 -0
  114. data/lib/emf/emf_plus/records/end_container.rb +20 -0
  115. data/lib/emf/emf_plus/records/end_of_file.rb +17 -0
  116. data/lib/emf/emf_plus/records/fill_brush.rb +21 -0
  117. data/lib/emf/emf_plus/records/fill_closed_curve.rb +38 -0
  118. data/lib/emf/emf_plus/records/fill_ellipse.rb +29 -0
  119. data/lib/emf/emf_plus/records/fill_path.rb +23 -0
  120. data/lib/emf/emf_plus/records/fill_pie.rb +34 -0
  121. data/lib/emf/emf_plus/records/fill_polygon.rb +32 -0
  122. data/lib/emf/emf_plus/records/fill_rects.rb +31 -0
  123. data/lib/emf/emf_plus/records/fill_region.rb +23 -0
  124. data/lib/emf/emf_plus/records/get_dc.rb +18 -0
  125. data/lib/emf/emf_plus/records/header.rb +35 -0
  126. data/lib/emf/emf_plus/records/multiply_world_transform.rb +24 -0
  127. data/lib/emf/emf_plus/records/object.rb +32 -0
  128. data/lib/emf/emf_plus/records/offset_clip.rb +24 -0
  129. data/lib/emf/emf_plus/records/primitives/argb.rb +20 -0
  130. data/lib/emf/emf_plus/records/primitives/bitmap.rb +32 -0
  131. data/lib/emf/emf_plus/records/primitives/bitmap_data.rb +21 -0
  132. data/lib/emf/emf_plus/records/primitives/blend_colors.rb +22 -0
  133. data/lib/emf/emf_plus/records/primitives/blend_factors.rb +21 -0
  134. data/lib/emf/emf_plus/records/primitives/bounded_array.rb +38 -0
  135. data/lib/emf/emf_plus/records/primitives/byte_reader.rb +25 -0
  136. data/lib/emf/emf_plus/records/primitives/character_range.rb +20 -0
  137. data/lib/emf/emf_plus/records/primitives/compressed_image.rb +17 -0
  138. data/lib/emf/emf_plus/records/primitives/focus_scale_data.rb +20 -0
  139. data/lib/emf/emf_plus/records/primitives/graphics_version.rb +21 -0
  140. data/lib/emf/emf_plus/records/primitives/language_identifier.rb +22 -0
  141. data/lib/emf/emf_plus/records/primitives/metafile.rb +19 -0
  142. data/lib/emf/emf_plus/records/primitives/palette.rb +21 -0
  143. data/lib/emf/emf_plus/records/primitives/path.rb +89 -0
  144. data/lib/emf/emf_plus/records/primitives/path_point_type.rb +21 -0
  145. data/lib/emf/emf_plus/records/primitives/path_point_type_rle.rb +23 -0
  146. data/lib/emf/emf_plus/records/primitives/point.rb +18 -0
  147. data/lib/emf/emf_plus/records/primitives/point_array.rb +42 -0
  148. data/lib/emf/emf_plus/records/primitives/point_f.rb +19 -0
  149. data/lib/emf/emf_plus/records/primitives/point_r.rb +19 -0
  150. data/lib/emf/emf_plus/records/primitives/rect.rb +20 -0
  151. data/lib/emf/emf_plus/records/primitives/rect_array.rb +24 -0
  152. data/lib/emf/emf_plus/records/primitives/rect_f.rb +21 -0
  153. data/lib/emf/emf_plus/records/primitives/region_node.rb +68 -0
  154. data/lib/emf/emf_plus/records/primitives/region_node_child_nodes.rb +22 -0
  155. data/lib/emf/emf_plus/records/primitives/region_node_path.rb +37 -0
  156. data/lib/emf/emf_plus/records/primitives/relative_coordinate.rb +32 -0
  157. data/lib/emf/emf_plus/records/primitives/string_format_data.rb +23 -0
  158. data/lib/emf/emf_plus/records/primitives/transform_matrix.rb +23 -0
  159. data/lib/emf/emf_plus/records/primitives.rb +41 -0
  160. data/lib/emf/emf_plus/records/reset_clip.rb +17 -0
  161. data/lib/emf/emf_plus/records/reset_world_transform.rb +17 -0
  162. data/lib/emf/emf_plus/records/restore.rb +20 -0
  163. data/lib/emf/emf_plus/records/rotate_world_transform.rb +24 -0
  164. data/lib/emf/emf_plus/records/save.rb +20 -0
  165. data/lib/emf/emf_plus/records/scale_world_transform.rb +26 -0
  166. data/lib/emf/emf_plus/records/serializable_object.rb +32 -0
  167. data/lib/emf/emf_plus/records/set_anti_alias_mode.rb +25 -0
  168. data/lib/emf/emf_plus/records/set_clip_path.rb +20 -0
  169. data/lib/emf/emf_plus/records/set_clip_rect.rb +24 -0
  170. data/lib/emf/emf_plus/records/set_clip_region.rb +20 -0
  171. data/lib/emf/emf_plus/records/set_compositing_mode.rb +21 -0
  172. data/lib/emf/emf_plus/records/set_compositing_quality.rb +21 -0
  173. data/lib/emf/emf_plus/records/set_interpolation_mode.rb +21 -0
  174. data/lib/emf/emf_plus/records/set_page_transform.rb +25 -0
  175. data/lib/emf/emf_plus/records/set_pixel_offset_mode.rb +21 -0
  176. data/lib/emf/emf_plus/records/set_rendering_origin.rb +22 -0
  177. data/lib/emf/emf_plus/records/set_text_contrast.rb +24 -0
  178. data/lib/emf/emf_plus/records/set_text_rendering_hint.rb +21 -0
  179. data/lib/emf/emf_plus/records/set_ts_clip.rb +40 -0
  180. data/lib/emf/emf_plus/records/set_ts_graphics.rb +88 -0
  181. data/lib/emf/emf_plus/records/set_world_transform.rb +22 -0
  182. data/lib/emf/emf_plus/records/shape_flags.rb +23 -0
  183. data/lib/emf/emf_plus/records/translate_world_transform.rb +26 -0
  184. data/lib/emf/emf_plus/records/unknown.rb +20 -0
  185. data/lib/emf/emf_plus/records.rb +107 -0
  186. data/lib/emf/emf_plus/serializer.rb +9 -2
  187. data/lib/emf/emf_plus/stream/conformance.rb +68 -0
  188. data/lib/emf/emf_plus/stream/editor.rb +21 -0
  189. data/lib/emf/emf_plus/stream/emf_gap.rb +20 -0
  190. data/lib/emf/emf_plus/stream/link_index.rb +30 -0
  191. data/lib/emf/emf_plus/stream/rebuild.rb +47 -0
  192. data/lib/emf/emf_plus/stream/record_handle.rb +26 -0
  193. data/lib/emf/emf_plus/stream/segment.rb +70 -0
  194. data/lib/emf/emf_plus/stream.rb +153 -0
  195. data/lib/emf/emf_plus.rb +9 -0
  196. data/lib/emf/emr/binary/dib_header.rb +142 -0
  197. data/lib/emf/emr/binary/ext_text_out_fields.rb +43 -0
  198. data/lib/emf/emr/binary/extended_pen_buffers.rb +23 -0
  199. data/lib/emf/emr/binary/fast_fill.rb +1164 -0
  200. data/lib/emf/emr/binary/font.rb +159 -0
  201. data/lib/emf/emr/binary/lazy_utf16_array.rb +82 -0
  202. data/lib/emf/emr/binary/pack_plan.rb +144 -0
  203. data/lib/emf/emr/binary/pack_plan_check.rb +75 -0
  204. data/lib/emf/emr/binary/pack_plan_exact.rb +130 -0
  205. data/lib/emf/emr/binary/pack_plan_generate.rb +116 -0
  206. data/lib/emf/emr/binary/pack_plan_values.rb +92 -0
  207. data/lib/emf/emr/binary/palette_body.rb +19 -0
  208. data/lib/emf/emr/binary/palette_entry.rb +18 -0
  209. data/lib/emf/emr/binary/record.rb +33 -0
  210. data/lib/emf/emr/binary/records/alpha_blend.rb +43 -0
  211. data/lib/emf/emr/binary/records/angle_arc.rb +2 -2
  212. data/lib/emf/emr/binary/records/bit_blt.rb +14 -0
  213. data/lib/emf/emr/binary/records/color_correct_palette.rb +55 -1
  214. data/lib/emf/emr/binary/records/colormatch_to_target_w.rb +76 -1
  215. data/lib/emf/emr/binary/records/create_colorspace.rb +67 -1
  216. data/lib/emf/emr/binary/records/create_colorspace_w.rb +90 -1
  217. data/lib/emf/emr/binary/records/create_dib_pattern_brush_pt.rb +14 -0
  218. data/lib/emf/emr/binary/records/create_font_indirect_w.rb +6 -2
  219. data/lib/emf/emr/binary/records/create_mono_brush.rb +14 -0
  220. data/lib/emf/emr/binary/records/create_palette.rb +19 -1
  221. data/lib/emf/emr/binary/records/delete_colorspace.rb +22 -1
  222. data/lib/emf/emr/binary/records/draw_escape.rb +45 -1
  223. data/lib/emf/emr/binary/records/ellipse.rb +1 -1
  224. data/lib/emf/emr/binary/records/eof.rb +9 -0
  225. data/lib/emf/emr/binary/records/ext_create_pen.rb +80 -1
  226. data/lib/emf/emr/binary/records/ext_escape.rb +45 -1
  227. data/lib/emf/emr/binary/records/ext_flood_fill.rb +9 -2
  228. data/lib/emf/emr/binary/records/ext_select_clip_rgn.rb +9 -1
  229. data/lib/emf/emr/binary/records/ext_text_out_a.rb +16 -3
  230. data/lib/emf/emr/binary/records/ext_text_out_w.rb +14 -3
  231. data/lib/emf/emr/binary/records/fill_rgn.rb +11 -2
  232. data/lib/emf/emr/binary/records/force_ufi_mapping.rb +38 -1
  233. data/lib/emf/emr/binary/records/frame_rgn.rb +12 -2
  234. data/lib/emf/emr/binary/records/gls_bounded_record.rb +61 -1
  235. data/lib/emf/emr/binary/records/gls_record.rb +42 -1
  236. data/lib/emf/emr/binary/records/gradient_fill.rb +7 -0
  237. data/lib/emf/emr/binary/records/invert_rgn.rb +11 -3
  238. data/lib/emf/emr/binary/records/mask_blt.rb +66 -0
  239. data/lib/emf/emr/binary/records/named_escape.rb +78 -1
  240. data/lib/emf/emr/binary/records/paint_rgn.rb +11 -3
  241. data/lib/emf/emr/binary/records/pixel_format.rb +76 -1
  242. data/lib/emf/emr/binary/records/plg_blt.rb +66 -0
  243. data/lib/emf/emr/binary/records/poly_textout_a.rb +128 -1
  244. data/lib/emf/emr/binary/records/poly_textout_w.rb +128 -1
  245. data/lib/emf/emr/binary/records/raw.rb +3 -0
  246. data/lib/emf/emr/binary/records/realize_palette.rb +1 -0
  247. data/lib/emf/emr/binary/records/rectangle.rb +1 -1
  248. data/lib/emf/emr/binary/records/resize_palette.rb +16 -2
  249. data/lib/emf/emr/binary/records/round_rect.rb +2 -2
  250. data/lib/emf/emr/binary/records/select_palette.rb +1 -0
  251. data/lib/emf/emr/binary/records/set_color_adjustment.rb +136 -0
  252. data/lib/emf/emr/binary/records/set_colorspace.rb +22 -1
  253. data/lib/emf/emr/binary/records/set_dibits_to_device.rb +43 -0
  254. data/lib/emf/emr/binary/records/set_icm_profile_a.rb +71 -1
  255. data/lib/emf/emr/binary/records/set_icm_profile_w.rb +71 -1
  256. data/lib/emf/emr/binary/records/set_linked_ufis.rb +49 -1
  257. data/lib/emf/emr/binary/records/set_miter_limit.rb +1 -1
  258. data/lib/emf/emr/binary/records/set_palette_entries.rb +20 -2
  259. data/lib/emf/emr/binary/records/set_text_justification.rb +8 -2
  260. data/lib/emf/emr/binary/records/small_text_out.rb +55 -4
  261. data/lib/emf/emr/binary/records/stretch_blt.rb +14 -0
  262. data/lib/emf/emr/binary/records/stretch_dibits.rb +14 -0
  263. data/lib/emf/emr/binary/records/transparent_blt.rb +43 -0
  264. data/lib/emf/emr/binary/records.rb +4 -2
  265. data/lib/emf/emr/binary/region_data.rb +33 -0
  266. data/lib/emf/emr/binary/state_record_body.rb +33 -0
  267. data/lib/emf/emr/binary/thin_shell.rb +226 -0
  268. data/lib/emf/emr/binary/value_view/layout.rb +101 -0
  269. data/lib/emf/emr/binary/value_view/reader_source.rb +88 -0
  270. data/lib/emf/emr/binary/value_view.rb +173 -0
  271. data/lib/emf/emr/binary/variable_buffers.rb +87 -0
  272. data/lib/emf/emr/binary.rb +4 -0
  273. data/lib/emf/emr/filled_record.rb +77 -0
  274. data/lib/emf/emr/parser.rb +239 -50
  275. data/lib/emf/emr/serializer.rb +4 -5
  276. data/lib/emf/emr.rb +1 -0
  277. data/lib/emf/error.rb +24 -1
  278. data/lib/emf/model/diagnostic.rb +41 -0
  279. data/lib/emf/model/emr/header.rb +40 -1
  280. data/lib/emf/model/emr/records.rb +142 -6
  281. data/lib/emf/model/emr/wire_values.rb +43 -0
  282. data/lib/emf/model/emr.rb +1 -0
  283. data/lib/emf/model/geometry/color.rb +19 -1
  284. data/lib/emf/model/metafile.rb +22 -2
  285. data/lib/emf/model/wmf/bitmap_records.rb +155 -0
  286. data/lib/emf/model/wmf/brush_records.rb +45 -0
  287. data/lib/emf/model/wmf/dc_escape_records.rb +67 -0
  288. data/lib/emf/model/wmf/font_record.rb +40 -0
  289. data/lib/emf/model/wmf/geometry_records.rb +107 -0
  290. data/lib/emf/model/wmf/header.rb +36 -0
  291. data/lib/emf/model/wmf/object_records.rb +71 -0
  292. data/lib/emf/model/wmf/palette_records.rb +68 -0
  293. data/lib/emf/model/wmf/records.rb +247 -0
  294. data/lib/emf/model/wmf/region_records.rb +112 -0
  295. data/lib/emf/model/wmf/state_records.rb +188 -0
  296. data/lib/emf/model/wmf/text_records.rb +36 -0
  297. data/lib/emf/model/wmf.rb +11 -0
  298. data/lib/emf/model.rb +2 -0
  299. data/lib/emf/native/bitmap_fields.rb +58 -0
  300. data/lib/emf/native/diagnostic_contract.rb +39 -0
  301. data/lib/emf/native/diagnostics.rb +81 -0
  302. data/lib/emf/native/emf_plus_loader.rb +35 -0
  303. data/lib/emf/native/emf_plus_snapshot.rb +96 -0
  304. data/lib/emf/native/emf_plus_source.rb +20 -0
  305. data/lib/emf/native/emf_plus_stream.rb +150 -0
  306. data/lib/emf/native/fields.rb +369 -0
  307. data/lib/emf/native/font_fields.rb +87 -0
  308. data/lib/emf/native/gradient_fields.rb +23 -0
  309. data/lib/emf/native/header.rb +35 -0
  310. data/lib/emf/native/header_errors.rb +30 -0
  311. data/lib/emf/native/hydrator.rb +151 -0
  312. data/lib/emf/native/lazy_wire.rb +100 -0
  313. data/lib/emf/native/legacy_state_fields.rb +22 -0
  314. data/lib/emf/native/library_path.rb +68 -0
  315. data/lib/emf/native/loader.rb +120 -0
  316. data/lib/emf/native/palette_fields.rb +45 -0
  317. data/lib/emf/native/poly_text_fields.rb +116 -0
  318. data/lib/emf/native/resource_failure.rb +28 -0
  319. data/lib/emf/native/small_text_fields.rb +27 -0
  320. data/lib/emf/native/snapshot.rb +102 -0
  321. data/lib/emf/native/wmf_bitmap_fields.rb +58 -0
  322. data/lib/emf/native/wmf_brush_fields.rb +62 -0
  323. data/lib/emf/native/wmf_fields.rb +117 -0
  324. data/lib/emf/native/wmf_hydrator.rb +143 -0
  325. data/lib/emf/native/wmf_loading.rb +49 -0
  326. data/lib/emf/native/wmf_palette_fields.rb +60 -0
  327. data/lib/emf/native/wmf_region_fields.rb +92 -0
  328. data/lib/emf/native/wmf_rejection.rb +84 -0
  329. data/lib/emf/native/wmf_remaining_fields.rb +70 -0
  330. data/lib/emf/native/wmf_snapshot.rb +51 -0
  331. data/lib/emf/native/wmf_text_fields.rb +56 -0
  332. data/lib/emf/native.rb +61 -0
  333. data/lib/emf/parse_evidence.rb +57 -0
  334. data/lib/emf/resource_budget/bitmap.rb +66 -0
  335. data/lib/emf/resource_budget/gradient.rb +30 -0
  336. data/lib/emf/resource_budget/poly_text.rb +85 -0
  337. data/lib/emf/resource_budget.rb +382 -0
  338. data/lib/emf/resource_limits/emf_records.rb +130 -0
  339. data/lib/emf/resource_limits.rb +58 -0
  340. data/lib/emf/version.rb +1 -1
  341. data/lib/emf/visitors/dump.rb +4 -0
  342. data/lib/emf/visitors/stats.rb +6 -5
  343. data/lib/emf/wmf/binary.rb +150 -1
  344. data/lib/emf/wmf/bitmap_codec.rb +118 -0
  345. data/lib/emf/wmf/brush_codec.rb +102 -0
  346. data/lib/emf/wmf/dc_escape_codec.rb +75 -0
  347. data/lib/emf/wmf/dib.rb +187 -0
  348. data/lib/emf/wmf/embedded_emf.rb +137 -0
  349. data/lib/emf/wmf/escape_views.rb +89 -0
  350. data/lib/emf/wmf/evidence.rb +255 -0
  351. data/lib/emf/wmf/geometry_codec.rb +49 -0
  352. data/lib/emf/wmf/palette_codec.rb +85 -0
  353. data/lib/emf/wmf/parser.rb +147 -2
  354. data/lib/emf/wmf/recovery.rb +95 -0
  355. data/lib/emf/wmf/region_codec.rb +151 -0
  356. data/lib/emf/wmf/serializer.rb +164 -2
  357. data/lib/emf/wmf/text_codec.rb +123 -0
  358. data/lib/emf/wmf.rb +7 -0
  359. data/lib/emf.rb +13 -9
  360. 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