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,1164 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Emf
4
+ module Emr
5
+ module Binary
6
+ # Copy of an instantiated BinData struct tree. BinData builds each lazy
7
+ # field by cloning a prototype, which dominates `new` plus first access
8
+ # for a record; cloning the finished tree gives the same objects (same
9
+ # classes, params, nil values, parent links) at a fraction of the cost.
10
+ # Relies on BinData::Struct#@field_objs and Base#@parent, as
11
+ # Emf::Binary::ClassFieldAccessors already does for @field_objs.
12
+ module WireCopy
13
+ module_function
14
+
15
+ # Drops a cached BinData lazy evaluator so a template copy does not
16
+ # evaluate parameters against the template.
17
+ def forget_evaluator(object)
18
+ return unless object.instance_variable_defined?(:@lazy_evaluator)
19
+
20
+ object.send(:remove_instance_variable, :@lazy_evaluator)
21
+ end
22
+
23
+ def call(template, parent)
24
+ copy = template.dup
25
+ copy.instance_variable_set(:@parent, parent)
26
+ objs = template.instance_variable_get(:@field_objs).map do |field|
27
+ field.respond_to?(:field_names) ? call(field, copy) : leaf(field, copy)
28
+ end
29
+ copy.instance_variable_set(:@field_objs, objs)
30
+ copy
31
+ end
32
+
33
+ # Field name to position in @field_objs.
34
+ def slots(sample)
35
+ fields = sample.send(:get_parameter, :fields)
36
+ fields.each_with_index.with_object({}) do |(field, index), slots|
37
+ slots[field.name_as_sym] = index if field.name_as_sym
38
+ end.freeze
39
+ end
40
+
41
+ # A copy of the template with each given field assigned, as BinData's
42
+ # constructor does after instantiating the fields one by one. BinData
43
+ # skips nil values in a constructor hash, so does this.
44
+ def build(template, slots, attrs)
45
+ return unless attrs.each_key.all? { |name| slots.key?(name) }
46
+
47
+ wire = call(template, nil)
48
+ objs = wire.instance_variable_get(:@field_objs)
49
+ attrs.each { |name, value| assign(objs[slots[name]], value) unless value.nil? }
50
+ wire
51
+ end
52
+
53
+ STRUCT_SLOTS = {}.compare_by_identity
54
+ private_constant :STRUCT_SLOTS
55
+
56
+ # Same result as `target.assign(value)` on a fresh (clear) target. A
57
+ # nested struct takes a symbol-keyed Hash of its own fields or a struct
58
+ # of exactly its class leaf by leaf, skipping BinData's clear and
59
+ # stringified-hash round trip; any other value goes through BinData.
60
+ def assign(target, value)
61
+ return target.assign(value) unless target.respond_to?(:field_names)
62
+
63
+ slots = (STRUCT_SLOTS[target.class] ||= slots(target))
64
+ if value.instance_of?(Hash) && value.each_key.all? { |name| slots.key?(name) }
65
+ objs = target.instance_variable_get(:@field_objs)
66
+ value.each { |name, leaf| assign(objs[slots[name]], leaf) unless leaf.nil? }
67
+ elsif value.instance_of?(target.class)
68
+ objs = target.instance_variable_get(:@field_objs)
69
+ slots.each_value { |index| assign(objs[index], source_field(value, index)) }
70
+ else
71
+ target.assign(value)
72
+ end
73
+ end
74
+
75
+ def source_field(struct, index)
76
+ struct.send(:instantiate_obj_at, index)
77
+ struct.instance_variable_get(:@field_objs)[index]
78
+ end
79
+
80
+ # BinData extends each array with its initial_length behavior; dup
81
+ # would drop that singleton module, clone keeps it.
82
+ def leaf(leaf, parent)
83
+ copy = leaf.is_a?(BinData::Array) ? leaf.clone : leaf.dup
84
+ copy.instance_variable_set(:@parent, parent)
85
+ copy
86
+ end
87
+ end
88
+
89
+ # Source generation for the lazy fields of a FastFill's wires (see Record):
90
+ # how to copy one field from the template and read its leaves from the
91
+ # record's bytes.
92
+ module LazySource
93
+ private
94
+
95
+ # The template's struct alone: no field object is copied, so each
96
+ # slot of its @field_objs is nil until `lazy_field` makes it.
97
+ def shell_lines(template)
98
+ @templates << template
99
+ ["wire = TEMPLATES[#{@templates.size - 1}].dup", "wire.instance_variable_set(:@parent, nil)",
100
+ "wire.instance_variable_set(:@field_objs, Array.new(#{template.instance_variable_get(:@field_objs).size}))"]
101
+ end
102
+
103
+ # One `when` per top-level field: a copy of its subtree with the
104
+ # values of the leaves beneath it, read from the record's bytes,
105
+ # assigned.
106
+ def lazy_cases(objs, slots, leaves, reads)
107
+ slots.map do |name, index|
108
+ field = objs[index]
109
+ first = leaves.index { |path, _| path.first == name }
110
+ lines = field.respond_to?(:field_names) ? copy_lines(field, "f", "wire") : leaf_lines(field, "f", "wire")
111
+ lines += lazy_assigns(name, leaves, reads, first)
112
+ "when #{index}\n #{lines.join("\n ")}\n f"
113
+ end
114
+ end
115
+
116
+ def lazy_assigns(name, leaves, reads, first)
117
+ return ["fill_unvalued(f, data, base)"] unless first
118
+
119
+ leaves.each_with_index.filter_map do |(path, leaf), position|
120
+ next unless path.first == name
121
+
122
+ verb = leaf.name == "Emf::Binary::Float32Le" ? "assign_bits" : "assign"
123
+ directive, at = reads[position]
124
+ "#{path.drop(1).inject('f') { |expr, step| "#{expr}.#{step}" }}.#{verb}(data.unpack1(\"#{directive}\", offset: base + #{at}))"
125
+ end
126
+ end
127
+ end
128
+
129
+ # Where each leaf of a fixed layout sits in the record's bytes, and the
130
+ # shell that reads them on demand.
131
+ module LeafReads
132
+ WIDTHS = { "V" => 4, "l<" => 4, "C" => 1 }.freeze
133
+ private_constant :WIDTHS
134
+
135
+ # `build` without a wire: a shell over the bytes PackPlan packs from
136
+ # the attributes, n_size as given. nil when there is no verified
137
+ # plan, the attributes are not ones it takes, or fields cannot be
138
+ # made lazily; the caller then builds.
139
+ def build_packed(attrs)
140
+ bytes = pack_plan&.pack(attrs, size: false) or return
141
+ bytes = bytes.b unless bytes.encoding == Encoding::BINARY
142
+ return eager(copy_shell, bytes, 0) unless @lazy_ok
143
+
144
+ @wire_class.allocate.thin(bytes, 0, self, @shell_params, @shell_names, true)
145
+ rescue StandardError
146
+ nil
147
+ end
148
+
149
+ private
150
+
151
+ # [field count, params, field names] a thin shell shares with the template.
152
+ def shell_identity(sample)
153
+ [sample.instance_variable_get(:@field_objs).size, sample.instance_variable_get(:@params),
154
+ sample.instance_variable_get(:@field_names)]
155
+ end
156
+
157
+ # A shell whose fields are made, from the record's bytes, as they are
158
+ # first used.
159
+ def lazy_wire(data, base)
160
+ return eager(copy_shell, data, base) unless @lazy_ok
161
+
162
+ @wire_class.allocate.thin(data, base, self, @shell_params, @shell_names, nil)
163
+ end
164
+
165
+ # The same record with every field made now: what lazy fill is
166
+ # checked against, and the fallback when the check fails.
167
+ def eager(wire, data, base)
168
+ objs = wire.instance_variable_get(:@field_objs)
169
+ objs.each_index { |index| objs[index] = lazy_field(index, wire, data, base) }
170
+ wire
171
+ end
172
+
173
+ # [directive, byte offset in the record] of each leaf, in leaf order;
174
+ # the layout must be gap-free up to the end of the fixed part.
175
+ def leaf_reads(leaves, directives)
176
+ at = 0
177
+ reads = leaves.map do |_, field|
178
+ directive = directives.fetch(field.name)
179
+ [directive, at].tap { at += WIDTHS.fetch(directive) }
180
+ end
181
+ raise SelfCheck::Unverified, "#{@wire_class}: leaves do not tile the record" unless at == @size
182
+
183
+ reads
184
+ end
185
+ end
186
+
187
+ # The self-check of FlatPoints, run once per point array filler.
188
+ module FlatPointsCheck
189
+ # The BinData::Array API the flat-points holder was written
190
+ # against (bindata 2.5.1): every reader of its element list goes
191
+ # through `elements`, `clear?` or `assign`.
192
+ ARRAY_METHODS = %i[<< [] []= assign at clear? concat debug_name_of do_num_bytes do_write each empty?
193
+ find_index find_index_of first index initialize_instance initialize_shared_instance
194
+ insert last length offset_of push size slice snapshot to_ary unshift].freeze
195
+ ARRAY_PRIVATE_METHODS = %i[append_new_element elements extend_array new_element slice_index slice_range
196
+ slice_start_length sum_num_bytes_below_index
197
+ sum_num_bytes_for_all_elements].freeze
198
+ private_constant :ARRAY_METHODS, :ARRAY_PRIVATE_METHODS
199
+
200
+ # Reads of a held array, one fresh pair (held, BinData-built) per
201
+ # scenario; each returns what it observed.
202
+ FLAT_SCENARIOS = [
203
+ ->(_wire, array, _write) { [array.size, array.first.snapshot, array.last.snapshot, array.to_a.map(&:snapshot)] },
204
+ ->(_wire, array, _write) { [array.map { |point| point.x.value + point.y.value }, array[1].x.value] },
205
+ ->(wire, array, _write) { [array.num_bytes, wire.num_bytes, array.empty?, array.clear?] },
206
+ ->(wire, _array, _write) { [wire.snapshot] },
207
+ lambda { |wire, array, write|
208
+ array << { x: 9, y: -9 }
209
+ [array.size, array.last.snapshot, write.call(wire)]
210
+ },
211
+ lambda { |wire, array, write|
212
+ array.assign([])
213
+ [array.size, write.call(wire)]
214
+ },
215
+ lambda { |_wire, array, _write|
216
+ array.assign([{ x: 1, y: 2 }])
217
+ [array.size, array.first.snapshot, array.clear?]
218
+ },
219
+ ->(_wire, array, _write) { [array.to_binary_s, array.clear?, array.snapshot] }
220
+ ].freeze
221
+ private_constant :FLAT_SCENARIOS
222
+
223
+ # True when this BinData still behaves as the flat-points holder
224
+ # assumes: the Array API is the one it was written against and a
225
+ # parsed record that holds its points flat reads, writes and
226
+ # edits like the same record built by BinData. Any difference, or
227
+ # an error, makes parsed and built records use eager points.
228
+ def verify_flat_points
229
+ return false unless BinData::Array.instance_methods(false).sort == ARRAY_METHODS.sort &&
230
+ BinData::Array.private_instance_methods(false).sort == ARRAY_PRIVATE_METHODS.sort
231
+
232
+ bytes = bindata_bytes(verification_record)
233
+ write = method(:bindata_bytes)
234
+ FLAT_SCENARIOS.all? do |scenario|
235
+ held = call(bytes).first
236
+ reference = @wire_class.read(bytes)
237
+ held.public_send(array_name).respond_to?(:hold_flat_points) &&
238
+ scenario.call(held, held.public_send(array_name), write) ==
239
+ scenario.call(reference, reference.public_send(array_name), write)
240
+ end
241
+ rescue StandardError
242
+ false
243
+ end
244
+ end
245
+
246
+ # Source of a FastFill's generated `build_attrs`, same result as
247
+ # WireCopy.build: every known key is fetched into a local first (an
248
+ # unknown key makes the count disagree and returns nil before any
249
+ # copy), then each non-nil value is assigned straight into the copy.
250
+ # A nested struct given as the same struct class is copied leaf by
251
+ # leaf inline; any other value goes through WireCopy.assign.
252
+ class BuildSource
253
+ include LazySource
254
+
255
+ ABSENT = Object.new.freeze
256
+
257
+ def initialize(template, slots, leaves = [], reads = [])
258
+ @struct_classes = []
259
+ @templates = []
260
+ @copy = copy_lines(template, "wire", "nil")
261
+ objs = template.instance_variable_get(:@field_objs)
262
+ @shell = shell_lines(template)
263
+ @lazy = lazy_cases(objs, slots, leaves, reads)
264
+ @fetches = []
265
+ @counts = []
266
+ @assigns = []
267
+ slots.each_with_index do |(name, index), position|
268
+ value = "v#{position}"
269
+ @fetches << "#{value} = attrs.fetch(:#{name}, ABSENT)"
270
+ @counts << "seen += 1 unless ABSENT.equal?(#{value})"
271
+ @assigns << assign(objs[index], index, value)
272
+ end
273
+ end
274
+
275
+ # A module holding `build_attrs` and the constants it refers to.
276
+ def to_module
277
+ source = to_s
278
+ classes = @struct_classes
279
+ Module.new.tap do |mod|
280
+ mod.const_set(:STRUCTS, classes.freeze)
281
+ mod.const_set(:ABSENT, ABSENT)
282
+ mod.const_set(:TEMPLATES, @templates.freeze)
283
+ mod.module_eval(source, __FILE__, __LINE__)
284
+ end
285
+ end
286
+
287
+ def to_s
288
+ <<~RUBY
289
+ def copy_template
290
+ #{@copy.join("\n ")}
291
+ wire
292
+ end
293
+
294
+ def copy_shell
295
+ #{@shell.join("\n ")}
296
+ wire
297
+ end
298
+
299
+ # The field at `index` of a shell, copied from the template and
300
+ # given the value read from `data`, the record starting at `base`.
301
+ def lazy_field(index, wire, data, base)
302
+ case index
303
+ #{@lazy.join("\n ")}
304
+ end
305
+ end
306
+
307
+ def build_attrs(attrs)
308
+ #{@fetches.join("\n ")}
309
+ seen = 0
310
+ #{@counts.join("\n ")}
311
+ return unless seen == attrs.size
312
+ #{@copy.join("\n ")}
313
+ objs = wire.instance_variable_get(:@field_objs)
314
+ #{@assigns.join("\n ")}
315
+ wire
316
+ end
317
+ RUBY
318
+ end
319
+
320
+ private
321
+
322
+ # Straight-line twin of WireCopy.call: dup of every struct and leaf of
323
+ # the template, parents pointed into the copy, field lists rebuilt.
324
+ def copy_lines(struct, var, parent)
325
+ @templates << struct
326
+ lines = ["#{var} = TEMPLATES[#{@templates.size - 1}].dup", "#{var}.instance_variable_set(:@parent, #{parent})"]
327
+ children = struct.instance_variable_get(:@field_objs).each_with_index.map do |field, index|
328
+ child = "#{var}_#{index}"
329
+ lines.concat(field.respond_to?(:field_names) ? copy_lines(field, child, var) : leaf_lines(field, child, var))
330
+ child
331
+ end
332
+ lines << "#{var}.instance_variable_set(:@field_objs, [#{children.join(', ')}])"
333
+ end
334
+
335
+ def leaf_lines(field, var, parent)
336
+ @templates << field
337
+ copier = field.is_a?(BinData::Array) ? "clone" : "dup"
338
+ ["#{var} = TEMPLATES[#{@templates.size - 1}].#{copier}", "#{var}.instance_variable_set(:@parent, #{parent})"]
339
+ end
340
+
341
+ def assign(field, index, value)
342
+ present = "unless #{value}.nil? || ABSENT.equal?(#{value})"
343
+ return "objs[#{index}].assign(#{value}) #{present}" unless field.respond_to?(:field_names)
344
+
345
+ @struct_classes << field.class
346
+ klass = "STRUCTS[#{@struct_classes.size - 1}]"
347
+ <<~RUBY.chomp
348
+ #{present}
349
+ if #{value}.instance_of?(#{klass})
350
+ t1 = objs[#{index}].instance_variable_get(:@field_objs)
351
+ #{struct_copy(field, 1, value).join("\n ")}
352
+ else
353
+ Emf::Emr::Binary::WireCopy.assign(objs[#{index}], #{value})
354
+ end
355
+ end
356
+ RUBY
357
+ end
358
+
359
+ # Leaf-by-leaf assignments from `source` into the target field list `t<depth>`.
360
+ def struct_copy(struct, depth, source)
361
+ fields = struct.instance_variable_get(:@field_objs)
362
+ WireCopy.slots(struct).flat_map do |name, index|
363
+ field = fields[index]
364
+ if field.respond_to?(:field_names)
365
+ ["t#{depth + 1} = t#{depth}[#{index}].instance_variable_get(:@field_objs)",
366
+ *struct_copy(field, depth + 1, "#{source}.#{name}")]
367
+ else
368
+ ["t#{depth}[#{index}].assign(#{source}.#{name})"]
369
+ end
370
+ end
371
+ end
372
+ end
373
+
374
+ # Self-check for a FastFill, run once when the filler is built. Proves on
375
+ # the installed BinData that a template copy is a faithful, independent
376
+ # twin of a fresh instance and that the generated reader and writer agree
377
+ # with BinData's own on a record with distinct leaf values.
378
+ module SelfCheck
379
+ # Each compares a lazy record with the read one after the same use.
380
+ LAZY_SCENARIOS = [
381
+ ->(lazy, ref, write, _) { lazy.snapshot == ref.snapshot && write.call(lazy) == write.call(ref) },
382
+ lambda { |lazy, ref, write, leaves|
383
+ path = leaves.last.first
384
+ last = ->(wire) { path.inject(wire) { |obj, name| obj.public_send(name) } }
385
+ last.call(lazy).value == last.call(ref).value && lazy.snapshot == ref.snapshot &&
386
+ write.call(lazy) == write.call(ref)
387
+ },
388
+ lambda { |lazy, ref, write, leaves|
389
+ [lazy, ref].each { |wire| leaves.first.first.inject(wire) { |o, n| o.public_send(n) }.assign(0x2A) }
390
+ lazy.snapshot == ref.snapshot && write.call(lazy) == write.call(ref)
391
+ },
392
+ lambda { |lazy, _ref, write, _|
393
+ lazy.assign(lazy.class.new)
394
+ fresh = lazy.class.new
395
+ lazy.snapshot == fresh.snapshot && write.call(lazy) == write.call(fresh)
396
+ },
397
+ lambda { |lazy, _ref, _write, _|
398
+ was_clear = lazy.clear?
399
+ lazy.clear
400
+ was_clear == false && lazy.snapshot == lazy.class.new.snapshot
401
+ }
402
+ ].freeze
403
+ private_constant :LAZY_SCENARIOS
404
+
405
+ # Raised when BinData's internals do not behave as WireCopy assumes.
406
+ class Unverified < StandardError; end
407
+
408
+ BINDATA_WRITER = BinData::Base.instance_method(:to_binary_s)
409
+ private_constant :BINDATA_WRITER
410
+
411
+ def self.included(base)
412
+ base.extend(ClassMethods)
413
+ end
414
+
415
+ # Class-level entry point.
416
+ module ClassMethods
417
+ # A verified filler for the class, or nil when the class cannot be
418
+ # filled safely: construction or the self-check raised or disagreed
419
+ # with plain BinData. Callers treat nil as "use BinData".
420
+ def guarded(klass)
421
+ filler = new(klass)
422
+ filler.verify!
423
+ filler
424
+ rescue StandardError
425
+ nil
426
+ end
427
+ end
428
+
429
+ # Raises Unverified (or whatever the internals raise) on any difference.
430
+ def verify!
431
+ fresh = @wire_class.new
432
+ verify_template(fresh)
433
+ verify_independent
434
+ sample = verification_record
435
+ bytes = bindata_bytes(sample) + verification_tail
436
+ verify_read(bytes)
437
+ verify_write(sample)
438
+ verify_build
439
+ @lazy_ok = verify_lazy(bytes)
440
+ self
441
+ end
442
+
443
+ private
444
+
445
+ # BinData::Struct as the lazy shell assumes (bindata 2.5.1): fields
446
+ # are reached only through instantiate_obj_at / instantiate_all_objs,
447
+ # and clear / clear? are the only readers of @field_objs besides.
448
+ STRUCT_METHODS = %i[[] []= assign clear clear? debug_name_of do_num_bytes do_read do_write each_pair
449
+ field_names has_key? initialize_instance initialize_shared_instance key?
450
+ offset_of snapshot].freeze
451
+ STRUCT_PRIVATE_METHODS = %i[as_stringified_hash assign_fields base_field_name define_field_accessors
452
+ define_field_accessors_for find_index_of find_obj_for_name include_obj?
453
+ include_obj_for_io? instantiate_all_objs instantiate_obj_at
454
+ sum_num_bytes_below_index sum_num_bytes_for_all_fields].freeze
455
+ private_constant :STRUCT_METHODS, :STRUCT_PRIVATE_METHODS
456
+
457
+ # True when a lazily filled record reads, writes and edits like the
458
+ # BinData-read one; false (eager fill) on any difference or error.
459
+ def verify_lazy(bytes)
460
+ return false unless BinData::Struct.instance_methods(false).sort == STRUCT_METHODS.sort &&
461
+ BinData::Struct.private_instance_methods(false).sort == STRUCT_PRIVATE_METHODS.sort
462
+
463
+ data = bytes.b.freeze
464
+ LAZY_SCENARIOS.all? do |scenario|
465
+ lazy = lazy_wire(data, 0)
466
+ reference = @wire_class.read(data)
467
+ lazy.instance_variable_get(:@lazy_filler) &&
468
+ scenario.call(lazy, reference, method(:bindata_bytes), @leaves) == true
469
+ end
470
+ rescue StandardError
471
+ false
472
+ end
473
+
474
+ def unverified!(what)
475
+ raise Unverified, "#{@wire_class}: #{what}"
476
+ end
477
+
478
+ def bindata_bytes(wire)
479
+ BINDATA_WRITER.bind_call(wire)
480
+ end
481
+
482
+ def verify_template(fresh)
483
+ [WireCopy.call(@template, nil), copy_template].each { |copy| verify_copy_of(copy, fresh) }
484
+ unverified!("slots do not cover the field list") unless @slots.keys == fresh.field_names
485
+ end
486
+
487
+ def verify_copy_of(copy, fresh)
488
+ same = copy.instance_of?(@wire_class) && copy.snapshot == fresh.snapshot &&
489
+ copy.num_bytes == fresh.num_bytes && bindata_bytes(copy) == bindata_bytes(fresh)
490
+ unverified!("template copy differs from a fresh instance") unless same
491
+ end
492
+
493
+ # No object of a copy is shared with the template, and every parent
494
+ # link points into the copy.
495
+ def verify_independent
496
+ [-> { WireCopy.call(@template, nil) }, -> { copy_template }].each { |copier| verify_independent_copy(copier) }
497
+ end
498
+
499
+ def verify_independent_copy(copier)
500
+ copy = copier.call
501
+ shared = tree_objects(@template).map(&:object_id) & tree_objects(copy).map(&:object_id)
502
+ unverified!("copy shares objects with the template") unless shared.empty?
503
+ unverified!("copy parent links are wrong") unless parents_consistent?(copy)
504
+ edit_leaf(copy)
505
+ unverified!("editing a copy changed the template") unless copier.call.snapshot == @wire_class.new.snapshot
506
+ end
507
+
508
+ def tree_objects(struct)
509
+ objs = struct.instance_variable_get(:@field_objs)
510
+ raise Unverified, "no field objects" unless objs.is_a?(::Array)
511
+
512
+ [struct] + objs.flat_map { |obj| obj.respond_to?(:field_names) ? tree_objects(obj) : [obj] }
513
+ end
514
+
515
+ def parents_consistent?(struct)
516
+ struct.instance_variable_get(:@field_objs).all? do |obj|
517
+ obj.instance_variable_get(:@parent).equal?(struct) &&
518
+ (!obj.respond_to?(:field_names) || parents_consistent?(obj))
519
+ end
520
+ end
521
+
522
+ def edit_leaf(wire)
523
+ path, field = @leaves.first
524
+ target = path.inject(wire) { |obj, name| obj.public_send(name) }
525
+ float?(field) ? target.assign_bits(1) : target.assign(1)
526
+ end
527
+
528
+ # A record with a distinct small value in every leaf.
529
+ def verification_record
530
+ wire = @wire_class.new
531
+ @leaves.each_with_index do |(path, field), index|
532
+ target = path.inject(wire) { |obj, name| obj.public_send(name) }
533
+ float?(field) ? target.assign_bits(index + 1) : target.assign(index + 1)
534
+ end
535
+ wire
536
+ end
537
+
538
+ def verification_tail
539
+ "\x07\x08".b
540
+ end
541
+
542
+ def verify_read(bytes)
543
+ wire, rest = call(bytes)
544
+ expected = @wire_class.read(bytes)
545
+ unverified!("call differs from read") unless wire.snapshot == expected.snapshot &&
546
+ bindata_bytes(wire) == bindata_bytes(expected)
547
+ unverified!("call kept the wrong suffix") unless rest == verification_tail
548
+ end
549
+
550
+ def verify_write(sample)
551
+ packed = pack(sample)
552
+ return if packed.nil? || packed == bindata_bytes(sample)
553
+
554
+ unverified!("pack differs from BinData's writer")
555
+ end
556
+
557
+ # Keyword construction with the first scalar field set.
558
+ def verify_build
559
+ name = @slots.each_key.find { |slot| @leaves.any? { |path, _| path == [slot] } }
560
+ return unless name
561
+
562
+ built = build(name => 1)
563
+ unverified!("build refused a known field") unless built
564
+ expected = @wire_class.new({ name => 1 })
565
+ same = built.snapshot == expected.snapshot && bindata_bytes(built) == bindata_bytes(expected)
566
+ unverified!("build differs from BinData's constructor") unless same
567
+ verify_build_copy
568
+ verify_build_structs
569
+ unverified!("build accepted an unknown key") if build(name => 1, no_such_field_: 1)
570
+ end
571
+
572
+ # The generated copy is a fresh, independent twin of the template.
573
+ def verify_build_copy
574
+ copy = build({})
575
+ unless copy.instance_of?(@wire_class) && copy.snapshot == @wire_class.new.snapshot
576
+ unverified!("build copy differs from a fresh instance")
577
+ end
578
+ shared = tree_objects(@template).map(&:object_id) & tree_objects(copy).map(&:object_id)
579
+ unverified!("build copy shares objects with the template") unless shared.empty? && parents_consistent?(copy)
580
+ end
581
+
582
+ # Every nested struct field, given as a distinct-valued struct of its
583
+ # own class, builds the same record as BinData's constructor.
584
+ def verify_build_structs
585
+ sample = verification_record
586
+ @slots.each_key do |slot|
587
+ next unless @template.public_send(slot).respond_to?(:field_names)
588
+
589
+ given = sample.public_send(slot)
590
+ built = build(slot => given)
591
+ expected = @wire_class.new({ slot => given })
592
+ same = built && built.snapshot == expected.snapshot && bindata_bytes(built) == bindata_bytes(expected)
593
+ unverified!("build of #{slot} differs from BinData's constructor") unless same
594
+ end
595
+ end
596
+ end
597
+
598
+ # Eager fill of fixed-layout records: one String#unpack for the whole
599
+ # record, then leaf assignment into a fresh instance of the same wire
600
+ # class. The unpack format and leaf paths are derived once per class
601
+ # from its field list, so the result is indistinguishable from `read`.
602
+ class FastFill
603
+ DIRECTIVES = {
604
+ "BinData::Uint32le" => "V",
605
+ "BinData::Int32le" => "l<",
606
+ "BinData::Uint8" => "C",
607
+ "Emf::Binary::Float32Le" => "V"
608
+ }.freeze
609
+ FLOAT_LEAF = "Emf::Binary::Float32Le"
610
+ EMPTY_SUFFIX = "".b.freeze
611
+ private_constant :DIRECTIVES, :FLOAT_LEAF, :EMPTY_SUFFIX
612
+
613
+ include SelfCheck
614
+ include LeafReads
615
+
616
+ def self.for(type_id)
617
+ BY_TYPE[type_id]
618
+ end
619
+
620
+ def self.for_class(wire_class)
621
+ BY_CLASS[wire_class] || POINT_ARRAY_BY_CLASS[wire_class] || TEXT_BY_CLASS[wire_class]
622
+ end
623
+
624
+ attr_reader :size, :wire_class, :template, :field_count
625
+
626
+ # The PackPlan for the class (bytes straight from attributes), or nil
627
+ # when no verified plan exists.
628
+ def pack_plan = defined?(@pack_plan) ? @pack_plan : (@pack_plan = PackPlan.build(self))
629
+
630
+ def initialize(klass)
631
+ @wire_class = klass
632
+ @lazy_ok = true
633
+ sample = klass.new
634
+ @size = sample.num_bytes
635
+ @leaves = leaves = leaf_paths(sample, [])
636
+ # leaf_paths touched every field, so the sample is fully instantiated.
637
+ @template = sample
638
+ @field_count, @shell_params, @shell_names = shell_identity(sample)
639
+ @format = leaves.map { |_, field| DIRECTIVES.fetch(field.name) }.join
640
+ @leaf_reads = leaf_reads(leaves, DIRECTIVES)
641
+ # Float layouts are not packed: measured on test-000, BinData writes
642
+ # SETWORLDTRANSFORM in 15 objects against 18+ for a value pack, and
643
+ # Float32Le must write its retained NaN bits itself.
644
+ @pack_format = @format unless leaves.any? { |_, field| float?(field) }
645
+ @slots = WireCopy.slots(sample)
646
+ generate_call(leaves)
647
+ generate_pack(leaves)
648
+ extend(BuildSource.new(@template, @slots, leaves, @leaf_reads).to_module)
649
+ end
650
+
651
+ # Keyword construction, `wire_class.new(**attrs)`; nil when a key is
652
+ # not a field, so the caller falls back to BinData. Generated per
653
+ # class by generate_build; subclasses override `build` and call super.
654
+ def build(attrs) = build_attrs(attrs)
655
+
656
+ # [wire, suffix] for one record's bytes. The wire reads its fields
657
+ # from a private frozen view of them when they are first used.
658
+ def call(record_bytes)
659
+ data = record_bytes.b.freeze
660
+ call_at(data, 0, data.bytesize)
661
+ end
662
+
663
+ # The same for the n_size-byte record at `offset` of the frozen
664
+ # binary `data`, which the wire keeps and reads fields from.
665
+ def call_at(data, offset, n_size)
666
+ [lazy_wire(data, offset), suffix(data, offset, n_size)]
667
+ end
668
+
669
+ # The wire `call_at` answers for a record that is exactly the layout
670
+ # (no suffix), without the [wire, suffix] pair; nil for any other
671
+ # n_size, which `call_at` reads.
672
+ def exact_wire(data, offset, n_size) = (lazy_wire(data, offset) if n_size == @size)
673
+
674
+ # The bytes a wire of the record at `base` of `data` is.
675
+ def record_size(_data, _base) = @size
676
+
677
+ # Whether `call_at` reads the n_size-byte record at `offset`: always
678
+ # for a fixed layout.
679
+ def fits?(_data, _offset, _n_size) = true
680
+
681
+ # Called by a lazy wire's field copy for a field `call` has no
682
+ # unpacked leaf value for (subclasses with such a field override it).
683
+ def fill_unvalued(_field, _data, _base); end
684
+
685
+ private
686
+
687
+ def float?(field)
688
+ field.name == FLOAT_LEAF
689
+ end
690
+
691
+ # Bytes of the record at `offset` of `data` past the `consumed` ones.
692
+ def suffix(data, offset, n_size, consumed = @size)
693
+ return EMPTY_SUFFIX if n_size == consumed
694
+
695
+ data.byteslice(offset + consumed, n_size - consumed).b.freeze
696
+ end
697
+
698
+ def leaf_paths(record, prefix)
699
+ record.field_names.flat_map do |name|
700
+ field = record.public_send(name)
701
+ if field.respond_to?(:field_names)
702
+ leaf_paths(field, prefix + [name])
703
+ else
704
+ [[prefix + [name], field.class]]
705
+ end
706
+ end
707
+ end
708
+
709
+ # One specialized `call` per wire class: the record is unpacked into
710
+ # locals and each leaf is assigned through the accessors BinData
711
+ # already resolved for the class, with no per-leaf dispatch loop.
712
+ def generate_call(_leaves); end
713
+
714
+ def generate_pack(leaves)
715
+ if @pack_format
716
+ reads = leaves.map { |path, _| "#{accessor_chain(path)}.value" }
717
+ body = "[#{reads.join(', ')}].pack(@pack_format)"
718
+ else
719
+ body = "nil"
720
+ end
721
+ define_generated(:pack, "def pack(wire)\n #{body}\nend\n")
722
+ end
723
+
724
+ def accessor_chain(path)
725
+ path.inject("wire") { |expr, name| "#{expr}.#{name}" }
726
+ end
727
+
728
+ def define_generated(name, source)
729
+ singleton_class.class_eval(source, __FILE__, __LINE__)
730
+ singleton_class.send(:public, name)
731
+ end
732
+
733
+ ENABLED_TYPES = [
734
+ TypeCodes::SELECTOBJECT, TypeCodes::MOVETOEX, TypeCodes::LINETO,
735
+ TypeCodes::DELETEOBJECT, TypeCodes::CREATEBRUSHINDIRECT,
736
+ TypeCodes::SETBKCOLOR, TypeCodes::SETPIXELV, TypeCodes::CREATEPEN,
737
+ TypeCodes::SETBKMODE, TypeCodes::SETTEXTCOLOR, TypeCodes::SETTEXTALIGN,
738
+ TypeCodes::ELLIPSE, TypeCodes::SETVIEWPORTORGEX, TypeCodes::RESTOREDC,
739
+ TypeCodes::SETWORLDTRANSFORM, TypeCodes::MODIFYWORLDTRANSFORM,
740
+ TypeCodes::SETMAPMODE, TypeCodes::SETWINDOWEXTEX, TypeCodes::SETWINDOWORGEX,
741
+ TypeCodes::SETVIEWPORTEXTEX, TypeCodes::SETPOLYFILLMODE, TypeCodes::SETROP2,
742
+ TypeCodes::SETSTRETCHBLTMODE, TypeCodes::SETBRUSHORGEX, TypeCodes::SAVEDC,
743
+ TypeCodes::BEGINPATH, TypeCodes::ENDPATH, TypeCodes::CLOSEFIGURE,
744
+ TypeCodes::FILLPATH, TypeCodes::STROKEANDFILLPATH, TypeCodes::STROKEPATH,
745
+ TypeCodes::RECTANGLE, TypeCodes::INTERSECTCLIPRECT, TypeCodes::EXCLUDECLIPRECT,
746
+ TypeCodes::SETMITERLIMIT, TypeCodes::SETICMMODE, TypeCodes::SETLAYOUT
747
+ ].freeze
748
+ BY_TYPE = ENABLED_TYPES.to_h { |type_id| [type_id, guarded(Records.lookup(type_id))] }.compact.freeze
749
+ BY_CLASS = BY_TYPE.values.to_h { |filler| [filler.wire_class, filler] }.freeze
750
+ private_constant :ENABLED_TYPES, :BY_TYPE, :BY_CLASS
751
+ end
752
+
753
+ class FastFill
754
+ # Extended onto the template's array (clones keep it). A built
755
+ # record holds its coordinates as one flat list of integers; the
756
+ # BinData points are made the first time anything reads the array's
757
+ # elements, so a record built and serialized untouched never pays
758
+ # for them. Every reader of @elements in BinData::Array goes through
759
+ # `elements`, `clear?` or `assign`, which are the methods covered here.
760
+ module FlatPoints
761
+ def hold_flat_points(flat, filler)
762
+ @elements = nil
763
+ @flat_points = flat
764
+ @point_filler = filler
765
+ end
766
+
767
+ def initialize_instance
768
+ @flat_points = @point_filler = nil
769
+ super
770
+ end
771
+
772
+ def clear?
773
+ @flat_points ? false : super
774
+ end
775
+
776
+ # Makes the points, so the array holds no filler (Marshal cannot
777
+ # dump generated code).
778
+ def settle_points
779
+ materialize_points if @flat_points
780
+ end
781
+
782
+ def assign(array)
783
+ return if equal?(array)
784
+
785
+ @flat_points = @point_filler = nil
786
+ super
787
+ end
788
+
789
+ # The coordinates of the given points as one list of Integers, the
790
+ # values `assign` would store; nil when any is not an Integer or
791
+ # there are no points, leaving those to the element-by-element copy.
792
+ def self.coords_of(points)
793
+ return if points.empty?
794
+
795
+ flat = Array.new(points.size * 2)
796
+ points.each_with_index do |source, index|
797
+ x, y = source.is_a?(Hash) ? [source[:x], source[:y]] : [source.x, source.y]
798
+ flat[index * 2] = x.respond_to?(:snapshot) ? x.snapshot : x
799
+ flat[(index * 2) + 1] = y.respond_to?(:snapshot) ? y.snapshot : y
800
+ end
801
+ return unless flat.all?(Integer)
802
+
803
+ flat
804
+ end
805
+
806
+ private
807
+
808
+ def elements
809
+ materialize_points if @flat_points
810
+ super
811
+ end
812
+
813
+ # Build the full element list before publishing it, so a concurrent
814
+ # reader sees either the flat list or complete elements.
815
+ def materialize_points
816
+ list = @point_filler.points_from(self, @flat_points)
817
+ @elements = list
818
+ @flat_points = @point_filler = nil
819
+ end
820
+ end
821
+
822
+ # Builds BinData points from the filler's point template (`@point`
823
+ # and its x and y leaves, set up by PointArrayFill).
824
+ module PointCopying
825
+ # Points as BinData elements, built from one flat coordinate list.
826
+ def points_from(array, flat)
827
+ Array.new(flat.size / 2) do |index|
828
+ point, x, y = point_copy(array)
829
+ x.assign(flat[index * 2])
830
+ y.assign(flat[(index * 2) + 1])
831
+ point
832
+ end
833
+ end
834
+
835
+ private
836
+
837
+ def point_copy(array)
838
+ point = @point.dup
839
+ x = @x_leaf.dup
840
+ y = @y_leaf.dup
841
+ point.instance_variable_set(:@parent, array)
842
+ x.instance_variable_set(:@parent, point)
843
+ y.instance_variable_set(:@parent, point)
844
+ point.instance_variable_set(:@field_objs, [x, y])
845
+ [point, x, y]
846
+ end
847
+
848
+ def copy_point(array, source)
849
+ point, x, y = point_copy(array)
850
+ x.assign(source.is_a?(Hash) ? source[:x] : source.x)
851
+ y.assign(source.is_a?(Hash) ? source[:y] : source.y)
852
+ point
853
+ end
854
+ end
855
+
856
+ # Header plus one counted array of PointL (32-bit) or PointS (16-bit)
857
+ # points. The array field, its count field, the point class, the
858
+ # unpack directive and the coordinate range are all read from the
859
+ # record template, so one class serves every such record. The header is unpacked and assigned like a fixed layout; the points
860
+ # come from one `unpack` and are built by copying a PointL template,
861
+ # so the array holds the same BinData elements a read produces.
862
+ # `call` returns nil when the count overruns the bytes, and `pack`
863
+ # when a coordinate is not an int32, leaving those to BinData.
864
+ class PointArrayFill < FastFill
865
+ include PointCopying
866
+ include FlatPointsCheck
867
+
868
+ LAYOUTS = {
869
+ "BinData::Int32le" => { directive: "l<", bytes: 8, range: (-(2**31))..((2**31) - 1) },
870
+ "BinData::Int16le" => { directive: "s<", bytes: 4, range: (-(2**15))..((2**15) - 1) }
871
+ }.freeze
872
+ private_constant :LAYOUTS
873
+
874
+ def initialize(klass)
875
+ super
876
+ array = @template.public_send(array_name)
877
+ @point = array.send(:new_element)
878
+ @point_class = @point.class
879
+ @directive, @point_bytes, @range = LAYOUTS.fetch(@point.x.class.name).values_at(:directive, :bytes, :range)
880
+ @x_leaf = @point.x
881
+ @y_leaf = @point.y
882
+ # The sample's num_bytes built the array's (empty) element list and
883
+ # cached evaluators that read the template's cptl; copies share
884
+ # neither.
885
+ array.instance_variable_set(:@elements, nil)
886
+ [@template, array, @point, @x_leaf, @y_leaf].each { |object| WireCopy.forget_evaluator(object) }
887
+ array.extend(FlatPoints)
888
+ @pack_points_format = "#{@format}#{@directive}*"
889
+ @hold_flat = true
890
+ @hold_flat = verify_flat_points
891
+ end
892
+
893
+ # `nil` when the count overruns the n_size bytes of the record.
894
+ def call_at(data, offset, n_size)
895
+ consumed = @size + (data.unpack1("V", offset: offset + @count_offset) * @point_bytes)
896
+ return if n_size < consumed
897
+
898
+ [lazy_wire(data, offset), suffix(data, offset, n_size, consumed)]
899
+ end
900
+
901
+ def fits?(data, offset, n_size)
902
+ record_size(data, offset) <= n_size
903
+ end
904
+
905
+ def record_size(data, base)
906
+ @size + (data.unpack1("V", offset: base + @count_offset) * @point_bytes)
907
+ end
908
+
909
+ # The array field of a lazy wire: its points, held flat or built.
910
+ def fill_unvalued(array, data, base)
911
+ count = data.unpack1("V", offset: base + @count_offset)
912
+ flat = data.unpack("#{@directive}#{count * 2}", offset: base + @size)
913
+ if @hold_flat
914
+ array.hold_flat_points(flat, self)
915
+ else
916
+ array.instance_variable_set(:@elements, points_from(array, flat))
917
+ end
918
+ end
919
+
920
+ # Keyword construction; the points are built from template copies
921
+ # when every element is a PointL or an {x:, y:} hash, else BinData's.
922
+ def build(attrs)
923
+ points = attrs[array_name]
924
+ return super unless points.is_a?(::Array) && points.all? { |source| fast_point?(source) }
925
+
926
+ wire = super(attrs.except(array_name))
927
+ return unless wire
928
+
929
+ array = wire.public_send(array_name)
930
+ flat = FlatPoints.coords_of(points)
931
+ if flat && @hold_flat
932
+ array.hold_flat_points(flat, self)
933
+ else
934
+ array.instance_variable_set(:@elements, points.map { |source| copy_point(array, source) })
935
+ end
936
+ wire
937
+ end
938
+
939
+ # [point class, coordinate directive, low, high, bytes per coordinate, count field]
940
+ def points_layout
941
+ [@point_class, @directive, @range.begin, @range.end, @point_bytes / 2, count_name]
942
+ end
943
+
944
+ def pack(wire)
945
+ array = wire.public_send(array_name)
946
+ coords = array.instance_variable_get(:@flat_points) || collect_coords(array)
947
+ return unless coords.all?(Integer) && coords.all?(@range)
948
+
949
+ header_values(wire).concat(coords).pack(@pack_points_format)
950
+ end
951
+
952
+ private
953
+
954
+ def collect_coords(array)
955
+ array.each_with_object([]) { |point, coords| coords << point.x.value << point.y.value }
956
+ end
957
+
958
+ # Two points, so the counted array is exercised by the self-check.
959
+ def verification_record
960
+ wire = super
961
+ wire.public_send(count_name).assign(2)
962
+ wire.public_send(array_name).assign([{ x: 3, y: 4 }, { x: -5, y: 6 }])
963
+ wire
964
+ end
965
+
966
+ # The record's one counted array field.
967
+ def array_name
968
+ @array_name ||= @template.field_names.find { |name| @template.public_send(name).is_a?(BinData::Array) }
969
+ end
970
+
971
+ def count_name
972
+ @template.public_send(array_name).send(:get_parameter, :initial_length)
973
+ end
974
+
975
+ def leaf_paths(record, prefix)
976
+ super.reject { |_, field| field <= BinData::Array }
977
+ end
978
+
979
+ def fast_point?(point)
980
+ point.instance_of?(@point_class) ||
981
+ (point.instance_of?(Hash) && point.size == 2 && point.key?(:x) && point.key?(:y))
982
+ end
983
+
984
+ def generate_call(leaves)
985
+ count = leaves.index { |path, _| path == [count_name] }
986
+ @count_offset = @leaf_reads.fetch(count).last
987
+ end
988
+
989
+ def generate_pack(leaves)
990
+ reads = leaves.map { |path, _| "#{accessor_chain(path)}.value" }
991
+ define_generated(:header_values, "def header_values(wire)\n [#{reads.join(', ')}]\nend\n")
992
+ end
993
+ end
994
+
995
+ POINT_ARRAY_TYPES = [
996
+ TypeCodes::POLYGON, TypeCodes::POLYLINE, TypeCodes::POLYBEZIER, TypeCodes::POLYBEZIERTO,
997
+ TypeCodes::POLYLINETO, TypeCodes::POLYGON16, TypeCodes::POLYLINE16,
998
+ TypeCodes::POLYBEZIER16, TypeCodes::POLYBEZIERTO16, TypeCodes::POLYLINETO16
999
+ ].freeze
1000
+ POINT_ARRAY_BY_TYPE = POINT_ARRAY_TYPES.to_h do |type_id|
1001
+ [type_id, PointArrayFill.guarded(Records.lookup(type_id))]
1002
+ end.compact.freeze
1003
+ POINT_ARRAY_BY_CLASS = POINT_ARRAY_BY_TYPE.values.to_h { |filler| [filler.wire_class, filler] }.freeze
1004
+ private_constant :POINT_ARRAY_TYPES, :POINT_ARRAY_BY_TYPE, :POINT_ARRAY_BY_CLASS
1005
+
1006
+ # The filler for a header plus point array record, which parses only
1007
+ # when its count fits the bytes.
1008
+ def self.for_points(type_id)
1009
+ POINT_ARRAY_BY_TYPE[type_id]
1010
+ end
1011
+ end
1012
+
1013
+ class FastFill
1014
+ # EMR_EXTTEXTOUTA/W: a fixed header, an optional clip rectangle (absent
1015
+ # when f_options has ETO_NO_RECT, 0x100) and `rest :trailing`, which
1016
+ # takes every remaining byte, so a parse never leaves a suffix. The
1017
+ # option bits are read from the bytes to pick the with-rect or
1018
+ # without-rect layout; both unpack the header in one call and copy
1019
+ # one template, the rect leaves just staying at their defaults when
1020
+ # absent, as BinData leaves them. `call` returns nil when the bytes
1021
+ # are too short for the layout they select, and `pack` when a scale is
1022
+ # NaN (Float32Le keeps NaN bits) or trailing is not a String, leaving
1023
+ # those to BinData.
1024
+ class TextFill < FastFill
1025
+ NO_RECT = 0x100
1026
+ OPTIONS_OFFSET = 52
1027
+ FLOAT_DIRECTIVE = "e"
1028
+ private_constant :NO_RECT, :OPTIONS_OFFSET, :FLOAT_DIRECTIVE
1029
+
1030
+ def initialize(klass)
1031
+ super
1032
+ @rect_size = @size
1033
+ @size = @rect_size - rect_bytes
1034
+ forget_evaluators(@template)
1035
+ end
1036
+
1037
+ def call_at(data, offset, n_size)
1038
+ call(data.byteslice(offset, n_size))
1039
+ end
1040
+
1041
+ # A text record's layout varies with its options, so `call_at` reads it.
1042
+ def exact_wire(_data, _offset, _n_size); end
1043
+
1044
+ def call(record_bytes)
1045
+ return if record_bytes.bytesize < @size
1046
+
1047
+ if record_bytes.unpack1("V", offset: OPTIONS_OFFSET).anybits?(NO_RECT)
1048
+ read_plain(record_bytes)
1049
+ elsif record_bytes.bytesize >= @rect_size
1050
+ read_rect(record_bytes)
1051
+ end
1052
+ end
1053
+
1054
+ def pack(wire)
1055
+ options = wire.f_options.value
1056
+ trailing = wire.trailing.value
1057
+ return unless options.is_a?(Integer) && trailing.is_a?(::String)
1058
+ return unless [wire.ex_scale.value, wire.ey_scale.value].all? { |v| v.is_a?(Numeric) && !(v.is_a?(Float) && v.nan?) }
1059
+
1060
+ if options.anybits?(NO_RECT)
1061
+ [*plain_values(wire), trailing].pack("#{@plain_pack_format}a*")
1062
+ else
1063
+ [*rect_values(wire), trailing].pack("#{@rect_pack_format}a*")
1064
+ end
1065
+ end
1066
+
1067
+ private
1068
+
1069
+ def rect_bytes
1070
+ @template.rcl.num_bytes
1071
+ end
1072
+
1073
+ def forget_evaluators(object)
1074
+ WireCopy.forget_evaluator(object)
1075
+ object.instance_variable_get(:@field_objs).each do |field|
1076
+ field.respond_to?(:field_names) ? forget_evaluators(field) : WireCopy.forget_evaluator(field)
1077
+ end
1078
+ end
1079
+
1080
+ def leaf_paths(record, prefix)
1081
+ super.reject { |path, _| path == [:trailing] }
1082
+ end
1083
+
1084
+ # Distinct trailing bytes, so the rest field is exercised.
1085
+ def verification_record
1086
+ wire = super
1087
+ wire.trailing.assign("abc".b)
1088
+ wire
1089
+ end
1090
+
1091
+ def verification_tail
1092
+ "".b
1093
+ end
1094
+
1095
+ # The base self-check covers the with-rect layout; this repeats the
1096
+ # read and write comparison with ETO_NO_RECT set, where BinData
1097
+ # skips the rect.
1098
+ def verify_write(sample)
1099
+ super
1100
+ sample.f_options.assign(NO_RECT)
1101
+ bytes = bindata_bytes(sample)
1102
+ unverified!("no-rect layout is not shorter") unless bytes.bytesize == @size + 3
1103
+ wire, rest = call(bytes)
1104
+ expected = @wire_class.read(bytes)
1105
+ same = wire.snapshot == expected.snapshot && bindata_bytes(wire) == bytes && rest.empty?
1106
+ unverified!("no-rect call differs from read") unless same
1107
+ unverified!("no-rect pack differs from BinData's writer") unless pack(sample) == bytes
1108
+ end
1109
+
1110
+ def generate_call(leaves)
1111
+ plain = leaves.reject { |path, _| path.first == :rcl }
1112
+ @rect_format = @format
1113
+ @plain_format = plain.map { |_, field| DIRECTIVES.fetch(field.name) }.join
1114
+ @rect_pack_format = pack_format(leaves)
1115
+ @plain_pack_format = pack_format(plain)
1116
+ define_reader(:read_rect, leaves, @rect_format, :@rect_size)
1117
+ define_reader(:read_plain, plain, @plain_format, :@size)
1118
+ define_values(:rect_values, leaves)
1119
+ define_values(:plain_values, plain)
1120
+ end
1121
+
1122
+ def generate_pack(_leaves); end
1123
+
1124
+ # Scales are packed as floats, everything else as in the read format.
1125
+ def pack_format(leaves)
1126
+ leaves.map { |_, field| float?(field) ? FLOAT_DIRECTIVE : DIRECTIVES.fetch(field.name) }.join
1127
+ end
1128
+
1129
+ def define_reader(name, leaves, format, size)
1130
+ locals = leaves.each_index.map { |i| "v#{i}" }
1131
+ lines = leaves.each_with_index.map do |(path, field), i|
1132
+ "#{accessor_chain(path)}.#{float?(field) ? 'assign_bits' : 'assign'}(v#{i})"
1133
+ end
1134
+ define_generated(name, <<~RUBY)
1135
+ def #{name}(record_bytes)
1136
+ #{locals.join(', ')} = record_bytes.unpack("#{format}")
1137
+ wire = copy_template
1138
+ #{lines.join("\n ")}
1139
+ wire.trailing.assign(record_bytes.byteslice(#{size}, record_bytes.bytesize - #{size}))
1140
+ [wire, EMPTY_SUFFIX]
1141
+ end
1142
+ RUBY
1143
+ end
1144
+
1145
+ # Pack-ready values; scale leaves are read as floats, not bits.
1146
+ def define_values(name, leaves)
1147
+ reads = leaves.map { |path, _| "#{accessor_chain(path)}.value" }
1148
+ define_generated(name, "def #{name}(wire)\n [#{reads.join(', ')}]\nend\n")
1149
+ end
1150
+ end
1151
+
1152
+ TEXT_TYPES = [TypeCodes::EXTTEXTOUTA, TypeCodes::EXTTEXTOUTW].freeze
1153
+ TEXT_BY_TYPE = TEXT_TYPES.to_h { |type_id| [type_id, TextFill.guarded(Records.lookup(type_id))] }.compact.freeze
1154
+ TEXT_BY_CLASS = TEXT_BY_TYPE.values.to_h { |filler| [filler.wire_class, filler] }.freeze
1155
+ private_constant :TEXT_TYPES, :TEXT_BY_TYPE, :TEXT_BY_CLASS
1156
+
1157
+ # The filler for the text-out records, whose layout depends on f_options.
1158
+ def self.for_text(type_id)
1159
+ TEXT_BY_TYPE[type_id]
1160
+ end
1161
+ end
1162
+ end
1163
+ end
1164
+ end