Update on purging DGN linestyles from AutoCAD drawings using .NET

Before the break, I had a characteristically insightful comment from Gilles Chanteau on a post regarding the DGNPURGE implementation that was released as a Hotfix for AutoCAD 2013 and 2014. As usual with these things, sometimes you don't see the wood for the trees once you've started down a particular path. Here's the comment in question:

Just a thought, you use: for (long i = 1; i < handseedTotal; i++) ...
to scan the whole database, it's a nice way i use too searching for proxies for example. But here you're looking only for entities to check their owners.
Won't it be more efficient to only iterate all BlockTableRecords?

In this case, Gilles cast fresh eyes on the solution and indeed found a better way to implement it (I ended up changing very few lines of code but it certainly made the implementation cleaner and much more efficient).

Today's post shares this implementation – which at this stage probably won't make it into an update to the DGN Hotfix, but people can certainly build the code from this post into an updated DLL, should they see the benefit – but also discusses a problem I spent some time troubleshooting earlier today.

The issue had been raised by Brock Priebe just before the break in a blog comment on the same post, but it wasn't something I could reproduce from my side: sometimes the DGN linestyle-related objects just end up turning into zombies. (Please don't worry: the walking dead aren't attacking along with the killer robots… for those unaware of the history, proxy objects were originally called zombies, back before the terminology was sanitized by the marketing department. 😉

Anyway, the point is that the DGNPURGE tool simply isn't designed to delete the proxy objects that get created when the DGN linestyle strokes aren't resurrected on drawing open. For each inaccessible object, it reports:

Unable to erase stroke (AcDbZombieObject): eNotAllowedForThisProxy.

In passing, just before the break, I pointed Brock to the Zombie Killer app that Gilles has published to Autodesk Exchange (nice bit of circularity, there :-), which apparently did help get rid of the DGN-related proxy objects in this situation.

But the question came up again, this afternoon, both on the discussion group and in an email from Jason Olesky. Jason had found that this problem wasn't drawing-specific, it was actually machine-specific. I'd seen it reported once or twice before, but had assumed it was due to drawing corruption rather than a problem with certain product installations.

We discussed back and forth by email and ended up realising that certain systems – some of which have been newly installed, which I find worrying – simply don't have the Registry entries to demand-load the AcDgnLS.dbx module when DGN linestyle information is found in a DWG. Jason found that copying the information across from a functioning system allowed this to work properly.

Here's the Registry export from my system, in case it helps someone:

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Autodesk\ObjectDBX\R19.1\Applications\AcDgnLS]
"LOADCTRLS"=dword:00000009
"LOADER"="AcDgnLS.dbx"
"DESCRIPTION"="DGN Line Style Component"

I still don't know why the Registry entries don't get created for some AutoCAD Civil 3D 2014 installations (I wonder if there isn't a problem with the second-stage installer, given that this is a key under HKLM rather than HKCU and Jason mentioned that a number of the users of problematic machines in his organisation hadn't actually run C3D before). In any case, knowing there's a workaround will hopefully be of help to people who run into this situation.

Incidentally, you will get exactly the same behaviour if you set DEMANDLOAD to 0 (rather than the default value of 3, which allows DBX and ARX modules to be demand-loaded on proxy detection and command invocation, respectively). This didn't end up being the cause of the problem here, but certainly helped confirm that the .DBX module not being loaded was the cause.

Here's the updated C# implementation that I mentioned earlier. To build it into a usable .NET DLL, you'll also need the ReferenceFiler implementation (that my project has in the ReferenceFiler.cs file, in case) from the original post, of course.

using System;

using System.IO;

using System.Runtime.InteropServices;

using Autodesk.AutoCAD.ApplicationServices.Core;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

 

namespace DgnPurger

{

  public class Commands

  {

    const string dgnLsDefName = "DGNLSDEF";

    const string dgnLsDictName = "ACAD_DGNLINESTYLECOMP";

 

    public struct ads_name

    {

      public IntPtr a;

      public IntPtr b;

    };

 

    [CommandMethod("DGNPURGE")]

    public static void PurgeDgnLinetypes()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      PurgeDgnLinetypesInDb(doc.Database, doc.Editor);

    }

 

    [CommandMethod("DGNPURGEEXT")]

    public static void PurgeDgnLinetypesExt()

