For those who have (quite understandably) not been following the story unfolding around this tool, here’s a quick timeline so far. I’ve largely pieced this together from emails, as I don’t typically tag blog updates with a date (something it seems I ought to consider starting).
- 12/12/12 – In response to a request from our Product Support team, the original implementation was posted to this blog.
- 03/28/13 – I posted an update to the original post with a minor fix to handle corrupted drawings a bit better (Update).
- 06/21/13 – A tool based on the (updated) code was published as an official tool (Update 2).
- 06/23/13 – Jimmy Bergmark identified an issue with the code, in that it purged information needed by compound linestyles even when in use. We removed the tool and I submitted an updated version of the code for review (Update 3).
The code has since been reviewed by the developers of the feature (thanks, Scott and Markus!), and a couple of suggestions have been made. Based on feedback – and confirmation of the specific compound linestyle components that contain direct references to other components – I’ve adjusted the code to look for two specific classes, “AcDbLSCompoundComponent” and “AcDbLSPointComponent” (rather than performing pattern-matching). I’ve also added a small section of code that follows the references on “AcDbLSSymbolComponent” objects and erases their associated anonymous blocks: this should remove (or at least reduce) the need to call a traditional PURGE after DGNPURGE has completed.
All in all these are minor changes – in addition to the more important fix of properly handling compound linestyle components – but worth publishing here for people to pick up.
The following C# code is now being rebuilt and tested by our QA team, and should form the basis of the republished tool (along with the “ReferenceFiler” implementation that has remained unchanged from the original post).
using System;
using System.Runtime.InteropServices;
using Autodesk.AutoCAD.ApplicationServices.Core;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Runtime;
using System.Collections.ObjectModel;
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 void PurgeDgnLinetypes()
{
var doc =
Application.DocumentManager.MdiActiveDocument;
var db = doc.Database;
var ed = doc.Editor;
using (var tr = doc.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
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."
);
}
}
tr.Commit();
}
}
// 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();
}
}
}
}
}
If you’d really like to build this yourself in advance of the official tool being available, this comment has some brief instructions. Hopefully the official tool will be reposted soon, but this should be of some help to people with an urgent need who are unfamiliar with .NET development.
Update:
The AutoCAD DGN Hotfix is now available. See this post for more details.