Files
DocumentService/FEATURE_7_PLAN.md
TekH 89436406ce Add PDF Stamp Operation feature with endpoints and tests
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.
2026-07-21 14:45:30 +02:00

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

  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

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:

  • 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:

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:

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! 🎉