How to scale text within a fixed rectangle with itext7? - c#

I'm trying to make a pdf document with itext7 in c# which should have fixed rectangles containing varying text that should scale within the boundaries of the (invisible) rectangles.
I have tried to find if there's automatic scaling, but so far only found auto-scaling for formfields. Since the pdf will be used for plotting text, formfields are of no use.
Code below is a snippet placing a 'box' with fixed dimensions, where all the text should be shown scaled (on one line)
float fontSize = 22f;
Text lineTxt = new Text("A VERY LONG TEXT SHOULD BE SCALED").SetFont(lineFont).SetFontSize(fontSize);
iText.Kernel.Geom.Rectangle lineTxtRect = new iText.Kernel.Geom.Rectangle(100, posHeight - 200, (float)plotline.producttype_plotmaxwidthpts, (float)plotline.producttype_plotmaxheightpts);
Div lineDiv = new Div();
lineDiv.SetMaxHeight((float)plotline.producttype_plotmaxheightpts);
lineDiv.SetWidth((float)plotline.producttype_plotmaxwidthpts);
lineDiv.SetHeight((float)plotline.producttype_plotmaxheightpts);
lineDiv.SetVerticalAlignment(VerticalAlignment.MIDDLE);
lineDiv.SetBorder(new DashedBorder(1));
Paragraph linePara = new Paragraph().Add(lineTxt).
SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER).
SetBorder(new DottedBorder(1)).
SetMultipliedLeading(0.7f).
SetMaxHeight((float)plotline.producttype_plotmaxheightpts).
SetHeight((float)plotline.producttype_plotmaxheightpts);
lineDiv.Add(linePara);
new Canvas(PageCanvas, pdf, lineTxtRect).Add(lineDiv).SetBorder(new SolidBorder(1f));

Layout module of iText 7 allows you to simulate rendering of an element (by creating the renderer tree from the element and then using Layout method) and check whether it fits the given area (by checking LayoutResult object). Thus what you can do is check whether the text fits into your fixed rectangle with the given font size. Then you can just do a binary search on the font size.
Here is a sample code:
PdfDocument pdfDocument = new PdfDocument(new PdfWriter(outFileName));
Text lineTxt = new Text("A VERY LONG TEXT SHOULD BE SCALED");
iText.Kernel.Geom.Rectangle lineTxtRect =
new iText.Kernel.Geom.Rectangle(100,200,100,100);
Div lineDiv = new Div();
lineDiv.SetVerticalAlignment(VerticalAlignment.MIDDLE);
lineDiv.SetBorder(new DashedBorder(1));
Paragraph linePara =
new Paragraph()
.Add(lineTxt)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.CENTER)
.SetBorder(new DottedBorder(1))
.SetMultipliedLeading(0.7f);
lineDiv.Add(linePara);
// 1 is the font size that is definitely small enough to draw all the text
float fontSizeL = 1;
// 20 is the maximum value of the font size you want to use
float fontSizeR = 20;
Canvas canvas =
new Canvas(
new PdfCanvas(pdfDocument.AddNewPage()),
pdfDocument,
lineTxtRect);
// Binary search on the font size
while (Math.Abs(fontSizeL - fontSizeR) > 1e-1) {
float curFontSize = (fontSizeL + fontSizeR) / 2;
lineDiv.SetFontSize(curFontSize);
// It is important to set parent for the current element renderer
// to a root renderer.
IRenderer renderer =
lineDiv.CreateRendererSubTree()
.SetParent(canvas.GetRenderer());
LayoutContext context =
new LayoutContext(
new LayoutArea(1, lineTxtRect));
if (renderer.Layout(context).GetStatus() == LayoutResult.FULL) {
// we can fit all the text with curFontSize
fontSizeL = curFontSize;
} else {
fontSizeR = curFontSize;
}
}
// Use the biggest font size that is still small enough to fit all the
// text.
lineDiv.SetFontSize(fontSizeL);
canvas.Add(lineDiv);
pdfDocument.Close();

Related

iText 7 C# creating a pdf from a template and adding text to it

