Cyotek Development Bloghttps://devblog.cyotek.com/tag/image/atom.xml2017-10-31T06:29:29ZPainting animated images using C#urn:uuid:208900b0-fcd0-4f0d-81ba-927e384cecdf2017-10-31T06:29:29Z2017-10-28T12:10:49Z<p>While reviewing <a href="/post/book-review-the-csharp-helper-top-100">The C# Top 100</a> it occurred to me that in my
own code base I have many bits of code which may make useful
blog posts, and that shouldn't take as long to write as the ones
I usually create. Plus, I've a fair amount of source code for
extending built in controls in various ways, creating new
controls from scratch and other useful library code - I need to
explore ways of decoupling some of that and releasing it for
anyone to use.</p>
<p>To get started with this idea is a simple article on painting
animated images using C#. If you assign an animated GIF file to
the <code>Image</code> property of a <code>Control</code>, the .NET Framework will
take care of animating the image for you. However, it only
provides this automatically for the <code>Image</code> property and not for
other properties such as <code>BackgroundImage</code>, or any custom image
properties you add to your own components.</p>
<p>Fortunately the framework doesn't secret the animation
functionality away and provides the static <code>ImageAnimator</code> class
(located in <code>System.Drawing</code>) to handle the bulk of the work.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/draw-animated-image-1c.gif" class="gallery" title="The demonstration program with an animated image" ><img src="https://images.cyotek.com/image/thumbnail/devblog/draw-animated-image-1c.gif" alt="The demonstration program with an animated image" decoding="async" loading="lazy" /></a><figcaption>The demonstration program with an animated image</figcaption></figure>
<p><em>(Image Credit: <a href="https://en.wikipedia.org/wiki/GIF#/media/File:Newtons_cradle_animation_book_2.gif" rel="external nofollow noopener">Dominique Toussaint</a>)</em></p>
<h2 id="checking-if-an-image-can-be-animated">Checking if an image can be animated</h2>
<p>The first thing to do is check if an animate can be animated.
While calling the various <code>ImageAnimator</code> methods with static
images (or even <code>null</code> references) won't crash, if your image is
static then there's no need to call them in the first place.</p>
<p>The <code>ImageAnimator.CanAnimate</code> method takes a source <code>Image</code> and
returns if it supports animation or not. Passing <code>null</code> to this
method will also return <code>false</code>.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> Image _image<span class="symbol">;</span>
<span class="keyword">private</span> <span class="keyword">bool</span> _isAnimating<span class="symbol">;</span>

