9 Using Death Socket in Your Project
Craig Oates edited this page 4 years ago

Although there are slight variations, you will use Death Socket (D.S.) as part of a three step process; Which are:

  1. Construct a specification for how you want the overlay to look.
  2. Pass the specification to D.S. in preparation of adding the grid to the image.
  3. Tell D.S. to add the grid to the image, synchronously or asynchronously.

Before we get into what the specifications look like, here is an example of the above steps in code,

(* For now, view the first line as the "construct a specification" part
of the above three steps. Info. on how to build an image specification
is a topic I will cover a little later. *)

Brush (buildSpec imgPath numRows numColumns pWidth colour newPath)
|> applyGridToImageAsync
|> Async.Start

For a more real-world example (with extra cruft), please read the add-grid function in "Commands.fs". This file is part of the DeathSocketCLI project and the link for it is as follows:

Note: Most of the "add-grid" functions will (by default) save the gridded image for you (using the save location part of the image specification). If you prefer, D.S. can create a gridded image and not save it. When that happens, it will return it as an in-memory buffer. If you are not sure on which function does what, use Visual Studio's intellisense. It will help you identify those which saves the image for you and those which return an in-memory buffer. Having said that, you are free to read the source code for these functions with the following link:

A Note about the Graphics Libraries Used by Death Socket (I.E. The System's Flow)

If you look at the source code for D.S., you will see I have included two graphics libraries. They are:

I have included these two libraries because D.S. is a .Net Standard 2.0 library. This means the code in D.S. can run on the traditional .Net framework and .Net Core. If you are building a traditional .Net application (E.G. a W.P.F. app.), I recommend you use System.Drawing. If you are creating a cross-platform application (E.G. a Xamarin app.), you will need to use the SkiaSharp parts of D.S. This means the general flow of the system looks like this,

deathsocket graphics lib. divide

 If you would like to know more about these graphics libraries, please use the following links:

This next bit is an aside; So, feel free to skip over this bit and jump straight into the next section... I admit I should have dropped the System.Drawing part and focused on SkiaSharp, at least in the long-term. The reason I did not is because using SkiaSharp with a Xamarin project annoyed me. Long story short... I developed D.S. as the foundation for Paint Grid which is a G.U.I. version of D.S. Paint Grid exists as a W.P.F. and Xamarin (MacOS) application. (The Mac version is not published at the time of writing.) I built the W.P.F. version first with System.Drawing and it was fine. I discovered Xamarin does not work with System.Drawing and it recommended SkiaSharp. This was not as easy to make as the W.P.F. version. So, by the time I finished writing the application, I was annoyed and wanted to throw my toys out of the pram. I decided to keep the System.Drawing code in D.S. so I would not need to use the SkiaSharp bits if I needed it for another project. Do not let this put you off using the SkiaSharp part of D.S., though. If you are building a cross-platform application, I recommend you use the SkiaSharp stuff. I would even go as far as saying I would probably write D.S. with just SkiaSharp if I had to start from scratch -- having calmed down.

The Pieces You Will Work With

As a consumer of D.S., you will not have access to every part of it. Instead, you work with two modules within a single name-space, called DeathSocket. The module's names are Domain and GridPainter.

deathsocket public modules

The above image shows the flow of a typical code-base using D.S. Looking at this, you would think the GridPainter module is where you should focus most of your time. This is not immediately true, though. If anything, forming good image-specifications is the more time consuming task.

To help you get started, I recommend you keep the following two files open when writing your code:

Domain Module

Within Domain, you will find the following types:

  • BrushSpec
  • RGBASpec
  • SkiaSpec
  • SkiaBufferSpec
  • SkiaRGBBufferSpec
  • SkiaRGBSpec
  • Imagespec
  • ImageType

You can split these types up into the following groups:

System.Drawing SkiaSharp Discriminated Unions
BrushSpec SkiaSpec ImageSpec
RGBASpec SkiaBufferSpec ImageType
SkiaRGBBufferSpec
SkiaRGBSpec

As you can see, the types fall into one of three groups. The name of the groups refer to the graphics libraries used by the functions which use those types -- excluding "Discriminated Unions" (D.U.). For example, any function which uses BrushSpec also uses the System.Drawing library. If you use a function which takes a SkiaSpec, that function will also use the SkiaSharp library. The types in the D.U. group mostly act as wrappers for the types in the other two groups. The main thing they (D.U's) offer is a way to generalise an input parameter for a function. For example, instead of writing separate functions for each type in the other two groups, I wrote one. Within that function, I specified it needed an ImageSpec. From here, I can deduce the type within the function and make the appropriate function calls. Here is a code example to help explain,

// Instead of separate functions for each type, I now have one.

let applyGridToImageAsync (spec: ImageSpec) =
           async {
               try
                   match spec with
                   | Brush b ->
                       validateIO b.originalPath b.savePath |> ignore
                       drawBrushSpecGrid b
                   | RGBA r ->
                       validateIO r.originalPath r.savePath |> ignore
                       drawRGBAGrid r
                   | Skia s ->
                       validateIO s.originalPath s.savePath |> ignore
                       drawSkiaGrid s
                   | SkiaRGB sR ->
                       validateIO sR.originalPath sR.savePath |> ignore
                       drawSkiaRGBGrid sR
                   | _ -> printfn "Inappropriate ImageSpec used here."
               with
               | :? FileNotFoundException as ex ->
                   printfn "File could not be found at %s" ex.Message
           }

The name of the other D.U. is ImageType. I view this as a way to refer to a type within the other two (non D.U.) groups without creating them. I do not foresee you using this D.U. much but it is there if you need it. The only place I have use it in DeathSocket is in the following function (found in "GridPainter.fs"),

let determineImageDimensions (imgType: ImageType) =
           try
               match imgType with
               | SkiaSharp s ->
                   validateFilePath s |> ignore
                   determineSkiaDimensions s
               | SystemDrawing d ->
                   validateFilePath d |> ignore
                   determineSystemDrawingDimensions d
           with
           | :? FileNotFoundException as ex ->
               printfn "%s" ex.Message
               reraise ()

The purpose of this function is to determine the dimensions of the image at the location specified in the ImageType. Having said that, DeathSocket uses System.Drawing and SkiaSharp. So, it needs to provide a way to use both libraries. I could have written two separate functions, one for each graphics library. But, that felt like a waste of effort. So, I used the ImageType and condensed it into one. To be clear, all this function needs is the path of the image. What the ImageType provides is extra context.

Instead of repeating all the code in "Domain.fs", I recommend you take a look at the file yourself. The link for that is as follows:

I have documented the code within the file using XML comments. So, you should get intellisense overlays when you use Visual Studio or Visual Studio Code.

GridPainter Module

Within GridPainter, you will find ten functions, which are as follows:

  1. applyImageToGridAsync
  2. scaleLineThickness
  3. determineImageDimensions
  4. determineHorizontalLines
  5. determineVerticalLines
  6. makeSolidBrushFromRGBA
  7. makeSolidBrushFromRGBASpec 
  8. createSKDataAsync
  9. determineSKHorizontalLines
  10. determineSKVerticalLines

Instead of repeating the code from that file here, I recommend you open up "GridPainter.fs" in a separate tab. Every function has XML comments explaining what they do. So, you should have intellisense in Visual Studio and Visual Studio Code. If you read "GridPainter.fs", you will notice I have marked out areas called "SkiaSharp Functions" and "System.Drawing Functions". This should help you denote which function use which graphics library. If you are unsure which function uses which library, you can refer back to the XML comments/intellisense. As a general rule, if a function has "SK" in its name, it uses SkiaSharp. If it does not, it uses System.Drawing. The link for GridPainter.fs is as follows:

In an attempt to get you up and running, I have provided the following table. It shows what graphics library is used by each function and should help alongside the Intellisense in Visual Studio.

System.Drawing SkiaSharp Both
determineHorizontalLines createSKDataAsync applyImageToGridAsync
determineVerticalLines determineSKHorizontalLines scaleLineThickness
makeSolidBrushFromRGBA determineSKVerticalLines determineImageDimensions
makeSolidBrushFromRGBASpec