# Feature 7 Plan: PDF Stamp Operation

## Overview
**Priority:** Phase 2, Priority 6  
**Endpoint:** `POST /api/pdf/operations/stamp`  
**Purpose:** Add text/image stamps to PDF documents (watermarks, confidential marks, approval stamps)

---

## Requirements (from CONTROLLER_ENDPOINTS.md)

### Endpoint Specification
- **Route:** `/api/pdf/operations/stamp`
- **Method:** POST
- **Input formats:** 
  - Multipart/form-data (PDF file + stamp parameters)
  - JSON (Base64-encoded PDF + stamp parameters)
- **Output:** Stamped PDF file (application/pdf)

### Stamp Types
1. **Text Stamp:** Custom text with font, size, color, rotation
2. **Image Stamp:** PNG/JPEG image overlay
3. **Predefined Stamps:** "CONFIDENTIAL", "APPROVED", "DRAFT", "VOID" (optional)

### Stamp Parameters
- **Position:** X, Y coordinates (with Origin support: BottomLeft/TopLeft)
- **Size:** Width, Height (optional if using image natural size)
- **Rotation:** Angle in degrees (0-360)
- **Opacity:** 0.0 (transparent) to 1.0 (opaque)
- **Page range:** Single page, multiple pages, or "all pages"

---

## Implementation Plan (7 Systematic Commits)

### Commit 1: Domain Layer
**File:** `DocumentOperator.Domain/Models/ValueObjects/StampType.cs`
```csharp
public enum StampType
{
    Text,
    Image,
    Predefined
}
```

**File:** `DocumentOperator.Domain/Models/ValueObjects/PredefinedStampType.cs`
```csharp
public enum PredefinedStampType
{
    Confidential,
    Approved,
    Draft,
    Void,
    ForReview
}
```

**File:** `DocumentOperator.Domain/Models/ValueObjects/StampPlacement.cs`
```csharp
public enum StampPlacement
{
    Foreground, // On top of content
    Background  // Behind content
}
```

---

### Commit 2: Infrastructure Interface
**File:** `DocumentOperator.Application/Common/Interfaces/IPdfProcessor.cs`

Add method:
```csharp
/// <summary>
/// Adds a stamp (text or image) to specified pages of a PDF document.
/// </summary>
Task<byte[]> AddStampAsync(
    Stream pdfStream,
    StampType stampType,
    int[]? pageNumbers, // null = all pages
    (double X, double Y) position,
    (double Width, double Height)? size = null, // null = auto-size for image
    AnnotationOrigin origin = AnnotationOrigin.BottomLeft,
    string? text = null, // Required for Text stamp
    string? fontName = null, // Default: Arial
    double? fontSize = null, // Default: 12
    string? color = null, // Hex color, default: "000000" (black)
    double? opacity = null, // 0.0-1.0, default: 0.5
    double? rotation = null, // Degrees, default: 0
    StampPlacement placement = StampPlacement.Foreground,
    byte[]? imageBytes = null, // Required for Image stamp
    PredefinedStampType? predefinedType = null); // Required for Predefined stamp
```

**Validation rules:**
- `text` required if `stampType == Text`
- `imageBytes` required if `stampType == Image`
- `predefinedType` required if `stampType == Predefined`
- `pageNumbers` can be null (all pages), empty array not allowed
- `opacity` must be 0.0-1.0
- `rotation` must be 0-360

---

### Commit 3: Infrastructure Implementation
**File:** `DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs`

**DevExpress API:**
- Use `PdfGraphics` to draw on page
- `PdfGraphics.DrawString()` for text stamps
- `PdfGraphics.DrawImage()` for image stamps
- Set opacity via `PdfBrush.Color` alpha channel
- Apply rotation via `PdfGraphics.RotateTransform()`

**Implementation strategy:**
1. Load PDF
2. Validate page numbers against document page count
3. For each target page:
   - Get `PdfGraphics` from `PdfDocumentProcessor.CreateGraphics()`
   - Convert coordinates if `origin == TopLeft`
   - Apply rotation transform
   - Set opacity via brush alpha
   - Draw text/image
   - Dispose graphics
4. Save to byte array

