In the code below, I am attempting to place a layer of text over existing text (the original existing text is "hidden" by writing it in white font; it is serving as a placeholder for this replacement process). The code below is very heavily reliant on code provided by Chris Haas in this post:
Getting Coordinates of string using ITextExtractionStrategy and LocationTextExtractionStrategy in Itextsharp
However, the "left" position for my text placement is fine, but the "bottom" is lower than the text I'm trying to overwrite (even though the font is the same). How can I alter this to get the correct coordinates of the text in the original document? The original document was created using tables for layout, so does that impact this "overwrite" process?
byte[] content;
string tmppath = #"C:\Junk\MSE_1.pdf";
using (MemoryStream output = new MemoryStream())
{
PdfReader pdf_rdr = new PdfReader(tmppath);
PdfStamper stamper = new PdfStamper(pdf_rdr, output);
for (int i = 1; i < pdf_rdr.NumberOfPages; i++)
{
//Create an instance of our strategy
var t = new MyLocationTextExtractionStrategy("stuff");
if (t != null)
{
//Parse the page of the document above
//(if the text was found)
var ex = PdfTextExtractor.GetTextFromPage(pdf_rdr, i, t);
//Loop through each chunk found
foreach (var p in t.myPoints)
{
Console.WriteLine(string.Format("Found text {0} at {1} x {2} on page {3}", p.Text, p.Rect.Left, p.Rect.Bottom, i.ToString()));
PdfContentByte pcb = stamper.GetOverContent(i);
pcb.BeginText();
try
{
BaseFont bf = BaseFont.CreateFont(#"C:\Junk\FontFiles\georgia.ttf", BaseFont.IDENTITY_H, BaseFont.EMBEDDED);
pcb.SetFontAndSize(bf, 10);
pcb.SetTextMatrix(p.Rect.Left, p.Rect.Bottom);
//pcb.SetTextRise(3);
pcb.ShowText("STUFF");
}
finally
{
pcb.EndText();
}
}
}
}
// Set the flattening flag to true, as the editing is done
stamper.FormFlattening = true;
// close the pdf stamper
stamper.Close();
//close the PDF reader
pdf_rdr.Close();
//put the output into the byte array
content = output.ToArray();
}
//write the content to a PDF file
using (FileStream fs = File.Create(#"C:\Junk\MSE_REPLACED.pdf"))
{
fs.Write(content, 0, (int)content.Length);
fs.Flush();
}
Chris' MyLocationTextExtractionStrategy only considers the descent line and the ascent line of the text pieces because it is interested in the area used by the string.
For your task, though, you need the base line because text drawing operations use the current position as base line start.
Thus, you should create a variant of Chris' class which manages the base line of the text instead of / in addition to its ascent and descent line.
PS: The OP asked in a comment
I'm afraid I'm not familiar with working with the base line. Do you have any examples you could share?
If you look at Chris' code you referred to, you'll see this RenderText implementation:
public override void RenderText(TextRenderInfo renderInfo) {
base.RenderText(renderInfo);
//Get the bounding box for the chunk of text
var bottomLeft = renderInfo.GetDescentLine().GetStartPoint();
var topRight = renderInfo.GetAscentLine().GetEndPoint();
//Create a rectangle from it
var rect = new iTextSharp.text.Rectangle(
bottomLeft[Vector.I1],
bottomLeft[Vector.I2],
topRight[Vector.I1],
topRight[Vector.I2]
);
//Add this to our main collection
this.myPoints.Add(new RectAndText(rect, renderInfo.GetText()));
}
As you see he stores the rectangle with the lower left corner at the start of the descent line and the upper right corner being the end of the ascent line.
If you replace
//Get the bounding box for the chunk of text
var bottomLeft = renderInfo.GetDescentLine().GetStartPoint();
var topRight = renderInfo.GetAscentLine().GetEndPoint();
by
//Get the bounding box for the chunk of text above the baseline
var bottomLeft = renderInfo.GetBaseline().GetStartPoint();
var topRight = renderInfo.GetAscentLine().GetEndPoint();
your overwriting code should work just fine.
Related
I am using C# and iText 7.2.1.
I want to draw texts right inside rectangles. I see in document that the positioning anchor of rectangles and paragraphs are both 'left-bottom corner'. But when I use the following code, they are not at the same location. Seems they have different understanding of the Y coordinate.
My code:
using iText.IO.Font;
using iText.IO.Font.Constants;
using iText.Kernel.Colors;
using iText.Kernel.Font;
using iText.Kernel.Pdf;
using iText.Kernel.Pdf.Canvas;
using iText.Layout;
using iText.Layout.Element;
namespace iTextTest
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
var str = "ABCD1234";
var fontSize = 32;
var x = 100;
var y = 700;
var writer = new PdfWriter("test.pdf");
var pdfdoc = new PdfDocument(writer);
var doc = new Document(pdfdoc);
var font = PdfFontFactory.CreateFont(StandardFonts.HELVETICA);
var width = font.GetWidth(str, fontSize);
var height = fontSize;
// Draw rectangle
var pdfPage = pdfdoc.AddNewPage();
var pdfCanvas = new PdfCanvas(pdfPage);
pdfCanvas.SetFillColor(ColorConstants.YELLOW);
pdfCanvas.Rectangle(x, y, width, height);
pdfCanvas.Fill();
// Draw text
var p = new Paragraph().Add(str).SetFont(font);
p.SetFontSize(fontSize).SetFontColor(ColorConstants.BLACK);
p.SetFixedPosition(x, y, width);
doc.Add(p);
doc.Close();
pdfdoc.Close();
writer.Close();
}
}
}
A line of a paragraph does not require the height of the font size but instead of the leading. For tightly set text the leading may equal the font size (or sometimes even be smaller than it) but for easy-to-read text the leading usually is larger than the font size.
When drawing a paragraph with iText, the extra space is located at the bottom of the line. This causes the empty space under the line you see in your output.
For your code to work, therefore, you have to set the leading to match your font size, i.e.
// Draw text
var p = new Paragraph().Add(str).SetFont(font);
p.SetFontSize(fontSize).SetFontColor(ColorConstants.BLACK);
p.SetFixedPosition(x, y, width);
p.SetFixedLeading(fontSize); // <-- added
doc.Add(p);
(TextPosition test TextAndPositionLikeLandings)
The result:
I'm using PdfFormXObject instead of PdfCanvas to apply background, border, and/or background color to a certain area of a page (mainly because PdfCanvas needs Page to construct, PdfFormXObject doesn't. As my content may last several pages). The problem is that the image is not positioned as expected if coordination (x = 0, y = 0) means bottom-left corner. I also want to position the Canvas to an fixed position but canvas.SetFixedPosition() seems not working. See attached original image and the image to be positioned at {x, y, width, height} = {100f, 100f, 200f, 200f} which should be at the bottom of the page(which is not) and it's also truncated somehow?
code
public void CreatePDF(string path)
{
var writer = new PdfWriter(path);
var pdf = new PdfDocument(writer);
var doc = new Document(pdf, PageSize.LETTER);
doc.SetMargins(18, 18, 18, 18);
var rect = new Rectangle(100f, 100f, 200f, 200f);
var temp = new PdfFormXObject(new Rectangle(rect.GetWidth(), rect.GetHeight()));
var ca = new Canvas(temp, pdf);
// ca.SetFixedPosition(rect.GetLeft(), rect.GetBottom(), rect.GetWidth());
var img = new Image(ImageDataFactory.Create(path));
img.SetFixedPosition(rect.GetLeft(), rect.GetBottom());
img.ScaleAbsolute(rect.GetWidth(), rect.GetHeight());
ca.Add(img);
ca.SetBackgroundColor(ColorConstants.BLUE); // not shown blue bg
ca.Close();
doc.Add(new Image(temp));
doc.Close();
pdf.Close();
}
original image
generated one(wrongly placed at the top)
update
Here is the working code after mkl's direction. But canvas cannot set border/background color:
public void CreatePDF(string path)
{
var writer = new PdfWriter(path);
var pdf = new PdfDocument(writer);
var doc = new Document(pdf, PageSize.LETTER);
doc.SetMargins(LETTER_MARGIN, LETTER_MARGIN, LETTER_MARGIN, LETTER_MARGIN);
var rect = new Rectangle(100f, 300f, 200f, 200f);
var w = Doc.GetPageEffectiveArea(PageSize.LETTER).GetWidth(); //576f
var h = Doc.GetPageEffectiveArea(PageSize.LETTER).GetHeight();//756f
var temp = new PdfFormXObject(new Rectangle(w, h));
var ca = new Canvas(temp, pdf);
ca.SetFixedPosition(0, 0, 576f);
ca.SetBorder(new SolidBorder(1f));//not work
ca.SetBackgroundColor(ColorConstants.BLUE);//not work
var img = new Image(ImageDataFactory.Create(path));
img.SetFixedPosition(rect.GetLeft(), rect.GetBottom());
img.ScaleAbsolute(rect.GetWidth(), rect.GetHeight());
ca.Add(img);
ca.Close();
doc.Add(new Image(temp));
doc.Close();
pdf.Close();
}
Update
I added a Div to the ca and set border and background color to the Div. Works perfectly.
TIA
First, you create a PdfFormXObject using a rectangle you only define width and height for:
var temp = new PdfFormXObject(new Rectangle(rect.GetWidth(), rect.GetHeight()));
Thus, the rectangle has the lower left corner at the origin of the form xobject coordinate system, i.e. lower left is (0,0) and upper right is (200,200).
On this area you position a 200×200 image at (100,100):
img.SetFixedPosition(rect.GetLeft(), rect.GetBottom());
img.ScaleAbsolute(rect.GetWidth(), rect.GetHeight());
ca.Add(img);
So most of the image is outside the xobject, only the lower left quadrant is inside, the one you see in your screenshot.
Then you add this xobject to the document without positioning:
doc.Add(new Image(temp));
Thus, you ask itext to put your 200×200 xobject where it fits given the existing content. So it will be positioned further up than you want.
To fix your issue, therefore, arrange the xobject area and the image position so that the image is in the area, and position the xobject as desired.
I am trying to render a PDF with a grid of images, using iTextSharp (v5.5.10). The images will all have the same dimensions and should be evenly distributed on one page.
However, with the code mentioned below, I'm having difficulty in setting the appropriate margins, or spacing between the cells.
Visually this means that the expected result is this:
The yellow highlighted lines are the problem where I'm getting the following result instead:
Notice how there are no spaces between the images? This is based on my following code:
public void CreateGridOfImages(string outputFilePath)
{
// note: these constants are in millimeters (mm),
// which are converted using the ToPoints() helper later on
const float spacingBetweenCells = 7;
const float imageWidth = 80;
const float imageHeight = 80;
const string[] images = new [] { "a.jpg", "b.jpg", "c.jpg" };
using (var stream = new FileStream(outputFilePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
{
var document = new iTextSharp.text.Document(PageSize.B2, 0f, 0f, 0f, 0f);
var writer = PdfWriter.GetInstance(document, stream);
try
{
document.Open();
var table = new PdfPTable(5);
table.DefaultCell.Border = Rectangle.NO_BORDER;
foreach (var imagePath in images)
{
var img = iTextSharp.text.Image.GetInstance(imagePath);
img.ScaleToFit(ToPoints(imageWidth), ToPoints(imageHeight));
var cell = new PdfPCell();
// THIS IS THE PROBLEM... HOW TO SET IMAGE SPACING?
var cellMargin = ToPoints(spacingBetweenCells);
cell.AddElement(img);
table.AddCell(cell);
}
document.Add(table);
}
catch (Exception ex)
{
throw ex;
}
finally
{
document.Close();
}
}
}
private float ToPoints(float millimeters)
{
// converts millimeters to points
return iTextSharp.text.Utilities.MillimetersToPoints(millimeters);
}
Now this seems trivial. And it problably is, but I've tried several options and could none of them to work properly (or at all):
Adding Paragraph() objects with paddings between each
Adding Padding to PdfPCell does not seem to work for me
Looked into custom IPdfPCellEvent samples
Absolutely positioning the images alltogether (forgetting the PdfPTable)
My hunch is that the IPdfPCellEvent seems the right approach. But all the iText options and variations are simply overwhelming.
Summarized, does anyone know how can I properly set the margins/spacing between the cells?
I assume you want to have the second table in this image:
The only way to create white spacing between cells in an iText table is to set the relevant borders to the background colour and play with the padding of the cell. My pivotal code for creating the cells is:
for(int i = 0; i < nrCols* nrRows;i++) {
var img = Image.GetInstance(imagePath);
img.ScaleToFit(ToPoints(imageWidth), ToPoints(imageHeight));
//Create cell
var imageCell = new PdfPCell();
imageCell.Image = img;
imageCell.Border = Rectangle.BOX;
imageCell.BorderColor = useColor? BaseColor.YELLOW : BaseColor.WHITE;
//Play with this value to change the spacing
imageCell.Padding = ToPoints(spacingBetweenCells/2);
imageCell.HorizontalAlignment = Element.ALIGN_CENTER;
grid.AddCell(imageCell);
}
As to why adding padding to the PdfPCell didn't seem to worrk:
The reason why borders are still drawn in your example, despite
table.DefaultCell.Border = Rectangle.NO_BORDER;
Is because you never use the default cell because you create a custom one with var cell = new PdfPCell(); and pass the custom one to table.AddCell(cell);. If you'd have used table.addCell(img), the border would not have been there (although your padding would still not be your desired spacing, since it isn't set on the default cell).
At the moment I'm trying to change the fonts used in a PDF document.
I therefore declared rectangles and extract the text using LocationTextExtractionStrategy:
System.util.RectangleJ rect = new System.util.RectangleJ(fX, fY, fWidth, fHeight);
RenderFilter[] rfArea = { new RegionTextRenderFilter(rect) };
ITextExtractionStrategy str = new FilteredTextRenderListener(new LocationTextExtractionStrategy(), rfArea);
string s = PdfTextExtractor.GetTextFromPage(reader, 1, str);
This works as expected, although it's quite complicated to find the right coordinates.
But how do I place the text in a new PDF document at the same starting location fX, fY so I don't break the layout?
I tried it using ColumnText but a multiline text is put one above the other:
Paragraph para = new Paragraph(fLineSpacing, s, font);
para.IndentationLeft = fLineIndentionLeft;
para.SpacingAfter = fSpacingAfter;
para.SpacingBefore = fSpacingBefore;
para.Alignment = iFontAlignment;
PdfContentByte cb = writer.DirectContent;
ColumnText ct = new ColumnText(cb);
ct.SetSimpleColumn(para, fX, fY + fHeight, fWidth + fX, fY, 0f, Element.ALIGN_LEFT);
ct.Go();
What am I missing? Or is there even something simpler? Using my approach I will have to cover the entire page defining lots of rectangles.
So, I'm having this problem using C# (.NET 4.0 + WinForms) and iTextSharp 5.1.2.
I have some scanned images stored on a DB and need to build on the fly PDF with those images. Some files have just one page and other ones hundreds. That is working just fine using:
foreach (var page in pages)
{
Image pageImage = Image.GetInstance(page.Image);
pageImage.ScaleToFit(document.PageSize.Width,document.PageSize.Height);
pageImage.Alignment = Image.ALIGN_TOP | Image.ALIGN_CENTER;
document.Add(pageImage);
document.NewPage();
//...
}
The problem is:
I need to add an small table at the bottom of the last page.
I try:
foreach (var page in pages)
{
Image pageImage = Image.GetInstance(page.Image);
pageImage.ScaleToFit(document.PageSize.Width,document.PageSize.Height);
pageImage.Alignment = Image.ALIGN_TOP | Image.ALIGN_CENTER;
document.Add(pageImage);
document.NewPage();
//...
}
Table t = new table....
document.Add(t);
The table is successfully added but IF the size of the image fits the page size of the document then the table is added on the next page.
I need to resize the last image of the document (if it has multiple ones, or the first if has only 1) in order to put the table directly on that page (with the image) and that both ocuppy just one page.
I try to scale the image by percent BUT given that the image size of the image that'll be on the last page is unknow and that it must FILL the biggest portion of the page I need to do that dinamically.
Any idea?
Let me give you a couple of things that might help you and then I'll give you a full working example that you should be able to customize.
The first thing is that the PdfPTable has a special method called WriteSelectedRows() that allows you to draw a table at an exact x,y coordinate. It has six overloads but the most commonly used one is probably:
PdfPTable.WriteSelectedRows(int rowStart,int rowEnd, float xPos, float yPos, PdfContentByte canvas)
To place a table with the upper left corner positioned at 400,400 you would call:
t.WriteSelectedRows(0, t.Rows.Count, 400, 400, writer.DirectContent);
Before calling this method you are required to set the table's width using SetTotalWidth() first:
//Set these to your absolute column width(s), whatever they are.
t.SetTotalWidth(new float[] { 200, 300 });
The second thing is that the height of the table isn't known until the entire table is rendered. This means that you can't know exactly where to place a table so that it truly is at the bottom. The solution to this is to render the table to a temporary document first and then calculate the height. Below is a method that I use to do this:
public static float CalculatePdfPTableHeight(PdfPTable table)
{
using (MemoryStream ms = new MemoryStream())
{
using (Document doc = new Document(PageSize.TABLOID))
{
using (PdfWriter w = PdfWriter.GetInstance(doc, ms))
{
doc.Open();
table.WriteSelectedRows(0, table.Rows.Count, 0, 0, w.DirectContent);
doc.Close();
return table.TotalHeight;
}
}
}
}
This can be called like this:
PdfPTable t = new PdfPTable(2);
//In order to use WriteSelectedRows you need to set the width of the table
t.SetTotalWidth(new float[] { 200, 300 });
t.AddCell("Hello");
t.AddCell("World");
t.AddCell("Test");
t.AddCell("Test");
float tableHeight = CalculatePdfPTableHeight(t);
So with all of that here's a full working WinForms example targetting iTextSharp 5.1.1.0 (I know you said 5.1.2 but this should work just the same). This sample looks for all JPEGs in a folder on the desktop called "Test". It then adds them to an 8.5"x11" PDF. Then on the last page of the PDF, or if there's only 1 JPEG to start with on the only page, it expands the height of the PDF by however tall the table that we're adding is and then places the table at the bottom left corner. See the comments in the code itself for further explanation.
using System;
using System.Text;
using System.Windows.Forms;
using iTextSharp.text;
using iTextSharp.text.pdf;
using System.IO;
namespace Full_Profile1
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
public static float CalculatePdfPTableHeight(PdfPTable table)
{
//Create a temporary PDF to calculate the height
using (MemoryStream ms = new MemoryStream())
{
using (Document doc = new Document(PageSize.TABLOID))
{
using (PdfWriter w = PdfWriter.GetInstance(doc, ms))
{
doc.Open();
table.WriteSelectedRows(0, table.Rows.Count, 0, 0, w.DirectContent);
doc.Close();
return table.TotalHeight;
}
}
}
}
private void Form1_Load(object sender, EventArgs e)
{
//Create our table
PdfPTable t = new PdfPTable(2);
//In order to use WriteSelectedRows you need to set the width of the table
t.SetTotalWidth(new float[] { 200, 300 });
t.AddCell("Hello");
t.AddCell("World");
t.AddCell("Test");
t.AddCell("Test");
//Calculate true height of the table so we can position it at the document's bottom
float tableHeight = CalculatePdfPTableHeight(t);
//Folder that we are working in
string workingFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop), "Test");
//PDF that we are creating
string outputFile = Path.Combine(workingFolder, "Output.pdf");
//Get an array of all JPEGs in the folder
String[] AllImages = Directory.GetFiles(workingFolder, "*.jpg", SearchOption.TopDirectoryOnly);
//Standard iTextSharp PDF init
using (FileStream fs = new FileStream(outputFile, FileMode.Create, FileAccess.Write, FileShare.None))
{
using (Document document = new Document(PageSize.LETTER))
{
using (PdfWriter writer = PdfWriter.GetInstance(document, fs))
{
//Open our document for writing
document.Open();
//We do not want any margins in the document probably
document.SetMargins(0, 0, 0, 0);
//Declare here, init in loop below
iTextSharp.text.Image pageImage;
//Loop through each image
for (int i = 0; i < AllImages.Length; i++)
{
//If we only have one image or we are on the second to last one
if ((AllImages.Length == 1) | (i == (AllImages.Length - 1)))
{
//Increase the size of the page by the height of the table
document.SetPageSize(new iTextSharp.text.Rectangle(0, 0, document.PageSize.Width, document.PageSize.Height + tableHeight));
}
//Add a new page to the PDF
document.NewPage();
//Create our image instance
pageImage = iTextSharp.text.Image.GetInstance(AllImages[i]);
pageImage.ScaleToFit(document.PageSize.Width, document.PageSize.Height);
pageImage.Alignment = iTextSharp.text.Image.ALIGN_TOP | iTextSharp.text.Image.ALIGN_CENTER;
document.Add(pageImage);
//If we only have one image or we are on the second to last one
if ((AllImages.Length == 1) | (i == (AllImages.Length - 1)))
{
//Draw the table to the bottom left corner of the document
t.WriteSelectedRows(0, t.Rows.Count, 0, tableHeight, writer.DirectContent);
}
}
//Close document for writing
document.Close();
}
}
}
this.Close();
}
}
}
EDIT
Below is an edit based on your comments. I'm only posting the contents of the for loop which is the only part that changed. When calling ScaleToFit you just need to take tableHeight into account.
//Loop through each image
for (int i = 0; i < AllImages.Length; i++)
{
//Add a new page to the PDF
document.NewPage();
//Create our image instance
pageImage = iTextSharp.text.Image.GetInstance(AllImages[i]);
//If we only have one image or we are on the second to last one
if ((AllImages.Length == 1) | (i == (AllImages.Length - 1)))
{
//Scale based on the height of document minus the table height
pageImage.ScaleToFit(document.PageSize.Width, document.PageSize.Height - tableHeight);
}
else
{
//Scale normally
pageImage.ScaleToFit(document.PageSize.Width, document.PageSize.Height);
}
pageImage.Alignment = iTextSharp.text.Image.ALIGN_TOP | iTextSharp.text.Image.ALIGN_CENTER;
document.Add(pageImage);
//If we only have one image or we are on the second to last one
if ((AllImages.Length == 1) | (i == (AllImages.Length - 1)))
{
//Draw the table to the bottom left corner of the document
t.WriteSelectedRows(0, t.Rows.Count, 0, tableHeight, writer.DirectContent);
}
}
Just use a method table.CalculateHeights() if you want to know the height of table.