Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
Markdown Monster - The Markdown Editor for Windows

Building a SnagIt Screen Capture Plugin for Live Writer


:P
On this page:

I spent a few hours a couple of days ago creating a plugin for Windows Live Writer that allows for easy screen captures. I've long been a huge fan of SnagIt from Techsmith and since SnagIt's capture functionality is available as a COM interface it's quite easy to expose the functionality in other applications. For example, I have SnagIt plugged into my Help Builder tool to provide for screen captures in the Help HTML content.

Blogging is kind of similar: image capturing is pretty vital and making the process directly integrated into Live Writer itself makes screen captures a real cinch. Combined with Live Writer's ability to send images directly to the Web site via HTTP image capturing and publishing has never been easier.

You can grab the plug in and code from:
http://www.west-wind.com/tools/snagitlivewriterplugin.asp

Here's what the plugin looks like inside of Windows Live Writer:

SnagIt Screen Capture Plug in for Live Writer

When you click the Insert SnagIt button this dialog pops up which exposes most of the SnagIt capture options. Clicking Capture then goes off and uses SnagIt's native image capture functionailty (you know the red box to capture whatever capture mode selection you've made) to capture the image to file. You can optionally pop up the image editor and edit the image the same way as you can with native SnagIt use.

Settings are not saved, unless you click the Save Settings link, which stores the settings for later reuse. The idea is that you typically have a standard set of capture settings and these are restored each time you capture. You can also opt to not save the image locally. Live Writer actually makes a copy of each image you embed into a post so there's no need to hang on to captured images. The plugin writes out the file and then delay deletes the file if you have the Don't save image file option set to reduce file clutter on the local drive.

 

Creating a Live Writer Plugin

One of the nice things about Live Writer is the fact that you can very easily create plugins for it. You can download a Windows Live Writer SDK which provides fairly complete documentation and a few simple examples of how to plug into Live Writer. There are a host of different types of plugins you can create, although it looks like all plugins only address the content in the editor, not the overall operation of Writer. For example, as far as I can tell you can't override the HTTP communication or the actual messages that Writer sends when communicating with the server which would be very cool!

In any case creating an add-in that inserts content into the active blog post at the cursor position is very simple to create. The concept of this plug in is pretty simple:

  • Create a class library project
  • Add a reference to the WindowsLive.Writer.Api assembly to your project
  • Implement the PlugIn API and handle the CreateContent method
  • Create the SnagItAutomation object
  • Bring up the Configuration Dialog that configures the object
  • Run the screen capture
  • Retrieve the filename that the capture writes to disk
  • Insert the image as an <img> tag into the blog post

At the highest level the implementation of Writer Content plugin is quite simple:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using WindowsLive.Writer.Api;
 
namespace SnagitScreenCapturePlugin
{
    [WriterPlugin( "7E113E74-A693-4bcd-8CF8-4C732654699C", 
        "SnagIt Screen Capture",        
         ImagePath = "Images.snagit.png",
         PublisherUrl = "http://www.west-wind.com/tools/SnagitLiveWriterPlugin.aspx",
         Description = "Embeds a screen capture image from SnagIt.")]
 
    [InsertableContentSource( "SnagIt Screen Capture" )]
    public class SnagitScreenCapturePlugin : ContentSource
    {
        public SnagitScreenCapturePlugin()
        {
        }
 
