diff --git a/.gitignore b/.gitignore index 9e0d188..fed4a42 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,8 @@ libs/postgresql-*.jar libs/slf4j-*.jar libs/sqlite-jdbc-*.jar libs/dynamic-loader-*.jar +libs/fontbox-*.jar +libs/pdfbox-*.jar # Gradle diff --git a/src/main/core/org/kissweb/PDF.java b/src/main/core/org/kissweb/PDF.java new file mode 100644 index 0000000..50957d4 --- /dev/null +++ b/src/main/core/org/kissweb/PDF.java @@ -0,0 +1,612 @@ +package org.kissweb; + + +import org.apache.log4j.Logger; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDPageContentStream.AppendMode; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.util.Matrix; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; + +/** + * This class creates PDF files with text, images, and line graphics. + * + * Author: Blake McBride + * Date: 3/5/16 + */ +public class PDF { + private final static Logger logger = Logger.getLogger(PDF.class); + + private PDDocument doc; + private PDPageContentStream contentStream = null; + private float posx=0, posy=0; + private float fontSize = 11f; + private PDFont font = PDType1Font.COURIER; + private PDPage page; + private boolean landscape = false; + private float pageHeight, pageWidth; + private final String outputFilename; + private PDRectangle pageSize = PDRectangle.LETTER; + private boolean inText = false; + + /** + * Begin a new PDF file + * + * @param fname the file name to be saved to (include the .pdf) + */ + public PDF(String fname) { + outputFilename = fname; + doc = new PDDocument(); + } + + /** + * Begin a new PDF file with an existing PDF file as its starting point. + * + * @param infile the name of the input PDF template file + * @param outfile the file name to be saved to (include the .pdf) + * @throws IOException + */ + public PDF(String infile, String outfile) throws IOException { + outputFilename = outfile; + doc = PDDocument.load(new File(infile)); + } + + /** + * Set font style and size + * + * @param fnt font style + * @param fs font size in points + */ + public void setFont(PDFont fnt, float fs) { + font = fnt; + fontSize = fs; + if (contentStream != null) { + try { + contentStream.setFont(font, fontSize); + } catch (IOException e) { + logger.error("Error", e); + } + } + } + + public void landscape() { + landscape = true; + } + + public void portrait() { + landscape = false; + } + + public PDRectangle setPageSize(PDRectangle ps) { + PDRectangle old = pageSize; + pageSize = ps; + return old; + } + + /** + * Output txt at line y, column x + * Lines and column numbers take font into account so, for example, typically letter paper would + * give 66 lines and 80 columns. + * + * @param y absolute line position top to bottom, line 1 to line ... + * @param x absolute column position left to right, column 1 to ... + * @param txt the text to be written + */ + public void textOut(int y, int x, String txt) { + float absx, absy, relx, rely; + if (fontSize > 11.9f && fontSize < 12.1f) + absx = (x+1) * 7.2f; // 12pt Courier font + else if (fontSize > 10.9f && fontSize < 11.1f) + absx = (x+3) * 6.6f; // 11pt Courier font + else + absx = (x+6) * 6f; // 10pt Courier font + absy = pageHeight - (y * 12f) + 2f; + try { + startText(); + relx = absx - posx; + rely = absy - posy; + contentStream.newLineAtOffset(relx, rely); + contentStream.showText(txt); + } catch (Exception e) { + logger.error("Error", e); + } + posx = absx; + posy = absy; + } + + /** + * Start output of text. + *

