After a break of a week, I thought it was time to take the QR Code application a little further, after our previous versions creating basic QR codes at a fixed location, using user-specified corners and using a jig.
The code in this post adds quite a bit of functionality to the application:
- The ability to encode various types of data
- Calendar events
- Contact information
- Email addresses
- Geo-locations
- Phone numbers
- Plain text
- URLs
- The ability to edit QR Codes
- The data used to create the QR Code is attached to the raster image as XData and gets used for default values
- The ability to query the URL of an existing QR Code
- i.e. the Google Code URL used to create the raster image
- The ability to decode an existing QR Code
- The Google ZXing Decoder is launched automatically with the URL of the selected QR Code
- Handy for checking the QR Code just after creation
The UI is still command-line based, but it’s altogether possible to bolt on a fancy GUI, should one so wish (and I may well extend that in a future post, we’ll see). In the main part the UI options have been modelled after those available on the ZXing Generator (leaving out a few data-types that don’t seem very relevant, such as “SMS” and “WiFi network”).
The source project now contains a number of different C# files:
- Commands.cs – our command definitions
- QrInput.cs – user input functions for our command-line data entry
- QrEncoder.cs – encoder functions to generate a Google Chart URL for a set of data
- RbEncoder.cs – encoder functions to create a ResultBuffer from the user-specified data
- demand-loading.cs – our usual file to auto-generate demand-loading Registry keys
There’s nothing very remarkable about the code: it did occur to me as I was hard-coding the input paths for the various data-types that it might be interesting to generalise the mechanism to use some kind of “schema” of the various data fields and use a more dynamic approach to query and encode the information. But I decided not to go down that path, for now, at least.
I decided to implement a custom exception type in QrInput.cs to denote user cancellation, mainly because it was proving cumbersome to force the return type to encode that condition in some way (such as a null string or Double.NaN), and I didn’t want to fall back on ref or out parameters. So when the user cancels we accept the overhead of throwing and handling an exception, which certainly makes the code simpler.
There’s still potential for replacing the generation of the QR Codes themselves to be performed locally, rather than via a web service. I think – overall – that the benefits of the current approach do outweigh the effort required to use a local QR Code library, but this is certainly something that could change (feedback welcome!).
As the code now spans multiple files I won’t list them all, but here is the main Commands.cs file:
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using System;
using DemandLoading;
namespace QRCodes
{
public class QRCodeApplication : IExtensionApplication
{
public void Initialize()
{
try
{
RegistryUpdate.RegisterForDemandLoading();
}
catch
{ }
}
public void Terminate()
{
}
// Base record name, also used as regApp name
const string recBase = "ADNP_QR";
class SquareRasterJig : EntityJig
{
Matrix3d _ucs;
Point3d _start = Point3d.Origin;
Point3d _end = Point3d.Origin;
public SquareRasterJig(
ObjectId defId,
Matrix3d ucs,
Point3d start
) : base(new RasterImage())
{
_start = start;
_ucs = ucs;
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(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 QR Code: ";
// 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, 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, 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, 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;
}
}
// Create a QR Code
[CommandMethod("ADNPLUGINS", "QR", CommandFlags.Modal)]
static public void QRCode()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
ResultBuffer rb;
// Get the data from the user and encode it into a URL
string url =
QrInput.GetUrlForQrCode(ed, null, out rb);
if (String.IsNullOrEmpty(url))
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
// Get the image dictionary's ID, if it already
// exists
ObjectId dictId =
RasterImageDef.GetImageDictionary(db);
if (dictId.IsNull)
{
// If it doesn't, create a new one
dictId =
RasterImageDef.CreateImageDictionary(db);
}
// Open the image dictionary
DBDictionary 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();
}
RasterImageDef rid = new RasterImageDef();
try
{
// Set its source image
rid.SourceFileName = url;
// Load it
rid.Load();
}
catch
{
ed.WriteMessage(
"\nUnable to create image object. " +
"Here is the URL to the image: {0}",
url
);
return;
}
// Put the definition in the dictionary
dict.UpgradeOpen();
ObjectId defId = dict.SetAt(recName, rid);
// Let the transaction know about it
tr.AddNewlyCreatedDBObject(rid, true);
// Now we start the placement of the RasterImage
PromptPointResult ppr =
ed.GetPoint("\nFirst corner of QR Code: ");
if (ppr.Status != PromptStatus.OK)
return;
// Call our jig to place the raster
SquareRasterJig jig =
new SquareRasterJig(
defId,
ed.CurrentUserCoordinateSystem,
ppr.Value
);
PromptResult prj = ed.Drag(jig);
// If it was cancelled then return
// (will abort the transaction)
if (prj.Status != PromptStatus.OK)
return;
// Get our entity and add it to the current space
RasterImage ri = (RasterImage)jig.GetEntity();
BlockTableRecord btr =
(BlockTableRecord)tr.GetObject(
db.CurrentSpaceId,
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);
// Let's add our message information as XData,
// for later editing
AddRegAppTableRecord(recBase);
ri.XData = rb;
rb.Dispose();
tr.Commit();
}
}
// Edit a QR Code using originally entered data defaults
[CommandMethod("ADNPLUGINS", "QRE", CommandFlags.Modal)]
static public void QREdit()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Database db = doc.Database;
Editor ed = doc.Editor;
// Select a QR Code for editing
ObjectId riId = GetQrCode(doc);
if (riId == ObjectId.Null)
return;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
RasterImage ri =
tr.GetObject(riId, OpenMode.ForRead)
as RasterImage;
if (ri != null)
{
// Get the RasterImage's XData
ResultBuffer rb =
ri.GetXDataForApplication(recBase);
// Now call the same input routine as
// when creating, but pass our XData defaults
ResultBuffer rb2;
string url =
QrInput.GetUrlForQrCode(ed, rb, out rb2);
rb.Dispose();
if (String.IsNullOrEmpty(url))
return;
// If we have a valid string returned, set
// it on the RasterImageDef
RasterImageDef rid =
(RasterImageDef)tr.GetObject(
ri.ImageDefId,
OpenMode.ForWrite
);
rid.SourceFileName = url;
rid.Load();
// Store the new defaults as XData
ri.UpgradeOpen();
ri.XData = rb2;
rb2.Dispose();
}
tr.Commit();
}
}
// Print the URL for a particular QR Code
[CommandMethod("ADNPLUGINS", "QRU", CommandFlags.Modal)]
static public void QRUrl()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
string url = GetQrCodeUrl(doc);
if (!String.IsNullOrEmpty(url))
{
ed.WriteMessage("\nUrl: {0}", url);
}
}
// Decode a QR Code in the drawing via the ZXing decoder
[CommandMethod("ADNPLUGINS", "QRD", CommandFlags.Modal)]
static public void QRDecode()
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
string url = GetQrCodeUrl(doc);
if (!String.IsNullOrEmpty(url))
{
System.Diagnostics.Process.Start(
QrEncoder.EncodeQrCodeDecoderUrl(url)
);
}
}
// Unregister the application for future demand-loading
[CommandMethod("ADNPLUGINS", "REMOVEQR", CommandFlags.Modal)]
static public void RemoveQRCodes()
{
DemandLoading.RegistryUpdate.UnregisterForDemandLoading();
Editor ed =
Autodesk.AutoCAD.ApplicationServices.Application.
DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage(
"\nThe QRCodes plugin will not be loaded" +
" automatically in future editing sessions.");
}
// Helper function to select a QR Code and return its URL
static private string GetQrCodeUrl(Document doc)
{
string res = null;
Database db = doc.Database;
Editor ed = doc.Editor;
// Select a QR Code
ObjectId riId = GetQrCode(doc);
if (riId != ObjectId.Null)
{
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
RasterImage ri =
tr.GetObject(riId, OpenMode.ForRead)
as RasterImage;
if (ri != null)
{
RasterImageDef rid =
(RasterImageDef)tr.GetObject(
ri.ImageDefId,
OpenMode.ForRead
);
res = rid.SourceFileName;
}
tr.Commit();
}
}
return res;
}
// Helper function to select a QR Code raster image
static private ObjectId GetQrCode(Document doc)
{
PromptEntityOptions peo =
new PromptEntityOptions("\nSelect QR Code: ");
peo.SetRejectMessage("\nMust be a raster image.");
peo.AddAllowedClass(typeof(RasterImage), true);
PromptEntityResult per =
doc.Editor.GetEntity(peo);
if (per.Status != PromptStatus.OK)
return ObjectId.Null;
return per.ObjectId;
}
// Helper function to add a registered application
static void AddRegAppTableRecord(string regAppName)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
Editor ed = doc.Editor;
Database db = doc.Database;
Transaction tr =
doc.TransactionManager.StartTransaction();
using (tr)
{
RegAppTable rat =
(RegAppTable)tr.GetObject(
db.RegAppTableId,
OpenMode.ForRead,
false
);
if (!rat.Has(regAppName))
{
rat.UpgradeOpen();
RegAppTableRecord ratr =
new RegAppTableRecord();
ratr.Name = regAppName;
rat.Add(ratr);
tr.AddNewlyCreatedDBObject(ratr, true);
}
tr.Commit();
}
}
}
}
When we run our QR command we can use the command-line interface to generate the various types of QR Code listed above:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: CA
Event title: This is an event
Start date & time: 1/1/11 12pm
End date & time(optional): 1/1/11 2pm
Location (optional): Somewhere sunny
Description (optional): Should be a blast
First corner of QR Code:
Second corner of QR Code:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: CO
Name: Kean Walmsley
Phone number (optional): +41 (32) 723-9499
Email address (optional): [email protected]
Address (optional): Puits-Godet 6, Case Postale 35
Address 2 (optional): CH-2002 Switzerland
Website (optional): http://blogs.autodesk.com/through-the-interface
Memo (optional): Work details
First corner of QR Code:
Second corner of QR Code:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: E
Email address: [email protected]
First corner of QR Code:
Second corner of QR Code:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: G
Latitude: 48.163415
Longitude: 11.480711
Query (optional): Autodesk Neuchatel
First corner of QR Code:
Second corner of QR Code:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: P
Phone number: +41 (32) 723-9499
First corner of QR Code:
Second corner of QR Code:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: T
Text: This is just some plain text. With punctuation, but nonetheless it's
pretty plain
First corner of QR Code:
Second corner of QR Code:
Command: QR
Type of data to encode [CAlendar/COntact/Email/Geolocation/Phone/Text/Url]
<Text>: U
Url: http://blogs.autodesk.com/through-the-interface
First corner of QR Code:
Second corner of QR Code:
Here are the various QR Codes inside AutoCAD:
We can use the QRD command to check one at random. This passes the image’s URL to ZXing, another Google service that decodes QR Codes:
Update
A big thanks to Barry Ralphs for giving this a try and noticing that the QR command was not working in paperspace layouts. Which – with hindsight – is obvious, as it was adding it to the modelspace rather than the current space. I’ve gone ahead and updated the code (just one line changed, but I also removed a now-redundant opening of the BlockTable) and the project linked to above. My apologies to those of you who have already built it into an application.