stegoeggo-cli 0.3.0

Command-line interface for the stegoeggo image protection library
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
# stegoeggo

Embed rights-reservation metadata and AI-training restriction notices into images, with optional best-effort steganographic markers for redundant evidence.

[![CI](https://github.com/eggstack/stegoeggo/actions/workflows/ci.yml/badge.svg)](https://github.com/eggstack/stegoeggo/actions/workflows/ci.yml)
[![Crates.io](https://img.shields.io/crates/v/stegoeggo)](https://crates.io/crates/stegoeggo)
[![Documentation](https://docs.rs/stegoeggo/badge.svg)](https://docs.rs/stegoeggo)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![MSRV](https://img.shields.io/badge/MSRV-1.87-blue.svg)](https://blog.rust-lang.org/)

## What stegoeggo is

stegoeggo is:

- A **legal-notice and rights-reservation metadata tool** for images.
- A way to make copyright and AI-training restrictions visible to metadata-aware systems.
- A best-effort redundant marking system when optional steganographic payloads are enabled.

## What stegoeggo is not

stegoeggo is **not**:

- A forensic watermarking system.
- A DRM system.
- A guarantee that marks survive arbitrary resizing, re-encoding, screenshots, cropping, or metadata stripping.
- A cryptographic proof that a model trained on a specific image.
- A data-poisoning tool.

## What it does

stegoeggo embeds multiple layers of rights-reservation and AI-training restriction metadata into images:

| Layer | Description |
|-------|-------------|
| **Metadata Injection** | Embeds rights-reservation and AI-training restriction markers in image headers using canonical `plus:DataMining` rights signals, XMP, and EXIF |
| **Steganography** | Optional hidden payloads embedded in image pixels (LSB) or DCT coefficients (JPEG) for redundant evidence |

### External Standards

- **PLUS License Data Format** - Emits `plus:DataMining` with official PLUS LDF controlled-vocabulary URIs for machine-readable rights signals (canonical per the [PLUS License Data Format](https://www.useplus.com/) specification). Legacy `Iptc4xmpExt:DMI-*` properties are still parsed for backward compatibility but not emitted by default.
- **ISCC** - Computes [Immutable Self-Certifying Constituent Content](https://iscc-project.github.io/) identifiers for content identification

## Release 5 Features

Release 5 adds cryptographic provenance, signing, and detached manifests:

| Feature | Description |
|---------|-------------|
| **Payload v3** | TLV extension format with domain-separated authentication for future-proof metadata |
| **Provenance Claims** | Canonical provenance assertions with digest binding and canonical serialization |
| **Ed25519 Signing** | Real Ed25519 signing via `ed25519-dalek` of protection payloads and provenance claims (`signatures` feature) |
| **Detached Manifests** | Signed sidecar manifests for distributing provenance outside the image (`detached-manifest` feature) |
| **Structured Verification** | `VerificationReport` with per-channel sub-results replaces the old `VerificationStatus` enum |

## Installation

### As a Library

Add to your `Cargo.toml`:

```toml
[dependencies]
stegoeggo = "0.2"
image = "0.25"  # Required for DynamicImage
```

For async support (Tokio-based WAF/CDN deployments):

```toml
[dependencies]
stegoeggo = { version = "0.2", features = ["async"] }
```

For Ed25519 signing (provenance claims, detached manifests):

```toml
[dependencies]
stegoeggo = { version = "0.2", features = ["signatures", "detached-manifest"] }
```

### As a CLI Tool

Build the binary from source:

```bash
cargo build --release
```

Or install directly:

```bash
cargo install stegoeggo-cli
```

## Quick Start

### CLI

```bash
# Embed legal-notice metadata with default settings (Standard level)
stegoeggo input.png -o output.png

# With explicit legal metadata (recommended for owned content)
stegoeggo artwork.png -o artwork_protected.png \
  --copyright-holder "Jane Artist" \
  --creator "Jane Artist" \
  --rights-url "https://example.com/rights/artwork" \
  --no-genai-training

# With full legal metadata including new v0.2 fields
stegoeggo photo.jpg --copyright-holder "Acme Corp" --creator "Jane Doe" \
  --credit-line "Photo by Jane Doe / Acme Corp" \
  --copyright-owner "Acme Corp" \
  --licensor-name "Acme Corp" --licensor-email "legal@acme.com" \
  --content-created-at "2024-01-15"

# Quick AI-training restriction
stegoeggo photo.jpg -o protected.jpg --no-ai-training

# Light protection (metadata only, minimal stego)
stegoeggo input.png -o output.png --level light

# Authenticated provenance (optional — requires MAC key)
stegoeggo artwork.png -o artwork_auth.png \
  --profile authenticated-provenance \
  --key deadbeefcafebabe \
  --copyright-holder "Jane Artist" \
  --rights-url "https://example.com/rights/artwork" \
  --no-ai-training

# Verify if an image is protected
stegoeggo protected.png --verify
```

### Library

```rust
use stegoeggo::{ProtectionPipeline, ProtectionContext, ProtectionLevel};
use image::DynamicImage;

// Create pipeline and context
let pipeline = ProtectionPipeline::new();
let ctx = ProtectionContext::default();

// Process an image — embeds metadata and optional steganographic markers
let img = DynamicImage::new_rgb8(512, 512);
let protected = pipeline.process(&img, ProtectionLevel::Standard, &ctx).unwrap();
```

### Request-Based API (recommended for new code)

```rust
use stegoeggo::{ProtectionRequest, RightsPolicy, RightsNotice, LegalMetadata};

let request = ProtectionRequest::metadata_only(
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
)
.with_legal_metadata(
    LegalMetadata::new()
        .with_copyright_holder("Example Corp")
        .with_usage_terms("All Rights Reserved"),
);

let protected = stegoeggo::process_request_bytes(&img_bytes, &request)?;
```

## Library Usage

### Processing Image Bytes

Process images from files or network sources without loading into DynamicImage:

```rust,no_run
use stegoeggo::{process_image_bytes, ProtectionContext, ProtectionLevel};

// Read image from file
let img_bytes = std::fs::read("image.png").unwrap();

// Process with automatic format detection. The byte API preserves the detected
// input format unless you set `ProtectionContext::with_format(...)`.
let ctx = ProtectionContext::default();
let protected = process_image_bytes(&img_bytes, ProtectionLevel::Standard, &ctx).unwrap();
```

### Parallel Processing

Process multiple images concurrently using Rayon:

```rust,no_run
use stegoeggo::{process_images_parallel, ProtectionContext, ProtectionLevel};
use image::DynamicImage;

let images: Vec<DynamicImage> = vec![
    image::open("image1.png").unwrap(),
    image::open("image2.png").unwrap(),
    image::open("image3.png").unwrap(),
];

let ctx = ProtectionContext::default();
let results = process_images_parallel(&images, ProtectionLevel::Standard, &ctx).unwrap();
```

Or process bytes in parallel:

```rust,ignore
use stegoeggo::{process_images_bytes_parallel, ProtectionContext, ProtectionLevel};

let image_bytes: Vec<Vec<u8>> = vec![
    std::fs::read("image1.png").unwrap(),
    std::fs::read("image2.png").unwrap(),
];

let protected = process_images_bytes_parallel(&image_bytes, ProtectionLevel::Standard, &ctx).unwrap();
```

### Request-Based API (Recommended)

The request-based API is the canonical way to use stegoeggo. It separates
rights policy from processing mechanics:

```rust
use stegoeggo::{ProtectionRequest, RightsPolicy, ProtectionPreset, RightsNotice};

// Metadata-only legal notice (fastest path)
let request = ProtectionRequest::metadata_only(
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
);

// With hidden marker
let request = ProtectionRequest::with_hidden_marker(
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
);

// Using a preset
let request = ProtectionRequest::from_preset(
    ProtectionPreset::AuthenticatedProvenance,
    RightsNotice::default(),
    RightsPolicy::ProhibitedAiMlTraining,
)
.with_mac_key(b"secret".to_vec());

let (protected, report) = stegoeggo::process_request_bytes_with_report(&img_bytes, &request)?;
println!("Metadata injected: {}", report.metadata_injected);
println!("Stego succeeded: {}", report.stego_succeeded);
```

### Key Generation and Signing (feature: `signatures`)

```rust
use stegoeggo::signing::{SigningKey, VerifyingKey};

// Generate a new signing key (uses ed25519-dalek for real Ed25519)
let signing_key = SigningKey::generate();
let verifying_key: VerifyingKey = signing_key.verifying_key();

// Sign a provenance claim — produces a 64-byte Ed25519 signature
let claim_bytes = b"provenance claim data";
let signature = signing_key.sign(claim_bytes);

// Verify
assert!(verifying_key.verify(claim_bytes, &signature).is_ok());
```

**Note:** Signing is experimental (feature-gated behind `signatures`). A valid signature proves only that the private key holder signed the claim bytes — it does not prove copyright ownership or authorship. See [SECURITY.md](SECURITY.md) for details.

### Detached Manifests (feature: `detached-manifest`)

```rust
use stegoeggo::detached::{DetachedManifest, ManifestBuilder};

// Create a manifest from protected image bytes
let manifest = ManifestBuilder::new()
    .with_provenance(claim)
    .with_signature(signature)
    .build();

// Serialize to JSON sidecar
let sidecar = manifest.to_json()?;

// Later, verify the sidecar against image bytes
let report = DetachedManifest::verify(&sidecar, &image_bytes)?;
```

### VerificationReport (structured results)

```rust
use stegoeggo::verification::VerificationReport;

let report: VerificationReport = stegoeggo::verify_image_bytes_detailed(&image_bytes)?;
println!("Stego: {:?}", report.stego);
println!("Metadata: {:?}", report.metadata);
println!("Signing: {:?}", report.signing);
```

### Protection Levels

The library provides three protection levels:

| Level | Strategy | Latency (512x512) | Use Case |
|-------|----------|-------------------|----------|
| `Disabled` | No protection | ~20 ns | Testing, whitelisted clients |
| `Light` | Metadata + minimal stego (Q-table seed for JPEG, LSB redundancy=1 for PNG/WebP) | ~0.8 ms | Metadata-only, low cost |
| `Standard` | Full stego (DCT F5 + metadata for JPEG, LSB + metadata for PNG/WebP) | ~0.8 ms | Default for most endpoints |

```rust
use stegoeggo::ProtectionLevel;

// Use different levels
let level = ProtectionLevel::Light;    // Metadata + minimal stego
let level = ProtectionLevel::Standard; // Stego + Metadata (default)
```

### Evidence Profiles

Evidence profiles control how protection warnings are interpreted and the default evidence posture. While `ProtectionLevel` controls how much processing occurs, `EvidenceProfile` answers "what evidence model is the caller trying to express?"

| Profile | MAC Key Required | Stego | Primary Use Case |
|---------|-----------------|-------|------------------|
| `LegalNotice` (default) | No | Optional | Standards-aligned metadata notice |
| `LegalNoticeWithStego` | No | Yes | Metadata notice plus best-effort hidden marker |
| `AuthenticatedProvenance` | Yes | Yes | Cryptographic proof of payload origin |
| `Maximal` | Optional | Yes | All available evidence channels |

```rust
use stegoeggo::{ProtectionContext, EvidenceProfile, LegalMetadata, ProtectionLevel};

// Legal notice only — no MAC key needed
let ctx = ProtectionContext::legal_notice()
    .with_legal_metadata(
        LegalMetadata::new()
            .with_copyright_holder("Jane Artist")
            .with_ai_constraints("No AI training permitted.")
    );

// Authenticated provenance — MAC key expected
let ctx = ProtectionContext::authenticated_provenance()
    .with_mac_key(b"secret-key".to_vec());

// Via builder
let ctx = ProtectionContext::new(0.5, 42)
    .with_evidence_profile(EvidenceProfile::Maximal);
```

### Legal Metadata Injection

Inject real legal metadata (copyright, contact info, usage terms). **Only use for content you own.**

`with_legal_metadata(...)` provides the content. When legal metadata is provided, `with_legal_claims(true)` is **auto-enabled** — you no longer need to call it explicitly. The explicit call is still supported but no longer required:

```rust,ignore
use stegoeggo::{process_image_bytes, ProtectionContext, LegalMetadata, ProtectionLevel};

let img_bytes = std::fs::read("image.png").unwrap();
let ctx = ProtectionContext::default()
    .with_legal_metadata(
        LegalMetadata::new()
            .with_copyright_holder("Your Company Name")
            .with_contact_email("legal@company.com")
            .with_usage_terms("All Rights Reserved. No AI training permitted.")
            .with_license_url("https://company.com/license")
    )
    .with_legal_claims(true);

let protected = process_image_bytes(&img_bytes, ProtectionLevel::Standard, &ctx).unwrap();
```

### DMI (Data Mining Inhibitor) Values

Set DMI metadata values for AI-training restrictions. The XMP writer emits canonical `plus:DataMining` properties with PLUS LDF vocabulary keys. Legacy `Iptc4xmpExt:DMI-*` properties are still parsed for backward compatibility but not emitted by default:

```rust
use stegoeggo::{ProtectionContext, DmiValue, ProtectionLevel};

let ctx = ProtectionContext::default()
    .with_dmi(DmiValue::ProhibitedAiMlTraining);
```

Available values:
- `Unspecified` - No restriction specified
- `Allowed` - Content may be used for AI/ML training
- `ProhibitedAiMlTraining` - Prohibited for AI/ML training
- `ProhibitedGenAiMlTraining` - Prohibited for generative AI training
- `ProhibitedExceptSearchEngineIndexing` - Prohibited except for search indexing
- `Prohibited` - All uses prohibited
- `ProhibitedSeeConstraints` - Prohibited, see constraints for details

Each variant maps to a canonical PLUS vocabulary key via `DmiValue::plus_vocab_key()` (e.g., `DMI-PROHIBITED-AIMLTRAINING`). Legacy IPTC keys can be parsed back via `DmiValue::from_plus_vocab_key()`.

### TDMRep Status

TDMRep (W3C Text and Data Mining Reservation Protocol) deployment artifacts (HTTP headers, `/.well-known/tdmrep.json`) are **deferred** from Release 1. StegoEggo currently emits PLUS image metadata only. Legacy `tdm:reserve_tdm` image properties are still parsed for backward compatibility diagnostics but are not emitted by default. The CLI `--tdm-reserved` flag is deprecated and now sets DMI to `ProhibitedSeeConstraints`.

### Optional: Authenticated Stego Provenance (MAC Key)

Provide a hex key for HMAC-SHA256 payload verification. Without a key, steganographic payloads use a non-cryptographic CRC32 checksum suitable for development and testing.

```rust
use stegoeggo::{ProtectionContext, ProtectionLevel};

// With MAC key — steganographic payloads are cryptographically verified
let key = vec![0xde, 0xad, 0xbe, 0xef, 0x12, 0x34, 0x56, 0x78];
let ctx = ProtectionContext::new(0.8, 42)
    .with_mac_key(key);

// Without key — same seed produces same output (checksum-based verification)
let ctx = ProtectionContext::new(0.8, 42);
```

> **Note:** `ProtectionContext::default()` uses `generate_random_seed()`, which is backed by the OS CSPRNG via the `getrandom` crate. The seed is unpredictable by design. For **reproducible** protection across runs, pass an explicit seed via `ProtectionContext::new(intensity, seed)`. In rare sandboxed environments where `getrandom` is unavailable, a time-based fallback is used and a warning is logged.

**Verification profiles:**

- **Without a MAC key** (legal-notice mode): Steganographic payload verification uses a non-cryptographic CRC32 checksum with ECC redundancy. Visible metadata markers prove intent and rights reservation. No MAC key is required for the legal-notice use case.
- **With a MAC key** (authenticated provenance mode): The library uses HMAC-SHA256 for cryptographic payload verification. This proves the hidden payload was generated by a party with the configured secret. Use this when you need cryptographic integrity for the steganographic channel.

The MAC key affects:
- Steganography payload verification (HMAC-SHA256 instead of simple checksum)

### Migration from ProtectionLevel API

The `ProtectionLevel` and `EvidenceProfile` APIs still work but are deprecated.
To migrate:

| Old API | New API |
|---------|---------|
| `process_image_bytes(&bytes, ProtectionLevel::Standard, &ctx)` | `process_request_bytes(&bytes, &request)` |
| `ctx.with_dmi(DmiValue::ProhibitedAiMlTraining)` | `RightsPolicy::ProhibitedAiMlTraining` in `ProtectionRequest` |
| `EvidenceProfile::LegalNotice` | `ProtectionPreset::LegalNotice` or `ProtectionChannels::metadata_only()` |
| `ctx.with_metadata_injection(false)` | `ProtectionChannels { rights_metadata: false, .. }` |

### Granular Control

Control individual protection components:

```rust
use stegoeggo::{LegalMetadata, ProtectionContext, ProtectionLevel};

// Minimal - stego only, no metadata
let ctx = ProtectionContext::new(0.5, 42)
    .with_metadata_injection(false);

// Full - metadata + legal claims (for owned content)
// Legal claims are auto-enabled when LegalMetadata is provided,
// but you can still pass `true` explicitly if desired.
let ctx = ProtectionContext::new(0.5, 42)
    .with_legal_metadata(LegalMetadata::new()
        .with_copyright_holder("My Company"));

// Limit maximum image dimension for processing
let ctx = ProtectionContext::new(0.5, 42)
    .with_max_dimension(2048);
```

### Performance Tuning

For latency-sensitive deployments:

```rust,ignore
use stegoeggo::{
    process_image_bytes_with_warnings, ImageOutputFormat, ProtectionContext, ProtectionLevel,
};

// Optimized context for WAF edge deployment
let seed = 42u64;
let mac_key = b"your-secret-key".to_vec();
let input_bytes = std::fs::read("image.png").unwrap();
let ctx = ProtectionContext::new(0.5, seed)
    .with_format(ImageOutputFormat::Png)      // or Jpeg for smaller files
    .with_mac_key(mac_key)                     // for authenticated provenance
    .with_stego_redundancy(2)                  // 1-10, lower = faster
    .with_jpeg_quality(85)                     // 1-100, lower = faster
    .with_progressive_jpeg(true);              // Progressive rendering for web

// Process and serve directly
let (protected_bytes, warnings) =
    process_image_bytes_with_warnings(&input_bytes, ProtectionLevel::Standard, &ctx).unwrap();

// Reverse proxies should log warnings and may enforce policy before serving.
for warning in &warnings {
    eprintln!("Warning: {warning}");
}
```

**Configuration Guide:**

| Parameter | Default | Range | Effect on Latency |
|----------|---------|-------|-------------------|
| `stego_redundancy` | derived | 1-10 | Higher = more robust verification, slower. Default: derived from `intensity` (1 below 0.3, 2 from 0.3 to 0.7, 3 above) |
| `jpeg_quality` | 90 | 1-100 | Higher = larger files, same speed |
| `progressive_jpeg` | false | bool | Progressive = faster perceived load |
| `output_format` | PNG | PNG/JPEG/WebP | JPEG = smallest files |

### Reverse Proxy Integration Contract

`stegoeggo` owns steganographic embedding and metadata injection. The reverse proxy
should own cache lookup/storage, request byte limits, concurrency limits,
timeouts, and serving policy.

Recommended hot-path shape:

```rust,ignore
use stegoeggo::{
    process_image_bytes_with_warnings, ImageOutputFormat, ProtectionContext, ProtectionLevel,
    ProtectionWarning,
};

let seed = 42u64;
let mac_key = b"your-secret-key".to_vec();
let origin_bytes = std::fs::read("image.png").unwrap();
let ctx = ProtectionContext::new(0.5, seed)
    .with_format(ImageOutputFormat::Png)
    .with_mac_key(mac_key)
    .with_max_dimension(4096)
    .with_stego_redundancy(1);

let (protected, warnings) =
    process_image_bytes_with_warnings(&origin_bytes, ProtectionLevel::Standard, &ctx).unwrap();

if warnings.iter().any(|w| matches!(w, ProtectionWarning::MissingMacKey)) {
    // Production policy should normally reject this configuration.
}
```

Use `severity_for_profile()` to determine if a warning is actionable for your evidence model:

```rust,ignore
use stegoeggo::{process_image_bytes_with_warnings, EvidenceProfile, ProtectionContext, ProtectionLevel};

let profile = EvidenceProfile::AuthenticatedProvenance;
let (protected, warnings) =
    process_image_bytes_with_warnings(&origin_bytes, ProtectionLevel::Standard, &ctx).unwrap();

for w in &warnings {
    match w.severity_for_profile(profile) {
        WarningSeverity::Error => eprintln!("FATAL: {w}"),
        WarningSeverity::Warning => eprintln!("WARN: {w}"),
        WarningSeverity::Info => {} // silently ignored
    }
}
```

Use `process_image_bytes_with_warnings()` rather than the `DynamicImage` API in
the proxy path. For JPEG-in/JPEG-out, this keeps protection on the byte/DCT fast
path. For PNG/WebP, the library must still decode and re-encode pixels to embed
LSB payloads, so cache protected outputs aggressively at the proxy layer.

For verification, prefer `verify_legal_notice()` for a comprehensive report of all
evidence channels. It extracts legal notice fields (copyright, creator, contact, etc.),
checks steganographic payload integrity, and returns an `EvidenceStrength` rating.
Use `verify_image_bytes_detailed()` for lower-level payload-only verification.

## CLI Usage

### Full Options Reference

```bash
stegoeggo [OPTIONS] <INPUT>

Arguments:
  <INPUT>                  Input image file(s)

Options:
  -o, --output <OUTPUT>    Output directory (batch) or file (single)
  --verify                Verify legal-notice report, evidence strength, and channels
  -l, --level <LEVEL>      Protection level: disabled, light, standard
  -p, --profile <PROFILE>  Evidence profile: legal-notice, legal-notice-stego,
                           authenticated-provenance, maximal (default: legal-notice)
  -i, --intensity <FLOAT> Protection intensity 0.0-1.0 (default: 0.5)
  -s, --seed <SEED>        Seed for reproducible results
  -f, --format <FORMAT>   Output format: png, jpg, webp (default: png)
  --stego-redundancy <N>  Stego redundancy 1-10 (default: 2). Higher = robust, lower = fast
  --jpeg-quality <N>       JPEG quality 1-100 (default: 90)
  --progressive            Use progressive JPEG encoding
  -v, --verbose            Print verbose output
  -d, --dmi <DMI>          AI-training restriction metadata (DMI value; emitted as canonical plus:DataMining)
  --metadata               Inject metadata (seed, DMI). Default: true for Light and Standard
  --legal-claims          Inject legal claims (copyright, usage terms) — only for content you own
  --copyright-holder <NAME>  Copyright holder name (e.g., 'Jane Doe' or 'Acme Corp')
  --creator <NAME>        Creator/author name (e.g., 'Jane Doe')
  --contact <EMAIL_OR_URL>  Contact email or URL for rights inquiries
  --rights-url <URL>      URL to full usage terms or license text
  --usage-terms <TEXT>    Brief usage terms summary (e.g., 'All rights reserved')
  --ai-constraints <TEXT>  AI-specific constraints (e.g., 'No training, no generation')
  --no-ai-training        Shorthand: prohibit AI/ML training and set default AI constraints
  --no-genai-training     Shorthand: prohibit generative AI training only
  --tdm-reserved          [DEPRECATED] Sets DMI ProhibitedSeeConstraints (TDMRep deferred)
  -k, --key <KEY>          Optional cryptographic key (hex string) for HMAC-SHA256 verification
  -j, --jobs <N>           Parallel jobs for batch processing (default: 1)
  --strict                 Exit with error if any warnings have error severity for the active profile
  -h, --help               Print help
  --version                Print version
```

### Examples

```bash
# Basic protection with default settings
stegoeggo photo.jpg -o photo_protected.png

# Light protection (metadata + minimal stego)
stegoeggo art.png -o art_protected.png --level light

# With custom intensity and seed
stegoeggo image.jpg -o output.png -i 0.8 -s 12345

# Convert format while protecting
stegoeggo image.png -o image.jpg -f jpg

# WAF-optimized: fast processing with progressive JPEG
stegoeggo image.png -o image.jpg -f jpg --stego-redundancy 1 --jpeg-quality 85 --progressive

# WAF-optimized: PNG output, minimal latency
stegoeggo image.png -o protected.png --stego-redundancy 1

# With legal metadata (explicit claims)
stegoeggo my_art.png -o protected.png --legal-claims --level standard

# With full legal metadata — auto-enables legal claims (no --legal-claims needed)
stegoeggo my_art.png -o protected.png \
  --copyright-holder "Jane Doe" \
  --contact "jane@example.com" \
  --rights-url "https://example.com/license" \
  --usage-terms "All rights reserved" \
  --no-ai-training

# Quick AI-training restriction
stegoeggo photo.jpg -o protected.jpg --no-genai-training

# With cryptographic key for authenticated provenance
stegoeggo image.png -o output.png --key a1b2c3d4e5f6

# Verify protection
stegoeggo output.png --verify
```

### Verification

Check if an image has been protected:

```bash
stegoeggo image.png --verify
```

Output examples:

```
# Protected image
Protected: Yes
Level: standard (id: 2)
Seed: 1234567890
Intensity: 0.50
Version: 2

# Unprotected image
Protected: No
This image does not contain a protection signature.
```

## How It Works

### 1. Metadata Injection

The library injects rights-reservation and AI-training restriction metadata into image headers:

**PNG:** tEXt and iTXt chunks
- `X-Protection-Seed`: Unique identifier for reproducibility
- `plus:DataMining`: Canonical PLUS LDF DMI value (e.g., `DMI-PROHIBITED-AIMLTRAINING`)
- Copyright/Contact/License: When legal claims enabled

**JPEG:** Comment markers and XMP packets
- COM markers for text metadata
- APP1 XMP packets with `plus:DataMining` (canonical) and legacy `Iptc4xmpExt:DMI-*` (parsed only)

**WebP:** EXIF and XML chunks
- Similar metadata injection with XMP-based DMI

### 2. Steganography (Optional)

Hidden payloads embedded in images for redundant verification evidence:

**PNG/WebP:** LSB (Least Significant Bit) embedding
- Payload embedded in the lowest bits of RGB channels
- Redundant passes for verification robustness
- Uses pseudo-random pixel selection based on seed

**JPEG:** DCT-based (F5-style) embedding
- Seed embedded in quantization tables when those tables are preserved
- DCT coefficient perturbation using F5-style no-zero variant
- Pixel-domain JPEG fallback removed; JPEG protection now goes through the DCT fast path

**Payload Structure:**

The library writes **payload v3** by default. Older payload versions (v1, v2) are still extracted for backward compatibility but are never written.

*v3 core header (32 bytes):*
```
Offset  Size  Field
0       2     Magic bytes ('S', 'E')
2       1     Version (3)
3       1     Header length (includes extensions and key ID)
4       2     Total payload length
6       8     Seed (little-endian)
14      2     Intensity (0–10000, little-endian)
16      1     DMI policy byte
17      8     Content hash (truncated)
25      1     Key ID length (0–32)
26      1     Auth algorithm (0=CRC32, 1=HMAC-SHA256, 2=Ed25519)
27      1     Auth tag length
28      2     Flags
30      2     Reserved
```

The v3 format supports TLV extensions for additional metadata and optionally carries an Ed25519 signature or HMAC-SHA256 authentication tag. The `signatures` feature adds 168 bytes of overhead for embedded Ed25519 signatures (64-byte signature + 36-byte public key extension + 68-byte detached signature extension).

Without an authentication key, the payload uses 3× repetition ECC with majority-vote decoding (`src/protected/ecc.rs`) for error recovery. With a MAC key, HMAC-SHA256 provides cryptographic integrity. With the `signatures` feature, Ed25519 provides non-repudiable signing.

## Integration Architecture

### Architecture Overview

```
+-----------------+     +-----------------+     +-----------------+
|   Image Source  |---->|   Protection    |---->|   Distribution  |
+-----------------+     |   Pipeline      |     +-----------------+
                        +-----------------+
                                |
                                |  1. Inject metadata markers (primary)
                                |  2. Embed steganographic markers (redundant)
                                |  3. Add legal claims (optional)
```

## Verification

### Programmatic Verification

```rust,ignore
use stegoeggo::{SteganographyProtector, MetadataTrapProtector};
use image::DynamicImage;

let protected_bytes = std::fs::read("protected.png").unwrap();
let img = image::load_from_memory(&protected_bytes).unwrap();

// Method 1: Steganography verification
let stego = SteganographyProtector::new();
if stego.verify_payload(&img) {
    println!("Image is protected by stegoeggo");

    // Extract payload details
    if let Some(payload) = stego.extract_payload(&img) {
        println!("Protection level: {}", payload.protection_level());
        println!("Seed: {}", payload.seed());
        println!("Intensity: {:.2}", payload.intensity());
        println!("Version: {}", payload.version());
    }
}

// Method 2: Extract seed from metadata
let seed = MetadataTrapProtector::extract_seed_from_image(&protected_bytes);
if let Some(seed) = seed {
    println!("Found protection seed: {}", seed);
}

// Method 3: Comprehensive legal notice verification (recommended)
let report = stegoeggo::verify_legal_notice(&protected_bytes, b"my-mac-key");
println!("Copyright holder: {:?}", report.copyright_holder());
println!("Evidence strength: {}", report.evidence_strength());
for channel in report.channels() {
    println!("  Channel: {}", channel);
}
```

#### Evidence Strength Levels

| Level | Meaning |
|-------|---------|
| `NoNoticeFound` | No metadata or steganographic markers detected |
| `MetadataNoticeOnly` | Legal notice metadata found, no stego payload verified |
| `MetadataNoticeAndBestEffortStego` | Metadata + unauthenticated stego payload verified |
| `MetadataNoticeAndAuthenticatedProvenance` | Metadata + MAC-authenticated stego payload verified |

#### Evidence Channels

The `NoticeVerification` report lists which evidence channels were detected:
`PngText`, `PngXmp`, `JpegComment`, `JpegXmp`, `JpegIptc`, `WebPXmp`, `WebPExif`, `LsbPayload`, `DctPayload`, `QTableSeed`.

### JPEG Limitations

JPEG's lossy compression can destroy steganography payloads embedded in pixel data. This is an inherent limitation of the JPEG format and cannot be fully avoided.

**Current behavior:**
- PNG/WebP: LSB steganography is fully supported and verifiable
- JPEG: F5-style DCT steganography stores a seed in quantization tables when those tables are preserved and embeds payload bits in coefficients

**Recommendations:**
- Use PNG output format for protected images when possible
- For JPEG, a quantization-table seed is detection only; full verification relies on DCT payload integrity or metadata
- The library automatically uses the best available extraction method
- The CLI handles this and reports accordingly

**Technical note:** The library uses F5-style DCT embedding for JPEG. The quantization-table seed is useful when tables are preserved, but generic JPEG re-encoding can regenerate those tables and lose the seed.

## Robustness & Survival

Different protection layers survive different image transformations. The truth, verified by the test suite in `tests/robustness.rs` and `tests/robust_stego_matrix` (in `tests/robustness.rs`):

### What survives common transformations

| Transformation | Visible metadata (DMI, XMP, EXIF, COM) | Q-table seed (JPEG) | LSB stego payload (PNG/WebP) | DCT stego payload (JPEG) |
|----------------|-----------------------------------------|---------------------|------------------------------|--------------------------|
| **File copy / re-hosting** | Yes | Yes | Yes | Yes |
| **PNG <-> PNG re-encode** | Yes | n/a | Yes (spread-spectrum + ECC + majority vote) | n/a |
| **WebP lossless <-> WebP lossless** | Yes | n/a | Yes (same as PNG) | n/a |
| **WebP lossy (any re-encode)** | Yes | n/a | No (lossy codec destroys LSBs) | n/a |
| **JPEG -> JPEG via `image` crate encoder** | No (encoder strips COM/APP1) | No (encoder rebuilds Q-tables) | No (decoded to pixels) | No |
| **JPEG -> JPEG via `stegoeggo` fast path** | Yes (re-injected) | Yes (re-injected) | n/a | Yes (DCT coeffs preserved) |
| **Format conversion (PNG <-> JPEG) via `image` crate** | No | No | No | No |
| **Format conversion (WebP <-> JPEG) via `image` crate** | No | n/a | No | n/a |
| **Crop** | No (clipped) | No | Yes with `with_tile_size()` (>=1 intact tile) | partial (tile-aligned crops without re-encode) |
| **Resize** | No (resampled) | No | No | No |
| **Naive metadata strip** | No | n/a | Yes (still extractable) | partial |
| **LSB-preserving noise** (e.g. contrast, brightness) | Yes | n/a | Yes | n/a |
| **LSB-flipping noise** (e.g. random LSB overwrites) | Yes | n/a | No without ECC / partial with ECC | n/a |

### Encoder reality check

The `image` crate (and most general-purpose JPEG encoders) **do not preserve** COM or APP1 markers, and **rebuild standard Q-tables from scratch** on every encode. This means the visible metadata channel and the Q-table seed channel are both single-encoding only when the image passes through a generic encoder. The `stegoeggo` custom transcoder (`JpegTranscoder`) preserves DCT coefficients and re-injects metadata, but only when the image is processed through `process_image_bytes` (not through an external re-encoder).

### WebP caveat

`stegoeggo` uses LSB embedding for WebP, which only survives **lossless** WebP round-trips. The `image` crate's `WebPEncoder::new_lossless` preserves LSBs; lossy WebP re-encoding (the common web delivery path) destroys the LSB payload. If you serve protected WebP, configure your CDN to deliver lossless WebP, or convert protected output to PNG/JPEG-in-WebP-container with a tool that preserves the bitstream.

### Recommendations

- **For maximum legal evidence**: Use PNG output. The visible metadata + LSB stego payload survive almost everything except cropping, resizing, and re-encoding through a non-`stegoeggo` JPEG encoder. For crop resistance, add `.with_tile_size(64)` to the protection context — this embeds the payload in every 64x64 tile so any crop containing at least one full tile is recoverable.
- **For CDN/WAF deployment**: Use `Standard` level with PNG output. JPEG output discards the LSB payload and visible metadata on every re-compression.
- **For authenticated provenance**: Set a MAC key via `with_mac_key()` to cryptographically sign steganographic payloads.
- **For the strongest claims about evidence**: Serve the protected image directly and reference its ISCC code. Don't rely on downstream consumers to preserve any of the embedded channels.

### Honest threat model

The primary deterrence mechanism is **visible metadata injection** — canonical `plus:DataMining` rights signals, copyright, and structured COM markers. These are detectable by PLUS/XMP-aware scrapers and provide the strongest legal evidence *when preserved*. The steganographic payload is a **bonus evidence channel**: useful for proving the image was processed by this library at the point of distribution, but it is not designed to survive re-encoding through a general-purpose image pipeline. The library is a deterrent, not a forensic watermark.

## Performance

Benchmarked on Apple M4 Pro (12 cores), version 0.2.

### In-Memory Processing (`DynamicImage` path)

| Image Size | Light | Standard |
|------------|-------|----------|
| 256x256 | 0.2 ms | 0.2 ms |
| 512x512 | 0.8 ms | 0.8 ms |
| 1024x1024 | 3.2 ms | 3.1 ms |
| 2560x2560 (2K) | 18 ms | 20 ms |
| 3840x3840 (4K) | 35 ms | 40 ms |

### Bytes-in/Bytes-out Processing (production path for WAF/CDN)

PNG in / PNG out — the "maximum legal evidence" path:

| Image Size | Light | Standard |
|------------|-------|----------|
| 512x512 | 0.7 ms | 0.7 ms |
| 2560x2560 (2K) | 11 ms | 13 ms |
| 3840x3840 (4K) | 25 ms | 29 ms |

### JPEG Fast Path

JPEG-in / JPEG-out bypasses pixel decode entirely and operates directly on DCT coefficients:

| Image Size | Time |
|------------|------|
| 256x256 | **1.3 us** |
| 512x512 | 1.6 ms |

### Tiled Embedding (crop-resistant mode)

JPEG with `with_tile_size(64)`:

| Image Size | Embed | Extract |
|------------|-------|---------|
| 256x256 | 1.5 ms | 270 ms |
| 1024x1024 | 253 ms | — |

### Allocations

Standard protection at 512x512: 60 allocations, 5.7 MB peak.

### Summary

- **<1 ms** for images up to 512x512
- **<5 ms** for images up to 1024x1024
- **<30 ms** for 4K images (bytes path, Standard level)
- JPEG fast path is sub-millisecond for small images

## Technical Details

### Image Format Support

| Format | Metadata | Stego |
|--------|----------|-------|
| PNG | tEXt/iTXt | LSB |
| JPEG | COM/XMP/EXIF | DCT (F5) |
| WebP | EXIF/XML | LSB |

### ISCC Computation

The library computes ISCC-**like** (Immutable Self-Certifying Constituent Content) identifiers for content identification. **Note:** these identifiers are not guaranteed to be interoperable with the standard ISCC specification — they use a custom DCT-based perceptual hash and SHA-256 instance code. They are suitable for in-application deduplication and provenance tracking, but should not be used for cross-ISCC-tool interoperability:

```rust,ignore
use stegoeggo::{compute_content_identifiers, Iscc};

let img = image::open("image.png").unwrap();
let iscc = compute_content_identifiers(&img);

println!("Content Code: {}", iscc.content);
println!("Data Code: {}", iscc.data);
println!("Instance Code: {}", iscc.instance);
println!("Full ISCC: {}", iscc.full);
```

> **Note:** `compute_iscc()` is deprecated. Use `compute_content_identifiers()` instead.

The `Iscc` struct fields:
- `meta` — optional metadata code (not set by default)
- `content` — content-derived identifier (DCT-based perceptual hash)
- `data` — data-derived identifier (raw file hash)
- `instance` — identical to `data` (per-file identifier)
- `full` — full ISCC URI (e.g., `ISCC:...`)

### Error Handling

The library uses `thiserror` for error handling:

```rust,ignore
use stegoeggo::{Error, Result};

fn process() -> Result<image::DynamicImage> {
    // Operations that may fail
}
```

Common errors:
- `Error::ImageDecode(String)` - Failed to decode image
- `Error::ImageEncode(String)` - Failed to encode image
- `Error::Metadata(String)` - Metadata injection failure

## External References

- [PLUS License Data Format](https://www.useplus.com/) - Canonical rights metadata standard (PLUS LDF controlled-vocabulary URIs)
- [IPTC Photo Metadata Standard](https://iptc.org/standards/photo-metadata/) - Legacy DMI tag specification (still parsed for backward compatibility)
- [ISCC Project](https://iscc-project.github.io/) - Content identification standard
- [F5 Steganography](https://en.wikipedia.org/wiki/Steganography#Embedding) - DCT-based steganographic technique
- [jpeg-encoder](https://crates.io/crates/jpeg-encoder) - JPEG encoding used
- [image crate](https://image.rs/) - Image processing foundation

## Legal Notice Model

See [docs/legal_notice_model.md](docs/legal_notice_model.md) for a detailed description of the legal notice and evidence model, including what metadata channels are embedded, what transformations commonly remove notices, and operational recommendations.

## External Metadata Conformance

The conformance suite validates that protected images expose correct
rights metadata to external tools. It uses a layered approach:

1. **Fixture manifest** — machine-readable TOML manifest with SHA-256 digests, expected values, and provenance
2. **Manifest validation** — structural checks (duplicate IDs, path traversal, invalid formats/categories, SHA-256 validity) run before any fixtures are processed
3. **Internal extraction** — `verify_legal_notice()` parses the image
4. **External extraction** — ExifTool extracts metadata independently
5. **Namespace-aware XMP validation** — xmllint validates XML structure
6. **Normalized comparison** — internal and external results are compared field-by-field
7. **Coverage enforcement** — strict mode requires explicit per-category and per-format minimums
8. **Machine-readable report** — JSON output with per-check pass/fail/warn

Strict mode requires `--manifest` and evaluates per-fixture expectations from the manifest. The harness returns stable exit codes (0–5) for scripting.

### Running Conformance Checks

```bash
# Build the conformance harness
cargo build --release --bin stegoeggo-conformance

# Run all fixtures with manifest verification (requires exiftool + xmllint)
./target/release/stegoeggo-conformance \
  --fixtures tests/fixtures/conformance \
  --manifest tests/fixtures/conformance/manifest.toml \
  --strict \
  --json report.json

# Or use the shell wrapper (checks for all required tools)
./scripts/verify_metadata_conformance.sh --strict --json report.json
```

### Expected Field Visibility by Format

| Field | PNG (tEXt/XMP) | JPEG (COM/XMP) | WebP (XMP) |
|-------|-----------------|-----------------|-------------|
| Copyright | `Copyright` | `Comment: Copyright (c) ...` | `dc:rights` |
| Creator | `Creator` | `Comment: Creator: ...` | `dc:creator` |
| Usage Terms | `UsageTerms` | `Comment: UsageTerms: ...` | `xmpRights:UsageTerms` |
| Rights URL | `WebStatement` | `Comment: WebStatement: ...` | `xmpRights:WebStatement` |
| AI Constraints | `AIConstraints` | `Comment: AIConstraints: ...` | `stegoeggo:AIConstraints` |
| DMI Policy | `XMP-plus:DataMining` | `XMP-plus:DataMining` | `XMP-plus:DataMining` |

### Caveats

- ExifTool is the authoritative external parser. Other tools may not expose
  all XMP properties depending on namespace support.
- PNG tEXt `XML:com.adobe.xmp` requires ExifTool to decode — plain `xmllint`
  cannot extract XMP from PNG containers.
- JPEG COM markers are stegoeggo-specific and may not be visible in all tools.
- WebP XMP visibility depends on the tool's support for `dc:rights`,
  `dc:creator`, `xmpRights:*`, and `stegoeggo:*` namespaces.

### What Conformance Does and Does Not Prove

**Proves:**
- Protected images expose correct rights metadata to external parsers (ExifTool)
- XMP is well-formed and namespace-correct
- Internal extraction matches external extraction field-by-field
- Metadata survives re-processing (idempotence)
- Unrelated metadata is preserved through the update path
- Format writers produce semantically equivalent metadata (PNG vs JPEG vs WebP)

**Does not prove:**
- Legal enforceability of embedded rights statements
- That all external tools will parse every XMP namespace
- That metadata survives arbitrary transformations (social media re-encoding, screenshots, aggressive cropping)
- That steganographic payloads survive lossy compression
- Compliance with any specific legal jurisdiction

### Installing External Tools

The conformance suite requires `exiftool` and `xmllint`.

**macOS (Homebrew):**
```bash
brew install exiftool libxml2
```

**Ubuntu/Debian:**
```bash
sudo apt-get install libimage-exiftool-perl libxml2-utils
```

**Fedora/RHEL:**
```bash
sudo dnf install perl-Image-ExifTool libxml2
```

**Arch Linux:**
```bash
sudo pacman -S perl-image-exiftool libxml2
```

### Adding Fixtures

1. Place the image in the appropriate `tests/fixtures/conformance/<category>/` directory
2. Add an entry to `tests/fixtures/conformance/manifest.toml` with provenance, SHA-256 digest, and expected values
3. Document provenance in `tests/fixtures/conformance/README.md`
4. Verify: `cargo run --bin stegoeggo-conformance -- --fixtures tests/fixtures/conformance --manifest tests/fixtures/conformance/manifest.toml --strict`

## Migration Guide

### From v0.2 (legacy metadata)

v0.2 used `ProtectionLevel::Light` for metadata-only and `ProtectionLevel::Standard` for stego + metadata. The API is unchanged — `process_image_bytes()` and `ProtectionContext` work identically. The main difference is that v0.3 emits canonical PLUS `DataMining` properties instead of legacy `Iptc4xmpExt:DMI-*` tags.

**Action required:** None. v0.3 reads legacy metadata written by v0.2.

### From `ProtectionLevel` to `ProtectionRequest`

`ProtectionLevel` still works but is deprecated for new code. The `ProtectionRequest` API provides finer control:

```rust
// Old (still works, deprecated):
use stegoeggo::{process_image_bytes, ProtectionContext, ProtectionLevel};
let ctx = ProtectionContext::new(0.5, 42);
let out = process_image_bytes(&bytes, ProtectionLevel::Standard, &ctx)?;

// New (preferred):
use stegoeggo::{process_request_bytes, ProtectionRequest, RightsPolicy};
let request = ProtectionRequest::metadata_only()
    .with_policy(RightsPolicy::ProhibitedAiMlTraining);
let out = process_request_bytes(&bytes, &request)?;
```

### From `compute_iscc()` to `compute_content_identifiers()`

The ISCC API was renamed for accuracy:

```rust
// Old (deprecated):
let iscc = compute_iscc(&img);

// New:
let ids = compute_content_identifiers(&img);
```

### From `EvidenceProfile` to `ProtectionPreset`

`EvidenceProfile` is deprecated. Use `ProtectionPreset` for preset-based configuration:

```rust
// Old (deprecated):
use stegoeggo::EvidenceProfile;
let ctx = ProtectionContext::new(0.5, 42)
    .with_evidence_profile(EvidenceProfile::LegalNoticeWithStego);

// New:
use stegoeggo::ProtectionPreset;
let request = ProtectionRequest::from_preset(ProtectionPreset::LegalNoticeWithStego);
```

### From `with_dmi()` to `RightsPolicy`

The `with_dmi()` builder method is deprecated. Use `RightsPolicy` directly:

```rust
// Old (deprecated):
let ctx = ProtectionContext::new(0.5, 42)
    .with_dmi(DmiValue::ProhibitedAiMlTraining);

// New:
let request = ProtectionRequest::metadata_only()
    .with_policy(RightsPolicy::ProhibitedAiMlTraining);
```

### From `with_inject_legal_claims()` / `with_metadata_injection()`

These three-state options are deprecated. Use `ProtectionChannels`:

```rust
// Old (deprecated):
let ctx = ProtectionContext::new(0.5, 42)
    .with_inject_legal_claims(true)
    .with_metadata_injection(false);

// New:
use stegoeggo::ProtectionChannels;
let channels = ProtectionChannels {
    rights_metadata: false,
    ..Default::default()
};
let request = ProtectionRequest::metadata_only().with_channels(channels);
```

For the full deprecation inventory, see [DEPRECATIONS.md](DEPRECATIONS.md).

## Contributor Checklist

Before submitting a change that affects metadata output:

- [ ] Canonical writer test updated
- [ ] Legacy reader test preserved
- [ ] External fixture added or reviewed (`tests/fixtures/conformance/`)
- [ ] Namespace-aware validation passes
- [ ] Cross-format matrix passes (PNG, JPEG, WebP)
- [ ] Preservation/idempotence passes
- [ ] Strict external conformance passes (`./scripts/verify_metadata_conformance.sh --strict`)

## Architecture

```
stegoeggo
+-- ProtectionPipeline        # Main orchestration
+-- Protector trait           # Strategy pattern for protectors
|   +-- PassthroughProtector      # No-op (Disabled level)
|   +-- MetadataTrapProtector     # Metadata injection (always)
|   +-- SteganographyProtector    # LSB/DCT embedding (Light: minimal, Standard: full)
+-- ProtectionLevel          # disabled -> light -> standard
+-- LegalMetadata            # Configurable legal metadata
+-- ProtectionContext        # Configuration for protection
+-- StegoPayload             # Extracted stego data
```

**Steganography intensity by level:**
- `Disabled`: none
- `Light`: minimal — Q-table seed (JPEG) or LSB redundancy=1 (PNG/WebP)
- `Standard`: full — DCT F5 (JPEG) or LSB + ECC + spread-spectrum (PNG/WebP)

## Safety & Ethics

This library uses `#![forbid(unsafe_code)]` throughout — no `unsafe` blocks exist in the library crate. All image processing is built on safe Rust with the `image` crate.

This library is designed to protect intellectual property from unauthorized AI training. It is intended for:

- Protecting personal photos from being scraped
- Defending artist portfolios from model training
- Securing proprietary images on CDNs
- Content owners who have not licensed their work for AI training

**We do not endorse:**
- Using this library for malicious purposes
- Circumventing legitimate AI services' terms of service
- Applying restrictions to images you do not own or have rights to
- Any use that violates applicable laws

This is a defensive tool for content protection, not an offensive weapon against AI systems.

**Disclaimer**: stegoeggo provides technical mechanisms for embedding metadata and steganographic markers. It does not provide legal advice. The effectiveness of rights-reservation metadata and AI-training restrictions depends on jurisdiction, applicable law, and the specific use case. Consult a qualified attorney for legal guidance regarding intellectual property protection.

## License

MIT License - see [LICENSE](LICENSE) for details.

## Contributing

Contributions are welcome! Please ensure:

1. Tests pass: `cargo test --all-features`
2. Code is formatted: `cargo fmt --check`
3. No clippy warnings: `cargo clippy --all-targets --all-features -- -D warnings`
4. Package builds: `cargo package --workspace`