blob: e3285d53288cbc72cb2e23ff2299206ce6d0a29f [file] [log] [blame]
/*
* Copyright 2015 The Apache Software Foundation.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.pdfbox.debugger.pagepane;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.AffineTransform;
import java.awt.geom.GeneralPath;
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 org.apache.fontbox.util.BoundingBox;
import org.apache.pdfbox.pdmodel.PDDocument;
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.PDType3CharProc;
import org.apache.pdfbox.pdmodel.font.PDType3Font;
import org.apache.pdfbox.pdmodel.font.PDVectorFont;
import org.apache.pdfbox.pdmodel.interactive.pagenavigation.PDThreadBead;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.pdfbox.text.TextPosition;
import org.apache.pdfbox.util.Matrix;
import org.apache.pdfbox.util.Vector;
/**
* Draws an overlay showing the locations of text found by PDFTextStripper and another heuristic.
*
* @author Ben Litchfield
* @author Tilman Hausherr
* @author John Hewson
*/
final class DebugTextOverlay
{
private final PDDocument document;
private final int pageIndex;
private final float scale;
private final boolean showTextStripper;
private final boolean showTextStripperBeads;
private final boolean showFontBBox;
private final boolean showGlyphBounds;
private class DebugTextStripper extends PDFTextStripper
{
private final Graphics2D graphics;
private AffineTransform flip;
DebugTextStripper(Graphics2D graphics) throws IOException
{
this.graphics = graphics;
}
public void stripPage(PDDocument document, PDPage page, int pageIndex, float scale) throws IOException
{
// flip y-axis
PDRectangle cropBox = page.getCropBox();
this.flip = new AffineTransform();
flip.translate(0, cropBox.getHeight());
flip.scale(1, -1);
// scale and rotate
transform(graphics, page, scale);
// set stroke width
graphics.setStroke(new BasicStroke(0.5f));
setStartPage(pageIndex + 1);
setEndPage(pageIndex + 1);
Writer dummy = new OutputStreamWriter(new ByteArrayOutputStream());
writeText(document, dummy);
if (DebugTextOverlay.this.showTextStripperBeads)
{
// beads in green
List<PDThreadBead> pageArticles = page.getThreadBeads();
for (PDThreadBead bead : pageArticles)
{
if (bead == null)
{
continue;
}
PDRectangle r = bead.getRectangle();
GeneralPath p = r.transform(Matrix.getTranslateInstance(-cropBox.getLowerLeftX(), cropBox.getLowerLeftY()));
Shape s = flip.createTransformedShape(p);
graphics.setColor(Color.green);
graphics.draw(s);
}
}
}
// scale rotate translate
private void transform(Graphics2D graphics, PDPage page, float scale)
{
graphics.scale(scale, scale);
int rotationAngle = page.getRotation();
PDRectangle cropBox = page.getCropBox();
if (rotationAngle != 0)
{
float translateX = 0;
float translateY = 0;
switch (rotationAngle)
{
case 90:
translateX = cropBox.getHeight();
break;
case 270:
translateY = cropBox.getWidth();
break;
case 180:
translateX = cropBox.getWidth();
translateY = cropBox.getHeight();
break;
default:
break;
}
graphics.translate(translateX, translateY);
graphics.rotate((float) Math.toRadians(rotationAngle));
}
}
@Override
protected void writeString(String string, List<TextPosition> textPositions) throws IOException
{
for (TextPosition text : textPositions)
{
if (DebugTextOverlay.this.showTextStripper)
{
AffineTransform at = (AffineTransform) flip.clone();
at.concatenate(text.getTextMatrix().createAffineTransform());
// in red:
// show rectangles with the "height" (not a real height, but used for text extraction
// heuristics, it is 1/2 of the bounding box height and starts at y=0)
Rectangle2D.Float rect = new Rectangle2D.Float(0, 0,
text.getWidthDirAdj() / text.getTextMatrix().getScalingFactorX(),
text.getHeightDir() / text.getTextMatrix().getScalingFactorY());
graphics.setColor(Color.red);
graphics.draw(at.createTransformedShape(rect));
}
if (DebugTextOverlay.this.showFontBBox)
{
// in blue:
// show rectangle with the real vertical bounds, based on the font bounding box y values
// usually, the height is identical to what you see when marking text in Adobe Reader
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 rect = new Rectangle2D.Float(0, bbox.getLowerLeftY(), xadvance, bbox.getHeight());
// glyph space -> user space
// note: text.getTextMatrix() is *not* the Text Matrix, it's the Text Rendering Matrix
AffineTransform at = (AffineTransform) flip.clone();
at.concatenate(text.getTextMatrix().createAffineTransform());
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);
}
graphics.setColor(Color.blue);
graphics.draw(at.createTransformedShape(rect));
}
}
}
@Override
protected void showGlyph(Matrix textRenderingMatrix, PDFont font, int code, Vector displacement) throws IOException
{
super.showGlyph(textRenderingMatrix, font, code, displacement);
if (!DebugTextOverlay.this.showGlyphBounds)
{
return;
}
AffineTransform at = textRenderingMatrix.createAffineTransform();
Shape bbox = calculateGlyphBounds(at, font, code, displacement);
if (bbox == null)
{
return;
}
Shape transformedBBox = flip.createTransformedShape(bbox);
// save
Color color = graphics.getColor();
Stroke stroke = graphics.getStroke();
Shape clip = graphics.getClip();
// draw
graphics.setClip(graphics.getDeviceConfiguration().getBounds());
graphics.setColor(Color.cyan);
graphics.setStroke(new BasicStroke(.5f));
graphics.draw(transformedBBox);
// restore
graphics.setStroke(stroke);
graphics.setColor(color);
graphics.setClip(clip);
}
private Shape calculateGlyphBounds(
AffineTransform at, PDFont font, int code,Vector displacement) throws IOException
{
at.concatenate(font.getFontMatrix().createAffineTransform());
// compute glyph path
GeneralPath path;
if (font instanceof PDType3Font)
{
// It is difficult to calculate the real individual glyph bounds for type 3 fonts
// because these are not vector fonts, the content stream could contain almost anything
// that is found in page content streams.
PDType3Font t3Font = (PDType3Font) font;
PDType3CharProc charProc = t3Font.getCharProc(code);
if (charProc == null)
{
return null;
}
BoundingBox fontBBox = t3Font.getBoundingBox();
PDRectangle glyphBBox = charProc.getGlyphBBox();
if (glyphBBox == null)
{
return null;
}
// PDFBOX-3850: glyph bbox could be larger than the font bbox
glyphBBox.setLowerLeftX(Math.max(fontBBox.getLowerLeftX(), glyphBBox.getLowerLeftX()));
glyphBBox.setLowerLeftY(Math.max(fontBBox.getLowerLeftY(), glyphBBox.getLowerLeftY()));
glyphBBox.setUpperRightX(Math.min(fontBBox.getUpperRightX(), glyphBBox.getUpperRightX()));
glyphBBox.setUpperRightY(Math.min(fontBBox.getUpperRightY(), glyphBBox.getUpperRightY()));
path = glyphBBox.toGeneralPath();
}
else
{
PDVectorFont vectorFont = (PDVectorFont) font;
path = vectorFont.getNormalizedPath(code);
if (path == null)
{
return null;
}
// stretch non-embedded glyph if it does not match the width contained in the PDF
if (!font.isEmbedded() && !font.isVertical() && !font.isStandard14() && font.hasExplicitWidth(code))
{
float fontWidth = font.getWidthFromFont(code);
if (fontWidth > 0 && // ignore spaces
Math.abs(fontWidth - displacement.getX() * 1000) > 0.0001)
{
float pdfWidth = displacement.getX() * 1000;
at.scale(pdfWidth / fontWidth, 1);
}
}
}
// compute visual bounds
return at.createTransformedShape(path.getBounds2D());
}
}
DebugTextOverlay(PDDocument document, int pageIndex, float scale,
boolean showTextStripper, boolean showTextStripperBeads,
boolean showFontBBox, boolean showGlyphBounds)
{
this.document = document;
this.pageIndex = pageIndex;
this.scale = scale;
this.showTextStripper = showTextStripper;
this.showTextStripperBeads = showTextStripperBeads;
this.showFontBBox = showFontBBox;
this.showGlyphBounds = showGlyphBounds;
}
public void renderTo(Graphics2D graphics) throws IOException
{
DebugTextStripper stripper = new DebugTextStripper(graphics);
stripper.stripPage(this.document, this.document.getPage(pageIndex), this.pageIndex, this.scale);
}
}