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