Blog

Articles and information on C# and .NET development topics

Converting 2D arrays to 1D and accessing as either 2D or 1D

While working on a recent gaming project, I was originally using 2D arrays to store information relating to the different levels in the game. But when it came to loop through the contents of these levels, it wasn't as straightforward to do a simple foreach loop due to the multiple dimensions.

Instead, I changed the code so that the 2D data was stored in a single dimension array. By using row-major order you can calculate any position in 2D space and map it into the 1D array. This then allows you to continue accessing the data using 2D co-ordinates, but opens up 1D access too.

Defining your array

Given the size of your 2D array, the 1D creation code is trivial:

T[] items = new T[width * height];

Converting 2D co-ordinates into 1D index

Once your have your array, converting a 2D co-ordinate such as 3, 4 into the correct index of your 1D array using row-major order using the following formula:

y * width + x

Converting 1D index into 2D co-ordinates

The calculation to convert a 1D index into a 2D co-ordinate is fairly straightforward:

y = index / width;
x = index - (y * height);

Putting it together - the ArrayMap<T> class

To avoid constantly having to repeat the calculations, I created a generic ArrayMap class that I could use to store any data type in a 1D array, and access the values using either indexes or co-ordinates, as well as adding enumerable support. The class is very straightforward to use:

ArrayMap<int> grid;
Size size;
int value;

size = new Size(10, 10);
value = 0;
grid = new ArrayMap<int>(size);

// set values via 2D co-ordinates
for (int y = 0; y < size.Height; y++)
{
  for (int x = 0; x < size.Width; x++)
  {
    grid[x, y] = value;
    value++;
  }
}

// get values via 2D co-ordinates
Console.WriteLine(grid[9, 0]); // 9
Console.WriteLine(grid[0, 9]); // 90
Console.WriteLine(grid[9, 9]); // 99

// set values via indexes
for (int i = 0; i < grid.Count; i++)
  grid[i] = i;

// get values via index
Console.WriteLine(grid[9]); // 9
Console.WriteLine(grid[90]); // 90
Console.WriteLine(grid[99]); // 99

// enumerate items
foreach (int i in grid)
  Console.WriteLine(i);

// get index
Console.WriteLine(grid.GetItemIndex(9, 9)); // 99

// get location
Console.WriteLine(grid.GetItemLocation(99)); // 9,9

Below is the full source to the class.

using System;
using System.Collections;
using System.Collections.Generic;

namespace BinaryRealms.Engine
{
  public class ArrayMap<T> : IEnumerable<T>
  {
    private T[] _items;
    private Size _size;

    public ArrayMap()
    { }

    public ArrayMap(int width, int height)
      : this(new Size(width, height))
    { }

    public ArrayMap(Size size)
      : this()
    {
      this.Size = size;
    }

    public IEnumerator<T> GetEnumerator()
    {
      foreach (T item in _items)
        yield return item;
    }

    public int GetItemIndex(int x, int y)
    {
      if (x < 0 || x >= this.Size.Width)
        throw new IndexOutOfRangeException("X is out of range");
      else if (y < 0 || y >= this.Size.Height)
        throw new IndexOutOfRangeException("Y is out of range");

      return y * this.Size.Width + x;
    }

    public int GetItemIndex(Point point)
    {
      return this.GetItemIndex(point.X, point.Y);
    }

    public Point GetItemLocation(int index)
    {
      Point point;

      if (index < 0 || index >= _items.Length)
        throw new IndexOutOfRangeException("Index is out of range");

      point = new Point();
      point.Y = index / this.Size.Width;
      point.X = index - (point.Y * this.Size.Height);

      return point;
    }

    public int Count
    { get { return _items.Length; } }

    public Size Size
    {
      get { return _size; }
      set
      {
        _size = value;
        _items = new T[_size.Width * _size.Height];
      }
    }

    public T this[Point location]
    {
      get { return this[location.X, location.Y]; }
      set { this[location.X, location.Y] = value; }
    }

    public T this[int x, int y]
    {
      get { return this[this.GetItemIndex(x, y)]; }
      set { this[this.GetItemIndex(x, y)] = value; }
    }