+ * You cannot intermix graphics with text output (although both can be on the same page). + * Text must be surrounded with startText() end endText(). + * However, when you start a new page, the system automatically starts in text mode. + */ + public void startText() { + if (!inText) { + try { + contentStream.beginText(); + contentStream.setNonStrokingColor(Color.BLACK); + inText = true; + posy = posx = 0; + } catch (Exception e) { + logger.error("Error", e); + } + } + } + + /** + * End text mode and go into graphics mode. + *

+ * Text and graphics cannot be output at the same time without switching between text and graphics mode. + */ + public void endGraphics() { + startText(); + } + + /** + * Output txt at dot position y, dot position x + * + * @param absy absolute dot position, top to bottom + * @param absx absolute dot position, left to right + * @param txt the text to be written + */ + public void textOutpx(float absy, float absx, String txt) { + float relx, rely; + absy = pageHeight - absy; + try { + startText(); + relx = absx - posx; + rely = absy - posy; + contentStream.newLineAtOffset(relx, rely); + contentStream.showText(txt); + } catch (Exception e) { + logger.error("Error", e); + } + posx = absx; + posy = absy; + } + + /** + * End current page and start a new page + */ + public void newPage() { + try { + if (contentStream != null) { + if (inText) + contentStream.endText(); + contentStream.close(); + } + page = new PDPage(pageSize); + contentStream = new PDPageContentStream(doc, page, AppendMode.OVERWRITE, false); + contentStream.setFont(font, fontSize); + doc.addPage(page); + startText(); + if (landscape) { + page.setRotation(90); + PDRectangle pageSize = page.getMediaBox(); + pageHeight = pageSize.getWidth(); // height <- width is correct! + pageWidth = pageSize.getHeight(); // width <- height is correct! + try { + contentStream.transform(new Matrix(0, 1, -1, 0, pageHeight, 0)); + } catch (Exception e) { + logger.error("Error", e); + } + } else { + PDRectangle pageSize = page.getMediaBox(); + pageHeight = pageSize.getHeight(); + pageWidth = pageSize.getWidth(); + } + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * Get an existing page. + * + * @param n the page number to get (starting at 0) + */ + public void getPage(int n) { + try { + if (contentStream != null) { + if (inText) + contentStream.endText(); + contentStream.close(); + } + page = doc.getPage(n); + contentStream = new PDPageContentStream(doc, page, AppendMode.APPEND, false); + contentStream.setFont(font, fontSize); + startText(); + if (landscape) { + page.setRotation(90); + PDRectangle pageSize = page.getMediaBox(); + pageHeight = pageSize.getWidth(); // height <- width is correct! + pageWidth = pageSize.getHeight(); // width <- height is correct! + try { + contentStream.transform(new Matrix(0, 1, -1, 0, pageHeight, 0)); + } catch (Exception e) { + logger.error("Error", e); + } + } else { + PDRectangle pageSize = page.getMediaBox(); + pageHeight = pageSize.getHeight(); + pageWidth = pageSize.getWidth(); + } + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * Draw a line + * + * @param ya upper left y point + * @param xa upper left x point + * @param yb lower right y point + * @param xb lower right x point + * @param thickness line thickness (-1 == no outside line) + */ + public void drawLine(float ya, float xa, float yb, float xb, float thickness) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + contentStream.setLineWidth(thickness); + contentStream.moveTo(xa, pageHeight-ya); + contentStream.lineTo(xb, pageHeight-yb); + contentStream.stroke(); + } catch (Exception e) { + logger.error("Error", e); + } + } + + /** + * Draw a rectangle + * + * @param ya upper left y point + * @param xa upper left x point + * @param yb lower right y point + * @param xb lower right x point + * @param thickness line thickness (-1 == no outside line) + * @param fill fill percent, -1=no fill, otherwise 0-255 where 0 is black and 255 is white + */ + public void drawRect(float ya, float xa, float yb, float xb, float thickness, int fill) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + + contentStream.addRect(xa, pageHeight-ya, xb-xa, ya-yb); + + if (fill > -1) { + final Color fillColor = new Color(fill); // Assuming fill is an RGB value + if (thickness > .01) { + contentStream.setLineWidth(thickness); + contentStream.setNonStrokingColor(fillColor); + contentStream.fillAndStroke(); + } else { + contentStream.setNonStrokingColor(fillColor); + contentStream.fill(); + } + } if (thickness > .01) { + contentStream.setLineWidth(thickness); + contentStream.stroke(); + } + } catch (Exception e) { + logger.error("Error", e); + } + } + + /** + * Output image file to PDF + *

+ * Positioning starts at upper left corner of paper. + * + * @param ypos place lower left corner of image at vertical position + * @param xpos place lower left corner of image at horizontal position + * @param scale scale image (1.0f means no scaling) + * @param filename name of file holding image + */ + public void imageOut(float ypos, float xpos, float scale, String filename) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + PDImageXObject pdImage = PDImageXObject.createFromFile(filename, doc); + contentStream.drawImage(pdImage, xpos, pageHeight-ypos, pdImage.getWidth()*scale, pdImage.getHeight()*scale); + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * Output scaled image to PDF. + *

+ * Positioning starts at upper left corner of paper. + * + * @param ypos place lower left corner of image at vertical position + * @param xpos place lower left corner of image at horizontal position + * @param scale scale image (1.0f means no scaling) + * @param image the image + */ + public void imageOut(float ypos, float xpos, float scale, byte [] image) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + ByteArrayInputStream bais = new ByteArrayInputStream(image); + BufferedImage bim = ImageIO.read(bais); + bais.close(); + PDImageXObject pdImage = LosslessFactory.createFromImage(doc, bim); + contentStream.drawImage(pdImage, xpos, pageHeight-ypos, pdImage.getWidth()*scale, pdImage.getHeight()*scale); + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * Outputs an image with a specific width to the PDF document retaining the image aspect ratio. + * + * @param ypos place lower left corner of image at vertical position + * @param xpos place lower left corner of image at horizontal position + * @param width the width of the image + * @param filename the filename of the image + */ + public void imageOutWidth(float ypos, float xpos, float width, String filename) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + PDImageXObject pdImage = PDImageXObject.createFromFile(filename, doc); + float aspectRatio = (float) pdImage.getHeight() / pdImage.getWidth(); + float height = width * aspectRatio; + contentStream.drawImage(pdImage, xpos, pageHeight - ypos, width, height); + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * Outputs an image with a specific height to the PDF document retaining the image aspect ratio. + * + * @param ypos place lower left corner of image at vertical position + * @param xpos place lower left corner of image at horizontal position + * @param height the height of the image + * @param filename the filename of the image + */ + public void imageOutHeight(float ypos, float xpos, float height, String filename) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + PDImageXObject pdImage = PDImageXObject.createFromFile(filename, doc); + float aspectRatio = (float) pdImage.getWidth() / pdImage.getHeight(); + float width = height * aspectRatio; + contentStream.drawImage(pdImage, xpos, pageHeight - ypos, width, height); + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * Output image to defined square on the page. + * + * @param ypos place lower left corner of image at vertical position + * @param xpos place lower left corner of image at horizontal position + * @param ypos2 place upper right corner of image at vertical position + * @param xpos2 place upper right corner of image at horizontal position + * @param image the image + */ + public void imageOut(float ypos, float xpos, float ypos2, float xpos2, byte [] image) { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + ByteArrayInputStream bais = new ByteArrayInputStream(image); + BufferedImage bim = ImageIO.read(bais); + bais.close(); + PDImageXObject pdImage = LosslessFactory.createFromImage(doc, bim); + + // Figure out how to scale the image to fit in the box while keeping the aspect ratio + int width = pdImage.getWidth(); + int height = pdImage.getHeight(); + float maxHeight = ypos - ypos2; + float maxWidth = xpos2 - xpos; + float yscale = maxHeight / height; + float xscale = maxWidth / width; + float scale = Math.min(yscale, xscale); + + contentStream.drawImage(pdImage, xpos, pageHeight-ypos, width*scale, height*scale); + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * End text mode and go into graphics mode. + * Text and graphics cannot be output at the same time without switching between text and graphics mode. + */ + public void endText() { + try { + if (inText) { + contentStream.endText(); + inText = false; + } + } catch (IOException e) { + logger.error("Error", e); + } + } + + /** + * End text mode and go into graphics mode. + * Text and graphics cannot be output at the same time without switching between text and graphics mode. + */ + public void startGraphics() { + endText(); + } + + /** + * End current page and end document + */ + public void endDocument() { + try { + if (contentStream != null) { + if (inText) + contentStream.endText(); + contentStream.close(); + } + doc.save(outputFilename); + } catch (Exception e) { + logger.error("Error", e); + } finally { + try { + if (doc != null) { + doc.close(); + doc = null; + } + } catch (Exception e) { + logger.error("Error", e); + } + } + } + + public PDDocument getDoc() { + return doc; + } + + public PDPage getPage() { + return page; + } + + public PDPageContentStream getContentStream() { + return contentStream; + } + + public void grid() { + float y, x; + float yo = 16, xo = 16; // page offsets + + setFont(PDType1Font.COURIER, 7); + // left side marks + for (y=0 ; y <= pageHeight ; y += 10) + if (0 == y%100) { + if (y != 0) + textOutpx(y-1, xo+3, "" + (int) y); + drawLine(y, xo, y, xo+20, .5f); + } else if (0 == y%50) { + drawLine(y, xo, y, xo+15, .5f); + } else + drawLine(y, xo, y, xo+10, .5f); + // right side marks + for (y=0 ; y <= pageHeight ; y += 10) + if (0 == y%100) { + if (y != 0) + textOutpx(y-1, pageWidth-xo-18, "" + (int) y); + drawLine(y, pageWidth-xo-20, y, pageWidth-xo, .5f); + } else if (0 == y%50) { + drawLine(y, pageWidth-xo-15, y, pageWidth-xo, .5f); + } else + drawLine(y, pageWidth-xo-10, y, pageWidth-xo, .5f); + // top marks + for (x=0 ; x <= pageWidth ; x += 10) + if (0 == x%100) { + if (x != 0) + textOutpx(yo+16, x+2, "" + (int) x); + drawLine(yo, x, yo+20, x, .5f); + } else if (0 == x%50) { + drawLine(yo, x, yo+15, x, .5f); + } else + drawLine(yo, x, yo+10, x, .5f); + // bottom marks + for (x=0 ; x <= pageWidth ; x += 10) + if (0 == x%100) { + if (x != 0) + textOutpx(pageHeight-yo-16, x+2, "" + (int) x); + drawLine(pageHeight-yo-20, x, pageHeight-yo, x, .5f); + } else if (0 == x%50) { + drawLine(pageHeight-yo-15, x, pageHeight-yo, x, .5f); + } else + drawLine(pageHeight-yo-10, x, pageHeight-yo, x, .5f); + } + + public static void main(String[] args) throws IOException { + test4(); + } + + private static void test1() { + PDF pdf = new PDF("mypdf.pdf"); + +// pdf.landscape(); + + pdf.newPage(); + + pdf.textOutpx(100, 100, "100x100"); + pdf.textOutpx(300, 100, "300x100"); + pdf.textOutpx(200, 100, "200x100"); + pdf.textOutpx(300, 300, "300x300"); + + pdf.imageOut(100, 200, 1, "WayToGo.png"); + + pdf.newPage(); + pdf.textOutpx(400, 200, "400x200"); + + pdf.endDocument(); + } + + private static void test2() { + PDF pdf = new PDF("mypdf.pdf"); + pdf.newPage(); + + for (int line=1 ; line <= 66 ; line++) + pdf.textOut(line, 20, line + "x20"); + pdf.textOut(25, 40, "25x40x"); + + pdf.drawLine(100, 100, 200, 200, 2); + pdf.drawRect(300, 300, 400, 400, 5, 200); + pdf.drawLine(250, 200, 250, 250, 2); + + pdf.textOut(1, 1, "1234567890123456789"); + + pdf.endDocument(); + } + + private static void test3() { + PDF pdf = new PDF("mypdf.pdf"); +// pdf.landscape(); + pdf.newPage(); + pdf.grid(); + + pdf.imageOut(300, 100, .2f, "WayToGo.png"); + + pdf.endDocument(); + } + + private static void test4() throws IOException { + PDF pdf = new PDF("/home/blake/Desktop/20200320 Vendor Letter KNH.pdf", "/home/blake/Desktop/res.pdf"); + pdf.getPage(0); + // pdf.grid(); + pdf.setFont(PDType1Font.COURIER, 12); + pdf.textOutpx(230, 250, "Blake McBride"); + pdf.endDocument(); + } + +} diff --git a/src/main/core/org/kissweb/builder/Tasks.java b/src/main/core/org/kissweb/builder/Tasks.java index 24670b2..2f11e87 100644 --- a/src/main/core/org/kissweb/builder/Tasks.java +++ b/src/main/core/org/kissweb/builder/Tasks.java @@ -342,6 +342,8 @@ private ForeignDependencies buildForeignDependencies() { dep.add("dynamic-loader-3.1.jar", LIBS, "https://oss.sonatype.org/service/local/artifact/maven/redirect?r=releases&g=org.dvare&a=dynamic-loader&v=3.1&e=jar"); // dep.add("dynamic-loader-3.0.jar", LIBS, "https://oss.sonatype.org/service/local/repositories/releases/content/org/dvare/dynamic-loader/3.0/dynamic-loader-3.0.jar"); dep.add("jquery-3.6.3.min.js", "src/main/frontend/lib", "https://code.jquery.com/jquery-3.6.3.min.js"); + dep.add("pdfbox-2.0.31.jar", LIBS, "https://repo1.maven.org/maven2/org/apache/pdfbox/pdfbox/2.0.31/pdfbox-2.0.31.jar"); + dep.add("fontbox-2.0.31.jar", LIBS, "https://repo1.maven.org/maven2/org/apache/pdfbox/fontbox/2.0.31/fontbox-2.0.31.jar"); // ag-grid appears to no longer be available through a CDN. Therefore, I am simply including it with the Kiss distribution //dep.add("ag-grid-community.noStyle.min.js", "src/main/frontend/lib", "https://cdnjs.cloudflare.com/ajax/libs/ag-grid/25.1.0/ag-grid-community.noStyle.min.js"); //dep.add("ag-grid.min.css", "src/main/frontend/lib", "https://cdnjs.cloudflare.com/ajax/libs/ag-grid/25.1.0/styles/ag-grid.min.css");