I have a one page pdf template and need to create a new document with several pages. Each page needs to be as the first page of the template. Then i need to add text to each page. The pages are copied but the text is not added.
This is my code:
PdfDocument pdfDoc = new PdfDocument(new PdfWriter(stream));
PdfDocument cover = new PdfDocument(new PdfReader(templatePath));
//First copy the pages
var totalPages=5;
var coverPage = cover.GetPage(1);
for (int i = 0; i < totalPages; i++)
{
//If i do it to a blank page the text is visible
//pdfDoc.AddNewPage();
//I have tried both methods:
pdfDoc.AddPage(coverPage.CopyTo(pdfDoc));
//cover.CopyPagesTo(1, 1, pdfDoc);
}
//Now i try to add text
Document doc = new Document(pdfDoc);
var font = PdfFontFactory.CreateFont(fontPath);
for (int i = 1; i <= totalPages; i++)
{
//Edited
Rectangle pagesize = pdfDoc.GetPage(i).GetPageSize();
doc.ShowTextAligned(new Paragraph("HEADER").SetFont(font).SetFontSize(22), pagesize.GetLeft(), pagesize.GetBottom(), i, TextAlignment.LEFT, VerticalAlignment.BOTTOM, 0);
//doc.ShowTextAligned(new Paragraph("HEADER").SetFont(font), 100, 700, i, TextAlignment.CENTER, VerticalAlignment.TOP, 0);
}
doc.Close();
cover.Close();
pdfDoc.Close();
I have tried this options:
Canvas instead of document with no result (see code below)
If i use the AddNewPage() and not the cover page, then the text is added to the blank page (both document and canvas methods).
If i open and write directly to the template document the text is visible but the size is very small and position of the text is different compared to 2)
This is the canvas code inside the for instruction:
var canvas = new PdfCanvas(pdfDoc.GetPage(i));
canvas.BeginText()
.SetFontAndSize(font, 22) //Edited
.MoveText(100, 700)
.ShowText("HEADER")
.EndText();
//UPDATED
Following the solution contributed by #mkl, i have changed the way i add the pages:
var coverPage = cover.GetPage(1);
Rectangle coverSize = coverPage.GetPageSize();
for (int i = 0; i < totalPaginas; i++)
{
//Taken from this example: https://kb.itextpdf.com/home/it7kb/ebooks/itext-7-jump-start-tutorial-for-java/chapter-6-reusing-existing-pdf-documents
PdfPage page = pdfDoc.AddNewPage(PageSize.A4);
PdfCanvas canvas = new PdfCanvas(page);
AffineTransform transformationMatrix = AffineTransform.GetScaleInstance(
page.GetPageSize().GetWidth() / coverSize.GetWidth(),
page.GetPageSize().GetHeight() / coverSize.GetHeight());
canvas.ConcatMatrix(transformationMatrix);
var pageCopy = coverPage.CopyAsFormXObject(pdfDoc);
canvas.AddXObjectAt(pageCopy, 0, 0);
//pdfDoc.AddNewPage();
//pdfDoc.AddPage(coverPage.CopyTo(pdfDoc));
//cover.CopyPagesTo(1, 1, pdfDoc);
}
Now i can see the text added, but the font size is much smaller than if instead of copying i do "pdfDoc.AddNewPage()", why is it? i would like it to be the correct font size.
Your code works in my tests. Maybe (100,700) is outside of the visual page area, which would typically be the case if your template page does not have its lower left corner at (0,0).
This should put the text in the lower left corner:
Rectangle pagesize = pdfDoc.GetPage(i).GetPageSize();
doc.ShowTextAligned(new Paragraph("HEADER"), pagesize.GetLeft(),
pagesize.GetBottom(), i, TextAlignment.LEFT, VerticalAlignment.BOTTOM, 0);
If that works, you can work from the pagesize rectangle to calculate the appropriate position for the text.
The solution to why the text dont appear is updated in my question at the bottom.
The reason why the coordinates dont match and the size is so small is the source template pdf that was exported with a very high px/inch for high level printing. Reducing it to 72ppp was the fix.

How to correct obsolete code for creating Canvas

