Wednesday, October 5, 2022

Extract Entity Image From Side-Loaded Database

There have been some discussions about getting image of an entity, and code samples could be found online. Most of them do the work with the entities in a drawing opened in AutoCAD.

I published 2 articles that could prove to be useful for this discussion:

In these 2 articles, I showed how we can use Autodesk.Autodesk.Internal.Utils.GetBlockImage() to generate the image of a block without having to open the drawing in AutoCAD as Document.

Seeing the question posted in the .NET discussion forum about exporting image of single entity, I thought it should be possible to use the same approach to do the job. With a short brain storm, I came up these steps to do this job:

1. As the user for drawing file name where the entity images to be extracted from;
2. Ask the user to select a folder to save the extracted images;
3. Open the drawing as side database;
4. Create an empty BlockTableRecord as a container to hold an entity from which the image is to be generated;
5. Loop through ModelSpace for each entity
a. clear entity from the container BlockTableRecord;
b. add a clone of the entity to the container BlockTableRecord;
c. Call GetBlockImage() method against the container BlockTableRecord;
d. Save the obtained imager;

With these steps in mind, I went ahead to implement them in code.

The class EntityImageExtracter:
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Internal;
using System;
using System.Linq;
 
namespace ExtractEntityImages
{
    public class EntityImageExtracter
    {
        private string _sourceDwg;
        private string _outputFolder;
 
        public EntityImageExtracter()
        {
 
        }
 
        // Notify the calling process
        public static Action<int> ExtractionStarted;
        public static Action<int> ExtractionProgressed;
        public static Action<int> ExtractionEnded;
 
        public void ExtractEntityImages(string sourceDwgstring outputFolder)
        {
            _sourceDwg = sourceDwg;
            _outputFolder = outputFolder;
 
            using (var db = new Database(falsetrue))
            {
                db.ReadDwgFile(_sourceDwg, FileOpenMode.OpenForReadAndAllShare, falsenull);
                ExtractImagesFromDatabase(db);
            }
        }
 
        #region private methods
 
        private void ExtractImagesFromDatabase(Database db)
        {
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(db), OpenMode.ForRead);
                var ids = model.Cast<ObjectId>();
 
                ExtractionStarted?.Invoke(ids.Count());
 
                var blkDef = CreateNewBlockTableRecord(db.BlockTableId, tran);
 
                var doneCount = 0;
                var count = 0;
                foreach(var id in ids)
                {
                    count++;
                    ExtractionProgressed?.Invoke(count);
 
                    var img = ExtractEntityImage(id, blkDef, tran);
                    if (img!=null)
                    {
                        //Save the image to file
                        var imgFile = $"{id.ObjectClass.DxfName}_{id.Handle.ToString()}.png";
                        img.Save(System.IO.Path.Combine(_outputFolder, imgFile));
                        doneCount++;
                        img.Dispose();
                    }
                }
 
                ExtractionEnded?.Invoke(doneCount);
 
                tran.Abort();
            }
        }
 
        private BlockTableRecord CreateNewBlockTableRecord(ObjectId btId, Transaction tran)
        {
            var bt = (BlockTable)tran.GetObject(btId, OpenMode.ForWrite);
 
            // make sure no duplicated block name
            var name = "ExtractImage";
            var i = 1;
            var blkName = $"{name}_{i}";
            while(bt.Has(blkName))
            {
                i++;
                blkName = $"{name}_{i}";
            }
 
            var blk = new BlockTableRecord();
            blk.Name = blkName;
            blk.Origin = Point3d.Origin;
 
            bt.Add(blk);
            tran.AddNewlyCreatedDBObject(blk, true);
 
            return blk;
        }
 
        private System.Drawing.Image ExtractEntityImage(
            ObjectId entId, BlockTableRecord tempBlk, Transaction tran)
        {
            var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
            var clone = ent.Clone() as Entity;
 
            // Use the center of the entity's extents at the block's origin
            MoveCenterToOrigin(clone);
            
            foreach (ObjectId id in tempBlk)
            {
                var dbObj = tran.GetObject(id, OpenMode.ForWrite);
                dbObj.Erase();
            }
 
            tempBlk.AppendEntity(clone);
            tran.AddNewlyCreatedDBObject(clone, true);
 
            // generate the image of the block definition
            // where only one entity exists, thus, the image of the entity
            var cl = Autodesk.AutoCAD.Colors.Color.FromColor(
                System.Drawing.Color.WhiteSmoke);
            var blkId = tempBlk.ObjectId;
            var imgPtr = Utils.GetBlockImage(blkId, 300, 300, cl);
            var image = System.Drawing.Image.FromHbitmap(imgPtr);
 
            return image;
        }
 
        private void MoveCenterToOrigin(Entity ent)
        {
            var exts = ent.GeometricExtents;
            var w = exts.MaxPoint.X - exts.MinPoint.X;
            var h= exts.MaxPoint.Y - exts.MinPoint.Y;
 
            var center = new Point3d(
                exts.MinPoint.X + w / 2.0, 
                exts.MinPoint.Y + h / 2.0, 
                exts.MinPoint.Z);
            var mt = Matrix3d.Displacement(center.GetVectorTo(Point3d.Origin));
            ent.TransformBy(mt);
        }
 
        #endregion
    }
}
The code is rather simple and straightforward. Following are the other code to make the process runnable.

