@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.
- package/package.json +69 -0
- package/scripts/crystal-lattice-catalog-translations.mjs +459 -0
- package/scripts/postinstall.mjs +27 -0
- package/scripts/sync-crystal-lattice-i18n.mjs +50 -0
- package/src/category/i18n/de.ts +55 -0
- package/src/category/i18n/en.ts +55 -0
- package/src/category/i18n/es.ts +55 -0
- package/src/category/i18n/fr.ts +55 -0
- package/src/category/i18n/id.ts +55 -0
- package/src/category/i18n/it.ts +55 -0
- package/src/category/i18n/ja.ts +56 -0
- package/src/category/i18n/ko.ts +56 -0
- package/src/category/i18n/nl.ts +55 -0
- package/src/category/i18n/pl.ts +55 -0
- package/src/category/i18n/pt.ts +55 -0
- package/src/category/i18n/ru.ts +55 -0
- package/src/category/i18n/sv.ts +55 -0
- package/src/category/i18n/tr.ts +55 -0
- package/src/category/i18n/zh.ts +56 -0
- package/src/category/index.ts +48 -0
- package/src/category/seo.astro +15 -0
- package/src/components/PreviewNavSidebar.astro +116 -0
- package/src/components/PreviewToolbar.astro +143 -0
- package/src/data.ts +10 -0
- package/src/entries.ts +48 -0
- package/src/env.d.ts +5 -0
- package/src/index.ts +29 -0
- package/src/layouts/PreviewLayout.astro +117 -0
- package/src/pages/[locale]/[slug].astro +162 -0
- package/src/pages/[locale].astro +261 -0
- package/src/pages/index.astro +3 -0
- package/src/tests/diacritics_density.test.ts +140 -0
- package/src/tests/faq_count.test.ts +19 -0
- package/src/tests/i18n_coverage.test.ts +36 -0
- package/src/tests/inverted_punctuation.test.ts +84 -0
- package/src/tests/locale_completeness.test.ts +24 -0
- package/src/tests/mocks/astro_mock.js +2 -0
- package/src/tests/no_en_dash.test.ts +70 -0
- package/src/tests/no_h1_in_components.test.ts +48 -0
- package/src/tests/schemas_fulfillment.test.ts +23 -0
- package/src/tests/script_density.test.ts +94 -0
- package/src/tests/seo_length.test.ts +46 -0
- package/src/tests/shared-test-helpers.ts +56 -0
- package/src/tests/slug_language_code_format.test.ts +23 -0
- package/src/tests/slug_uniqueness.test.ts +81 -0
- package/src/tests/title_quality.test.ts +55 -0
- package/src/tests/tool_exports.test.ts +34 -0
- package/src/tests/tool_validation.test.ts +16 -0
- package/src/tests/widmark.test.ts +28 -0
- package/src/tool/forensic-age-estimator/bibliography.astro +14 -0
- package/src/tool/forensic-age-estimator/bibliography.ts +16 -0
- package/src/tool/forensic-age-estimator/component.astro +321 -0
- package/src/tool/forensic-age-estimator/dental-skeletal-third-molar-age-estimator.css +634 -0
- package/src/tool/forensic-age-estimator/entry.ts +32 -0
- package/src/tool/forensic-age-estimator/i18n/de.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/en.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/es.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/fr.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/id.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/it.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/ja.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/ko.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/nl.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/pl.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/pt.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/ru.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/sv.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/tr.ts +245 -0
- package/src/tool/forensic-age-estimator/i18n/zh.ts +246 -0
- package/src/tool/forensic-age-estimator/index.ts +11 -0
- package/src/tool/forensic-age-estimator/logic.ts +81 -0
- package/src/tool/forensic-age-estimator/seo.astro +15 -0
- package/src/tool/forensic-blood-test-simulator/bibliography.astro +6 -0
- package/src/tool/forensic-blood-test-simulator/bibliography.ts +12 -0
- package/src/tool/forensic-blood-test-simulator/component.astro +168 -0
- package/src/tool/forensic-blood-test-simulator/dom-utils.ts +40 -0
- package/src/tool/forensic-blood-test-simulator/entry.ts +32 -0
- package/src/tool/forensic-blood-test-simulator/forensic-presumptive-blood-testing-luminol-kastle-meyer-simulator.css +532 -0
- package/src/tool/forensic-blood-test-simulator/helpers.ts +16 -0
- package/src/tool/forensic-blood-test-simulator/i18n/de.ts +185 -0
- package/src/tool/forensic-blood-test-simulator/i18n/en.ts +185 -0
- package/src/tool/forensic-blood-test-simulator/i18n/es.ts +185 -0
- package/src/tool/forensic-blood-test-simulator/i18n/fr.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/id.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/it.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/ja.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/ko.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/nl.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/pl.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/pt.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/ru.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/sv.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/tr.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/i18n/zh.ts +220 -0
- package/src/tool/forensic-blood-test-simulator/index.ts +11 -0
- package/src/tool/forensic-blood-test-simulator/logic.ts +174 -0
- package/src/tool/forensic-blood-test-simulator/script.ts +192 -0
- package/src/tool/forensic-blood-test-simulator/seo.astro +15 -0
- package/src/tool/forensic-fiber-comparison-microscope/bibliography.astro +6 -0
- package/src/tool/forensic-fiber-comparison-microscope/bibliography.ts +16 -0
- package/src/tool/forensic-fiber-comparison-microscope/component.astro +146 -0
- package/src/tool/forensic-fiber-comparison-microscope/entry.ts +32 -0
- package/src/tool/forensic-fiber-comparison-microscope/forensic-fiber-comparison-microscope.css +629 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/de.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/en.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/es.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/fr.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/id.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/it.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/ja.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/ko.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/nl.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/pl.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/pt.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/ru.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/sv.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/tr.ts +246 -0
- package/src/tool/forensic-fiber-comparison-microscope/i18n/zh.ts +250 -0
- package/src/tool/forensic-fiber-comparison-microscope/index.ts +11 -0
- package/src/tool/forensic-fiber-comparison-microscope/logic.ts +244 -0
- package/src/tool/forensic-fiber-comparison-microscope/render.ts +265 -0
- package/src/tool/forensic-fiber-comparison-microscope/seo.astro +15 -0
- package/src/tool/forensic-fiber-comparison-microscope/view.ts +267 -0
- package/src/tool/forensic-glass-becke-line-simulator/bibliography.astro +6 -0
- package/src/tool/forensic-glass-becke-line-simulator/bibliography.ts +16 -0
- package/src/tool/forensic-glass-becke-line-simulator/component.astro +81 -0
- package/src/tool/forensic-glass-becke-line-simulator/entry.ts +32 -0
- package/src/tool/forensic-glass-becke-line-simulator/forensic-glass-becke-line-simulator.css +392 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/de.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/en.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/es.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/fr.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/id.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/it.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/ja.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/ko.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/nl.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/pl.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/pt.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/ru.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/sv.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/tr.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/i18n/zh.ts +231 -0
- package/src/tool/forensic-glass-becke-line-simulator/index.ts +11 -0
- package/src/tool/forensic-glass-becke-line-simulator/logic.ts +100 -0
- package/src/tool/forensic-glass-becke-line-simulator/seo.astro +15 -0
- package/src/tool/forensic-glass-becke-line-simulator/view.ts +281 -0
- package/src/tool/forensic-image-authenticity-analyzer/bibliography.astro +9 -0
- package/src/tool/forensic-image-authenticity-analyzer/bibliography.ts +7 -0
- package/src/tool/forensic-image-authenticity-analyzer/component.astro +250 -0
- package/src/tool/forensic-image-authenticity-analyzer/entry.ts +29 -0
- package/src/tool/forensic-image-authenticity-analyzer/forensic-image-metadata-authenticity-analyzer.css +679 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/de.ts +105 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/en.ts +105 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/es.ts +105 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/fr.ts +105 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/id.ts +76 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/it.ts +105 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/ja.ts +72 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/ko.ts +72 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/nl.ts +75 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/pl.ts +75 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/pt.ts +105 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/ru.ts +75 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/sv.ts +76 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/tr.ts +76 -0
- package/src/tool/forensic-image-authenticity-analyzer/i18n/zh.ts +71 -0
- package/src/tool/forensic-image-authenticity-analyzer/index.ts +11 -0
- package/src/tool/forensic-image-authenticity-analyzer/logic.ts +283 -0
- package/src/tool/forensic-image-authenticity-analyzer/seo.astro +10 -0
- package/src/tool/forensic-microcrystal-drug-simulator/bibliography.astro +6 -0
- package/src/tool/forensic-microcrystal-drug-simulator/bibliography.ts +12 -0
- package/src/tool/forensic-microcrystal-drug-simulator/component.astro +240 -0
- package/src/tool/forensic-microcrystal-drug-simulator/entry.ts +32 -0
- package/src/tool/forensic-microcrystal-drug-simulator/forensic-microcrystal-drug-simulator.css +430 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/de.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/en.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/es.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/fr.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/id.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/it.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/ja.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/ko.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/nl.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/pl.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/pt.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/ru.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/sv.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/tr.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/i18n/zh.ts +244 -0
- package/src/tool/forensic-microcrystal-drug-simulator/index.ts +11 -0
- package/src/tool/forensic-microcrystal-drug-simulator/logic.ts +189 -0
- package/src/tool/forensic-microcrystal-drug-simulator/seo.astro +15 -0
- package/src/tool/forensic-sex-determinator/bibliography.astro +14 -0
- package/src/tool/forensic-sex-determinator/bibliography.ts +12 -0
- package/src/tool/forensic-sex-determinator/component.astro +463 -0
- package/src/tool/forensic-sex-determinator/entry.ts +32 -0
- package/src/tool/forensic-sex-determinator/forensic-sex-determinator.css +413 -0
- package/src/tool/forensic-sex-determinator/i18n/de.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/en.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/es.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/fr.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/id.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/it.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/ja.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/ko.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/nl.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/pl.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/pt.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/ru.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/sv.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/tr.ts +211 -0
- package/src/tool/forensic-sex-determinator/i18n/zh.ts +211 -0
- package/src/tool/forensic-sex-determinator/index.ts +11 -0
- package/src/tool/forensic-sex-determinator/logic.ts +89 -0
- package/src/tool/forensic-sex-determinator/seo.astro +15 -0
- package/src/tool/forensic-stature-estimator/bibliography.astro +14 -0
- package/src/tool/forensic-stature-estimator/bibliography.ts +12 -0
- package/src/tool/forensic-stature-estimator/component.astro +49 -0
- package/src/tool/forensic-stature-estimator/components/EstimationPanel.astro +65 -0
- package/src/tool/forensic-stature-estimator/components/OsteometricBoard.astro +39 -0
- package/src/tool/forensic-stature-estimator/components/OsteometricSelector.astro +109 -0
- package/src/tool/forensic-stature-estimator/dom-utils.ts +71 -0
- package/src/tool/forensic-stature-estimator/entry.ts +32 -0
- package/src/tool/forensic-stature-estimator/forensic-stature-estimator.css +689 -0
- package/src/tool/forensic-stature-estimator/helpers.ts +51 -0
- package/src/tool/forensic-stature-estimator/i18n/de.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/en.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/es.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/fr.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/id.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/it.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/ja.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/ko.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/nl.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/pl.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/pt.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/ru.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/sv.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/tr.ts +196 -0
- package/src/tool/forensic-stature-estimator/i18n/zh.ts +196 -0
- package/src/tool/forensic-stature-estimator/index.ts +11 -0
- package/src/tool/forensic-stature-estimator/logic.ts +119 -0
- package/src/tool/forensic-stature-estimator/script.ts +288 -0
- package/src/tool/forensic-stature-estimator/seo.astro +15 -0
- package/src/tool/forensic-tlc-ink-simulator/bibliography.astro +6 -0
- package/src/tool/forensic-tlc-ink-simulator/bibliography.ts +16 -0
- package/src/tool/forensic-tlc-ink-simulator/component.astro +245 -0
- package/src/tool/forensic-tlc-ink-simulator/entry.ts +32 -0
- package/src/tool/forensic-tlc-ink-simulator/forensic-tlc-ink-simulator.css +462 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/de.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/en.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/es.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/fr.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/id.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/it.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/ja.ts +235 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/ko.ts +235 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/nl.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/pl.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/pt.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/ru.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/sv.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/tr.ts +243 -0
- package/src/tool/forensic-tlc-ink-simulator/i18n/zh.ts +235 -0
- package/src/tool/forensic-tlc-ink-simulator/index.ts +11 -0
- package/src/tool/forensic-tlc-ink-simulator/logic.ts +152 -0
- package/src/tool/forensic-tlc-ink-simulator/seo.astro +15 -0
- package/src/tool/gsr-dispersion-calculator/bibliography.astro +6 -0
- package/src/tool/gsr-dispersion-calculator/bibliography.ts +16 -0
- package/src/tool/gsr-dispersion-calculator/component.astro +294 -0
- package/src/tool/gsr-dispersion-calculator/entry.ts +32 -0
- package/src/tool/gsr-dispersion-calculator/gsr-dispersion-calculator.css +305 -0
- package/src/tool/gsr-dispersion-calculator/i18n/de.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/en.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/es.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/fr.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/id.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/it.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/ja.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/ko.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/nl.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/pl.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/pt.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/ru.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/sv.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/tr.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/i18n/zh.ts +272 -0
- package/src/tool/gsr-dispersion-calculator/index.ts +11 -0
- package/src/tool/gsr-dispersion-calculator/logic.ts +148 -0
- package/src/tool/gsr-dispersion-calculator/seo.astro +15 -0
- package/src/tool/widmark-alcohol-simulator/bibliography.astro +14 -0
- package/src/tool/widmark-alcohol-simulator/bibliography.ts +12 -0
- package/src/tool/widmark-alcohol-simulator/component.astro +453 -0
- package/src/tool/widmark-alcohol-simulator/entry.ts +32 -0
- package/src/tool/widmark-alcohol-simulator/i18n/de.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/en.ts +206 -0
- package/src/tool/widmark-alcohol-simulator/i18n/es.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/fr.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/id.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/it.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/ja.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/ko.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/nl.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/pl.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/pt.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/ru.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/sv.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/tr.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/i18n/zh.ts +193 -0
- package/src/tool/widmark-alcohol-simulator/index.ts +11 -0
- package/src/tool/widmark-alcohol-simulator/logic.ts +97 -0
- package/src/tool/widmark-alcohol-simulator/seo.astro +15 -0
- package/src/tool/widmark-alcohol-simulator/widmark-alcohol-simulator.css +386 -0
- package/src/tools.ts +27 -0
- package/src/types.ts +70 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'forenzicheskiy-analizator-metadannyh-i-podlinnosti-izobrazheniy';
|
|
5
|
+
const title = 'Форензический анализатор метаданных и подлинности изображений';
|
|
6
|
+
const description = 'Проверяйте заголовки изображений, EXIF-данные съемки, GPS-координаты, следы редакторов и сырые байты локально в браузере.';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{ name: 'Сохраните исходное доказательство', text: 'Работайте с форензической копией и храните исходный файл и его криптографический хэш вне этого браузерного инструмента.' },
|
|
10
|
+
{ name: 'Загрузите изображение локально', text: 'Перетащите или выберите JPEG либо PNG. Файл читается в памяти браузера и не загружается в сеть.' },
|
|
11
|
+
{ name: 'Проверьте метаданные и местоположение', text: 'Сопоставьте время съемки, данные камеры, программное обеспечение и GPS-поля с материалами дела и журналом получения.' },
|
|
12
|
+
{ name: 'Интерпретируйте сигналы целостности', text: 'Сигнатуры редактирования и отсутствующие поля рассматривайте как следственные зацепки, а не как доказательство подделки.' },
|
|
13
|
+
{ name: 'Изучите шестнадцатеричный просмотр', text: 'Используйте выделенные зоны заголовка и метаданных, чтобы распознать структуру контейнера и зафиксировать смещения для дальнейшего анализа.' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const faq = [
|
|
17
|
+
{ question: 'Могут ли метаданные доказать подлинность фотографии?', answer: 'Нет. Метаданные можно удалить, скопировать или изменить. Установление подлинности требует сочетания структуры файла, происхождения, хэшей, визуального анализа, анализа сжатия и валидированных форензических методов.' },
|
|
18
|
+
{ question: 'Доказывает ли след Adobe или GIMP злонамеренное редактирование?', answer: 'Нет. Это лишь указывает на то, что программное обеспечение могло записать метаданные или экспортировать файл. Законная цветокоррекция, редакционная обработка или подготовка доказательства могут оставить тот же след.' },
|
|
19
|
+
{ question: 'Загружается ли изображение в сеть?', answer: 'Нет. Анализ выполняется в памяти браузера. Тем не менее перед открытием чувствительных материалов в любом ПО соблюдайте политику обращения с доказательствами вашей организации.' },
|
|
20
|
+
{ question: 'Почему GPS-данные могут отсутствовать?', answer: 'Камера могла не поддерживать GPS, запись местоположения могла быть отключена, платформа могла удалить метаданные, либо файл мог быть перекодирован.' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const content: ToolLocaleContent = {
|
|
24
|
+
slug,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
ui: {
|
|
28
|
+
privacy: 'Только локальное двоичное исследование',
|
|
29
|
+
dropTitle: 'Поместите изображение на стол доказательств',
|
|
30
|
+
dropHint: 'Перетащите сюда JPEG или PNG либо выберите файл. Ничего не загружается.',
|
|
31
|
+
chooseFile: 'Выбрать изображение',
|
|
32
|
+
replaceFile: 'Заменить изображение',
|
|
33
|
+
waiting: 'Ожидание доказательства',
|
|
34
|
+
metadata: 'Метаданные съемки',
|
|
35
|
+
integrity: 'Сигналы целостности',
|
|
36
|
+
location: 'Зафиксированное местоположение',
|
|
37
|
+
hex: 'Шестнадцатеричное окно доказательств',
|
|
38
|
+
hexHint: 'Первые 512 байт · бирюзовый заголовок · янтарные метаданные · нейтральные данные изображения',
|
|
39
|
+
noData: 'Нет читаемого значения',
|
|
40
|
+
noGps: 'Читаемые GPS-координаты не найдены.',
|
|
41
|
+
mapLink: 'Открыть координаты в OpenStreetMap',
|
|
42
|
+
score: 'Эвристическая уверенность',
|
|
43
|
+
disclaimer: 'Высокий балл не доказывает подлинность. Сохраняйте оригинал, вычисляйте криптографические хэши и используйте валидированные лабораторные процедуры для выводов по делу.',
|
|
44
|
+
fileName: 'Файл',
|
|
45
|
+
fileSize: 'Размер',
|
|
46
|
+
fileType: 'Контейнер',
|
|
47
|
+
camera: 'Камера',
|
|
48
|
+
captured: 'Снято',
|
|
49
|
+
software: 'Программное обеспечение',
|
|
50
|
+
coordinates: 'Координаты',
|
|
51
|
+
statusNoObvious: 'Явных признаков редактирования не обнаружено',
|
|
52
|
+
statusReview: 'Рекомендуется проверка',
|
|
53
|
+
statusEditing: 'Обнаружен след редактирования',
|
|
54
|
+
processing: 'Чтение двоичных данных...',
|
|
55
|
+
loadError: 'Не удалось проанализировать файл. Выберите корректное изображение JPEG или PNG.',
|
|
56
|
+
},
|
|
57
|
+
seo: [
|
|
58
|
+
{ type: 'title', text: 'Как анализировать метаданные изображения и признаки подлинности', level: 2 },
|
|
59
|
+
{ type: 'paragraph', html: 'Форензический анализатор метаданных изображения помогает следователям, журналистам, юристам, специалистам по комплаенсу и исследователям отвечать на вопрос с высокой поисковой мотивацией: <strong>что именно метаданные изображения могут рассказать о фотографии?</strong> Метаданные могут давать полезные подсказки о съемке, месте, программной обработке и структуре файла, но не являются самостоятельной машиной истины. Их главная ценность - в первичной сортировке и выявлении файлов, требующих углубленного анализа.' },
|
|
60
|
+
{ type: 'paragraph', html: 'Этот браузерный инструмент предназначен для пользователей, которым нужно больше, чем сырой EXIF-дамп. Он локально читает выбранный JPEG или PNG и показывает поля камеры, временные отметки съемки, теги ПО, координаты, признаки контейнера и начальные байты файла в одном месте. Это соответствует типичным поисковым намерениям вроде <em>проверка подлинности фото</em>, <em>анализ EXIF</em> или <em>как понять, что изображение редактировали</em>.' },
|
|
61
|
+
{ type: 'paragraph', html: 'Ключевой принцип в том, что результат нужно читать как контекст, а не как вердикт. Файл может содержать полезные метаданные и при этом вводить в заблуждение. Он может почти не содержать метаданных и при этом быть подлинным. Поэтому метаданные следует рассматривать как один слой доказательств, который нужно сопоставлять с происхождением, хэшами, показаниями и валидированными методами анализа.' },
|
|
62
|
+
{ type: 'title', text: 'Что может и что не может рассказать EXIF', level: 3 },
|
|
63
|
+
{ type: 'paragraph', html: 'Поля EXIF, такие как модель камеры, выдержка, диафрагма, ISO, фокусное расстояние и наличие вспышки, помогают следователю восстановить условия съёмки и сопоставить их с показаниями. Временная метка особенно полезна для проверки хронологии событий. Однако эти данные могут быть сфальсифицированы или удалены без следа. Инструменты вроде Adobe Lightroom, ExifTool и мобильные редакторы позволяют перезаписывать большинство полей. Поэтому интерпретация EXIF требует осторожности и учёта всей совокупности доказательств. Отсутствие EXIF не равно подделке, а полный набор метаданных не гарантирует подлинность. EXIF, GPS, сигнатуры редакторов и структура файла работают как взаимодополняющие слои, и только их совокупность может дать обоснованные выводы.' },
|
|
64
|
+
{ type: 'title', text: 'Как ответственно читать GPS-метаданные', level: 3 },
|
|
65
|
+
{ type: 'paragraph', html: 'GPS-координаты в изображении указывают, где был сделан снимок, и могут служить важной уликой при расследовании. Однако ответственное чтение GPS-метаданных требует понимания их ограничений: точность координат зависит от приёмника, облачного покрова и условий съёмки; координаты могут отсутствовать, если геотегирование было отключено или метаданные были удалены; возможна подмена координат с помощью редакторов метаданных. Координаты следует сопоставлять с журналом получения доказательства, показаниями свидетелей и другими материалами дела. Долгота и широта могут быть записаны в разных форматах, и инструмент отображает их в десятичных градусах для удобства проверки по картографическим сервисам.' },
|
|
66
|
+
],
|
|
67
|
+
faq,
|
|
68
|
+
bibliography,
|
|
69
|
+
howTo,
|
|
70
|
+
schemas: [
|
|
71
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
|
|
72
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
73
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'forensisk-bildmetadata-och-autenticitetsanalysator';
|
|
5
|
+
const title = 'Forensisk Analysator för Bildmetadata och Äkthet';
|
|
6
|
+
const description = 'Inspektera bildhuvuden, EXIF-data för upptagning, GPS-koordinater, spår av redigeringsprogram och råa byte lokalt i webbläsaren.';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{ name: 'Bevara originalbeviset', text: 'Arbeta från en forensisk kopia och förvara källfilen samt dess kryptografiska hash utanför detta webbläsarverktyg.' },
|
|
10
|
+
{ name: 'Ladda en bild lokalt', text: 'Dra in eller välj en JPEG eller PNG. Filen läses i webbläsarens minne och laddas inte upp.' },
|
|
11
|
+
{ name: 'Granska metadata och plats', text: 'Jämför upptagningstid, kamerauppgifter, programvara och GPS-fält med ärendets berättelse och insamlingsloggar.' },
|
|
12
|
+
{ name: 'Tolkar integritetssignaler', text: 'Behandla redigeringssignaturer och saknade fält som utredningsspår, inte som bevis på manipulation.' },
|
|
13
|
+
{ name: 'Undersök den hexadecimala förhandsvisningen', text: 'Använd de markerade huvud- och metadatazonerna för att identifiera containerstrukturen och dokumentera offset för vidare analys.' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const faq = [
|
|
17
|
+
{ question: 'Kan metadata bevisa att ett foto är äkta?', answer: 'Nej. Metadata kan tas bort, kopieras eller ändras. Autentisering kräver en kombination av filstruktur, proveniens, hashar, visuell granskning, kompressionsanalys och validerade forensiska metoder.' },
|
|
18
|
+
{ question: 'Bevisar en Adobe- eller GIMP-signatur skadlig redigering?', answer: 'Nej. Det visar bara att programvara kan ha skrivit metadata eller exporterat filen. Legitima färgkorrigeringar, redaktionell bearbetning eller bevisförberedelse kan ge samma signatur.' },
|
|
19
|
+
{ question: 'Laddas bilden upp?', answer: 'Nej. Analysen sker i webbläsarens minne. Följ ändå din organisations rutiner för bevishantering innan du öppnar känsligt material i någon programvara.' },
|
|
20
|
+
{ question: 'Varför kan GPS-data saknas?', answer: 'Kameran kanske inte stöder GPS, platsregistrering kan ha varit avstängd, en plattform kan ha tagit bort metadata eller filen kan ha kodats om.' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const content: ToolLocaleContent = {
|
|
24
|
+
slug,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
ui: {
|
|
28
|
+
privacy: 'Endast lokal binär undersökning',
|
|
29
|
+
dropTitle: 'Placera en bild på bevisbordet',
|
|
30
|
+
dropHint: 'Släpp en JPEG eller PNG här eller välj en fil. Ingenting laddas upp.',
|
|
31
|
+
chooseFile: 'Välj bild',
|
|
32
|
+
replaceFile: 'Byt bild',
|
|
33
|
+
waiting: 'Väntar på bevis',
|
|
34
|
+
metadata: 'Upptagningsmetadata',
|
|
35
|
+
integrity: 'Integritetssignaler',
|
|
36
|
+
location: 'Registrerad plats',
|
|
37
|
+
hex: 'Hexadecimalt bevisfönster',
|
|
38
|
+
hexHint: 'Första 512 byte · cyan huvud · bärnstensfärgade metadata · neutrala bilddata',
|
|
39
|
+
noData: 'Inget läsbart värde',
|
|
40
|
+
noGps: 'Inga läsbara GPS-koordinater hittades.',
|
|
41
|
+
mapLink: 'Öppna koordinater i OpenStreetMap',
|
|
42
|
+
score: 'Heuristisk tillförlitlighet',
|
|
43
|
+
disclaimer: 'Ett högt poängvärde fastställer inte äkthet. Bevara originalet, beräkna kryptografiska hashar och använd validerade laboratorieflöden för slutsatser i ärendet.',
|
|
44
|
+
fileName: 'Fil',
|
|
45
|
+
fileSize: 'Storlek',
|
|
46
|
+
fileType: 'Container',
|
|
47
|
+
camera: 'Kamera',
|
|
48
|
+
captured: 'Tagen',
|
|
49
|
+
software: 'Programvara',
|
|
50
|
+
coordinates: 'Koordinater',
|
|
51
|
+
statusNoObvious: 'Inga tydliga tecken på redigering',
|
|
52
|
+
statusReview: 'Granskning rekommenderas',
|
|
53
|
+
statusEditing: 'Redigeringssignatur upptäckt',
|
|
54
|
+
processing: 'Läser binärt bevismaterial...',
|
|
55
|
+
loadError: 'Filen kunde inte analyseras. Välj en giltig JPEG- eller PNG-bild.',
|
|
56
|
+
},
|
|
57
|
+
seo: [
|
|
58
|
+
{ type: 'title', text: 'Hur man analyserar bildmetadata och äkthetssignaler', level: 2 },
|
|
59
|
+
{ type: 'paragraph', html: 'En forensisk analysator för bildmetadata hjälper utredare, journalister, jurister, compliance-granskare och forskare att besvara en fråga med hög sökintention: <strong>vad kan bildmetadata faktiskt avslöja om ett fotografi?</strong> Metadata kan ge användbara ledtrådar om upptagning, plats, programvarubehandling och filstruktur, men fungerar inte som en fristående sanningsmaskin. Det största värdet ligger i triage och i att hitta vilka filer som kräver djupare granskning.' },
|
|
60
|
+
{ type: 'paragraph', html: 'Det här webbläsarverktyget är utformat för användare som vill ha mer än en rå EXIF-dump. Det läser den valda JPEG- eller PNG-filen lokalt och visar kamerafält, tidsstämplar, programvarutaggar, koordinater, containerledtrådar och de första byten på ett ställe. Det möter vanliga sökintentioner bakom uttryck som <em>kontrollera bildens äkthet</em>, <em>analysera EXIF-metadata</em> och <em>hur ser man om en bild har redigerats</em>.' },
|
|
61
|
+
{ type: 'paragraph', html: 'Den viktigaste principen är att resultatet ska läsas som kontext, inte som dom. En fil kan innehålla användbar metadata och ändå vara vilseledande. Därför bör metadata behandlas som ett bevislager som måste jämföras med proveniens, hashar, vittnesmål och validerade undersökningsmetoder.' },
|
|
62
|
+
{ type: 'title', text: 'Vad EXIF kan och inte kan säga om ett fotografi', level: 3 },
|
|
63
|
+
{ type: 'paragraph', html: 'EXIF-metadata (Exchangeable Image File Format) lagrar information som kameratillverkare, modell, objektiv, slutartid, bländare, ISO, vitbalans, blixtstatus och datum för upptagning. Dessa fält skapas av kamerans firmware vid exponeringsögonblicket och är ofta den första informationskällan i en forensisk undersökning. EXIF kan dock inte bevisa att en bild är oredigerad: nästan alla fält kan skrivas om med standardverktyg som ExifTool, Adobe Photoshop eller GIMP. Det går inte heller att från EXIF ensam avgöra om motivet är verkligt eller om bilden är ett montage. EXIF-tidsstämpeln anger när kameran registrerade filen, men om kamerans klocka var felaktig eller om filen har kopierats mellan enheter kan tidsuppgiften vara missvisande. I utredningssammanhang ska EXIF därför behandlas som en indikation som måste styrkas av kompletterande bevis såsom kryptografiska hashar, loggfiler från insamlingsenheten och visuell granskning av bildinnehållet.' },
|
|
64
|
+
{ type: 'title', text: 'Hur man läser GPS-metadata ansvarsfullt i forensisk kontext', level: 3 },
|
|
65
|
+
{ type: 'paragraph', html: 'GPS-koordinater i EXIF lagras som latitud och longitud i grader, minuter och sekunder (DMS) tillsammans med en referensriktning (N/S/E/W). Många kameror, särskilt inbyggda kameror i smartphones, registrerar dessa koordinater automatiskt om platstjänster är aktiverade. För en utredare är GPS-data användbar för att knyta en bild till en geografisk plats, men den måste tolkas med försiktighet. Koordinaterna kan vara frånvarande om platstjänster var avstängda, om efterföljande programvara har tagit bort dem eller om bilden har beskärts och exporterats på nytt. Det är också möjligt att manipulera GPS-fält manuellt för att placera en bild på en falsk plats. Kontrollera alltid koordinaternas överensstämmelse med annan bevisning såsom samtida mobilmaster, loggar, vittnesmål och kartdata. Använd verktyget för att dokumentera koordinaterna som en del av en bredare beviskedja snarare än som ett självständigt faktum.' },
|
|
66
|
+
],
|
|
67
|
+
faq,
|
|
68
|
+
bibliography,
|
|
69
|
+
howTo,
|
|
70
|
+
schemas: [
|
|
71
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
|
|
72
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
73
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'adli-goruntu-metadata-ve-ozgunluk-analizoru';
|
|
5
|
+
const title = 'Adli Görüntü Metadata ve Özgünlük Analizörü';
|
|
6
|
+
const description = 'Görüntü başlıklarını, EXIF çekim ayrıntılarını, GPS koordinatlarını, düzenleme yazılımı izlerini ve ham baytları tarayıcıda yerel olarak inceleyin.';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{ name: 'Orijinal delili koruyun', text: 'Adli kopya üzerinden çalışın ve kaynak dosyayı ile kriptografik özetini bu tarayıcı aracının dışında saklayın.' },
|
|
10
|
+
{ name: 'Görüntüyü yerel olarak yükleyin', text: 'Bir JPEG veya PNG sürükleyin ya da seçin. Dosya tarayıcı belleğinde okunur ve yüklenmez.' },
|
|
11
|
+
{ name: 'Metadata ve konumu gözden geçirin', text: 'Çekim zamanı, kamera kimliği, yazılım ve GPS alanlarını olay anlatımı ve edinim kayıtlarıyla karşılaştırın.' },
|
|
12
|
+
{ name: 'Bütünlük sinyallerini yorumlayın', text: 'Düzenleme imzaları ve eksik alanları manipülasyon kanıtı değil, soruşturma ipucu olarak değerlendirin.' },
|
|
13
|
+
{ name: 'Onaltılık önizlemeyi inceleyin', text: 'Vurgulanan başlık ve metadata bölgelerini kullanarak kapsayıcı yapısını tanıyın ve daha derin inceleme için ofsetleri belgeleyin.' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const faq = [
|
|
17
|
+
{ question: 'Metadata bir fotoğrafın özgün olduğunu kanıtlayabilir mi?', answer: 'Hayır. Metadata kaldırılabilir, kopyalanabilir veya değiştirilebilir. Kimlik doğrulama; dosya yapısı, köken, özetler, görsel inceleme, sıkıştırma analizi ve doğrulanmış adli yöntemlerin birlikte kullanılmasını gerektirir.' },
|
|
18
|
+
{ question: 'Adobe veya GIMP imzası kötü niyetli düzenlemeyi kanıtlar mı?', answer: 'Hayır. Yalnızca bir yazılımın metadata yazmış veya dosyayı dışa aktarmış olabileceğini gösterir. Meşru renk düzeltme, editoryal iş akışı veya delil hazırlığı aynı izi bırakabilir.' },
|
|
19
|
+
{ question: 'Görüntü yükleniyor mu?', answer: 'Hayır. Analiz tarayıcı belleğinde yapılır. Yine de hassas materyali herhangi bir yazılımda açmadan önce kuruluşunuzun delil işleme politikasını izleyin.' },
|
|
20
|
+
{ question: 'GPS verileri neden eksik olabilir?', answer: 'Kamera GPS desteklemiyor olabilir, konum kaydı kapalı olabilir, bir platform metadata\'yı kaldırmış olabilir veya dosya yeniden kodlanmış olabilir.' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const content: ToolLocaleContent = {
|
|
24
|
+
slug,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
ui: {
|
|
28
|
+
privacy: 'Yalnızca yerel ikili inceleme',
|
|
29
|
+
dropTitle: 'Görüntüyü delil masasına yerleştirin',
|
|
30
|
+
dropHint: 'Buraya bir JPEG veya PNG bırakın ya da dosya seçin. Hiçbir şey yüklenmez.',
|
|
31
|
+
chooseFile: 'Görüntü seç',
|
|
32
|
+
replaceFile: 'Görüntüyü değiştir',
|
|
33
|
+
waiting: 'Delil bekleniyor',
|
|
34
|
+
metadata: 'Çekim metadata\'sı',
|
|
35
|
+
integrity: 'Bütünlük sinyalleri',
|
|
36
|
+
location: 'Kaydedilen konum',
|
|
37
|
+
hex: 'Onaltılık delil penceresi',
|
|
38
|
+
hexHint: 'İlk 512 bayt · camgöbeği başlık · kehribar metadata · nötr görüntü verisi',
|
|
39
|
+
noData: 'Okunabilir değer yok',
|
|
40
|
+
noGps: 'Okunabilir GPS koordinatı bulunamadı.',
|
|
41
|
+
mapLink: 'Koordinatları OpenStreetMap\'te aç',
|
|
42
|
+
score: 'Sezgisel güven',
|
|
43
|
+
disclaimer: 'Yüksek puan özgünlüğü kanıtlamaz. Orijinali koruyun, kriptografik özetler hesaplayın ve vaka sonuçları için doğrulanmış laboratuvar iş akışları kullanın.',
|
|
44
|
+
fileName: 'Dosya',
|
|
45
|
+
fileSize: 'Boyut',
|
|
46
|
+
fileType: 'Kapsayıcı',
|
|
47
|
+
camera: 'Kamera',
|
|
48
|
+
captured: 'Çekildi',
|
|
49
|
+
software: 'Yazılım',
|
|
50
|
+
coordinates: 'Koordinatlar',
|
|
51
|
+
statusNoObvious: 'Belirgin düzenleme göstergesi yok',
|
|
52
|
+
statusReview: 'İnceleme önerilir',
|
|
53
|
+
statusEditing: 'Düzenleme imzası tespit edildi',
|
|
54
|
+
processing: 'İkili delil okunuyor...',
|
|
55
|
+
loadError: 'Dosya analiz edilemedi. Geçerli bir JPEG veya PNG görüntüsü seçin.',
|
|
56
|
+
},
|
|
57
|
+
seo: [
|
|
58
|
+
{ type: 'title', text: 'Görüntü metadata\'sı ve özgünlük göstergeleri nasıl analiz edilir', level: 2 },
|
|
59
|
+
{ type: 'paragraph', html: 'Adli görüntü metadata analizörü; araştırmacıların, gazetecilerin, hukuk ekiplerinin, uyum denetçilerinin ve araştırmacıların <strong>görüntü metadata\'sı bir fotoğraf hakkında gerçekten ne söyleyebilir?</strong> sorusuna cevap bulmasına yardımcı olur. Metadata, çekim, konum, yazılım işlemesi ve dosya yapısı hakkında yararlı ipuçları sunabilir; ancak tek başına bir doğruluk makinesi değildir. En büyük değeri ön elemede ortaya çıkar.' },
|
|
60
|
+
{ type: 'paragraph', html: 'Bu tarayıcı aracı, ham EXIF dökümünden daha fazlasını isteyen kullanıcılar için tasarlanmıştır. Seçilen JPEG veya PNG\'yi yerel olarak okur ve kamera alanlarını, çekim zaman damgalarını, yazılım etiketlerini, koordinatları, kapsayıcı ipuçlarını ve dosyanın ilk baytlarını tek yerde gösterir. Bu da <em>fotoğraf özgünlük kontrolü</em>, <em>EXIF metadata analizi</em> ve <em>bir görüntünün düzenlenip düzenlenmediği nasıl anlaşılır</em> gibi arama niyetlerine karşılık gelir.' },
|
|
61
|
+
{ type: 'paragraph', html: 'En önemli ilke, sonucun hüküm olarak değil bağlam olarak okunması gerektiğidir. Bir dosya yararlı metadata içerebilir ve yine de yanıltıcı olabilir. Bu nedenle metadata; köken, özetler, tanık anlatımları ve doğrulanmış inceleme yöntemleriyle karşılaştırılması gereken bir kanıt katmanı olarak ele alınmalıdır.' },
|
|
62
|
+
{ type: 'title', text: 'EXIF\'in Söyleyebilecekleri ve Söyleyemeyecekleri', level: 3 },
|
|
63
|
+
{ type: 'paragraph', html: 'EXIF (Exchangeable Image File Format) metadata\'sı bir fotoğraf makinesinin çekim anında dosyaya kaydettiği teknik bilgiler bütünüdür. Kamera modeli ve üreticisi, çekim tarihi ve saati, enstantane hızı, diyafram açıklığı, ISO değeri, odak uzaklığı, flaş kullanımı, beyaz dengesi ve çözünürlük gibi parametreleri içerir. Bu alanlar bir fotoğrafın hangi koşullar altında çekildiğine dair değerli ipuçları sağlar. Ancak EXIF metadata\'sının sınırlamalarını anlamak da en az içerdiği bilgiler kadar önemlidir. EXIF alanları standart metadata düzenleme araçlarıyla kolayca değiştirilebilir, kopyalanabilir veya tamamen silinebilir. Bir fotoğrafın EXIF verilerinin bozulmamış olması, o fotoğrafın orijinal olduğu anlamına gelmez. Bu nedenle EXIF analizi, her zaman diğer adli yöntemlerle ve bağımsız kanıtlarla desteklenmesi gereken bir başlangıç noktasıdır.' },
|
|
64
|
+
{ type: 'title', text: 'GPS Metadata\'yı Sorumlu Bir Şekilde Okuma', level: 3 },
|
|
65
|
+
{ type: 'paragraph', html: 'GPS metadata\'sı bir görüntünün çekildiği coğrafi konumu enlem ve boylam koordinatları olarak kaydeder. Bazı kameralar ve akıllı telefonlar ayrıca yükseklik, yön ve GPS hassasiyet bilgilerini de ekler. Bu veriler bir fotoğrafın nerede çekildiğini harita üzerinde doğrulamak için kritik öneme sahiptir. GPS metadata\'sını sorumlu bir şekilde okumak, bu verileri mutlak gerçek olarak değil, doğrulanması gereken bir ipucu olarak ele almayı gerektirir. Koordinatlar yanlış olabilir, kasıtlı olarak değiştirilmiş olabilir veya GPS sinyali alınamayan bir ortamda çekim yapıldığı için hatalı kaydedilmiş olabilir. Ayrıca, mahremiyet ve güvenlik açısından, GPS metadata\'sı hassas konum bilgilerini ifşa edebileceğinden, delil paylaşımı öncesinde bu alanların temizlenmesi veya maskelenmesi değerlendirilmelidir.' },
|
|
66
|
+
],
|
|
67
|
+
faq,
|
|
68
|
+
bibliography,
|
|
69
|
+
howTo,
|
|
70
|
+
schemas: [
|
|
71
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
|
|
72
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
73
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { bibliography } from '../bibliography';
|
|
2
|
+
import type { ToolLocaleContent } from '../../../types';
|
|
3
|
+
|
|
4
|
+
const slug = 'forensic-image-metadata-authenticity-analyzer';
|
|
5
|
+
const title = '图像元数据与真实性法证分析器';
|
|
6
|
+
const description = '在浏览器本地检查图像头信息、EXIF 拍摄数据、GPS 坐标、编辑软件痕迹以及原始字节。';
|
|
7
|
+
|
|
8
|
+
const howTo = [
|
|
9
|
+
{ name: '保留原始证据', text: '请基于法证副本开展分析,并在本工具之外保存源文件及其加密哈希。' },
|
|
10
|
+
{ name: '本地载入图像', text: '拖放或选择 JPEG 或 PNG。文件只会在浏览器内存中读取,不会上传。' },
|
|
11
|
+
{ name: '查看元数据与位置', text: '将拍摄时间、相机信息、软件字段和 GPS 字段与案件叙述及获取记录进行比对。' },
|
|
12
|
+
{ name: '解读完整性信号', text: '将编辑痕迹和缺失字段视为调查线索,而不是操纵结论。' },
|
|
13
|
+
{ name: '检查十六进制预览', text: '利用高亮显示的头部和元数据区域识别容器结构,并记录偏移量以便进一步检验。' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
const faq = [
|
|
17
|
+
{ question: '元数据能证明照片是真实的吗?', answer: '不能。元数据可以被删除、复制或修改。真实性判断需要结合文件结构、来源、哈希、视觉检查、压缩分析以及经过验证的法证方法。' },
|
|
18
|
+
{ question: '出现 Adobe 或 GIMP 痕迹是否就说明存在恶意编辑?', answer: '不是。它只表明某个软件可能写入了元数据或导出了文件。合法的色彩校正、编辑流程或证据整理也可能留下同样的痕迹。' },
|
|
19
|
+
{ question: '图像会被上传吗?', answer: '不会。分析在浏览器内存中完成。但在任何软件中打开敏感材料之前,仍应遵守你所在机构的证据处理政策。' },
|
|
20
|
+
{ question: '为什么 GPS 数据可能缺失?', answer: '可能是相机不支持 GPS、定位记录被关闭、平台移除了元数据,或者文件经过重新编码。' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
export const content: ToolLocaleContent = {
|
|
24
|
+
slug,
|
|
25
|
+
title,
|
|
26
|
+
description,
|
|
27
|
+
ui: {
|
|
28
|
+
privacy: '仅限本地二进制检查',
|
|
29
|
+
dropTitle: '将图像放到证据台上',
|
|
30
|
+
dropHint: '把 JPEG 或 PNG 拖到这里,或选择一个文件。不会上传任何内容。',
|
|
31
|
+
chooseFile: '选择图像',
|
|
32
|
+
replaceFile: '替换图像',
|
|
33
|
+
waiting: '等待证据',
|
|
34
|
+
metadata: '拍摄元数据',
|
|
35
|
+
integrity: '完整性信号',
|
|
36
|
+
location: '记录位置',
|
|
37
|
+
hex: '十六进制证据窗口',
|
|
38
|
+
hexHint: '前 512 字节 · 青色头部 · 琥珀色元数据 · 中性图像数据',
|
|
39
|
+
noData: '无可读值',
|
|
40
|
+
noGps: '未找到可读的 GPS 坐标。',
|
|
41
|
+
mapLink: '在 OpenStreetMap 中打开坐标',
|
|
42
|
+
score: '启发式置信度',
|
|
43
|
+
disclaimer: '高分并不等于真实性。请保留原始文件、计算加密哈希,并使用经过验证的实验室流程得出案件结论。',
|
|
44
|
+
fileName: '文件',
|
|
45
|
+
fileSize: '大小',
|
|
46
|
+
fileType: '容器',
|
|
47
|
+
camera: '相机',
|
|
48
|
+
captured: '拍摄于',
|
|
49
|
+
software: '软件',
|
|
50
|
+
coordinates: '坐标',
|
|
51
|
+
statusNoObvious: '未发现明显编辑迹象',
|
|
52
|
+
statusReview: '建议复核',
|
|
53
|
+
statusEditing: '检测到编辑痕迹',
|
|
54
|
+
processing: '正在读取二进制证据...',
|
|
55
|
+
loadError: '无法分析该文件。请选择有效的 JPEG 或 PNG 图像。',
|
|
56
|
+
},
|
|
57
|
+
seo: [
|
|
58
|
+
{ type: 'title', text: '如何分析图像元数据和真实性信号', level: 2 },
|
|
59
|
+
{ type: 'paragraph', html: '图像元数据法证分析器可以帮助调查人员、记者、法务团队、合规审查人员和研究者回答一个高意图问题:<strong>图像元数据究竟能告诉我们什么?</strong> 元数据能够提供关于拍摄、地点、软件处理和文件结构的线索,但它并不是独立的真相机器。它最大的价值在于分流和筛查,帮助你判断哪些文件值得进一步深入检查。' },
|
|
60
|
+
{ type: 'paragraph', html: '这个浏览器工具面向那些不满足于原始 EXIF 转储的用户。它会在本地读取所选 JPEG 或 PNG,并集中呈现相机字段、拍摄时间、软件标签、坐标、容器线索以及文件开头字节。这回应了诸如 <em>照片真实性检查</em>、<em>EXIF 元数据分析</em>、<em>如何判断图片是否被编辑</em> 等常见搜索意图。' },
|
|
61
|
+
{ type: 'paragraph', html: '最重要的原则是:结果应该被视为语境,而不是裁决。一个文件可能拥有有用的元数据却依然具有误导性;也可能元数据很少但依然真实。因此,元数据必须作为证据层之一,与来源、哈希、证言、设备历史和经过验证的检验方法进行交叉比对。' },
|
|
62
|
+
],
|
|
63
|
+
faq,
|
|
64
|
+
bibliography,
|
|
65
|
+
howTo,
|
|
66
|
+
schemas: [
|
|
67
|
+
{ '@context': 'https://schema.org', '@type': 'SoftwareApplication', name: title, description, applicationCategory: 'ForensicApplication', operatingSystem: 'Any' },
|
|
68
|
+
{ '@context': 'https://schema.org', '@type': 'FAQPage', mainEntity: faq.map((item) => ({ '@type': 'Question', name: item.question, acceptedAnswer: { '@type': 'Answer', text: item.answer } })) },
|
|
69
|
+
{ '@context': 'https://schema.org', '@type': 'HowTo', name: title, step: howTo.map((step) => ({ '@type': 'HowToStep', name: step.name, text: step.text })) },
|
|
70
|
+
],
|
|
71
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { forensicImageAuthenticityAnalyzer } from './entry';
|
|
2
|
+
import type { ToolDefinition } from '../../types';
|
|
3
|
+
|
|
4
|
+
export * from './entry';
|
|
5
|
+
|
|
6
|
+
export const FORENSIC_IMAGE_AUTHENTICITY_ANALYZER_TOOL: ToolDefinition = {
|
|
7
|
+
entry: forensicImageAuthenticityAnalyzer,
|
|
8
|
+
Component: () => import('./component.astro'),
|
|
9
|
+
SEOComponent: () => import('./seo.astro'),
|
|
10
|
+
BibliographyComponent: () => import('./bibliography.astro'),
|
|
11
|
+
};
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
export interface ExifMetadata {
|
|
2
|
+
make?: string;
|
|
3
|
+
model?: string;
|
|
4
|
+
software?: string;
|
|
5
|
+
dateTime?: string;
|
|
6
|
+
latitude?: number;
|
|
7
|
+
longitude?: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IntegrityFinding {
|
|
11
|
+
severity: 'info' | 'warning' | 'alert';
|
|
12
|
+
title: string;
|
|
13
|
+
detail: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface IntegrityResult {
|
|
17
|
+
score: number;
|
|
18
|
+
classification: 'no-obvious-indicators' | 'review-recommended' | 'editing-signatures-detected';
|
|
19
|
+
format: string;
|
|
20
|
+
findings: IntegrityFinding[];
|
|
21
|
+
signatures: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface HexSegment {
|
|
25
|
+
offset: number;
|
|
26
|
+
bytes: number[];
|
|
27
|
+
kind: 'header' | 'metadata' | 'image';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface TiffContext {
|
|
31
|
+
buffer: ArrayBuffer;
|
|
32
|
+
view: DataView;
|
|
33
|
+
tiffStart: number;
|
|
34
|
+
littleEndian: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const decoder = new TextDecoder('latin1');
|
|
38
|
+
const EDITOR_SIGNATURES = ['Adobe Photoshop', 'Adobe Lightroom', 'GIMP', 'Snapseed', 'Pixelmator', 'Canva', 'ImageMagick'];
|
|
39
|
+
|
|
40
|
+
function readAscii(bytes: Uint8Array, start: number, length: number): string {
|
|
41
|
+
return decoder.decode(bytes.slice(start, start + length)).replace(/\0+$/, '').trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function rational(view: DataView, offset: number, littleEndian: boolean): number {
|
|
45
|
+
const numerator = view.getUint32(offset, littleEndian);
|
|
46
|
+
const denominator = view.getUint32(offset + 4, littleEndian);
|
|
47
|
+
return denominator === 0 ? 0 : numerator / denominator;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getUnitSize(type: number): number {
|
|
51
|
+
switch (type) {
|
|
52
|
+
case 3:
|
|
53
|
+
return 2;
|
|
54
|
+
case 4:
|
|
55
|
+
return 4;
|
|
56
|
+
case 5:
|
|
57
|
+
return 8;
|
|
58
|
+
default:
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getValueAddress(context: TiffContext, type: number, count: number, valueOffset: number): number {
|
|
64
|
+
return count * getUnitSize(type) <= 4 ? valueOffset : context.tiffStart + context.view.getUint32(valueOffset, context.littleEndian);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function readTiffValue(context: TiffContext, type: number, count: number, valueOffset: number): string | number {
|
|
68
|
+
const { buffer, view, littleEndian } = context;
|
|
69
|
+
const address = getValueAddress(context, type, count, valueOffset);
|
|
70
|
+
switch (type) {
|
|
71
|
+
case 2:
|
|
72
|
+
return readAscii(new Uint8Array(buffer), address, count);
|
|
73
|
+
case 3:
|
|
74
|
+
return view.getUint16(address, littleEndian);
|
|
75
|
+
case 4:
|
|
76
|
+
return view.getUint32(address, littleEndian);
|
|
77
|
+
case 5:
|
|
78
|
+
return rational(view, address, littleEndian);
|
|
79
|
+
default:
|
|
80
|
+
return '';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readEntries(context: TiffContext, offset: number, callback: (tag: number, type: number, count: number, valueOffset: number) => void): void {
|
|
85
|
+
const { buffer, view, tiffStart, littleEndian } = context;
|
|
86
|
+
if (offset < tiffStart || offset + 2 >= buffer.byteLength) return;
|
|
87
|
+
const entryCount = view.getUint16(offset, littleEndian);
|
|
88
|
+
for (let index = 0; index < entryCount; index++) {
|
|
89
|
+
const entry = offset + 2 + index * 12;
|
|
90
|
+
if (entry + 12 > buffer.byteLength) break;
|
|
91
|
+
callback(
|
|
92
|
+
view.getUint16(entry, littleEndian),
|
|
93
|
+
view.getUint16(entry + 2, littleEndian),
|
|
94
|
+
view.getUint32(entry + 4, littleEndian),
|
|
95
|
+
entry + 8,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function applyIfdMetadata(metadata: ExifMetadata, tag: number, value: string | number): void {
|
|
101
|
+
switch (tag) {
|
|
102
|
+
case 0x010f:
|
|
103
|
+
metadata.make = String(value);
|
|
104
|
+
break;
|
|
105
|
+
case 0x0110:
|
|
106
|
+
metadata.model = String(value);
|
|
107
|
+
break;
|
|
108
|
+
case 0x0131:
|
|
109
|
+
metadata.software = String(value);
|
|
110
|
+
break;
|
|
111
|
+
case 0x0132:
|
|
112
|
+
metadata.dateTime = String(value);
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function toDecimalDegrees(values: number[]): number {
|
|
118
|
+
return (values[0] ?? 0) + (values[1] ?? 0) / 60 + (values[2] ?? 0) / 3600;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function detectFormat(bytes: Uint8Array, text: string): string {
|
|
122
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8) return 'JPEG';
|
|
123
|
+
if (text.startsWith('\u0089PNG')) return 'PNG';
|
|
124
|
+
return 'Unknown';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function collectSignatures(text: string, metadata: ExifMetadata): string[] {
|
|
128
|
+
const signatures = EDITOR_SIGNATURES.filter((name) => text.toLowerCase().includes(name.toLowerCase()));
|
|
129
|
+
if (metadata.software && !signatures.includes(metadata.software)) signatures.push(metadata.software);
|
|
130
|
+
return signatures;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function createSignatureFinding(signatures: string[]): IntegrityFinding {
|
|
134
|
+
if (signatures.length) {
|
|
135
|
+
return { severity: 'alert', title: 'Image-processing software signature found', detail: signatures.join(', ') };
|
|
136
|
+
}
|
|
137
|
+
return { severity: 'info', title: 'No known editor name found', detail: 'Absence of a text signature does not prove that the image is original.' };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function createHeaderFinding(format: string): IntegrityFinding {
|
|
141
|
+
if (format === 'Unknown') {
|
|
142
|
+
return {
|
|
143
|
+
severity: 'warning',
|
|
144
|
+
title: 'Unrecognized file signature',
|
|
145
|
+
detail: 'The header does not match the JPEG or PNG signatures supported by this educational analyzer.',
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
severity: 'info',
|
|
150
|
+
title: `${format} signature is structurally recognizable`,
|
|
151
|
+
detail: 'The leading bytes are consistent with the detected image container.',
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getFindingPenalty(finding: IntegrityFinding): number {
|
|
156
|
+
switch (finding.severity) {
|
|
157
|
+
case 'alert':
|
|
158
|
+
return 35;
|
|
159
|
+
case 'warning':
|
|
160
|
+
return 12;
|
|
161
|
+
default:
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getClassification(score: number, signatures: string[]): IntegrityResult['classification'] {
|
|
167
|
+
if (signatures.length) return 'editing-signatures-detected';
|
|
168
|
+
if (score < 75) return 'review-recommended';
|
|
169
|
+
return 'no-obvious-indicators';
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readPrimaryIfd(context: TiffContext, metadata: ExifMetadata): { exifIfd: number; gpsIfd: number } {
|
|
173
|
+
const firstIfd = context.tiffStart + context.view.getUint32(context.tiffStart + 4, context.littleEndian);
|
|
174
|
+
let exifIfd = 0;
|
|
175
|
+
let gpsIfd = 0;
|
|
176
|
+
|
|
177
|
+
readEntries(context, firstIfd, (tag, type, count, valueOffset) => {
|
|
178
|
+
const value = readTiffValue(context, type, count, valueOffset);
|
|
179
|
+
applyIfdMetadata(metadata, tag, value);
|
|
180
|
+
if (tag === 0x8769) exifIfd = context.tiffStart + Number(value);
|
|
181
|
+
if (tag === 0x8825) gpsIfd = context.tiffStart + Number(value);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
return { exifIfd, gpsIfd };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function readExifDate(context: TiffContext, metadata: ExifMetadata, exifIfd: number): void {
|
|
188
|
+
if (!exifIfd) return;
|
|
189
|
+
readEntries(context, exifIfd, (tag, type, count, valueOffset) => {
|
|
190
|
+
if (tag === 0x9003 || tag === 0x9004) {
|
|
191
|
+
metadata.dateTime = String(readTiffValue(context, type, count, valueOffset));
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function readGps(context: TiffContext, metadata: ExifMetadata, gpsIfd: number): void {
|
|
197
|
+
if (!gpsIfd) return;
|
|
198
|
+
|
|
199
|
+
let latitudeRef = 'N';
|
|
200
|
+
let longitudeRef = 'E';
|
|
201
|
+
let latitude: number[] = [];
|
|
202
|
+
let longitude: number[] = [];
|
|
203
|
+
|
|
204
|
+
readEntries(context, gpsIfd, (tag, type, count, valueOffset) => {
|
|
205
|
+
const pointer = getValueAddress(context, type, count, valueOffset);
|
|
206
|
+
if (tag === 1) latitudeRef = readAscii(new Uint8Array(context.buffer), pointer, count);
|
|
207
|
+
if (tag === 3) longitudeRef = readAscii(new Uint8Array(context.buffer), pointer, count);
|
|
208
|
+
if (tag === 2) latitude = [0, 1, 2].map((index) => rational(context.view, pointer + index * 8, context.littleEndian));
|
|
209
|
+
if (tag === 4) longitude = [0, 1, 2].map((index) => rational(context.view, pointer + index * 8, context.littleEndian));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
if (latitude.length === 3) metadata.latitude = toDecimalDegrees(latitude);
|
|
213
|
+
if (longitude.length === 3) metadata.longitude = toDecimalDegrees(longitude);
|
|
214
|
+
if (metadata.latitude !== undefined && latitudeRef === 'S') metadata.latitude *= -1;
|
|
215
|
+
if (metadata.longitude !== undefined && longitudeRef === 'W') metadata.longitude *= -1;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class ExifExtractor {
|
|
219
|
+
extract(buffer: ArrayBuffer): ExifMetadata {
|
|
220
|
+
const bytes = new Uint8Array(buffer);
|
|
221
|
+
for (let index = 2; index < bytes.length - 10; index++) {
|
|
222
|
+
if (bytes[index] === 0xff && bytes[index + 1] === 0xe1 && readAscii(bytes, index + 4, 4) === 'Exif') {
|
|
223
|
+
return this.parseTiff(buffer, index + 10);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return {};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private parseTiff(buffer: ArrayBuffer, tiffStart: number): ExifMetadata {
|
|
230
|
+
const view = new DataView(buffer);
|
|
231
|
+
const byteOrder = view.getUint16(tiffStart, false);
|
|
232
|
+
const littleEndian = byteOrder === 0x4949;
|
|
233
|
+
if (!littleEndian && byteOrder !== 0x4d4d) return {};
|
|
234
|
+
|
|
235
|
+
const context: TiffContext = { buffer, view, tiffStart, littleEndian };
|
|
236
|
+
const metadata: ExifMetadata = {};
|
|
237
|
+
const { exifIfd, gpsIfd } = readPrimaryIfd(context, metadata);
|
|
238
|
+
readExifDate(context, metadata, exifIfd);
|
|
239
|
+
readGps(context, metadata, gpsIfd);
|
|
240
|
+
return metadata;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export class IntegrityChecker {
|
|
245
|
+
inspect(buffer: ArrayBuffer, metadata: ExifMetadata): IntegrityResult {
|
|
246
|
+
const bytes = new Uint8Array(buffer);
|
|
247
|
+
const text = decoder.decode(bytes);
|
|
248
|
+
const format = detectFormat(bytes, text);
|
|
249
|
+
const signatures = collectSignatures(text, metadata);
|
|
250
|
+
const findings: IntegrityFinding[] = [];
|
|
251
|
+
|
|
252
|
+
findings.push(createHeaderFinding(format));
|
|
253
|
+
findings.push(createSignatureFinding(signatures));
|
|
254
|
+
if (!metadata.dateTime) findings.push({ severity: 'warning', title: 'Capture timestamp unavailable', detail: 'The EXIF capture date is missing or could not be decoded.' });
|
|
255
|
+
if (!metadata.make && !metadata.model) findings.push({ severity: 'warning', title: 'Camera identity unavailable', detail: 'No readable camera make or model was found in EXIF.' });
|
|
256
|
+
|
|
257
|
+
const penalty = findings.reduce((total, finding) => total + getFindingPenalty(finding), 0);
|
|
258
|
+
const score = Math.max(0, 100 - penalty);
|
|
259
|
+
return {
|
|
260
|
+
score,
|
|
261
|
+
classification: getClassification(score, signatures),
|
|
262
|
+
format,
|
|
263
|
+
findings,
|
|
264
|
+
signatures,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function createHexSegments(buffer: ArrayBuffer, limit = 512): HexSegment[] {
|
|
270
|
+
const bytes = Array.from(new Uint8Array(buffer).slice(0, limit));
|
|
271
|
+
const segments: HexSegment[] = [];
|
|
272
|
+
for (let offset = 0; offset < bytes.length; offset += 16) {
|
|
273
|
+
let kind: HexSegment['kind'] = 'image';
|
|
274
|
+
if (offset < 16) kind = 'header';
|
|
275
|
+
else if (offset < 256) kind = 'metadata';
|
|
276
|
+
segments.push({
|
|
277
|
+
offset,
|
|
278
|
+
bytes: bytes.slice(offset, offset + 16),
|
|
279
|
+
kind,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return segments;
|
|
283
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { SEORenderer } 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
|
+
if (!content) return null;
|
|
9
|
+
---
|
|
10
|
+
{content.seo.length > 0 && <SEORenderer content={{ locale, sections: content.seo }} />}
|