**Private helper methods:**
- `AddTextStamp(PdfGraphics graphics, ...)`
- `AddImageStamp(PdfGraphics graphics, byte[] imageBytes, ...)`
- `AddPredefinedStamp(PdfGraphics graphics, PredefinedStampType type, ...)` (delegates to AddTextStamp with predefined text/style)
- `CalculatePageNumbers(int totalPages, int[]? requestedPages)` → returns array of zero-based indices

---

### Commit 4: Infrastructure Unit Tests
**File:** `DocumentOperator.Tests/Unit/Infrastructure/Services/PdfProcessing/DevExpressPdfProcessorTests.cs`

**Tests (10-12):**
1. `AddStampAsync_TextStampSinglePage_ReturnsStampedPdf`
2. `AddStampAsync_TextStampAllPages_ReturnsStampedPdf`
3. `AddStampAsync_ImageStampWithRotation_ReturnsStampedPdf`
4. `AddStampAsync_PredefinedStampConfidential_ReturnsStampedPdf`
5. `AddStampAsync_WithOpacity_ReturnsStampedPdf`
6. `AddStampAsync_TopLeftOrigin_ReturnsStampedPdf`
7. `AddStampAsync_EmptyStream_ThrowsBadRequestException`
8. `AddStampAsync_InvalidPageNumber_ThrowsBadRequestException`
9. `AddStampAsync_TextStampWithoutText_ThrowsBadRequestException`
10. `AddStampAsync_ImageStampWithoutBytes_ThrowsBadRequestException`
11. `AddStampAsync_InvalidOpacity_ThrowsBadRequestException`
12. `AddStampAsync_InvalidRotation_ThrowsBadRequestException`

---

### Commit 5: Application Layer
**File:** `DocumentOperator.Application/AddStamp/AddStampCommand.cs`

**Command/Handler/Validator merged:**
```csharp
public record AddStampCommand : IRequest<byte[]>
{
    public required Stream PdfStream { get; init; }
    public required StampType StampType { get; init; }
    public int[]? PageNumbers { get; init; } // null = all pages
    public required (double X, double Y) Position { get; init; }
    public (double Width, double Height)? Size { get; init; }
    public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
    public string? Text { get; init; }
    public string? FontName { get; init; }
    public double? FontSize { get; init; }
    public string? Color { get; init; }
    public double? Opacity { get; init; }
    public double? Rotation { get; init; }
    public StampPlacement Placement { get; init; } = StampPlacement.Foreground;
    public byte[]? ImageBytes { get; init; }
    public PredefinedStampType? PredefinedType { get; init; }
}
```

**Validator:**
- Stream not null
- Text required if StampType == Text
- ImageBytes required if StampType == Image
- PredefinedType required if StampType == Predefined
- Color regex: `^[0-9A-Fa-f]{6}$`
- Opacity: 0.0-1.0
- Rotation: 0-360
- FontSize > 0 if provided

---

### Commit 6: API Endpoints + DTOs
**File:** `DocumentOperator.API/Controllers/PdfOperationsController.cs`

**Two endpoints:**
1. **Multipart:** `POST /api/pdf/operations/stamp` (multipart/form-data)
2. **JSON:** `POST /api/pdf/operations/stamp` (application/json)

**DTOs:**
```csharp
public class AddStampMultipartRequest
{
    public required IFormFile File { get; set; }
    public required StampType StampType { get; set; }
    public int[]? PageNumbers { get; set; } // Comma-separated in form: "1,3,5"
    public required double X { get; set; }
    public required double Y { get; set; }
    public double? Width { get; set; }
    public double? Height { get; set; }
    public AnnotationOrigin Origin { get; set; } = AnnotationOrigin.BottomLeft;
    public string? Text { get; set; }
    public string? FontName { get; set; }
    public double? FontSize { get; set; }
    public string? Color { get; set; }
    public double? Opacity { get; set; }
    public double? Rotation { get; set; }
    public StampPlacement Placement { get; set; } = StampPlacement.Foreground;
    public IFormFile? ImageFile { get; set; } // Optional image for Image stamp
    public PredefinedStampType? PredefinedType { get; set; }
}

public record AddStampBase64Request
{
    public required string Base64Pdf { get; init; }
    public required StampType StampType { get; init; }
    public int[]? PageNumbers { get; init; }
    public required double X { get; init; }
    public required double Y { get; init; }
    public double? Width { get; init; }
    public double? Height { get; init; }
    public AnnotationOrigin Origin { get; init; } = AnnotationOrigin.BottomLeft;
    public string? Text { get; init; }
    public string? FontName { get; init; }
    public double? FontSize { get; init; }
    public string? Color { get; init; }
    public double? Opacity { get; init; }
    public double? Rotation { get; init; }
    public StampPlacement Placement { get; init; } = StampPlacement.Foreground;
    public string? Base64Image { get; init; } // Base64-encoded image bytes
    public PredefinedStampType? PredefinedType { get; init; }
}
```