        public override DialogResult CreateContent(IWin32Window dialogOwner, ref string newContent)
        {        
            DialogResult dr = DialogResult.OK;
 
            // *** Result Output file captured
            string OutputFile = null;
 
            try
            {
                SnagItAutomation SnagIt = SnagItAutomation.Create();
                SnagIt.ActiveForm = Form.ActiveForm;
 
                SnagItConfigurationForm ConfigForm = new SnagItConfigurationForm(SnagIt);
                if (ConfigForm.ShowDialog() == DialogResult.Cancel)
                    return DialogResult.Cancel;
 
                OutputFile = SnagIt.CaptureImageToFile();
 
                SnagIt.SaveSettings();
            }
            catch (Exception ex)
            {
                MessageBox.Show("Failed to capture image:\r\n\r\n" + ex.Message,
                                "SnagIt Capture Exception", MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                return DialogResult.Cancel;
            }
 
            // *** Just embed the image
            if (!string.IsNullOrEmpty(OutputFile))                    
                newContent = @"<img src='file:///" + OutputFile + "'>\r\n";
 
            return dr;
        }
 
 
    }
}

This is really all there's to the plugin portion of this particular plugin. All the work that happens for displaying the dialog and running the screen capture has really nothing to do with the plugin itself and is freestanding. The CreateContent method only cares about a result value that is the content HTML that is to be embedded into the page along with a DialogResult return value that indicates whether the operation was successful.

Automating SnagIt with COM through .NET

At the crux of this plugin is the automation of SnagIt. SnagIt has a COM interface that can be automated quite easily. Although I might have chosen to simply import the COM type library as a COM Interop assembly, I decided I didn't want to do this as to avoid having to distribute another assembly with the plugin. So rather than use a COM Interop assembly the code I use utilizes Reflection to get at the COM properties and methods.

The key method of the SnagItAutomation class demonstrates how to use the SnagIt COM objects for capture automation:

 
/// <summary>
/// Captures an image to file and returns the filename
/// </summary>
/// <returns></returns>
public string CaptureImageToFile()
{
    FormWindowState OldState = this.ActiveForm.WindowState;
    if (this.ActiveForm != null)
        this.ActiveForm.WindowState = FormWindowState.Minimized;
 
    Application.DoEvents();
 
    /// *** Capture first access to check if SnagIt is installed
    try
    {
        wwUtils.SetPropertyExCom(this.SnagItCom, "OutputImageFile.Directory", this.CapturePath);
    }
    catch
    {
        throw new Exception("SnagIt isn't installed - COM Access failed.\r\nPlease install SnagIt from Techsmith Corporation (www.techsmith.com\\snagit).");
    }
 
 
    wwUtils.SetPropertyCom(this.SnagItCom,"EnablePreviewWindow",this.ShowPreviewWindow);
    wwUtils.SetPropertyExCom(this.SnagItCom,"OutputImageFile.Filename", "captured_Image.png");
 
    wwUtils.SetPropertyExCom(this.SnagItCom, "Input", this.CaptureMode);
 
 
    wwUtils.SetPropertyExCom(this.SnagItCom, "OutputImageFile.FileType", (int)this.OutputFileCaptureFormat);           
    wwUtils.SetPropertyExCom(this.SnagItCom,"Filters.ColorConversion.ColorDepth",this.ColorDepth);
    wwUtils.SetPropertyExCom(this.SnagItCom, "OutputImageFile.ColorDepth", this.ColorDepth);
 
    wwUtils.SetPropertyExCom(this.SnagItCom, "IncludeCursor", this.IncludeCursor);
 
    if (this.DelayInSeconds > 0)
    {
        wwUtils.SetPropertyExCom(this.SnagItCom, "DelayOptions.EnableDelayedCapture", true);
        wwUtils.SetPropertyExCom(this.SnagItCom, "DelayOptions.DelaySeconds", this.DelayInSeconds);
    }
 
 
    // *** Need to delay a little here so that the form has properly minimized first
    // *** especially under Vista
    for (int i = 0; i < 20; i++)
    {
        Application.DoEvents(); 
        Thread.Sleep(5);                
    }            
 
    wwUtils.CallMethodCom(this.SnagItCom, "Capture");
 
    try
    {
        bool TimedOut = true;
        while (true)
        {
            if ((bool)wwUtils.GetPropertyCom(this.SnagItCom, "IsCaptureDone"))
            {
                TimedOut = false;
                break;
            }
 
            Thread.Sleep(100);
            Application.DoEvents();
        }
    }
    // *** No catch let it throw
    finally
    {
        this._OutputCaptureFile = wwUtils.GetPropertyCom(this.SnagItCom, "LastFileWritten") as string;
 
        if (this.ActiveForm != null)
            this.ActiveForm.WindowState = OldState;
 
        Marshal.ReleaseComObject(this.SnagItCom);
    }
 
 
    // *** If deleting the file we'll fire on a new thread and then delay by 
    // *** a few seconds until Writer has picked up the image.
    if ((this.DeleteImageFromDisk))
    {
        Thread thread = new Thread( new ParameterizedThreadStart(DeleteImage));
        thread.Start(this.OutputCaptureFile);                
    }
 
 
    return this.OutputCaptureFile;
}

The code basically uses a COM instance - SnagItCom - that is created like this:

/// <summary>

/// Snagit COM Instance
/// </summary>        
public object SnagItCom
{
    get 
    {
        if (_SnagItCom != null)
            return _SnagItCom;
 
        try
        {
            Type loT = Type.GetTypeFromProgID(SNAGIT_PROGID);
            this._SnagItCom = Activator.CreateInstance(loT);
        }
        catch
        {
            return null;
        }
 
        return _SnagItCom; 
    }            
}
private object _SnagItCom = null;


The instance is just a raw COM object wrapper and then Reflection is used to access this COM instance to set properties and eventually call the Capture() method. Notice that code first minimizes Writer so that it doesn't get in the way of any captures you make.

The Capture method requires a little explanation - the method is asynchronous, so it returns immediately. So to determine when the capture is complete you need to continually check the IsCaptureDone property until it is true. The capture is done either when you cancelled the capture or when you saved the capture image to file.

At this point all that needs to happen is to retrieve the LastFileWritten property which returns the full path to the captured image file which is returned as part of the method call.

The Plugin's CreateContent method then picks up this file name builds the HTML image tag and returns this HTML back through the reference newContent parameter, which in turn injects the HTML into the writer document. The Image file link is simply created like this:

<img src="file:///c:\temp\testimage.png">

Live Writer then internally copies the file to its own temporary store and instead embeds a template reference into the document. If you were to check the HTML after an image was embedded you get:

<img src="$testimage.png">

Because Writer makes an internal copy, you can actually delete the captured image file from disk, and there's an option to delete the capture file on the options dialog. When this option is chosen, the plugin creates a new thread and fires up this thread and calls the DeleteImageFile() method of the class. This method then simply sleeps for 10 seconds and then deletes the image file specified.

Storing Settings

The plugin has a Save Settings option which persists all the settings that were made to the object in the registry. It uses Serialization to basically persist the state of the SnagItAutomation object into a binary buffer which is then stored in the registry:

/// <summary>
/// Saves the current settings of this object to the registry
/// </summary>
/// <returns></returns>
public bool SaveSettings()
{
    SnagItAutomation Snag = this;
 
    byte[] Buffer = null;
 
    if (!wwUtils.SerializeObject(Snag,out Buffer))
        return false;
 
    RegistryKey SubKey = Registry.CurrentUser.OpenSubKey(REGISTRY_STORAGE_SUBKEY,true);
    if (SubKey == null)
        SubKey = Registry.CurrentUser.CreateSubKey(REGISTRY_STORAGE_SUBKEY);
    if (SubKey == null)
        return false;
 
    SubKey.SetValue("ConfigData", Buffer, RegistryValueKind.Binary);
    SubKey.Close();
 
    return true;
}
 
/// <summary>
/// Factory method that creates teh SnagItAutomation object by trying to read last capture settings
/// from the registry.
/// </summary>
/// <returns></returns>
public static SnagItAutomation Create()
{
    byte[] Buffer = null;
 
 
    RegistryKey SubKey = Registry.CurrentUser.OpenSubKey(REGISTRY_STORAGE_SUBKEY);
    if (SubKey != null)
        Buffer = SubKey.GetValue("ConfigData",null,RegistryValueOptions.None) as byte[];
 
 
    if (Buffer == null)
        return new SnagItAutomation();
 
    // *** Force Assembly resolving code to fire so we can load the assembly
    AppDomain.CurrentDomain.AssemblyResolve +=
        new ResolveEventHandler(CurrentDomain_AssemblyResolve);
 
    SnagItAutomation SnagIt = wwUtils.DeSerializeObject(Buffer,typeof(SnagItAutomation)) as SnagItAutomation;
 
    // *** Unhook the event handler for the rest of the application
    AppDomain.CurrentDomain.AssemblyResolve -= new ResolveEventHandler(CurrentDomain_AssemblyResolve);
 
    if (SnagIt == null)
        return new SnagItAutomation();
 
    return SnagIt;
}

/// <summary>
/// Handle custom loading of the current assembly if the assmebly won't
/// resolve with the name.
/// </summary>
/// <param name="sender"></param>
/// <param name="args"></param>
/// <returns></returns>
static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args)
{
    try
    {
        Assembly assembly = System.Reflection.Assembly.Load(args.Name);
        if (assembly != null)
            return assembly;
    }
    catch { ;}
 
    return Assembly.GetExecutingAssembly();            
}

