TIBCO Spotfire Community

Welcome to TIBCO Spotfire Community Sign in | Join | Help

Loading images from an URL using Data Functions

In this example we will show how to write a simple data function executor extension that load images from a column of URLs and returns the images as binary large objects.

First we need to create a function executor which takes in an input parameter called “url” which contains strings that are supposed to be URLs and returns an output parameter names “image” that is a list of images as binary large objects.

namespace ImageFromURL
{
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Security;
using Spotfire.Dxp.Application.Extension;
using Spotfire.Dxp.Data;
using Spotfire.Dxp.Data.Collections;

/// <summary>
/// This is a simple function executor that loads images from URLs.
/// </summary>
public class ImageFromURLFunctionExecutor : CustomDataFunctionExecutor
{
/// <summary>
/// Execute a function invocation. The executor is expected to add the output results to the invocation
/// object.
/// </summary>
/// <param name="invocation">The function invocation.</param>
/// <returns>
/// The prompt models to return, the implementor is expected to use the yield return pattern.
/// </returns>
protected override IEnumerable<object> ExecuteFunctionCore(Spotfire.Dxp.Data.DataFunctions.DataFunctionInvocation invocation)
{
// Get the input as a row reader, this one always expect an url parameter.
DataRowReader urlReader = invocation.GetInput("url");

// Create a pageable list to store the result in.
PageableListSettings settings = new PageableListSettings();

// Since we only write the data sequentially we can get better performance by not
// allowing random writes.
settings.CanReplaceValue = false;
PageableList<BinaryLargeObject> output = new PageableList<BinaryLargeObject>(settings);

// Cursor that contains the input URL
DataValueCursor<string> cursor = (DataValueCursor<string>)urlReader.Columns[0].Cursor;

// Loop over all input rows.
while (urlReader.MoveNext())
{
if (!cursor.IsCurrentValueValid)
{
// Invalid value as input so return invalid value as output.
output.Add(null);
continue;
}

string url = cursor.CurrentValue;

try
{
// Use the standard web request API to retrieve the images.
Uri uri = new Uri(url);

WebRequest webRequest = WebRequest.Create(uri);

WebResponse response = webRequest.GetResponse();

MemoryStream memoryStream = new MemoryStream();
Stream responseStream = response.GetResponseStream();
byte[] buf = new byte[8 * 1024];
int n = 1;
while (n > 0)
{
n = responseStream.Read(buf, 0, buf.Length);
memoryStream.Write(buf, 0, n);
}

memoryStream.Seek(0, SeekOrigin.Begin);

BinaryLargeObject image = BinaryLargeObject.Create(memoryStream);
output.Add(image);
}
catch (UriFormatException)
{
output.Add(null);
}
catch (NotSupportedException)
{
output.Add(null);
}
catch (SecurityException)
{
output.Add(null);
}
}

// Assign the result.
invocation.SetOutput("image", new ImageReader(output));

// No need to prompt so just break.
yield break;
}

/// <summary>
/// Simple reader to return the images.
/// </summary>
private class ImageReader : CustomDataRowReader
{
/// <summary>
/// The list of the images as binary large objects.
/// </summary>
private readonly PageableList<BinaryLargeObject> list;

/// <summary>
/// The cursor to assign the result for the current row.
/// </summary>
private readonly MutableValueCursor<BinaryLargeObject> cursor;

/// <summary>
/// The columns returned by the reader.
/// </summary>
private readonly List<DataRowReaderColumn> columns;

/// <summary>
/// The current index to return.
/// </summary>
private int index = -1;

/// <summary>
/// Initializes a new instance of the <see cref="ImageReader"/> class.
/// </summary>
/// <param name="list">The list of the images as binary large objects.</param>
public ImageReader(PageableList<BinaryLargeObject> list)
{
this.list = list;
this.columns = new List<DataRowReaderColumn>();
this.cursor = (MutableValueCursor<BinaryLargeObject>)DataValueCursor.CreateMutableCursor(DataType.Binary);

// Say that the result is of type image/png..
DataColumnProperties properties = new DataColumnProperties();
properties.ContentType = "image/png";

// Create the metadata for the put column.
DataRowReaderColumn column =
new DataRowReaderColumn(
"Image",
DataType.Binary,
properties,
this.cursor);

this.columns.Add(column);
}


/// <summary>
/// The implementor should provide the result properties.
/// </summary>
/// <returns>The result properties.</returns>
/// <remarks>
/// This method is only called once.
/// </remarks>
protected override ResultProperties GetResultPropertiesCore()
{
// Table metadata can be returned here, but we have nothing to return for now.
return new ResultProperties();
}

/// <summary>
/// The implementor should implement this method to reset the
/// enumerator. If this method is called then the <see cref="M:Spotfire.Dxp.Data.DataRowReader.MoveNextCore"/>
/// method should return the first row again when called the next time.
/// </summary>
protected override void ResetCore()
{
this.index = -1;
}

/// <summary>
/// The implementor should provide a list of <see cref="T:Spotfire.Dxp.Data.DataRowReaderColumn"/>s that it
/// returns.
/// </summary>
/// <returns>The metadata of the columns that are returned by the reader.</returns>
/// <remarks>
/// This method is only called once.
/// </remarks>
protected override IEnumerable<DataRowReaderColumn> GetColumnsCore()
{
return this.columns;
}

/// <summary>
/// Advance to the next row.
/// The implementor should update all <see cref="T:Spotfire.Dxp.Data.DataValueCursor"/>s in the <see cref="T:Spotfire.Dxp.Data.DataRowReaderColumn"/>s
/// with values for the next row.
/// </summary>
/// <returns>
/// <c>true</c> if there are more rows; otherwise <c>false</c>.
/// </returns>
protected override bool MoveNextCore()
{
this.index++;
if (this.index >= this.list.Count)
{
// We are done, past the final row.
return false;
}

// Get the image for the current row and assign to the cursor.
BinaryLargeObject image = this.list[this.index];
if (image == null)
{
this.cursor.MutableDataValue.SetNullValue();
}
else
{
this.cursor.MutableDataValue.ValidValue = image;
}

return true;
}
}
}
}

