# 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 /// /// Adds a stamp (text or image) to specified pages of a PDF document. /// Task 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 { 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!** 🎉