As I mentioned a couple of days ago there's an interesting problem that crops up with deserialization in that the plugin assembly can't be found by the deserializer in .NET because the plugin assembly actually lives in a not-known folder of the application which results in an Assembly load error. The work around for this is to manually hook the AssemblyResolve event on the AppDomain to ensure that the correct assembly is returned. In this case I was lucky enough to directly pass GetExecutingAssembly() back - in other situations Assembly.LoadFromFile() might do the trick by parsing the assembly filename out of the full assembly name and looking in a particular path.

 

So there you have it. Creating an add-in is pretty easy with Live Writer and it's one reason that I've started to get into Live Writer and have now weaned myself from Word. There are a few plugins that help in this respect including the set of plugins on CodePlex, which provide the vital capability to paste HTML and past VS.NET Code directly into Writer.

 

I meant this to be a quick little project and indeed getting the base capture functionality set up took only a couple of hours. But the devil's always in the details and cleaning up the UI for the dialog, dealing with the Save Settings issues I ran into and creating the install package all took a bit longer than it should have. Before I knew it the whole evening was gone <s>...

Anyway, I've posted the plug-in and the code, so you can play with it on your own. Hopefully you find it as useful as I do...

Posted in .NET  Live Writer  WebLog  

The Voices of Reason


 

Dan Neuman
March 19, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Rick: Just curious what you use for a blogging service? Is it a part of WWWC5.X, or how did you add it to your site?

