Implemented the "PDF Stamp Operation" feature, allowing users to add text, image, or predefined stamps to PDF documents. - Added `FEATURE_7_PLAN.md` with detailed implementation plan. - Introduced enums (`StampType`, `PredefinedStampType`, `StampPlacement`) in the domain layer. - Added `AddStampAsync` method to `IPdfProcessor` interface. - Implemented `AddStampAsync` in `DevExpressPdfProcessor` using DevExpress API. - Created unit tests for `AddStampAsync` covering various scenarios. - Added `AddStampCommand` in the application layer with validation rules. - Created two new endpoints in `PdfOperationsController` for multipart and Base64 inputs. - Added DTOs for handling endpoint requests. - Wrote integration tests for endpoints to ensure correctness. - Updated `AGENTS.md` and added DevExpress API references. - Documented challenges, considerations, and estimated effort. This commit completes the "PDF Stamp Operation" feature with full test coverage and documentation.
11 KiB
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
- Text Stamp: Custom text with font, size, color, rotation
- Image Stamp: PNG/JPEG image overlay
- 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
public enum StampType
{
Text,
Image,
Predefined
}
File: DocumentOperator.Domain/Models/ValueObjects/PredefinedStampType.cs
public enum PredefinedStampType
{
Confidential,
Approved,
Draft,
Void,
ForReview
}
File: DocumentOperator.Domain/Models/ValueObjects/StampPlacement.cs
public enum StampPlacement
{
Foreground, // On top of content
Background // Behind content
}
Commit 2: Infrastructure Interface
File: DocumentOperator.Application/Common/Interfaces/IPdfProcessor.cs
Add method:
/// <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:
textrequired ifstampType == TextimageBytesrequired ifstampType == ImagepredefinedTyperequired ifstampType == PredefinedpageNumberscan be null (all pages), empty array not allowedopacitymust be 0.0-1.0rotationmust be 0-360
Commit 3: Infrastructure Implementation
File: DocumentOperator.Infrastructure/Services/PdfProcessing/DevExpressPdfProcessor.cs
DevExpress API:
- Use
PdfGraphicsto draw on page PdfGraphics.DrawString()for text stampsPdfGraphics.DrawImage()for image stamps- Set opacity via
PdfBrush.Coloralpha channel - Apply rotation via
PdfGraphics.RotateTransform()
Implementation strategy:
- Load PDF
- Validate page numbers against document page count
- For each target page:
- Get
PdfGraphicsfromPdfDocumentProcessor.CreateGraphics() - Convert coordinates if
origin == TopLeft - Apply rotation transform
- Set opacity via brush alpha
- Draw text/image
- Dispose graphics
- Get
- 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):
AddStampAsync_TextStampSinglePage_ReturnsStampedPdfAddStampAsync_TextStampAllPages_ReturnsStampedPdfAddStampAsync_ImageStampWithRotation_ReturnsStampedPdfAddStampAsync_PredefinedStampConfidential_ReturnsStampedPdfAddStampAsync_WithOpacity_ReturnsStampedPdfAddStampAsync_TopLeftOrigin_ReturnsStampedPdfAddStampAsync_EmptyStream_ThrowsBadRequestExceptionAddStampAsync_InvalidPageNumber_ThrowsBadRequestExceptionAddStampAsync_TextStampWithoutText_ThrowsBadRequestExceptionAddStampAsync_ImageStampWithoutBytes_ThrowsBadRequestExceptionAddStampAsync_InvalidOpacity_ThrowsBadRequestExceptionAddStampAsync_InvalidRotation_ThrowsBadRequestException
Commit 5: Application Layer
File: DocumentOperator.Application/AddStamp/AddStampCommand.cs
Command/Handler/Validator merged:
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:
- Multipart:
POST /api/pdf/operations/stamp(multipart/form-data) - JSON:
POST /api/pdf/operations/stamp(application/json)
DTOs:
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):
AddStampFromFile_TextStampSinglePage_ReturnsStampedPdfAddStampFromBase64_TextStampAllPages_ReturnsStampedPdfAddStampFromFile_ImageStamp_ReturnsStampedPdfAddStampFromBase64_PredefinedStamp_ReturnsStampedPdfAddStampFromBase64_WithRotationAndOpacity_ReturnsStampedPdfAddStampFromBase64_InvalidBase64_Returns400AddStampFromBase64_TextStampWithoutText_Returns400AddStampFromBase64_ImageStampWithoutImage_Returns400AddStampFromBase64_InvalidPageNumber_Returns400AddStampFromBase64_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)→PdfGraphicsPdfGraphics.DrawString(string text, PdfFont font, PdfBrush brush, RectangleF bounds)PdfGraphics.DrawImage(Image image, RectangleF bounds)PdfGraphics.RotateTransform(float angle)PdfGraphics.SetTransparency(float opacity)or useColor.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 (
CreateGraphicswithPdfGraphicsContentStreamType.Foreground) - Background: Draw before page content (
CreateGraphicswithPdfGraphicsContentStreamType.Background)
Potential Challenges
- Image format support: DevExpress may require specific image formats (PNG, JPEG). Need to validate.
- Font availability: Custom fonts may not be available on server. Default to Arial.
- Text wrapping: If text is too long, may overflow stamp bounds. Truncate or wrap?
- 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! 🎉