theus-cli-linux-x64 1.2.1 → 1.3.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/README.md +15 -0
- package/RELEASE.json +6 -6
- package/bin/theus +0 -0
- package/package.json +1 -1
- package/vendor/chrome-extension/README.md +142 -0
- package/vendor/chrome-extension/background.js +540 -0
- package/vendor/chrome-extension/cdp-core.js +400 -0
- package/vendor/chrome-extension/cdp-core.test.ts +321 -0
- package/vendor/chrome-extension/cdp.js +672 -0
- package/vendor/chrome-extension/icons/icon128.png +0 -0
- package/vendor/chrome-extension/icons/icon16.png +0 -0
- package/vendor/chrome-extension/icons/icon32.png +0 -0
- package/vendor/chrome-extension/icons/icon48.png +0 -0
- package/vendor/chrome-extension/manifest.json +36 -0
- package/vendor/chrome-extension/popup.html +78 -0
- package/vendor/chrome-extension/popup.js +47 -0
- package/vendor/chrome-extension/recorder.js +316 -0
- package/vendor/higiene-motor/PARCHES.md +244 -0
- package/vendor/higiene-motor/audit_dir.py +166 -0
- package/vendor/higiene-motor/audit_lib.py +333 -0
- package/vendor/higiene-motor/audit_website.py +635 -0
- package/vendor/higiene-motor/av_meta.py +498 -0
- package/vendor/higiene-motor/check_staged.py +71 -0
- package/vendor/higiene-motor/clean_ctrlregen.py +212 -0
- package/vendor/higiene-motor/clean_file.py +237 -0
- package/vendor/higiene-motor/clean_image.py +218 -0
- package/vendor/higiene-motor/clean_staged.py +143 -0
- package/vendor/higiene-motor/clean_text.py +108 -0
- package/vendor/higiene-motor/common.py +486 -0
- package/vendor/higiene-motor/container_meta.py +2522 -0
- package/vendor/higiene-motor/detect_gumbel.py +308 -0
- package/vendor/higiene-motor/detect_text_watermark.py +613 -0
- package/vendor/higiene-motor/format_dispatch.py +157 -0
- package/vendor/higiene-motor/hook_written_file.py +253 -0
- package/vendor/higiene-motor/image_meta.py +2171 -0
- package/vendor/higiene-motor/inspect_file.py +138 -0
- package/vendor/higiene-motor/inspect_image.py +72 -0
- package/vendor/higiene-motor/inspect_text.py +86 -0
- package/vendor/higiene-motor/markdiffusion_harness.py +533 -0
- package/vendor/higiene-motor/rewrite_text.py +737 -0
- package/vendor/higiene-motor/score_stylometry.py +460 -0
- package/vendor/higiene-motor/score_synthid.py +165 -0
- package/vendor/higiene-motor/server.py +1059 -0
- package/vendor/higiene-motor/synthid_score_server.py +176 -0
- package/vendor/higiene-motor/text_detectors.py +433 -0
- package/vendor/higiene-motor/text_unicode.py +730 -0
- package/vendor/theus-arsenal/.arsenal-nombres-ola2.json +0 -6
- package/vendor/theus-arsenal/.arsenal-nombres-ola3.json +0 -72
- package/vendor/theus-arsenal/.mailmap +0 -2
- package/vendor/theus-arsenal/.theus-plugin/marketplace.json +0 -25
- package/vendor/theus-arsenal/.theus-plugin/plugin.json +0 -20
- package/vendor/theus-arsenal/AUTHORS.md +0 -8
- package/vendor/theus-arsenal/CLEANROOM_REPLACEMENT_PLAN.md +0 -11
- package/vendor/theus-arsenal/LICENSE +0 -26
- package/vendor/theus-arsenal/PROVENANCE.md +0 -9
- package/vendor/theus-arsenal/README.md +0 -41
- package/vendor/theus-arsenal/THIRD_PARTY.md +0 -80
- package/vendor/theus-arsenal/agents/actualizador-docs.md +0 -123
- package/vendor/theus-arsenal/agents/analista-comentarios.md +0 -53
- package/vendor/theus-arsenal/agents/analista-conversaciones.md +0 -60
- package/vendor/theus-arsenal/agents/analista-tests-pr.md +0 -53
- package/vendor/theus-arsenal/agents/analista-tipos.md +0 -49
- package/vendor/theus-arsenal/agents/arquitecto-accesibilidad.md +0 -148
- package/vendor/theus-arsenal/agents/arquitecto-codigo.md +0 -79
- package/vendor/theus-arsenal/agents/arquitecto-homelab.md +0 -90
- package/vendor/theus-arsenal/agents/arquitecto-redes.md +0 -116
- package/vendor/theus-arsenal/agents/arquitecto-sistemas.md +0 -219
- package/vendor/theus-arsenal/agents/bifurcador-opensource.md +0 -206
- package/vendor/theus-arsenal/agents/cazador-fallos-silenciosos.md +0 -58
- package/vendor/theus-arsenal/agents/consultor-documentacion.md +0 -76
- package/vendor/theus-arsenal/agents/diagnosticador-redes.md +0 -135
- package/vendor/theus-arsenal/agents/ejecutor-e2e.md +0 -96
- package/vendor/theus-arsenal/agents/empaquetador-opensource.md +0 -257
- package/vendor/theus-arsenal/agents/escaner-secretos.md +0 -196
- package/vendor/theus-arsenal/agents/especialista-seo.md +0 -70
- package/vendor/theus-arsenal/agents/estratega-marketing.md +0 -158
- package/vendor/theus-arsenal/agents/evaluador-agentes.md +0 -214
- package/vendor/theus-arsenal/agents/evaluador-harness.md +0 -217
- package/vendor/theus-arsenal/agents/explorador-codigo.md +0 -79
- package/vendor/theus-arsenal/agents/extractor-specs.md +0 -214
- package/vendor/theus-arsenal/agents/generador-harness.md +0 -139
- package/vendor/theus-arsenal/agents/guia-tdd.md +0 -99
- package/vendor/theus-arsenal/agents/jefe-de-gabinete.md +0 -162
- package/vendor/theus-arsenal/agents/limpiador-refactors.md +0 -93
- package/vendor/theus-arsenal/agents/optimizador-rendimiento.md +0 -454
- package/vendor/theus-arsenal/agents/planificador-harness.md +0 -107
- package/vendor/theus-arsenal/agents/planificador.md +0 -221
- package/vendor/theus-arsenal/agents/resolutor-apps-harmonyos.md +0 -181
- package/vendor/theus-arsenal/agents/resolutor-builds-cpp.md +0 -98
- package/vendor/theus-arsenal/agents/resolutor-builds-dart.md +0 -209
- package/vendor/theus-arsenal/agents/resolutor-builds-django.md +0 -253
- package/vendor/theus-arsenal/agents/resolutor-builds-go.md +0 -102
- package/vendor/theus-arsenal/agents/resolutor-builds-java.md +0 -274
- package/vendor/theus-arsenal/agents/resolutor-builds-kotlin.md +0 -128
- package/vendor/theus-arsenal/agents/resolutor-builds-pytorch.md +0 -128
- package/vendor/theus-arsenal/agents/resolutor-builds-react.md +0 -214
- package/vendor/theus-arsenal/agents/resolutor-builds-rust.md +0 -156
- package/vendor/theus-arsenal/agents/resolutor-builds-swift.md +0 -169
- package/vendor/theus-arsenal/agents/resolutor-builds.md +0 -122
- package/vendor/theus-arsenal/agents/revisor-bd.md +0 -97
- package/vendor/theus-arsenal/agents/revisor-codigo.md +0 -291
- package/vendor/theus-arsenal/agents/revisor-config-red.md +0 -116
- package/vendor/theus-arsenal/agents/revisor-cpp.md +0 -80
- package/vendor/theus-arsenal/agents/revisor-csharp.md +0 -109
- package/vendor/theus-arsenal/agents/revisor-django.md +0 -168
- package/vendor/theus-arsenal/agents/revisor-fastapi.md +0 -82
- package/vendor/theus-arsenal/agents/revisor-flutter.md +0 -251
- package/vendor/theus-arsenal/agents/revisor-go.md +0 -84
- package/vendor/theus-arsenal/agents/revisor-java.md +0 -192
- package/vendor/theus-arsenal/agents/revisor-kotlin.md +0 -171
- package/vendor/theus-arsenal/agents/revisor-ml.md +0 -159
- package/vendor/theus-arsenal/agents/revisor-php.md +0 -108
- package/vendor/theus-arsenal/agents/revisor-python.md +0 -106
- package/vendor/theus-arsenal/agents/revisor-react.md +0 -166
- package/vendor/theus-arsenal/agents/revisor-rust.md +0 -102
- package/vendor/theus-arsenal/agents/revisor-seguridad.md +0 -116
- package/vendor/theus-arsenal/agents/revisor-software-clinico.md +0 -95
- package/vendor/theus-arsenal/agents/revisor-swift.md +0 -115
- package/vendor/theus-arsenal/agents/revisor-typescript.md +0 -123
- package/vendor/theus-arsenal/agents/revisor-vue.md +0 -205
- package/vendor/theus-arsenal/agents/simplificador-codigo.md +0 -55
- package/vendor/theus-arsenal/commands/actualizar-docs.md +0 -88
- package/vendor/theus-arsenal/commands/actualizar-mapas-codigo.md +0 -78
- package/vendor/theus-arsenal/commands/aprender-evaluar.md +0 -109
- package/vendor/theus-arsenal/commands/aprender.md +0 -74
- package/vendor/theus-arsenal/commands/arreglar-build.md +0 -66
- package/vendor/theus-arsenal/commands/bucle-multiperspectiva.md +0 -153
- package/vendor/theus-arsenal/commands/build-cpp.md +0 -173
- package/vendor/theus-arsenal/commands/build-flutter.md +0 -164
- package/vendor/theus-arsenal/commands/build-go.md +0 -183
- package/vendor/theus-arsenal/commands/build-gradle.md +0 -75
- package/vendor/theus-arsenal/commands/build-kotlin.md +0 -176
- package/vendor/theus-arsenal/commands/build-react.md +0 -187
- package/vendor/theus-arsenal/commands/build-rust.md +0 -187
- package/vendor/theus-arsenal/commands/campana-marketing.md +0 -125
- package/vendor/theus-arsenal/commands/checkpoint.md +0 -78
- package/vendor/theus-arsenal/commands/cobertura-tests.md +0 -73
- package/vendor/theus-arsenal/commands/configurar-gestor-paquetes.md +0 -86
- package/vendor/theus-arsenal/commands/consulta.md +0 -164
- package/vendor/theus-arsenal/commands/control-calidad.md +0 -50
- package/vendor/theus-arsenal/commands/crear-skill.md +0 -77
- package/vendor/theus-arsenal/commands/enrutar-modelo.md +0 -30
- package/vendor/theus-arsenal/commands/epica-desbloquear.md +0 -17
- package/vendor/theus-arsenal/commands/epica-descomponer.md +0 -22
- package/vendor/theus-arsenal/commands/epica-publicar.md +0 -21
- package/vendor/theus-arsenal/commands/epica-reclamar.md +0 -25
- package/vendor/theus-arsenal/commands/epica-revisar.md +0 -22
- package/vendor/theus-arsenal/commands/epica-sincronizar.md +0 -21
- package/vendor/theus-arsenal/commands/epica-validar.md +0 -21
- package/vendor/theus-arsenal/commands/evolucionar.md +0 -176
- package/vendor/theus-arsenal/commands/funcionalidad.md +0 -49
- package/vendor/theus-arsenal/commands/guardar-sesion.md +0 -278
- package/vendor/theus-arsenal/commands/harness-build.md +0 -111
- package/vendor/theus-arsenal/commands/harness-diseno.md +0 -46
- package/vendor/theus-arsenal/commands/informe-costos.md +0 -90
- package/vendor/theus-arsenal/commands/iniciar-bucle.md +0 -36
- package/vendor/theus-arsenal/commands/instintos-estado.md +0 -53
- package/vendor/theus-arsenal/commands/instintos-exportar.md +0 -69
- package/vendor/theus-arsenal/commands/instintos-importar.md +0 -120
- package/vendor/theus-arsenal/commands/lienzo-planes.md +0 -48
- package/vendor/theus-arsenal/commands/limpiar-refactor.md +0 -84
- package/vendor/theus-arsenal/commands/modo-seguro.md +0 -96
- package/vendor/theus-arsenal/commands/orq-agregar-funcionalidad.md +0 -36
- package/vendor/theus-arsenal/commands/orq-cambiar-funcionalidad.md +0 -38
- package/vendor/theus-arsenal/commands/orq-construir-mvp.md +0 -36
- package/vendor/theus-arsenal/commands/orq-corregir-defecto.md +0 -40
- package/vendor/theus-arsenal/commands/orq-refinar-codigo.md +0 -44
- package/vendor/theus-arsenal/commands/orq-revisar.md +0 -135
- package/vendor/theus-arsenal/commands/plan-prd.md +0 -160
- package/vendor/theus-arsenal/commands/plan.md +0 -198
- package/vendor/theus-arsenal/commands/podar.md +0 -31
- package/vendor/theus-arsenal/commands/pr.md +0 -181
- package/vendor/theus-arsenal/commands/promover.md +0 -41
- package/vendor/theus-arsenal/commands/proyectos.md +0 -33
- package/vendor/theus-arsenal/commands/prp-commit.md +0 -112
- package/vendor/theus-arsenal/commands/prp-implementar.md +0 -385
- package/vendor/theus-arsenal/commands/prp-planificar.md +0 -501
- package/vendor/theus-arsenal/commands/reanudar-sesion.md +0 -160
- package/vendor/theus-arsenal/commands/revisar-codigo.md +0 -289
- package/vendor/theus-arsenal/commands/revisar-cpp.md +0 -132
- package/vendor/theus-arsenal/commands/revisar-fastapi.md +0 -39
- package/vendor/theus-arsenal/commands/revisar-flutter.md +0 -115
- package/vendor/theus-arsenal/commands/revisar-go.md +0 -148
- package/vendor/theus-arsenal/commands/revisar-kotlin.md +0 -140
- package/vendor/theus-arsenal/commands/revisar-pr.md +0 -37
- package/vendor/theus-arsenal/commands/revisar-python.md +0 -297
- package/vendor/theus-arsenal/commands/revisar-react.md +0 -170
- package/vendor/theus-arsenal/commands/revisar-rust.md +0 -142
- package/vendor/theus-arsenal/commands/revisar-vue.md +0 -174
- package/vendor/theus-arsenal/commands/salud-skills.md +0 -52
- package/vendor/theus-arsenal/commands/sesiones.md +0 -345
- package/vendor/theus-arsenal/commands/test-cpp.md +0 -252
- package/vendor/theus-arsenal/commands/test-flutter.md +0 -143
- package/vendor/theus-arsenal/commands/test-go.md +0 -268
- package/vendor/theus-arsenal/commands/test-kotlin.md +0 -312
- package/vendor/theus-arsenal/commands/test-react.md +0 -265
- package/vendor/theus-arsenal/commands/test-rust.md +0 -309
- package/vendor/theus-arsenal/commands/vigilancia-canary.md +0 -43
- package/vendor/theus-arsenal/hooks/hooks.json +0 -274
- package/vendor/theus-arsenal/package.json +0 -6
- package/vendor/theus-arsenal/rules/angular/coding-style.md +0 -182
- package/vendor/theus-arsenal/rules/angular/hooks.md +0 -25
- package/vendor/theus-arsenal/rules/angular/patterns.md +0 -249
- package/vendor/theus-arsenal/rules/angular/security.md +0 -87
- package/vendor/theus-arsenal/rules/angular/testing.md +0 -164
- package/vendor/theus-arsenal/rules/arkts/coding-style.md +0 -153
- package/vendor/theus-arsenal/rules/arkts/hooks.md +0 -80
- package/vendor/theus-arsenal/rules/arkts/patterns.md +0 -236
- package/vendor/theus-arsenal/rules/arkts/security.md +0 -141
- package/vendor/theus-arsenal/rules/arkts/testing.md +0 -126
- package/vendor/theus-arsenal/rules/common/agents.md +0 -49
- package/vendor/theus-arsenal/rules/common/code-review.md +0 -123
- package/vendor/theus-arsenal/rules/common/coding-style.md +0 -90
- package/vendor/theus-arsenal/rules/common/development-workflow.md +0 -44
- package/vendor/theus-arsenal/rules/common/git-workflow.md +0 -24
- package/vendor/theus-arsenal/rules/common/hooks.md +0 -32
- package/vendor/theus-arsenal/rules/common/patterns.md +0 -31
- package/vendor/theus-arsenal/rules/common/performance.md +0 -53
- package/vendor/theus-arsenal/rules/common/security.md +0 -29
- package/vendor/theus-arsenal/rules/common/testing.md +0 -57
- package/vendor/theus-arsenal/rules/cpp/coding-style.md +0 -44
- package/vendor/theus-arsenal/rules/cpp/hooks.md +0 -39
- package/vendor/theus-arsenal/rules/cpp/patterns.md +0 -51
- package/vendor/theus-arsenal/rules/cpp/security.md +0 -51
- package/vendor/theus-arsenal/rules/cpp/testing.md +0 -44
- package/vendor/theus-arsenal/rules/csharp/coding-style.md +0 -72
- package/vendor/theus-arsenal/rules/csharp/hooks.md +0 -25
- package/vendor/theus-arsenal/rules/csharp/patterns.md +0 -50
- package/vendor/theus-arsenal/rules/csharp/security.md +0 -58
- package/vendor/theus-arsenal/rules/csharp/testing.md +0 -50
- package/vendor/theus-arsenal/rules/dart/coding-style.md +0 -159
- package/vendor/theus-arsenal/rules/dart/hooks.md +0 -68
- package/vendor/theus-arsenal/rules/dart/patterns.md +0 -261
- package/vendor/theus-arsenal/rules/dart/security.md +0 -135
- package/vendor/theus-arsenal/rules/dart/testing.md +0 -215
- package/vendor/theus-arsenal/rules/fsharp/coding-style.md +0 -112
- package/vendor/theus-arsenal/rules/fsharp/hooks.md +0 -26
- package/vendor/theus-arsenal/rules/fsharp/patterns.md +0 -111
- package/vendor/theus-arsenal/rules/fsharp/security.md +0 -76
- package/vendor/theus-arsenal/rules/fsharp/testing.md +0 -62
- package/vendor/theus-arsenal/rules/golang/coding-style.md +0 -32
- package/vendor/theus-arsenal/rules/golang/hooks.md +0 -17
- package/vendor/theus-arsenal/rules/golang/patterns.md +0 -45
- package/vendor/theus-arsenal/rules/golang/security.md +0 -34
- package/vendor/theus-arsenal/rules/golang/testing.md +0 -31
- package/vendor/theus-arsenal/rules/java/coding-style.md +0 -114
- package/vendor/theus-arsenal/rules/java/hooks.md +0 -18
- package/vendor/theus-arsenal/rules/java/patterns.md +0 -147
- package/vendor/theus-arsenal/rules/java/security.md +0 -101
- package/vendor/theus-arsenal/rules/java/testing.md +0 -133
- package/vendor/theus-arsenal/rules/kotlin/coding-style.md +0 -86
- package/vendor/theus-arsenal/rules/kotlin/hooks.md +0 -17
- package/vendor/theus-arsenal/rules/kotlin/patterns.md +0 -146
- package/vendor/theus-arsenal/rules/kotlin/security.md +0 -82
- package/vendor/theus-arsenal/rules/kotlin/testing.md +0 -128
- package/vendor/theus-arsenal/rules/nuxt/coding-style.md +0 -47
- package/vendor/theus-arsenal/rules/nuxt/hooks.md +0 -39
- package/vendor/theus-arsenal/rules/nuxt/patterns.md +0 -54
- package/vendor/theus-arsenal/rules/nuxt/security.md +0 -48
- package/vendor/theus-arsenal/rules/nuxt/testing.md +0 -49
- package/vendor/theus-arsenal/rules/php/coding-style.md +0 -40
- package/vendor/theus-arsenal/rules/php/hooks.md +0 -24
- package/vendor/theus-arsenal/rules/php/patterns.md +0 -33
- package/vendor/theus-arsenal/rules/php/security.md +0 -37
- package/vendor/theus-arsenal/rules/php/testing.md +0 -38
- package/vendor/theus-arsenal/rules/python/coding-style.md +0 -42
- package/vendor/theus-arsenal/rules/python/fastapi.md +0 -58
- package/vendor/theus-arsenal/rules/python/hooks.md +0 -19
- package/vendor/theus-arsenal/rules/python/patterns.md +0 -39
- package/vendor/theus-arsenal/rules/python/security.md +0 -30
- package/vendor/theus-arsenal/rules/python/testing.md +0 -38
- package/vendor/theus-arsenal/rules/react/coding-style.md +0 -109
- package/vendor/theus-arsenal/rules/react/hooks.md +0 -187
- package/vendor/theus-arsenal/rules/react/patterns.md +0 -194
- package/vendor/theus-arsenal/rules/react/security.md +0 -180
- package/vendor/theus-arsenal/rules/react/testing.md +0 -208
- package/vendor/theus-arsenal/rules/react-native/accessibility.md +0 -55
- package/vendor/theus-arsenal/rules/react-native/coding-style.md +0 -71
- package/vendor/theus-arsenal/rules/react-native/hooks.md +0 -28
- package/vendor/theus-arsenal/rules/react-native/patterns.md +0 -88
- package/vendor/theus-arsenal/rules/react-native/performance.md +0 -45
- package/vendor/theus-arsenal/rules/react-native/production-readiness.md +0 -51
- package/vendor/theus-arsenal/rules/react-native/security.md +0 -43
- package/vendor/theus-arsenal/rules/react-native/testing.md +0 -52
- package/vendor/theus-arsenal/rules/ruby/coding-style.md +0 -46
- package/vendor/theus-arsenal/rules/ruby/hooks.md +0 -37
- package/vendor/theus-arsenal/rules/ruby/patterns.md +0 -44
- package/vendor/theus-arsenal/rules/ruby/security.md +0 -51
- package/vendor/theus-arsenal/rules/ruby/testing.md +0 -51
- package/vendor/theus-arsenal/rules/rust/coding-style.md +0 -151
- package/vendor/theus-arsenal/rules/rust/hooks.md +0 -16
- package/vendor/theus-arsenal/rules/rust/patterns.md +0 -168
- package/vendor/theus-arsenal/rules/rust/security.md +0 -141
- package/vendor/theus-arsenal/rules/rust/testing.md +0 -154
- package/vendor/theus-arsenal/rules/swift/coding-style.md +0 -47
- package/vendor/theus-arsenal/rules/swift/hooks.md +0 -20
- package/vendor/theus-arsenal/rules/swift/patterns.md +0 -66
- package/vendor/theus-arsenal/rules/swift/security.md +0 -33
- package/vendor/theus-arsenal/rules/swift/testing.md +0 -45
- package/vendor/theus-arsenal/rules/typescript/coding-style.md +0 -199
- package/vendor/theus-arsenal/rules/typescript/hooks.md +0 -22
- package/vendor/theus-arsenal/rules/typescript/patterns.md +0 -52
- package/vendor/theus-arsenal/rules/typescript/security.md +0 -28
- package/vendor/theus-arsenal/rules/typescript/testing.md +0 -18
- package/vendor/theus-arsenal/rules/vue/coding-style.md +0 -54
- package/vendor/theus-arsenal/rules/vue/hooks.md +0 -45
- package/vendor/theus-arsenal/rules/vue/patterns.md +0 -56
- package/vendor/theus-arsenal/rules/vue/security.md +0 -46
- package/vendor/theus-arsenal/rules/vue/testing.md +0 -53
- package/vendor/theus-arsenal/rules/web/coding-style.md +0 -108
- package/vendor/theus-arsenal/rules/web/design-quality.md +0 -75
- package/vendor/theus-arsenal/rules/web/hooks.md +0 -141
- package/vendor/theus-arsenal/rules/web/patterns.md +0 -91
- package/vendor/theus-arsenal/rules/web/performance.md +0 -76
- package/vendor/theus-arsenal/rules/web/security.md +0 -69
- package/vendor/theus-arsenal/rules/web/testing.md +0 -67
- package/vendor/theus-arsenal/scripts/hooks/bash-hook-dispatcher.js +0 -192
- package/vendor/theus-arsenal/scripts/hooks/block-no-verify.js +0 -556
- package/vendor/theus-arsenal/scripts/hooks/check-console-log.js +0 -92
- package/vendor/theus-arsenal/scripts/hooks/check-hook-enabled.js +0 -16
- package/vendor/theus-arsenal/scripts/hooks/config-protection.js +0 -171
- package/vendor/theus-arsenal/scripts/hooks/design-quality-check.js +0 -132
- package/vendor/theus-arsenal/scripts/hooks/doc-file-warning.js +0 -110
- package/vendor/theus-arsenal/scripts/hooks/modo-seguro.js +0 -158
- package/vendor/theus-arsenal/scripts/hooks/notificacion-escritorio.js +0 -208
- package/vendor/theus-arsenal/scripts/hooks/plugin-hook-bootstrap.js +0 -280
- package/vendor/theus-arsenal/scripts/hooks/post-bash-command-log.js +0 -87
- package/vendor/theus-arsenal/scripts/hooks/post-bash-dispatcher.js +0 -24
- package/vendor/theus-arsenal/scripts/hooks/post-bash-pr-created.js +0 -60
- package/vendor/theus-arsenal/scripts/hooks/post-edit-accumulator.js +0 -94
- package/vendor/theus-arsenal/scripts/hooks/post-edit-console-warn.js +0 -54
- package/vendor/theus-arsenal/scripts/hooks/post-edit-format.js +0 -110
- package/vendor/theus-arsenal/scripts/hooks/post-edit-typecheck.js +0 -98
- package/vendor/theus-arsenal/scripts/hooks/pre-bash-commit-quality.js +0 -447
- package/vendor/theus-arsenal/scripts/hooks/pre-bash-dev-server-block.js +0 -232
- package/vendor/theus-arsenal/scripts/hooks/pre-bash-dispatcher.js +0 -27
- package/vendor/theus-arsenal/scripts/hooks/pre-compact.js +0 -101
- package/vendor/theus-arsenal/scripts/hooks/pretooluse-visible-output.js +0 -49
- package/vendor/theus-arsenal/scripts/hooks/quality-gate.js +0 -168
- package/vendor/theus-arsenal/scripts/hooks/run-with-flags.js +0 -271
- package/vendor/theus-arsenal/scripts/hooks/salud-mcp.js +0 -749
- package/vendor/theus-arsenal/scripts/hooks/seguimiento-costos.js +0 -161
- package/vendor/theus-arsenal/scripts/hooks/session-end.js +0 -340
- package/vendor/theus-arsenal/scripts/hooks/session-start.js +0 -906
- package/vendor/theus-arsenal/scripts/hooks/stop-format-typecheck.js +0 -263
- package/vendor/theus-arsenal/scripts/hooks/suggest-compact.js +0 -286
- package/vendor/theus-arsenal/scripts/hooks/theus-context-monitor.js +0 -289
- package/vendor/theus-arsenal/scripts/hooks/verificacion-factual.js +0 -1278
- package/vendor/theus-arsenal/scripts/lib/agent-data-home.js +0 -96
- package/vendor/theus-arsenal/scripts/lib/cost-estimate.js +0 -66
- package/vendor/theus-arsenal/scripts/lib/hook-flags.js +0 -79
- package/vendor/theus-arsenal/scripts/lib/llm-summary.js +0 -185
- package/vendor/theus-arsenal/scripts/lib/loopback-guard.js +0 -54
- package/vendor/theus-arsenal/scripts/lib/observer-sessions.js +0 -237
- package/vendor/theus-arsenal/scripts/lib/package-manager.d.ts +0 -120
- package/vendor/theus-arsenal/scripts/lib/package-manager.js +0 -434
- package/vendor/theus-arsenal/scripts/lib/path-safety.js +0 -86
- package/vendor/theus-arsenal/scripts/lib/plan-canvas/markdown.js +0 -285
- package/vendor/theus-arsenal/scripts/lib/plan-canvas/sdk.js +0 -238
- package/vendor/theus-arsenal/scripts/lib/plan-canvas/server.js +0 -534
- package/vendor/theus-arsenal/scripts/lib/plan-canvas/sessions.js +0 -274
- package/vendor/theus-arsenal/scripts/lib/plan-canvas/ui.js +0 -546
- package/vendor/theus-arsenal/scripts/lib/project-detect.js +0 -451
- package/vendor/theus-arsenal/scripts/lib/resolve-formatter.js +0 -193
- package/vendor/theus-arsenal/scripts/lib/resolve-theus-root.js +0 -101
- package/vendor/theus-arsenal/scripts/lib/session-aliases.d.ts +0 -138
- package/vendor/theus-arsenal/scripts/lib/session-aliases.js +0 -483
- package/vendor/theus-arsenal/scripts/lib/session-bridge.js +0 -155
- package/vendor/theus-arsenal/scripts/lib/session-manager.d.ts +0 -137
- package/vendor/theus-arsenal/scripts/lib/session-manager.js +0 -555
- package/vendor/theus-arsenal/scripts/lib/shell-split.js +0 -86
- package/vendor/theus-arsenal/scripts/lib/shell-substitution.js +0 -501
- package/vendor/theus-arsenal/scripts/lib/skill-evolution/dashboard.js +0 -414
- package/vendor/theus-arsenal/scripts/lib/skill-evolution/health.js +0 -285
- package/vendor/theus-arsenal/scripts/lib/skill-evolution/index.js +0 -22
- package/vendor/theus-arsenal/scripts/lib/skill-evolution/provenance.js +0 -200
- package/vendor/theus-arsenal/scripts/lib/skill-evolution/tracker.js +0 -157
- package/vendor/theus-arsenal/scripts/lib/skill-evolution/versioning.js +0 -246
- package/vendor/theus-arsenal/scripts/lib/transcript-context.js +0 -232
- package/vendor/theus-arsenal/scripts/lib/utils.d.ts +0 -205
- package/vendor/theus-arsenal/scripts/lib/utils.js +0 -658
- package/vendor/theus-arsenal/scripts/plan-canvas.js +0 -341
- package/vendor/theus-arsenal/scripts/setup-package-manager.js +0 -46
- package/vendor/theus-arsenal/scripts/skills-health.js +0 -143
- package/vendor/theus-arsenal/skills/a11y-frontend/SKILL.md +0 -444
- package/vendor/theus-arsenal/skills/accesibilidad/SKILL.md +0 -144
- package/vendor/theus-arsenal/skills/acelerador-throughput-datos/SKILL.md +0 -72
- package/vendor/theus-arsenal/skills/agente-scraper-datos/SKILL.md +0 -771
- package/vendor/theus-arsenal/skills/analisis-plataformas-competencia/SKILL.md +0 -137
- package/vendor/theus-arsenal/skills/api-x/SKILL.md +0 -233
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/SKILL.md +0 -335
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/observer-loop.sh +0 -374
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/observer.md +0 -188
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/session-guardian.sh +0 -153
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/agents/start-observer.sh +0 -253
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/config.json +0 -8
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/hooks/observe.sh +0 -594
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/detect-project.sh +0 -325
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/instinct-cli.py +0 -1918
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/lib/arsenal-dir.sh +0 -23
- package/vendor/theus-arsenal/skills/aprendizaje-continuo-v2/scripts/test_parse_instinct.py +0 -1420
- package/vendor/theus-arsenal/skills/arquitectura-hexagonal/SKILL.md +0 -275
- package/vendor/theus-arsenal/skills/arquitectura-limpia-android/SKILL.md +0 -340
- package/vendor/theus-arsenal/skills/arquitectura-pipelines-recomendacion/SKILL.md +0 -112
- package/vendor/theus-arsenal/skills/atlas-datos-prediccion/SKILL.md +0 -53
- package/vendor/theus-arsenal/skills/auditoria-arquitectura-agentes/SKILL.md +0 -257
- package/vendor/theus-arsenal/skills/auditoria-produccion/SKILL.md +0 -180
- package/vendor/theus-arsenal/skills/auditoria-rutas-clic/SKILL.md +0 -243
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/SKILL.md +0 -180
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/examples/high-score-example.md +0 -89
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/examples/low-score-example.md +0 -87
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/references/evaluation-criteria.md +0 -71
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/references/hook-integration.md +0 -90
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/scripts/evaluate.py +0 -409
- package/vendor/theus-arsenal/skills/autoevaluacion-agente/templates/evaluation-report.md +0 -86
- package/vendor/theus-arsenal/skills/automatizacion-ssh-netmiko/SKILL.md +0 -176
- package/vendor/theus-arsenal/skills/base-datos-pubmed/SKILL.md +0 -174
- package/vendor/theus-arsenal/skills/base-datos-uspto/SKILL.md +0 -173
- package/vendor/theus-arsenal/skills/benchmark/SKILL.md +0 -92
- package/vendor/theus-arsenal/skills/bitacora-crecimiento/SKILL.md +0 -120
- package/vendor/theus-arsenal/skills/bucle-verificacion/SKILL.md +0 -125
- package/vendor/theus-arsenal/skills/buscar-primero/SKILL.md +0 -184
- package/vendor/theus-arsenal/skills/busqueda-exa/SKILL.md +0 -111
- package/vendor/theus-arsenal/skills/cache-por-hash/SKILL.md +0 -160
- package/vendor/theus-arsenal/skills/campana-marketing/SKILL.md +0 -118
- package/vendor/theus-arsenal/skills/capacidad-producto/SKILL.md +0 -138
- package/vendor/theus-arsenal/skills/cazador-vulnerabilidades/SKILL.md +0 -104
- package/vendor/theus-arsenal/skills/compactacion-estrategica/SKILL.md +0 -115
- package/vendor/theus-arsenal/skills/comparador-baskets-prediccion/SKILL.md +0 -69
- package/vendor/theus-arsenal/skills/compras-energia/SKILL.md +0 -206
- package/vendor/theus-arsenal/skills/concurrencia-swift-6-2/SKILL.md +0 -224
- package/vendor/theus-arsenal/skills/consejo/SKILL.md +0 -201
- package/vendor/theus-arsenal/skills/construccion-harness-agentes/SKILL.md +0 -78
- package/vendor/theus-arsenal/skills/constructor-conectores/SKILL.md +0 -119
- package/vendor/theus-arsenal/skills/constructor-equipos/SKILL.md +0 -169
- package/vendor/theus-arsenal/skills/consulta-documentacion/SKILL.md +0 -92
- package/vendor/theus-arsenal/skills/consultas-genomicas-gget/SKILL.md +0 -170
- package/vendor/theus-arsenal/skills/contacto-inversores/SKILL.md +0 -95
- package/vendor/theus-arsenal/skills/corrutinas-flows-kotlin/SKILL.md +0 -284
- package/vendor/theus-arsenal/skills/creacion-video-remotion/SKILL.md +0 -46
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/3d.md +0 -86
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/animations.md +0 -29
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/charts-bar-chart.tsx +0 -173
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/text-animations-typewriter.tsx +0 -100
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets/text-animations-word-highlight.tsx +0 -108
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/assets.md +0 -78
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/audio.md +0 -172
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/calculate-metadata.md +0 -104
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/can-decode.md +0 -75
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/charts.md +0 -58
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/compositions.md +0 -146
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/display-captions.md +0 -126
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/extract-frames.md +0 -229
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/fonts.md +0 -152
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-audio-duration.md +0 -58
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-video-dimensions.md +0 -68
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/get-video-duration.md +0 -58
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/gifs.md +0 -138
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/images.md +0 -130
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/import-srt-captions.md +0 -67
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/lottie.md +0 -67
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/measuring-dom-nodes.md +0 -34
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/measuring-text.md +0 -143
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/sequencing.md +0 -106
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/tailwind.md +0 -11
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/text-animations.md +0 -20
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/timing.md +0 -179
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/transcribe-captions.md +0 -19
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/transitions.md +0 -122
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/trimming.md +0 -52
- package/vendor/theus-arsenal/skills/creacion-video-remotion/rules/videos.md +0 -171
- package/vendor/theus-arsenal/skills/cumplimiento-aduanero/SKILL.md +0 -270
- package/vendor/theus-arsenal/skills/cumplimiento-hipaa/SKILL.md +0 -78
- package/vendor/theus-arsenal/skills/datos-sensibles-salud/SKILL.md +0 -149
- package/vendor/theus-arsenal/skills/decimales-tokens-evm/SKILL.md +0 -128
- package/vendor/theus-arsenal/skills/decisiones-arquitectura/SKILL.md +0 -206
- package/vendor/theus-arsenal/skills/demos-ui/SKILL.md +0 -469
- package/vendor/theus-arsenal/skills/desarrollo-por-intencion/SKILL.md +0 -369
- package/vendor/theus-arsenal/skills/descubrimiento-marca/SKILL.md +0 -149
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/10_proposito-porque.md +0 -42
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/20_posicionamiento.md +0 -47
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/30_audiencia-nicho.md +0 -53
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/40_personalidad-arquetipo.md +0 -61
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/50_voz-tono.md +0 -62
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/60_narrativa-historia.md +0 -54
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/70_tension-fundador.md +0 -50
- package/vendor/theus-arsenal/skills/descubrimiento-marca/references/90_SINTESIS.md +0 -133
- package/vendor/theus-arsenal/skills/descubrimiento-plugins-laravel/SKILL.md +0 -230
- package/vendor/theus-arsenal/skills/destilado-reglas/SKILL.md +0 -263
- package/vendor/theus-arsenal/skills/destilado-reglas/scripts/scan-rules.sh +0 -58
- package/vendor/theus-arsenal/skills/destilado-reglas/scripts/scan-skills.sh +0 -129
- package/vendor/theus-arsenal/skills/diagnostico-bgp/SKILL.md +0 -170
- package/vendor/theus-arsenal/skills/direccion-estetica/SKILL.md +0 -220
- package/vendor/theus-arsenal/skills/direccion-estetica/references/taxonomia-generos.md +0 -89
- package/vendor/theus-arsenal/skills/diseno-api/SKILL.md +0 -522
- package/vendor/theus-arsenal/skills/diseno-liquid-glass/SKILL.md +0 -282
- package/vendor/theus-arsenal/skills/e2e-escritorio-windows/SKILL.md +0 -879
- package/vendor/theus-arsenal/skills/edicion-video/SKILL.md +0 -289
- package/vendor/theus-arsenal/skills/enfoque-producto/SKILL.md +0 -91
- package/vendor/theus-arsenal/skills/entornos-flox/SKILL.md +0 -493
- package/vendor/theus-arsenal/skills/estandares-codigo/SKILL.md +0 -548
- package/vendor/theus-arsenal/skills/estandares-cpp/SKILL.md +0 -727
- package/vendor/theus-arsenal/skills/estandares-java/SKILL.md +0 -389
- package/vendor/theus-arsenal/skills/estilo-legado/SKILL.md +0 -149
- package/vendor/theus-arsenal/skills/estructura-informes-competencia/SKILL.md +0 -95
- package/vendor/theus-arsenal/skills/evaluacion-academica/SKILL.md +0 -165
- package/vendor/theus-arsenal/skills/explorador-skills/SKILL.md +0 -129
- package/vendor/theus-arsenal/skills/flujo-git/SKILL.md +0 -714
- package/vendor/theus-arsenal/skills/flujo-ingenieria-ml/SKILL.md +0 -343
- package/vendor/theus-arsenal/skills/flujo-tdd/SKILL.md +0 -584
- package/vendor/theus-arsenal/skills/forja-personas/SKILL.md +0 -298
- package/vendor/theus-arsenal/skills/forja-personas/gacha.py +0 -224
- package/vendor/theus-arsenal/skills/forja-personas/gacha.sh +0 -5
- package/vendor/theus-arsenal/skills/forja-personas/references/avatar-style.md +0 -123
- package/vendor/theus-arsenal/skills/forja-personas/references/boundary-rules.md +0 -53
- package/vendor/theus-arsenal/skills/forja-personas/references/error-handling.md +0 -53
- package/vendor/theus-arsenal/skills/forja-personas/references/identity-tension.md +0 -48
- package/vendor/theus-arsenal/skills/forja-personas/references/naming-system.md +0 -39
- package/vendor/theus-arsenal/skills/forja-personas/references/output-template.md +0 -166
- package/vendor/theus-arsenal/skills/fundamentos-animacion/SKILL.md +0 -296
- package/vendor/theus-arsenal/skills/generacion-iconos-ios/SKILL.md +0 -156
- package/vendor/theus-arsenal/skills/generacion-iconos-ios/scripts/generate_icons.swift +0 -258
- package/vendor/theus-arsenal/skills/generacion-iconos-ios/scripts/iconify_gen.sh +0 -235
- package/vendor/theus-arsenal/skills/gestion-excepciones-logisticas/SKILL.md +0 -208
- package/vendor/theus-arsenal/skills/gestion-transportistas/SKILL.md +0 -204
- package/vendor/theus-arsenal/skills/guia-adopcion-ml/SKILL.md +0 -64
- package/vendor/theus-arsenal/skills/harness-agente-autonomo/SKILL.md +0 -252
- package/vendor/theus-arsenal/skills/harness-eval-clinico/SKILL.md +0 -212
- package/vendor/theus-arsenal/skills/harness-evaluacion/SKILL.md +0 -296
- package/vendor/theus-arsenal/skills/harness-generador-evaluador/SKILL.md +0 -254
- package/vendor/theus-arsenal/skills/homelab-diseno-red/SKILL.md +0 -120
- package/vendor/theus-arsenal/skills/homelab-dns-pihole/SKILL.md +0 -264
- package/vendor/theus-arsenal/skills/homelab-preparacion-red/SKILL.md +0 -144
- package/vendor/theus-arsenal/skills/homelab-segmentacion-vlan/SKILL.md +0 -307
- package/vendor/theus-arsenal/skills/homelab-vpn-wireguard/SKILL.md +0 -299
- package/vendor/theus-arsenal/skills/ingenieria-agentica/SKILL.md +0 -96
- package/vendor/theus-arsenal/skills/inspeccion-animaciones-blender/SKILL.md +0 -164
- package/vendor/theus-arsenal/skills/instaladores-python/SKILL.md +0 -820
- package/vendor/theus-arsenal/skills/integracion-jira/SKILL.md +0 -307
- package/vendor/theus-arsenal/skills/inteligencia-leads/SKILL.md +0 -312
- package/vendor/theus-arsenal/skills/inteligencia-leads/agents/agente-enriquecimiento.md +0 -87
- package/vendor/theus-arsenal/skills/inteligencia-leads/agents/mapeador-mutuos.md +0 -76
- package/vendor/theus-arsenal/skills/inteligencia-leads/agents/puntuador-senales.md +0 -60
- package/vendor/theus-arsenal/skills/inteligencia-leads/agents/redactor-outreach.md +0 -102
- package/vendor/theus-arsenal/skills/inteligencia-mercados-prediccion/SKILL.md +0 -53
- package/vendor/theus-arsenal/skills/introspeccion-agente/SKILL.md +0 -150
- package/vendor/theus-arsenal/skills/inventario-skills/SKILL.md +0 -194
- package/vendor/theus-arsenal/skills/inventario-skills/scripts/quick-diff.sh +0 -87
- package/vendor/theus-arsenal/skills/inventario-skills/scripts/save-results.sh +0 -57
- package/vendor/theus-arsenal/skills/inventario-skills/scripts/scan.sh +0 -170
- package/vendor/theus-arsenal/skills/investigacion-mercado/SKILL.md +0 -87
- package/vendor/theus-arsenal/skills/investigacion-oraculos-prediccion/SKILL.md +0 -59
- package/vendor/theus-arsenal/skills/investigacion-profunda/SKILL.md +0 -161
- package/vendor/theus-arsenal/skills/keccak256-nodejs/SKILL.md +0 -100
- package/vendor/theus-arsenal/skills/libro-decisiones-recursivo/SKILL.md +0 -82
- package/vendor/theus-arsenal/skills/lienzo-planes/SKILL.md +0 -111
- package/vendor/theus-arsenal/skills/limpieza-configuracion/SKILL.md +0 -116
- package/vendor/theus-arsenal/skills/logistica-inversa-devoluciones/SKILL.md +0 -241
- package/vendor/theus-arsenal/skills/manejo-errores/SKILL.md +0 -375
- package/vendor/theus-arsenal/skills/materiales-inversores/SKILL.md +0 -101
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/SKILL.md +0 -170
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/forget.mjs +0 -44
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/info.mjs +0 -24
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/init.mjs +0 -152
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/list.mjs +0 -40
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/migrate.mjs +0 -214
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/resume.mjs +0 -36
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/save.mjs +0 -210
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/commands/shared.mjs +0 -402
- package/vendor/theus-arsenal/skills/memoria-por-proyecto/hooks/session-start.mjs +0 -238
- package/vendor/theus-arsenal/skills/metodologia-benchmarks/SKILL.md +0 -131
- package/vendor/theus-arsenal/skills/migraciones-bd/SKILL.md +0 -430
- package/vendor/theus-arsenal/skills/modelos-fundacionales-en-dispositivo/SKILL.md +0 -244
- package/vendor/theus-arsenal/skills/modo-seguro/SKILL.md +0 -86
- package/vendor/theus-arsenal/skills/motion-avanzado/SKILL.md +0 -592
- package/vendor/theus-arsenal/skills/motor-de-contenido/SKILL.md +0 -136
- package/vendor/theus-arsenal/skills/no-conformidades-calidad/SKILL.md +0 -249
- package/vendor/theus-arsenal/skills/onboarding-codigo/SKILL.md +0 -244
- package/vendor/theus-arsenal/skills/operaciones-agentes-empresariales/SKILL.md +0 -51
- package/vendor/theus-arsenal/skills/operaciones-conocimiento/SKILL.md +0 -159
- package/vendor/theus-arsenal/skills/operaciones-correo/SKILL.md +0 -123
- package/vendor/theus-arsenal/skills/operaciones-facturacion/SKILL.md +0 -142
- package/vendor/theus-arsenal/skills/operaciones-flujo-proyectos/SKILL.md +0 -110
- package/vendor/theus-arsenal/skills/operaciones-github/SKILL.md +0 -143
- package/vendor/theus-arsenal/skills/operaciones-google-workspace/SKILL.md +0 -100
- package/vendor/theus-arsenal/skills/operaciones-investigacion/SKILL.md +0 -124
- package/vendor/theus-arsenal/skills/operaciones-mensajes/SKILL.md +0 -103
- package/vendor/theus-arsenal/skills/operaciones-terminal/SKILL.md +0 -110
- package/vendor/theus-arsenal/skills/optimizacion-benchmark/SKILL.md +0 -70
- package/vendor/theus-arsenal/skills/optimizacion-costos-llm/SKILL.md +0 -182
- package/vendor/theus-arsenal/skills/optimizador-conexiones/SKILL.md +0 -191
- package/vendor/theus-arsenal/skills/optimizador-paralelismo/SKILL.md +0 -75
- package/vendor/theus-arsenal/skills/optimizador-prompts/SKILL.md +0 -374
- package/vendor/theus-arsenal/skills/orq-agregar-funcionalidad/SKILL.md +0 -37
- package/vendor/theus-arsenal/skills/orq-cambiar-funcionalidad/SKILL.md +0 -36
- package/vendor/theus-arsenal/skills/orq-construir-mvp/SKILL.md +0 -38
- package/vendor/theus-arsenal/skills/orq-corregir-defecto/SKILL.md +0 -36
- package/vendor/theus-arsenal/skills/orq-pipeline/SKILL.md +0 -134
- package/vendor/theus-arsenal/skills/orq-refinar-codigo/SKILL.md +0 -49
- package/vendor/theus-arsenal/skills/orquestacion-equipos-agentes/SKILL.md +0 -117
- package/vendor/theus-arsenal/skills/pagos-agentes-x402/SKILL.md +0 -237
- package/vendor/theus-arsenal/skills/patrones-angular/SKILL.md +0 -153
- package/vendor/theus-arsenal/skills/patrones-angular/references/angular-animations.md +0 -160
- package/vendor/theus-arsenal/skills/patrones-angular/references/angular-aria.md +0 -410
- package/vendor/theus-arsenal/skills/patrones-angular/references/cli.md +0 -86
- package/vendor/theus-arsenal/skills/patrones-angular/references/component-harnesses.md +0 -59
- package/vendor/theus-arsenal/skills/patrones-angular/references/component-styling.md +0 -91
- package/vendor/theus-arsenal/skills/patrones-angular/references/components.md +0 -117
- package/vendor/theus-arsenal/skills/patrones-angular/references/creating-services.md +0 -97
- package/vendor/theus-arsenal/skills/patrones-angular/references/data-resolvers.md +0 -69
- package/vendor/theus-arsenal/skills/patrones-angular/references/define-routes.md +0 -67
- package/vendor/theus-arsenal/skills/patrones-angular/references/defining-providers.md +0 -72
- package/vendor/theus-arsenal/skills/patrones-angular/references/di-fundamentals.md +0 -120
- package/vendor/theus-arsenal/skills/patrones-angular/references/e2e-testing.md +0 -56
- package/vendor/theus-arsenal/skills/patrones-angular/references/effects.md +0 -83
- package/vendor/theus-arsenal/skills/patrones-angular/references/hierarchical-injectors.md +0 -43
- package/vendor/theus-arsenal/skills/patrones-angular/references/host-elements.md +0 -80
- package/vendor/theus-arsenal/skills/patrones-angular/references/injection-context.md +0 -63
- package/vendor/theus-arsenal/skills/patrones-angular/references/inputs.md +0 -101
- package/vendor/theus-arsenal/skills/patrones-angular/references/linked-signal.md +0 -59
- package/vendor/theus-arsenal/skills/patrones-angular/references/loading-strategies.md +0 -61
- package/vendor/theus-arsenal/skills/patrones-angular/references/mcp.md +0 -67
- package/vendor/theus-arsenal/skills/patrones-angular/references/navigate-to-routes.md +0 -69
- package/vendor/theus-arsenal/skills/patrones-angular/references/outputs.md +0 -86
- package/vendor/theus-arsenal/skills/patrones-angular/references/reactive-forms.md +0 -122
- package/vendor/theus-arsenal/skills/patrones-angular/references/rendering-strategies.md +0 -44
- package/vendor/theus-arsenal/skills/patrones-angular/references/resource.md +0 -77
- package/vendor/theus-arsenal/skills/patrones-angular/references/route-animations.md +0 -56
- package/vendor/theus-arsenal/skills/patrones-angular/references/route-guards.md +0 -52
- package/vendor/theus-arsenal/skills/patrones-angular/references/router-lifecycle.md +0 -45
- package/vendor/theus-arsenal/skills/patrones-angular/references/router-testing.md +0 -87
- package/vendor/theus-arsenal/skills/patrones-angular/references/show-routes-with-outlets.md +0 -68
- package/vendor/theus-arsenal/skills/patrones-angular/references/signal-forms.md +0 -795
- package/vendor/theus-arsenal/skills/patrones-angular/references/signals-overview.md +0 -94
- package/vendor/theus-arsenal/skills/patrones-angular/references/tailwind-css.md +0 -69
- package/vendor/theus-arsenal/skills/patrones-angular/references/template-driven-forms.md +0 -114
- package/vendor/theus-arsenal/skills/patrones-angular/references/testing-fundamentals.md +0 -65
- package/vendor/theus-arsenal/skills/patrones-animacion/SKILL.md +0 -432
- package/vendor/theus-arsenal/skills/patrones-backend/SKILL.md +0 -560
- package/vendor/theus-arsenal/skills/patrones-cdss-clinico/SKILL.md +0 -249
- package/vendor/theus-arsenal/skills/patrones-cisco-ios/SKILL.md +0 -148
- package/vendor/theus-arsenal/skills/patrones-clickhouse/SKILL.md +0 -443
- package/vendor/theus-arsenal/skills/patrones-compose-multiplatform/SKILL.md +0 -298
- package/vendor/theus-arsenal/skills/patrones-dart-flutter/SKILL.md +0 -566
- package/vendor/theus-arsenal/skills/patrones-despliegue/SKILL.md +0 -426
- package/vendor/theus-arsenal/skills/patrones-django/SKILL.md +0 -739
- package/vendor/theus-arsenal/skills/patrones-django-celery/SKILL.md +0 -456
- package/vendor/theus-arsenal/skills/patrones-docker/SKILL.md +0 -363
- package/vendor/theus-arsenal/skills/patrones-dotnet/SKILL.md +0 -321
- package/vendor/theus-arsenal/skills/patrones-emr-clinico/SKILL.md +0 -168
- package/vendor/theus-arsenal/skills/patrones-fastapi/SKILL.md +0 -513
- package/vendor/theus-arsenal/skills/patrones-frontend/SKILL.md +0 -655
- package/vendor/theus-arsenal/skills/patrones-go/SKILL.md +0 -674
- package/vendor/theus-arsenal/skills/patrones-jpa/SKILL.md +0 -156
- package/vendor/theus-arsenal/skills/patrones-kotlin/SKILL.md +0 -717
- package/vendor/theus-arsenal/skills/patrones-kotlin-exposed/SKILL.md +0 -724
- package/vendor/theus-arsenal/skills/patrones-kotlin-ktor/SKILL.md +0 -694
- package/vendor/theus-arsenal/skills/patrones-kubernetes/SKILL.md +0 -754
- package/vendor/theus-arsenal/skills/patrones-laravel/SKILL.md +0 -414
- package/vendor/theus-arsenal/skills/patrones-mcp/SKILL.md +0 -66
- package/vendor/theus-arsenal/skills/patrones-mysql/SKILL.md +0 -420
- package/vendor/theus-arsenal/skills/patrones-nestjs/SKILL.md +0 -229
- package/vendor/theus-arsenal/skills/patrones-nextjs/SKILL.md +0 -56
- package/vendor/theus-arsenal/skills/patrones-nuxt4/SKILL.md +0 -99
- package/vendor/theus-arsenal/skills/patrones-perl/SKILL.md +0 -508
- package/vendor/theus-arsenal/skills/patrones-postgres/SKILL.md +0 -141
- package/vendor/theus-arsenal/skills/patrones-prisma/SKILL.md +0 -399
- package/vendor/theus-arsenal/skills/patrones-python/SKILL.md +0 -750
- package/vendor/theus-arsenal/skills/patrones-pytorch/SKILL.md +0 -400
- package/vendor/theus-arsenal/skills/patrones-quarkus/SKILL.md +0 -729
- package/vendor/theus-arsenal/skills/patrones-react/SKILL.md +0 -339
- package/vendor/theus-arsenal/skills/patrones-react-native/SKILL.md +0 -325
- package/vendor/theus-arsenal/skills/patrones-redis/SKILL.md +0 -401
- package/vendor/theus-arsenal/skills/patrones-rust/SKILL.md +0 -504
- package/vendor/theus-arsenal/skills/patrones-springboot/SKILL.md +0 -314
- package/vendor/theus-arsenal/skills/patrones-swiftui/SKILL.md +0 -261
- package/vendor/theus-arsenal/skills/patrones-tinystruct/SKILL.md +0 -281
- package/vendor/theus-arsenal/skills/patrones-tinystruct/references/architecture.md +0 -90
- package/vendor/theus-arsenal/skills/patrones-tinystruct/references/data-handling.md +0 -60
- package/vendor/theus-arsenal/skills/patrones-tinystruct/references/database.md +0 -99
- package/vendor/theus-arsenal/skills/patrones-tinystruct/references/routing.md +0 -64
- package/vendor/theus-arsenal/skills/patrones-tinystruct/references/system-usage.md +0 -97
- package/vendor/theus-arsenal/skills/patrones-tinystruct/references/testing.md +0 -72
- package/vendor/theus-arsenal/skills/patrones-vite/SKILL.md +0 -449
- package/vendor/theus-arsenal/skills/patrones-vue/SKILL.md +0 -469
- package/vendor/theus-arsenal/skills/persistencia-actores-swift/SKILL.md +0 -148
- package/vendor/theus-arsenal/skills/pipeline-opensource/SKILL.md +0 -254
- package/vendor/theus-arsenal/skills/planificacion-demanda-inventario/SKILL.md +0 -236
- package/vendor/theus-arsenal/skills/planificador-trades-prediccion/SKILL.md +0 -60
- package/vendor/theus-arsenal/skills/plano-tecnico/SKILL.md +0 -102
- package/vendor/theus-arsenal/skills/presentaciones-html/SKILL.md +0 -189
- package/vendor/theus-arsenal/skills/presentaciones-html/STYLE_PRESETS.md +0 -330
- package/vendor/theus-arsenal/skills/presentaciones-html/animation-patterns.md +0 -122
- package/vendor/theus-arsenal/skills/presentaciones-html/html-template.md +0 -419
- package/vendor/theus-arsenal/skills/presentaciones-html/scripts/export-pdf.sh +0 -418
- package/vendor/theus-arsenal/skills/presentaciones-html/scripts/extract-pptx.py +0 -96
- package/vendor/theus-arsenal/skills/presentaciones-html/viewport-base.css +0 -153
- package/vendor/theus-arsenal/skills/presupuesto-contexto/SKILL.md +0 -263
- package/vendor/theus-arsenal/skills/procesamiento-documentos-nutrient/SKILL.md +0 -168
- package/vendor/theus-arsenal/skills/programacion-produccion/SKILL.md +0 -220
- package/vendor/theus-arsenal/skills/publicacion-cruzada/SKILL.md +0 -117
- package/vendor/theus-arsenal/skills/publicador-social/SKILL.md +0 -121
- package/vendor/theus-arsenal/skills/puerta-de-entrega/SKILL.md +0 -164
- package/vendor/theus-arsenal/skills/puerta-de-entrega/hooks/puerta-entrega.py +0 -242
- package/vendor/theus-arsenal/skills/pulir-interfaces/SKILL.md +0 -126
- package/vendor/theus-arsenal/skills/qa-navegador/SKILL.md +0 -103
- package/vendor/theus-arsenal/skills/ranking-grafo-social/SKILL.md +0 -153
- package/vendor/theus-arsenal/skills/recuperacion-iterativa/SKILL.md +0 -236
- package/vendor/theus-arsenal/skills/redaccion-articulos/SKILL.md +0 -81
- package/vendor/theus-arsenal/skills/regex-vs-llm/SKILL.md +0 -221
- package/vendor/theus-arsenal/skills/regresion-codigo-ia/SKILL.md +0 -388
- package/vendor/theus-arsenal/skills/rendimiento-react/SKILL.md +0 -567
- package/vendor/theus-arsenal/skills/revision-codigo-flutter-dart/SKILL.md +0 -442
- package/vendor/theus-arsenal/skills/revision-literatura-cientifica/SKILL.md +0 -191
- package/vendor/theus-arsenal/skills/revision-multiperspectiva/SKILL.md +0 -302
- package/vendor/theus-arsenal/skills/revision-riesgo-prediccion/SKILL.md +0 -56
- package/vendor/theus-arsenal/skills/revision-seguridad/SKILL.md +0 -505
- package/vendor/theus-arsenal/skills/revision-seguridad/cloud-infrastructure-security.md +0 -361
- package/vendor/theus-arsenal/skills/runtime-bun/SKILL.md +0 -83
- package/vendor/theus-arsenal/skills/salud-codigo-codescene/SKILL.md +0 -163
- package/vendor/theus-arsenal/skills/salud-interfaces-red/SKILL.md +0 -143
- package/vendor/theus-arsenal/skills/seguimiento-costos/SKILL.md +0 -122
- package/vendor/theus-arsenal/skills/seguridad-agentes-trading/SKILL.md +0 -145
- package/vendor/theus-arsenal/skills/seguridad-amm-defi/SKILL.md +0 -166
- package/vendor/theus-arsenal/skills/seguridad-django/SKILL.md +0 -648
- package/vendor/theus-arsenal/skills/seguridad-laravel/SKILL.md +0 -945
- package/vendor/theus-arsenal/skills/seguridad-perl/SKILL.md +0 -507
- package/vendor/theus-arsenal/skills/seguridad-quarkus/SKILL.md +0 -474
- package/vendor/theus-arsenal/skills/seguridad-springboot/SKILL.md +0 -279
- package/vendor/theus-arsenal/skills/seo/SKILL.md +0 -166
- package/vendor/theus-arsenal/skills/sistema-diseno/SKILL.md +0 -81
- package/vendor/theus-arsenal/skills/sistema-operativo-agentico/SKILL.md +0 -375
- package/vendor/theus-arsenal/skills/sistemas-latencia-critica/SKILL.md +0 -65
- package/vendor/theus-arsenal/skills/tdd-django/SKILL.md +0 -735
- package/vendor/theus-arsenal/skills/tdd-laravel/SKILL.md +0 -673
- package/vendor/theus-arsenal/skills/tdd-quarkus/SKILL.md +0 -817
- package/vendor/theus-arsenal/skills/tdd-springboot/SKILL.md +0 -165
- package/vendor/theus-arsenal/skills/testing-cpp/SKILL.md +0 -328
- package/vendor/theus-arsenal/skills/testing-csharp/SKILL.md +0 -321
- package/vendor/theus-arsenal/skills/testing-e2e/SKILL.md +0 -325
- package/vendor/theus-arsenal/skills/testing-go/SKILL.md +0 -719
- package/vendor/theus-arsenal/skills/testing-kotlin/SKILL.md +0 -829
- package/vendor/theus-arsenal/skills/testing-perl/SKILL.md +0 -479
- package/vendor/theus-arsenal/skills/testing-protocolos-di-swift/SKILL.md +0 -196
- package/vendor/theus-arsenal/skills/testing-python/SKILL.md +0 -815
- package/vendor/theus-arsenal/skills/testing-react/SKILL.md +0 -421
- package/vendor/theus-arsenal/skills/testing-rust/SKILL.md +0 -506
- package/vendor/theus-arsenal/skills/tours-de-codigo/SKILL.md +0 -252
- package/vendor/theus-arsenal/skills/traduccion-documentos-visado/SKILL.md +0 -121
- package/vendor/theus-arsenal/skills/ui-a-vue/SKILL.md +0 -141
- package/vendor/theus-arsenal/skills/validacion-config-red/SKILL.md +0 -211
- package/vendor/theus-arsenal/skills/verificacion-django/SKILL.md +0 -470
- package/vendor/theus-arsenal/skills/verificacion-factual/SKILL.md +0 -142
- package/vendor/theus-arsenal/skills/verificacion-laravel/SKILL.md +0 -184
- package/vendor/theus-arsenal/skills/verificacion-quarkus/SKILL.md +0 -485
- package/vendor/theus-arsenal/skills/verificacion-springboot/SKILL.md +0 -237
- package/vendor/theus-arsenal/skills/videodb/SKILL.md +0 -374
- package/vendor/theus-arsenal/skills/videodb/reference/api-reference.md +0 -550
- package/vendor/theus-arsenal/skills/videodb/reference/capture-reference.md +0 -407
- package/vendor/theus-arsenal/skills/videodb/reference/capture.md +0 -101
- package/vendor/theus-arsenal/skills/videodb/reference/editor.md +0 -443
- package/vendor/theus-arsenal/skills/videodb/reference/generative.md +0 -331
- package/vendor/theus-arsenal/skills/videodb/reference/rtstream-reference.md +0 -564
- package/vendor/theus-arsenal/skills/videodb/reference/rtstream.md +0 -65
- package/vendor/theus-arsenal/skills/videodb/reference/search.md +0 -230
- package/vendor/theus-arsenal/skills/videodb/reference/streaming.md +0 -406
- package/vendor/theus-arsenal/skills/videodb/reference/use-cases.md +0 -118
- package/vendor/theus-arsenal/skills/videodb/scripts/ws_listener.py +0 -279
- package/vendor/theus-arsenal/skills/videos-manim/SKILL.md +0 -91
- package/vendor/theus-arsenal/skills/videos-manim/assets/network_graph_scene.py +0 -52
- package/vendor/theus-arsenal/skills/vigilancia-canary/SKILL.md +0 -108
- package/vendor/theus-arsenal/skills/voz-de-marca/SKILL.md +0 -106
|
@@ -0,0 +1,2171 @@
|
|
|
1
|
+
"""Detect and strip C2PA / AI-related metadata from raster images (stdlib).
|
|
2
|
+
|
|
3
|
+
Supported formats: PNG, JPEG, WebP, AVIF/HEIC (ISOBMFF), BMP, GIF, and TIFF
|
|
4
|
+
(classic and BigTIFF).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import re
|
|
13
|
+
import struct
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import urllib.error
|
|
17
|
+
import urllib.request
|
|
18
|
+
import zlib
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
from urllib.parse import urlparse
|
|
23
|
+
|
|
24
|
+
from common import (
|
|
25
|
+
c2patool_probe_note,
|
|
26
|
+
classify_finding_confidence,
|
|
27
|
+
safe_arg,
|
|
28
|
+
safe_write_bytes,
|
|
29
|
+
subprocess_creationflags,
|
|
30
|
+
subprocess_preexec_fn,
|
|
31
|
+
which,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
35
|
+
|
|
36
|
+
# Optional HTTP SynthID scorer sidecar (synthid_score_server.py). When
|
|
37
|
+
# WATERMARKS_SYNTHID_SCORER_URL is set, run_synthid_score calls the sidecar
|
|
38
|
+
# instead of a local reverse-SynthID checkout — this keeps the published core
|
|
39
|
+
# image free of the non-commercial upstream code. Read per call so tests can
|
|
40
|
+
# set the env vars after import.
|
|
41
|
+
DEFAULT_SYNTHID_SCORER_TIMEOUT = 60.0
|
|
42
|
+
|
|
43
|
+
# CtrlRegen is torch-based and needs far more address space than the stdlib
|
|
44
|
+
# parsers. The invoking clean_image.py subprocess applies these higher,
|
|
45
|
+
# env-overridable caps instead of the default child limits in common.py.
|
|
46
|
+
_CTRLREGEN_RLIMIT_AS = int(os.environ.get("WATERMARKS_CTRLREGEN_RLIMIT_AS", str(32 << 30)))
|
|
47
|
+
_CTRLREGEN_RLIMIT_FSIZE = int(os.environ.get("WATERMARKS_CTRLREGEN_RLIMIT_FSIZE", str(2 << 30)))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ctrlregen_preexec_fn() -> None:
|
|
51
|
+
"""Higher resource caps for the CtrlRegen subprocess (torch memory)."""
|
|
52
|
+
try:
|
|
53
|
+
import resource
|
|
54
|
+
|
|
55
|
+
resource.setrlimit(resource.RLIMIT_AS, (_CTRLREGEN_RLIMIT_AS, _CTRLREGEN_RLIMIT_AS))
|
|
56
|
+
resource.setrlimit(
|
|
57
|
+
resource.RLIMIT_FSIZE, (_CTRLREGEN_RLIMIT_FSIZE, _CTRLREGEN_RLIMIT_FSIZE)
|
|
58
|
+
)
|
|
59
|
+
except (ImportError, OSError, ValueError):
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
ctrlregen_subprocess_preexec_fn = ctrlregen_preexec_fn if os.name == "posix" else None
|
|
64
|
+
|
|
65
|
+
PNG_SIG = b"\x89PNG\r\n\x1a\n"
|
|
66
|
+
JPEG_SOI = b"\xff\xd8"
|
|
67
|
+
WEBP_RIFF = b"RIFF"
|
|
68
|
+
WEBP_SIG = b"WEBP"
|
|
69
|
+
BMP_SIG = b"BM"
|
|
70
|
+
GIF_SIGS = (b"GIF87a", b"GIF89a")
|
|
71
|
+
TIFF_LE_SIG = b"II*\x00"
|
|
72
|
+
TIFF_BE_SIG = b"MM\x00*"
|
|
73
|
+
TIFF_LE_BIG_SIG = b"II+\x00"
|
|
74
|
+
TIFF_BE_BIG_SIG = b"MM\x00+"
|
|
75
|
+
|
|
76
|
+
# Chunk types / content patterns associated with C2PA / AI provenance
|
|
77
|
+
C2PA_MARKERS = (
|
|
78
|
+
b"c2pa",
|
|
79
|
+
b"C2PA",
|
|
80
|
+
b"jumb",
|
|
81
|
+
b"JUMB",
|
|
82
|
+
b"c2ma",
|
|
83
|
+
b"contentcredentials",
|
|
84
|
+
b"contentauth",
|
|
85
|
+
b"cai:",
|
|
86
|
+
b"http://ns.adobe.com/xmp/InstanceID/", # not solely C2PA
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
AI_META_HINTS = (
|
|
90
|
+
b"c2pa",
|
|
91
|
+
b"C2PA",
|
|
92
|
+
b"contentcredentials",
|
|
93
|
+
b"ContentCredentials",
|
|
94
|
+
b"digitalSourceType",
|
|
95
|
+
b"trainedAlgorithmicMedia",
|
|
96
|
+
b"compositeWithTrainedAlgorithmicMedia",
|
|
97
|
+
b"algorithmicMedia",
|
|
98
|
+
b"AIGC",
|
|
99
|
+
b"aigc",
|
|
100
|
+
b"AI generated",
|
|
101
|
+
b"Generated by",
|
|
102
|
+
b"Claude",
|
|
103
|
+
b"Anthropic",
|
|
104
|
+
b"OpenAI",
|
|
105
|
+
b"SynthID",
|
|
106
|
+
b"dcterms:provenance",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# JPEG COM is unkeyed free text, so only unambiguous provenance markers are
|
|
110
|
+
# evidence of AI metadata. Bare vendor names and "Generated by" are ordinary
|
|
111
|
+
# comment text too (for example, "Claude Monet" or "Generated by ImageMagick").
|
|
112
|
+
JPEG_C2PA_MARKERS = (
|
|
113
|
+
b"c2pa",
|
|
114
|
+
b"c2ma",
|
|
115
|
+
b"contentcredentials",
|
|
116
|
+
b"contentauth",
|
|
117
|
+
b"cai:",
|
|
118
|
+
)
|
|
119
|
+
JPEG_COM_AI_HINTS = (
|
|
120
|
+
*JPEG_C2PA_MARKERS,
|
|
121
|
+
b"digitalSourceType",
|
|
122
|
+
b"trainedAlgorithmicMedia",
|
|
123
|
+
b"compositeWithTrainedAlgorithmicMedia",
|
|
124
|
+
b"algorithmicMedia",
|
|
125
|
+
b"AIGC",
|
|
126
|
+
b"AI generated",
|
|
127
|
+
b"Generated by AI",
|
|
128
|
+
b"SynthID",
|
|
129
|
+
b"dcterms:provenance",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Well-known AI generator product/model names. These are matched ONLY
|
|
133
|
+
# against the values of generator-bearing PNG text-chunk keys (Software,
|
|
134
|
+
# Creator, parameters) — see _generator_product_hits — and deliberately
|
|
135
|
+
# kept out of the flat AI_META_HINTS blob scan: bare "Gemini", "Sora",
|
|
136
|
+
# or "Firefly" are ordinary words that can appear in captions, and only a
|
|
137
|
+
# generator field makes them evidence of AI provenance.
|
|
138
|
+
AI_GENERATOR_PRODUCTS = (
|
|
139
|
+
b"ChatGPT",
|
|
140
|
+
b"DALL-E",
|
|
141
|
+
b"Midjourney",
|
|
142
|
+
b"Stable Diffusion",
|
|
143
|
+
b"SDXL",
|
|
144
|
+
b"FLUX",
|
|
145
|
+
b"DreamStudio",
|
|
146
|
+
b"Leonardo AI",
|
|
147
|
+
b"Leonardo.Ai",
|
|
148
|
+
b"Craiyon",
|
|
149
|
+
b"NovelAI",
|
|
150
|
+
b"Ideogram",
|
|
151
|
+
b"TensorArt",
|
|
152
|
+
b"Recraft",
|
|
153
|
+
b"Clipdrop",
|
|
154
|
+
b"DeepAI",
|
|
155
|
+
b"NightCafe",
|
|
156
|
+
b"Bing Image Creator",
|
|
157
|
+
b"Adobe Firefly",
|
|
158
|
+
b"Firefly",
|
|
159
|
+
b"Gemini",
|
|
160
|
+
b"Imagen",
|
|
161
|
+
b"Grok",
|
|
162
|
+
b"Sora",
|
|
163
|
+
b"Veo",
|
|
164
|
+
b"Kling",
|
|
165
|
+
b"Runway",
|
|
166
|
+
b"Luma",
|
|
167
|
+
b"Qwen",
|
|
168
|
+
b"GPT-4",
|
|
169
|
+
b"GPT-5",
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# PNG text-chunk keys whose value can name the generating model/tool.
|
|
173
|
+
# tEXt/iTXt "Software" and "Creator" are the classic generator fields;
|
|
174
|
+
# "parameters" is what Stable Diffusion WebUI writes with the full
|
|
175
|
+
# generation string. Values of other keys (Comment, Description, Title,
|
|
176
|
+
# ...) are free text and are never scanned for product names.
|
|
177
|
+
_GENERATOR_TEXT_KEYS = ("software", "creator", "parameters")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class ImageInspectReport:
|
|
182
|
+
path: str
|
|
183
|
+
format: str # png | jpeg | webp | avif | heic | bmp | gif | tiff | unknown
|
|
184
|
+
has_c2pa: bool
|
|
185
|
+
has_ai_metadata: bool
|
|
186
|
+
findings: list[str] = field(default_factory=list)
|
|
187
|
+
tools: dict[str, Any] = field(default_factory=dict)
|
|
188
|
+
synthid: dict[str, Any] | None = None
|
|
189
|
+
notes: list[str] = field(default_factory=list)
|
|
190
|
+
|
|
191
|
+
def to_dict(self) -> dict:
|
|
192
|
+
return {
|
|
193
|
+
"path": self.path,
|
|
194
|
+
"format": self.format,
|
|
195
|
+
"has_c2pa": self.has_c2pa,
|
|
196
|
+
"has_ai_metadata": self.has_ai_metadata,
|
|
197
|
+
"findings": self.findings,
|
|
198
|
+
"findings_confidence": [classify_finding_confidence(f) for f in self.findings],
|
|
199
|
+
"tools": self.tools,
|
|
200
|
+
"synthid": self.synthid,
|
|
201
|
+
"notes": self.notes,
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def detect_format(data: bytes) -> str:
|
|
206
|
+
if data.startswith(PNG_SIG):
|
|
207
|
+
return "png"
|
|
208
|
+
if data.startswith(JPEG_SOI):
|
|
209
|
+
return "jpeg"
|
|
210
|
+
if len(data) >= 12 and data[:4] == WEBP_RIFF and data[8:12] == WEBP_SIG:
|
|
211
|
+
return "webp"
|
|
212
|
+
if len(data) >= 12 and data[4:8] == b"ftyp":
|
|
213
|
+
box_size = struct.unpack(">I", data[0:4])[0]
|
|
214
|
+
header_chunk = (
|
|
215
|
+
data[8 : min(box_size, len(data), 64)]
|
|
216
|
+
if box_size >= 8
|
|
217
|
+
else data[8 : min(len(data), 64)]
|
|
218
|
+
)
|
|
219
|
+
if any(b in header_chunk for b in (b"avif", b"avis", b"avio")):
|
|
220
|
+
return "avif"
|
|
221
|
+
if any(
|
|
222
|
+
b in header_chunk
|
|
223
|
+
for b in (b"heic", b"heix", b"hevc", b"heim", b"heis", b"mif1", b"msf1", b"heif")
|
|
224
|
+
):
|
|
225
|
+
return "heic"
|
|
226
|
+
if data[:2] == BMP_SIG:
|
|
227
|
+
return "bmp"
|
|
228
|
+
if data[:6] in GIF_SIGS:
|
|
229
|
+
return "gif"
|
|
230
|
+
if data[:4] in (TIFF_LE_SIG, TIFF_BE_SIG, TIFF_LE_BIG_SIG, TIFF_BE_BIG_SIG):
|
|
231
|
+
return "tiff"
|
|
232
|
+
return "unknown"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _contains_any(blob: bytes, needles: tuple[bytes, ...]) -> list[str]:
|
|
236
|
+
found = []
|
|
237
|
+
lower = blob.lower()
|
|
238
|
+
for n in needles:
|
|
239
|
+
if n.lower() in lower:
|
|
240
|
+
try:
|
|
241
|
+
found.append(n.decode("ascii", errors="replace"))
|
|
242
|
+
except Exception:
|
|
243
|
+
found.append(repr(n))
|
|
244
|
+
return found
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _png_text_entries(payload: bytes, ctype: bytes) -> list[tuple[str, str]]:
|
|
248
|
+
"""Parse a PNG text-chunk payload into (key, value) pairs.
|
|
249
|
+
|
|
250
|
+
Handles tEXt (latin-1), zTXt (zlib-compressed text), and iTXt
|
|
251
|
+
(UTF-8, optionally compressed). Malformed or undecodable chunks
|
|
252
|
+
yield whatever pairs are recoverable; nothing is raised.
|
|
253
|
+
"""
|
|
254
|
+
entries: list[tuple[str, str]] = []
|
|
255
|
+
if ctype == b"tEXt":
|
|
256
|
+
key, sep, text = payload.partition(b"\x00")
|
|
257
|
+
if sep:
|
|
258
|
+
entries.append(
|
|
259
|
+
(
|
|
260
|
+
key.decode("latin-1", errors="replace"),
|
|
261
|
+
text.decode("latin-1", errors="replace"),
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
elif ctype == b"zTXt":
|
|
265
|
+
key, sep, rest = payload.partition(b"\x00")
|
|
266
|
+
if not sep or len(rest) < 2:
|
|
267
|
+
return entries
|
|
268
|
+
try:
|
|
269
|
+
text = zlib.decompress(rest[1:])
|
|
270
|
+
except zlib.error:
|
|
271
|
+
return entries
|
|
272
|
+
entries.append(
|
|
273
|
+
(
|
|
274
|
+
key.decode("latin-1", errors="replace"),
|
|
275
|
+
text.decode("latin-1", errors="replace"),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
elif ctype == b"iTXt":
|
|
279
|
+
key, sep, rest = payload.partition(b"\x00")
|
|
280
|
+
if not sep or len(rest) < 4:
|
|
281
|
+
return entries
|
|
282
|
+
comp_flag = rest[0]
|
|
283
|
+
rest = rest[2:] # skip compression flag + method
|
|
284
|
+
_lang, sep2, rest = rest.partition(b"\x00")
|
|
285
|
+
if not sep2:
|
|
286
|
+
return entries
|
|
287
|
+
_tkey, sep3, text = rest.partition(b"\x00")
|
|
288
|
+
if not sep3:
|
|
289
|
+
return entries
|
|
290
|
+
if comp_flag == 1:
|
|
291
|
+
try:
|
|
292
|
+
text = zlib.decompress(text)
|
|
293
|
+
except zlib.error:
|
|
294
|
+
return entries
|
|
295
|
+
entries.append(
|
|
296
|
+
(
|
|
297
|
+
key.decode("latin-1", errors="replace"),
|
|
298
|
+
text.decode("utf-8", errors="replace"),
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
return entries
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _generator_product_hits(entries: list[tuple[str, str]]) -> list[str]:
|
|
305
|
+
"""Product-name hits scoped to generator-bearing text-chunk keys.
|
|
306
|
+
|
|
307
|
+
Returns labels like "Software=ChatGPT" (one per matching product).
|
|
308
|
+
Matching is a case-insensitive substring check on the value, mirroring
|
|
309
|
+
_contains_any, but only for _GENERATOR_TEXT_KEYS values.
|
|
310
|
+
"""
|
|
311
|
+
hits: list[str] = []
|
|
312
|
+
for key, value in entries:
|
|
313
|
+
if key.strip().lower() not in _GENERATOR_TEXT_KEYS:
|
|
314
|
+
continue
|
|
315
|
+
low = value.lower()
|
|
316
|
+
for product in AI_GENERATOR_PRODUCTS:
|
|
317
|
+
label = product.decode("ascii", errors="replace")
|
|
318
|
+
if label.lower() in low:
|
|
319
|
+
hits.append(f"{key.strip()}={label}")
|
|
320
|
+
return hits
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _png_text_hits(payload: bytes, ctype: bytes) -> tuple[list[str], list[str]]:
|
|
324
|
+
"""Return flat-marker and product-name hits from a PNG text chunk."""
|
|
325
|
+
entries = _png_text_entries(payload, ctype)
|
|
326
|
+
decoded = "\n".join(value for _key, value in entries).encode("utf-8")
|
|
327
|
+
hits = _contains_any(payload + b"\n" + decoded, AI_META_HINTS + C2PA_MARKERS)
|
|
328
|
+
return hits, _generator_product_hits(entries)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _text_chunk_is_ai(payload: bytes, ctype: bytes) -> bool:
|
|
332
|
+
"""True when a PNG text chunk carries AI/C2PA markers.
|
|
333
|
+
|
|
334
|
+
Flat markers (AI_META_HINTS + C2PA_MARKERS) match the raw payload and
|
|
335
|
+
decoded text; generator product names only match generator-bearing key
|
|
336
|
+
values (see _generator_product_hits).
|
|
337
|
+
"""
|
|
338
|
+
hits, product_hits = _png_text_hits(payload, ctype)
|
|
339
|
+
return bool(hits or product_hits)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def inspect_png(data: bytes) -> tuple[bool, bool, list[str]]:
|
|
343
|
+
findings: list[str] = []
|
|
344
|
+
has_c2pa = False
|
|
345
|
+
has_ai = False
|
|
346
|
+
if not data.startswith(PNG_SIG):
|
|
347
|
+
return False, False, ["not a PNG"]
|
|
348
|
+
pos = 8
|
|
349
|
+
while pos + 8 <= len(data):
|
|
350
|
+
length = struct.unpack(">I", data[pos : pos + 4])[0]
|
|
351
|
+
ctype = data[pos + 4 : pos + 8]
|
|
352
|
+
chunk_start = pos + 8
|
|
353
|
+
chunk_end = chunk_start + length
|
|
354
|
+
if chunk_end + 4 > len(data):
|
|
355
|
+
findings.append(f"truncated chunk {ctype!r}")
|
|
356
|
+
break
|
|
357
|
+
payload = data[chunk_start:chunk_end]
|
|
358
|
+
name = ctype.decode("latin-1", errors="replace")
|
|
359
|
+
# Private/ancillary chunks sometimes used for JUMBF/C2PA
|
|
360
|
+
if ctype in (b"caBX", b"juMB", b"jumb") or ctype.startswith(b"c2"):
|
|
361
|
+
has_c2pa = True
|
|
362
|
+
findings.append(f"PNG chunk {name} (possible C2PA container)")
|
|
363
|
+
if ctype in (b"tEXt", b"zTXt", b"iTXt", b"eXIf"):
|
|
364
|
+
if ctype in (b"tEXt", b"zTXt", b"iTXt"):
|
|
365
|
+
hits, product_hits = _png_text_hits(payload, ctype)
|
|
366
|
+
else:
|
|
367
|
+
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
368
|
+
product_hits = []
|
|
369
|
+
if hits or product_hits:
|
|
370
|
+
has_ai = True
|
|
371
|
+
if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
|
|
372
|
+
has_c2pa = True
|
|
373
|
+
parts = [", ".join(hits[:8])] if hits else []
|
|
374
|
+
if product_hits:
|
|
375
|
+
parts.append(f"AI generator ({', '.join(product_hits[:8])})")
|
|
376
|
+
findings.append(f"PNG {name}: {'; '.join(parts)}")
|
|
377
|
+
if ctype == b"IEND":
|
|
378
|
+
break
|
|
379
|
+
pos = chunk_end + 4 # skip CRC
|
|
380
|
+
# Whole-file scan fallback
|
|
381
|
+
whole = _contains_any(data, C2PA_MARKERS)
|
|
382
|
+
if whole and not has_c2pa:
|
|
383
|
+
has_c2pa = True
|
|
384
|
+
findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
|
|
385
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def inspect_jpeg(data: bytes) -> tuple[bool, bool, list[str]]:
|
|
389
|
+
findings: list[str] = []
|
|
390
|
+
has_c2pa = False
|
|
391
|
+
has_ai = False
|
|
392
|
+
if not data.startswith(JPEG_SOI):
|
|
393
|
+
return False, False, ["not a JPEG"]
|
|
394
|
+
i = 2
|
|
395
|
+
n = len(data)
|
|
396
|
+
while i + 4 <= n:
|
|
397
|
+
if data[i] != 0xFF:
|
|
398
|
+
i += 1
|
|
399
|
+
continue
|
|
400
|
+
# skip fill
|
|
401
|
+
while i < n and data[i] == 0xFF:
|
|
402
|
+
i += 1
|
|
403
|
+
if i >= n:
|
|
404
|
+
break
|
|
405
|
+
marker = data[i]
|
|
406
|
+
i += 1
|
|
407
|
+
if marker in (0xD8, 0xD9): # SOI/EOI
|
|
408
|
+
continue
|
|
409
|
+
if marker == 0xDA: # SOS — image data follows
|
|
410
|
+
break
|
|
411
|
+
if marker >= 0xD0 and marker <= 0xD7: # RSTn
|
|
412
|
+
continue
|
|
413
|
+
if i + 2 > n:
|
|
414
|
+
break
|
|
415
|
+
seglen = struct.unpack(">H", data[i : i + 2])[0]
|
|
416
|
+
if seglen < 2 or i + seglen > n:
|
|
417
|
+
findings.append(f"bad segment length at marker 0x{marker:02X}")
|
|
418
|
+
break
|
|
419
|
+
payload = data[i + 2 : i + seglen]
|
|
420
|
+
i += seglen
|
|
421
|
+
|
|
422
|
+
if marker == 0xFE: # COM
|
|
423
|
+
hits = _contains_any(payload, JPEG_COM_AI_HINTS)
|
|
424
|
+
if hits:
|
|
425
|
+
has_ai = True
|
|
426
|
+
if any(
|
|
427
|
+
h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
|
|
428
|
+
):
|
|
429
|
+
has_c2pa = True
|
|
430
|
+
findings.append(f"JPEG COM: {', '.join(hits[:8])}")
|
|
431
|
+
# APP11 (0xEB) often holds JUMBF/C2PA
|
|
432
|
+
if marker == 0xEB:
|
|
433
|
+
has_c2pa = True
|
|
434
|
+
findings.append("JPEG APP11 segment (JUMBF/C2PA common)")
|
|
435
|
+
if marker in (0xE1, 0xE2, 0xED, 0xEE, 0xEB): # APP1,2,13,14,11
|
|
436
|
+
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
437
|
+
if hits:
|
|
438
|
+
has_ai = True
|
|
439
|
+
if any(
|
|
440
|
+
h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
|
|
441
|
+
):
|
|
442
|
+
has_c2pa = True
|
|
443
|
+
findings.append(f"JPEG APP{marker - 0xE0}: {', '.join(hits[:8])}")
|
|
444
|
+
|
|
445
|
+
whole = _contains_any(data, JPEG_C2PA_MARKERS)
|
|
446
|
+
if whole and not has_c2pa:
|
|
447
|
+
has_c2pa = True
|
|
448
|
+
findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
|
|
449
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def _webp_chunks(data: bytes) -> tuple[list[tuple[bytes, bytes, bytes]], list[str]]:
|
|
453
|
+
if detect_format(data) != "webp":
|
|
454
|
+
return [], ["not a WebP"]
|
|
455
|
+
|
|
456
|
+
notes: list[str] = []
|
|
457
|
+
declared_size = struct.unpack("<I", data[4:8])[0]
|
|
458
|
+
if declared_size + 8 != len(data):
|
|
459
|
+
notes.append(f"RIFF size mismatch: header={declared_size + 8} actual={len(data)}")
|
|
460
|
+
|
|
461
|
+
chunks: list[tuple[bytes, bytes, bytes]] = []
|
|
462
|
+
pos = 12
|
|
463
|
+
while pos + 8 <= len(data):
|
|
464
|
+
fourcc = data[pos : pos + 4]
|
|
465
|
+
length = struct.unpack("<I", data[pos + 4 : pos + 8])[0]
|
|
466
|
+
payload_start = pos + 8
|
|
467
|
+
payload_end = payload_start + length
|
|
468
|
+
padded_end = payload_end + (length & 1)
|
|
469
|
+
if padded_end > len(data):
|
|
470
|
+
name = fourcc.decode("latin-1", errors="replace")
|
|
471
|
+
notes.append(f"truncated WebP chunk {name}")
|
|
472
|
+
break
|
|
473
|
+
chunks.append((fourcc, data[payload_start:payload_end], data[payload_end:padded_end]))
|
|
474
|
+
pos = padded_end
|
|
475
|
+
if pos != len(data) and not any("truncated" in note for note in notes):
|
|
476
|
+
notes.append(f"trailing WebP bytes: {len(data) - pos}")
|
|
477
|
+
return chunks, notes
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def inspect_webp(data: bytes) -> tuple[bool, bool, list[str]]:
|
|
481
|
+
chunks, findings = _webp_chunks(data)
|
|
482
|
+
if not chunks and findings == ["not a WebP"]:
|
|
483
|
+
return False, False, findings
|
|
484
|
+
|
|
485
|
+
has_c2pa = False
|
|
486
|
+
has_ai = False
|
|
487
|
+
for fourcc, payload, _padding in chunks:
|
|
488
|
+
name = fourcc.decode("latin-1", errors="replace")
|
|
489
|
+
if fourcc.upper() == b"C2PA":
|
|
490
|
+
has_c2pa = True
|
|
491
|
+
has_ai = True
|
|
492
|
+
findings.append("WebP C2PA chunk")
|
|
493
|
+
continue
|
|
494
|
+
if fourcc in (b"XMP ", b"EXIF"):
|
|
495
|
+
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
496
|
+
if hits:
|
|
497
|
+
has_ai = True
|
|
498
|
+
if any(
|
|
499
|
+
hit.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth")
|
|
500
|
+
for hit in hits
|
|
501
|
+
):
|
|
502
|
+
has_c2pa = True
|
|
503
|
+
findings.append(f"WebP {name}: {', '.join(hits[:8])}")
|
|
504
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
XMP_UUID = b"\xbe\x7a\xcf\xcb\x97\xa9\x42\xe8\x9c\x71\x99\x94\x91\xe3\xaf\xac"
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _parse_isobmff_boxes(
|
|
511
|
+
data: bytes, start: int = 0, end: int | None = None
|
|
512
|
+
) -> tuple[list[tuple[bytes, bytes, int, int]], int]:
|
|
513
|
+
"""Parse top-level or container ISOBMFF boxes.
|
|
514
|
+
|
|
515
|
+
Returns (boxes, scanned_end): the list of (fourcc, payload,
|
|
516
|
+
total_box_size, header_size) tuples and the offset where the walk
|
|
517
|
+
stopped -- the start of the first box that could not be parsed (an
|
|
518
|
+
overrunning box, or a run of fewer than 8 trailing bytes), or `end`
|
|
519
|
+
when the whole buffer parsed.
|
|
520
|
+
"""
|
|
521
|
+
if end is None:
|
|
522
|
+
end = len(data)
|
|
523
|
+
boxes = []
|
|
524
|
+
pos = start
|
|
525
|
+
while pos + 8 <= end:
|
|
526
|
+
size = struct.unpack(">I", data[pos : pos + 4])[0]
|
|
527
|
+
fourcc = data[pos + 4 : pos + 8]
|
|
528
|
+
header_size = 8
|
|
529
|
+
if size == 1:
|
|
530
|
+
if pos + 16 > end:
|
|
531
|
+
break
|
|
532
|
+
size = struct.unpack(">Q", data[pos + 8 : pos + 16])[0]
|
|
533
|
+
header_size = 16
|
|
534
|
+
elif size == 0:
|
|
535
|
+
size = end - pos
|
|
536
|
+
|
|
537
|
+
if size < header_size or pos + size > end:
|
|
538
|
+
break
|
|
539
|
+
payload = data[pos + header_size : pos + size]
|
|
540
|
+
boxes.append((fourcc, payload, size, header_size))
|
|
541
|
+
pos += size
|
|
542
|
+
return boxes, pos
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def _build_isobmff_box(fourcc: bytes, payload: bytes, header_size: int = 8) -> bytes:
|
|
546
|
+
"""Serialize an ISOBMFF box while preserving its header width."""
|
|
547
|
+
size = len(payload) + header_size
|
|
548
|
+
if header_size == 16:
|
|
549
|
+
return struct.pack(">I4sQ", 1, fourcc, size) + payload
|
|
550
|
+
return struct.pack(">I4s", size, fourcc) + payload
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _isobmff_free_box(size: int, header_size: int = 8) -> bytes:
|
|
554
|
+
"""Return an equal-size free box so later absolute offsets stay valid."""
|
|
555
|
+
return _build_isobmff_box(b"free", b"\x00" * (size - header_size), header_size)
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
def inspect_isobmff(data: bytes, fmt: str = "avif") -> tuple[bool, bool, list[str]]:
|
|
559
|
+
findings: list[str] = []
|
|
560
|
+
has_c2pa = False
|
|
561
|
+
has_ai = False
|
|
562
|
+
|
|
563
|
+
boxes, _ = _parse_isobmff_boxes(data)
|
|
564
|
+
if not boxes:
|
|
565
|
+
# Box parsing failed (e.g. the first box's size overruns a truncated
|
|
566
|
+
# download) — that is exactly when the whole-file byte scan below is
|
|
567
|
+
# most useful, and every sibling inspector (png/jpeg/gif/tiff) still
|
|
568
|
+
# runs its equivalent after a truncation. Run the fallback, then
|
|
569
|
+
# report the parse failure alongside whatever it found (#167).
|
|
570
|
+
whole = _contains_any(data, C2PA_MARKERS)
|
|
571
|
+
if whole:
|
|
572
|
+
findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
|
|
573
|
+
has_c2pa = True
|
|
574
|
+
findings.append(f"not a valid {fmt.upper()} (no ISOBMFF boxes found)")
|
|
575
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
576
|
+
|
|
577
|
+
for fourcc, payload, _, _ in boxes:
|
|
578
|
+
name = fourcc.decode("latin-1", errors="replace")
|
|
579
|
+
if fourcc in (b"jumb", b"c2pa") or name.lower().startswith("c2"):
|
|
580
|
+
has_c2pa = True
|
|
581
|
+
findings.append(f"{fmt.upper()} top-level box {name} (C2PA/JUMBF manifest)")
|
|
582
|
+
elif fourcc == b"uuid":
|
|
583
|
+
if payload.startswith(XMP_UUID):
|
|
584
|
+
has_ai = True
|
|
585
|
+
xmp_text = payload[16:]
|
|
586
|
+
hits = _contains_any(xmp_text, AI_META_HINTS + C2PA_MARKERS)
|
|
587
|
+
if hits:
|
|
588
|
+
findings.append(f"{fmt.upper()} XMP uuid box: {', '.join(hits[:8])}")
|
|
589
|
+
else:
|
|
590
|
+
findings.append(f"{fmt.upper()} XMP uuid box")
|
|
591
|
+
if any(
|
|
592
|
+
h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
|
|
593
|
+
):
|
|
594
|
+
has_c2pa = True
|
|
595
|
+
else:
|
|
596
|
+
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
597
|
+
if hits:
|
|
598
|
+
has_ai = True
|
|
599
|
+
findings.append(f"{fmt.upper()} uuid box: {', '.join(hits[:8])}")
|
|
600
|
+
if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
|
|
601
|
+
has_c2pa = True
|
|
602
|
+
elif fourcc == b"meta":
|
|
603
|
+
meta_sub, _ = _parse_isobmff_boxes(payload, start=4)
|
|
604
|
+
for s_fourcc, s_payload, _, _ in meta_sub:
|
|
605
|
+
s_name = s_fourcc.decode("latin-1", errors="replace")
|
|
606
|
+
if s_fourcc in (b"jumb", b"c2pa") or s_name.lower().startswith("c2"):
|
|
607
|
+
has_c2pa = True
|
|
608
|
+
findings.append(f"{fmt.upper()} meta sub-box {s_name} (C2PA/JUMBF container)")
|
|
609
|
+
elif s_fourcc == b"uuid":
|
|
610
|
+
if s_payload.startswith(XMP_UUID):
|
|
611
|
+
has_ai = True
|
|
612
|
+
hits = _contains_any(s_payload[16:], AI_META_HINTS + C2PA_MARKERS)
|
|
613
|
+
if hits:
|
|
614
|
+
findings.append(f"{fmt.upper()} meta XMP uuid: {', '.join(hits[:8])}")
|
|
615
|
+
else:
|
|
616
|
+
findings.append(f"{fmt.upper()} meta XMP uuid box")
|
|
617
|
+
if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
|
|
618
|
+
has_c2pa = True
|
|
619
|
+
else:
|
|
620
|
+
hits = _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS)
|
|
621
|
+
if hits:
|
|
622
|
+
has_ai = True
|
|
623
|
+
findings.append(f"{fmt.upper()} meta uuid: {', '.join(hits[:8])}")
|
|
624
|
+
elif s_fourcc in (b"iinf", b"infe", b"iref", b"iloc", b"xml ", b"bxml"):
|
|
625
|
+
hits = _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS)
|
|
626
|
+
if hits:
|
|
627
|
+
has_ai = True
|
|
628
|
+
if any(h.lower() in ("c2pa", "contentcredentials", "jumb") for h in hits):
|
|
629
|
+
has_c2pa = True
|
|
630
|
+
findings.append(f"{fmt.upper()} meta/{s_name}: {', '.join(hits[:8])}")
|
|
631
|
+
|
|
632
|
+
whole = _contains_any(data, C2PA_MARKERS)
|
|
633
|
+
if whole and not has_c2pa:
|
|
634
|
+
has_c2pa = True
|
|
635
|
+
findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
|
|
636
|
+
|
|
637
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
# ---------------------------------------------------------------------------
|
|
641
|
+
# BMP
|
|
642
|
+
# ---------------------------------------------------------------------------
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _bmp_payload_extent(data: bytes) -> tuple[int, int] | None:
|
|
646
|
+
"""Return (pixel_offset, pixel_size) for a BMP, or None.
|
|
647
|
+
|
|
648
|
+
Locates the true image payload by parsing the BITMAPFILEHEADER and the DIB
|
|
649
|
+
header (BITMAPINFOHEADER 40, V4 108, or V5 124 bytes), so trailing
|
|
650
|
+
non-image bytes can be told apart from pixel data. Uncompressed rows are
|
|
651
|
+
padded to 4-byte boundaries; compressed or embedded-image BMPs (RLE, JPEG,
|
|
652
|
+
PNG) rely on the declared biSizeImage, and anything unparseable returns
|
|
653
|
+
None so callers stay conservative.
|
|
654
|
+
"""
|
|
655
|
+
if len(data) < 30 or data[:2] != BMP_SIG:
|
|
656
|
+
return None
|
|
657
|
+
pixel_offset = struct.unpack("<I", data[10:14])[0]
|
|
658
|
+
dib_size = struct.unpack("<I", data[14:18])[0]
|
|
659
|
+
if dib_size < 40 or 14 + dib_size > len(data):
|
|
660
|
+
return None
|
|
661
|
+
width = struct.unpack("<i", data[18:22])[0]
|
|
662
|
+
height = struct.unpack("<i", data[22:26])[0]
|
|
663
|
+
bpp = struct.unpack("<H", data[28:30])[0]
|
|
664
|
+
compression = struct.unpack("<I", data[30:34])[0]
|
|
665
|
+
size_image = struct.unpack("<I", data[34:38])[0]
|
|
666
|
+
if width <= 0 or bpp == 0 or pixel_offset > len(data):
|
|
667
|
+
return None
|
|
668
|
+
if compression == 0: # BI_RGB
|
|
669
|
+
row_size = ((width * bpp + 31) // 32) * 4
|
|
670
|
+
size = row_size * abs(height)
|
|
671
|
+
elif size_image:
|
|
672
|
+
size = size_image
|
|
673
|
+
else:
|
|
674
|
+
return None
|
|
675
|
+
return pixel_offset, size
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def _bmp_trailing(data: bytes) -> bytes:
|
|
679
|
+
"""Bytes after the image payload, which is where non-standard BMP metadata lives."""
|
|
680
|
+
extent = _bmp_payload_extent(data)
|
|
681
|
+
if extent is None:
|
|
682
|
+
return b""
|
|
683
|
+
pixel_offset, size = extent
|
|
684
|
+
end = pixel_offset + size
|
|
685
|
+
return data[end:] if end < len(data) else b""
|
|
686
|
+
|
|
687
|
+
|
|
688
|
+
def inspect_bmp(data: bytes) -> tuple[bool, bool, list[str]]:
|
|
689
|
+
"""Inspect a BMP for trailing (non-standard) metadata.
|
|
690
|
+
|
|
691
|
+
BMP has no standardized metadata container; the only realistic place
|
|
692
|
+
provenance data can live is after the image payload. Pixel bytes are never
|
|
693
|
+
scanned, so compressed/embedded-image data cannot false-positive.
|
|
694
|
+
"""
|
|
695
|
+
findings: list[str] = []
|
|
696
|
+
if len(data) < 14 or data[:2] != BMP_SIG:
|
|
697
|
+
return False, False, ["not a BMP"]
|
|
698
|
+
trailing = _bmp_trailing(data)
|
|
699
|
+
has_c2pa = False
|
|
700
|
+
has_ai = False
|
|
701
|
+
if trailing:
|
|
702
|
+
hits = _contains_any(trailing, AI_META_HINTS + C2PA_MARKERS)
|
|
703
|
+
if hits:
|
|
704
|
+
has_ai = True
|
|
705
|
+
if any(
|
|
706
|
+
h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
|
|
707
|
+
):
|
|
708
|
+
has_c2pa = True
|
|
709
|
+
findings.append(f"BMP trailing metadata: {', '.join(hits[:6])}")
|
|
710
|
+
else:
|
|
711
|
+
findings.append(f"BMP has {len(trailing)} unrecognized trailing byte(s)")
|
|
712
|
+
else:
|
|
713
|
+
findings.append("BMP has no metadata (header-only raster format)")
|
|
714
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
def strip_bmp(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
|
|
718
|
+
"""Strip trailing non-image bytes from a BMP and fix the file-size field.
|
|
719
|
+
|
|
720
|
+
The pixel payload is located via the DIB header and left byte-identical;
|
|
721
|
+
anything after it is treated as metadata. The 4-byte file-size field at
|
|
722
|
+
offset 2 is rewritten to the truncated length.
|
|
723
|
+
"""
|
|
724
|
+
if len(data) < 14 or data[:2] != BMP_SIG:
|
|
725
|
+
raise ValueError("not BMP")
|
|
726
|
+
extent = _bmp_payload_extent(data)
|
|
727
|
+
if extent is None:
|
|
728
|
+
return data, ["BMP header not fully parsed; left unchanged"]
|
|
729
|
+
pixel_offset, size = extent
|
|
730
|
+
end = pixel_offset + size
|
|
731
|
+
if end >= len(data):
|
|
732
|
+
return data, ["no BMP trailing metadata to strip"]
|
|
733
|
+
trailing = data[end:]
|
|
734
|
+
hits = _contains_any(trailing, AI_META_HINTS + C2PA_MARKERS)
|
|
735
|
+
if not strip_all_metadata and not hits:
|
|
736
|
+
return data, ["BMP trailing bytes kept (keep-non-ai-metadata)"]
|
|
737
|
+
out = bytearray(data[:end])
|
|
738
|
+
out[2:6] = struct.pack("<I", end)
|
|
739
|
+
reason = f" ({', '.join(hits[:4])})" if hits else ""
|
|
740
|
+
return bytes(out), [f"drop {len(trailing)} BMP trailing byte(s){reason}"]
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
# ---------------------------------------------------------------------------
|
|
744
|
+
# GIF
|
|
745
|
+
# ---------------------------------------------------------------------------
|
|
746
|
+
|
|
747
|
+
|
|
748
|
+
def _gif_extension_info(data: bytes, start: int, n: int) -> tuple[int, int, bytes] | None:
|
|
749
|
+
"""Return (end, label, payload) for the extension block at *start* (0x21).
|
|
750
|
+
|
|
751
|
+
The payload is the concatenation of all sub-blocks after the label; *end*
|
|
752
|
+
is the offset just past the terminating 0x00.
|
|
753
|
+
"""
|
|
754
|
+
if start + 2 > n:
|
|
755
|
+
return None
|
|
756
|
+
label = data[start + 1]
|
|
757
|
+
pos = start + 2
|
|
758
|
+
payload = bytearray()
|
|
759
|
+
while pos < n:
|
|
760
|
+
size = data[pos]
|
|
761
|
+
pos += 1
|
|
762
|
+
if size == 0:
|
|
763
|
+
return pos, label, bytes(payload)
|
|
764
|
+
if pos + size > n:
|
|
765
|
+
return None
|
|
766
|
+
payload.extend(data[pos : pos + size])
|
|
767
|
+
pos += size
|
|
768
|
+
return None
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _gif_image_end(data: bytes, start: int, n: int) -> int | None:
|
|
772
|
+
"""Return the offset just past the image block starting at *start* (0x2C)."""
|
|
773
|
+
pos = start + 1
|
|
774
|
+
if pos + 9 > n:
|
|
775
|
+
return None
|
|
776
|
+
packed = data[pos + 8]
|
|
777
|
+
pos += 9
|
|
778
|
+
if packed & 0x80: # local color table present
|
|
779
|
+
pos += 3 * (1 << ((packed & 0x07) + 1))
|
|
780
|
+
if pos >= n:
|
|
781
|
+
return None
|
|
782
|
+
pos += 1 # LZW minimum code size
|
|
783
|
+
while pos < n:
|
|
784
|
+
size = data[pos]
|
|
785
|
+
pos += 1
|
|
786
|
+
if size == 0:
|
|
787
|
+
return pos
|
|
788
|
+
if pos + size > n:
|
|
789
|
+
return None
|
|
790
|
+
pos += size
|
|
791
|
+
return None
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
GIF_XMP_APPLICATION_ID = b"XMP DataXMP"
|
|
795
|
+
# Application extensions that control rendering rather than carrying provenance;
|
|
796
|
+
# dropping them would change animation looping or color, so they are kept.
|
|
797
|
+
_GIF_CONTROL_APPLICATION_IDS = (b"NETSCAPE2.0", b"ICCRGBG1012")
|
|
798
|
+
|
|
799
|
+
|
|
800
|
+
def inspect_gif(data: bytes) -> tuple[bool, bool, list[str]]:
|
|
801
|
+
"""Inspect GIF comment / application extensions and XMP payloads."""
|
|
802
|
+
findings: list[str] = []
|
|
803
|
+
has_c2pa = False
|
|
804
|
+
has_ai = False
|
|
805
|
+
if data[:6] not in GIF_SIGS:
|
|
806
|
+
return False, False, ["not a GIF"]
|
|
807
|
+
n = len(data)
|
|
808
|
+
pos = 6
|
|
809
|
+
if pos + 7 > n:
|
|
810
|
+
return False, False, ["truncated GIF header"]
|
|
811
|
+
packed = data[pos + 4]
|
|
812
|
+
pos += 7
|
|
813
|
+
if packed & 0x80: # global color table present
|
|
814
|
+
pos += 3 * (1 << ((packed & 0x07) + 1))
|
|
815
|
+
while pos < n:
|
|
816
|
+
block = data[pos]
|
|
817
|
+
if block == 0x3B: # trailer
|
|
818
|
+
break
|
|
819
|
+
if block == 0x21:
|
|
820
|
+
info = _gif_extension_info(data, pos, n)
|
|
821
|
+
if info is None:
|
|
822
|
+
findings.append("truncated GIF extension")
|
|
823
|
+
break
|
|
824
|
+
end, label, payload = info
|
|
825
|
+
if label == 0xFE:
|
|
826
|
+
findings.append("GIF comment extension present")
|
|
827
|
+
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
828
|
+
if hits:
|
|
829
|
+
has_ai = True
|
|
830
|
+
findings.append(f"GIF comment: {', '.join(hits[:6])}")
|
|
831
|
+
elif label == 0xFF:
|
|
832
|
+
if payload.startswith(GIF_XMP_APPLICATION_ID):
|
|
833
|
+
findings.append("GIF XMP application extension present")
|
|
834
|
+
hits = _contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
835
|
+
if hits:
|
|
836
|
+
has_ai = True
|
|
837
|
+
findings.append(f"GIF XMP: {', '.join(hits[:6])}")
|
|
838
|
+
elif any(m in payload[:11] for m in (b"c2pa", b"jumb", b"C2PA", b"JUMB")):
|
|
839
|
+
has_c2pa = True
|
|
840
|
+
findings.append("GIF application extension (possible C2PA)")
|
|
841
|
+
pos = end
|
|
842
|
+
elif block == 0x2C:
|
|
843
|
+
end = _gif_image_end(data, pos, n)
|
|
844
|
+
if end is None:
|
|
845
|
+
findings.append("truncated GIF image block")
|
|
846
|
+
break
|
|
847
|
+
pos = end
|
|
848
|
+
else:
|
|
849
|
+
pos += 1
|
|
850
|
+
whole = _contains_any(data, C2PA_MARKERS)
|
|
851
|
+
if whole and not has_c2pa:
|
|
852
|
+
has_c2pa = True
|
|
853
|
+
findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
|
|
854
|
+
if not findings:
|
|
855
|
+
findings.append("no GIF metadata extensions found")
|
|
856
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
def strip_gif(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
|
|
860
|
+
"""Rebuild the GIF without comment/XMP metadata, keeping control blocks.
|
|
861
|
+
|
|
862
|
+
Comment (0xFE) and XMP application (0xFF "XMP DataXMP") extensions carry
|
|
863
|
+
metadata and are dropped in strip-all mode; unknown application extensions
|
|
864
|
+
are dropped too. NETSCAPE2.0 (loop count) and ICC (color) extensions are
|
|
865
|
+
rendering control, not provenance, so they are preserved unless they
|
|
866
|
+
themselves contain AI/C2PA markers. Graphic-control, plain-text, and image
|
|
867
|
+
blocks are always copied verbatim.
|
|
868
|
+
"""
|
|
869
|
+
if data[:6] not in GIF_SIGS:
|
|
870
|
+
raise ValueError("not GIF")
|
|
871
|
+
actions: list[str] = []
|
|
872
|
+
out = bytearray(data[:6])
|
|
873
|
+
n = len(data)
|
|
874
|
+
pos = 6
|
|
875
|
+
if pos + 7 > n:
|
|
876
|
+
raise ValueError("truncated GIF header")
|
|
877
|
+
packed = data[pos + 4]
|
|
878
|
+
out.extend(data[pos : pos + 7])
|
|
879
|
+
pos += 7
|
|
880
|
+
if packed & 0x80: # global color table present
|
|
881
|
+
gct_size = 3 * (1 << ((packed & 0x07) + 1))
|
|
882
|
+
out.extend(data[pos : pos + gct_size])
|
|
883
|
+
pos += gct_size
|
|
884
|
+
|
|
885
|
+
while pos < n:
|
|
886
|
+
block = data[pos]
|
|
887
|
+
if block == 0x3B: # trailer
|
|
888
|
+
out.extend(data[pos:])
|
|
889
|
+
break
|
|
890
|
+
if block == 0x21:
|
|
891
|
+
info = _gif_extension_info(data, pos, n)
|
|
892
|
+
if info is None:
|
|
893
|
+
out.extend(data[pos:])
|
|
894
|
+
break
|
|
895
|
+
end, label, payload = info
|
|
896
|
+
drop = False
|
|
897
|
+
name = "extension"
|
|
898
|
+
if label == 0xFE:
|
|
899
|
+
name = "comment"
|
|
900
|
+
drop = strip_all_metadata or bool(
|
|
901
|
+
_contains_any(payload, AI_META_HINTS + C2PA_MARKERS)
|
|
902
|
+
)
|
|
903
|
+
elif label == 0xFF:
|
|
904
|
+
ident = payload[:11]
|
|
905
|
+
marker_hit = bool(_contains_any(payload, AI_META_HINTS + C2PA_MARKERS))
|
|
906
|
+
if ident == GIF_XMP_APPLICATION_ID:
|
|
907
|
+
name = "XMP application"
|
|
908
|
+
drop = strip_all_metadata or marker_hit
|
|
909
|
+
elif ident in _GIF_CONTROL_APPLICATION_IDS:
|
|
910
|
+
name = "control application"
|
|
911
|
+
drop = marker_hit # keep looping/ICC unless it carries markers
|
|
912
|
+
else:
|
|
913
|
+
name = "application"
|
|
914
|
+
drop = strip_all_metadata or marker_hit
|
|
915
|
+
if drop:
|
|
916
|
+
actions.append(f"drop GIF {name} extension")
|
|
917
|
+
else:
|
|
918
|
+
out.extend(data[pos:end])
|
|
919
|
+
pos = end
|
|
920
|
+
elif block == 0x2C:
|
|
921
|
+
end = _gif_image_end(data, pos, n)
|
|
922
|
+
if end is None:
|
|
923
|
+
out.extend(data[pos:])
|
|
924
|
+
break
|
|
925
|
+
out.extend(data[pos:end])
|
|
926
|
+
pos = end
|
|
927
|
+
else:
|
|
928
|
+
out.extend(data[pos : pos + 1])
|
|
929
|
+
pos += 1
|
|
930
|
+
|
|
931
|
+
if not actions:
|
|
932
|
+
actions.append("no GIF metadata blocks removed (already clean or none matched)")
|
|
933
|
+
return bytes(out), actions
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
# ---------------------------------------------------------------------------
|
|
937
|
+
# TIFF (classic + BigTIFF)
|
|
938
|
+
# ---------------------------------------------------------------------------
|
|
939
|
+
|
|
940
|
+
_TIFF_TYPE_SIZES = {1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8}
|
|
941
|
+
|
|
942
|
+
# Provenance / descriptive metadata tags stripped when cleaning TIFF.
|
|
943
|
+
_TIFF_META_TAG_NAMES = {
|
|
944
|
+
269: "DocumentName",
|
|
945
|
+
270: "ImageDescription",
|
|
946
|
+
271: "Make",
|
|
947
|
+
272: "Model",
|
|
948
|
+
305: "Software",
|
|
949
|
+
306: "DateTime",
|
|
950
|
+
315: "Artist",
|
|
951
|
+
316: "HostComputer",
|
|
952
|
+
33432: "Copyright",
|
|
953
|
+
40091: "XPTitle",
|
|
954
|
+
40092: "XPComment",
|
|
955
|
+
40093: "XPAuthor",
|
|
956
|
+
40094: "XPKeywords",
|
|
957
|
+
40095: "XPSubject",
|
|
958
|
+
700: "XMP",
|
|
959
|
+
33723: "IPTC/NAA",
|
|
960
|
+
34377: "Photoshop",
|
|
961
|
+
34665: "ExifIFD",
|
|
962
|
+
34853: "GPSInfo",
|
|
963
|
+
37500: "MakerNote",
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
_TIFF_DROP_TAGS = frozenset(_TIFF_META_TAG_NAMES)
|
|
967
|
+
|
|
968
|
+
# Structural tags that must survive even when their payload looks marker-ish
|
|
969
|
+
# (offset lists, color tables, compression tables, DNG core tags).
|
|
970
|
+
_TIFF_KEEP_TAGS = frozenset(
|
|
971
|
+
{
|
|
972
|
+
254,
|
|
973
|
+
255,
|
|
974
|
+
256,
|
|
975
|
+
257,
|
|
976
|
+
258,
|
|
977
|
+
259,
|
|
978
|
+
262,
|
|
979
|
+
263,
|
|
980
|
+
264,
|
|
981
|
+
265,
|
|
982
|
+
266,
|
|
983
|
+
273,
|
|
984
|
+
274,
|
|
985
|
+
277,
|
|
986
|
+
278,
|
|
987
|
+
279,
|
|
988
|
+
282,
|
|
989
|
+
283,
|
|
990
|
+
284,
|
|
991
|
+
285,
|
|
992
|
+
286,
|
|
993
|
+
287,
|
|
994
|
+
288,
|
|
995
|
+
289,
|
|
996
|
+
290,
|
|
997
|
+
291,
|
|
998
|
+
292,
|
|
999
|
+
293,
|
|
1000
|
+
294,
|
|
1001
|
+
295,
|
|
1002
|
+
296,
|
|
1003
|
+
297,
|
|
1004
|
+
301,
|
|
1005
|
+
302,
|
|
1006
|
+
304,
|
|
1007
|
+
320,
|
|
1008
|
+
321,
|
|
1009
|
+
322,
|
|
1010
|
+
323,
|
|
1011
|
+
324,
|
|
1012
|
+
325,
|
|
1013
|
+
326,
|
|
1014
|
+
327,
|
|
1015
|
+
328,
|
|
1016
|
+
329,
|
|
1017
|
+
330,
|
|
1018
|
+
331,
|
|
1019
|
+
332,
|
|
1020
|
+
333,
|
|
1021
|
+
334,
|
|
1022
|
+
336,
|
|
1023
|
+
338,
|
|
1024
|
+
339,
|
|
1025
|
+
340,
|
|
1026
|
+
341,
|
|
1027
|
+
342,
|
|
1028
|
+
343,
|
|
1029
|
+
344,
|
|
1030
|
+
345,
|
|
1031
|
+
346,
|
|
1032
|
+
347,
|
|
1033
|
+
512,
|
|
1034
|
+
513,
|
|
1035
|
+
514,
|
|
1036
|
+
515,
|
|
1037
|
+
516,
|
|
1038
|
+
517,
|
|
1039
|
+
518,
|
|
1040
|
+
519,
|
|
1041
|
+
520,
|
|
1042
|
+
521,
|
|
1043
|
+
529,
|
|
1044
|
+
530,
|
|
1045
|
+
531,
|
|
1046
|
+
532,
|
|
1047
|
+
533,
|
|
1048
|
+
33421,
|
|
1049
|
+
33422,
|
|
1050
|
+
33423,
|
|
1051
|
+
34675,
|
|
1052
|
+
34676,
|
|
1053
|
+
*range(50706, 50742), # DNG structural tags
|
|
1054
|
+
}
|
|
1055
|
+
)
|
|
1056
|
+
|
|
1057
|
+
MAX_TIFF_IFDS = 4096
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def _tiff_layout(data: bytes) -> tuple[str, bool] | None:
|
|
1061
|
+
"""Return (byte_order, bigtiff) for a classic or BigTIFF, else None."""
|
|
1062
|
+
if data[:2] == b"II" and data[2:4] == b"*\x00":
|
|
1063
|
+
return "<", False
|
|
1064
|
+
if data[:2] == b"MM" and data[2:4] == b"\x00*":
|
|
1065
|
+
return ">", False
|
|
1066
|
+
if data[:2] == b"II" and data[2:4] == b"+\x00":
|
|
1067
|
+
return "<", True
|
|
1068
|
+
if data[:2] == b"MM" and data[2:4] == b"\x00+":
|
|
1069
|
+
return ">", True
|
|
1070
|
+
return None
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
def _parse_tiff_ifds(data: bytes) -> tuple[str, bool, dict[int, dict[str, Any]]]:
|
|
1074
|
+
"""Parse every reachable TIFF IFD (classic 12-byte or BigTIFF 20-byte entries).
|
|
1075
|
+
|
|
1076
|
+
Returns (byte_order, bigtiff, {offset: {count, entries, next, block_len}}).
|
|
1077
|
+
Each entry is {tag, type, count, value (inline field bytes), byte_size,
|
|
1078
|
+
value_offset}; value_offset is None when the value is inline. Cycle-
|
|
1079
|
+
protected with a hard cap on the number of IFDs.
|
|
1080
|
+
"""
|
|
1081
|
+
layout = _tiff_layout(data)
|
|
1082
|
+
if layout is None:
|
|
1083
|
+
raise ValueError("not a TIFF")
|
|
1084
|
+
bo, bigtiff = layout
|
|
1085
|
+
n = len(data)
|
|
1086
|
+
count_len = 8 if bigtiff else 2
|
|
1087
|
+
off_len = 8 if bigtiff else 4
|
|
1088
|
+
entry_size = 20 if bigtiff else 12
|
|
1089
|
+
header_size = 16 if bigtiff else 8
|
|
1090
|
+
count_fmt = bo + ("Q" if bigtiff else "H")
|
|
1091
|
+
off_fmt = bo + ("Q" if bigtiff else "I")
|
|
1092
|
+
if n < header_size:
|
|
1093
|
+
return bo, bigtiff, {}
|
|
1094
|
+
first = struct.unpack(off_fmt, data[header_size - off_len : header_size])[0]
|
|
1095
|
+
if first == 0 or first + count_len > n:
|
|
1096
|
+
return bo, bigtiff, {}
|
|
1097
|
+
ifds: dict[int, dict[str, Any]] = {}
|
|
1098
|
+
seen: set[int] = set()
|
|
1099
|
+
todo = [first]
|
|
1100
|
+
while todo and len(ifds) < MAX_TIFF_IFDS:
|
|
1101
|
+
off = todo.pop()
|
|
1102
|
+
if off in seen or off + count_len > n:
|
|
1103
|
+
continue
|
|
1104
|
+
seen.add(off)
|
|
1105
|
+
count = struct.unpack(count_fmt, data[off : off + count_len])[0]
|
|
1106
|
+
block_len = count_len + count * entry_size
|
|
1107
|
+
entries: list[dict[str, Any]] = []
|
|
1108
|
+
if off + block_len + off_len <= n:
|
|
1109
|
+
next_ptr = struct.unpack(off_fmt, data[off + block_len : off + block_len + off_len])[0]
|
|
1110
|
+
else:
|
|
1111
|
+
block_len = max(0, n - off - off_len)
|
|
1112
|
+
next_ptr = 0
|
|
1113
|
+
for i in range(count):
|
|
1114
|
+
e = off + count_len + i * entry_size
|
|
1115
|
+
if e + entry_size > n:
|
|
1116
|
+
break
|
|
1117
|
+
tag = struct.unpack(bo + "H", data[e : e + 2])[0]
|
|
1118
|
+
ftype = struct.unpack(bo + "H", data[e + 2 : e + 4])[0]
|
|
1119
|
+
fcount = struct.unpack(off_fmt, data[e + 4 : e + 4 + off_len])[0]
|
|
1120
|
+
value = data[e + 4 + off_len : e + entry_size]
|
|
1121
|
+
byte_size = fcount * _TIFF_TYPE_SIZES.get(ftype, 1)
|
|
1122
|
+
value_offset = None
|
|
1123
|
+
if byte_size > len(value):
|
|
1124
|
+
value_offset = struct.unpack(off_fmt, value[:off_len])[0]
|
|
1125
|
+
entries.append(
|
|
1126
|
+
{
|
|
1127
|
+
"tag": tag,
|
|
1128
|
+
"type": ftype,
|
|
1129
|
+
"count": fcount,
|
|
1130
|
+
"value": value,
|
|
1131
|
+
"byte_size": byte_size,
|
|
1132
|
+
"value_offset": value_offset,
|
|
1133
|
+
}
|
|
1134
|
+
)
|
|
1135
|
+
ifds[off] = {
|
|
1136
|
+
"count": count,
|
|
1137
|
+
"entries": entries,
|
|
1138
|
+
"next": next_ptr,
|
|
1139
|
+
"block_len": block_len,
|
|
1140
|
+
}
|
|
1141
|
+
if next_ptr:
|
|
1142
|
+
todo.append(next_ptr)
|
|
1143
|
+
for ent in entries:
|
|
1144
|
+
if ent["tag"] in (34665, 34853, 40965):
|
|
1145
|
+
ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
|
|
1146
|
+
if ptr:
|
|
1147
|
+
todo.append(ptr)
|
|
1148
|
+
return bo, bigtiff, ifds
|
|
1149
|
+
|
|
1150
|
+
|
|
1151
|
+
def _tiff_entry_payload(data: bytes, ent: dict[str, Any]) -> bytes | None:
|
|
1152
|
+
if ent["value_offset"] is None:
|
|
1153
|
+
return ent["value"][: ent["byte_size"]] if ent["byte_size"] <= len(ent["value"]) else None
|
|
1154
|
+
vo = ent["value_offset"]
|
|
1155
|
+
return data[vo : vo + ent["byte_size"]]
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def inspect_tiff(data: bytes) -> tuple[bool, bool, list[str]]:
|
|
1159
|
+
"""Walk the IFD chains (classic or BigTIFF) and report metadata tags."""
|
|
1160
|
+
findings: list[str] = []
|
|
1161
|
+
has_c2pa = False
|
|
1162
|
+
has_ai = False
|
|
1163
|
+
try:
|
|
1164
|
+
_bo, _big, ifds = _parse_tiff_ifds(data)
|
|
1165
|
+
except ValueError:
|
|
1166
|
+
return False, False, ["not a valid TIFF"]
|
|
1167
|
+
if not ifds:
|
|
1168
|
+
return False, False, ["TIFF with no image file directories"]
|
|
1169
|
+
for _off, ifd in sorted(ifds.items()):
|
|
1170
|
+
for ent in ifd["entries"]:
|
|
1171
|
+
tag = ent["tag"]
|
|
1172
|
+
payload = _tiff_entry_payload(data, ent)
|
|
1173
|
+
hits = _contains_any(payload or b"", AI_META_HINTS + C2PA_MARKERS)
|
|
1174
|
+
if hits:
|
|
1175
|
+
if any(
|
|
1176
|
+
h.lower() in ("c2pa", "contentcredentials", "jumb", "contentauth") for h in hits
|
|
1177
|
+
):
|
|
1178
|
+
has_c2pa = True
|
|
1179
|
+
has_ai = True
|
|
1180
|
+
findings.append(f"TIFF tag {tag}: {', '.join(hits[:6])}")
|
|
1181
|
+
name = _TIFF_META_TAG_NAMES.get(tag)
|
|
1182
|
+
if name:
|
|
1183
|
+
label = "sub-IFD" if tag in (34665, 34853, 40965) else "tag"
|
|
1184
|
+
findings.append(f"TIFF {label} {tag} ({name}) present")
|
|
1185
|
+
whole = _contains_any(data, C2PA_MARKERS)
|
|
1186
|
+
if whole and not has_c2pa:
|
|
1187
|
+
has_c2pa = True
|
|
1188
|
+
findings.append(f"byte-scan C2PA markers: {', '.join(whole[:6])}")
|
|
1189
|
+
if not findings:
|
|
1190
|
+
findings.append("no TIFF metadata tags found")
|
|
1191
|
+
return has_c2pa, has_ai or has_c2pa, findings
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
def _collect_tiff_sub_ifd_drops(
|
|
1195
|
+
off_fmt: str,
|
|
1196
|
+
off_len: int,
|
|
1197
|
+
ifds: dict[int, dict[str, Any]],
|
|
1198
|
+
data_len: int,
|
|
1199
|
+
ptr: int,
|
|
1200
|
+
drop_ranges: list[tuple[int, int]],
|
|
1201
|
+
drop_ifd_ranges: list[tuple[int, int]],
|
|
1202
|
+
seen: set[int],
|
|
1203
|
+
) -> None:
|
|
1204
|
+
"""Record the region and value payloads of a dropped sub-IFD chain."""
|
|
1205
|
+
sub = ifds.get(ptr)
|
|
1206
|
+
if sub is None or ptr in seen:
|
|
1207
|
+
return
|
|
1208
|
+
seen.add(ptr)
|
|
1209
|
+
drop_ifd_ranges.append((ptr, min(ptr + sub["block_len"] + off_len, data_len)))
|
|
1210
|
+
for ent in sub["entries"]:
|
|
1211
|
+
if ent["tag"] in (34665, 34853):
|
|
1212
|
+
p2 = struct.unpack(off_fmt, ent["value"][:off_len])[0]
|
|
1213
|
+
if p2:
|
|
1214
|
+
_collect_tiff_sub_ifd_drops(
|
|
1215
|
+
off_fmt, off_len, ifds, data_len, p2, drop_ranges, drop_ifd_ranges, seen
|
|
1216
|
+
)
|
|
1217
|
+
elif ent["value_offset"] is not None:
|
|
1218
|
+
vo, vs = ent["value_offset"], ent["byte_size"]
|
|
1219
|
+
if vo + vs <= data_len:
|
|
1220
|
+
drop_ranges.append((vo, vo + vs))
|
|
1221
|
+
|
|
1222
|
+
|
|
1223
|
+
def strip_tiff(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
|
|
1224
|
+
"""Drop TIFF metadata tags (classic or BigTIFF) without moving referenced data.
|
|
1225
|
+
|
|
1226
|
+
Each IFD entry region is patched in place — kept entries copied verbatim,
|
|
1227
|
+
dropped entries removed, the block zero-padded to its original length — so
|
|
1228
|
+
every offset (strip/tile offsets, next-IFD pointers, color tables) stays
|
|
1229
|
+
valid. Payloads of dropped tags and orphaned sub-IFD chains are zeroed
|
|
1230
|
+
unless a kept entry still references them.
|
|
1231
|
+
"""
|
|
1232
|
+
bo, bigtiff, ifds = _parse_tiff_ifds(data)
|
|
1233
|
+
if not ifds:
|
|
1234
|
+
raise ValueError("not a valid TIFF (no image file directories)")
|
|
1235
|
+
n = len(data)
|
|
1236
|
+
off_len = 8 if bigtiff else 4
|
|
1237
|
+
off_fmt = bo + ("Q" if bigtiff else "I")
|
|
1238
|
+
count_fmt = bo + ("Q" if bigtiff else "H")
|
|
1239
|
+
actions: list[str] = []
|
|
1240
|
+
kept: dict[int, list[dict[str, Any]]] = {}
|
|
1241
|
+
drop_ranges: list[tuple[int, int]] = []
|
|
1242
|
+
drop_ifd_ranges: list[tuple[int, int]] = []
|
|
1243
|
+
|
|
1244
|
+
for off, ifd in sorted(ifds.items()):
|
|
1245
|
+
keep_here: list[dict[str, Any]] = []
|
|
1246
|
+
for ent in ifd["entries"]:
|
|
1247
|
+
tag = ent["tag"]
|
|
1248
|
+
payload = _tiff_entry_payload(data, ent)
|
|
1249
|
+
marker_hit = bool(_contains_any(payload or b"", AI_META_HINTS + C2PA_MARKERS))
|
|
1250
|
+
if tag in (34665, 34853):
|
|
1251
|
+
ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
|
|
1252
|
+
sub = ifds.get(ptr)
|
|
1253
|
+
if sub is not None:
|
|
1254
|
+
sub_blob = data[ptr : min(ptr + sub["block_len"] + off_len, n)]
|
|
1255
|
+
marker_hit = marker_hit or bool(
|
|
1256
|
+
_contains_any(sub_blob, AI_META_HINTS + C2PA_MARKERS)
|
|
1257
|
+
)
|
|
1258
|
+
for sent in sub["entries"]:
|
|
1259
|
+
s_payload = _tiff_entry_payload(data, sent)
|
|
1260
|
+
if _contains_any(s_payload or b"", AI_META_HINTS + C2PA_MARKERS):
|
|
1261
|
+
marker_hit = True
|
|
1262
|
+
break
|
|
1263
|
+
drop = False
|
|
1264
|
+
if tag in _TIFF_DROP_TAGS:
|
|
1265
|
+
drop = strip_all_metadata or marker_hit
|
|
1266
|
+
elif marker_hit and tag not in _TIFF_KEEP_TAGS:
|
|
1267
|
+
drop = True
|
|
1268
|
+
if not drop:
|
|
1269
|
+
keep_here.append(ent)
|
|
1270
|
+
continue
|
|
1271
|
+
name = _TIFF_META_TAG_NAMES.get(tag)
|
|
1272
|
+
actions.append(
|
|
1273
|
+
f"drop TIFF tag {tag} ({name})" if name else f"drop TIFF tag {tag} (AI markers)"
|
|
1274
|
+
)
|
|
1275
|
+
if tag in (34665, 34853):
|
|
1276
|
+
ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
|
|
1277
|
+
_collect_tiff_sub_ifd_drops(
|
|
1278
|
+
off_fmt, off_len, ifds, n, ptr, drop_ranges, drop_ifd_ranges, set()
|
|
1279
|
+
)
|
|
1280
|
+
elif ent["value_offset"] is not None and ent["value_offset"] + ent["byte_size"] <= n:
|
|
1281
|
+
drop_ranges.append((ent["value_offset"], ent["value_offset"] + ent["byte_size"]))
|
|
1282
|
+
kept[off] = keep_here
|
|
1283
|
+
|
|
1284
|
+
# Ranges still referenced from the root through kept entries must never be
|
|
1285
|
+
# zeroed. An orphaned sub-IFD (its pointer dropped) contributes nothing.
|
|
1286
|
+
reachable: set[int] = set()
|
|
1287
|
+
|
|
1288
|
+
def _mark_reachable(off: int) -> None:
|
|
1289
|
+
if off in reachable or off not in ifds:
|
|
1290
|
+
return
|
|
1291
|
+
reachable.add(off)
|
|
1292
|
+
for ent in kept.get(off, []):
|
|
1293
|
+
if ent["tag"] in (34665, 34853, 40965):
|
|
1294
|
+
_mark_reachable(struct.unpack(off_fmt, ent["value"][:off_len])[0])
|
|
1295
|
+
if ifds[off]["next"]:
|
|
1296
|
+
_mark_reachable(ifds[off]["next"])
|
|
1297
|
+
|
|
1298
|
+
root = (
|
|
1299
|
+
struct.unpack(off_fmt, data[16 - off_len : 16])[0]
|
|
1300
|
+
if bigtiff
|
|
1301
|
+
else struct.unpack(off_fmt, data[8 - off_len : 8])[0]
|
|
1302
|
+
)
|
|
1303
|
+
_mark_reachable(root)
|
|
1304
|
+
|
|
1305
|
+
referenced: list[tuple[int, int]] = []
|
|
1306
|
+
for off in sorted(reachable):
|
|
1307
|
+
for ent in kept.get(off, []):
|
|
1308
|
+
if ent["value_offset"] is not None and ent["value_offset"] + ent["byte_size"] <= n:
|
|
1309
|
+
referenced.append((ent["value_offset"], ent["value_offset"] + ent["byte_size"]))
|
|
1310
|
+
if ent["tag"] in (34665, 34853, 40965):
|
|
1311
|
+
ptr = struct.unpack(off_fmt, ent["value"][:off_len])[0]
|
|
1312
|
+
sub = ifds.get(ptr)
|
|
1313
|
+
if sub is not None:
|
|
1314
|
+
referenced.append((ptr, min(ptr + sub["block_len"] + off_len, n)))
|
|
1315
|
+
|
|
1316
|
+
def _covered(ranges: list[tuple[int, int]], start: int, end: int) -> bool:
|
|
1317
|
+
return any(s <= start and end <= e for s, e in ranges)
|
|
1318
|
+
|
|
1319
|
+
zero: list[tuple[int, int]] = []
|
|
1320
|
+
for s, e in drop_ranges:
|
|
1321
|
+
if not _covered(referenced, s, e):
|
|
1322
|
+
zero.append((s, e))
|
|
1323
|
+
for s, e in drop_ifd_ranges:
|
|
1324
|
+
if not _covered(referenced, s, e):
|
|
1325
|
+
zero.append((s, e))
|
|
1326
|
+
|
|
1327
|
+
# Patch IFD entry regions in place, then zero dropped payloads.
|
|
1328
|
+
out = bytearray(data)
|
|
1329
|
+
for off, ifd in sorted(ifds.items()):
|
|
1330
|
+
entries = kept.get(off, [])
|
|
1331
|
+
block = bytearray()
|
|
1332
|
+
block.extend(struct.pack(count_fmt, len(entries)))
|
|
1333
|
+
for ent in entries:
|
|
1334
|
+
block.extend(struct.pack(bo + "H", ent["tag"]))
|
|
1335
|
+
block.extend(struct.pack(bo + "H", ent["type"]))
|
|
1336
|
+
block.extend(struct.pack(off_fmt, ent["count"]))
|
|
1337
|
+
block.extend(ent["value"])
|
|
1338
|
+
block.extend(struct.pack(off_fmt, ifd["next"]))
|
|
1339
|
+
region_len = ifd["block_len"] + off_len
|
|
1340
|
+
if off + region_len <= n:
|
|
1341
|
+
if len(block) > region_len:
|
|
1342
|
+
block = block[:region_len]
|
|
1343
|
+
out[off : off + region_len] = block + b"\x00" * (region_len - len(block))
|
|
1344
|
+
for s, e in zero:
|
|
1345
|
+
out[s:e] = b"\x00" * (e - s)
|
|
1346
|
+
|
|
1347
|
+
if not actions:
|
|
1348
|
+
actions.append("no TIFF metadata tags removed (already clean or none matched)")
|
|
1349
|
+
return bytes(out), actions
|
|
1350
|
+
|
|
1351
|
+
|
|
1352
|
+
def run_optional_tools(path: Path) -> dict[str, Any]:
|
|
1353
|
+
tools: dict[str, Any] = {}
|
|
1354
|
+
c2patool = which("c2patool")
|
|
1355
|
+
if c2patool:
|
|
1356
|
+
try:
|
|
1357
|
+
r = subprocess.run(
|
|
1358
|
+
[c2patool, safe_arg(str(path))],
|
|
1359
|
+
capture_output=True,
|
|
1360
|
+
text=True,
|
|
1361
|
+
timeout=30,
|
|
1362
|
+
preexec_fn=subprocess_preexec_fn,
|
|
1363
|
+
check=False,
|
|
1364
|
+
creationflags=subprocess_creationflags,
|
|
1365
|
+
)
|
|
1366
|
+
out = (r.stdout or "") + (r.stderr or "")
|
|
1367
|
+
low = out.lower()
|
|
1368
|
+
# Negative markers must veto every positive branch, so the
|
|
1369
|
+
# positive alternatives are parenthesised: c2patool reports a
|
|
1370
|
+
# missing manifest as "Error: No claim found", which contains
|
|
1371
|
+
# the substring "claim" and would otherwise read as a hit.
|
|
1372
|
+
no_manifest = "no claim" in low or "no jumbf" in low
|
|
1373
|
+
has_manifest = (
|
|
1374
|
+
"claim" in low or "c2pa" in low or "manifest" in low
|
|
1375
|
+
) and not no_manifest
|
|
1376
|
+
# c2patool exits non-zero for a missing manifest too, so the exit
|
|
1377
|
+
# code alone cannot separate "asset is clean" from "the probe
|
|
1378
|
+
# never ran". Treat the run as conclusive only when it either
|
|
1379
|
+
# found a manifest or said in so many words that there is none.
|
|
1380
|
+
# Anything else -- a crash before main(), a kill, an unrecognized
|
|
1381
|
+
# error -- leaves the C2PA question unanswered, and callers must
|
|
1382
|
+
# not read that as a negative.
|
|
1383
|
+
conclusive = has_manifest or no_manifest
|
|
1384
|
+
entry: dict[str, Any] = {
|
|
1385
|
+
"available": True,
|
|
1386
|
+
"returncode": r.returncode,
|
|
1387
|
+
"snippet": out[:2000],
|
|
1388
|
+
"has_manifest": has_manifest,
|
|
1389
|
+
"ok": conclusive,
|
|
1390
|
+
}
|
|
1391
|
+
if not conclusive:
|
|
1392
|
+
entry["error"] = f"exit {r.returncode}, unrecognized output"
|
|
1393
|
+
tools["c2patool"] = entry
|
|
1394
|
+
except Exception as e:
|
|
1395
|
+
tools["c2patool"] = {
|
|
1396
|
+
"available": True,
|
|
1397
|
+
"ok": False,
|
|
1398
|
+
"has_manifest": False,
|
|
1399
|
+
"error": str(e),
|
|
1400
|
+
}
|
|
1401
|
+
else:
|
|
1402
|
+
tools["c2patool"] = {"available": False}
|
|
1403
|
+
|
|
1404
|
+
exiftool = which("exiftool")
|
|
1405
|
+
if exiftool:
|
|
1406
|
+
try:
|
|
1407
|
+
r = subprocess.run(
|
|
1408
|
+
[exiftool, "-G1", "-a", "-s", safe_arg(str(path))],
|
|
1409
|
+
capture_output=True,
|
|
1410
|
+
text=True,
|
|
1411
|
+
timeout=30,
|
|
1412
|
+
preexec_fn=subprocess_preexec_fn,
|
|
1413
|
+
check=False,
|
|
1414
|
+
creationflags=subprocess_creationflags,
|
|
1415
|
+
)
|
|
1416
|
+
out = r.stdout or ""
|
|
1417
|
+
interesting = [
|
|
1418
|
+
line
|
|
1419
|
+
for line in out.splitlines()
|
|
1420
|
+
if re.search(
|
|
1421
|
+
r"c2pa|content.?credential|AIGC|digitalSource|XMP|EXIF|IPTC|jumb",
|
|
1422
|
+
line,
|
|
1423
|
+
re.I,
|
|
1424
|
+
)
|
|
1425
|
+
]
|
|
1426
|
+
tools["exiftool"] = {
|
|
1427
|
+
"available": True,
|
|
1428
|
+
"interesting_lines": interesting[:50],
|
|
1429
|
+
}
|
|
1430
|
+
except Exception as e:
|
|
1431
|
+
tools["exiftool"] = {"available": True, "error": str(e)}
|
|
1432
|
+
else:
|
|
1433
|
+
tools["exiftool"] = {"available": False}
|
|
1434
|
+
return tools
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def _synthid_score_http(
|
|
1438
|
+
path: Path, base_url: str, api_key: str, timeout: float
|
|
1439
|
+
) -> dict[str, Any] | None:
|
|
1440
|
+
"""Score *path* via the HTTP sidecar (synthid_score_server.py)."""
|
|
1441
|
+
try:
|
|
1442
|
+
data = path.read_bytes()
|
|
1443
|
+
except OSError as e:
|
|
1444
|
+
return {"available": False, "error": f"cannot read {path}: {e}"}
|
|
1445
|
+
body = json.dumps({"file": base64.b64encode(data).decode("ascii")}).encode("utf-8")
|
|
1446
|
+
headers = {"Content-Type": "application/json"}
|
|
1447
|
+
if api_key:
|
|
1448
|
+
headers["Authorization"] = f"Bearer {api_key}"
|
|
1449
|
+
if urlparse(base_url).scheme not in ("http", "https"):
|
|
1450
|
+
return {"available": False, "error": f"refusing non-http(s) scorer endpoint: {base_url}"}
|
|
1451
|
+
# S310: URL scheme is restricted to http/https just above.
|
|
1452
|
+
req = urllib.request.Request( # noqa: S310
|
|
1453
|
+
base_url.rstrip("/") + "/score",
|
|
1454
|
+
data=body,
|
|
1455
|
+
headers=headers,
|
|
1456
|
+
method="POST",
|
|
1457
|
+
)
|
|
1458
|
+
try:
|
|
1459
|
+
with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310
|
|
1460
|
+
payload = json.loads(resp.read().decode("utf-8"))
|
|
1461
|
+
except (
|
|
1462
|
+
urllib.error.HTTPError,
|
|
1463
|
+
urllib.error.URLError,
|
|
1464
|
+
TimeoutError,
|
|
1465
|
+
OSError,
|
|
1466
|
+
json.JSONDecodeError,
|
|
1467
|
+
) as e:
|
|
1468
|
+
return {"available": False, "error": f"SynthID scorer sidecar unreachable: {e}"}
|
|
1469
|
+
if not isinstance(payload, dict):
|
|
1470
|
+
return {"available": False, "error": "bad scorer sidecar response"}
|
|
1471
|
+
return payload
|
|
1472
|
+
|
|
1473
|
+
|
|
1474
|
+
def _synthid_python(upstream: Path) -> str:
|
|
1475
|
+
"""Prefer the checkout venv so the scorer deps (cv2, sklearn) are importable."""
|
|
1476
|
+
if os.name == "nt":
|
|
1477
|
+
venv = upstream / ".venv" / "Scripts" / "python.exe"
|
|
1478
|
+
else:
|
|
1479
|
+
venv = upstream / ".venv" / "bin" / "python"
|
|
1480
|
+
if venv.is_file():
|
|
1481
|
+
return str(venv)
|
|
1482
|
+
return sys.executable
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def run_synthid_score(
|
|
1486
|
+
path: Path,
|
|
1487
|
+
upstream_dir: str | None = None,
|
|
1488
|
+
) -> dict[str, Any] | None:
|
|
1489
|
+
"""Run the optional reverse-SynthID scorer.
|
|
1490
|
+
|
|
1491
|
+
Uses the HTTP sidecar when WATERMARKS_SYNTHID_SCORER_URL is set,
|
|
1492
|
+
otherwise a subprocess against a local checkout. Returns None when the
|
|
1493
|
+
scorer is not configured; a dict with "available": False and an "error"
|
|
1494
|
+
when it is configured but unavailable (e.g. exit 3), so callers can
|
|
1495
|
+
distinguish "not scored" from "scored and clean".
|
|
1496
|
+
"""
|
|
1497
|
+
scorer_url = os.environ.get("WATERMARKS_SYNTHID_SCORER_URL", "").strip()
|
|
1498
|
+
if scorer_url:
|
|
1499
|
+
api_key = os.environ.get("WATERMARKS_SYNTHID_SCORER_API_KEY", "").strip()
|
|
1500
|
+
try:
|
|
1501
|
+
timeout = float(os.environ.get("WATERMARKS_SYNTHID_SCORER_TIMEOUT", "60"))
|
|
1502
|
+
except ValueError:
|
|
1503
|
+
timeout = DEFAULT_SYNTHID_SCORER_TIMEOUT
|
|
1504
|
+
return _synthid_score_http(path, scorer_url, api_key, timeout)
|
|
1505
|
+
if upstream_dir is None:
|
|
1506
|
+
upstream_dir = os.environ.get("REVERSE_SYNTHID_DIR")
|
|
1507
|
+
if not upstream_dir:
|
|
1508
|
+
return None
|
|
1509
|
+
|
|
1510
|
+
script = SCRIPTS_DIR / "score_synthid.py"
|
|
1511
|
+
cmd = [
|
|
1512
|
+
_synthid_python(Path(upstream_dir)),
|
|
1513
|
+
str(script),
|
|
1514
|
+
str(path),
|
|
1515
|
+
"--upstream-dir",
|
|
1516
|
+
str(upstream_dir),
|
|
1517
|
+
"--json",
|
|
1518
|
+
]
|
|
1519
|
+
try:
|
|
1520
|
+
r = subprocess.run(
|
|
1521
|
+
cmd,
|
|
1522
|
+
capture_output=True,
|
|
1523
|
+
text=True,
|
|
1524
|
+
timeout=180,
|
|
1525
|
+
preexec_fn=subprocess_preexec_fn,
|
|
1526
|
+
check=False,
|
|
1527
|
+
creationflags=subprocess_creationflags,
|
|
1528
|
+
)
|
|
1529
|
+
except Exception as e:
|
|
1530
|
+
return {"available": False, "error": str(e)}
|
|
1531
|
+
|
|
1532
|
+
if r.returncode == 3:
|
|
1533
|
+
return {
|
|
1534
|
+
"available": False,
|
|
1535
|
+
"error": (r.stderr or "SynthID scorer unavailable (exit 3)").strip()[:2000],
|
|
1536
|
+
}
|
|
1537
|
+
if r.returncode != 0:
|
|
1538
|
+
return {"available": False, "error": (r.stderr or "").strip()[:2000]}
|
|
1539
|
+
try:
|
|
1540
|
+
return json.loads(r.stdout or "{}")
|
|
1541
|
+
except json.JSONDecodeError as e:
|
|
1542
|
+
return {"available": False, "error": f"bad scorer JSON: {e}"}
|
|
1543
|
+
|
|
1544
|
+
|
|
1545
|
+
def _ctrlregen_python(upstream: Path) -> str:
|
|
1546
|
+
"""Prefer the checkout venv so torch/diffusers are importable."""
|
|
1547
|
+
if os.name == "nt":
|
|
1548
|
+
venv = upstream / ".venv" / "Scripts" / "python.exe"
|
|
1549
|
+
else:
|
|
1550
|
+
venv = upstream / ".venv" / "bin" / "python"
|
|
1551
|
+
if venv.is_file():
|
|
1552
|
+
return str(venv)
|
|
1553
|
+
return sys.executable
|
|
1554
|
+
|
|
1555
|
+
|
|
1556
|
+
def _markdiffusion_python(upstream: Path | None) -> str:
|
|
1557
|
+
"""Prefer the bootstrap venv so torch/diffusers/markdiffusion importable."""
|
|
1558
|
+
if upstream is not None:
|
|
1559
|
+
if os.name == "nt":
|
|
1560
|
+
venv = upstream / ".venv" / "Scripts" / "python.exe"
|
|
1561
|
+
else:
|
|
1562
|
+
venv = upstream / ".venv" / "bin" / "python"
|
|
1563
|
+
if venv.is_file():
|
|
1564
|
+
return str(venv)
|
|
1565
|
+
return sys.executable
|
|
1566
|
+
|
|
1567
|
+
|
|
1568
|
+
def run_markdiffusion_purify(
|
|
1569
|
+
path: Path,
|
|
1570
|
+
output: Path,
|
|
1571
|
+
*,
|
|
1572
|
+
upstream_dir: str | None = None,
|
|
1573
|
+
strength: float = 0.3,
|
|
1574
|
+
model: str | None = None,
|
|
1575
|
+
size: int = 512,
|
|
1576
|
+
steps: int = 50,
|
|
1577
|
+
device: str | None = None,
|
|
1578
|
+
timeout: int = 3600,
|
|
1579
|
+
) -> dict[str, Any]:
|
|
1580
|
+
"""Run the optional MarkDiffusion DiffusionPurification remover in a subprocess.
|
|
1581
|
+
|
|
1582
|
+
Returns ``{"available": False, "error": ...}`` when the backend is not
|
|
1583
|
+
configured, its dependencies are missing, or it fails at runtime; a
|
|
1584
|
+
successful run is ``{"available": True, ...}``.
|
|
1585
|
+
"""
|
|
1586
|
+
if upstream_dir is None:
|
|
1587
|
+
upstream_dir = os.environ.get("MARKDIFFUSION_DIR")
|
|
1588
|
+
|
|
1589
|
+
upstream = Path(upstream_dir).expanduser().resolve() if upstream_dir else None
|
|
1590
|
+
if upstream is not None and not upstream.is_dir():
|
|
1591
|
+
return {
|
|
1592
|
+
"available": False,
|
|
1593
|
+
"error": f"MarkDiffusion dir not found: {upstream}",
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
script = SCRIPTS_DIR / "markdiffusion_harness.py"
|
|
1597
|
+
cmd = [
|
|
1598
|
+
_markdiffusion_python(upstream),
|
|
1599
|
+
str(script),
|
|
1600
|
+
"purify",
|
|
1601
|
+
str(path),
|
|
1602
|
+
"-o",
|
|
1603
|
+
str(output),
|
|
1604
|
+
"--purification-strength",
|
|
1605
|
+
str(strength),
|
|
1606
|
+
"--size",
|
|
1607
|
+
str(size),
|
|
1608
|
+
"--steps",
|
|
1609
|
+
str(steps),
|
|
1610
|
+
"--json",
|
|
1611
|
+
]
|
|
1612
|
+
if upstream is not None:
|
|
1613
|
+
cmd += ["--upstream-dir", str(upstream)]
|
|
1614
|
+
if model:
|
|
1615
|
+
cmd += ["--model", str(model)]
|
|
1616
|
+
if device:
|
|
1617
|
+
cmd += ["--device", str(device)]
|
|
1618
|
+
|
|
1619
|
+
try:
|
|
1620
|
+
r = subprocess.run(
|
|
1621
|
+
cmd,
|
|
1622
|
+
capture_output=True,
|
|
1623
|
+
text=True,
|
|
1624
|
+
timeout=timeout,
|
|
1625
|
+
preexec_fn=ctrlregen_subprocess_preexec_fn,
|
|
1626
|
+
check=False,
|
|
1627
|
+
creationflags=subprocess_creationflags,
|
|
1628
|
+
)
|
|
1629
|
+
except subprocess.TimeoutExpired:
|
|
1630
|
+
return {
|
|
1631
|
+
"available": False,
|
|
1632
|
+
"error": f"DiffusionPurification timed out after {timeout}s",
|
|
1633
|
+
}
|
|
1634
|
+
except Exception as e:
|
|
1635
|
+
return {"available": False, "error": str(e)}
|
|
1636
|
+
|
|
1637
|
+
if r.returncode != 0:
|
|
1638
|
+
return {"available": False, "error": (r.stderr or "").strip()[:2000]}
|
|
1639
|
+
try:
|
|
1640
|
+
payload = json.loads(r.stdout or "{}")
|
|
1641
|
+
except json.JSONDecodeError as e:
|
|
1642
|
+
return {
|
|
1643
|
+
"available": False,
|
|
1644
|
+
"error": f"bad MarkDiffusion adapter JSON: {e}",
|
|
1645
|
+
}
|
|
1646
|
+
payload["available"] = True
|
|
1647
|
+
return payload
|
|
1648
|
+
|
|
1649
|
+
|
|
1650
|
+
def run_ctrlregen_clean(
|
|
1651
|
+
path: Path,
|
|
1652
|
+
output: Path,
|
|
1653
|
+
*,
|
|
1654
|
+
upstream_dir: str | None = None,
|
|
1655
|
+
strength: float = 0.25,
|
|
1656
|
+
steps: int = 50,
|
|
1657
|
+
device: str | None = None,
|
|
1658
|
+
seed: int | None = None,
|
|
1659
|
+
timeout: int = 3600,
|
|
1660
|
+
) -> dict[str, Any]:
|
|
1661
|
+
"""Run the optional CtrlRegen remover in a subprocess.
|
|
1662
|
+
|
|
1663
|
+
Returns ``{"available": False, "error": ...}`` when the remover is not
|
|
1664
|
+
configured, its dependencies are missing, or it fails at runtime; a
|
|
1665
|
+
successful run is ``{"available": True, ...}``.
|
|
1666
|
+
"""
|
|
1667
|
+
if upstream_dir is None:
|
|
1668
|
+
upstream_dir = os.environ.get("NOAI_WATERMARK_DIR")
|
|
1669
|
+
if not upstream_dir:
|
|
1670
|
+
return {
|
|
1671
|
+
"available": False,
|
|
1672
|
+
"error": "CtrlRegen not configured (set NOAI_WATERMARK_DIR or pass --ctrlregen-dir)",
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
upstream = Path(upstream_dir).expanduser().resolve()
|
|
1676
|
+
if not upstream.is_dir():
|
|
1677
|
+
return {"available": False, "error": f"CtrlRegen dir not found: {upstream}"}
|
|
1678
|
+
|
|
1679
|
+
script = SCRIPTS_DIR / "clean_ctrlregen.py"
|
|
1680
|
+
cmd = [
|
|
1681
|
+
_ctrlregen_python(upstream),
|
|
1682
|
+
str(script),
|
|
1683
|
+
str(path),
|
|
1684
|
+
"-o",
|
|
1685
|
+
str(output),
|
|
1686
|
+
"--upstream-dir",
|
|
1687
|
+
str(upstream),
|
|
1688
|
+
"--strength",
|
|
1689
|
+
str(strength),
|
|
1690
|
+
"--steps",
|
|
1691
|
+
str(steps),
|
|
1692
|
+
"--json",
|
|
1693
|
+
]
|
|
1694
|
+
if device:
|
|
1695
|
+
cmd += ["--device", str(device)]
|
|
1696
|
+
if seed is not None:
|
|
1697
|
+
cmd += ["--seed", str(seed)]
|
|
1698
|
+
|
|
1699
|
+
try:
|
|
1700
|
+
r = subprocess.run(
|
|
1701
|
+
cmd,
|
|
1702
|
+
capture_output=True,
|
|
1703
|
+
text=True,
|
|
1704
|
+
timeout=timeout,
|
|
1705
|
+
preexec_fn=ctrlregen_subprocess_preexec_fn,
|
|
1706
|
+
check=False,
|
|
1707
|
+
creationflags=subprocess_creationflags,
|
|
1708
|
+
)
|
|
1709
|
+
except subprocess.TimeoutExpired:
|
|
1710
|
+
return {"available": False, "error": f"CtrlRegen timed out after {timeout}s"}
|
|
1711
|
+
except Exception as e:
|
|
1712
|
+
return {"available": False, "error": str(e)}
|
|
1713
|
+
|
|
1714
|
+
if r.returncode != 0:
|
|
1715
|
+
return {"available": False, "error": (r.stderr or "").strip()[:2000]}
|
|
1716
|
+
try:
|
|
1717
|
+
payload = json.loads(r.stdout or "{}")
|
|
1718
|
+
except json.JSONDecodeError as e:
|
|
1719
|
+
return {"available": False, "error": f"bad CtrlRegen adapter JSON: {e}"}
|
|
1720
|
+
payload["available"] = True
|
|
1721
|
+
return payload
|
|
1722
|
+
|
|
1723
|
+
|
|
1724
|
+
def inspect_image(
|
|
1725
|
+
path: Path,
|
|
1726
|
+
synthid_dir: str | None = None,
|
|
1727
|
+
) -> ImageInspectReport:
|
|
1728
|
+
data = path.read_bytes()
|
|
1729
|
+
fmt = detect_format(data)
|
|
1730
|
+
if fmt == "png":
|
|
1731
|
+
has_c2pa, has_ai, findings = inspect_png(data)
|
|
1732
|
+
elif fmt == "jpeg":
|
|
1733
|
+
has_c2pa, has_ai, findings = inspect_jpeg(data)
|
|
1734
|
+
elif fmt == "webp":
|
|
1735
|
+
has_c2pa, has_ai, findings = inspect_webp(data)
|
|
1736
|
+
elif fmt in ("avif", "heic"):
|
|
1737
|
+
has_c2pa, has_ai, findings = inspect_isobmff(data, fmt)
|
|
1738
|
+
elif fmt == "bmp":
|
|
1739
|
+
has_c2pa, has_ai, findings = inspect_bmp(data)
|
|
1740
|
+
elif fmt == "gif":
|
|
1741
|
+
has_c2pa, has_ai, findings = inspect_gif(data)
|
|
1742
|
+
elif fmt == "tiff":
|
|
1743
|
+
has_c2pa, has_ai, findings = inspect_tiff(data)
|
|
1744
|
+
else:
|
|
1745
|
+
has_c2pa, has_ai, findings = (
|
|
1746
|
+
False,
|
|
1747
|
+
False,
|
|
1748
|
+
["unsupported format (PNG/JPEG/WebP/AVIF/HEIC/BMP/GIF/TIFF)"],
|
|
1749
|
+
)
|
|
1750
|
+
|
|
1751
|
+
notes: list[str] = []
|
|
1752
|
+
if fmt == "unknown":
|
|
1753
|
+
notes.append(
|
|
1754
|
+
"format not fully inspected; only PNG/JPEG/WebP/AVIF/HEIC/BMP/GIF/TIFF are supported"
|
|
1755
|
+
)
|
|
1756
|
+
|
|
1757
|
+
tools = run_optional_tools(path)
|
|
1758
|
+
# Elevate flags from tools
|
|
1759
|
+
ct = tools.get("c2patool") or {}
|
|
1760
|
+
if ct.get("has_manifest"):
|
|
1761
|
+
has_c2pa = True
|
|
1762
|
+
findings.append("c2patool reports a C2PA-related manifest")
|
|
1763
|
+
probe_note = c2patool_probe_note(tools)
|
|
1764
|
+
if probe_note:
|
|
1765
|
+
notes.append(probe_note)
|
|
1766
|
+
|
|
1767
|
+
return ImageInspectReport(
|
|
1768
|
+
path=str(path),
|
|
1769
|
+
format=fmt,
|
|
1770
|
+
has_c2pa=has_c2pa,
|
|
1771
|
+
has_ai_metadata=has_ai,
|
|
1772
|
+
findings=findings,
|
|
1773
|
+
tools=tools,
|
|
1774
|
+
synthid=run_synthid_score(path, synthid_dir),
|
|
1775
|
+
notes=notes,
|
|
1776
|
+
)
|
|
1777
|
+
|
|
1778
|
+
|
|
1779
|
+
def _png_chunk(ctype: bytes, payload: bytes) -> bytes:
|
|
1780
|
+
crc = zlib.crc32(ctype)
|
|
1781
|
+
crc = zlib.crc32(payload, crc) & 0xFFFFFFFF
|
|
1782
|
+
return struct.pack(">I", len(payload)) + ctype + payload + struct.pack(">I", crc)
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
def strip_png(data: bytes, *, strip_all_text: bool = True) -> tuple[bytes, list[str]]:
|
|
1786
|
+
if not data.startswith(PNG_SIG):
|
|
1787
|
+
raise ValueError("not PNG")
|
|
1788
|
+
actions: list[str] = []
|
|
1789
|
+
out = bytearray(PNG_SIG)
|
|
1790
|
+
pos = 8
|
|
1791
|
+
while pos + 8 <= len(data):
|
|
1792
|
+
length = struct.unpack(">I", data[pos : pos + 4])[0]
|
|
1793
|
+
ctype = data[pos + 4 : pos + 8]
|
|
1794
|
+
chunk_start = pos + 8
|
|
1795
|
+
chunk_end = chunk_start + length
|
|
1796
|
+
if chunk_end + 4 > len(data):
|
|
1797
|
+
# A truncated chunk (interrupted download): copy the remainder
|
|
1798
|
+
# verbatim instead of dropping it — dropping turned a recoverable
|
|
1799
|
+
# image into an unopenable husk while reporting "already clean"
|
|
1800
|
+
# (#170). Note it as an action so the run is never mistaken for
|
|
1801
|
+
# an ordinary no-op clean.
|
|
1802
|
+
out.extend(data[pos:])
|
|
1803
|
+
actions.append(
|
|
1804
|
+
f"kept {len(data) - pos} bytes of truncated chunk {ctype.decode('latin-1', errors='replace')} tail (file truncated)"
|
|
1805
|
+
)
|
|
1806
|
+
break
|
|
1807
|
+
payload = data[chunk_start:chunk_end]
|
|
1808
|
+
crc_bytes = data[chunk_end : chunk_end + 4]
|
|
1809
|
+
pos = chunk_end + 4
|
|
1810
|
+
name = ctype.decode("latin-1", errors="replace")
|
|
1811
|
+
|
|
1812
|
+
drop = False
|
|
1813
|
+
if ctype in (b"eXIf", b"caBX") or ctype.startswith(b"c2"):
|
|
1814
|
+
drop = True
|
|
1815
|
+
actions.append(f"drop chunk {name}")
|
|
1816
|
+
elif ctype in (b"tEXt", b"zTXt", b"iTXt"):
|
|
1817
|
+
if strip_all_text or _text_chunk_is_ai(payload, ctype):
|
|
1818
|
+
drop = True
|
|
1819
|
+
actions.append(f"drop chunk {name}")
|
|
1820
|
+
elif _contains_any(ctype + payload, C2PA_MARKERS) and ctype not in (
|
|
1821
|
+
b"IHDR",
|
|
1822
|
+
b"IDAT",
|
|
1823
|
+
b"IEND",
|
|
1824
|
+
b"PLTE",
|
|
1825
|
+
b"tRNS",
|
|
1826
|
+
b"gAMA",
|
|
1827
|
+
b"pHYs",
|
|
1828
|
+
b"sRGB",
|
|
1829
|
+
b"cHRM",
|
|
1830
|
+
b"iCCP",
|
|
1831
|
+
):
|
|
1832
|
+
drop = True
|
|
1833
|
+
actions.append(f"drop chunk {name} (C2PA marker in payload)")
|
|
1834
|
+
|
|
1835
|
+
if not drop:
|
|
1836
|
+
out.extend(struct.pack(">I", length) + ctype + payload + crc_bytes)
|
|
1837
|
+
if ctype == b"IEND":
|
|
1838
|
+
break
|
|
1839
|
+
if not actions:
|
|
1840
|
+
actions.append("no PNG metadata chunks removed (already clean or none matched)")
|
|
1841
|
+
return bytes(out), actions
|
|
1842
|
+
|
|
1843
|
+
|
|
1844
|
+
def strip_jpeg(data: bytes, *, strip_all_app: bool = True) -> tuple[bytes, list[str]]:
|
|
1845
|
+
if not data.startswith(JPEG_SOI):
|
|
1846
|
+
raise ValueError("not JPEG")
|
|
1847
|
+
actions: list[str] = []
|
|
1848
|
+
out = bytearray(JPEG_SOI)
|
|
1849
|
+
i = 2
|
|
1850
|
+
n = len(data)
|
|
1851
|
+
while i < n:
|
|
1852
|
+
if data[i] != 0xFF:
|
|
1853
|
+
# unexpected; copy rest
|
|
1854
|
+
out.extend(data[i:])
|
|
1855
|
+
actions.append("copied remainder after non-marker byte")
|
|
1856
|
+
break
|
|
1857
|
+
while i < n and data[i] == 0xFF:
|
|
1858
|
+
i += 1
|
|
1859
|
+
if i >= n:
|
|
1860
|
+
break
|
|
1861
|
+
marker = data[i]
|
|
1862
|
+
i += 1
|
|
1863
|
+
|
|
1864
|
+
if marker == 0xD9: # EOI
|
|
1865
|
+
out.extend(b"\xff\xd9")
|
|
1866
|
+
break
|
|
1867
|
+
if marker == 0xD8: # nested SOI?
|
|
1868
|
+
continue
|
|
1869
|
+
if 0xD0 <= marker <= 0xD7:
|
|
1870
|
+
out.extend(bytes([0xFF, marker]))
|
|
1871
|
+
continue
|
|
1872
|
+
if marker == 0xDA: # SOS — copy through end
|
|
1873
|
+
# need length of SOS header then entropy-coded data to EOI
|
|
1874
|
+
if i + 2 > n:
|
|
1875
|
+
break
|
|
1876
|
+
seglen = struct.unpack(">H", data[i : i + 2])[0]
|
|
1877
|
+
# Find EOI from here carefully: after SOS segment header, scan for FF D9
|
|
1878
|
+
# not preceded by stuffed FF 00 issues — simple approach: copy from FF DA to end
|
|
1879
|
+
# Reconstruct: FF DA + rest of file
|
|
1880
|
+
out.extend(b"\xff\xda")
|
|
1881
|
+
out.extend(data[i:])
|
|
1882
|
+
actions.append("preserved entropy-coded scan (SOS→EOF)")
|
|
1883
|
+
break
|
|
1884
|
+
|
|
1885
|
+
if i + 2 > n:
|
|
1886
|
+
break
|
|
1887
|
+
seglen = struct.unpack(">H", data[i : i + 2])[0]
|
|
1888
|
+
if seglen < 2 or i + seglen > n:
|
|
1889
|
+
out.extend(data[i - 2 :]) # best effort
|
|
1890
|
+
actions.append("truncated segment; copied remainder")
|
|
1891
|
+
break
|
|
1892
|
+
payload = data[i + 2 : i + seglen]
|
|
1893
|
+
next_i = i + seglen
|
|
1894
|
+
|
|
1895
|
+
# APP0 is JFIF — keep for compatibility unless full strip of all APP
|
|
1896
|
+
keep = False
|
|
1897
|
+
drop = False
|
|
1898
|
+
if 0xE0 <= marker <= 0xEF: # APPn
|
|
1899
|
+
if marker == 0xEB: # APP11 JUMBF/C2PA
|
|
1900
|
+
drop = True
|
|
1901
|
+
actions.append("drop APP11 (C2PA/JUMBF)")
|
|
1902
|
+
elif strip_all_app and marker != 0xE0:
|
|
1903
|
+
# keep APP0 (JFIF) by default
|
|
1904
|
+
drop = True
|
|
1905
|
+
actions.append(f"drop APP{marker - 0xE0}")
|
|
1906
|
+
elif _contains_any(payload, AI_META_HINTS + C2PA_MARKERS):
|
|
1907
|
+
drop = True
|
|
1908
|
+
actions.append(f"drop APP{marker - 0xE0} (AI/C2PA markers)")
|
|
1909
|
+
else:
|
|
1910
|
+
keep = True
|
|
1911
|
+
elif marker == 0xFE: # COM
|
|
1912
|
+
hits = _contains_any(payload, JPEG_COM_AI_HINTS)
|
|
1913
|
+
if strip_all_app or hits:
|
|
1914
|
+
drop = True
|
|
1915
|
+
actions.append("drop COM comment")
|
|
1916
|
+
else:
|
|
1917
|
+
keep = True
|
|
1918
|
+
else:
|
|
1919
|
+
keep = True
|
|
1920
|
+
|
|
1921
|
+
if keep and not drop:
|
|
1922
|
+
out.extend(bytes([0xFF, marker]))
|
|
1923
|
+
out.extend(data[i : i + seglen])
|
|
1924
|
+
i = next_i
|
|
1925
|
+
|
|
1926
|
+
if not actions:
|
|
1927
|
+
actions.append("no JPEG APP segments removed")
|
|
1928
|
+
return bytes(out), actions
|
|
1929
|
+
|
|
1930
|
+
|
|
1931
|
+
def strip_webp(data: bytes, *, strip_all_metadata: bool = True) -> tuple[bytes, list[str]]:
|
|
1932
|
+
chunks, notes = _webp_chunks(data)
|
|
1933
|
+
if not chunks and notes == ["not a WebP"]:
|
|
1934
|
+
raise ValueError("not WebP")
|
|
1935
|
+
if notes:
|
|
1936
|
+
raise ValueError("malformed WebP: " + "; ".join(notes))
|
|
1937
|
+
|
|
1938
|
+
actions: list[str] = []
|
|
1939
|
+
kept: list[tuple[bytes, bytes, bytes]] = []
|
|
1940
|
+
removed_flags = 0
|
|
1941
|
+
metadata_flags = {b"ICCP": 0x20, b"EXIF": 0x08, b"XMP ": 0x04}
|
|
1942
|
+
|
|
1943
|
+
for fourcc, payload, padding in chunks:
|
|
1944
|
+
drop = fourcc.upper() == b"C2PA"
|
|
1945
|
+
if fourcc in metadata_flags:
|
|
1946
|
+
drop = strip_all_metadata or bool(_contains_any(payload, AI_META_HINTS + C2PA_MARKERS))
|
|
1947
|
+
if drop:
|
|
1948
|
+
name = fourcc.decode("latin-1", errors="replace")
|
|
1949
|
+
actions.append(f"drop WebP chunk {name}")
|
|
1950
|
+
removed_flags |= metadata_flags.get(fourcc, 0)
|
|
1951
|
+
else:
|
|
1952
|
+
kept.append((fourcc, payload, padding))
|
|
1953
|
+
|
|
1954
|
+
body = bytearray(WEBP_SIG)
|
|
1955
|
+
for fourcc, payload, padding in kept:
|
|
1956
|
+
chunk = payload
|
|
1957
|
+
if fourcc == b"VP8X" and len(chunk) >= 1 and removed_flags:
|
|
1958
|
+
chunk = bytes([chunk[0] & ~removed_flags]) + chunk[1:]
|
|
1959
|
+
body.extend(fourcc)
|
|
1960
|
+
body.extend(struct.pack("<I", len(chunk)))
|
|
1961
|
+
body.extend(chunk)
|
|
1962
|
+
body.extend(padding if len(chunk) & 1 else b"")
|
|
1963
|
+
|
|
1964
|
+
if not actions:
|
|
1965
|
+
actions.append("no WebP metadata chunks removed (already clean or none matched)")
|
|
1966
|
+
return WEBP_RIFF + struct.pack("<I", len(body)) + bytes(body), actions
|
|
1967
|
+
|
|
1968
|
+
|
|
1969
|
+
def strip_isobmff(
|
|
1970
|
+
data: bytes, fmt: str = "avif", *, strip_all_metadata: bool = True
|
|
1971
|
+
) -> tuple[bytes, list[str]]:
|
|
1972
|
+
boxes, scanned_end = _parse_isobmff_boxes(data)
|
|
1973
|
+
if not boxes:
|
|
1974
|
+
raise ValueError(f"not a valid {fmt.upper()} (no ISOBMFF boxes)")
|
|
1975
|
+
|
|
1976
|
+
# scanned_end is where the box walk stopped: the parser halts at the first
|
|
1977
|
+
# box whose declared size overruns the data (a truncated download). The
|
|
1978
|
+
# rebuild below used to emit only the boxes that parsed — dropping a
|
|
1979
|
+
# truncated mdat, the actual coded image, while reporting "already clean"
|
|
1980
|
+
# (#170); the tail is appended verbatim afterwards instead.
|
|
1981
|
+
actions: list[str] = []
|
|
1982
|
+
out = bytearray()
|
|
1983
|
+
|
|
1984
|
+
for fourcc, payload, size, header_size in boxes:
|
|
1985
|
+
name = fourcc.decode("latin-1", errors="replace")
|
|
1986
|
+
if fourcc in (b"jumb", b"c2pa") or name.lower().startswith("c2"):
|
|
1987
|
+
actions.append(f"drop top-level {name} box (C2PA/JUMBF)")
|
|
1988
|
+
out.extend(_isobmff_free_box(size, header_size))
|
|
1989
|
+
continue
|
|
1990
|
+
|
|
1991
|
+
if fourcc == b"uuid":
|
|
1992
|
+
if payload.startswith(XMP_UUID):
|
|
1993
|
+
actions.append(f"drop top-level {name} box (XMP metadata)")
|
|
1994
|
+
out.extend(_isobmff_free_box(size, header_size))
|
|
1995
|
+
continue
|
|
1996
|
+
if strip_all_metadata or _contains_any(payload, AI_META_HINTS + C2PA_MARKERS):
|
|
1997
|
+
actions.append(f"drop top-level {name} box (UUID metadata)")
|
|
1998
|
+
out.extend(_isobmff_free_box(size, header_size))
|
|
1999
|
+
continue
|
|
2000
|
+
|
|
2001
|
+
if fourcc == b"meta":
|
|
2002
|
+
meta_verflags = payload[:4] if len(payload) >= 4 else b"\x00\x00\x00\x00"
|
|
2003
|
+
sub_boxes, _ = _parse_isobmff_boxes(payload, start=4)
|
|
2004
|
+
clean_sub = bytearray()
|
|
2005
|
+
for s_fourcc, s_payload, s_size, s_hdr in sub_boxes:
|
|
2006
|
+
s_name = s_fourcc.decode("latin-1", errors="replace")
|
|
2007
|
+
if s_fourcc in (b"jumb", b"c2pa") or s_name.lower().startswith("c2"):
|
|
2008
|
+
actions.append(f"drop meta sub-box {s_name} (C2PA/JUMBF)")
|
|
2009
|
+
clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
|
|
2010
|
+
continue
|
|
2011
|
+
if s_fourcc == b"uuid":
|
|
2012
|
+
if s_payload.startswith(XMP_UUID):
|
|
2013
|
+
actions.append(f"drop meta sub-box {s_name} (XMP metadata)")
|
|
2014
|
+
clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
|
|
2015
|
+
continue
|
|
2016
|
+
if strip_all_metadata or _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS):
|
|
2017
|
+
actions.append(f"drop meta sub-box {s_name} (UUID metadata)")
|
|
2018
|
+
clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
|
|
2019
|
+
continue
|
|
2020
|
+
if s_fourcc in (b"xml ", b"bxml") and (
|
|
2021
|
+
strip_all_metadata or _contains_any(s_payload, AI_META_HINTS + C2PA_MARKERS)
|
|
2022
|
+
):
|
|
2023
|
+
actions.append(f"drop meta sub-box {s_name} (XML metadata)")
|
|
2024
|
+
clean_sub.extend(_isobmff_free_box(s_size, s_hdr))
|
|
2025
|
+
continue
|
|
2026
|
+
clean_sub.extend(_build_isobmff_box(s_fourcc, s_payload, s_hdr))
|
|
2027
|
+
|
|
2028
|
+
new_meta_payload = meta_verflags + clean_sub
|
|
2029
|
+
out.extend(_build_isobmff_box(b"meta", new_meta_payload, header_size))
|
|
2030
|
+
continue
|
|
2031
|
+
|
|
2032
|
+
out.extend(_build_isobmff_box(fourcc, payload, header_size))
|
|
2033
|
+
|
|
2034
|
+
if scanned_end < len(data):
|
|
2035
|
+
tail = data[scanned_end:]
|
|
2036
|
+
out.extend(tail)
|
|
2037
|
+
# An 8-byte header whose box overruns the data is a truncated download
|
|
2038
|
+
# worth reporting; fewer than 8 leftover bytes is merely trailing junk
|
|
2039
|
+
# and is kept verbatim without claiming the file was truncated (#170).
|
|
2040
|
+
if len(tail) >= 8:
|
|
2041
|
+
actions.append(f"kept {len(tail)} bytes of truncated tail (file truncated)")
|
|
2042
|
+
|
|
2043
|
+
if not actions:
|
|
2044
|
+
actions.append(f"no {fmt.upper()} metadata boxes removed (already clean or none matched)")
|
|
2045
|
+
|
|
2046
|
+
return bytes(out), actions
|
|
2047
|
+
|
|
2048
|
+
|
|
2049
|
+
def clean_image(
|
|
2050
|
+
path: Path,
|
|
2051
|
+
dest: Path,
|
|
2052
|
+
*,
|
|
2053
|
+
strip_all_metadata: bool = True,
|
|
2054
|
+
synthid_dir: str | None = None,
|
|
2055
|
+
remove_pixel: str | None = None,
|
|
2056
|
+
ctrlregen_dir: str | None = None,
|
|
2057
|
+
ctrlregen_strength: float = 0.25,
|
|
2058
|
+
ctrlregen_steps: int = 50,
|
|
2059
|
+
ctrlregen_device: str | None = None,
|
|
2060
|
+
ctrlregen_seed: int | None = None,
|
|
2061
|
+
ctrlregen_timeout: int = 3600,
|
|
2062
|
+
markdiffusion_dir: str | None = None,
|
|
2063
|
+
markdiffusion_strength: float = 0.3,
|
|
2064
|
+
markdiffusion_model: str | None = None,
|
|
2065
|
+
markdiffusion_size: int = 512,
|
|
2066
|
+
markdiffusion_steps: int = 50,
|
|
2067
|
+
markdiffusion_device: str | None = None,
|
|
2068
|
+
markdiffusion_timeout: int = 3600,
|
|
2069
|
+
) -> dict[str, Any]:
|
|
2070
|
+
synthid_before = run_synthid_score(path, synthid_dir)
|
|
2071
|
+
data = path.read_bytes()
|
|
2072
|
+
fmt = detect_format(data)
|
|
2073
|
+
if fmt == "png":
|
|
2074
|
+
cleaned, actions = strip_png(data, strip_all_text=strip_all_metadata)
|
|
2075
|
+
elif fmt == "jpeg":
|
|
2076
|
+
cleaned, actions = strip_jpeg(data, strip_all_app=strip_all_metadata)
|
|
2077
|
+
elif fmt == "webp":
|
|
2078
|
+
cleaned, actions = strip_webp(data, strip_all_metadata=strip_all_metadata)
|
|
2079
|
+
elif fmt in ("avif", "heic"):
|
|
2080
|
+
cleaned, actions = strip_isobmff(data, fmt, strip_all_metadata=strip_all_metadata)
|
|
2081
|
+
elif fmt == "bmp":
|
|
2082
|
+
cleaned, actions = strip_bmp(data, strip_all_metadata=strip_all_metadata)
|
|
2083
|
+
elif fmt == "gif":
|
|
2084
|
+
cleaned, actions = strip_gif(data, strip_all_metadata=strip_all_metadata)
|
|
2085
|
+
elif fmt == "tiff":
|
|
2086
|
+
cleaned, actions = strip_tiff(data, strip_all_metadata=strip_all_metadata)
|
|
2087
|
+
else:
|
|
2088
|
+
raise ValueError(f"unsupported format: {fmt}")
|
|
2089
|
+
|
|
2090
|
+
# Optional exiftool pass for residual tags
|
|
2091
|
+
safe_write_bytes(dest, cleaned)
|
|
2092
|
+
exiftool = which("exiftool")
|
|
2093
|
+
if exiftool and strip_all_metadata:
|
|
2094
|
+
try:
|
|
2095
|
+
subprocess.run(
|
|
2096
|
+
[
|
|
2097
|
+
exiftool,
|
|
2098
|
+
"-all=",
|
|
2099
|
+
"-overwrite_original",
|
|
2100
|
+
safe_arg(str(dest)),
|
|
2101
|
+
],
|
|
2102
|
+
capture_output=True,
|
|
2103
|
+
text=True,
|
|
2104
|
+
timeout=60,
|
|
2105
|
+
check=False,
|
|
2106
|
+
preexec_fn=subprocess_preexec_fn,
|
|
2107
|
+
creationflags=subprocess_creationflags,
|
|
2108
|
+
)
|
|
2109
|
+
actions.append("exiftool -all= pass")
|
|
2110
|
+
except Exception as e:
|
|
2111
|
+
actions.append(f"exiftool failed: {e}")
|
|
2112
|
+
|
|
2113
|
+
pixel_removal: dict[str, Any] | None = None
|
|
2114
|
+
if remove_pixel:
|
|
2115
|
+
if remove_pixel == "ctrlregen":
|
|
2116
|
+
pixel_removal = run_ctrlregen_clean(
|
|
2117
|
+
dest,
|
|
2118
|
+
dest,
|
|
2119
|
+
upstream_dir=ctrlregen_dir,
|
|
2120
|
+
strength=ctrlregen_strength,
|
|
2121
|
+
steps=ctrlregen_steps,
|
|
2122
|
+
device=ctrlregen_device,
|
|
2123
|
+
seed=ctrlregen_seed,
|
|
2124
|
+
timeout=ctrlregen_timeout,
|
|
2125
|
+
)
|
|
2126
|
+
if pixel_removal.get("available"):
|
|
2127
|
+
actions.append(f"CtrlRegen pixel removal (strength {ctrlregen_strength})")
|
|
2128
|
+
else:
|
|
2129
|
+
actions.append(
|
|
2130
|
+
"CtrlRegen pixel removal skipped: "
|
|
2131
|
+
f"{pixel_removal.get('error', 'unknown error')}"
|
|
2132
|
+
)
|
|
2133
|
+
elif remove_pixel == "diffusion":
|
|
2134
|
+
pixel_removal = run_markdiffusion_purify(
|
|
2135
|
+
dest,
|
|
2136
|
+
dest,
|
|
2137
|
+
upstream_dir=markdiffusion_dir,
|
|
2138
|
+
strength=markdiffusion_strength,
|
|
2139
|
+
model=markdiffusion_model,
|
|
2140
|
+
size=markdiffusion_size,
|
|
2141
|
+
steps=markdiffusion_steps,
|
|
2142
|
+
device=markdiffusion_device,
|
|
2143
|
+
timeout=markdiffusion_timeout,
|
|
2144
|
+
)
|
|
2145
|
+
if pixel_removal.get("available"):
|
|
2146
|
+
actions.append(
|
|
2147
|
+
f"DiffusionPurification pixel removal (strength {markdiffusion_strength})"
|
|
2148
|
+
)
|
|
2149
|
+
else:
|
|
2150
|
+
actions.append(
|
|
2151
|
+
"DiffusionPurification pixel removal skipped: "
|
|
2152
|
+
f"{pixel_removal.get('error', 'unknown error')}"
|
|
2153
|
+
)
|
|
2154
|
+
else:
|
|
2155
|
+
raise ValueError(f"unknown pixel remover: {remove_pixel}")
|
|
2156
|
+
|
|
2157
|
+
after = inspect_image(dest, synthid_dir=synthid_dir)
|
|
2158
|
+
return {
|
|
2159
|
+
"input": str(path),
|
|
2160
|
+
"output": str(dest),
|
|
2161
|
+
"format": fmt,
|
|
2162
|
+
"actions": actions,
|
|
2163
|
+
"bytes_in": len(data),
|
|
2164
|
+
"bytes_out": dest.stat().st_size,
|
|
2165
|
+
"still_has_c2pa": after.has_c2pa,
|
|
2166
|
+
"still_has_ai_metadata": after.has_ai_metadata,
|
|
2167
|
+
"post_findings": after.findings,
|
|
2168
|
+
"synthid_before": synthid_before,
|
|
2169
|
+
"synthid_after": after.synthid,
|
|
2170
|
+
"pixel_removal": pixel_removal,
|
|
2171
|
+
}
|