svg_conform 0.1.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 (335) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/rake.yml +15 -0
  3. data/.github/workflows/release.yml +23 -0
  4. data/.github/workflows/svgcheck-compatibility.yml +135 -0
  5. data/.gitignore +12 -0
  6. data/.rspec +3 -0
  7. data/.rubocop.yml +13 -0
  8. data/.rubocop_todo.yml +183 -0
  9. data/CODE_OF_CONDUCT.md +132 -0
  10. data/Gemfile +14 -0
  11. data/README.adoc +1384 -0
  12. data/Rakefile +15 -0
  13. data/config/profiles/base.yml +34 -0
  14. data/config/profiles/lucid_fix.yml +37 -0
  15. data/config/profiles/metanorma.yml +212 -0
  16. data/config/profiles/no_external_css.yml +20 -0
  17. data/config/profiles/svg_1_2_rfc.yml +284 -0
  18. data/config/profiles/svg_1_2_rfc_with_rdf.yml +145 -0
  19. data/config/svgcheck_mapping.yml +180 -0
  20. data/docs/profiles.adoc +547 -0
  21. data/docs/rdf_metadata_support.adoc +212 -0
  22. data/docs/remediation.adoc +732 -0
  23. data/docs/requirements.adoc +709 -0
  24. data/examples/demo.rb +116 -0
  25. data/examples/requirements_demo.rb +240 -0
  26. data/exe/svg_conform +7 -0
  27. data/lib/svg_conform/batch_report.rb +70 -0
  28. data/lib/svg_conform/cli.rb +107 -0
  29. data/lib/svg_conform/commands/check.rb +390 -0
  30. data/lib/svg_conform/commands/profiles.rb +118 -0
  31. data/lib/svg_conform/commands/svgcheck.rb +90 -0
  32. data/lib/svg_conform/commands/svgcheck_compare.rb +92 -0
  33. data/lib/svg_conform/commands/svgcheck_compatibility.rb +43 -0
  34. data/lib/svg_conform/commands/svgcheck_generate.rb +262 -0
  35. data/lib/svg_conform/compatibility/analysis_context.rb +68 -0
  36. data/lib/svg_conform/compatibility/comparison_result.rb +147 -0
  37. data/lib/svg_conform/compatibility/compatibility_analyzer.rb +85 -0
  38. data/lib/svg_conform/compatibility/file_processor.rb +109 -0
  39. data/lib/svg_conform/compatibility/pattern_discovery.rb +319 -0
  40. data/lib/svg_conform/compatibility/report_formatter.rb +359 -0
  41. data/lib/svg_conform/compatibility/svg_analysis_engine.rb +316 -0
  42. data/lib/svg_conform/compatibility/validity_analysis.rb +252 -0
  43. data/lib/svg_conform/compatibility/xml_analysis_engine.rb +198 -0
  44. data/lib/svg_conform/compatibility_analyzer.rb +285 -0
  45. data/lib/svg_conform/conformance_report.rb +267 -0
  46. data/lib/svg_conform/constants.rb +199 -0
  47. data/lib/svg_conform/css_color.rb +262 -0
  48. data/lib/svg_conform/document.rb +203 -0
  49. data/lib/svg_conform/external_checkers/svgcheck/compatibility_engine.rb +166 -0
  50. data/lib/svg_conform/external_checkers/svgcheck/output_generator.rb +101 -0
  51. data/lib/svg_conform/external_checkers/svgcheck/parser.rb +200 -0
  52. data/lib/svg_conform/external_checkers/svgcheck/report_comparator.rb +175 -0
  53. data/lib/svg_conform/external_checkers/svgcheck/report_generator.rb +82 -0
  54. data/lib/svg_conform/external_checkers/svgcheck/validation_pipeline.rb +249 -0
  55. data/lib/svg_conform/external_checkers/svgcheck.rb +56 -0
  56. data/lib/svg_conform/external_checkers.rb +34 -0
  57. data/lib/svg_conform/fixer.rb +56 -0
  58. data/lib/svg_conform/profile.rb +164 -0
  59. data/lib/svg_conform/profiles.rb +60 -0
  60. data/lib/svg_conform/remediation_engine.rb +92 -0
  61. data/lib/svg_conform/remediation_result.rb +36 -0
  62. data/lib/svg_conform/remediation_runner.rb +225 -0
  63. data/lib/svg_conform/remediations/base_remediation.rb +165 -0
  64. data/lib/svg_conform/remediations/color_remediation.rb +226 -0
  65. data/lib/svg_conform/remediations/font_embedding_remediation.rb +145 -0
  66. data/lib/svg_conform/remediations/font_remediation.rb +122 -0
  67. data/lib/svg_conform/remediations/image_embedding_remediation.rb +154 -0
  68. data/lib/svg_conform/remediations/invalid_id_references_remediation.rb +129 -0
  69. data/lib/svg_conform/remediations/namespace_attribute_remediation.rb +244 -0
  70. data/lib/svg_conform/remediations/namespace_remediation.rb +151 -0
  71. data/lib/svg_conform/remediations/no_external_css_remediation.rb +192 -0
  72. data/lib/svg_conform/remediations/style_promotion_remediation.rb +93 -0
  73. data/lib/svg_conform/remediations/viewbox_remediation.rb +127 -0
  74. data/lib/svg_conform/remediations.rb +40 -0
  75. data/lib/svg_conform/report_comparator.rb +772 -0
  76. data/lib/svg_conform/requirements/allowed_elements_requirement.rb +367 -0
  77. data/lib/svg_conform/requirements/base_requirement.rb +98 -0
  78. data/lib/svg_conform/requirements/color_restrictions_requirement.rb +126 -0
  79. data/lib/svg_conform/requirements/element_requirement_config.rb +75 -0
  80. data/lib/svg_conform/requirements/font_family_requirement.rb +133 -0
  81. data/lib/svg_conform/requirements/forbidden_content_requirement.rb +60 -0
  82. data/lib/svg_conform/requirements/id_reference_requirement.rb +133 -0
  83. data/lib/svg_conform/requirements/invalid_id_references_requirement.rb +132 -0
  84. data/lib/svg_conform/requirements/link_validation_requirement.rb +55 -0
  85. data/lib/svg_conform/requirements/namespace_attributes_requirement.rb +211 -0
  86. data/lib/svg_conform/requirements/namespace_requirement.rb +294 -0
  87. data/lib/svg_conform/requirements/no_external_css_requirement.rb +132 -0
  88. data/lib/svg_conform/requirements/no_external_fonts_requirement.rb +121 -0
  89. data/lib/svg_conform/requirements/no_external_images_requirement.rb +91 -0
  90. data/lib/svg_conform/requirements/style_promotion_requirement.rb +72 -0
  91. data/lib/svg_conform/requirements/style_requirement.rb +226 -0
  92. data/lib/svg_conform/requirements/viewbox_required_requirement.rb +96 -0
  93. data/lib/svg_conform/requirements.rb +49 -0
  94. data/lib/svg_conform/semantic_comparator.rb +829 -0
  95. data/lib/svg_conform/validation_context.rb +408 -0
  96. data/lib/svg_conform/validation_result.rb +146 -0
  97. data/lib/svg_conform/validator.rb +91 -0
  98. data/lib/svg_conform/version.rb +5 -0
  99. data/lib/svg_conform.rb +68 -0
  100. data/lib/tasks/fixtures.rake +321 -0
  101. data/lib/tasks/svgcheck.rake +111 -0
  102. data/reference-docs/SVG-1.2-RFC.rnc.txt +1676 -0
  103. data/reference-docs/Scalable Vector Graphics (SVG) 1.1 (Second Edition).html +40764 -0
  104. data/reference-docs/Scalable Vector Graphics (SVG) Tiny 1.2 Specification.html +44591 -0
  105. data/reference-docs/rfc7996.txt +2971 -0
  106. data/sig/svg_conform.rbs +4 -0
  107. data/spec/fixtures/allowed_elements/inputs/basic_violations.svg +21 -0
  108. data/spec/fixtures/allowed_elements/repair/basic_violations.svg +18 -0
  109. data/spec/fixtures/color_restrictions/inputs/basic_violations.svg +23 -0
  110. data/spec/fixtures/color_restrictions/repair/basic_violations.svg +23 -0
  111. data/spec/fixtures/comprehensive/inputs/multiple_violations.svg +21 -0
  112. data/spec/fixtures/comprehensive/repair/multiple_violations.svg +16 -0
  113. data/spec/fixtures/font_family/inputs/basic_violations.svg +17 -0
  114. data/spec/fixtures/font_family/repair/basic_violations.svg +17 -0
  115. data/spec/fixtures/forbidden_content/inputs/basic_violations.svg +27 -0
  116. data/spec/fixtures/forbidden_content/repair/basic_violations.svg +19 -0
  117. data/spec/fixtures/id_reference/inputs/basic_violations.svg +17 -0
  118. data/spec/fixtures/id_reference/repair/basic_violations.svg +15 -0
  119. data/spec/fixtures/link_validation/inputs/basic_violations.svg +20 -0
  120. data/spec/fixtures/link_validation/repair/basic_violations.svg +16 -0
  121. data/spec/fixtures/lucid/inputs/simple.svg +67 -0
  122. data/spec/fixtures/lucid/repair/simple.svg +63 -0
  123. data/spec/fixtures/namespace/inputs/basic_violations.svg +20 -0
  124. data/spec/fixtures/namespace/repair/basic_violations.svg +17 -0
  125. data/spec/fixtures/namespace_attributes/inputs/basic_violations.svg +16 -0
  126. data/spec/fixtures/namespace_attributes/repair/basic_violations.svg +15 -0
  127. data/spec/fixtures/no_external_css/inputs/basic_violations.svg +20 -0
  128. data/spec/fixtures/no_external_css/repair/basic_violations.svg +18 -0
  129. data/spec/fixtures/style/inputs/basic_violations.svg +22 -0
  130. data/spec/fixtures/style/repair/basic_violations.svg +22 -0
  131. data/spec/fixtures/style_promotion/inputs/basic_test.svg +15 -0
  132. data/spec/fixtures/style_promotion/repair/basic_test.svg +15 -0
  133. data/spec/fixtures/svg_1_2_rfc/inputs/allowed_elements_violations.svg +18 -0
  134. data/spec/fixtures/svg_1_2_rfc/inputs/color_restrictions_violations.svg +23 -0
  135. data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.code +1 -0
  136. data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.err +0 -0
  137. data/spec/fixtures/svgcheck/check/DrawBerry-sample-2.svg.out +23 -0
  138. data/spec/fixtures/svgcheck/check/IETF-test.svg.code +1 -0
  139. data/spec/fixtures/svgcheck/check/IETF-test.svg.err +0 -0
  140. data/spec/fixtures/svgcheck/check/IETF-test.svg.out +20 -0
  141. data/spec/fixtures/svgcheck/check/circle.svg.code +1 -0
  142. data/spec/fixtures/svgcheck/check/circle.svg.err +0 -0
  143. data/spec/fixtures/svgcheck/check/circle.svg.out +2 -0
  144. data/spec/fixtures/svgcheck/check/colors.svg.code +1 -0
  145. data/spec/fixtures/svgcheck/check/colors.svg.err +0 -0
  146. data/spec/fixtures/svgcheck/check/colors.svg.out +13 -0
  147. data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.code +1 -0
  148. data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.err +0 -0
  149. data/spec/fixtures/svgcheck/check/dia-sample-svg.svg.out +76 -0
  150. data/spec/fixtures/svgcheck/check/example-dot.svg.code +1 -0
  151. data/spec/fixtures/svgcheck/check/example-dot.svg.err +0 -0
  152. data/spec/fixtures/svgcheck/check/example-dot.svg.out +11 -0
  153. data/spec/fixtures/svgcheck/check/full-tiny.svg.code +1 -0
  154. data/spec/fixtures/svgcheck/check/full-tiny.svg.err +0 -0
  155. data/spec/fixtures/svgcheck/check/full-tiny.svg.out +5835 -0
  156. data/spec/fixtures/svgcheck/check/good.svg.code +1 -0
  157. data/spec/fixtures/svgcheck/check/good.svg.err +0 -0
  158. data/spec/fixtures/svgcheck/check/good.svg.out +1 -0
  159. data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.code +1 -0
  160. data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.err +0 -0
  161. data/spec/fixtures/svgcheck/check/httpbis-proxy20-fig6.svg.out +5 -0
  162. data/spec/fixtures/svgcheck/check/malformed.svg.code +1 -0
  163. data/spec/fixtures/svgcheck/check/malformed.svg.err +0 -0
  164. data/spec/fixtures/svgcheck/check/malformed.svg.out +8 -0
  165. data/spec/fixtures/svgcheck/check/rfc-svg.svg.code +1 -0
  166. data/spec/fixtures/svgcheck/check/rfc-svg.svg.err +0 -0
  167. data/spec/fixtures/svgcheck/check/rfc-svg.svg.out +1 -0
  168. data/spec/fixtures/svgcheck/check/rfc.xml.code +1 -0
  169. data/spec/fixtures/svgcheck/check/rfc.xml.err +0 -0
  170. data/spec/fixtures/svgcheck/check/rfc.xml.out +2 -0
  171. data/spec/fixtures/svgcheck/check/rgb.svg.code +1 -0
  172. data/spec/fixtures/svgcheck/check/rgb.svg.err +0 -0
  173. data/spec/fixtures/svgcheck/check/rgb.svg.out +9 -0
  174. data/spec/fixtures/svgcheck/check/svg-wordle.svg.code +1 -0
  175. data/spec/fixtures/svgcheck/check/svg-wordle.svg.err +0 -0
  176. data/spec/fixtures/svgcheck/check/svg-wordle.svg.out +508 -0
  177. data/spec/fixtures/svgcheck/check/threshold.svg.code +1 -0
  178. data/spec/fixtures/svgcheck/check/threshold.svg.err +0 -0
  179. data/spec/fixtures/svgcheck/check/threshold.svg.out +20 -0
  180. data/spec/fixtures/svgcheck/check/utf8.svg.code +1 -0
  181. data/spec/fixtures/svgcheck/check/utf8.svg.err +0 -0
  182. data/spec/fixtures/svgcheck/check/utf8.svg.out +162 -0
  183. data/spec/fixtures/svgcheck/check/viewBox-both.svg.code +1 -0
  184. data/spec/fixtures/svgcheck/check/viewBox-both.svg.err +0 -0
  185. data/spec/fixtures/svgcheck/check/viewBox-both.svg.out +4 -0
  186. data/spec/fixtures/svgcheck/check/viewBox-height.svg.code +1 -0
  187. data/spec/fixtures/svgcheck/check/viewBox-height.svg.err +0 -0
  188. data/spec/fixtures/svgcheck/check/viewBox-height.svg.out +3 -0
  189. data/spec/fixtures/svgcheck/check/viewBox-none.svg.code +1 -0
  190. data/spec/fixtures/svgcheck/check/viewBox-none.svg.err +0 -0
  191. data/spec/fixtures/svgcheck/check/viewBox-none.svg.out +3 -0
  192. data/spec/fixtures/svgcheck/check/viewBox-width.svg.code +1 -0
  193. data/spec/fixtures/svgcheck/check/viewBox-width.svg.err +0 -0
  194. data/spec/fixtures/svgcheck/check/viewBox-width.svg.out +3 -0
  195. data/spec/fixtures/svgcheck/inputs/DrawBerry-sample-2.svg +28 -0
  196. data/spec/fixtures/svgcheck/inputs/IETF-test.svg +28 -0
  197. data/spec/fixtures/svgcheck/inputs/circle.svg +3 -0
  198. data/spec/fixtures/svgcheck/inputs/colors.svg +18 -0
  199. data/spec/fixtures/svgcheck/inputs/dia-sample-svg.svg +47 -0
  200. data/spec/fixtures/svgcheck/inputs/example-dot.svg +75 -0
  201. data/spec/fixtures/svgcheck/inputs/full-tiny.svg +16194 -0
  202. data/spec/fixtures/svgcheck/inputs/good.svg +19 -0
  203. data/spec/fixtures/svgcheck/inputs/httpbis-proxy20-fig6.svg +2 -0
  204. data/spec/fixtures/svgcheck/inputs/malformed.svg +11 -0
  205. data/spec/fixtures/svgcheck/inputs/rfc-svg.svg +1028 -0
  206. data/spec/fixtures/svgcheck/inputs/rfc.xml +37 -0
  207. data/spec/fixtures/svgcheck/inputs/rgb.svg +9 -0
  208. data/spec/fixtures/svgcheck/inputs/svg-wordle.svg +330 -0
  209. data/spec/fixtures/svgcheck/inputs/threshold.svg +26 -0
  210. data/spec/fixtures/svgcheck/inputs/utf8.svg +448 -0
  211. data/spec/fixtures/svgcheck/inputs/viewBox-both.svg +3 -0
  212. data/spec/fixtures/svgcheck/inputs/viewBox-height.svg +3 -0
  213. data/spec/fixtures/svgcheck/inputs/viewBox-none.svg +3 -0
  214. data/spec/fixtures/svgcheck/inputs/viewBox-width.svg +3 -0
  215. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.code +1 -0
  216. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.err +0 -0
  217. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.file +0 -0
  218. data/spec/fixtures/svgcheck/repair/DrawBerry-sample-2.svg.out +23 -0
  219. data/spec/fixtures/svgcheck/repair/IETF-test.svg.code +1 -0
  220. data/spec/fixtures/svgcheck/repair/IETF-test.svg.err +0 -0
  221. data/spec/fixtures/svgcheck/repair/IETF-test.svg.file +29 -0
  222. data/spec/fixtures/svgcheck/repair/IETF-test.svg.out +20 -0
  223. data/spec/fixtures/svgcheck/repair/circle.svg.code +1 -0
  224. data/spec/fixtures/svgcheck/repair/circle.svg.err +0 -0
  225. data/spec/fixtures/svgcheck/repair/circle.svg.file +4 -0
  226. data/spec/fixtures/svgcheck/repair/circle.svg.out +2 -0
  227. data/spec/fixtures/svgcheck/repair/colors.svg.code +1 -0
  228. data/spec/fixtures/svgcheck/repair/colors.svg.err +0 -0
  229. data/spec/fixtures/svgcheck/repair/colors.svg.file +19 -0
  230. data/spec/fixtures/svgcheck/repair/colors.svg.out +13 -0
  231. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.code +1 -0
  232. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.err +0 -0
  233. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.file +47 -0
  234. data/spec/fixtures/svgcheck/repair/dia-sample-svg.svg.out +76 -0
  235. data/spec/fixtures/svgcheck/repair/example-dot.svg.code +1 -0
  236. data/spec/fixtures/svgcheck/repair/example-dot.svg.err +0 -0
  237. data/spec/fixtures/svgcheck/repair/example-dot.svg.file +69 -0
  238. data/spec/fixtures/svgcheck/repair/example-dot.svg.out +11 -0
  239. data/spec/fixtures/svgcheck/repair/good.svg.code +1 -0
  240. data/spec/fixtures/svgcheck/repair/good.svg.err +0 -0
  241. data/spec/fixtures/svgcheck/repair/good.svg.file +0 -0
  242. data/spec/fixtures/svgcheck/repair/good.svg.out +1 -0
  243. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.code +1 -0
  244. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.err +0 -0
  245. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.file +73 -0
  246. data/spec/fixtures/svgcheck/repair/httpbis-proxy20-fig6.svg.out +5 -0
  247. data/spec/fixtures/svgcheck/repair/malformed.svg.code +1 -0
  248. data/spec/fixtures/svgcheck/repair/malformed.svg.err +0 -0
  249. data/spec/fixtures/svgcheck/repair/malformed.svg.file +9 -0
  250. data/spec/fixtures/svgcheck/repair/malformed.svg.out +8 -0
  251. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.code +1 -0
  252. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.err +0 -0
  253. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.file +0 -0
  254. data/spec/fixtures/svgcheck/repair/rfc-svg.svg.out +1 -0
  255. data/spec/fixtures/svgcheck/repair/rfc.xml.code +1 -0
  256. data/spec/fixtures/svgcheck/repair/rfc.xml.err +0 -0
  257. data/spec/fixtures/svgcheck/repair/rfc.xml.file +37 -0
  258. data/spec/fixtures/svgcheck/repair/rfc.xml.out +2 -0
  259. data/spec/fixtures/svgcheck/repair/rgb.svg.code +1 -0
  260. data/spec/fixtures/svgcheck/repair/rgb.svg.err +0 -0
  261. data/spec/fixtures/svgcheck/repair/rgb.svg.file +10 -0
  262. data/spec/fixtures/svgcheck/repair/rgb.svg.out +9 -0
  263. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.code +1 -0
  264. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.err +0 -0
  265. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.file +112 -0
  266. data/spec/fixtures/svgcheck/repair/svg-wordle.svg.out +508 -0
  267. data/spec/fixtures/svgcheck/repair/threshold.svg.code +1 -0
  268. data/spec/fixtures/svgcheck/repair/threshold.svg.err +0 -0
  269. data/spec/fixtures/svgcheck/repair/threshold.svg.file +27 -0
  270. data/spec/fixtures/svgcheck/repair/threshold.svg.out +20 -0
  271. data/spec/fixtures/svgcheck/repair/utf8.svg.code +1 -0
  272. data/spec/fixtures/svgcheck/repair/utf8.svg.err +0 -0
  273. data/spec/fixtures/svgcheck/repair/utf8.svg.file +381 -0
  274. data/spec/fixtures/svgcheck/repair/utf8.svg.out +162 -0
  275. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.code +1 -0
  276. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.err +0 -0
  277. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.file +4 -0
  278. data/spec/fixtures/svgcheck/repair/viewBox-both.svg.out +4 -0
  279. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.code +1 -0
  280. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.err +0 -0
  281. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.file +4 -0
  282. data/spec/fixtures/svgcheck/repair/viewBox-height.svg.out +3 -0
  283. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.code +1 -0
  284. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.err +0 -0
  285. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.file +4 -0
  286. data/spec/fixtures/svgcheck/repair/viewBox-none.svg.out +3 -0
  287. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.code +1 -0
  288. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.err +0 -0
  289. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.file +4 -0
  290. data/spec/fixtures/svgcheck/repair/viewBox-width.svg.out +3 -0
  291. data/spec/fixtures/viewbox_required/inputs/missing_viewbox.svg +10 -0
  292. data/spec/fixtures/viewbox_required/repair/missing_viewbox.svg +10 -0
  293. data/spec/spec_helper.rb +16 -0
  294. data/spec/svg_conform/batch_report_spec.rb +99 -0
  295. data/spec/svg_conform/commands/check_command_spec.rb +90 -0
  296. data/spec/svg_conform/commands/profiles_command_spec.rb +20 -0
  297. data/spec/svg_conform/commands/svgcheck_compare_command_spec.rb +13 -0
  298. data/spec/svg_conform/commands/svgcheck_compatibility_command_spec.rb +13 -0
  299. data/spec/svg_conform/commands/svgcheck_generate_command_spec.rb +14 -0
  300. data/spec/svg_conform/profiles/base_profile_spec.rb +42 -0
  301. data/spec/svg_conform/profiles/lucid_fix_profile_spec.rb +46 -0
  302. data/spec/svg_conform/profiles/lucid_profile_spec.rb +84 -0
  303. data/spec/svg_conform/profiles/metanorma_profile_spec.rb +62 -0
  304. data/spec/svg_conform/profiles/no_external_css_profile_spec.rb +66 -0
  305. data/spec/svg_conform/profiles/svg_1_2_rfc_profile_spec.rb +200 -0
  306. data/spec/svg_conform/profiles/svg_1_2_rfc_with_rdf_profile_spec.rb +81 -0
  307. data/spec/svg_conform/remediations/color_remediation_spec.rb +95 -0
  308. data/spec/svg_conform/remediations/font_embedding_remediation_spec.rb +20 -0
  309. data/spec/svg_conform/remediations/font_remediation_spec.rb +95 -0
  310. data/spec/svg_conform/remediations/image_embedding_remediation_spec.rb +20 -0
  311. data/spec/svg_conform/remediations/invalid_id_references_remediation_spec.rb +97 -0
  312. data/spec/svg_conform/remediations/namespace_attribute_remediation_spec.rb +97 -0
  313. data/spec/svg_conform/remediations/namespace_remediation_spec.rb +95 -0
  314. data/spec/svg_conform/remediations/no_external_css_remediation_spec.rb +97 -0
  315. data/spec/svg_conform/remediations/style_promotion_remediation_spec.rb +97 -0
  316. data/spec/svg_conform/remediations/viewbox_remediation_spec.rb +95 -0
  317. data/spec/svg_conform/requirements/allowed_elements_requirement_spec.rb +118 -0
  318. data/spec/svg_conform/requirements/color_restrictions_requirement_spec.rb +168 -0
  319. data/spec/svg_conform/requirements/font_family_requirement_spec.rb +188 -0
  320. data/spec/svg_conform/requirements/forbidden_content_requirement_spec.rb +195 -0
  321. data/spec/svg_conform/requirements/id_reference_requirement_spec.rb +78 -0
  322. data/spec/svg_conform/requirements/invalid_id_references_requirement_spec.rb +78 -0
  323. data/spec/svg_conform/requirements/link_validation_requirement_spec.rb +78 -0
  324. data/spec/svg_conform/requirements/namespace_attributes_requirement_spec.rb +86 -0
  325. data/spec/svg_conform/requirements/namespace_requirement_spec.rb +184 -0
  326. data/spec/svg_conform/requirements/no_external_css_requirement_spec.rb +78 -0
  327. data/spec/svg_conform/requirements/no_external_fonts_requirement_spec.rb +20 -0
  328. data/spec/svg_conform/requirements/no_external_images_requirement_spec.rb +20 -0
  329. data/spec/svg_conform/requirements/style_promotion_requirement_spec.rb +78 -0
  330. data/spec/svg_conform/requirements/style_requirement_spec.rb +76 -0
  331. data/spec/svg_conform/requirements/viewbox_required_requirement_spec.rb +165 -0
  332. data/spec/svg_conform_spec.rb +32 -0
  333. data/spec/svgcheck_compatibility_spec.rb +355 -0
  334. data/svg_conform.gemspec +35 -0
  335. metadata +436 -0
