Example usage for org.apache.pdfbox.pdmodel PDDocument close

List of usage examples for org.apache.pdfbox.pdmodel PDDocument close

Introduction

In this page you can find the example usage for org.apache.pdfbox.pdmodel PDDocument close.

Prototype

@Override
public void close() throws IOException 

Source Link

Document

This will close the underlying COSDocument object.

Usage

From source file:net.yacy.cider.parser.idiom.pdfIdiom.java

License:Open Source License

@Override
public Model parse(DataSource source) throws ParserException {
    // create an empty Model
    Model model = ModelFactory.createDefaultModel();
    Resource resource = source.hasURI() ? model.createResource(source.getURI().toNormalform(true, true))
            : model.createResource();/*from  w  w w  . j  a  v  a  2  s  .co  m*/

    // open pdf document
    final PDDocument theDocument;
    final PDFParser parser;
    try {
        parser = new PDFParser(source.getStream());
        parser.parse();
        theDocument = parser.getPDDocument();
    } catch (IOException e) {
        log.error(e.getMessage(), e);
        throw new ParserException(e.getMessage(), source.getURI());
    }

    if (theDocument.isEncrypted()) {
        try {
            theDocument.openProtection(new StandardDecryptionMaterial(""));
        } catch (BadSecurityHandlerException e) {
            throw new ParserException("PDF Encrypted (BadSecurityHandlerException): " + e.getMessage(),
                    source.getURI(), e);
        } catch (IOException e) {
            throw new ParserException("PDF Encrypted (IOException): " + e.getMessage(), source.getURI(), e);
        } catch (CryptographyException e) {
            throw new ParserException("PDF Encrypted (CryptographyException): " + e.getMessage(),
                    source.getURI(), e);
        }
        final AccessPermission perm = theDocument.getCurrentAccessPermission();
        if (perm == null || !perm.canExtractContent())
            throw new ParserException("PDF cannot be decrypted", source.getURI());
    }

    // get metadata
    final PDDocumentInformation theDocInfo = theDocument.getDocumentInformation();
    String docTitle = null, docSubject = null, docAuthor = null, docKeywordStr = null;
    if (theDocInfo != null) {
        docTitle = theDocInfo.getTitle();
        docSubject = theDocInfo.getSubject();
        docAuthor = theDocInfo.getAuthor();
        docKeywordStr = theDocInfo.getKeywords();
    }

    if (docAuthor != null && docAuthor.length() > 0) {
        resource.addProperty(VCARD.FN, docAuthor);
        resource.addProperty(DC.creator, docAuthor);
    }
    if (docSubject != null && docSubject.length() > 0) {
        resource.addProperty(DC.subject, docSubject);
    }
    if (docTitle != null && docTitle.length() > 0) {
        resource.addProperty(DC.title, docTitle);
    }
    String[] docKeywords = null;
    if (docKeywordStr != null && docKeywordStr.length() > 0) {
        docKeywords = docKeywordStr.split(" |,");
        resource.addProperty(DC.coverage, concat(docKeywords));
    }

    // get the content
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    Writer writer;
    try {
        writer = new OutputStreamWriter(baos, "UTF-8");
    } catch (UnsupportedEncodingException e1) {
        writer = new OutputStreamWriter(baos);
    }
    try {
        final PDFTextStripper stripper = new PDFTextStripper();
        stripper.writeText(theDocument, writer);
        theDocument.close();
        writer.close();
    } catch (IOException e) {
        if (writer != null)
            try {
                writer.close();
            } catch (final Exception ex) {
            }
        throw new ParserException("PDF content reader", source.getURI(), e);
    }
    String content;
    try {
        content = new String(baos.toByteArray(), "UTF-8");
    } catch (UnsupportedEncodingException e) {
        content = new String(baos.toByteArray());
    }
    if (content != null && content.length() > 0) {
        resource.addProperty(CIDER.data_content_text, content);
    }

    return model;
}

From source file:net.yacy.document.parser.pdfParser.java

License:Open Source License