    public T this[int index]
    {
      get { return _items[index]; }
      set { _items[index] = value; }
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
      return this.GetEnumerator();
    }
  }
}

Currently I'm using this class without any problems, but if you spot any errors or think it could do with anything else, please let me know!

1 comment | | Trackback specific URL for this entry

AngelCode bitmap font parsing using C#

While writing some bitmap font processing for an OpenGL project, I settled on using AngelCode's BMFont utility to generate both the textures and the font definition. However, this means I then needed to write a parser in order to use this in my OpenGL solution.

This library is a generic parser for the BMFont format - it doesn't include any rendering functionality or exotic references and should be usable in any version of .NET from 2.0 upwards. BMFont can generate fonts in three formats - binary, text and XML. The library currently supports text and XML, I may add binary support at another time; but currently I'm happy using the text format.

Note: This library only provides parsing functionality for loading BMFont data. It is up to you to provide functionality used to load textures and render characters

Overview of the library

There are four main classes used to describe a font:

  • BitmapFont - the main class representing the font and its attributes
  • Character - representing a single character
  • Kerning - represents the kerning between a pair of characters
  • Page - represents a texture page

There is also a support class, Padding, as I didn't want to reference System.Windows.Forms in order to use its own and using a Rectangle instead would be confusing. You can replace with this System.Windows.Forms version if you want.

Finally, the BitmapFontLoader class is a static class that will handle the loading of your fonts.

Loading a font

To load a font, call BitmapFontLoader.LoadFontFromFile. This will attempt to auto detect the file type and load a font. Alternatively, if you already know the file type in advance, then call the variations BitmapFontLoader.LoadFontFromTextFile or BitmapFontLoader.LoadFontFromXmlFile.

Each of these functions returns a new BitmapFont object on success.

Using a font

The BitmapFont class returns all the information specified in the font file, such as the attributes used to create the font. Most of these not directly used and are there only for if your application needs to know how the font was generated (for example if the textures are packed or not). The main things you would be interested in are:

  • Characters - this property contains all the characters defined in the font.
  • Kernings - this property contains all kerning definitions. However, mostly you should use the GetKerning method to get the kerning for a pair of characters.
  • Pages -this property contains the filenames of the textures used by the font. You'll need to manually load the relevant textures.
  • LineHeight - this property returns the line height. When rending text across multiple lines, use this property for incrementing the vertical coordinate - don't just use the largest character height or you'll end up with inconsistent line heights.

The Character class describes a single character. Your rendering functionality will probably need to use all of the properties it contains:

  • Bounds - the location and size of the character in the source texture.
  • TexturePage - the index of the page containing the source texture.
  • Offset - an offset to use when rendering the character so it lines up correctly with other characters.
  • XAdvance - the value to increment the horizontal coordinate by. Don't forgot to combine this value with the result of a call to GetKerning.

Example rendering using GDI

The sample project which accompanies this article shows a very basic way of rending using GDI; however this is just for demonstration purposes and you should probably come up with something more efficient in a real application!

    private void DrawCharacter(Graphics g, Character character, int x, int y)
    {
      g.DrawImage(_textures[character.TexturePage], new RectangleF(x, y, character.Bounds.Width, character.Bounds.Height), character.Bounds, GraphicsUnit.Pixel);
    }

    private void DrawPreview()
    {
      Size size;
      Bitmap image;
      string normalizedText;
      int x;
      int y;
      char previousCharacter;

      previousCharacter = ' ';
      normalizedText = _font.NormalizeLineBreaks(previewTextBox.Text);
      size = _font.MeasureFont(normalizedText);

      if (size.Height != 0 && size.Width != 0)
      {
        image = new Bitmap(size.Width, size.Height);
        x = 0;
        y = 0;

        using (Graphics g = Graphics.FromImage(image))
        {
          foreach (char character in normalizedText)
          {
            switch (character)
            {
              case '\n':
                x = 0;
                y += _font.LineHeight;
                break;
              default:
                Character data;
                int kerning;

                data = _font[character];
                kerning = _font.GetKerning(previousCharacter, character);

                this.DrawCharacter(g, data, x + data.Offset.X + kerning, y + data.Offset.Y);

                x += data.XAdvance + kerning;
                break;
            }

            previousCharacter = character;
          }
        }

        previewImageBox.Image = image;
      }
    }