We also need to create a type identifier for the executor which is needed for registration.

namespace ImageFromURL
{
using Spotfire.Dxp.Application.Extension;

/// <summary>
/// Type identifiers for the image function exectuor.
/// </summary>
public class TypeIdentifiers : CustomTypeIdentifiers
{
/// <summary>
/// The image from URL executor.
/// </summary>
public static CustomTypeIdentifier ImageFromURLExecutor = CreateTypeIdentifier("ImageFromURL", "Image from URL.", "Downloads an image from an URL");
}
}
 

Now we are almost done, but we need to have a DataFunctionDefinition for this executor. To make this simple we create a tool that creates a function definition and stores it in the library so that we can use the default DataFunctions UI to execute it. This tool creates and saves the function definition in the library, it only needs to be run once then the function definition could just be import and exported to be moved to a different Spotfire server. Since we only need to run this one we make it very simple and assumes that there is a folder called “imagefromurl” in the library where we store the definition. We can always move it later once it has been stored.

namespace ImageFromURL
{
using Spotfire.Dxp.Application;
using Spotfire.Dxp.Application.Extension;
using Spotfire.Dxp.Data;
using Spotfire.Dxp.Data.DataFunctions;
using Spotfire.Dxp.Framework.Library;

/// <summary>
/// One-off tool to create and save a function definition in the library.
/// Assumes that there is an imagefromurl folder in the library.
/// </summary>
public class RegisterFunctionTool : CustomTool<AnalysisApplication>
{
/// <summary>
/// Initializes a new instance of the <see cref="RegisterFunctionTool"/> class.
/// </summary>
public RegisterFunctionTool()
: base("Register Image from URL tool.")
{
}

/// <summary>
/// Implement this method to perform the tool-specific logic.
/// </summary>
/// <param name="context">The context of the tool. Is never <c>null</c>.</param>
/// <remarks>Note that this method is only called if
/// <paramref name="context"/> is not <c>null</c> and
/// <see cref="M:Spotfire.Dxp.Application.Tool`1.IsEnabled(`0)"/>
/// returns <c>true</c>.</remarks>
protected override void ExecuteCore(AnalysisApplication context)
{
// Create the function definition builder.
DataFunctionDefinitionBuilder builder =
new DataFunctionDefinitionBuilder("Image from URL", TypeIdentifiers.ImageFromURLExecutor);

// One input argument called url which is a string column.
InputParameterBuilder inputBuilder = new InputParameterBuilder("url", ParameterType.Column);
inputBuilder.AddAllowedDataType(DataType.String);
builder.InputParameters.Add(inputBuilder.Build());

// One output parameter called image which is a column.
OutputParameterBuilder outputBuilder = new OutputParameterBuilder("image", ParameterType.Column);
builder.OutputParameters.Add(outputBuilder.Build());

DataFunctionDefinition functionDefinition = builder.Build();

LibraryManager lm = context.GetService<LibraryManager>();

// Find the folder where it should be stored.
LibraryItem li = lm.Search("imagefromurl")[0];

// Save the function definition in the library.
DataFunctionDefinition saved;
functionDefinition.SaveAs(li, functionDefinition.FunctionName, new string[] { }, out saved);
}
}
}

To finish things, we finally add the AddIn to register the tool and function executor.

namespace ImageFromURL
{
    using Spotfire.Dxp.Application.Extension;

    /// <summary>Addin registration.
    /// </summary>
    public sealed class CustomAddIn : AddIn
    {
        /// <summary>
        /// Override this method to register data function executors with the application.
        /// </summary>
        /// <param name="registrar">Object responsible for registering new data function executors.</param>
        protected override void RegisterDataFunctionExecutors(AddIn.DataFunctionExecutorRegistrar registrar)
        {
            base.RegisterDataFunctionExecutors(registrar);

            registrar.Register(TypeIdentifiers.ImageFromURLExecutor, new ImageFromURLFunctionExecutor());
        }

        /// <summary>
        /// Override this method to extend the application with new tools.
        /// Each new tool should inherit from <see cref="T:Spotfire.Dxp.Application.Extension.CustomTool`1"/>,
        /// and is then registered with the <see cref="T:Spotfire.Dxp.Application.Extension.AddIn.ToolRegistrar"/>.
        /// </summary>
        /// <param name="registrar">Object responsible for registering new tools.</param>
        protected override void RegisterTools(AddIn.ToolRegistrar registrar)
        {
            base.RegisterTools(registrar);

            registrar.Register(new RegisterFunctionTool());
        }
    }
}

Now we can first run the “Register Image from URL Tool” from the Tools menu and then use the Tools->Data Functions menu item to run the image from URL function.

image

We then select to add columns with the result:

image

Finally we press OK and the images are retrieved and added to the table.

image

Comments