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,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ # META_ESCAPE_ENHANCED_METAFILE (Escape 0x000F): an EMF stream split across consecutive
6
+ # Escape records. Segment bytes stay inert; assembly concatenates them and does not
7
+ # parse the EMF (use Emf.parse on the result). Records are never modified.
8
+ #
9
+ # ByteCount is checked as 34 plus CurrentRecordSize. The literal MS-WMF text says
10
+ # "34 plus EnhancedMetafileDataSize", which cannot hold for a stream split into
11
+ # 8,192-byte segments; the two agree for a single segment.
12
+ module EmbeddedEmf
13
+ ESCAPE = 0x000F
14
+ METADATA_BYTES = 34
15
+ MAX_SEGMENT_BYTES = 8192
16
+ COMMENT_IDENTIFIER = 0x43464D57
17
+ COMMENT_TYPE = 1
18
+
19
+ # Raised for an invalid segment or sequence; reason is a Symbol such as
20
+ # :comment_identifier, :segment_too_large, :remaining_mismatch or :incomplete.
21
+ class Invalid < Emf::Error
22
+ def initialize(reason)
23
+ @reason = reason
24
+ super("invalid embedded EMF escape: #{reason}")
25
+ end
26
+
27
+ attr_reader :reason
28
+ end
29
+
30
+ Segment = Struct.new(:version, :checksum, :flags, :record_count, :current_size, :remaining_bytes, :total_size, :data,
31
+ keyword_init: true)
32
+ # checksum_valid is the one's complement of the XOR of the stream's 16-bit words
33
+ # compared with the first segment's Checksum; a mismatch is reported, not rejected.
34
+ Stream = Struct.new(:version, :checksum, :checksum_valid, :bytes, keyword_init: true)
35
+ # error is nil when the run assembled into stream.
36
+ Sequence = Struct.new(:first_record, :segment_count, :stream, :error, keyword_init: true)
37
+
38
+ module_function
39
+
40
+ def escape?(record)
41
+ record.is_a?(Model::Wmf::Records::Escape) && record.escape_function == ESCAPE
42
+ end
43
+
44
+ # Validates one segment against the record's own fields. Raises Invalid.
45
+ def segment(record)
46
+ raise Invalid, :not_embedded_emf unless escape?(record) && record.type_id & 0xFF == 0x26
47
+
48
+ data = record.data
49
+ raise Invalid, :transport unless record.padding.nil? != data.bytesize.odd? && data.bytesize <= 0xFFFF
50
+ raise Invalid, :metadata if data.bytesize < METADATA_BYTES
51
+
52
+ identifier, type, version, checksum, flags, count, current, remaining, total = data.unpack("V3vV5")
53
+ validate_metadata(identifier, type, flags, count, current, remaining, total)
54
+ raise Invalid, :metadata unless data.bytesize == METADATA_BYTES + current
55
+
56
+ Segment.new(version: version, checksum: checksum, flags: flags, record_count: count, current_size: current,
57
+ remaining_bytes: remaining, total_size: total, data: data.byteslice(METADATA_BYTES..))
58
+ end
59
+
60
+ def validate_metadata(identifier, type, flags, count, current, remaining, total)
61
+ raise Invalid, :comment_identifier unless identifier == COMMENT_IDENTIFIER
62
+ raise Invalid, :comment_type unless type == COMMENT_TYPE
63
+ raise Invalid, :non_zero_flags unless flags.zero?
64
+ raise Invalid, :zero_record_count if count.zero?
65
+ raise Invalid, :segment_too_large if current > MAX_SEGMENT_BYTES
66
+ raise Invalid, :inconsistent_sizes if current + remaining > total
67
+ end
68
+
69
+ def checksum(bytes)
70
+ bytes = bytes.b
71
+ bytes += "\0".b if bytes.bytesize.odd?
72
+ ~bytes.unpack("v*").reduce(0, :^) & 0xFFFF
73
+ end
74
+
75
+ # Assembles escapes that must form exactly one complete sequence, in order.
76
+ # Raises Invalid; max_bytes bounds the declared total before any copying.
77
+ def assemble(records, max_bytes: Wmf::Binary::MAX_INPUT_BYTES)
78
+ head = nil
79
+ bytes = "".b
80
+ seen = 0
81
+ complete = false
82
+ records.each do |record|
83
+ raise Invalid, :record_count_mismatch if complete
84
+
85
+ segment = segment(record)
86
+ head ||= segment
87
+ check_against(head, segment, bytes.bytesize, max_bytes)
88
+ bytes << segment.data
89
+ seen += 1
90
+ complete = segment.remaining_bytes.zero?
91
+ raise Invalid, :record_count_mismatch unless complete == (seen == head.record_count)
92
+ end
93
+ raise Invalid, :incomplete unless complete
94
+
95
+ Stream.new(version: head.version, checksum: head.checksum, checksum_valid: checksum(bytes) == head.checksum, bytes: bytes)
96
+ end
97
+
98
+ def check_against(head, segment, consumed, max_bytes)
99
+ same = segment.record_count == head.record_count && segment.total_size == head.total_size
100
+ raise Invalid, :sequence_mismatch unless same
101
+ raise Invalid, :limit_exceeded if head.total_size > max_bytes
102
+ raise Invalid, :remaining_mismatch unless head.total_size - consumed - segment.current_size == segment.remaining_bytes
103
+ end
104
+
105
+ # Groups consecutive 0x000F escapes in a record list into Sequences. A run ends when it
106
+ # completes, fails validation (the failing segment is included) or another record follows.
107
+ def sequences(records, max_bytes: Wmf::Binary::MAX_INPUT_BYTES)
108
+ result = []
109
+ index = 0
110
+ while index < records.length
111
+ (index += 1 and next) unless escape?(records[index])
112
+
113
+ stop, outcome = scan_run(records, index, max_bytes)
114
+ result << Sequence.new(first_record: index, segment_count: stop - index, stream: outcome.is_a?(Stream) ? outcome : nil,
115
+ error: outcome.is_a?(Stream) ? nil : outcome)
116
+ index = stop
117
+ end
118
+ result
119
+ end
120
+
121
+ def scan_run(records, first, max_bytes)
122
+ stop = first
123
+ outcome = :incomplete
124
+ while stop < records.length && escape?(records[stop])
125
+ stop += 1
126
+ outcome = begin
127
+ assemble(records[first...stop], max_bytes: max_bytes)
128
+ rescue Invalid => e
129
+ e.reason
130
+ end
131
+ break unless outcome == :incomplete
132
+ end
133
+ [stop, outcome]
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ # Read-only views of recognized Escape subtypes. Data stays inert: nothing is
6
+ # executed, interpreted or decoded, and the record's raw fields remain the source
7
+ # of truth, so serialization is unaffected by taking a view.
8
+ module EscapeViews
9
+ # kind is the subtype name; fields holds the raw numeric fields (empty for empty subtypes).
10
+ Payload = Struct.new(:kind, :fields, keyword_init: true)
11
+ # kind names a counted subtype; data is the exact counted bytes.
12
+ CountedPayload = Struct.new(:kind, :data, keyword_init: true)
13
+ SpecialPassthrough2 = Struct.new(:reserved, :data, keyword_init: true)
14
+
15
+ EMPTY = {
16
+ 0x0002 => :abort_doc, 0x1000 => :begin_path, 0x1010 => :close_channel, 0x0202 => :download_face,
17
+ 0x100F => :download_header, 0x1002 => :end_path, 0x000B => :end_doc, 0x0200 => :ext_text_out,
18
+ 0x002A => :get_device_units, 0x0100 => :get_extended_text_metrics, 0x0201 => :get_face_name,
19
+ 0x0102 => :get_pair_kern_table, 0x000C => :get_phys_page_size, 0x000D => :get_printing_offset,
20
+ 0x000E => :get_scaling_factor, 0x0801 => :metafile_driver, 0x0001 => :new_frame, 0x0003 => :next_band,
21
+ 0x0026 => :postscript_ignore, 0x100E => :open_channel, 0x0C01 => :query_dib_support
22
+ }.freeze
23
+
24
+ # [kind, byte length, unpack format, value names] keyed by escape function.
25
+ FIXED = {
26
+ 0x1001 => [:clip_to_path, 4, "v2", %i[clip_function reserved1]],
27
+ 0x0019 => [:draw_pattern_rect, 20, "l<4v2", %i[position_x position_y size_width size_height style pattern]],
28
+ 0x0021 => [:eps_printing, 2, "v", %i[set_eps_printing]],
29
+ 0x1019 => [:get_ps_feature_setting, 4, "l<", %i[feature]],
30
+ 0x0008 => [:query_esc_support, 2, "v", %i[query]],
31
+ 0x0011 => [:set_copy_count, 2, "v", %i[copy_count]],
32
+ 0x0015 => [:set_line_cap, 4, "l<", %i[cap]],
33
+ 0x0016 => [:set_line_join, 4, "l<", %i[join]],
34
+ 0x0017 => [:set_miter_limit, 4, "l<", %i[miter_limit]]
35
+ }.freeze
36
+
37
+ COUNTED = {
38
+ 0x0013 => :passthrough, 0x0025 => :postscript_data, 0x1013 => :postscript_passthrough, 0x000A => :start_doc,
39
+ 0x1017 => :check_jpeg_format, 0x1018 => :check_png_format, 0x0004 => :set_color_table
40
+ }.freeze
41
+ START_DOC_LIMIT = 260
42
+ SPCLPASSTHROUGH2 = 0x11D8
43
+
44
+ module_function
45
+
46
+ # Empty or fixed-size payload; nil for unknown subtypes, unexpected sizes or padding.
47
+ def typed_payload(record)
48
+ return unless dc_escape?(record) && record.padding.nil?
49
+
50
+ data = record.data
51
+ return (kind = EMPTY[record.escape_function]) && Payload.new(kind: kind, fields: {}) if data.empty?
52
+
53
+ kind, length, format, names = FIXED[record.escape_function]
54
+ return unless kind && data.bytesize == length
55
+
56
+ Payload.new(kind: kind, fields: names.zip(data.unpack(format)).to_h)
57
+ end
58
+
59
+ # The complete counted payload; nil for other subtypes, invalid alignment or oversized STARTDOC.
60
+ def typed_counted_payload(record)
61
+ return unless dc_escape?(record) && aligned?(record)
62
+
63
+ kind = COUNTED[record.escape_function] or return
64
+ return if kind == :start_doc && record.data.bytesize >= START_DOC_LIMIT
65
+
66
+ CountedPayload.new(kind: kind, data: record.data)
67
+ end
68
+
69
+ # SPCLPASSTHROUGH2 only when its inner size consumes the exact payload.
70
+ def typed_special_passthrough2_payload(record)
71
+ return unless dc_escape?(record) && record.escape_function == SPCLPASSTHROUGH2 && aligned?(record)
72
+ return unless record.data.bytesize >= 6
73
+
74
+ reserved, size = record.data.unpack("Vv")
75
+ return unless record.data.bytesize == 6 + size
76
+
77
+ SpecialPassthrough2.new(reserved: reserved, data: record.data.byteslice(6..))
78
+ end
79
+
80
+ def dc_escape?(record)
81
+ record.type_id & 0xFF == 0x26
82
+ end
83
+
84
+ def aligned?(record)
85
+ record.padding.nil? != record.data.bytesize.odd? && record.data.bytesize <= 0xFFFF
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,255 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ # Neutral evidence for a WMF input the parser rejected, and for the header drifts and record shapes it recovers from. Both
6
+ # backends choose the failing check (the Ruby parser by its own raise, the native backend by its
7
+ # first-rejection reason) and the record offset;
8
+ # the declared and actual values are then read from the input bytes the same way for both.
9
+ #
10
+ # REASONS maps the stable rejection reason (codec * 100 + n, never renumbered) to its check name and the
11
+ # message text the contract raises (`%d` stands for one number; record texts are wrapped as
12
+ # "WMF <text> at offset <offset>").
13
+ module Evidence
14
+ REASONS = {
15
+ 101 => [:input_too_large, "WMF input too large"],
16
+ 201 => [:placeable_truncated, "truncated WMF placeable header"],
17
+ 202 => [:placeable_reserved_nonzero, "WMF placeable reserved field must be zero"],
18
+ 203 => [:placeable_checksum_mismatch, "WMF placeable checksum mismatch"],
19
+ 301 => [:header_truncated, "truncated WMF standard header"],
20
+ 302 => [:header_type_invalid, "invalid WMF Type"],
21
+ 303 => [:header_size_invalid, "invalid WMF HeaderSize"],
22
+ 304 => [:header_version_invalid, "invalid WMF Version"],
23
+ 305 => [:header_file_size_invalid, "invalid WMF file Size"],
24
+ 306 => [:placeable_handle_nonzero, "WMF on-disk placeable handle must be zero"],
25
+ 1801 => [:max_record_mismatch, "WMF MaxRecord does not match largest record"],
26
+ 401 => [:record_limit_exceeded, "record limit exceeded"],
27
+ 402 => [:record_header_truncated, "truncated record header"],
28
+ 403 => [:record_length_invalid, "invalid record length %d"],
29
+ 405 => [:eof_missing, "missing EOF record"],
30
+ 406 => [:eof_length_invalid, "invalid EOF length"],
31
+ 501 => [:fixed_record_length_invalid, "invalid fixed record length"],
32
+ 601 => [:state_record_length_invalid, "invalid state record length"],
33
+ 701 => [:font_record_length_invalid, "invalid font record length"],
34
+ 801 => [:object_record_length_invalid, "invalid object record length"],
35
+ 901 => [:point_count_missing, "missing point array count"],
36
+ 902 => [:polygon_counts_truncated, "truncated polygon counts"],
37
+ 903 => [:point_count_invalid, "invalid point count"],
38
+ 904 => [:point_array_length_mismatch, "point array length mismatch"],
39
+ 1001 => [:text_record_truncated, "truncated text record"],
40
+ 1002 => [:text_rectangle_truncated, "truncated text rectangle"],
41
+ 1003 => [:text_length_negative, "negative text length"],
42
+ 1004 => [:text_string_truncated, "truncated text string"],
43
+ 1005 => [:text_spacing_truncated, "truncated text spacing array"],
44
+ 1101 => [:palette_prefix_truncated, "truncated palette prefix"],
45
+ 1102 => [:palette_entries_truncated, "truncated palette entries"],
46
+ 1201 => [:region_prefix_truncated, "truncated region/clipping prefix"],
47
+ 1202 => [:region_header_truncated, "truncated region header"],
48
+ 1203 => [:region_header_invalid, "invalid region header"],
49
+ 1204 => [:region_scan_truncated, "truncated region scan"],
50
+ 1205 => [:region_scan_invalid, "invalid region scan"],
51
+ 1206 => [:region_scan_size_invalid, "invalid region scan size or maximum"],
52
+ 1301 => [:pattern_brush_prefix_truncated, "truncated pattern brush prefix"],
53
+ 1302 => [:pattern_brush_extent_invalid, "invalid pattern brush extent"],
54
+ 1303 => [:dib_brush_prefix_truncated, "truncated DIB brush prefix"],
55
+ 1304 => [:dib_brush_color_usage_invalid, "invalid DIB brush color usage"],
56
+ 1401 => [:bitmap_prefix_truncated, "truncated bitmap transfer prefix"],
57
+ 1402 => [:bitmap_source_empty, "empty bitmap transfer source"],
58
+ 1501 => [:geometry_prefix_truncated, "truncated geometry prefix"],
59
+ 1502 => [:flood_fill_mode_invalid, "invalid extended flood fill mode"],
60
+ 1601 => [:restore_dc_prefix_truncated, "truncated RestoreDc prefix"],
61
+ 1602 => [:escape_prefix_truncated, "truncated Escape prefix"],
62
+ 1603 => [:escape_data_truncated, "truncated Escape data or padding"],
63
+ 1701 => [:object_table_full, "object table capacity exceeded"],
64
+ 1702 => [:object_reference_invalid, "invalid object reference %d"],
65
+ 1703 => [:object_reference_not_palette, "object reference %d is not a palette"],
66
+ 1704 => [:object_reference_not_region, "object reference %d is not a region"],
67
+ 1705 => [:region_reference_invalid, "invalid region reference %d"],
68
+ 1706 => [:brush_reference_invalid, "invalid brush reference %d"],
69
+ 1707 => [:object_reference_not_brush, "object reference %d is not a brush"]
70
+ }.freeze
71
+
72
+ # Recovered, not rejected: the header's Size (305) or MaxRecord (1801) disagrees with the record stream. Both
73
+ # backends keep the model and add a ParseError carrying this evidence to Metafile#errors (see .drift_errors).
74
+ DRIFT_REASONS = [305, 1801].freeze
75
+ DRIFT_MESSAGES = {
76
+ 305 => "WMF header Size is %<declared>d words but the records end at %<actual>d words",
77
+ 1801 => "WMF header MaxRecord is %<declared>d words but the largest record is %<actual>d words"
78
+ }.freeze
79
+
80
+ # Recovered by Wmf::Recovery, the placeable reader and Binary.validate_objects: the model is kept and the
81
+ # evidence is a ParseError on Metafile#errors. RETIRED_REASONS can no longer be raised at all; 701 (font), 801
82
+ # (object record) and 1704 (a fill or frame region naming a non-region) are still raised for the shapes that
83
+ # cannot be recovered. Reference reasons are 1702 (no live object) and 1704 (a non-region for a clip, paint or
84
+ # invert record).
85
+ RETIRED_REASONS = [203, 406, 1003, 1702].freeze
86
+
87
+ INPUT_REASONS = [101, 201, 202, 203, 301, 302, 303, 304, 305, 306, 1801].freeze
88
+ REFERENCE_REASONS = (1702..1707).to_a.freeze
89
+ WORD_BYTES = 2
90
+
91
+ MESSAGE_PATTERNS = REASONS.except(*DRIFT_REASONS, *RETIRED_REASONS).to_h do |reason, (_check, text)|
92
+ pattern = Regexp.escape(text).sub("%d", "(\\d+)")
93
+ [reason, INPUT_REASONS.include?(reason) ? /\A#{pattern}\z/ : /\AWMF #{pattern} at offset (\d+)\z/]
94
+ end.freeze
95
+ private_constant :MESSAGE_PATTERNS
96
+
97
+ module_function
98
+
99
+ # Evidence for a FormatError or ParseError raised by the Ruby parser, or nil when it is not a contract rejection.
100
+ def from_error(error, bytes)
101
+ reason, pattern = MESSAGE_PATTERNS.find { |_code, candidate| candidate.match?(error.message) }
102
+ return unless reason
103
+
104
+ detail = REFERENCE_REASONS.include?(reason) ? pattern.match(error.message)[1].to_i : nil
105
+ build(reason, bytes, error.respond_to?(:offset) ? error.offset : nil, detail)
106
+ end
107
+
108
+ # ParseErrors (with evidence) for each header field that disagrees with the record stream of `bytes`, which
109
+ # the parser has already accepted. Both backends call this, so their errors are identical.
110
+ def drift_errors(bytes)
111
+ DRIFT_REASONS.filter_map do |reason|
112
+ evidence = build(reason, bytes, nil)
113
+ next if evidence.declared == evidence.actual
114
+
115
+ ParseError.new(offset: evidence.offset, record_code: nil,
116
+ message: format(DRIFT_MESSAGES.fetch(reason), declared: evidence.declared, actual: evidence.actual))
117
+ .with_evidence(evidence)
118
+ end
119
+ end
120
+
121
+ # The ParseError (with evidence) for a recovered `reason`. A whole-input reason (203) carries the evidence
122
+ # offset and no record; a record reason carries the record's offset and function.
123
+ def recovered_error(reason, bytes, offset, function = nil, detail = nil)
124
+ evidence = build(reason, bytes, offset, detail)
125
+ text = REASONS.fetch(reason).last.sub("%d") { detail.to_s }
126
+ message = INPUT_REASONS.include?(reason) ? text : "WMF #{text} at offset #{offset}"
127
+ ParseError.new(offset: evidence.offset, record_code: function, message: message).with_evidence(evidence)
128
+ end
129
+
130
+ # Callback for Binary.validate_objects: appends the recovered reference errors to `errors`.
131
+ def reference_recovery(bytes, errors)
132
+ lambda do |record, reason, index|
133
+ errors << recovered_error(reason, bytes, record.offset, record.type_id, index)
134
+ end
135
+ end
136
+
137
+ # Recovered errors in input order (stable for equal offsets), so both backends list them identically.
138
+ def ordered(errors)
139
+ errors.each_with_index.sort_by { |error, index| [error.offset.to_i, index] }.map(&:first)
140
+ end
141
+
142
+ # The one evidence builder both backends use once they know the reason and (for record failures) the offset.
143
+ def build(reason, bytes, offset, detail = nil)
144
+ check = REASONS.fetch(reason).first
145
+ values = INPUT_REASONS.include?(reason) ? input_values(reason, bytes) : record_values(reason, bytes, offset, detail)
146
+ offset, declared, actual = values
147
+ ParseEvidence.new(check: check, offset: offset, declared: declared, actual: actual)
148
+ end
149
+
150
+ def input_values(reason, bytes)
151
+ view = View.new(bytes)
152
+ case reason
153
+ when 101 then [0, Binary::MAX_INPUT_BYTES, bytes.bytesize]
154
+ when 201 then [0, Binary::PLACEABLE_BYTES, bytes.bytesize]
155
+ when 202 then [16, 0, bytes.unpack1("V", offset: 16)]
156
+ when 203 then [20, bytes.unpack1("v", offset: 20), Binary.checksum(bytes.byteslice(0, 20))]
157
+ when 306 then [4, bytes.unpack1("v", offset: 4), 0]
158
+ else header_values(reason, bytes, view)
159
+ end
160
+ end
161
+
162
+ def header_values(reason, bytes, view)
163
+ case reason
164
+ when 301 then [view.start, Binary::HEADER_BYTES, bytes.bytesize - view.start]
165
+ when 302 then [view.start, view.u16(0), nil]
166
+ when 303 then [view.start + 2, view.u16(2), 9]
167
+ when 304 then [view.start + 4, view.u16(4), nil]
168
+ when 305 then [view.start + 6, view.u32(6), view.stream_words]
169
+ else [view.start + 12, view.u32(12), view.largest_record_words]
170
+ end
171
+ end
172
+
173
+ def record_values(reason, bytes, offset, detail)
174
+ view = View.new(bytes)
175
+ words = view.u32_at(offset)
176
+ declared, actual = record_pair(reason, view, offset, words, detail)
177
+ [offset, declared, actual]
178
+ end
179
+
180
+ def record_pair(reason, view, offset, words, detail)
181
+ case reason
182
+ when 401 then [Binary::MAX_RECORDS, Binary::MAX_RECORDS]
183
+ when 402 then [6, view.limit - offset]
184
+ when 403 then [words, (view.limit - offset) / WORD_BYTES]
185
+ when 405 then [view.limit, offset]
186
+ when 406 then [words, 3]
187
+ when 501, 601, 701, 801 then [words, view.required_words(reason, offset)]
188
+ when 1701 then [view.u16(10), nil]
189
+ when *REFERENCE_REASONS then [detail, view.u16(10)]
190
+ else [words, nil]
191
+ end
192
+ end
193
+
194
+ # Reads fields of an input that already passed the checks before the one being described.
195
+ class View
196
+ def initialize(bytes)
197
+ @bytes = bytes
198
+ @start = bytes.bytesize >= 4 && bytes.unpack1("V") == Binary::PLACEABLE_KEY ? Binary::PLACEABLE_BYTES : 0
199
+ end
200
+
201
+ attr_reader :start
202
+
203
+ def u16(offset)
204
+ @bytes.unpack1("v", offset: @start + offset)
205
+ end
206
+
207
+ def u32(offset)
208
+ @bytes.unpack1("V", offset: @start + offset)
209
+ end
210
+
211
+ def u32_at(offset)
212
+ @bytes.unpack1("V", offset: offset) if offset && offset + 4 <= @bytes.bytesize
213
+ end
214
+
215
+ def limit
216
+ @bytes.bytesize
217
+ end
218
+
219
+ # Words from the standard header to the end of the first EOF record (or of the input without one).
220
+ def stream_words
221
+ (record_walk.last - @start) / WORD_BYTES
222
+ end
223
+
224
+ def largest_record_words
225
+ record_walk.first
226
+ end
227
+
228
+ # [largest record in words, offset after the walk]; the walk follows the parser's: it ends after the
229
+ # first EOF record and never reads past the input, whatever the header declares.
230
+ def record_walk
231
+ @record_walk ||= begin
232
+ offset = @start + Binary::HEADER_BYTES
233
+ largest = 0
234
+ while offset + 6 <= limit
235
+ words = u32_at(offset)
236
+ break if words < 3
237
+
238
+ largest = [largest, words].max
239
+ offset += words * WORD_BYTES
240
+ break if @bytes.unpack1("v", offset: offset - (words * WORD_BYTES) + 4).zero?
241
+ end
242
+ [largest, [offset, limit].min]
243
+ end
244
+ end
245
+
246
+ def required_words(reason, offset)
247
+ function = @bytes.unpack1("v", offset: offset + 4) & 0xFF
248
+ record_class = { 501 => Binary::FIXED_RECORDS, 601 => Binary::STATE_RECORDS, 701 => nil, 801 => Binary::OBJECT_RECORDS }
249
+ record_class = reason == 701 ? Model::Wmf::Records::CreateFontIndirect : record_class.fetch(reason).fetch(function)
250
+ reason == 501 ? 3 + record_class::WIRE_FIELDS.length : 3 + (record_class::PAYLOAD_BYTES / WORD_BYTES)
251
+ end
252
+ end
253
+ end
254
+ end
255
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ module GeometryCodec
6
+ RECORDS = {
7
+ 0x10 => Model::Wmf::Records::ScaleWindowExt,
8
+ 0x12 => Model::Wmf::Records::ScaleViewportExt,
9
+ 0x19 => Model::Wmf::Records::FloodFill,
10
+ 0x1F => Model::Wmf::Records::SetPixel,
11
+ 0x48 => Model::Wmf::Records::ExtFloodFill,
12
+ 0x1D => Model::Wmf::Records::PatBlt
13
+ }.freeze
14
+ FUNCTIONS = RECORDS.invert.freeze
15
+
16
+ module_function
17
+
18
+ def decode(raw, function, offset)
19
+ klass = RECORDS.fetch(function & 0xFF)
20
+ prefix = 6 + klass::PAYLOAD_BYTES
21
+ yield "truncated geometry prefix" if raw.bytesize < prefix
22
+ fields = klass::WIRE_FIELDS.zip(raw.byteslice(6, klass::PAYLOAD_BYTES).unpack(klass::WIRE_FORMAT)).to_h
23
+ yield "invalid extended flood fill mode" if klass == Model::Wmf::Records::ExtFloodFill && fields[:mode] > 1
24
+ klass.new(**fields, type_id: function, offset: offset, suffix: raw.byteslice(prefix..))
25
+ end
26
+
27
+ def byte_size(record)
28
+ 6 + record.class::PAYLOAD_BYTES + record.suffix.bytesize
29
+ end
30
+
31
+ def encode(record)
32
+ function = Binary.uint16(record.type_id, "RecordFunction")
33
+ raise SerializeError, "WMF geometry RecordFunction mismatch" unless function & 0xFF == FUNCTIONS[record.class]
34
+ unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
35
+ raise SerializeError, "WMF geometry suffix must be a String with even length"
36
+ end
37
+ raise SerializeError, "WMF geometry too large" if byte_size(record) > Binary::MAX_INPUT_BYTES
38
+
39
+ values = record.class::WIRE_FIELDS.zip(record.class::WIRE_TYPES).map do |field, type|
40
+ Binary.public_send(type, record.public_send(field), field)
41
+ end
42
+ raise SerializeError, "invalid WMF extended flood fill mode" if record.is_a?(Model::Wmf::Records::ExtFloodFill) && record.mode > 1
43
+
44
+ payload = values.pack(record.class::WIRE_FORMAT) + record.suffix.b
45
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Wmf
5
+ module PaletteCodec
6
+ RECORDS = {
7
+ 0xF7 => Model::Wmf::Records::CreatePalette,
8
+ 0x37 => Model::Wmf::Records::SetPalEntries,
9
+ 0x36 => Model::Wmf::Records::AnimatePalette,
10
+ 0x34 => Model::Wmf::Records::SelectPalette,
11
+ 0x39 => Model::Wmf::Records::ResizePalette,
12
+ 0x35 => Model::Wmf::Records::RealizePalette
13
+ }.freeze
14
+ FUNCTIONS = RECORDS.invert.freeze
15
+ ENTRY_FIELDS = %i[red green blue flags].freeze
16
+
17
+ module_function
18
+
19
+ def decode(raw, function, offset)
20
+ klass = RECORDS.fetch(function & 0xFF)
21
+ fields = { type_id: function, offset: offset }
22
+ prefix = 6
23
+ if klass <= Model::Wmf::Records::PaletteRecord
24
+ yield "truncated palette prefix" if raw.bytesize < 10
25
+ start, count = raw.byteslice(6, 4).unpack("v2")
26
+ prefix = 10 + (count * 4)
27
+ yield "truncated palette entries" if raw.bytesize < prefix
28
+ fields[:start] = start
29
+ fields[:entries] = raw.byteslice(10, count * 4).unpack("C*").each_slice(4).map do |values|
30
+ Model::Wmf::Records::PaletteEntry.new(**ENTRY_FIELDS.zip(values).to_h)
31
+ end
32
+ elsif klass != Model::Wmf::Records::RealizePalette
33
+ prefix = 8
34
+ yield "truncated palette prefix" if raw.bytesize < prefix
35
+ field = klass == Model::Wmf::Records::SelectPalette ? :object_index : :entry_count
36
+ fields[field] = raw.byteslice(6, 2).unpack1("v")
37
+ end
38
+ klass.new(**fields, suffix: raw.byteslice(prefix..))
39
+ end
40
+
41
+ def byte_size(record)
42
+ prefix = if record.is_a?(Model::Wmf::Records::PaletteRecord)
43
+ 10 + (record.entries.length * 4)
44
+ elsif record.is_a?(Model::Wmf::Records::RealizePalette)
45
+ 6
46
+ else
47
+ 8
48
+ end
49
+ prefix + record.suffix.bytesize
50
+ end
51
+
52
+ def encode(record)
53
+ function = Binary.uint16(record.type_id, "RecordFunction")
54
+ raise SerializeError, "WMF palette RecordFunction mismatch" unless function & 0xFF == FUNCTIONS[record.class]
55
+ unless record.suffix.is_a?(String) && record.suffix.bytesize.even?
56
+ raise SerializeError, "WMF palette suffix must be a String with even length"
57
+ end
58
+
59
+ payload = encode_payload(record) + record.suffix.b
60
+ [3 + (payload.bytesize / 2), function].pack("Vv") + payload
61
+ end
62
+
63
+ def encode_payload(record)
64
+ case record
65
+ when Model::Wmf::Records::PaletteRecord
66
+ raise SerializeError, "WMF palette entries must be an array" unless record.entries.is_a?(Array)
67
+
68
+ start = Binary.uint16(record.start, "start")
69
+ count = Binary.uint16(record.entries.length, "entry count")
70
+ entries = record.entries.map do |entry|
71
+ raise SerializeError, "WMF palette entry must be a PaletteEntry" unless entry.is_a?(Model::Wmf::Records::PaletteEntry)
72
+
73
+ ENTRY_FIELDS.map { |field| Binary.uint8(entry.public_send(field), field) }.pack("C4")
74
+ end
75
+ [start, count].pack("v2") + entries.join
76
+ when Model::Wmf::Records::SelectPalette
77
+ [Binary.uint16(record.object_index, "object_index")].pack("v")
78
+ when Model::Wmf::Records::ResizePalette
79
+ [Binary.uint16(record.entry_count, "entry_count")].pack("v")
80
+ else "".b
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end