Purging DGN linestyles from drawings not open in the AutoCAD editor

This request came in a few weeks ago regarding the source code for the recently posted DGN clean-up tool:

Would you modify the tool so that it can be processed by the equivalent figure ObjectDBX (of course for AutoCAD, not RealDWG)? I would like to give the Database object to the function, and the tool would have to process them in batch (a tool for loading drawings on my side).

It seemed like a reasonable enough request, so I spent some time putting together a version of the code that can be used on non-editor resident drawings. I changed the existing DGNPURGE command to make use of a helper function that takes two arguments – the Database to work on and the Editor to display information to the user – and then used that same function to purge unwanted DGN linestyles from a drawing selected by the user.

I took some file selection code from this post and some bits and pieces from this post, as well as picking up this helpful tip from our friends at The Swamp to open the drawing for read in order for the thumbnail bitmap to be retained properly in the output drawing.

Here's the resultant C# code. It should be trivial to either build this into a RealDWG app or simply extend it to work on a folder of drawings. As it stands the DGNPURGE command and its new sibling, DGNPURGEEXT, should be scriptable using ScriptPro 2.0 or a custom build of it (using either a full AutoCAD-based product or the Core Console).

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;

    };

 

    [DllImport("acdb19.dll",

      CharSet = CharSet.Unicode,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "acdbHandEnt")]

    public static extern int acdbHandEnt(string h, ref ads_name n);

 

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

 

      // To determine the references from objects in the database,

      // we need to open every object. One reasonably efficient way

      // to do so is to loop through all handles in the possible

      // handle space for this drawing (starting with 1, ending with

      // the value of "HANDSEED") and open each object we can

 

      // Get the last handle in the db

 

      var handseed = db.Handseed;

 

      // Copy the handseed total into an efficient raw datatype

 

      var handseedTotal = handseed.Value;

 

      // Loop from 1 to the last handle (could be a big loop)

 

      var ename = new ads_name();

 

      for (long i = 1; i < handseedTotal; i++)

      {

        // Get a handle from the counter

 

        var handle = Convert.ToString(i, 16);

 

        // Get the entity name using acdbHandEnt()

 

        var res = acdbHandEnt(handle, ref ename);

 

        if (res != 5100) // RTNORM

          continue;

 

        // Convert the entity name to an ObjectId

 

        var id = new ObjectId(ename.a);

 

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

        }

      }

    }

  }

}

14 responses to “Purging DGN linestyles from drawings not open in the AutoCAD editor”

  1. Thank you very much, it works! However, before a loop reading drawings (which is not here) is the need to remember the workingdatabase, and after leaving the loop to restore it. Otherwise, it comes to crash AutoCAD.

  2. Yes, that's why I showed that in the DGNPURGEEXT implementation. You could put that into the helper function, of you prefer, but as it's redundant for DGNPURGE I chose to leave it outside.

    Kean

  3. I have other problems with the drawings on a different topic: Flattening drawings. Express flatten not cope, superflatten too. Maybe I could send you some drawings for analysis and can you do about it remedied? I am not a member of ADN, and I think that it is so nasty thing that no one will not. This should solve a pro.

  4. Help please. This tool is going to crash in the attached drawing (link).

    dropbox.com/s/ku9bagkaak6v0sg/4228-1000-15-DW-500_RA_1.dwg

  5. Gilles Chanteau Avatar
    Gilles Chanteau

    Hi Kean,

    Is there a reason you P/Invoke acdbHandEnt rather than using db.TryGetObjectId(new Handle(i), out id) ?

  6. Hi Gilles,

    I seem to recall there was a reason, but I can't remember what it was off the top of my head. I'll take a look after the weekend (I'm still on holiday, this week) and will let you know if indeed there was one.

    Cheers,

    Kean

  7. Hi again Gilles,

    Apologies for the delayed reply.

    The reason is pretty simple: according to my original post I picked that part of the code up from this post on the AutoCAD DevBlog.

    When I get some time I'll give it a try to see whether TryGetObjectId() gives comparable results.

    Thanks & regards,

    Kean

  8. Gilles Chanteau Avatar

    Hi Kean,

    By my side, I use TryGetObjectId() and it seems to give comparable result (I don't know about performances).
    In the post on the AutoCAD DevBlog you mention, there's a comment from Maxence Delannoy which suggest to use TryGetObjectId() too, French thinking ?...

  9. Hi Gilles,

    Cool - thanks for letting me know. I'll be sure to take a look.

    You might well be right about it being down to French thinking. I'd be more inclined to agree if Philippe, the author of the post, wasn't also French. 😉

    I see Maxence's comment was posted some time before I picked the code up. Shame on me for not having spotted it sooner.

    Regards,

    Kean

  10. how we can use it?
    pls. help me

  11. How we can "DGNPURGE" use without opening AutoCAD?

  12. This version of the command works on drawings not loaded in the AutoCAD editor, but AutoCAD still needs to be running. You could either build the code into a .NET DLL and NETLOAD it into the Core Console (search this blog for more on that) or copy/paste the code into a RealDWG app if you really want to run this outside of AutoCAD (you'd need to license the RealDWG toolkit for this, of course).

    Kean

  13. Hi,

    I just found this in my spam comments. Is it still an issue for you? If so, let me know and I'll take a look.

    Regards,

    Kean

Leave a Reply to badziewiak Cancel reply

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