The PdfEditor class lets you add new content to an existing PDF without rewriting the rest of the document. The editor is instantiated with the source PDF bytes or file path and an optional password. The standard claimed by the source document and its existing page geometry are inherited by the editor. Content is added at absolute page coordinates using the dedicated Add methods. Every Add method takes the target page number as the first argument. If the rendered content overflows the available space a new page is created automatically by the engine. The resulting modified PDF is produced with PdfEditorSave.
Text is rendered by building a PdfTextElement (the same element used when creating a PDF from scratch) and passing it to PdfEditorAddText(Int32, PdfTextElement). The first argument is the 1-based page number where the text should appear. The element carries position, width, alignment, font, background color, rotation, accessibility metadata and the per-page render callbacks. The call returns a PdfTextRenderInfo describing where the text was actually drawn. The result includes the page number of the last page it spanned. The caller must capture this page number to keep the running page cursor in sync.
Fonts are obtained through PdfFontManager. Use PdfFontManagerCreateStandardFont(PdfStandardFont, Single, PdfFontStyle, PdfColor) for the built-in fonts. For TrueType or OpenType fonts the recommended approach is a two-step pattern. Load the font file once into a EvoPdf.NextPdfBaseFont with PdfFontManagerCreateBaseFont(String, Boolean, IListInt32). Then derive styled EvoPdf.NextPdfFont instances from it with PdfFontManagerCreateFont(PdfBaseFont, Single, PdfFontStyle, PdfColor). The font subset is then embedded only once. A convenience overload PdfFontManagerCreateFont(String, Single, PdfFontStyle, PdfColor) combines both steps in a single call and is fine when the font is used only once in the edit.
Text elements support the full feature set you have when creating from scratch. Background highlights are configured through BackgroundColor and BackgroundOpacity. Per-page decoration is available through OnBeforePageRender and OnAfterPageRender which receive a EvoPdf.NextPdfTextPageRenderInfo carrying the per-page substring, page number and RenderedRectangle. Multi-page flow is enabled with ContinueOnNextPage. Rotation is configured through RotationDegrees. Right-to-left scripts are enabled by setting Direction to RightToLeft.
When the editor adds text that overflows the current page, it allocates a new page with the geometry inherited from the source document and continues rendering on it. The page number of the last page that received content is exposed through PageNumber on the LastPageRectangle of the returned info object. Capture it into the local currentPage variable after every Add call so subsequent calls land on the right page.
Accessibility metadata is preserved across the edit. When the source document is PDF/UA-1, PDF/UA-2 or PDF/A-2a, every new text element should set Accessibility.StructureType to the correct role. Decorative content such as page-number badges or watermarks should be tagged with PdfStructureTypeArtifact.
The editor is instantiated with either the source PDF as a byte buffer or as a file path. An optional password argument is required when the source PDF is encrypted. Pass the owner password when available so the editor can write back any permission-restricted content.
string password = string.IsNullOrEmpty(ownerPassword) ? userPassword : ownerPassword;
using PdfEditor pdfEditor = new PdfEditor(inputPdfBytes, password);The standard, language and page geometry are inherited from the source document. The editor cannot change them. To produce a PDF/A version from a non-PDF/A source you need to convert it through HtmlToPdfConverter or PdfDocument instead.
Every Add call on the editor returns information that includes the page number of the last page touched by the rendered element. Capture it after every call so the running currentPage variable always reflects where the next element should land.
PdfTextElement element = new PdfTextElement(text, font) { X = 0, Y = crtYPos, Width = contentWidth };
var info = pdfEditor.AddText(currentPage, element);
currentPage = info.LastPageRectangle.PageNumber; // follow the engine if it overflowed
crtYPos = (int)info.LastPageRectangle.Bounds.Bottom + ySeparator;The OnBeforePageRender and OnAfterPageRender callbacks fire for every page the element spans. When the callback needs to draw a decoration (background rectangle or border) call the per-page Add overloads on the editor. Pass the rendered rectangle PageNumber from the info argument so the decoration lands on the same page as the underlying text.
using System;
using System.IO;
using System.Threading.Tasks;
using System.Text;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Http;
using EvoPdf_Next_AspNetDemo.Models;
using EvoPdf_Next_AspNetDemo.Models.PDF_Editor;
// Use EVO PDF Namespace
using EvoPdf.Next;
namespace EvoPdf_Next_AspNetDemo.Controllers.PDF_Editor
{
public class Add_Text_to_Existing_PDFController : Controller
{
private const int leftMargin = 36;
private const int topMargin = 36;
private const int contentWidth = 595 - 72;
private const int contentHeight = 842 - 72;
private readonly IWebHostEnvironment m_hostingEnvironment;
public Add_Text_to_Existing_PDFController(IWebHostEnvironment hostingEnvironment)
{
m_hostingEnvironment = hostingEnvironment;
}
public IActionResult Index()
{
var model = SetViewModel();
return View(model);
}
[HttpPost]
public async Task<IActionResult> EditPdf(Add_Text_to_Existing_PDF_ViewModel model)
{
if (!ModelState.IsValid)
{
var errorMessage = ModelStateHelper.GetModelErrors(ModelState);
throw new ValidationException(errorMessage);
}
// Set license key received after purchase to use the library in licensed mode
// Set the license key received after purchase to use the library in licensed mode; leave it commented for demo mode
// Licensing.LicenseKey = "your-license-key";
byte[] inputPdfBytes = null;
// If an uploaded file exists, use it with priority
if (model.PdfFile != null && model.PdfFile.Length > 0)
{
try
{
using var ms = new MemoryStream();
await model.PdfFile.CopyToAsync(ms);
inputPdfBytes = ms.ToArray();
}
catch (Exception ex)
{
throw new Exception("Failed to read the uploaded PDF file", ex);
}
}
else
{
// Otherwise, fall back to the URL
string pdfUrl = model.PdfFileUrl?.Trim();
if (string.IsNullOrWhiteSpace(pdfUrl))
throw new Exception("No PDF file provided: upload a file or specify a URL");
try
{
if (pdfUrl.StartsWith("file://", StringComparison.OrdinalIgnoreCase))
{
string localPath = new Uri(pdfUrl).LocalPath;
inputPdfBytes = await System.IO.File.ReadAllBytesAsync(localPath);
}
else
{
using var httpClient = new System.Net.Http.HttpClient();
inputPdfBytes = await httpClient.GetByteArrayAsync(pdfUrl);
}
}
catch (Exception ex)
{
throw new Exception("Could not download the PDF file from URL", ex);
}
}
// Open the loaded PDF for editing. PdfEditor inherits the
// standard (PDF/A, PDF/UA etc.) and Language from the source
// document, so no PdfDocumentCreateSettings is needed. Page
// size and margins are also taken from the existing pages;
// we apply our own margins manually below via leftMargin /
// topMargin since PdfEditor draws at absolute page coordinates
string password = string.IsNullOrEmpty(model.OwnerPassword) ? model.UserPassword : model.OwnerPassword;
using PdfEditor pdfEditor = new PdfEditor(inputPdfBytes, password);
pdfEditor.PdfDocumentInfo.Title = "PDF Text Demo";
string fontsPath = GetDemoFontsPath();
string fontFilePath = Path.Combine(fontsPath, "DejaVuSerif.ttf");
PdfBaseFont baseFont = PdfFontManager.CreateBaseFont(fontFilePath);
PdfFont titleFont = PdfFontManager.CreateFont(baseFont, 18f,
PdfFontStyle.Bold | PdfFontStyle.Underline, PdfColor.Black);
PdfFont sectionFont = PdfFontManager.CreateFont(baseFont, 14f,
PdfFontStyle.Bold, PdfColor.DarkBlue);
PdfFont bodyFont = PdfFontManager.CreateFont(baseFont, 11f,
PdfFontStyle.Normal, PdfColor.Black);
PdfFont smallFont = PdfFontManager.CreateFont(baseFont, 9f,
PdfFontStyle.Normal, PdfColor.DarkGray);
const int xLeft = leftMargin;
const int ySeparator = 10;
int currentPage = 1;
int crtYPos = topMargin;
// ===== Section 1: Title =====
PdfTextElement titleElement = new PdfTextElement(
"PDF Text Demo", titleFont)
{
X = xLeft,
Y = crtYPos,
Alignment = PdfTextAlignment.Center,
Width = contentWidth
};
titleElement.Accessibility.StructureType = PdfStructureType.Heading1;
PdfTextRenderInfo titleInfo = pdfEditor.AddText(currentPage, titleElement);
currentPage = titleInfo.LastPageRectangle.PageNumber;
crtYPos = (int)titleInfo.LastPageRectangle.Bounds.Bottom + ySeparator * 2;
// ===== Section 2: BackgroundColor + BackgroundOpacity =====
PdfTextElement sectionLabel1 = new PdfTextElement(
"1. BackgroundColor + BackgroundOpacity", sectionFont)
{ X = xLeft, Y = crtYPos };
sectionLabel1.Accessibility.StructureType = PdfStructureType.Heading2;
var sectionLabel1Info = pdfEditor.AddText(currentPage, sectionLabel1);
currentPage = sectionLabel1Info.LastPageRectangle.PageNumber;
crtYPos = (int)sectionLabel1Info.LastPageRectangle.Bounds.Bottom + ySeparator;
PdfTextElement highlighted = new PdfTextElement(
"This paragraph has a yellow highlight background drawn behind the text as an " +
"Artifact (it does not appear in the structure tree). The background covers the " +
"full column width and the actual used height.",
bodyFont)
{
X = xLeft,
Y = crtYPos,
Width = contentWidth,
BackgroundColor = PdfColor.Yellow,
BackgroundOpacity = 0.4f
};
var highlightedInfo = pdfEditor.AddText(currentPage, highlighted);
currentPage = highlightedInfo.LastPageRectangle.PageNumber;
crtYPos = (int)highlightedInfo.LastPageRectangle.Bounds.Bottom + ySeparator * 2;
// ===== Section 3: OnBeforePageRender + OnAfterPageRender =====
PdfTextElement sectionLabel2 = new PdfTextElement(
"2. OnBeforePageRender (under) + OnAfterPageRender (over)", sectionFont)
{ X = xLeft, Y = crtYPos };
sectionLabel2.Accessibility.StructureType = PdfStructureType.Heading2;
var sectionLabel2Info = pdfEditor.AddText(currentPage, sectionLabel2);
currentPage = sectionLabel2Info.LastPageRectangle.PageNumber;
crtYPos = (int)sectionLabel2Info.LastPageRectangle.Bounds.Bottom + ySeparator;
PdfTextElement decoratedText = new PdfTextElement(
"Below text is drawn an under-layer rectangle in the OnBeforePageRender callback, " +
"while the OnAfterPageRender callback adds a thin border around the rendered area. " +
"Both callbacks receive the same PdfTextPageRenderInfo type but the rectangle in " +
"OnBeforePageRender is the predicted column area while in OnAfterPageRender it is " +
"the actual rendered area.",
bodyFont)
{
X = xLeft,
Y = crtYPos,
Width = contentWidth
};
// Under-layer painted before the text is drawn
decoratedText.OnBeforePageRender = preInfo =>
{
var col = preInfo.RenderedRectangle.Bounds;
pdfEditor.AddRectangle(preInfo.RenderedRectangle.PageNumber, new PdfRectangleElement(
col.X - 2, col.Y - 2, col.Width + 4, col.Height + 4)
{
FillColor = PdfColor.LightBlue,
FillOpacity = 0.35f,
BorderColor = null
});
};
// Post-render hook for the border that follows the actually used area
decoratedText.OnAfterPageRender = postInfo =>
{
var rect = postInfo.RenderedRectangle.Bounds;
pdfEditor.AddRectangle(postInfo.RenderedRectangle.PageNumber, new PdfRectangleElement(
rect.X - 2, rect.Y - 2, rect.Width + 4, rect.Height + 4)
{
FillColor = null,
BorderColor = PdfColor.Blue,
Border = new PdfLineStyle { LineWidth = 1f }
});
};
var decoratedTextInfo = pdfEditor.AddText(currentPage, decoratedText);
currentPage = decoratedTextInfo.LastPageRectangle.PageNumber;
crtYPos = (int)decoratedTextInfo.LastPageRectangle.Bounds.Bottom + ySeparator * 2;
// ===== Section 4: Rotated text + QuadPoints outline =====
EnsureSpaceOnPage(ref crtYPos, ref currentPage, 180, pdfEditor, contentHeight, topMargin);
PdfTextElement sectionLabel3 = new PdfTextElement(
"3. Rotated text with QuadPoints-based outline", sectionFont)
{ X = xLeft, Y = crtYPos };
sectionLabel3.Accessibility.StructureType = PdfStructureType.Heading2;
var sectionLabel3Info = pdfEditor.AddText(currentPage, sectionLabel3);
currentPage = sectionLabel3Info.LastPageRectangle.PageNumber;
crtYPos = (int)sectionLabel3Info.LastPageRectangle.Bounds.Bottom + ySeparator;
PdfTextElement rotated = new PdfTextElement(
"Rotated 25 degrees around the top-left corner. The polygon below follows the " +
"rotation using the four-corner QuadPoints returned by PdfTextPageRenderInfo.",
bodyFont)
{
X = xLeft + 60,
Y = crtYPos + 10,
Width = 350,
RotationDegrees = -25,
RotationPivot = PdfRotationPivot.TopLeft,
BackgroundColor = PdfColor.LightYellow,
BackgroundOpacity = 0.5f
};
// Trace a polygon along the rotated quad after rendering.
rotated.OnAfterPageRender = postInfo =>
{
var quad = postInfo.RenderedRectangle.QuadPoints;
pdfEditor.AddPolygon(postInfo.RenderedRectangle.PageNumber, new PdfPolygonElement(
quad.TopLeft, quad.TopRight, quad.BottomRight, quad.BottomLeft)
{
FillColor = null,
BorderColor = PdfColor.Red,
Border = new PdfLineStyle { LineWidth = 1.2f, DashStyle = PdfLineDashStyle.Dashed }
});
};
PdfTextRenderInfo rotatedInfo = pdfEditor.AddText(currentPage, rotated);
currentPage = rotatedInfo.LastPageRectangle.PageNumber;
// Advance Y past the rotated area's axis-aligned bounding box.
crtYPos = (int)rotatedInfo.LastPageRectangle.Bounds.Bottom + ySeparator * 2;
// ===== Section 5: Multi-page continuation + per-page RenderedText =====
currentPage = pdfEditor.AddPage();
crtYPos = topMargin;
PdfTextElement sectionLabel4 = new PdfTextElement(
"4. Multi-page continuation + per-page RenderedText", sectionFont)
{ X = xLeft, Y = crtYPos };
sectionLabel4.Accessibility.StructureType = PdfStructureType.Heading2;
var sectionLabel4Info = pdfEditor.AddText(currentPage, sectionLabel4);
currentPage = sectionLabel4Info.LastPageRectangle.PageNumber;
crtYPos = (int)sectionLabel4Info.LastPageRectangle.Bounds.Bottom + ySeparator;
// Build a long text that will overflow several pages.
string longTextSource = LoadAlphabetText();
StringBuilder longBuilder = new StringBuilder();
for (int i = 0; i < 8; i++) longBuilder.AppendLine(longTextSource);
string longText = longBuilder.ToString();
PdfTextElement multipage = new PdfTextElement(longText, bodyFont)
{
X = xLeft,
Y = crtYPos,
Width = contentWidth,
Alignment = PdfTextAlignment.Left,
ContinueOnNextPage = true,
BackgroundColor = PdfColor.WhiteSmoke,
BackgroundOpacity = 1f
};
// Per-page hook: draw a small page-number badge using OnAfterPageRender
multipage.OnAfterPageRender = info =>
{
var r = info.RenderedRectangle.Bounds;
string badge = $"page {info.RenderedRectangle.PageNumber} - {info.RenderedText.Length} chars";
// Place the badge to the right of the rendered text top edge
var badgeText = new PdfTextElement(badge, smallFont)
{
X = (float)r.Right - 110,
Y = (float)r.Y - 12,
Width = 110,
Alignment = PdfTextAlignment.Right
};
badgeText.Accessibility.StructureType = PdfStructureType.Artifact;
pdfEditor.AddText(info.RenderedRectangle.PageNumber, badgeText);
};
PdfTextRenderInfo multipageInfo = pdfEditor.AddText(currentPage, multipage);
currentPage = multipageInfo.LastPageRectangle.PageNumber;
// ===== Section 6: Summary of pages rendered =====
currentPage = pdfEditor.AddPage();
crtYPos = topMargin;
PdfTextElement summaryLabel = new PdfTextElement(
"5. Summary: text rendered per page (from Pages list)", sectionFont)
{ X = xLeft, Y = crtYPos };
summaryLabel.Accessibility.StructureType = PdfStructureType.Heading2;
var summaryLabelInfo = pdfEditor.AddText(currentPage, summaryLabel);
currentPage = summaryLabelInfo.LastPageRectangle.PageNumber;
crtYPos = (int)summaryLabelInfo.LastPageRectangle.Bounds.Bottom + ySeparator;
// Walk the Pages list returned by the multipage render and report
// the first 80 characters of each page
for (int i = 0; i < multipageInfo.Pages.Count; i++)
{
var page = multipageInfo.Pages[i];
string preview = page.RenderedText.Length > 80
? page.RenderedText.Substring(0, 80).Replace("\n", " ").Replace("\r", "") + "..."
: page.RenderedText.Replace("\n", " ").Replace("\r", "");
string entry = $"Page {page.RenderedRectangle.PageNumber}: " +
$"{page.RenderedText.Length} chars, " +
$"bounds=({page.RenderedRectangle.Bounds.X:F0}, " +
$"{page.RenderedRectangle.Bounds.Y:F0}, " +
$"{page.RenderedRectangle.Bounds.Width:F0}x" +
$"{page.RenderedRectangle.Bounds.Height:F0}) - " +
$"\"{preview}\"";
PdfTextElement entryElement = new PdfTextElement(entry, smallFont)
{
X = xLeft,
Y = crtYPos,
Width = contentWidth
};
var entryElementInfo = pdfEditor.AddText(currentPage, entryElement);
currentPage = entryElementInfo.LastPageRectangle.PageNumber;
crtYPos = (int)entryElementInfo.LastPageRectangle.Bounds.Bottom + 4;
EnsureSpaceOnPage(ref crtYPos, ref currentPage, 30, pdfEditor, contentHeight, topMargin);
}
byte[] outPdfBuffer = pdfEditor.Save();
FileResult fileResult = new FileContentResult(outPdfBuffer, "application/pdf");
fileResult.FileDownloadName = "PdfTextEditDemo.pdf";
return fileResult;
}
private string LoadAlphabetText()
{
string textsPath = GetDemoTextsPath();
string alphabetFilePath = Path.Combine(textsPath, "Alphabet.txt");
if (System.IO.File.Exists(alphabetFilePath))
return System.IO.File.ReadAllText(alphabetFilePath);
// Fallback so the demo runs even without the alphabet file.
return "The quick brown fox jumps over the lazy dog. " +
"Pack my box with five dozen liquor jugs. " +
"Sphinx of black quartz, judge my vow. " +
"How vexingly quick daft zebras jump. ";
}
private void EnsureSpaceOnPage(ref int crtYPos, ref int currentPage, int requestedHeight, PdfEditor pdfEditor, int contentHeight, int topMargin)
{
if (crtYPos + requestedHeight > contentHeight + topMargin)
{
currentPage = pdfEditor.AddPage();
crtYPos = topMargin;
}
}
private Add_Text_to_Existing_PDF_ViewModel SetViewModel()
{
var model = new Add_Text_to_Existing_PDF_ViewModel();
HttpRequest request = ControllerContext.HttpContext.Request;
UriBuilder uriBuilder = new UriBuilder
{
Scheme = request.Scheme,
Host = request.Host.Host,
Path = request.PathBase.ToString() + request.Path.ToString(),
Query = request.QueryString.ToString()
};
if (request.Host.Port != null)
uriBuilder.Port = (int)request.Host.Port;
string currentPageUrl = uriBuilder.Uri.AbsoluteUri;
string rootUrl = currentPageUrl.Substring(
0, currentPageUrl.Length - "Add_Text_to_Existing_PDF".Length);
// Default input is empty.pdf so this demo edits a fresh
// blank A4 page. The user can upload another PDF or paste
// a different URL
model.PdfFileUrl = rootUrl + "/DemoAppFiles/Input/PDF_Files/empty.pdf";
return model;
}
private string GetDemoFilesPath() => m_hostingEnvironment.ContentRootPath + "/wwwroot" + "/DemoAppFiles/Input/";
private string GetDemoImagesPath() => Path.Combine(GetDemoFilesPath(), "Image_Files");
private string GetDemoFontsPath() => Path.Combine(GetDemoFilesPath(), "Font_Files");
private string GetDemoTextsPath() => Path.Combine(GetDemoFilesPath(), "Text_Files");
}
}