Friday, June 28, 2019

Attach PDF Underlay - How to Handle Multiple Page PDF File

PDF underlay was introduced in AutoCAD 2010 (I believe, but I could be wrong). As late comer of an AutoCAD native object, the existing AutoCAD COM APIs only provide very limited means to handle it: one cannot create (i.e. attaching PDF underlay) it programmatically. Fortunately, AutoCAD .NET APIs give programmers full access to this new object, so that it can be created/updated/deleted with .NET API code properly.

There have been lots of efforts and tools made to convert/import graphics in PDF file into AutoCAD drawing as native AutoCAD entities in many years. Lately, AutoCAD finally added its own functionality to convert graphics in PDF file into native AutoCAD entities. This trend may reduce the need to use PDF as underlay in drawing. But, as long as PDF underlay remains in AutoCAD, there are always chances for AutoCAD programmers to deal with it in code.

There is not much information/sample code available on line regarding using .NET APIs to handle PDF underlay. Autodesk's ADN DevBlock - AutoCAD has this article, which is the only sample code I could find so far.

The real technical difficulty for us to deal with PDF underlay, as far as I can tell, occurs when the PDF file has multiple pages and only selected page or pages to be attached as underlay or underlays into a drawing. There was a question asked in AutoCAD's .NET discussion forum asked more than a year ago and re-asked again recently, which went unanswered so far. I have just spent a bit of time on coding PDF underlay, focusing on attaching selected page or pages in PDF file. So, I share the know-how I have learnt in this article.

Here are the things we, as programmer, need to pay attention:

1. The Relation Between PdfDefinition and PdfReference

In the surface, at least from CAD user's point of view, attaching PDF underlay into AutoCAD drawing is very similar to attaching another drawing as Xref, or inserting a block from a block file (*.dwg). So the relation between PdfDefinition and PdfReference is a bit similar to the relation between BlockTableRecord and BlockReference. However, for BlockTableRecord, it is stored in BlockTable, keyed with its name, while for PdfDefinition, it is stored in a named dictionary, call "ACAD_PDFDEFINITIONS". Each dictionary entry key is named by following convention of "[FileName] - [Page #]", if the PdfDefinition is created by AutoCAD built-in command, but it can also be any string value. PdfReference's name property is read/write-accessible, as opposed to BlockReference's name property as read-only (i.e. it is the name of the BlockTableRecord). Also, as programmer, we need to pay extra attention to PdfDefinition's read/write-able property "ItemName", and PdfReference's read/write-able property "NameOfSheet". These 2 properties somehow affect one another when one is set. More on this later.

