/* * Created on 06 mar 2016 * Copyright 2015 by Andrea Vacondio (andrea.vacondio@gmail.com). * This file is part of Sejda. * * Sejda is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Sejda is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with Sejda. If not, see <http://www.gnu.org/licenses/>. */ package org.sejda.impl.sambox.component; import static java.util.Objects.nonNull; import java.awt.Color; import java.awt.Point; import java.awt.geom.AffineTransform; import java.awt.geom.Point2D; import java.io.IOException; import java.util.List; import org.sejda.core.support.util.StringUtils; import org.sejda.impl.sambox.util.FontUtils; import org.sejda.model.HorizontalAlign; import org.sejda.model.VerticalAlign; import org.sejda.model.exception.TaskIOException; import org.sejda.model.exception.UnsupportedTextException; import org.sejda.sambox.pdmodel.PDDocument; import org.sejda.sambox.pdmodel.PDPage; import org.sejda.sambox.pdmodel.PDPageContentStream; import org.sejda.sambox.pdmodel.PDPageContentStream.AppendMode; import org.sejda.sambox.pdmodel.common.PDRectangle; import org.sejda.sambox.pdmodel.font.PDFont; import org.sejda.sambox.pdmodel.graphics.color.PDColor; import org.sejda.sambox.pdmodel.graphics.color.PDDeviceRGB; import org.sejda.sambox.pdmodel.graphics.state.RenderingMode; import org.sejda.sambox.util.Matrix; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Component capable of writing text to a pdf page * * @author Andrea Vacondio */ public class PageTextWriter { private static final Logger LOG = LoggerFactory.getLogger(PageTextWriter.class); private PDDocument document; // TODO define as a params member private static final Float DEFAULT_MARGIN = 30F; /** * @param document * the document where we want to write the footer */ public PageTextWriter(PDDocument document) { this.document = document; } public void write(PDPage page, HorizontalAlign hAlign, VerticalAlign vAlign, String rawLabel, PDFont font, Double fontSize, Color color) throws TaskIOException { try { String label = StringUtils.normalizeWhitespace(rawLabel); List<TextWithFont> resolvedStringsToFonts = FontUtils.resolveFonts(label, font, document); float stringWidth = 0.0f; for (TextWithFont stringAndFont : resolvedStringsToFonts) { String s = stringAndFont.getText(); PDFont f = stringAndFont.getFont(); stringWidth += f.getStringWidth(s) * fontSize.floatValue() / 1000f; } PDRectangle pageSize = page.getCropBox().rotate(page.getRotation()); Point2D position = new Point2D.Double(hAlign.position(pageSize.getWidth(), stringWidth, DEFAULT_MARGIN), vAlign.position(pageSize.getHeight(), DEFAULT_MARGIN - fontSize.floatValue())); write(page, position, label, font, fontSize, color); } catch (IOException e) { throw new TaskIOException("An error occurred writing the header or footer of the page.", e); } } public void write(PDPage page, Point2D position, String rawLabel, PDFont font, Double fontSize, Color color) throws TaskIOException { float[] components = new float[] { color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f }; PDColor pdColor = new PDColor(components, PDDeviceRGB.INSTANCE); write(page, position, rawLabel, font, fontSize, pdColor); } public void write(PDPage page, Point2D position, String rawLabel, PDFont font, Double fontSize, PDColor color) throws TaskIOException { write(page, position, rawLabel, font, fontSize, color, RenderingMode.FILL); } public void write(PDPage page, Point2D position, String rawLabel, PDFont font, Double fontSize, PDColor color, RenderingMode renderingMode) throws TaskIOException { String label = StringUtils.normalizeWhitespace(rawLabel); List<TextWithFont> resolvedStringsToFonts = FontUtils.resolveFonts(label, font, document); int offset = 0; PDRectangle pageSize = page.getMediaBox().rotate(page.getRotation()); // cropped docs have an offset between crop and media box that needs to be taken into account PDRectangle mediaSize = page.getMediaBox(); PDRectangle cropSize = page.getCropBox(); double cropOffsetX = cropSize.getLowerLeftX(); double cropOffsetY = cropSize.getLowerLeftY(); // adjust for rotation if (page.getRotation() == 90) { cropOffsetX = cropSize.getLowerLeftY(); cropOffsetY = mediaSize.getUpperRightX() - cropSize.getUpperRightX(); } else if (page.getRotation() == 180) { cropOffsetX = mediaSize.getUpperRightX() - cropSize.getUpperRightX(); cropOffsetY = mediaSize.getUpperRightY() - cropSize.getUpperRightY(); } else if (page.getRotation() == 270) { cropOffsetX = mediaSize.getUpperRightY() - cropSize.getUpperRightY(); cropOffsetY = cropSize.getLowerLeftX(); } LOG.trace("media: {} crop: {}", mediaSize, cropSize); LOG.trace("offsets: {}, {} and rotation", cropOffsetX, cropOffsetY, page.getRotation()); position = new Point((int) position.getX() + (int) cropOffsetX, (int) position.getY() + (int) cropOffsetY); try (PDPageContentStream contentStream = new PDPageContentStream(document, page, AppendMode.APPEND, true, true)) { contentStream.beginText(); contentStream.setTextRenderingMode(renderingMode); contentStream.setNonStrokingColor(color); for (TextWithFont stringAndFont : resolvedStringsToFonts) { try { PDFont resolvedFont = stringAndFont.getFont(); String resolvedLabel = stringAndFont.getText(); double resolvedFontSize = fontSize; if (resolvedFont == null) { throw new UnsupportedTextException("Unable to find suitable font for string \"" + StringUtils.asUnicodes(resolvedLabel) + "\"", resolvedLabel); } // when switching from one font to the other (eg: some letters aren't supported by the original font) // letter size might vary. try to find the best fontSize for the new font so that it matches the height of // the previous letter if (resolvedFont != font) { // resolvedFontSize = resolvedFontSize(font, fontSize, stringAndFont); } Point2D resolvedPosition = new Point((int) position.getX() + offset, (int) position.getY()); contentStream.setFont(resolvedFont, (float) resolvedFontSize); if (page.getRotation() > 0) { LOG.trace("Unrotated position {}", resolvedPosition); Point2D rotatedPosition = findPositionInRotatedPage(page.getRotation(), pageSize, resolvedPosition); LOG.trace("Will write string '{}' using font {} at position {}", resolvedLabel, resolvedFont.getName(), rotatedPosition); AffineTransform tx = AffineTransform.getTranslateInstance(rotatedPosition.getX(), rotatedPosition.getY()); tx.rotate(Math.toRadians(page.getRotation())); contentStream.setTextMatrix(new Matrix(tx)); } else { LOG.trace("Will write string '{}' using font {} at position {}", resolvedLabel, resolvedFont.getName(), resolvedPosition); contentStream.setTextMatrix(new Matrix(AffineTransform .getTranslateInstance(resolvedPosition.getX(), resolvedPosition.getY()))); } LOG.trace("Text position {}", resolvedPosition); contentStream.showText(resolvedLabel); // sometimes the string width is reported incorrectly, too small. when writing ' ' (space) it leads to missing spaces. // use the largest value between font average width and text string width double textWidth = Math.max(resolvedFont.getAverageFontWidth(), resolvedFont.getStringWidth(resolvedLabel)) / 1000 * fontSize; offset += textWidth; } catch (IOException e) { throw new TaskIOException("An error occurred writing text to the page.", e); } } contentStream.setTextRenderingMode(RenderingMode.FILL); contentStream.endText(); } catch (IOException e) { throw new TaskIOException("An error occurred writing the header or footer of the page.", e); } } // private double resolvedFontSize(PDFont font, Double fontSize, TextWithFont stringAndFont) { // if (nonNull(font.getFontDescriptor()) && nonNull(stringAndFont.getFont().getFontDescriptor())) { // try { // if (font.getFontDescriptor().getFontBoundingBox() != null // && stringAndFont.getFont().getFontDescriptor().getFontBoundingBox() != null) { // double desiredLetterHeight = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 // * fontSize; // double actualLetterHeight = FontUtils.calculateBBoxHeight(stringAndFont.getText(), // stringAndFont.getFont()) / 1000 * fontSize; // double resolvedFontSize = fontSize / (actualLetterHeight / desiredLetterHeight); // LOG.debug( // "Fallback font size calculation: desired vs actual heights: {} vs {}, original vs calculated font size: {} vs {}", // desiredLetterHeight, actualLetterHeight, fontSize, resolvedFontSize); // return resolvedFontSize; // } // } catch (Exception e) { // LOG.warn("Could not calculate fallback font size", e); // } // } // return fontSize; // } /** * Calculates the string's width. * * @throws TaskIOException */ public int getStringWidth(String rawLabel, PDFont font, float fontSize) throws TaskIOException { String label = StringUtils.normalizeWhitespace(rawLabel); List<TextWithFont> resolvedStringsToFonts = FontUtils.resolveFonts(label, font, document); int offset = 0; for (TextWithFont stringAndFont : resolvedStringsToFonts) { try { PDFont resolvedFont = stringAndFont.getFont(); if (nonNull(resolvedFont)) { // sometimes the string width is reported incorrectly, too small. when writing ' ' (space) it leads to missing spaces. // use the largest value between font average width and text string width double textWidth = Math.max(resolvedFont.getAverageFontWidth(), resolvedFont.getStringWidth(stringAndFont.getText())) / 1000 * fontSize; offset += textWidth; } } catch (IOException e) { throw new TaskIOException("An error occurred writing text to the page.", e); } } return offset; } private Point2D findPositionInRotatedPage(int rotation, PDRectangle pageSize, Point2D position) { LOG.debug("Found rotation {}", rotation); // flip AffineTransform transform = AffineTransform.getScaleInstance(1, -1); if (rotation == 90) { transform.translate(pageSize.getHeight(), 0); } if (rotation == 180) { transform.translate(pageSize.getWidth(), -pageSize.getHeight()); } if (rotation == 270) { transform.translate(0, -pageSize.getWidth()); } transform.rotate(Math.toRadians(-rotation)); // flip transform.scale(1, -1); return transform.transform(position, null); } }