├── src ├── main │ ├── resources │ │ ├── META-INF │ │ │ └── services │ │ │ │ ├── com.markit.image.ImageWatermarker │ │ │ │ ├── com.markit.pdf.draw.DrawPdfWatermarker │ │ │ │ ├── com.markit.pdf.overlay.font.FontProvider │ │ │ │ ├── com.markit.pdf.overlay.OverlayPdfWatermarker │ │ │ │ ├── com.markit.video.VideoWatermarker │ │ │ │ ├── com.markit.image.TextBasedWatermarkPainter │ │ │ │ ├── com.markit.pdf.WatermarkPdfServiceFactory │ │ │ │ ├── com.markit.video.ffmpeg.CommandExecutor │ │ │ │ ├── com.markit.image.ImageBasedWatermarkPainter │ │ │ │ ├── com.markit.pdf.overlay.trademark.TrademarkService │ │ │ │ ├── com.markit.pdf.overlay.ImageBasedOverlayWatermarker │ │ │ │ ├── com.markit.pdf.overlay.TextBasedOverlayWatermarker │ │ │ │ ├── com.markit.pdf.overlay.opacity.GraphicsStateManager │ │ │ │ ├── com.markit.video.ffmpeg.filters.FilterChainBuilder │ │ │ │ ├── com.markit.pdf.overlay.rotation.MatrixTransformationProvider │ │ │ │ └── com.markit.video.ffmpeg.filters.FilterStepBuilder │ │ └── font │ │ │ └── a3arialrusnormal.ttf │ └── java │ │ └── com │ │ └── markit │ │ ├── api │ │ ├── positioning │ │ │ ├── Coordinates.kt │ │ │ ├── package-info.java │ │ │ ├── WatermarkPosition.java │ │ │ ├── WatermarkPositionCoordinates.kt │ │ │ └── PositionCoordinates.kt │ │ ├── package-info.java │ │ ├── formats │ │ │ ├── package-info.java │ │ │ ├── video │ │ │ │ ├── WatermarkVideoService.java │ │ │ │ └── WatermarkVideoBuilder.java │ │ │ ├── image │ │ │ │ ├── WatermarkImageService.java │ │ │ │ └── WatermarkImageBuilder.java │ │ │ └── pdf │ │ │ │ ├── WatermarkPDFService.java │ │ │ │ └── WatermarkPDFBuilder.java │ │ ├── builders │ │ │ ├── package-info.java │ │ │ ├── TextBasedWatermarkBuilder.java │ │ │ ├── PositionStepBuilder.java │ │ │ ├── VisualWatermarkBuilder.java │ │ │ ├── BaseWatermarkBuilder.java │ │ │ └── DefaultVisualWatermarkBuilder.java │ │ ├── WatermarkingMethod.kt │ │ ├── WatermarkProcessor.java │ │ ├── Font.kt │ │ ├── WatermarkService.java │ │ ├── WatermarkAttributes.kt │ │ └── DefaultWatermarkService.java │ │ ├── pdf │ │ ├── package-info.java │ │ ├── overlay │ │ │ ├── package-info.java │ │ │ ├── font │ │ │ │ ├── package-info.java │ │ │ │ ├── FontProvider.java │ │ │ │ └── DefaultFontProvider.java │ │ │ ├── rotation │ │ │ │ ├── TransformationType.java │ │ │ │ ├── MatrixTransformationProvider.java │ │ │ │ └── DefaultMatrixTransformationProvider.java │ │ │ ├── opacity │ │ │ │ ├── GraphicsStateManager.java │ │ │ │ └── DefaultGraphicsStateManager.java │ │ │ ├── positioning │ │ │ │ ├── WatermarkPositioner.java │ │ │ │ └── OverlayMethodPositionCoordinates.java │ │ │ ├── trademark │ │ │ │ ├── TrademarkService.java │ │ │ │ └── DefaultTrademarkService.java │ │ │ ├── TextBasedOverlayWatermarker.java │ │ │ ├── OverlayPdfWatermarker.java │ │ │ ├── ImageBasedOverlayWatermarker.java │ │ │ ├── DefaultOverlayPdfWatermarker.java │ │ │ ├── DefaultImageBasedOverlayWatermarker.java │ │ │ └── DefaultTextBasedOverlayWatermarker.java │ │ ├── draw │ │ │ ├── package-info.java │ │ │ ├── DrawPdfWatermarker.java │ │ │ └── DefaultDrawPdfWatermarker.java │ │ ├── PdfWatermarkProcessor.java │ │ ├── DefaultWatermarkPdfServiceFactory.java │ │ ├── WatermarkPdfServiceFactory.java │ │ ├── WatermarkPdfService.java │ │ └── DefaultWatermarkPdfService.java │ │ ├── image │ │ ├── package-info.java │ │ ├── TextBasedWatermarkPainter.java │ │ ├── ImageBasedWatermarkPainter.java │ │ ├── WatermarkPositioner.java │ │ ├── ImageWatermarker.java │ │ ├── ImageTypeDetector.java │ │ ├── positioning │ │ │ └── DrawMethodPositionCoordinates.java │ │ ├── ImageConverter.java │ │ ├── DefaultImageBasedWatermarkPainter.java │ │ ├── DefaultImageWatermarker.java │ │ └── DefaultTextBasedWatermarkPainter.java │ │ ├── servicelocator │ │ ├── package-info.java │ │ ├── Prioritizable.java │ │ ├── DefaultServiceLocator.java │ │ └── ServiceFactory.java │ │ ├── WatermarkApplication.java │ │ ├── exceptions │ │ ├── package-info.java │ │ ├── ExecutorNotFoundException.kt │ │ ├── InvalidPDFFileException.kt │ │ ├── AsyncWatermarkPdfException.kt │ │ ├── UnsupportedFileTypeException.kt │ │ ├── WatermarkingException.kt │ │ ├── ConvertBufferedImageToBytesException.kt │ │ ├── ClosePDFDocumentException.kt │ │ └── ConvertBytesToBufferedImageException.kt │ │ ├── video │ │ ├── package-info.java │ │ ├── ffmpeg │ │ │ ├── package-info.java │ │ │ ├── probes │ │ │ │ ├── package-info.java │ │ │ │ ├── VideoDimensions.kt │ │ │ │ └── VideoInfoExtractor.java │ │ │ ├── filters │ │ │ │ ├── package-info.java │ │ │ │ ├── FilterStepType.kt │ │ │ │ ├── FilterResult.kt │ │ │ │ ├── FilterStepAttributes.kt │ │ │ │ ├── FilterChainBuilder.java │ │ │ │ ├── FilterStepBuilder.java │ │ │ │ ├── FilterStepBuilderFactory.java │ │ │ │ ├── DefaultFilterChainBuilder.java │ │ │ │ ├── TextFilterStepBuilder.java │ │ │ │ └── OverlayFilterStepBuilder.java │ │ │ ├── CommandExecutor.java │ │ │ ├── FFmpegVideoWatermarker.java │ │ │ └── FFmpegCommandExecutor.java │ │ └── VideoWatermarker.java │ │ └── utils │ │ └── ValidationUtils.java └── test │ ├── resources │ ├── logo.png │ ├── image.JPG │ └── video.mp4 │ └── java │ └── com │ └── markit │ ├── utils │ └── FileUtils.kt │ ├── image │ └── WatermarkJpegText.kt │ ├── pdf │ ├── EmptyTextWatermarkExceptionTest.kt │ ├── LandscapePageOrientationTextBasedTest.kt │ ├── WatermarkPdfTest.kt │ ├── PdfPageRotationTextBasedTest.kt │ ├── ComplexWatermarkingTest.kt │ └── ImageBasedWatermarkTest.kt │ └── video │ └── VideoWatermarkingTest.kt ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ ├── mvn.yml │ └── bug-reproduction-instructions.yml ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── README.md └── pom.xml /src/main/resources/META-INF/services/com.markit.image.ImageWatermarker: -------------------------------------------------------------------------------- 1 | com.markit.image.DefaultImageWatermarker -------------------------------------------------------------------------------- /src/test/resources/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegCheban/WaterMarkIt/HEAD/src/test/resources/logo.png -------------------------------------------------------------------------------- /src/test/resources/image.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegCheban/WaterMarkIt/HEAD/src/test/resources/image.JPG -------------------------------------------------------------------------------- /src/test/resources/video.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegCheban/WaterMarkIt/HEAD/src/test/resources/video.mp4 -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.draw.DrawPdfWatermarker: -------------------------------------------------------------------------------- 1 | com.markit.pdf.draw.DefaultDrawPdfWatermarker -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.font.FontProvider: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.font.DefaultFontProvider -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.OverlayPdfWatermarker: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.DefaultOverlayPdfWatermarker -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.video.VideoWatermarker: -------------------------------------------------------------------------------- 1 | com.markit.video.ffmpeg.FFmpegVideoWatermarker 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.image.TextBasedWatermarkPainter: -------------------------------------------------------------------------------- 1 | com.markit.image.DefaultTextBasedWatermarkPainter 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.WatermarkPdfServiceFactory: -------------------------------------------------------------------------------- 1 | com.markit.pdf.DefaultWatermarkPdfServiceFactory 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.video.ffmpeg.CommandExecutor: -------------------------------------------------------------------------------- 1 | com.markit.video.ffmpeg.FFmpegCommandExecutor 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.image.ImageBasedWatermarkPainter: -------------------------------------------------------------------------------- 1 | com.markit.image.DefaultImageBasedWatermarkPainter 2 | 3 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.trademark.TrademarkService: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.trademark.DefaultTrademarkService -------------------------------------------------------------------------------- /src/main/java/com/markit/api/positioning/Coordinates.kt: -------------------------------------------------------------------------------- 1 | package com.markit.api.positioning 2 | 3 | data class Coordinates(val x: Int, val y: Int) -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.ImageBasedOverlayWatermarker: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.DefaultImageBasedOverlayWatermarker -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.TextBasedOverlayWatermarker: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.DefaultTextBasedOverlayWatermarker -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.opacity.GraphicsStateManager: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.opacity.DefaultGraphicsStateManager -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.video.ffmpeg.filters.FilterChainBuilder: -------------------------------------------------------------------------------- 1 | com.markit.video.ffmpeg.filters.DefaultFilterChainBuilder -------------------------------------------------------------------------------- /src/main/resources/font/a3arialrusnormal.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OlegCheban/WaterMarkIt/HEAD/src/main/resources/font/a3arialrusnormal.ttf -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "maven" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" -------------------------------------------------------------------------------- /src/main/java/com/markit/api/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Public API for watermark configuration and processing services. 3 | */ 4 | package com.markit.api; -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * PDF watermarking services including draw and overlay strategies. 3 | */ 4 | package com.markit.pdf; -------------------------------------------------------------------------------- /src/main/java/com/markit/image/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Image watermarking painters, converters, and orchestration services. 3 | */ 4 | package com.markit.image; -------------------------------------------------------------------------------- /src/main/java/com/markit/servicelocator/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Lightweight service locator and prioritization SPI. 3 | */ 4 | package com.markit.servicelocator; -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.pdf.overlay.rotation.MatrixTransformationProvider: -------------------------------------------------------------------------------- 1 | com.markit.pdf.overlay.rotation.DefaultMatrixTransformationProvider -------------------------------------------------------------------------------- /src/main/java/com/markit/WatermarkApplication.java: -------------------------------------------------------------------------------- 1 | package com.markit; 2 | 3 | public class WatermarkApplication { 4 | public static void main(String[] args) { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Exception hierarchy for watermarking operations and IO conversions. 3 | */ 4 | package com.markit.exceptions; -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Overlay-mode PDF watermarking services and utilities. 3 | */ 4 | package com.markit.pdf.overlay; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Video watermarking services using FFmpeg filters and overlays. 3 | */ 4 | package com.markit.video; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Abstractions for handling watermarking across file formats. 3 | */ 4 | package com.markit.api.formats; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/draw/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Draw-mode watermarking for PDFs using direct content operations. 3 | */ 4 | package com.markit.pdf.draw; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/font/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Font provider SPI for PDF overlay watermarking. 3 | */ 4 | package com.markit.pdf.overlay.font; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/builders/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Fluent builders for configuring watermark attributes and steps. 3 | */ 4 | package com.markit.api.builders; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * FFmpeg-based video watermarking execution and command management. 3 | */ 4 | package com.markit.video.ffmpeg; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/probes/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Video metadata extraction utilities using ffprobe. 3 | */ 4 | package com.markit.video.ffmpeg.probes; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/positioning/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Shared positioning models used by multiple watermarking methods. 3 | */ 4 | package com.markit.api.positioning; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * FFmpeg filter chain builders for video watermarking. 3 | */ 4 | package com.markit.video.ffmpeg.filters; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/com.markit.video.ffmpeg.filters.FilterStepBuilder: -------------------------------------------------------------------------------- 1 | com.markit.video.ffmpeg.filters.OverlayFilterStepBuilder 2 | com.markit.video.ffmpeg.filters.TextFilterStepBuilder -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/rotation/TransformationType.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.rotation; 2 | 3 | public enum TransformationType { 4 | IMAGE_TRANSFORM, 5 | TEXT_TRANSFORM 6 | } 7 | -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/ExecutorNotFoundException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.0 6 | */ 7 | class ExecutorNotFoundException : RuntimeException() -------------------------------------------------------------------------------- /src/main/java/com/markit/api/WatermarkingMethod.kt: -------------------------------------------------------------------------------- 1 | package com.markit.api 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.0 6 | */ 7 | enum class WatermarkingMethod { 8 | DRAW, 9 | OVERLAY 10 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/InvalidPDFFileException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.3.0 6 | */ 7 | class InvalidPDFFileException(cause: Throwable) : RuntimeException(cause) -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/AsyncWatermarkPdfException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.0 6 | */ 7 | class AsyncWatermarkPdfException(cause: Throwable) : RuntimeException(cause) -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/UnsupportedFileTypeException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.0 6 | */ 7 | class UnsupportedFileTypeException(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/probes/VideoDimensions.kt: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.probes 2 | 3 | /** 4 | * 5 | * @author Oleg Cheban 6 | * @since 1.4.0 7 | */ 8 | data class VideoDimensions(val width: Int, val height: Int) 9 | -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/WatermarkingException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.0 6 | */ 7 | class WatermarkingException(message: String, cause: Throwable) : RuntimeException(message, cause) -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/ConvertBufferedImageToBytesException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.2.2 6 | */ 7 | class ConvertBufferedImageToBytesException(message: String) : RuntimeException(message) -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/ClosePDFDocumentException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.2.2 6 | */ 7 | class ClosePDFDocumentException(message: String, cause: Throwable) : RuntimeException(message, cause) 8 | -------------------------------------------------------------------------------- /src/main/java/com/markit/exceptions/ConvertBytesToBufferedImageException.kt: -------------------------------------------------------------------------------- 1 | package com.markit.exceptions 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.2.2 6 | */ 7 | class ConvertBytesToBufferedImageException(message: String) : RuntimeException(message) 8 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/FilterStepType.kt: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters 2 | 3 | /** 4 | * ffmpeg filter types using for watermarking 5 | * 6 | * @author Oleg Cheban 7 | * @since 1.4.0 8 | */ 9 | enum class FilterStepType { 10 | OVERLAY, DRAWTEXT 11 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/api/WatermarkProcessor.java: -------------------------------------------------------------------------------- 1 | package com.markit.api; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | 6 | /** 7 | * @author Oleg Cheban 8 | * @since 1.0 9 | */ 10 | @FunctionalInterface 11 | public interface WatermarkProcessor { 12 | byte[] apply(List watermarks) throws IOException; 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/positioning/WatermarkPosition.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.positioning; 2 | 3 | /** 4 | * @author Oleg Cheban 5 | * @since 1.0 6 | */ 7 | public enum WatermarkPosition { 8 | CENTER, 9 | TOP_LEFT, 10 | TOP_RIGHT, 11 | TOP_CENTER, 12 | BOTTOM_LEFT, 13 | BOTTOM_RIGHT, 14 | BOTTOM_CENTER, 15 | TILED 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/opacity/GraphicsStateManager.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.opacity; 2 | 3 | import com.markit.servicelocator.Prioritizable; 4 | import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; 5 | 6 | public interface GraphicsStateManager extends Prioritizable { 7 | PDExtendedGraphicsState createOpacityState(int opacity); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/markit/utils/ValidationUtils.java: -------------------------------------------------------------------------------- 1 | package com.markit.utils; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | 5 | /** 6 | * @author Oleg Cheban 7 | * @since 1.3.3 8 | */ 9 | public class ValidationUtils { 10 | 11 | public static boolean validateWatermarkAttributes(WatermarkAttributes watermark) { 12 | return !(watermark.getText().isEmpty() && watermark.getImage().isEmpty()); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/FilterResult.kt: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters 2 | 3 | import java.io.File 4 | 5 | /** 6 | * ffmpeg input data (filters (drawtext and overlay), files of watermarks, and labels) 7 | * 8 | * @author Oleg Cheban 9 | * @since 1.4.0 10 | */ 11 | data class FilterResult( 12 | val filter: String, 13 | val lastLabel: String, 14 | val tempImages: List = emptyList() 15 | ) -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/PdfWatermarkProcessor.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import org.apache.pdfbox.pdmodel.PDDocument; 5 | 6 | import java.io.IOException; 7 | import java.util.List; 8 | 9 | /** 10 | * @author Oleg Cheban 11 | * @since 1.0 12 | */ 13 | @FunctionalInterface 14 | interface PdfWatermarkProcessor { 15 | void apply(PDDocument document, List attributes) throws IOException; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/Font.kt: -------------------------------------------------------------------------------- 1 | package com.markit.api 2 | 3 | import org.apache.pdfbox.pdmodel.font.PDFont 4 | import org.apache.pdfbox.pdmodel.font.PDType1Font 5 | 6 | enum class Font(val pdFont: PDFont, val awtFontName: String, val boldPdFont: PDFont) { 7 | ARIAL(PDType1Font.HELVETICA, "Arial", PDType1Font.HELVETICA_BOLD), 8 | TIMES_NEW_ROMAN(PDType1Font.TIMES_ROMAN, "Times New Roman", PDType1Font.TIMES_BOLD), 9 | COURIER(PDType1Font.COURIER, "Courier New", PDType1Font.COURIER_BOLD); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/rotation/MatrixTransformationProvider.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.rotation; 2 | 3 | import com.markit.api.positioning.Coordinates; 4 | import com.markit.servicelocator.Prioritizable; 5 | import org.apache.pdfbox.util.Matrix; 6 | 7 | public interface MatrixTransformationProvider extends Prioritizable { 8 | 9 | Matrix createRotationMatrix(Coordinates coordinates, float watermarkWidth, float watermarkHeight, int rotationDegrees, TransformationType type); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/TextBasedWatermarkPainter.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | 6 | import java.awt.Graphics2D; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * The interface for text-based watermark painting 11 | * 12 | * @author Oleg Cheban 13 | * @since 1.3.5 14 | */ 15 | public interface TextBasedWatermarkPainter extends Prioritizable { 16 | 17 | void draw(Graphics2D g2d, BufferedImage sourceImage, WatermarkAttributes attr); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/FilterStepAttributes.kt: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters 2 | 3 | import java.io.File 4 | 5 | /** 6 | * 7 | * @author Oleg Cheban 8 | * @since 1.4.0 9 | */ 10 | data class FilterStepAttributes( 11 | val filter: String, 12 | val lastLabel: String, 13 | val step: Int, 14 | val empty: Boolean, 15 | val tempImages: List = emptyList() 16 | ) { 17 | constructor(filter: String, lastLabel: String, step: Int, first: Boolean) : 18 | this(filter, lastLabel, step, first, emptyList()) 19 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/image/ImageBasedWatermarkPainter.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | 6 | import java.awt.Graphics2D; 7 | import java.awt.image.BufferedImage; 8 | 9 | /** 10 | * The interface for image-based watermark painting 11 | * 12 | * @author Oleg Cheban 13 | * @since 1.3.5 14 | */ 15 | public interface ImageBasedWatermarkPainter extends Prioritizable { 16 | 17 | void draw(Graphics2D g2d, BufferedImage sourceImage, WatermarkAttributes attr); 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | !**/src/main/**/target/ 4 | !**/src/test/**/target/ 5 | 6 | ### IntelliJ IDEA ### 7 | .idea 8 | *.iws 9 | *.iml 10 | *.ipr 11 | 12 | 13 | ### Eclipse ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | 22 | ### NetBeans ### 23 | /nbproject/private/ 24 | /nbbuild/ 25 | /dist/ 26 | /nbdist/ 27 | /.nb-gradle/ 28 | build/ 29 | !**/src/main/**/build/ 30 | !**/src/test/**/build/ 31 | 32 | ### VS Code ### 33 | .vscode/ 34 | 35 | ### Mac OS ### 36 | .DS_Store 37 | .aider* 38 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/DefaultWatermarkPdfServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf; 2 | 3 | import java.util.concurrent.Executor; 4 | 5 | /** 6 | * Default factory for creating {@link DefaultWatermarkPdfService} instances. 7 | */ 8 | public class DefaultWatermarkPdfServiceFactory implements WatermarkPdfServiceFactory { 9 | 10 | @Override 11 | public WatermarkPdfService create(Executor executor) { 12 | return new DefaultWatermarkPdfService(executor); 13 | } 14 | 15 | @Override 16 | public int getPriority() { 17 | return DEFAULT_PRIORITY; 18 | } 19 | } 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/VideoWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.video; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | 6 | import java.io.File; 7 | import java.util.List; 8 | 9 | /** 10 | * Interface for adding watermarks to videos. 11 | * 12 | * @author Oleg Cheban 13 | * @since 1.4.0 14 | */ 15 | public interface VideoWatermarker extends Prioritizable { 16 | 17 | byte[] watermark(byte[] sourceVideoBytes, List attrs) throws Exception; 18 | 19 | byte[] watermark(File file, List attrs) throws Exception; 20 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/api/positioning/WatermarkPositionCoordinates.kt: -------------------------------------------------------------------------------- 1 | package com.markit.api.positioning 2 | 3 | import com.markit.api.WatermarkAttributes 4 | 5 | /** 6 | * 7 | * @author Oleg Cheban 8 | * @since 1.0 9 | */ 10 | interface WatermarkPositionCoordinates { 11 | 12 | fun center(): Coordinates 13 | 14 | fun topLeft(): Coordinates 15 | 16 | fun topRight(): Coordinates 17 | 18 | fun topCenter(): Coordinates 19 | 20 | fun bottomLeft(): Coordinates 21 | 22 | fun bottomRight(): Coordinates 23 | 24 | fun bottomCenter(): Coordinates 25 | 26 | fun tiled(attr: WatermarkAttributes): List 27 | } -------------------------------------------------------------------------------- /src/test/java/com/markit/utils/FileUtils.kt: -------------------------------------------------------------------------------- 1 | package com.markit.utils 2 | 3 | import java.io.File 4 | import java.io.InputStream 5 | import java.nio.file.Files 6 | 7 | object FileUtils { 8 | 9 | fun readFileFromClasspathAsBytes(fileName: String): ByteArray? { 10 | val classLoader = Thread.currentThread().contextClassLoader 11 | val inputStream: InputStream? = classLoader.getResourceAsStream(fileName) 12 | return inputStream?.readBytes() 13 | } 14 | 15 | fun outputFile(result: ByteArray, filename: String) { 16 | val outputFile = File(filename) 17 | Files.write(outputFile.toPath(), result) 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/opacity/DefaultGraphicsStateManager.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.opacity; 2 | 3 | import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; 4 | 5 | public class DefaultGraphicsStateManager implements GraphicsStateManager { 6 | 7 | @Override 8 | public PDExtendedGraphicsState createOpacityState(int opacity) { 9 | var transparencyState = new PDExtendedGraphicsState(); 10 | transparencyState.setNonStrokingAlphaConstant((float) (opacity / 100.0)); 11 | return transparencyState; 12 | } 13 | 14 | @Override 15 | public int getPriority() { 16 | return DEFAULT_PRIORITY; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/positioning/WatermarkPositioner.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.positioning; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Oleg Cheban 10 | * @since 1.0 11 | */ 12 | public class WatermarkPositioner { 13 | 14 | public static List defineXY( 15 | WatermarkAttributes attr, int imageWidth, int imageHeight, int watermarkWidth, int watermarkHeight) { 16 | return new OverlayMethodPositionCoordinates(imageWidth, imageHeight, watermarkWidth, watermarkHeight) 17 | .getCoordinatesForAttributes(attr); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/markit/servicelocator/Prioritizable.java: -------------------------------------------------------------------------------- 1 | package com.markit.servicelocator; 2 | 3 | /** 4 | * The interface using for the services that are created via factories, utilizing the Java ServiceLoader mechanism 5 | * It's necessary when multiple implementations of a service are loaded, and selection based on priority is required. 6 | * 7 | * @author Oleg Cheban 8 | * @since 1.3.2 9 | */ 10 | public interface Prioritizable { 11 | 12 | int DEFAULT_PRIORITY = 1; 13 | 14 | /** 15 | * Returns the priority of the implementation. 16 | * Higher values indicate higher priority. 17 | * 18 | * @return the priority of the implementation 19 | */ 20 | int getPriority(); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/WatermarkPdfServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf; 2 | 3 | import com.markit.servicelocator.Prioritizable; 4 | 5 | import java.util.concurrent.Executor; 6 | 7 | /** 8 | * Factory for creating {@link WatermarkPdfService} instances, optionally with an Executor. 9 | * 10 | * @author Oleg Cheban 11 | * @since 1.4.0 12 | */ 13 | public interface WatermarkPdfServiceFactory extends Prioritizable { 14 | 15 | /** 16 | * Create a {@link WatermarkPdfService} instance. 17 | * 18 | * @param executor optional executor to be used by the service; may be null 19 | * @return a {@link WatermarkPdfService} 20 | */ 21 | WatermarkPdfService create(Executor executor); 22 | } 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/WatermarkPositioner.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import com.markit.image.positioning.DrawMethodPositionCoordinates; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * @author Oleg Cheban 11 | * @since 1.0 12 | */ 13 | public class WatermarkPositioner { 14 | 15 | public static List defineXY( 16 | WatermarkAttributes attr, int imageWidth, int imageHeight, int watermarkWidth, int watermarkHeight) { 17 | return new DrawMethodPositionCoordinates(imageWidth, imageHeight, watermarkWidth, watermarkHeight) 18 | .getCoordinatesForAttributes(attr); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/WatermarkPdfService.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import org.apache.pdfbox.pdmodel.PDDocument; 5 | 6 | import java.io.IOException; 7 | import java.util.List; 8 | 9 | /** 10 | * watermark pdf files 11 | * 12 | * @author Oleg Cheban 13 | * @since 1.0 14 | */ 15 | public interface WatermarkPdfService { 16 | 17 | /** 18 | * Adds a text watermark to a PDF file. 19 | * 20 | * @param pdDocument The pdfbox pdf file representation to which the watermark will be applied. 21 | * @param attrs The attributes of watermark 22 | * @return A byte array representing the watermarked PDF file. 23 | */ 24 | byte[] watermark(PDDocument pdDocument, List attrs) throws IOException; 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/trademark/TrademarkService.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.trademark; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import com.markit.servicelocator.Prioritizable; 6 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * The interface for overlaying trademarks 12 | * 13 | * @author Oleg Cheban 14 | * @since 1.3.5 15 | */ 16 | public interface TrademarkService extends Prioritizable { 17 | 18 | /** 19 | * Adds a trademark 20 | * 21 | * @param contentStream pdf page content stream 22 | * @param attr a trademark has to check watermark attributes such as color, rotation, font, text, and size of text 23 | * @param c the watermark coordinates 24 | */ 25 | void overlayTrademark(PDPageContentStream contentStream, WatermarkAttributes attr, Coordinates c) throws IOException; 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/TextBasedOverlayWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | import org.apache.pdfbox.pdmodel.PDDocument; 6 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 7 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * The interface for adding text-based watermarks 13 | * 14 | * @author Oleg Cheban 15 | * @since 1.3.5 16 | */ 17 | public interface TextBasedOverlayWatermarker extends Prioritizable { 18 | 19 | /** 20 | * Adds a watermark 21 | * 22 | * @param contentStream pdf page content stream 23 | * @param pdRectangle the page boundaries in default user space units (PDF points) 24 | * @param attr the watermark attributes 25 | */ 26 | void overlay(PDDocument document, PDPageContentStream contentStream, PDRectangle pdRectangle, WatermarkAttributes attr) throws IOException; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/draw/DrawPdfWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.draw; 2 | 3 | import com.markit.servicelocator.Prioritizable; 4 | import com.markit.api.WatermarkAttributes; 5 | import com.markit.api.WatermarkingMethod; 6 | import org.apache.pdfbox.pdmodel.PDDocument; 7 | 8 | import java.io.IOException; 9 | import java.util.List; 10 | 11 | /** 12 | * An interface for adding watermarks to a PDF page. ({@link WatermarkingMethod#DRAW method} 13 | * 14 | * @author Oleg Cheban 15 | * @since 1.0 16 | */ 17 | public interface DrawPdfWatermarker extends Prioritizable { 18 | 19 | /** 20 | * Draw a text watermark to a specific page of a PDF document. 21 | * 22 | * @param document The PDF document to which the watermark will be applied. 23 | * @param pageIndex The index of the page to be watermarked (zero-based). 24 | * @param attrs The attributes of watermark 25 | */ 26 | void watermark(PDDocument document, int pageIndex, List attrs) throws IOException; 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/OverlayPdfWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.WatermarkingMethod; 5 | import com.markit.servicelocator.Prioritizable; 6 | import org.apache.pdfbox.pdmodel.PDDocument; 7 | 8 | import java.io.IOException; 9 | import java.util.List; 10 | 11 | /** 12 | * The interface for applying watermarks to a PDF page via overlay mode. ({@link WatermarkingMethod#OVERLAY method} 13 | * 14 | * @author Oleg Cheban 15 | * @since 1.0 16 | */ 17 | public interface OverlayPdfWatermarker extends Prioritizable { 18 | /** 19 | * Overlay a text watermark to a specific page of a PDF document. 20 | * 21 | * @param document The PDF document to which the watermark will be applied. 22 | * @param pageIndex The index of the page to be watermarked (zero-based). 23 | * @param attrs The attributes of watermark 24 | */ 25 | void watermark(PDDocument document, int pageIndex, List attrs) throws IOException; 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository 2 | on: 3 | release: 4 | types: [created] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - name: Set up Maven Central Repository 11 | uses: actions/setup-java@v4 12 | with: 13 | java-version: '11' 14 | distribution: 'temurin' 15 | server-id: central 16 | server-username: MAVEN_USERNAME 17 | server-password: MAVEN_PASSWORD 18 | gpg-private-key: ${{ secrets.GPG_SIGNING_KEY }} 19 | gpg-passphrase: MAVEN_GPG_PASSPHRASE 20 | - name: Set version 21 | run: mvn versions:set -DnewVersion=${{ github.event.release.tag_name }} 22 | - name: Publish package 23 | run: mvn -P release --batch-mode deploy -DskipTests 24 | env: 25 | MAVEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }} 26 | MAVEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }} 27 | MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSWORD }} -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/ImageBasedOverlayWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 6 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 7 | import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; 8 | 9 | import java.io.IOException; 10 | 11 | /** 12 | * The interface for adding image-based watermarks 13 | * 14 | * @author Oleg Cheban 15 | * @since 1.3.5 16 | */ 17 | public interface ImageBasedOverlayWatermarker extends Prioritizable { 18 | 19 | /** 20 | * Adds a watermark 21 | * 22 | * @param contentStream pdf page content stream 23 | * @param imageXObject the image of watermark 24 | * @param pdRectangle the page boundaries in default user space units (PDF points) 25 | * @param attr the watermark attributes 26 | */ 27 | void overlay(PDPageContentStream contentStream, PDImageXObject imageXObject, PDRectangle pdRectangle, WatermarkAttributes attr) throws IOException; 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/com/markit/image/WatermarkJpegText.kt: -------------------------------------------------------------------------------- 1 | package com.markit.image 2 | 3 | import com.markit.utils.FileUtils 4 | import com.markit.api.positioning.WatermarkPosition 5 | import com.markit.api.WatermarkService 6 | import org.junit.jupiter.api.Test 7 | import java.io.IOException 8 | import kotlin.test.assertNotNull 9 | import kotlin.test.assertTrue 10 | 11 | class WatermarkJpegText { 12 | 13 | @Test 14 | @Throws(IOException::class) 15 | fun `given jpeg file when apply centered image-based watermark then make watermarked jpeg`() { 16 | val result = WatermarkService.create() 17 | .watermarkImage(FileUtils.readFileFromClasspathAsBytes("image.JPG")) 18 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 19 | .size(25) 20 | .position(WatermarkPosition.CENTER).end() 21 | .opacity(10) 22 | .apply() 23 | 24 | assertNotNull(result, "The resulting byte array should not be null") 25 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/servicelocator/DefaultServiceLocator.java: -------------------------------------------------------------------------------- 1 | package com.markit.servicelocator; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * Utility class for locating and retrieving instances of a given interface type using the {@link ServiceLoader} mechanism. 7 | * This class provides a static method to find all implementations of a specified interface available in the classpath. 8 | * 9 | * @author Oleg Cheban 10 | * @since 1.3.2 11 | */ 12 | public class DefaultServiceLocator { 13 | 14 | public static List find(Class interfaceType) { 15 | List allInstances = new ArrayList<>(); 16 | final Iterator services = ServiceLoader.load(interfaceType, Thread.currentThread().getContextClassLoader()).iterator(); 17 | 18 | while (services.hasNext()) { 19 | try { 20 | final T service = services.next(); 21 | allInstances.add(service); 22 | } catch (Throwable e) { 23 | throw new RuntimeException(); 24 | } 25 | } 26 | 27 | return Collections.unmodifiableList(allInstances); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Oleg Cheban 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/ImageWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.util.List; 9 | 10 | /** 11 | * Interface for adding watermarks to images. 12 | * 13 | * @author Oleg Cheban 14 | * @since 1.0 15 | */ 16 | public interface ImageWatermarker extends Prioritizable { 17 | 18 | /** 19 | * Adds a text watermark to the given image. 20 | * 21 | * @param sourceImageBytes The image in byte array format. 22 | * @param attrs The attributes of watermark 23 | * @return A byte array representing the watermarked image. 24 | */ 25 | byte[] watermark(byte[] sourceImageBytes, List attrs) throws IOException; 26 | 27 | /** 28 | * Adds a text watermark to the given image. 29 | * 30 | * @param file The source file of image. 31 | * @param attrs The attributes of watermark 32 | * @return A byte array representing the watermarked image. 33 | */ 34 | byte[] watermark(File file, List attrs) throws IOException; 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/builders/TextBasedWatermarkBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.builders; 2 | 3 | import com.markit.api.Font; 4 | 5 | import java.awt.*; 6 | 7 | /** 8 | * Text-based watermarks builder 9 | * 10 | * @author Oleg Cheban 11 | * @since 1.3.3 12 | */ 13 | public interface TextBasedWatermarkBuilder { 14 | 15 | /** 16 | * The color of the text 17 | * 18 | * @param color The color for the text 19 | * @see Color 20 | */ 21 | TextBasedWatermarkBuilder color(Color color); 22 | 23 | /** 24 | * The font of the text 25 | * 26 | * @param font The font for the text 27 | * @see com.markit.api.Font 28 | */ 29 | TextBasedWatermarkBuilder font(Font font); 30 | 31 | /** 32 | * Makes the text bold 33 | */ 34 | TextBasedWatermarkBuilder bold(); 35 | 36 | /** 37 | * Adds a trademark symbol to the text 38 | */ 39 | TextBasedWatermarkBuilder addTrademark(); 40 | 41 | /** 42 | * Finish working with TextBasedWatermarkBuilder and back to the WatermarkBuilderType 43 | */ 44 | WatermarkBuilderType end(); 45 | 46 | } 47 | -------------------------------------------------------------------------------- /.github/workflows/mvn.yml: -------------------------------------------------------------------------------- 1 | name: mvn 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | mvn: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-22.04] 15 | java: [11,17] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: actions/setup-java@v4 19 | with: 20 | distribution: 'temurin' 21 | java-version: ${{ matrix.java }} 22 | - name: Install FFmpeg 23 | run: | 24 | sudo apt-get install -y ffmpeg 25 | - uses: actions/cache@v4 26 | with: 27 | path: ~/.m2/repository 28 | key: ${{ runner.os }}-jdk-${{ matrix.java }}-maven-${{ hashFiles('**/pom.xml') }} 29 | restore-keys: | 30 | ${{ runner.os }}-jdk-${{ matrix.java }}-maven- 31 | - run: java -version 32 | - run: mvn -version 33 | - run: mvn --errors --batch-mode clean install 34 | - name: Upload Code Coverage 35 | uses: qltysh/qlty-action/coverage@v2 36 | with: 37 | token: ${{ secrets.QLTY_COVERAGE_TOKEN }} 38 | files: target/site/jacoco/jacoco.xml 39 | add-prefix: src/main/java/ -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/video/WatermarkVideoService.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.formats.video; 2 | 3 | import com.markit.api.builders.TextBasedWatermarkBuilder; 4 | import com.markit.api.builders.VisualWatermarkBuilder; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * The Watermark Service for applying watermarks to videos 10 | * 11 | * @since 1.4.0 12 | */ 13 | public interface WatermarkVideoService { 14 | 15 | /** 16 | * Text-based watermarking method 17 | * 18 | * @param text The text for the watermark 19 | */ 20 | TextBasedWatermarkBuilder withText(String text); 21 | 22 | /** 23 | * Image-based watermarking method 24 | */ 25 | WatermarkVideoBuilder withImage(byte[] image); 26 | 27 | /** 28 | * Image-based watermarking method 29 | */ 30 | WatermarkVideoBuilder withImage(java.awt.image.BufferedImage image); 31 | 32 | /** 33 | * Image-based watermarking method 34 | */ 35 | WatermarkVideoBuilder withImage(File image); 36 | 37 | /** 38 | * The videos watermarking builder 39 | */ 40 | interface WatermarkVideoBuilder extends VisualWatermarkBuilder {} 41 | } 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/font/FontProvider.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.font; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | import org.apache.pdfbox.pdmodel.PDDocument; 6 | import org.apache.pdfbox.pdmodel.font.PDFont; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * Interface for providing fonts based on watermark attributes. 12 | * Implementations can handle different font types, languages, or custom requirements. 13 | */ 14 | public interface FontProvider extends Prioritizable { 15 | 16 | /** 17 | * Loads and returns the appropriate font for the given attributes 18 | * 19 | * @param document the PDF document to load the font into 20 | * @return the loaded font 21 | * @throws IOException if font loading fails 22 | */ 23 | PDFont loadFont(PDDocument document, WatermarkAttributes attributes) throws IOException; 24 | 25 | /** 26 | * Indicates whether this provider can handle the given attributes 27 | * 28 | * @param attributes the watermark attributes 29 | * @return true if this provider can handle the attributes 30 | */ 31 | boolean canHandle(WatermarkAttributes attributes); 32 | 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/markit/pdf/EmptyTextWatermarkExceptionTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.pdf 2 | 3 | import com.markit.api.positioning.WatermarkPosition 4 | import com.markit.api.WatermarkService 5 | import com.markit.api.WatermarkingMethod 6 | import org.apache.pdfbox.pdmodel.PDDocument 7 | import org.apache.pdfbox.pdmodel.PDPage 8 | import org.apache.pdfbox.pdmodel.common.PDRectangle 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import org.junit.jupiter.api.assertThrows 12 | import java.io.IOException 13 | 14 | class EmptyTextWatermarkExceptionTest : WatermarkPdfTest() { 15 | @BeforeEach 16 | override fun initDocument() { 17 | document = PDDocument().apply { 18 | addPage(PDPage(PDRectangle.A4)) 19 | } 20 | } 21 | 22 | @Test 23 | @Throws(IOException::class) 24 | fun `given empty withText when draw method then throw exception`() { 25 | assertThrows { 26 | WatermarkService.create() 27 | .watermarkPDF(document) 28 | .withText("").end() 29 | .position(WatermarkPosition.CENTER).end() 30 | .method(WatermarkingMethod.DRAW) 31 | .apply() 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/CommandExecutor.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg; 2 | 3 | import com.markit.servicelocator.Prioritizable; 4 | import com.markit.video.ffmpeg.filters.FilterResult; 5 | 6 | import java.io.File; 7 | 8 | /** 9 | * Executes an ffmpeg command to apply the constructed filter chain to a video. 10 | *