The Bitmap Font Viewer application

Also included in the download for this article is a simple Windows Forms application for viewing a bitmap font.

Note: All of the fonts I have created and tested were unpacked. The font viewer does not support packed textures, and while it will still load the font, it will not draw glyphs properly as it isn't able to do any of the magic with channels that the packed texture requires. In addition, as .NET doesn't support the TGA format by default, neither does this sample project.

Final Thoughts

Unlike my other articles, I haven't really gone into the source code or pointed out how it works, however it should all be simple to understand and use (despite having virtually no documentation) - please let me know if you think otherwise!

As mentioned above, I'm currently not using packed textures. The font parser will give you all the information you need regarding channels for extracting the information, but could probably be nicer done, such as using enums instead of magic ints - I may address this in a future update, along side implementing the binary file format.

Ideally the best way to use this code would be to inherit or extend the BitmapFont class. Therefore it would probably be better directly embedding the source code into your application, change the namespaces to match your own solution, then build from there.

I haven't tested with many fancy fonts - it's probable that the MeasureFont method doesn't handle cases of fonts with have a larger draw area than their basic box size.

Updates to this project will be posted to either CodePlex or GitHub - this article will be updated once the code is up.

Downloads:

  • BitmapFontParser.zip

    (500.33 KB | 02 January 2012 )

    Source code for a parser for AngelCode BMFont text and XML font formats.

2 comments | | Trackback specific URL for this entry

Detecting if an application is running as an elevated process, and spawning a new process using elevated permissions

Recently I was writing some code to allow a program to register itself to start with Windows for all users. On Windows 7 with User Account Control (UAC) enabled, trying to write to the relevant registry key without having elevated permissions throws an UnauthorizedAccessException exception. If you want to make these sorts of modifications to a system, the application needs to be running as an administrator.

Checking if your application is running with elevated permissions

To check if your application is currently running with elevated permissions, you simply need to see if the current user belongs to the Administrator user group.

// Requires "using System.Security.Principal;"

public bool IsElevated
{
  get
  {
    return new WindowsPrincipal(WindowsIdentity.GetCurrent()).IsInRole(WindowsBuiltInRole.Administrator);
  }
}

public void SomeMethod()
{
  if (this.IsElevated)
  {
    // running as administrator
  }
}

Running an application as an administrator

While it might be possible to elevate your applications process via the LogonUser API, this requires user names and passwords, and isn't a trivial task. So we'll ignore this approach in favour of something much more simplistic and less likely to go wrong, not to mention not requiring admin passwords.

You're probably already aware that there are various "verbs" predefined for dealing with specific actions relating to interaction with a file, such as print and open. While these verbs are normally configured on file associations in the Windows Registry, you can also force a process to be run under the administration account by specifying the runas verb.

Note: Specifying this verb in Windows XP displays a dialog allowing a user to be selected. Unfortunately this means that it's possible for the spawned application to not have the required permissions either - remember to check that you have permission to do an action before actually attempting it!

For my scenario, the core application shouldn't need to run in elevated mode, so I decided to create a generic stub program which would accept a number of arguments for if the startup program should be registered or unregistered, and the title and location used to perform the action. Then the main application simply spawned this process in administration mode to apply the users choice.

ProcessStartInfo startInfo;

startInfo = new ProcessStartInfo();
startInfo.FileName = Path.Combine(Path.GetDirectoryName(Application.ExecutablePath), "regstart.exe"); // replace with your filename
startInfo.Arguments = string.Empty; // if you need to pass any command line arguments to your stub, enter them here
startInfo.UseShellExecute = true;
startInfo.Verb = "runas";

Process.Start(startInfo);

Note: Although I haven't included it in the example above, you may wish to handle the Win32Exception that can be thrown by the Process.Start method. If the user cancels the UAC prompt, this exception will be automatically thrown with the ERROR_CANCELLED (1223) error code.

With the runas verb specified, the application is now run in elevated mode, and the operating system asks the user for permission to continue. Unfortunately, if your application isn't signed, then you get a scarier version of the prompt, as displayed above. If your application is signed, then you'll get something similar to the screenshot below.

2 comments | | Trackback specific URL for this entry