    {

      var doc =

        Application.DocumentManager.MdiActiveDocument;

      var ed = doc.Editor;

 

      var pofo = new PromptOpenFileOptions("\nSelect file to purge");

 

      // Use the command-line version if FILEDIA is 0 or

      // CMDACTIVE indicates we're being called from a script

      // or from LISP

 

      short fd = (short)Application.GetSystemVariable("FILEDIA");

      short ca = (short)Application.GetSystemVariable("CMDACTIVE");

 

      pofo.PreferCommandLine = (fd == 0 || (ca & 36) > 0);

      pofo.Filter = "DWG (*.dwg)|*.dwg|All files (*.*)|*.*";

 

      // Ask the user to select a DWG file to purge

 

      var pfnr = ed.GetFileNameForOpen(pofo);

      if (pfnr.Status == PromptStatus.OK)

      {

     &#
160; 
// Make sure the file exists

        // (it should unless entered via the command-line)

 

        if (!File.Exists(pfnr.StringResult))

        {

          ed.WriteMessage(

            "\nCould not find file: \"{0}\".",

            pfnr.StringResult

          );

          return;

        }

 

        try

        {

          // We'll just suffix the selected filename with "-purged"

          // for the output location. This file will be overwritten

          // if the command is called multiple times

 

          var output =

            Path.GetDirectoryName(pfnr.StringResult) + "\\" +

            Path.GetFileNameWithoutExtension(pfnr.StringResult) +

            "-purged" +

            Path.GetExtension(pfnr.StringResult);

 

          // Assume a post-R12 drawing

 

          using (var db = new Database(false, true))

          {

            // Read the DWG file into our Database object

 

            db.ReadDwgFile(

              pfnr.StringResult,

              FileOpenMode.OpenForReadAndReadShare,

              false,

              ""

            );

 

            // No graphical changes, so we can keep the preview

            // bitmap

 

            db.RetainOriginalThumbnailBitmap = true;

 

            // We'll store the current working database, to reset

            // after the purge operation< /p>

 

            var wdb = HostApplicationServices.WorkingDatabase;

            HostApplicationServices.WorkingDatabase = db;

 

            // Purge unused DGN linestyles from the drawing

            // (returns false if nothing is erased)

 

            if (PurgeDgnLinetypesInDb(db, ed))

            {

              // Check the version of the drawing to save back to

 

              var ver =

                (db.LastSavedAsVersion == DwgVersion.MC0To0 ?

                  DwgVersion.Current :

                  db.LastSavedAsVersion

                );

 

              // Now we can save

 

              db.SaveAs(output, ver);

 

              ed.WriteMessage(

                "\nSaved purged file to \"{0}\".",

                output

              );

            }

 

            // Still need to reset the working database

 

            HostApplicationServices.WorkingDatabase = wdb;

          }

        }

        catch (Autodesk.AutoCAD.Runtime.Exception ex)

        {

          ed.WriteMessage("\nException: {0}", ex.Message);

        }

      }

    }

 

    // Helper function to be shared between our command

    // implementations

 

    private static bool PurgeDgnLinetypesInDb(Database db, Editor ed)

    {

      using (var tr = db.TransactionManager.StartTransaction())

      {

        // Start by getting all the "complex" DGN li
netypes

        // from the linetype table

 

        var linetypes = CollectComplexLinetypeIds(db, tr);

 

        // Store a count before we start removing the ones

        // that are referenced

 

        var ltcnt = linetypes.Count;

 

        // Remove any from the "to remove" list that need to be

        // kept (as they have references from objects other

        // than anonymous blocks)

 

        var ltsToKeep =

          PurgeLinetypesReferencedNotByAnonBlocks(db, tr, linetypes);

 

        // Now we collect the DGN stroke entries from the NOD

 

        var strokes = CollectStrokeIds(db, tr);

 

        // Store a count before we start removing the ones

        // that are referenced

 

        var strkcnt = strokes.Count;

 

        // Open up each of the "keeper" linetypes, and go through

        // their data, removing any NOD entries from the "to

        // remove" list that are referenced

 

        PurgeStrokesReferencedByLinetypes(tr, ltsToKeep, strokes);

 

        // Erase each of the NOD entries that are safe to remove

 

        int erasedStrokes = 0;

 

        foreach (ObjectId id in strokes)

        {

          try

          {

            var obj = tr.GetObject(id, OpenMode.ForWrite);

            obj.Erase();

            if (

              obj.GetRXClass().Name.Equals("AcDbLSSymbolComponent")

            )

            {

              EraseReferencedAnonBlocks(tr, obj);

            }

            erasedStrokes++;

          }

          catch (System.Exception ex)

          {

            ed.WriteMessage(

              "\nUnable to erase stroke ({0}): {1}",

              id.ObjectClass.Name,

              ex.Message

            );

          }

        }

 

        // And the same for the complex linetypes

 

        int erasedLinetypes = 0;

 

        foreach (ObjectId id in linetypes)

        {

          try

          {

            var obj = tr.GetObject(id, OpenMode.ForWrite);

            obj.Erase();

            erasedLinetypes++;

          }

          catch (System.Exception ex)

          {

            ed.WriteMessage(

              "\nUnable to erase linetype ({0}): {1}",

              id.ObjectClass.Name,

              ex.Message

            );

          }

        }

 

        // Remove the DGN stroke dictionary from the NOD if empty

 

        bool erasedDict = false;

 

        var nod =

          (DBDictionary)tr.GetObject(

            db.NamedObjectsDictionaryId, OpenMode.ForRead

          );

 

        ed.WriteMessage(

          "\nPurged {0} unreferenced complex linetype records" +

          " (of {1}).",

          erasedLinetypes, ltcnt

        );

 

        ed.WriteMessage(

          "\nPurged {0} unreferenced strokes (of {1}).",

          erasedStrokes, strkcnt

      &#
160; );

 

        if (nod.Contains(dgnLsDictName))

        {

          var dgnLsDict =

            (DBDictionary)tr.GetObject(

              (ObjectId)nod[dgnLsDictName],

              OpenMode.ForRead

            );

 

          if (dgnLsDict.Count == 0)

          {

            dgnLsDict.UpgradeOpen();

            dgnLsDict.Erase();

 

            ed.WriteMessage(

              "\nRemoved the empty DGN linetype stroke dictionary."

            );

 

            erasedDict = true;

          }

        }

 

        tr.Commit();

 

        // Return whether we have actually found anything to erase

 

        return (

          erasedLinetypes > 0 || erasedStrokes > 0 || erasedDict

        );

      }

    }

 