This code using iText7 runs. It copies a PDF file and, on the copy, prints a rectangle on top of page 1 with a red border around a text id or message:
public static void InsertIdPdf(string sourceFilename, string targetFilename, string idText)
{
if (idText.Length > 0)
{
PdfDocument sourcePdf = new PdfDocument(new PdfReader(sourceFilename));
PdfDocument targetPdf = new PdfDocument(new PdfWriter(targetFilename));
sourcePdf.CopyPagesTo(1, sourcePdf.GetNumberOfPages(), targetPdf);
Document document = new Document(targetPdf, new PageSize(PageSize.A4));
PdfPage firstPage = targetPdf.GetFirstPage();
iText.Kernel.Geom.Rectangle pageSize = firstPage.GetCropBox().MoveUp(4);
Canvas canvas = new Canvas(new PdfCanvas(firstPage, true), targetPdf, pageSize);
PdfFont idFont = PdfFontFactory.CreateFont(StandardFonts.HELVETICA);
Style idStyle = new Style()
.SetFont(idFont)
.SetFontSize(8)
.SetPaddingRight(3)
.SetTextAlignment(iText.Layout.Properties.TextAlignment.RIGHT)
.SetBackgroundColor(ColorConstants.WHITE);
Paragraph paragraph = new Paragraph(idText)
.SetBorder(new SolidBorder(ColorConstants.RED, (float)0.7))
.AddStyle(idStyle);
IRenderer renderer = paragraph.CreateRendererSubTree();
renderer.SetParent(document.GetRenderer()).Layout(new LayoutContext(new LayoutArea(1, pageSize)));
canvas.Add(paragraph);
document.Close();
sourcePdf.Close();
targetPdf.Close();
}
}
However, iText7 claims this line to be obsolete because the last argument, the Rectangle, may be removed in a future version:
Canvas canvas = new Canvas(new PdfCanvas(firstPage, true), targetPdf, pageSize);
I've tried to adjust the line, removing pageSize to satisfy iText7, but then the rectangle will print below the normal text on the page and flipped (mirrored) vertically.
So, how to adjust the line to not be obsolete and still print the rectangle at the top?
Also, notice the .MoveUp(4) for pageSize. The value 4 is an empiric value that moves the rectangle to the absolute top of the page. If omitted, the rectangle will be positioned with a small gab to the top of the page. This gab is independent of the font size.
So, why 4? I dislike magic numbers and would prefer a calculated value. What expression could be used to calculate this value?
Reference is the two questions here at SO:
Adding page number text to pdf copy gets flipped/mirrored with iText7
iText7: How to get the real width of a Paragraph
I wasn't able to reproduce this but I am now. I think it was something about the printable area of the document I was trying.
That's aside, the adjustment of the obsolete part is pretty simple. Just change
Canvas canvas = new Canvas(new PdfCanvas(firstPage, true), targetPdf, pageSize);
to
Canvas canvas = new Canvas(new PdfCanvas(firstPage, true), pageSize);
The obsoletion is the pdfDocument parameter.
About the magic number 4. I think it is about the DPI, I really don't know what it is. But the most relevant resource I could find is already in the community: Points -> pixels iText (im)precision

Create multiline text in PDF document Xamarin Forms Syncfusion

I have struggled a bit with the official Syncfusion Xamarin Forms PDF Documentation. I have managed to create a table, bind it to values and append it to a PdfDocument.
However odd enough, I cannot seem to draw text strings on separate lines. Each text that I try to draw using graphics.DrawString() or TextElement gets drawn on top of each other in the first line of the document.
All the string are AppResources, concatenating each other to form a report from resx files and class objects that represent my data. Many of them need to be rendered as separate phrases and paragraphs.
Is there a working sample on how to append text line after line and in between them to have tables?
Thank you!
Drawing text one after another is possible by using Essential PDF. We can draw text by one after another by using PdfTextElement with PdfLayoutResult or specifying its position in PdfGraphics.DrawString method. Please refer the below code snippet and sample for more details.
//Creates a new PDF document.
PdfDocument doc = new PdfDocument();
//Adds a page.
PdfPage page = doc.Pages.Add();
//create a new PDF string format
PdfStringFormat drawFormat = new PdfStringFormat();
drawFormat.WordWrap = PdfWordWrapType.Word;
drawFormat.Alignment = PdfTextAlignment.Justify;
drawFormat.LineAlignment = PdfVerticalAlignment.Top;
//Set the font.
PdfFont font = new PdfStandardFont(PdfFontFamily.Helvetica, 10f);
//Create a brush.
PdfBrush brush = PdfBrushes.Red;
//bounds
RectangleF bounds = new RectangleF(new PointF(10, 10), new SizeF(page.Graphics.ClientSize.Width - 30, page.Graphics.ClientSize.Height - 20));
//Create a new text elememt
PdfTextElement element = new PdfTextElement(text, font, brush);
//Set the string format
element.StringFormat = drawFormat;
//Draw the text element
PdfLayoutResult result = element.Draw(page, bounds);
// Draw the string one after another.
result = element.Draw(result.Page, new RectangleF(result.Bounds.X, result.Bounds.Bottom + 10, result.Bounds.Width, result.Bounds.Height));
// Creates a PdfLightTable.
PdfLightTable pdfLightTable = new PdfLightTable();
//Add colums to light table
pdfLightTable.Columns.Add(new PdfColumn("Name"));
pdfLightTable.Columns.Add(new PdfColumn("Age"));
pdfLightTable.Columns.Add(new PdfColumn("Sex"));
//Add row
pdfLightTable.Rows.Add(new string[] { "abc", "21", "Male" });
//Includes the style to display the header of the light table.
pdfLightTable.Style.ShowHeader = true;
//Draws PdfLightTable and returns the rendered bounds.
result = pdfLightTable.Draw(page, new PointF(result.Bounds.Left,result.Bounds.Bottom+20));
//draw string with returned bounds from table
result = element.Draw(result.Page, result.Bounds.X, result.Bounds.Bottom + 10);
//draw string with returned bounds from table
element.Draw(result.Page, result.Bounds.X, result.Bounds.Bottom + 10);
MemoryStream stream = new MemoryStream();
//Saves the document.
doc.Save(stream);
doc.Close(true);
Sample link: http://www.syncfusion.com/downloads/support/directtrac/171058/ze/App2938608856
Please let us know if you need any further assistance.