Extending the ImageBox component to display the contents of a PDF file using C#

In this article, I'll describe how to extend the ImageBox control discussed in earlier articles to be able to display PDF files with the help of the GhostScript library and the conversion library described in the previous article.

Getting Started

You can download the source code used in this article from the links below, these are:

  • Cyotek.GhostScript - core library providing GhostScript integration support
  • Cyotek.GhostScript.PdfConversion - support library for converting a PDF document into images
  • PdfImageBoxSample - sample project containing an updated ImageBox control, and the extended PdfImageBox.

Please note that the native GhostScript DLL is not included in these downloads, you will need to obtain that from the GhostScript project page.

Extending the ImageBox

To start extending the ImageBox, create a new class and inherit the ImageBox control. I also decided to override some of the default properties, so I added a constructor which sets the new values.

    public PdfImageBox()
    {
      // override some of the original ImageBox defaults
      this.GridDisplayMode = ImageBoxGridDisplayMode.None;
      this.BackColor = SystemColors.AppWorkspace;
      this.ImageBorderStyle = ImageBoxBorderStyle.FixedSingleDropShadow;

      // new pdf conversion settings
      this.Settings = new Pdf2ImageSettings();
    }

To ensure correct designer support, override versions of the properties with new DefaultValue attributes were added. With this done, it's time to add the new properties that will support viewing PDF files. The new properties are:

  • PdfFileName - the filename of the PDF to view
  • PdfPassword - specifies the password of the PDF file if one is required to open it (note, I haven't actually tested that this works!)
  • Settings - uses the Pdf2ImageSettings class discussed earlier to control quality settings for the converted document.
  • PageCache - an internal dictionary which stores a Bitmap against a page number to cache pages after these have loaded.

With the exception of PageCache, each of these properties also has backing event for change notifications, and as Pdf2ImageSettings implements INotifyPropertyChanged we'll also bind an event detect when the individual setting properties are modified.

    [Category("Appearance"), DefaultValue(typeof(Pdf2ImageSettings), "")]
    public virtual Pdf2ImageSettings Settings
    {
      get { return _settings; }
      set
      {
        if (this.Settings != value)
        {
          if (_settings != null)
            _settings.PropertyChanged -= SettingsPropertyChangedHandler;

          _settings = value;
          _settings.PropertyChanged += SettingsPropertyChangedHandler;

          this.OnSettingsChanged(EventArgs.Empty);
        }
      }
    }
    
    private void SettingsPropertyChangedHandler(object sender, PropertyChangedEventArgs e)
    {
      this.OnSettingsChanged(e);
    }

    protected virtual void OnSettingsChanged(EventArgs e)
    {
      this.OpenPDF();

      if (this.SettingsChanged != null)
        this.SettingsChanged(this, e);
    }

Navigation support

Although the PdfImageBox doesn't supply a user interface for navigating to different pages, we want to make it easy for the hosting application to provide one. To support this, a new CurrentPage property will be added for allowing the active page to retrieved or set, and also a number of readonly CanMove* properties. These properties allow the host to query which navigation options are applicable in order to present the correct UI.

    [Browsable(false)]
    public virtual int PageCount
    { get { return _converter != null ? _converter.PageCount : 0; } }

    [Category("Appearance"), DefaultValue(1)]
    public int CurrentPage
    {
      get { return _currentPage; }
      set
      {
        if (this.CurrentPage != value)
        {
          if (value < 1 || value > this.PageCount)
            throw new ArgumentException("Page number is out of bounds");

          _currentPage = value;

          this.OnCurrentPageChanged(EventArgs.Empty);
        }
      }
    }

    [Browsable(false)]
    public bool CanMoveFirst
    { get { return this.PageCount != 0 && this.CurrentPage != 1; } }

    [Browsable(false)]
    public bool CanMoveLast
    { get { return this.PageCount != 0 && this.CurrentPage != this.PageCount; } }

    [Browsable(false)]
    public bool CanMoveNext
    { get { return this.PageCount != 0 && this.CurrentPage < this.PageCount; } }

    [Browsable(false)]
    public bool CanMovePrevious
    { get { return this.PageCount != 0 && this.CurrentPage > 1; } }

Again, to make it easier for the host to connect to the control, we also add some helper navigation methods.

    public void FirstPage()
    {
      this.CurrentPage = 1;
    }

    public void LastPage()
    {
      this.CurrentPage = this.PageCount;
    }

    public void NextPage()
    {
      this.CurrentPage++;
    }

    public void PreviousPage()
    {
      this.CurrentPage--;
    }

Finally, it can sometimes take a few seconds to convert a page in a PDF file. To allow the host to provide a busy notification, such as setting the wait cursor or displaying a status bar message, we'll add a pair of events which will be called before and after a page is converted.

public event EventHandler LoadingPage;

public event EventHandler LoadedPage;

Opening the PDF file

Each of the property changed handlers in turn call the OpenPDF method. This method first clears any existing image cache and then initializes the conversion class based on the current PDF file name and quality settings. If the specified file is a valid PDF, the first page is converted, cached, and displayed.

    public void OpenPDF()
    {
      this.CleanUp();

      if (!this.DesignMode)
      {
        _converter = new Pdf2Image()
        {
          PdfFileName = this.PdfFileName,
          PdfPassword = this.PdfPassword,
          Settings = this.Settings
        };

        this.Image = null;
        this.PageCache= new Dictionary<int, Bitmap>();
        _currentPage = 1;

        if (this.PageCount != 0)
        {
          _currentPage = 0;
          this.CurrentPage = 1;
        }
      }
    }

    private void CleanUp()
    {
      // release  bitmaps
      if (this.PageCache != null)
      {
        foreach (KeyValuePair<int, Bitmap> pair in this.PageCache)
          pair.Value.Dispose();
        this.PageCache = null;
      }
    }

Displaying the image

Each time the CurrentPage property is changed, it calls the SetPageImage method. This method first checks to ensure the specified page is present in the cache. If it is not, it will load the page in. Once the page is in the cache, it is then displayed in the ImageBox, and the user can then pan and zoom as with any other image.

    protected virtual void SetPageImage()
    {
      if (!this.DesignMode && this.PageCache != null)
      {
        lock (_lock)
        {
          if (!this.PageCache.ContainsKey(this.CurrentPage))
          {
            this.OnLoadingPage(EventArgs.Empty);
            this.PageCache.Add(this.CurrentPage, _converter.GetImage(this.CurrentPage));
            this.OnLoadedPage(EventArgs.Empty);
          }

          this.Image = this.PageCache[this.CurrentPage];
        }
      }
    }

Note that we operate a lock during the execution of this method, to ensure that you can't try and load the same page twice.

With this method in place, the control is complete and ready to be used as a basic PDF viewer. In order to keep the article down to a reasonable size, I've excluded some of the definitions, overloads and helper methods; these can all be found in the sample download below.

The sample project demonstrates all the features described above and provides an example setting up a user interface for navigating a PDF document.

Future changes

At the moment, the PdfImageBox control processes on page at a time and caches the results. This means that navigation through already viewed pages is fast, but displaying new pages can be less than ideal. A possible enhancement would be to make the control multithreaded, and continue to load pages on a background thread.

Another issue is that as the control is caching the converted images in memory, it may use a lot of memory in order to display large PDF files. Not quite sure on the best approach to resolve this one, either to "expire" older pages, or to keep only a fixed number in memory. Or even save each page to a temporary disk file.

Finally, I haven't put in any handling at all for if the converter fails to convert a given page... I'll add this to a future update, and hopefully get the code hosted on an SVN server for interested parties.

Downloads:

  • PdfImageBoxSample.zip

    (513.29 KB | 04 September 2011 )

    Sample project showing how to extend the ImageBox control in order to display convert and display PDF files in a .NET WinForms application with the help of GhostScript.

  • Cyotek.GhostScript.zip

    (11.68 KB | 04 September 2011 )

    Work in progress class library for providing GhostScript integration in a .NET application.

  • Cyotek.GhostScript.PdfConversion.zip

    (5.43 KB | 04 September 2011 )

    Class library for converting PDF files into images using GhostScript. Also requires the Cyotek.GhostScript assembly.

2 comments | | Trackback specific URL for this entry

Recent News

Recent Articles

Most Popular

Tags

Advertisments