In this previous post, we saw some of the issues around executing standard Python code to de-skew raster images inside IronPython (and the effects those differences can have on the results).
In this post, we’re going to build the ability to execute our Python code from a .NET module loaded inside AutoCAD with the help of IronPython. The next step will be to add in an HTML5 user interface that calls into AutoCAD using the JavaScript API introduced in AutoCAD 2014.
Things have changed a bit since we first saw IronPython inside AutoCAD, most notably the additional of the dynamic keyword in C#, which makes life easier. And we were solving a different problem with that series of posts, that of calling through to AutoCAD’s .NET API from an IronPython module. In this case we simply want to execute our Python code to generate an image file on disk which we’ll then pick up and run with inside our C# command definition. Which is certainly a more straightforward task.
I used IronPython 2.7.3 to develop and test this code – which is now over a year old – and I’ve just noticed that 2.7.4 was released over the weekend. I’ll give that a try and post a comment if anything needs to be updated when using this latest (at the time of writing) version.
You’ll see from the below code that we’re actually using _engine.Runtime.UseFile() rather than _engine.ExecuteFile(), which we’ve used in the past. This returns a dynamic object corresponding to the module we’ve loaded, which we can then call into directly to execute the deskew() function with the arguments we want to pass in. Which is all pretty handy.
Here’s the C# code we’re going to use to host the IronPython runtime and use it to execute our Python code:
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using IronPython.Hosting;
using Microsoft.Scripting.Hosting;
[assembly: CommandClass(typeof(DeskewRaster.Commands))]
namespace DeskewRaster
{
public class Commands
{
private ScriptEngine _engine = null;
private ScriptRuntime _runtime = null;
private dynamic _scope = null;
private SimpleLogger _logger = new SimpleLogger();
private string _owd = null;
[CommandMethod("DESKEW_IMAGE", CommandFlags.NoHistory)]
public void DeskewRasterImage()
{
const string recBase = "ADSKEW_";
var doc =
Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
var db = doc.Database;
_owd = Directory.GetCurrentDirectory();
try
{
var asm = Assembly.GetExecutingAssembly();
var expath = Path.GetDirectoryName(asm.Location) + "\\";
var pypath = expath + "Python\\";
var pyfile = pypath + "deskew.py";
var pr = ed.GetString("\nName of input image");
if (pr.Status != PromptStatus.OK)
return;
var name = new Uri(pr.StringResult).LocalPath;
pr = ed.GetString("\nName of output image");
if (pr.Status != PromptStatus.OK)
return;
var outname = new Uri(pr.StringResult).LocalPath;
pr = ed.GetString("\nWindow coordinates");
if (pr.Status != PromptStatus.OK)
return;
var coords = pr.StringResult.Split(",".ToCharArray());
if (coords.Length != 8)
{
ed.WriteMessage(
"\nExpecting 8 coordinate values, received {0}.",
coords.Length
);
return;
}
var pdr = ed.GetDouble("\nWidth over height");
if (pdr.Status != PromptStatus.OK)
return;
var xscale = pdr.Value;
Directory.SetCurrentDirectory(pypath);
dynamic deskmod = UsePythonScript(pyfile);
if (deskmod != null)
{
deskmod.deskew(
name, outname,
Convert.ToInt32(coords[0]),
Convert.ToInt32(coords[1]),
Convert.ToInt32(coords[2]),
Convert.ToInt32(coords[3]),
Convert.ToInt32(coords[6]),
Convert.ToInt32(coords[7]),
Convert.ToInt32(coords[4]),
Convert.ToInt32(coords[5]),
xscale,
_logger
);
using (var tr = doc.TransactionManager.StartTransaction())
{
var dictId =
RasterImageDef.GetImageDictionary(db);
if (dictId.IsNull)
{
// Image dictionary doesn't exist, create new
dictId =
RasterImageDef.CreateImageDictionary(db);
}
// Open the image dictionary
var dict =
(DBDictionary)tr.GetObject(
dictId,
OpenMode.ForRead
);
// Get a unique record name for our raster image
// definition
int i = 0;
string recName = recBase + i.ToString();
while (dict.Contains(recName))
{
i++;
recName = recBase + i.ToString();
}
var rid = new RasterImageDef();
// Set its source image
rid.SourceFileName = outname;
// Load it
rid.Load();
dict.UpgradeOpen();
ObjectId defId = dict.SetAt(recName, rid);
// Let the transaction know
tr.AddNewlyCreatedDBObject(rid, true);
var ppr =
ed.GetPoint("\nFirst corner of de-skewed raster");
if (ppr.Status != PromptStatus.OK)
return;
// Call our jig to define the raster
var jig =
new RectangularRasterJig(
defId,
ed.CurrentUserCoordinateSystem,
ppr.Value,
xscale
);
var prj = ed.Drag(jig);
if (prj.Status != PromptStatus.OK)
{
rid.Erase();
return;
}
// Get our entity and add it to the modelspace
var ri = (RasterImage)jig.GetEntity();
var bt =
(BlockTable)tr.GetObject(
db.BlockTableId,
OpenMode.ForRead
);
var btr =
(BlockTableRecord)tr.GetObject(
bt[BlockTableRecord.ModelSpace],
OpenMode.ForWrite
);
btr.AppendEntity(ri);
tr.AddNewlyCreatedDBObject(ri, true);
// Create a reactor between the RasterImage and the
// RasterImageDef to avoid the "unreferenced"
// warning in the XRef palette
RasterImage.EnableReactors(true);
ri.AssociateRasterDef(rid);
tr.Commit();
}
}
}
catch (System.Exception ex)
{
ed.WriteMessage(
"\nProblem executing script: {0}", ex.Message
);
}
finally
{
Directory.SetCurrentDirectory(_owd);
}
}
private dynamic UsePythonScript(string file)
{
// If the file exists, let's load and execute it
// (we could/should probably add some more robust
// exception handling here)
dynamic ret = null;
if (File.Exists(file))
{
try
{
if (_runtime == null)
{
_engine = Python.CreateEngine();
_scope = _engine.CreateScope();
string dir =
Path.GetDirectoryName(Path.GetDirectoryName(file));
string cwd =
Environment.GetFolderPath(
Environment.SpecialFolder.ProgramFilesX86
) + @"\IronPython 2.7\Lib";
var paths = _engine.GetSearchPaths();
AddPathToList(dir, paths);
AddPathToList(cwd, paths);
_engine.SetSearchPaths(paths);
_runtime = _engine.Runtime;
}
ret = _runtime.UseFile(file);
}
catch (System.Exception ex)
{
var doc =
Application.DocumentManager.MdiActiveDocument;
var ed = doc.Editor;
ed.WriteMessage(
"\nProblem executing script: {0}.", ex.Message
);
}
}
return ret;
}
private static void AddPathToList(
string dir, ICollection<string> paths
)
{
if (
!String.IsNullOrWhiteSpace(dir) &&
!paths.Contains(dir)
)
{
paths.Add(dir);
}
}
}
}
This code defines a command with “no history” (which means it won’t show up in the command history or be available for automatic command completion while typing) called DESKEW_IMAGE. The idea is that when we implement the additional HTML UI for this application, it will (from its own command) launch the DESKEW_IMAGE command with the parameters as established in the UI. This is why we assume that the paths to the input and output image files have been URL-encoded, as they will be by the HTML UI.
So the command we have today is really just a helper that calls the Python code we saw last time and then – once the corrected image has been created – launches a jig based on code from this previous post to create a RasterImage referencing the image:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using System;
namespace DeskewRaster
{
public class RectangularRasterJig : EntityJig
{
Matrix3d _ucs;
Point3d _start = Point3d.Origin;
Point3d _end = Point3d.Origin;
double _xscale;
public RectangularRasterJig(
ObjectId defId,
Matrix3d ucs,
Point3d start,
double xscale
)
: base(new RasterImage())
{
_start = start;
_ucs = ucs;
_xscale = xscale;
RasterImage ri = (RasterImage)Entity;
ri.ImageDefId = defId;
// Create a near zero size default image,
// to avoid the boundary flicker
double size = Tolerance.Global.EqualPoint;
ri.Orientation =
new CoordinateSystem3d(
_start,
new Vector3d(xscale * size, 0, 0),
new Vector3d(0, size, 0)
);
ri.ShowImage = true;
}
protected override SamplerStatus Sampler(
JigPrompts prompts
)
{
JigPromptPointOptions opts =
new JigPromptPointOptions();
opts.UserInputControls =
(UserInputControls.Accept3dCoordinates |
UserInputControls.NoNegativeResponseAccepted);
opts.Message = "\nSecond corner of de-skewed raster: ";
// Get the point itself
PromptPointResult res = prompts.AcquirePoint(opts);
if (res.Status == PromptStatus.OK)
{
// Convert the supplied point into UCS
Point3d tmp =
res.Value.TransformBy(_ucs.Inverse());
// Check if changed (reduces flicker)
if (_end == tmp)
{
return SamplerStatus.NoChange;
}
else
{
_end = tmp;
return SamplerStatus.OK;
}
}
return SamplerStatus.Cancel;
}
protected override bool Update()
{
RasterImage ri = (RasterImage)Entity;
// Get offset between the two corners
Vector3d diff = _end - _start;
// Get the smallest of the X and Y
// (could also be the largest - this is a choice)
double size =
Math.Min(Math.Abs(diff.X), Math.Abs(diff.Y));
// If we're at zero size, don't update
if (size < Tolerance.Global.EqualPoint)
return false;
// Determing the image's orientation...
// The original will depend on the order of the corners
// It will be offset to the left and/or down depending
// on the values of the vector between the two points
Point3d orig;
// The axes stay the same, as we will always keep the
// image oriented the same way relative to the UCS
Vector3d xAxis = new Vector3d(size * _xscale, 0, 0);
Vector3d yAxis = new Vector3d(0, size, 0);
if (diff.X > 0 && diff.Y > 0) // Dragging top-right
orig = _start;
else if (diff.X < 0 && diff.Y > 0) // Top-left
orig = _start + new Vector3d(-size * _xscale, 0, 0);
else if (diff.X > 0 && diff.Y < 0) // Bottom-right
orig = _start + new Vector3d(0, -size, 0);
else // if (diff.X < 0 && diff.Y < 0) // Bottom-left
orig = _start - new Vector3d(size * _xscale, size, 0);
// Set the image's orientation in WCS
ri.Orientation =
new CoordinateSystem3d(
orig.TransformBy(_ucs),
xAxis.TransformBy(_ucs),
yAxis.TransformBy(_ucs)
);
return true;
}
public Entity GetEntity()
{
return Entity;
}
}
}
Something else that’s worth noting. We have a SimpleLogger class – borrowed and modified from this very helpful article – which reports progress back to AutoCAD via the command-line and the progress meter. If the Python code has a logger set it will call into it, causing AutoCAD’s UI to be updated during some code that’s quite fundamentally non-AutoCAD resident. I expect this to be a handy technique for people to use in various interop scenarios between AutoCAD and IronPython.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Windows.Forms;
using acApp = Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
namespace DeskewRaster
{
public class SimpleLogger
{
private UInt32 _entryCount = 0;
private Editor _ed =
acApp.Application.DocumentManager.
MdiActiveDocument.Editor;
private ProgressMeter _pm;
public class Entry
{
public enum EntryType
{
Info,
Warning,
Error,
Fault
}
private EntryType _entryType;
private DateTime _timestamp;
private String _msg;
private UInt32 _index;
private Entry()
{
}
public Entry(EntryType entryType, String msg, UInt32 index)
{
_msg = msg;
_timestamp = DateTime.Now;
_entryType = entryType;
_index = index;
}
public String msg { get { return _msg; } }
public DateTime timestamp { get { return _timestamp; } }
public EntryType entryType { get { return _entryType; } }
public UInt32 index { get { return _index; } }
public override string ToString()
{
return
String.Format(
"[{0}][{1}][{2}][{3}]", _timestamp, _index,
_entryType, _msg
);
}
}
private List<Entry> _entries = new List<Entry>();
public void Reset()
{
_entries = new List<Entry>();
}
public Int32 Count
{
get
{
return _entries.Count;
}
}
/// <summary>
/// Gets the first entry in log and removes it from the log.
/// Returns null if the log is empty.
/// </summary>
/// <returns></returns>
public Entry GetFirst()
{
Entry result = null;
if (_entries.Count > 0)
{
result = _entries[0];
_entries.RemoveAt(0);
}
return result;
}
/// <summary>
/// Retrives all the entries from the log. The log will be
/// empty after the operation has been executed.
/// </summary>
/// <returns></returns>
public List<Entry> GetAll()
{
List<Entry> result = _entries;
_entries = new List<Entry>();
return result;
}
public void CreateProgressMeter(string msg, int limit)
{
_pm = new ProgressMeter();
_pm.SetLimit(limit);
_pm.Start(msg);
AddInfo(msg);
}
public void Progress()
{
_pm.MeterProgress();
Refresh();
}
public void FinishProgressMeter()
{
_pm.Stop();
_pm.Dispose();
_pm = null;
Refresh();
}
public void Refresh()
{
Application.DoEvents();
}
public void AddInfo(String msg)
{
_entries.Add(
new Entry(Entry.EntryType.Info, msg, _entryCount++)
);
_ed.WriteMessage("{0}\n", msg);
Refresh();
}
public void AddWarning(String msg)
{
_entries.Add(
new Entry(Entry.EntryType.Warning, msg, _entryCount++)
);
_ed.WriteMessage("Warning: {0}\n", msg);
Refresh();
}
public void AddError(String msg)
{
_entries.Add(
new Entry(Entry.EntryType.Error, msg, _entryCount++)
);
_ed.WriteMessage("Error: {0}\n", msg);
Refresh();
}
public void AddFault(String msg)
{
_entries.Add(
new Entry(Entry.EntryType.Fault, msg, _entryCount++)
);
_ed.WriteMessage("Fault: {0}\n", msg);
Refresh();
}
public void AddFault(System.Exception ex)
{
String msg = ex.Message;
if (ex.InnerException != null)
msg += " (+INNER): " + ex.InnerException.Message;
_entries.Add(
new Entry(Entry.EntryType.Fault, msg, _entryCount++)
);
_ed.WriteMessage("Fault: {0}\n", msg);
Refresh();
}
}
}
For now we’re not actually going to see this code in action. For that we’re going to wait until we plug in the HTML UI, so we can make sense of what’s happening and see it all working together. I expect to post that early next week (please shout if you need to see something sooner :-).
Another possibility has occurred to me in recent days. If you actually want to avoid integrating IronPython into your application, it shouldn’t be too hard to create a web-service based on the Python code. This would allow us to call the web-service directly from the HTML5 UI and decouple the application even more from AutoCAD (we’d then just launch the command to insert the raster image).
Considering cloud providers, a strong contender for hosting this would be Google App Engine, as it’s very Python-centric (unsurprisingly, as the author of the language – Guido van Rossum – worked there from 2005 to late 2012 when he left to join Dropbox). This would be my first foray into using GAE – a fun way to get a feel for the technology.