September 2014

Sun Mon Tue Wed Thu Fri Sat
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30        










« AU 2013 classes available on-demand | Main | Forcing AutoCAD object snapping using .NET »

January 09, 2014

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)

      {

        // 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

 

            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 linetypes

        // 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

        );

 

        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")

      )

      {

        // 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();

        }

      }

    }

  }

}

blog comments powered by Disqus

Feed/Share

10 Random Posts