@Override
public Document[] parse(final AnchorURL location, final String mimeType, final String charset,
        final VocabularyScraper scraper, final int timezoneOffset, final InputStream source)
        throws Parser.Failure, InterruptedException {

    // check memory for parser
    if (!MemoryControl.request(200 * 1024 * 1024, false))
        throw new Parser.Failure("Not enough Memory available for pdf parser: " + MemoryControl.available(),
                location);//from  w  w  w .  jav  a2s  . c  o  m

    // create a pdf parser
    PDDocument pdfDoc;
    //final PDFParser pdfParser;
    try {
        Thread.currentThread().setPriority(Thread.MIN_PRIORITY); // the pdfparser is a big pain
        pdfDoc = PDDocument.load(source);
        //PDFParser pdfParser = new PDFParser(source);
        //pdfParser.parse();
        //pdfDoc = pdfParser.getPDDocument();
    } catch (final IOException e) {
        throw new Parser.Failure(e.getMessage(), location);
    } finally {
        Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
    }

    if (pdfDoc.isEncrypted()) {
        try {
            pdfDoc.openProtection(new StandardDecryptionMaterial(""));
        } catch (final BadSecurityHandlerException e) {
            try {
                pdfDoc.close();
            } catch (final IOException ee) {
            }
            throw new Parser.Failure("Document is encrypted (1): " + e.getMessage(), location);
        } catch (final IOException e) {
            try {
                pdfDoc.close();
            } catch (final IOException ee) {
            }
            throw new Parser.Failure("Document is encrypted (2): " + e.getMessage(), location);
        } catch (final CryptographyException e) {
            try {
                pdfDoc.close();
            } catch (final IOException ee) {
            }
            throw new Parser.Failure("Document is encrypted (3): " + e.getMessage(), location);
        }
        final AccessPermission perm = pdfDoc.getCurrentAccessPermission();
        if (perm == null || !perm.canExtractContent()) {
            try {
                pdfDoc.close();
            } catch (final IOException ee) {
            }
            throw new Parser.Failure("Document is encrypted and cannot be decrypted", location);
        }
    }

    // extracting some metadata
    PDDocumentInformation info = pdfDoc.getDocumentInformation();
    String docTitle = null, docSubject = null, docAuthor = null, docPublisher = null, docKeywordStr = null;
    Date docDate = new Date();
    if (info != null) {
        docTitle = info.getTitle();
        docSubject = info.getSubject();
        docAuthor = info.getAuthor();
        docPublisher = info.getProducer();
        if (docPublisher == null || docPublisher.isEmpty())
            docPublisher = info.getCreator();
        docKeywordStr = info.getKeywords();
        try {
            if (info.getModificationDate() != null)
                docDate = info.getModificationDate().getTime();
        } catch (IOException e) {
        }
        // unused:
        // info.getTrapped());
    }
    info = null;

    if (docTitle == null || docTitle.isEmpty()) {
        docTitle = MultiProtocolURL.unescape(location.getFileName());
    }
    if (docTitle == null) {
        docTitle = docSubject;
    }
    String[] docKeywords = null;
    if (docKeywordStr != null) {
        docKeywords = docKeywordStr.split(" |,");
    }

    Collection<AnchorURL>[] pdflinks = null;
    Document[] result = null;
    try {
        // get the links
        pdflinks = extractPdfLinks(pdfDoc);

        // get the fulltext (either per document or for each page)
        final PDFTextStripper stripper = new PDFTextStripper("UTF-8");

        if (individualPages) {
            // this is a hack which stores individual pages of the source pdf into individual index documents
            // the new documents will get a virtual link with a post argument page=X appended to the original url

            // collect text
            int pagecount = pdfDoc.getNumberOfPages();
            String[] pages = new String[pagecount];
            for (int page = 1; page <= pagecount; page++) {
                stripper.setStartPage(page);
                stripper.setEndPage(page);
                pages[page - 1] = stripper.getText(pdfDoc);
                //System.out.println("PAGE " + page + ": " + pages[page - 1]);
            }

            // create individual documents for each page
            assert pages.length == pdflinks.length : "pages.length = " + pages.length + ", pdflinks.length = "
                    + pdflinks.length;
            result = new Document[Math.min(pages.length, pdflinks.length)];
            String loc = location.toNormalform(true);
            for (int page = 0; page < result.length; page++) {
                result[page] = new Document(
                        new AnchorURL(loc + (loc.indexOf('?') > 0 ? '&' : '?') + individualPagePropertyname
                                + '=' + (page + 1)), // these are virtual new pages; we cannot combine them with '#' as that would be removed when computing the urlhash
                        mimeType, "UTF-8", this, null, docKeywords, singleList(docTitle), docAuthor,
                        docPublisher, null, null, 0.0f, 0.0f,
                        pages == null || page > pages.length ? new byte[0] : UTF8.getBytes(pages[page]),
                        pdflinks == null || page >= pdflinks.length ? null : pdflinks[page], null, null, false,
                        docDate);
            }
        } else {
            // collect the whole text at once
            final CharBuffer writer = new CharBuffer(odtParser.MAX_DOCSIZE);
            byte[] contentBytes = new byte[0];
            stripper.setEndPage(3); // get first 3 pages (always)
            writer.append(stripper.getText(pdfDoc));
            contentBytes = writer.getBytes(); // remember text in case of interrupting thread

            if (pdfDoc.getNumberOfPages() > 3) { // spare creating/starting thread if all pages read
                stripper.setStartPage(4); // continue with page 4 (terminated, resulting in no text)
                stripper.setEndPage(Integer.MAX_VALUE); // set to default
                // we start the pdf parsing in a separate thread to ensure that it can be terminated
                final PDDocument pdfDocC = pdfDoc;
                final Thread t = new Thread() {
                    @Override
                    public void run() {
                        Thread.currentThread().setName("pdfParser.getText:" + location);
                        try {
                            writer.append(stripper.getText(pdfDocC));
                        } catch (final Throwable e) {
                        }
                    }
                };
                t.start();
                t.join(3000); // pdfbox likes to forget to terminate ... (quite often)
                if (t.isAlive())
                    t.interrupt();
            }
            contentBytes = writer.getBytes(); // get final text before closing writer

            Collection<AnchorURL> pdflinksCombined = new HashSet<AnchorURL>();
            for (Collection<AnchorURL> pdflinksx : pdflinks)
                if (pdflinksx != null)
                    pdflinksCombined.addAll(pdflinksx);
            result = new Document[] { new Document(location, mimeType, "UTF-8", this, null, docKeywords,
                    singleList(docTitle), docAuthor, docPublisher, null, null, 0.0f, 0.0f, contentBytes,
                    pdflinksCombined, null, null, false, docDate) };
        }
    } catch (final Throwable e) {
        //close the writer (in finally)
        //throw new Parser.Failure(e.getMessage(), location);
    } finally {
        try {
            pdfDoc.close();
        } catch (final Throwable e) {
        }
    }

    // clear resources in pdfbox. they say that is resolved but it's not. see:
    // https://issues.apache.org/jira/browse/PDFBOX-313
    // https://issues.apache.org/jira/browse/PDFBOX-351
    // https://issues.apache.org/jira/browse/PDFBOX-441
    // the pdfbox still generates enormeous number of object allocations and don't delete these
    // the following Object are statically stored and never flushed:
    // COSFloat, COSArray, COSInteger, COSObjectKey, COSObject, COSDictionary,
    // COSStream, COSString, COSName, COSDocument, COSInteger[], COSNull
    // the great number of these objects can easily be seen in Java Visual VM
    // we try to get this shit out of the memory here by forced clear calls, hope the best the rubbish gets out.
    pdfDoc = null;
    clean_up_idiotic_PDFParser_font_cache_which_eats_up_tons_of_megabytes();

    return result;
}

