diff --git a/DeathSocket/Domain.fs b/DeathSocket/Domain.fs index 1044c5b..834f72c 100644 --- a/DeathSocket/Domain.fs +++ b/DeathSocket/Domain.fs @@ -8,9 +8,9 @@ open SkiaSharp open System.Threading - /// The specification used to note the orignial file's location and the - /// changes the user would like to make to it. (including the save - /// location of the modified version). Use this spec if you are using + /// This specification is used to note the orignial file's location and + /// the changes the user would like to make to it. (including the save + /// location of the modified version). Use this spec. if you are using /// System.Drawing. type BrushSpec = { /// The original path of the image which the grid is being added to. @@ -27,11 +27,11 @@ ///The number of columns the grid will have. columns: int } - /// The specification used to note the orignial file's location and the - /// changes the user would like to make to it (including the save + /// This specification is used to note the orignial file's location and + /// the changes the user would like to make to it (including the save /// location of the modified version). This specification includes - /// individual RGBA values to describe the grid's colour. Use this spec. - /// if you are using System.Drawing. + /// individual RGBA values to describe the grid's colour. Use this + /// spec. if you are using System.Drawing. type RGBASpec = { /// The original path of the image which the grid is being added to. originalPath: string @@ -53,10 +53,10 @@ ///The number of columns the grid will have. columns: int } - /// The specification used to note the orignial file's location and the - /// changes the user would like to make to it (including the save + /// This specification is used to note the orignial file's location and + /// the changes the user would like to make to it (including the save /// location of the modified version). Use this spec. if you are using - /// the Skia-Sharp library. + /// the SkiaSharp library. type SkiaSpec = { /// The original path of the image which the grid is being added to. originalPath: string @@ -71,11 +71,53 @@ ///The number of columns the grid will have. columns: int } - /// The specification used to note the orignial file's location and the - /// changes the user would like to make to it (including the save + /// This specification is used to create a gridded image as an SKData + /// buffer. This spec. allows you to use one of the pre-formed + /// SKColors. This makes it quicker and easier to develop your code + /// when compared to SkiaRGBStream. That requires setting individual + /// RGB values. You this spec. if you are using the SkiaSharp library. + type SkiaBufferSpec = + { /// The location of the image the grid will be applied to + filePath: string + /// The (SkiaSharp) colour used to draw the grid. + skColour: SKColor + /// The thickness of the line on the grid. + penWidth: float32 + /// The number of rows the grid will have. + rows: int + ///The number of columns the grid will have. + columns: int } + + /// This specification is used to create a gridded image as a SKData + /// buffer. This spec. allows you to specify the colour of the grid + /// using individual RGB values. This gives you greater control of your + /// colour choice than a SkiaStreamSpec, which requires a pre-formed + /// SKColor value. you this spec. if you are using the SkiaSharp + /// library. + type SkiaRGBBufferSpec = + { /// The location of the image the grid will be applied to + filePath: string + /// The amount of red (RGB) the grid's line will have. + /// (0 = no red, 255 = red turned up to eleven). + red: float32 + /// The amount of green (RGB) the grid's line will have. + /// (0 = no green, 255 = green turned up to eleven). + green: float32 + /// The amount of blue (RGB) the grid's line will have. + /// (0 = no blue, 255 = blue turned up to eleven). + blue: float32 + /// The thickness of the line on the grid. + penWidth: float32 + /// The number of rows the grid will have. + rows: int + ///The number of columns the grid will have. + columns: int } + + /// This specification is used to note the orignial file's location and + /// the changes the user would like to make to it (including the save /// location of the modified version). This specification includes /// individual RGB values to describe the grid's colour. Use this spec. - /// if you are using the Skia-Sharp library. + /// if you are using the SkiaSharp library. type SkiaRGBSpec = { /// The original path of the image which the grid is being added to. originalPath: string @@ -100,12 +142,20 @@ /// A Discriminated Union representing the various specification types /// Death Socket can use to apply a grid to an image. type ImageSpec = + /// Spec. which uses System.Drawing and its built-in (colour) Brush. | Brush of BrushSpec + /// Spec. which uses System.Drawing and individual RGBA values. | RGBA of RGBASpec + /// Spec. which uses SkiaSharp and its built-in SKColors. | Skia of SkiaSpec + /// Spec. which uses SkiaSharp and individual RGB values. | SkiaRGB of SkiaRGBSpec + /// Spec. which uses SkiaSharp and its built-in SKColors. + | SkiaBuffer of SkiaBufferSpec + /// Spec. which uses SkiaSharp and individual RGB values. + | SkiaRGBBuffer of SkiaRGBBufferSpec - /// A Discriminated Union representing a type of image. The type refers + /// A Discriminated Union denoting a type of image. The type refers /// to the graphics library used to read the image file. /// The graphics library determines how the image is read and /// manipulated in memory and the functions you can perform on it. @@ -114,5 +164,7 @@ /// the recommended choice. If you using Xamarin, SkiaSharp is the /// recommended choice. type ImageType = + /// Denotes an image created using SkiaSharp | SkiaSharp of string + /// Denotes an image created using System.Drawing | SystemDrawing of string \ No newline at end of file diff --git a/DeathSocket/GridPainter.fs b/DeathSocket/GridPainter.fs index 7240dd5..2e8126e 100644 --- a/DeathSocket/GridPainter.fs +++ b/DeathSocket/GridPainter.fs @@ -15,10 +15,13 @@ namespace DeathSocket open Validation open ImageServices + open SkiaSharp /// /// Uses the information included in spec to create a gridded image. /// It then asynchronously saves it. Uses .jpg or .png formats only. + /// Please note: Any buffer-based spec's (E.G. SkiaBufferSpec) will be + /// ignored by this function. /// /// /// The specification used to generate the new gridded image. The @@ -46,6 +49,7 @@ namespace DeathSocket | SkiaRGB sR -> validateIO sR.originalPath sR.savePath |> ignore drawSkiaRGBGrid sR + | _ -> printfn "[DEATH SOCKET INFO] Inappropriate ImageSpec used here. Please use none buffer-based spec's when using this function." with | :? FileNotFoundException as ex -> printfn "File could not be found at %s" ex.Message @@ -104,7 +108,7 @@ namespace DeathSocket reraise () // System.Drawing Functions - // ======================================================================== + // ==================================================================== /// /// Determines the (Pen) points needed to draw the appropriate number @@ -162,7 +166,45 @@ namespace DeathSocket makeBrushFromRGBASpec spec // SkiaSharp Functions - // ======================================================================== + // ==================================================================== + + // NOT TESTED + + /// + /// Uses the information included in spec to create a SKData buffer. + /// The buffer will contain a gridded image which you can then use in + /// any way you see fit. Accepted image formats are limited to .jpg + /// and .png. Please note: Any non buffer-based spec's (E.G. SkiaSpec) + /// will be ignored by this function. + /// + /// + /// The specification used to generate the new SKData buffer. The + /// ImageSpec is a discriminated union, consisting of a SkiaBuffer and + /// a SkiaRGBBuffer spec. + /// + /// + /// Make sure the image, which is used to create the SKData buffer, + /// is not in use or needed by another program/process. + /// This is because it is locked whilst in this function. + /// + let createSKDataAsync (spec: ImageSpec) = + async { + try + match spec with + | SkiaBuffer s -> + validateFilePath s.filePath |> ignore + return createSkiaBuffer s + | SkiaRGBBuffer sR -> + validateFilePath sR.filePath |> ignore + return createSkiaRGBBuffer sR + | _ -> + printfn "[DEATH SOCKET INFO] Inappropriate ImageSpec used here. Please use buffer-based spec's when using this function. Returning an empty SKData object." + return SKData.Empty + with + | :? FileNotFoundException as ex -> + printfn "%s" ex.Message |> ignore + return SKData.Empty + } /// /// Determines the (SKPoints) points needed to draw the appropriate diff --git a/DeathSocket/ImageServices.fs b/DeathSocket/ImageServices.fs index d75ed25..26ca891 100644 --- a/DeathSocket/ImageServices.fs +++ b/DeathSocket/ImageServices.fs @@ -8,13 +8,25 @@ open SkiaSharp open System + (* Why the Use of Repeated Code + =========================================================================== + In this file, you will find code which seems duplicated. The areas in + question are mostly the graphics/drawing parts of the file. Before you + consider refactoring this code, please note the memory footprint of this + library. By using "use", the objects created are diposed of properly. This, + also, means it is difficult to pass/return them from one function to the + next. Therefore, the decision made was to accept the duplicated code in + exchange for a smaller memory footprint. Several (large) undisposed images + lurking in an end users RAM can grid their computer to a halt. On top of + that, they have no means to make alterations to the code. *) + let setLineThickness pDimension aDimension lineWidth = if (pDimension <= 0.0 || aDimension <= 0.0 || lineWidth <= 0.0) then raise (new DivideByZeroException ("[ERROR] The images height and width must be greater than 0.")) else lineWidth / (pDimension / aDimension) - // SkiaSharp Functions - // ======================================================================== + (* SkiaSharp Functions + ======================================================================== *) let createSKHorizontalLines width height rows = let interval = float32 (height / rows) @@ -113,8 +125,87 @@ use saveStream = File.OpenWrite (spec.savePath) data.SaveTo (saveStream) - // System.Drawing Functions - // ======================================================================== + // Does not work when using SkiaSharp NuGet v. 1.68 + let createSkiaBuffer (spec: SkiaBufferSpec) = + use fileStream = File.Open (spec.filePath, FileMode.Open) + use skStream = new SKManagedStream (fileStream) + use bitmap = SKBitmap.Decode (skStream) + use shader = + SKShader.CreateBitmap (bitmap, SKShaderTileMode.Mirror, SKShaderTileMode.Mirror) + let imageInfo = new SKImageInfo (bitmap.Width, bitmap.Height) + use surface = SKSurface.Create (imageInfo) + use canvas = surface.Canvas + canvas.Clear (SKColors.White) + + use imageFill = new SKPaint () + imageFill.Style <- SKPaintStyle.Fill + imageFill.Shader <- shader + canvas.DrawPaint (imageFill) + + use stroke = new SKPaint () + stroke.Style <- SKPaintStyle.Stroke + stroke.StrokeWidth <- spec.penWidth + stroke.Color <- spec.skColour + let horizontalLines = + createSKHorizontalLines (bitmap.Width) (bitmap.Height) (spec.rows) + let verticalLines = + createSKVerticalLines (bitmap.Width) (bitmap.Height) (spec.columns) + for hLine in horizontalLines do + canvas.DrawLine (hLine.[0], hLine.[1], stroke) + for vLine in verticalLines do + canvas.DrawLine (vLine.[0], vLine.[1], stroke) + + use snapshot = surface.Snapshot () + let data = + match Path.GetExtension (spec.filePath) with + | ".jpg" | ".JPG" -> snapshot.Encode (SKEncodedImageFormat.Jpeg, 100) + | ".png"| ".PNG" -> snapshot.Encode (SKEncodedImageFormat.Png, 100) + | _ -> invalidArg "filePath" "The file type must be a .jpg or .png file." + // (data) Buffer returned -- does not dispose automatically. + data + + // Does not work when using SkiaSharp NuGet v. 1.68 + let createSkiaRGBBuffer (spec: SkiaRGBBufferSpec) = + use fileStream = File.Open (spec.filePath, FileMode.Open) + use skStream = new SKManagedStream (fileStream) + use bitmap = SKBitmap.Decode (skStream) + use shader = + SKShader.CreateBitmap (bitmap, SKShaderTileMode.Mirror, SKShaderTileMode.Mirror) + let imageInfo = new SKImageInfo (bitmap.Width, bitmap.Height) + use surface = SKSurface.Create (imageInfo) + use canvas = surface.Canvas + canvas.Clear (SKColors.White) + + use imageFill = new SKPaint () + imageFill.Style <- SKPaintStyle.Fill + imageFill.Shader <- shader + canvas.DrawPaint (imageFill) + + use stroke = new SKPaint () + stroke.Style <- SKPaintStyle.Stroke + stroke.StrokeWidth <- spec.penWidth + stroke.Color <- + new SKColor ((byte spec.red), (byte spec.green), (byte spec.blue)) + let horizontalLines = + createSKHorizontalLines (bitmap.Width) (bitmap.Height) (spec.rows) + let verticalLines = + createSKVerticalLines (bitmap.Width) (bitmap.Height) (spec.columns) + for hLine in horizontalLines do + canvas.DrawLine (hLine.[0], hLine.[1], stroke) + for vLine in verticalLines do + canvas.DrawLine (vLine.[0], vLine.[1], stroke) + + use snapshot = surface.Snapshot () + let data = + match Path.GetExtension (spec.filePath) with + | ".jpg" | ".JPG" -> snapshot.Encode (SKEncodedImageFormat.Jpeg, 100) + | ".png"| ".PNG" -> snapshot.Encode (SKEncodedImageFormat.Png, 100) + | _ -> invalidArg "filePath" "The file type must be a .jpg or .png file." + // (data) Buffer returned -- does not dispose automatically. + data + + (* System.Drawing Functions + ======================================================================== *) let createHorizontalLines width height rows = let interval = height / rows @@ -135,13 +226,15 @@ (* Note on Use of Temp. File in Functions which Add A Grid Overlay =========================================================================== The temp. file is used in the functions below are there to convert images - with indexed pixels. Instead of listing them all, just assume any function - with a "*Spec" type as a parameter will use this "temp" file. *) + with indexed pixels. Instead of listing them all (future additions), just + assume any function with a "*Spec" type as a parameter will use this "temp" + file. *) let drawBrushSpecGrid (spec: BrushSpec) = use original = Bitmap.FromFile spec.originalPath use temp = new Bitmap(original) - use clone = temp.Clone(new Rectangle(0, 0, temp.Width, temp.Height), PixelFormat.Format32bppArgb) + use clone = + temp.Clone(new Rectangle(0, 0, temp.Width, temp.Height), PixelFormat.Format32bppArgb) use graphics = Graphics.FromImage(clone) use pen = new Pen (spec.colour, width = spec.penWidth) graphics.DrawImage(original,new Rectangle(0, 0, clone.Width, clone.Height)) @@ -156,7 +249,8 @@ let drawRGBAGrid (spec: RGBASpec) = use original = Bitmap.FromFile spec.originalPath use temp = new Bitmap (original) - use clone = temp.Clone (new Rectangle(0, 0, temp.Width, temp.Height), PixelFormat.Format32bppArgb) + use clone = + temp.Clone (new Rectangle(0, 0, temp.Width, temp.Height), PixelFormat.Format32bppArgb) use graphics = Graphics.FromImage(clone) use pen = new Pen ((makeBrushFromRGBASpec spec), width = spec.penWidth) graphics.DrawImage (original,new Rectangle(0, 0, clone.Width, clone.Height))