Merge pull request #459 from Artem-ka-create/issue-372-pdfbox
Issue 372 pdfbox
This commit is contained in:
commit
9d052b310f
29 changed files with 1002 additions and 235 deletions
|
@ -81,10 +81,12 @@ dependencies {
|
|||
// 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
|
||||
|
||||
// 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'
|
||||
|
@ -100,9 +102,9 @@ dependencies {
|
|||
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()
|
||||
|
|
|
@ -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=?????????
|
|
@ -823,3 +823,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
|
|
@ -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
|
||||
|
||||
|
||||
###########################
|
||||
# #
|
||||
|
@ -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">
|
||||
<script th:inline="javascript">
|
||||
const currentVersion = /*[[${@appVersion}]]*/ '';
|
||||
const noFavourites = /*[[#{noFavourites}]]*/ '';
|
||||
</script>
|
||||
<script th:src="@{js/githubVersion.js}"></script>
|
||||
</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,7 +39,7 @@
|
|||
</a>
|
||||
</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}=='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">
|
||||
|
@ -62,8 +62,8 @@
|
|||
|
||||
|
||||
</div>
|
||||
</li>
|
||||
<li class="nav-item nav-item-separator"></li>
|
||||
</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">
|
||||
<img class="icon" src="images/arrow-left-right.svg" alt="icon" style="width: 16px; height: 16px; vertical-align: middle;">
|
||||
|
@ -91,7 +91,7 @@
|
|||
</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">
|
||||
|
@ -105,6 +105,8 @@
|
|||
<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>
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
||||
|
@ -179,7 +181,7 @@
|
|||
</li>
|
||||
|
||||
<!-- Search Button and Search Bar -->
|
||||
<li class="nav-item position-relative">
|
||||
<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>
|
||||
|
@ -191,45 +193,45 @@
|
|||
<!-- Search Results -->
|
||||
<div id="searchResults" class="border p-2 bg-white search-results"></div>
|
||||
</div>
|
||||
</li>
|
||||
<style>
|
||||
#search-icon i {
|
||||
</li>
|
||||
<style>
|
||||
#search-icon i {
|
||||
font-size: 24px; /* Adjust this to your desired size */
|
||||
transition: color 0.3s;
|
||||
}
|
||||
}
|
||||
|
||||
#search-icon:hover i {
|
||||
#search-icon:hover i {
|
||||
color: #666; /* Adjust this to your hover color */
|
||||
}
|
||||
}
|
||||
|
||||
#navbarSearch {
|
||||
#navbarSearch {
|
||||
transition: all 0.3s;
|
||||
max-height: 0;
|
||||
overflow: hidden;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#navbarSearch.show {
|
||||
#navbarSearch.show {
|
||||
max-height: 300px; /* Adjust this to your desired max height */
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
.search-input {
|
||||
transition: border 0.3s, box-shadow 0.3s;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
.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 {
|
||||
#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 {
|
||||
/* 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 */
|
||||
|
@ -237,9 +239,9 @@
|
|||
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>
|
||||
|
||||
|
@ -301,11 +303,11 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<script src="js/settings.js"></script>
|
||||
<script src="js/settings.js"></script>
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
<body>
|
||||
|
||||
<div id="page-container">
|
||||
<div id="page-container">
|
||||
<div id="content-wrap">
|
||||
<div th:insert="~{fragments/navbar.html :: navbar}"></div>
|
||||
<!-- Jumbotron -->
|
||||
|
@ -93,13 +93,14 @@
|
|||
<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>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
Loading…
Reference in a new issue