**Endpoint names:**
- `Name = "AddStampFromFile"`
- `Name = "AddStampFromBase64"`

---

### Commit 7: Integration Tests + Documentation
**File:** `DocumentOperator.Tests/Integration/API/PdfOperationsControllerTests.cs`

**Tests (8-10):**
1. `AddStampFromFile_TextStampSinglePage_ReturnsStampedPdf`
2. `AddStampFromBase64_TextStampAllPages_ReturnsStampedPdf`
3. `AddStampFromFile_ImageStamp_ReturnsStampedPdf`
4. `AddStampFromBase64_PredefinedStamp_ReturnsStampedPdf`
5. `AddStampFromBase64_WithRotationAndOpacity_ReturnsStampedPdf`
6. `AddStampFromBase64_InvalidBase64_Returns400`
7. `AddStampFromBase64_TextStampWithoutText_Returns400`
8. `AddStampFromBase64_ImageStampWithoutImage_Returns400`
9. `AddStampFromBase64_InvalidPageNumber_Returns400`
10. `AddStampFromBase64_InvalidOpacity_Returns400`

**Update AGENTS.md:**
- Test count: 101 → ~120 passed
- PdfOperationsController status: 2/3 → 3/3 endpoints (merge + annotate + stamp ALL DONE)
- Add Feature 7 to test breakdown

---

## DevExpress API References

**Key classes:**
- `PdfDocumentProcessor.CreateGraphics(int pageIndex)` → `PdfGraphics`
- `PdfGraphics.DrawString(string text, PdfFont font, PdfBrush brush, RectangleF bounds)`
- `PdfGraphics.DrawImage(Image image, RectangleF bounds)`
- `PdfGraphics.RotateTransform(float angle)`
- `PdfGraphics.SetTransparency(float opacity)` or use `Color.FromArgb(alpha, r, g, b)`
- `PdfFont.Create(string fontName, float fontSize)`
- `PdfBrush.Create(Color color)`

**Coordinate system:**
- Same as annotations: BottomLeft origin by default
- Need to convert if user specifies TopLeft

**Stamp placement:**
- Foreground: Draw after page content (`CreateGraphics` with `PdfGraphicsContentStreamType.Foreground`)
- Background: Draw before page content (`CreateGraphics` with `PdfGraphicsContentStreamType.Background`)

---

## Potential Challenges

1. **Image format support:** DevExpress may require specific image formats (PNG, JPEG). Need to validate.
2. **Font availability:** Custom fonts may not be available on server. Default to Arial.
3. **Text wrapping:** If text is too long, may overflow stamp bounds. Truncate or wrap?
4. **Performance:** Adding stamp to all pages of large PDF (500+ pages) may be slow. Consider async/streaming.

---

## Estimated Effort

- **Complexity:** Medium (similar to annotation, but with graphics drawing)
- **Time:** 4-6 hours (7 commits)
- **Test coverage:** ~20 tests (12 unit + 10 integration)
- **Expected test count after completion:** ~120 passed, 7 skipped

---

## Success Criteria

✅ All 7 commits completed systematically  
✅ Build: 0 errors, 0 warnings (ignore DevExpress trial warnings)  
✅ Tests: ~120 passed, 7 skipped, 0 failed  
✅ Swagger: Both stamp endpoints visible and testable  
✅ Documentation: AGENTS.md updated with Feature 7 status  

---

**Ready to start when you approve!** 🎉
