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
@@ -5,8 +5,153 @@ module Emf
5
5
  module Parser
6
6
  module_function
7
7
 
8
- def call(_bytes)
9
- raise Error, "WMF parser not yet implemented — see TODO.impl/07-wmf-wire-domain-parser.md"
8
+ def call(bytes)
9
+ ResourceLimits.check!(:input_bytes, Binary::MAX_INPUT_BYTES, bytes.bytesize)
10
+
11
+ errors = []
12
+ placeable, start = read_placeable(bytes, errors)
13
+ header = read_header(bytes, start, placeable)
14
+ records = read_records(bytes, start + Binary::HEADER_BYTES, bytes.bytesize, errors)
15
+ Binary.validate_objects(records, header.number_of_objects,
16
+ recovered: Evidence.reference_recovery(bytes, errors)) do |record, message|
17
+ parse_error(record.offset, record.type_id, message)
18
+ end
19
+ # The walk is bounded by the real input and ends at the first EOF record, so a header Size or MaxRecord that
20
+ # disagrees with the records is recovered, like the shapes of Recovery and the object references that
21
+ # name nothing usable: the model is kept and each departure is recorded in errors.
22
+ stop = records.last.offset + (records.last.declared_size_words * 2)
23
+ Model::Metafile.new(format: :wmf, header: header, records: records,
24
+ errors: Evidence.ordered(errors + Evidence.drift_errors(bytes)),
25
+ trailing: bytes.byteslice(stop..).to_s)
26
+ end
27
+
28
+ def read_placeable(bytes, errors)
29
+ return [nil, 0] unless bytes.bytesize >= 4 && bytes.unpack1("V") == Binary::PLACEABLE_KEY
30
+ raise FormatError, "truncated WMF placeable header" if bytes.bytesize < Binary::PLACEABLE_BYTES
31
+
32
+ _key, handle, left, top, right, bottom, inch, reserved, checksum = bytes.unpack("Vvs<4vVv")
33
+ raise FormatError, "WMF placeable reserved field must be zero" unless reserved.zero?
34
+
35
+ errors << Evidence.recovered_error(203, bytes, nil) unless checksum == Binary.checksum(bytes.byteslice(0, 20))
36
+
37
+ header = Model::Wmf::PlaceableHeader.new(handle: handle, left: left, top: top, right: right, bottom: bottom,
38
+ inch: inch, checksum: checksum)
39
+ [header, Binary::PLACEABLE_BYTES]
40
+ end
41
+
42
+ def read_header(bytes, start, placeable)
43
+ raise FormatError, "truncated WMF standard header" if bytes.bytesize - start < Binary::HEADER_BYTES
44
+
45
+ type, header_size, version, size, objects, max_record, members = bytes.byteslice(start, Binary::HEADER_BYTES).unpack("v3VvVv")
46
+ raise FormatError, "invalid WMF Type" unless [1, 2].include?(type)
47
+ raise FormatError, "invalid WMF HeaderSize" unless header_size == 9
48
+ raise FormatError, "invalid WMF Version" unless [0x100, 0x300].include?(version)
49
+ raise FormatError, "WMF on-disk placeable handle must be zero" if type == 2 && placeable && !placeable.handle.zero?
50
+
51
+ Model::Wmf::Header.new(type: type, version: version, size_words: size, number_of_objects: objects,
52
+ max_record_words: max_record, number_of_members: members, placeable: placeable)
53
+ end
54
+
55
+ def read_records(bytes, offset, limit, errors)
56
+ records = []
57
+ while offset < limit
58
+ ResourceLimits.check!(:record_count, Binary::MAX_RECORDS, records.length + 1) do
59
+ Evidence.build(401, bytes, offset)
60
+ end
61
+ parse_error(offset, nil, "truncated record header") if limit - offset < 6
62
+ words, function = bytes.byteslice(offset, 6).unpack("Vv")
63
+ parse_error(offset, function, "invalid record length #{words}") if words < 3 || words > (limit - offset) / 2
64
+ raw = bytes.byteslice(offset, words * 2)
65
+ record, reason = Recovery.record(raw, function, offset)
66
+ errors << Evidence.recovered_error(reason, bytes, offset, function) if reason
67
+ record = (record || decode_record(raw, function, offset)).declare_size(words)
68
+ records << record
69
+ offset += raw.bytesize
70
+ return records if record.is_a?(Model::Wmf::Records::Eof)
71
+ end
72
+ parse_error(offset, nil, "missing EOF record")
73
+ end
74
+
75
+ def decode_record(raw, function, offset)
76
+ return Model::Wmf::Records::Eof.new(offset: offset) if function.zero?
77
+
78
+ return decode_prefix_record(raw, function, offset) if Binary::PREFIX_FUNCTIONS.include?(function & 0xFF)
79
+ return decode_object(raw, function, offset) if Binary::OBJECT_RECORDS.key?(function & 0xFF)
80
+ if Binary::TEXT_FUNCTIONS.include?(function & 0xFF)
81
+ return TextCodec.decode(raw, function, offset) { |message| parse_error(offset, function, message) }
82
+ end
83
+ return decode_point_array(raw, function, offset) if Binary::POINT_ARRAY_RECORDS.key?(function & 0xFF)
84
+
85
+ record_class = Binary::FIXED_RECORDS[function & 0xFF]
86
+ return Model::Wmf::Records::UnknownRecord.new(type_id: function, raw_bytes: raw, offset: offset) unless record_class
87
+
88
+ fields = record_class::WIRE_FIELDS
89
+ parse_error(offset, function, "invalid fixed record length") unless raw.bytesize == 6 + (fields.length * 2)
90
+ values = raw.byteslice(6..).unpack("s<*")
91
+ record_class.new(**fields.zip(values).to_h, type_id: function, offset: offset)
92
+ end
93
+
94
+ def decode_prefix_record(raw, function, offset)
95
+ return decode_font(raw, function, offset) if function & 0xFF == 0xFB
96
+ return decode_state(raw, function, offset) if Binary::STATE_RECORDS.key?(function & 0xFF)
97
+
98
+ codec = [PaletteCodec, RegionCodec, BrushCodec, BitmapCodec, GeometryCodec, DcEscapeCodec]
99
+ .find { |candidate| candidate::RECORDS.key?(function & 0xFF) }
100
+ codec.decode(raw, function, offset) { |message| parse_error(offset, function, message) }
101
+ end
102
+
103
+ def decode_state(raw, function, offset)
104
+ record_class = Binary::STATE_RECORDS.fetch(function & 0xFF)
105
+ prefix_size = 6 + record_class::PAYLOAD_BYTES
106
+ parse_error(offset, function, "invalid state record length") if raw.bytesize < prefix_size
107
+ values = raw.byteslice(6, record_class::PAYLOAD_BYTES).unpack(record_class::WIRE_FORMAT)
108
+ fields = record_class::WIRE_FIELDS.zip(values).to_h
109
+ record_class.new(**fields, type_id: function, offset: offset, suffix: raw.byteslice(prefix_size..))
110
+ end
111
+
112
+ def decode_font(raw, function, offset)
113
+ record_class = Model::Wmf::Records::CreateFontIndirect
114
+ prefix_size = 6 + record_class::PAYLOAD_BYTES
115
+ parse_error(offset, function, "invalid font record length") if raw.bytesize < prefix_size
116
+ values = raw.byteslice(6, record_class::PAYLOAD_BYTES).unpack(record_class::WIRE_FORMAT)
117
+ fields = record_class::WIRE_FIELDS.zip(values).to_h
118
+ record_class.new(**fields, type_id: function, offset: offset, suffix: raw.byteslice(prefix_size..))
119
+ end
120
+
121
+ def decode_object(raw, function, offset)
122
+ record_class = Binary::OBJECT_RECORDS.fetch(function & 0xFF)
123
+ parse_error(offset, function, "invalid object record length") unless raw.bytesize == 6 + record_class::PAYLOAD_BYTES
124
+ values = raw.byteslice(6..).unpack(record_class::WIRE_FORMAT)
125
+ record_class.new(**record_class::WIRE_FIELDS.zip(values).to_h, type_id: function, offset: offset)
126
+ end
127
+
128
+ def decode_point_array(raw, function, offset)
129
+ parse_error(offset, function, "missing point array count") if raw.bytesize < 8
130
+ record_class = Binary::POINT_ARRAY_RECORDS.fetch(function & 0xFF)
131
+ if record_class == Model::Wmf::Records::PolyPolygon
132
+ polygon_count = raw.byteslice(6, 2).unpack1("v")
133
+ points_start = 8 + (polygon_count * 2)
134
+ parse_error(offset, function, "truncated polygon counts") if raw.bytesize < points_start
135
+ counts = raw.byteslice(8, polygon_count * 2).unpack("v*")
136
+ point_count = counts.sum
137
+ else
138
+ point_count = raw.byteslice(6, 2).unpack1("s<")
139
+ minimum = record_class == Model::Wmf::Records::Polygon ? 2 : 0
140
+ parse_error(offset, function, "invalid point count") if point_count < minimum
141
+ points_start = 8
142
+ end
143
+ parse_error(offset, function, "point array length mismatch") unless raw.bytesize == points_start + (point_count * 4)
144
+
145
+ points = raw.byteslice(points_start..).unpack("s<*").each_slice(2).map do |x, y|
146
+ Model::Wmf::Records::PointS.new(x: x, y: y)
147
+ end
148
+ fields = { points: points, type_id: function, offset: offset }
149
+ fields[:points_per_polygon] = counts if counts
150
+ record_class.new(**fields)
151
+ end
152
+
153
+ def parse_error(offset, function, message)
154
+ raise ParseError.new(offset: offset, record_code: function, message: "WMF #{message} at offset #{offset}")
10
155
  end
