mirror of
				https://github.com/Frooodle/Stirling-PDF.git
				synced 2025-10-25 11:17:28 +02:00 
			
		
		
		
	Merge pull request #459 from Artem-ka-create/issue-372-pdfbox
Issue 372 pdfbox
This commit is contained in:
		
						commit
						9d052b310f
					
				
							
								
								
									
										58
									
								
								build.gradle
									
									
									
									
									
								
							
							
						
						
									
										58
									
								
								build.gradle
									
									
									
									
									
								
							| @ -46,15 +46,15 @@ launch4j { | ||||
|   outfile="Stirling-PDF.exe" | ||||
|   headerType="console" | ||||
|   jarTask = tasks.bootJar | ||||
|    | ||||
| 
 | ||||
|   errTitle="Encountered error, Do you have Java 17?" | ||||
|   downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe"  | ||||
|   downloadUrl="https://download.oracle.com/java/17/latest/jdk-17_windows-x64_bin.exe" | ||||
|   variables=["BROWSER_OPEN=true"] | ||||
|   jreMinVersion="17" | ||||
|    | ||||
| 
 | ||||
|   mutexName="Stirling-PDF" | ||||
|   windowTitle="Stirling-PDF" | ||||
|    | ||||
| 
 | ||||
|   messagesStartupError="An error occurred while starting Stirling-PDF" | ||||
|   //messagesJreNotFoundError="This application requires a Java Runtime Environment, Please download Java 17." | ||||
|   messagesJreVersionError="You are running the wrong version of Java, Please download Java 17." | ||||
| @ -63,46 +63,48 @@ launch4j { | ||||
| } | ||||
| 
 | ||||
| dependencies { | ||||
| 	implementation 'org.yaml:snakeyaml:2.1' | ||||
| 	implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2' | ||||
| 	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2' | ||||
| 	 | ||||
| 	if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { | ||||
|     implementation 'org.yaml:snakeyaml:2.1' | ||||
|     implementation 'org.springframework.boot:spring-boot-starter-web:3.1.2' | ||||
|     implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.1.2' | ||||
| 
 | ||||
|     if (System.getenv('DOCKER_ENABLE_SECURITY') != 'false') { | ||||
|         implementation 'org.springframework.boot:spring-boot-starter-security:3.1.2' | ||||
|         implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5:3.1.2.RELEASE' | ||||
|         implementation "org.springframework.boot:spring-boot-starter-data-jpa" | ||||
| 		implementation "com.h2database:h2" | ||||
|         implementation "com.h2database:h2" | ||||
|     } | ||||
| 	 | ||||
| 	testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.4' | ||||
| 	 | ||||
| 	 | ||||
| 
 | ||||
| 	// https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio | ||||
| 	implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4' | ||||
| 	implementation 'commons-io:commons-io:2.13.0' | ||||
| 	 | ||||
|     testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.4' | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|     // https://mvnrepository.com/artifact/org.apache.pdfbox/jbig2-imageio | ||||
|     implementation group: 'org.apache.pdfbox', name: 'jbig2-imageio', version: '3.0.4' | ||||
|     implementation 'commons-io:commons-io:2.13.0' | ||||
|     implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' | ||||
|      | ||||
| 	//general PDF | ||||
| 
 | ||||
|     //general PDF | ||||
| 
 | ||||
|     // https://mvnrepository.com/artifact/com.opencsv/opencsv | ||||
|     implementation group: 'com.opencsv', name: 'opencsv', version: '5.7.1' | ||||
|     implementation 'org.apache.pdfbox:pdfbox:2.0.29' | ||||
|     implementation 'org.apache.pdfbox:xmpbox:2.0.29' | ||||
|     implementation 'org.bouncycastle:bcprov-jdk15on:1.70' | ||||
|     implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'  | ||||
|     implementation 'org.bouncycastle:bcpkix-jdk15on:1.70' | ||||
|     implementation 'org.springframework.boot:spring-boot-starter-actuator' | ||||
|     implementation 'io.micrometer:micrometer-core' | ||||
|     implementation group: 'com.google.zxing', name: 'core', version: '3.5.2' | ||||
|     // https://mvnrepository.com/artifact/org.commonmark/commonmark | ||||
| 	implementation 'org.commonmark:commonmark:0.21.0' | ||||
|     implementation 'org.commonmark:commonmark:0.21.0' | ||||
|     // https://mvnrepository.com/artifact/com.github.vladimir-bukhtoyarov/bucket4j-core | ||||
| 	implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0' | ||||
|      | ||||
|     developmentOnly("org.springframework.boot:spring-boot-devtools") | ||||
| 	compileOnly 'org.projectlombok:lombok:1.18.28' | ||||
| 	annotationProcessor 'org.projectlombok:lombok:1.18.28' | ||||
|     implementation 'com.github.vladimir-bukhtoyarov:bucket4j-core:7.6.0' | ||||
| 
 | ||||
|     developmentOnly("org.springframework.boot:spring-boot-devtools") | ||||
|     compileOnly 'org.projectlombok:lombok:1.18.28' | ||||
|     annotationProcessor 'org.projectlombok:lombok:1.18.28' | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| task writeVersion { | ||||
|     def propsFile = file('src/main/resources/version.properties') | ||||
|     def props = new Properties() | ||||
| @ -128,7 +130,7 @@ jar { | ||||
|         attributes 'Implementation-Title': 'Stirling-PDF', | ||||
|                    'Implementation-Version': project.version | ||||
|     } | ||||
|      | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| tasks.named('test') { | ||||
|  | ||||
| @ -0,0 +1,120 @@ | ||||
| package stirling.software.SPDF.controller.api; | ||||
| 
 | ||||
| import com.opencsv.CSVWriter; | ||||
| import io.swagger.v3.oas.annotations.Operation; | ||||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||||
| import org.apache.pdfbox.pdmodel.PDDocument; | ||||
| import org.apache.pdfbox.pdmodel.PDPage; | ||||
| import org.slf4j.Logger; | ||||
| import org.slf4j.LoggerFactory; | ||||
| import org.springframework.http.ContentDisposition; | ||||
| import org.springframework.http.HttpHeaders; | ||||
| import org.springframework.http.MediaType; | ||||
| import org.springframework.http.ResponseEntity; | ||||
| import org.springframework.web.bind.annotation.*; | ||||
| import stirling.software.SPDF.controller.api.strippers.PDFTableStripper; | ||||
| import stirling.software.SPDF.model.api.extract.PDFFilePage; | ||||
| 
 | ||||
| import java.awt.*; | ||||
| import java.io.ByteArrayInputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.StringWriter; | ||||
| import java.util.ArrayList; | ||||
| import java.util.List; | ||||
| 
 | ||||
| @RestController | ||||
| @RequestMapping("/api/v1/extract/pdf-to-csv") | ||||
| @Tag(name = "General", description = "General APIs") | ||||
| public class ExtractController { | ||||
| 
 | ||||
|     private static final Logger logger = LoggerFactory.getLogger(CropController.class); | ||||
| 
 | ||||
|     @PostMapping(consumes = "multipart/form-data") | ||||
|     @Operation(summary = "Extracts a PDF document to csv", description = "This operation takes an input PDF file and returns CSV file of whole page. Input:PDF Output:CSV Type:SISO") | ||||
|     public ResponseEntity<String> PdfToCsv(@ModelAttribute PDFFilePage form) | ||||
|             throws IOException { | ||||
| 
 | ||||
|         ArrayList<String> tableData = new ArrayList<>(); | ||||
|         int columnsCount = 0; | ||||
| 
 | ||||
|         try (PDDocument document = PDDocument.load(new ByteArrayInputStream(form.getFileInput().getBytes()))) { | ||||
|             final double res = 72; // PDF units are at 72 DPI | ||||
|             PDFTableStripper stripper = new PDFTableStripper(); | ||||
|             stripper.setSortByPosition(true); | ||||
|             stripper.setRegion(new Rectangle((int) Math.round(1.0 * res), (int) Math.round(1 * res), (int) Math.round(6 * res), (int) Math.round(9.0 * res))); | ||||
| 
 | ||||
|             PDPage pdPage = document.getPage(form.getPageId() - 1); | ||||
|             stripper.extractTable(pdPage); | ||||
|             columnsCount = stripper.getColumns(); | ||||
| 
 | ||||
|             for (int c = 0; c < columnsCount; ++c) { | ||||
|                 for(int r=0; r<stripper.getRows(); ++r) { | ||||
|                     tableData.add(stripper.getText(r, c)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         ArrayList<String> notEmptyColumns = new ArrayList<>(); | ||||
| 
 | ||||
|         for (String item: tableData) { | ||||
|             if(!item.trim().isEmpty()){ | ||||
|                 notEmptyColumns.add(item); | ||||
|             }else{ | ||||
|                 columnsCount--; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         List<String> fullTable  =  notEmptyColumns.stream().map((entity)-> | ||||
|             entity.replace('\n',' ').replace('\r',' ').trim().replaceAll("\\s{2,}", "|")).toList(); | ||||
| 
 | ||||
|         int rowsCount = fullTable.get(0).split("\\|").length; | ||||
| 
 | ||||
|         ArrayList<String> headersList = getTableHeaders(columnsCount,fullTable); | ||||
|         ArrayList<String> recordList = getRecordsList(rowsCount,fullTable); | ||||
| 
 | ||||
| 
 | ||||
|         StringWriter writer = new StringWriter(); | ||||
|         try (CSVWriter csvWriter = new CSVWriter(writer)) { | ||||
|             csvWriter.writeNext(headersList.toArray(new String[0])); | ||||
|             for (String record : recordList) { | ||||
|                 csvWriter.writeNext(record.split("\\|")); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         HttpHeaders headers = new HttpHeaders(); | ||||
|         headers.setContentDisposition(ContentDisposition.builder("attachment").filename(form.getFileInput().getOriginalFilename().replaceFirst("[.][^.]+$", "") + "_extracted.csv").build()); | ||||
|         headers.setContentType(MediaType.parseMediaType("text/csv")); | ||||
| 
 | ||||
|         return ResponseEntity.ok() | ||||
|                 .headers(headers) | ||||
|                 .body(writer.toString()); | ||||
|     } | ||||
| 
 | ||||
|     private ArrayList<String> getRecordsList( int rowsCounts ,List<String> items){ | ||||
|         ArrayList<String> recordsList = new ArrayList<>(); | ||||
| 
 | ||||
|             for (int b=1; b<rowsCounts;b++) { | ||||
|                 StringBuilder strbldr = new StringBuilder(); | ||||
| 
 | ||||
|                 for (int i=0;i<items.size();i++){ | ||||
|                     String[] parts = items.get(i).split("\\|"); | ||||
|                     strbldr.append(parts[b]); | ||||
|                     if (i!= items.size()-1){ | ||||
|                         strbldr.append("|"); | ||||
|                     } | ||||
|                 } | ||||
|                 recordsList.add(strbldr.toString()); | ||||
|             } | ||||
| 
 | ||||
|         return recordsList; | ||||
|     } | ||||
|     private ArrayList<String> getTableHeaders(int columnsCount, List<String> items){ | ||||
|         ArrayList<String> resultList = new ArrayList<>(); | ||||
|         for (int i=0;i<columnsCount;i++){ | ||||
|             String[] parts = items.get(i).split("\\|"); | ||||
|             resultList.add(parts[0]); | ||||
|         } | ||||
| 
 | ||||
|         return resultList; | ||||
|     } | ||||
| } | ||||
| @ -0,0 +1,354 @@ | ||||
| package stirling.software.SPDF.controller.api.strippers; | ||||
| 
 | ||||
| import org.apache.fontbox.util.BoundingBox; | ||||
| import org.apache.pdfbox.pdmodel.PDPage; | ||||
| import org.apache.pdfbox.pdmodel.common.PDRectangle; | ||||
| import org.apache.pdfbox.pdmodel.font.PDFont; | ||||
| import org.apache.pdfbox.pdmodel.font.PDType3Font; | ||||
| import org.apache.pdfbox.text.PDFTextStripper; | ||||
| import org.apache.pdfbox.text.PDFTextStripperByArea; | ||||
| import org.apache.pdfbox.text.TextPosition; | ||||
| 
 | ||||
| import java.awt.*; | ||||
| import java.awt.geom.AffineTransform; | ||||
| import java.awt.geom.Rectangle2D; | ||||
| import java.io.ByteArrayOutputStream; | ||||
| import java.io.IOException; | ||||
| import java.io.OutputStreamWriter; | ||||
| import java.io.Writer; | ||||
| import java.util.List; | ||||
| import java.util.*; | ||||
| 
 | ||||
| /** | ||||
|  * | ||||
|  * Class to extract tabular data from a PDF. | ||||
|  * Works by making a first pass of the page to group all nearby text items | ||||
|  * together, and then inferring a 2D grid from these regions. Each table cell | ||||
|  * is then extracted using a PDFTextStripperByArea object. | ||||
|  * | ||||
|  * Works best when | ||||
|  * headers are included in the detected region, to ensure representative text | ||||
|  * in every column. | ||||
|  * | ||||
|  * Based upon DrawPrintTextLocations PDFBox example | ||||
|  * (https://svn.apache.org/viewvc/pdfbox/trunk/examples/src/main/java/org/apache/pdfbox/examples/util/DrawPrintTextLocations.java) | ||||
|  * | ||||
|  * @author Beldaz | ||||
|  */ | ||||
| public class PDFTableStripper extends PDFTextStripper | ||||
| { | ||||
| 
 | ||||
|     /** | ||||
|      * This will print the documents data, for each table cell. | ||||
|      * | ||||
|      * @param args The command line arguments. | ||||
|      * | ||||
|      * @throws IOException If there is an error parsing the document. | ||||
|      */ | ||||
|     /* | ||||
|      *  Used in methods derived from DrawPrintTextLocations | ||||
|      */ | ||||
|     private AffineTransform flipAT; | ||||
|     private AffineTransform rotateAT; | ||||
| 
 | ||||
|     /** | ||||
|      *  Regions updated by calls to writeString | ||||
|      */ | ||||
|     private Set<Rectangle2D> boxes; | ||||
| 
 | ||||
|     // Border to allow when finding intersections | ||||
|     private double dx = 1.0; // This value works for me, feel free to tweak (or add setter) | ||||
|     private double dy = 0.000; // Rows of text tend to overlap, so need to extend | ||||
| 
 | ||||
|     /** | ||||
|      *  Region in which to find table (otherwise whole page) | ||||
|      */ | ||||
|     private Rectangle2D regionArea; | ||||
| 
 | ||||
|     /** | ||||
|      * Number of rows in inferred table | ||||
|      */ | ||||
|     private int nRows=0; | ||||
| 
 | ||||
|     /** | ||||
|      * Number of columns in inferred table | ||||
|      */ | ||||
|     private int nCols=0; | ||||
| 
 | ||||
|     /** | ||||
|      * This is the object that does the text extraction | ||||
|      */ | ||||
|     private PDFTextStripperByArea regionStripper; | ||||
| 
 | ||||
|     /** | ||||
|      * 1D intervals - used for calculateTableRegions() | ||||
|      * @author Beldaz | ||||
|      * | ||||
|      */ | ||||
|     public static class Interval { | ||||
|         double start; | ||||
|         double end; | ||||
|         public Interval(double start, double end) { | ||||
|             this.start=start; this.end = end; | ||||
|         } | ||||
|         public void add(Interval col) { | ||||
|             if(col.start<start) | ||||
|                 start = col.start; | ||||
|             if(col.end>end) | ||||
|                 end = col.end; | ||||
|         } | ||||
|         public static void addTo(Interval x, LinkedList<Interval> columns) { | ||||
|             int p = 0; | ||||
|             Iterator<Interval> it = columns.iterator(); | ||||
|             // Find where x should go | ||||
|             while(it.hasNext()) { | ||||
|                 Interval col = it.next(); | ||||
|                 if(x.end>=col.start) { | ||||
|                     if(x.start<=col.end) { // overlaps | ||||
|                         x.add(col); | ||||
|                         it.remove(); | ||||
|                     } | ||||
|                     break; | ||||
|                 } | ||||
|                 ++p; | ||||
|             } | ||||
|             while(it.hasNext()) { | ||||
|                 Interval col = it.next(); | ||||
|                 if(x.start>col.end) | ||||
|                     break; | ||||
|                 x.add(col); | ||||
|                 it.remove(); | ||||
|             } | ||||
|             columns.add(p, x); | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     /** | ||||
|      * Instantiate a new PDFTableStripper object. | ||||
|      * | ||||
|      * @param document | ||||
|      * @throws IOException If there is an error loading the properties. | ||||
|      */ | ||||
|     public PDFTableStripper() throws IOException | ||||
|     { | ||||
|         super.setShouldSeparateByBeads(false); | ||||
|         regionStripper = new PDFTextStripperByArea(); | ||||
|         regionStripper.setSortByPosition( true ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Define the region to group text by. | ||||
|      * | ||||
|      * @param rect The rectangle area to retrieve the text from. | ||||
|      */ | ||||
|     public void setRegion(Rectangle2D rect ) | ||||
|     { | ||||
|         regionArea = rect; | ||||
|     } | ||||
| 
 | ||||
|     public int getRows() | ||||
|     { | ||||
|         return nRows; | ||||
|     } | ||||
| 
 | ||||
|     public int getColumns() | ||||
|     { | ||||
|         return nCols; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the text for the region, this should be called after extractTable(). | ||||
|      * | ||||
|      * @return The text that was identified in that region. | ||||
|      */ | ||||
|     public String getText(int row, int col) | ||||
|     { | ||||
|         return regionStripper.getTextForRegion("el"+col+"x"+row); | ||||
|     } | ||||
| 
 | ||||
|     public void extractTable(PDPage pdPage) throws IOException | ||||
|     { | ||||
|         setStartPage(getCurrentPageNo()); | ||||
|         setEndPage(getCurrentPageNo()); | ||||
| 
 | ||||
|         boxes = new HashSet<Rectangle2D>(); | ||||
|         // flip y-axis | ||||
|         flipAT = new AffineTransform(); | ||||
|         flipAT.translate(0, pdPage.getBBox().getHeight()); | ||||
|         flipAT.scale(1, -1); | ||||
| 
 | ||||
|         // page may be rotated | ||||
|         rotateAT = new AffineTransform(); | ||||
|         int rotation = pdPage.getRotation(); | ||||
|         if (rotation != 0) | ||||
|         { | ||||
|             PDRectangle mediaBox = pdPage.getMediaBox(); | ||||
|             switch (rotation) | ||||
|             { | ||||
|                 case 90: | ||||
|                     rotateAT.translate(mediaBox.getHeight(), 0); | ||||
|                     break; | ||||
|                 case 270: | ||||
|                     rotateAT.translate(0, mediaBox.getWidth()); | ||||
|                     break; | ||||
|                 case 180: | ||||
|                     rotateAT.translate(mediaBox.getWidth(), mediaBox.getHeight()); | ||||
|                     break; | ||||
|                 default: | ||||
|                     break; | ||||
|             } | ||||
|             rotateAT.rotate(Math.toRadians(rotation)); | ||||
|         } | ||||
|         // Trigger processing of the document so that writeString is called. | ||||
|         try (Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream())) { | ||||
|             super.output = dummy; | ||||
|             super.processPage(pdPage); | ||||
|         } | ||||
| 
 | ||||
|         Rectangle2D[][] regions = calculateTableRegions(); | ||||
| 
 | ||||
| //        System.err.println("Drawing " + nCols + "x" + nRows + "="+ nRows*nCols + " regions"); | ||||
|         for(int i=0; i<nCols; ++i) { | ||||
|             for(int j=0; j<nRows; ++j) { | ||||
|                 final Rectangle2D region = regions[i][j]; | ||||
|                 regionStripper.addRegion("el"+i+"x"+j, region); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         regionStripper.extractRegions(pdPage); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Infer a rectangular grid of regions from the boxes field. | ||||
|      * | ||||
|      * @return 2D array of table regions (as Rectangle2D objects). Note that | ||||
|      * some of these regions may have no content. | ||||
|      */ | ||||
|     private Rectangle2D[][] calculateTableRegions() { | ||||
| 
 | ||||
|         // Build up a list of all table regions, based upon the populated | ||||
|         // regions of boxes field. Treats the horizontal and vertical extents | ||||
|         // of each box as distinct | ||||
|         LinkedList<Interval> columns = new LinkedList<Interval>(); | ||||
|         LinkedList<Interval> rows = new LinkedList<Interval>(); | ||||
| 
 | ||||
|         for(Rectangle2D box: boxes) { | ||||
|             Interval x = new Interval(box.getMinX(), box.getMaxX()); | ||||
|             Interval y = new Interval(box.getMinY(), box.getMaxY()); | ||||
| 
 | ||||
|             Interval.addTo(x, columns); | ||||
|             Interval.addTo(y, rows); | ||||
|         } | ||||
| 
 | ||||
|         nRows = rows.size(); | ||||
|         nCols = columns.size(); | ||||
|         Rectangle2D[][] regions = new Rectangle2D[nCols][nRows]; | ||||
|         int i=0; | ||||
|         // Label regions from top left, rather than the transformed orientation | ||||
|         for(Interval column: columns) { | ||||
|             int j=0; | ||||
|             for(Interval row: rows) { | ||||
|                 regions[nCols-i-1][nRows-j-1] = new Rectangle2D.Double(column.start, row.start, column.end - column.start, row.end - row.start); | ||||
|                 ++j; | ||||
|             } | ||||
|             ++i; | ||||
|         } | ||||
| 
 | ||||
|         return regions; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Register each character's bounding box, updating boxes field to maintain | ||||
|      * a list of all distinct groups of characters. | ||||
|      * | ||||
|      * Overrides the default functionality of PDFTextStripper. | ||||
|      * Most of this is taken from DrawPrintTextLocations.java, with extra steps | ||||
|      * at end of main loop | ||||
|      */ | ||||
|     @Override | ||||
|     protected void writeString(String string, List<TextPosition> textPositions) throws IOException | ||||
|     { | ||||
|         for (TextPosition text : textPositions) | ||||
|         { | ||||
|             // glyph space -> user space | ||||
|             // note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix | ||||
|             AffineTransform at = text.getTextMatrix().createAffineTransform(); | ||||
|             PDFont font = text.getFont(); | ||||
|             BoundingBox bbox = font.getBoundingBox(); | ||||
| 
 | ||||
|             // advance width, bbox height (glyph space) | ||||
|             float xadvance = font.getWidth(text.getCharacterCodes()[0]); // todo: should iterate all chars | ||||
|             Rectangle2D.Float rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight()); | ||||
| 
 | ||||
|             if (font instanceof PDType3Font) | ||||
|             { | ||||
|                 // bbox and font matrix are unscaled | ||||
|                 at.concatenate(font.getFontMatrix().createAffineTransform()); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // bbox and font matrix are already scaled to 1000 | ||||
|                 at.scale(1/1000f, 1/1000f); | ||||
|             } | ||||
|             Shape s = at.createTransformedShape(rect); | ||||
|             s = flipAT.createTransformedShape(s); | ||||
|             s = rotateAT.createTransformedShape(s); | ||||
| 
 | ||||
| 
 | ||||
|             // | ||||
|             // Merge character's bounding box with boxes field | ||||
|             // | ||||
|             Rectangle2D bounds = s.getBounds2D(); | ||||
|             // Pad sides to detect almost touching boxes | ||||
|             Rectangle2D hitbox = bounds.getBounds2D(); | ||||
|             hitbox.add(bounds.getMinX() - dx , bounds.getMinY() - dy); | ||||
|             hitbox.add(bounds.getMaxX() + dx , bounds.getMaxY() + dy); | ||||
| 
 | ||||
|             // Find all overlapping boxes | ||||
|             List<Rectangle2D> intersectList = new ArrayList<Rectangle2D>(); | ||||
|             for(Rectangle2D box: boxes) { | ||||
|                 if(box.intersects(hitbox)) { | ||||
|                     intersectList.add(box); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             // Combine all touching boxes and update | ||||
|             // (NOTE: Potentially this could leave some overlapping boxes un-merged, | ||||
|             // but it's sufficient for now and get's fixed up in calculateTableRegions) | ||||
|             for(Rectangle2D box: intersectList) { | ||||
|                 bounds.add(box); | ||||
|                 boxes.remove(box); | ||||
|             } | ||||
|             boxes.add(bounds); | ||||
| 
 | ||||
|         } | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * This method does nothing in this derived class, because beads and regions are incompatible. Beads are | ||||
|      * ignored when stripping by area. | ||||
|      * | ||||
|      * @param aShouldSeparateByBeads The new grouping of beads. | ||||
|      */ | ||||
|     @Override | ||||
|     public final void setShouldSeparateByBeads(boolean aShouldSeparateByBeads) | ||||
|     { | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Adapted from PDFTextStripperByArea | ||||
|      * {@inheritDoc} | ||||
|      */ | ||||
|     @Override | ||||
|     protected void processTextPosition( TextPosition text ) | ||||
|     { | ||||
|         if(regionArea!=null && !regionArea.contains( text.getX(), text.getY() ) ) { | ||||
|             // skip character | ||||
|         } else { | ||||
|             super.processTextPosition( text ); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @ -99,6 +99,14 @@ public class ConverterWebController { | ||||
|         return modelAndView; | ||||
|     } | ||||
| 
 | ||||
|     @GetMapping("/pdf-to-csv") | ||||
|     @Hidden | ||||
|     public ModelAndView pdfToCSV() { | ||||
|         ModelAndView modelAndView = new ModelAndView("convert/pdf-to-csv"); | ||||
|         modelAndView.addObject("currentPage", "pdf-to-csv"); | ||||
|         return modelAndView; | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
|     @GetMapping("/pdf-to-pdfa") | ||||
|     @Hidden | ||||
|  | ||||
| @ -0,0 +1,18 @@ | ||||
| package stirling.software.SPDF.model.api.extract; | ||||
| 
 | ||||
| import io.swagger.v3.oas.annotations.media.Schema; | ||||
| import lombok.Data; | ||||
| import lombok.EqualsAndHashCode; | ||||
| import stirling.software.SPDF.model.api.PDFFile; | ||||
| 
 | ||||
| @Data | ||||
| @EqualsAndHashCode(callSuper=true) | ||||
| public class PDFFilePage extends PDFFile { | ||||
| 
 | ||||
| 
 | ||||
|     @Schema(description = "Number of chosen page", type = "number") | ||||
|     private int pageId; | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=تحويل PDF إلى XML | ||||
| PDFToXML.header=تحويل PDF إلى XML | ||||
| PDFToXML.credit=تستخدم هذه الخدمة LibreOffice لتحويل الملفات. | ||||
| PDFToXML.submit=تحويل | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title= PDF ??? CSV | ||||
| PDFToCSV.header=PDF ??? CSV | ||||
| PDFToCSV.submit=?????? | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF към XML | ||||
| PDFToXML.header=PDF към XML | ||||
| PDFToXML.credit=Тази услуга използва LibreOffice за преобразуване на файлове. | ||||
| PDFToXML.submit=Преобразуване | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF ??? CSV | ||||
| PDFToCSV.header=PDF ??? CSV | ||||
| PDFToCSV.submit=???????? | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF a XML | ||||
| PDFToXML.header=PDF a XML | ||||
| PDFToXML.credit=Utilitza LibreOffice per a la conversió d'Arxius. | ||||
| PDFToXML.submit=Converteix | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF a CSV | ||||
| PDFToCSV.header=PDF a CSV | ||||
| PDFToCSV.submit=Extracte | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF in XML | ||||
| PDFToXML.header=PDF in XML | ||||
| PDFToXML.credit=Dieser Dienst verwendet LibreOffice für die Dateikonvertierung. | ||||
| PDFToXML.submit=Konvertieren | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF zu CSV | ||||
| PDFToCSV.header=PDF zu CSV | ||||
| PDFToCSV.submit=Extrakt | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF \u03C3\u03B5 XML | ||||
| PDFToXML.header=PDF \u03C3\u03B5 XML | ||||
| PDFToXML.credit=\u0391\u03C5\u03C4\u03AE \u03B7 \u03C5\u03C0\u03B7\u03C1\u03B5\u03C3\u03AF\u03B1 \u03C7\u03C1\u03B7\u03C3\u03B9\u03BC\u03BF\u03C0\u03BF\u03B9\u03B5\u03AF LibreOffice \u03B3\u03B9\u03B1 \u03C4\u03B7 \u03BC\u03B5\u03C4\u03B1\u03C4\u03C1\u03BF\u03C0\u03AE \u03C4\u03C9\u03BD \u03B1\u03C1\u03C7\u03B5\u03AF\u03C9\u03BD. | ||||
| PDFToXML.submit=\u039C\u03B5\u03C4\u03B1\u03C4\u03C1\u03BF\u03C0\u03AE | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF ?? CSV | ||||
| PDFToCSV.header=PDF ?? CSV | ||||
| PDFToCSV.submit=????????? | ||||
| @ -822,4 +822,9 @@ PDFToHTML.submit=Convert | ||||
| PDFToXML.title=PDF to XML | ||||
| PDFToXML.header=PDF to XML | ||||
| PDFToXML.credit=This service uses LibreOffice for file conversion. | ||||
| PDFToXML.submit=Convert | ||||
| PDFToXML.submit=Convert | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF to CSV | ||||
| PDFToCSV.header=PDF to CSV | ||||
| PDFToCSV.submit=Extract | ||||
| @ -93,7 +93,6 @@ account.accountSettings=Account Settings | ||||
| account.adminSettings=Admin Settings - View and Add Users | ||||
| account.userControlSettings=User Control Settings | ||||
| account.changeUsername=Change Username | ||||
| account.changeUsername=Change Username | ||||
| account.password=Confirmation Password | ||||
| account.oldPassword=Old password | ||||
| account.newPassword=New Password | ||||
| @ -334,7 +333,12 @@ showJS.tags=JS | ||||
| 
 | ||||
| home.autoRedact.title=Auto Redact | ||||
| home.autoRedact.desc=Auto Redacts(Blacks out) text in a PDF based on input text | ||||
| showJS.tags=JS | ||||
| autoRedact.tags=JS | ||||
| 
 | ||||
| home.tableExtraxt.title=Table Extraction | ||||
| home.tableExtraxt.desc=Table Extraction from PDF to CSV | ||||
| tableExtraxt.tags=CSV | ||||
| 
 | ||||
| 
 | ||||
| ########################### | ||||
| #                         # | ||||
| @ -563,7 +567,7 @@ ScannerImageSplit.selectText.8=Sets the minimum contour area threshold for a pho | ||||
| ScannerImageSplit.selectText.9=Border Size: | ||||
| ScannerImageSplit.selectText.10=Sets the size of the border added and removed to prevent white borders in the output (default: 1). | ||||
| 
 | ||||
|                              | ||||
| 
 | ||||
| #OCR | ||||
| ocr.title=OCR / Scan Cleanup | ||||
| ocr.header=Cleanup Scans / OCR (Optical Character Recognition) | ||||
| @ -682,8 +686,8 @@ imageToPDF.selectText.2=Auto rotate PDF | ||||
| imageToPDF.selectText.3=Multi file logic (Only enabled if working with multiple images) | ||||
| imageToPDF.selectText.4=Merge into single PDF | ||||
| imageToPDF.selectText.5=Convert to separate PDFs | ||||
|      | ||||
|                                     | ||||
| 
 | ||||
| 
 | ||||
| #pdfToImage | ||||
| pdfToImage.title=PDF to Image | ||||
| pdfToImage.header=PDF to Image | ||||
| @ -773,7 +777,6 @@ changeMetadata.keywords=Keywords: | ||||
| changeMetadata.modDate=Modification Date (yyyy/MM/dd HH:mm:ss): | ||||
| changeMetadata.producer=Producer: | ||||
| changeMetadata.subject=Subject: | ||||
| changeMetadata.title=Title: | ||||
| changeMetadata.trapped=Trapped: | ||||
| changeMetadata.selectText.4=Other Metadata: | ||||
| changeMetadata.selectText.5=Add Custom Metadata Entry | ||||
| @ -823,3 +826,8 @@ PDFToXML.title=PDF to XML | ||||
| PDFToXML.header=PDF to XML | ||||
| PDFToXML.credit=This service uses LibreOffice for file conversion. | ||||
| PDFToXML.submit=Convert | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF to CSV | ||||
| PDFToCSV.header=PDF to CSV | ||||
| PDFToCSV.submit=Extract | ||||
|  | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF a XML | ||||
| PDFToXML.header=PDF a XML | ||||
| PDFToXML.credit=Este servicio utiliza LibreOffice para la conversión de archivos | ||||
| PDFToXML.submit=Convertir | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF a CSV | ||||
| PDFToCSV.header=PDF a CSV | ||||
| PDFToCSV.submit=Extracto | ||||
|  | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDFa XML bihurtu | ||||
| PDFToXML.header=PDFa XML bihurtu | ||||
| PDFToXML.credit=Zerbitzu honek LibreOffice erabiltzen du fitxategiak bihurtzeko | ||||
| PDFToXML.submit=Bihurtu | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF a CSV | ||||
| PDFToCSV.header=PDF a CSV | ||||
| PDFToCSV.submit=Extracto | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF en XML | ||||
| PDFToXML.header=PDF en XML | ||||
| PDFToXML.credit=Ce service utilise LibreOffice pour la conversion de fichiers. | ||||
| PDFToXML.submit=Convertir | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF en CSV | ||||
| PDFToCSV.header=PDF en CSV | ||||
| PDFToCSV.submit=Extrait | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=Da PDF a XML | ||||
| PDFToXML.header=Da PDF a XML | ||||
| PDFToXML.credit=Questo servizio utilizza LibreOffice per la conversione. | ||||
| PDFToXML.submit=Converti | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=Da PDF a CSV | ||||
| PDFToCSV.header=Da PDF a CSV | ||||
| PDFToCSV.submit=Estratto | ||||
|  | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDFをXMLに変換 | ||||
| PDFToXML.header=PDFをXMLに変換 | ||||
| PDFToXML.credit=本サービスはファイル変換にLibreOfficeを使用しています。 | ||||
| PDFToXML.submit=変換 | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF??CSV? | ||||
| PDFToCSV.header=PDF??CSV? | ||||
| PDFToCSV.submit=???? | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF to XML | ||||
| PDFToXML.header=PDF를 XML로 변환 | ||||
| PDFToXML.credit=이 서비스는 파일 변환을 위해 LibreOffice를 사용합니다. | ||||
| PDFToXML.submit=변환 | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF? CSV? | ||||
| PDFToCSV.header=PDF? CSV? | ||||
| PDFToCSV.submit=?? | ||||
|  | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF naar XML | ||||
| PDFToXML.header=PDF naar XML | ||||
| PDFToXML.credit=Deze service gebruikt LibreOffice voor bestandsconversie. | ||||
| PDFToXML.submit=Converteren | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF naar CSV | ||||
| PDFToCSV.header=PDF naar CSV | ||||
| PDFToCSV.submit=Extract | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF na XML | ||||
| PDFToXML.header=PDF na XML | ||||
| PDFToXML.credit=Ta usługa używa LibreOffice do konwersji plików. | ||||
| PDFToXML.submit=Konwertuj | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF na CSV | ||||
| PDFToCSV.header=PDF na CSV | ||||
| PDFToCSV.submit=Wyci?g | ||||
|  | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF para XML | ||||
| PDFToXML.header=PDF para XML | ||||
| PDFToXML.credit=Este serviço usa o LibreOffice para Conversão de Arquivos. | ||||
| PDFToXML.submit=Converter | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF para CSV | ||||
| PDFToCSV.header=PDF para CSV | ||||
| PDFToCSV.submit=Eztennañ | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF către XML | ||||
| PDFToXML.header=PDF către XML | ||||
| PDFToXML.credit=Acest serviciu utilizează LibreOffice pentru conversia fișierului. | ||||
| PDFToXML.submit=Convert | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF în CSV | ||||
| PDFToCSV.header=PDF în CSV | ||||
| PDFToCSV.submit=Extrage | ||||
|  | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF в XML | ||||
| PDFToXML.header=PDF в XML | ||||
| PDFToXML.credit=Этот сервис использует LibreOffice для преобразования файлов. | ||||
| PDFToXML.submit=Конвертировать | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF ? CSV | ||||
| PDFToCSV.header=PDF ? CSV | ||||
| PDFToCSV.submit=??????? | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF till XML | ||||
| PDFToXML.header=PDF till XML | ||||
| PDFToXML.credit=Denna tjänst använder LibreOffice för filkonvertering. | ||||
| PDFToXML.submit=Konvertera | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF till CSV | ||||
| PDFToCSV.header=PDF till CSV | ||||
| PDFToCSV.submit=Navvit | ||||
| @ -823,3 +823,8 @@ PDFToXML.title=PDF To XML | ||||
| PDFToXML.header=将PDF转换为XML | ||||
| PDFToXML.credit=此服务使用LibreOffice进行文件转换。 | ||||
| PDFToXML.submit=转换 | ||||
| 
 | ||||
| #PDFToCSV | ||||
| PDFToCSV.title=PDF ? CSV | ||||
| PDFToCSV.header=PDF ? CSV | ||||
| PDFToCSV.submit=?? | ||||
							
								
								
									
										1
									
								
								src/main/resources/static/images/pdf-csv.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/main/resources/static/images/pdf-csv.svg
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1 @@ | ||||
| <svg xmlns="http://www.w3.org/2000/svg" height="1em" viewBox="0 0 512 512"><!--! Font Awesome Free 6.4.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M0 64C0 28.7 28.7 0 64 0H224V128c0 17.7 14.3 32 32 32H384V304H176c-35.3 0-64 28.7-64 64V512H64c-35.3 0-64-28.7-64-64V64zm384 64H256V0L384 128zM200 352h16c22.1 0 40 17.9 40 40v8c0 8.8-7.2 16-16 16s-16-7.2-16-16v-8c0-4.4-3.6-8-8-8H200c-4.4 0-8 3.6-8 8v80c0 4.4 3.6 8 8 8h16c4.4 0 8-3.6 8-8v-8c0-8.8 7.2-16 16-16s16 7.2 16 16v8c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V392c0-22.1 17.9-40 40-40zm133.1 0H368c8.8 0 16 7.2 16 16s-7.2 16-16 16H333.1c-7.2 0-13.1 5.9-13.1 13.1c0 5.2 3 9.9 7.8 12l37.4 16.6c16.3 7.2 26.8 23.4 26.8 41.2c0 24.9-20.2 45.1-45.1 45.1H304c-8.8 0-16-7.2-16-16s7.2-16 16-16h42.9c7.2 0 13.1-5.9 13.1-13.1c0-5.2-3-9.9-7.8-12l-37.4-16.6c-16.3-7.2-26.8-23.4-26.8-41.2c0-24.9 20.2-45.1 45.1-45.1zm98.9 0c8.8 0 16 7.2 16 16v31.6c0 23 5.5 45.6 16 66c10.5-20.3 16-42.9 16-66V368c0-8.8 7.2-16 16-16s16 7.2 16 16v31.6c0 34.7-10.3 68.7-29.6 97.6l-5.1 7.7c-3 4.5-8 7.1-13.3 7.1s-10.3-2.7-13.3-7.1l-5.1-7.7c-19.3-28.9-29.6-62.9-29.6-97.6V368c0-8.8 7.2-16 16-16z"/></svg> | ||||
| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										158
									
								
								src/main/resources/templates/convert/pdf-to-csv.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/main/resources/templates/convert/pdf-to-csv.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,158 @@ | ||||
| <!DOCTYPE html> | ||||
| <html th:lang="${#locale.toString()}" th:lang-direction="#{language.direction}" xmlns:th="http://www.thymeleaf.org"> | ||||
| 
 | ||||
| <th:block th:insert="~{fragments/common :: head(title=#{PDFToCSV.title})}"></th:block> | ||||
| 
 | ||||
| 
 | ||||
| <body> | ||||
| <div id="page-container"> | ||||
|     <div id="content-wrap"> | ||||
| 
 | ||||
|         <div class="container"> | ||||
|             <div class="row justify-content-center"> | ||||
|                 <div class="col-md-6"> | ||||
|                     <h2 th:text="#{PDFToCSV.header}"></h2> | ||||
|                     <form id="PDFToCSVForm" th:action="@{api/v1/extract/pdf-to-csv}" method="post" enctype="multipart/form-data"> | ||||
|                         <input  id="pageId" type="hidden" name="pageId" /> | ||||
|                         <div th:replace="~{fragments/common :: fileSelector(name='fileInput', multiple=false, accept='application/pdf')}"></div> | ||||
|                         <button type="submit" class="btn btn-primary" th:text="#{PDFToCSV.submit}"></button> | ||||
|                     </form> | ||||
|                     <p id="instruction-text" style="margin: 0; display: none">Choose page to extract table</p> | ||||
| 
 | ||||
|                     <div style="position: relative; display: inline-block;"> | ||||
|                         <div> | ||||
| 
 | ||||
|                             <div style="display:none ;margin: 3px;position: absolute;top: 0;width: 120px;justify-content:space-between;z-index: 10" id="pagination-button-container"> | ||||
|                                 <button id='previous-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff;  ;border: none;outline: none; border-radius: 4px;'> < </button> | ||||
|                                 <button id='next-page-btn' style='opacity: 80% ; width: 50px; height: 30px; display: flex;align-items: center;justify-content: center; background: grey; color: #ffffff;  ;border: none;outline: none; border-radius: 4px;'> > </button> | ||||
|                             </div> | ||||
| 
 | ||||
|                             <canvas id="crop-pdf-canvas" style="position: absolute; top: 0; left: 0; z-index: 1;"></canvas> | ||||
|                         </div> | ||||
|                         <canvas id="overlayCanvas" style="position: absolute; top: 0; left: 0; z-index: 2;"></canvas> | ||||
|                     </div> | ||||
| 
 | ||||
|                     <script> | ||||
| 
 | ||||
|                         let pdfCanvas  = document.getElementById('crop-pdf-canvas'); | ||||
|                         let overlayCanvas = document.getElementById('overlayCanvas'); | ||||
|                         // let paginationBtnContainer = ; | ||||
| 
 | ||||
|                         let context = pdfCanvas.getContext('2d'); | ||||
| 
 | ||||
|                         let btn1Object = document.getElementById('previous-page-btn'); | ||||
|                         let btn2Object = document.getElementById('next-page-btn'); | ||||
|                         overlayCanvas.width = pdfCanvas.width; | ||||
|                         overlayCanvas.height = pdfCanvas.height; | ||||
| 
 | ||||
|                         let fileInput = document.getElementById('fileInput-input'); | ||||
| 
 | ||||
|                         let file; | ||||
| 
 | ||||
|                         let pdfDoc = null; | ||||
|                         let pageId = document.getElementById('pageId'); | ||||
|                         let currentPage = 1; | ||||
|                         let totalPages = 0; | ||||
| 
 | ||||
|                         let startX = 0; | ||||
|                         let startY = 0; | ||||
|                         let rectWidth = 0; | ||||
|                         let rectHeight = 0; | ||||
| 
 | ||||
|                         btn1Object.addEventListener('click',function (e){ | ||||
| 
 | ||||
|                             if (currentPage !== 1) { | ||||
|                                 currentPage = currentPage - 1; | ||||
|                                 pageId.value = currentPage; | ||||
| 
 | ||||
|                                 if (file.type === 'application/pdf') { | ||||
|                                     let reader = new FileReader(); | ||||
|                                     reader.onload = function (ev) { | ||||
|                                         let typedArray = new Uint8Array(reader.result); | ||||
|                                         pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js' | ||||
|                                         pdfjsLib.getDocument(typedArray).promise.then(function (pdf) { | ||||
|                                             pdfDoc = pdf; | ||||
|                                             totalPages = pdf.numPages; | ||||
|                                             renderPage(currentPage); | ||||
|                                         }); | ||||
|                                     }; | ||||
|                                     reader.readAsArrayBuffer(file); | ||||
|                                 } | ||||
|                             } | ||||
|                         }); | ||||
| 
 | ||||
|                         btn2Object.addEventListener('click',function (e){ | ||||
| 
 | ||||
|                             if (currentPage !== totalPages){ | ||||
| 
 | ||||
|                                 currentPage=currentPage+1; | ||||
|                                 pageId.value = currentPage; | ||||
| 
 | ||||
|                                 if (file.type === 'application/pdf') { | ||||
|                                     let reader = new FileReader(); | ||||
|                                     reader.onload = function(ev) { | ||||
|                                         let typedArray = new Uint8Array(reader.result); | ||||
|                                         pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js' | ||||
|                                         pdfjsLib.getDocument(typedArray).promise.then(function(pdf) { | ||||
|                                             pdfDoc = pdf; | ||||
|                                             totalPages = pdf.numPages; | ||||
|                                             renderPage(currentPage); | ||||
|                                         }); | ||||
|                                     }; | ||||
|                                     reader.readAsArrayBuffer(file); | ||||
|                                 } | ||||
|                             } | ||||
|                         }); | ||||
| 
 | ||||
|                         fileInput.addEventListener('change', function(e) { | ||||
| 
 | ||||
|                             file = e.target.files[0]; | ||||
|                             if (file.type === 'application/pdf') { | ||||
|                                 let reader = new FileReader(); | ||||
|                                 reader.onload = function(ev) { | ||||
|                                     let typedArray = new Uint8Array(reader.result); | ||||
|                                     pdfjsLib.GlobalWorkerOptions.workerSrc = 'pdfjs/pdf.worker.js' | ||||
|                                     pdfjsLib.getDocument(typedArray).promise.then(function(pdf) { | ||||
|                                         pdfDoc = pdf; | ||||
|                                         totalPages = pdf.numPages; | ||||
|                                         renderPage(currentPage); | ||||
|                                     }); | ||||
|                                     pageId.value = currentPage; | ||||
| 
 | ||||
|                                 }; | ||||
|                                 reader.readAsArrayBuffer(file); | ||||
|                                 document.getElementById("pagination-button-container").style.display="flex"; | ||||
|                                 document.getElementById("instruction-text").style.display="block"; | ||||
|                             } | ||||
|                         }); | ||||
| 
 | ||||
| 
 | ||||
|                         function renderPage(pageNumber) { | ||||
|                             pdfDoc.getPage(pageNumber).then(function(page) { | ||||
|                                 let viewport = page.getViewport({ scale: 1.0 }); | ||||
|                                 pdfCanvas.width = viewport.width; | ||||
|                                 pdfCanvas.height = viewport.height; | ||||
| 
 | ||||
|                                 overlayCanvas.width = viewport.width;  // Match overlay canvas size with PDF canvas | ||||
|                                 overlayCanvas.height = viewport.height; | ||||
| 
 | ||||
|                                 let renderContext = { canvasContext: context, viewport: viewport }; | ||||
|                                 page.render(renderContext); | ||||
|                                 pdfCanvas.classList.add("shadow-canvas"); | ||||
|                             }); | ||||
|                         } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|                     </script> | ||||
| 
 | ||||
|                 </div> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
|     <div th:insert="~{fragments/footer.html :: footer}"></div> | ||||
| </div> | ||||
| </body> | ||||
| </html> | ||||
| @ -1,13 +1,13 @@ | ||||
| <div th:fragment="navbar" class="mx-auto"> | ||||
| <script src="js/languageSelection.js"></script> | ||||
|     <script src="js/languageSelection.js"></script> | ||||
| 
 | ||||
| <script th:inline="javascript"> | ||||
| 	const currentVersion =  /*[[${@appVersion}]]*/ ''; | ||||
| 	const noFavourites =  /*[[#{noFavourites}]]*/ ''; | ||||
| </script> | ||||
| <script th:src="@{js/githubVersion.js}"></script> | ||||
|     <script th:inline="javascript"> | ||||
|         const currentVersion =  /*[[${@appVersion}]]*/ ''; | ||||
|         const noFavourites =  /*[[#{noFavourites}]]*/ ''; | ||||
|     </script> | ||||
|     <script th:src="@{js/githubVersion.js}"></script> | ||||
| 
 | ||||
| <link rel="stylesheet" href="css/navbar.css"> | ||||
|     <link rel="stylesheet" href="css/navbar.css"> | ||||
| 
 | ||||
|     <nav class="navbar navbar-expand-lg navbar-light bg-light"> | ||||
|         <div class="container "> | ||||
| @ -39,33 +39,33 @@ | ||||
|                         </a> | ||||
|                     </li>--> | ||||
| 
 | ||||
| <li class="nav-item nav-item-separator"></li> | ||||
|     <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''"> | ||||
|     <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|         <img class="icon" src="images/file-earmark-pdf.svg" alt="icon"> | ||||
|         <span class="icon-text"  th:text="#{navbar.pageOps}"></span> | ||||
|     </a> | ||||
|     <div class="dropdown-menu" aria-labelledby="navbarDropdown"> | ||||
|         <!-- Existing menu items --> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc', 'merge.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc', 'split.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags')}"></div> | ||||
| 		<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags')}"></div> | ||||
| 		<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags')}"></div> | ||||
| 		<div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'auto-split-pdf', 'images/layout-split.svg', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc', 'crop.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-page', 'images/extract.svg', 'home.extractPage.title', 'home.extractPage.desc', 'extractPage.tags')}"></div> | ||||
|         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-single-page', 'images/single-page.svg', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags')}"></div> | ||||
|                     <li class="nav-item nav-item-separator"></li> | ||||
|                     <li class="nav-item dropdown" th:classappend="${currentPage}=='remove-pages' OR ${currentPage}=='merge-pdfs' OR ${currentPage}=='split-pdfs' OR ${currentPage}=='crop' OR ${currentPage}=='adjust-contrast' OR ${currentPage}=='pdf-organizer' OR ${currentPage}=='rotate-pdf' OR ${currentPage}=='multi-page-layout' OR ${currentPage}=='scale-pages' OR ${currentPage}=='auto-split-pdf' OR ${currentPage}=='extract-page' OR ${currentPage}=='pdf-to-single-page' ? 'active' : ''"> | ||||
|                         <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                             <img class="icon" src="images/file-earmark-pdf.svg" alt="icon"> | ||||
|                             <span class="icon-text"  th:text="#{navbar.pageOps}"></span> | ||||
|                         </a> | ||||
|                         <div class="dropdown-menu" aria-labelledby="navbarDropdown"> | ||||
|                             <!-- Existing menu items --> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('merge-pdfs', 'images/union.svg', 'home.merge.title', 'home.merge.desc', 'merge.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('split-pdfs', 'images/layout-split.svg', 'home.split.title', 'home.split.desc', 'split.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'pdf-organizer', 'images/sort-numeric-down.svg', 'home.pdfOrganiser.title', 'home.pdfOrganiser.desc', 'pdfOrganiser.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'rotate-pdf', 'images/arrow-clockwise.svg', 'home.rotate.title', 'home.rotate.desc', 'rotate.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'remove-pages', 'images/file-earmark-x.svg', 'home.removePages.title', 'home.removePages.desc', 'removePages.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'multi-page-layout', 'images/page-layout.svg', 'home.pageLayout.title', 'home.pageLayout.desc', 'pageLayout.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'scale-pages', 'images/scale-pages.svg', 'home.scalePages.title', 'home.scalePages.desc', 'scalePages.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ( 'auto-split-pdf', 'images/layout-split.svg', 'home.autoSplitPDF.title', 'home.autoSplitPDF.desc', 'autoSplitPDF.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('adjust-contrast', 'images/adjust-contrast.svg', 'home.adjust-contrast.title', 'home.adjust-contrast.desc', 'adjust-contrast.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('crop', 'images/crop.svg', 'home.crop.title', 'home.crop.desc', 'crop.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('extract-page', 'images/extract.svg', 'home.extractPage.title', 'home.extractPage.desc', 'extractPage.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-single-page', 'images/single-page.svg', 'home.PdfToSinglePage.title', 'home.PdfToSinglePage.desc', 'PdfToSinglePage.tags')}"></div> | ||||
| 
 | ||||
| 
 | ||||
|     </div> | ||||
| </li> | ||||
| <li class="nav-item nav-item-separator"></li> | ||||
|                         </div> | ||||
|                     </li> | ||||
|                     <li class="nav-item nav-item-separator"></li> | ||||
|                     <li class="nav-item dropdown" th:classappend="${currentPage}=='pdf-to-img' OR ${currentPage}=='img-to-pdf' OR ${currentPage}=='pdf-to-pdfa' OR ${currentPage}=='file-to-pdf' OR ${currentPage}=='xlsx-to-pdf' OR ${currentPage}=='pdf-to-word' OR ${currentPage}=='pdf-to-presentation' OR ${currentPage}=='pdf-to-text' OR ${currentPage}=='pdf-to-html' OR ${currentPage}=='pdf-to-xml' ? 'active' : ''"><a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" | ||||
|                         aria-haspopup="true" aria-expanded="false"> | ||||
|                                                                                                                                                                                                                                                                                                                                                                                                                                   aria-haspopup="true" aria-expanded="false"> | ||||
|                         <img class="icon" src="images/arrow-left-right.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> | ||||
|                         <span class="icon-text"  th:text="#{navbar.convert}"></span> | ||||
|                     </a> | ||||
| @ -75,7 +75,7 @@ | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('file-to-pdf', 'images/file.svg', 'home.fileToPDF.title', 'home.fileToPDF.desc', 'fileToPDF.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('html-to-pdf', 'images/html.svg', 'home.HTMLToPDF.title', 'home.HTMLToPDF.desc', 'HTMLToPDF.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('url-to-pdf', 'images/url.svg', 'home.URLToPDF.title', 'home.URLToPDF.desc', 'URLToPDF.tags')}"></div> | ||||
| 							<div th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'images/markdown.svg', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('markdown-to-pdf', 'images/markdown.svg', 'home.MarkdownToPDF.title', 'home.MarkdownToPDF.desc', 'MarkdownToPDF.tags')}"></div> | ||||
|                             <hr class="dropdown-divider"> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-img', 'images/image.svg', 'home.pdfToImage.title', 'home.pdfToImage.desc', 'pdfToImage.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('pdf-to-word', 'images/file-earmark-word.svg', 'home.PDFToWord.title', 'home.PDFToWord.desc', 'PDFToWord.tags')}"></div> | ||||
| @ -91,32 +91,34 @@ | ||||
|                         </div> | ||||
|                     </li> | ||||
| 
 | ||||
| <li class="nav-item nav-item-separator"></li> | ||||
|                     <li class="nav-item nav-item-separator"></li> | ||||
| 
 | ||||
|                   <li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='add-watermark' OR ${currentPage}=='cert-sign' OR ${currentPage}=='sanitize-pdf' ? 'active' : ''"> | ||||
|                     <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                         <img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text"  th:text="#{navbar.security}"></span> | ||||
|                     </a> | ||||
|                     <div class="dropdown-menu" aria-labelledby="navbarDropdown"> | ||||
|                         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags')}"></div> | ||||
|                         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags')}"></div> | ||||
|                         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags')}"></div> | ||||
|                         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div> | ||||
|                         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div> | ||||
|                         <div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div> | ||||
| 						<div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'images/eraser-fill.svg', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags')}"></div> | ||||
|                     </div> | ||||
|                 </li> | ||||
|                     <li class="nav-item dropdown" th:classappend="${currentPage}=='add-password' OR ${currentPage}=='remove-password' OR ${currentPage}=='add-watermark' OR ${currentPage}=='cert-sign' OR ${currentPage}=='sanitize-pdf' ? 'active' : ''"> | ||||
|                         <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                             <img class="icon" src="images/shield-check.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> <span class="icon-text"  th:text="#{navbar.security}"></span> | ||||
|                         </a> | ||||
|                         <div class="dropdown-menu" aria-labelledby="navbarDropdown"> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-password', 'images/lock.svg', 'home.addPassword.title', 'home.addPassword.desc', 'addPassword.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('remove-password', 'images/unlock.svg', 'home.removePassword.title', 'home.removePassword.desc', 'removePassword.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('change-permissions', 'images/shield-lock.svg', 'home.permissions.title', 'home.permissions.desc', 'permissions.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-watermark', 'images/droplet.svg', 'home.watermark.title', 'home.watermark.desc', 'watermark.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('cert-sign', 'images/award.svg', 'home.certSign.title', 'home.certSign.desc', 'certSign.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('sanitize-pdf', 'images/sanitize.svg', 'home.sanitizePdf.title', 'home.sanitizePdf.desc', 'sanitizePdf.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-redact', 'images/eraser-fill.svg', 'home.autoRedact.title', 'home.autoRedact.desc', 'autoRedact.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-extract', 'images/eraser-fill.svg', 'home.tableExtraxt.title', 'home.tableExtraxt.desc', 'tableExtraxt.tags')}"></div> | ||||
| 
 | ||||
|                   <li class="nav-item nav-item-separator"></li> | ||||
|                         </div> | ||||
|                     </li> | ||||
| 
 | ||||
|                     <li class="nav-item nav-item-separator"></li> | ||||
|                     <li class="nav-item dropdown" th:classappend="${currentPage}=='sign' OR ${currentPage}=='repair' OR ${currentPage}=='compare' OR ${currentPage}=='show-javascript' OR ${currentPage}=='flatten' OR ${currentPage}=='remove-blanks' OR ${currentPage}=='extract-image-scans' OR ${currentPage}=='change-metadata' OR ${currentPage}=='add-image' OR ${currentPage}=='ocr-pdf' OR ${currentPage}=='change-permissions' OR ${currentPage}=='extract-images' OR ${currentPage}=='compress-pdf' OR ${currentPage}=='add-page-numbers' OR ${currentPage}=='auto-rename' OR ${currentPage}=='get-info-on-pdf' ? 'active' : ''"> | ||||
|                         <a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                         <img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> | ||||
|                         <span class="icon-text"  th:text="#{navbar.other}"></span> | ||||
|                             <img class="icon" src="images/card-list.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;"> | ||||
|                             <span class="icon-text"  th:text="#{navbar.other}"></span> | ||||
| 
 | ||||
|                         </a> | ||||
|                         <div class="dropdown-menu" aria-labelledby="navbarDropdown"> | ||||
|                         	<!--<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags')}"></div> --> | ||||
|                             <!--<div th:replace="~{fragments/navbarEntry :: navbarEntry ('pipeline', 'images/pipeline.svg', 'home.pipeline.title', 'home.pipeline.desc', 'pipeline.tags')}"></div> --> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('ocr-pdf', 'images/search.svg', 'home.ocr.title', 'home.ocr.desc', 'ocr.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-image', 'images/file-earmark-richtext.svg', 'home.addImage.title', 'home.addImage.desc', 'addImage.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('compress-pdf', 'images/file-zip.svg', 'home.compressPdfs.title', 'home.compressPdfs.desc', 'compressPdfs.tags')}"></div> | ||||
| @ -130,8 +132,8 @@ | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('compare', 'images/scales.svg', 'home.compare.title', 'home.compare.desc', 'compare.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('add-page-numbers', 'images/add-page-numbers.svg', 'home.add-page-numbers.title', 'home.add-page-numbers.desc', 'add-page-numbers.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('auto-rename', 'images/fonts.svg', 'home.auto-rename.title', 'home.auto-rename.desc', 'auto-rename.tags')}"></div> | ||||
|                         	<div th:replace="~{fragments/navbarEntry :: navbarEntry ('get-info-on-pdf', 'images/info.svg', 'home.getPdfInfo.title', 'home.getPdfInfo.desc', 'getPdfInfo.tags')}"></div> | ||||
|                         	<div th:replace="~{fragments/navbarEntry :: navbarEntry ('show-javascript', 'images/js.svg', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('get-info-on-pdf', 'images/info.svg', 'home.getPdfInfo.title', 'home.getPdfInfo.desc', 'getPdfInfo.tags')}"></div> | ||||
|                             <div th:replace="~{fragments/navbarEntry :: navbarEntry ('show-javascript', 'images/js.svg', 'home.showJS.title', 'home.showJS.desc', 'showJS.tags')}"></div> | ||||
|                         </div> | ||||
|                     </li> | ||||
| 
 | ||||
| @ -147,23 +149,23 @@ | ||||
|                         </div> | ||||
|                     </li> | ||||
| 
 | ||||
|           | ||||
| 
 | ||||
| 					<script src="js/languageSelection.js"></script> | ||||
| 					<script src="js/darkmode.js"></script> | ||||
| 
 | ||||
|     				<li class="nav-item"> | ||||
|                     <script src="js/languageSelection.js"></script> | ||||
|                     <script src="js/darkmode.js"></script> | ||||
| 
 | ||||
|                     <li class="nav-item"> | ||||
|                         <a class="nav-link" id="dark-mode-toggle" href="#"> | ||||
|                           <img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" /> | ||||
|                             <img class="navbar-icon" id="dark-mode-icon" src="moon.svg" alt="icon" /> | ||||
|                         </a> | ||||
|                       </li> | ||||
|                     </li> | ||||
| 
 | ||||
|                     <li class="nav-item dropdown"> | ||||
|                         <a class="nav-link dropdown-toggle" href="#" id="languageDropdown" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> | ||||
|                         <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2 globe-icon" viewBox="0 0 20 20"> | ||||
|                           <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/> | ||||
|                         </svg> | ||||
|                          </a> | ||||
|                             <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="bi bi-globe2 globe-icon" viewBox="0 0 20 20"> | ||||
|                                 <path d="M0 8a8 8 0 1 1 16 0A8 8 0 0 1 0 8zm7.5-6.923c-.67.204-1.335.82-1.887 1.855-.143.268-.276.56-.395.872.705.157 1.472.257 2.282.287V1.077zM4.249 3.539c.142-.384.304-.744.481-1.078a6.7 6.7 0 0 1 .597-.933A7.01 7.01 0 0 0 3.051 3.05c.362.184.763.349 1.198.49zM3.509 7.5c.036-1.07.188-2.087.436-3.008a9.124 9.124 0 0 1-1.565-.667A6.964 6.964 0 0 0 1.018 7.5h2.49zm1.4-2.741a12.344 12.344 0 0 0-.4 2.741H7.5V5.091c-.91-.03-1.783-.145-2.591-.332zM8.5 5.09V7.5h2.99a12.342 12.342 0 0 0-.399-2.741c-.808.187-1.681.301-2.591.332zM4.51 8.5c.035.987.176 1.914.399 2.741A13.612 13.612 0 0 1 7.5 10.91V8.5H4.51zm3.99 0v2.409c.91.03 1.783.145 2.591.332.223-.827.364-1.754.4-2.741H8.5zm-3.282 3.696c.12.312.252.604.395.872.552 1.035 1.218 1.65 1.887 1.855V11.91c-.81.03-1.577.13-2.282.287zm.11 2.276a6.696 6.696 0 0 1-.598-.933 8.853 8.853 0 0 1-.481-1.079 8.38 8.38 0 0 0-1.198.49 7.01 7.01 0 0 0 2.276 1.522zm-1.383-2.964A13.36 13.36 0 0 1 3.508 8.5h-2.49a6.963 6.963 0 0 0 1.362 3.675c.47-.258.995-.482 1.565-.667zm6.728 2.964a7.009 7.009 0 0 0 2.275-1.521 8.376 8.376 0 0 0-1.197-.49 8.853 8.853 0 0 1-.481 1.078 6.688 6.688 0 0 1-.597.933zM8.5 11.909v3.014c.67-.204 1.335-.82 1.887-1.855.143-.268.276-.56.395-.872A12.63 12.63 0 0 0 8.5 11.91zm3.555-.401c.57.185 1.095.409 1.565.667A6.963 6.963 0 0 0 14.982 8.5h-2.49a13.36 13.36 0 0 1-.437 3.008zM14.982 7.5a6.963 6.963 0 0 0-1.362-3.675c-.47.258-.995.482-1.565.667.248.92.4 1.938.437 3.008h2.49zM11.27 2.461c.177.334.339.694.482 1.078a8.368 8.368 0 0 0 1.196-.49 7.01 7.01 0 0 0-2.275-1.52c.218.283.418.597.597.932zm-.488 1.343a7.765 7.765 0 0 0-.395-.872C9.835 1.897 9.17 1.282 8.5 1.077V4.09c.81-.03 1.577-.13 2.282-.287z"/> | ||||
|                             </svg> | ||||
|                         </a> | ||||
|                         <div class="dropdown-menu" aria-labelledby="languageDropdown"> | ||||
|                             <th:block th:insert="~{fragments/languages :: langs}"></th:block> | ||||
|                         </div> | ||||
| @ -178,68 +180,68 @@ | ||||
| 
 | ||||
|                     </li> | ||||
| 
 | ||||
|              <!-- Search Button and Search Bar --> | ||||
| <li class="nav-item position-relative"> | ||||
|     <a href="#" class="nav-link" id="search-icon"> | ||||
|         <img class="navbar-icon" src="images/search.svg" alt="icon" width="24" height="24"> | ||||
|     </a> | ||||
|     <!-- Search Bar --> | ||||
|     <div class="collapse position-absolute" id="navbarSearch"> | ||||
|         <form class="d-flex p-2 bg-white border search-form" id="searchForm"> | ||||
|             <input class="form-control search-input" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput"> | ||||
|         </form> | ||||
|         <!-- Search Results --> | ||||
|         <div id="searchResults" class="border p-2 bg-white search-results"></div> | ||||
|     </div> | ||||
| </li> | ||||
| <style> | ||||
| #search-icon i { | ||||
|     font-size: 24px; /* Adjust this to your desired size */ | ||||
|     transition: color 0.3s; | ||||
| } | ||||
|                     <!-- Search Button and Search Bar --> | ||||
|                     <li class="nav-item position-relative"> | ||||
|                         <a href="#" class="nav-link" id="search-icon"> | ||||
|                             <img class="navbar-icon" src="images/search.svg" alt="icon" width="24" height="24"> | ||||
|                         </a> | ||||
|                         <!-- Search Bar --> | ||||
|                         <div class="collapse position-absolute" id="navbarSearch"> | ||||
|                             <form class="d-flex p-2 bg-white border search-form" id="searchForm"> | ||||
|                                 <input class="form-control search-input" type="search" placeholder="Search" aria-label="Search" id="navbarSearchInput"> | ||||
|                             </form> | ||||
|                             <!-- Search Results --> | ||||
|                             <div id="searchResults" class="border p-2 bg-white search-results"></div> | ||||
|                         </div> | ||||
|                     </li> | ||||
|                     <style> | ||||
|                         #search-icon i { | ||||
|                             font-size: 24px; /* Adjust this to your desired size */ | ||||
|                             transition: color 0.3s; | ||||
|                         } | ||||
| 
 | ||||
| #search-icon:hover i { | ||||
|     color: #666; /* Adjust this to your hover color */ | ||||
| } | ||||
|                         #search-icon:hover i { | ||||
|                             color: #666; /* Adjust this to your hover color */ | ||||
|                         } | ||||
| 
 | ||||
| #navbarSearch { | ||||
|     transition: all 0.3s; | ||||
|     max-height: 0; | ||||
|     overflow: hidden; | ||||
|                         #navbarSearch { | ||||
|                             transition: all 0.3s; | ||||
|                             max-height: 0; | ||||
|                             overflow: hidden; | ||||
| 
 | ||||
| } | ||||
|                         } | ||||
| 
 | ||||
| #navbarSearch.show { | ||||
|     max-height: 300px; /* Adjust this to your desired max height */ | ||||
| } | ||||
|                         #navbarSearch.show { | ||||
|                             max-height: 300px; /* Adjust this to your desired max height */ | ||||
|                         } | ||||
| 
 | ||||
| .search-input { | ||||
|     transition: border 0.3s, box-shadow 0.3s; | ||||
|                         .search-input { | ||||
|                             transition: border 0.3s, box-shadow 0.3s; | ||||
| 
 | ||||
| } | ||||
|                         } | ||||
| 
 | ||||
| .search-input:focus { | ||||
|     border-color: #666; /* Adjust this to your focus color */ | ||||
|     box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adjust this to your desired shadow */ | ||||
| } | ||||
|                         .search-input:focus { | ||||
|                             border-color: #666; /* Adjust this to your focus color */ | ||||
|                             box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); /* Adjust this to your desired shadow */ | ||||
|                         } | ||||
| 
 | ||||
| #searchResults { | ||||
|     max-width: 300px; /* Adjust to your preferred width */ | ||||
|     transition: height 0.3s ease; /* Smooth height transition */ | ||||
| } | ||||
|                         #searchResults { | ||||
|                             max-width: 300px; /* Adjust to your preferred width */ | ||||
|                             transition: height 0.3s ease; /* Smooth height transition */ | ||||
|                         } | ||||
| 
 | ||||
| /* Set a fixed height and styling for each search result item */ | ||||
| .search-results a { | ||||
|     display: flex; | ||||
|     align-items: center; | ||||
|     gap: 10px; /* space between icon and text */ | ||||
|     height: 40px; /* Adjust based on your design */ | ||||
|     overflow: hidden; /* Prevent content from overflowing */ | ||||
|     white-space: nowrap; /* Prevent text from wrapping to next line */ | ||||
|     text-overflow: ellipsis; /* Truncate text if it's too long */ | ||||
| } | ||||
|                         /* Set a fixed height and styling for each search result item */ | ||||
|                         .search-results a { | ||||
|                             display: flex; | ||||
|                             align-items: center; | ||||
|                             gap: 10px; /* space between icon and text */ | ||||
|                             height: 40px; /* Adjust based on your design */ | ||||
|                             overflow: hidden; /* Prevent content from overflowing */ | ||||
|                             white-space: nowrap; /* Prevent text from wrapping to next line */ | ||||
|                             text-overflow: ellipsis; /* Truncate text if it's too long */ | ||||
|                         } | ||||
| 
 | ||||
| </style> | ||||
|                     </style> | ||||
| 
 | ||||
|                 </ul> | ||||
| 
 | ||||
| @ -251,61 +253,61 @@ | ||||
|         <script src="js/search.js"></script> | ||||
|     </nav> | ||||
| 
 | ||||
| 	<div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div> | ||||
|     <div th:insert="~{fragments/errorBannerPerPage.html :: errorBannerPerPage}"></div> | ||||
|     <div class="modal fade" id="settingsModal" tabindex="-1" role="dialog" aria-labelledby="settingsModalLabel" aria-hidden="true"> | ||||
|     <div class="modal-dialog modal-dialog-centered" role="document"> | ||||
|         <div class="modal-content dark-card"> | ||||
|             <div class="modal-header"> | ||||
|                 <h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5> | ||||
|                 <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||
|             </div> | ||||
|             <div class="modal-body"> | ||||
|                 <div class="d-flex justify-content-between align-items-center mb-3"> | ||||
|                     <p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p> | ||||
|                     <a href="https://github.com/sponsors/Frooodle" target="_blank"> | ||||
|                         <button type="button" class="btn btn-sm btn-outline-primary">Sponsor Stirling-PDF</button> | ||||
|         <div class="modal-dialog modal-dialog-centered" role="document"> | ||||
|             <div class="modal-content dark-card"> | ||||
|                 <div class="modal-header"> | ||||
|                     <h5 class="modal-title" id="settingsModalLabel" th:text="#{settings.title}"></h5> | ||||
|                     <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | ||||
|                 </div> | ||||
|                 <div class="modal-body"> | ||||
|                     <div class="d-flex justify-content-between align-items-center mb-3"> | ||||
|                         <p class="mb-0" th:utext="#{settings.appVersion} + ' ' + ${@appVersion}"></p> | ||||
|                         <a href="https://github.com/sponsors/Frooodle" target="_blank"> | ||||
|                             <button type="button" class="btn btn-sm btn-outline-primary">Sponsor Stirling-PDF</button> | ||||
|                         </a> | ||||
|                         <a href="swagger-ui/index.html" target="_blank"> | ||||
|                             <button type="button" class="btn btn-sm btn-outline-primary">API</button> | ||||
|                         </a> | ||||
|                         <a href="https://github.com/Frooodle/Stirling-PDF/releases" target="_blank"> | ||||
|                             <button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button> | ||||
|                         </a> | ||||
|                     </div> | ||||
|                     <div class="mb-3"> | ||||
|                         <label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label> | ||||
|                         <select class="form-control" id="downloadOption"> | ||||
|                             <option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option> | ||||
|                             <option value="newWindow" th:utext="#{settings.downloadOption.2}"></option> | ||||
|                             <option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option> | ||||
|                         </select> | ||||
|                     </div> | ||||
|                     <div class="mb-3"> | ||||
|                         <label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label> | ||||
|                         <input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4"> | ||||
|                         <span id="zipThresholdValue" class="ms-2"></span> | ||||
|                     </div> | ||||
|                     <div class="mb-3 form-check"> | ||||
|                         <input type="checkbox" class="form-check-input" id="boredWaiting"> | ||||
|                         <label class="form-check-label" for="boredWaiting" th:text="#{bored}"></label> | ||||
|                     </div> | ||||
|                     <a th:if="${@loginEnabled}" href="account" target="_blank"> | ||||
|                         <button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button> | ||||
|                     </a> | ||||
|                     <a href="swagger-ui/index.html" target="_blank"> | ||||
|                         <button type="button" class="btn btn-sm btn-outline-primary">API</button> | ||||
|                 </div> | ||||
|                 <div class="modal-footer"> | ||||
|                     <a th:if="${@loginEnabled}" href="/logout"> | ||||
|                         <button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button> | ||||
|                     </a> | ||||
|                     <a href="https://github.com/Frooodle/Stirling-PDF/releases" target="_blank"> | ||||
| 					    <button type="button" class="btn btn-sm btn-outline-primary" id="update-btn" th:utext="#{settings.update}"></button> | ||||
| 					</a> | ||||
|                     <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button> | ||||
|                 </div> | ||||
|                 <div class="mb-3"> | ||||
|                     <label for="downloadOption" th:utext="#{settings.downloadOption.title}"></label> | ||||
|                     <select class="form-control" id="downloadOption"> | ||||
|                         <option value="sameWindow" th:utext="#{settings.downloadOption.1}"></option> | ||||
|                         <option value="newWindow" th:utext="#{settings.downloadOption.2}"></option> | ||||
|                         <option value="downloadFile" th:utext="#{settings.downloadOption.3}"></option> | ||||
|                     </select> | ||||
|                 </div> | ||||
|                 <div class="mb-3"> | ||||
|                     <label for="zipThreshold" th:utext="#{settings.zipThreshold}"></label> | ||||
|                     <input type="range" class="form-range" min="1" max="9" step="1" id="zipThreshold" value="4"> | ||||
|                     <span id="zipThresholdValue" class="ms-2"></span> | ||||
|                 </div> | ||||
|                 <div class="mb-3 form-check"> | ||||
|                     <input type="checkbox" class="form-check-input" id="boredWaiting"> | ||||
|                     <label class="form-check-label" for="boredWaiting" th:text="#{bored}"></label> | ||||
|                 </div> | ||||
|                 <a th:if="${@loginEnabled}" href="account" target="_blank"> | ||||
|                     <button type="button" class="btn btn-sm btn-outline-primary" th:text="#{settings.accountSettings}">Account Settings</button> | ||||
|                 </a> | ||||
|             </div> | ||||
|             <div class="modal-footer"> | ||||
|             	<a th:if="${@loginEnabled}" href="/logout"> | ||||
| 	                <button type="button" class="btn btn-danger" th:text="#{settings.signOut}">Sign Out</button> | ||||
| 	            </a> | ||||
|                 <button type="button" class="btn btn-secondary" data-bs-dismiss="modal" th:text="#{close}"></button> | ||||
|             </div> | ||||
|         </div> | ||||
|     </div> | ||||
| </div> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| <script src="js/settings.js"></script> | ||||
|     <script src="js/settings.js"></script> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -8,98 +8,99 @@ | ||||
| 
 | ||||
| <body> | ||||
| 
 | ||||
|     <div id="page-container"> | ||||
|         <div id="content-wrap"> | ||||
|             <div th:insert="~{fragments/navbar.html :: navbar}"></div> | ||||
|             <!-- Jumbotron --> | ||||
|             <div class="bg-light p-5 rounded d-none d-md-block" id="jumbotron"> | ||||
|                 <div class="container"> | ||||
|                     <h1 class="display-4" th:text="${@appName}"></h1> | ||||
|                     <p class="lead" th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}"></p> | ||||
|                 </div> | ||||
| <div id="page-container"> | ||||
|     <div id="content-wrap"> | ||||
|         <div th:insert="~{fragments/navbar.html :: navbar}"></div> | ||||
|         <!-- Jumbotron --> | ||||
|         <div class="bg-light p-5 rounded d-none d-md-block" id="jumbotron"> | ||||
|             <div class="container"> | ||||
|                 <h1 class="display-4" th:text="${@appName}"></h1> | ||||
|                 <p class="lead" th:text="${@homeText != 'null' and @homeText != null and @homeText != ''} ? ${@homeText} : #{home.desc}"></p> | ||||
|             </div> | ||||
| 			<br class="d-md-none"> | ||||
|             <!-- Features --> | ||||
|             <script src="js/homecard.js"></script> | ||||
|             	 | ||||
|             <div class=" container"> | ||||
|         </div> | ||||
|         <br class="d-md-none"> | ||||
|         <!-- Features --> | ||||
|         <script src="js/homecard.js"></script> | ||||
| 
 | ||||
|         <div class=" container"> | ||||
|             <br> | ||||
|             <input type="text" id="searchBar" onkeyup="filterCards()" th:placeholder="#{home.searchBar}"> | ||||
|             <div class="features-container "> | ||||
|             	 | ||||
|             	 | ||||
|                  <!-- <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div> --> | ||||
| 
 | ||||
| 
 | ||||
|                 <!-- <div th:replace="~{fragments/card :: card(id='pipeline', cardTitle=#{home.pipeline.title}, cardText=#{home.pipeline.desc}, cardLink='pipeline', svgPath='images/pipeline.svg')}"></div> --> | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='view-pdf', cardTitle=#{home.viewPdf.title}, cardText=#{home.viewPdf.desc}, cardLink='view-pdf', svgPath='images/book-opened.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='multi-tool', cardTitle=#{home.multiTool.title}, cardText=#{home.multiTool.desc}, cardLink='multi-tool', svgPath='images/tools.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='merge-pdfs', cardTitle=#{home.merge.title}, cardText=#{home.merge.desc}, cardLink='merge-pdfs', svgPath='images/union.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='split-pdfs', cardTitle=#{home.split.title}, cardText=#{home.split.desc}, cardLink='split-pdfs', svgPath='images/layout-split.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='rotate-pdf', cardTitle=#{home.rotate.title}, cardText=#{home.rotate.desc}, cardLink='rotate-pdf', svgPath='images/arrow-clockwise.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='crop', cardTitle=#{home.crop.title}, cardText=#{home.crop.desc}, cardLink='crop', svgPath='images/crop.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='add-page-numbers', cardTitle=#{home.add-page-numbers.title}, cardText=#{home.add-page-numbers.desc}, cardLink='add-page-numbers', svgPath='images/add-page-numbers.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='adjust-contrast', cardTitle=#{home.adjust-contrast.title}, cardText=#{home.adjust-contrast.desc}, cardLink='adjust-contrast', svgPath='images/adjust-contrast.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='img-to-pdf', cardTitle=#{home.imageToPdf.title}, cardText=#{home.imageToPdf.desc}, cardLink='img-to-pdf', svgPath='images/image.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-img', cardTitle=#{home.pdfToImage.title}, cardText=#{home.pdfToImage.desc}, cardLink='pdf-to-img', svgPath='images/image.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-organizer', cardTitle=#{home.pdfOrganiser.title}, cardText=#{home.pdfOrganiser.desc}, cardLink='pdf-organizer', svgPath='images/sort-numeric-down.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='add-image', cardTitle=#{home.addImage.title}, cardText=#{home.addImage.desc}, cardLink='add-image', svgPath='images/file-earmark-richtext.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='add-watermark', cardTitle=#{home.watermark.title}, cardText=#{home.watermark.desc}, cardLink='add-watermark', svgPath='images/droplet.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='file-to-pdf', cardTitle=#{home.fileToPDF.title}, cardText=#{home.fileToPDF.desc}, cardLink='file-to-pdf', svgPath='images/file.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='remove-pages', cardTitle=#{home.removePages.title}, cardText=#{home.removePages.desc}, cardLink='remove-pages', svgPath='images/file-earmark-x.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='add-password', cardTitle=#{home.addPassword.title}, cardText=#{home.addPassword.desc}, cardLink='add-password', svgPath='images/lock.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='remove-password', cardTitle=#{home.removePassword.title}, cardText=#{home.removePassword.desc}, cardLink='remove-password', svgPath='images/unlock.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='compress-pdf', cardTitle=#{home.compressPdfs.title}, cardText=#{home.compressPdfs.desc}, cardLink='compress-pdf', svgPath='images/file-zip.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='change-metadata', cardTitle=#{home.changeMetadata.title}, cardText=#{home.changeMetadata.desc}, cardLink='change-metadata', svgPath='images/clipboard-data.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='change-permissions', cardTitle=#{home.permissions.title}, cardText=#{home.permissions.desc}, cardLink='change-permissions', svgPath='images/shield-lock.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='ocr-pdf', cardTitle=#{home.ocr.title}, cardText=#{home.ocr.desc}, cardLink='ocr-pdf', svgPath='images/search.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='extract-images', cardTitle=#{home.extractImages.title}, cardText=#{home.extractImages.desc}, cardLink='extract-images', svgPath='images/images.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-pdfa', cardTitle=#{home.pdfToPDFA.title}, cardText=#{home.pdfToPDFA.desc}, cardLink='pdf-to-pdfa', svgPath='images/file-earmark-pdf.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-word', cardTitle=#{home.PDFToWord.title}, cardText=#{home.PDFToWord.desc}, cardLink='pdf-to-word', svgPath='images/file-earmark-word.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-presentation', cardTitle=#{home.PDFToPresentation.title}, cardText=#{home.PDFToPresentation.desc}, cardLink='pdf-to-presentation', svgPath='images/file-earmark-ppt.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-text', cardTitle=#{home.PDFToText.title}, cardText=#{home.PDFToText.desc}, cardLink='pdf-to-text', svgPath='images/filetype-txt.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-html', cardTitle=#{home.PDFToHTML.title}, cardText=#{home.PDFToHTML.desc}, cardLink='pdf-to-html', svgPath='images/filetype-html.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-xml', cardTitle=#{home.PDFToXML.title}, cardText=#{home.PDFToXML.desc}, cardLink='pdf-to-xml', svgPath='images/filetype-xml.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='extract-image-scans', cardTitle=#{home.ScannerImageSplit.title}, cardText=#{home.ScannerImageSplit.desc}, cardLink='extract-image-scans', svgPath='images/scanner.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='sign', cardTitle=#{home.sign.title}, cardText=#{home.sign.desc}, cardLink='sign', svgPath='images/sign.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='flatten', cardTitle=#{home.flatten.title}, cardText=#{home.flatten.desc}, cardLink='flatten', svgPath='images/flatten.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='repair', cardTitle=#{home.repair.title}, cardText=#{home.repair.desc}, cardLink='repair', svgPath='images/wrench.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='remove-blanks', cardTitle=#{home.removeBlanks.title}, cardText=#{home.removeBlanks.desc}, cardLink='remove-blanks', svgPath='images/blank-file.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='compare', cardTitle=#{home.compare.title}, cardText=#{home.compare.desc}, cardLink='compare', svgPath='images/scales.svg')}"></div> | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='cert-sign', cardTitle=#{home.certSign.title}, cardText=#{home.certSign.desc}, cardLink='cert-sign', svgPath='images/award.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='multi-page-layout', cardTitle=#{home.pageLayout.title}, cardText=#{home.pageLayout.desc}, cardLink='multi-page-layout', svgPath='images/page-layout.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='scale-pages', cardTitle=#{home.scalePages.title}, cardText=#{home.scalePages.desc}, cardLink='scale-pages', svgPath='images/scale-pages.svg')}"></div> | ||||
|                  | ||||
|                  | ||||
| 
 | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='auto-rename', cardTitle=#{home.auto-rename.title}, cardText=#{home.auto-rename.desc}, cardLink='auto-rename', svgPath='images/fonts.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='auto-split-pdf', cardTitle=#{home.autoSplitPDF.title}, cardText=#{home.autoSplitPDF.desc}, cardLink='auto-split-pdf', svgPath='images/layout-split.svg')}"></div> | ||||
| 				<div th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', svgPath='images/sanitize.svg')}"></div> | ||||
| 				 | ||||
| 				<div th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', svgPath='images/url.svg')}"></div> | ||||
| 				<div th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', svgPath='images/html.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='sanitize-pdf', cardTitle=#{home.sanitizePdf.title}, cardText=#{home.sanitizePdf.desc}, cardLink='sanitize-pdf', svgPath='images/sanitize.svg')}"></div> | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='url-to-pdf', cardTitle=#{home.URLToPDF.title}, cardText=#{home.URLToPDF.desc}, cardLink='url-to-pdf', svgPath='images/url.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='html-to-pdf', cardTitle=#{home.HTMLToPDF.title}, cardText=#{home.HTMLToPDF.desc}, cardLink='html-to-pdf', svgPath='images/html.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='markdown-to-pdf', cardTitle=#{home.MarkdownToPDF.title}, cardText=#{home.MarkdownToPDF.desc}, cardLink='markdown-to-pdf', svgPath='images/markdown.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='get-info-on-pdf', cardTitle=#{home.getPdfInfo.title}, cardText=#{home.getPdfInfo.desc}, cardLink='get-info-on-pdf', svgPath='images/info.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='extract-page', cardTitle=#{home.extractPage.title}, cardText=#{home.extractPage.desc}, cardLink='extract-page', svgPath='images/extract.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-single-page', cardTitle=#{home.PdfToSinglePage.title}, cardText=#{home.PdfToSinglePage.desc}, cardLink='pdf-to-single-page', svgPath='images/single-page.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='show-javascript', cardTitle=#{home.showJS.title}, cardText=#{home.showJS.desc}, cardLink='show-javascript', svgPath='images/js.svg')}"></div> | ||||
|                 <div th:replace="~{fragments/card :: card(id='auto-redact', cardTitle=#{home.autoRedact.title}, cardText=#{home.autoRedact.desc}, cardLink='auto-redact', svgPath='images/eraser-fill.svg')}"></div> | ||||
|                  | ||||
|                  | ||||
|                  | ||||
|                  | ||||
| 
 | ||||
|                 <div th:replace="~{fragments/card :: card(id='pdf-to-csv', cardTitle=#{home.tableExtraxt.title}, cardText=#{home.tableExtraxt.desc}, cardLink='pdf-to-csv', svgPath='images/pdf-csv.svg')}"></div> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|             </div> | ||||
|         </div> </div> | ||||
|         <div th:insert="~{fragments/footer.html :: footer}"></div> | ||||
|     </div> | ||||
|     <div th:insert="~{fragments/footer.html :: footer}"></div> | ||||
| </div> | ||||
| </body> | ||||
| 
 | ||||
| </html> | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user