When adding new BlockTableRecord to BLockTable, the BlockTableRecord's name must be unique. When a new BlockTableRecord is created by inserting a block drawing database, if the block name is the same as an existing BlockTableRecord, AutoCAD simply update the existing BlockTableRecord with the same name. However, since new PdfDefinition is added to DBDictionary.SetAt() with a string key, it is possible to create a new PdfDefinition (thus a new ObjectId) and set it to the PdfDefinition's NamedDictionary with the same key (i.e. previous PdfDefinition's ObjectId in the DBDictionaryEntry is replaced). If the previous PdfDefinition had a corresponding PdfReference, that PdfReference would not be updated to be the reference of the new PdfDefinition, because it had "hard link" to the previous PdfDefinition's ObjectId. The end result is that the PdfReference of previous PdfDefinition would remain visible in AutoCAD editor, but if selected, the Properties window would not show PdfUnderlay related properties (because of its definition is no longer found in PdfDefinition Named Dictionary). And if the drawing is closed and reopened in AutoCAD, the old PdfReference would no longer be there. One can run the code from the aforementioned article of Autodesk's ADN DevBlock - AutoCAD multiple times (make sure to add PdfReferences in different positions, so that to make things more visually verifiable).

What I have learnt here is, with BlockTableRecord, we can simply decide whether we want to add new one, or update existing one by its name (hence the usability of SymbolTable.Has() method). But for Pdf underlay, PdfDefinition's key in the NamedDictionary does not indicates the PdfDefinition is to the same PDF file as another PdfDefinition. So, when create PdbDefinition, in spite the convention is to use the file name (plus " - [page #]) as the key, we need to always check if the key is already used in the NamedDictionary to make sure the new PdfDefinition not replace an existing one, even the 2 definition may point to the same PDF source file.

2. Handle Multiple PDF Pages

Each PdfReference only shows one PDF page visually. Obviously, the attaching process (creating PdfReference object) needs to know which page in PDF file is to be the PdfReference instance. Here is what PdfDefinition.ItemName, PdfReference.NameOfSheet and PdfReference.Name come into play when the PDF source file has multiple pages:

a. Creating PdfDefinition without setting its ItemName property and creating a corresponding PdfReference without setting its NameOfSheet property.

In this case, the PdfReference shows the first page of the PDF file.

b. Creating PdfDefinition with setting its ItemName property to a valid page number (a string value), but creating corresponding PdfReference without setting its NameOfSheet property.

In this case, PdfReference's NameOfSheet property is set to the same as PdfDefinition's ItemName property (page number in string value) when the PdfReference.DefinitionId property is assigned to a PdfDefinition.

c. Creating PdfDefinition without setting its ItemName property, but creating corresponding PdfReference with its NameOfSheet property to a valid page number (string value).

In this case, the PdfDefinition's ItemName property would be set to the same value as NameOfSheet property of the PdfReference. Also interestingly, this could happen when the PdfDefinition is not opened explicitly in the transaction where the PdfReference is created and its NameOfShoeet property is set; meaning AutoCAD opens the PdfDefinition object behind the scene and updates its ItemName property automatically when PdfRefernece's NameOfSheet property is set.

d. When setting either PdfDefinition's ItemName property or PdfReference's NameOfSheet property, the string value number be a valid page number (from "1" to the max page number). If the value is not a numeric, an eInvalidValue exception is raised, while if the value is out valid page range, an eOutOfRange exception is raised. Obviously setting either ItemName property in PdfDefinition, or NameOfSheet property in PdfReference decides which PDF page showing as underlay. There is no need to set both properties, but if you do, the last one being set wins, by natural logic, it usually is NameOfSheet property of the PdfReference, because PdfReference can only be created properly after PdfDefinition being already created.

e. PdfDefinition does not have a "Name" property, while PdfReference's Name property is read/write-accessible. After a PdfReference is "newed" and before its DefinitionId is assigned to a PdfDefinition, PdfReference.Name cannot be set: an eNullObjectId error would be raised. Once DefinitionId is assigned, the PdfDefinition's key in named directory would be assigned to the PdfReference's Name property. However, if s PdfReference's DefinitionId is set and its Name property is set to whatever value, that value will be used to update the corresponding PdfDefinition's DNDictionaryEntry's key in "ACAD_PDFDEFINITIONS" named dictionary by AutoCAD behind the scene.

f. Since AutoCAD knows if the value (page number) for ItemName/NameOfSheet is valid or in range, it is obvious that when PdfDefinition's SourceFileName property is set, AutoCAD does something in the background (reading the PDF file, thus knowing how many pages in the PDF file). However, the .NET APIs does not provide anything in this regard to let our programming work easier. When we code our PDF underlay attaching process, we need to know how many pages in a PDF exist, so that we can attach desired page to as a PDF underlay, or allow the user to choose which page to attach.

So, the solution is outside AutoCAD .NET APIs, it is all up to us, the programmer, to know how many pages in a PDF file prior to the underlay attaching process. Fortunately, when working with MS .NET platform, it is rather easy for this task. There is well known open licensed PDF manipulation tool iTextSharp, which was developed originally for Java platform and being ported to .NET platform later. Lately, the iTextSharp's .NET version has been overhauled as iText.

With iTextSharp, or iText (or whatever PDF manipulation software of your choice), we can examine the PDF file to determine how many pages in the it; and then, we may present a UI, such as the one with AutoCAD's built-in UI for command "PDFATTACH". How cool the UI can be would depend on the PDF manipulation software you choose: from simply listing all page numbers, or listing preview image of all pages, for user to choose. In my code example, I used iText library for my own PDF underlay process.

3. Example of PDF Underlay Attaching Code

a. Class that determines the page count in a PDF file

public class PdfUtils
    {
        public static int GetPdfDocumentPageCount(string pdfFileName)
        {
            var count = 0;
            using(var doc = GetReadOnlyPdfDocumentFromFile(pdfFileName))
            {
                count = doc.GetNumberOfPages();
            }
 
            return count;
        }
 
        #region private methods
 
        private static PdfDocument GetReadOnlyPdfDocumentFromFile(string fileName)
        {
            if (!File.Exists(fileName))
            {
                throw new FileNotFoundException(
                    $"Cannot find PDF file\n{fileName}.");
            }
 
            if (!Path.GetFileName(fileName).ToUpper().EndsWith(".PDF"))
            {
                throw new InvalidOperationException(
                    $"Not a *.PDF file: {Path.GetFileName(fileName)}");
            }
 
            var reader = new PdfReader(fileName);
            return new PdfDocument(reader);
        }
 
        #endregion
    }


b. An UI for user to choose PDF page(s)


The UI is simplified with only one multi-select-able Listbox, listing all available pages in a PDF file. A suitable PDF viewer control could be placed on the UI to show page content visually, which would help user to make selection.

The form's code behind:

using System;
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Windows.Forms;
 
namespace PdfUnderlayAttach
{
    public partial class dlgSelectPdfPages : Form
    {
        public dlgSelectPdfPages()
        {
            InitializeComponent();
        }
 
        public void SetAvailablePages(int pages)
        {
            for (int i=1; i<=pagesi++)
            {
                lstPages.Items.Add("Page " + i);
            }
 
            lstPages.SelectedIndex = -1;
        }
 
        public IEnumerable<int> SelectedPages
        {
            get
            {
                return from index in lstPages.SelectedIndices.Cast<int>()
                       orderby index ascending
                       select index + 1;
            }
        }
 
        private void LstPages_SelectedIndexChanged(object senderEventArgs e)
        {
            btnOK.Enabled = lstPages.SelectedItems.Count > 0;
        }
    }
}

c. Class that does the actual underlay attaching work

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Runtime;
using System.Collections.Generic;
using System.Linq;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(PdfUnderlayAttach.PdfUnderlayCommand))]
 
