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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module EmfPlus
5
+ # Joins continued EmfPlusObject chunks into logical objects. A chain ends
6
+ # when TotalObjectSize bytes of ObjectData are accumulated, not when the C
7
+ # flag clears: Windows leaves C set on the final chunk. A final chunk with C
8
+ # clear (the layout the spec describes) also ends the chain.
9
+ module ObjectChain
10
+ # One object, possibly spread over several records. Chunk boundaries and
11
+ # source records are kept so `split` can return them unchanged.
12
+ class LogicalObject
13
+ attr_reader :object_type, :emf_object_id, :data, :chunk_sizes, :offset
14
+
15
+ def initialize(chunks)
16
+ @chunks = chunks.freeze
17
+ @object_type = chunks.first.object_type
18
+ @offset = chunks.first.offset
19
+ @emf_object_id = chunks.first.emf_object_id
20
+ @data = chunks.map(&:object_data).join.b.freeze
21
+ @chunk_sizes = chunks.map { |chunk| chunk.object_data.bytesize }.freeze
22
+ @chunk_flags = chunks.map(&:flags).freeze
23
+ end
24
+
25
+ def total_size = data.bytesize
26
+
27
+ # Joined ObjectData decoded by type; works for chained objects, which
28
+ # no single Records::Object chunk can describe.
29
+ def body = @body ||= Objects.decode(self)
30
+
31
+ # With the original chunk sizes the source records come back as read:
32
+ # padding, offsets and comment indexes included. Other sizes re-chunk
33
+ # the data with zero padding, every chunk continued.
34
+ def split(sizes = chunk_sizes)
35
+ return @chunks.dup if sizes == chunk_sizes
36
+
37
+ check_sizes(sizes)
38
+ offset = 0
39
+ sizes.each_with_index.map do |size, index|
40
+ chunk = data.byteslice(offset, size)
41
+ offset += size
42
+ Records.decode(rechunked_frame(chunk, @chunks.fetch(index, @chunks.last)))
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def check_sizes(sizes)
49
+ return if sizes.any? && sizes.all? { |size| size.is_a?(Integer) && size.positive? } && sizes.sum == total_size
50
+
51
+ raise ArgumentError, "split sizes #{sizes.inspect} must be positive and sum to #{total_size}"
52
+ end
53
+
54
+ def rechunked_frame(chunk, source)
55
+ flags = @chunk_flags.first | Records::Object::CONTINUED_FLAG
56
+ Frame.new(type: Records::Object::TYPE, flags: flags, size: 0, data_size: 0, data: "".b,
57
+ offset: source.offset, comment_index: source.comment_index)
58
+ .with_data([total_size].pack("V") + chunk)
59
+ end
60
+ end
61
+
62
+ module_function
63
+
64
+ # Returns the records with each object (single or chained) as a
65
+ # LogicalObject; every other record passes through unchanged.
66
+ def call(records)
67
+ entries = []
68
+ queue = records.dup
69
+ until queue.empty?
70
+ record = queue.shift
71
+ entries << (record.is_a?(Records::Object) ? LogicalObject.new(take_chain(record, queue)) : record)
72
+ end
73
+ entries
74
+ end
75
+
76
+ def take_chain(first, queue)
77
+ return [first] unless first.continued?
78
+
79
+ chunks = [first]
80
+ accumulated = first.object_data.bytesize
81
+ while accumulated < first.total_object_size
82
+ chunk = next_chunk(first, chunks.last, queue.shift)
83
+ chunks << chunk
84
+ accumulated += chunk.object_data.bytesize
85
+ break unless chunk.continued?
86
+ end
87
+ unless accumulated == first.total_object_size
88
+ fail_chain(first,
89
+ "chain holds #{accumulated} bytes, TotalObjectSize is #{first.total_object_size}")
90
+ end
91
+ chunks
92
+ end
93
+
94
+ def next_chunk(first, previous, candidate)
95
+ matches = candidate.is_a?(Records::Object) &&
96
+ candidate.object_type == first.object_type && candidate.emf_object_id == first.emf_object_id &&
97
+ (!candidate.continued? || candidate.total_object_size == first.total_object_size)
98
+ fail_chain(previous, "continued EmfPlusObject chain not completed by a matching chunk") unless matches
99
+ candidate
100
+ end
101
+
102
+ def fail_chain(record, message)
103
+ raise ParseError.new(offset: record.offset, record_code: record.type,
104
+ message: "#{message} (chain started near offset #{record.offset})")
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module EmfPlus
5
+ # An object ID outside the 64-slot table (0..63).
6
+ class ObjectIdError < Emf::Error; end
7
+
8
+ # The EMF+ object table (MS-EMFPLUS 2.3.5.1): 64 slots, IDs 0..63, over a
9
+ # sequence of framed records. A stream index is a record's position in that
10
+ # sequence. A new EmfPlusObject with an existing ID replaces the slot from
11
+ # its position on, so lookups are time-indexed: `lookup(id, at: n)` returns
12
+ # the object defined most recently before position n, and ID reuse never
13
+ # changes what an earlier draw resolved to.
14
+ #
15
+ # An object's position is that of its last chunk, the point where it is
16
+ # complete. Problems found while building are collected in `errors` and
17
+ # never raised; a record or chain that cannot be placed keeps its records.
18
+ class ObjectTable
19
+ SLOTS = 64
20
+
21
+ Diagnostic = Struct.new(:kind, :stream_index, :id, :message, :records, keyword_init: true)
22
+ Resolution = Struct.new(:role, :raw_id, :object, :diagnostic, :solid_color, keyword_init: true)
23
+ Placement = Struct.new(:stream_index, :logical)
24
+ private_constant :Placement
25
+
26
+ # Reads chain records off a queue of [record, stream index] pairs and
27
+ # remembers them, so a failed chain can hand back everything it consumed.
28
+ class Tape
29
+ attr_reader :taken
30
+
31
+ def initialize(queue)
32
+ @queue = queue
33
+ @taken = []
34
+ end
35
+
36
+ def shift
37
+ pair = @queue.shift
38
+ @taken << pair if pair
39
+ pair&.first
40
+ end
41
+ end
42
+ private_constant :Tape
43
+
44
+ # What each draw record references: [role, object type, reader for the
45
+ # ID, reader for the S flag or nil].
46
+ module References
47
+ PEN = [:pen, Objects::PEN, :pen_id, nil].freeze
48
+ BRUSH = [:brush, Objects::BRUSH, :brush_id, :solid_color?].freeze
49
+ FONT = [:font, Objects::FONT, :font_object_id, nil].freeze
50
+ FORMAT = [:string_format, Objects::STRING_FORMAT, :format_id, nil].freeze
51
+ IMAGE = [:image, Objects::IMAGE, :image_object_id, nil].freeze
52
+ IMAGE_ATTRIBUTES = [:image_attributes, Objects::IMAGE_ATTRIBUTES, :image_attributes_id, nil].freeze
53
+ PATH = [:path, Objects::PATH, :path_id, nil].freeze
54
+ REGION = [:region, Objects::REGION, :region_id, nil].freeze
55
+ CLIP_PATH = [:path, Objects::PATH, :emf_object_id, nil].freeze
56
+ CLIP_REGION = [:region, Objects::REGION, :emf_object_id, nil].freeze
57
+
58
+ BY_RECORD = {
59
+ Records::DrawRects => [PEN], Records::DrawLines => [PEN], Records::DrawEllipse => [PEN],
60
+ Records::DrawPie => [PEN], Records::DrawArc => [PEN], Records::DrawClosedCurve => [PEN],
61
+ Records::DrawCurve => [PEN], Records::DrawBeziers => [PEN], Records::DrawPath => [PATH, PEN],
62
+ Records::FillRects => [BRUSH], Records::FillPolygon => [BRUSH], Records::FillEllipse => [BRUSH],
63
+ Records::FillPie => [BRUSH], Records::FillClosedCurve => [BRUSH], Records::FillPath => [PATH, BRUSH],
64
+ Records::FillRegion => [REGION, BRUSH],
65
+ Records::DrawString => [FONT, BRUSH, FORMAT], Records::DrawDriverString => [FONT, BRUSH],
66
+ Records::DrawImage => [IMAGE, IMAGE_ATTRIBUTES], Records::DrawImagePoints => [IMAGE, IMAGE_ATTRIBUTES],
67
+ Records::SetClipPath => [CLIP_PATH], Records::SetClipRegion => [CLIP_REGION]
68
+ }.freeze
69
+ end
70
+ private_constant :References
71
+
72
+ attr_reader :errors
73
+
74
+ def initialize(records)
75
+ @placements = Hash.new { |hash, id| hash[id] = [] }
76
+ @errors = []
77
+ build(records.to_a)
78
+ @placements.default_proc = nil
79
+ @placements.each_value(&:freeze)
80
+ @errors.freeze
81
+ end
82
+
83
+ # The LogicalObject in slot `id` as of stream position `at` (objects
84
+ # defined at earlier positions only), or nil when the slot is empty then.
85
+ def lookup(id, at:) # rubocop:disable Naming/MethodParameterName
86
+ check_id!(id)
87
+ placements = @placements.fetch(id, [])
88
+ placements.reverse_each { |placement| return placement.logical if placement.stream_index < at }
89
+ nil
90
+ end
91
+
92
+ # All 64 slots as of stream position `at`.
93
+ def at(stream_index)
94
+ Array.new(SLOTS) { |id| lookup(id, at: stream_index) }
95
+ end
96
+
97
+ # One Resolution per object reference the record carries, resolved as of
98
+ # its stream position `at`: the slot's object, or nil with a Diagnostic
99
+ # (empty slot, wrong object type, ID outside the table). A solid-colour
100
+ # BrushId is kept raw and not looked up. A record with no references
101
+ # gives an empty array.
102
+ def resolve(record, at:) # rubocop:disable Naming/MethodParameterName
103
+ References::BY_RECORD.fetch(record.class, []).map { |reference| resolution(record, reference, at) }
104
+ end
105
+
106
+ private
107
+
108
+ def build(records)
109
+ queue = records.each_with_index.to_a
110
+ until queue.empty?
111
+ record, index = queue.shift
112
+ place_chain(record, index, queue) if record.is_a?(Records::Object)
113
+ end
114
+ end
115
+
116
+ def place_chain(first, first_index, queue)
117
+ tape = Tape.new(queue)
118
+ chunks = ObjectChain.take_chain(first, tape)
119
+ place(ObjectChain::LogicalObject.new(chunks), tape.taken.last&.last || first_index)
120
+ rescue ParseError => e
121
+ unplaced_chain(first, first_index, queue, tape.taken, e)
122
+ end
123
+
124
+ def place(logical, index)
125
+ id = logical.emf_object_id
126
+ if id < SLOTS
127
+ @placements[id] << Placement.new(index, logical)
128
+ else
129
+ add_error(:object_id_out_of_range, index, id, "object ID #{id} is outside 0..#{SLOTS - 1}", logical.split)
130
+ end
131
+ end
132
+
133
+ def unplaced_chain(first, first_index, queue, taken, error)
134
+ stray = taken.last
135
+ queue.unshift(taken.pop) if stray && !chunk_of?(first, stray.first)
136
+ records = [first] + taken.map(&:first)
137
+ add_error(:incomplete_chain, taken.last&.last || first_index, first.emf_object_id, error.message, records)
138
+ end
139
+
140
+ def chunk_of?(first, record)
141
+ record.is_a?(Records::Object) && record.object_type == first.object_type &&
142
+ record.emf_object_id == first.emf_object_id
143
+ end
144
+
145
+ def add_error(kind, index, id, message, records)
146
+ @errors << Diagnostic.new(kind: kind, stream_index: index, id: id, message: message, records: records).freeze
147
+ end
148
+
149
+ def check_id!(id)
150
+ return if id.is_a?(Integer) && id >= 0 && id < SLOTS
151
+
152
+ raise ObjectIdError, "object ID #{id.inspect} is outside 0..#{SLOTS - 1}"
153
+ end
154
+
155
+ def resolution(record, (role, type, id_reader, solid_reader), position)
156
+ raw_id = record.public_send(id_reader).to_i
157
+ return Resolution.new(role: role, raw_id: raw_id, solid_color: true) if solid_reader && record.public_send(solid_reader)
158
+
159
+ found = lookup(raw_id, at: position)
160
+ kind = problem(found, type)
161
+ return Resolution.new(role: role, raw_id: raw_id, object: found) unless kind
162
+
163
+ diagnostic = Diagnostic.new(kind: kind, stream_index: position, id: raw_id, records: [record],
164
+ message: "#{role} reference #{raw_id}: #{kind.to_s.tr('_', ' ')}").freeze
165
+ Resolution.new(role: role, raw_id: raw_id, diagnostic: diagnostic)
166
+ rescue ObjectIdError
167
+ Resolution.new(role: role, raw_id: raw_id, diagnostic: out_of_range(record, role, raw_id, position))
168
+ end
169
+
170
+ def out_of_range(record, role, raw_id, position)
171
+ Diagnostic.new(kind: :object_id_out_of_range, stream_index: position, id: raw_id, records: [record],
172
+ message: "#{role} reference #{raw_id}: object ID outside 0..#{SLOTS - 1}").freeze
173
+ end
174
+
175
+ def problem(found, type)
176
+ return :empty_slot unless found
177
+
178
+ :wrong_type unless found.object_type == type
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,296 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf/emf_plus/records/primitives/graphics_version"
4
+ require "emf/emf_plus/records/primitives/argb"
5
+ require "emf/emf_plus/records/primitives/point_f"
6
+ require "emf/emf_plus/records/primitives/rect_f"
7
+ require "emf/emf_plus/records/primitives/transform_matrix"
8
+ require "emf/emf_plus/records/primitives/blend_colors"
9
+ require "emf/emf_plus/records/primitives/blend_factors"
10
+ require "emf/emf_plus/records/primitives/focus_scale_data"
11
+
12
+ module Emf
13
+ module EmfPlus
14
+ module Objects
15
+ # EmfPlusBrush (2.2.1.1): Version, Type, then the BrushData the Type
16
+ # selects. Every count is checked against the bytes left before BinData
17
+ # allocates, so a hostile count cannot hang or exhaust memory.
18
+ #
19
+ # The nested EmfPlusPath of a path gradient boundary (BoundaryPathData)
20
+ # and the EmfPlusImage of a texture brush decode through the Path and
21
+ # Image objects. A nested body that does not parse stays raw bytes, so
22
+ # the brush still round-trips.
23
+ class Brush < Typed
24
+ SOLID = 0
25
+ HATCH = 1
26
+ TEXTURE = 2
27
+ PATH_GRADIENT = 3
28
+ LINEAR_GRADIENT = 4
29
+
30
+ DATA_PATH = 0x01
31
+ DATA_TRANSFORM = 0x02
32
+ DATA_PRESET_COLORS = 0x04
33
+ DATA_BLEND_FACTORS_H = 0x08
34
+ DATA_BLEND_FACTORS_V = 0x10
35
+ DATA_FOCUS_SCALES = 0x40
36
+
37
+ P = Records::Primitives
38
+ private_constant :P
39
+
40
+ class BrushHeader < Records::Body
41
+ endian :little
42
+ graphics_version :version
43
+ uint32 :brush_type
44
+ end
45
+
46
+ # EmfPlusSolidBrushData (2.2.2.43).
47
+ class SolidBrushData < Records::Body
48
+ endian :little
49
+ argb :solid_color
50
+ end
51
+
52
+ # EmfPlusHatchBrushData (2.2.2.20).
53
+ class HatchBrushData < Records::Body
54
+ endian :little
55
+ uint32 :hatch_style
56
+ argb :fore_color
57
+ argb :back_color
58
+ end
59
+
60
+ # EmfPlusTextureBrushData (2.2.2.45) without OptionalData.
61
+ class TextureBrushData < Records::Body
62
+ endian :little
63
+ uint32 :brush_data_flags
64
+ int32 :wrap_mode
65
+ end
66
+
67
+ # EmfPlusLinearGradientBrushData (2.2.2.24) without OptionalData.
68
+ class LinearGradientBrushData < Records::Body
69
+ endian :little
70
+ uint32 :brush_data_flags
71
+ int32 :wrap_mode
72
+ rect_f :rect_f
73
+ argb :start_color
74
+ argb :end_color
75
+ uint32 :reserved1
76
+ uint32 :reserved2
77
+ end
78
+
79
+ # EmfPlusPathGradientBrushData (2.2.2.29) up to SurroundingColorCount.
80
+ class PathGradientBrushData < Records::Body
81
+ endian :little
82
+ uint32 :brush_data_flags
83
+ int32 :wrap_mode
84
+ argb :center_color
85
+ point_f :center_point_f
86
+ end
87
+
88
+ class SurroundingColors < Records::Body
89
+ endian :little
90
+ uint32 :surrounding_color_count
91
+ array :surrounding_color, type: :argb, initial_length: :surrounding_color_count
92
+ end
93
+
94
+ # EmfPlusBoundaryPathData (2.2.2.6).
95
+ class BoundaryPathData < Records::Body
96
+ endian :little
97
+ int32 :boundary_path_size
98
+ string :boundary_path_data, read_length: :boundary_path_size
99
+ end
100
+
101
+ # EmfPlusBoundaryPointData (2.2.2.7).
102
+ class BoundaryPointData < Records::Body
103
+ endian :little
104
+ int32 :boundary_point_count
105
+ array :boundary_point_data, type: :point_f, initial_length: :boundary_point_count
106
+ end
107
+
108
+ class Malformed < StandardError; end
109
+ private_constant :Malformed
110
+
111
+ module Nested
112
+ Source = Struct.new(:object_type, :data, :offset)
113
+
114
+ module_function
115
+
116
+ # The decoded nested object, or nil when its bytes do not parse.
117
+ def decode(klass, object_type, data)
118
+ klass.new(Source.new(object_type, data, 0))
119
+ rescue Emf::ParseError
120
+ nil
121
+ end
122
+ end
123
+ private_constant :Nested
124
+
125
+ # BoundaryPathData with its EmfPlusPath decoded. Size and bytes follow
126
+ # the path, so an edit to it re-serializes with the right size.
127
+ class BoundaryPath
128
+ attr_reader :path
129
+
130
+ def initialize(record)
131
+ @raw = record.boundary_path_data.to_s.b.freeze
132
+ @path = Nested.decode(Path, PATH, @raw)
133
+ end
134
+
135
+ def boundary_path_data = path ? path.to_binary : @raw
136
+ def boundary_path_size = boundary_path_data.bytesize
137
+ def to_binary_s = [boundary_path_size].pack("l<") + boundary_path_data
138
+ def num_bytes = 4 + boundary_path_size
139
+ end
140
+
141
+ # Ordered pieces of the object; strings are raw nested objects.
142
+ class Body
143
+ attr_reader :parts
144
+
145
+ def initialize(parts) = @parts = parts
146
+
147
+ def version = parts.fetch(:header).version
148
+
149
+ def num_bytes = parts.each_value.sum { |part| bytes(part).bytesize }
150
+
151
+ def to_binary_s = parts.each_value.map { |part| bytes(part) }.join.b
152
+
153
+ private
154
+
155
+ def bytes(part)
156
+ case part
157
+ when String then part
158
+ when Typed then part.to_binary
159
+ else part.to_binary_s
160
+ end
161
+ end
162
+ end
163
+
164
+ # Walks the data once, reading each piece the flags select.
165
+ class Reader
166
+ def initialize(data)
167
+ @data = data
168
+ @pos = 0
169
+ @parts = {}
170
+ end
171
+
172
+ def read
173
+ take(:header, BrushHeader)
174
+ read_brush_data(@parts.fetch(:header).brush_type)
175
+ Body.new(@parts)
176
+ end
177
+
178
+ private
179
+
180
+ def read_brush_data(brush_type)
181
+ case brush_type
182
+ when SOLID then take(:data, SolidBrushData)
183
+ when HATCH then take(:data, HatchBrushData)
184
+ when TEXTURE then read_texture
185
+ when PATH_GRADIENT then read_path_gradient
186
+ when LINEAR_GRADIENT then read_linear_gradient
187
+ else take_rest(:unknown_data)
188
+ end
189
+ end
190
+
191
+ def read_texture
192
+ take(:data, TextureBrushData)
193
+ take(:transform, P::TransformMatrix) if flag?(DATA_TRANSFORM)
194
+ take_rest(:image)
195
+ decode_image
196
+ end
197
+
198
+ def decode_image
199
+ raw = @parts[:image]
200
+ return unless raw
201
+
202
+ @parts[:image] = Nested.decode(Image, IMAGE, raw) || raw
203
+ end
204
+
205
+ def read_linear_gradient
206
+ take(:data, LinearGradientBrushData)
207
+ take(:transform, P::TransformMatrix) if flag?(DATA_TRANSFORM)
208
+ if flag?(DATA_PRESET_COLORS)
209
+ take_counted(:blend_colors, P::BlendColors, 8)
210
+ else
211
+ # The spec lists the vertical pattern before the horizontal one.
212
+ take_counted(:blend_factors_v, P::BlendFactors, 8) if flag?(DATA_BLEND_FACTORS_V)
213
+ take_counted(:blend_factors_h, P::BlendFactors, 8) if flag?(DATA_BLEND_FACTORS_H)
214
+ end
215
+ end
216
+
217
+ def read_path_gradient
218
+ take(:data, PathGradientBrushData)
219
+ take_counted(:surrounding_colors, SurroundingColors, 4)
220
+ flag?(DATA_PATH) ? read_boundary_path : take_counted(:boundary_points, BoundaryPointData, 8, signed: true)
221
+ read_path_gradient_optional
222
+ end
223
+
224
+ def read_boundary_path
225
+ take_counted(:boundary_path, BoundaryPathData, 1, signed: true)
226
+ @parts[:boundary_path] = BoundaryPath.new(@parts[:boundary_path])
227
+ end
228
+
229
+ def read_path_gradient_optional
230
+ take(:transform, P::TransformMatrix) if flag?(DATA_TRANSFORM)
231
+ if flag?(DATA_PRESET_COLORS)
232
+ take_counted(:blend_colors, P::BlendColors, 8)
233
+ elsif flag?(DATA_BLEND_FACTORS_H)
234
+ take_counted(:blend_factors_h, P::BlendFactors, 8)
235
+ end
236
+ take(:focus_scales, P::FocusScaleData) if flag?(DATA_FOCUS_SCALES)
237
+ end
238
+
239
+ def flag?(mask) = @parts.fetch(:data).brush_data_flags.anybits?(mask)
240
+
241
+ def take(name, klass)
242
+ record = klass.new
243
+ raise Malformed, "#{name} needs #{record.num_bytes} bytes, #{remaining} left" if record.num_bytes > remaining
244
+
245
+ @parts[name] = record.read(@data.byteslice(@pos..))
246
+ @pos += record.num_bytes
247
+ end
248
+
249
+ # `item_size` bytes per counted element after the 4-byte count.
250
+ def take_counted(name, klass, item_size, signed: false)
251
+ raise Malformed, "#{name} count needs 4 bytes, #{remaining} left" if remaining < 4
252
+
253
+ count = @data.byteslice(@pos, 4).unpack1(signed ? "l<" : "V")
254
+ raise Malformed, "#{name} count #{count} is negative" if count.negative?
255
+
256
+ needed = 4 + (count * item_size)
257
+ raise Malformed, "#{name} of #{count} entries needs #{needed} bytes, #{remaining} left" if needed > remaining
258
+
259
+ @parts[name] = klass.read(@data.byteslice(@pos..))
260
+ @pos += @parts[name].num_bytes
261
+ end
262
+
263
+ def take_rest(name)
264
+ rest = @data.byteslice(@pos..)
265
+ @parts[name] = rest.freeze unless rest.empty?
266
+ @pos += rest.bytesize
267
+ end
268
+
269
+ def remaining = @data.bytesize - @pos
270
+ end
271
+
272
+ def brush_type = body.parts.fetch(:header).brush_type
273
+ def data = body.parts[:data]
274
+ def brush_data_flags = data.brush_data_flags
275
+ def transform = body.parts[:transform]
276
+ def surrounding_colors = body.parts[:surrounding_colors]
277
+ def boundary_path = body.parts[:boundary_path]
278
+ def boundary_points = body.parts[:boundary_points]
279
+ def blend_colors = body.parts[:blend_colors]
280
+ def blend_factors_h = body.parts[:blend_factors_h]
281
+ def blend_factors_v = body.parts[:blend_factors_v]
282
+ def focus_scales = body.parts[:focus_scales]
283
+ def image = body.parts[:image]
284
+ def unknown_data = body.parts[:unknown_data]
285
+
286
+ private
287
+
288
+ def read_body(data, logical)
289
+ Reader.new(data).read
290
+ rescue IOError, BinData::ValidityError, Malformed => e
291
+ fail_parse(logical, e.message)
292
+ end
293
+ end
294
+ end
295
+ end
296
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "emf/binary/float32_le"
4
+ require "emf/emf_plus/records/primitives/graphics_version"
5
+ require "emf/emf_plus/records/primitives/point_f"
6
+ require "emf/emf_plus/records/primitives/path"
7
+
8
+ module Emf
9
+ module EmfPlus
10
+ module Objects
11
+ # EmfPlusCustomLineCap (2.2.1.2): Version, Type, then the default
12
+ # (2.2.2.13) or adjustable arrow (2.2.2.12) data the Type selects.
13
+ class CustomLineCap < Typed
14
+ DEFAULT = 0
15
+ ADJUSTABLE_ARROW = 1
16
+
17
+ DATA_FILL_PATH = 0x01
18
+ DATA_LINE_PATH = 0x02
19
+
20
+ class CustomLineCapHeader < Records::Body
21
+ endian :little
22
+ graphics_version :version
23
+ uint32 :cap_type
24
+ end
25
+
26
+ # EmfPlusCustomLineCapData (2.2.2.13) without OptionalData.
27
+ class CustomLineCapDefaultData < Records::Body
28
+ endian :little
29
+ uint32 :custom_line_cap_data_flags
30
+ uint32 :base_cap
31
+ float32_le :base_inset
32
+ uint32 :stroke_start_cap
33
+ uint32 :stroke_end_cap
34
+ uint32 :stroke_join
35
+ float32_le :stroke_miter_limit
36
+ float32_le :width_scale
37
+ point_f :fill_hot_spot
38
+ point_f :stroke_hot_spot
39
+ end
40
+
41
+ # EmfPlusCustomLineCapArrowData (2.2.2.12).
42
+ class CustomLineCapArrowData < Records::Body
43
+ endian :little
44
+ float32_le :width
45
+ float32_le :height
46
+ float32_le :middle_inset
47
+ uint32 :fill_state
48
+ uint32 :line_start_cap
49
+ uint32 :line_end_cap
50
+ uint32 :line_join
51
+ float32_le :line_miter_limit
52
+ float32_le :width_scale
53
+ point_f :fill_hot_spot
54
+ point_f :line_hot_spot
55
+ end
56
+
57
+ P = Records::Primitives
58
+ private_constant :P
59
+
60
+ # Walks the data once, reading the optional paths the flags select.
61
+ class Reader
62
+ def initialize(data)
63
+ @cursor = Parts::Cursor.new(data)
64
+ end
65
+
66
+ def read
67
+ @cursor.take(:header, CustomLineCapHeader)
68
+ read_cap_data(@cursor.parts.fetch(:header).cap_type)
69
+ Parts::Body.new(@cursor.parts)
70
+ end
71
+
72
+ private
73
+
74
+ def read_cap_data(cap_type)
75
+ case cap_type
76
+ when DEFAULT then read_default
77
+ when ADJUSTABLE_ARROW then @cursor.take(:data, CustomLineCapArrowData)
78
+ else @cursor.take_rest(:unknown_data)
79
+ end
80
+ end
81
+
82
+ def read_default
83
+ @cursor.take(:data, CustomLineCapDefaultData)
84
+ flags = @cursor.parts.fetch(:data).custom_line_cap_data_flags
85
+ @cursor.take_sized(:fill_path) { |bytes| P::Path.new.read(bytes) } if flags.anybits?(DATA_FILL_PATH)
86
+ @cursor.take_sized(:line_path) { |bytes| P::Path.new.read(bytes) } if flags.anybits?(DATA_LINE_PATH)
87
+ end
88
+ end
89
+
90
+ def cap_type = body.parts.fetch(:header).cap_type
91
+ def data = body.parts[:data]
92
+ def fill_path = body.parts[:fill_path]&.inner
93
+ def line_path = body.parts[:line_path]&.inner
94
+ def unknown_data = body.parts[:unknown_data]
95
+
96
+ private
97
+
98
+ def read_body(data, logical)
99
+ Reader.new(data).read
100
+ rescue IOError, BinData::ValidityError, Parts::Malformed => e
101
+ fail_parse(logical, e.message)
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end