Rick Strahl
March 19, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

The blogging engine on this site is a custom ASP.NET application I built. But most services include support for MetaBlogApi which supports posting from generic tools like Live Writer. The blogging engine that comes with West Wind Web Connection is a more basic implementation and currently it doesn't have MetaWebLogApi support.


April 19, 2007

# Claudio Lassala in Software Development: Screen Capture

Claudio Lassala in Software Development: Screen Capture

Anon
April 21, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Hi - doesn't work with Blogger. "Images cannot be published because the weblog does not support image publishing". One of those things I guess - but you might want to make that clear on your site before users download / install. Thx.

Rick Strahl
April 21, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Eh, that's Live Writer's problem not the problem of the plug-in <s>... The plug in just creates the content - Live Writer's responsible for uploading the content to various blog engines.

I'm pretty sure Live Writer does work with Blogger though. You might have to switch to using MetaWebLog API in the configuration.

Laurent Duveau
April 28, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Hi,
Great work Rick, I'll blog about this!
First time used I was stuck with an exception (Exception has been thrown by the target of an invocation). I finally found the cause : SnagIt was already running.

Also you made wrong copy/paste for property description, starting at "image cursor capture"... but not a big deal.

Thanks again for that.

# Customer Support - Integration with SnagIt


Jeff
November 08, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

I just discovered Windows Live Writer, and I'm loving it... Then I thought "Wow, it would be great if it worked with SnagIt... Then I discovered this!

It's great, and I'm not one to look a gift horse in the mouth, but I was wondering if you had given any thought to making a SnagIt "accessory" so one could launch SnagIt the usual way, and THEN dump the image into Live Writer. That would just kind of jive with my own workflow better. Maybe you know of someone who's already done this?

Li Yang
December 15, 2007

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Thank God now TechSmith provides free licenses for SnagIt 7 so I can start to use it.

Mike Moore
December 02, 2008

# re: Building a SnagIt Screen Capture Plugin for Live Writer

I am attempting to ues your SnagIt capture plugin for WLW. However, after pressing the "capture" button an error message appears saying:

Failed to capture image:

SnagIt isn't installed - COM Access failed.
Please install SnagIt from Techsmith Corporation.

I am running SnagIt v9.0.2.

Any ideas as to how to correct this?

Thanks.

VC
October 22, 2013

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Hi Rick,
Thanks for sharing these scripts. Just wanted to check if this solution works with Snagit V 11 ?

Thanks in advance,

VC

Rick Strahl
October 22, 2013

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Yup works all the way up to SnagIt 11.

Berne
November 05, 2013

# re: Building a SnagIt Screen Capture Plugin for Live Writer

Hi, did you ever try to send keys to snagit to start a capture? I would like to use the COM interface, but I have 3 snagit profiles (activated using CTRL-1, 2 and 3) and I need to activate the correct one. the COM interface does not expose the profiles OR the associated printer setup.

Rick Strahl
November 05, 2013

# re: Building a SnagIt Screen Capture Plugin for Live Writer

@Berne - sorry no. The COM interface only allows explicit captures so you have to specify exactly what to capture. You might want to check with Techsmith's docs - they have updated the API over the years and actually documented it better.

http://download.techsmith.com/snagit/docs/comserver/enu/snagitcom.pdf

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024