namespace PdfUnderlayAttach
{
    public class PdfUnderlayCommand
    {
        [CommandMethod("MyPdfAttach")]
        public static void InsertPdfUnderlay()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var pdfFile = SelectPdfFileForOpening();
            if (!string.IsNullOrEmpty(pdfFile))
            {
                try
                {
                    var pageCount = PdfUtils.GetPdfDocumentPageCount(pdfFile);
                    IEnumerable<intselectedPageNumbers;
                    if (pageCount == 1)
                        selectedPageNumbers = new List<int>(new int[] { 1 });
                    else
                    {
                        selectedPageNumbers = AskUserForPageNumbers(pageCount);
                    }
 
                    if (selectedPageNumbers != null && selectedPageNumbers.Count() > 0)
                    {
                        AttachPdfUnderlays(dwgpdfFileselectedPageNumbers);
                    }
                }
                catch (System.Exception ex)
                {
                    ed.WriteMessage("\nError:\n{0}."ex.Message);
                }
                finally
                {
                    
                }
            }
 
            Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
        }
 
        #region private methods
 
        private static string SelectPdfFileForOpening()
        {
            string fName = null;
 
            using (var dlg = new System.Windows.Forms.OpenFileDialog())
            {
                dlg.Title = "Select PDF File";
                dlg.Filter = "PDF File (*.pdf)|*.pdf";
                dlg.Multiselect = false;
                dlg.InitialDirectory = @"C:\Temp\MultiPagePdf";
                if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
                {
                    fName = dlg.FileName;
                }
            }
 
            return fName;
        }
 
        private static IEnumerable<intAskUserForPageNumbers(int pageCount)
        {
            IEnumerable<intnumbers = null;
 
            using (var dlg=new dlgSelectPdfPages())
            {
                dlg.SetAvailablePages(pageCount);
                var res = CadApp.ShowModalDialog(CadApp.MainWindow.Handle, dlgfalse);
                if (res== System.Windows.Forms.DialogResult.OK)
                {
                    numbers = dlg.SelectedPages;
                }
            }
 
            return numbers;
        }
 
        private static void AttachPdfUnderlays(
            Document dwgstring pdfFileIEnumerable<intpageNumbers)
        {
            foreach (var pageNumber in pageNumbers)
            {
                if (!SelectInsertionPoint(dwg.Editor,pageNumber,  out Point3d position))
                {
                    dwg.Editor.WriteMessage("\n*Cancel*");
                    return;
                }
                AttachPdfUnderlay(dwg.Database, pdfFilepageNumberposition);
            }
        }
 
