EDIT in bold
I have a table where several cells contain Paragraphs with some arbitrarily long text. The table width is determined via useAllAvailableWidth, and I am also invoking setAutoLayout.
I am using the Renderer framework to set the Paragraph's font size to the largest possible value without clipping off any content.
In particular I wish to achieve similar results to iText Maximum Font Size, however that questions was written for itext5. I am using itext7.
I have read this sample, and I have come up with the following (partial) solution thanks to a previous answer:
class FontSizeRenderer(val content: Paragraph) : ParagraphRenderer(content) {
override fun getNextRenderer() = FontSizeRenderer(content)
override fun layout(layoutContext: LayoutContext?): LayoutResult {
val currentFontSize = content.getProperty<UnitValue>(Property.FONT_SIZE).value
return layoutBinarySearch(layoutContext, 1f, currentFontSize, 20)
}
private tailrec fun layoutBinarySearch(layoutContext: LayoutContext?, minFontSize: Float, maxFontSize: Float, iterationThreshold: Int): LayoutResult {
val currentLayout = super.layout(layoutContext)
if (iterationThreshold <= 0) {
return currentLayout
}
val currentFontSize = content.getProperty<UnitValue>(Property.FONT_SIZE).value
return if (currentLayout.status == LayoutResult.FULL) {
val increment = (currentFontSize + maxFontSize) / 2
content.setFontSize(increment)
layoutBinarySearch(layoutContext, currentFontSize, maxFontSize, iterationThreshold - 1)
} else {
val decrement = (minFontSize + currentFontSize) / 2
content.setFontSize(decrement)
layoutBinarySearch(layoutContext, minFontSize, currentFontSize, iterationThreshold - 1)
}
}
}
When using this renderer in a fully-fledged table, it "kinda works" in a sense that it starts to recurse but it stops too early.
The expected output string in the bottom cell on the first page is Scramble: R' U' F R F2 D2 R' B2 U2 R F2 R' B2 R' B F U' L2 B' R' B' U' R F2 R' U' F. The full code sample can be inspected (and downloaded) at this repository, under webscrambles/src/main/kotlin/org/worldcubeassociation/tnoodle/server/webscrambles/pdf, files FmcSolutionSheet.kt and util/FooRenderer.kt.
How can I adjust my Renderer to prevent the Cell from overflowing?
Having a layout element you can create a renderer subtree out of it and try to lay it out in a given area. Then depending on whether the element fit completely in the area or there was not enough space you can increase or decrease your font size and try again.
To test out different font sizes, binary search is our best friend and allows a giant speed up.
First off, a helper function to set the font size of the element recursively:
private void setFontSizeRecursively(IElement element, float size) {
element.setProperty(Property.FONT_SIZE, UnitValue.createPointValue(size));
if (element instanceof com.itextpdf.layout.element.AbstractElement) {
for (Object child : ((AbstractElement) element).getChildren()) {
setFontSizeRecursively((IElement) child, size);
}
}
}
Now the meaty part of the code. In the binary search body we scale all the font sizes of the elements and emulate layout to see if our root element (table) fits into the given area. Then we shift the left or right search boundary and try again until we converge. You can tweak the number of iterations (set at 20 in the code) to your own taste for the accuracy/speed trade-off. You can also tweak left and right bounds of the binary search if you can make such judgment depending on the expected input. Finally, we set the font size scale we converged at and do the final layout.
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName));
Document document = new Document(pdfDocument);
Table table = new Table(UnitValue.createPercentArray(new float[] {20, 40, 40}));
for (int i = 0; i < 9; i++) {
table.addCell(new Cell().add(new Paragraph("Hello random text")));
}
float lFontSize = 1f;
float rFontSize = 100f;
float desiredWidth = 300;
float desiredHeight = 400;
table.setWidth(UnitValue.createPointValue(desiredWidth));
for (int i = 0; i < 20; i++) {
float midFontSize = (lFontSize + rFontSize) / 2;
setFontSizeRecursively(table, midFontSize);
IRenderer tableRenderer = table.createRendererSubTree().setParent(document.getRenderer());
LayoutResult result = tableRenderer.layout(new LayoutContext(new LayoutArea(1,
new Rectangle(0, 0, desiredWidth, desiredHeight)))); // You can tweak desired size to fit the table in
if (result.getStatus() == LayoutResult.FULL) {
lFontSize = midFontSize;
} else {
rFontSize = midFontSize;
}
}
setFontSizeRecursively(table, lFontSize);
document.add(table);
document.close();
Related
I am using PdfExplicitDestination as a page number, for titles by reading the existing pdf content from the page,
but I need to point the focus on specific text content while click on the bookmark.
for (int page = 1; page <= pdf.getNumberOfPages(); page++) {
ITextExtractionStrategy strategy = new SimpleTextExtractionStrategy();
String currentText = PdfTextExtractor.getTextFromPage(pdf.getPage(page), strategy);
if (currentText.contains("title")) {
k.addDestination(PdfExplicitDestination.createXYZ(pdf.getPage(page), pdf.getPage(page).getPageSize().getLeft(), pdf.getPage(page).getPageSize().getTop(), 0));
//System.out.println(currentText);
}
}
I need to find the position of the title in the pdf page to set "float top" value.
PdfExplicitDestination.createXYZ(pageNum, left, top, zoom)
Can any one please help to get it from the existing content in the pdf.
This task can be approached in a number of ways. One of the way is to go over page content in "stripes" (rectangles with small height), and only consider content from such a small rectangle at a time. If you find a text piece in such rectangle then you know that somewhere between upper and lower bound of Y position given by the rectangle coordinates lies the desired text content. You can e.g. create the destination to point to the topmost coordinate in that case - it might be a bit above the desired text but the difference will be small depending on the rectangle height you select.
The following code snipped contains example implementation of the presented idea. There are two parameters - windowHeight which must be tall enough to fit a piece of content you are looking for, but the smaller this variable is the better accuracy you get in the result. Parameter step defines how many such rectangles of height windowHeight we will try on each page. The smaller the parameter is the better accuracy you get, but bigger parameter values optimize performance. Up to a specific use case to tweak those trade-offs.
final float windowHeight = 30;
final float step = 10;
for (int page = 1; page <= pdf.getNumberOfPages(); page++) {
Rectangle pageSize = pdf.getPage(page).getPageSize();
for (float upperPoint = pageSize.getHeight(); upperPoint > 0; upperPoint -= step) {
IEventFilter filter = new TextRegionEventFilter(new Rectangle(0, upperPoint - windowHeight, pageSize.getWidth(), windowHeight));
LocationTextExtractionStrategy strategy = new LocationTextExtractionStrategy();
FilteredTextEventListener listener = new FilteredTextEventListener(strategy, filter);
new PdfCanvasProcessor(listener).processPageContent(pdf.getPage(page));
if (strategy.getResultantText().contains("title")) {
float top = upperPoint; // This is the topmost point of the rectangle
break; // Break here not to capture same text twice
}
}
}
sorry,you may not understand my problems,because i am not good at english.
i want to add some labels at the top and bottom of pdf,but the label position can set minus.if i set a minus,i should make heigh larger to set label.
i got help from How to resize existing pdf page size
to change my pdf pagesize.then i encountered another problem,when i set lly as a
minus, my text was truncated,then i want to add bottom length to top, but i do not know how to move the text up to make the text in center.
key codes
float newTop = rectangle.getTop();
if (printSet.getHeaderMargins() < 0) {
newTop += height2Offset(PrintSet.defaultMargins - printSet.getHeaderMargins());
headMargins = height2Offset(PrintSet.defaultMargins);
}
if (printSet.getFooterMargins() < 0) {
newTop += height2Offset(PrintSet.defaultMargins - printSet.getFooterMargins());
footMargins = height2Offset(PrintSet.defaultMargins);
}
float[] newBoxValues = new float[] {
rectangle.getLeft(),
rectangle.getBottom(),
rectangle.getRight(),
newTop
};
PdfArray newBox = new PdfArray(newBoxValues);
PdfDictionary pageDict = reader.getPageN(page + 1);
pageDict.put(PdfName.CROPBOX, newBox);
pageDict.put(PdfName.MEDIABOX, newBox);
Currently, you are defining a new page size like this:
float[] newBoxValues = new float[] {
rectangle.getLeft(),
rectangle.getBottom(),
rectangle.getRight(),
newTop
};
This creates a bigger rectangle, but that rectangle only expands the page towards the top.
I think you should create the new rectangle like this:
float[] newBoxValues = new float[] {
rectangle.getLeft(),
rectangle.getBottom() - extramarginBottom,
rectangle.getRight(),
rectangle.getTop() + extramarginTop
};
I can't help you define the value of extramarginBottom and extramarginTop because I'm not sure what your height2Offset() method is supposed to do, nor what PrintSet.defaultMargins, printSet.getHeaderMargins(), and printSet.getFooterMargins() are about.
Basically, extramarginTop is the height to be added at the top, whereas extramarginBottom is the height to be added at the bottom:
I have a small problem and I'm trying for some time to find out a solution.
Long story short I have to remove the top part of each page from a pdf with itext. I managed to do this with CROPBOX, but the problem is that this will make the pages smaller by removing the top part.
Can someone help me to implement this so the page size remains the same. My idea would be to override the top page with a white rectangle, but after many tries I didn't manage to do this.
This is the current code I'm using to crop the page.
PdfRectangle rect = new PdfRectangle(55, 0, 1000, 1000);
PdfDictionary pageDict;
for (int curentPage = 2; curentPage <= pdfReader.getNumberOfPages(); curentPage++) {
pageDict = pdfReader.getPageN(curentPage);
pageDict.put(PdfName.CROPBOX, rect);
}
In your code sample, you are cropping the pages. This reduces the visible size of the page.
Based on your description, you don't want cropping. Instead you want clipping.
I've written an example that clips the content of all pages of a PDF by introducing a margin of 200 user units (that's quite a margin). The example is called ClipPdf and you can see a clipped page here: hero_clipped.pdf (the iText superhero has lost arms, feet and part of his head in the clipping process.)
public void manipulatePdf(String src, String dest) throws IOException, DocumentException {
PdfReader reader = new PdfReader(src);
PdfStamper stamper = new PdfStamper(reader, new FileOutputStream(dest));
int n = reader.getNumberOfPages();
PdfDictionary page;
PdfArray media;
for (int p = 1; p <= n; p++) {
page = reader.getPageN(p);
media = page.getAsArray(PdfName.CROPBOX);
if (media == null) {
media = page.getAsArray(PdfName.MEDIABOX);
}
float llx = media.getAsNumber(0).floatValue() + 200;
float lly = media.getAsNumber(1).floatValue() + 200;
float w = media.getAsNumber(2).floatValue() - media.getAsNumber(0).floatValue() - 400;
float h = media.getAsNumber(3).floatValue() - media.getAsNumber(1).floatValue() - 400;
String command = String.format(
"\nq %.2f %.2f %.2f %.2f re W n\nq\n",
llx, lly, w, h);
stamper.getUnderContent(p).setLiteral(command);
stamper.getOverContent(p).setLiteral("\nQ\nQ\n");
}
stamper.close();
reader.close();
}
Obviously, you need to study this code before using it. Once you understand this code, you'll know that this code will only work for pages that aren't rotated. If you understand the code well, you should have no problem adapting the example for rotated pages.
Update
The re operator constructs a rectangle. It takes four parameters (the values preceding the operator) that define a rectangle: the x coordinate of the lower-left corner, the y coordinate of the lower-left corner, the width and the height.
The W operator sets the clipping path. We have just drawn a rectangle; this rectangle will be used to clip the content that follows.
The n operator starts a new path. It discards the paths we've constructed so far. In this case, it prevents that the rectangle we have drawn (and that we use as clipping path) is actually drawn.
The q and Q operators save and restore the graphics state stack, but that's rather obvious.
All of this is explained in ISO-32000-1 (available online if you Google well) and in the book The ABC of PDF.
There is a method in PDFBox's font class, PDFont, named getFontHeight which sounds simple enough. However I don't quite understand the documentation and what the parameters stand for.
getFontHeight
This will get the font width for a character.
Parameters:
c - The character code to get the width for.
offset - The offset into the array. length
The length of the data.
Returns: The width is in 1000 unit of text space, ie 333 or 777
Is this method the right one to use to get the height of a character in PDFBox and if so how? Is it some kind of relationship between font height and font size I can use instead?
I believe the answer marked right requires some additional clarification. There are no "error" per font for getHeight() and hence I believe it is not a good practice manually guessing the coefficient for each new font.
Guess it could be nice for your purposes simply use CapHeight instead of Height.
float height = ( font.getFontDescriptor().getCapHeight()) / 1000 * fontSize;
That will return the value similar to what you are trying to get by correcting the Height with 0.865 for Helvetica. But it will be universal for any font.
PDFBox docs do not explain too much what is it. But you can look at the image in the wikipedia Cap_height article to understand better how it is working and choose the parameter fit to your particular task.
https://en.wikipedia.org/wiki/Cap_height
EDIT: Cap height was what I was looking for. See the accepted answer.
After digging through the source of PDFBox I found that this should do the trick of calculating the font height.
int fontSize = 14;
PDFont font = PDType1Font.HELVETICA;
font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize
The method isn't perfect though. If you draw a rectangle with the height 200 and a Y with the font size 200 you get the font height 231.2 calculated with the above method even though it actually is printed smaller then the rectangle.
Every font has a different error but with helvetica it is close to 13.5 precent too much independently of font size. Therefore, to get the right font height for helvetica this works...
font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize * 0.865
Maybe use this?
http://pdfbox.apache.org/apidocs/org/apache/pdfbox/util/TextPosition.html
Seems like a wrap-around util for text. I haven't looked in the source if it accounts for font error though.
this is a working method for splitting the text and finding the height
public float heightForWidth(float width) throws IOException {
float height = 0;
String[] split = getTxt().split("(?<=\\W)");
int[] possibleWrapPoints = new int[split.length];
possibleWrapPoints[0] = split[0].length();
for (int i = 1; i < split.length; i++) {
possibleWrapPoints[i] = possibleWrapPoints[i - 1] + split[i].length();
}
float leading = font.getFontDescriptor().getFontBoundingBox().getHeight() / 1000 * fontSize;
int start = 0;
int end = 0;
for (int i : possibleWrapPoints) {
float w = font.getStringWidth(getTxt().substring(start, i)) / 1000 * fontSize;
if (start < end && w > width) {
height += leading;
start = end;
}
end = i;
}
height += leading;
return height + 3;
}
For imported True Type Fonts the total height of the font is
(org.apache.pdfbox.pdmodel.font.PDFont.getFontDescriptor().getDescent() + org.apache.pdfbox.pdmodel.font.PDFont.getFontDescriptor().getAscent() + org.apache.pdfbox.pdmodel.font.PDFont.getFontDescriptor().getLeading()) * point size * org.apache.pdfbox.pdmodel.font.PDFont.getFontMatrix().getValue(0, 0)
You will find that font.getFontDescriptor().getFontBoundingBox().getHeight() is 20% larger than the above value as it includes a 20% leading on the above value, but if you take the top value and remove 20%, the font will be right next too each other
I have 2 Mat objects, overlay and background.
How do I put my overlay Mat on top of my background Mat such that only the non-transparent pixels of the overlay Mat completely obscures the background Mat?
I have tried addWeighted() which combines the 2 Mat but both "layers" are still visible.
The overlay Mat has a transparent channel while the background Mat does not.
The pixel in the overlay Mat is either completely transparent or fully obscure.
Both Mats are of the same size.
The function addWeighted won't work since it will use the same alpha value to all the pixels. To do exactly what you are saying, to only replace the non transparent values in the background, you can create a small function for that, like this:
cv::Mat blending(cv::Mat& overlay, cv::Mat& background){
//must have same size for this to work
assert(overlay.cols == background.cols && overlay.rows == background.rows);
cv::Mat result = background.clone();
for (int i = 0; i < result.rows; i++){
for (int j = 0; j < result.cols; j++){
cv::Vec4b pix = overlay.at<cv::Vec4b>(i,j);
if (pix[3] == 0){
result.at<cv::Vec3b>(i,j) = cv::Vec3b(pix[0], pix[1], pix[2]);
}
}
}
return result;
}
I am not sure if the transparent value in opencv is 0 or 255, so change it accordingly.... I think it is 0 for non-transparent adn 255 for fully transparent.
If you want to use the value of the alpha channel as a rate to blend, then change it a little to this:
cv::Mat blending(cv::Mat& overlay, cv::Mat& background){
//must have same size for this to work
assert(overlay.cols == background.cols && overlay.rows == background.rows);
cv::Mat result = background.clone();
for (int i = 0; i < result.rows; i++){
for (int j = 0; j < result.cols; j++){
cv::Vec4b pix = overlay.at<cv::Vec4b>(i,j);
double alphaRate = 1.0 - pix[3]/255.0;
result.at<cv::Vec3b>(i,j) = (1.0 - alphaRate) * cv::Vec3b(pix[0], pix[1], pix[2]) + result.at<cv::Vec3b>(i,j) * alphaRate;
}
}
return result;
}
Sorry for the code being in C++ and not in JAVA, but I think you can get an idea. Basically is just a loop in the pixels and changing the pixels in the copy of background to those of the overlay if they are not transparent.
* EDIT *
I will answer your comment with this edit, since it may take space. The problem is how OpenCV matrix works. For an image with alpha, the data is organized as an array like BGRA BGRA .... BGRA, and the basic operations like add, multiply and so on work in matrices with the same dimensions..... you can always try to separate the matrix with split (this will re write the matrix so it may be slow), then change the alpha channel to double (again, rewrite) and then do the multiplication and adding of the matrices. It should be faster since OpenCV optimizes these functions.... also you can do this in GPU....
Something like this:
cv::Mat blending(cv::Mat& overlay, cv::Mat& background){
std::vector<cv::Mat> channels;
cv::split(overlay, channels);
channels[3].convertTo(channels[3], CV_64F, 1.0/255.0);
cv::Mat newOverlay, result;
cv::merge(channels, newOverlay);
result = newOverlay * channels[3] + ((1 - channels[3]) * background);
return result;
}
Not sure if OpenCV allows a CV_8U to multiply a CV_64F, or if this will be faster or not.... but it may be.
Also, the ones with loops has no problem in threads, so it can be optimized... running this in release mode will greatly increase the speed too since the .at function of OpenCV does several asserts.... that in release mode are not done. Not sure if this can be change in JAVA though...
I was able to port api55's edited answer for java:
private void merge(Mat background, Mat overlay) {
List<Mat> backgroundChannels = new ArrayList<>();
Core.split(background, backgroundChannels);
List<Mat> overlayChannels = new ArrayList<>();
Core.split(overlay, overlayChannels);
// compute "alphaRate = 1 - overlayAlpha / 255"
Mat overlayAlphaChannel = overlayChannels.get(3);
Mat alphaRate = new Mat(overlayAlphaChannel.size(), overlayAlphaChannel.type());
Core.divide(overlayAlphaChannel, new Scalar(255), alphaRate);
Core.absdiff(alphaRate, new Scalar(1), alphaRate);
for (int i = 0; i < 3; i++) {
// compute "(1 - alphaRate) * overlay"
Mat overlayChannel = overlayChannels.get(i);
Mat temp = new Mat(alphaRate.size(), alphaRate.type());
Core.absdiff(alphaRate, new Scalar(1), temp);
Core.multiply(temp, overlayChannel, overlayChannel);
temp.release();
// compute "background * alphaRate"
Mat backgroundChannel = backgroundChannels.get(i);
Core.multiply(backgroundChannel, alphaRate, backgroundChannel);
// compute the merged channel
Core.add(backgroundChannel, overlayChannel, backgroundChannel);
}
alphaRate.release();
Core.merge(backgroundChannels, background);
}
it is a lot faster compared to the double nested loop calculation.