Resize existing image in DocX using OpenXML sdk

Got template docx with image placeholder which replaced by correct picture.
private void SetImagePartData(ImagePart imagePart, byte[] data)
{
if (imagePart != null)
{
using (var writer = new BinaryWriter(imagePart.GetStream()))
{
writer.Write(data);
}
}
}
but it preserves placeholder size. How to change it to actual image size? Byte array is aqquared from image on server, so size is known.
If you mean a content control with your placeholder you can use following code I once needed:
//Get SdtElement (can be a block, run... so I use the base class) with corresponding Tag
SdtElement block = doc.MainDocumentPart.Document.Body.Descendants<SdtElement>()
.FirstOrDefault(sdt => sdt.SdtProperties.GetFirstChild<Tag>()?.Val == contentControlTag);
//Get First drawing Element and get the original sizes of placeholder SDT
//I use SDT placeholder size as maximum size to calculate picture size with correct ratios
Drawing sdtImage = block.Descendants<Drawing>().First();
double sdtWidth = sdtImage.Inline.Extent.Cx;
double sdtHeight = sdtImage.Inline.Extent.Cy;
double sdtRatio = sdtWidth / sdtHeight;
*Calculate final width/height of image*
//Resize picture placeholder
sdtImage.Inline.Extent.Cx = finalWidth;
sdtImage.Inline.Extent.Cy = finalHeight;
//Change width/height of picture shapeproperties Transform
//This will override above height/width until you manually drag image for example
sdtImage.Inline.Graphic.GraphicData
.GetFirstChild<DocumentFormat.OpenXml.Drawing.Pictures.Picture>()
.ShapeProperties.Transform2D.Extents.Cx = finalWidth;
sdtImage.Inline.Graphic.GraphicData
.GetFirstChild<DocumentFormat.OpenXml.Drawing.Pictures.Picture>()
.ShapeProperties.Transform2D.Extents.Cy = finalHeight;
But you can use it if you are just using an image in your word document too. You just need to locate the correct Drawing element which contains the reference to your imagepart. Then you can use the bottom part of the code to adjust the image size. It's important you adjust both the Transform2D x and y as well as the Inline x and y or the image size won't be changed.

How to get the HEIGHT of the Run or Paragraph

I found the Run or Paragraph in FlowDocument and now I need to know the HEIGHT of it.
i.e.
while (navigator.CompareTo(flowDocViewer.Document.ContentEnd) < 0)
{
TextPointerContext context = navigator.GetPointerContext(LogicalDirection.Backward);
Run run = navigator.Parent as Run;
// I need to get HEIGHT of Run in pixels somehow
Is it possible to do in fact?
Thank you!
A little function i am using. The input is a string containing a Section. You can easily render other blockelements like Paragraph.
You also can omit the second parameter of the Parse method.
The trick is not to measure the Paragraph, but the ViewBox which contains a RichTextBox. This is needed to actually render the Flowdocument. The ViewBox dynamically gets the size of the rtb. Maybe you even can do this without the ViewBox. I spent some time to figure this out and it works for me.
Note that Width of the RichTextBox is set to double.MaxValue. This means when you want to measure a single paragraph it has to be very long or everything is in one line. So this only makes sense when you know the Width of your output device. As this is a FlowDocument there is no Width, it flows ;)
I use this to paginate a FlowDocument where i know the paper size.
The returned Height is device independent units.
private double GetHeaderFooterHeight(string headerFooter)
{
var section = (Section)XamlReader.Parse(headerFooter, _pd.ParserContext);
var flowDoc = new FlowDocument();
flowDoc.Blocks.Add(section);
var richtextbox = new RichTextBox { Width = double.MaxValue, Document = flowDoc };
var viewbox = new Viewbox { Child = richtextbox };
viewbox.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
viewbox.Arrange(new Rect(viewbox.DesiredSize));
var size = new Size() { Height = viewbox.ActualHeight, Width = viewbox.ActualWidth };
return size.Height;
}

Categories