    // Collect the complex DGN linetypes from the linetype table

 

    private static ObjectIdCollection CollectComplexLinetypeIds(

      Database db, Transaction tr

    )

    {

      var ids = new ObjectIdCollection();

 

      var lt =

        (LinetypeTable)tr.GetObject(

          db.LinetypeTableId, OpenMode.ForRead

        );

      foreach (var ltId in lt)

      {

        // Complex DGN linetypes have an extension dictionary

        // with a certain record inside

 

        var obj = tr.GetObject(ltId
,
OpenMode.ForRead);

        if (obj.ExtensionDictionary != ObjectId.Null)

        {

          var exd =

            (DBDictionary)tr.GetObject(

              obj.ExtensionDictionary, OpenMode.ForRead

            );

          if (exd.Contains(dgnLsDefName))

          {

            ids.Add(ltId);

          }

        }

      }

      return ids;

    }

 

    // Collect the DGN stroke entries from the NOD

 

    private static ObjectIdCollection CollectStrokeIds(

      Database db, Transaction tr

    )

    {

      var ids = new ObjectIdCollection();

 

      var nod =

        (DBDictionary)tr.GetObject(

          db.NamedObjectsDictionaryId, OpenMode.ForRead

        );

 

      // Strokes are stored in a particular dictionary

 

      if (nod.Contains(dgnLsDictName))

      {

        var dgnDict =

          (DBDictionary)tr.GetObject(

            (ObjectId)nod[dgnLsDictName],

            OpenMode.ForRead

          );

 

        foreach (var item in dgnDict)

        {

          ids.Add(item.Value);

        }

      }

 

      return ids;

    }

 

    // Remove the linetype IDs that have references from objects

    // other than anonymous blocks from the list passed in,

    // returning the ones removed in a separate list

 

    private static ObjectIdCollection

      PurgeLinetypesReferencedNotByAnonBlocks(

        Database db, Transaction tr, ObjectIdCollection ids

      )

    {

      var keepers = new ObjectIdCollection();

 

      // Open the block table record

 

      var bt =

        (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);

      foreach (var btrId in bt)

      {

        // Open each block definition in the drawing

 

        var btr =

          (BlockTableRecord)tr.GetObject(btrId, OpenMode.ForRead);

 

        // And open each entity in each block

 

        foreach (var id in btr)

        {

          // Open the object and check its linetype

 

          var obj = tr.GetObject(id, OpenMode.ForRead, true);

          var ent = obj as Entity;

          if (ent != null && !ent.IsErased)

          {

            if (ids.Contains(ent.LinetypeId))

            {

              // If the owner does not belong to an anonymous

              // block, then we take it seriously as a reference

 

              var owner =

                (BlockTableRecord)tr.GetObject(

      
            ent.OwnerId,
OpenMode.ForRead

                );

              if (

                !owner.Name.StartsWith("*") ||

                owner.Name.ToUpper() == BlockTableRecord.ModelSpace||

                owner.Name.ToUpper().StartsWith(

                  BlockTableRecord.PaperSpace

                )

              )

              {

                // Move the linetype ID from the "to remove" list

                // to the "to keep" list

 

                ids.Remove(ent.LinetypeId);

                keepers.Add(ent.LinetypeId);

              }

            }

          }

        }

      }

      return keepers;

    }

 

