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,168 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WMF object-table lifecycle and playback-DC depth rules over the public record
4
+ # sequence (MS-WMF 3.1.4.1 WMF Object Table, 2.3.4 object records, 2.3.5.10
5
+ # META_RESTOREDC). One walk replays the table exactly as the spec describes it:
6
+ # a created object takes the lowest free index below header NumberOfObjects,
7
+ # META_DELETEOBJECT frees it, and later records must refer to live indexes of the
8
+ # right object type. Records the model keeps opaque (UnknownRecord) are skipped.
9
+ return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
10
+
11
+ module Emf
12
+ module Conformance
13
+ module WmfObjectRules
14
+ WMF = %i[wmf].freeze
15
+ R = Emf::Model::Wmf::Records
16
+ STOCK_FLAG = 0x8000
17
+ GDI_IGNORES = "GDI skips a record that names an unusable object instead of rejecting the metafile"
18
+
19
+ CREATORS = {
20
+ R::CreatePenIndirect => :pen, R::CreateBrushIndirect => :brush, R::CreatePatternBrush => :brush,
21
+ R::DibCreatePatternBrush => :brush, R::CreateFontIndirect => :font, R::CreatePalette => :palette,
22
+ R::CreateRegion => :region
23
+ }.freeze
24
+ REGION_USERS = [R::FillRegion, R::FrameRegion, R::InvertRegion, R::PaintRegion, R::SelectClipRegion].freeze
25
+ BRUSH_USERS = [R::FillRegion, R::FrameRegion].freeze
26
+
27
+ # id => [citation, tolerant_reason, kind]
28
+ RULES = {
29
+ "wmf.object.capacity" => ["MS-WMF 3.1.4.1 WMF Object Table, 2.3.2.2 META_HEADER (NumberOfObjects)", nil, :object_capacity],
30
+ "wmf.object.select_reference" => ["MS-WMF 2.3.4.10 META_SELECTOBJECT (ObjectIndex), 3.1.4.1; stock-object selectors " \
31
+ "(high bit 0x8000) are an implementation rule, MS-WMF defines no selector encoding",
32
+ GDI_IGNORES, :object_reference],
33
+ "wmf.object.delete_reference" => ["MS-WMF 2.3.4.7 META_DELETEOBJECT (ObjectIndex), 3.1.4.1; stock objects are never " \
34
+ "deleted (1.1 Glossary), the 0x8000 selector bit is an implementation rule",
35
+ GDI_IGNORES, :object_reference],
36
+ "wmf.object.palette_reference" => ["MS-WMF 2.3.4.11 META_SELECTPALETTE (Palette), 3.1.4.1", GDI_IGNORES, :object_type],
37
+ "wmf.object.region_reference" => [
38
+ "MS-WMF 2.3.3.6 META_FILLREGION, 2.3.3.8 META_FRAMEREGION, 2.3.3.9 META_INVERTREGION, " \
39
+ "2.3.3.11 META_PAINTREGION, 2.3.4.9 META_SELECTCLIPREGION (Region), 3.1.4.1", GDI_IGNORES, :object_type
40
+ ],
41
+ "wmf.object.brush_reference" => [
42
+ "MS-WMF 2.3.3.6 META_FILLREGION, 2.3.3.8 META_FRAMEREGION (Brush), 3.1.4.1", GDI_IGNORES, :object_type
43
+ ],
44
+ "wmf.dc.restore_depth" => ["MS-WMF 2.3.5.10 META_RESTOREDC (nSavedDC), 2.3.5.11 META_SAVEDC, 3.1.5; the outcome " \
45
+ "of an nSavedDC that names no saved context is an implementation rule, MS-WMF defines " \
46
+ "none", GDI_IGNORES, :dc_stack]
47
+ }.freeze
48
+
49
+ module_function
50
+
51
+ # Replays the record sequence and returns [rule_id, record, index, message] findings.
52
+ def walk(context)
53
+ table = Table.new(context.header.number_of_objects.to_i)
54
+ found = []
55
+ context.records.each_with_index do |record, index|
56
+ table.step(record) { |id, message| found << [id, record, index + 1, message] }
57
+ end
58
+ found
59
+ end
60
+
61
+ def findings(context, id)
62
+ walk(context).filter_map do |rule_id, record, index, message|
63
+ next unless rule_id == id
64
+
65
+ { message: message, offset: record.offset, record_index: index, record_type: record.type_id.to_i }
66
+ end
67
+ end
68
+
69
+ # The conceptual WMF Object Table of MS-WMF 3.1.4.1 plus the SaveDC depth.
70
+ class Table
71
+ def initialize(capacity)
72
+ @capacity = capacity
73
+ @slots = []
74
+ @next_free = 0
75
+ @saved = 0
76
+ end
77
+
78
+ def step(record, &)
79
+ kind = CREATORS.find { |klass, _| record.is_a?(klass) }&.last
80
+ return create(kind, &) if kind
81
+
82
+ case record
83
+ when R::SelectObject then select_object(record, &)
84
+ when R::DeleteObject then delete_object(record, &)
85
+ when R::SelectPalette then expect(record.object_index, :palette, "palette", "wmf.object.palette_reference", &)
86
+ when R::FillRegion, R::FrameRegion then fill_region(record, &)
87
+ when *REGION_USERS then expect(record.object_index, :region, "region", "wmf.object.region_reference", &)
88
+ when R::SaveDc, R::RestoreDc then device_context(record, &)
89
+ end
90
+ end
91
+
92
+ private
93
+
94
+ def live(index) = index.negative? ? nil : @slots[index]
95
+
96
+ def create(kind)
97
+ @next_free += 1 while @next_free < @capacity && @slots[@next_free]
98
+ if @next_free < @capacity
99
+ @slots[@next_free] = kind
100
+ @next_free += 1
101
+ else
102
+ yield "wmf.object.capacity", "#{kind} object has no free WMF Object Table index; " \
103
+ "NumberOfObjects is #{@capacity} and every index is in use"
104
+ end
105
+ end
106
+
107
+ def select_object(record)
108
+ index = record.object_index.to_i
109
+ return if index.anybits?(STOCK_FLAG) # stock objects live outside the metafile table
110
+ return if live(index)
111
+
112
+ yield "wmf.object.select_reference", "SelectObject index #{index} names no live object in the WMF Object Table"
113
+ end
114
+
115
+ def delete_object(record)
116
+ index = record.object_index.to_i
117
+ if index.anybits?(STOCK_FLAG)
118
+ yield "wmf.object.delete_reference", "DeleteObject index 0x#{index.to_s(16).upcase} is a stock object, which cannot be deleted"
119
+ elsif live(index)
120
+ @slots[index] = nil
121
+ @next_free = index if index < @next_free
122
+ else
123
+ yield "wmf.object.delete_reference", "DeleteObject index #{index} names no live object in the WMF Object Table"
124
+ end
125
+ end
126
+
127
+ def fill_region(record, &)
128
+ expect(record.region_index, :region, "region", "wmf.object.region_reference", &)
129
+ expect(record.brush_index, :brush, "brush", "wmf.object.brush_reference", &)
130
+ end
131
+
132
+ def expect(index, kind, label, id)
133
+ index = index.to_i
134
+ found = live(index)
135
+ if found.nil?
136
+ yield id, "#{label} index #{index} names no live object in the WMF Object Table"
137
+ elsif found != kind
138
+ yield id, "#{label} index #{index} refers to a #{found} object, MUST refer to a #{kind} object"
139
+ end
140
+ end
141
+
142
+ def device_context(record, &)
143
+ return @saved += 1 if record.is_a?(R::SaveDc)
144
+
145
+ restore(record.saved_dc.to_i, &)
146
+ end
147
+
148
+ def restore(saved_dc)
149
+ depth = if saved_dc.negative? then @saved + saved_dc
150
+ elsif saved_dc.positive? && saved_dc <= @saved then saved_dc - 1
151
+ end
152
+ if depth&.>=(0)
153
+ @saved = depth
154
+ else
155
+ yield "wmf.dc.restore_depth", "RestoreDc nSavedDC #{saved_dc} names no saved device context (#{@saved} saved)"
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ registry = Registry.loading
162
+ WmfObjectRules::RULES.each do |id, (citation, reason, kind)|
163
+ registry.register(id: id, formats: WmfObjectRules::WMF, citation: citation, kind: kind, tolerant_reason: reason) do |context|
164
+ WmfObjectRules.findings(context, id)
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,191 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WMF fixed-field, enum and point-array rules over typed public records
4
+ # (MS-WMF 2.3.3 drawing, 2.3.4 object, 2.3.5 state records). Records the model
5
+ # keeps opaque (UnknownRecord) are never judged here.
6
+ return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
7
+
8
+ module Emf
9
+ module Conformance
10
+ module WmfRecordRules
11
+ WMF = %i[wmf].freeze
12
+ R = Emf::Model::Wmf::Records
13
+ IGNORED = "GDI ignores an out-of-range value instead of rejecting the record"
14
+
15
+ # id => [record class, field, valid values (Range/Array), citation, name of the enumeration]
16
+ ENUMS = {
17
+ "wmf.enum.bk_mode" => [R::SetBkMode, :mode, [1, 2], "MS-WMF 2.1.1.20 MixMode Enumeration; " \
18
+ "2.3.5.15 META_SETBKMODE (BkMode)", "MixMode"],
19
+ "wmf.enum.map_mode" => [R::SetMapMode, :mode, (1..8), "MS-WMF 2.1.1.16 MapMode Enumeration; " \
20
+ "2.3.5.17 META_SETMAPMODE (MapMode)", "MapMode"],
21
+ "wmf.enum.poly_fill_mode" => [R::SetPolyFillMode, :mode, [1, 2], "MS-WMF 2.1.1.25 PolyFillMode Enumeration; " \
22
+ "2.3.5.20 META_SETPOLYFILLMODE (PolyFillMode)",
23
+ "PolyFillMode"],
24
+ "wmf.enum.rop2" => [R::SetRop2, :mode, (1..16), "MS-WMF 2.1.1.2 BinaryRasterOperation Enumeration; " \
25
+ "2.3.5.22 META_SETROP2 (DrawMode)", "BinaryRasterOperation"],
26
+ "wmf.enum.stretch_blt_mode" => [R::SetStretchBltMode, :mode, (1..4), "MS-WMF 2.1.1.30 StretchMode Enumeration; " \
27
+ "2.3.5.23 META_SETSTRETCHBLTMODE (StretchMode)",
28
+ "StretchMode"],
29
+ "wmf.enum.ext_flood_fill_mode" => [R::ExtFloodFill, :mode, [0, 1], "MS-WMF 2.1.1.9 FloodFill Enumeration; " \
30
+ "2.3.3.4 META_EXTFLOODFILL (Mode)", "FloodFill"],
31
+ "wmf.enum.brush_style" => [R::CreateBrushIndirect, :brush_style, (0..9), "MS-WMF 2.1.1.4 BrushStyle Enumeration; " \
32
+ "2.3.4.1 META_CREATEBRUSHINDIRECT, " \
33
+ "2.2.2.10 LogBrush Object (BrushStyle)",
34
+ "BrushStyle"]
35
+ }.freeze
36
+
37
+ BS_PATTERN = 3
38
+ COLOR_USAGE = [R::DibCreatePatternBrush, R::SetDibToDev, R::StretchDib].freeze
39
+ TEXT_ALIGN_MASK = 0x011F
40
+ HORIZONTAL_ALIGN = [0x0, 0x2, 0x6].freeze # TA_LEFT, TA_RIGHT, TA_CENTER (bit 0x4 alone is no value)
41
+ VERTICAL_ALIGN = [0x0, 0x8, 0x18].freeze # TA_TOP, TA_BOTTOM, TA_BASELINE (bit 0x10 alone is no value)
42
+ LAYOUT_MASK = 0x0009 # LAYOUT_RTL | LAYOUT_BITMAPORIENTATIONPRESERVED; LAYOUT_LTR is 0
43
+ PEN_CAPS = [0x0000, 0x0100, 0x0200].freeze
44
+ PEN_JOINS = [0x0000, 0x1000, 0x2000].freeze
45
+
46
+ module_function
47
+
48
+ def enum(context, klass, field, domain, name)
49
+ each_of(context, klass) do |record|
50
+ value = record.public_send(field)
51
+ next if domain.include?(value)
52
+
53
+ "#{klass.name.split('::').last} #{field} is #{value.inspect}, not a #{name} value"
54
+ end
55
+ end
56
+
57
+ def color_usage(context)
58
+ each_of(context, *COLOR_USAGE) do |record|
59
+ value = record.color_usage
60
+ # META_DIBCREATEPATTERNBRUSH: with Style BS_PATTERN, DIB_RGB_COLORS is used regardless of this field.
61
+ next if record.is_a?(R::DibCreatePatternBrush) && record.style == BS_PATTERN
62
+ next if [0, 1, 2].include?(value)
63
+
64
+ "color usage is #{value.inspect}, MUST be DIB_RGB_COLORS, DIB_PAL_COLORS or DIB_PAL_INDICES"
65
+ end
66
+ end
67
+
68
+ def hatch_style(context)
69
+ each_of(context, R::CreateBrushIndirect) do |record|
70
+ next unless record.brush_style == 2 && !(0..5).cover?(record.brush_hatch)
71
+
72
+ "BS_HATCHED brush has HatchStyle #{record.brush_hatch.inspect}, MUST be 0..5"
73
+ end
74
+ end
75
+
76
+ def pen_style(context)
77
+ each_of(context, R::CreatePenIndirect) do |record|
78
+ style = record.pen_style.to_i
79
+ next if (style & 0xF) <= 8 && PEN_CAPS.include?(style & 0x0F00) && PEN_JOINS.include?(style & 0xF000)
80
+
81
+ format("pen style 0x%04X is not a PenStyle style, end-cap and join combination", style)
82
+ end
83
+ end
84
+
85
+ def text_align(context)
86
+ each_of(context, R::SetTextAlign) do |record|
87
+ value = record.alignment.to_i
88
+ if value.anybits?(~TEXT_ALIGN_MASK)
89
+ format("text alignment 0x%04X sets bits outside the TextAlignmentMode flags", value)
90
+ elsif !HORIZONTAL_ALIGN.include?(value & 0x6)
91
+ "text alignment 0x#{hex(value, 4)} has horizontal bits 0x#{(value & 0x6).to_s(16).upcase}, " \
92
+ "MUST be TA_LEFT, TA_RIGHT or TA_CENTER"
93
+ elsif !VERTICAL_ALIGN.include?(value & 0x18)
94
+ "text alignment 0x#{hex(value, 4)} has vertical bits 0x#{hex(value & 0x18, 2)}, " \
95
+ "MUST be TA_TOP, TA_BOTTOM or TA_BASELINE"
96
+ end
97
+ end
98
+ end
99
+
100
+ def hex(value, width) = value.to_s(16).upcase.rjust(width, "0")
101
+
102
+ def layout(context)
103
+ each_of(context, R::SetLayout) do |record|
104
+ next if record.layout.to_i.nobits?(~LAYOUT_MASK)
105
+
106
+ format("layout 0x%04X sets bits outside the Layout flags", record.layout.to_i)
107
+ end
108
+ end
109
+
110
+ def point_count(context)
111
+ each_of(context, R::Polygon, R::Polyline) do |record|
112
+ count = record.points.size
113
+ if count > 0x7FFF
114
+ "#{count} points do not fit the signed 16-bit NumberOfPoints"
115
+ elsif record.is_a?(R::Polygon) && count == 1
116
+ "polygon has #{count} point(s), MUST have at least 2"
117
+ end
118
+ end
119
+ end
120
+
121
+ # NumberOfPoints 0 is the one count below 2 the parser keeps (Wmf::Recovery); it draws nothing.
122
+ def polygon_empty(context)
123
+ each_of(context, R::Polygon) { |record| "polygon has no points, MUST have at least 2" if record.points.empty? }
124
+ end
125
+
126
+ def polypolygon_counts(context)
127
+ each_of(context, R::PolyPolygon) do |record|
128
+ counts = record.points_per_polygon
129
+ if counts.size > 0xFFFF
130
+ "#{counts.size} polygons do not fit the unsigned 16-bit NumberOfPolygons"
131
+ elsif counts.sum != record.points.size
132
+ "PolyPolygon counts add up to #{counts.sum} points but the record holds #{record.points.size}"
133
+ elsif counts.any? { |count| count > 0xFFFF }
134
+ "a PolyPolygon polygon count exceeds the unsigned 16-bit range"
135
+ end
136
+ end
137
+ end
138
+
139
+ def point_range(context)
140
+ each_of(context, R::Polygon, R::Polyline, R::PolyPolygon) do |record|
141
+ bad = record.points.find { |point| !(-0x8000..0x7FFF).cover?(point.x) || !(-0x8000..0x7FFF).cover?(point.y) }
142
+ "point (#{bad.x}, #{bad.y}) is outside the signed 16-bit coordinate range" if bad
143
+ end
144
+ end
145
+
146
+ def each_of(context, *classes)
147
+ context.records.each_with_index.filter_map do |record, index|
148
+ next unless classes.any? { |klass| record.is_a?(klass) }
149
+
150
+ message = yield record
151
+ next unless message
152
+
153
+ { message: message, offset: record.offset, record_index: index + 1, record_type: record.type_id.to_i }
154
+ end
155
+ end
156
+ end
157
+
158
+ registry = Registry.loading
159
+ WmfRecordRules::ENUMS.each do |id, (klass, field, domain, citation, name)|
160
+ registry.register(id: id, formats: WmfRecordRules::WMF, citation: citation, kind: :enum_domain,
161
+ tolerant_reason: WmfRecordRules::IGNORED) do |context|
162
+ WmfRecordRules.enum(context, klass, field, domain, name)
163
+ end
164
+ end
165
+ [
166
+ ["wmf.enum.color_usage", :color_usage,
167
+ "MS-WMF 2.1.1.6 ColorUsage Enumeration; " \
168
+ "2.3.4.8 META_DIBCREATEPATTERNBRUSH, 2.3.1.4 META_SETDIBTODEV, 2.3.1.6 META_STRETCHDIB (ColorUsage)",
169
+ :enum_domain, WmfRecordRules::IGNORED],
170
+ ["wmf.enum.hatch_style", :hatch_style, "MS-WMF 2.1.1.12 HatchStyle Enumeration; " \
171
+ "2.2.2.10 LogBrush Object (BrushHatch), 2.3.4.1 META_CREATEBRUSHINDIRECT", :enum_domain, WmfRecordRules::IGNORED],
172
+ ["wmf.enum.pen_style", :pen_style, "MS-WMF 2.1.1.23 PenStyle Enumeration; " \
173
+ "2.2.1.4 Pen Object (PenStyle), 2.3.4.5 META_CREATEPENINDIRECT", :enum_domain, WmfRecordRules::IGNORED],
174
+ ["wmf.flags.text_align", :text_align, "MS-WMF 2.1.2.3 TextAlignmentMode Flags, 2.1.2.4 VerticalTextAlignmentMode Flags; " \
175
+ "2.3.5.24 META_SETTEXTALIGN (TextAlignmentMode)", :flag_mask, WmfRecordRules::IGNORED],
176
+ ["wmf.flags.layout", :layout, "MS-WMF 2.1.1.13 Layout Enumeration; " \
177
+ "2.3.5.16 META_SETLAYOUT (Layout)", :flag_mask, WmfRecordRules::IGNORED],
178
+ ["wmf.array.point_count", :point_count, "MS-WMF 2.3.3.14 META_POLYLINE, 2.3.3.15 META_POLYGON (NumberOfPoints)", :point_count, nil],
179
+ ["wmf.array.polypolygon_counts", :polypolygon_counts, "MS-WMF 2.2.2.17 PolyPolygon Object (NumberOfPolygons, aPointsPerPolygon); " \
180
+ "2.3.3.16 META_POLYPOLYGON",
181
+ :array_counts, nil],
182
+ ["wmf.array.polygon_empty", :polygon_empty, "MS-WMF 2.3.3.15 META_POLYGON (NumberOfPoints)", :point_count,
183
+ "PowerPoint writes a polygon with no points (Apache POI empty-polygon-close.wmf); readers draw nothing and import the file"],
184
+ ["wmf.fixed.point_range", :point_range, "MS-WMF 2.2.2.16 PointS Object", :coordinate_range, nil]
185
+ ].each do |id, method, citation, kind, reason|
186
+ registry.register(id: id, formats: WmfRecordRules::WMF, citation: citation, kind: kind, tolerant_reason: reason) do |context|
187
+ WmfRecordRules.public_send(method, context)
188
+ end
189
+ end
190
+ end
191
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ # WMF rejected-input diagnostics and declared-size rules.
4
+ #
5
+ # Rejections: a WMF the parser refuses never becomes a Metafile, so the lens context is :unparseable and holds the
6
+ # exception. Its neutral Emf::ParseEvidence (check, offset, declared, actual) names the reason; each reason with a
7
+ # `wmf.reject.rNNNN` row becomes one finding citing that row. Rejection is a no-model outcome, so strict and
8
+ # tolerant both report an error (no tolerant_reason), like emf.parse.unparseable. Reasons that cannot surface
9
+ # are left to emf.parse.unparseable: 101 and 401 need a 200 MiB or million-record input, and 301-303 are shadowed by
10
+ # the format detector, which turns such inputs into :unrecognised_signature before any WMF check runs. 305 (header
11
+ # Size) and 1801 (MaxRecord) are not rejections: the parser recovers, keeps the model and records the drift in
12
+ # Metafile#errors, so the declared-size rules below (wmf.header.size_matches_records, wmf.header.max_record) report
13
+ # it, as a warning in the tolerant profile. The same holds for 203 (reported by wmf.placeable.checksum), 406, 701
14
+ # (a font short of its face name) and 801 (an object record with surplus bytes), reported by wmf.record.declared_size,
15
+ # 1003 (wmf.text.negative_length), and 1702 and the clip/paint/invert part of 1704 (the wmf.object.*_reference rules).
16
+ # 701, 801 and 1704 stay rejections for the shapes that cannot be recovered.
17
+ #
18
+ # Declared size: on an accepted model each record carries the rdSize read from the source file
19
+ # (Record#declared_size_words, nil for records built in memory). The rules compare it with the layout and with the
20
+ # header's MaxRecord and Size.
21
+ return if Emf::Conformance::Registry.loaded_elsewhere?(__FILE__)
22
+
23
+ module Emf
24
+ module Conformance
25
+ module WmfRejectionRules
26
+ WMF = %i[wmf unparseable].freeze
27
+ SHADOWED = [101, 401, 301, 302, 303].freeze
28
+ FUNCTION_OFFSET = 4
29
+ FIRST_RECORD_REASON = 402
30
+
31
+ # evidence check => reason code, for every reason that can produce a named finding
32
+ CHECKS = Emf::Wmf::Evidence::REASONS.except(*SHADOWED, *Emf::Wmf::Evidence::DRIFT_REASONS,
33
+ *Emf::Wmf::Evidence::RETIRED_REASONS).to_h do |reason, (check, _text)|
34
+ [check, reason]
35
+ end.freeze
36
+
37
+ module_function
38
+
39
+ def id(reason) = format("wmf.reject.r%04d", reason)
40
+
41
+ # The reason code a parse failure names, or nil when the failure is not a rejection with a wmf.reject row.
42
+ def reason_for(failure)
43
+ CHECKS[failure&.evidence&.check]
44
+ end
45
+
46
+ def findings(context, reason)
47
+ failure = context.parse_failure
48
+ return [] unless reason_for(failure) == reason
49
+
50
+ evidence = failure.evidence
51
+ [{ message: message(reason, evidence), offset: evidence.offset, record_type: function(context.bytes, evidence, reason) }]
52
+ end
53
+
54
+ def message(reason, evidence)
55
+ text = Emf::Wmf::Evidence::REASONS.fetch(reason).last.sub("%d") { evidence.declared.to_s }
56
+ values = { declared: evidence.declared, actual: evidence.actual }.compact.map { |name, value| "#{name} #{value}" }
57
+ "#{text} at offset #{evidence.offset}#{" (#{values.join(', ')})" unless values.empty?}"
58
+ end
59
+
60
+ # Record function word of the failing record, for record-level reasons whose bytes are available.
61
+ def function(bytes, evidence, reason)
62
+ return if reason < FIRST_RECORD_REASON || bytes.nil? || evidence.offset.nil?
63
+ return if evidence.offset + FUNCTION_OFFSET + 2 > bytes.bytesize
64
+
65
+ bytes.unpack1("v", offset: evidence.offset + FUNCTION_OFFSET)
66
+ end
67
+ end
68
+
69
+ module WmfDeclaredSizeRules
70
+ WMF = %i[wmf].freeze
71
+ R = Emf::Model::Wmf::Records
72
+ B = Emf::Wmf::Binary
73
+ MIN_WORDS = 3
74
+ HEADER_WORDS = 9
75
+ SIZE_RECOVERY = "the parser recovers by framing the record with its RecordSize: surplus bytes after an EOF or an " \
76
+ "object payload are ignored and a font face name shorter than 32 bytes is zero-padded; the " \
77
+ "departure is recorded in Metafile#errors (seen in LibreOffice test files)"
78
+ HEADER_DRIFT = "the parser recovered: the model is built from the records actually present and the header drift " \
79
+ "is recorded in Metafile#errors (seen in files from Inkscape and ImageMagick)"
80
+
81
+ module_function
82
+
83
+ # [exact?, words] the record layout requires, or nil when the layout leaves the size open.
84
+ def layout(record)
85
+ case record
86
+ when R::Eof then [true, MIN_WORDS]
87
+ when R::CreateFontIndirect, *B::STATE_RECORDS.values then [false, MIN_WORDS + (record.class::PAYLOAD_BYTES / 2)]
88
+ when *B::FIXED_RECORDS.values then [true, MIN_WORDS + record.class::WIRE_FIELDS.length]
89
+ when *B::OBJECT_RECORDS.values then [true, MIN_WORDS + (record.class::PAYLOAD_BYTES / 2)]
90
+ end
91
+ end
92
+
93
+ def declared_size(context)
94
+ context.records.each_with_index.filter_map do |record, index|
95
+ words = record.declared_size_words
96
+ next unless words
97
+
98
+ problem = size_problem(record, words)
99
+ at(record, index, problem) if problem
100
+ end
101
+ end
102
+
103
+ def size_problem(record, words)
104
+ return "RecordSize is #{words} words, a record is at least #{MIN_WORDS} words" if words < MIN_WORDS
105
+
106
+ exact, expected = layout(record)
107
+ return unless expected
108
+
109
+ name = record.class.name.split("::").last
110
+ return "RecordSize is #{words} words, the #{name} layout is #{expected} words" if exact && words != expected
111
+ return unless !exact && words < expected
112
+
113
+ "RecordSize is #{words} words, the #{name} layout needs at least #{expected} words"
114
+ end
115
+
116
+ def buffer_declared_size(context)
117
+ context.records.each_with_index.filter_map do |record, index|
118
+ words = record.declared_size_words
119
+ encoded = words && buffer_words(record)
120
+ next unless encoded && encoded != words
121
+
122
+ at(record, index, "RecordSize is #{words} words but the #{record.class.name.split('::').last} buffers occupy #{encoded} words")
123
+ end
124
+ end
125
+
126
+ def buffer_words(record)
127
+ return unless B.prefix_codec(record) || record.is_a?(R::TextOut)
128
+
129
+ Emf::Wmf::Serializer.serialize_record(record).bytesize / 2
130
+ rescue Emf::SerializeError
131
+ nil
132
+ end
133
+
134
+ def max_record(context)
135
+ sizes = context.records.map(&:declared_size_words)
136
+ return [] if sizes.empty? || sizes.any?(&:nil?)
137
+
138
+ largest = sizes.max
139
+ declared = context.header.max_record_words.to_i
140
+ return [] if declared == largest
141
+
142
+ [{ message: "WMF header MaxRecord is #{declared} words, the largest record is #{largest} words",
143
+ offset: context.header.placeable ? 34 : 12 }]
144
+ end
145
+
146
+ def size_matches_records(context)
147
+ sizes = context.records.map(&:declared_size_words)
148
+ return [] if sizes.empty? || sizes.any?(&:nil?)
149
+
150
+ total = HEADER_WORDS + sizes.sum
151
+ declared = context.header.size_words.to_i
152
+ return [] if declared == total
153
+
154
+ [{ message: "WMF header Size is #{declared} words, the header and the records add up to #{total} words",
155
+ offset: context.header.placeable ? 28 : 6 }]
156
+ end
157
+
158
+ def at(record, index, message)
159
+ { message: message, offset: record.offset, record_index: index + 1, record_type: record.type_id.to_i }
160
+ end
161
+ end
162
+
163
+ registry = Registry.loading
164
+ manifest = Manifest.rows.to_h { |row| [row.id, row.citation] }
165
+ WmfRejectionRules::CHECKS.each do |check, reason|
166
+ id = WmfRejectionRules.id(reason)
167
+ registry.register(id: id, formats: WmfRejectionRules::WMF, citation: manifest.fetch(id), kind: check) do |context|
168
+ WmfRejectionRules.findings(context, reason)
169
+ end
170
+ end
171
+
172
+ [
173
+ ["wmf.record.declared_size", :declared_size, :declared_size, WmfDeclaredSizeRules::SIZE_RECOVERY],
174
+ ["wmf.buffer.declared_size", :buffer_declared_size, :buffer_declared_size],
175
+ ["wmf.header.max_record", :max_record, :max_record, WmfDeclaredSizeRules::HEADER_DRIFT],
176
+ ["wmf.header.size_matches_records", :size_matches_records, :size_matches_records, WmfDeclaredSizeRules::HEADER_DRIFT]
177
+ ].each do |id, method, kind, tolerant_reason|
178
+ registry.register(id: id, formats: WmfDeclaredSizeRules::WMF, citation: manifest.fetch(id), kind: kind,
179
+ tolerant_reason: tolerant_reason) do |context|
180
+ WmfDeclaredSizeRules.public_send(method, context)
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Emf
6
+ module Conformance
7
+ # Parses the input through the normal entry point and applies every rule
8
+ # that matches the metafile's format.
9
+ class Runner
10
+ def initialize(profile, registry)
11
+ @profile = profile
12
+ @registry = registry
13
+ end
14
+
15
+ def call(input, now:)
16
+ filename, bytes, metafile = load(input)
17
+ context = build_context(metafile, bytes)
18
+ Report.new(filename: filename, profile: @profile.name, timestamp: now, diagnostics: diagnostics(context))
19
+ end
20
+
21
+ private
22
+
23
+ def load(input)
24
+ case input
25
+ when Model::Metafile then [nil, nil, input]
26
+ when Pathname then [input.to_s, File.binread(input), nil]
27
+ when String then path?(input) ? [input, File.binread(input), nil] : [nil, input.b, nil]
28
+ else raise ArgumentError, "expected a String, Pathname or Emf::Model::Metafile, got #{input.class}"
29
+ end
30
+ end
31
+
32
+ def path?(input)
33
+ input.encoding.ascii_compatible? && !input.include?("\0") && input.bytesize < 4096 && File.file?(input)
34
+ end
35
+
36
+ def build_context(metafile, bytes)
37
+ return Context.new(metafile: metafile, bytes: bytes) if metafile
38
+
39
+ Context.new(metafile: Emf.parse(bytes), bytes: bytes)
40
+ rescue Emf::Native::Unavailable
41
+ raise
42
+ rescue Emf::Error => e
43
+ Context.new(bytes: bytes, parse_failure: e)
44
+ end
45
+
46
+ def diagnostics(context)
47
+ found = @registry.rules_for(context.format).flat_map do |rule|
48
+ rule.findings(context).map { |finding| diagnostic(rule, finding) }
49
+ end
50
+ found.each_with_index.sort_by { |d, index| [d.offset || -1, d.requirement_id, index] }.map(&:first)
51
+ end
52
+
53
+ def diagnostic(rule, finding)
54
+ Diagnostic.new(requirement_id: rule.id, spec_citation: rule.citation, severity: @profile.severity_for(rule),
55
+ kind: rule.kind, format: rule.formats.first, offset: finding[:offset], record_index: finding[:record_index],
56
+ record_type: finding[:record_type], message: finding.fetch(:message),
57
+ waiver: @profile.waiver_for(rule))
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Conformance
5
+ # The TernaryRasterOperation values of MS-WMF 2.1.1.31, keyed by their operation index (bits 16..23).
6
+ # Extracted from the specification's per-operation "Reverse Polish" entries (all 256), normalising the text's
7
+ # formatting slips: DPSOAN is printed with nine digits (000570389) and SPDNOX with seven (0390604).
8
+ module TernaryRasterOperations
9
+ # rubocop:disable-next Metrics/CollectionLiteralLength -- the specification's table; kept in Ruby so the gem ships it
10
+ VALUES = [
11
+ 0x0000_0042, 0x0001_0289, 0x0002_0C89, 0x0003_00AA, 0x0004_0C88, 0x0005_00A9, 0x0006_0865, 0x0007_02C5,
12
+ 0x0008_0F08, 0x0009_0245, 0x000A_0329, 0x000B_0B2A, 0x000C_0324, 0x000D_0B25, 0x000E_08A5, 0x000F_0001,
13
+ 0x0010_0C85, 0x0011_00A6, 0x0012_0868, 0x0013_02C8, 0x0014_0869, 0x0015_02C9, 0x0016_5CCA, 0x0017_1D54,
14
+ 0x0018_0D59, 0x0019_1CC8, 0x001A_06C5, 0x001B_0768, 0x001C_06CA, 0x001D_0766, 0x001E_01A5, 0x001F_0385,
15
+ 0x0020_0F09, 0x0021_0248, 0x0022_0326, 0x0023_0B24, 0x0024_0D55, 0x0025_1CC5, 0x0026_06C8, 0x0027_1868,
16
+ 0x0028_0369, 0x0029_16CA, 0x002A_0CC9, 0x002B_1D58, 0x002C_0784, 0x002D_060A, 0x002E_064A, 0x002F_0E2A,
17
+ 0x0030_032A, 0x0031_0B28, 0x0032_0688, 0x0033_0008, 0x0034_06C4, 0x0035_1864, 0x0036_01A8, 0x0037_0388,
18
+ 0x0038_078A, 0x0039_0604, 0x003A_0644, 0x003B_0E24, 0x003C_004A, 0x003D_18A4, 0x003E_1B24, 0x003F_00EA,
19
+ 0x0040_0F0A, 0x0041_0249, 0x0042_0D5D, 0x0043_1CC4, 0x0044_0328, 0x0045_0B29, 0x0046_06C6, 0x0047_076A,
20
+ 0x0048_0368, 0x0049_16C5, 0x004A_0789, 0x004B_0605, 0x004C_0CC8, 0x004D_1954, 0x004E_0645, 0x004F_0E25,
21
+ 0x0050_0325, 0x0051_0B26, 0x0052_06C9, 0x0053_0764, 0x0054_08A9, 0x0055_0009, 0x0056_01A9, 0x0057_0389,
22
+ 0x0058_0785, 0x0059_0609, 0x005A_0049, 0x005B_18A9, 0x005C_0649, 0x005D_0E29, 0x005E_1B29, 0x005F_00E9,
23
+ 0x0060_0365, 0x0061_16C6, 0x0062_0786, 0x0063_0608, 0x0064_0788, 0x0065_0606, 0x0066_0046, 0x0067_18A8,
24
+ 0x0068_58A6, 0x0069_0145, 0x006A_01E9, 0x006B_178A, 0x006C_01E8, 0x006D_1785, 0x006E_1E28, 0x006F_0C65,
25
+ 0x0070_0CC5, 0x0071_1D5C, 0x0072_0648, 0x0073_0E28, 0x0074_0646, 0x0075_0E26, 0x0076_1B28, 0x0077_00E6,
26
+ 0x0078_01E5, 0x0079_1786, 0x007A_1E29, 0x007B_0C68, 0x007C_1E24, 0x007D_0C69, 0x007E_0955, 0x007F_03C9,
27
+ 0x0080_03E9, 0x0081_0975, 0x0082_0C49, 0x0083_1E04, 0x0084_0C48, 0x0085_1E05, 0x0086_17A6, 0x0087_01C5,
28
+ 0x0088_00C6, 0x0089_1B08, 0x008A_0E06, 0x008B_0666, 0x008C_0E08, 0x008D_0668, 0x008E_1D7C, 0x008F_0CE5,
29
+ 0x0090_0C45, 0x0091_1E08, 0x0092_17A9, 0x0093_01C4, 0x0094_17AA, 0x0095_01C9, 0x0096_0169, 0x0097_588A,
30
+ 0x0098_1888, 0x0099_0066, 0x009A_0709, 0x009B_07A8, 0x009C_0704, 0x009D_07A6, 0x009E_16E6, 0x009F_0345,
31
+ 0x00A0_00C9, 0x00A1_1B05, 0x00A2_0E09, 0x00A3_0669, 0x00A4_1885, 0x00A5_0065, 0x00A6_0706, 0x00A7_07A5,
32
+ 0x00A8_03A9, 0x00A9_0189, 0x00AA_0029, 0x00AB_0889, 0x00AC_0744, 0x00AD_06E9, 0x00AE_0B06, 0x00AF_0229,
33
+ 0x00B0_0E05, 0x00B1_0665, 0x00B2_1974, 0x00B3_0CE8, 0x00B4_070A, 0x00B5_07A9, 0x00B6_16E9, 0x00B7_0348,
34
+ 0x00B8_074A, 0x00B9_06E6, 0x00BA_0B09, 0x00BB_0226, 0x00BC_1CE4, 0x00BD_0D7D, 0x00BE_0269, 0x00BF_08C9,
35
+ 0x00C0_00CA, 0x00C1_1B04, 0x00C2_1884, 0x00C3_006A, 0x00C4_0E04, 0x00C5_0664, 0x00C6_0708, 0x00C7_07AA,
36
+ 0x00C8_03A8, 0x00C9_0184, 0x00CA_0749, 0x00CB_06E4, 0x00CC_0020, 0x00CD_0888, 0x00CE_0B08, 0x00CF_0224,
37
+ 0x00D0_0E0A, 0x00D1_066A, 0x00D2_0705, 0x00D3_07A4, 0x00D4_1D78, 0x00D5_0CE9, 0x00D6_16EA, 0x00D7_0349,
38
+ 0x00D8_0745, 0x00D9_06E8, 0x00DA_1CE9, 0x00DB_0D75, 0x00DC_0B04, 0x00DD_0228, 0x00DE_0268, 0x00DF_08C8,
39
+ 0x00E0_03A5, 0x00E1_0185, 0x00E2_0746, 0x00E3_06EA, 0x00E4_0748, 0x00E5_06E5, 0x00E6_1CE8, 0x00E7_0D79,
40
+ 0x00E8_1D74, 0x00E9_5CE6, 0x00EA_02E9, 0x00EB_0849, 0x00EC_02E8, 0x00ED_0848, 0x00EE_0086, 0x00EF_0A08,
41
+ 0x00F0_0021, 0x00F1_0885, 0x00F2_0B05, 0x00F3_022A, 0x00F4_0B0A, 0x00F5_0225, 0x00F6_0265, 0x00F7_08C5,
42
+ 0x00F8_02E5, 0x00F9_0845, 0x00FA_0089, 0x00FB_0A09, 0x00FC_008A, 0x00FD_0A0A, 0x00FE_02A9, 0x00FF_0062
43
+ ].freeze
44
+ BY_INDEX = VALUES.to_h { |value| [(value >> 16) & 0xFF, value] }.freeze
45
+
46
+ module_function
47
+
48
+ # The specified value for this operation index.
49
+ def expected(value) = BY_INDEX[(value >> 16) & 0xFF]
50
+ end
51
+ end
52
+ end