11 | * Implementations are responsible for invoking the ffmpeg binary (or library bindings), 12 | * wiring inputs, mapping the last label from {@link com.markit.video.ffmpeg.filters.FilterResult}, 13 | * and returning the processed video bytes. 14 | *

15 | * 16 | *

17 | * Implementations may also manage temporary files produced during chain building. 18 | *

19 | * 20 | * @author Oleg Cheban 21 | * @since 1.4.0 22 | */ 23 | public interface CommandExecutor extends Prioritizable { 24 | /** 25 | * Execute the ffmpeg process with the provided filter graph. 26 | * 27 | * @param input the input video file to process 28 | * @param data the filter graph, last label, and any temporary resources 29 | * @return the resulting video as a byte array 30 | * @throws Exception if execution fails or ffmpeg returns a non-zero exit code 31 | */ 32 | byte[] execute(File input, FilterResult data) throws Exception; 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/markit/servicelocator/ServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.markit.servicelocator; 2 | 3 | import java.util.*; 4 | 5 | /** 6 | * @author Oleg Cheban 7 | * @since 1.3.2 8 | */ 9 | public class ServiceFactory { 10 | private static final ServiceFactory instance = new ServiceFactory<>(); 11 | private final Map, T> serviceInstances = new HashMap<>(); 12 | public static ServiceFactory getInstance() { 13 | return instance; 14 | } 15 | 16 | private ServiceFactory() { 17 | } 18 | 19 | public T getService(Class clazz) { 20 | if (serviceInstances.containsKey(clazz)){ 21 | return serviceInstances.get(clazz); 22 | } 23 | 24 | final Set serviceClasses = new TreeSet<>((o1, o2) -> -1 * Integer.compare(o1.getPriority(), o2.getPriority())); 25 | serviceClasses.addAll(DefaultServiceLocator.find(clazz)); 26 | 27 | try { 28 | @SuppressWarnings("unchecked") 29 | T service = (T) serviceClasses.iterator().next().getClass().getConstructor().newInstance(); 30 | this.serviceInstances.put(clazz, service); 31 | return service; 32 | } catch (Exception e) { 33 | throw new RuntimeException(e); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/ImageTypeDetector.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import javax.imageio.ImageIO; 4 | import java.io.*; 5 | import java.util.Iterator; 6 | 7 | /** 8 | * @author Krishnanand 9 | * @since 1.4.2 10 | */ 11 | public class ImageTypeDetector { 12 | 13 | public static String detect(File file) { 14 | try (InputStream is = new FileInputStream(file)) { 15 | return detect(is); 16 | } catch (IOException e) { 17 | throw new UnsupportedOperationException("Unable to detect image type from file: " + file, e); 18 | } 19 | } 20 | 21 | public static String detect(byte[] bytes) { 22 | try (InputStream is = new ByteArrayInputStream(bytes)) { 23 | return detect(is); 24 | } catch (IOException e) { 25 | throw new UnsupportedOperationException("Unable to detect image type from byte[]", e); 26 | } 27 | } 28 | 29 | private static String detect(InputStream is) throws IOException { 30 | Iterator readers = ImageIO.getImageReaders(ImageIO.createImageInputStream(is)); 31 | if (readers.hasNext()) { 32 | return readers.next().getFormatName().toUpperCase(); 33 | } 34 | throw new UnsupportedOperationException("Unsupported or unknown image format"); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/font/DefaultFontProvider.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.font; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import org.apache.pdfbox.pdmodel.PDDocument; 5 | import org.apache.pdfbox.pdmodel.font.PDFont; 6 | import org.apache.pdfbox.pdmodel.font.PDType0Font; 7 | 8 | import java.io.IOException; 9 | import java.io.InputStream; 10 | 11 | public class DefaultFontProvider implements FontProvider { 12 | 13 | @Override 14 | public PDFont loadFont(PDDocument document, WatermarkAttributes attributes) throws IOException { 15 | final String fontPath = "font/a3arialrusnormal.ttf"; 16 | ClassLoader classloader = Thread.currentThread().getContextClassLoader(); 17 | try (InputStream fontStream = classloader.getResourceAsStream(fontPath)) { 18 | if (fontStream == null) { 19 | throw new IOException("Cyrillic font not found at path: " + fontPath); 20 | } 21 | var font = PDType0Font.load(document, fontStream); 22 | attributes.setCyrillicFont(font); 23 | return font; 24 | } 25 | } 26 | 27 | @Override 28 | public boolean canHandle(WatermarkAttributes attributes) { 29 | return attributes.isCyrillic(); 30 | } 31 | 32 | @Override 33 | public int getPriority() { 34 | return DEFAULT_PRIORITY; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/image/WatermarkImageService.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.formats.image; 2 | 3 | import com.markit.api.builders.TextBasedWatermarkBuilder; 4 | import com.markit.api.builders.VisualWatermarkBuilder; 5 | 6 | import java.awt.image.BufferedImage; 7 | import java.io.File; 8 | 9 | /** 10 | * The Watermark Service for applying watermarks to images 11 | * 12 | * @author Oleg Cheban 13 | * @since 1.3.0 14 | */ 15 | public interface WatermarkImageService { 16 | 17 | /** 18 | * Text-based watermarking method 19 | * 20 | * @param text The text for the watermark 21 | */ 22 | TextBasedWatermarkBuilder withText(String text); 23 | 24 | /** 25 | * Image-based watermarking method 26 | * 27 | * @param image the Byte array representation of the image 28 | */ 29 | WatermarkImageBuilder withImage(byte[] image); 30 | 31 | /** 32 | * Image-based watermarking method 33 | * 34 | * @param image the BufferedImage representation of the image 35 | */ 36 | WatermarkImageBuilder withImage(BufferedImage image); 37 | 38 | /** 39 | * Image-based watermarking method 40 | * 41 | * @param image the File object representing the image 42 | */ 43 | WatermarkImageBuilder withImage(File image); 44 | 45 | /** 46 | * The images watermarking builder 47 | */ 48 | interface WatermarkImageBuilder extends VisualWatermarkBuilder {} 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/FFmpegVideoWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | import com.markit.servicelocator.ServiceFactory; 6 | import com.markit.video.VideoWatermarker; 7 | import com.markit.video.ffmpeg.filters.FilterChainBuilder; 8 | import com.markit.video.ffmpeg.filters.FilterResult; 9 | 10 | import java.io.File; 11 | import java.nio.file.Files; 12 | import java.util.List; 13 | 14 | /** 15 | * 16 | * @author Oleg Cheban 17 | * @since 1.4.0 18 | */ 19 | public class FFmpegVideoWatermarker implements VideoWatermarker { 20 | 21 | @Override 22 | public byte[] watermark(byte[] sourceVideoBytes, List attrs) throws Exception { 23 | File input = Files.createTempFile("wmk-video-src", ".mp4").toFile(); 24 | Files.write(input.toPath(), sourceVideoBytes); 25 | 26 | try { 27 | return watermark(input, attrs); 28 | } finally { 29 | input.delete(); 30 | } 31 | } 32 | 33 | @Override 34 | public byte[] watermark(File file, List attrs) throws Exception { 35 | var executor = (CommandExecutor) ServiceFactory.getInstance().getService(CommandExecutor.class); 36 | var filterChainBuilder = (FilterChainBuilder) ServiceFactory.getInstance().getService(FilterChainBuilder.class); 37 | 38 | FilterResult filter = filterChainBuilder.build(file, attrs); 39 | return executor.execute(file, filter); 40 | } 41 | 42 | @Override 43 | public int getPriority() { 44 | return Prioritizable.DEFAULT_PRIORITY; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/FilterChainBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | 6 | import java.io.File; 7 | import java.util.List; 8 | 9 | /** 10 | * Builds a complete ffmpeg video filter chain for watermarking. 11 | *

12 | * Implementations translate high-level {@link com.markit.api.WatermarkAttributes} 13 | * into a concrete ffmpeg filter graph string and auxiliary data (e.g., temp 14 | * image files) represented by {@link FilterResult}. The resulting filter chain 15 | * can then be executed by a {@link com.markit.video.ffmpeg.CommandExecutor}. 16 | *

17 | * 18 | *

19 | * Implementations may inspect the source {@link java.io.File} and attributes 20 | * to decide which {@link FilterStepBuilder} steps to include (e.g. drawtext, 21 | * overlay) and how to connect labels between steps. 22 | *

23 | * 24 | * @author Oleg Cheban 25 | * @since 1.4.0 26 | */ 27 | public interface FilterChainBuilder extends Prioritizable { 28 | 29 | /** 30 | * Build a runnable filter graph for the provided video and watermark instructions. 31 | * 32 | * @param video the input video file to watermark 33 | * @param attributes ordered list of high-level watermark attributes to apply 34 | * @return a {@link FilterResult} containing the ffmpeg filter string, the last label 35 | * to map from, and any temporary image files that must be cleaned up 36 | * @throws Exception if the chain cannot be constructed (invalid attributes, IO issues, etc.) 37 | */ 38 | FilterResult build(File video, List attributes) throws Exception; 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/com/markit/video/VideoWatermarkingTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.video 2 | 3 | import com.markit.utils.FileUtils 4 | import com.markit.api.positioning.WatermarkPosition 5 | import com.markit.api.WatermarkService 6 | import org.junit.jupiter.api.Test 7 | import java.awt.Color 8 | import java.io.IOException 9 | import kotlin.test.assertTrue 10 | import kotlin.test.assertNotNull 11 | 12 | class VideoWatermarkingTest { 13 | 14 | @Test 15 | @Throws(IOException::class) 16 | fun `given video when apply several watermarks then make watermarked vidio`() { 17 | val result = WatermarkService.create() 18 | .watermarkVideo(FileUtils.readFileFromClasspathAsBytes("video.mp4")) 19 | .withText("WaterMarkIt").color(Color.RED).end() 20 | .opacity(50) 21 | .position(WatermarkPosition.CENTER).end() 22 | .size(30) 23 | .and() 24 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 25 | .position(WatermarkPosition.BOTTOM_RIGHT).end() 26 | .size(8) 27 | .and() 28 | .withText("WaterMarkIt").end() 29 | .position(WatermarkPosition.BOTTOM_LEFT).end() 30 | .size(40) 31 | .and() 32 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 33 | .position(WatermarkPosition.TOP_LEFT).end() 34 | .size(8) 35 | .apply(); 36 | 37 | assertNotNull(result, "The resulting byte array should not be null") 38 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 39 | //FileUtils.outputFile(result, "video_watermark.mp4") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/builders/PositionStepBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.builders; 2 | 3 | /** 4 | * Interface for adjusting the position of watermarks 5 | * 6 | * @author Oleg Cheban 7 | * @since 1.3.3 8 | */ 9 | public interface PositionStepBuilder { 10 | 11 | /** 12 | * Adjusts the position of the watermark relative to its default location 13 | * 14 | * @param x The horizontal offset in pixels 15 | * @param y The vertical offset in pixels 16 | */ 17 | PositionStepBuilder adjust(int x, int y); 18 | 19 | /** 20 | * Sets the vertical spacing between multiple tiled watermarks on the page. 21 | * This is only relevant when the watermark is tiled. 22 | * 23 | * @param spacing The spacing between tiles in pixels along the vertical axis. 24 | * A larger value increases the distance between adjacent watermarks vertically. 25 | * @return The current instance of {@code WatermarkPDFBuilder} for method chaining. 26 | */ 27 | PositionStepBuilder verticalSpacing(int spacing); 28 | 29 | /** 30 | * Sets the horizontal spacing between multiple tiled watermarks on the page. 31 | * This is only relevant when the watermark is tiled. 32 | * 33 | * @param spacing The spacing between tiles in pixels along the horizontal axis. 34 | * A larger value increases the distance between adjacent watermarks horizontally. 35 | * @return The current instance of {@code WatermarkPDFBuilder} for method chaining. 36 | */ 37 | PositionStepBuilder horizontalSpacing(int spacing); 38 | 39 | /** 40 | * Finish working with PositionStepBuilder and back to the WatermarkBuilderType 41 | */ 42 | WatermarkBuilderType end(); 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/FilterStepBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.Prioritizable; 5 | import com.markit.video.ffmpeg.probes.VideoDimensions; 6 | 7 | import java.util.List; 8 | 9 | /** 10 | * Builds a single step of an ffmpeg filter graph used for video watermarking. 11 | *

12 | * Each implementation handles a particular {@link FilterStepType} (e.g., text drawing 13 | * or image overlay) and produces the partial filter string, the updated last label, 14 | * and any temporary resources required for the step. 15 | *

16 | * 17 | *

18 | * Steps are typically chained by a {@link FilterChainBuilder}, which passes the 19 | * {@code lastLabel} from the previous step into the next one. 20 | *

21 | * 22 | * @author Oleg Cheban 23 | * @since 1.4.0 24 | */ 25 | public interface FilterStepBuilder extends Prioritizable { 26 | 27 | /** 28 | * Build this step's contribution to the filter graph. 29 | * 30 | * @param attrs watermark attributes relevant to this step 31 | * @param dimensions probed video dimensions for positioning and scaling 32 | * @param lastLabel the label of the previous step's output (or input stream label) 33 | * @param step sequential index of this step in the overall chain 34 | * @param isEmptyFilter whether the chain has not added any filters yet (first step) 35 | * @return {@link FilterStepAttributes} describing this step's filter fragment and state 36 | * @throws Exception if the step cannot be constructed (invalid params, IO issues, etc.) 37 | */ 38 | FilterStepAttributes build(List attrs, VideoDimensions dimensions, String lastLabel, int step, boolean isEmptyFilter) throws Exception; 39 | 40 | FilterStepType getFilterStepType(); 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/video/WatermarkVideoBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.formats.video; 2 | 3 | import com.markit.api.WatermarkProcessor; 4 | import com.markit.api.builders.DefaultVisualWatermarkBuilder; 5 | import com.markit.exceptions.WatermarkingException; 6 | import com.markit.servicelocator.ServiceFactory; 7 | import com.markit.video.VideoWatermarker; 8 | 9 | import java.io.File; 10 | 11 | public final class WatermarkVideoBuilder 12 | extends DefaultVisualWatermarkBuilder 13 | implements WatermarkVideoService, WatermarkVideoService.WatermarkVideoBuilder { 14 | 15 | public WatermarkVideoBuilder(byte[] fileBytes) { 16 | super(createWatermarkProcessor(fileBytes)); 17 | } 18 | 19 | public WatermarkVideoBuilder(File file) { 20 | super(createWatermarkProcessor(file)); 21 | } 22 | 23 | private static WatermarkProcessor createWatermarkProcessor(File file) { 24 | return watermarks -> { 25 | try { 26 | return getVideoWatermarker().watermark(file, watermarks); 27 | } catch (Exception e) { 28 | throw new WatermarkingException("Error watermarking the video", e); 29 | } 30 | }; 31 | } 32 | 33 | private static WatermarkProcessor createWatermarkProcessor(byte[] fileBytes) { 34 | return watermarks -> { 35 | try { 36 | return getVideoWatermarker().watermark(fileBytes, watermarks); 37 | } catch (Exception e) { 38 | throw new WatermarkingException("Error watermarking the video", e); 39 | } 40 | }; 41 | } 42 | 43 | private static VideoWatermarker getVideoWatermarker() { 44 | return (VideoWatermarker) ServiceFactory.getInstance().getService(VideoWatermarker.class); 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/trademark/DefaultTrademarkService.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.trademark; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 6 | import org.apache.pdfbox.util.Matrix; 7 | 8 | import java.io.IOException; 9 | 10 | /** 11 | * @author Oleg Cheban 12 | * @since 1.0 13 | */ 14 | public class DefaultTrademarkService implements TrademarkService { 15 | 16 | private static final String TRADEMARK_SYMBOL = "®"; 17 | 18 | @Override 19 | public void overlayTrademark(PDPageContentStream contentStream, WatermarkAttributes attr, Coordinates c) throws IOException { 20 | final int trademarkFontSize = attr.getSize() / 4; 21 | 22 | contentStream.beginText(); 23 | contentStream.setFont(attr.getResolvedPdfFont(), trademarkFontSize); 24 | contentStream.setNonStrokingColor(attr.getColor()); 25 | contentStream.setTextMatrix(setTransformationMatrix(c, attr.getPdfWatermarkTextWidth(), attr.getPdfWatermarkTextHeight(), attr.getRotationDegrees())); 26 | contentStream.showText(TRADEMARK_SYMBOL); 27 | contentStream.endText(); 28 | } 29 | 30 | private Matrix setTransformationMatrix(Coordinates c, float textWidth, float textHeight, int rotationDegrees) { 31 | Matrix matrix = new Matrix(); 32 | matrix.translate(c.getX() + textWidth / 2, c.getY() + textHeight / 2); 33 | rotate(matrix, rotationDegrees); 34 | matrix.translate(textWidth / 2, textHeight / 2); 35 | return matrix; 36 | } 37 | 38 | private void rotate(Matrix matrix, int rotationDegrees){ 39 | if (rotationDegrees != 0){ 40 | matrix.rotate(Math.toRadians(rotationDegrees)); 41 | } 42 | } 43 | 44 | @Override 45 | public int getPriority() { 46 | return DEFAULT_PRIORITY; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/positioning/OverlayMethodPositionCoordinates.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.positioning; 2 | 3 | import com.markit.api.positioning.Coordinates; 4 | import com.markit.api.positioning.PositionCoordinates; 5 | 6 | /** 7 | * @author Oleg Cheban 8 | * @since 1.0 9 | */ 10 | public class OverlayMethodPositionCoordinates extends PositionCoordinates { 11 | private final int EDGE_SIZE = 10; 12 | 13 | public OverlayMethodPositionCoordinates(int pageWidth, int pageHeight, int watermarkWidth, int watermarkHeight) { 14 | super(pageWidth, pageHeight, watermarkWidth, watermarkHeight); 15 | } 16 | 17 | @Override 18 | public Coordinates center() { 19 | return new Coordinates((getPageWidth() - getWatermarkWidth()) / 2,(getPageHeight() - getWatermarkHeight()) / 2); 20 | } 21 | 22 | @Override 23 | public Coordinates topLeft() { 24 | return new Coordinates(EDGE_SIZE, getPageHeight() - getWatermarkHeight() - EDGE_SIZE); 25 | } 26 | 27 | @Override 28 | public Coordinates topRight() { 29 | return new Coordinates(getPageWidth() - getWatermarkWidth() - EDGE_SIZE,getPageHeight() - getWatermarkHeight() - EDGE_SIZE); 30 | } 31 | 32 | @Override 33 | public Coordinates topCenter() { 34 | return new Coordinates((getPageWidth() - getWatermarkWidth()) / 2, getPageHeight() - getWatermarkHeight() - EDGE_SIZE); 35 | } 36 | 37 | @Override 38 | public Coordinates bottomLeft() { 39 | return new Coordinates(EDGE_SIZE, getWatermarkHeight()); 40 | } 41 | 42 | @Override 43 | public Coordinates bottomRight() { 44 | return new Coordinates(getPageWidth() - getWatermarkWidth() - EDGE_SIZE, getWatermarkHeight()); 45 | } 46 | 47 | @Override 48 | public Coordinates bottomCenter() { 49 | return new Coordinates((getPageWidth() - getWatermarkWidth()) / 2, getWatermarkHeight()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/image/WatermarkImageBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.formats.image; 2 | 3 | import com.markit.api.WatermarkProcessor; 4 | import com.markit.api.builders.DefaultVisualWatermarkBuilder; 5 | import com.markit.exceptions.WatermarkingException; 6 | import com.markit.image.ImageWatermarker; 7 | import com.markit.servicelocator.ServiceFactory; 8 | 9 | import java.io.File; 10 | 11 | /** 12 | * @author Oleg Cheban 13 | * @since 1.3.0 14 | */ 15 | public final class WatermarkImageBuilder 16 | extends DefaultVisualWatermarkBuilder 17 | implements WatermarkImageService, WatermarkImageService.WatermarkImageBuilder { 18 | 19 | public WatermarkImageBuilder(byte[] fileBytes) { 20 | super(createWatermarkProcessor(fileBytes)); 21 | } 22 | 23 | public WatermarkImageBuilder(File file) { 24 | super(createWatermarkProcessor(file)); 25 | } 26 | 27 | private static WatermarkProcessor createWatermarkProcessor(File file) { 28 | return watermarks -> { 29 | try { 30 | return getImageWatermarker().watermark(file, watermarks); 31 | } catch (Exception e) { 32 | throw new WatermarkingException("Error watermarking the image", e); 33 | } 34 | }; 35 | } 36 | 37 | private static WatermarkProcessor createWatermarkProcessor(byte[] fileBytes) { 38 | return watermarks -> { 39 | try { 40 | return getImageWatermarker().watermark(fileBytes, watermarks); 41 | } catch (Exception e) { 42 | throw new WatermarkingException("Error watermarking the image", e); 43 | } 44 | }; 45 | } 46 | 47 | private static ImageWatermarker getImageWatermarker() { 48 | return (ImageWatermarker) ServiceFactory.getInstance().getService(ImageWatermarker.class); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/positioning/DrawMethodPositionCoordinates.java: -------------------------------------------------------------------------------- 1 | package com.markit.image.positioning; 2 | 3 | import com.markit.api.positioning.Coordinates; 4 | import com.markit.api.positioning.PositionCoordinates; 5 | 6 | /** 7 | * @author Oleg Cheban 8 | * @since 1.0 9 | */ 10 | public class DrawMethodPositionCoordinates extends PositionCoordinates { 11 | private final int MIN_X_EDGE_SIZE = 10; 12 | private final int MIN_Y_EDGE_SIZE = 20; 13 | 14 | public DrawMethodPositionCoordinates(int pageWidth, int pageHeight, int watermarkWidth, int watermarkHeight) { 15 | super(pageWidth, pageHeight, watermarkWidth, watermarkHeight); 16 | } 17 | 18 | @Override 19 | public Coordinates center() { 20 | return new Coordinates((getPageWidth() - getWatermarkWidth()) / 2, (getPageHeight() - getWatermarkHeight()) / 2 ); 21 | } 22 | 23 | @Override 24 | public Coordinates topLeft() { 25 | return new Coordinates(MIN_X_EDGE_SIZE, MIN_Y_EDGE_SIZE); 26 | } 27 | 28 | @Override 29 | public Coordinates topRight() { 30 | return new Coordinates(getPageWidth() - getWatermarkWidth() - MIN_X_EDGE_SIZE, MIN_Y_EDGE_SIZE); 31 | } 32 | 33 | @Override 34 | public Coordinates topCenter() { 35 | return new Coordinates((getPageWidth() - getWatermarkWidth()) / 2, MIN_Y_EDGE_SIZE); 36 | } 37 | 38 | @Override 39 | public Coordinates bottomLeft() { 40 | return new Coordinates(MIN_X_EDGE_SIZE, getPageHeight() - getWatermarkHeight()); 41 | } 42 | 43 | @Override 44 | public Coordinates bottomRight() { 45 | return new Coordinates(getPageWidth() - getWatermarkWidth() - MIN_X_EDGE_SIZE, getPageHeight() - getWatermarkHeight()); 46 | } 47 | 48 | @Override 49 | public Coordinates bottomCenter() { 50 | return new Coordinates((getPageWidth() - getWatermarkWidth()) / 2, getPageHeight() - getWatermarkHeight()); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/builders/VisualWatermarkBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.builders; 2 | 3 | import com.markit.api.positioning.WatermarkPosition; 4 | 5 | /** 6 | * Watermark Builder 7 | * 8 | * @author Oleg Cheban 9 | * @since 1.3.3 10 | */ 11 | public interface VisualWatermarkBuilder { 12 | 13 | /** 14 | * Sets the size of the watermark 15 | */ 16 | WatermarkBuilderType size(int size); 17 | 18 | /** 19 | * Sets the opacity of the watermark 20 | */ 21 | WatermarkBuilderType opacity(int opacity); 22 | 23 | /** 24 | * Sets the rotation of the watermark 25 | */ 26 | WatermarkBuilderType rotation(int degree); 27 | 28 | /** 29 | * Defines the position of the watermark on the file 30 | * 31 | * @param watermarkPosition The position to place the watermark (e.g., CENTER, TILED). 32 | * @see WatermarkPosition 33 | */ 34 | PositionStepBuilder position(WatermarkPosition watermarkPosition); 35 | 36 | /** 37 | * Defines the position of the watermark using direct coordinates 38 | * 39 | * @param x The x-coordinate for the watermark position 40 | * @param y The y-coordinate for the watermark position 41 | */ 42 | WatermarkBuilderType position(int x, int y); 43 | 44 | /** 45 | * Enables or disables the watermark based on a specific condition 46 | * 47 | * @param condition: A boolean value that determines whether the watermark is enabled (true) or disabled (false) 48 | */ 49 | WatermarkBuilderType enableIf(boolean condition); 50 | 51 | /** 52 | * Adds another watermark configuration to the file 53 | * 54 | * @return The watermark service for configuring another watermark 55 | */ 56 | WatermarkServiceType and(); 57 | 58 | /** 59 | * Applies the watermark to the file and returns the result as a byte array 60 | * 61 | * @return A byte array representing the watermarked file 62 | */ 63 | byte[] apply(); 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/ImageConverter.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.exceptions.ConvertBufferedImageToBytesException; 4 | import com.markit.exceptions.ConvertBytesToBufferedImageException; 5 | 6 | import javax.imageio.ImageIO; 7 | import java.awt.image.BufferedImage; 8 | import java.io.ByteArrayInputStream; 9 | import java.io.ByteArrayOutputStream; 10 | import java.io.File; 11 | import java.io.IOException; 12 | import java.util.Optional; 13 | import java.util.function.Supplier; 14 | 15 | /** 16 | * @author Oleg Cheban 17 | * @since 1.0 18 | */ 19 | public class ImageConverter { 20 | private final String ERR_MSG = "I/O error during image conversion"; 21 | 22 | public BufferedImage convertToBufferedImage(byte[] imageBytes) { 23 | return convert(() -> { 24 | try { 25 | return ImageIO.read(new ByteArrayInputStream(imageBytes)); 26 | } catch (IOException e) { 27 | throw new ConvertBytesToBufferedImageException(ERR_MSG); 28 | } 29 | }); 30 | } 31 | 32 | public BufferedImage convertToBufferedImage(File file) { 33 | return convert(() -> { 34 | try { 35 | return ImageIO.read(file); 36 | } catch (IOException e) { 37 | throw new ConvertBytesToBufferedImageException(ERR_MSG); 38 | } 39 | }); 40 | } 41 | 42 | private BufferedImage convert(Supplier imageSupplier) { 43 | return Optional.ofNullable(imageSupplier.get()) 44 | .orElseThrow(() -> new ConvertBytesToBufferedImageException("Failed to convert image bytes to BufferedImage")); 45 | } 46 | 47 | public byte[] convertToByteArray(BufferedImage image, String imageType) { 48 | var baos = new ByteArrayOutputStream(); 49 | 50 | try { 51 | ImageIO.write(image, imageType, baos); 52 | } catch (IOException e) { 53 | throw new ConvertBufferedImageToBytesException(ERR_MSG); 54 | } 55 | return baos.toByteArray(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/markit/pdf/LandscapePageOrientationTextBasedTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.pdf 2 | 3 | import com.markit.api.WatermarkingMethod 4 | import com.markit.api.WatermarkService 5 | import org.apache.pdfbox.pdmodel.PDDocument 6 | import org.apache.pdfbox.pdmodel.PDPage 7 | import org.apache.pdfbox.pdmodel.common.PDRectangle 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import java.io.IOException 11 | import kotlin.test.assertTrue 12 | 13 | class LandscapePageOrientationTextBasedTest : WatermarkPdfTest() { 14 | @BeforeEach 15 | override fun initDocument() { 16 | document = PDDocument().apply { 17 | val landscapePage = PDPage(PDRectangle.A4).apply { 18 | mediaBox = PDRectangle(PDRectangle.A4.height, PDRectangle.A4.width) 19 | } 20 | addPage(landscapePage) 21 | } 22 | } 23 | 24 | @Test 25 | @Throws(IOException::class) 26 | fun `given Landscape Pdf when Draw Method then Make Watermarked Pdf`() { 27 | // When 28 | val result = WatermarkService.create() 29 | .watermarkPDF(document) 30 | .withText("Sample Watermark").end() 31 | .method(WatermarkingMethod.DRAW) 32 | .apply() 33 | 34 | // Then 35 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 36 | assertTrue(validateImageContent(result)); 37 | } 38 | 39 | @Test 40 | @Throws(IOException::class) 41 | fun `given Landscape Pdf when Overlay Method then Make Watermarked Pdf`() { 42 | // Given 43 | val watermarkText = "Sample Watermark" 44 | 45 | // When 46 | val result = WatermarkService.create() 47 | .watermarkPDF(document) 48 | .withText(watermarkText).end() 49 | .size(50) 50 | .method(WatermarkingMethod.OVERLAY) 51 | .apply() 52 | 53 | // Then 54 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 55 | assertTrue(validateWatermarkText(result, watermarkText)); 56 | } 57 | } -------------------------------------------------------------------------------- /.github/workflows/bug-reproduction-instructions.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report Reproduction Check 2 | 3 | on: 4 | issues: 5 | types: [opened] 6 | 7 | permissions: 8 | contents: read 9 | issues: write 10 | models: read 11 | 12 | jobs: 13 | reproduction-steps-check: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Fetch Issue 17 | id: issue 18 | uses: actions/github-script@v7 19 | with: 20 | script: | 21 | const issue = await github.rest.issues.get({ 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | issue_number: context.issue.number 25 | }) 26 | core.setOutput('title', issue.data.title) 27 | core.setOutput('body', issue.data.body) 28 | - name: Analyze Issue For Reproduction 29 | if: contains(join(github.event.issue.labels.*.name, ','), 'bug') 30 | id: analyze-issue 31 | uses: actions/ai-inference@v1 32 | with: 33 | model: mistral-ai/ministral-3b 34 | prompt: | 35 | Given a bug report title and text for an application, return 'pass' if there is enough information to reliably reproduce the issue — meaning the report clearly describes the steps to reproduce the problem, specifies the expected and actual behavior, and includes environment details such as browser and operating system. 36 | 37 | If any of these elements are missing or unclear, return a brief, friendly description of what is missing instead of 'pass'. 38 | 39 | --- 40 | Title: ${{ steps.issue.outputs.title }} 41 | Body: ${{ steps.issue.outputs.body }} 42 | - name: Comment On Issue 43 | if: contains(join(github.event.issue.labels.*.name, ','), 'bug') && steps.analyze-issue.outputs.response != 'pass' 44 | uses: actions/github-script@v7 45 | env: 46 | AI_RESPONSE: ${{ steps.analyze-issue.outputs.response }} 47 | with: 48 | script: | 49 | await github.rest.issues.createComment({ 50 | owner: context.repo.owner, 51 | repo: context.repo.repo, 52 | issue_number: context.issue.number, 53 | body: process.env.AI_RESPONSE 54 | }) -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/FFmpegCommandExecutor.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg; 2 | 3 | import com.markit.video.ffmpeg.filters.FilterResult; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.File; 7 | import java.io.InputStreamReader; 8 | import java.nio.file.Files; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | 12 | /** 13 | * 14 | * @author Oleg Cheban 15 | * @since 1.4.0 16 | */ 17 | public class FFmpegCommandExecutor implements CommandExecutor { 18 | 19 | @Override 20 | public byte[] execute(File input, FilterResult data) throws Exception { 21 | File output = Files.createTempFile("wmk-video-out", ".mp4").toFile(); 22 | 23 | List cmd = new ArrayList<>(); 24 | cmd.add("ffmpeg"); 25 | cmd.add("-y"); 26 | cmd.add("-i"); 27 | cmd.add(input.getAbsolutePath()); 28 | 29 | for (File img : data.getTempImages()) { 30 | cmd.add("-i"); 31 | cmd.add(img.getAbsolutePath()); 32 | } 33 | 34 | if (!data.getFilter().isEmpty()) { 35 | cmd.add("-filter_complex"); 36 | cmd.add(data.getFilter()); 37 | cmd.add("-map"); 38 | cmd.add(data.getLastLabel()); 39 | cmd.add("-map"); 40 | cmd.add("0:a?"); 41 | } 42 | 43 | cmd.add("-c:v"); 44 | cmd.add("libx264"); 45 | cmd.add("-preset"); 46 | cmd.add("veryfast"); 47 | cmd.add("-crf"); 48 | cmd.add("23"); 49 | cmd.add(output.getAbsolutePath()); 50 | 51 | ProcessBuilder pb = new ProcessBuilder(cmd); 52 | pb.redirectErrorStream(true); 53 | Process p = pb.start(); 54 | 55 | try (BufferedReader br = new BufferedReader(new InputStreamReader(p.getInputStream()))) { 56 | while (br.readLine() != null) { /* drain */ } 57 | } 58 | 59 | int code = p.waitFor(); 60 | if (code != 0) throw new RuntimeException("ffmpeg failed with code " + code); 61 | 62 | byte[] bytes = Files.readAllBytes(output.toPath()); 63 | 64 | output.delete(); 65 | 66 | for (File img : data.getTempImages()) img.delete(); 67 | 68 | return bytes; 69 | } 70 | 71 | @Override 72 | public int getPriority() { 73 | return DEFAULT_PRIORITY; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/rotation/DefaultMatrixTransformationProvider.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay.rotation; 2 | 3 | import com.markit.api.positioning.Coordinates; 4 | import org.apache.pdfbox.util.Matrix; 5 | 6 | public class DefaultMatrixTransformationProvider implements MatrixTransformationProvider { 7 | 8 | @Override 9 | public Matrix createRotationMatrix( 10 | Coordinates coordinates, 11 | float watermarkWidth, 12 | float watermarkHeight, 13 | int rotationDegrees, 14 | TransformationType type) { 15 | float translateX = coordinates.getX() + watermarkWidth / 2; 16 | float translateY = coordinates.getY() + watermarkHeight / 2; 17 | var matrix = new Matrix(); 18 | 19 | matrix.translate(translateX, translateY); 20 | 21 | matrix.rotate(Math.toRadians(rotationDegrees)); 22 | 23 | /** 24 | * Different back-transformation logic is required due to the fundamental difference 25 | * in how PDFBox handles coordinate systems for images versus text: 26 | * 27 | * For IMAGES: 28 | * - drawImage() coordinates specify the bottom-left corner of the image 29 | * - The image is drawn from this fixed point outward 30 | * - To rotate around center, we must return the coordinate system back to its 31 | * original position: translate(-translateX, -translateY) 32 | * 33 | * For TEXT: 34 | * - Text transformation matrix defines a new coordinate system for text rendering 35 | * - After translate(centerX, centerY), the origin (0,0) moves to the text center 36 | * - Text must then be positioned relative to this new rotated origin 37 | * - Therefore we translate by half dimensions from the new center: translate(-width/2, -height/2) 38 | */ 39 | 40 | switch (type) { 41 | case IMAGE_TRANSFORM: 42 | matrix.translate(-translateX, -translateY); 43 | break; 44 | case TEXT_TRANSFORM: 45 | matrix.translate(-watermarkWidth / 2, -watermarkHeight / 2); 46 | break; 47 | } 48 | 49 | return matrix; 50 | } 51 | 52 | @Override 53 | public int getPriority() { 54 | return DEFAULT_PRIORITY; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/probes/VideoInfoExtractor.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.probes; 2 | 3 | import org.jetbrains.annotations.NotNull; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.File; 7 | import java.io.IOException; 8 | import java.io.InputStreamReader; 9 | 10 | /** 11 | * 12 | * @author Oleg Cheban 13 | * @since 1.4.0 14 | */ 15 | public class VideoInfoExtractor { 16 | public static VideoDimensions getVideoDimensions(File videoFile) throws IOException, InterruptedException { 17 | Process process = getProcess(videoFile); 18 | 19 | try (BufferedReader reader = new BufferedReader( 20 | new InputStreamReader(process.getInputStream()))) { 21 | 22 | String output = reader.readLine(); 23 | int exitCode = process.waitFor(); 24 | 25 | if (exitCode != 0) { 26 | throw new RuntimeException("ffprobe failed with exit code: " + exitCode); 27 | } 28 | 29 | if (output != null && !output.trim().isEmpty()) { 30 | String[] dimensions = output.trim().split(","); 31 | if (dimensions.length == 2) { 32 | int width = Integer.parseInt(dimensions[0]); 33 | int height = Integer.parseInt(dimensions[1]); 34 | return new VideoDimensions(width, height); 35 | } 36 | } 37 | 38 | throw new RuntimeException("Could not parse video dimensions from ffprobe output: " + output); 39 | } 40 | } 41 | 42 | @NotNull 43 | private static Process getProcess(File videoFile) throws IOException { 44 | if (!videoFile.exists()) { 45 | throw new IOException("Video file does not exist: " + videoFile.getAbsolutePath()); 46 | } 47 | 48 | if (!videoFile.canRead()) { 49 | throw new IOException("Cannot read video file: " + videoFile.getAbsolutePath()); 50 | } 51 | 52 | ProcessBuilder pb = new ProcessBuilder( 53 | "ffprobe", 54 | "-v", "quiet", 55 | "-select_streams", "v:0", 56 | "-show_entries", "stream=width,height", 57 | "-of", "csv=p=0", 58 | videoFile.getAbsolutePath() 59 | ); 60 | 61 | Process process = pb.start(); 62 | return process; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/FilterStepBuilderFactory.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters; 2 | 3 | import com.markit.servicelocator.DefaultServiceLocator; 4 | 5 | import java.util.HashMap; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.stream.Collectors; 9 | 10 | /** 11 | * Factory class for retrieving FilterStepBuilder implementations based on Step type. 12 | * 13 | * @author Oleg Cheban 14 | * @since 1.4.0 15 | */ 16 | public class FilterStepBuilderFactory { 17 | 18 | private static final FilterStepBuilderFactory instance = new FilterStepBuilderFactory(); 19 | 20 | private final Map cachedBuilders = new HashMap<>(); 21 | 22 | private FilterStepBuilderFactory() { 23 | } 24 | 25 | public static FilterStepBuilderFactory getInstance() { 26 | return instance; 27 | } 28 | 29 | /** 30 | * Retrieves the highest priority FilterStepBuilder for the given Step type. 31 | * 32 | * @param filterStepType the Step type (OVERLAY or TEXT) 33 | * @return the highest priority FilterStepBuilder for the given type 34 | * @throws RuntimeException if no builder is found for the given step type 35 | */ 36 | public FilterStepBuilder getBuilder(FilterStepType filterStepType) { 37 | if (cachedBuilders.containsKey(filterStepType)) { 38 | return cachedBuilders.get(filterStepType); 39 | } 40 | 41 | // Load all FilterStepBuilder implementations 42 | List allBuilders = DefaultServiceLocator.find(FilterStepBuilder.class); 43 | 44 | // Filter by step type and sort by priority (highest first) 45 | List buildersForStep = allBuilders.stream() 46 | .filter(builder -> builder.getFilterStepType() == filterStepType) 47 | .sorted((o1, o2) -> -1 * Integer.compare(o1.getPriority(), o2.getPriority())) 48 | .collect(Collectors.toList()); 49 | 50 | if (buildersForStep.isEmpty()) { 51 | throw new RuntimeException("No FilterStepBuilder found for step type: " + filterStepType); 52 | } 53 | 54 | // Get the highest priority builder 55 | FilterStepBuilder builder = buildersForStep.get(0); 56 | cachedBuilders.put(filterStepType, builder); 57 | 58 | return builder; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/positioning/PositionCoordinates.kt: -------------------------------------------------------------------------------- 1 | package com.markit.api.positioning 2 | 3 | import com.markit.api.WatermarkAttributes 4 | 5 | /** 6 | * @author Oleg Cheban 7 | * @since 1.0 8 | */ 9 | abstract class PositionCoordinates( 10 | protected val pageWidth: Int, 11 | protected val pageHeight: Int, 12 | protected val watermarkWidth: Int, 13 | protected val watermarkHeight: Int 14 | ) : WatermarkPositionCoordinates { 15 | 16 | fun getCoordinatesForAttributes(attr: WatermarkAttributes): List { 17 | // If custom coordinates are being used, return them directly 18 | if (attr.customCoordinates) { 19 | return listOf(Coordinates(attr.positionCoordinates.x, attr.positionCoordinates.y)) 20 | } 21 | 22 | var coordinates = when (attr.position) { 23 | WatermarkPosition.CENTER -> listOf(center()) 24 | WatermarkPosition.TOP_LEFT -> listOf(topLeft()) 25 | WatermarkPosition.TOP_RIGHT -> listOf(topRight()) 26 | WatermarkPosition.TOP_CENTER -> listOf(topCenter()) 27 | WatermarkPosition.BOTTOM_LEFT -> listOf(bottomLeft()) 28 | WatermarkPosition.BOTTOM_RIGHT -> listOf(bottomRight()) 29 | WatermarkPosition.BOTTOM_CENTER -> listOf(bottomCenter()) 30 | WatermarkPosition.TILED -> tiled(attr) 31 | } 32 | 33 | // Apply position adjustment if needed 34 | if (attr.positionCoordinates.x != 0 || attr.positionCoordinates.y != 0) { 35 | coordinates = coordinates.map { 36 | Coordinates( 37 | it.x + attr.positionCoordinates.x, 38 | it.y + attr.positionCoordinates.y 39 | ) 40 | } 41 | } 42 | return coordinates 43 | } 44 | 45 | override fun tiled(attr: WatermarkAttributes): List { 46 | val numHorizontal = (pageWidth + watermarkWidth - 1) / watermarkWidth 47 | val numVertical = (pageHeight + watermarkHeight - 1) / watermarkHeight 48 | return (0 until numHorizontal).flatMap { i -> 49 | (0 until numVertical).map { j -> Coordinates( 50 | (i * watermarkWidth) + (i * attr.horizontalSpacing), 51 | (j * watermarkHeight) + (j * attr.verticalSpacing) 52 | ) 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/WatermarkService.java: -------------------------------------------------------------------------------- 1 | package com.markit.api; 2 | 3 | import com.markit.api.formats.image.WatermarkImageService; 4 | import com.markit.api.formats.pdf.WatermarkPDFService; 5 | import com.markit.api.formats.video.WatermarkVideoService; 6 | import org.apache.pdfbox.pdmodel.PDDocument; 7 | 8 | import java.io.File; 9 | import java.util.Objects; 10 | import java.util.concurrent.Executor; 11 | 12 | /** 13 | * Watermark Service for applying watermarks to different file types. 14 | * 15 | * @author Oleg Cheban 16 | * @since 1.0 17 | */ 18 | public interface WatermarkService { 19 | 20 | static FileFormatSelector create() { 21 | return new DefaultWatermarkService(); 22 | } 23 | 24 | static FileFormatSelector create(Executor executor) { 25 | Objects.requireNonNull(executor, "executor is required"); 26 | return new DefaultWatermarkService(executor); 27 | } 28 | 29 | /** 30 | * Selector that provides a watermarking service for a specific file 31 | */ 32 | interface FileFormatSelector { 33 | 34 | /** 35 | * Sets the PDF file to be watermarked using a byte array. 36 | */ 37 | WatermarkPDFService watermarkPDF(byte[] fileBytes); 38 | 39 | /** 40 | * Sets the PDF file to be watermarked using a File object. 41 | */ 42 | WatermarkPDFService watermarkPDF(File file); 43 | 44 | /** 45 | * Sets the PDF file to be watermarked using a PDDocument pdfbox object. 46 | * 47 | * @param document The PDF document to be watermarked. 48 | * @see PDDocument 49 | */ 50 | WatermarkPDFService watermarkPDF(PDDocument document); 51 | 52 | /** 53 | * @param file The image file to be watermarked. 54 | */ 55 | WatermarkImageService watermarkImage(File file); 56 | 57 | /** 58 | * @param fileBytes The byte array representing the source image file. 59 | */ 60 | WatermarkImageService watermarkImage(byte[] fileBytes); 61 | 62 | /** 63 | * Sets the video file to be watermarked using a byte array. 64 | */ 65 | WatermarkVideoService watermarkVideo(byte[] fileBytes); 66 | 67 | /** 68 | * Sets the video file to be watermarked using a File object. 69 | */ 70 | WatermarkVideoService watermarkVideo(File file); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/WatermarkAttributes.kt: -------------------------------------------------------------------------------- 1 | package com.markit.api 2 | 3 | import com.markit.api.positioning.Coordinates 4 | import com.markit.api.positioning.WatermarkPosition 5 | import org.apache.pdfbox.pdmodel.PDDocument 6 | import org.apache.pdfbox.pdmodel.font.PDFont 7 | import java.awt.Color 8 | import java.awt.image.BufferedImage 9 | import java.util.* 10 | import java.util.function.Predicate 11 | 12 | /** 13 | * @author Oleg Cheban 14 | * @since 1.0 15 | */ 16 | data class WatermarkAttributes ( 17 | var text: String = "", 18 | var image: Optional = Optional.empty(), 19 | var size: Int = 50, 20 | var color: Color = Color.BLACK, 21 | var opacity: Int = 40, 22 | var font: Font = Font.ARIAL, 23 | var dpi: Float = 300f, 24 | var trademark: Boolean = false, 25 | var rotationDegrees: Int = 0, 26 | var method: WatermarkingMethod = WatermarkingMethod.DRAW, 27 | var position: WatermarkPosition = WatermarkPosition.CENTER, 28 | var positionCoordinates: Coordinates = Coordinates(0, 0), 29 | var customCoordinates: Boolean = false, 30 | var verticalSpacing: Int = 50, 31 | var horizontalSpacing: Int = 50, 32 | var documentPredicate: Predicate = Predicate { true }, 33 | var pagePredicate: Predicate = Predicate { true }, 34 | var visible: Boolean = true, 35 | var isBold: Boolean = false, 36 | var adjustTextSizeCf: Float = 2.5f, 37 | var cyrillicFont: PDFont? = null 38 | ) { 39 | //virtual attributes 40 | val isTextWatermark: Boolean 41 | get() = text.isNotEmpty() && visible 42 | 43 | val isImageWatermark: Boolean 44 | get() = image.isPresent && visible 45 | 46 | val isCyrillic: Boolean 47 | get() = text.any { Character.UnicodeBlock.of(it) == Character.UnicodeBlock.CYRILLIC } 48 | 49 | val resolvedPdfFont 50 | get() = when { 51 | isCyrillic -> requireNotNull(cyrillicFont) { "Cyrillic font must be provided for Cyrillic text" } 52 | isBold -> font.boldPdFont 53 | else -> font.pdFont 54 | } 55 | 56 | val pdfWatermarkTextWidth: Float 57 | get() = resolvedPdfFont.getStringWidth(text) / 1000f * size / adjustTextSizeCf 58 | 59 | val pdfWatermarkTextHeight: Float 60 | get() = resolvedPdfFont.fontDescriptor.capHeight / 1000f * size / adjustTextSizeCf 61 | 62 | val pdfTextSize: Float 63 | get() = size / adjustTextSizeCf 64 | 65 | val imageTextSize: Float 66 | get() = size * 1.7f 67 | } -------------------------------------------------------------------------------- /src/test/java/com/markit/pdf/WatermarkPdfTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.pdf 2 | 3 | import org.apache.pdfbox.pdmodel.PDDocument 4 | import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject 5 | import org.apache.pdfbox.text.PDFTextStripper 6 | import org.junit.jupiter.api.AfterEach 7 | import java.io.ByteArrayInputStream 8 | import java.io.IOException 9 | 10 | abstract class WatermarkPdfTest { 11 | 12 | protected lateinit var document: PDDocument 13 | 14 | abstract fun initDocument(); 15 | 16 | @AfterEach 17 | fun closeDocument() { 18 | if (::document.isInitialized) { 19 | document.close() 20 | } 21 | } 22 | 23 | /** 24 | * Validates that a PDF byte array contains the expected watermark text 25 | */ 26 | fun validateWatermarkText(pdfBytes: ByteArray, expectedText: String, ignoreCase: Boolean = true): Boolean { 27 | return try { 28 | val document = PDDocument.load(ByteArrayInputStream(pdfBytes)) 29 | document.use { doc -> 30 | val textStripper = PDFTextStripper() 31 | textStripper.startPage = 1 32 | textStripper.endPage = doc.numberOfPages 33 | val extractedText = textStripper.getText(doc) 34 | extractedText.contains(expectedText, ignoreCase) 35 | } 36 | } catch (e: IOException) { 37 | false 38 | } 39 | } 40 | 41 | /** 42 | * Validates that a PDF has the expected number of pages 43 | */ 44 | fun validatePageCount(pdfBytes: ByteArray, expectedPageCount: Int): Boolean { 45 | return try { 46 | val document = PDDocument.load(ByteArrayInputStream(pdfBytes)) 47 | document.use { doc -> 48 | doc.numberOfPages == expectedPageCount 49 | } 50 | } catch (e: IOException) { 51 | false 52 | } 53 | } 54 | 55 | /** 56 | * Validates that a PDF contains image content (for image watermarks) 57 | */ 58 | fun validateImageContent(pdfBytes: ByteArray): Boolean { 59 | return try { 60 | val document = PDDocument.load(ByteArrayInputStream(pdfBytes)) 61 | document.use { doc -> 62 | doc.pages.any { page -> 63 | page.resources?.xObjectNames?.any { name -> 64 | page.resources.getXObject(name) is PDImageXObject 65 | } ?: false 66 | } 67 | } 68 | } catch (e: IOException) { 69 | false 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/DefaultOverlayPdfWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.pdf.overlay.opacity.GraphicsStateManager; 5 | import com.markit.servicelocator.ServiceFactory; 6 | import org.apache.pdfbox.pdmodel.PDDocument; 7 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 8 | import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; 9 | 10 | import java.io.IOException; 11 | import java.util.List; 12 | 13 | /** 14 | * @author Oleg Cheban 15 | * @since 1.0 16 | */ 17 | public class DefaultOverlayPdfWatermarker implements OverlayPdfWatermarker { 18 | 19 | @Override 20 | public void watermark(PDDocument document, int pageIndex, List attrs) throws IOException { 21 | var page = document.getPage(pageIndex); 22 | 23 | try (PDPageContentStream contentStream = 24 | new PDPageContentStream(document, page, PDPageContentStream.AppendMode.APPEND, true, true)) { 25 | 26 | attrs.forEach(attr -> { 27 | try { 28 | var graphicsStateManager = (GraphicsStateManager) ServiceFactory.getInstance() 29 | .getService(GraphicsStateManager.class); 30 | var opacityState = graphicsStateManager.createOpacityState(attr.getOpacity()); 31 | contentStream.setGraphicsStateParameters(opacityState); 32 | 33 | if (attr.getImage().isPresent()){ 34 | var image = LosslessFactory.createFromImage(document, attr.getImage().get()); 35 | var imageBasedOverlayWatermarker = (ImageBasedOverlayWatermarker) ServiceFactory.getInstance() 36 | .getService(ImageBasedOverlayWatermarker.class); 37 | imageBasedOverlayWatermarker.overlay(contentStream, image, page.getMediaBox(), attr); 38 | } else { 39 | var textBasedOverlayWatermarker = (TextBasedOverlayWatermarker) ServiceFactory.getInstance() 40 | .getService(TextBasedOverlayWatermarker.class); 41 | textBasedOverlayWatermarker.overlay(document, contentStream, page.getMediaBox(), attr); 42 | } 43 | } catch (IOException e) { 44 | throw new RuntimeException(e); 45 | } 46 | }); 47 | } 48 | } 49 | 50 | @Override 51 | public int getPriority() { 52 | return DEFAULT_PRIORITY; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/DefaultWatermarkService.java: -------------------------------------------------------------------------------- 1 | package com.markit.api; 2 | 3 | import com.markit.api.formats.image.WatermarkImageBuilder; 4 | import com.markit.api.formats.image.WatermarkImageService; 5 | import com.markit.api.formats.pdf.WatermarkPDFBuilder; 6 | import com.markit.api.formats.pdf.WatermarkPDFService; 7 | import com.markit.api.formats.video.WatermarkVideoBuilder; 8 | import com.markit.api.formats.video.WatermarkVideoService; 9 | import com.markit.exceptions.InvalidPDFFileException; 10 | import org.apache.pdfbox.pdmodel.PDDocument; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.util.concurrent.Executor; 15 | 16 | /** 17 | * Main entry point for adding watermarks to various file formats. 18 | * Acts as a factory for format-specific watermark builders that provide 19 | * a fluent DSL for configuring and applying watermarks. 20 | * 21 | * @author Oleg Cheban 22 | * @since 1.0 23 | */ 24 | public class DefaultWatermarkService implements WatermarkService.FileFormatSelector { 25 | 26 | private Executor executor; 27 | 28 | public DefaultWatermarkService() { 29 | } 30 | 31 | public DefaultWatermarkService(Executor e) { 32 | this.executor = e; 33 | } 34 | 35 | @Override 36 | public WatermarkPDFService watermarkPDF(byte[] fileBytes) { 37 | try { 38 | return new WatermarkPDFBuilder(PDDocument.load(fileBytes), executor); 39 | } catch (IOException e) { 40 | throw new InvalidPDFFileException(e); 41 | } 42 | } 43 | 44 | @Override 45 | public WatermarkPDFService watermarkPDF(File file) { 46 | try { 47 | return new WatermarkPDFBuilder(PDDocument.load(file), executor); 48 | } catch (IOException e) { 49 | throw new InvalidPDFFileException(e); 50 | } 51 | } 52 | 53 | @Override 54 | public WatermarkPDFService watermarkPDF(PDDocument document) { 55 | return new WatermarkPDFBuilder(document, executor); 56 | } 57 | 58 | @Override 59 | public WatermarkImageService watermarkImage(File file) { 60 | return new WatermarkImageBuilder(file); 61 | } 62 | 63 | @Override 64 | public WatermarkImageService watermarkImage(byte[] fileBytes) { 65 | return new WatermarkImageBuilder(fileBytes); 66 | } 67 | 68 | public WatermarkVideoService watermarkVideo(byte[] fileBytes) { 69 | return new WatermarkVideoBuilder(fileBytes); 70 | } 71 | 72 | @Override 73 | public WatermarkVideoService watermarkVideo(File file) { 74 | return new WatermarkVideoBuilder(file); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/DefaultImageBasedOverlayWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import com.markit.pdf.overlay.positioning.WatermarkPositioner; 6 | import com.markit.pdf.overlay.rotation.MatrixTransformationProvider; 7 | import com.markit.pdf.overlay.rotation.TransformationType; 8 | import com.markit.servicelocator.ServiceFactory; 9 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 10 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 11 | import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; 12 | 13 | import java.io.IOException; 14 | 15 | public class DefaultImageBasedOverlayWatermarker implements ImageBasedOverlayWatermarker { 16 | 17 | @Override 18 | public void overlay(PDPageContentStream contentStream, PDImageXObject imageXObject, PDRectangle pdRectangle, WatermarkAttributes attr) throws IOException { 19 | float imageWidth = (int) (imageXObject.getWidth() * (attr.getSize() / 300f)); 20 | float imageHeight = (int) (imageXObject.getHeight() * (attr.getSize() / 300f)); 21 | 22 | var coordinates = WatermarkPositioner.defineXY( 23 | attr, (int) pdRectangle.getWidth(), (int) pdRectangle.getHeight(), (int) imageWidth, (int) imageHeight); 24 | 25 | for (Coordinates c : coordinates) { 26 | if (attr.getRotationDegrees() != 0) { 27 | applyRotationAndDraw(contentStream, imageXObject, c, imageWidth, imageHeight, attr.getRotationDegrees()); 28 | } else { 29 | contentStream.drawImage(imageXObject, c.getX(), c.getY(), imageWidth, imageHeight); 30 | } 31 | } 32 | } 33 | 34 | private void applyRotationAndDraw( 35 | PDPageContentStream contentStream, PDImageXObject imageXObject, 36 | Coordinates c, float width, float height, int rotationDegrees) throws IOException { 37 | contentStream.saveGraphicsState(); 38 | 39 | var textTransformationProvider = (MatrixTransformationProvider) ServiceFactory.getInstance() 40 | .getService(MatrixTransformationProvider.class); 41 | 42 | var rotationMatrix = textTransformationProvider.createRotationMatrix( 43 | c, width, height, rotationDegrees, TransformationType.IMAGE_TRANSFORM); 44 | 45 | contentStream.transform(rotationMatrix); 46 | contentStream.drawImage(imageXObject, c.getX(), c.getY(), width, height); 47 | contentStream.restoreGraphicsState(); 48 | } 49 | 50 | @Override 51 | public int getPriority() { 52 | return DEFAULT_PRIORITY; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/builders/BaseWatermarkBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.builders; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.WatermarkProcessor; 5 | import com.markit.exceptions.WatermarkingException; 6 | import com.markit.utils.ValidationUtils; 7 | 8 | import java.io.IOException; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Objects; 12 | 13 | /** 14 | * Base service for applying watermarks to files. 15 | * 16 | * @param The concrete service type that extends this class 17 | * @author Oleg Cheban 18 | * @since 1.3.3 19 | */ 20 | public class BaseWatermarkBuilder { 21 | 22 | private final WatermarkProcessor watermarkProcessor; 23 | 24 | private final List watermarks = new ArrayList<>(); 25 | 26 | private WatermarkAttributes watermark; 27 | 28 | protected BaseWatermarkBuilder(WatermarkProcessor watermarkProcessor) { 29 | this.watermark = new WatermarkAttributes(); 30 | this.watermarkProcessor = Objects.requireNonNull(watermarkProcessor, "WatermarkProcessor must not be null"); 31 | } 32 | 33 | /** 34 | * Adds the watermark to the list and prepares for configuring another one. 35 | * 36 | * @return The service instance 37 | * @throws IllegalArgumentException if the current watermark attributes are invalid 38 | */ 39 | public T and() { 40 | approvePreviousWatermarkAttributes(); 41 | watermark = new WatermarkAttributes(); 42 | @SuppressWarnings("unchecked") 43 | var service = (T) this; 44 | return service; 45 | } 46 | 47 | /** 48 | * Applies all configured watermarks to the file. 49 | * 50 | * @return The watermarked file as a byte array 51 | * @throws WatermarkingException if an error occurs during watermarking 52 | */ 53 | public byte[] apply() { 54 | try { 55 | approvePreviousWatermarkAttributes(); 56 | return this.watermarkProcessor.apply(this.watermarks); 57 | } catch (IOException e) { 58 | throw new WatermarkingException("Error watermarking the file", e); 59 | } 60 | } 61 | 62 | protected WatermarkAttributes getWatermark() { 63 | return watermark; 64 | } 65 | 66 | private void approvePreviousWatermarkAttributes(){ 67 | Objects.requireNonNull(watermark, "Current watermark must not be null"); 68 | boolean isValid = ValidationUtils.validateWatermarkAttributes(watermark); 69 | if (!isValid) { 70 | throw new IllegalArgumentException("Invalid watermark attributes"); 71 | } 72 | watermarks.add(watermark); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/pdf/WatermarkPDFService.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.formats.pdf; 2 | 3 | import com.markit.api.builders.TextBasedWatermarkBuilder; 4 | import com.markit.api.WatermarkingMethod; 5 | import com.markit.api.builders.VisualWatermarkBuilder; 6 | import org.apache.pdfbox.pdmodel.PDDocument; 7 | 8 | import java.awt.image.BufferedImage; 9 | import java.io.File; 10 | import java.util.function.Predicate; 11 | 12 | /** 13 | * The Watermark Service for applying watermarks to PDFs 14 | * 15 | * @author Oleg Cheban 16 | * @since 1.3.0 17 | */ 18 | public interface WatermarkPDFService { 19 | 20 | /** 21 | * Text-based watermarking method 22 | * 23 | * @param text The text for the watermark 24 | */ 25 | TextBasedWatermarkBuilder withText(String text); 26 | 27 | /** 28 | * Image-based watermarking method 29 | * 30 | * @param image the Byte array representation of the image 31 | */ 32 | WatermarkPDFBuilder withImage(byte[] image); 33 | 34 | /** 35 | * Image-based watermarking method 36 | * 37 | * @param image the BufferedImage representation of the image 38 | */ 39 | WatermarkPDFBuilder withImage(BufferedImage image); 40 | 41 | /** 42 | * Image-based watermarking method 43 | * 44 | * @param image the File object representing the image 45 | */ 46 | WatermarkPDFBuilder withImage(File image); 47 | 48 | /** 49 | * The PDFs watermarking builder 50 | */ 51 | interface WatermarkPDFBuilder extends VisualWatermarkBuilder { 52 | 53 | /** 54 | * The watermarking method (default is DRAW) 55 | * 56 | * @param watermarkingMethod The method to use for watermarking 57 | * @see WatermarkingMethod 58 | */ 59 | WatermarkPDFBuilder method(WatermarkingMethod watermarkingMethod); 60 | 61 | /** 62 | * The DPI for PDF 63 | */ 64 | WatermarkPDFBuilder dpi(int dpi); 65 | 66 | /** 67 | * Filters documents to determine which should receive the watermark. 68 | * 69 | * @param predicate A condition that takes a PDDocument as input and returns true/false 70 | */ 71 | WatermarkPDFBuilder documentFilter(Predicate predicate); 72 | 73 | /** 74 | * Filters pages to determine which should receive the watermark. 75 | * 76 | * @param predicate A condition that takes a page number as input and returns true/false (the indexes starts from 0) 77 | */ 78 | WatermarkPDFBuilder pageFilter(Predicate predicate); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/markit/pdf/PdfPageRotationTextBasedTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.pdf 2 | 3 | import com.markit.api.WatermarkingMethod 4 | import com.markit.api.positioning.WatermarkPosition 5 | import com.markit.api.WatermarkService 6 | import org.apache.pdfbox.pdmodel.PDDocument 7 | import org.apache.pdfbox.pdmodel.PDPage 8 | import org.apache.pdfbox.pdmodel.common.PDRectangle 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import java.io.IOException 12 | import kotlin.test.assertNotNull 13 | import kotlin.test.assertTrue 14 | 15 | 16 | class PdfPageRotationTextBasedTest : WatermarkPdfTest() { 17 | @BeforeEach 18 | override fun initDocument() { 19 | document = PDDocument().apply { 20 | addPage(PDPage(PDRectangle.A4).apply { rotation = 0 }) 21 | addPage(PDPage(PDRectangle.A4).apply { rotation = 90 }) 22 | addPage(PDPage(PDRectangle.A4).apply { rotation = 180 }) 23 | addPage(PDPage(PDRectangle.A4).apply { rotation = 270 }) 24 | } 25 | } 26 | 27 | @Test 28 | @Throws(IOException::class) 29 | fun `given Pdf with 0, 90, 180 and 270 degrees page rotation when Draw Method then Make Watermarked Pdf`() { 30 | // When 31 | val result = WatermarkService.create() 32 | .watermarkPDF(document) 33 | .withText("Sample Watermark").end() 34 | .size(50) 35 | .position(WatermarkPosition.CENTER).end() 36 | .method(WatermarkingMethod.DRAW) 37 | .apply() 38 | 39 | // Then 40 | assertNotNull(result, "The resulting byte array should not be null") 41 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 42 | assertTrue(validateImageContent(result)); 43 | } 44 | 45 | @Test 46 | @Throws(IOException::class) 47 | fun `given Pdf with 0, 90, 180 and 270 degrees page rotation when Overlay Method then Make Watermarked Pdf`() { 48 | // Given 49 | val watermarkText = "Sample Watermark" 50 | 51 | // When 52 | val result = WatermarkService.create() 53 | .watermarkPDF(document) 54 | .withText(watermarkText) 55 | .end() 56 | .size(50) 57 | .position(WatermarkPosition.CENTER).end() 58 | .method(WatermarkingMethod.OVERLAY) 59 | .apply() 60 | 61 | // Then 62 | assertNotNull(result, "The resulting byte array should not be null") 63 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 64 | assertTrue(validateWatermarkText(result, watermarkText)); 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/api/formats/pdf/WatermarkPDFBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.formats.pdf; 2 | 3 | import com.markit.api.builders.DefaultVisualWatermarkBuilder; 4 | import com.markit.api.WatermarkingMethod; 5 | import com.markit.exceptions.ClosePDFDocumentException; 6 | import com.markit.pdf.WatermarkPdfServiceFactory; 7 | import com.markit.servicelocator.ServiceFactory; 8 | import org.apache.pdfbox.pdmodel.PDDocument; 9 | import org.jetbrains.annotations.NotNull; 10 | 11 | import java.io.IOException; 12 | import java.util.Objects; 13 | import java.util.concurrent.Executor; 14 | import java.util.function.Predicate; 15 | 16 | /** 17 | * @author Oleg Cheban 18 | * @since 1.3.0 19 | */ 20 | public final class WatermarkPDFBuilder 21 | extends DefaultVisualWatermarkBuilder 22 | implements WatermarkPDFService, WatermarkPDFService.WatermarkPDFBuilder { 23 | 24 | private PDDocument document; 25 | 26 | public WatermarkPDFBuilder(PDDocument pdfDoc, Executor executor) { 27 | super(watermarks -> getPdfServiceFactory().create(executor).watermark(pdfDoc, watermarks)); 28 | Objects.requireNonNull(pdfDoc, "PDDocument cannot be null"); 29 | this.document = pdfDoc; 30 | } 31 | 32 | @Override 33 | public WatermarkPDFBuilder method(WatermarkingMethod watermarkingMethod) { 34 | getWatermark().setMethod(watermarkingMethod); 35 | return this; 36 | } 37 | 38 | @Override 39 | public WatermarkPDFBuilder dpi(int dpi) { 40 | getWatermark().setDpi((float) dpi); 41 | return this; 42 | } 43 | 44 | @Override 45 | public WatermarkPDFBuilder documentFilter(Predicate predicate) { 46 | getWatermark().setDocumentPredicate(predicate); 47 | return this; 48 | } 49 | 50 | @Override 51 | public WatermarkPDFBuilder pageFilter(Predicate predicate) { 52 | getWatermark().setPagePredicate(predicate); 53 | return this; 54 | } 55 | 56 | @NotNull 57 | @Override 58 | public byte[] apply() { 59 | try { 60 | return super.apply(); 61 | } finally { 62 | closeDocument(); 63 | } 64 | } 65 | 66 | private void closeDocument() { 67 | try { 68 | document.close(); 69 | } catch (IOException e) { 70 | throw new ClosePDFDocumentException("Failed to close the document", e); 71 | } 72 | } 73 | 74 | private static WatermarkPdfServiceFactory getPdfServiceFactory() { 75 | return (WatermarkPdfServiceFactory) ServiceFactory.getInstance().getService(WatermarkPdfServiceFactory.class); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/DefaultImageBasedWatermarkPainter.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | 6 | import java.awt.AlphaComposite; 7 | import java.awt.Graphics2D; 8 | import java.awt.image.BufferedImage; 9 | 10 | /** 11 | * Default implementation of {@link ImageBasedWatermarkPainter}. 12 | * 13 | * @author Oleg Cheban 14 | * @since 1.3.5 15 | */ 16 | public class DefaultImageBasedWatermarkPainter implements ImageBasedWatermarkPainter { 17 | 18 | @Override 19 | public void draw(Graphics2D g2d, BufferedImage sourceImage, WatermarkAttributes attr) { 20 | BufferedImage watermarkImage = attr.getImage().get(); 21 | var alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) (attr.getOpacity() / 100f)); 22 | configureGraphics(g2d, alphaChannel); 23 | int watermarkWidth = (int) (watermarkImage.getWidth() * (attr.getSize() / 200f)); 24 | int watermarkHeight = (int) (watermarkImage.getHeight() * (attr.getSize() / 200f)); 25 | var coordinates = WatermarkPositioner.defineXY(attr, sourceImage.getWidth(), sourceImage.getHeight(), watermarkWidth, watermarkHeight); 26 | coordinates.forEach(v -> drawWatermark(g2d, watermarkImage, v, watermarkWidth, watermarkHeight, attr.getRotationDegrees())); 27 | } 28 | 29 | private void configureGraphics(Graphics2D g2d, AlphaComposite alphaChannel) { 30 | g2d.setComposite(alphaChannel); 31 | } 32 | 33 | private void drawWatermark(Graphics2D g2d, BufferedImage watermarkImage, Coordinates c, int width, int height, int rotation) { 34 | applyWithOptionalRotation(g2d, rotation, c, width, height, () -> { 35 | g2d.drawImage(watermarkImage, c.getX(), c.getY(), width, height, null); 36 | }); 37 | } 38 | 39 | private void applyWithOptionalRotation(Graphics2D g2d, int rotation, Coordinates c, int width, int height, Runnable drawAction) { 40 | var originalTransform = g2d.getTransform(); 41 | if (rotation != 0) { 42 | applyRotation(g2d, rotation, c, width, height); 43 | } 44 | drawAction.run(); 45 | 46 | if (rotation != 0) { 47 | g2d.setTransform(originalTransform); 48 | } 49 | } 50 | 51 | private void applyRotation(Graphics2D g2d, int rotation, Coordinates c, int width, int height) { 52 | double centerX = c.getX() + width / 2.0; 53 | double centerY = c.getY() + height / 2.0; 54 | g2d.rotate(-Math.toRadians(rotation), centerX, centerY); 55 | } 56 | 57 | @Override 58 | public int getPriority() { 59 | return DEFAULT_PRIORITY; 60 | } 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/main/java/com/markit/image/DefaultImageWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.servicelocator.ServiceFactory; 5 | 6 | import javax.imageio.ImageIO; 7 | import java.awt.image.BufferedImage; 8 | import java.io.File; 9 | import java.util.List; 10 | 11 | /** 12 | * @author Oleg Cheban 13 | * @since 1.0 14 | */ 15 | public class DefaultImageWatermarker implements ImageWatermarker { 16 | private final ImageConverter imageConverter = new ImageConverter(); 17 | 18 | @Override 19 | public byte[] watermark(byte[] sourceImageBytes, List attrs) { 20 | if (isByteArrayEmpty(sourceImageBytes)) { 21 | return sourceImageBytes; 22 | } 23 | var imageType = ImageTypeDetector.detect(sourceImageBytes); 24 | validateImageType(imageType); 25 | 26 | BufferedImage image = imageConverter.convertToBufferedImage(sourceImageBytes); 27 | return watermark(image, imageType, attrs); 28 | } 29 | 30 | @Override 31 | public byte[] watermark(File file, List attrs) { 32 | var imageType = ImageTypeDetector.detect(file); 33 | validateImageType(imageType); 34 | 35 | BufferedImage image = imageConverter.convertToBufferedImage(file); 36 | return watermark(image, imageType, attrs); 37 | } 38 | 39 | public byte[] watermark(BufferedImage sourceImage, String imageType, List attrs) { 40 | var g2d = sourceImage.createGraphics(); 41 | 42 | attrs.forEach(attr -> { 43 | if (attr.getImage().isPresent()){ 44 | var imagePainter = (ImageBasedWatermarkPainter) ServiceFactory.getInstance() 45 | .getService(ImageBasedWatermarkPainter.class); 46 | imagePainter.draw(g2d, sourceImage, attr); 47 | } else { 48 | var textPainter = (TextBasedWatermarkPainter) ServiceFactory.getInstance() 49 | .getService(TextBasedWatermarkPainter.class); 50 | textPainter.draw(g2d, sourceImage, attr); 51 | } 52 | }); 53 | g2d.dispose(); 54 | return imageConverter.convertToByteArray(sourceImage, imageType); 55 | } 56 | 57 | public boolean isByteArrayEmpty(byte[] byteArray) { 58 | return byteArray == null || byteArray.length == 0; 59 | } 60 | 61 | private void validateImageType(String imageType) { 62 | if (!ImageIO.getImageWritersByFormatName(imageType.toLowerCase()).hasNext()) { 63 | throw new UnsupportedOperationException("No writer found for " + imageType); 64 | } 65 | } 66 | 67 | @Override 68 | public int getPriority() { 69 | return DEFAULT_PRIORITY; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/test/java/com/markit/pdf/ComplexWatermarkingTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.pdf 2 | 3 | import com.markit.utils.FileUtils 4 | import com.markit.api.Font 5 | import com.markit.api.positioning.WatermarkPosition 6 | import com.markit.api.WatermarkService 7 | import com.markit.api.WatermarkingMethod 8 | import org.apache.pdfbox.pdmodel.PDDocument 9 | import org.apache.pdfbox.pdmodel.PDPage 10 | import org.junit.jupiter.api.BeforeEach 11 | import org.junit.jupiter.api.Test 12 | import java.awt.Color 13 | import java.io.IOException 14 | import java.time.LocalDateTime 15 | import java.util.concurrent.Executors 16 | import kotlin.test.assertTrue 17 | import kotlin.test.assertNotNull 18 | 19 | class ComplexWatermarkingTest : WatermarkPdfTest() { 20 | @BeforeEach 21 | override fun initDocument() { 22 | document = PDDocument().apply { 23 | addPage(PDPage()) 24 | addPage(PDPage()) 25 | addPage(PDPage()) 26 | } 27 | } 28 | 29 | @Test 30 | @Throws(IOException::class) 31 | fun `given Multi-Page Pdf when Apply Several Watermarks then Make Watermarked Pdf`() { 32 | val watermarkText = "WaterMarkIt" 33 | val timestampText = LocalDateTime.now().toString() 34 | 35 | val result = WatermarkService.create( 36 | Executors.newFixedThreadPool( 37 | Runtime.getRuntime().availableProcessors() 38 | ) 39 | ) 40 | .watermarkPDF(document) 41 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 42 | .position(WatermarkPosition.CENTER).end() 43 | .dpi(130) 44 | .opacity(20) 45 | .size(50) 46 | .and() 47 | .withText(watermarkText) 48 | .addTrademark() 49 | .bold() 50 | .font(Font.ARIAL) 51 | .color(Color.BLUE).end() 52 | .position(WatermarkPosition.TILED) 53 | .horizontalSpacing(10).end() 54 | .opacity(10) 55 | .method(WatermarkingMethod.OVERLAY) 56 | .rotation(25) 57 | .size(55) 58 | .and() 59 | .withText(timestampText).end() 60 | .method(WatermarkingMethod.OVERLAY) 61 | .position(WatermarkPosition.TOP_RIGHT) 62 | .adjust(0, -10).end() 63 | .size(30) 64 | .apply() 65 | 66 | assertNotNull(result, "The resulting byte array should not be null") 67 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 68 | assertTrue(validateWatermarkText(result, watermarkText)); 69 | assertTrue(validatePageCount(result, 3)); 70 | assertTrue(validateImageContent(result)); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/overlay/DefaultTextBasedOverlayWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.overlay; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import com.markit.pdf.overlay.font.DefaultFontProvider; 6 | import com.markit.pdf.overlay.font.FontProvider; 7 | import com.markit.pdf.overlay.positioning.WatermarkPositioner; 8 | import com.markit.pdf.overlay.rotation.MatrixTransformationProvider; 9 | import com.markit.pdf.overlay.rotation.TransformationType; 10 | import com.markit.pdf.overlay.trademark.TrademarkService; 11 | import com.markit.servicelocator.ServiceFactory; 12 | import org.apache.pdfbox.pdmodel.PDDocument; 13 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 14 | import org.apache.pdfbox.pdmodel.common.PDRectangle; 15 | 16 | import java.io.IOException; 17 | 18 | public class DefaultTextBasedOverlayWatermarker implements TextBasedOverlayWatermarker { 19 | 20 | @Override 21 | public void overlay(PDDocument document, PDPageContentStream contentStream, PDRectangle pdRectangle, WatermarkAttributes attr) throws IOException { 22 | initFonts(document, contentStream, attr); 23 | 24 | var coordinates = WatermarkPositioner.defineXY(attr, 25 | (int) pdRectangle.getWidth(), (int) pdRectangle.getHeight (), 26 | (int) attr.getPdfWatermarkTextWidth(), (int) attr.getPdfWatermarkTextHeight()); 27 | 28 | for (Coordinates c : coordinates) { 29 | var textTransformationProvider = (MatrixTransformationProvider) ServiceFactory.getInstance() 30 | .getService(MatrixTransformationProvider.class); 31 | 32 | var matrix = textTransformationProvider.createRotationMatrix( 33 | c, attr.getPdfWatermarkTextWidth(), attr.getPdfWatermarkTextHeight(), attr.getRotationDegrees(), TransformationType.TEXT_TRANSFORM); 34 | 35 | contentStream.beginText(); 36 | contentStream.setFont(attr.getResolvedPdfFont(), attr.getPdfTextSize()); 37 | contentStream.setNonStrokingColor(attr.getColor()); 38 | contentStream.setTextMatrix(matrix); 39 | contentStream.showText(attr.getText()); 40 | contentStream.endText(); 41 | 42 | if (attr.getTrademark()) { 43 | var trademarkService = (TrademarkService) ServiceFactory.getInstance().getService(TrademarkService.class); 44 | trademarkService.overlayTrademark(contentStream, attr, c); 45 | } 46 | } 47 | } 48 | 49 | private void initFonts(PDDocument document, PDPageContentStream contentStream, WatermarkAttributes attr) throws IOException { 50 | var fontProvider = (DefaultFontProvider) ServiceFactory.getInstance().getService(FontProvider.class); 51 | if (fontProvider.canHandle(attr)) { 52 | contentStream.setFont(fontProvider.loadFont(document, attr), attr.getPdfTextSize()); 53 | } 54 | } 55 | 56 | @Override 57 | public int getPriority() { 58 | return DEFAULT_PRIORITY; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/DefaultFilterChainBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.video.ffmpeg.probes.VideoDimensions; 5 | import com.markit.video.ffmpeg.probes.VideoInfoExtractor; 6 | 7 | import java.io.File; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * 13 | * @author Oleg Cheban 14 | * @since 1.4.0 15 | */ 16 | public class DefaultFilterChainBuilder implements FilterChainBuilder { 17 | 18 | @Override 19 | public FilterResult build(File video, List attributes) throws Exception { 20 | StringBuilder filter = new StringBuilder(); 21 | List tempImages = new ArrayList<>(); 22 | String lastLabel = "[0:v]"; 23 | int step = 0; 24 | boolean isEmptyFilter = true; 25 | 26 | VideoDimensions dimensions = VideoInfoExtractor.getVideoDimensions(video); 27 | 28 | // Build text filters 29 | List textAttributes = getTextAttributes(attributes); 30 | if (!textAttributes.isEmpty()) { 31 | FilterStepBuilder textBuilder = FilterStepBuilderFactory.getInstance().getBuilder(FilterStepType.DRAWTEXT); 32 | FilterStepAttributes textStep = textBuilder.build(textAttributes, dimensions, lastLabel, step, isEmptyFilter); 33 | filter.append(textStep.getFilter()); 34 | lastLabel = textStep.getLastLabel(); 35 | step = textStep.getStep(); 36 | isEmptyFilter = textStep.getEmpty(); 37 | } 38 | 39 | // Build image overlays 40 | List imageAttributes = getImageAttributes(attributes); 41 | if (!imageAttributes.isEmpty()) { 42 | FilterStepBuilder overlayBuilder = FilterStepBuilderFactory.getInstance().getBuilder(FilterStepType.OVERLAY); 43 | FilterStepAttributes imageStep = overlayBuilder.build(imageAttributes, dimensions, lastLabel, step, isEmptyFilter); 44 | filter.append(imageStep.getFilter()); 45 | tempImages.addAll(imageStep.getTempImages()); 46 | lastLabel = imageStep.getLastLabel(); 47 | } 48 | 49 | return new FilterResult(filter.toString(), lastLabel, tempImages); 50 | } 51 | 52 | private List getTextAttributes(List attributes) { 53 | List textAttrs = new ArrayList<>(); 54 | for (WatermarkAttributes attr : attributes) { 55 | if (attr.isTextWatermark()) { 56 | textAttrs.add(attr); 57 | } 58 | } 59 | return textAttrs; 60 | } 61 | 62 | private List getImageAttributes(List attributes) { 63 | List imageAttrs = new ArrayList<>(); 64 | for (WatermarkAttributes attr : attributes) { 65 | if (attr.isImageWatermark()) { 66 | imageAttrs.add(attr); 67 | } 68 | } 69 | return imageAttrs; 70 | } 71 | 72 | @Override 73 | public int getPriority() { 74 | return DEFAULT_PRIORITY; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/test/java/com/markit/pdf/ImageBasedWatermarkTest.kt: -------------------------------------------------------------------------------- 1 | package com.markit.pdf 2 | 3 | import com.markit.utils.FileUtils 4 | import com.markit.api.positioning.WatermarkPosition 5 | import com.markit.api.WatermarkService 6 | import org.apache.pdfbox.pdmodel.PDDocument 7 | import org.apache.pdfbox.pdmodel.PDPage 8 | import org.apache.pdfbox.pdmodel.common.PDRectangle 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import java.io.IOException 12 | import kotlin.test.assertNotNull 13 | import kotlin.test.assertTrue 14 | 15 | class ImageBasedWatermarkTest : WatermarkPdfTest() { 16 | @BeforeEach 17 | override fun initDocument() { 18 | document = PDDocument().apply { 19 | addPage(PDPage(PDRectangle.A4)) 20 | } 21 | } 22 | 23 | @Test 24 | @Throws(IOException::class) 25 | fun `given Pdf when Image Watermark and DPI then Apply Watermark`() { 26 | // When 27 | val result = WatermarkService.create() 28 | .watermarkPDF(document) 29 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 30 | .size(15) 31 | .dpi(100) 32 | .position(WatermarkPosition.CENTER).end() 33 | .apply() 34 | 35 | // Then 36 | assertNotNull(result, "The resulting byte array should not be null") 37 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 38 | assertTrue(validateImageContent(result)); 39 | } 40 | 41 | @Test 42 | @Throws(IOException::class) 43 | fun `given Pdf when Image Watermark and Adjust then Apply Watermark`() { 44 | // When 45 | val result = WatermarkService.create() 46 | .watermarkPDF(document) 47 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 48 | .size(25) 49 | .position(WatermarkPosition.TILED) 50 | .adjust(50, 50) 51 | .end() 52 | .apply() 53 | 54 | // Then 55 | assertNotNull(result, "The resulting byte array should not be null") 56 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 57 | assertTrue(validateImageContent(result)); 58 | } 59 | 60 | @Test 61 | @Throws(IOException::class) 62 | fun `given Pdf when Image Watermark with TOP_CENTER position then apply Watermark`() { 63 | // When 64 | val result = WatermarkService.create() 65 | .watermarkPDF(document) 66 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 67 | .size(25) 68 | .position(WatermarkPosition.TOP_CENTER) 69 | .end() 70 | .apply() 71 | 72 | // Then 73 | assertNotNull(result, "The resulting byte array should not be null") 74 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 75 | assertTrue(validateImageContent(result)); 76 | } 77 | 78 | @Test 79 | @Throws(IOException::class) 80 | fun `given Pdf when Image Watermark with BOTTOM_CENTER position then Apply Watermark`() { 81 | // When 82 | val result = WatermarkService.create() 83 | .watermarkPDF(document) 84 | .withImage(FileUtils.readFileFromClasspathAsBytes("logo.png")) 85 | .size(25) 86 | .position(WatermarkPosition.BOTTOM_CENTER) 87 | .end() 88 | .apply() 89 | 90 | // Then 91 | assertNotNull(result, "The resulting byte array should not be null") 92 | assertTrue(result.isNotEmpty(), "The resulting byte array should not be empty") 93 | assertTrue(validateImageContent(result)); 94 | } 95 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/image/DefaultTextBasedWatermarkPainter.java: -------------------------------------------------------------------------------- 1 | package com.markit.image; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | 6 | import java.awt.AlphaComposite; 7 | import java.awt.Color; 8 | import java.awt.Font; 9 | import java.awt.Graphics2D; 10 | import java.awt.font.FontRenderContext; 11 | import java.awt.font.TextLayout; 12 | import java.awt.geom.Rectangle2D; 13 | import java.awt.image.BufferedImage; 14 | 15 | /** 16 | * Default implementation of {@link TextBasedWatermarkPainter}. 17 | * 18 | * @author Oleg Cheban 19 | * @since 1.3.5 20 | */ 21 | public class DefaultTextBasedWatermarkPainter implements TextBasedWatermarkPainter { 22 | 23 | @Override 24 | public void draw(Graphics2D g2d, BufferedImage sourceImage, WatermarkAttributes attr) { 25 | var alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, (float) (attr.getOpacity() / 100.0)); 26 | var fontSize = calculateFontSize((int) (attr.getImageTextSize()), sourceImage.getWidth(), sourceImage.getHeight()); 27 | var fontStyle = attr.isBold() ? Font.BOLD : Font.PLAIN; 28 | var font = new Font(attr.getFont().getAwtFontName(), fontStyle, fontSize); 29 | configureGraphics(g2d, alphaChannel, attr.getColor(), font); 30 | FontRenderContext frc = g2d.getFontRenderContext(); 31 | TextLayout watermarkLayout = new TextLayout(attr.getText(), font, frc); 32 | Rectangle2D rect = watermarkLayout.getBounds(); 33 | 34 | var coordinates = WatermarkPositioner.defineXY(attr, sourceImage.getWidth(), sourceImage.getHeight(), (int) rect.getWidth(), (int) rect.getHeight()); 35 | coordinates.forEach(v -> drawWatermark(g2d, watermarkLayout, attr, rect, v, font, fontSize)); 36 | } 37 | 38 | private int calculateFontSize(int textSize, int imageWidth, int imageHeight) { 39 | if (textSize > 0) return textSize; 40 | return Math.min(imageWidth, imageHeight) / 10; 41 | } 42 | 43 | private void configureGraphics(Graphics2D g2d, AlphaComposite alphaChannel, Color color, Font font) { 44 | g2d.setComposite(alphaChannel); 45 | g2d.setColor(color); 46 | g2d.setFont(font); 47 | } 48 | 49 | private void drawWatermark(Graphics2D g2d, TextLayout watermarkLayout, WatermarkAttributes attr, Rectangle2D rect, Coordinates c, Font baseFont, int baseFontSize) { 50 | applyWithOptionalRotation(g2d, attr.getRotationDegrees(), c, rect, () -> { 51 | watermarkLayout.draw(g2d, c.getX(), c.getY()); 52 | 53 | if (attr.getTrademark()) { 54 | drawTrademark(g2d, baseFont, baseFontSize, rect, c); 55 | } 56 | }); 57 | } 58 | 59 | private void applyWithOptionalRotation(Graphics2D g2d, int rotation, Coordinates c, Rectangle2D rect, Runnable drawAction) { 60 | var originalTransform = g2d.getTransform(); 61 | if (rotation != 0) { 62 | applyRotation(g2d, rotation, c, rect); 63 | } 64 | drawAction.run(); 65 | 66 | if (rotation != 0) { 67 | // Restore original transform to avoid accumulating transforms across tiles 68 | g2d.setTransform(originalTransform); 69 | } 70 | } 71 | 72 | private void applyRotation(Graphics2D g2d, int rotation, Coordinates c, Rectangle2D rect) { 73 | double centerX = c.getX() + rect.getWidth() / 2; 74 | double centerY = c.getY() + rect.getHeight() / 2; 75 | g2d.rotate(-Math.toRadians(rotation), centerX, centerY); 76 | } 77 | 78 | private void drawTrademark(Graphics2D g2d, Font baseFont, int baseFontSize, Rectangle2D rect, Coordinates c) { 79 | FontRenderContext frc = g2d.getFontRenderContext(); 80 | Font smallFont = baseFont.deriveFont((float) baseFontSize / 2); 81 | TextLayout trademarkLayout = new TextLayout("®", smallFont, frc); 82 | trademarkLayout.draw(g2d, (float) (c.getX() + rect.getWidth()) + 5, c.getY() - (baseFontSize / 2f)); 83 | } 84 | 85 | @Override 86 | public int getPriority() { 87 | return DEFAULT_PRIORITY; 88 | } 89 | } 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/draw/DefaultDrawPdfWatermarker.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf.draw; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.image.ImageConverter; 5 | import com.markit.image.ImageWatermarker; 6 | import com.markit.servicelocator.ServiceFactory; 7 | import org.apache.pdfbox.pdmodel.PDDocument; 8 | import org.apache.pdfbox.pdmodel.PDPage; 9 | import org.apache.pdfbox.pdmodel.PDPageContentStream; 10 | import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; 11 | import org.apache.pdfbox.rendering.PDFRenderer; 12 | import org.apache.pdfbox.util.Matrix; 13 | 14 | import java.io.IOException; 15 | import java.util.Comparator; 16 | import java.util.List; 17 | 18 | /** 19 | * @author Oleg Cheban 20 | * @since 1.0 21 | */ 22 | public class DefaultDrawPdfWatermarker implements DrawPdfWatermarker { 23 | private final static float DEFAULT_DPI = 300f; 24 | 25 | public DefaultDrawPdfWatermarker() { 26 | } 27 | 28 | @Override 29 | public void watermark(PDDocument document, int pageIndex, List attrs) throws IOException { 30 | var imageConverter = new ImageConverter(); 31 | var page = document.getPage(pageIndex); 32 | var pdfRenderer = new PDFRenderer(document); 33 | var image = pdfRenderer.renderImageWithDPI(pageIndex, getDPI(attrs)); 34 | var imageWatermarker = (ImageWatermarker) ServiceFactory.getInstance().getService(ImageWatermarker.class); 35 | 36 | // Apply watermark to the rendered image 37 | var watermarkedImageBytes = imageWatermarker.watermark(imageConverter.convertToByteArray(image, "JPEG"), attrs); 38 | 39 | // Create a PDImageXObject from the watermarked image bytes 40 | var pdImage = PDImageXObject.createFromByteArray(document, watermarkedImageBytes, "watermarked"); 41 | 42 | // Replace the original image in the PDF with the watermarked image 43 | replaceImageInPDF( 44 | document, 45 | pdImage, 46 | page, 47 | page.getCropBox().getLowerLeftX(), 48 | page.getCropBox().getLowerLeftY(), 49 | page.getCropBox().getWidth(), 50 | page.getCropBox().getHeight() 51 | ); 52 | } 53 | 54 | private float getDPI(List attrs){ 55 | return attrs.stream() 56 | .map(WatermarkAttributes::getDpi) 57 | .min(Comparator.naturalOrder()) 58 | .orElse(DEFAULT_DPI); 59 | } 60 | 61 | private void replaceImageInPDF( 62 | PDDocument document, 63 | PDImageXObject watermarkedImage, 64 | PDPage page, 65 | float x, 66 | float y, 67 | float width, 68 | float height) throws IOException { 69 | try (var contentStream = new PDPageContentStream(document, page, PDPageContentStream.AppendMode.OVERWRITE, false)) { 70 | adjustPageRotation(contentStream, page); 71 | contentStream.drawImage(watermarkedImage, x, y, width, height); 72 | } 73 | } 74 | 75 | /** 76 | * Pages may have arbitrary rotations (e.g., scanned documents or mixed orientation files). 77 | * This method provides a solution for handling pages where the rotation is not 0°. 78 | * It ensures that the transformation matrix is adjusted so that the added watermark appears correctly on the rotated page. 79 | */ 80 | private void adjustPageRotation(PDPageContentStream contentStream, PDPage page) throws IOException { 81 | final int rotation = page.getRotation(); 82 | final float pageWidth = page.getMediaBox().getWidth(); 83 | final float pageHeight = page.getMediaBox().getHeight(); 84 | final int D_90 = 90, D_180 = 180, D_270 = 270; 85 | float scaleX = 1, scaleY = 1; 86 | switch (rotation) { 87 | case D_90: 88 | contentStream.transform(Matrix.getRotateInstance(Math.toRadians(D_90), 0, 0)); 89 | contentStream.transform(Matrix.getTranslateInstance(0, -pageWidth)); 90 | scaleX = pageHeight / pageWidth; 91 | scaleY = pageWidth / pageHeight; 92 | break; 93 | case D_180: 94 | contentStream.transform(Matrix.getRotateInstance(Math.toRadians(D_180), 0, 0)); 95 | contentStream.transform(Matrix.getTranslateInstance(-pageWidth, -pageHeight)); 96 | break; 97 | case D_270: 98 | contentStream.transform(Matrix.getRotateInstance(Math.toRadians(D_270), 0, 0)); 99 | contentStream.transform(Matrix.getTranslateInstance(-pageHeight, 0)); 100 | scaleX = pageHeight / pageWidth; 101 | scaleY = pageWidth / pageHeight; 102 | break; 103 | } 104 | contentStream.transform(Matrix.getScaleInstance(scaleX, scaleY)); 105 | } 106 | 107 | @Override 108 | public int getPriority() { 109 | return DEFAULT_PRIORITY; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/java/com/markit/api/builders/DefaultVisualWatermarkBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.api.builders; 2 | 3 | import com.markit.api.Font; 4 | import com.markit.api.WatermarkProcessor; 5 | import com.markit.api.positioning.Coordinates; 6 | import com.markit.api.positioning.WatermarkPosition; 7 | import com.markit.image.ImageConverter; 8 | 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | import java.io.File; 12 | import java.util.Objects; 13 | import java.util.Optional; 14 | import java.util.function.Supplier; 15 | 16 | /** 17 | * @author Oleg Cheban 18 | * @since 1.3.0 19 | */ 20 | public class DefaultVisualWatermarkBuilder extends BaseWatermarkBuilder 21 | implements PositionStepBuilder, TextBasedWatermarkBuilder { 22 | 23 | public DefaultVisualWatermarkBuilder(WatermarkProcessor watermarkProcessor) { 24 | super(watermarkProcessor); 25 | } 26 | 27 | public TextBasedWatermarkBuilder withText(String text) { 28 | Objects.requireNonNull(text); 29 | getWatermark().setText(text); 30 | return this; 31 | } 32 | 33 | public TextBasedWatermarkBuilder color(Color color) { 34 | Objects.requireNonNull(color); 35 | getWatermark().setColor(color); 36 | return this; 37 | } 38 | 39 | @Override 40 | public TextBasedWatermarkBuilder font(Font font) { 41 | Objects.requireNonNull(font); 42 | getWatermark().setFont(font); 43 | return this; 44 | } 45 | 46 | @Override 47 | public TextBasedWatermarkBuilder bold() { 48 | getWatermark().setBold(true); 49 | return this; 50 | } 51 | 52 | public TextBasedWatermarkBuilder addTrademark() { 53 | getWatermark().setTrademark(true); 54 | return this; 55 | } 56 | 57 | public WatermarkBuilder withImage(byte[] image) { 58 | Objects.requireNonNull(image); 59 | var imageConverter = new ImageConverter(); 60 | return withImage(() -> imageConverter.convertToBufferedImage(image)); 61 | } 62 | 63 | public WatermarkBuilder withImage(BufferedImage image) { 64 | Objects.requireNonNull(image); 65 | return withImage(() -> image); 66 | } 67 | 68 | public WatermarkBuilder withImage(File image) { 69 | Objects.requireNonNull(image); 70 | var imageConverter = new ImageConverter(); 71 | return withImage(() -> imageConverter.convertToBufferedImage(image)); 72 | } 73 | 74 | public WatermarkBuilder size(int size) { 75 | if (size < 0 || size > 300) 76 | throw new IllegalArgumentException("Size must be between 0 and 300"); 77 | 78 | getWatermark().setSize(size); 79 | return builder(); 80 | } 81 | 82 | public WatermarkBuilder opacity(int opacity) { 83 | if (opacity < 0 || opacity > 100) 84 | throw new IllegalArgumentException("Opacity must be between 0 and 100"); 85 | 86 | getWatermark().setOpacity(opacity); 87 | return builder(); 88 | } 89 | 90 | public WatermarkBuilder rotation(int degree) { 91 | getWatermark().setRotationDegrees(degree); 92 | return builder(); 93 | } 94 | 95 | public WatermarkBuilder enableIf(boolean condition) { 96 | getWatermark().setVisible(condition); 97 | return builder(); 98 | } 99 | 100 | public WatermarkBuilder end() { 101 | return builder(); 102 | } 103 | 104 | private WatermarkBuilder builder() { 105 | @SuppressWarnings("unchecked") 106 | var result = (WatermarkBuilder) this; 107 | return result; 108 | } 109 | 110 | public PositionStepBuilder position(WatermarkPosition watermarkPosition) { 111 | Objects.requireNonNull(watermarkPosition); 112 | getWatermark().setPosition(watermarkPosition); 113 | return this; 114 | } 115 | 116 | public WatermarkBuilder position(int x, int y) { 117 | getWatermark().setCustomCoordinates(true); 118 | getWatermark().setPositionCoordinates(new Coordinates(x, y)); 119 | return builder(); 120 | } 121 | 122 | public PositionStepBuilder adjust(int x, int y) { 123 | var adjustment = new Coordinates(x, y); 124 | getWatermark().setPositionCoordinates(adjustment); 125 | return this; 126 | } 127 | 128 | public PositionStepBuilder verticalSpacing(int spacing) { 129 | getWatermark().setVerticalSpacing(spacing); 130 | return this; 131 | } 132 | 133 | public PositionStepBuilder horizontalSpacing(int spacing) { 134 | getWatermark().setHorizontalSpacing(spacing); 135 | return this; 136 | } 137 | 138 | private WatermarkBuilder withImage(Supplier imageSupplier) { 139 | getWatermark().setImage(Optional.of(imageSupplier.get())); 140 | return builder(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributor's Guide for WaterMarkIt 2 | 3 | Thank you for your interest in contributing to WaterMarkIt! This guide will walk you through the process of setting up the project, making contributions, and ensuring your code adheres to the project's formatting and style guidelines. 4 | 5 | ## Table of Contents 6 | - [Getting Started](#getting-started) 7 | - [Forking the Repository](#forking-the-repository) 8 | - [Cloning the Repository](#cloning-the-repository) 9 | - [Setting Up the Development Environment](#setting-up-the-development-environment) 10 | 11 | - [Making Contributions](#making-contributions) 12 | - [Creating a Branch](#creating-a-branch) 13 | - [Making Changes](#making-changes) 14 | - [Committing Changes](#committing-changes) 15 | - [Pushing Changes](#pushing-changes) 16 | - [Creating a Pull Request](#creating-a-pull-request) 17 | 18 | - [Code Formatting and Style Guidelines](#code-formatting-and-style-guidelines) 19 | - [General Formatting Rules](#general-formatting-rules) 20 | - [Java Code Style](#java-code-style) 21 | - [Kotlin Code Style](#kotlin-code-style) 22 | 23 | - [Testing](#testing) 24 | - [Code Review Process](#code-review-process) 25 | - [Additional Resources](#additional-resources) 26 | 27 | --- 28 | 29 | ## Getting Started 30 | 31 | ### Forking the Repository 32 | 1. Go to the [WaterMarkIt GitHub repository](https://github.com/OlegCheban/WaterMarkIt). 33 | 2. Click the "Fork" button in the top-right corner of the page. This will create a copy of the repository under your GitHub account. 34 | 35 | ### Cloning the Repository 36 | 1. Navigate to your forked repository on GitHub. 37 | 2. Click the "Code" button and copy the repository URL. 38 | 3. Open your terminal and run the following command to clone the repository: 39 | ```bash 40 | git clone https://github.com/YOUR_USERNAME/WaterMarkIt.git 41 | 4. Navigate to the cloned repository: 42 | ```bash 43 | cd WaterMarkIt 44 | 45 | ### Setting Up the Development Environment 46 | 1. Ensure you have Java 11 or higher installed. You can check your Java version by running: 47 | ```bash 48 | java -version 49 | 2. Install [Maven](https://maven.apache.org/) if you don't already have it. 50 | 3. Build the project using Maven: 51 | ```bash 52 | mvn clean install 53 | 4. Set up your IDE (e.g., IntelliJ IDEA, Eclipse) to use the project's Maven configuration. 54 | 55 | --- 56 | ## Making Contributions 57 | 58 | ### Creating a Branch 59 | Before making changes, create a new branch for your work: 60 | 61 | ```bash 62 | git checkout -b feature/your-feature-name 63 | ``` 64 | 65 | Use a descriptive branch name that reflects the purpose of your changes. 66 | 67 | ### Making Changes 68 | Make your changes to the codebase. Ensure you follow the Code Formatting and Style Guidelines. 69 | 70 | Add new tests if you are introducing new functionality or modifying existing behavior. 71 | 72 | ### Committing Changes 73 | Stage your changes: 74 | 75 | ```bash 76 | git add . 77 | ``` 78 | 79 | Commit your changes with a descriptive commit message: 80 | 81 | ```bash 82 | git commit -m "Your descriptive commit message" 83 | ``` 84 | 85 | ### Pushing Changes 86 | Push your changes to your forked repository: 87 | 88 | ```bash 89 | git push origin feature/your-feature-name 90 | ``` 91 | 92 | ### Creating a Pull Request 93 | 1. Go to your forked repository on GitHub 94 | 2. Click the "Compare & pull request" button next to your branch 95 | 3. Fill out the pull request template with a clear description of your changes 96 | 4. Submit the pull request and wait for feedback from the maintainers 97 | 98 | --- 99 | ## Code Formatting and Style Guidelines 100 | ### General Formatting Rules 101 | - Use 4 spaces for indentation (no tabs). 102 | - Ensure all files end with a newline. 103 | 104 | ### Java Code Style 105 | - Follow the Google Java Style Guide for Java code. 106 | - Use camelCase for variable and method names. 107 | - Use PascalCase for class names. 108 | - Use UPPER_SNAKE_CASE for constants. 109 | - Always include Javadoc comments for public classes, methods, and fields. 110 | - Use @Override annotations for overridden methods. 111 | - Don't use Java POJO, use Kotlin data classes instead. 112 | 113 | ### Kotlin Code Style 114 | - Follow the Kotlin Coding Conventions. 115 | - Use data class for simple data structures. 116 | 117 | --- 118 | ### Testing 119 | - Write unit tests for all new functionality using [JUnit 5](https://junit.org/junit5/) 120 | - Ensure all tests pass before submitting a pull request: 121 | ```bash 122 | mvn test 123 | ``` 124 | - Use descriptive test method names that explain the purpose of the test 125 | 126 | --- 127 | ### Code Review Process 128 | - After submitting a pull request, the maintainers will review your changes 129 | - Address any feedback or requested changes promptly 130 | - Once approved, your changes will be merged into the main branch 131 | 132 | --- 133 | ### Additional Resources 134 | - [GitHub Flow Guide](https://guides.github.com/introduction/flow/) 135 | - [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) 136 | - [Kotlin Coding Conventions](https://kotlinlang.org/docs/coding-conventions.html) 137 | - [Maven Documentation](https://maven.apache.org/guides/) 138 | 139 | --- 140 | Thank you for contributing to WaterMarkIt! Your efforts help make this project better for everyone. 141 | 142 | Happy coding! 🚀 143 | -------------------------------------------------------------------------------- /src/main/java/com/markit/pdf/DefaultWatermarkPdfService.java: -------------------------------------------------------------------------------- 1 | package com.markit.pdf; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.WatermarkingMethod; 5 | import com.markit.exceptions.ExecutorNotFoundException; 6 | import com.markit.pdf.draw.DrawPdfWatermarker; 7 | import com.markit.pdf.overlay.OverlayPdfWatermarker; 8 | import com.markit.servicelocator.ServiceFactory; 9 | import org.apache.commons.logging.Log; 10 | import org.apache.commons.logging.LogFactory; 11 | import org.apache.pdfbox.pdmodel.PDDocument; 12 | 13 | import java.io.ByteArrayOutputStream; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | import java.util.List; 17 | import java.util.Optional; 18 | import java.util.concurrent.CompletableFuture; 19 | import java.util.concurrent.Executor; 20 | import java.util.stream.Collectors; 21 | 22 | /** 23 | * @author Oleg Cheban 24 | * @since 1.0 25 | */ 26 | public class DefaultWatermarkPdfService implements WatermarkPdfService { 27 | private static final Log logger = LogFactory.getLog(DefaultWatermarkPdfService.class); 28 | private final Optional executorService; 29 | 30 | public DefaultWatermarkPdfService(Executor es) { 31 | this.executorService = Optional.ofNullable(es); 32 | } 33 | 34 | @Override 35 | public byte[] watermark(PDDocument document, List attrs) throws IOException { 36 | applyWatermark(document, attrs, WatermarkingMethod.DRAW, this::draw); 37 | applyWatermark(document, attrs, WatermarkingMethod.OVERLAY, this::overlay); 38 | removeSecurity(document); 39 | return convertPDDocumentToByteArray(document); 40 | } 41 | 42 | private void applyWatermark(PDDocument document, List attrs, 43 | WatermarkingMethod method, PdfWatermarkProcessor action) throws IOException { 44 | var filteredAttrs = attrs.stream() 45 | .filter(WatermarkAttributes::getVisible) 46 | .filter(attr -> attr.getDocumentPredicate().test(document)) 47 | .filter(attr -> attr.getMethod().equals(method)) 48 | .collect(Collectors.toList()); 49 | if (!filteredAttrs.isEmpty()) { 50 | action.apply(document, filteredAttrs); 51 | } 52 | } 53 | 54 | private void overlay(PDDocument document, List attrs) throws IOException { 55 | var overlayService = (OverlayPdfWatermarker) ServiceFactory.getInstance().getService(OverlayPdfWatermarker.class); 56 | int numberOfPages = document.getNumberOfPages(); 57 | for (int pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { 58 | List filteredAttrs = filterAttrsByPageIndex(attrs, pageIndex); 59 | if (!filteredAttrs.isEmpty()){ 60 | overlayService.watermark(document, pageIndex, filteredAttrs); 61 | } 62 | } 63 | } 64 | 65 | private void draw(PDDocument document, List attrs) { 66 | int numberOfPages = document.getNumberOfPages(); 67 | if (executorService.isEmpty()) { 68 | sync(document, numberOfPages, attrs); 69 | } else { 70 | async(document, numberOfPages, attrs); 71 | } 72 | } 73 | 74 | private void async(PDDocument document, int numberOfPages, List attrs){ 75 | if (executorService.isEmpty()){ 76 | logger.error("An empty executor"); 77 | throw new ExecutorNotFoundException(); 78 | } 79 | 80 | List> futures = new ArrayList<>(); 81 | for (int pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { 82 | final int pIndex = pageIndex; 83 | futures.add( 84 | CompletableFuture.runAsync( 85 | () -> { 86 | try { 87 | draw(document, pIndex, attrs); 88 | } catch (IOException e) { 89 | logPageException(e, pIndex); 90 | } 91 | }, 92 | executorService.get() 93 | ) 94 | ); 95 | } 96 | var allOf = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])); 97 | allOf.join(); 98 | } 99 | 100 | private void sync(PDDocument document, int numberOfPages, List attrs) { 101 | for (int pageIndex = 0; pageIndex < numberOfPages; pageIndex++) { 102 | try { 103 | draw(document, pageIndex, attrs); 104 | } catch (IOException e) { 105 | logPageException(e, pageIndex); 106 | } 107 | } 108 | } 109 | 110 | private void draw(PDDocument document, int pageIndex, List attrs) throws IOException { 111 | var drawService = (DrawPdfWatermarker) ServiceFactory.getInstance().getService(DrawPdfWatermarker.class); 112 | List filteredAttrs = filterAttrsByPageIndex(attrs, pageIndex); 113 | if (!filteredAttrs.isEmpty()) drawService.watermark(document, pageIndex, filteredAttrs); 114 | } 115 | 116 | private static List filterAttrsByPageIndex(List attrs, int pIndex) { 117 | return attrs.stream() 118 | .filter(attr -> attr.getPagePredicate().test(pIndex)) 119 | .collect(Collectors.toList()); 120 | } 121 | 122 | private void logPageException(Exception e, int pageIndex){ 123 | logger.error(String.format("An error occurred during watermarking on page number %d", pageIndex), e); 124 | } 125 | 126 | private byte[] convertPDDocumentToByteArray(PDDocument document) throws IOException { 127 | try (var baos = new ByteArrayOutputStream()) { 128 | document.save(baos); 129 | return baos.toByteArray(); 130 | } 131 | } 132 | 133 | private void removeSecurity(PDDocument document) { 134 | if (document.isEncrypted()){ 135 | document.setAllSecurityToBeRemoved(true); 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/TextFilterStepBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import com.markit.image.WatermarkPositioner; 6 | import com.markit.video.ffmpeg.probes.VideoDimensions; 7 | 8 | import java.awt.*; 9 | import java.awt.font.FontRenderContext; 10 | import java.awt.font.TextLayout; 11 | import java.awt.geom.Rectangle2D; 12 | import java.awt.image.BufferedImage; 13 | import java.util.List; 14 | 15 | /** 16 | * drawtext filter chain builder 17 | * 18 | * @author Oleg Cheban 19 | * @since 1.4.0 20 | */ 21 | public class TextFilterStepBuilder implements FilterStepBuilder { 22 | @Override 23 | public FilterStepType getFilterStepType() { 24 | return FilterStepType.DRAWTEXT; 25 | } 26 | 27 | @Override 28 | public FilterStepAttributes build(List attrs, VideoDimensions dimensions, 29 | String lastLabel, int step, boolean isEmptyFilter) { 30 | StringBuilder filter = new StringBuilder(); 31 | Graphics2D g2d = createGraphicsContext(dimensions); 32 | 33 | try { 34 | for (WatermarkAttributes attr : attrs) { 35 | FilterBuildContext context = processWatermark(attr, dimensions, g2d, lastLabel, step, isEmptyFilter); 36 | appendFilters(filter, context); 37 | 38 | lastLabel = context.lastLabel; 39 | step = context.step; 40 | isEmptyFilter = context.isEmptyFilter; 41 | } 42 | } finally { 43 | g2d.dispose(); 44 | } 45 | 46 | return new FilterStepAttributes(filter.toString(), lastLabel, step, isEmptyFilter); 47 | } 48 | 49 | private Graphics2D createGraphicsContext(VideoDimensions dimensions) { 50 | BufferedImage tempImage = new BufferedImage( 51 | dimensions.getWidth(), 52 | dimensions.getHeight(), 53 | BufferedImage.TYPE_INT_ARGB 54 | ); 55 | return tempImage.createGraphics(); 56 | } 57 | 58 | private FilterBuildContext processWatermark(WatermarkAttributes attr, VideoDimensions dimensions, 59 | Graphics2D g2d, String lastLabel, int step, boolean isEmptyFilter) { 60 | Font font = createFont(attr); 61 | Rectangle2D textBounds = calculateTextBounds(attr.getText(), font, g2d.getFontRenderContext()); 62 | List coordinates = calculateCoordinates(attr, dimensions, textBounds); 63 | 64 | return new FilterBuildContext(coordinates, attr, lastLabel, step, isEmptyFilter); 65 | } 66 | 67 | private Font createFont(WatermarkAttributes attr) { 68 | int fontStyle = attr.isBold() ? Font.BOLD : Font.PLAIN; 69 | return new Font(attr.getFont().getAwtFontName(), fontStyle, attr.getSize()); 70 | } 71 | 72 | private Rectangle2D calculateTextBounds(String text, Font font, FontRenderContext frc) { 73 | TextLayout layout = new TextLayout(text, font, frc); 74 | return layout.getBounds(); 75 | } 76 | 77 | private List calculateCoordinates(WatermarkAttributes attr, VideoDimensions dimensions, 78 | Rectangle2D textBounds) { 79 | return WatermarkPositioner.defineXY( 80 | attr, 81 | dimensions.getWidth(), 82 | dimensions.getHeight(), 83 | (int) textBounds.getWidth(), 84 | (int) textBounds.getHeight() 85 | ); 86 | } 87 | 88 | private void appendFilters(StringBuilder filter, FilterBuildContext context) { 89 | for (Coordinates coord : context.coordinates) { 90 | String drawtextFilter = buildDrawtextFilter(context.attr, coord, context.inLabel, context.outLabel); 91 | 92 | if (!context.isEmptyFilter) { 93 | filter.append(","); 94 | } 95 | filter.append(drawtextFilter); 96 | 97 | context.advance(); 98 | } 99 | } 100 | 101 | private String buildDrawtextFilter(WatermarkAttributes attr, Coordinates coord, String inLabel, String outLabel) { 102 | return String.format( 103 | "%sdrawtext=text='%s':fontcolor=%s@%s:fontsize=%d:x=%s:y=%s%s", 104 | inLabel, 105 | attr.getText(), 106 | getColorValue(attr.getColor()), 107 | getOpacityValue(attr.getOpacity()), 108 | attr.getSize(), 109 | coord.getX(), 110 | coord.getY(), 111 | outLabel 112 | ); 113 | } 114 | 115 | /** 116 | * method converts a Java Color object to ffmpeg's expected hexadecimal color format 117 | */ 118 | private String getColorValue(Color color) { 119 | return String.format("%02x%02x%02x", color.getRed(), color.getGreen(), color.getBlue()); 120 | } 121 | 122 | /** 123 | * method converts a percentage-based opacity (0-100) to FFmpeg's decimal format (0.0-1.0). 124 | */ 125 | private float getOpacityValue(int opacity) { 126 | return Math.max(0, Math.min(100, opacity)) / 100f; 127 | } 128 | 129 | @Override 130 | public int getPriority() { 131 | return DEFAULT_PRIORITY; 132 | } 133 | 134 | /** 135 | * Context holder for filter building state 136 | */ 137 | private static class FilterBuildContext { 138 | private final List coordinates; 139 | private final WatermarkAttributes attr; 140 | private String lastLabel; 141 | private int step; 142 | private boolean isEmptyFilter; 143 | private String inLabel; 144 | private String outLabel; 145 | 146 | FilterBuildContext(List coordinates, WatermarkAttributes attr, 147 | String lastLabel, int step, boolean isEmptyFilter) { 148 | this.coordinates = coordinates; 149 | this.attr = attr; 150 | this.lastLabel = lastLabel; 151 | this.step = step; 152 | this.isEmptyFilter = isEmptyFilter; 153 | updateLabels(); 154 | } 155 | 156 | private void updateLabels() { 157 | this.inLabel = step == 0 ? "[0:v]" : lastLabel; 158 | this.outLabel = "[v" + (step + 1) + "]"; 159 | } 160 | 161 | void advance() { 162 | this.lastLabel = this.outLabel; 163 | this.isEmptyFilter = false; 164 | this.step++; 165 | updateLabels(); 166 | } 167 | } 168 | } -------------------------------------------------------------------------------- /src/main/java/com/markit/video/ffmpeg/filters/OverlayFilterStepBuilder.java: -------------------------------------------------------------------------------- 1 | package com.markit.video.ffmpeg.filters; 2 | 3 | import com.markit.api.WatermarkAttributes; 4 | import com.markit.api.positioning.Coordinates; 5 | import com.markit.image.WatermarkPositioner; 6 | import com.markit.video.ffmpeg.probes.VideoDimensions; 7 | 8 | import javax.imageio.ImageIO; 9 | import java.awt.*; 10 | import java.awt.image.BufferedImage; 11 | import java.io.File; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * overlay filter chain builder 19 | * 20 | * @author Oleg Cheban 21 | * @since 1.4.0 22 | */ 23 | public class OverlayFilterStepBuilder implements FilterStepBuilder { 24 | @Override 25 | public FilterStepType getFilterStepType() { 26 | return FilterStepType.OVERLAY; 27 | } 28 | 29 | @Override 30 | public FilterStepAttributes build(List attrs, VideoDimensions dimensions, 31 | String lastLabel, int step, boolean isEmptyFilter) throws Exception { 32 | StringBuilder filter = new StringBuilder(); 33 | List tempImages = new ArrayList<>(); 34 | 35 | for (WatermarkAttributes attr : attrs) { 36 | OverlayContext context = processOverlay(attr, dimensions, tempImages, lastLabel, step, isEmptyFilter); 37 | appendOverlayFilters(filter, context); 38 | 39 | lastLabel = context.lastLabel; 40 | step = context.step; 41 | isEmptyFilter = context.isEmptyFilter; 42 | } 43 | 44 | return new FilterStepAttributes(filter.toString(), lastLabel, step, isEmptyFilter, tempImages); 45 | } 46 | 47 | private OverlayContext processOverlay(WatermarkAttributes attr, VideoDimensions dimensions, 48 | List tempImages, String lastLabel, int step, 49 | boolean isEmptyFilter) throws Exception { 50 | BufferedImage originalImage = attr.getImage().get(); 51 | Dimension targetDimensions = calculateTargetDimensions(originalImage, attr.getSize()); 52 | BufferedImage scaledImage = scaleImage(originalImage, targetDimensions); 53 | List coordinates = calculateOverlayCoordinates(attr, dimensions, targetDimensions); 54 | 55 | return new OverlayContext(scaledImage, coordinates, tempImages, lastLabel, step, isEmptyFilter); 56 | } 57 | 58 | private Dimension calculateTargetDimensions(BufferedImage image, int sizePercentage) { 59 | double scale = sizePercentage / 100.0; 60 | int width = Math.max(1, (int) Math.round(image.getWidth() * scale)); 61 | int height = Math.max(1, (int) Math.round(image.getHeight() * scale)); 62 | return new Dimension(width, height); 63 | } 64 | 65 | private BufferedImage scaleImage(BufferedImage original, Dimension targetSize) { 66 | BufferedImage scaled = createScaledImage(targetSize); 67 | Graphics2D g2d = scaled.createGraphics(); 68 | 69 | try { 70 | configureGraphics(g2d); 71 | g2d.drawImage(original, 0, 0, targetSize.width, targetSize.height, null); 72 | } finally { 73 | g2d.dispose(); 74 | } 75 | 76 | return scaled; 77 | } 78 | 79 | private BufferedImage createScaledImage(Dimension size) { 80 | return new BufferedImage(size.width, size.height, BufferedImage.TYPE_INT_ARGB); 81 | } 82 | 83 | private void configureGraphics(Graphics2D g2d) { 84 | g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); 85 | } 86 | 87 | private List calculateOverlayCoordinates(WatermarkAttributes attr, VideoDimensions dimensions, 88 | Dimension overlaySize) { 89 | return WatermarkPositioner.defineXY( 90 | attr, 91 | dimensions.getWidth(), 92 | dimensions.getHeight(), 93 | overlaySize.width, 94 | overlaySize.height 95 | ); 96 | } 97 | 98 | private void appendOverlayFilters(StringBuilder filter, OverlayContext context) throws IOException { 99 | for (Coordinates coord : context.coordinates) { 100 | File tempImageFile = saveTempImage(context.scaledImage); 101 | context.tempImages.add(tempImageFile); 102 | 103 | String overlayFilter = buildOverlayFilter(coord, context.inLabel, context.outLabel, 104 | context.tempImages.size()); 105 | 106 | if (!context.isEmptyFilter) { 107 | filter.append(","); 108 | } 109 | filter.append(overlayFilter); 110 | 111 | context.advance(); 112 | } 113 | } 114 | 115 | private File saveTempImage(BufferedImage image) throws IOException { 116 | File tempFile = Files.createTempFile("wmk-img", ".png").toFile(); 117 | ImageIO.write(image, "png", tempFile); 118 | return tempFile; 119 | } 120 | 121 | private String buildOverlayFilter(Coordinates coord, String inLabel, String outLabel, int imageIndex) { 122 | return String.format("%s[%d:v]overlay=x=%d:y=%d%s", 123 | inLabel, 124 | imageIndex, 125 | coord.getX(), 126 | coord.getY(), 127 | outLabel 128 | ); 129 | } 130 | 131 | @Override 132 | public int getPriority() { 133 | return DEFAULT_PRIORITY; 134 | } 135 | 136 | /** 137 | * Context holder for overlay filter building state 138 | */ 139 | private static class OverlayContext { 140 | private final BufferedImage scaledImage; 141 | private final List coordinates; 142 | private final List tempImages; 143 | private String lastLabel; 144 | private int step; 145 | private boolean isEmptyFilter; 146 | private String inLabel; 147 | private String outLabel; 148 | 149 | OverlayContext(BufferedImage scaledImage, List coordinates, List tempImages, 150 | String lastLabel, int step, boolean isEmptyFilter) { 151 | this.scaledImage = scaledImage; 152 | this.coordinates = coordinates; 153 | this.tempImages = tempImages; 154 | this.lastLabel = lastLabel; 155 | this.step = step; 156 | this.isEmptyFilter = isEmptyFilter; 157 | updateLabels(); 158 | } 159 | 160 | private void updateLabels() { 161 | this.inLabel = step == 0 ? "[0:v]" : lastLabel; 162 | this.outLabel = "[v" + (step + 1) + "]"; 163 | } 164 | 165 | void advance() { 166 | this.lastLabel = this.outLabel; 167 | this.isEmptyFilter = false; 168 | this.step++; 169 | updateLabels(); 170 | } 171 | } 172 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Code climate](https://api.codeclimate.com/v1/badges/0cd17315421a1bec3587/maintainability)](https://codeclimate.com/github/OlegCheban/WaterMarkIt/maintainability) 2 | [![Code Coverage](https://qlty.sh/gh/OlegCheban/projects/WaterMarkIt/coverage.svg)](https://qlty.sh/gh/OlegCheban/projects/WaterMarkIt) 3 | [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/OlegCheban/WaterMarkIt) 4 | [![javadoc](https://img.shields.io/badge/javadoc-1.4.1-brightgreen.svg)](https://javadoc.io/doc/io.github.watermark-lab/WaterMarkIt/latest/index.html) 5 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/OlegCheban/WaterMarkIt) 6 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/OlegCheban/WaterMarkIt/blob/master/LICENSE) 7 | # WaterMarkIt 8 | 9 | A lightweight, framework-agnostic Java library for adding watermarks to various file types, including PDFs and videos. The library was developed to address the challenge of creating watermarks that cannot be easily removed from PDF files. Many PDF editors allow users to edit even secured files, and when a watermark is added as a separate layer, it can be easily removed. 10 | 11 | ## Features 12 | 13 | - **Internal DSL**: Provides a user-friendly way to configure and apply watermarks with ease, while also ensuring type safety at compilation time. 14 | 15 | - **Types of Watermarks**: 16 | - Text-based watermarks 17 | - Image-based watermarks 18 | 19 | - **Customizable Watermarks**: Customize various aspects of your watermark, including: 20 | - Font 21 | - Color 22 | - Size 23 | - Position 24 | - Rotation 25 | - Opacity 26 | - DPI 27 | 28 | - **Trademarks**: A capability to add the trademark symbol ® to text-based watermarks. 29 | 30 | - **Page orientation support**: Full support for both portrait and landscape orientations. 31 | 32 | - **Supported Formats**: 33 | - PDF 34 | - Images (JPEG, PNG, etc.) 35 | - Videos (MP4, MOV, AVI, MKV, etc) 36 | 37 | - **Drawn Watermarks**: The library provides the `WatermarkingMethod.DRAW` method to add watermarks to PDF files that can't be easily removed. This mode generates an image from a PDF page, applies watermarks to the image, and replaces all layers of the page with the modified image. 38 | 39 | - **Multithreading**: Leverages a thread pool for efficient watermarking. Particularly useful for the `WatermarkingMethod.DRAW` method and multi-page files such as PDFs, enabling parallel watermarking with a separate thread for each page. 40 | 41 | ## Getting Started 42 | 43 | ### Prerequisites 44 | 45 | - Java 11 or higher 46 | - Maven or Gradle 47 | 48 | ### Installation 49 | 50 | **For Maven**, add the following dependency to your `pom.xml`: 51 | 52 | ```xml 53 | 54 | io.github.watermark-lab 55 | WaterMarkIt 56 | 1.4.1 57 | 58 | ``` 59 | 60 | **For Gradle**, add the following to your `build.gradle`: 61 | ```kotlin 62 | implementation 'io.github.watermark-lab:WaterMarkIt:1.4.1' 63 | ``` 64 | 65 | ### Usage 66 | 67 | ```java 68 | WatermarkService.create( 69 | //use a thread pool when necessary - for instance, for large PDFs with many pages 70 | Executors.newFixedThreadPool( 71 | Runtime.getRuntime().availableProcessors() 72 | ) 73 | ) 74 | .watermarkPDF(new File("path/to/file.pdf")) 75 | .withImage(new File("path/to/watermark.png")) 76 | .position(WatermarkPosition.CENTER).end() 77 | .opacity(20) 78 | .and() 79 | .withText("WaterMarkIt") 80 | .font(Font.ARIAL) 81 | .bold() 82 | .color(Color.BLUE) 83 | .addTrademark() 84 | .end() 85 | .position(WatermarkPosition.TILED) 86 | .adjust(35, 0) 87 | .horizontalSpacing(10) 88 | .end() 89 | .opacity(10) 90 | .rotation(25) 91 | .size(110) 92 | .and() 93 | .withText(LocalDateTime.now().toString()).end() 94 | .position(WatermarkPosition.TOP_RIGHT) 95 | .adjust(0, -30) 96 | .end() 97 | .size(50) 98 | .apply() 99 | ``` 100 | ![Screenshot](https://github.com/user-attachments/assets/5d573ee8-ddf3-4204-8c33-502099bb39eb) 101 | 102 | ### Watermarking conditions 103 | ```java 104 | // skip the first page (the page index starts from 0) 105 | WatermarkService.create() 106 | .watermarkPDF(document) 107 | .withText("Text-based Watermark").end() 108 | .pageFilter(index -> index >= 1) 109 | .apply() 110 | ``` 111 | 112 | ```java 113 | // don't add a watermark for the owner of the file; the owner has access to the original file. 114 | WatermarkService.create() 115 | .watermarkPDF(document) 116 | .withText("Text-based Watermark").end() 117 | .enableIf(!isOwner) 118 | .apply() 119 | ``` 120 | 121 | ```java 122 | // Apply watermark only if the document has more than 3 pages 123 | WatermarkService.create() 124 | .watermarkPDF(document) 125 | .withText("Text-based Watermark").end() 126 | .documentFilter(document -> document.getNumberOfPages() > 3) 127 | .apply() 128 | ``` 129 | 130 | ### Video Watermarking 131 | 132 | The library supports adding watermarks to video files using FFmpeg. This feature allows you to apply both text-based and image-based watermarks to various video formats. 133 | 134 | #### Prerequisites for Video Watermarking 135 | 136 | - **FFmpeg**: The library requires FFmpeg to be installed on your system and available in the system PATH. FFmpeg is used internally to process video files and apply watermarks. 137 | 138 | **Installation:** 139 | - **Windows**: Download from [FFmpeg official website](https://ffmpeg.org/download.html) or use package managers like Chocolatey (`choco install ffmpeg`) 140 | - **macOS**: Use Homebrew (`brew install ffmpeg`) 141 | - **Linux**: Use your distribution's package manager (e.g., `sudo apt install ffmpeg` on Ubuntu) 142 | 143 | ```java 144 | WatermarkService.create() 145 | .watermarkVideo(videoFile) 146 | .withText("WaterMarkIt") 147 | .color(Color.RED) 148 | .end() 149 | .opacity(50) 150 | .position(WatermarkPosition.CENTER).end() 151 | .size(30) 152 | .and() 153 | .withImage(logoFile) 154 | .position(WatermarkPosition.BOTTOM_RIGHT).end() 155 | .size(8) 156 | .apply(); 157 | ``` 158 | 159 | ## Extensibility and Customization 160 | 161 | The library uses Java's ServiceLoader mechanism to load implementations of various services. You can override the services that implement the `Prioritizable` interface. 162 | 163 | 1. Create your own implementation of the desired service interface 164 | 2. Implement the `getPriority()` method to return a value higher than the default implementation 165 | 3. Register your implementation in the `META-INF/services` directory 166 | 167 | ```java 168 | public class CustomPdfWatermarker implements DrawPdfWatermarker { 169 | @Override 170 | public int getPriority() { 171 | // Return a value higher than default to take precedence 172 | return Prioritizable.DEFAULT_PRIORITY + 1; 173 | } 174 | 175 | @Override 176 | public void watermark(PDDocument document, int pageIndex, List attrs) throws IOException { 177 | // Custom watermarking implementation 178 | } 179 | } 180 | ``` 181 | 182 | Then create a file `META-INF/services/com.markit.pdf.draw.DrawPdfWatermarker` containing: 183 | ``` 184 | com.example.CustomPdfWatermarker 185 | ``` 186 | 187 | ## Why Kotlin? 188 | 189 | While WaterMarkIt is primarily a Java library targeting Java 11 for better compatibility, we selectively use Kotlin in specific areas to enhance code quality and developer experience: 190 | 191 | ### Use Cases 192 | 193 | - **Test Code**: Kotlin's concise syntax and powerful testing features make our test suite more readable and maintainable 194 | - **Data Classes**: Java 11 lacks records (introduced in Java 14+), so we use Kotlin's data classes for immutable value objects 195 | - **Enums**: Kotlin enums provide cleaner syntax for associating data with enum values 196 | - **Exception Classes**: Custom exceptions benefit from Kotlin's concise class declarations with automatic constructor generation 197 | 198 | ### For Contributors 199 | 200 | When contributing to WaterMarkIt: 201 | - **Use Java** for all public APIs and core library functionality 202 | - **Use Kotlin** for test classes, internal data structures, and utility classes where it provides clear benefits 203 | 204 | ## Dependencies 205 | - **Apache PDFBox**: [Apache PDFBox](https://pdfbox.apache.org/) - A Java library for working with PDF documents. 206 | - **JAI Image I/O**: [JAI Image I/O](https://github.com/jai-imageio/jai-imageio-core) - Image I/O library for Java, supporting various image formats. 207 | - **commons-logging**: [Apache Commons Logging](https://commons.apache.org/proper/commons-logging/) - A simple logging facade for Java. 208 | 209 | ## Contributing 210 | 211 | We welcome contributions from the community! If you'd like to contribute to WaterMarkIt, please read our [Contributing Guide](CONTRIBUTING.md) for details on how to get started, our coding standards, and the pull request process. 212 | 213 | Your contributions help make WaterMarkIt better for everyone! 214 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | io.github.watermark-lab 8 | WaterMarkIt 9 | 1.4.1 10 | jar 11 | 12 | ${project.groupId}:${project.artifactId} 13 | A lightweight Java library for adding watermarks to various file types, including PDFs and images 14 | https://watermarkit.com 15 | 16 | 17 | 18 | MIT License 19 | http://www.opensource.org/licenses/mit-license.php 20 | 21 | 22 | 23 | 24 | 25 | Oleg Cheban 26 | oleg.cheban1989@gmail.com 27 | https://github.com/OlegCheban 28 | 29 | 30 | 31 | 32 | scm:git:git@github.com:OlegCheban/WaterMarkIt.git 33 | scm:git:git@github.com:OlegCheban/WaterMarkIt.git 34 | https://github.com/OlegCheban/WaterMarkIt 35 | 36 | 37 | 38 | 11 39 | UTF-8 40 | 11 41 | 2.0.32 42 | 1.4.0 43 | 1.4.0 44 | 3.0.4 45 | 5.13.4 46 | 2.2.21 47 | 0.8.14 48 | 3.14.1 49 | 3.3.1 50 | 2.1.0 51 | 52 | 53 | 54 | 55 | org.apache.pdfbox 56 | pdfbox 57 | ${pdfbox.version} 58 | 59 | 60 | com.github.jai-imageio 61 | jai-imageio-core 62 | ${ai-imageio-core.version} 63 | 64 | 65 | com.github.jai-imageio 66 | jai-imageio-jpeg2000 67 | ${jai-imageio-jpeg2000.version} 68 | 69 | 70 | org.apache.pdfbox 71 | jbig2-imageio 72 | ${jbig2-imageio.version} 73 | 74 | 75 | org.junit.jupiter 76 | junit-jupiter-api 77 | ${junit.version} 78 | test 79 | 80 | 81 | org.jetbrains.kotlin 82 | kotlin-stdlib-jdk8 83 | ${kotlin.version} 84 | 85 | 86 | org.jetbrains.kotlin 87 | kotlin-test 88 | ${kotlin.version} 89 | test 90 | 91 | 92 | 93 | 94 | 95 | 96 | org.apache.maven.plugins 97 | maven-source-plugin 98 | ${maven-source-plugin.verson} 99 | 100 | 101 | attach-sources 102 | verify 103 | 104 | jar-no-fork 105 | 106 | 107 | 108 | 109 | 110 | org.jetbrains.dokka 111 | dokka-maven-plugin 112 | ${dokka-maven-plugin.version} 113 | 114 | 115 | package 116 | 117 | javadocJar 118 | 119 | 120 | 121 | 122 | 123 | org.jetbrains.kotlin 124 | kotlin-maven-plugin 125 | ${kotlin.version} 126 | 127 | 128 | compile 129 | compile 130 | 131 | compile 132 | 133 | 134 | 135 | src/main/java 136 | target/generated-sources/annotations 137 | 138 | 139 | 140 | 141 | test-compile 142 | test-compile 143 | 144 | test-compile 145 | 146 | 147 | 148 | src/test/java 149 | target/generated-test-sources/test-annotations 150 | 151 | 152 | 153 | 154 | 155 | 1.8 156 | 157 | 158 | 159 | org.apache.maven.plugins 160 | maven-compiler-plugin 161 | ${maven-compiler-plugin.version} 162 | 163 | 164 | default-compile 165 | none 166 | 167 | 168 | default-testCompile 169 | none 170 | 171 | 172 | compile 173 | compile 174 | 175 | compile 176 | 177 | 178 | 179 | testCompile 180 | test-compile 181 | 182 | testCompile 183 | 184 | 185 | 186 | 187 | 11 188 | true 189 | 190 | -Werror 191 | 192 | 193 | 194 | 195 | org.jacoco 196 | jacoco-maven-plugin 197 | ${jacoco.verson} 198 | 199 | 200 | 201 | prepare-agent 202 | 203 | 204 | 205 | report 206 | prepare-package 207 | 208 | report 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | release 219 | 220 | 221 | 222 | org.sonatype.central 223 | central-publishing-maven-plugin 224 | 0.9.0 225 | true 226 | 227 | central 228 | true 229 | true 230 | 231 | 232 | 233 | org.apache.maven.plugins 234 | maven-gpg-plugin 235 | 3.2.8 236 | 237 | 238 | sign-artifacts 239 | verify 240 | 241 | sign 242 | 243 | 244 | 245 | 246 | 247 | --pinentry-mode 248 | loopback 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | --------------------------------------------------------------------------------