11
156
  end
12
157
  end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ # Record shapes the specification rules out but real producers write and readers (LibreOffice) import. Each is
6
+ # framed by its own declared RecordSize inside the input, so the record is kept and the departure is recorded in
7
+ # Metafile#errors instead of raising. Both backends call .record on the same raw record bytes, so the models
8
+ # are identical. Anything not listed here, and any shape that cannot be framed safely, is left to the strict
9
+ # decoders, which reject it.
10
+ #
11
+ # 406 an EOF record longer than three words: it still ends the stream; the surplus is ignored
12
+ # 801 an object record (pen, brush, select, delete) with bytes after its payload: the payload is read, the
13
+ # surplus is ignored; a shorter record is still rejected
14
+ # 701 a font record shorter than 32 face-name bytes but holding the 18 fixed bytes: the face name is
15
+ # zero-padded; a record without the fixed bytes is still rejected
16
+ # 1003 a text record with a negative StringLength: no string can be framed, so the record is kept verbatim
17
+ # as an UnknownRecord; a truncated text record or rectangle is still rejected first
18
+ # 903 a META_POLYGON with NumberOfPoints 0 and no point bytes (PowerPoint, seen in Apache POI's
19
+ # empty-polygon-close.wmf): kept as a Polygon without points; 1 and negative counts are still rejected
20
+ # 1203 a META_CREATEREGION whose ObjectCount is two bytes wide (libUEMF, the library Inkscape writes WMF with,
21
+ # declares it uint16, so every later field sits two bytes early) and whose RegionSize, read there, is exactly
22
+ # the body: the region is kept verbatim as an UnknownRecord (it still takes its object-table slot, from the
23
+ # low byte of its function); any other invalid header is still rejected
24
+ module Recovery
25
+ EOF_BYTES = 6
26
+ FONT_FIXED_BYTES = 18
27
+ # function => [offset of StringLength, shortest record]
28
+ TEXT_COUNTS = { 0x21 => [6, 12], 0x32 => [10, 14] }.freeze
29
+ CLIP_OR_OPAQUE = 6
30
+ RECTANGLE_BYTES = 8
31
+ POLYGON = 0x24
32
+ REGION = 0xFF
33
+ POLYGON_EMPTY_BYTES = 8
34
+
35
+ module_function
36
+
37
+ # [record, reason] when `raw` (one whole record) is a recoverable shape, otherwise nil.
38
+ def record(raw, function, offset)
39
+ low = function & 0xFF
40
+ return [Model::Wmf::Records::Eof.new(offset: offset), 406] if function.zero? && raw.bytesize > EOF_BYTES
41
+ return object(raw, function, offset) if Binary::OBJECT_RECORDS.key?(low)
42
+ return font(raw, function, offset) if low == 0xFB
43
+ return polygon(raw, function, offset) if low == POLYGON
44
+ return region(raw, function, offset) if low == REGION
45
+
46
+ text(raw, function, offset) if TEXT_COUNTS.key?(low)
47
+ end
48
+
49
+ def object(raw, function, offset)
50
+ klass = Binary::OBJECT_RECORDS.fetch(function & 0xFF)
51
+ return unless raw.bytesize > 6 + klass::PAYLOAD_BYTES
52
+
53
+ values = raw.byteslice(6, klass::PAYLOAD_BYTES).unpack(klass::WIRE_FORMAT)
54
+ [klass.new(**klass::WIRE_FIELDS.zip(values).to_h, type_id: function, offset: offset), 801]
55
+ end
56
+
57
+ def font(raw, function, offset)
58
+ klass = Model::Wmf::Records::CreateFontIndirect
59
+ return unless raw.bytesize.between?(6 + FONT_FIXED_BYTES, 6 + klass::PAYLOAD_BYTES - 1)
60
+
61
+ values = raw.byteslice(6..).ljust(klass::PAYLOAD_BYTES, "\0".b).unpack(klass::WIRE_FORMAT)
62
+ [klass.new(**klass::WIRE_FIELDS.zip(values).to_h, type_id: function, offset: offset), 701]
63
+ end
64
+
65
+ def polygon(raw, function, offset)
66
+ return unless raw.bytesize == POLYGON_EMPTY_BYTES && raw.byteslice(6, 2).unpack1("s<").zero?
67
+
68
+ [Model::Wmf::Records::Polygon.new(points: [], type_id: function, offset: offset), 903]
69
+ end
70
+
71
+ def region(raw, function, offset)
72
+ body = raw.byteslice(6..)
73
+ return unless body.bytesize >= RegionCodec::HEADER_BYTES && RegionCodec.header_invalid?(body)
74
+
75
+ kind, size, count = body.unpack("@2v@6s<@8s<")
76
+ return unless kind == RegionCodec::KIND && size == body.bytesize && !count.negative?
77
+
78
+ [Model::Wmf::Records::UnknownRecord.new(type_id: function, raw_bytes: raw, offset: offset), 1203]
79
+ end
80
+
81
+ def text(raw, function, offset)
82
+ count_at, minimum = TEXT_COUNTS.fetch(function & 0xFF)
83
+ return if raw.bytesize < minimum
84
+ return if function & 0xFF == 0x32 && rectangle_truncated?(raw, minimum)
85
+ return unless raw.byteslice(count_at, 2).unpack1("s<").negative?
86
+
87
+ [Model::Wmf::Records::UnknownRecord.new(type_id: function, raw_bytes: raw, offset: offset), 1003]
88
+ end
89
+
90
+ def rectangle_truncated?(raw, minimum)
91
+ raw.byteslice(12, 2).unpack1("v").anybits?(CLIP_OR_OPAQUE) && raw.bytesize < minimum + RECTANGLE_BYTES
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,151 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ module RegionCodec
6
+ RECORDS = {
7
+ 0xFF => Model::Wmf::Records::CreateRegion,
8
+ 0x28 => Model::Wmf::Records::FillRegion,
9
+ 0x29 => Model::Wmf::Records::FrameRegion,
10
+ 0x2A => Model::Wmf::Records::InvertRegion,
11
+ 0x2B => Model::Wmf::Records::PaintRegion,
12
+ 0x2C => Model::Wmf::Records::SelectClipRegion,
13
+ 0x20 => Model::Wmf::Records::OffsetClipRgn,
14
+ 0x15 => Model::Wmf::Records::ExcludeClipRect,
15
+ 0x16 => Model::Wmf::Records::IntersectClipRect
16
+ }.freeze
17
+ FUNCTIONS = RECORDS.invert.freeze
18
+ HEADER_BYTES = 22
19
+ KIND = 6
20
+ BOUNDS_FIELDS = %i[left top right bottom].freeze
21
+
22
+ module_function
23
+
24
+ def decode(raw, function, offset, &)
25
+ klass = RECORDS.fetch(function & 0xFF)
26
+ if klass == Model::Wmf::Records::CreateRegion
27
+ fields, size = decode_region(raw.byteslice(6..), &)
28
+ else
29
+ size = klass::WIRE_FIELDS.length * 2
30
+ yield "truncated region/clipping prefix" if raw.bytesize < 6 + size
31
+ fields = klass::WIRE_FIELDS.zip(raw.byteslice(6, size).unpack(klass::WIRE_FORMAT)).to_h
32
+ end
33
+ klass.new(**fields, type_id: function, offset: offset, suffix: raw.byteslice((6 + size)..))
34
+ end
35
+
36
+ def decode_region(body, &)
37
+ yield "truncated region header" if body.bytesize < HEADER_BYTES
38
+ next_in_chain, _kind, object_count, size, count, max_scan, *bounds = body.unpack("v2Vs<7")
39
+ yield "invalid region header" if header_invalid?(body)
40
+ preflight_scans(body, size, count, max_scan, &)
41
+ cursor = 22
42
+ scans = Array.new(count) do
43
+ points, top, bottom = body.byteslice(cursor, 6).unpack("v3")
44
+ lines = body.byteslice(cursor + 6, points * 2).unpack("v*").each_slice(2).map do |left, right|
45
+ Model::Wmf::Records::RegionScanLine.new(left: left, right: right)
46
+ end
47
+ cursor += 8 + (points * 2)
48
+ Model::Wmf::Records::RegionScan.new(top: top, bottom: bottom, lines: lines)
49
+ end
50
+ [{ next_in_chain: next_in_chain, object_count: object_count,
51
+ bounds: Model::Wmf::Records::RegionBounds.new(**BOUNDS_FIELDS.zip(bounds).to_h), scans: scans }, size]
52
+ end
53
+
54
+ def header_fields(body)
55
+ body.unpack("@2v@8s<s<")
56
+ end
57
+
58
+ # The spec's header checks, shared with Wmf::Recovery so a recovered shape is exactly one this rejects.
59
+ def header_invalid?(body)
60
+ kind, size, count = header_fields(body)
61
+ max_scan = body.unpack1("@12s<")
62
+ kind != KIND || size < HEADER_BYTES || size > body.bytesize || count.negative? || max_scan.negative? ||
63
+ count > (size - HEADER_BYTES) / 8
64
+ end
65
+
66
+ def preflight_scans(body, size, count, max_scan)
67
+ cursor = 22
68
+ observed_max = 0
69
+ count.times do
70
+ yield "truncated region scan" if size - cursor < 8
71
+ points = body.byteslice(cursor, 2).unpack1("v")
72
+ scan_size = 8 + (points * 2)
73
+ if points.odd? || scan_size > size - cursor || body.byteslice(cursor + scan_size - 2, 2).unpack1("v") != points
74
+ yield "invalid region scan"
75
+ end
76
+ observed_max = [observed_max, points].max
77
+ cursor += scan_size
78
+ end
79
+ yield "invalid region scan size or maximum" unless cursor == size && observed_max == max_scan
80
+ end
81
+
82
+ def byte_size(record)
83
+ size = if record.is_a?(Model::Wmf::Records::CreateRegion)
84
+ 22 + record.scans.sum { |scan| 8 + (scan.lines.length * 4) }
85
+ else
86
+ record.class::WIRE_FIELDS.length * 2
87
+ end
88
+ 6 + size + record.suffix.bytesize
89
+ end
90
+
91
+ def encode(record)
92
+ function = Binary.uint16(record.type_id, "RecordFunction")
93
+ raise SerializeError, "WMF region RecordFunction mismatch" unless function & 0xFF == FUNCTIONS[record.class]
94
+ unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
95
+ raise SerializeError, "WMF region suffix must be a String with even length"
96
+ end
97
+
98
+ payload = encode_payload(record) + record.suffix.b
99
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
100
+ end
101
+
102
+ def encode_payload(record)
103
+ return encode_region(record) if record.is_a?(Model::Wmf::Records::CreateRegion)
104
+
105
+ values = record.class::WIRE_FIELDS.zip(record.class::WIRE_TYPES).map do |field, type|
106
+ Binary.public_send(type, record.public_send(field), field)
107
+ end
108
+ values.pack(record.class::WIRE_FORMAT)
109
+ end
110
+
111
+ def encode_region(record)
112
+ unless record.bounds.is_a?(Model::Wmf::Records::RegionBounds) && record.scans.is_a?(Array)
113
+ raise SerializeError, "WMF region requires RegionBounds and an array of scans"
114
+ end
115
+
116
+ count = Binary.integer(record.scans.length, 0..0x7FFF, "scan count")
117
+ size, max_scan = region_layout(record.scans)
118
+ bounds = BOUNDS_FIELDS.map { |field| Binary.int16(record.bounds.public_send(field), field) }
119
+ header = [Binary.uint16(record.next_in_chain, "next_in_chain"), 6, Binary.uint32(record.object_count, "object_count"),
120
+ size, count, max_scan, *bounds].pack("v2Vs<7")
121
+ header + record.scans.map { |scan| encode_scan(scan) }.join
122
+ end
123
+
124
+ def region_layout(scans)
125
+ size = 22
126
+ max_scan = 0
127
+ scans.each do |scan|
128
+ unless scan.is_a?(Model::Wmf::Records::RegionScan) && scan.lines.is_a?(Array)
129
+ raise SerializeError, "WMF region scan requires RegionScan with an array of lines"
130
+ end
131
+
132
+ points = Binary.integer(scan.lines.length * 2, 0..0x7FFF, "scan point count")
133
+ size = Binary.integer(size + 8 + (points * 2), 22..0x7FFF, "region size")
134
+ max_scan = [max_scan, points].max
135
+ end
136
+ [size, max_scan]
137
+ end
138
+
139
+ def encode_scan(scan)
140
+ points = scan.lines.length * 2
141
+ prefix = [points, Binary.uint16(scan.top, "scan top"), Binary.uint16(scan.bottom, "scan bottom")].pack("v3")
142
+ lines = scan.lines.map do |line|
143
+ raise SerializeError, "WMF region line must be a RegionScanLine" unless line.is_a?(Model::Wmf::Records::RegionScanLine)
144
+
145
+ [Binary.uint16(line.left, "scan left"), Binary.uint16(line.right, "scan right")].pack("v2")
146
+ end
147
+ prefix + lines.join + [points].pack("v")
148
+ end
149
+ end
150
+ end
151
+ end
@@ -5,8 +5,170 @@ module Emf
5
5
  module Serializer