From source file:neuralclassification.Classificator.java

String readText(String filepath, String name) {
    PDDocument pdfDocument = null;
    String paper = null;// w  w  w . j a va  2s  .c  o m
    try {
        pdfDocument = PDDocument.load(new File(filepath + "/" + name));
        PDFTextStripper stripper = new PDFTextStripper();
        paper = stripper.getText(pdfDocument);
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        if (pdfDocument != null)
            try {
                pdfDocument.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    }

    return paper;
}

From source file:neuralclassification.Trainer.java

String readText(String name) {
    PDDocument pdfDocument = null;
    String paper = null;/*ww  w.  j a  va2  s.c om*/
    try {
        pdfDocument = PDDocument.load(new File(filepath + "/" + name));
        PDFTextStripper stripper = new PDFTextStripper();
        paper = stripper.getText(pdfDocument);
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        if (pdfDocument != null)
            try {
                pdfDocument.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
    }

    return paper;
}

From source file:nominas.sei.form.Principal.java

private void ordenaNominas(String rutaEntrada, String rutaSalida) {
    ArrayList<PaginaNomina> paginasNomina = new ArrayList<PaginaNomina>();

    for (int x = 0; x < 1; x++) {//RECORREMOS EL ARREGLO CON LOS NOMBRES DE ARCHIVO

        try {//from w w  w  .ja va 2 s  .c  om
            PDDocument pd = PDDocument.load(rutaEntrada); //CARGAR EL PDF
            List l = pd.getDocumentCatalog().getAllPages();//NUMERO LAS PAGINAS DEL ARCHIVO
            Object[] obj = l.toArray();//METO EN UN OBJETO LA LISTA DE PAGINAS PARA MANIPULARLA
            for (int i = 0; i < l.size(); i++) {
                PDPage page = (PDPage) obj[i];//PAGE ES LA PAGINA 1 DE LA QUE CONSTA EL ARCHIVO
                PageFormat pageFormat = pd.getPageFormat(0);//PROPIEDADES DE LA PAGINA (FORMATO)
                Double d1 = new Double(pageFormat.getHeight());//ALTO
                Double d2 = new Double(pageFormat.getWidth());//ANCHO
                int width = d1.intValue();//ANCHO
                int eigth = 1024;//ALTO

                PDFTextStripperByArea stripper = new PDFTextStripperByArea();//COMPONENTE PARA ACCESO AL TEXTO
                Rectangle rect = new Rectangle(0, 0, width, eigth);//DEFNIR AREA DONDE SE BUSCARA EL TEXTO
                stripper.addRegion("area1", rect);//REGISTRAMOS LA REGION CON UN NOMBRE
                stripper.extractRegions(page);//EXTRAE TEXTO DEL AREA

                String contenido = new String();//CONTENIDO = A LO QUE CONTENGA EL AREA O REGION
                contenido = (stripper.getTextForRegion("area1"));
                String[] lines = contenido.split("[\\r\\n]+");
                String nombre = lines[1].substring(28, lines[1].length() - 10);//Separamos el nombre
                PaginaNomina nomina = new PaginaNomina(page, nombre);
                paginasNomina.add(nomina);
            }
            Collections.sort(paginasNomina);
            // Create a new empty document
            PDDocument document = new PDDocument();

            for (int i = 0; i < paginasNomina.size(); i++) {
                System.out.println(paginasNomina.get(i).getNombre());
                document.addPage(paginasNomina.get(i).getPagina());
            }
            // Save the newly created document
            document.save(rutaSalida);

            // finally make sure that the document is properly
            // closed.
            document.close();
            pd.close();//CERRAMOS OBJETO ACROBAT
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } //CATCH
    } //FOR
}

From source file:nominas.sei.NominasSEI.java

/**
 * @param args the command line arguments
 *//*from w  ww . j a v  a2  s .c om*/
public static void main(String[] args) {

    ArrayList<PaginaNomina> paginasNomina = new ArrayList<PaginaNomina>();

    for (int x = 0; x < 1; x++) {//RECORREMOS EL ARREGLO CON LOS NOMBRES DE ARCHIVO
        String ruta = new String();//VARIABLE QUE DETERMINARA LA RUTA DEL ARCHIVO A LEER.
        ruta = (".\\NOMINAS.pdf"); //SE ALMACENA LA RUTA DEL ARCHIVO A LEER. 

        try {
            PDDocument pd = PDDocument.load(ruta); //CARGAR EL PDF
            List l = pd.getDocumentCatalog().getAllPages();//NUMERO LAS PAGINAS DEL ARCHIVO
            Object[] obj = l.toArray();//METO EN UN OBJETO LA LISTA DE PAGINAS PARA MANIPULARLA
            for (int i = 0; i < l.size(); i++) {
                PDPage page = (PDPage) obj[i];//PAGE ES LA PAGINA 1 DE LA QUE CONSTA EL ARCHIVO
                PageFormat pageFormat = pd.getPageFormat(0);//PROPIEDADES DE LA PAGINA (FORMATO)
                Double d1 = new Double(pageFormat.getHeight());//ALTO
                Double d2 = new Double(pageFormat.getWidth());//ANCHO
                int width = d1.intValue();//ANCHO
                int eigth = 1024;//ALTO

                PDFTextStripperByArea stripper = new PDFTextStripperByArea();//COMPONENTE PARA ACCESO AL TEXTO
                Rectangle rect = new Rectangle(0, 0, width, eigth);//DEFNIR AREA DONDE SE BUSCARA EL TEXTO
                stripper.addRegion("area1", rect);//REGISTRAMOS LA REGION CON UN NOMBRE
                stripper.extractRegions(page);//EXTRAE TEXTO DEL AREA

                String contenido = new String();//CONTENIDO = A LO QUE CONTENGA EL AREA O REGION
                contenido = (stripper.getTextForRegion("area1"));
                String[] lines = contenido.split("[\\r\\n]+");
                String nombre = lines[1].substring(28, lines[1].length() - 10);
                PaginaNomina nomina = new PaginaNomina(page, nombre);
                paginasNomina.add(nomina);
            }
            Collections.sort(paginasNomina);
            // Create a new empty document
            PDDocument document = new PDDocument();

            for (int i = 0; i < paginasNomina.size(); i++) {
                System.out.println(paginasNomina.get(i).getNombre());
                document.addPage(paginasNomina.get(i).getPagina());
            }
            // Save the newly created document
            document.save("NominasOrdenadas.pdf");

            // finally make sure that the document is properly
            // closed.
            document.close();
            pd.close();//CERRAMOS OBJETO ACROBAT
        } catch (Exception e) {
            System.out.println(e.getMessage());
        } //CATCH
    } //FOR

}

From source file:noprint.NoPrint.java

/**
 * @param args the command line arguments
 * @throws IOException in case input file is can't be read or output written
 * @throws org.apache.pdfbox.pdmodel.encryption.BadSecurityHandlerException
 * @throws org.apache.pdfbox.exceptions.COSVisitorException
 *//* ww w.java2  s  . c o m*/
public static void main(String[] args) throws IOException, BadSecurityHandlerException, COSVisitorException {
    String infile = "input.pdf";
    String outfile = "output.pdf";

    String ownerPass = "";
    String userPass = "";
    /**
     * TODO: read up what the actual difference is between
     * userpassword and ownerpassword.
     */
    int keylength = 40;

    AccessPermission ap = new AccessPermission();
    PDDocument document = null;

    ap.setCanAssembleDocument(true);
    ap.setCanExtractContent(true);
    ap.setCanExtractForAccessibility(true);
    ap.setCanFillInForm(true);
    ap.setCanModify(true);
    ap.setCanModifyAnnotations(true);
    ap.setCanPrintDegraded(true);

    ap.setCanPrint(false);
    // YOU CAN'T PRINT
    // at least not when your PDFreader adheres to DRM (some don't)
    // also this is trivial to remove

    document = PDDocument.load(infile);

    if (!document.isEncrypted()) {
        StandardProtectionPolicy spp;
        spp = new StandardProtectionPolicy(ownerPass, userPass, ap);
        spp.setEncryptionKeyLength(keylength);
        document.protect(spp);
        document.save(outfile);
    }

    if (document != null) {
        document.close();
    }
}

From source file:nz.co.testamation.core.reader.pdf.PdfContentReaderImpl.java

License:Apache License

private String getPdfText(CloseableHttpResponse response) throws IOException {
    PDDocument load = PDDocument.load(response.getEntity().getContent());
    try {/*from  w ww  . j  a  va  2 s  . c  o m*/
        return new PDFTextStripper().getText(load).replaceAll("\\s+", " ");
    } finally {
        load.close();
    }
}

From source file:ocr_pdf.OCR_PDF.java

License:GNU General Public License

/**
 * Get PDFBox output of airport diagram.
 * @param fileName name of airport diagram PDF file.
 * @return text representation of airport diagram.
 *//*from   w w  w  . ja  v  a 2  s  .com*/
static String getTextPDFBox(String fileName) {
    PDFParser parser;
    String parsedText = null;
    PDFTextStripper pdfStripper = null;
    PDDocument pdDoc = null;
    COSDocument cosDoc = null;
    File file = new File(fileName);
    if (!file.isFile()) {
        System.err.println("File " + fileName + " does not exist.");
        return null;
    }
    try {
        parser = new PDFParser(new FileInputStream(file));
    } catch (IOException e) {
        System.err.println("Unable to open PDF Parser. " + e.getMessage());
        return null;
    }
    try {
        parser.parse();
        cosDoc = parser.getDocument();
        pdfStripper = new PDFTextStripper();
        //true doesn't work so well.
        pdfStripper.setSortByPosition(false);
        pdDoc = new PDDocument(cosDoc);
        parsedText = pdfStripper.getText(pdDoc);
    } catch (Exception e) {
        System.err.println("An exception occured in parsing the PDF Document." + e.getMessage());
    } finally {
        try {
            if (cosDoc != null)
                cosDoc.close();
            if (pdDoc != null)
                pdDoc.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    return parsedText;
}

From source file:openstitcher.core.PDFRenderer.java

License:Open Source License

public static void render(Design design, String location, ArrayList<LegendEntry> legend)
        throws IOException, COSVisitorException {
    PDDocument document = new PDDocument();
    PDPage page = new PDPage();
    document.addPage(page);/*  w  ww.ja v a 2s. co  m*/
    PDFont font = PDType1Font.HELVETICA;
    PDPageContentStream contentStream = new PDPageContentStream(document, page);

    float pageWidth = page.getMediaBox().getWidth();
    float pageHeight = page.getMediaBox().getHeight();

    float sideMargin = 70.0f;
    float capsMargin = 30.0f;

    // draw the document title
    contentStream.beginText();
    contentStream.setFont(font, 24.0f);
    contentStream.moveTextPositionByAmount(sideMargin, pageHeight - capsMargin - 24.0f);
    contentStream.drawString(design.title);
    contentStream.endText();

    // draw the document author
    contentStream.beginText();
    contentStream.setFont(font, 12.0f);
    contentStream.moveTextPositionByAmount(sideMargin, pageHeight - capsMargin - (24.0f + 12.0f));
    contentStream.drawString(design.author);
    contentStream.endText();

    // draw the document license
    contentStream.beginText();
    contentStream.setFont(font, 12.0f);
    contentStream.moveTextPositionByAmount(sideMargin, pageHeight - capsMargin - (24.0f + 12.0f + 12.0f));
    contentStream.drawString(design.license);
    contentStream.endText();

    // draw the physical size
    contentStream.beginText();
    contentStream.setFont(font, 12.0f);
    contentStream.moveTextPositionByAmount(pageWidth - sideMargin - 100.0f,
            pageHeight - capsMargin - (24.0f + 12.0f + 12.0f));
    contentStream.drawString(design.physicalSize);
    contentStream.endText();

    // draw the pattern
    float gridWidth = pageWidth - (sideMargin * 2);
    float widthPerCell = gridWidth / (float) design.pattern.getPatternWidth();
    float gridStartX = sideMargin;
    float gridStopX = sideMargin + (widthPerCell * design.pattern.getPatternWidth());
    float gridStartY = pageHeight - capsMargin - (24.0f + 12.0f + 12.0f + 12.0f);
    float gridStopY = (pageHeight - capsMargin - (24.0f + 12.0f + 12.0f + 12.0f))
            - (widthPerCell * design.pattern.getPatternHeight());

    // draw the pattern: background
    for (int i = 0; i < design.pattern.getPatternWidth(); i++) {
        for (int j = 0; j < design.pattern.getPatternHeight(); j++) {
            Yarn cellYarn = design.pattern.getPatternCell(i, j);
            if (cellYarn != null) {
                contentStream.setNonStrokingColor(cellYarn.color);
                contentStream.fillRect(gridStartX + (widthPerCell * i),
                        gridStartY - (widthPerCell * j) - widthPerCell, widthPerCell, widthPerCell);
            }

        }
    }

    // draw the pattern: grid outline
    contentStream.setStrokingColor(Color.black);
    for (int i = 0; i < design.pattern.getPatternWidth() + 1; i++) {
        // draw vertical lines
        float xCoord = gridStartX + (widthPerCell * i);
        if (i % 5 == 0) {
            contentStream.setLineWidth(2.0f);
        } else {
            contentStream.setLineWidth(1.0f);
        }
        contentStream.drawLine(xCoord, gridStartY, xCoord, gridStopY);
    }
    for (int i = 0; i < design.pattern.getPatternHeight() + 1; i++) {
        // draw horizontal lines
        float yCoord = gridStartY - (widthPerCell * i);
        if (i % 5 == 0) {
            contentStream.setLineWidth(2.0f);
        } else {
            contentStream.setLineWidth(1.0f);
        }
        contentStream.drawLine(gridStartX, yCoord, gridStopX, yCoord);
    }

    // draw the pattern: characters
    contentStream.setNonStrokingColor(Color.black);
    float centeringOffset = widthPerCell / 5.0f;
    for (int i = 0; i < design.pattern.getPatternWidth(); i++) {
        for (int j = 0; j < design.pattern.getPatternHeight(); j++) {
            Yarn cellYarn = design.pattern.getPatternCell(i, j);
            if (cellYarn != null) {
                int index = LegendEntry.findIndexByYarn(legend, cellYarn);
                if (index == -1) {
                    throw new RuntimeException("Cell did not exist in legend.");
                }
                contentStream.beginText();
                contentStream.setFont(font, widthPerCell);
                contentStream.moveTextPositionByAmount(gridStartX + (widthPerCell * i) + centeringOffset,
                        gridStartY - (widthPerCell * j) - widthPerCell + centeringOffset);
                contentStream.drawString(legend.get(index).character);
                contentStream.endText();
            }
        }
    }

    // draw the legend
    float legendWidth = pageWidth - (sideMargin * 2);
    float widthPerLegendCell = legendWidth / (float) legend.size();
    float legendStartX = sideMargin;
    float legendStopX = pageWidth - sideMargin;
    float legendStartY = capsMargin + 12.0f;
    float legendStopY = capsMargin;
    float legendCellPadding = 1.0f;
    float exampleCellWidth = 10.0f;

    for (int i = 0; i < legend.size(); i++) {
        // draw box
        contentStream.setNonStrokingColor(legend.get(i).yarn.color);
        contentStream.fillRect(legendStartX + legendCellPadding + (i * widthPerLegendCell),
                legendStopY + legendCellPadding, exampleCellWidth, exampleCellWidth);

        // draw character
        contentStream.beginText();
        contentStream.setNonStrokingColor(legend.get(i).fontColor);
        contentStream.setFont(font, 10.0f);
        contentStream.moveTextPositionByAmount(legendStartX + legendCellPadding + (i * widthPerLegendCell),
                legendStopY + legendCellPadding);
        contentStream.drawString(legend.get(i).character);
        contentStream.endText();

        // draw yarn name
        contentStream.beginText();
        contentStream.setNonStrokingColor(Color.black);
        contentStream.setFont(font, 10.0f);
        contentStream.moveTextPositionByAmount(legendStartX + legendCellPadding + exampleCellWidth
                + legendCellPadding + (i * widthPerLegendCell), legendStopY + legendCellPadding);
        contentStream.drawString(legend.get(i).yarn.name);
        contentStream.endText();
    }

    contentStream.close();
    document.save(location);
    document.close();
}