@jjlmoya/utils-forensic-science 1.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 (316) hide show
  1. package/package.json +69 -0
  2. package/scripts/crystal-lattice-catalog-translations.mjs +459 -0
  3. package/scripts/postinstall.mjs +27 -0
  4. package/scripts/sync-crystal-lattice-i18n.mjs +50 -0
  5. package/src/category/i18n/de.ts +55 -0
  6. package/src/category/i18n/en.ts +55 -0
  7. package/src/category/i18n/es.ts +55 -0
  8. package/src/category/i18n/fr.ts +55 -0
  9. package/src/category/i18n/id.ts +55 -0
  10. package/src/category/i18n/it.ts +55 -0
  11. package/src/category/i18n/ja.ts +56 -0
  12. package/src/category/i18n/ko.ts +56 -0
  13. package/src/category/i18n/nl.ts +55 -0
  14. package/src/category/i18n/pl.ts +55 -0
  15. package/src/category/i18n/pt.ts +55 -0
  16. package/src/category/i18n/ru.ts +55 -0
  17. package/src/category/i18n/sv.ts +55 -0
  18. package/src/category/i18n/tr.ts +55 -0
  19. package/src/category/i18n/zh.ts +56 -0
  20. package/src/category/index.ts +48 -0
  21. package/src/category/seo.astro +15 -0
  22. package/src/components/PreviewNavSidebar.astro +116 -0
  23. package/src/components/PreviewToolbar.astro +143 -0
  24. package/src/data.ts +10 -0
  25. package/src/entries.ts +48 -0
  26. package/src/env.d.ts +5 -0
  27. package/src/index.ts +29 -0
  28. package/src/layouts/PreviewLayout.astro +117 -0
  29. package/src/pages/[locale]/[slug].astro +162 -0
  30. package/src/pages/[locale].astro +261 -0
  31. package/src/pages/index.astro +3 -0
  32. package/src/tests/diacritics_density.test.ts +140 -0
  33. package/src/tests/faq_count.test.ts +19 -0
  34. package/src/tests/i18n_coverage.test.ts +36 -0
  35. package/src/tests/inverted_punctuation.test.ts +84 -0
  36. package/src/tests/locale_completeness.test.ts +24 -0
  37. package/src/tests/mocks/astro_mock.js +2 -0
  38. package/src/tests/no_en_dash.test.ts +70 -0
  39. package/src/tests/no_h1_in_components.test.ts +48 -0
  40. package/src/tests/schemas_fulfillment.test.ts +23 -0
  41. package/src/tests/script_density.test.ts +94 -0
  42. package/src/tests/seo_length.test.ts +46 -0
  43. package/src/tests/shared-test-helpers.ts +56 -0
  44. package/src/tests/slug_language_code_format.test.ts +23 -0
  45. package/src/tests/slug_uniqueness.test.ts +81 -0
  46. package/src/tests/title_quality.test.ts +55 -0
  47. package/src/tests/tool_exports.test.ts +34 -0
  48. package/src/tests/tool_validation.test.ts +16 -0
  49. package/src/tests/widmark.test.ts +28 -0
  50. package/src/tool/forensic-age-estimator/bibliography.astro +14 -0
  51. package/src/tool/forensic-age-estimator/bibliography.ts +16 -0
  52. package/src/tool/forensic-age-estimator/component.astro +321 -0
  53. package/src/tool/forensic-age-estimator/dental-skeletal-third-molar-age-estimator.css +634 -0
  54. package/src/tool/forensic-age-estimator/entry.ts +32 -0
  55. package/src/tool/forensic-age-estimator/i18n/de.ts +245 -0
  56. package/src/tool/forensic-age-estimator/i18n/en.ts +245 -0
  57. package/src/tool/forensic-age-estimator/i18n/es.ts +245 -0
  58. package/src/tool/forensic-age-estimator/i18n/fr.ts +245 -0
  59. package/src/tool/forensic-age-estimator/i18n/id.ts +245 -0
  60. package/src/tool/forensic-age-estimator/i18n/it.ts +245 -0
  61. package/src/tool/forensic-age-estimator/i18n/ja.ts +245 -0
  62. package/src/tool/forensic-age-estimator/i18n/ko.ts +245 -0
  63. package/src/tool/forensic-age-estimator/i18n/nl.ts +245 -0
  64. package/src/tool/forensic-age-estimator/i18n/pl.ts +245 -0
  65. package/src/tool/forensic-age-estimator/i18n/pt.ts +245 -0
  66. package/src/tool/forensic-age-estimator/i18n/ru.ts +245 -0
  67. package/src/tool/forensic-age-estimator/i18n/sv.ts +245 -0
  68. package/src/tool/forensic-age-estimator/i18n/tr.ts +245 -0
  69. package/src/tool/forensic-age-estimator/i18n/zh.ts +246 -0
  70. package/src/tool/forensic-age-estimator/index.ts +11 -0
  71. package/src/tool/forensic-age-estimator/logic.ts +81 -0
  72. package/src/tool/forensic-age-estimator/seo.astro +15 -0
  73. package/src/tool/forensic-blood-test-simulator/bibliography.astro +6 -0
  74. package/src/tool/forensic-blood-test-simulator/bibliography.ts +12 -0
  75. package/src/tool/forensic-blood-test-simulator/component.astro +168 -0
  76. package/src/tool/forensic-blood-test-simulator/dom-utils.ts +40 -0
  77. package/src/tool/forensic-blood-test-simulator/entry.ts +32 -0
  78. package/src/tool/forensic-blood-test-simulator/forensic-presumptive-blood-testing-luminol-kastle-meyer-simulator.css +532 -0
  79. package/src/tool/forensic-blood-test-simulator/helpers.ts +16 -0
  80. package/src/tool/forensic-blood-test-simulator/i18n/de.ts +185 -0
  81. package/src/tool/forensic-blood-test-simulator/i18n/en.ts +185 -0
  82. package/src/tool/forensic-blood-test-simulator/i18n/es.ts +185 -0
  83. package/src/tool/forensic-blood-test-simulator/i18n/fr.ts +220 -0
  84. package/src/tool/forensic-blood-test-simulator/i18n/id.ts +220 -0
  85. package/src/tool/forensic-blood-test-simulator/i18n/it.ts +220 -0
  86. package/src/tool/forensic-blood-test-simulator/i18n/ja.ts +220 -0
  87. package/src/tool/forensic-blood-test-simulator/i18n/ko.ts +220 -0
  88. package/src/tool/forensic-blood-test-simulator/i18n/nl.ts +220 -0
  89. package/src/tool/forensic-blood-test-simulator/i18n/pl.ts +220 -0
  90. package/src/tool/forensic-blood-test-simulator/i18n/pt.ts +220 -0
  91. package/src/tool/forensic-blood-test-simulator/i18n/ru.ts +220 -0
  92. package/src/tool/forensic-blood-test-simulator/i18n/sv.ts +220 -0
  93. package/src/tool/forensic-blood-test-simulator/i18n/tr.ts +220 -0
  94. package/src/tool/forensic-blood-test-simulator/i18n/zh.ts +220 -0
  95. package/src/tool/forensic-blood-test-simulator/index.ts +11 -0
  96. package/src/tool/forensic-blood-test-simulator/logic.ts +174 -0
  97. package/src/tool/forensic-blood-test-simulator/script.ts +192 -0
  98. package/src/tool/forensic-blood-test-simulator/seo.astro +15 -0
  99. package/src/tool/forensic-fiber-comparison-microscope/bibliography.astro +6 -0
  100. package/src/tool/forensic-fiber-comparison-microscope/bibliography.ts +16 -0
  101. package/src/tool/forensic-fiber-comparison-microscope/component.astro +146 -0
  102. package/src/tool/forensic-fiber-comparison-microscope/entry.ts +32 -0
  103. package/src/tool/forensic-fiber-comparison-microscope/forensic-fiber-comparison-microscope.css +629 -0
  104. package/src/tool/forensic-fiber-comparison-microscope/i18n/de.ts +250 -0
  105. package/src/tool/forensic-fiber-comparison-microscope/i18n/en.ts +246 -0
  106. package/src/tool/forensic-fiber-comparison-microscope/i18n/es.ts +246 -0
  107. package/src/tool/forensic-fiber-comparison-microscope/i18n/fr.ts +246 -0
  108. package/src/tool/forensic-fiber-comparison-microscope/i18n/id.ts +250 -0
  109. package/src/tool/forensic-fiber-comparison-microscope/i18n/it.ts +246 -0
  110. package/src/tool/forensic-fiber-comparison-microscope/i18n/ja.ts +250 -0
  111. package/src/tool/forensic-fiber-comparison-microscope/i18n/ko.ts +246 -0
  112. package/src/tool/forensic-fiber-comparison-microscope/i18n/nl.ts +246 -0
  113. package/src/tool/forensic-fiber-comparison-microscope/i18n/pl.ts +250 -0
  114. package/src/tool/forensic-fiber-comparison-microscope/i18n/pt.ts +250 -0
  115. package/src/tool/forensic-fiber-comparison-microscope/i18n/ru.ts +250 -0
  116. package/src/tool/forensic-fiber-comparison-microscope/i18n/sv.ts +250 -0
  117. package/src/tool/forensic-fiber-comparison-microscope/i18n/tr.ts +246 -0
  118. package/src/tool/forensic-fiber-comparison-microscope/i18n/zh.ts +250 -0
  119. package/src/tool/forensic-fiber-comparison-microscope/index.ts +11 -0
  120. package/src/tool/forensic-fiber-comparison-microscope/logic.ts +244 -0
  121. package/src/tool/forensic-fiber-comparison-microscope/render.ts +265 -0
  122. package/src/tool/forensic-fiber-comparison-microscope/seo.astro +15 -0
  123. package/src/tool/forensic-fiber-comparison-microscope/view.ts +267 -0
  124. package/src/tool/forensic-glass-becke-line-simulator/bibliography.astro +6 -0
  125. package/src/tool/forensic-glass-becke-line-simulator/bibliography.ts +16 -0
  126. package/src/tool/forensic-glass-becke-line-simulator/component.astro +81 -0
  127. package/src/tool/forensic-glass-becke-line-simulator/entry.ts +32 -0
  128. package/src/tool/forensic-glass-becke-line-simulator/forensic-glass-becke-line-simulator.css +392 -0
  129. package/src/tool/forensic-glass-becke-line-simulator/i18n/de.ts +231 -0
  130. package/src/tool/forensic-glass-becke-line-simulator/i18n/en.ts +231 -0
  131. package/src/tool/forensic-glass-becke-line-simulator/i18n/es.ts +231 -0
  132. package/src/tool/forensic-glass-becke-line-simulator/i18n/fr.ts +231 -0
  133. package/src/tool/forensic-glass-becke-line-simulator/i18n/id.ts +231 -0
  134. package/src/tool/forensic-glass-becke-line-simulator/i18n/it.ts +231 -0
  135. package/src/tool/forensic-glass-becke-line-simulator/i18n/ja.ts +231 -0
  136. package/src/tool/forensic-glass-becke-line-simulator/i18n/ko.ts +231 -0
  137. package/src/tool/forensic-glass-becke-line-simulator/i18n/nl.ts +231 -0
  138. package/src/tool/forensic-glass-becke-line-simulator/i18n/pl.ts +231 -0
  139. package/src/tool/forensic-glass-becke-line-simulator/i18n/pt.ts +231 -0
  140. package/src/tool/forensic-glass-becke-line-simulator/i18n/ru.ts +231 -0
  141. package/src/tool/forensic-glass-becke-line-simulator/i18n/sv.ts +231 -0
  142. package/src/tool/forensic-glass-becke-line-simulator/i18n/tr.ts +231 -0
  143. package/src/tool/forensic-glass-becke-line-simulator/i18n/zh.ts +231 -0
  144. package/src/tool/forensic-glass-becke-line-simulator/index.ts +11 -0
  145. package/src/tool/forensic-glass-becke-line-simulator/logic.ts +100 -0
  146. package/src/tool/forensic-glass-becke-line-simulator/seo.astro +15 -0
  147. package/src/tool/forensic-glass-becke-line-simulator/view.ts +281 -0
  148. package/src/tool/forensic-image-authenticity-analyzer/bibliography.astro +9 -0
  149. package/src/tool/forensic-image-authenticity-analyzer/bibliography.ts +7 -0
  150. package/src/tool/forensic-image-authenticity-analyzer/component.astro +250 -0
  151. package/src/tool/forensic-image-authenticity-analyzer/entry.ts +29 -0
  152. package/src/tool/forensic-image-authenticity-analyzer/forensic-image-metadata-authenticity-analyzer.css +679 -0
  153. package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -0
  154. package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -0
  155. package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -0
  156. package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -0
  157. package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +76 -0
  158. package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -0
  159. package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +72 -0
  160. package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +72 -0
  161. package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +75 -0
  162. package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +75 -0
  163. package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -0
  164. package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +75 -0
  165. package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +76 -0
  166. package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +76 -0
  167. package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +71 -0
  168. package/src/tool/forensic-image-authenticity-analyzer/index.ts +11 -0
  169. package/src/tool/forensic-image-authenticity-analyzer/logic.ts +283 -0
  170. package/src/tool/forensic-image-authenticity-analyzer/seo.astro +10 -0
  171. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.astro +6 -0
  172. package/src/tool/forensic-microcrystal-drug-simulator/bibliography.ts +12 -0
  173. package/src/tool/forensic-microcrystal-drug-simulator/component.astro +240 -0
  174. package/src/tool/forensic-microcrystal-drug-simulator/entry.ts +32 -0
  175. package/src/tool/forensic-microcrystal-drug-simulator/forensic-microcrystal-drug-simulator.css +430 -0
  176. package/src/tool/forensic-microcrystal-drug-simulator/i18n/de.ts +244 -0
  177. package/src/tool/forensic-microcrystal-drug-simulator/i18n/en.ts +244 -0
  178. package/src/tool/forensic-microcrystal-drug-simulator/i18n/es.ts +244 -0
  179. package/src/tool/forensic-microcrystal-drug-simulator/i18n/fr.ts +244 -0
  180. package/src/tool/forensic-microcrystal-drug-simulator/i18n/id.ts +244 -0
  181. package/src/tool/forensic-microcrystal-drug-simulator/i18n/it.ts +244 -0
  182. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ja.ts +244 -0
  183. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ko.ts +244 -0
  184. package/src/tool/forensic-microcrystal-drug-simulator/i18n/nl.ts +244 -0
  185. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pl.ts +244 -0
  186. package/src/tool/forensic-microcrystal-drug-simulator/i18n/pt.ts +244 -0
  187. package/src/tool/forensic-microcrystal-drug-simulator/i18n/ru.ts +244 -0
  188. package/src/tool/forensic-microcrystal-drug-simulator/i18n/sv.ts +244 -0
  189. package/src/tool/forensic-microcrystal-drug-simulator/i18n/tr.ts +244 -0
  190. package/src/tool/forensic-microcrystal-drug-simulator/i18n/zh.ts +244 -0
  191. package/src/tool/forensic-microcrystal-drug-simulator/index.ts +11 -0
  192. package/src/tool/forensic-microcrystal-drug-simulator/logic.ts +189 -0
  193. package/src/tool/forensic-microcrystal-drug-simulator/seo.astro +15 -0
  194. package/src/tool/forensic-sex-determinator/bibliography.astro +14 -0
  195. package/src/tool/forensic-sex-determinator/bibliography.ts +12 -0
  196. package/src/tool/forensic-sex-determinator/component.astro +463 -0
  197. package/src/tool/forensic-sex-determinator/entry.ts +32 -0
  198. package/src/tool/forensic-sex-determinator/forensic-sex-determinator.css +413 -0
  199. package/src/tool/forensic-sex-determinator/i18n/de.ts +211 -0
  200. package/src/tool/forensic-sex-determinator/i18n/en.ts +211 -0
  201. package/src/tool/forensic-sex-determinator/i18n/es.ts +211 -0
  202. package/src/tool/forensic-sex-determinator/i18n/fr.ts +211 -0
  203. package/src/tool/forensic-sex-determinator/i18n/id.ts +211 -0
  204. package/src/tool/forensic-sex-determinator/i18n/it.ts +211 -0
  205. package/src/tool/forensic-sex-determinator/i18n/ja.ts +211 -0
  206. package/src/tool/forensic-sex-determinator/i18n/ko.ts +211 -0
  207. package/src/tool/forensic-sex-determinator/i18n/nl.ts +211 -0
  208. package/src/tool/forensic-sex-determinator/i18n/pl.ts +211 -0
  209. package/src/tool/forensic-sex-determinator/i18n/pt.ts +211 -0
  210. package/src/tool/forensic-sex-determinator/i18n/ru.ts +211 -0
  211. package/src/tool/forensic-sex-determinator/i18n/sv.ts +211 -0
  212. package/src/tool/forensic-sex-determinator/i18n/tr.ts +211 -0
  213. package/src/tool/forensic-sex-determinator/i18n/zh.ts +211 -0
  214. package/src/tool/forensic-sex-determinator/index.ts +11 -0
  215. package/src/tool/forensic-sex-determinator/logic.ts +89 -0
  216. package/src/tool/forensic-sex-determinator/seo.astro +15 -0
  217. package/src/tool/forensic-stature-estimator/bibliography.astro +14 -0
  218. package/src/tool/forensic-stature-estimator/bibliography.ts +12 -0
  219. package/src/tool/forensic-stature-estimator/component.astro +49 -0
  220. package/src/tool/forensic-stature-estimator/components/EstimationPanel.astro +65 -0
  221. package/src/tool/forensic-stature-estimator/components/OsteometricBoard.astro +39 -0
  222. package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +109 -0
  223. package/src/tool/forensic-stature-estimator/dom-utils.ts +71 -0
  224. package/src/tool/forensic-stature-estimator/entry.ts +32 -0
  225. package/src/tool/forensic-stature-estimator/forensic-stature-estimator.css +689 -0
  226. package/src/tool/forensic-stature-estimator/helpers.ts +51 -0
  227. package/src/tool/forensic-stature-estimator/i18n/de.ts +196 -0
  228. package/src/tool/forensic-stature-estimator/i18n/en.ts +196 -0
  229. package/src/tool/forensic-stature-estimator/i18n/es.ts +196 -0
  230. package/src/tool/forensic-stature-estimator/i18n/fr.ts +196 -0
  231. package/src/tool/forensic-stature-estimator/i18n/id.ts +196 -0
  232. package/src/tool/forensic-stature-estimator/i18n/it.ts +196 -0
  233. package/src/tool/forensic-stature-estimator/i18n/ja.ts +196 -0
  234. package/src/tool/forensic-stature-estimator/i18n/ko.ts +196 -0
  235. package/src/tool/forensic-stature-estimator/i18n/nl.ts +196 -0
  236. package/src/tool/forensic-stature-estimator/i18n/pl.ts +196 -0
  237. package/src/tool/forensic-stature-estimator/i18n/pt.ts +196 -0
  238. package/src/tool/forensic-stature-estimator/i18n/ru.ts +196 -0
  239. package/src/tool/forensic-stature-estimator/i18n/sv.ts +196 -0
  240. package/src/tool/forensic-stature-estimator/i18n/tr.ts +196 -0
  241. package/src/tool/forensic-stature-estimator/i18n/zh.ts +196 -0
  242. package/src/tool/forensic-stature-estimator/index.ts +11 -0
  243. package/src/tool/forensic-stature-estimator/logic.ts +119 -0
  244. package/src/tool/forensic-stature-estimator/script.ts +288 -0
  245. package/src/tool/forensic-stature-estimator/seo.astro +15 -0
  246. package/src/tool/forensic-tlc-ink-simulator/bibliography.astro +6 -0
  247. package/src/tool/forensic-tlc-ink-simulator/bibliography.ts +16 -0
  248. package/src/tool/forensic-tlc-ink-simulator/component.astro +245 -0
  249. package/src/tool/forensic-tlc-ink-simulator/entry.ts +32 -0
  250. package/src/tool/forensic-tlc-ink-simulator/forensic-tlc-ink-simulator.css +462 -0
  251. package/src/tool/forensic-tlc-ink-simulator/i18n/de.ts +243 -0
  252. package/src/tool/forensic-tlc-ink-simulator/i18n/en.ts +243 -0
  253. package/src/tool/forensic-tlc-ink-simulator/i18n/es.ts +243 -0
  254. package/src/tool/forensic-tlc-ink-simulator/i18n/fr.ts +243 -0
  255. package/src/tool/forensic-tlc-ink-simulator/i18n/id.ts +243 -0
  256. package/src/tool/forensic-tlc-ink-simulator/i18n/it.ts +243 -0
  257. package/src/tool/forensic-tlc-ink-simulator/i18n/ja.ts +235 -0
  258. package/src/tool/forensic-tlc-ink-simulator/i18n/ko.ts +235 -0
  259. package/src/tool/forensic-tlc-ink-simulator/i18n/nl.ts +243 -0
  260. package/src/tool/forensic-tlc-ink-simulator/i18n/pl.ts +243 -0
  261. package/src/tool/forensic-tlc-ink-simulator/i18n/pt.ts +243 -0
  262. package/src/tool/forensic-tlc-ink-simulator/i18n/ru.ts +243 -0
  263. package/src/tool/forensic-tlc-ink-simulator/i18n/sv.ts +243 -0
  264. package/src/tool/forensic-tlc-ink-simulator/i18n/tr.ts +243 -0
  265. package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +235 -0
  266. package/src/tool/forensic-tlc-ink-simulator/index.ts +11 -0
  267. package/src/tool/forensic-tlc-ink-simulator/logic.ts +152 -0
  268. package/src/tool/forensic-tlc-ink-simulator/seo.astro +15 -0
  269. package/src/tool/gsr-dispersion-calculator/bibliography.astro +6 -0
  270. package/src/tool/gsr-dispersion-calculator/bibliography.ts +16 -0
  271. package/src/tool/gsr-dispersion-calculator/component.astro +294 -0
  272. package/src/tool/gsr-dispersion-calculator/entry.ts +32 -0
  273. package/src/tool/gsr-dispersion-calculator/gsr-dispersion-calculator.css +305 -0
  274. package/src/tool/gsr-dispersion-calculator/i18n/de.ts +272 -0
  275. package/src/tool/gsr-dispersion-calculator/i18n/en.ts +272 -0
  276. package/src/tool/gsr-dispersion-calculator/i18n/es.ts +272 -0
  277. package/src/tool/gsr-dispersion-calculator/i18n/fr.ts +272 -0
  278. package/src/tool/gsr-dispersion-calculator/i18n/id.ts +272 -0
  279. package/src/tool/gsr-dispersion-calculator/i18n/it.ts +272 -0
  280. package/src/tool/gsr-dispersion-calculator/i18n/ja.ts +272 -0
  281. package/src/tool/gsr-dispersion-calculator/i18n/ko.ts +272 -0
  282. package/src/tool/gsr-dispersion-calculator/i18n/nl.ts +272 -0
  283. package/src/tool/gsr-dispersion-calculator/i18n/pl.ts +272 -0
  284. package/src/tool/gsr-dispersion-calculator/i18n/pt.ts +272 -0
  285. package/src/tool/gsr-dispersion-calculator/i18n/ru.ts +272 -0
  286. package/src/tool/gsr-dispersion-calculator/i18n/sv.ts +272 -0
  287. package/src/tool/gsr-dispersion-calculator/i18n/tr.ts +272 -0
  288. package/src/tool/gsr-dispersion-calculator/i18n/zh.ts +272 -0
  289. package/src/tool/gsr-dispersion-calculator/index.ts +11 -0
  290. package/src/tool/gsr-dispersion-calculator/logic.ts +148 -0
  291. package/src/tool/gsr-dispersion-calculator/seo.astro +15 -0
  292. package/src/tool/widmark-alcohol-simulator/bibliography.astro +14 -0
  293. package/src/tool/widmark-alcohol-simulator/bibliography.ts +12 -0
  294. package/src/tool/widmark-alcohol-simulator/component.astro +453 -0
  295. package/src/tool/widmark-alcohol-simulator/entry.ts +32 -0
  296. package/src/tool/widmark-alcohol-simulator/i18n/de.ts +193 -0
  297. package/src/tool/widmark-alcohol-simulator/i18n/en.ts +206 -0
  298. package/src/tool/widmark-alcohol-simulator/i18n/es.ts +193 -0
  299. package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +193 -0
  300. package/src/tool/widmark-alcohol-simulator/i18n/id.ts +193 -0
  301. package/src/tool/widmark-alcohol-simulator/i18n/it.ts +193 -0
  302. package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +193 -0
  303. package/src/tool/widmark-alcohol-simulator/i18n/ko.ts +193 -0
  304. package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +193 -0
  305. package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +193 -0
  306. package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +193 -0
  307. package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +193 -0
  308. package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +193 -0
  309. package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +193 -0
  310. package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +193 -0
  311. package/src/tool/widmark-alcohol-simulator/index.ts +11 -0
  312. package/src/tool/widmark-alcohol-simulator/logic.ts +97 -0
  313. package/src/tool/widmark-alcohol-simulator/seo.astro +15 -0
  314. package/src/tool/widmark-alcohol-simulator/widmark-alcohol-simulator.css +386 -0
  315. package/src/tools.ts +27 -0
  316. package/src/types.ts +70 -0