_image <span class="symbol">=</span> LoadImageFromSomewhere<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
_isAnimating <span class="symbol">=</span> ImageAnimator<span class="symbol">.</span>CanAnimate<span class="symbol">(</span>_image<span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/draw-animated-image-1b.png" class="gallery" title="The demonstration program with a static image" ><img src="https://images.cyotek.com/image/thumbnail/devblog/draw-animated-image-1b.png" alt="The demonstration program with a static image" decoding="async" loading="lazy" /></a><figcaption>The demonstration program with a static image</figcaption></figure>
<p><em>(Image Credit: <a href="http://www.publicdomainpictures.net/view-image.php?image=18490" rel="external nofollow noopener">Vera Kratochvil</a>)</em></p>
<h2 id="preparing-for-animation">Preparing for animation</h2>
<p>If your image supports animation, the next step is to call
<code>ImageAnimator.Animate</code>, passing in both the source image and an
<code>EventHandler</code> to receive change notifications.</p>
<p>This method will create a background thread that will
periodically check watched images and advance frames as
required. When it detects a new frame should be painted, it will
call the event handler registered for the image, allowing you to
handle the update, e.g. repaint your control.</p>
<blockquote>
<p>Only one thread is created no matter how many images are being
animated</p>
</blockquote>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> <span class="keyword">void</span> SetupAnimation<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 _isAnimating <span class="symbol">=</span> ImageAnimator<span class="symbol">.</span>CanAnimate<span class="symbol">(</span>_image<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>_isAnimating<span class="symbol">)</span>
 <span class="symbol">{</span>
 ImageAnimator<span class="symbol">.</span>Animate<span class="symbol">(</span>_image<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>OnFrameChangedHandler<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="keyword">private</span> <span class="keyword">void</span> OnFrameChangedHandler<span class="symbol">(</span><span class="keyword">object</span> sender<span class="symbol">,</span> EventArgs eventArgs<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="comment">// trigger a repaint of our image</span>
 customPaintPanel<span class="symbol">.</span>Invalidate<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<blockquote>
<p>One mistake I sometimes see developers do is calling the
<code>Refresh</code> method of a custom Windows Forms control. Calling
<code>Refresh</code> will force the control and its children to be
repainted immediately. An alternative way is to call
<code>Invalidate</code> (without any arguments) which will mark the
window to be repainted without forcing the paint or repainting
child windows - generally this is more suitable and reduces
the number of unneeded repaints.</p>
</blockquote>
<h2 id="halting-animation">Halting animation</h2>
<p>When you are finished with the source image, you should call
<code>ImageAnimator.StopAnimate</code> to remove the image and callback
from the list of watched images.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> <span class="keyword">void</span> CleanUp<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="comment">// disable animations</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_isAnimating<span class="symbol">)</span>
 <span class="symbol">{</span>
 ImageAnimator<span class="symbol">.</span>StopAnimate<span class="symbol">(</span>_image<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>OnFrameChangedHandler<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 _isAnimating <span class="symbol">=</span> <span class="keyword">false</span><span class="symbol">;</span>

 _image<span class="symbol">.</span>Dispose<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 _image <span class="symbol">=</span> <span class="keyword">null</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="painting-the-image">Painting the image</h2>
<p>The current frame is part of the <code>Image</code> instance's metadata, so
you don't need to do anything specific to paint animated images
vs static. With that said, the <code>ImageAnimator</code> class tracks the
current frame separately and doesn't update the source <code>Image</code>
until requested, which you do by calling
<code>ImageAnimator.UpdateFrames</code>.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> <span class="keyword">void</span> customPaintPanel_Paint<span class="symbol">(</span><span class="keyword">object</span> sender<span class="symbol">,</span> PaintEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_isAnimating<span class="symbol">)</span>
 <span class="symbol">{</span>
 ImageAnimator<span class="symbol">.</span>UpdateFrames<span class="symbol">(</span>_image<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>DrawImage<span class="symbol">(</span>_image<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="wrapping-up">Wrapping up</h2>
<p>As you can probably see, this is quite a simple process and
makes it easy to support animated graphics in applications that
reference <code>System.Drawing</code>.</p>
<p>A complete example demonstrating how to use the <code>ImageAnimator</code>
class is available from the link below.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2017-10-28 - First published</li>
<li>2020-11-22 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/painting-animated-images-using-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comReading and writing farbfeld images using C#urn:uuid:f55d4c11-f78e-4292-8cdb-39fd5b0eff742017-03-04T06:29:21Z2016-01-19T17:00:44Z<p>Normally when I load textures in OpenGL, I have a PNG file which
I load into a <code>System.Drawing.Bitmap</code> and from there I pull out
the bytes and pass to <code>glTexImage2D</code>. It works, but seems a bit
silly having to create the bitmap in the first place. For this
reason, I was toying with the idea of creating a very simple
image format so I could just read the data directly without
requiring intermediate objects.</p>
<p>While mulling this idea over, I spotted an article on <a href="https://news.ycombinator.com/item?id=10890873" rel="external nofollow noopener">Hacker
News</a> describing a similar and simple image format named
<a href="http://tools.suckless.org/farbfeld/" rel="external nofollow noopener">farbfeld</a>. This format by <a href="http://suckless.org/" rel="external nofollow noopener">suckless.org</a> is described as
&quot;a lossless image format which is easy to parse, pipe and
compress&quot;.</p>
<p>Not having much else to do on a Friday night, I decided I'd
write a C# encoder and decoder for this format, along with a
basic GUI app for viewing and converting farbfeld images.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/farbfeld-viewer.png" class="gallery" title="A simple program for viewing and converting farbfeld images" ><img src="https://images.cyotek.com/image/thumbnail/devblog/farbfeld-viewer.png" alt="A simple program for viewing and converting farbfeld images" decoding="async" loading="lazy" /></a><figcaption>A simple program for viewing and converting farbfeld images</figcaption></figure><h2 id="the-format">The format</h2>
<table>
<thead>
<tr>
<th style="text-align: right;">Bytes</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td style="text-align: right;">8</td>
<td>&quot;farbfeld&quot; magic value</td>
</tr>
<tr>
<td style="text-align: right;">4</td>
<td>32-Bit BE unsigned integer (width)</td>
</tr>
<tr>
<td style="text-align: right;">4</td>
<td>32-Bit BE unsigned integer (height)</td>
</tr>
<tr>
<td style="text-align: right;">[2222]</td>
<td>4x16-Bit BE unsigned integers [RGBA] / pixel, row-aligned</td>
</tr>
</tbody>
</table>
<p>As you can see, it's about as simple as you can get, barring the
big-endian encoding I suppose. The main thing we have to worry
about is that farbeld stores RGBA values in the range 0-65535,
whereas in .NET-land we tend to use 0-255.</p>
<h2 id="decoding-an-image">Decoding an image</h2>
<p>Decoding an image is fairly straight forward. The difficult part
is turning those values into a .NET image in a fast manner.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">bool</span> IsFarbfeldImage<span class="symbol">(</span>Stream stream<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">]</span> buffer<span class="symbol">;</span>

 buffer <span class="symbol">=</span> <span class="keyword">new</span> <span class="keyword">byte</span><span class="symbol">[</span><span class="number">8</span><span class="symbol">]</span><span class="symbol">;</span>

 stream<span class="symbol">.</span>Read<span class="symbol">(</span>buffer<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> buffer<span class="symbol">.</span>Length<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> buffer<span class="symbol">[</span><span class="number">0</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;f&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">1</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;a&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">2</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;r&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">3</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;b&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">4</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;f&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">5</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;e&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">6</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;l&#39;</span> <span class="symbol">&amp;&amp;</span> buffer<span class="symbol">[</span><span class="number">7</span><span class="symbol">]</span> <span class="symbol">==</span> <span class="string">&#39;d&#39;</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> Bitmap Decode<span class="symbol">(</span>Stream stream<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> width<span class="symbol">;</span>
 <span class="keyword">int</span> height<span class="symbol">;</span>
 <span class="keyword">int</span> length<span class="symbol">;</span>
 ArgbColor<span class="symbol">[</span><span class="symbol">]</span> pixels<span class="symbol">;</span>

 width <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">32</span>BigEndian<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 height <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">32</span>BigEndian<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 length <span class="symbol">=</span> width <span class="symbol">*</span> height<span class="symbol">;</span>
 pixels <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ReadPixelData<span class="symbol">(</span>stream<span class="symbol">,</span> length<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>CreateBitmap<span class="symbol">(</span>width<span class="symbol">,</span> height<span class="symbol">,</span> pixels<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">private</span> ArgbColor<span class="symbol">[</span><span class="symbol">]</span> ReadPixelData<span class="symbol">(</span>Stream stream<span class="symbol">,</span> <span class="keyword">int</span> length<span class="symbol">)</span>
<span class="symbol">{</span>
 ArgbColor<span class="symbol">[</span><span class="symbol">]</span> pixels<span class="symbol">;</span>

 pixels <span class="symbol">=</span> <span class="keyword">new</span> ArgbColor<span class="symbol">[</span>length<span class="symbol">]</span><span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> i <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> i <span class="symbol">&lt;</span> length<span class="symbol">;</span> i<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> r<span class="symbol">;</span>
 <span class="keyword">int</span> g<span class="symbol">;</span>
 <span class="keyword">int</span> b<span class="symbol">;</span>
 <span class="keyword">int</span> a<span class="symbol">;</span>

 r <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">16</span>BigEndian<span class="symbol">(</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">257</span><span class="symbol">;</span>
 g <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">16</span>BigEndian<span class="symbol">(</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">257</span><span class="symbol">;</span>
 b <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">16</span>BigEndian<span class="symbol">(</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">257</span><span class="symbol">;</span>
 a <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">16</span>BigEndian<span class="symbol">(</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">257</span><span class="symbol">;</span>

 pixels<span class="symbol">[</span>i<span class="symbol">]</span> <span class="symbol">=</span> <span class="keyword">new</span> ArgbColor<span class="symbol">(</span>a<span class="symbol">,</span> r<span class="symbol">,</span> g<span class="symbol">,</span> b<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">return</span> pixels<span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">private</span> Bitmap CreateBitmap<span class="symbol">(</span><span class="keyword">int</span> width<span class="symbol">,</span> <span class="keyword">int</span> height<span class="symbol">,</span> IList<span class="symbol">&lt;</span>ArgbColor<span class="symbol">&gt;</span> pixels<span class="symbol">)</span>
<span class="symbol">{</span>
 Bitmap bitmap<span class="symbol">;</span>
 BitmapData bitmapData<span class="symbol">;</span>

 bitmap <span class="symbol">=</span> <span class="keyword">new</span> Bitmap<span class="symbol">(</span>width<span class="symbol">,</span> height<span class="symbol">,</span> PixelFormat<span class="symbol">.</span>Format<span class="number">32</span>bppArgb<span class="symbol">)</span><span class="symbol">;</span>

 bitmapData <span class="symbol">=</span> bitmap<span class="symbol">.</span>LockBits<span class="symbol">(</span><span class="keyword">new</span> Rectangle<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> width<span class="symbol">,</span> height<span class="symbol">)</span><span class="symbol">,</span> ImageLockMode<span class="symbol">.</span>ReadWrite<span class="symbol">,</span> PixelFormat<span class="symbol">.</span>Format<span class="number">32</span>bppArgb<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">unsafe</span>
 <span class="symbol">{</span>
 ArgbColor<span class="symbol">*</span> pixelPtr<span class="symbol">;</span>

 pixelPtr <span class="symbol">=</span> <span class="symbol">(</span>ArgbColor<span class="symbol">*</span><span class="symbol">)</span>bitmapData<span class="symbol">.</span>Scan<span class="number">0</span><span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> i <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> i <span class="symbol">&lt;</span> width <span class="symbol">*</span> height<span class="symbol">;</span> i<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="symbol">*</span>pixelPtr <span class="symbol">=</span> pixels<span class="symbol">[</span>i<span class="symbol">]</span><span class="symbol">;</span>
 pixelPtr<span class="symbol">++</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>

 bitmap<span class="symbol">.</span>UnlockBits<span class="symbol">(</span>bitmapData<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> bitmap<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="encoding-an-image">Encoding an image</h2>
<p>As with decoding, the difficult of encoding mainly lies in
getting the pixel data quickly. In this implementation, only
32bit RGBA images are supported. I will update it at some point
to support other colour depths (or at the very least add a hack
to convert lesser depths to 32bpp).</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">void</span> Encode<span class="symbol">(</span>Stream stream<span class="symbol">,</span> Bitmap image<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> width<span class="symbol">;</span>
 <span class="keyword">int</span> height<span class="symbol">;</span>
 ArgbColor<span class="symbol">[</span><span class="symbol">]</span> pixels<span class="symbol">;</span>

 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;f&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;a&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;r&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;b&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;f&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;e&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;l&#39;</span><span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteByte<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="string">&#39;d&#39;</span><span class="symbol">)</span><span class="symbol">;</span>

 width <span class="symbol">=</span> image<span class="symbol">.</span>Width<span class="symbol">;</span>
 height <span class="symbol">=</span> image<span class="symbol">.</span>Height<span class="symbol">;</span>

 stream<span class="symbol">.</span>WriteBigEndian<span class="symbol">(</span>width<span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteBigEndian<span class="symbol">(</span>height<span class="symbol">)</span><span class="symbol">;</span>

 pixels <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetPixels<span class="symbol">(</span>image<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">foreach</span> <span class="symbol">(</span>ArgbColor pixel <span class="keyword">in</span> pixels<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">ushort</span> r<span class="symbol">;</span>
 <span class="keyword">ushort</span> g<span class="symbol">;</span>
 <span class="keyword">ushort</span> b<span class="symbol">;</span>
 <span class="keyword">ushort</span> a<span class="symbol">;</span>

 r <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">ushort</span><span class="symbol">)</span><span class="symbol">(</span>pixel<span class="symbol">.</span>R <span class="symbol">*</span> <span class="number">257</span><span class="symbol">)</span><span class="symbol">;</span>
 g <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">ushort</span><span class="symbol">)</span><span class="symbol">(</span>pixel<span class="symbol">.</span>G <span class="symbol">*</span> <span class="number">257</span><span class="symbol">)</span><span class="symbol">;</span>
 b <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">ushort</span><span class="symbol">)</span><span class="symbol">(</span>pixel<span class="symbol">.</span>B <span class="symbol">*</span> <span class="number">257</span><span class="symbol">)</span><span class="symbol">;</span>
 a <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">ushort</span><span class="symbol">)</span><span class="symbol">(</span>pixel<span class="symbol">.</span>A <span class="symbol">*</span> <span class="number">257</span><span class="symbol">)</span><span class="symbol">;</span>

 stream<span class="symbol">.</span>WriteBigEndian<span class="symbol">(</span>r<span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteBigEndian<span class="symbol">(</span>g<span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteBigEndian<span class="symbol">(</span>b<span class="symbol">)</span><span class="symbol">;</span>
 stream<span class="symbol">.</span>WriteBigEndian<span class="symbol">(</span>a<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="keyword">private</span> ArgbColor<span class="symbol">[</span><span class="symbol">]</span> GetPixels<span class="symbol">(</span>Bitmap bitmap<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> width<span class="symbol">;</span>
 <span class="keyword">int</span> height<span class="symbol">;</span>
 BitmapData bitmapData<span class="symbol">;</span>
 ArgbColor<span class="symbol">[</span><span class="symbol">]</span> results<span class="symbol">;</span>

 width <span class="symbol">=</span> bitmap<span class="symbol">.</span>Width<span class="symbol">;</span>
 height <span class="symbol">=</span> bitmap<span class="symbol">.</span>Height<span class="symbol">;</span>
 results <span class="symbol">=</span> <span class="keyword">new</span> ArgbColor<span class="symbol">[</span>width <span class="symbol">*</span> height<span class="symbol">]</span><span class="symbol">;</span>
 bitmapData <span class="symbol">=</span> bitmap<span class="symbol">.</span>LockBits<span class="symbol">(</span><span class="keyword">new</span> Rectangle<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> width<span class="symbol">,</span> height<span class="symbol">)</span><span class="symbol">,</span> ImageLockMode<span class="symbol">.</span>WriteOnly<span class="symbol">,</span> PixelFormat<span class="symbol">.</span>Format<span class="number">32</span>bppArgb<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">unsafe</span>
 <span class="symbol">{</span>
 ArgbColor<span class="symbol">*</span> pixel<span class="symbol">;</span>

 pixel <span class="symbol">=</span> <span class="symbol">(</span>ArgbColor<span class="symbol">*</span><span class="symbol">)</span>bitmapData<span class="symbol">.</span>Scan<span class="number">0</span><span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> row <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> row <span class="symbol">&lt;</span> height<span class="symbol">;</span> row<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> col <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> col <span class="symbol">&lt;</span> width<span class="symbol">;</span> col<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 results<span class="symbol">[</span>row <span class="symbol">*</span> width <span class="symbol">+</span> col<span class="symbol">]</span> <span class="symbol">=</span> <span class="symbol">*</span>pixel<span class="symbol">;</span>

 pixel<span class="symbol">++</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>

 bitmap<span class="symbol">.</span>UnlockBits<span class="symbol">(</span>bitmapData<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> results<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="nothing-complicated">Nothing complicated</h2>
<p>As you can see, it's a remarkably simple format and very easy to
process. However, it does mean that images tend to be large - in
my testing a standard HD image was 16MB for example. Of course,
as you'll probably be using this for some specific process
you'll be able to handle compression yourself.</p>
<p>After further reflection, I decided I wouldn't be using this
format as it wouldn't quite fit my OpenGL scenario, as OpenGL
(or at least the bits I'm familiar with) expect an array of
bytes, one per channel, unlike farbfeld which uses two (and the
larger value range as mentioned at the start). But I took the
source I wrote for farbfeld, refactored it to use single bytes
(and little-endian encoding for the other values), and that way
I could just do something like this</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">]</span> pixels<span class="symbol">;</span>
<span class="keyword">int</span> length<span class="symbol">;</span>

width <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">32</span>LittleEndian<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
height <span class="symbol">=</span> stream<span class="symbol">.</span>ReadUInt<span class="number">32</span>LittleEndian<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
length <span class="symbol">=</span> width <span class="symbol">*</span> height <span class="symbol">*</span> <span class="number">4</span><span class="symbol">;</span>
pixels <span class="symbol">=</span> <span class="keyword">new</span> <span class="keyword">byte</span><span class="symbol">[</span>length<span class="symbol">]</span><span class="symbol">;</span>
stream<span class="symbol">.</span>Read<span class="symbol">(</span>pixels<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> length<span class="symbol">)</span><span class="symbol">;</span>

GL<span class="symbol">.</span>TexImage<span class="number">2</span>D<span class="symbol">(</span>TextureTarget<span class="symbol">.</span>Texture<span class="number">2</span>D<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> PixelInternalFormat<span class="symbol">.</span>Rgba<span class="symbol">,</span> width<span class="symbol">,</span> height<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> PixelFormat<span class="symbol">.</span>Rgba<span class="symbol">,</span> PixelType<span class="symbol">.</span>UnsignedByte<span class="symbol">,</span> pixels<span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>No <code>System.Drawing.Bitmap</code>, decoder class or complicated
decoding required!</p>
<h2 id="the-full-source">The full source</h2>
<p>The source presented here is abridged, you can get the full
version from the <a href="https://github.com/cyotek/Cyotek.Drawing.Imaging.Farbfeld/" rel="external nofollow noopener">GitHub repository</a>.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2016-01-19 - First published</li>
<li>2017-03-04 - Corrected divisor</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/reading-and-writing-farbfeld-images-using-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comEven more algorithms for dithering images using C#urn:uuid:13fbfc7c-65f9-47a2-99fb-2a23bb07e05b2015-06-13T10:55:53Z2015-06-13T10:55:53Z<p>Although I should really be working on adding the dithering
algorithms into <a href="https://cyotek.com/cyotek-gif-animator">Gif Animator</a>, I thought it would be useful
to expand the repertoire of algorithms available for use with it
and the other projects I'm working on.</p>
<h2 id="adding-a-general-purpose-base-class">Adding a general purpose base class</h2>
<p>I decided to re-factor the class I created for the <a href="/post/dithering-an-image-using-the-burkes-algorithm-in-csharp">Burkes</a>
algorithm to make it suitable for adding other error diffusion
filters with a minimal amount of code.</p>
<p>First, I added a new abstract class, <code>ErrorDiffusionDithering</code>.
The constructor of this class requires you to pass in the matrix
used to disperse the error to neighbouring pixels, the divisor,
and whether or not to use bit shifting. The reason for the last
parameter is the <a href="/post/dithering-an-image-using-the-floyd-steinberg-algorithm-in-csharp">Floyd-Steinberg</a> and Burkes algorithms
covered in my earlier posts had divisors that were powers of
two, and so could therefore be bit shifted for faster division.
Not all algorithms use a power of two divisor though and so we
need to be flexible.</p>
<p>The constructor then stores the matrix, and pre-calculates a
couple of other values to avoid repeating these each time the
<code>Diffuse</code> method is called.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> ErrorDiffusionDithering<span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">,</span><span class="symbol">]</span> matrix<span class="symbol">,</span> <span class="keyword">byte</span> divisor<span class="symbol">,</span> <span class="keyword">bool</span> useShifting<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>matrix <span class="symbol">==</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentNullException<span class="symbol">(</span><span class="string">&quot;matrix&quot;</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">if</span> <span class="symbol">(</span>matrix<span class="symbol">.</span>Length <span class="symbol">==</span> <span class="number">0</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Matrix is empty.&quot;</span><span class="symbol">,</span> <span class="string">&quot;matrix&quot;</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 _matrix <span class="symbol">=</span> matrix<span class="symbol">;</span>
 _matrixWidth <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="symbol">(</span>matrix<span class="symbol">.</span>GetUpperBound<span class="symbol">(</span><span class="number">1</span><span class="symbol">)</span> <span class="symbol">+</span> <span class="number">1</span><span class="symbol">)</span><span class="symbol">;</span>
 _matrixHeight <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="symbol">(</span>matrix<span class="symbol">.</span>GetUpperBound<span class="symbol">(</span><span class="number">0</span><span class="symbol">)</span> <span class="symbol">+</span> <span class="number">1</span><span class="symbol">)</span><span class="symbol">;</span>
 _divisor <span class="symbol">=</span> divisor<span class="symbol">;</span>
 _useShifting <span class="symbol">=</span> useShifting<span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> i <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> i <span class="symbol">&lt;</span> _matrixWidth<span class="symbol">;</span> i<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>matrix<span class="symbol">[</span><span class="number">0</span><span class="symbol">,</span> i<span class="symbol">]</span> <span class="symbol">!=</span> <span class="number">0</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 _startingOffset <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="symbol">(</span>i <span class="symbol">-</span> <span class="number">1</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>The actual dithering implementation is unchanged from original
matrix handling code, with the exception of supporting bit
shifting or integer division, and not having to work out the
current pixel in the matrix, width or height.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">void</span> IErrorDiffusion<span class="symbol">.</span>Diffuse<span class="symbol">(</span>ArgbColor<span class="symbol">[</span><span class="symbol">]</span> data<span class="symbol">,</span> ArgbColor original<span class="symbol">,</span> ArgbColor transformed<span class="symbol">,</span> <span class="keyword">int</span> x<span class="symbol">,</span> <span class="keyword">int</span> y<span class="symbol">,</span> <span class="keyword">int</span> width<span class="symbol">,</span> <span class="keyword">int</span> height<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> redError<span class="symbol">;</span>
 <span class="keyword">int</span> blueError<span class="symbol">;</span>
 <span class="keyword">int</span> greenError<span class="symbol">;</span>

 redError <span class="symbol">=</span> original<span class="symbol">.</span>R <span class="symbol">-</span> transformed<span class="symbol">.</span>R<span class="symbol">;</span>
 blueError <span class="symbol">=</span> original<span class="symbol">.</span>G <span class="symbol">-</span> transformed<span class="symbol">.</span>G<span class="symbol">;</span>
 greenError <span class="symbol">=</span> original<span class="symbol">.</span>B <span class="symbol">-</span> transformed<span class="symbol">.</span>B<span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> row <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> row <span class="symbol">&lt;</span> _matrixHeight<span class="symbol">;</span> row<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> offsetY<span class="symbol">;</span>

 offsetY <span class="symbol">=</span> y <span class="symbol">+</span> row<span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> col <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> col <span class="symbol">&lt;</span> _matrixWidth<span class="symbol">;</span> col<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> coefficient<span class="symbol">;</span>
 <span class="keyword">int</span> offsetX<span class="symbol">;</span>

 coefficient <span class="symbol">=</span> _matrix<span class="symbol">[</span>row<span class="symbol">,</span> col<span class="symbol">]</span><span class="symbol">;</span>
 offsetX <span class="symbol">=</span> x <span class="symbol">+</span> <span class="symbol">(</span>col <span class="symbol">-</span> _startingOffset<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>coefficient <span class="symbol">!=</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> offsetX <span class="symbol">&gt;</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> offsetX <span class="symbol">&lt;</span> width <span class="symbol">&amp;&amp;</span> offsetY <span class="symbol">&gt;</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> offsetY <span class="symbol">&lt;</span> height<span class="symbol">)</span>
 <span class="symbol">{</span>
 ArgbColor offsetPixel<span class="symbol">;</span>
 <span class="keyword">int</span> offsetIndex<span class="symbol">;</span>
 <span class="keyword">int</span> newR<span class="symbol">;</span>
 <span class="keyword">int</span> newG<span class="symbol">;</span>
 <span class="keyword">int</span> newB<span class="symbol">;</span>

 offsetIndex <span class="symbol">=</span> offsetY <span class="symbol">*</span> width <span class="symbol">+</span> offsetX<span class="symbol">;</span>
 offsetPixel <span class="symbol">=</span> data<span class="symbol">[</span>offsetIndex<span class="symbol">]</span><span class="symbol">;</span>

 <span class="comment">// if the UseShifting property is set, then bit shift the values by the specified</span>
 <span class="comment">// divisor as this is faster than integer division. Otherwise, use integer division</span>

 <span class="keyword">if</span> <span class="symbol">(</span>_useShifting<span class="symbol">)</span>
 <span class="symbol">{</span>
 newR <span class="symbol">=</span> <span class="symbol">(</span>redError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> _divisor<span class="symbol">;</span>
 newG <span class="symbol">=</span> <span class="symbol">(</span>greenError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> _divisor<span class="symbol">;</span>
 newB <span class="symbol">=</span> <span class="symbol">(</span>blueError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> _divisor<span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 newR <span class="symbol">=</span> <span class="symbol">(</span>redError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">/</span> _divisor<span class="symbol">;</span>
 newG <span class="symbol">=</span> <span class="symbol">(</span>greenError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">/</span> _divisor<span class="symbol">;</span>
 newB <span class="symbol">=</span> <span class="symbol">(</span>blueError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">/</span> _divisor<span class="symbol">;</span>
 <span class="symbol">}</span>

 offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> newR<span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> newG<span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> newB<span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 data<span class="symbol">[</span>offsetIndex<span class="symbol">]</span> <span class="symbol">=</span> offsetPixel<span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="burkes-dithering-redux">Burkes Dithering, redux</h2>
<p>The <code>BurkesDithering</code> class now looks like this</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">sealed</span> <span class="keyword">class</span> BurksDithering <span class="symbol">:</span> ErrorDiffusionDithering
<span class="symbol">{</span>
 <span class="keyword">public</span> BurksDithering<span class="symbol">(</span><span class="symbol">)</span>
 <span class="symbol">:</span> <span class="keyword">base</span><span class="symbol">(</span><span class="keyword">new</span> <span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">,</span><span class="symbol">]</span>
 <span class="symbol">{</span>
 <span class="symbol">{</span>
 <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">8</span><span class="symbol">,</span> <span class="number">4</span>
 <span class="symbol">}</span><span class="symbol">,</span>
 <span class="symbol">{</span>
 <span class="number">2</span><span class="symbol">,</span> <span class="number">4</span><span class="symbol">,</span> <span class="number">8</span><span class="symbol">,</span> <span class="number">4</span><span class="symbol">,</span> <span class="number">2</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span><span class="symbol">,</span> <span class="number">5</span><span class="symbol">,</span> <span class="keyword">true</span><span class="symbol">)</span>
 <span class="symbol">{</span> <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>No code, just the matrix and the bit shifted divisor of 5, which
will divide each result by 32. Nice!</p>
<h2 id="more-algorithms">More Algorithms</h2>
<p>As well as opening the door to allowing a user to define a
custom dither matrix, it also makes it trivial to implement a
number of other common error diffusion matrixes. The <a href="https://github.com/cyotek/Dithering" rel="external nofollow noopener">GitHub
Repository</a> now offers the
following algorithms</p>
<ul>
<li>Atkinson</li>
<li>Burkes</li>
<li>Floyd-Steinberg</li>
<li>Jarvis, Judice &amp; Ninke</li>
<li>Sierra</li>
<li>Two Row Sierra</li>
<li>Sierra Light</li>
<li>Stucki</li>
</ul>
<p>Which is a fairly nice array.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-atkinson.png" class="gallery" title="An example of Atkinson dithering" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-atkinson.png" alt="An example of Atkinson dithering" decoding="async" loading="lazy" /></a><figcaption>An example of Atkinson dithering</figcaption></figure><figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">sealed</span> <span class="keyword">class</span> AtkinsonDithering <span class="symbol">:</span> ErrorDiffusionDithering
<span class="symbol">{</span>
 <span class="keyword">public</span> AtkinsonDithering<span class="symbol">(</span><span class="symbol">)</span>
 <span class="symbol">:</span> <span class="keyword">base</span><span class="symbol">(</span><span class="keyword">new</span> <span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">,</span><span class="symbol">]</span>
 <span class="symbol">{</span>
 <span class="symbol">{</span>
 <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">1</span><span class="symbol">,</span> <span class="number">1</span>
 <span class="symbol">}</span><span class="symbol">,</span>
 <span class="symbol">{</span>
 <span class="number">1</span><span class="symbol">,</span> <span class="number">1</span><span class="symbol">,</span> <span class="number">1</span><span class="symbol">,</span> <span class="number">0</span>
 <span class="symbol">}</span><span class="symbol">,</span>
 <span class="symbol">{</span>
 <span class="number">0</span><span class="symbol">,</span> <span class="number">1</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span><span class="symbol">,</span> <span class="number">3</span><span class="symbol">,</span> <span class="keyword">true</span><span class="symbol">)</span>
 <span class="symbol">{</span> <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="random-dithering">Random Dithering</h2>
<p>There's a rather old (in internet terms anyway!) text file
floating around named <a href="http://www.efg2.com/Lab/Library/ImageProcessing/DHALF.TXT" rel="external nofollow noopener">DHALF.TXT</a> (based in turn on an even
older document named <code>DITHER.TXT</code>) that has a ton of useful
information on dithering, and with the exception of the
Altkinson algorithm (I took that from <a href="http://www.tannerhelland.com/4660/dithering-eleven-algorithms-source-code/#attachment_4677" rel="external nofollow noopener">here</a> is where I have
pulled all the error weights and divisors from.</p>
<p>One of the sections in this document dealt with random
dithering. Although I didn't think I would ever use it myself, I
thought I'd add an implementation of it anyway to see what it's
like.</p>
<p>Unlike the error diffusion methods, random dithering affects
only a single pixel at a time, and does not consider or modify
its neighbours. You also have a modicum of control over it too,
if you can control the initial seed of the random number
generator.</p>
<p>The <code>DHALF.TXT</code> text sums it up succinctly: For each dot in our
grayscale image, we generate a random number in the range 0 -
255: if the random number is greater than the image value at
that dot, the display device plots the dot white; otherwise, it
plots it black. That's it.</p>
<p>And here's our implementation (ignoring the fact that it isn't
error diffusion and all of a sudden our <code>IErrorDiffusion</code>
interface is named wrong!)</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">void</span> IErrorDiffusion<span class="symbol">.</span>Diffuse<span class="symbol">(</span>ArgbColor<span class="symbol">[</span><span class="symbol">]</span> data<span class="symbol">,</span> ArgbColor original<span class="symbol">,</span> ArgbColor transformed<span class="symbol">,</span> <span class="keyword">int</span> x<span class="symbol">,</span> <span class="keyword">int</span> y<span class="symbol">,</span> <span class="keyword">int</span> width<span class="symbol">,</span> <span class="keyword">int</span> height<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">byte</span> gray<span class="symbol">;</span>

 gray <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="symbol">(</span><span class="number">0.299</span> <span class="symbol">*</span> original<span class="symbol">.</span>R <span class="symbol">+</span> <span class="number">0.587</span> <span class="symbol">*</span> original<span class="symbol">.</span>G <span class="symbol">+</span> <span class="number">0.114</span> <span class="symbol">*</span> original<span class="symbol">.</span>B<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>gray <span class="symbol">&gt;</span> _random<span class="symbol">.</span>Next<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> <span class="number">255</span><span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 data<span class="symbol">[</span>y <span class="symbol">*</span> width <span class="symbol">+</span> x<span class="symbol">]</span> <span class="symbol">=</span> _white<span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 data<span class="symbol">[</span>y <span class="symbol">*</span> width <span class="symbol">+</span> x<span class="symbol">]</span> <span class="symbol">=</span> _black<span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>(Although I reversed black and white from the original
description as otherwise it looked completely wrong)</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-random.png" class="gallery" title="Random dithering - it doesn't actually look too bad" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-random.png" alt="Random dithering - it doesn't actually look too bad" decoding="async" loading="lazy" /></a><figcaption>Random dithering - it doesn't actually look too bad</figcaption></figure><figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-random-color.png" class="gallery" title="Another example of random dithering, this time using colour" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-random-color.png" alt="Another example of random dithering, this time using colour" decoding="async" loading="lazy" /></a><figcaption>Another example of random dithering, this time using colour</figcaption></figure>
<p>I was surprised to see it actually doesn't look that bad.</p>
<h2 id="continuation">Continuation</h2>
<p>I've almost got a full house of useful dithering algorithms now.
About the only thing left for me to do is to implement a ordered
Bayer dithering as I really like the look of this type, and
reminds me of games and computers of yesteryear. So there's
still at least one more article to follow in this series!</p>
<p>The updated source code with all these algorithms is available
from the <a href="https://github.com/cyotek/Dithering" rel="external nofollow noopener">GitHub repository</a>.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2015-06-13 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/even-more-algorithms-for-dithering-images-using-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comDithering an image using the Burkes algorithm in C#urn:uuid:119ddf2c-d60c-459f-9438-743b434a8c752015-06-06T11:02:11Z2015-06-06T11:02:11Z<p>In my <a href="/post/dithering-an-image-using-the-floyd-steinberg-algorithm-in-csharp">previous post</a>, I described how to dither an image in
C# using the Floyd‑Steinberg algorithm. Continuing this theme,
this post will cover the Burkes algorithm.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-threshold.png" class="gallery" title="An example of 1bit conversion via a threshold" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-threshold.png" alt="An example of 1bit conversion via a threshold" decoding="async" loading="lazy" /></a><figcaption>An example of 1bit conversion via a threshold</figcaption></figure>
<p>I will be using the same demonstration application as from the
previous post, so I won't go over how this works again.</p>
<h2 id="burkes-dithering">Burkes dithering</h2>
<p>As with Floyd‑Steinberg, the Burkes algorithm is an error
diffusion algorithm, which is to say for each pixel an &quot;<em>error</em>&quot;
is generated and then distributed to pixels around the source.
Unlike Floyd‑Steinberg however (which modifies 4 surrounding
pixels), Burkes modifies 7 pixels.</p>
<blockquote>
<p>Burkes is actually a modified version of the Stucki algorithm,
which in turn is an evolution of the Jarvis algorithms.</p>
</blockquote>
<p>The diagram below shows the distribution of the error
coefficients.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-burkes-diagram.png" class="gallery" title="How the error of the current pixel is diffused to its neighbours" ><img src="https://images.cyotek.com/image/devblog/dithering-burkes-diagram.png" alt="How the error of the current pixel is diffused to its neighbours" decoding="async" loading="lazy" /></a><figcaption>How the error of the current pixel is diffused to its neighbours</figcaption></figure>
<ul>
<li>8 for the pixel to the right of the current pixel</li>
<li>4 for the second pixel to the right</li>
<li>2 for the pixel below and two to the left</li>
<li>4 for the pixel below and to the left</li>
<li>8 for the pixel below</li>
<li>4 for the pixel below and to the right</li>
<li>2 for the pixel below and two to the right</li>
</ul>
<p>Unlike Floyd‑Steinberg, the error result in this algorithm is
divided by 32. But as that's still a power of two, once again we
can use bit shifting to perform the division.</p>
<p>Due to the additional calculations I would assume that this
algorithm will be slightly slower than Floyd‑Steinberg, but as
of yet I haven't ran any form of benchmarks to test this.</p>
<h2 id="applying-the-algorithm">Applying the algorithm</h2>
<p>In my Floyd‑Steinberg example, I replicated the calculations
four times for each pixel. As there are now seven sets of
calculations with Burkes, I decided to store the coefficients in
a 2D array mimicing the diagram above, then iterating this. I'm
not entirely convinced this is the best approach, but it does
seem to be working.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> <span class="keyword">byte</span><span class="symbol">[</span><span class="symbol">,</span><span class="symbol">]</span> _matrix <span class="symbol">=</span>
<span class="symbol">{</span>
 <span class="symbol">{</span>
 <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">8</span><span class="symbol">,</span> <span class="number">4</span>
 <span class="symbol">}</span><span class="symbol">,</span>
 <span class="symbol">{</span>
 <span class="number">2</span><span class="symbol">,</span> <span class="number">4</span><span class="symbol">,</span> <span class="number">8</span><span class="symbol">,</span> <span class="number">4</span><span class="symbol">,</span> <span class="number">2</span>
 <span class="symbol">}</span>
<span class="symbol">}</span><span class="symbol">;</span>

<span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">int</span> _matrixHeight <span class="symbol">=</span> <span class="number">2</span><span class="symbol">;</span>

<span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">int</span> _matrixStartX <span class="symbol">=</span> <span class="number">2</span><span class="symbol">;</span>

<span class="keyword">private</span> <span class="keyword">const</span> <span class="keyword">int</span> _matrixWidth <span class="symbol">=</span> <span class="number">5</span><span class="symbol">;</span>
</pre>
</figure>
<p>This sets up the matrix as a static that is only created once.
I've also added some constants to control the offsets as I can't
create an array with a non-zero lower bound. This does smell a
bit so I'll be revisiting this!</p>
<p>Below is the code to dither a single pixel. Remember that the
demonstration program uses a 1D array of <code>ArgbColor</code> structs to
make it easy to read and understand, but you could equally use
direct pointer manipulation on a bitmap's bits, with lots of
extra code to handle different colour depths.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">int</span> redError<span class="symbol">;</span>
<span class="keyword">int</span> blueError<span class="symbol">;</span>
<span class="keyword">int</span> greenError<span class="symbol">;</span>

redError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>R <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>R<span class="symbol">;</span>
blueError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>G <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>G<span class="symbol">;</span>
greenError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>B <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>B<span class="symbol">;</span>

<span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> row <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> row <span class="symbol">&lt;</span> _matrixHeight<span class="symbol">;</span> row<span class="symbol">++</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> offsetY<span class="symbol">;</span>

 offsetY <span class="symbol">=</span> y <span class="symbol">+</span> row<span class="symbol">;</span>

 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> col <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> col <span class="symbol">&lt;</span> _matrixWidth<span class="symbol">;</span> col<span class="symbol">++</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> coefficient<span class="symbol">;</span>
 <span class="keyword">int</span> offsetX<span class="symbol">;</span>

 coefficient <span class="symbol">=</span> _matrix<span class="symbol">[</span>row<span class="symbol">,</span> col<span class="symbol">]</span><span class="symbol">;</span>
 offsetX <span class="symbol">=</span> x <span class="symbol">+</span> <span class="symbol">(</span>col <span class="symbol">-</span> _matrixStartX<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>coefficient <span class="symbol">!=</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> offsetX <span class="symbol">&gt;</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> offsetX <span class="symbol">&lt;</span> width <span class="symbol">&amp;&amp;</span> offsetY <span class="symbol">&gt;</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> offsetY <span class="symbol">&lt;</span> height<span class="symbol">)</span>
 <span class="symbol">{</span>
 ArgbColor offsetPixel<span class="symbol">;</span>
 <span class="keyword">int</span> offsetIndex<span class="symbol">;</span>

 offsetIndex <span class="symbol">=</span> offsetY <span class="symbol">*</span> width <span class="symbol">+</span> offsetX<span class="symbol">;</span>
 offsetPixel <span class="symbol">=</span> original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>redError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">5</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>greenError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">5</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>blueError <span class="symbol">*</span> coefficient<span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">5</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span> <span class="symbol">=</span> offsetPixel<span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Due to the loop this code is now shorter than the
Floyd‑Steinberg version. It's also less readable due the
coefficients being stored in a 2D matrix. Of course, the
algorithm is fixed and won't change so perhaps that's not an
issue, but if performance really was a concern you can unroll
the loop and duplicate all that code. I'll stick with the loop!</p>
<h2 id="final-output">Final Output</h2>
<p>The image below shows our sample image dithered using the Burkes
algorithm. It's very similar to the output created via
Floyd–Steinberg, albeit darker.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-burkes.png" class="gallery" title="The final result - a bitmap transformed with Burkes dithering" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-burkes.png" alt="The final result - a bitmap transformed with Burkes dithering" decoding="async" loading="lazy" /></a><figcaption>The final result - a bitmap transformed with Burkes dithering</figcaption></figure>
<p>Again, by changing the threshold at which colours are converted
to black or white, we can affect the output of the dithering
even if the conversion is to solid black.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-burkes-extreme.png" class="gallery" title="The non-dithered version of this image is solid black" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-burkes-extreme.png" alt="The non-dithered version of this image is solid black" decoding="async" loading="lazy" /></a><figcaption>The non-dithered version of this image is solid black</figcaption></figure><h2 id="source-code">Source Code</h2>
<p>The latest source code for this demonstration (which will be
extended over time to include additional algorithms) can be
found at our <a href="https://github.com/cyotek/Dithering" rel="external nofollow noopener">GitHib page</a>.</p>
<p>The source code from the time this article was created is
available from the link below, however may not be fully up to
date.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2015-06-06 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/dithering-an-image-using-the-burkes-algorithm-in-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comDithering an image using the Floyd‑Steinberg algorithm in C#urn:uuid:24c8a964-e1ca-424d-9200-e58642d4d0982015-06-06T11:01:41Z2015-06-06T11:01:41Z<p>In my <a href="/post/an-introduction-to-dithering-images">previous introductory post</a>, I briefly described the
concept of dithering an image. In this article, I will describe
how to dither an image in C# using the Floyd–Steinberg
algorithm.</p>
<h2 id="the-demo-application">The Demo Application</h2>
<p>For this series of articles, I'll be using the same demo
application, the source of which can be found on <a href="https://github.com/cyotek/Dithering" rel="external nofollow noopener">GitHib</a>.
There's a few things about the demo I wish to cover before I get
onto the actual topic of dithering.</p>
<p>Algorithms can be a tricky thing to learn about, and so I don't
want the demo to be horribly complicated by including a
additional complex code unrelated to dithering. At the same
time, bitmap operations are expensive, so there is already some
advanced code present.</p>
<p>As I mentioned in my introduction, dithering is part of a
process. For this demo, the process will be converting a 32bit
image into a 1bit image as this is the simplest conversion I can
stick in a demo. <strong>This does not mean that the dithering
techniques can only be used to convert an image to black and
white, it is simply to make the demo easier to understand</strong>.</p>
<p>I have however broken this rule when it comes to the actual
image processing. The .NET <code>Bitmap</code> object offers <code>SetPixel</code> and
<code>GetPixel</code> methods. You should try and avoid using these as they
will utterly destroy the performance of whatever it is you are
trying to do. The best way of accessing pixel data is to access
it directly using <code>Bitmap.LockBits</code>, pointer manipulation, then
<code>Bitmap.UnlockBits</code>. In this demo, I use this approach to create
a custom array of colours, and while it is very fast, if you
want better performance it is probably better to manipulate
individual bytes via pointers. However, this requires much more
complex code to account for different colour depths and is well
beyond the scope of this demo.</p>
<blockquote>
<p>I did a version of the demo program using <code>SetPixel</code> and
<code>GetPixel</code>. Saying it was slow was an understatement. Just
pretend these methods don't exist!</p>
</blockquote>
<h2 id="converting-a-colour-to-black-or-white">Converting a colour to black or white</h2>
<p>In order to convert the image to 2 colours, I scan each pixel
and convert it to grayscale. If the grayscale value is around
50% (127 in .NET's 0 - 255 range), then the transformed pixel
will be black, otherwise it will be white.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">byte</span> gray<span class="symbol">;</span>

gray <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">byte</span><span class="symbol">)</span><span class="symbol">(</span><span class="number">0.299</span> <span class="symbol">*</span> pixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="number">0.587</span> <span class="symbol">*</span> pixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="number">0.114</span> <span class="symbol">*</span> pixel<span class="symbol">.</span>B<span class="symbol">)</span><span class="symbol">;</span>

<span class="keyword">return</span> gray <span class="symbol">&lt;</span> <span class="number">128</span> <span class="symbol">?</span> <span class="keyword">new</span> ArgbColor<span class="symbol">(</span>pixel<span class="symbol">.</span>A<span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">)</span> <span class="symbol">:</span> <span class="keyword">new</span> ArgbColor<span class="symbol">(</span>pixel<span class="symbol">.</span>A<span class="symbol">,</span> <span class="number">255</span><span class="symbol">,</span> <span class="number">255</span><span class="symbol">,</span> <span class="number">255</span><span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>This actually creates quite a nice result from our demonstration
image, but results will vary depending on the image.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-threshold.png" class="gallery" title="An example of 1bit conversion via a threshold" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-threshold.png" alt="An example of 1bit conversion via a threshold" decoding="async" loading="lazy" /></a><figcaption>An example of 1bit conversion via a threshold</figcaption></figure><h2 id="floydsteinberg-dithering">Floyd‑Steinberg dithering</h2>
<p>The Floyd‑Steinberg algorithm is an error diffusion algorithm,
meaning for each pixel an &quot;<em>error</em>&quot; is generated and then
distributed to four pixels around the surrounding the current
pixel. Each of the four offset pixels has a different weight -
the error is multiplied by the weight, divided by 16 and then
added to the existing value of the offset pixel.</p>
<p>As a picture is definitely worth a thousand words, the diagram
below shows the weights.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-floyd-steinberg-diagram.png" class="gallery" title="How the error of the current pixel is diffused to its neighbours" ><img src="https://images.cyotek.com/image/devblog/dithering-floyd-steinberg-diagram.png" alt="How the error of the current pixel is diffused to its neighbours" decoding="async" loading="lazy" /></a><figcaption>How the error of the current pixel is diffused to its neighbours</figcaption></figure>
<ul>
<li>7 for the pixel to the right of the current pixel</li>
<li>3 for the pixel below and to the left</li>
<li>5 for the pixel below</li>
<li>1 for the pixel below and to the right</li>
</ul>
<h2 id="calculating-the-error">Calculating the error</h2>
<p>The error calculation in our demonstration program is simple,
although in actuality it's 3 errors, one for the red, green and
blue channels. All we are doing is taking the difference between
the channels transformed value from the original value.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
redError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>R <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>R<span class="symbol">;</span>
blueError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>G <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>G<span class="symbol">;</span>
greenError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>B <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>B<span class="symbol">;</span>
</pre>
</figure>
<h2 id="applying-the-error">Applying the error</h2>
<p>Once we have our error, it's just a case of getting each
neighbouring pixels to adjust, and applying each error the
appropriate channel. The <code>ToByte</code> extension method in the
snippet below simply converts the calculated integer to a byte,
while ensuring it is in the 0-255 range.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>redError <span class="symbol">*</span> <span class="number">7</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>greenError <span class="symbol">*</span> <span class="number">7</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>blueError <span class="symbol">*</span> <span class="number">7</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<blockquote>
<h3 id="bit-shifting-for-division">Bit shifting for division</h3>
<p>As 16 is a power of two, it means we can use bit shifting to
do the division. While this may be slightly less readable if
you aren't hugely familiar with it, it ought to be faster. I
did a quick benchmark test using a sample of 1 million, 10
million and then 100 million random numbers. Using bit
shifting to divide each sample by 16 took roughly two thirds
of the time it took to do the same sets with integer division.
This is probably a useful thing to know when performing
thousands of operations processing an image.</p>
</blockquote>
<h2 id="dithering-a-single-pixel">Dithering a single pixel</h2>
<p>Here's the code used by the demonstration program to dither a
single source pixel - the <code>ArbColor</code> data representing each
pixel is stored in a one-dimensional array using <a href="/post/converting-2d-arrays-to-1d-and-accessing-as-either-2d-or-1d">row-major
order</a>.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
ArgbColor offsetPixel<span class="symbol">;</span>
<span class="keyword">int</span> redError<span class="symbol">;</span>
<span class="keyword">int</span> blueError<span class="symbol">;</span>
<span class="keyword">int</span> greenError<span class="symbol">;</span>
<span class="keyword">int</span> offsetIndex<span class="symbol">;</span>
<span class="keyword">int</span> index<span class="symbol">;</span>

index <span class="symbol">=</span> y <span class="symbol">*</span> width <span class="symbol">+</span> x<span class="symbol">;</span>
redError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>R <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>R<span class="symbol">;</span>
blueError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>G <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>G<span class="symbol">;</span>
greenError <span class="symbol">=</span> originalPixel<span class="symbol">.</span>B <span class="symbol">-</span> transformedPixel<span class="symbol">.</span>B<span class="symbol">;</span>

<span class="keyword">if</span> <span class="symbol">(</span>x <span class="symbol">+</span> <span class="number">1</span> <span class="symbol">&lt;</span> width<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="comment">// right</span>
 offsetIndex <span class="symbol">=</span> index <span class="symbol">+</span> <span class="number">1</span><span class="symbol">;</span>
 offsetPixel <span class="symbol">=</span> original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>redError <span class="symbol">*</span> <span class="number">7</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>greenError <span class="symbol">*</span> <span class="number">7</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>blueError <span class="symbol">*</span> <span class="number">7</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span> <span class="symbol">=</span> offsetPixel<span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">if</span> <span class="symbol">(</span>y <span class="symbol">+</span> <span class="number">1</span> <span class="symbol">&lt;</span> height<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>x <span class="symbol">-</span> <span class="number">1</span> <span class="symbol">&gt;</span> <span class="number">0</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="comment">// left and down</span>
 offsetIndex <span class="symbol">=</span> index <span class="symbol">+</span> width <span class="symbol">-</span> <span class="number">1</span><span class="symbol">;</span>
 offsetPixel <span class="symbol">=</span> original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>redError <span class="symbol">*</span> <span class="number">3</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>greenError <span class="symbol">*</span> <span class="number">3</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>blueError <span class="symbol">*</span> <span class="number">3</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span> <span class="symbol">=</span> offsetPixel<span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// down</span>
 offsetIndex <span class="symbol">=</span> index <span class="symbol">+</span> width<span class="symbol">;</span>
 offsetPixel <span class="symbol">=</span> original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>redError <span class="symbol">*</span> <span class="number">5</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>greenError <span class="symbol">*</span> <span class="number">5</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>blueError <span class="symbol">*</span> <span class="number">5</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span> <span class="symbol">=</span> offsetPixel<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>x <span class="symbol">+</span> <span class="number">1</span> <span class="symbol">&lt;</span> width<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="comment">// right and down</span>
 offsetIndex <span class="symbol">=</span> index <span class="symbol">+</span> width <span class="symbol">+</span> <span class="number">1</span><span class="symbol">;</span>
 offsetPixel <span class="symbol">=</span> original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>R <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>R <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>redError <span class="symbol">*</span> <span class="number">1</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>G <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>G <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>greenError <span class="symbol">*</span> <span class="number">1</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 offsetPixel<span class="symbol">.</span>B <span class="symbol">=</span> <span class="symbol">(</span>offsetPixel<span class="symbol">.</span>B <span class="symbol">+</span> <span class="symbol">(</span><span class="symbol">(</span>blueError <span class="symbol">*</span> <span class="number">1</span><span class="symbol">)</span> <span class="symbol">&gt;&gt;</span> <span class="number">4</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">.</span>ToByte<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 original<span class="symbol">[</span>offsetIndex<span class="symbol">]</span> <span class="symbol">=</span> offsetPixel<span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Much of the code is duplicated, with a different co-efficient
for the multiplication, and (importantly!) guards to skip pixels
when the current pixel is either the first or last pixel in the
row, or is within the final row.</p>
<h2 id="and-the-result">And the result?</h2>
<p>The image below shows our sample image dithered using the
Floyd–Steinberg algorithm. It doesn't look too bad!</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-floyd-steinberg.png" class="gallery" title="The final result - a bitmap transformed with Floyd–Steinberg dithering" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-floyd-steinberg.png" alt="The final result - a bitmap transformed with Floyd–Steinberg dithering" decoding="async" loading="lazy" /></a><figcaption>The final result - a bitmap transformed with Floyd–Steinberg dithering</figcaption></figure>
<p>By changing the threshold at which colours are converted to
black or white, we can affect the output of the dithering even
if the conversion is to solid black.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-floyd-steinberg-extreme.png" class="gallery" title="A slightly more extreme black and white conversion still dithers fairly well" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-floyd-steinberg-extreme.png" alt="A slightly more extreme black and white conversion still dithers fairly well" decoding="async" loading="lazy" /></a><figcaption>A slightly more extreme black and white conversion still dithers fairly well</figcaption></figure>
<p><em>(Note: The thumbnail hasn't resized well, the actual size
version looks better)</em></p>
<h2 id="source-code">Source Code</h2>
<p>The latest source code for this demonstration (which will be
extended over time to include additional algorithms) can be
found at our <a href="https://github.com/cyotek/Dithering" rel="external nofollow noopener">GitHib page</a>.</p>
<p>The source code from the time this article was created is
available from the link below, however may not be fully up to
date.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2015-06-06 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/dithering-an-image-using-the-floyd-steinberg-algorithm-in-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comAn introduction to dithering imagesurn:uuid:d1a52a83-2650-49b0-a59d-3a30c327f9602015-06-06T11:00:01Z2015-06-06T11:00:01Z<p>When you reduce the number of colours in an image, it's often
hard to get a 1:1 match, and so typically you can expect to see
banding in an image - areas of unbroken solid colours where once
multiple similar colours were present. Such banding can often
ruin the look of the image, however by using dithering
algorithms you can reduce such banding and greatly improve the
appearance of the reduced image.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-original.png" class="gallery" title="The sample image our demonstration program will be using, a picture of the Tower of London" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-original.png" alt="The sample image our demonstration program will be using, a picture of the Tower of London" decoding="async" loading="lazy" /></a><figcaption>The sample image our demonstration program will be using, a picture of the Tower of London</figcaption></figure>
<p>Here we see a nice view of the Tower of London (Image Credit:
<a href="http://www.publicdomainpictures.net/view-image.php?image=18490" rel="external nofollow noopener">Vera Kratochvil</a>). Lets say we wanted to reduce the number
of colours in this image to 256 using the <a href="http://en.wikipedia.org/wiki/Web_colors" rel="external nofollow noopener">web safe colour
palette</a>.</p>
<p>If we simply reduce the colour depth by matching the nearest
colour in the old palette to one in the new, then we'll get
something similar to the image below. As is quite evident, the
skyline has been badly effected by banding.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-banding.png" class="gallery" title="Not exactly the best representation of the original image" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-banding.png" alt="Not exactly the best representation of the original image" decoding="async" loading="lazy" /></a><figcaption>Not exactly the best representation of the original image</figcaption></figure>
<p>However, by applying a technique known as dithering, we can
still reduce the colour depth using exactly the same palette,
and get something comparable to the original and more
aesthetically pleasing.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-errordiffusion.png" class="gallery" title="That looks a lot better!" ><img src="https://images.cyotek.com/image/thumbnail/devblog/dithering-errordiffusion.png" alt="That looks a lot better!" decoding="async" loading="lazy" /></a><figcaption>That looks a lot better!</figcaption></figure><h2 id="types-of-dithering">Types of dithering</h2>
<p>There are several different types of dithering, mostly falling
into <strong>Ordered</strong> or <strong>Unordered</strong> categories.</p>
<p>Ordered dithering uses a patterned matrix in order to dither the
image. An example of this is the very distinctive (and
nostalgic!) <em>Bayer</em> algorithm.</p>
<p>Unordered, or error diffusion, dithering calculates an error
value for each pixel and then propagates this to the
neighbouring pixels, often with very good results. The most well
known of these is <em>Floyd–Steinberg</em>, although there are several
more such as <em>Burkes</em>, and <em>Sierra</em>.</p>
<blockquote>
<p>You could potentially use dithering for applications other
than images. An image is simply a block of pixel data, i.e.
colours. Colours are just numbers, and so is a great deal of
other data. So in theory you can dither a lot more than &quot;just&quot;
images.</p>
</blockquote>
<h2 id="dithering-via-error-diffusion">Dithering via Error Diffusion</h2>
<p>For at least the first part of this series, I will be
concentrating on error diffusion. For this algorithm, you scan
the image from left to right, top to bottom and visit each
pixel. Then, for each pixel, you calculate a value known as the
&quot;error&quot;.</p>
<p>After calculating the error it is then applied to one or more
neighbouring values that haven't yet been processed. Generally,
this would mean adjusting at least 3 neighbouring cells, but
depending on the algorithm this could be quite a few more. I'll
go into this in more detail when I describe individual dithering
algorithms in subsequent posts.</p>
<blockquote>
<p>So how do you determine the error? Well, hopefully is clear
that you don't dither an image as a single process. There has
to be another piece in the puzzle, a process to transform a
value. The <em>error</em> therefore is the difference between the
original and new values. When it comes to images, typically
this is going to a form of colour reduction, for example 32bit
(16 million colours) to 8bit (256 colours).</p>
</blockquote>
<p>The diagram below tries to show what I mean - the grey boxes are
pixels that have been processed. The blue box is the pixel that
is currently being transformed, with the green therefore being
unprocessed pixels and candidates for the error diffusion. The
arrows simply highlight that the candidates are always forward
of the current pixel, and not behind it.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/dithering-diagram.png" class="gallery" title="A small illustration to try and demonstrate how the error diffusion works" ><img src="https://images.cyotek.com/image/devblog/dithering-diagram.png" alt="A small illustration to try and demonstrate how the error diffusion works" decoding="async" loading="lazy" /></a><figcaption>A small illustration to try and demonstrate how the error diffusion works</figcaption></figure>
<blockquote>
<p>It's worth repeating that the error is <strong>not</strong> applied to any
previously transformed value. If you do modify an already
processed value, then you would need to have some way of
reprocessing it (as the combined value+error may not be valid
for your reduction method), which could get messy fast.</p>
</blockquote>
<h2 id="next-steps">Next Steps</h2>
<p>Hopefully this article serves as at least a basic and high level
overview of dithering - additional posts will deal with the
actual implementation of dithering.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2015-06-06 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/an-introduction-to-dithering-images .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comDisplaying the contents of a PDF file in an ASP.NET application using GhostScripturn:uuid:808da458-9ecb-459e-8797-0c9f80a1be6e2012-07-10T19:06:46Z2012-07-10T19:06:46Z<p>After receiving quite a few requests on making the <a href="/post/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript">PDF image
conversion</a> work in a web application, I wanted to see how
hard it would be to do. Not hard at all as it turns out, I had a
nice working sample running with a bare 5 minutes of work.</p>
<p>The sample available for download below is a basic ASP.NET
application, comprised of a single page with an <code>IHttpHandler</code>
for displaying the image. In order to make this sample as easy
as possible, it uses pure server side controls and code, nothing
client side.</p>
<h2 id="getting-started">Getting Started</h2>
<p>In order to run this sample, you'll need the
<a href="https://www.cyotek.com/downloads/view/Cyotek.GhostScript.zip/"><code>Cyotek.GhostScript</code></a> and
<a href="https://www.cyotek.com/downloads/view/Cyotek.GhostScript.PdfConversion.zip/"><code>Cyotek.GhostScript.PdfConversion.zip</code></a> components described
in a <a href="/post/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript">previous article</a>.</p>
<p>You'll also need to download <a href="http://www.ghostscript.com/" rel="external nofollow noopener">GhostScript</a>. As with my other
articles on the subject, please make sure you check their
license terms - they seem very keen that people don't use the
GPL version or distribute GhostScript without a commercial
license.</p>
<h2 id="locating-gsdll32.dll">Locating gsdll32.dll</h2>
<p>In order for this to work, <code>gsdll32.dll</code> needs to be somewhere
in your applications path. This could be in your <code>system32</code>
directory on 32bit Windows, or <code>SysWOW64</code> on 64bit Windows.</p>
<p>While developing this sample, I also tried having the file in
the bin directory of the website - this also worked fine.
However, as the website was running on my local machine, it's
probably running in Full Trust, and I have no idea if it will
work in Medium Trust or lower.</p>
<h3 id="im-running-64bit-windows">I'm running 64bit Windows</h3>
<p>Congratulations! I have nothing but issues with 32bit web
servers. But I digress. The sample projects I have provided on
this website all use the 32bit version of GhostScript. There is
a 64bit version available, but I haven't downloaded it to test.
Your options should be as follows:</p>
<ul>
<li>Build against the 64bit GhostScript DLL. This may need some
refactoring if their public API has changed. At the very
least, you'll need to change the DLL filename in the native
method calls.</li>
<li>Using IIS7 or higher? Keep using the 32bit version, and set
your worker pool to run in 32bit mode</li>
<li>Using IIS6? Commiserations, I feel your pain. The only option
here, if you stay 32bit, is to have the entire IIS run as
32bit.</li>
</ul>
<p>I have tested on a Windows 7 Professional 64bit machine as
follows:</p>
<ul>
<li>Firstly, using IISExpress which is running as a 32bit process</li>
<li>Secondly, using IIS7 with a custom application pool running in
32bit mode</li>
</ul>
<p>Both of these scenarios worked perfectly well.</p>
<h2 id="creating-the-solution">Creating the solution</h2>
<p>Create a new <strong>ASP.NET Web Forms Site</strong></p>
<blockquote>
<p>Note: Even though this example uses pure WebForms, there's no
reason that this sort of code won't work fine in ASP.NET MVC
or any other .NET framework of your choice.</p>
</blockquote>
<p>Open up <code>Default.aspx</code> and add some controls similar to the
following:</p>
<figure class="lang-html highlight"><figcaption><span>html</span></figcaption><pre class="code">
<span class="htmlServerSideScript">&lt;%</span><span class="literal">@</span> <span class="name">Page</span> <span class="name">Language</span><span class="symbol">=</span><span class="attribute">&quot;C#&quot;</span> <span class="name">AutoEventWireup</span><span class="symbol">=</span><span class="attribute">&quot;true&quot;</span> <span class="name">CodeBehind</span><span class="symbol">=</span><span class="attribute">&quot;Default.aspx.cs&quot;</span> <span class="name">Inherits</span><span class="symbol">=</span><span class="attribute">&quot;GhostScriptWebTest._Default&quot;</span> <span class="htmlServerSideScript">%&gt;</span>

<span class="literal">&lt;!</span><span class="name">DOCTYPE</span> <span class="name">html</span> <span class="name">PUBLIC</span> <span class="attribute">&quot;-//W3C//DTD XHTML 1.0 Transitional//EN&quot;</span> <span class="attribute">&quot;http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd&quot;</span><span class="literal">&gt;</span>

<span class="literal">&lt;</span><span class="name">html</span> <span class="name">xmlns</span><span class="symbol">=</span><span class="attribute">&quot;http://www.w3.org/1999/xhtml&quot;</span><span class="literal">&gt;</span>
<span class="literal">&lt;</span><span class="name">head</span> <span class="name">runat</span><span class="symbol">=</span><span class="attribute">&quot;server&quot;</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">title</span><span class="literal">&gt;</span>PDF Conversion Example<span class="literal">&lt;/</span><span class="name">title</span><span class="literal">&gt;</span>
<span class="literal">&lt;/</span><span class="name">head</span><span class="literal">&gt;</span>
<span class="literal">&lt;</span><span class="name">body</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">form</span> <span class="name">id</span><span class="symbol">=</span><span class="attribute">&quot;form1&quot;</span> <span class="name">runat</span><span class="symbol">=</span><span class="attribute">&quot;server&quot;</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">div</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">p</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">asp</span><span class="literal">:</span><span class="name">LinkButton</span> <span class="name">runat</span><span class="symbol">=</span><span class="attribute">&quot;server&quot;</span> <span class="name">ID</span><span class="symbol">=</span><span class="attribute">&quot;previousLinkButton&quot;</span> <span class="name">Text</span><span class="symbol">=</span><span class="attribute">&quot;Previous&quot;</span> <span class="name">OnClick</span><span class="symbol">=</span><span class="attribute">&quot;previousLinkButton_Click&quot;</span> <span class="literal">/&gt;</span>
 <span class="literal">&lt;</span><span class="name">asp</span><span class="literal">:</span><span class="name">LinkButton</span> <span class="name">runat</span><span class="symbol">=</span><span class="attribute">&quot;server&quot;</span> <span class="name">ID</span><span class="symbol">=</span><span class="attribute">&quot;nextLinkButton&quot;</span> <span class="name">Text</span><span class="symbol">=</span><span class="attribute">&quot;Next&quot;</span> <span class="name">OnClick</span><span class="symbol">=</span><span class="attribute">&quot;nextLinkButton_Click&quot;</span> <span class="literal">/&gt;</span>
 <span class="literal">&lt;/</span><span class="name">p</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">p</span><span class="literal">&gt;</span>
 <span class="literal">&lt;</span><span class="name">asp</span><span class="literal">:</span><span class="name">Image</span> <span class="name">runat</span><span class="symbol">=</span><span class="attribute">&quot;server&quot;</span> <span class="name">ID</span><span class="symbol">=</span><span class="attribute">&quot;pdfImage&quot;</span> <span class="name">ImageUrl</span><span class="symbol">=</span><span class="attribute">&quot;~/PdfImage.ashx?fileName=sample.pdf&amp;page=1&quot;</span> <span class="literal">/&gt;</span>
 <span class="literal">&lt;/</span><span class="name">p</span><span class="literal">&gt;</span>
 <span class="literal">&lt;/</span><span class="name">div</span><span class="literal">&gt;</span>
 <span class="literal">&lt;/</span><span class="name">form</span><span class="literal">&gt;</span>
<span class="literal">&lt;/</span><span class="name">body</span><span class="literal">&gt;</span>
<span class="literal">&lt;/</span><span class="name">html</span><span class="literal">&gt;</span>
</pre>
</figure>
<p>The controls should be fairly self explanatory! The main thing
of interest is the <code>pdfImage</code> Image control - this will call a
<em>Generic Handler</em> that I'll describe in the next section. Note
that VS2010 and VS2012 have another option, an <strong>ASP.NET
Handler</strong> - this implements the same <code>IHttpHandler</code> interface
but doesn't have a <code>.ashx</code> file and is registered differently.
If you are using IIS7 or above, you're probably better off using
that.</p>
<p>Note that by default the <code>pdfImage</code> control is pointing to a
sample file named <em>sample.pdf</em> - add any old PDF to the root of
your website and name it sample. Ensure that the <strong>Build
Action</strong> for the PDF is set to <strong>Content</strong>, otherwise it won't
be deployed with your application.</p>
<h3 id="creating-the-image-handler">Creating the image handler</h3>
<p>Tutorials on creating image handlers with <code>IHttpHandler</code> can be
found scattered throughout the net, so I'll not go into how they
work, but just describe the implementation I'm using in this
example. Add a new generic handler to your project, then fill in
the <code>ProcessRequest</code> method as follows. Make sure you add the
two GhostScript API components to your solution and add
references to them to your web application first!</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">using</span> System<span class="symbol">;</span>
<span class="keyword">using</span> System<span class="symbol">.</span>Drawing<span class="symbol">;</span>
<span class="keyword">using</span> System<span class="symbol">.</span>Drawing<span class="symbol">.</span>Imaging<span class="symbol">;</span>
<span class="keyword">using</span> System<span class="symbol">.</span>Web<span class="symbol">;</span>
<span class="keyword">using</span> Cyotek<span class="symbol">.</span>GhostScript<span class="symbol">.</span>PdfConversion<span class="symbol">;</span>

<span class="keyword">namespace</span> GhostScriptWebTest
<span class="symbol">{</span>
 <span class="keyword">public</span> <span class="keyword">class</span> PdfImage <span class="symbol">:</span> IHttpHandler
 <span class="symbol">{</span>
 <span class="keyword">public</span> <span class="keyword">void</span> ProcessRequest<span class="symbol">(</span>HttpContext context<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">string</span> fileName<span class="symbol">;</span>
 <span class="keyword">int</span> pageNumber<span class="symbol">;</span>
 Pdf<span class="number">2</span>Image convertor<span class="symbol">;</span>
 Bitmap image<span class="symbol">;</span>

 fileName <span class="symbol">=</span> context<span class="symbol">.</span>Server<span class="symbol">.</span>MapPath<span class="symbol">(</span><span class="string">&quot;~/&quot;</span> <span class="symbol">+</span> context<span class="symbol">.</span>Request<span class="symbol">.</span>QueryString<span class="symbol">[</span><span class="string">&quot;fileName&quot;</span><span class="symbol">]</span><span class="symbol">)</span><span class="symbol">;</span>
 pageNumber <span class="symbol">=</span> Convert<span class="symbol">.</span>ToInt<span class="number">32</span><span class="symbol">(</span>context<span class="symbol">.</span>Request<span class="symbol">.</span>QueryString<span class="symbol">[</span><span class="string">&quot;page&quot;</span><span class="symbol">]</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// convert the image</span>
 convertor <span class="symbol">=</span> <span class="keyword">new</span> Pdf<span class="number">2</span>Image<span class="symbol">(</span>fileName<span class="symbol">)</span><span class="symbol">;</span>
 image <span class="symbol">=</span> convertor<span class="symbol">.</span>GetImage<span class="symbol">(</span>pageNumber<span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// set the content type</span>
 context<span class="symbol">.</span>Response<span class="symbol">.</span>ContentType <span class="symbol">=</span> <span class="string">&quot;image/png&quot;</span><span class="symbol">;</span>

 <span class="comment">// save the image directly to the response stream</span>
 image<span class="symbol">.</span>Save<span class="symbol">(</span>context<span class="symbol">.</span>Response<span class="symbol">.</span>OutputStream<span class="symbol">,</span> ImageFormat<span class="symbol">.</span>Png<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">public</span> <span class="keyword">bool</span> IsReusable
 <span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">true</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Again, this is extremely simple code. I extract the query string
of the request to obtain the file name of the PDF document to
convert, and the page to display. I then create an instance of
the <code>Pdf2Image</code> class, and grab an image of the specified page.</p>
<p>Next, you need to set the <code>ContentType</code> of the <code>Response</code> object
so the web browser knows what to do with your content. Finally,
I save the image directly to the response's <code>OutputStream</code>. Make
sure that the format you save the image as matches the content
type you've specified.</p>
<p>With these steps complete, building and running the website
should present you with a pair of hyper links, and the first
page of your PDF file as a static image. [Well, it will if you
add a pair of blank event handlers for those defined for the two
hyperlink buttons anyway]</p>
<h2 id="simple-navigation">Simple navigation</h2>
<p>Now that we can display our PDF, we'll add some basic
navigation. Open up the code behind file for <code>Default.aspx</code> and
fill in the event handlers for the two hyperlink buttons.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">using</span> System<span class="symbol">;</span>
<span class="keyword">using</span> System<span class="symbol">.</span>Collections<span class="symbol">.</span>Specialized<span class="symbol">;</span>
<span class="keyword">using</span> System<span class="symbol">.</span>Web<span class="symbol">;</span>
<span class="keyword">using</span> Cyotek<span class="symbol">.</span>GhostScript<span class="symbol">.</span>PdfConversion<span class="symbol">;</span>

<span class="keyword">namespace</span> GhostScriptWebTest
<span class="symbol">{</span>
 <span class="keyword">public</span> <span class="keyword">partial</span> <span class="keyword">class</span> _Default <span class="symbol">:</span> System<span class="symbol">.</span>Web<span class="symbol">.</span>UI<span class="symbol">.</span>Page
 <span class="symbol">{</span>
 <span class="keyword">protected</span> <span class="keyword">void</span> previousLinkButton_Click<span class="symbol">(</span><span class="keyword">object</span> sender<span class="symbol">,</span> EventArgs e<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>IncrementPage<span class="symbol">(</span><span class="symbol">-</span><span class="number">1</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">protected</span> <span class="keyword">void</span> nextLinkButton_Click<span class="symbol">(</span><span class="keyword">object</span> sender<span class="symbol">,</span> EventArgs e<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>IncrementPage<span class="symbol">(</span><span class="number">1</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">private</span> <span class="keyword">void</span> IncrementPage<span class="symbol">(</span><span class="keyword">int</span> increment<span class="symbol">)</span>
 <span class="symbol">{</span>
 NameValueCollection queryString<span class="symbol">;</span>
 <span class="keyword">int</span> pageNumber<span class="symbol">;</span>
 <span class="keyword">string</span> pdfFileName<span class="symbol">;</span>
 Pdf<span class="number">2</span>Image converter<span class="symbol">;</span>

 queryString <span class="symbol">=</span> HttpUtility<span class="symbol">.</span>ParseQueryString<span class="symbol">(</span>pdfImage<span class="symbol">.</span>ImageUrl<span class="symbol">.</span>Substring<span class="symbol">(</span>pdfImage<span class="symbol">.</span>ImageUrl<span class="symbol">.</span>IndexOf<span class="symbol">(</span><span class="string">&quot;?&quot;</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 pdfFileName <span class="symbol">=</span> queryString<span class="symbol">[</span><span class="string">&quot;fileName&quot;</span><span class="symbol">]</span><span class="symbol">;</span>
 pageNumber <span class="symbol">=</span> Convert<span class="symbol">.</span>ToInt<span class="number">32</span><span class="symbol">(</span>queryString<span class="symbol">[</span><span class="string">&quot;page&quot;</span><span class="symbol">]</span><span class="symbol">)</span> <span class="symbol">+</span> increment<span class="symbol">;</span>
 converter <span class="symbol">=</span> <span class="keyword">new</span> Pdf<span class="number">2</span>Image<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Server<span class="symbol">.</span>MapPath<span class="symbol">(</span><span class="string">&quot;~/&quot;</span> <span class="symbol">+</span> pdfFileName<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>pageNumber <span class="symbol">&gt;</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> pageNumber <span class="symbol">&lt;=</span> converter<span class="symbol">.</span>PageCount<span class="symbol">)</span>
 pdfImage<span class="symbol">.</span>ImageUrl <span class="symbol">=</span> <span class="keyword">string</span><span class="symbol">.</span>Format<span class="symbol">(</span><span class="string">&quot;~/PdfImage.ashx?fileName={0}&amp;page={1}&quot;</span><span class="symbol">,</span> pdfFileName<span class="symbol">,</span> pageNumber<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>As with the image handler, this code simply extracts the file
name of the PDF file and the current page number. It also
creates a new instance of the <code>Pdf2Image</code> class in order to
obtain the number of pages in the document. If the new page
number is in range, it updates the <code>ImageUrl</code> of the <code>pdfImage</code>
causing the image handler to pull back the next page.</p>
<h2 id="in-conclusion">In Conclusion</h2>
<p>This sample is pretty inefficient and at the very least should
be caching the images. But, it's as simple an example as I can
make. Hopefully someone will find it useful. At the present time
I'm not working with the GhostScript API library so I suspect
this will be the last article on the subject for the time being.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2012-07-10 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/displaying-the-contents-of-a-pdf-file-in-an-asp-net-application-using-ghostscript .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comArcade explosion generatorurn:uuid:c44b7b56-f7ca-4f7d-9c20-82619ff939202012-06-05T09:47:03Z2012-06-03T19:57:59Z<p>Over the past few weeks I've been messing around creating a
unique graphics for our <a href="http://binaryrealms.co.uk/jewel-rush" rel="external nofollow noopener">Jewel Rush</a> game. One of the things
I was experimenting with was explosion animations. Although
tools exist for <a href="http://www.positech.co.uk/content/explosion/explosiongenerator.html" rel="external nofollow noopener">generating explosions</a> the problem with most
of these is that they create large sprites which don't shrink
well, and the output is a bit more realistic than what I was
looking for.</p>
<p>And while I'm competent enough to do application graphics (more
or less!), gaming graphics are a completely different kettle of
fish!</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/aeg1a.png" class="gallery" title="A screenshot from Missile Command" ><img src="https://images.cyotek.com/image/thumbnail/devblog/aeg1a.png" alt="A screenshot from Missile Command" decoding="async" loading="lazy" /></a><figcaption>A screenshot from Missile Command</figcaption></figure>
<p>Above is a screenshot from Missile Command, a classic from
Atari. That's the sort of explosions I wanted to create, so I
wrote a small tool that would create these sort of graphics in a
random (but reproducible) fashion and export them to images for
use in other tools such as <a href="https://cyotek.com/cyotek-spriter">Spriter</a>. As it turned out, the
graphics it produces didn't end up quite that way (I was having
problems with the intersection stuff) but it's usable enough for
the purposes I want.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/aeg1b.png" class="gallery" title="A sample of the default output" ><img src="https://images.cyotek.com/image/thumbnail/devblog/aeg1b.png" alt="A sample of the default output" decoding="async" loading="lazy" /></a><figcaption>A sample of the default output</figcaption></figure><figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/aeg1c.png" class="gallery" title="Another sample using some custom settings" ><img src="https://images.cyotek.com/image/thumbnail/devblog/aeg1c.png" alt="Another sample using some custom settings" decoding="async" loading="lazy" /></a><figcaption>Another sample using some custom settings</figcaption></figure>
<p>The application was thrown together over the weekend so it's
probably not hugely robust and may contain a small army of bugs.
But it works and is possibly an interesting starting point for
other projects. There's some interesting bits of code here and
there, although I'm not writing about the implementation of the
code.</p>
<h2 id="application-features">Application Features</h2>
<ul>
<li>Configuration settings can be saved and reloaded for tweaking
of favoured settings. Uses basic Reflection serialization as
<code>XmlSerializer</code> can't handle colors without having to create
duplicate color properties in string format.</li>
<li>Can export either a complete sprite sheet, or the individual
images</li>
<li>Copy the sprite sheet to the clipboard (although I noticed
that transparent doesn't work if you do, something to look at
later)</li>
<li>Uses the ImageBox (of course!) for displaying previews</li>
<li>The <code>TrackBar</code> control embedded in the <code>ToolStrip</code> is a custom
component inheriting from <code>ToolStripControlHost</code> which can be
reused. And once you understand the principles, it's so easy
to host other controls.</li>
</ul>
<h2 id="graphic-settings">Graphic Settings</h2>
<ul>
<li>Either specify a seed to always recreate the same explosion,
or use a random seed each time. (If you find a seed you like,
clicking the seed number in the status bar will apply it to
your configuration settings).</li>
<li>Specify the number of animation frames that will be generated,
and the size of the frames</li>
<li>Specify the maximum number of explosion booms available at
once. There's also an option to automatically remove and
recreate &quot;expired&quot; blooms.</li>
<li>Choose the colors used to render the blooms</li>
<li>Specify the percentage by which blooms grow (and shrink), and
how many growth states there are. Once a bloom has shrunk to
its minimum size, it is expired and no longer draw.</li>
<li>Anti alias options, useful if you don't want pixel graphics</li>
<li>Border size and growth</li>
<li>Set a random order, in which newly created blooms will be
inserted randomly in the list.</li>
<li>An experimental mask mode which was supposed to enable me to
create those Missile Command style XOR drawing. However, it
doesn't really work and I'll probably have another go at it at
some point.</li>
</ul>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/aeg1d.png" class="gallery" title="An example generated by the project" ><img src="https://images.cyotek.com/image/devblog/aeg1d.png" alt="An example generated by the project" decoding="async" loading="lazy" /></a><figcaption>An example generated by the project</figcaption></figure><figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/aeg1e.png" class="gallery" title="Another example of generated output" ><img src="https://images.cyotek.com/image/devblog/aeg1e.png" alt="Another example of generated output" decoding="async" loading="lazy" /></a><figcaption>Another example of generated output</figcaption></figure><h2 id="room-for-improvement">Room for improvement</h2>
<p>Everything can be improved, one of the ideas I'd had for this
tool was greater control over blooms, allowing you configure
their locations etc with better precision but it wasn't
necessary for the graphic I was creating. As mentioned above,
the masking doesn't work as expected, it would have been nice if
it did. Some better rendering would be a plus too, at the moment
the &quot;explosions&quot; are simple rings of color. Some noise or other
minor particle effects to make them a little less uniform would
probably look interesting.</p>
<h2 id="source-code">Source Code</h2>
<p>Source code, and optionally pre-compiled binaries, are available
from the link below. The code has been compiled against the .NET
3.5 Client Profile. Due to some minor use of Linq and auto
generated properties a small amount of work would be needed to
compile against .NET 2.0. I'm afraid comments are somewhat
lacking as well, I wasn't planning on releasing this publicly
originally.</p>
<p>If anyone creates any interesting graphics or improves upon the
code, we'd <a href="https://cyotek.com/contact">love to hear</a> from you.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2012-06-03 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/arcade-explosion-generator .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comExtending the ImageBox component to display the contents of a PDF file using C#urn:uuid:6127dd18-25af-4467-ac03-cf9ca52236512011-09-04T16:48:55Z2011-09-04T16:48:55Z<p>In this article, I'll describe how to extend the <a href="/tag/imagebox">ImageBox</a>
control discussed in earlier articles to be able to display PDF
files with the help of the <a href="http://www.ghostscript.com/" rel="external nofollow noopener">GhostScript library</a> and the
conversion library described in the <a href="/post/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript">previous article</a>.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/pdfimagebox.png" class="gallery" title="A sample application demonstrating displaying a PDF file in the ImageBox control" ><img src="https://images.cyotek.com/image/thumbnail/devblog/pdfimagebox.png" alt="A sample application demonstrating displaying a PDF file in the ImageBox control" decoding="async" loading="lazy" /></a><figcaption>A sample application demonstrating displaying a PDF file in the ImageBox control</figcaption></figure><h2 id="getting-started">Getting Started</h2>
<p>You can download the source code used in this article from the
links below, these are:</p>
<ul>
<li><strong>Cyotek.GhostScript</strong> - core library providing GhostScript
integration support</li>
<li><strong>Cyotek.GhostScript.PdfConversion</strong> - support library for
converting a PDF document into images</li>
<li><strong>PdfImageBoxSample</strong> - sample project containing an updated
<code>ImageBox</code> control, and the extended <code>PdfImageBox</code>.</li>
</ul>
<blockquote>
<p>Please note that the native GhostScript DLL is not included in
these downloads, you will need to obtain that from the
<a href="http://www.ghostscript.com/" rel="external nofollow noopener">GhostScript project page</a>.</p>
</blockquote>
<h2 id="extending-the-imagebox">Extending the ImageBox</h2>
<p>To start extending the <code>ImageBox</code>, create a new class and
inherit the <code>ImageBox</code> control. I also decided to override some
of the default properties, so I added a constructor which sets
the new values.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> PdfImageBox<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="comment">// override some of the original ImageBox defaults</span>
 <span class="keyword">this</span><span class="symbol">.</span>GridDisplayMode <span class="symbol">=</span> ImageBoxGridDisplayMode<span class="symbol">.</span>None<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>BackColor <span class="symbol">=</span> SystemColors<span class="symbol">.</span>AppWorkspace<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>ImageBorderStyle <span class="symbol">=</span> ImageBoxBorderStyle<span class="symbol">.</span>FixedSingleDropShadow<span class="symbol">;</span>

 <span class="comment">// new pdf conversion settings</span>
 <span class="keyword">this</span><span class="symbol">.</span>Settings <span class="symbol">=</span> <span class="keyword">new</span> Pdf<span class="number">2</span>ImageSettings<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>To ensure correct designer support, override versions of the
properties with new <code>DefaultValue</code> attributes were added. With
this done, it's time to add the new properties that will support
viewing PDF files. The new properties are:</p>
<ul>
<li><code>PdfFileName</code> - the filename of the PDF to view</li>
<li><code>PdfPassword</code> - specifies the password of the PDF file if one
is required to open it (<em>note, I haven't actually tested that
this works!</em>)</li>
<li><code>Settings</code> - uses the <code>Pdf2ImageSettings</code> class <a href="/post/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript">discussed
earlier</a> to control quality settings for the converted
document.</li>
<li><code>PageCache</code> - an internal dictionary which stores a <code>Bitmap</code>
against a page number to cache pages after these have loaded.</li>
</ul>
<p>With the exception of <code>PageCache</code>, each of these properties also
has backing event for change notifications, and as
<code>Pdf2ImageSettings</code> implements <code>INotifyPropertyChanged</code> we'll
also bind an event detect when the individual setting properties
are modified.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="symbol">[</span>Category<span class="symbol">(</span><span class="string">&quot;Appearance&quot;</span><span class="symbol">)</span><span class="symbol">,</span> DefaultValue<span class="symbol">(</span><span class="keyword">typeof</span><span class="symbol">(</span>Pdf<span class="number">2</span>ImageSettings<span class="symbol">)</span><span class="symbol">,</span> <span class="string">&quot;&quot;</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">virtual</span> Pdf<span class="number">2</span>ImageSettings Settings
<span class="symbol">{</span>
 <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> _settings<span class="symbol">;</span> <span class="symbol">}</span>
 <span class="keyword">set</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Settings <span class="symbol">!=</span> value<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_settings <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 _settings<span class="symbol">.</span>PropertyChanged <span class="symbol">-=</span> SettingsPropertyChangedHandler<span class="symbol">;</span>

 _settings <span class="symbol">=</span> value<span class="symbol">;</span>
 _settings<span class="symbol">.</span>PropertyChanged <span class="symbol">+=</span> SettingsPropertyChangedHandler<span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>OnSettingsChanged<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="keyword">private</span> <span class="keyword">void</span> SettingsPropertyChangedHandler<span class="symbol">(</span><span class="keyword">object</span> sender<span class="symbol">,</span> PropertyChangedEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnSettingsChanged<span class="symbol">(</span>e<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> OnSettingsChanged<span class="symbol">(</span>EventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>OpenPDF<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>SettingsChanged <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>SettingsChanged<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">,</span> e<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="navigation-support">Navigation support</h2>
<p>Although the <code>PdfImageBox</code> 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
<code>CurrentPage</code> property will be added for allowing the active
page to retrieved or set, and also a number of readonly
<code>CanMove*</code> properties. These properties allow the host to query
which navigation options are applicable in order to present the
correct UI.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="symbol">[</span>Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">virtual</span> <span class="keyword">int</span> PageCount
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> _converter <span class="symbol">!=</span> <span class="keyword">null</span> <span class="symbol">?</span> _converter<span class="symbol">.</span>PageCount <span class="symbol">:</span> <span class="number">0</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>

<span class="symbol">[</span>Category<span class="symbol">(</span><span class="string">&quot;Appearance&quot;</span><span class="symbol">)</span><span class="symbol">,</span> DefaultValue<span class="symbol">(</span><span class="number">1</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">int</span> CurrentPage
<span class="symbol">{</span>
 <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> _currentPage<span class="symbol">;</span> <span class="symbol">}</span>
 <span class="keyword">set</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">!=</span> value<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>value <span class="symbol">&lt;</span> <span class="number">1</span> <span class="symbol">||</span> value <span class="symbol">&gt;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Page number is out of bounds&quot;</span><span class="symbol">)</span><span class="symbol">;</span>

 _currentPage <span class="symbol">=</span> value<span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>OnCurrentPageChanged<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="symbol">[</span>Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">bool</span> CanMoveFirst
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount <span class="symbol">!=</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">!=</span> <span class="number">1</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>

<span class="symbol">[</span>Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">bool</span> CanMoveLast
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount <span class="symbol">!=</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">!=</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>

<span class="symbol">[</span>Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">bool</span> CanMoveNext
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount <span class="symbol">!=</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">&lt;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>

<span class="symbol">[</span>Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">bool</span> CanMovePrevious
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount <span class="symbol">!=</span> <span class="number">0</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">&gt;</span> <span class="number">1</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>
</pre>
</figure>
<p>Again, to make it easier for the host to connect to the control,
we also add some helper navigation methods.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">void</span> FirstPage<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">=</span> <span class="number">1</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> <span class="keyword">void</span> LastPage<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> <span class="keyword">void</span> NextPage<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>CurrentPage<span class="symbol">++</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> <span class="keyword">void</span> PreviousPage<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>CurrentPage<span class="symbol">--</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>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.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">event</span> EventHandler LoadingPage<span class="symbol">;</span>

<span class="keyword">public</span> <span class="keyword">event</span> EventHandler LoadedPage<span class="symbol">;</span>
</pre>
</figure>
<h4 id="opening-the-pdf-file">Opening the PDF file</h4>
<p>Each of the property changed handlers in turn call the <code>OpenPDF</code>
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.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">void</span> OpenPDF<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>CleanUp<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>DesignMode<span class="symbol">)</span>
 <span class="symbol">{</span>
 _converter <span class="symbol">=</span> <span class="keyword">new</span> Pdf<span class="number">2</span>Image<span class="symbol">(</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 PdfFileName <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>PdfFileName<span class="symbol">,</span>
 PdfPassword <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>PdfPassword<span class="symbol">,</span>
 Settings <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>Settings
 <span class="symbol">}</span><span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">=</span> <span class="keyword">null</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>PageCache<span class="symbol">=</span> <span class="keyword">new</span> Dictionary<span class="symbol">&lt;</span><span class="keyword">int</span><span class="symbol">,</span> Bitmap<span class="symbol">&gt;</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 _currentPage <span class="symbol">=</span> <span class="number">1</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>PageCount <span class="symbol">!=</span> <span class="number">0</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 _currentPage <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>CurrentPage <span class="symbol">=</span> <span class="number">1</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="keyword">private</span> <span class="keyword">void</span> CleanUp<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="comment">// release bitmaps</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>PageCache <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">foreach</span> <span class="symbol">(</span>KeyValuePair<span class="symbol">&lt;</span><span class="keyword">int</span><span class="symbol">,</span> Bitmap<span class="symbol">&gt;</span> pair <span class="keyword">in</span> <span class="keyword">this</span><span class="symbol">.</span>PageCache<span class="symbol">)</span>
 pair<span class="symbol">.</span>Value<span class="symbol">.</span>Dispose<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>PageCache <span class="symbol">=</span> <span class="keyword">null</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<h4 id="displaying-the-image">Displaying the image</h4>
<p>Each time the <code>CurrentPage</code> property is changed, it calls the
<code>SetPageImage</code> 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 <code>ImageBox</code>, and the user can then pan and zoom
as with any other image.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> SetPageImage<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>DesignMode <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCache <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">lock</span> <span class="symbol">(</span>_lock<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>PageCache<span class="symbol">.</span>ContainsKey<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>CurrentPage<span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnLoadingPage<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>PageCache<span class="symbol">.</span>Add<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>CurrentPage<span class="symbol">,</span> _converter<span class="symbol">.</span>GetImage<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>CurrentPage<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnLoadedPage<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>PageCache<span class="symbol">[</span><span class="keyword">this</span><span class="symbol">.</span>CurrentPage<span class="symbol">]</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>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.</p>
<p>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.</p>
<p>The sample project demonstrates all the features described above
and provides an example setting up a user interface for
navigating a PDF document.</p>
<h2 id="future-changes">Future changes</h2>
<p>At the moment, the <code>PdfImageBox</code> 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.</p>
<p>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 &quot;expire&quot; older pages, or to keep only a
fixed number in memory. Or even save each page to a temporary
disk file.</p>
<p>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.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2011-09-04 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/extending-the-imagebox-component-to-display-the-contents-of-a-pdf-file-using-csharp .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comConvert a PDF into a series of images using C# and GhostScripturn:uuid:9d2ef296-4c45-4df2-a1b2-24936bcbe8f52012-07-11T17:41:18Z2011-09-04T16:38:15Z<p>An application I was recently working on received PDF files from
a webservice which it then needed to store in a database. I
wanted the ability to display previews of these documents within
the application. While there are a number of solutions for
creating PDF files from C#, options for viewing a PDF within
your application is much more limited, unless you purchase
expensive commercial products, or use COM interop to embed
Acrobat Reader into your application.</p>
<p>This article describes an alternate solution, in which the pages
in a PDF are converted into images using GhostScript, from where
you can then display them in your application.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/pdfimagebox.png" class="gallery" title="A sample application demonstrating displaying a PDF file in the ImageBox control" ><img src="https://images.cyotek.com/image/thumbnail/devblog/pdfimagebox.png" alt="A sample application demonstrating displaying a PDF file in the ImageBox control" decoding="async" loading="lazy" /></a><figcaption>A sample application demonstrating displaying a PDF file in the ImageBox control</figcaption></figure>
<p>In order to avoid huge walls of text, this article has been
split into two parts, the first dealing with the actual
conversion of a PDF, and the <a href="/post/extending-the-imagebox-component-to-display-the-contents-of-a-pdf-file-using-csharp">second</a> demonstrates how to
extend the <a href="/tag/imagebox">ImageBox</a> control to display the images.</p>
<h2 id="caveat-emptor">Caveat emptor</h2>
<p>Before we start, some quick points.</p>
<ul>
<li>The method I'm about to demonstrate converts each page of the
PDF into an image. This means that it is very suitable for
viewing, but interactive elements such as forms, hyperlinks
and even good old text selection are not available.</li>
<li>GhostScript has a number of licenses associated with it but I
can't find any information of the pricing of commercial
licenses.</li>
<li>The GhostScript API Integration library used by this project
isn't complete and I'm not going to go into the bells and
whistles of how it works in this pair of articles - once I've
completed the outstanding functionality I'll create a new
article for it.</li>
</ul>
<h2 id="getting-started">Getting Started</h2>
<p>You can download the two libraries used in this article from the
links below, these are:</p>
<ul>
<li><strong>Cyotek.GhostScript</strong> - core library providing GhostScript
integration support</li>
<li><strong>Cyotek.GhostScript.PdfConversion</strong> - support library for
converting a PDF document into images</li>
</ul>
<blockquote>
<p>Please note that the native GhostScript DLL is not included in
these downloads, you will need to obtain that from the
<a href="http://www.ghostscript.com/" rel="external nofollow noopener">GhostScript project page</a></p>
</blockquote>
<h2 id="using-the-ghostscriptapi-class">Using the GhostScriptAPI class</h2>
<p>As mentioned above, the core GhostScript library isn't complete
yet, so I'll just give a description of the basic functionality
required by the conversion library.</p>
<p>The <code>GhostScriptAPI</code> class handles all communication with
GhostScript. When you create an instance of the class, it
automatically calls <code>gsapi_new_instance</code> in the native
GhostScript DLL. When the class is disposed, it will
automatically release any handles and calls the native
<code>gsapi_exit</code> and <code>gsapi_delete_instance</code> methods.</p>
<p>In order to actually call GhostScript, you call the <code>Execute</code>
method, passing in either a string array of all the arguments to
pass to GhostScript, or a typed dictionary of commands and
values. The <code>GhostScriptCommand</code> enum contains most of the
commands supported by GhostScript, which may be a preferable
approach rather than trying to remember the parameter names
themselves.</p>
<h2 id="defining-conversion-settings">Defining conversion settings</h2>
<p>The <code>Pdf2ImageSettings</code> class allows you to customize various
properties of the output image. The following properties are
available:</p>
<ul>
<li><code>AntiAliasMode</code> - specifies the antialiasing level between
Low, Medium and High. This internally will set the
<code>dTextAlphaBits</code> and <code>dGraphicsAlphaBits</code> GhostScript switches
to appropriate values.</li>
<li><code>Dpi</code> - dots per inch. Internally sets the <code>r</code> switch. This
property is not used if a paper size is set.</li>
<li><code>GridFitMode</code> - controls the text readability mode. Internally
sets the <code>dGridFitTT</code> switch.</li>
<li><code>ImageFormat</code> - specifies the output image format. Internally
sets the <code>sDEVICE</code> switch.</li>
<li><code>PaperSize</code> - specifies a paper size from one of the standard
sizes supported by GhostScript.</li>
<li><code>TrimMode</code> - specifies how the image should be sized. Your
milage may vary if you try and use the paper size option.
Internally sets either the <code>dFIXEDMEDIA</code> and <code>sPAPERSIZE</code> or
the <code>dUseCropBox</code> or the <code>dUseTrimBox</code> switches.</li>
</ul>
<p>Typical settings could look like this:</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
Pdf<span class="number">2</span>ImageSettings settings<span class="symbol">;</span>

settings <span class="symbol">=</span> <span class="keyword">new</span> Pdf<span class="number">2</span>ImageSettings<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
settings<span class="symbol">.</span>AntiAliasMode <span class="symbol">=</span> AntiAliasMode<span class="symbol">.</span>High<span class="symbol">;</span>
settings<span class="symbol">.</span>Dpi <span class="symbol">=</span> <span class="number">300</span><span class="symbol">;</span>
settings<span class="symbol">.</span>GridFitMode <span class="symbol">=</span> GridFitMode<span class="symbol">.</span>Topological<span class="symbol">;</span>
settings<span class="symbol">.</span>ImageFormat <span class="symbol">=</span> ImageFormat<span class="symbol">.</span>Png<span class="number">24</span><span class="symbol">;</span>
settings<span class="symbol">.</span>TrimMode <span class="symbol">=</span> PdfTrimMode<span class="symbol">.</span>CropBox<span class="symbol">;</span>
</pre>
</figure>
<h2 id="converting-the-pdf">Converting the PDF</h2>
<p>To convert a PDF file into a series of images, use the
<code>Pdf2Image</code> class. The following properties and methods are
offered:</p>
<ul>
<li><code>ConvertPdfPageToImage</code> - converts a given page in the PDF
into an image which is saved to disk</li>
<li><code>GetImage</code> - converts a page in the PDF into an image and
returns the image</li>
<li><code>GetImages</code> - converts a range of pages into the PDF into
images and returns an image array</li>
<li><code>PageCount</code> - returns the number of pages in the source PDF</li>
<li><code>PdfFilename</code> - returns or sets the filename of the PDF
document to convert</li>
<li><code>PdfPassword</code> - returns or sets the password of the PDF
document to convert</li>
<li><code>Settings</code> - returns or sets the settings object described
above</li>
</ul>
<p>A typical example to convert the first image in a PDF document:</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
Bitmap firstPage <span class="symbol">=</span> <span class="keyword">new</span> Pdf<span class="number">2</span>Image<span class="symbol">(</span><span class="string">&quot;sample.pdf&quot;</span><span class="symbol">)</span><span class="symbol">.</span>GetImage<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<h2 id="the-inner-workings">The inner workings</h2>
<p>Most of the code in the class is taken up with the
<code>GetConversionArguments</code> method. This method looks at the
various properties of the conversion such as output format,
quality, etc, and returns the appropriate commands to pass to
GhostScript:</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">virtual</span> IDictionary<span class="symbol">&lt;</span>GhostScriptCommand<span class="symbol">,</span> <span class="keyword">object</span><span class="symbol">&gt;</span> GetConversionArguments<span class="symbol">(</span><span class="keyword">string</span> pdfFileName<span class="symbol">,</span> <span class="keyword">string</span> outputImageFileName<span class="symbol">,</span> <span class="keyword">int</span> pageNumber<span class="symbol">,</span> <span class="keyword">string</span> password<span class="symbol">,</span> Pdf<span class="number">2</span>ImageSettings settings<span class="symbol">)</span>
<span class="symbol">{</span>
 IDictionary<span class="symbol">&lt;</span>GhostScriptCommand<span class="symbol">,</span> <span class="keyword">object</span><span class="symbol">&gt;</span> arguments<span class="symbol">;</span>

 arguments <span class="symbol">=</span> <span class="keyword">new</span> Dictionary<span class="symbol">&lt;</span>GhostScriptCommand<span class="symbol">,</span> <span class="keyword">object</span><span class="symbol">&gt;</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// basic GhostScript setup</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>Silent<span class="symbol">,</span> <span class="keyword">null</span><span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>Safer<span class="symbol">,</span> <span class="keyword">null</span><span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>Batch<span class="symbol">,</span> <span class="keyword">null</span><span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>NoPause<span class="symbol">,</span> <span class="keyword">null</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// specify the output</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>Device<span class="symbol">,</span> GhostScriptAPI<span class="symbol">.</span>GetDeviceName<span class="symbol">(</span>settings<span class="symbol">.</span>ImageFormat<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>OutputFile<span class="symbol">,</span> outputImageFileName<span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// page numbers</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>FirstPage<span class="symbol">,</span> pageNumber<span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>LastPage<span class="symbol">,</span> pageNumber<span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// graphics options</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>UseCIEColor<span class="symbol">,</span> <span class="keyword">null</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>settings<span class="symbol">.</span>AntiAliasMode <span class="symbol">!=</span> AntiAliasMode<span class="symbol">.</span>None<span class="symbol">)</span>
 <span class="symbol">{</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>TextAlphaBits<span class="symbol">,</span> settings<span class="symbol">.</span>AntiAliasMode<span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>GraphicsAlphaBits<span class="symbol">,</span> settings<span class="symbol">.</span>AntiAliasMode<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>GridToFitTT<span class="symbol">,</span> settings<span class="symbol">.</span>GridFitMode<span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// image size</span>
 <span class="keyword">if</span> <span class="symbol">(</span>settings<span class="symbol">.</span>TrimMode <span class="symbol">!=</span> PdfTrimMode<span class="symbol">.</span>PaperSize<span class="symbol">)</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>Resolution<span class="symbol">,</span> settings<span class="symbol">.</span>Dpi<span class="symbol">.</span>ToString<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">switch</span> <span class="symbol">(</span>settings<span class="symbol">.</span>TrimMode<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">case</span> PdfTrimMode<span class="symbol">.</span>PaperSize<span class="symbol">:</span>
 <span class="keyword">if</span> <span class="symbol">(</span>settings<span class="symbol">.</span>PaperSize <span class="symbol">!=</span> PaperSize<span class="symbol">.</span>Default<span class="symbol">)</span>
 <span class="symbol">{</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>FixedMedia<span class="symbol">,</span> <span class="keyword">true</span><span class="symbol">)</span><span class="symbol">;</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>PaperSize<span class="symbol">,</span> settings<span class="symbol">.</span>PaperSize<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> PdfTrimMode<span class="symbol">.</span>TrimBox<span class="symbol">:</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>UseTrimBox<span class="symbol">,</span> <span class="keyword">true</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> PdfTrimMode<span class="symbol">.</span>CropBox<span class="symbol">:</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>UseCropBox<span class="symbol">,</span> <span class="keyword">true</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// pdf password</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">string</span><span class="symbol">.</span>IsNullOrEmpty<span class="symbol">(</span>password<span class="symbol">)</span><span class="symbol">)</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>PDFPassword<span class="symbol">,</span> password<span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// pdf filename</span>
 arguments<span class="symbol">.</span>Add<span class="symbol">(</span>GhostScriptCommand<span class="symbol">.</span>InputFile<span class="symbol">,</span> pdfFileName<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> arguments<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>As you can see from the method above, the commands are being
returned as a strongly typed dictionary - the <code>GhostScriptAPI</code>
class will convert these into the correct GhostScript commands,
but the enum is much easier to work with from your code! The
following is an example of the typical GhostScript commands to
convert a single page in a PDF document:</p>
<figure class="lang-text highlight"><figcaption><span>text</span></figcaption><pre class="code">
-q -dSAFER -dBATCH -dNOPAUSE -sDEVICE=png16m -sOutputFile=tmp78BC.tmp -dFirstPage=1 -dLastPage=1 -dUseCIEColor -dTextAlphaBits=4 -dGraphicsAlphaBits=4 -dGridFitTT=2 -r150 -dUseCropBox=true sample.pdf
</pre>
</figure>
<p>The next step is to call GhostScript and convert the PDF which
is done using the <code>ConvertPdfPageToImage</code> method:</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">void</span> ConvertPdfPageToImage<span class="symbol">(</span><span class="keyword">string</span> outputFileName<span class="symbol">,</span> <span class="keyword">int</span> pageNumber<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>pageNumber <span class="symbol">&lt;</span> <span class="number">1</span> <span class="symbol">||</span> pageNumber <span class="symbol">&gt;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Page number is out of bounds&quot;</span><span class="symbol">,</span> <span class="string">&quot;pageNumber&quot;</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">using</span> <span class="symbol">(</span>GhostScriptAPI api <span class="symbol">=</span> <span class="keyword">new</span> GhostScriptAPI<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">)</span>
 api<span class="symbol">.</span>Execute<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>GetConversionArguments<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>_pdfFileName<span class="symbol">,</span> outputFileName<span class="symbol">,</span> pageNumber<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>PdfPassword<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>Settings<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>As you can see, this is a very simple call - create an instance
of the GhostScriptAPI class and then pass in the list of
parameters to execute. The <code>GhostScriptAPI</code> class takes care of
everything else.</p>
<p>Once the file is saved to disk, you can then load it into a
<code>Bitmap</code> or <code>Image</code> object for use in your application. Don't
forget to delete the file when you are finished with it!</p>
<p>Alternatively, the <code>GetImage</code> method will convert the file and
return the bitmap image for you, automatically deleting the
temporary file. This saves you from having to worry about
providing and deleting the output file, but it does mean you are
responsible for disposing of the returned bitmap.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> Bitmap GetImage<span class="symbol">(</span><span class="keyword">int</span> pageNumber<span class="symbol">)</span>
<span class="symbol">{</span>
 Bitmap result<span class="symbol">;</span>
 <span class="keyword">string</span> workFile<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>pageNumber <span class="symbol">&lt;</span> <span class="number">1</span> <span class="symbol">||</span> pageNumber <span class="symbol">&gt;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Page number is out of bounds&quot;</span><span class="symbol">,</span> <span class="string">&quot;pageNumber&quot;</span><span class="symbol">)</span><span class="symbol">;</span>

 workFile <span class="symbol">=</span> Path<span class="symbol">.</span>GetTempFileName<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">try</span>
 <span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>ConvertPdfPageToImage<span class="symbol">(</span>workFile<span class="symbol">,</span> pageNumber<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">using</span> <span class="symbol">(</span>FileStream stream <span class="symbol">=</span> <span class="keyword">new</span> FileStream<span class="symbol">(</span>workFile<span class="symbol">,</span> FileMode<span class="symbol">.</span>Open<span class="symbol">,</span> FileAccess<span class="symbol">.</span>Read<span class="symbol">)</span><span class="symbol">)</span>
 result <span class="symbol">=</span> <span class="keyword">new</span> Bitmap<span class="symbol">(</span>stream<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">finally</span>
 <span class="symbol">{</span>
 File<span class="symbol">.</span>Delete<span class="symbol">(</span>workFile<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">return</span> result<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>You could also convert a range of pages at once using the
<code>GetImages</code> method:</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> Bitmap<span class="symbol">[</span><span class="symbol">]</span> GetImages<span class="symbol">(</span><span class="keyword">int</span> startPage<span class="symbol">,</span> <span class="keyword">int</span> lastPage<span class="symbol">)</span>
<span class="symbol">{</span>
 List<span class="symbol">&lt;</span>Bitmap<span class="symbol">&gt;</span> results<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>startPage <span class="symbol">&lt;</span> <span class="number">1</span> <span class="symbol">||</span> startPage <span class="symbol">&gt;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Start page number is out of bounds&quot;</span><span class="symbol">,</span> <span class="string">&quot;startPage&quot;</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>lastPage <span class="symbol">&lt;</span> <span class="number">1</span> <span class="symbol">||</span> lastPage <span class="symbol">&gt;</span> <span class="keyword">this</span><span class="symbol">.</span>PageCount<span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Last page number is out of bounds&quot;</span><span class="symbol">,</span> <span class="string">&quot;lastPage&quot;</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span>lastPage <span class="symbol">&lt;</span> startPage<span class="symbol">)</span>
 <span class="keyword">throw</span> <span class="keyword">new</span> ArgumentException<span class="symbol">(</span><span class="string">&quot;Last page cannot be less than start page&quot;</span><span class="symbol">,</span> <span class="string">&quot;lastPage&quot;</span><span class="symbol">)</span><span class="symbol">;</span>

 results <span class="symbol">=</span> <span class="keyword">new</span> List<span class="symbol">&lt;</span>Bitmap<span class="symbol">&gt;</span><span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> i <span class="symbol">=</span> startPage<span class="symbol">;</span> i <span class="symbol">&lt;=</span> lastPage<span class="symbol">;</span> i<span class="symbol">++</span><span class="symbol">)</span>
 results<span class="symbol">.</span>Add<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>GetImage<span class="symbol">(</span>i<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> results<span class="symbol">.</span>ToArray<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h3 id="in-conclusion">In conclusion</h3>
<p>The above methods provide a simple way of providing basic PDF
viewing in your applications. In the <a href="/post/extending-the-imagebox-component-to-display-the-contents-of-a-pdf-file-using-csharp">next part</a>] of this
series, we describe how to extend the <a href="/tag/imagebox">ImageBox</a> component to
support conversion and navigation.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2011-09-04 - First published</li>
<li>2012-07-10 - Added follow up article links</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/convert-a-pdf-into-a-series-of-images-using-csharp-and-ghostscript .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comCreating a scrollable and zoomable image viewer in C# Part 4urn:uuid:6344e9eb-fd6a-42d7-9a0d-3d09d3883bfa2012-11-25T09:20:14Z2010-08-28T15:49:10Z<p>In the conclusion to our series on building a scrollable and
zoomable image viewer, we'll add support for zooming, auto
centering, size to fit and some display optimizations and
enhancements.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/imgbox-4a.png" class="gallery" title="The ImageBox sample application showing a zoomed in image." ><img src="https://images.cyotek.com/image/thumbnail/devblog/imgbox-4a.png" alt="The ImageBox sample application showing a zoomed in image." decoding="async" loading="lazy" /></a><figcaption>The ImageBox sample application showing a zoomed in image.</figcaption></figure><figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/imgbox-4b.png" class="gallery" title="The ImageBox sample application showing a zoomed out image, with auto centering and the transparency grid only displayed behind the image." ><img src="https://images.cyotek.com/image/thumbnail/devblog/imgbox-4b.png" alt="The ImageBox sample application showing a zoomed out image, with auto centering and the transparency grid only displayed behind the image." decoding="async" loading="lazy" /></a><figcaption>The ImageBox sample application showing a zoomed out image, with auto centering and the transparency grid only displayed behind the image.</figcaption></figure><h2 id="getting-started">Getting Started</h2>
<p>Unlike parts 2 and 3, we're actually adding quite a lot of new
functionality, some of it more complicated than others.</p>
<p>First, we're going to remove the <code>ShowGrid</code> property. This
originally was a simple on/off flag, but we want more control
this time.</p>
<p>We've also got a number of new properties and backing events to
add:</p>
<ul>
<li><code>AutoCenter</code> - controls if the image is automatically centered
in the display area if the image isn't scrolled.</li>
<li><code>SizeToFit</code> - if this property is set, the image will
automatically zoom to the maximum size for displaying the
entire image.</li>
<li><code>GridDisplayMode</code> - this property, which replaces <code>ShowGrid</code>
will determine how the background grid is to be drawn.</li>
<li><code>InterpolationMode</code> - determines how the zoomed image will be
rendered.</li>
<li><code>Zoom</code> - allows you to specify the zoom level.</li>
<li><code>ZoomIncrement</code> - specifies how much the zoom is increased or
decreased using the scroll wheel.</li>
<li><code>ZoomFactor</code> - this protected property returns the current
zoom as used internally for scaling.</li>
<li><code>ScaledImageWidth</code> and <code>ScaledImageHeight</code> - these protected
properties return the size of the image adjusted for the
current zoom.</li>
</ul>
<p>Usually the properties are simple assignments, which compare the
values before assignment and raise an event. The zoom property
is slightly different as it will ensure that the new value fits
within a given range before setting it.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> <span class="keyword">int</span> MinZoom <span class="symbol">=</span> <span class="number">10</span><span class="symbol">;</span>
<span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> <span class="keyword">int</span> MaxZoom <span class="symbol">=</span> <span class="number">3500</span><span class="symbol">;</span>

<span class="symbol">[</span>DefaultValue<span class="symbol">(</span><span class="number">100</span><span class="symbol">)</span><span class="symbol">,</span> Category<span class="symbol">(</span><span class="string">&quot;Appearance&quot;</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">int</span> Zoom
<span class="symbol">{</span>
 <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> _zoom<span class="symbol">;</span> <span class="symbol">}</span>
 <span class="keyword">set</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>value <span class="symbol">&lt;</span> ImageBox<span class="symbol">.</span>MinZoom<span class="symbol">)</span>
 value <span class="symbol">=</span> ImageBox<span class="symbol">.</span>MinZoom<span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span>value <span class="symbol">&gt;</span> ImageBox<span class="symbol">.</span>MaxZoom<span class="symbol">)</span>
 value <span class="symbol">=</span> ImageBox<span class="symbol">.</span>MaxZoom<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>_zoom <span class="symbol">!=</span> value<span class="symbol">)</span>
 <span class="symbol">{</span>
 _zoom <span class="symbol">=</span> value<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnZoomChanged<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Using the <code>MinZoom</code> and <code>MaxZoom</code> constants we are specifying a
minimum value of 10% and a maximum of 3500%. The values you are
assign are more or less down to your own personal preferences -
I don't have any indications of what a &quot;best&quot; maximum value
would be.</p>
<p>Setting the <code>SizeToFit</code> property should disable the <code>AutoPan</code>
property and vice versa.</p>
<h2 id="layout-updates">Layout Updates</h2>
<p>Several parts of the component work from the image size, however
as these now need to account for any zoom level, all such calls
now use the <code>ScaledImageWidth</code> and <code>ScaledImageHeight</code>
properties.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">int</span> ScaledImageHeight
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span> <span class="symbol">?</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Size<span class="symbol">.</span>Height <span class="symbol">*</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomFactor<span class="symbol">)</span> <span class="symbol">:</span> <span class="number">0</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">int</span> ScaledImageWidth
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span> <span class="symbol">?</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Size<span class="symbol">.</span>Width <span class="symbol">*</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomFactor<span class="symbol">)</span> <span class="symbol">:</span> <span class="number">0</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">double</span> ZoomFactor
<span class="symbol">{</span> <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">/</span> <span class="number">100</span><span class="symbol">;</span> <span class="symbol">}</span> <span class="symbol">}</span>
</pre>
</figure>
<p>The <code>AdjustLayout</code> method which determines the appropriate
course of action when certain properties are changed has been
updated to support the size to fit functionality by calling the
new <code>ZoomToFit</code> method.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> AdjustLayout<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoSize<span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustSize<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>SizeToFit<span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>ZoomToFit<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoScroll<span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustViewPort<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>Invalidate<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> <span class="keyword">virtual</span> <span class="keyword">void</span> ZoomToFit<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 Rectangle innerRectangle<span class="symbol">;</span>
 <span class="keyword">double</span> zoom<span class="symbol">;</span>
 <span class="keyword">double</span> aspectRatio<span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>AutoScrollMinSize <span class="symbol">=</span> Size<span class="symbol">.</span>Empty<span class="symbol">;</span>

 innerRectangle <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetInsideViewPort<span class="symbol">(</span><span class="keyword">true</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Width <span class="symbol">&gt;</span> <span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Height<span class="symbol">)</span>
 <span class="symbol">{</span>
 aspectRatio <span class="symbol">=</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span>innerRectangle<span class="symbol">.</span>Width<span class="symbol">)</span> <span class="symbol">/</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Width<span class="symbol">)</span><span class="symbol">;</span>
 zoom <span class="symbol">=</span> aspectRatio <span class="symbol">*</span> <span class="number">100.0</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>innerRectangle<span class="symbol">.</span>Height <span class="symbol">&lt;</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Height <span class="symbol">*</span> zoom<span class="symbol">)</span> <span class="symbol">/</span> <span class="number">100.0</span><span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 aspectRatio <span class="symbol">=</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span>innerRectangle<span class="symbol">.</span>Height<span class="symbol">)</span> <span class="symbol">/</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Height<span class="symbol">)</span><span class="symbol">;</span>
 zoom <span class="symbol">=</span> aspectRatio <span class="symbol">*</span> <span class="number">100.0</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 aspectRatio <span class="symbol">=</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span>innerRectangle<span class="symbol">.</span>Height<span class="symbol">)</span> <span class="symbol">/</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Height<span class="symbol">)</span><span class="symbol">;</span>
 zoom <span class="symbol">=</span> aspectRatio <span class="symbol">*</span> <span class="number">100.0</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>innerRectangle<span class="symbol">.</span>Width <span class="symbol">&lt;</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Width <span class="symbol">*</span> zoom<span class="symbol">)</span> <span class="symbol">/</span> <span class="number">100.0</span><span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 aspectRatio <span class="symbol">=</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span>innerRectangle<span class="symbol">.</span>Width<span class="symbol">)</span> <span class="symbol">/</span> <span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Width<span class="symbol">)</span><span class="symbol">;</span>
 zoom <span class="symbol">=</span> aspectRatio <span class="symbol">*</span> <span class="number">100.0</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>

 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span>Math<span class="symbol">.</span>Round<span class="symbol">(</span>Math<span class="symbol">.</span>Floor<span class="symbol">(</span>zoom<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Due to the additional complexity in positioning and sizing,
we're also adding functions to return the different regions in
use by the control.</p>
<ul>
<li><code>GetImageViewPort</code> - returns a rectangle representing the size
of the drawn image.</li>
<li><code>GetInsideViewPort</code> - returns a rectangle representing the
client area of the control, offset by the current border
style, and optionally padding.</li>
<li><code>GetSourceImageRegion</code> - returns a rectangle representing the
area of the source image that will be drawn onto the control.</li>
</ul>
<p>The sample project has been updated to be able to display the
results of the <code>GetImageViewPort</code> and <code>GetSourceImageRegion</code>
functions.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> <span class="keyword">virtual</span> Rectangle GetImageViewPort<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 Rectangle viewPort<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 Rectangle innerRectangle<span class="symbol">;</span>
 Point offset<span class="symbol">;</span>

 innerRectangle <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetInsideViewPort<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoCenter<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> x<span class="symbol">;</span>
 <span class="keyword">int</span> y<span class="symbol">;</span>

 x <span class="symbol">=</span> <span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>HScroll <span class="symbol">?</span> <span class="symbol">(</span>innerRectangle<span class="symbol">.</span>Width <span class="symbol">-</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>ScaledImageWidth <span class="symbol">+</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Horizontal<span class="symbol">)</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">2</span> <span class="symbol">:</span> <span class="number">0</span><span class="symbol">;</span>
 y <span class="symbol">=</span> <span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>VScroll <span class="symbol">?</span> <span class="symbol">(</span>innerRectangle<span class="symbol">.</span>Height <span class="symbol">-</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>ScaledImageHeight <span class="symbol">+</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Vertical<span class="symbol">)</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">2</span> <span class="symbol">:</span> <span class="number">0</span><span class="symbol">;</span>

 offset <span class="symbol">=</span> <span class="keyword">new</span> Point<span class="symbol">(</span>x<span class="symbol">,</span> y<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 offset <span class="symbol">=</span> Point<span class="symbol">.</span>Empty<span class="symbol">;</span>

 viewPort <span class="symbol">=</span> <span class="keyword">new</span> Rectangle<span class="symbol">(</span>offset<span class="symbol">.</span>X <span class="symbol">+</span> innerRectangle<span class="symbol">.</span>Left <span class="symbol">+</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Left<span class="symbol">,</span> offset<span class="symbol">.</span>Y <span class="symbol">+</span> innerRectangle<span class="symbol">.</span>Top <span class="symbol">+</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Top<span class="symbol">,</span> innerRectangle<span class="symbol">.</span>Width <span class="symbol">-</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Horizontal <span class="symbol">+</span> <span class="symbol">(</span>offset<span class="symbol">.</span>X <span class="symbol">*</span> <span class="number">2</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">,</span> innerRectangle<span class="symbol">.</span>Height <span class="symbol">-</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Vertical <span class="symbol">+</span> <span class="symbol">(</span>offset<span class="symbol">.</span>Y <span class="symbol">*</span> <span class="number">2</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 viewPort <span class="symbol">=</span> Rectangle<span class="symbol">.</span>Empty<span class="symbol">;</span>

 <span class="keyword">return</span> viewPort<span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> Rectangle GetInsideViewPort<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">return</span> <span class="keyword">this</span><span class="symbol">.</span>GetInsideViewPort<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> <span class="keyword">virtual</span> Rectangle GetInsideViewPort<span class="symbol">(</span><span class="keyword">bool</span> includePadding<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> left<span class="symbol">;</span>
 <span class="keyword">int</span> top<span class="symbol">;</span>
 <span class="keyword">int</span> width<span class="symbol">;</span>
 <span class="keyword">int</span> height<span class="symbol">;</span>
 <span class="keyword">int</span> borderOffset<span class="symbol">;</span>

 borderOffset <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetBorderOffset<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 left <span class="symbol">=</span> borderOffset<span class="symbol">;</span>
 top <span class="symbol">=</span> borderOffset<span class="symbol">;</span>
 width <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ClientSize<span class="symbol">.</span>Width <span class="symbol">-</span> <span class="symbol">(</span>borderOffset <span class="symbol">*</span> <span class="number">2</span><span class="symbol">)</span><span class="symbol">;</span>
 height <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ClientSize<span class="symbol">.</span>Height <span class="symbol">-</span> <span class="symbol">(</span>borderOffset <span class="symbol">*</span> <span class="number">2</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>includePadding<span class="symbol">)</span>
 <span class="symbol">{</span>
 left <span class="symbol">+=</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Left<span class="symbol">;</span>
 top <span class="symbol">+=</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Top<span class="symbol">;</span>
 width <span class="symbol">-=</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Horizontal<span class="symbol">;</span>
 height <span class="symbol">-=</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Vertical<span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">return</span> <span class="keyword">new</span> Rectangle<span class="symbol">(</span>left<span class="symbol">,</span> top<span class="symbol">,</span> width<span class="symbol">,</span> height<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">public</span> <span class="keyword">virtual</span> Rectangle GetSourceImageRegion<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> sourceLeft<span class="symbol">;</span>
 <span class="keyword">int</span> sourceTop<span class="symbol">;</span>
 <span class="keyword">int</span> sourceWidth<span class="symbol">;</span>
 <span class="keyword">int</span> sourceHeight<span class="symbol">;</span>
 Rectangle viewPort<span class="symbol">;</span>
 Rectangle region<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 viewPort <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetImageViewPort<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 sourceLeft <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span><span class="symbol">-</span><span class="keyword">this</span><span class="symbol">.</span>AutoScrollPosition<span class="symbol">.</span>X <span class="symbol">/</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomFactor<span class="symbol">)</span><span class="symbol">;</span>
 sourceTop <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span><span class="symbol">-</span><span class="keyword">this</span><span class="symbol">.</span>AutoScrollPosition<span class="symbol">.</span>Y <span class="symbol">/</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomFactor<span class="symbol">)</span><span class="symbol">;</span>
 sourceWidth <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span>viewPort<span class="symbol">.</span>Width <span class="symbol">/</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomFactor<span class="symbol">)</span><span class="symbol">;</span>
 sourceHeight <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span>viewPort<span class="symbol">.</span>Height <span class="symbol">/</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomFactor<span class="symbol">)</span><span class="symbol">;</span>

 region <span class="symbol">=</span> <span class="keyword">new</span> Rectangle<span class="symbol">(</span>sourceLeft<span class="symbol">,</span> sourceTop<span class="symbol">,</span> sourceWidth<span class="symbol">,</span> sourceHeight<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 region <span class="symbol">=</span> Rectangle<span class="symbol">.</span>Empty<span class="symbol">;</span>

 <span class="keyword">return</span> region<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="drawing-the-control">Drawing the control</h2>
<p>As with the previous versions, the control is drawn by
overriding <code>OnPaint</code>, this time we are not using clip regions or
drawing the entire image even if only a portion of it is
visible.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="comment">// draw the borders</span>
<span class="keyword">switch</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>BorderStyle<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">case</span> BorderStyle<span class="symbol">.</span>FixedSingle<span class="symbol">:</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ForeColor<span class="symbol">,</span> ButtonBorderStyle<span class="symbol">.</span>Solid<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> BorderStyle<span class="symbol">.</span>Fixed<span class="number">3</span>D<span class="symbol">:</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="number">3</span>D<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> Border<span class="number">3</span>DStyle<span class="symbol">.</span>Sunken<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Depending on the value of the <code>GridDisplayMode</code> property, the
background tile grid will either not be displayed, will be
displayed to fill the client area of the control, or new for
this update, to only fill the area behind the image. The
remainder of the control is filled with the background color.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
Rectangle innerRectangle<span class="symbol">;</span>

innerRectangle <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetInsideViewPort<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

<span class="comment">// draw the background</span>
<span class="keyword">using</span> <span class="symbol">(</span>SolidBrush brush <span class="symbol">=</span> <span class="keyword">new</span> SolidBrush<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>BackColor<span class="symbol">)</span><span class="symbol">)</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>brush<span class="symbol">,</span> innerRectangle<span class="symbol">)</span><span class="symbol">;</span>

<span class="keyword">if</span> <span class="symbol">(</span>_texture <span class="symbol">!=</span> <span class="keyword">null</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>GridDisplayMode <span class="symbol">!=</span> ImageBoxGridDisplayMode<span class="symbol">.</span>None<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">switch</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>GridDisplayMode<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">case</span> ImageBoxGridDisplayMode<span class="symbol">.</span>Image<span class="symbol">:</span>
 Rectangle fillRectangle<span class="symbol">;</span>

 fillRectangle <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetImageViewPort<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>_texture<span class="symbol">,</span> fillRectangle<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span>fillRectangle<span class="symbol">.</span>Equals<span class="symbol">(</span>innerRectangle<span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 fillRectangle<span class="symbol">.</span>Inflate<span class="symbol">(</span><span class="number">1</span><span class="symbol">,</span> <span class="number">1</span><span class="symbol">)</span><span class="symbol">;</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> fillRectangle<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ForeColor<span class="symbol">,</span> ButtonBorderStyle<span class="symbol">.</span>Solid<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> ImageBoxGridDisplayMode<span class="symbol">.</span>Client<span class="symbol">:</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>_texture<span class="symbol">,</span> innerRectangle<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Previous versions of the control drew the entire image using the
<code>DrawImageUnscaled</code> method of the <code>Graphics</code> object. In this
final version, we're going to be a little more intelligent and
only draw the visible area, removing the need for the previous
clip region. The <code>InterpolationMode</code> is used to determine how
the image is drawn when it is zoomed in or out.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="comment">// draw the image</span>
g<span class="symbol">.</span>InterpolationMode <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>InterpolationMode<span class="symbol">;</span>
g<span class="symbol">.</span>DrawImage<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>GetImageViewPort<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>GetSourceImageRegion<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">,</span> GraphicsUnit<span class="symbol">.</span>Pixel<span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<h2 id="zooming-support">Zooming Support</h2>
<p>With the control now all set up and fully supporting zoom, it's
time to allow the end user to be able to change the zoom.</p>
<p>The first step is to disable the ability to double click the
control, by modifying the control styles in the constructor.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">this</span><span class="symbol">.</span>SetStyle<span class="symbol">(</span>ControlStyles<span class="symbol">.</span>StandardDoubleClick<span class="symbol">,</span> <span class="keyword">false</span><span class="symbol">)</span><span class="symbol">;</span>
</pre>
</figure>
<p>We're going to allow the zoom to be changed two ways - by either
scrolling the mouse wheel, or left/right clicking the control.</p>
<p>By overriding <code>OnMouseWheel</code>, we can be notified when the user
spins the wheel, and in which direction. We then adjust the zoom
using the value of the <code>ZoomIncrement</code> property. If a modifier
key such as Shift or Control is pressed, then we'll modify the
zoom by five times the increment.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnMouseWheel<span class="symbol">(</span>MouseEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>SizeToFit<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> increment<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>Control<span class="symbol">.</span>ModifierKeys <span class="symbol">==</span> Keys<span class="symbol">.</span>None<span class="symbol">)</span>
 increment <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomIncrement<span class="symbol">;</span>
 <span class="keyword">else</span>
 increment <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ZoomIncrement <span class="symbol">*</span> <span class="number">5</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>e<span class="symbol">.</span>Delta <span class="symbol">&lt;</span> <span class="number">0</span><span class="symbol">)</span>
 increment <span class="symbol">=</span> <span class="symbol">-</span>increment<span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">+=</span> increment<span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Normally, whenever we override a method, we always call it's
base implementation. However, in this case we will not; the
<code>ScrollbableControl</code> that we inherit from uses the mouse wheel
to scroll the viewport and there doesn't seem to be a way to
disable this undesirable behaviour.</p>
<p>As we also want to allow the user to be able to click the
control with the left mouse button to zoom in, and either the
right mouse button or left button holding a modifier key to zoom
out, we'll also override <code>OnMouseClick</code>.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnMouseClick<span class="symbol">(</span>MouseEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>IsPanning <span class="symbol">&amp;&amp;</span> <span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>SizeToFit<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>e<span class="symbol">.</span>Button <span class="symbol">==</span> MouseButtons<span class="symbol">.</span>Left <span class="symbol">&amp;&amp;</span> Control<span class="symbol">.</span>ModifierKeys <span class="symbol">==</span> Keys<span class="symbol">.</span>None<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">&gt;=</span> <span class="number">100</span><span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span>Math<span class="symbol">.</span>Round<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">+</span> <span class="number">100</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">100</span><span class="symbol">)</span> <span class="symbol">*</span> <span class="number">100</span><span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">&gt;=</span> <span class="number">75</span><span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="number">100</span><span class="symbol">;</span>
 <span class="keyword">else</span>
 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">/</span> <span class="number">0.75</span>F<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span>e<span class="symbol">.</span>Button <span class="symbol">==</span> MouseButtons<span class="symbol">.</span>Right <span class="symbol">||</span> <span class="symbol">(</span>e<span class="symbol">.</span>Button <span class="symbol">==</span> MouseButtons<span class="symbol">.</span>Left <span class="symbol">&amp;&amp;</span> Control<span class="symbol">.</span>ModifierKeys <span class="symbol">!=</span> Keys<span class="symbol">.</span>None<span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">&gt;</span> <span class="number">100</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">&lt;=</span> <span class="number">125</span><span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="number">100</span><span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">&gt;</span> <span class="number">100</span><span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span>Math<span class="symbol">.</span>Round<span class="symbol">(</span><span class="symbol">(</span><span class="keyword">double</span><span class="symbol">)</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">-</span> <span class="number">100</span><span class="symbol">)</span> <span class="symbol">/</span> <span class="number">100</span><span class="symbol">)</span> <span class="symbol">*</span> <span class="number">100</span><span class="symbol">;</span>
 <span class="keyword">else</span>
 <span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Zoom <span class="symbol">*</span> <span class="number">0.75</span>F<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>

 <span class="keyword">base</span><span class="symbol">.</span>OnMouseClick<span class="symbol">(</span>e<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Unlike with the mouse wheel and it's fixed increment, we want to
use a different approach with clicking. If zooming out and the
percentage is more than 100, then the zoom level will be set to
the current zoom level + 100, but rounded to the nearest 100,
and the same in reverse for zooming in.</p>
<p>If the current zoom is less than 100, then the new value will +-
75% of the current zoom, or reset to 100 if the new value falls
between 75 and 125.</p>
<p>This results in a nicer zoom experience then just using a fixed
value.</p>
<h2 id="sample-project">Sample Project</h2>
<p>You can download the final sample project from the link below.</p>
<h2 id="whats-next">What's next?</h2>
<p>One of the really annoying issues with this control that has
plagued me during writing this series is scrolling the
component. During scrolling there is an annoying flicker as the
original contents are moved, then the new contents are drawn. At
present I don't have a solution for this, I've tried overriding
various <code>WM_*</code> messages but without success. A future update to
this component will either fix this issue, or do it's own
scrollbar support without inheriting from <code>ScrollableControl</code>,
although I'd like to avoid this latter solution.</p>
<p>If anyone knows of a solution please let us know!</p>
<p>Another enhancement would be intelligent use of the
interpolation mode. Currently the control uses a fixed value,
but some values are better when zoomed in, and some better when
zoomed out. The ability for the control to automatically select
the most appropriate mode would be useful.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2010-08-28 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-4 .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comCreating a scrollable and zoomable image viewer in C# Part 3urn:uuid:4e72fcb8-5729-4038-a693-087d0e9e8f592012-11-25T09:21:03Z2010-08-23T18:08:01Z<p>After part 2 added scrolling support, we are now going to extend
this to support keyboard scrolling and panning with the mouse.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/imgbox-3.png" class="gallery" title="The ImageBox component, demonstrated in a sample application" ><img src="https://images.cyotek.com/image/thumbnail/devblog/imgbox-3.png" alt="The ImageBox component, demonstrated in a sample application" decoding="async" loading="lazy" /></a><figcaption>The ImageBox component, demonstrated in a sample application</figcaption></figure><h2 id="design-support">Design support</h2>
<p>In order to enable panning, we're going to add three new
properties. The <code>AutoPan</code> property will control if the user can
click and drag the image with the mouse in order to scroll.
Also, we'll add an <code>InvertMouse</code> property to control how the
scrolling works. Finally the <code>IsPanning</code> property; however it
can only be read publicly, not set.</p>
<p>As well as the backing events for the above properties, we'll
also add extra events - <code>PanStart</code> and <code>PanEnd</code> The normal
<code>Scroll</code> event will be utilized while panning is in progress
rather than a custom event.</p>
<h2 id="mouse-panning">Mouse Panning</h2>
<p>To pan with the mouse, the user needs to &quot;grab&quot; the control by
clicking and holding down the left mouse button. As they move
the mouse, the control should automatically scroll in the
opposite direction the mouse is moving (or if <code>InvertMouse</code> is
set, in the same direction). Once the button is released,
scrolling should stop.</p>
<p>We'll implement this by overriding <code>OnMouseMove</code> and
<code>OnMouseUp</code>, shown below.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnMouseMove<span class="symbol">(</span>MouseEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">base</span><span class="symbol">.</span>OnMouseMove<span class="symbol">(</span>e<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>e<span class="symbol">.</span>Button <span class="symbol">==</span> MouseButtons<span class="symbol">.</span>Left <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>AutoPan <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>IsPanning<span class="symbol">)</span>
 <span class="symbol">{</span>
 _startMousePosition <span class="symbol">=</span> e<span class="symbol">.</span>Location<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>IsPanning <span class="symbol">=</span> <span class="keyword">true</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>IsPanning<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> x<span class="symbol">;</span>
 <span class="keyword">int</span> y<span class="symbol">;</span>
 Point position<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">!</span><span class="keyword">this</span><span class="symbol">.</span>InvertMouse<span class="symbol">)</span>
 <span class="symbol">{</span>
 x <span class="symbol">=</span> <span class="symbol">-</span>_startScrollPosition<span class="symbol">.</span>X <span class="symbol">+</span> <span class="symbol">(</span>_startMousePosition<span class="symbol">.</span>X <span class="symbol">-</span> e<span class="symbol">.</span>Location<span class="symbol">.</span>X<span class="symbol">)</span><span class="symbol">;</span>
 y <span class="symbol">=</span> <span class="symbol">-</span>_startScrollPosition<span class="symbol">.</span>Y <span class="symbol">+</span> <span class="symbol">(</span>_startMousePosition<span class="symbol">.</span>Y <span class="symbol">-</span> e<span class="symbol">.</span>Location<span class="symbol">.</span>Y<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 x <span class="symbol">=</span> <span class="symbol">-</span><span class="symbol">(</span>_startScrollPosition<span class="symbol">.</span>X <span class="symbol">+</span> <span class="symbol">(</span>_startMousePosition<span class="symbol">.</span>X <span class="symbol">-</span> e<span class="symbol">.</span>Location<span class="symbol">.</span>X<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 y <span class="symbol">=</span> <span class="symbol">-</span><span class="symbol">(</span>_startScrollPosition<span class="symbol">.</span>Y <span class="symbol">+</span> <span class="symbol">(</span>_startMousePosition<span class="symbol">.</span>Y <span class="symbol">-</span> e<span class="symbol">.</span>Location<span class="symbol">.</span>Y<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 position <span class="symbol">=</span> <span class="keyword">new</span> Point<span class="symbol">(</span>x<span class="symbol">,</span> y<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>UpdateScrollPosition<span class="symbol">(</span>position<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnMouseUp<span class="symbol">(</span>MouseEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">base</span><span class="symbol">.</span>OnMouseUp<span class="symbol">(</span>e<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>IsPanning<span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>IsPanning <span class="symbol">=</span> <span class="keyword">false</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> UpdateScrollPosition<span class="symbol">(</span>Point position<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>AutoScrollPosition <span class="symbol">=</span> position<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>Invalidate<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnScroll<span class="symbol">(</span><span class="keyword">new</span> ScrollEventArgs<span class="symbol">(</span>ScrollEventType<span class="symbol">.</span>ThumbPosition<span class="symbol">,</span> <span class="number">0</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p><code>UpdateScrollPosition</code> is a common method to set the viewport
and refresh the control. The <code>IsPanning</code> property is used to
notify the control internally that a pan operation has been
started. It will also set a semi-appropriate cursor (we'll look
at custom cursors another time), and raise either the <code>PanStart</code>
or <code>PanEnd</code> events.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="symbol">[</span>DefaultValue<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">,</span> DesignerSerializationVisibility<span class="symbol">(</span>DesignerSerializationVisibility<span class="symbol">.</span>Hidden<span class="symbol">)</span><span class="symbol">,</span> Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">bool</span> IsPanning
<span class="symbol">{</span>
 <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> _isPanning<span class="symbol">;</span> <span class="symbol">}</span>
 <span class="keyword">protected</span> <span class="keyword">set</span>
 <span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_isPanning <span class="symbol">!=</span> value<span class="symbol">)</span>
 <span class="symbol">{</span>
 _isPanning <span class="symbol">=</span> value<span class="symbol">;</span>
 _startScrollPosition <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>AutoScrollPosition<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>value<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>Cursor <span class="symbol">=</span> Cursors<span class="symbol">.</span>SizeAll<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnPanStart<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>Cursor <span class="symbol">=</span> Cursors<span class="symbol">.</span>Default<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>OnPanEnd<span class="symbol">(</span>EventArgs<span class="symbol">.</span>Empty<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="keyboard-scrolling">Keyboard Scrolling</h2>
<p>The first two versions of this component effectively disabled
keyboard support via the <code>ControlStyles.Selectable</code> control
style and <code>TabStop</code> property. However, we now want to allow
keyboard support. So the first thing we do is remove the call to
disable the selectable style and resetting of the tab stop
property from the constructor. We also remove the custom
<code>TabStop</code> property we had implemented for attribute overriding.</p>
<p>With this done, we can now add some keyboard support. As the
<code>ScrollableControl</code> doesn't natively support this, we'll do it
ourselves by overriding <code>OnKeyDown</code>. One of the initial
drawbacks is that it won't always capture special keys, such as
the arrow keys.</p>
<p>In order for it to do so we need to let the control know that
such keys are required by overriding <code>IsInputKey</code> - if this
returns <code>true</code>, then the specified key is required and will be
captured in <code>OnKeyDown</code>.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">bool</span> IsInputKey<span class="symbol">(</span>Keys keyData<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">bool</span> result<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="symbol">(</span>keyData <span class="symbol">&amp;</span> Keys<span class="symbol">.</span>Right<span class="symbol">)</span> <span class="symbol">==</span> Keys<span class="symbol">.</span>Right <span class="symbol">|</span> <span class="symbol">(</span>keyData <span class="symbol">&amp;</span> Keys<span class="symbol">.</span>Left<span class="symbol">)</span> <span class="symbol">==</span> Keys<span class="symbol">.</span>Left <span class="symbol">|</span> <span class="symbol">(</span>keyData <span class="symbol">&amp;</span> Keys<span class="symbol">.</span>Up<span class="symbol">)</span> <span class="symbol">==</span> Keys<span class="symbol">.</span>Up <span class="symbol">|</span> <span class="symbol">(</span>keyData <span class="symbol">&amp;</span> Keys<span class="symbol">.</span>Down<span class="symbol">)</span> <span class="symbol">==</span> Keys<span class="symbol">.</span>Down<span class="symbol">)</span>
 result <span class="symbol">=</span> <span class="keyword">true</span><span class="symbol">;</span>
 <span class="keyword">else</span>
 result <span class="symbol">=</span> <span class="keyword">base</span><span class="symbol">.</span>IsInputKey<span class="symbol">(</span>keyData<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">return</span> result<span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnKeyDown<span class="symbol">(</span>KeyEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">base</span><span class="symbol">.</span>OnKeyDown<span class="symbol">(</span>e<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">switch</span> <span class="symbol">(</span>e<span class="symbol">.</span>KeyCode<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">case</span> Keys<span class="symbol">.</span>Left<span class="symbol">:</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustScroll<span class="symbol">(</span><span class="symbol">-</span><span class="symbol">(</span>e<span class="symbol">.</span>Modifiers <span class="symbol">==</span> Keys<span class="symbol">.</span>None <span class="symbol">?</span> <span class="keyword">this</span><span class="symbol">.</span>HorizontalScroll<span class="symbol">.</span>SmallChange <span class="symbol">:</span> <span class="keyword">this</span><span class="symbol">.</span>HorizontalScroll<span class="symbol">.</span>LargeChange<span class="symbol">)</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> Keys<span class="symbol">.</span>Right<span class="symbol">:</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustScroll<span class="symbol">(</span>e<span class="symbol">.</span>Modifiers <span class="symbol">==</span> Keys<span class="symbol">.</span>None <span class="symbol">?</span> <span class="keyword">this</span><span class="symbol">.</span>HorizontalScroll<span class="symbol">.</span>SmallChange <span class="symbol">:</span> <span class="keyword">this</span><span class="symbol">.</span>HorizontalScroll<span class="symbol">.</span>LargeChange<span class="symbol">,</span> <span class="number">0</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> Keys<span class="symbol">.</span>Up<span class="symbol">:</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustScroll<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> <span class="symbol">-</span><span class="symbol">(</span>e<span class="symbol">.</span>Modifiers <span class="symbol">==</span> Keys<span class="symbol">.</span>None <span class="symbol">?</span> <span class="keyword">this</span><span class="symbol">.</span>VerticalScroll<span class="symbol">.</span>SmallChange <span class="symbol">:</span> <span class="keyword">this</span><span class="symbol">.</span>VerticalScroll<span class="symbol">.</span>LargeChange<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> Keys<span class="symbol">.</span>Down<span class="symbol">:</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustScroll<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> e<span class="symbol">.</span>Modifiers <span class="symbol">==</span> Keys<span class="symbol">.</span>None <span class="symbol">?</span> <span class="keyword">this</span><span class="symbol">.</span>VerticalScroll<span class="symbol">.</span>SmallChange <span class="symbol">:</span> <span class="keyword">this</span><span class="symbol">.</span>VerticalScroll<span class="symbol">.</span>LargeChange<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> AdjustScroll<span class="symbol">(</span><span class="keyword">int</span> x<span class="symbol">,</span> <span class="keyword">int</span> y<span class="symbol">)</span>
<span class="symbol">{</span>
 Point scrollPosition<span class="symbol">;</span>

 scrollPosition <span class="symbol">=</span> <span class="keyword">new</span> Point<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>HorizontalScroll<span class="symbol">.</span>Value <span class="symbol">+</span> x<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>VerticalScroll<span class="symbol">.</span>Value <span class="symbol">+</span> y<span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>UpdateScrollPosition<span class="symbol">(</span>scrollPosition<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>When the left, right, up or down arrow keys are pressed, the
control checks to see if a modifier such as shift or control is
active. If not, then the control is scrolled either horizontally
or vertically using the &quot;small change&quot; value of the appropriate
scrollbar. If a modifier was set, then the scroll is made using
the &quot;large change&quot; value.</p>
<p>The <code>AdjustScroll</code> method is used to &quot;nudge&quot; the scrollbars in
the given direction, using values read from the
<code>HorizontalScroll</code> and <code>VerticalScroll</code> - reading the
<code>AutoScrollPosition</code> property didn't return appropriate results
in our testing.</p>
<h4 id="sample-project">Sample Project</h4>
<p>You can download the third sample project from the links below.
The final article in the series will add autofit, centering and
of course, zoom support.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2010-08-23 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-3 .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comCreating a scrollable and zoomable image viewer in C# Part 2urn:uuid:16fc4d3c-b9d4-469f-88b2-3b920b63e7e92012-11-25T09:20:51Z2010-08-13T20:22:52Z<p>In the second part of our <a href="/post/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-1">Creating a scrollable and zoomable
image viewer in C#</a> series we will update our component to
support automatic scrolling when auto size is disabled and the
image is larger than the client area of the control.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/imgbox-2.png" class="gallery" title="The ImageBox component, demonstrated in a sample application" ><img src="https://images.cyotek.com/image/thumbnail/devblog/imgbox-2.png" alt="The ImageBox component, demonstrated in a sample application" decoding="async" loading="lazy" /></a><figcaption>The ImageBox component, demonstrated in a sample application</figcaption></figure><h2 id="setting-up-auto-scrolling">Setting up auto scrolling</h2>
<p>Originally we inherited from <code>Control</code>, however this does not
support automatic scrolling. Rather than reinventing the wheel
at this point, we'll change the control to inherit from
<code>ScrollableControl</code> instead. This will expose a number of new
members, the ones we need are:</p>
<ul>
<li><code>AutoScroll</code> - Enables or disables automatic scrolling</li>
<li><code>AutoScrollMinSize</code> - Specifies the minimum size before
scrollbars appear</li>
<li><code>AutoScrollPosition</code> - Specifies the current scroll position</li>
<li><code>OnScroll</code> - Raised when the scroll position is changed</li>
</ul>
<p>Using the above we can now offer full scrolling.</p>
<p>As the control will take care of the scrolling behaviour, we
don't want the <code>AutoScrollMinSize</code> property to be available, so
we'll declare a new version of it and hide it with attributes.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="symbol">[</span>Browsable<span class="symbol">(</span><span class="keyword">false</span><span class="symbol">)</span><span class="symbol">,</span> EditorBrowsable<span class="symbol">(</span>EditorBrowsableState<span class="symbol">.</span>Never<span class="symbol">)</span><span class="symbol">,</span> DesignerSerializationVisibility<span class="symbol">(</span>DesignerSerializationVisibility<span class="symbol">.</span>Hidden<span class="symbol">)</span><span class="symbol">]</span>
<span class="keyword">public</span> <span class="keyword">new</span> Size AutoScrollMainSize
<span class="symbol">{</span>
 <span class="keyword">get</span> <span class="symbol">{</span> <span class="keyword">return</span> <span class="keyword">base</span><span class="symbol">.</span>AutoScrollMinSize<span class="symbol">;</span> <span class="symbol">}</span>
 <span class="keyword">set</span> <span class="symbol">{</span> <span class="keyword">base</span><span class="symbol">.</span>AutoScrollMinSize <span class="symbol">=</span> value<span class="symbol">;</span> <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>Initially the component only offered auto sizing and so we had
defined an <code>AdjustSize</code> method which was called in response to
various events and property changes. As we now need to set up
the scrolling area if <code>AutoScroll</code> is enabled, this method is no
longer as suitable. Instead, we add a pair of new methods,
<code>AdjustLayout</code> and <code>AdjustScrolling</code>. Existing calls to
<code>AdjustSize</code> are changed to call <code>AdjustLayout</code> instead, and
this method now calls either <code>AdjustScrolling</code> or <code>AdjustSize</code>
depending on the state of the <code>AutoSize</code> and <code>AutoScroll</code>
properties.</p>
<p>The <code>AdjustScrolling</code> method is used to set the
<code>AutoScrollMainSize</code> property. When this is correctly set, the
<code>ScrollableControl</code> will automatically take care of displaying
scrollbars.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> AdjustLayout<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoSize<span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustSize<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">else</span> <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoScroll<span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>AdjustScrolling<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>

<span class="keyword">protected</span> <span class="keyword">virtual</span> <span class="keyword">void</span> AdjustScrolling<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoScroll <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="keyword">this</span><span class="symbol">.</span>AutoScrollMinSize <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">.</span>Size<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="reacting-to-scroll-changes">Reacting to scroll changes</h2>
<p>By overriding the <code>OnScroll</code> event we get notifications whenever
the user scrolls the control, and can therefore redraw the
image.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnScroll<span class="symbol">(</span>ScrollEventArgs se<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">this</span><span class="symbol">.</span>Invalidate<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">base</span><span class="symbol">.</span>OnScroll<span class="symbol">(</span>se<span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="painting-adjustments">Painting adjustments</h2>
<p>The initial version of our <code>ImageBox</code> tiled a bitmap across the
client area of the control. In this new version, when we create
the background tile, we now create a new <code>TextureBrush</code>. During
drawing we can call <code>FillRectangle</code> and pass in the new brush
and it will be tiled for us.</p>
<p>Another shortcoming of the first version was the borders. These
were painted last, so that if the image was larger than the
controls client area, the image wouldn't be painted on top of
the borders. Now, the borders are drawn first and a clip region
applied to prevent any overlap.</p>
<p>Finally of course, the position of the drawn image needs to
reflect any scrollbar offset.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnPaint<span class="symbol">(</span>PaintEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">int</span> borderOffset<span class="symbol">;</span>
 Rectangle innerRectangle<span class="symbol">;</span>

 borderOffset <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>GetBorderOffset<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span>borderOffset <span class="symbol">!=</span> <span class="number">0</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="comment">// draw the borders</span>
 <span class="keyword">switch</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>BorderStyle<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">case</span> BorderStyle<span class="symbol">.</span>FixedSingle<span class="symbol">:</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ForeColor<span class="symbol">,</span> ButtonBorderStyle<span class="symbol">.</span>Solid<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> BorderStyle<span class="symbol">.</span>Fixed<span class="number">3</span>D<span class="symbol">:</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="number">3</span>D<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> Border<span class="number">3</span>DStyle<span class="symbol">.</span>Sunken<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// clip the background so we don&#39;t overwrite the border</span>
 innerRectangle <span class="symbol">=</span> Rectangle<span class="symbol">.</span>Inflate<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> <span class="symbol">-</span>borderOffset<span class="symbol">,</span> <span class="symbol">-</span>borderOffset<span class="symbol">)</span><span class="symbol">;</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>SetClip<span class="symbol">(</span>innerRectangle<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 innerRectangle <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">;</span>

 <span class="comment">// draw the background</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_texture <span class="symbol">!=</span> <span class="keyword">null</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>ShowGrid<span class="symbol">)</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>_texture<span class="symbol">,</span> innerRectangle<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 <span class="keyword">using</span> <span class="symbol">(</span>SolidBrush brush <span class="symbol">=</span> <span class="keyword">new</span> SolidBrush<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>BackColor<span class="symbol">)</span><span class="symbol">)</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>brush<span class="symbol">,</span> innerRectangle<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// draw the image</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">int</span> left<span class="symbol">;</span>
 <span class="keyword">int</span> top<span class="symbol">;</span>

 left <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Left <span class="symbol">+</span> borderOffset<span class="symbol">;</span>
 top <span class="symbol">=</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Top <span class="symbol">+</span> borderOffset<span class="symbol">;</span>

 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>AutoScroll<span class="symbol">)</span>
 <span class="symbol">{</span>
 left <span class="symbol">+=</span> <span class="keyword">this</span><span class="symbol">.</span>AutoScrollPosition<span class="symbol">.</span>X<span class="symbol">;</span>
 top <span class="symbol">+=</span> <span class="keyword">this</span><span class="symbol">.</span>AutoScrollPosition<span class="symbol">.</span>Y<span class="symbol">;</span>
 <span class="symbol">}</span>

 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>DrawImageUnscaled<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">,</span> <span class="keyword">new</span> Point<span class="symbol">(</span>left<span class="symbol">,</span> top<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// reset the clipping</span>
 <span class="keyword">if</span> <span class="symbol">(</span>borderOffset <span class="symbol">!=</span> <span class="number">0</span><span class="symbol">)</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>ResetClip<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="sample-project">Sample Project</h2>
<p>You can download the second sample project from the link below.
The next article in the series will look at panning the image
using the mouse within the client area of the image control.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2010-08-13 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-2 .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.comCreating a scrollable and zoomable image viewer in C# Part 1urn:uuid:02fd4283-ddf1-4672-9a64-39b46b02c3f72012-11-25T09:20:38Z2010-08-12T20:14:30Z<p>This is the first part in a series of articles that will result
in a component for viewing an image. The final component will
support zooming and scrolling.</p>
<p>In this first part, we're going to create a basic image viewer,
without the scrolling and zooming. Rather than having a plain
background however, we're going to create a two tone checker box
effect which is often used for showing transparent images. We'll
also allow this to be disabled and a solid colour used instead.</p>
<figure class="screenshot" ><a href="https://images.cyotek.com/image/devblog/imgbox.png" class="gallery" title="The sample project in action." ><img src="https://images.cyotek.com/image/thumbnail/devblog/imgbox.png" alt="The sample project in action." decoding="async" loading="lazy" /></a><figcaption>The sample project in action.</figcaption></figure><h2 id="creating-the-component">Creating the component</h2>
<p>The component inherits from <code>Control</code> rather than something
like <code>PictureBox</code> or <code>Panel</code> as we want to provide a lot of
our own behaviour.</p>
<p>The first thing we'll do is override some properties - to hide
the ones we won't be using such as <code>Text</code> and <code>Font</code>, and to
modify others, such as making <code>AutoSize</code> visible, and changing
the default value of <code>BackColor</code>.</p>
<p>Next is to add some new properties. We'll create the following
properties and respective change events:</p>
<ul>
<li><code>BorderStyle</code> - A standard border style.</li>
<li><code>GridCellSize</code> - The basic cell size.</li>
<li><code>GridColor</code> and <code>GridColorAlternate</code> - The colors used to
create the checkerboard style background.</li>
<li><code>GridScale</code> - A property for scaling the <code>GridCellSize</code> for
user interface options.</li>
<li><code>Image</code> - The image to be displayed.</li>
<li><code>ShowGrid</code> - Flag to determine if the checkerboard background
should be displayed.</li>
</ul>
<p>As we are offering auto size support, we also override some
existing events so we can resize when certain actions occur,
such as changing the control's padding or parent.</p>
<h2 id="setting-control-styles">Setting control styles</h2>
<p>As well as setting up default property values, the component's
constructor also adjusts several control styles.</p>
<ul>
<li><code>AllPaintingInWmPaint</code> - We don't need a separate
<code>OnPaintBackground</code> and <code>OnPaint</code> mechanism, <code>OnPaint</code> will do
fine.</li>
<li><code>UserPaint</code> - As we are doing entirely our own painting, we
disable the base <strong>Control</strong>'s painting.</li>
<li><code>OptimizedDoubleBuffer</code> - Double buffering means the painting
will occur in a memory buffer before being transferred to the
screen, reducing flicker.</li>
<li><code>ResizeRedraw</code> - Automatically redraw the component if it is
resized.</li>
<li><code>Selectable</code> - We disable this flag as we don't want the
control to be receiving focus.</li>
</ul>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">public</span> ImageBox<span class="symbol">(</span><span class="symbol">)</span>
<span class="symbol">{</span>
 InitializeComponent<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>SetStyle<span class="symbol">(</span>ControlStyles<span class="symbol">.</span>AllPaintingInWmPaint <span class="symbol">|</span> ControlStyles<span class="symbol">.</span>UserPaint <span class="symbol">|</span> ControlStyles<span class="symbol">.</span>OptimizedDoubleBuffer<span class="symbol">|</span> ControlStyles<span class="symbol">.</span>ResizeRedraw<span class="symbol">,</span> <span class="keyword">true</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>SetStyle<span class="symbol">(</span>ControlStyles<span class="symbol">.</span>Selectable<span class="symbol">,</span> <span class="keyword">false</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>UpdateStyles<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">this</span><span class="symbol">.</span>BackColor <span class="symbol">=</span> Color<span class="symbol">.</span>White<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>TabStop <span class="symbol">=</span> <span class="keyword">false</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>AutoSize <span class="symbol">=</span> <span class="keyword">true</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>GridScale <span class="symbol">=</span> ImageBoxGridScale<span class="symbol">.</span>Small<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>ShowGrid <span class="symbol">=</span> <span class="keyword">true</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>GridColor <span class="symbol">=</span> Color<span class="symbol">.</span>Gainsboro<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>GridColorAlternate <span class="symbol">=</span> Color<span class="symbol">.</span>White<span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>GridCellSize <span class="symbol">=</span> <span class="number">8</span><span class="symbol">;</span>
 <span class="keyword">this</span><span class="symbol">.</span>BorderStyle <span class="symbol">=</span> BorderStyle<span class="symbol">.</span>FixedSingle<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="creating-the-background">Creating the background</h2>
<p>The <code>CreateGridTileImage</code> method creates a tile of a 2x2 grid
using many of the properties listed above which is then tiled
across the background of the control.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">virtual</span> Bitmap CreateGridTileImage<span class="symbol">(</span><span class="keyword">int</span> cellSize<span class="symbol">,</span> Color firstColor<span class="symbol">,</span> Color secondColor<span class="symbol">)</span>
<span class="symbol">{</span>
 Bitmap result<span class="symbol">;</span>
 <span class="keyword">int</span> width<span class="symbol">;</span>
 <span class="keyword">int</span> height<span class="symbol">;</span>
 <span class="keyword">float</span> scale<span class="symbol">;</span>

 <span class="comment">// rescale the cell size</span>
 <span class="keyword">switch</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>GridScale<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">case</span> ImageBoxGridScale<span class="symbol">.</span>Medium<span class="symbol">:</span>
 scale <span class="symbol">=</span> <span class="number">1.5</span>F<span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> ImageBoxGridScale<span class="symbol">.</span>Large<span class="symbol">:</span>
 scale <span class="symbol">=</span> <span class="number">2</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">default</span><span class="symbol">:</span>
 scale <span class="symbol">=</span> <span class="number">1</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 cellSize <span class="symbol">=</span> <span class="symbol">(</span><span class="keyword">int</span><span class="symbol">)</span><span class="symbol">(</span>cellSize <span class="symbol">*</span> scale<span class="symbol">)</span><span class="symbol">;</span>

 <span class="comment">// draw the tile</span>
 width <span class="symbol">=</span> cellSize <span class="symbol">*</span> <span class="number">2</span><span class="symbol">;</span>
 height <span class="symbol">=</span> cellSize <span class="symbol">*</span> <span class="number">2</span><span class="symbol">;</span>
 result <span class="symbol">=</span> <span class="keyword">new</span> Bitmap<span class="symbol">(</span>width<span class="symbol">,</span> height<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">using</span> <span class="symbol">(</span>Graphics g <span class="symbol">=</span> Graphics<span class="symbol">.</span>FromImage<span class="symbol">(</span>result<span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">using</span> <span class="symbol">(</span>SolidBrush brush <span class="symbol">=</span> <span class="keyword">new</span> SolidBrush<span class="symbol">(</span>firstColor<span class="symbol">)</span><span class="symbol">)</span>
 g<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>brush<span class="symbol">,</span> <span class="keyword">new</span> Rectangle<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> width<span class="symbol">,</span> height<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>

 <span class="keyword">using</span> <span class="symbol">(</span>SolidBrush brush <span class="symbol">=</span> <span class="keyword">new</span> SolidBrush<span class="symbol">(</span>secondColor<span class="symbol">)</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 g<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>brush<span class="symbol">,</span> <span class="keyword">new</span> Rectangle<span class="symbol">(</span><span class="number">0</span><span class="symbol">,</span> <span class="number">0</span><span class="symbol">,</span> cellSize<span class="symbol">,</span> cellSize<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 g<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>brush<span class="symbol">,</span> <span class="keyword">new</span> Rectangle<span class="symbol">(</span>cellSize<span class="symbol">,</span> cellSize<span class="symbol">,</span> cellSize<span class="symbol">,</span> cellSize<span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>

 <span class="keyword">return</span> result<span class="symbol">;</span>
<span class="symbol">}</span>
</pre>
</figure>
<h2 id="painting-the-control">Painting the control</h2>
<p>As described above, we've disabled all default painting, so we
simply need to override <code>OnPaint</code> and do our custom painting
here.</p>
<figure class="lang-csharp highlight"><figcaption><span>csharp</span></figcaption><pre class="code">
<span class="keyword">protected</span> <span class="keyword">override</span> <span class="keyword">void</span> OnPaint<span class="symbol">(</span>PaintEventArgs e<span class="symbol">)</span>
<span class="symbol">{</span>
 <span class="keyword">if</span> <span class="symbol">(</span>_gridTile <span class="symbol">!=</span> <span class="keyword">null</span> <span class="symbol">&amp;&amp;</span> <span class="keyword">this</span><span class="symbol">.</span>ShowGrid<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="comment">// draw the background</span>
 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> x <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> x <span class="symbol">&lt;</span> <span class="keyword">this</span><span class="symbol">.</span>ClientSize<span class="symbol">.</span>Width<span class="symbol">;</span> x <span class="symbol">+=</span> _gridTile<span class="symbol">.</span>Size<span class="symbol">.</span>Width<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">for</span> <span class="symbol">(</span><span class="keyword">int</span> y <span class="symbol">=</span> <span class="number">0</span><span class="symbol">;</span> y <span class="symbol">&lt;</span> <span class="keyword">this</span><span class="symbol">.</span>ClientSize<span class="symbol">.</span>Height<span class="symbol">;</span> y <span class="symbol">+=</span> _gridTile<span class="symbol">.</span>Size<span class="symbol">.</span>Height<span class="symbol">)</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>DrawImageUnscaled<span class="symbol">(</span>_gridTile<span class="symbol">,</span> x<span class="symbol">,</span> y<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>
 <span class="symbol">}</span>
 <span class="keyword">else</span>
 <span class="symbol">{</span>
 <span class="keyword">using</span> <span class="symbol">(</span>SolidBrush brush <span class="symbol">=</span> <span class="keyword">new</span> SolidBrush<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>BackColor<span class="symbol">)</span><span class="symbol">)</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>FillRectangle<span class="symbol">(</span>brush<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// draw the image</span>
 <span class="keyword">if</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image <span class="symbol">!=</span> <span class="keyword">null</span><span class="symbol">)</span>
 <span class="symbol">{</span>
 e<span class="symbol">.</span>Graphics<span class="symbol">.</span>DrawImageUnscaled<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Image<span class="symbol">,</span> <span class="keyword">new</span> Point<span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Left <span class="symbol">+</span> <span class="keyword">this</span><span class="symbol">.</span>GetBorderOffset<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>Padding<span class="symbol">.</span>Top <span class="symbol">+</span> <span class="keyword">this</span><span class="symbol">.</span>GetBorderOffset<span class="symbol">(</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">)</span><span class="symbol">;</span>
 <span class="symbol">}</span>

 <span class="comment">// draw the borders</span>
 <span class="keyword">switch</span> <span class="symbol">(</span><span class="keyword">this</span><span class="symbol">.</span>BorderStyle<span class="symbol">)</span>
 <span class="symbol">{</span>
 <span class="keyword">case</span> BorderStyle<span class="symbol">.</span>FixedSingle<span class="symbol">:</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ForeColor<span class="symbol">,</span> ButtonBorderStyle<span class="symbol">.</span>Solid<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="keyword">case</span> BorderStyle<span class="symbol">.</span>Fixed<span class="number">3</span>D<span class="symbol">:</span>
 ControlPaint<span class="symbol">.</span>DrawBorder<span class="number">3</span>D<span class="symbol">(</span>e<span class="symbol">.</span>Graphics<span class="symbol">,</span> <span class="keyword">this</span><span class="symbol">.</span>ClientRectangle<span class="symbol">,</span> Border<span class="number">3</span>DStyle<span class="symbol">.</span>Sunken<span class="symbol">)</span><span class="symbol">;</span>
 <span class="keyword">break</span><span class="symbol">;</span>
 <span class="symbol">}</span>
<span class="symbol">}</span>
</pre>
</figure>
<p>First, we either draw a solid background using the <code>BackColor</code>
property if <code>ShowGrid</code> is <code>false</code>, otherwise we tile the grid
image created earlier.</p>
<p>Next we draw the actual image, if one has been set. The image is
offset based on the border style and padding.</p>
<p>Finally we draw the border style to ensure it appears on top of
the image if autosize is disabled and the control is too small.</p>
<h4 id="sample-project">Sample Project</h4>
<p>You can download the first sample project from the links below.
The next article in the series will look at implementing
scrolling for when the image is larger than the display area of
the control.</p>
<h2 id="update-history">Update History</h2>
<ul>
<li>2010-08-12 - First published</li>
<li>2020-11-21 - Updated formatting</li>
</ul>

<p><small>
All content <a href="https://devblog.cyotek.com/copyright-and-trademarks">Copyright (c) by Cyotek Ltd</a> or its respective writers. Permission to reproduce news and web log entries and other RSS feed content in unmodified form without notice is granted provided they are not used to endorse or promote any products or opinions (other than what was expressed by the author) and without taking them out of context. Written permission from the copyright owner must be obtained for everything else.<br />Original URL of this content is https://devblog.cyotek.com/post/creating-a-scrollable-and-zoomable-image-viewer-in-csharp-part-1 .
</small></p>Richard Mosshttps://www.cyotek.com/richard.moss@cyotek.com