A helper class for user to select drawing file and output folder:
using System.IO;
using System.Windows.Forms;
 
namespace ExtractEntityImages
{
    public class GenericHelper
    {
        public static string SelectSourceDrawingFile()
        {
            var fName = "";
            using (var dlg = new OpenFileDialog())
            {
                dlg.Title = "Select Drawing File";
                dlg.Filter = "AutoCAD Drawing (*.dwg)|*.dwg";
                dlg.Multiselect = false;
                if (dlg.ShowDialog() == DialogResult.OK)
                {
                    fName = dlg.FileName;
                }
            }
            return fName;
        }
 
        public static string SelectImageOutputFolder(string initPath="")
        {
            var folder = "";
 
            using (var dlg = new FolderBrowserDialog())
            {
                dlg.Description = "Select Image Output Folder";
                if (!string.IsNullOrEmpty(initPath))
                {
                    dlg.SelectedPath = initPath;
                }
                dlg.ShowNewFolderButton = true;
                if (dlg.ShowDialog()== DialogResult.OK)
                {
                    folder = dlg.SelectedPath;
                }
            }
 
            return folder;
        }
 
        public static void ClearImageOutputFolder(string path)
        {
            var files=Directory.GetFiles(path);
            if (files.Length>0)
            {
                foreach (var f in files)
                {
                    File.Delete(f);
                }
            }
        }
    }
}
The Command class to run the process:
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assembly: CommandClass(typeof(ExtractEntityImages.MyCommands))]
 
namespace ExtractEntityImages
{
    public class MyCommands
    {
        [CommandMethod("GetEntImages")]
        public static void ExtractEntityImagedFromDrawing()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            string sourceDwg = GenericHelper.SelectSourceDrawingFile();
            if (string.IsNullOrEmpty(sourceDwg))
            {
                ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            var initPath = dwg.IsNamedDrawing ?
                System.IO.Path.GetDirectoryName(dwg.Name) : "";
            var outputFolder = GenericHelper.SelectImageOutputFolder(initPath);
            if (string.IsNullOrEmpty(outputFolder))
            {
                ed.WriteMessage("\n*Cancel*\n");
                return;
            }
 
            GenericHelper.ClearImageOutputFolder(outputFolder);
 
            ProgressMeter meter = null;
            try
            {
                var extractedCount = 0;
 
                EntityImageExtracter.ExtractionStarted = (count) =>
                {
                    meter = new ProgressMeter();
                    meter.SetLimit(count);
                    meter.Start("Extracting entity images...");
                };
 
                EntityImageExtracter.ExtractionProgressed = (index) =>
                {
                    meter.MeterProgress();
                };
 
                EntityImageExtracter.ExtractionEnded = (totalCount) =>
                {
                    extractedCount = totalCount;
                };
 
                var extracter = new EntityImageExtracter();
                extracter.ExtractEntityImages(sourceDwg, outputFolder);
 
                ed.WriteMessage(
                    $"\nExtracted image count: {extractedCount}\n");
            }
            catch(System.Exception ex)
            {
                ed.WriteMessage(
                    $"\nError:\n{ex.Message}\n");
            }
            finally
            {
                if (meter!=null)
                {
                    meter.Stop();
                    meter.Dispose();
                }
            }
        }
    }
}

Since the entire process of generating image is done with an side-loaded database, so I did not bother to remove the container BlockTableRecord, as long as the side database is not to be saved for the changes.

I only test it with simple drawing, as following video clip shows, the result satisfies me with its speed and the quality of the image. The only thing in the whole thing that might be potentially problematic is the key factor of the approach - the method GetBlockImage() - is from Autodesk.AutoCAD.Internal namespace, meaning use it at your own risk, if Autodesk wants to break it for whatever reason.

See the video clip bellow: