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