@@ -0,0 +1,226 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_remediation"
4
+
5
+ module SvgConform
6
+ module Remediations
7
+ # Remediation action for color-related issues that matches svgcheck behavior
8
+ class ColorRemediation < BaseRemediation
9
+ attribute :type, :string, default: -> { "ColorRemediation" }
10
+
11
+ DEFAULT_COLOR_THRESHOLD = 764 # Default threshold if not specified in requirement
12
+ COLOR_DEFAULT = "black"
13
+
14
+ COLOR_MAP = {
15
+ "rgb(0,0,0)" => "black",
16
+ }.freeze
17
+
18
+ def apply(document, context)
19
+ # Store the context for use in other methods
20
+ @context = context
21
+ @failed_requirements = context[:failed_requirements] if context.is_a?(Hash)
22
+
23
+ changes = []
24
+
25
+ # Find all elements with color attributes
26
+ document.traverse do |node|
27
+ next unless node.respond_to?(:name) && node.name
28
+
29
+ changes.concat(fix_color_attributes(node))
30
+ changes.concat(fix_style_colors(node))
31
+ end
32
+
33
+ changes
34
+ end
35
+
36
+ private
37
+
38
+ def fix_color_attributes(node)
39
+ changes = []
40
+ color_attributes = %w[fill stroke color stop-color flood-color
41
+ lighting-color]
42
+
43
+ color_attributes.each do |attr_name|
44
+ value = get_attribute(node, attr_name)
45
+ next if value.nil? || value.empty?
46
+
47
+ new_color = convert_color_value(value)
48
+ next unless new_color != value
49
+
50
+ set_attribute(node, attr_name, new_color)
51
+ changes << log_change(
52
+ :attribute_modified,
53
+ "The attribute '#{attr_name}' does not allow the value '#{value}', replaced with '#{new_color}'",
54
+ node,
55
+ )
56
+ end
57
+
58
+ changes
59
+ end
60
+
61
+ def fix_style_colors(node)
62
+ changes = []
63
+ style_value = get_attribute(node, "style")
64
+ return changes unless style_value
65
+
66
+ styles = parse_style(style_value)
67
+ color_properties = %w[fill stroke color stop-color flood-color
68
+ lighting-color]
69
+ style_changed = false
70
+
71
+ color_properties.each do |prop|
72
+ value = styles[prop]
73
+ next if value.nil? || value.empty?
74
+
75
+ new_color = convert_color_value(value)
76
+ if new_color != value
77
+ styles[prop] = new_color
78
+ style_changed = true
79
+ end
80
+ end
81
+
82
+ if style_changed
83
+ new_style = build_style(styles)
84
+ set_attribute(node, "style", new_style)
85
+ changes << log_change(
86
+ :attribute_modified,
87
+ "Updated style colors",
88
+ node,
89
+ )
90
+ end
91
+
92
+ changes
93
+ end
94
+
95
+ def convert_color_value(color_value)
96
+ v = color_value.strip
97
+
98
+ # Check if it's already a valid color according to profile
99
+ return color_value if valid_color?(v)
100
+
101
+ # Handle grey/gray conversion - check if they're allowed, otherwise convert to default
102
+ if %w[grey gray GREY GRAY].include?(v)
103
+ return v if valid_color?(v) # If grey is allowed in profile, keep it
104
+
105
+ return COLOR_DEFAULT # Otherwise convert to black to match svgcheck
106
+ end
107
+
108
+ # Check color map first
109
+ return COLOR_MAP[v] if COLOR_MAP.key?(v)
110
+
111
+ # Handle different color formats
112
+ if v.start_with?("rgb(") && v.end_with?(")")
113
+ convert_rgb_color(v)
114
+ elsif v.start_with?("#")
115
+ convert_hex_color(v)
116
+ else
117
+ # Unknown color, convert to default
118
+ COLOR_DEFAULT
119
+ end
120
+ end
121
+
122
+ def valid_color?(color)
123
+ # Get allowed colors from the color restrictions requirement in failed requirements
124
+ color_requirement = find_color_requirement
125
+ if color_requirement.respond_to?(:allowed_colors)
126
+ allowed_colors = color_requirement.allowed_colors
127
+ # Check for exact case-sensitive matches first
128
+ return true if allowed_colors.include?(color)
129
+
130
+ # Check for case-insensitive hex color matches
131
+ if color.match?(/^#[0-9a-fA-F]{6}$/)
132
+ hex_allowed = allowed_colors.select do |c|
133
+ c.match?(/^#[0-9a-fA-F]{6}$/)
134
+ end
135
+ return hex_allowed.any? do |allowed|
136
+ allowed.downcase == color.downcase
137
+ end
138
+ end
139
+
140
+ return false
141
+ end
142
+
143
+ # Fallback to default validation
144
+ %w[black white #ffffff #000000 none inherit
145
+ currentcolor].include?(color.downcase)
146
+ end
147
+
148
+ def find_color_requirement
149
+ return nil unless @failed_requirements
150
+
151
+ @failed_requirements.find do |failure|
152
+ requirement_id = failure.requirement_id || failure.rule&.id
153
+ requirement_id == "color_restrictions"
154
+ end&.rule
155
+ end
156
+
157
+ def convert_rgb_color(rgb_string)
158
+ # Extract RGB values from rgb(r,g,b) or rgb(r%,g%,b%)
159
+ match = rgb_string.match(/rgb\(([^)]+)\)/)
160
+ return COLOR_DEFAULT unless match
161
+
162
+ values = match[1].split(",").map(&:strip)
163
+ return COLOR_DEFAULT unless values.length == 3
164
+
165
+ shade = if values.first.include?("%")
166
+ # Percentage values
167
+ values.sum { |v| v.gsub("%", "").to_f * 255 / 100 }
168
+ else
169
+ # Integer values
170
+ values.sum(&:to_i)
171
+ end
172
+
173
+ shade > color_threshold ? "white" : COLOR_DEFAULT
174
+ end
175
+
176
+ def convert_hex_color(hex_string)
177
+ if hex_string.length == 7 # #rrggbb
178
+ r = hex_string[1..2].to_i(16)
179
+ g = hex_string[3..4].to_i(16)
180
+ b = hex_string[5..6].to_i(16)
181
+ shade = r + g + b
182
+ elsif hex_string.length == 4 # #rgb
183
+ r = hex_string[1].to_i(16) * 17 # Convert single digit to double
184
+ g = hex_string[2].to_i(16) * 17
185
+ b = hex_string[3].to_i(16) * 17
186
+ shade = r + g + b
187
+ else
188
+ return COLOR_DEFAULT
189
+ end
190
+
191
+ shade > color_threshold ? "white" : COLOR_DEFAULT
192
+ end
193
+
194
+ def color_threshold
195
+ # Get threshold from the color requirement configuration, fall back to default
196
+ color_requirement = find_color_requirement
197
+ if color_requirement.respond_to?(:black_and_white_threshold) && color_requirement.black_and_white_threshold
198
+ color_requirement.black_and_white_threshold
199
+ else
200
+ DEFAULT_COLOR_THRESHOLD
201
+ end
202
+ end
203
+
204
+ def parse_style(style_value)
205
+ return {} if style_value.nil? || style_value.empty?
206
+
207
+ styles = {}
208
+ style_value.split(";").each do |declaration|
209
+ next if declaration.strip.empty?
210
+
211
+ parts = declaration.split(":", 2)
212
+ next unless parts.size == 2
213
+
214
+ property = parts[0].strip
215
+ value = parts[1].strip
216
+ styles[property] = value unless property.empty? || value.empty?
217
+ end
218
+ styles
219
+ end
220
+
221
+ def build_style(styles)
222
+ styles.map { |prop, value| "#{prop}:#{value}" }.join(";")
223
+ end
224
+ end
225
+ end
226
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_remediation"
4
+
5
+ module SvgConform
6
+ module Remediations
7
+ # Remediation for embedding external fonts
8
+ # NOTE: This is a placeholder - actual font embedding would require
9
+ # fetching external fonts and converting them to data URIs
10
+ class FontEmbeddingRemediation < BaseRemediation
11
+ attribute :type, :string, default: -> { "FontEmbeddingRemediation" }
12
+
13
+ yaml do
14
+ map "id", to: :id
15
+ map "description", to: :description
16
+ map "type", to: :type
17
+ map "targets", to: :targets
18
+ end
19
+
20
+ def apply(document, _context)
21
+ changes = []
22
+
23
+ # Find all style elements with @font-face rules
24
+ document.xpath("//style").each do |style_node|
25
+ content = style_node.text || ""
26
+ next unless content.include?("@font-face")
27
+
28
+ modified_content = embed_fonts_in_css(content, changes)
29
+ if modified_content != content
30
+ style_node.content = modified_content
31
+ end
32
+ end
33
+
34
+ # Find font-face elements with external references
35
+ document.xpath("//font-face-src/font-face-uri").each do |uri_node|
36
+ href = get_attribute(uri_node,
37
+ "xlink:href") || get_attribute(uri_node, "href")
38
+ next unless href && !href.start_with?("data:", "#")
39
+
40
+ # Convert external font to data URI
41
+ embedded_uri = fetch_and_embed_font(href)
42
+ if embedded_uri
43
+ set_attribute(uri_node, "xlink:href", embedded_uri)
44
+ changes << {
45
+ type: "font_embedded",
46
+ element: "font-face-uri",
47
+ message: "Embedded external font: #{href}",
48
+ }
49
+ else
50
+ changes << {
51
+ type: "font_embedding_failed",
52
+ element: "font-face-uri",
53
+ message: "Failed to embed font: #{href}",
54
+ }
55
+ end
56
+ end
57
+
58
+ changes
59
+ end
60
+
61
+ def can_remediate?
62
+ true
63
+ end
64
+
65
+ private
66
+
67
+ def embed_fonts_in_css(css_content, changes)
68
+ # Match @font-face blocks and replace external URLs with data URIs
69
+ css_content.gsub(/@font-face\s*\{([^}]+)\}/m) do |_font_face_block|
70
+ font_face_content = ::Regexp.last_match(1)
71
+
72
+ # Find src: url(...) declarations
73
+ modified_content = font_face_content.gsub(/src\s*:\s*url\s*\(\s*['"]?([^'")\s]+)['"]?\s*\)/i) do |url_match|
74
+ url = ::Regexp.last_match(1)
75
+
76
+ # Skip if already embedded or internal reference
77
+ next url_match if url.start_with?("data:", "#")
78
+
79
+ # Fetch and embed the font
80
+ embedded_uri = fetch_and_embed_font(url)
81
+ if embedded_uri
82
+ changes << {
83
+ type: "font_embedded",
84
+ element: "style",
85
+ message: "Embedded external font: #{url}",
86
+ }
87
+ "src: url(#{embedded_uri})"
88
+ else
89
+ changes << {
90
+ type: "font_embedding_failed",
91
+ element: "style",
92
+ message: "Failed to embed font: #{url}",
93
+ }
94
+ url_match
95
+ end
96
+ end
97
+
98
+ "@font-face {#{modified_content}}"
99
+ end
100
+ end
101
+
102
+ def fetch_and_embed_font(url)
103
+ require "net/http"
104
+ require "uri"
105
+ require "base64"
106
+
107
+ # Parse URL
108
+ uri = URI.parse(url)
109
+
110
+ # Fetch the font
111
+ response = Net::HTTP.get_response(uri)
112
+ return nil unless response.is_a?(Net::HTTPSuccess)
113
+
114
+ # Get content type or infer from URL
115
+ content_type = response["content-type"] || infer_font_mime_type(url)
116
+
117
+ # Encode as base64
118
+ base64_data = Base64.strict_encode64(response.body)
119
+
120
+ # Return data URI
121
+ "data:#{content_type};base64,#{base64_data}"
122
+ rescue StandardError => e
123
+ warn "Failed to fetch font #{url}: #{e.message}"
124
+ nil
125
+ end
126
+
127
+ def infer_font_mime_type(url)
128
+ case url.downcase
129
+ when /\.woff2$/
130
+ "font/woff2"
131
+ when /\.woff$/
132
+ "font/woff"
133
+ when /\.ttf$/
134
+ "font/ttf"
135
+ when /\.otf$/
136
+ "font/otf"
137
+ when /\.eot$/
138
+ "application/vnd.ms-fontobject"
139
+ else
140
+ "font/woff2" # Default to woff2
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_remediation"
4
+
5
+ module SvgConform
6
+ module Remediations
7
+ # Remediation action for font-related issues that matches svgcheck behavior
8
+ class FontRemediation < BaseRemediation
9
+ attribute :type, :string, default: -> { "FontRemediation" }
10
+ attribute :default_family, :string, default: -> { "sans-serif" }
11
+ attribute :mapping, :hash, default: -> { {} }
12
+
13
+ def apply(document, _context)
14
+ changes = []
15
+ # Find all elements with font-family attributes
16
+ document.traverse do |node|
17
+ next unless node.respond_to?(:name) && node.name
18
+
19
+ changes.concat(fix_font_family_attribute(node, default_family,
20
+ mapping))
21
+ changes.concat(fix_style_font_family(node, default_family, mapping))
22
+ end
23
+
24
+ changes
25
+ end
26
+
27
+ private
28
+
29
+ def fix_font_family_attribute(node, default_family, mapping)
30
+ changes = []
31
+ font_family = get_attribute(node, "font-family")
32
+ return changes unless font_family
33
+
34
+ new_family = convert_font_family(font_family, default_family, mapping)
35
+ if new_family != font_family
36
+ set_attribute(node, "font-family", new_family)
37
+ changes << log_change(
38
+ :attribute_modified,
39
+ "Changed font-family from '#{font_family}' to '#{new_family}'",
40
+ node,
41
+ )
42
+ end
43
+
44
+ changes
45
+ end
46
+
47
+ def fix_style_font_family(node, default_family, mapping)
48
+ changes = []
49
+ style_value = get_attribute(node, "style")
50
+ return changes unless style_value
51
+
52
+ styles = parse_style(style_value)
53
+ font_family_style = styles["font-family"]
54
+ return changes unless font_family_style
55
+
56
+ new_family = convert_font_family(font_family_style, default_family,
57
+ mapping)
58
+ if new_family != font_family_style
59
+ styles["font-family"] = new_family
60
+ new_style = build_style(styles)
61
+ set_attribute(node, "style", new_style)
62
+ changes << log_change(
63
+ :attribute_modified,
64
+ "Updated font-family in style from '#{font_family_style}' to '#{new_family}'",
65
+ node,
66
+ )
67
+ end
68
+
69
+ changes
70
+ end
71
+
72
+ def convert_font_family(font_family, default_family, mapping)
73
+ # Parse font family list (can be comma-separated)
74
+ families = font_family.split(",").map(&:strip)
75
+
76
+ # Check if any family is valid according to svgcheck's allowed values
77
+ valid_families = %w[serif sans-serif monospace inherit]
78
+
79
+ converted_families = families.map do |family|
80
+ # Remove quotes if present
81
+ clean_family = family.gsub(/['"]/, "").strip
82
+
83
+ # Check mapping first
84
+ mapped = mapping[clean_family]
85
+ return mapped if mapped
86
+
87
+ # Check if it's already a valid family according to svgcheck
88
+ if valid_families.include?(clean_family.downcase)
89
+ clean_family.downcase
90
+ else
91
+ # svgcheck replaces any invalid font family with 'sans-serif'
92
+ default_family
93
+ end
94
+ end
95
+
96
+ # Return the first converted family or default
97
+ converted_families.first || default_family
98
+ end
99
+
100
+ def parse_style(style_value)
101
+ return {} if style_value.nil? || style_value.empty?
102
+
103
+ styles = {}
104
+ style_value.split(";").each do |declaration|
105
+ next if declaration.strip.empty?
106
+
107
+ parts = declaration.split(":", 2)
108
+ next unless parts.size == 2
109
+
110
+ property = parts[0].strip
111
+ value = parts[1].strip
112
+ styles[property] = value unless property.empty? || value.empty?
113
+ end
114
+ styles
115
+ end
116
+
117
+ def build_style(styles)
118
+ styles.map { |prop, value| "#{prop}:#{value}" }.join(";")
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_remediation"
4
+
5
+ module SvgConform
6
+ module Remediations
7
+ # Remediation for embedding external images
8
+ class ImageEmbeddingRemediation < BaseRemediation
9
+ attribute :type, :string, default: -> { "ImageEmbeddingRemediation" }
10
+
11
+ yaml do
12
+ map "id", to: :id
13
+ map "description", to: :description
14
+ map "type", to: :type
15
+ map "targets", to: :targets
16
+ end
17
+
18
+ def apply(document, _context)
19
+ changes = []
20
+
21
+ # Find all image elements with external references
22
+ document.xpath("//image").each do |image_node|
23
+ href = get_attribute(image_node,
24
+ "href") || get_attribute(image_node,
25
+ "xlink:href")
26
+ next unless href && !href.start_with?("data:", "#")
27
+
28
+ # Convert external image to data URI
29
+ embedded_uri = fetch_and_embed_image(href)
30
+ if embedded_uri
31
+ # Update both href variants for compatibility
32
+ set_attribute(image_node, "href", embedded_uri)
33
+ if get_attribute(
34
+ image_node, "xlink:href"
35
+ )
36
+ set_attribute(image_node, "xlink:href",
37
+ embedded_uri)
38
+ end
39
+
40
+ changes << {
41
+ type: "image_embedded",
42
+ element: "image",
43
+ message: "Embedded external image: #{href}",
44
+ }
45
+ else
46
+ changes << {
47
+ type: "image_embedding_failed",
48
+ element: "image",
49
+ message: "Failed to embed image: #{href}",
50
+ }
51
+ end
52
+ end
53
+
54
+ # Find style attributes with background images
55
+ document.traverse do |node|
56
+ next unless element?(node)
57
+
58
+ style_value = get_attribute(node, "style")
59
+ next unless style_value&.include?("url(")
60
+
61
+ modified_style = embed_images_in_style(style_value, changes)
62
+ if modified_style != style_value
63
+ set_attribute(node, "style", modified_style)
64
+ end
65
+ end
66
+
67
+ changes
68
+ end
69
+
70
+ def can_remediate?
71
+ true
72
+ end
73
+
74
+ private
75
+
76
+ def embed_images_in_style(style_value, changes)
77
+ # SECURITY: Prevent ReDoS by limiting input length and using bounded quantifiers
78
+ # GitHub CodeQL: Regular expression with excessive backtracking
79
+ return style_value if style_value.length > 10_000
80
+
81
+ # Replace url() references with data URIs
82
+ # Use bounded quantifier {1,2000} to prevent exponential backtracking
83
+ style_value.gsub(/url\s*\(\s*['"]?([^'")\s]{1,2000})['"]?\s*\)/i) do |url_match|
84
+ url = ::Regexp.last_match(1)
85
+
86
+ # Skip if already embedded or internal reference
87
+ next url_match if url.start_with?("data:", "#")
88
+
89
+ # Fetch and embed the image
90
+ embedded_uri = fetch_and_embed_image(url)
91
+ if embedded_uri
92
+ changes << {
93
+ type: "image_embedded",
94
+ element: "style",
95
+ message: "Embedded external image: #{url}",
96
+ }
97
+ "url(#{embedded_uri})"
98
+ else
99
+ changes << {
100
+ type: "image_embedding_failed",
101
+ element: "style",
102
+ message: "Failed to embed image: #{url}",
103
+ }
104
+ url_match
105
+ end
106
+ end
107
+ end
108
+
109
+ def fetch_and_embed_image(url)
110
+ require "net/http"
111
+ require "uri"
112
+ require "base64"
113
+
114
+ # Parse URL
115
+ uri = URI.parse(url)
116
+
117
+ # Fetch the image
118
+ response = Net::HTTP.get_response(uri)
119
+ return nil unless response.is_a?(Net::HTTPSuccess)
120
+
121
+ # Get content type or infer from URL
122
+ content_type = response["content-type"] || infer_image_mime_type(url)
123
+
124
+ # Encode as base64
125
+ base64_data = Base64.strict_encode64(response.body)
126
+
127
+ # Return data URI
128
+ "data:#{content_type};base64,#{base64_data}"
129
+ rescue StandardError => e
130
+ warn "Failed to fetch image #{url}: #{e.message}"
131
+ nil
132
+ end
133
+
134
+ def infer_image_mime_type(url)
135
+ case url.downcase
136
+ when /\.png$/
137
+ "image/png"
138
+ when /\.jpe?g$/
139
+ "image/jpeg"
140
+ when /\.gif$/
141
+ "image/gif"
142
+ when /\.svg$/
143
+ "image/svg+xml"
144
+ when /\.webp$/
145
+ "image/webp"
146
+ when /\.bmp$/
147
+ "image/bmp"
148
+ else
149
+ "image/png" # Default to PNG
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end