    // Remove the stroke objects that have references from

    // complex linetypes (or from other stroke objects, as we

    // recurse) from the list passed in

 

    private static void PurgeStrokesReferencedByLinetypes(

      Transaction tr,

      ObjectIdCollection tokeep,

      ObjectIdCollection nodtoremove

    )

    {

      foreach (ObjectId id in tokeep)

      {

        PurgeStrokesReferencedByObject(tr, nodtoremove, id);

      }

    }

 

    // Remove the stroke objects that have references from this

    // particular complex linetype or stroke object from the list

    // passed in

 

    private static void PurgeStrokesReferencedByObject(

      Transaction tr, ObjectIdCollection nodIds, ObjectId id

    )

    {

      var obj = tr.GetObject(id, OpenMode.ForRead);

      if (obj.ExtensionDictionary != ObjectId.Null)

      {

        // Get the extension dictionary

 

        var exd =

          (DBDictionary)tr.GetObject(

            obj.ExtensionDictionary, OpenMode.ForRead

          );

 

        // And the "DGN Linestyle Definition" object

 

        if (exd.Contains(dgnLsDefName))

        {

          var lsdef =

            tr.GetObject(

              exd.GetAt(dgnLsDefName), OpenMode.ForRead

            );

 

          // Use a DWG filer to extract the references

 

          var refFiler = new ReferenceFiler();

          lsdef.DwgOut(refFiler);

 

          // Loop through the references and remove any from the

          // list passed in

 

          foreach (ObjectId refid in refFiler.HardPointerIds)

          {

            if (nodIds.Contains(refid))

            {

              nodIds.Remove(refid);

            }

 

            // We need to recurse, as linetype strokes can reference

            // other linetype strokes

 

            PurgeStrokesReferencedByObject(tr, nodIds, refid);

          }

        }

      }

      else if (

        obj.GetRXClass().Name.Equals("AcDbLSCompoundComponent") ||

        obj.GetRXClass().Name.Equals("AcDbLSPointComponent")

  &#
160;   )

      {

        // We also need to consider compound components, which

        // don't use objects in their extension dictionaries to

        // manage references to strokes...

 

        // Use a DWG filer to extract the references from the

        // object itself

 

        var refFiler = new ReferenceFiler();

        obj.DwgOut(refFiler);

 

        // Loop through the references and remove any from the

        // list passed in

 

        foreach (ObjectId refid in refFiler.HardPointerIds)

        {

          if (nodIds.Contains(refid))

          {

            nodIds.Remove(refid);

          }

 

          // We need to recurse, as linetype strokes can reference

          // other linetype strokes

 

          PurgeStrokesReferencedByObject(tr, nodIds, refid);

        }

      }

    }

 

    // Erase the anonymous blocks referenced by an object

 

    private static void EraseReferencedAnonBlocks(

      Transaction tr, DBObject obj

    )

    {

      var refFiler = new ReferenceFiler();

      obj.DwgOut(refFiler);

 

      // Loop through the references and erase any

      // anonymous block definitions

      //

      foreach (ObjectId refid in refFiler.HardPointerIds)

      {

        BlockTableRecord btr =

          tr.GetObject(refid, OpenMode.ForRead) as BlockTableRecord;

        if (btr !=
null && btr.IsAnonymous)

        {

          btr.UpgradeOpen();

          btr.Erase();

        }

      }

    }

  }

}

