package gov.nysenate.openleg.client.view.transcript; import gov.nysenate.openleg.model.transcript.Transcript; import gov.nysenate.openleg.processor.transcript.TranscriptLine; import gov.nysenate.openleg.util.TranscriptTextUtils; import org.apache.pdfbox.exceptions.COSVisitorException; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.pdmodel.PDPage; import org.apache.pdfbox.pdmodel.edit.PDPageContentStream; import org.apache.pdfbox.pdmodel.font.PDFont; import org.apache.pdfbox.pdmodel.font.PDType1Font; import java.io.IOException; import java.io.OutputStream; import java.time.LocalDateTime; import java.util.List; /** * Pdf representation of a transcript designed to match the formatting * of the official transcripts. */ public class TranscriptPdfView { // Kirkland started on May 16, 2011 private static LocalDateTime KIRKLAND_START_TIME = LocalDateTime.of(2011, 5, 16, 0, 0, 0); // Candyco started Jan 1st, 2005 private static LocalDateTime CANDYCO_START_TIME = LocalDateTime.of(2005, 1, 1, 0, 0, 0); // Candyco also did 1999-2003 private static LocalDateTime CANDYCO_1999_START = LocalDateTime.of(1999, 1, 1, 0, 0, 0); private static LocalDateTime CANDYCO_2003_END = LocalDateTime.of(2004, 1, 1, 0, 0, 0); // Pauline Williman did 1993-1998 private static LocalDateTime WILLIMAN_START = LocalDateTime.of(1993, 1, 1, 0, 0, 0); private static LocalDateTime WILLIMAN_END = LocalDateTime.of(1999, 1, 1, 0, 0, 0); private static Float bot = 90f; private static Float right = 575f; private static Float top = 710f; private static Float left = 105f; private static Float fontSize = 12f; private static Float fontWidth = 7f; public static final int NO_LINE_NUM_INDENT = 11; public static final int STENOGRAPHER_LINE_NUM = 26; public static void writeTranscriptPdf(Transcript transcript, OutputStream outputStream) throws IOException, COSVisitorException { if (transcript == null) { throw new IllegalArgumentException("Supplied transcript cannot be null when converting to pdf."); } try (PDDocument doc = new PDDocument()) { PDFont font = PDType1Font.COURIER; List<List<String>> pages = TranscriptTextUtils.getPdfFormattedPages(transcript.getText()); for (List<String> page : pages) { PDPage pg = new PDPage(PDPage.PAGE_SIZE_LETTER); PDPageContentStream contentStream = new PDPageContentStream(doc, pg); drawBorder(contentStream); contentStream.beginText(); contentStream.setFont(font, fontSize); moveStreamToTopOfPage(contentStream); int lineCount = drawPageText(page, contentStream); drawStenographer(transcript, contentStream, lineCount); contentStream.endText(); contentStream.close(); doc.addPage(pg); } doc.save(outputStream); } } /** * Draw page text, correctly aligning page numbers, line numbers, and lines without line numbers. * Also insert information about the Stenographer. * <p> * Page numbers should be right aligned above the border. * Line numbers should aligned left of the left vertical border. * Lines without line numbers should have their spacing between each vertical line similar * to other transcripts. * The stenographer should be centered at the bottom of the page * </p> */ private static int drawPageText(List<String> page, PDPageContentStream contentStream) throws IOException { int lineCount = 0; for (String ln : page) { TranscriptLine line = new TranscriptLine(ln); if (line.isPageNumber()) { drawPageNumber(line.fullText().trim(), contentStream); } else { drawText(contentStream, line); lineCount++; } } return lineCount; } private static void drawText(PDPageContentStream contentStream, TranscriptLine line) throws IOException { int indent; String text; if (line.hasLineNumber()) { indent = lineNumberLength(line) + 1; text = line.fullText().trim(); } else { indent = NO_LINE_NUM_INDENT; text = line.fullText(); } float offset = left - indent * fontWidth; drawLine(text, offset, contentStream); } private static void drawBorder(PDPageContentStream contentStream) throws IOException { contentStream.drawLine(left, top, left, bot); contentStream.drawLine(left, top, right, top); contentStream.drawLine(left, bot, right, bot); contentStream.drawLine(right, top, right, bot); } private static void drawLine(String line, float offset, PDPageContentStream contentStream) throws IOException { contentStream.moveTextPositionByAmount(offset, -fontSize); contentStream.drawString(line); contentStream.moveTextPositionByAmount(-offset, -fontSize); } private static void drawPageNumber(String line, PDPageContentStream contentStream) throws IOException { float offset = right - (line.length() + 1) * fontWidth; contentStream.moveTextPositionByAmount(offset, fontWidth * 2); contentStream.drawString(line); contentStream.moveTextPositionByAmount(-offset, -fontSize * 2); } private static void moveStreamToTopOfPage(PDPageContentStream contentStream) throws IOException { contentStream.moveTextPositionByAmount(0, top - fontWidth); } private static int lineNumberLength(TranscriptLine line) { return line.fullText().trim().split("\\s")[0].length(); } private static void drawStenographer(Transcript transcript, PDPageContentStream contentStream, int lineCount) throws IOException { String stenographer = ""; if (transcript.getDateTime().isAfter(KIRKLAND_START_TIME)) { stenographer = "Kirkland Reporting Service"; } else if (transcript.getDateTime().isAfter(CANDYCO_START_TIME)) { stenographer = "Candyco Transcription Service, Inc."; } else if (transcript.getDateTime().isAfter(CANDYCO_1999_START) && transcript.getDateTime().isBefore(CANDYCO_2003_END)) { stenographer = "Candyco Transcription Service, Inc."; } else if (transcript.getDateTime().isAfter(WILLIMAN_START) && transcript.getDateTime().isBefore(WILLIMAN_END)) { stenographer = "Pauline Williman, Certified Shorthand Reporter"; } float offset = (lineCount - STENOGRAPHER_LINE_NUM) * 2 * fontSize; // * 2 because of double spacing. contentStream.moveTextPositionByAmount(left + (right - left - stenographer.length() * fontWidth) / 2, offset); contentStream.drawString(stenographer); } }