        private static ObjectId AttachPdfUnderlay(
            Database dbstring pdfFileNameint pageNumberPoint3d insPoint)
        {
            var pdfRefId = ObjectId.Null;
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var pdfDefId = CreatePdfDefinition(dbpdfFileNamepageNumbertran);
                pdfRefId = CreatePdfReference(pdfDefIddbpdfFileNamepageNumberinsPointtran);
                tran.Commit();
            }
 
            return pdfRefId;
        }
 
        private static bool SelectInsertionPoint(Editor edint pageNumberout Point3d point)
        {
            point = Point3d.Origin;
 
            var res = ed.GetPoint($"\nSelect insertion point for page {pageNumber} of the PDF file:");
            if (res.Status== PromptStatus.OK)
            {
                point = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        private static ObjectId CreatePdfDefinition(
            Database dbstring pdfFileNameint pageNumberTransaction tran)
        {
            var defId = ObjectId.Null;
 
            var pdfDicId = GetPdfDefinitionDictionary(dbtran);
            var dic = (DBDictionary)tran.GetObject(pdfDicIdOpenMode.ForWrite);
            var fName = System.IO.Path.GetFileNameWithoutExtension(pdfFileName);
            var defKey =  fName + " - " + pageNumber;
 
            int i = 0;
            while (dic.Contains(defKey))
            {
                i++;
                defKey = fName + "_" + i.ToString().PadLeft(2, '0') + " - " + pageNumber;
            }
 
            var pdfDef = new PdfDefinition();
            pdfDef.SourceFileName = pdfFileName;
 
            defId = dic.SetAt(defKeypdfDef);
            tran.AddNewlyCreatedDBObject(pdfDeftrue);
 
            return defId;
        }
 
        private static ObjectId GetPdfDefinitionDictionary(Database dbTransaction tran)
        {
            ObjectId dicId = ObjectId.Null;
 
            var namedDic = (DBDictionary)tran.GetObject(db.NamedObjectsDictionaryId, OpenMode.ForRead);
            var pdfDicKey = UnderlayDefinition.GetDictionaryKey(typeof(PdfDefinition));
            if (!namedDic.Contains(pdfDicKey))
            {
                var dic = new DBDictionary();
                namedDic.UpgradeOpen();
                dicId = namedDic.SetAt(pdfDicKeydic);
                tran.AddNewlyCreatedDBObject(dictrue);
            }
            else
            {
                dicId = namedDic.GetAt(pdfDicKey);
            }
 
            return dicId;
        }
 
        private static ObjectId CreatePdfReference(
            ObjectId pdfDefIdDatabase dbstring pdfFileNameint pageNumberPoint3d positionTransaction tran)
        {
            var pdfId = ObjectId.Null;
 
            var space = (BlockTableRecord)tran.GetObject(db.CurrentSpaceId, OpenMode.ForWrite);
 
            var pdf = new PdfReference();
            pdf.DefinitionId = pdfDefId;
            pdf.SetDatabaseDefaults(db);
            pdf.Position = position;
 
            pdf.NameOfSheet = pageNumber.ToString();
 
            pdfId = space.AppendEntity(pdf);
            tran.AddNewlyCreatedDBObject(pdftrue);
 
            return pdfId;
        }
 
        #endregion
    }
}

c. Add references to iText libraries into AutoCAD add-in project

As aforementioned, in order to know page count in a PDF prior to attaching underlay process, we need some software tool that can deal with PDF file. Since iText is the latest replacement for iTextSharp, I used iText in this project. To add iText reference to this project, go to NuGet Package Manager UI, search for "itext", then choose "itext7" package to be added to the project.

While iText is meant for replacing iTextSharp, one thing that does not please me is iText comes with a group of 12 Dll-files, as opposed to old good single Dll-file of iTextSharp (with bigger file size, of course), all of them have to be deployed with the small AutoCAD add-in Dll-file; not to mention more possible Dll-files may also come into play, if I want to incorporate a PDF viewing component in the UI for user to select PDF pages.

As usual, see this video clip showing how the code works.


Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.