6
6
  module_function
7
7
 
8
- def call(_metafile)
9
- raise SerializeError, "WMF serializer not yet implemented — see TODO.impl/07-wmf-wire-domain-parser.md"
8
+ def call(metafile)
9
+ raise SerializeError, "expected a WMF metafile" unless metafile.format == :wmf && metafile.header.is_a?(Model::Wmf::Header)
10
+ raise SerializeError, "WMF record limit exceeded" if metafile.records.length > Binary::MAX_RECORDS
11
+
12
+ validate_eof(metafile.records)
13
+ records = metafile.records.map { |record| serialize_record(record) }
14
+ capacity = Binary.uint16(metafile.header.number_of_objects, "NumberOfObjects")
15
+ Binary.validate_objects(metafile.records, capacity) do |_record, message|
16
+ raise SerializeError, "WMF #{message}"
17
+ end
18
+ size = 9 + records.sum { |bytes| bytes.bytesize / 2 }
19
+ raise SerializeError, "WMF input too large" if (size * 2) + metafile.trailing.bytesize > Binary::MAX_INPUT_BYTES
20
+
21
+ header = serialize_header(metafile.header, size, records.map { |bytes| bytes.bytesize / 2 }.max)
22
+ placeable = serialize_placeable(metafile.header)
23
+ bytes = placeable + header + records.join + metafile.trailing
24
+ raise SerializeError, "WMF input too large" if bytes.bytesize > Binary::MAX_INPUT_BYTES
25
+
26
+ bytes
27
+ end
28
+
29
+ def validate_eof(records)
30
+ return if records.last.is_a?(Model::Wmf::Records::Eof) && records.one?(Model::Wmf::Records::Eof)
31
+
32
+ raise SerializeError, "WMF requires one final EOF record"
33
+ end
34
+
35
+ def serialize_header(header, size, max_record)
36
+ raise SerializeError, "invalid WMF Type" unless [1, 2].include?(header.type)
37
+ raise SerializeError, "invalid WMF Version" unless [0x100, 0x300].include?(header.version)
38
+
39
+ [header.type, 9, header.version, size, Binary.uint16(header.number_of_objects, "NumberOfObjects"),
40
+ max_record, Binary.uint16(header.number_of_members, "NumberOfMembers")].pack("v3VvVv")
41
+ end
42
+
43
+ def serialize_placeable(header)
44
+ placeable = header.placeable
45
+ return "".b unless placeable
46
+ raise SerializeError, "WMF on-disk placeable handle must be zero" if header.type == 2 && !placeable.handle.zero?
47
+
48
+ bytes = [Binary::PLACEABLE_KEY, Binary.uint16(placeable.handle, "handle"),
49
+ Binary.int16(placeable.left, "left"), Binary.int16(placeable.top, "top"),
50
+ Binary.int16(placeable.right, "right"), Binary.int16(placeable.bottom, "bottom"),
51
+ Binary.uint16(placeable.inch, "inch"), 0].pack("Vvs<4vV")
52
+ bytes + [Binary.checksum(bytes)].pack("v")
53
+ end
54
+
55
+ def serialize_record(record)
56
+ codec = Binary.prefix_codec(record)
57
+ return codec.encode(record) if codec
58
+
59
+ case record
60
+ when Model::Wmf::Records::Eof then [3, 0].pack("Vv")
61
+ when Model::Wmf::Records::PointRecord, Model::Wmf::Records::RectangleRecord, Model::Wmf::Records::OffsetRecord
62
+ serialize_fixed(record)
63
+ when Model::Wmf::Records::StateRecord then serialize_state(record)
64
+ when Model::Wmf::Records::CreateFontIndirect then serialize_font(record)
65
+ when Model::Wmf::Records::ObjectRecord then serialize_object(record)
66
+ when Model::Wmf::Records::TextOut then TextCodec.encode(record)
67
+ when Model::Wmf::Records::PointArrayRecord then serialize_point_array(record)
68
+ when Model::Wmf::Records::UnknownRecord then serialize_unknown(record)
69
+ else raise SerializeError, "unsupported WMF record #{record.class}"
70
+ end
71
+ end
72
+
73
+ def serialize_fixed(record)
74
+ function = Binary.uint16(record.type_id, "RecordFunction")
75
+ expected = Binary::FIXED_FUNCTIONS[record.class]
76
+ raise SerializeError, "WMF fixed RecordFunction mismatch" unless function & 0xFF == expected
77
+
78
+ values = record.class::WIRE_FIELDS.map { |field| Binary.int16(record.public_send(field), field) }
79
+ [3 + values.length, function, *values].pack("Vvs<*")
80
+ end
81
+
82
+ def serialize_state(record)
83
+ function = Binary.uint16(record.type_id, "RecordFunction")
84
+ expected = Binary::STATE_FUNCTIONS[record.class]
85
+ raise SerializeError, "WMF state RecordFunction mismatch" unless function & 0xFF == expected
86
+ unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
87
+ raise SerializeError, "WMF state suffix must be a String with even length"
88
+ end
89
+
90
+ values = record.class::WIRE_FIELDS.zip(record.class::WIRE_TYPES).map do |field, type|
91
+ Binary.public_send(type, record.public_send(field), field)
92
+ end
93
+ payload = values.pack(record.class::WIRE_FORMAT) + record.suffix.b
94
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
95
+ end
96
+
97
+ def serialize_font(record)
98
+ function = Binary.uint16(record.type_id, "RecordFunction")
99
+ raise SerializeError, "WMF font RecordFunction mismatch" unless function & 0xFF == 0xFB
100
+ unless record.face_name.is_a?(String) && record.face_name.bytesize == 32
101
+ raise SerializeError, "WMF font face_name must be a String with 32 bytes"
102
+ end
103
+ unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
104
+ raise SerializeError, "WMF font suffix must be a String with even length"
105
+ end
106
+
107
+ values = record.class::SIGNED_FIELDS.map { |field| Binary.int16(record.public_send(field), field) }
108
+ values += record.class::BYTE_FIELDS.map { |field| Binary.uint8(record.public_send(field), field) }
109
+ payload = [*values, record.face_name.b].pack(record.class::WIRE_FORMAT) + record.suffix.b
110
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
111
+ end
112
+
113
+ def serialize_object(record)
114
+ function = Binary.uint16(record.type_id, "RecordFunction")
115
+ expected = Binary::OBJECT_FUNCTIONS[record.class]
116
+ raise SerializeError, "WMF object RecordFunction mismatch" unless function & 0xFF == expected
117
+
118
+ values = record.class::WIRE_FIELDS.zip(record.class::WIRE_TYPES).map do |field, type|
119
+ Binary.public_send(type, record.public_send(field), field)
120
+ end
121
+ payload = values.pack(record.class::WIRE_FORMAT)
122
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
123
+ end
124
+
125
+ def serialize_point_array(record)
126
+ function = Binary.uint16(record.type_id, "RecordFunction")
127
+ expected = Binary::POINT_ARRAY_FUNCTIONS[record.class]
128
+ raise SerializeError, "WMF point array RecordFunction mismatch" unless function & 0xFF == expected
129
+ raise SerializeError, "WMF points must be an array" unless record.points.is_a?(Array)
130
+
131
+ counts = serialize_point_counts(record)
132
+ coordinates = record.points.flat_map do |point|
133
+ raise SerializeError, "WMF point must be a PointS" unless point.is_a?(Model::Wmf::Records::PointS)
134
+
135
+ [Binary.int16(point.x, "x"), Binary.int16(point.y, "y")]
136
+ end
137
+ payload = counts + coordinates.pack("s<*")
138
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
139
+ end
140
+
141
+ def serialize_point_counts(record)
142
+ unless record.is_a?(Model::Wmf::Records::PolyPolygon)
143
+ count = Binary.int16(record.points.length, "NumberOfPoints")
144
+ raise SerializeError, "WMF Polygon requires at least two points" if record.is_a?(Model::Wmf::Records::Polygon) && count < 2
145
+
146
+ return [count].pack("s<")
147
+ end
148
+ counts = record.points_per_polygon
149
+ raise SerializeError, "WMF polygon counts must be an array" unless counts.is_a?(Array)
150
+
151
+ polygon_count = Binary.uint16(counts.length, "NumberOfPolygons")
152
+ counts.each { |count| Binary.uint16(count, "PointsPerPolygon") }
153
+ raise SerializeError, "WMF polygon point count mismatch" unless counts.sum == record.points.length
154
+
155
+ [polygon_count, *counts].pack("v*")
156
+ end
157
+
158
+ # A text record with a negative StringLength, which the parser keeps verbatim (Recovery).
159
+ def kept_verbatim?(raw, function)
160
+ Recovery.record(raw, function, nil)&.last == 1003
161
+ end
162
+
163
+ def serialize_unknown(record)
164
+ raw = record.raw_bytes
165
+ words, function = raw.unpack("Vv")
166
+ unless raw.bytesize >= 6 && raw.bytesize.even? && words == raw.bytesize / 2 && function == record.type_id &&
167
+ !function.zero? && (!Binary::TYPED_FUNCTIONS.include?(function & 0xFF) || kept_verbatim?(raw, function))
168
+ raise SerializeError, "invalid raw WMF record"
169
+ end
170
+
171
+ raw
10
172
  end
11
173
  end
12
174
  end