@@ -0,0 +1,281 @@
1
+ import { RefractiveIndexCalculator } from './logic';
2
+ import type { FocusDirection, GlassReference } from './logic';
3
+
4
+ type Ui = Record<string, string>;
5
+ type Theme = { center: string; background: string; glass: string; halo: string; grain: string; text: string };
6
+ type Result = ReturnType<RefractiveIndexCalculator['simulate']>;
7
+
8
+ const STORAGE_KEY = 'forensic-glass-becke-line-simulator-state';
9
+ const GLASS_VALUES: GlassReference[] = ['sodaLime', 'borosilicate', 'leadCrystal', 'tempered'];
10
+ const GLASS_TINTS: Record<GlassReference, string> = { sodaLime: '#b9d7d3', borosilicate: '#c8dff2', leadCrystal: '#d6ccf4', tempered: '#cfe4bd' };
11
+ const HALO_SCALE: Record<GlassReference, number> = { sodaLime: 1, borosilicate: 0.42, leadCrystal: 1, tempered: 1 };
12
+
13
+ function byId<T extends HTMLElement>(id: string): T | null { return document.getElementById(id) as T | null; }
14
+
15
+ function isGlassReference(value: unknown): value is GlassReference { return typeof value === 'string' && GLASS_VALUES.includes(value as GlassReference); }
16
+
17
+ function currentTheme(): Theme {
18
+ const dark = document.documentElement.classList.contains('theme-dark') || document.body.classList.contains('theme-dark');
19
+ if (dark) return { center: '#64717a', background: '#101820', glass: '#253943', halo: '#f6c95c', grain: 'rgba(255,255,255,0.16)', text: '#f8fafc' };
20
+ return { center: '#ffffff', background: '#edf7f4', glass: '#b9d7d3', halo: '#d97706', grain: 'rgba(15,23,42,0.12)', text: '#15242d' };
21
+ }
22
+
23
+ function drawBorosilicate(ctx: CanvasRenderingContext2D, radius: number): void {
24
+ ctx.moveTo(-radius * 0.62, -radius * 0.22);
25
+ ctx.bezierCurveTo(-radius * 0.5, -radius * 0.62, radius * 0.36, -radius * 0.64, radius * 0.58, -radius * 0.2);
26
+ ctx.bezierCurveTo(radius * 0.78, radius * 0.2, radius * 0.22, radius * 0.64, -radius * 0.42, radius * 0.46);
27
+ ctx.bezierCurveTo(-radius * 0.78, radius * 0.34, -radius * 0.84, radius * 0.02, -radius * 0.62, -radius * 0.22);
28
+ }
29
+
30
+ function drawLeadCrystal(ctx: CanvasRenderingContext2D, radius: number): void { drawPolygon(ctx, radius, [[0, -0.68], [0.28, -0.24], [0.74, -0.12], [0.34, 0.18], [0.42, 0.62], [0, 0.36], [-0.46, 0.58], [-0.34, 0.14], [-0.74, -0.16], [-0.26, -0.28]]); }
31
+
32
+ function drawPolygon(ctx: CanvasRenderingContext2D, radius: number, points: number[][]): void { points.forEach(([x, y], index) => index === 0 ? ctx.moveTo(radius * x, radius * y) : ctx.lineTo(radius * x, radius * y)); }
33
+
34
+ function drawGlassPath(ctx: CanvasRenderingContext2D, glass: GlassReference, radius: number): void {
35
+ if (glass === 'borosilicate') drawBorosilicate(ctx, radius);
36
+ else if (glass === 'leadCrystal') drawLeadCrystal(ctx, radius);
37
+ else if (glass === 'tempered') drawPolygon(ctx, radius, [[-0.7, -0.48], [0.66, -0.34], [0.7, 0.38], [-0.2, 0.64], [-0.76, 0.16]]);
38
+ else drawPolygon(ctx, radius, [[-0.72, -0.44], [0.58, -0.56], [0.78, 0.18], [0.14, 0.62], [-0.64, 0.38]]);
39
+ }
40
+
41
+ export function initBeckeLineSimulator(ui: Ui): void { new BeckeLineView(ui).init(); }
42
+
43
+ class BeckeLineView {
44
+ private readonly model = new RefractiveIndexCalculator();
45
+ private readonly canvas = byId<HTMLCanvasElement>('becke-canvas');
46
+ private readonly context = this.canvas?.getContext('2d') ?? null;
47
+ private readonly glassTrigger = byId<HTMLButtonElement>('becke-glass-trigger');
48
+ private readonly glassLabel = byId('becke-glass-label');
49
+ private readonly glassMenu = byId('becke-glass-menu');
50
+ private readonly temperatureInput = byId<HTMLInputElement>('becke-temperature');
51
+ private glassReference: GlassReference = 'sodaLime';
52
+ private unitSystem: 'metric' | 'imperial' = 'metric';
53
+ private focusDirection: FocusDirection = 'raised';
54
+ private animationFrame = 0;
55
+ private previousHaloOffset = 0;
56
+
57
+ constructor(private readonly ui: Ui) {}
58
+
59
+ init(): void {
60
+ this.restoreState();
61
+ this.bindEvents();
62
+ this.updateButtons();
63
+ this.render();
64
+ new MutationObserver(() => this.render()).observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
65
+ }
66
+
67
+ private labels(): Record<GlassReference, string> { return { sodaLime: this.ui.sodaLime, borosilicate: this.ui.borosilicate, leadCrystal: this.ui.leadCrystal, tempered: this.ui.tempered }; }
68
+
69
+ private setText(id: string, value: string): void { const element = byId(id); if (element) element.textContent = value; }
70
+
71
+ private formatTemperature(celsius: number, fahrenheit: number): string { return this.unitSystem === 'metric' ? `${celsius.toFixed(1)}\u00b0${this.ui.unitCelsius}` : `${fahrenheit.toFixed(1)}\u00b0${this.ui.unitFahrenheit}`; }
72
+
73
+ private updateButtons(): void {
74
+ document.querySelectorAll<HTMLButtonElement>('[data-becke-unit]').forEach((button) => { button.dataset.active = button.dataset.beckeUnit === this.unitSystem ? 'true' : 'false'; });
75
+ document.querySelectorAll<HTMLButtonElement>('[data-becke-focus]').forEach((button) => { button.dataset.active = button.dataset.beckeFocus === this.focusDirection ? 'true' : 'false'; });
76
+ document.querySelectorAll<HTMLButtonElement>('[data-becke-glass]').forEach((button) => { button.setAttribute('aria-selected', button.dataset.beckeGlass === this.glassReference ? 'true' : 'false'); });
77
+ if (this.glassLabel) this.glassLabel.textContent = this.labels()[this.glassReference];
78
+ }
79
+
80
+ private setGlassMenuOpen(open: boolean): void { this.glassTrigger?.setAttribute('aria-expanded', open ? 'true' : 'false'); this.glassMenu?.classList.toggle('is-open', open); }
81
+
82
+ private bindEvents(): void {
83
+ this.glassTrigger?.addEventListener('click', () => this.setGlassMenuOpen(this.glassTrigger?.getAttribute('aria-expanded') !== 'true'));
84
+ document.querySelectorAll<HTMLButtonElement>('[data-becke-glass]').forEach((button) => this.bindGlassButton(button));
85
+ this.temperatureInput?.addEventListener('input', () => this.render());
86
+ document.querySelectorAll<HTMLButtonElement>('[data-becke-unit]').forEach((button) => this.bindUnitButton(button));
87
+ document.querySelectorAll<HTMLButtonElement>('[data-becke-focus]').forEach((button) => this.bindFocusButton(button));
88
+ document.addEventListener('click', (event) => this.closeMenuFromOutside(event.target as Node));
89
+ document.addEventListener('keydown', (event) => { if (event.key === 'Escape') this.setGlassMenuOpen(false); });
90
+ }
91
+
92
+ private bindGlassButton(button: HTMLButtonElement): void {
93
+ button.addEventListener('click', () => {
94
+ if (!isGlassReference(button.dataset.beckeGlass)) return;
95
+ this.glassReference = button.dataset.beckeGlass;
96
+ this.setGlassMenuOpen(false);
97
+ this.updateButtons();
98
+ this.render();
99
+ });
100
+ }
101
+
102
+ private bindUnitButton(button: HTMLButtonElement): void {
103
+ button.addEventListener('click', () => {
104
+ this.unitSystem = button.dataset.beckeUnit === 'imperial' ? 'imperial' : 'metric';
105
+ this.updateButtons();
106
+ this.render();
107
+ });
108
+ }
109
+
110
+ private bindFocusButton(button: HTMLButtonElement): void {
111
+ button.addEventListener('click', () => {
112
+ this.focusDirection = button.dataset.beckeFocus === 'lowered' ? 'lowered' : 'raised';
113
+ this.updateButtons();
114
+ this.render();
115
+ });
116
+ }
117
+
118
+ private closeMenuFromOutside(target: Node): void { if (!this.glassMenu?.contains(target) && !this.glassTrigger?.contains(target)) this.setGlassMenuOpen(false); }
119
+
120
+ private restoreState(): void {
121
+ try {
122
+ this.applySavedState(JSON.parse(localStorage.getItem(STORAGE_KEY) ?? '{}') as Record<string, unknown>);
123
+ } catch {
124
+ localStorage.removeItem(STORAGE_KEY);
125
+ }
126
+ }
127
+
128
+ private applySavedState(saved: Record<string, unknown>): void {
129
+ if (isGlassReference(saved.glass)) this.glassReference = saved.glass;
130
+ if (typeof saved.temperature === 'number' && this.temperatureInput) this.temperatureInput.value = String(saved.temperature);
131
+ if (saved.unitSystem === 'metric' || saved.unitSystem === 'imperial') this.unitSystem = saved.unitSystem;
132
+ if (saved.focusDirection === 'raised' || saved.focusDirection === 'lowered') this.focusDirection = saved.focusDirection;
133
+ }
134
+
135
+ private render(): void {
136
+ const result = this.model.simulate({ temperatureC: Number(this.temperatureInput?.value ?? 96), glass: this.glassReference, focusDirection: this.focusDirection });
137
+ this.animateDraw(result);
138
+ this.setText('becke-temperature-label', this.formatTemperature(result.temperatureC, result.temperatureF));
139
+ this.setText('becke-liquid-ri', result.liquidIndex.toFixed(5));
140
+ this.setText('becke-glass-ri', result.glassIndex.toFixed(3));
141
+ this.setText('becke-delta-ri', result.delta.toFixed(5));
142
+ this.setText('becke-match', `${result.matchQuality}%`);
143
+ this.setText('becke-interpretation', this.ui[result.interpretationKey] ?? result.interpretationKey);
144
+ this.setText('becke-material-chip', this.labels()[this.glassReference]);
145
+ this.saveState();
146
+ }
147
+
148
+ private saveState(): void {
149
+ localStorage.setItem(STORAGE_KEY, JSON.stringify({ glass: this.glassReference, temperature: Number(this.temperatureInput?.value ?? 96), unitSystem: this.unitSystem, focusDirection: this.focusDirection }));
150
+ }
151
+
152
+ private drawGrain(width: number, height: number, grain: string): void {
153
+ if (!this.context) return;
154
+ for (let i = 0; i < 90; i++) {
155
+ this.context.fillStyle = grain;
156
+ this.context.fillRect((i * 83) % width, (i * 47) % height, 1.4, 1.4);
157
+ }
158
+ }
159
+
160
+ private drawGlass(radius: number, result: Result, animatedHaloOffset: number, theme: Theme): void {
161
+ if (!this.context) return;
162
+ this.context.rotate(-0.16);
163
+ this.fillGlass(radius);
164
+ this.strokeHalo(radius, result, animatedHaloOffset, theme);
165
+ this.strokeGlass(radius);
166
+ this.drawInclusions(radius);
167
+ }
168
+
169
+ private fillGlass(radius: number): void {
170
+ if (!this.context) return;
171
+ this.context.beginPath();
172
+ drawGlassPath(this.context, this.glassReference, radius);
173
+ this.context.closePath();
174
+ this.context.fillStyle = GLASS_TINTS[this.glassReference];
175
+ this.context.globalAlpha = 0.5;
176
+ this.context.fill();
177
+ this.context.globalAlpha = 1;
178
+ }
179
+
180
+ private strokeHalo(radius: number, result: Result, offset: number, theme: Theme): void {
181
+ if (!this.context) return;
182
+ this.context.save();
183
+ this.context.translate(offset, offset * 0.35);
184
+ this.context.shadowColor = theme.halo;
185
+ this.context.shadowBlur = 22 * result.haloOpacity;
186
+ this.context.lineWidth = Math.max(1, 8 * result.haloOpacity);
187
+ this.context.strokeStyle = result.lineDirection === 'matched' ? 'rgba(255,255,255,0.32)' : theme.halo;
188
+ this.context.setLineDash(result.lineDirection === 'matched' ? [10, 16] : []);
189
+ this.context.beginPath();
190
+ drawGlassPath(this.context, this.glassReference, radius);
191
+ this.context.closePath();
192
+ this.context.stroke();
193
+ this.context.restore();
194
+ }
195
+
196
+ private strokeGlass(radius: number): void {
197
+ if (!this.context) return;
198
+ this.context.beginPath();
199
+ drawGlassPath(this.context, this.glassReference, radius);
200
+ this.context.closePath();
201
+ this.context.lineWidth = 1.5;
202
+ this.context.strokeStyle = 'rgba(255,255,255,0.34)';
203
+ this.context.stroke();
204
+ this.context.shadowBlur = 0;
205
+ }
206
+
207
+ private drawInclusions(radius: number): void {
208
+ if (!this.context) return;
209
+ this.context.beginPath();
210
+ this.context.arc(-radius * 0.18, -radius * 0.08, radius * (this.glassReference === 'leadCrystal' ? 0.06 : 0.09), 0, Math.PI * 2);
211
+ this.context.arc(radius * 0.26, radius * 0.12, radius * (this.glassReference === 'borosilicate' ? 0.035 : 0.055), 0, Math.PI * 2);
212
+ if (this.glassReference === 'tempered') this.drawTemperedStressMarks(radius);
213
+ this.context.fillStyle = 'rgba(255,255,255,0.28)';
214
+ this.context.fill();
215
+ }
216
+
217
+ private drawTemperedStressMarks(radius: number): void { this.context?.rect(-radius * 0.5, -radius * 0.18, radius * 0.18, radius * 0.035); this.context?.rect(radius * 0.08, radius * 0.28, radius * 0.24, radius * 0.035); }
218
+
219
+ private draw(result: Result, animatedHaloOffset = result.haloOffset): void {
220
+ if (!this.canvas || !this.context) return;
221
+ const theme = currentTheme();
222
+ const width = this.canvas.width;
223
+ const height = this.canvas.height;
224
+ const centerX = width / 2;
225
+ const centerY = height / 2;
226
+ const radius = Math.min(width, height) * 0.36;
227
+ this.drawBackground(centerX, centerY, radius, theme);
228
+ this.context.save();
229
+ this.clipScope(centerX, centerY, radius);
230
+ this.drawGrain(width, height, theme.grain);
231
+ this.context.translate(centerX, centerY);
232
+ this.drawGlass(radius, result, animatedHaloOffset, theme);
233
+ this.context.restore();
234
+ this.drawScopeRing(centerX, centerY, radius);
235
+ this.drawCanvasLabel(result, height, theme.text);
236
+ }
237
+
238
+ private drawBackground(centerX: number, centerY: number, radius: number, theme: Theme): void {
239
+ if (!this.canvas || !this.context) return;
240
+ const background = this.context.createRadialGradient(centerX, centerY, radius * 0.1, centerX, centerY, radius * 1.45);
241
+ background.addColorStop(0, theme.center);
242
+ background.addColorStop(1, theme.background);
243
+ this.context.fillStyle = background;
244
+ this.context.fillRect(0, 0, this.canvas.width, this.canvas.height);
245
+ }
246
+
247
+ private clipScope(centerX: number, centerY: number, radius: number): void { this.context?.beginPath(); this.context?.arc(centerX, centerY, radius * 1.35, 0, Math.PI * 2); this.context?.clip(); }
248
+
249
+ private drawScopeRing(centerX: number, centerY: number, radius: number): void {
250
+ if (!this.context) return;
251
+ this.context.beginPath();
252
+ this.context.arc(centerX, centerY, radius * 1.35, 0, Math.PI * 2);
253
+ this.context.lineWidth = 12;
254
+ this.context.strokeStyle = 'rgba(15, 23, 42, 0.18)';
255
+ this.context.stroke();
256
+ }
257
+
258
+ private drawCanvasLabel(result: Result, height: number, textColor: string): void {
259
+ if (!this.context) return;
260
+ const focusLabel = this.focusDirection === 'raised' ? this.ui.focusRaised : this.ui.focusLowered;
261
+ const label = result.lineDirection === 'matched' ? this.ui.canvasMatched : `${this.ui.canvasHalo}${this.ui.canvasFocusSeparator}${focusLabel}`;
262
+ this.context.fillStyle = textColor;
263
+ this.context.font = '600 24px system-ui, sans-serif';
264
+ this.context.fillText(label, 28, height - 34);
265
+ }
266
+
267
+ private animateDraw(result: Result): void {
268
+ cancelAnimationFrame(this.animationFrame);
269
+ const startOffset = this.previousHaloOffset;
270
+ const targetOffset = result.haloOffset * HALO_SCALE[this.glassReference];
271
+ const startedAt = performance.now();
272
+ const frame = (now: number): void => {
273
+ const progress = Math.min(1, (now - startedAt) / 260);
274
+ const offset = startOffset + ((targetOffset - startOffset) * (1 - Math.pow(1 - progress, 3)));
275
+ this.draw(result, offset);
276
+ if (progress < 1) this.animationFrame = requestAnimationFrame(frame);
277
+ else this.previousHaloOffset = targetOffset;
278
+ };
279
+ this.animationFrame = requestAnimationFrame(frame);
280
+ }
281
+ }
@@ -0,0 +1,9 @@
1
+ ---
2
+ import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
3
+ import { forensicImageAuthenticityAnalyzer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+ interface Props { locale?: KnownLocale; }
6
+ const { locale = 'en' } = Astro.props;
7
+ const content = await forensicImageAuthenticityAnalyzer.i18n[locale]?.();
8
+ ---
9
+ {content && <SharedBibliography links={content.bibliography} />}
@@ -0,0 +1,7 @@
1
+ import type { BibliographyEntry } from '../../types';
2
+
3
+ export const bibliography: BibliographyEntry[] = [
4
+ { name: 'CIPA - Exchangeable image file format for digital still cameras: Exif 3.0', url: 'https://www.cipa.jp/std/documents/e/DC-008-Translation-2023-E.pdf' },
5
+ { name: 'NIST - Digital Investigation Techniques: A NIST Scientific Foundation Review', url: 'https://doi.org/10.6028/NIST.IR.8354-draft' },
6
+ { name: 'SWGDE - Best Practices for Image Authentication', url: 'https://www.swgde.org/documents/published-complete-listing/' },
7
+ ];
@@ -0,0 +1,250 @@
1
+ ---
2
+ import './forensic-image-metadata-authenticity-analyzer.css';
3
+ import { forensicImageAuthenticityAnalyzer } from './index';
4
+ import type { KnownLocale } from '../../types';
5
+
6
+ interface Props { locale?: KnownLocale; }
7
+
8
+ const { locale = 'en' } = Astro.props;
9
+ const content = await forensicImageAuthenticityAnalyzer.i18n[locale]?.();
10
+ if (!content) return null;
11
+ const { ui } = content;
12
+ ---
13
+ <section class="fia-lab" data-image-analyzer>
14
+ <header class="fia-header">
15
+ <span class="fia-kicker">{ui.privacy}</span>
16
+ <div class="fia-pulse"><i></i><span id="fia-state">{ui.waiting}</span></div>
17
+ </header>
18
+
19
+ <label class="fia-drop" id="fia-drop" data-has-image="false">
20
+ <input id="fia-input" type="file" accept="image/jpeg,image/png,.jpg,.jpeg,.png" />
21
+ <span class="fia-scene" aria-hidden="true">
22
+ <span class="fia-orbit fia-orbit-a"></span>
23
+ <span class="fia-orbit fia-orbit-b"></span>
24
+ <span class="fia-aperture"><i></i></span>
25
+ <span class="fia-scan-line"></span>
26
+ <span class="fia-scene-label fia-label-exif">EXIF</span>
27
+ <span class="fia-scene-label fia-label-gps">GPS</span>
28
+ <span class="fia-scene-label fia-label-hex">FF D8</span>
29
+ </span>
30
+ <span class="fia-drop-copy">
31
+ <strong>{ui.dropTitle}</strong>
32
+ <small>{ui.dropHint}</small>
33
+ <span class="fia-button" id="fia-button">
34
+ <span class="fia-button-icon" id="fia-button-icon">+</span>
35
+ <span class="fia-button-label" id="fia-button-label">{ui.chooseFile}</span>
36
+ </span>
37
+ </span>
38
+ </label>
39
+
40
+ <div class="fia-workbench" id="fia-workbench" hidden>
41
+ <article class="fia-panel fia-file">
42
+ <div class="fia-preview"><img id="fia-preview" alt="" /></div>
43
+ <dl>
44
+ <div><dt>{ui.fileName}</dt><dd id="fia-name">-</dd></div>
45
+ <div><dt>{ui.fileSize}</dt><dd id="fia-size">-</dd></div>
46
+ <div><dt>{ui.fileType}</dt><dd id="fia-format">-</dd></div>
47
+ </dl>
48
+ </article>
49
+
50
+ <article class="fia-panel fia-integrity">
51
+ <div class="fia-panel-title"><span>02</span><h3>{ui.integrity}</h3></div>
52
+ <div class="fia-score-ring">
53
+ <svg viewBox="0 0 120 120" aria-hidden="true">
54
+ <circle cx="60" cy="60" r="51"></circle>
55
+ <circle id="fia-score-arc" cx="60" cy="60" r="51"></circle>
56
+ </svg>
57
+ <div class="fia-score">
58
+ <strong id="fia-score">-</strong>
59
+ <span>/ 100</span>
60
+ <small>{ui.score}</small>
61
+ </div>
62
+ </div>
63
+ <div class="fia-status" id="fia-status">-</div>
64
+ <div class="fia-findings" id="fia-findings"></div>
65
+ </article>
66
+
67
+ <article class="fia-panel fia-metadata">
68
+ <div class="fia-panel-title"><span>01</span><h3>{ui.metadata}</h3></div>
69
+ <dl>
70
+ <div><dt>{ui.camera}</dt><dd id="fia-camera">{ui.noData}</dd></div>
71
+ <div><dt>{ui.captured}</dt><dd id="fia-date">{ui.noData}</dd></div>
72
+ <div><dt>{ui.software}</dt><dd id="fia-software">{ui.noData}</dd></div>
73
+ <div><dt>{ui.coordinates}</dt><dd id="fia-coordinates">{ui.noData}</dd></div>
74
+ </dl>
75
+ </article>
76
+
77
+ <article class="fia-panel fia-location">
78
+ <div class="fia-panel-title"><span>03</span><h3>{ui.location}</h3></div>
79
+ <div class="fia-map" id="fia-map"><div class="fia-grid"></div><div class="fia-equator"></div><div class="fia-meridian"></div><div class="fia-pin" id="fia-pin" hidden></div></div>
80
+ <p id="fia-no-gps">{ui.noGps}</p>
81
+ <a id="fia-map-link" target="_blank" rel="noopener noreferrer" hidden>{ui.mapLink}</a>
82
+ </article>
83
+
84
+ <article class="fia-panel fia-hex">
85
+ <div class="fia-panel-title"><span>04</span><div><h3>{ui.hex}</h3><p>{ui.hexHint}</p></div></div>
86
+ <pre id="fia-hex-output"></pre>
87
+ </article>
88
+ </div>
89
+
90
+ <p class="fia-disclaimer">{ui.disclaimer}</p>
91
+ </section>
92
+
93
+ <script id="fia-ui" type="application/json" set:html={JSON.stringify(ui)}></script>
94
+ <script>
95
+ import { ExifExtractor, IntegrityChecker, createHexSegments } from './logic';
96
+ import type { ExifMetadata, IntegrityFinding, IntegrityResult } from './logic';
97
+
98
+ const ui = JSON.parse(document.getElementById('fia-ui')?.textContent || '{}');
99
+ const input = document.getElementById('fia-input') as HTMLInputElement | null;
100
+ const drop = document.getElementById('fia-drop') as HTMLElement | null;
101
+ const workbench = document.getElementById('fia-workbench');
102
+ const extractor = new ExifExtractor();
103
+ const checker = new IntegrityChecker();
104
+
105
+ const text = (id: string, value: string) => {
106
+ const element = document.getElementById(id);
107
+ if (element) element.textContent = value;
108
+ };
109
+
110
+ function renderHex(buffer: ArrayBuffer): void {
111
+ const output = document.getElementById('fia-hex-output');
112
+ if (!output) return;
113
+ output.innerHTML = '';
114
+ for (const segment of createHexSegments(buffer)) {
115
+ const row = document.createElement('span');
116
+ row.className = `fia-hex-row fia-${segment.kind}`;
117
+ row.textContent = `${segment.offset.toString(16).padStart(8, '0')} ${segment.bytes.map((byte) => byte.toString(16).padStart(2, '0')).join(' ').padEnd(47, ' ')} ${segment.bytes.map((byte) => byte >= 32 && byte <= 126 ? String.fromCharCode(byte) : '.').join('')}\n`;
118
+ output.appendChild(row);
119
+ }
120
+ }
121
+
122
+ function getStatusLabel(classification: IntegrityResult['classification']): string {
123
+ if (classification === 'editing-signatures-detected') return ui.statusEditing;
124
+ if (classification === 'review-recommended') return ui.statusReview;
125
+ return ui.statusNoObvious;
126
+ }
127
+
128
+ function formatCoordinates(latitude?: number, longitude?: number): string {
129
+ if (latitude === undefined || longitude === undefined) return ui.noData;
130
+ return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;
131
+ }
132
+
133
+ function updateSummary(file: File, metadata: ExifMetadata, result: IntegrityResult): void {
134
+ text('fia-name', file.name);
135
+ text('fia-size', `${(file.size / 1024).toFixed(1)} KB`);
136
+ text('fia-format', result.format);
137
+ text('fia-camera', [metadata.make, metadata.model].filter(Boolean).join(' ') || ui.noData);
138
+ text('fia-date', metadata.dateTime || ui.noData);
139
+ text('fia-software', metadata.software || ui.noData);
140
+ text('fia-coordinates', formatCoordinates(metadata.latitude, metadata.longitude));
141
+ }
142
+
143
+ function updateIntegrity(result: IntegrityResult): void {
144
+ const scoreArc = document.getElementById('fia-score-arc') as SVGCircleElement | null;
145
+ const status = document.getElementById('fia-status');
146
+ text('fia-score', String(result.score));
147
+ text('fia-status', getStatusLabel(result.classification));
148
+ if (scoreArc) scoreArc.style.strokeDashoffset = String(320 - result.score * 3.2);
149
+ if (status) status.dataset.level = result.classification;
150
+ }
151
+
152
+ function appendFinding(findings: HTMLElement, finding: IntegrityFinding): void {
153
+ const item = document.createElement('div');
154
+ const strong = document.createElement('strong');
155
+ const small = document.createElement('small');
156
+ item.className = `fia-finding fia-${finding.severity}`;
157
+ strong.textContent = finding.title;
158
+ small.textContent = finding.detail;
159
+ item.append(strong, small);
160
+ findings.appendChild(item);
161
+ }
162
+
163
+ function renderFindings(result: IntegrityResult): void {
164
+ const findings = document.getElementById('fia-findings');
165
+ if (!findings) return;
166
+ findings.innerHTML = '';
167
+ result.findings.forEach((finding) => appendFinding(findings, finding));
168
+ }
169
+
170
+ function updatePreview(file: File): void {
171
+ const preview = document.getElementById('fia-preview') as HTMLImageElement | null;
172
+ if (preview) preview.src = URL.createObjectURL(file);
173
+ }
174
+
175
+ function resetMap(): void {
176
+ const pin = document.getElementById('fia-pin') as HTMLElement | null;
177
+ const noGps = document.getElementById('fia-no-gps');
178
+ const link = document.getElementById('fia-map-link') as HTMLAnchorElement | null;
179
+ if (pin) pin.hidden = true;
180
+ if (link) link.hidden = true;
181
+ if (noGps) noGps.hidden = false;
182
+ }
183
+
184
+ function updateMap(metadata: ExifMetadata): void {
185
+ const pin = document.getElementById('fia-pin') as HTMLElement | null;
186
+ const noGps = document.getElementById('fia-no-gps');
187
+ const link = document.getElementById('fia-map-link') as HTMLAnchorElement | null;
188
+ if (!pin || !link || metadata.latitude === undefined || metadata.longitude === undefined) {
189
+ resetMap();
190
+ return;
191
+ }
192
+
193
+ pin.hidden = false;
194
+ pin.style.left = `${((metadata.longitude ?? 0) + 180) / 360 * 100}%`;
195
+ pin.style.top = `${(90 - (metadata.latitude ?? 0)) / 180 * 100}%`;
196
+ link.hidden = false;
197
+ link.href = `https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=14/${metadata.latitude}/${metadata.longitude}`;
198
+ if (noGps) noGps.hidden = true;
199
+ }
200
+
201
+ function showLoadedState(file: File): void {
202
+ if (workbench) workbench.hidden = false;
203
+ if (drop) drop.dataset.hasImage = 'true';
204
+ text('fia-button-label', ui.replaceFile);
205
+ text('fia-button-icon', '\u21bb');
206
+ text('fia-state', file.name);
207
+ }
208
+
209
+ async function analyze(file: File): Promise<void> {
210
+ text('fia-state', ui.processing);
211
+ try {
212
+ const buffer = await file.arrayBuffer();
213
+ const metadata = extractor.extract(buffer);
214
+ const result = checker.inspect(buffer, metadata);
215
+ showLoadedState(file);
216
+ updateSummary(file, metadata, result);
217
+ updateIntegrity(result);
218
+ renderFindings(result);
219
+ updatePreview(file);
220
+ updateMap(metadata);
221
+ renderHex(buffer);
222
+ } catch {
223
+ text('fia-state', ui.loadError);
224
+ }
225
+ }
226
+
227
+ input?.addEventListener('change', () => {
228
+ const file = input.files?.[0];
229
+ if (file) void analyze(file);
230
+ });
231
+
232
+ for (const eventName of ['dragenter', 'dragover']) {
233
+ drop?.addEventListener(eventName, (event) => {
234
+ event.preventDefault();
235
+ drop.classList.add('is-dragging');
236
+ });
237
+ }
238
+
239
+ for (const eventName of ['dragleave', 'drop']) {
240
+ drop?.addEventListener(eventName, (event) => {
241
+ event.preventDefault();
242
+ drop.classList.remove('is-dragging');
243
+ });
244
+ }
245
+
246
+ drop?.addEventListener('drop', (event) => {
247
+ const file = (event as DragEvent).dataTransfer?.files[0];
248
+ if (file) void analyze(file);
249
+ });
250
+ </script>
@@ -0,0 +1,29 @@
1
+ import type { ScienceToolEntry, ToolLocaleContent } from '../../types';
2
+
3
+ export interface ImageAuthenticityUI {
4
+ [key: string]: string;
5
+ }
6
+
7
+ export type ImageAuthenticityLocaleContent = ToolLocaleContent<ImageAuthenticityUI>;
8
+
9
+ export const forensicImageAuthenticityAnalyzer: ScienceToolEntry<ImageAuthenticityUI> = {
10
+ id: 'forensic-image-authenticity-analyzer',
11
+ icons: { bg: 'mdi:image-search-outline', fg: 'mdi:shield-search' },
12
+ i18n: {
13
+ de: () => import('./i18n/de').then((module) => module.content),
14
+ en: () => import('./i18n/en').then((module) => module.content),
15
+ es: () => import('./i18n/es').then((module) => module.content),
16
+ fr: () => import('./i18n/fr').then((module) => module.content),
17
+ id: () => import('./i18n/id').then((module) => module.content),
18
+ it: () => import('./i18n/it').then((module) => module.content),
19
+ ja: () => import('./i18n/ja').then((module) => module.content),
20
+ ko: () => import('./i18n/ko').then((module) => module.content),
21
+ nl: () => import('./i18n/nl').then((module) => module.content),
22
+ pl: () => import('./i18n/pl').then((module) => module.content),
23
+ pt: () => import('./i18n/pt').then((module) => module.content),
24
+ ru: () => import('./i18n/ru').then((module) => module.content),
25
+ sv: () => import('./i18n/sv').then((module) => module.content),
26
+ tr: () => import('./i18n/tr').then((module) => module.content),
27
+ zh: () => import('./i18n/zh').then((module) => module.content),
28
+ },
29
+ };