12 responses to “Update on purging DGN linestyles from AutoCAD drawings using .NET”

  1. Kean, at what point will the functionality of the hotfix become part of CORE AutoCAD?

    Can you post a table of some sort describing the state of this functionality in 2013 and 2014 including each version patched with the various SP's available.

    I do not have a good handle on which version(s) are equipped with what, or if every version currently out needs the HotFix.

    Thanks!

  2. RK,

    As you know I can't make predictions about the future when it comes to product releases.

    It's worth bearing in mind that the Hotfix actually has two things: it has a modified .DBX to stop the problem from propagating plus it has a clean-up tool that allows you to purge DGN linestyles when you have unwanted ones.

    My piece was obviously on the clean-up side of things, but I'd guess you're talking more about the .DBX change (as the clean-up tools isn't really product specific: it's easy to take the code and get it working on old versions, too).

    I think AutoCAD 2014 SP 1 probably has the .DBX fix, judging from the readme, but it doesn't have the clean-up tool.

    Regards,

    Kean

  3. The old version worked fine. I cannot get the new DgnLsPurge.dll to load. Yes, I have "unblocked" it.

    I get this error:

    Assembly file name: C:Program FilesAutodeskAutoCAD Civil 3D 2013DgnLsPurge.dll Cannot load assembly. Error details: System.IO.FileLoadException: Could not load file or assembly 'file:///C:Program FilesAutodeskAutoCAD Civil 3D 2013DgnLsPurge.dll' or one of its dependencies. Operation is not supported. (Exception from HRESULT: 0x80131515)

    File name: 'file:///C:Program FilesAutodeskAutoCAD Civil 3D 2013DgnLsPurge.dll' ---> System.NotSupportedException: An attempt was made to load an assembly from a network location which would have caused the assembly to be sandboxed in previous versions of the .NET Framework. This release of the .NET Framework does not enable CAS policy by default, so this load may be dangerous. If this load is not intended to sandbox the assembly, please enable the loadFromRemoteSources switch. See go.microsoft.com/fwlink/?LinkId=155569 for more information.
    at System.Reflection.RuntimeAssembly._nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, RuntimeAssembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection, Boolean suppressSecurityChecks)
    at System.Reflection.RuntimeAssembly.InternalLoadAssemblyName(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection, Boolean suppressSecurityChecks)
    at System.Reflection.RuntimeAssembly.InternalLoadFrom(String assemblyFile, Evidence securityEvidence, Byte[] hashValue, AssemblyHashAlgorithm hashAlgorithm, Boolean forIntrospection, Boolean suppressSecurityChecks, StackCrawlMark& stackMark)
    at System.Reflection.Assembly.LoadFrom(String assemblyFile)
    at Autodesk.AutoCAD.Runtime.ExtensionLoader.Load(String fileName)
    at loadmgd()

  4. Just regarding the Civil 3d registry entry. On my machine the AcDgnLS.dbx entry is located under HKCU. The registry export is below.

    Windows Registry Editor Version 5.00

    [HKEY_CURRENT_USER\Software\Autodesk\ObjectDBX\R19.1\Applications\AcDgnLS]
    "LOADCTRLS"=dword:00000009
    "LOADER"="AcDgnLS.dbx"
    "DESCRIPTION"="DGN Line Style Component"

  5. Interesting - thanks, Andrew. I expect the second-stage installer places entries under HKCU (unless it runs with elevated privileges - it's been a while since I looked at all that).

    Kean

  6. This may be old news, but I found another way of purging DGN linetypes:

    Open the drawing.

    Click on the FIle Menu

    Click on Export

    Enter a new or temporary file name and select Block (*.dwg) from the "Files of type" pulldown.

    Clixk on Save

    When prompted "Enter name of existing block or [= (block=output file)/* (whole drawing)] <define new="" drawing="">:" type an asterix (*) for the whole drawing then press enter.

    The program will save the new file.

    Close the original file with the DGN linetypes and open the new file to check it. If it's ok, do a "Save as" to the old file name to overwrite the old file.

    This has worked every time I've needed to do it.

    1. You`re a genious!

    2. Brilliant. I've been p*ssing about trying to get the hotfix to work without success, but this is just too easy.

    3. I'm pretty using your method you will lose all C3D objects. We have always saved back to get rid of the .dgn styles if we don't have any C3D.

    4. An interesting caveat I've found in this (Thanks for the idea, BTW!)
      I exported the entire file, not exporting as a block as indicated above.
      I've had multiple files xref'd to the 'infected file' - and when you would export, it would bind the xrefs to the new file as blocks. Not a bad issue, but annoying. What I've discovered is that if you unload (not detach - just unload) your xref's prior to performing the export, they remain xrefs vs. being bound in as blocks in the new, exported file.

  7. Why didn't Autodesk fix the underlying problem? The DGNLSDEF and ACAD_DGNLINESTYLECOMP aren't being mapped correctly when things (like lines and polylines with the DGN Linetypes) are copied from one drawing to another. Happen to have some code to fix the mapping between the DGNLSDEF and the ACAD_DGNLINESTYLECOMP? Thanks.

    1. I don't have any news on this, unfortunately - it's not an area I'm currently involved in. Please post to the relevant discussion forum - someone there should be able to provide clarity.

      Merry Christmas,

      Kean

Leave a Reply to Romans Cancel reply

Your email address will not be published. Required fields are marked *