2023-09-09 19:21:55 +02:00
package stirling.software.SPDF.controller.api.misc ;
2023-06-04 23:40:14 +02:00
2024-04-22 00:06:44 +02:00
import java.awt.AlphaComposite ;
2024-04-24 20:59:01 +02:00
import java.awt.BasicStroke ;
2023-06-25 10:16:32 +02:00
import java.awt.Color ;
2024-04-22 00:06:44 +02:00
import java.awt.GradientPaint ;
import java.awt.Graphics2D ;
2024-04-24 20:59:01 +02:00
import java.awt.RenderingHints ;
2023-06-25 10:16:32 +02:00
import java.awt.geom.AffineTransform ;
2024-04-24 20:59:01 +02:00
import java.awt.geom.Ellipse2D ;
import java.awt.geom.Path2D ;
2023-06-25 10:16:32 +02:00
import java.awt.image.AffineTransformOp ;
import java.awt.image.BufferedImage ;
import java.awt.image.BufferedImageOp ;
import java.awt.image.ConvolveOp ;
import java.awt.image.Kernel ;
2023-09-03 02:24:02 +02:00
import java.io.ByteArrayOutputStream ;
2023-06-04 23:40:14 +02:00
import java.io.IOException ;
2023-10-05 22:29:56 +02:00
import java.security.SecureRandom ;
2024-04-22 00:06:44 +02:00
import java.util.ArrayList ;
import java.util.List ;
2023-06-25 10:16:32 +02:00
import java.util.Random ;
2024-01-13 00:15:27 +01:00
import org.apache.pdfbox.Loader ;
2023-06-04 23:40:14 +02:00
import org.apache.pdfbox.pdmodel.PDDocument ;
2024-04-22 00:06:44 +02:00
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory ;
2023-06-25 10:16:32 +02:00
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject ;
import org.apache.pdfbox.rendering.ImageType ;
import org.apache.pdfbox.rendering.PDFRenderer ;
2023-06-04 23:40:14 +02:00
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
import org.springframework.http.ResponseEntity ;
2023-09-09 19:51:43 +02:00
import org.springframework.web.bind.annotation.ModelAttribute ;
2024-04-22 00:06:44 +02:00
import org.springframework.web.bind.annotation.PostMapping ;
2023-09-12 00:19:50 +02:00
import org.springframework.web.bind.annotation.RequestMapping ;
2023-06-04 23:40:14 +02:00
import org.springframework.web.bind.annotation.RestController ;
import org.springframework.web.multipart.MultipartFile ;
2024-02-08 03:40:33 +01:00
import io.github.pixee.security.Filenames ;
2024-04-27 00:27:40 +02:00
import io.swagger.v3.oas.annotations.Hidden ;
2023-06-04 23:40:14 +02:00
import io.swagger.v3.oas.annotations.Operation ;
2023-06-25 10:16:32 +02:00
import io.swagger.v3.oas.annotations.tags.Tag ;
2023-12-30 20:11:27 +01:00
2023-09-09 19:51:43 +02:00
import stirling.software.SPDF.model.api.PDFFile ;
2024-04-22 00:06:44 +02:00
import stirling.software.SPDF.utils.PdfUtils ;
2023-06-04 23:40:14 +02:00
import stirling.software.SPDF.utils.WebResponseUtils ;
@RestController
2023-09-12 00:19:50 +02:00
@RequestMapping ( " /api/v1/misc " )
@Tag ( name = " Misc " , description = " Miscellaneous APIs " )
2023-06-24 00:29:53 +02:00
public class FakeScanControllerWIP {
2023-06-04 23:40:14 +02:00
2023-06-24 00:29:53 +02:00
private static final Logger logger = LoggerFactory . getLogger ( FakeScanControllerWIP . class ) ;
2023-06-04 23:40:14 +02:00
2024-04-27 00:27:40 +02:00
// TODO finish
2024-04-24 20:59:01 +02:00
@PostMapping ( consumes = " multipart/form-data " , value = " /fake-scan " )
2024-04-27 00:27:40 +02:00
@Hidden
2024-04-24 20:59:01 +02:00
@Operation (
summary = " Repair a PDF file " ,
description =
" This endpoint repairs a given PDF file by running Ghostscript command. The PDF is first saved to a temporary location, repaired, read back, and then returned as a response. " )
2024-04-22 00:06:44 +02:00
public ResponseEntity < byte [ ] > fakeScan ( @ModelAttribute PDFFile request ) throws IOException {
2023-09-09 19:51:43 +02:00
MultipartFile inputFile = request . getFileInput ( ) ;
2023-06-04 23:40:14 +02:00
2024-04-22 00:06:44 +02:00
// Load the PDF document
2024-01-13 00:15:27 +01:00
PDDocument document = Loader . loadPDF ( inputFile . getBytes ( ) ) ;
2024-04-22 00:06:44 +02:00
PDFRenderer renderer = new PDFRenderer ( document ) ;
List < BufferedImage > images = new ArrayList < > ( ) ;
// Convert each page to an image
for ( int i = 0 ; i < document . getNumberOfPages ( ) ; i + + ) {
BufferedImage image = renderer . renderImageWithDPI ( i , 150 , ImageType . GRAY ) ;
images . add ( processImage ( image ) ) ;
2023-06-04 23:40:14 +02:00
}
document . close ( ) ;
2023-12-30 20:11:27 +01:00
2024-04-22 00:06:44 +02:00
// Create a new PDF document with the processed images
ByteArrayOutputStream baos = new ByteArrayOutputStream ( ) ;
PDDocument newDocument = new PDDocument ( ) ;
for ( BufferedImage img : images ) {
// PDPageContentStream contentStream = new PDPageContentStream(newDocument, new
// PDPage());
PDImageXObject pdImage = JPEGFactory . createFromImage ( newDocument , img ) ;
PdfUtils . addImageToDocument ( newDocument , pdImage , " maintainAspectRatio " , false ) ;
}
newDocument . save ( baos ) ;
newDocument . close ( ) ;
// Return the optimized PDF as a response
String outputFilename =
Filenames . toSimpleFileName ( inputFile . getOriginalFilename ( ) )
. replaceFirst ( " [.][^.]+$ " , " " )
+ " _scanned.pdf " ;
return WebResponseUtils . boasToWebResponse ( baos , outputFilename ) ;
}
public BufferedImage processImage ( BufferedImage image ) {
// Rotation
2024-04-27 00:27:40 +02:00
image = softenEdges ( image , 50 ) ;
image = rotate ( image , 1 ) ;
2024-04-24 20:59:01 +02:00
2024-04-22 00:06:44 +02:00
image = applyGaussianBlur ( image , 0 . 5 ) ;
2024-04-27 00:27:40 +02:00
addGaussianNoise ( image , 0 . 5 ) ;
2024-04-22 00:06:44 +02:00
image = linearStretch ( image ) ;
2024-04-27 00:27:40 +02:00
addDustAndHairs ( image , 3 ) ;
2024-04-22 00:06:44 +02:00
return image ;
}
2024-04-24 20:59:01 +02:00
private BufferedImage rotate ( BufferedImage image , double rotation ) {
2024-04-22 00:06:44 +02:00
2024-04-24 20:59:01 +02:00
double rotationRequired = Math . toRadians ( rotation ) ;
2024-04-22 00:06:44 +02:00
double locationX = image . getWidth ( ) / 2 ;
double locationY = image . getHeight ( ) / 2 ;
2023-06-04 23:40:14 +02:00
AffineTransform tx =
AffineTransform . getRotateInstance ( rotationRequired , locationX , locationY ) ;
2024-04-22 00:06:44 +02:00
AffineTransformOp op = new AffineTransformOp ( tx , AffineTransformOp . TYPE_BICUBIC ) ;
return op . filter ( image , null ) ;
}
private BufferedImage applyGaussianBlur ( BufferedImage image , double sigma ) {
int radius = 3 ; // Fixed radius size for simplicity
int size = 2 * radius + 1 ;
float [ ] data = new float [ size * size ] ;
double sum = 0 . 0 ;
for ( int i = - radius ; i < = radius ; i + + ) {
for ( int j = - radius ; j < = radius ; j + + ) {
double xDistance = i * i ;
double yDistance = j * j ;
double g = Math . exp ( - ( xDistance + yDistance ) / ( 2 * sigma * sigma ) ) ;
data [ ( i + radius ) * size + j + radius ] = ( float ) g ;
sum + = g ;
2023-12-30 20:11:27 +01:00
}
}
2024-04-22 00:06:44 +02:00
// Normalize the kernel
for ( int i = 0 ; i < data . length ; i + + ) {
data [ i ] / = sum ;
}
2023-12-30 20:11:27 +01:00
2024-04-22 00:06:44 +02:00
Kernel kernel = new Kernel ( size , size , data ) ;
BufferedImageOp op = new ConvolveOp ( kernel , ConvolveOp . EDGE_NO_OP , null ) ;
return op . filter ( image , null ) ;
}
2023-12-30 20:11:27 +01:00
2024-04-22 00:06:44 +02:00
public BufferedImage softenEdges ( BufferedImage image , int featherRadius ) {
int width = image . getWidth ( ) ;
int height = image . getHeight ( ) ;
BufferedImage output = new BufferedImage ( width , height , BufferedImage . TYPE_INT_ARGB ) ;
Graphics2D g2 = output . createGraphics ( ) ;
2024-04-24 20:59:01 +02:00
g2 . setRenderingHint ( RenderingHints . KEY_ANTIALIASING , RenderingHints . VALUE_ANTIALIAS_ON ) ;
g2 . setRenderingHint ( RenderingHints . KEY_RENDERING , RenderingHints . VALUE_RENDER_QUALITY ) ;
g2 . setRenderingHint (
RenderingHints . KEY_INTERPOLATION , RenderingHints . VALUE_INTERPOLATION_BICUBIC ) ;
2024-04-22 00:06:44 +02:00
g2 . drawImage ( image , 0 , 0 , null ) ;
g2 . setComposite ( AlphaComposite . DstIn ) ;
2024-04-27 00:27:40 +02:00
// Top edge
2024-04-22 00:06:44 +02:00
g2 . setPaint (
new GradientPaint (
2024-04-27 00:27:40 +02:00
0 ,
0 ,
new Color ( 0 , 0 , 0 , 1f ) ,
0 ,
featherRadius * 2 ,
new Color ( 0 , 0 , 0 , 0f ) ) ) ;
g2 . fillRect ( 0 , 0 , width , featherRadius ) ;
// Bottom edge
2024-04-22 00:06:44 +02:00
g2 . setPaint (
new GradientPaint (
0 ,
2024-04-27 00:27:40 +02:00
height - featherRadius * 2 ,
2024-04-22 00:06:44 +02:00
new Color ( 0 , 0 , 0 , 0f ) ,
0 ,
height ,
new Color ( 0 , 0 , 0 , 1f ) ) ) ;
2024-04-27 00:27:40 +02:00
g2 . fillRect ( 0 , height - featherRadius , width , featherRadius ) ;
// Left edge
2024-04-22 00:06:44 +02:00
g2 . setPaint (
new GradientPaint (
2024-04-27 00:27:40 +02:00
0 ,
0 ,
new Color ( 0 , 0 , 0 , 1f ) ,
featherRadius * 2 ,
0 ,
new Color ( 0 , 0 , 0 , 0f ) ) ) ;
g2 . fillRect ( 0 , 0 , featherRadius , height ) ;
// Right edge
2024-04-22 00:06:44 +02:00
g2 . setPaint (
new GradientPaint (
2024-04-27 00:27:40 +02:00
width - featherRadius * 2 ,
2024-04-22 00:06:44 +02:00
0 ,
new Color ( 0 , 0 , 0 , 0f ) ,
width ,
0 ,
new Color ( 0 , 0 , 0 , 1f ) ) ) ;
2024-04-27 00:27:40 +02:00
g2 . fillRect ( width - featherRadius , 0 , featherRadius , height ) ;
2024-04-22 00:06:44 +02:00
g2 . dispose ( ) ;
return output ;
}
2023-12-30 20:11:27 +01:00
2024-04-24 20:59:01 +02:00
private void addDustAndHairs ( BufferedImage image , float intensity ) {
int width = image . getWidth ( ) ;
int height = image . getHeight ( ) ;
Graphics2D g2d = image . createGraphics ( ) ;
Random random = new SecureRandom ( ) ;
// Set rendering hints for better quality
g2d . setRenderingHint ( RenderingHints . KEY_ANTIALIASING , RenderingHints . VALUE_ANTIALIAS_ON ) ;
// Calculate the number of artifacts based on intensity
int numSpots = ( int ) ( intensity * 10 ) ;
int numHairs = ( int ) ( intensity * 20 ) ;
// Add spots with more variable sizes
g2d . setColor ( new Color ( 100 , 100 , 100 , 50 ) ) ; // Semi-transparent gray
for ( int i = 0 ; i < numSpots ; i + + ) {
int x = random . nextInt ( width ) ;
int y = random . nextInt ( height ) ;
int ovalSize = 1 + random . nextInt ( 3 ) ; // Base size + variable component
if ( random . nextFloat ( ) > 0 . 9 ) {
// 10% chance to get a larger spot
ovalSize + = random . nextInt ( 3 ) ;
}
g2d . fill ( new Ellipse2D . Double ( x , y , ovalSize , ovalSize ) ) ;
}
// Add hairs
g2d . setStroke ( new BasicStroke ( 0 . 5f ) ) ; // Thin stroke for hairs
g2d . setColor ( new Color ( 80 , 80 , 80 , 40 ) ) ; // Slightly lighter and more transparent
for ( int i = 0 ; i < numHairs ; i + + ) {
int x1 = random . nextInt ( width ) ;
int y1 = random . nextInt ( height ) ;
int x2 = x1 + random . nextInt ( 20 ) - 10 ; // Random length and direction
int y2 = y1 + random . nextInt ( 20 ) - 10 ;
Path2D . Double hair = new Path2D . Double ( ) ;
hair . moveTo ( x1 , y1 ) ;
hair . curveTo ( x1 , y1 , ( x1 + x2 ) / 2 , ( y1 + y2 ) / 2 , x2 , y2 ) ;
g2d . draw ( hair ) ;
}
g2d . dispose ( ) ;
}
2024-04-22 00:06:44 +02:00
private void addGaussianNoise ( BufferedImage image , double strength ) {
Random rand = new SecureRandom ( ) ;
int width = image . getWidth ( ) ;
int height = image . getHeight ( ) ;
for ( int i = 0 ; i < width ; i + + ) {
for ( int j = 0 ; j < height ; j + + ) {
int rgba = image . getRGB ( i , j ) ;
int alpha = ( rgba > > 24 ) & 0xff ;
int red = ( rgba > > 16 ) & 0xff ;
int green = ( rgba > > 8 ) & 0xff ;
int blue = rgba & 0xff ;
// Apply Gaussian noise
red = ( int ) ( red + rand . nextGaussian ( ) * strength ) ;
green = ( int ) ( green + rand . nextGaussian ( ) * strength ) ;
blue = ( int ) ( blue + rand . nextGaussian ( ) * strength ) ;
// Clamping values to the 0-255 range
red = Math . min ( Math . max ( 0 , red ) , 255 ) ;
green = Math . min ( Math . max ( 0 , green ) , 255 ) ;
blue = Math . min ( Math . max ( 0 , blue ) , 255 ) ;
image . setRGB ( i , j , ( alpha < < 24 ) | ( red < < 16 ) | ( green < < 8 ) | blue ) ;
}
}
}
2023-12-30 20:11:27 +01:00
2024-04-22 00:06:44 +02:00
public BufferedImage linearStretch ( BufferedImage image ) {
int width = image . getWidth ( ) ;
int height = image . getHeight ( ) ;
int min = 255 ;
int max = 0 ;
// First pass: find the min and max grayscale values
for ( int y = 0 ; y < height ; y + + ) {
for ( int x = 0 ; x < width ; x + + ) {
int rgb = image . getRGB ( x , y ) ;
int gray =
( int )
( ( ( rgb > > 16 ) & 0xff ) * 0 . 299
+ ( ( rgb > > 8 ) & 0xff ) * 0 . 587
+ ( rgb & 0xff ) * 0 . 114 ) ; // Convert to grayscale
if ( gray < min ) min = gray ;
if ( gray > max ) max = gray ;
}
2023-06-04 23:40:14 +02:00
}
2024-04-22 00:06:44 +02:00
// Second pass: stretch the histogram
for ( int y = 0 ; y < height ; y + + ) {
for ( int x = 0 ; x < width ; x + + ) {
int rgb = image . getRGB ( x , y ) ;
int alpha = ( rgb > > 24 ) & 0xff ;
int red = ( rgb > > 16 ) & 0xff ;
int green = ( rgb > > 8 ) & 0xff ;
int blue = rgb & 0xff ;
// Apply linear stretch to each channel
red = ( int ) ( ( ( red - min ) / ( float ) ( max - min ) ) * 255 ) ;
green = ( int ) ( ( ( green - min ) / ( float ) ( max - min ) ) * 255 ) ;
blue = ( int ) ( ( ( blue - min ) / ( float ) ( max - min ) ) * 255 ) ;
// Set new RGB value maintaining the alpha channel
rgb = ( alpha < < 24 ) | ( red < < 16 ) | ( green < < 8 ) | blue ;
image . setRGB ( x , y , rgb ) ;
}
}
return image ;
2023-06-04 23:40:14 +02:00
}
}