diff --git a/DeathSocket/ColourServices.fs b/DeathSocket/ColourServices.fs index 4fba086..1226438 100644 --- a/DeathSocket/ColourServices.fs +++ b/DeathSocket/ColourServices.fs @@ -2,8 +2,6 @@ open System.Drawing open DeathSocket.Domain - open System - open SkiaSharp let makeBrushFromRGBASpec (spec: RGBASpec) = let a = int spec.alpha diff --git a/DeathSocket/DeathSocket.fsproj b/DeathSocket/DeathSocket.fsproj index ab27a1b..bb8c781 100644 --- a/DeathSocket/DeathSocket.fsproj +++ b/DeathSocket/DeathSocket.fsproj @@ -4,7 +4,7 @@ netstandard2.0 true true - 0.6-alpha + 0.7.1-alpha Craig Oates Death Socket A .Net Standard library which you can plug into to project and draw gridded overlays onto you images. Please note, Death Socket uses System.Drawing brushes and not System.Media brushes. For futher information, visit the projects repository on GitHub. To get a sense of what Death Socket can do, there is a console program you can download seperately (on GitHub). @@ -15,15 +15,17 @@ https://github.com/CraigOates/Death-Socket/tree/master Git deathsocket craig oates image overlay f# - This release deprecated the "applyGrid" functions and refactored them into one single function. - 0.6.0.0 + This release fixes the pen thickness scaling bugs in 0.7. + 0.7.1.0 https://github.com/CraigOates/Death-Socket/blob/master/.github/Images/death-socket-logo.png + 0.7.1.0 - + + diff --git a/DeathSocket/Domain.fs b/DeathSocket/Domain.fs index 5bf5a4b..a6d9276 100644 --- a/DeathSocket/Domain.fs +++ b/DeathSocket/Domain.fs @@ -6,10 +6,11 @@ open System.Drawing 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. @@ -26,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 @@ -52,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 @@ -70,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 @@ -96,10 +139,36 @@ ///The number of columns the grid will have. columns: int } - /// Discriminated Union representing the various specification types + /// 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 - | SkiaRGB of SkiaRGBSpec \ No newline at end of file + /// 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 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. + /// When creating an ImageType, pass in the file path of the image + /// as a string. If you are on Windows or using Mono, SystemDrawing is + /// 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 + + type internal longestDimension = + | Width + | Height \ No newline at end of file diff --git a/DeathSocket/GridPainter.fs b/DeathSocket/GridPainter.fs index e961c71..9045f8d 100644 --- a/DeathSocket/GridPainter.fs +++ b/DeathSocket/GridPainter.fs @@ -13,24 +13,23 @@ namespace DeathSocket /// functions. module GridPainter = - open Validation + open ValidationServices + open ImagePrep open ImageServices - open System + 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 /// ImageSpec is a discriminated union, consisting of a Brush, RGBA, /// Skia or SkiaRGB spec. /// - /// - /// If the file the grid is being applied to cannot be found, - /// a FileNotFoundException will be thrown. - /// - /// /// Make sure the image, which is having the overlay added to it, /// is not in use or needed by another program/process. /// This is because it is locked whilst in this function. @@ -51,13 +50,70 @@ 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 - } + } - // System.Drawing Functions - // ======================================================================== + // NOT TESTED. + /// + /// Determines the current scale an image is viewed at (E.G. scaled + /// preview in image viewer). The (pen) line thickness is then updated + /// to match this preview scale and can be used when drawing a grid + /// line on the scaled preview of the image. + /// + /// + /// The width or height of the image when previewed (I.E. in a GUI). + /// + /// + /// The width or height of the actual image. + /// + /// + /// The thickness of the pen used to draw the grid line. + /// + /// + /// You should find you only need this function when dealing with + /// previewing an image in a GUI. Another thing to note is the + /// SkiaSharp based functions already scale the grid lines for you. So, + /// you should not need to use this function when using them. + /// + let scaleLineThickness (previewDimensions: int * int) (actualDimensions: int * int) (lineThickness: double) = + validateDimensions previewDimensions |> ignore + validateDimensions actualDimensions |> ignore + validateLineThickness |> ignore + adjustLineThickness previewDimensions actualDimensions lineThickness + + /// + /// Reads an (jpg or png) image and return its width and height as a + /// tuple, (width * hight). + /// + /// + /// The name of the graphics library used to read and determine the + /// images dimensions. + /// + /// + /// The image type is determined by the graphics library used. It is + /// called "ImageType" and not "GraphicsLibUsed" because the image/file + /// information is the thing this function cares about most of all. How + /// it is read and what library it uses it secondary. + /// + 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 () + + (* System.Drawing Functions + ==================================================================== *) /// /// Determines the (Pen) points needed to draw the appropriate number @@ -68,7 +124,9 @@ namespace DeathSocket /// The width of the image. /// The height of the image. /// The number of rows the grid should have. - /// You will probably only need these when dealing with GUI's. + /// + /// You will probably only need these when dealing with GUI's. + /// let determineHorizontalLines (width: int) (height: int) (rows: int) = createHorizontalLines width height rows @@ -80,68 +138,14 @@ namespace DeathSocket /// /// The width of the image. /// The height of the image. - /// The number of columns the grid should have. - /// You will probably only need these when dealing with GUI's. - let determineVerticalLines (width: int) (height: int) (columns: int) = - createVerticalLines width height columns - - /// - /// Uses the information included in spec to create a gridded image. - /// It then asynchronously saves it. Uses .jpg or .png formats only. - /// Use this function when targeting .Net/Mono). - /// - /// - /// The specification used to generate the new gridded image - /// - /// - /// If the file the grid is being applied to cannot be found, - /// a FileNotFoundException will be thrown. - /// - /// - [] - let applyBrushSpecGridAsync (spec: BrushSpec) = - async { - try - validateFilePath spec.originalPath |> ignore - validateSaveFileType spec.savePath |> ignore - drawBrushSpecGrid spec |> ignore - with - | :? FileNotFoundException as ex -> - printfn "File could not be found at %s" ex.Message - } - - /// - /// Uses the information included in spec to create a gridded image. It - /// then asynchronously saves it. Uses .jpg or .png formats only. - /// Use this function when targeting .Net/Mono). - /// - /// - /// The specification used to generate the gridded image. + /// + /// The number of columns the grid should have. /// - /// - /// If the file the grid is being applied to cannot be found, - /// a FileNotFoundException will be thrown. - /// - /// + /// You will probably only need these when dealing with GUI's. /// - [] - let applyRGBAGridAsync (spec: RGBASpec) = - async { - try - validateFilePath spec.originalPath |> ignore - validateSaveFileType spec.savePath |> ignore - drawRGBAGrid spec - with - | :? FileNotFoundException as ex -> - printfn "File could not be found at %s" ex.Message - } + let determineVerticalLines (width: int) (height: int) (columns: int) = + createVerticalLines width height columns /// /// Creates a Sytsem.Drawing SolidBrush from the individual RGBA values. @@ -163,8 +167,8 @@ namespace DeathSocket /// Use this function when targeting .Net/Mono). /// /// - ///The specification which the brush is made from. - /// + /// The specification which the brush is made from. + /// /// /// Death Socket uses System.Drawing and not System.Media for colours /// and brushes. @@ -172,8 +176,44 @@ namespace DeathSocket let makeSolidBrushFromRGBASpec (spec: RGBASpec) = makeBrushFromRGBASpec spec - // SkiaSharp Functions - // ======================================================================== + (* SkiaSharp Functions + ==================================================================== *) + + /// + /// 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 @@ -184,8 +224,10 @@ namespace DeathSocket /// The width of the image. /// The height of the image. /// The number of rows the grid should have. - /// This function is part of the SkiaSharp functions provided - /// by Death Socket. + /// + /// This function is part of the SkiaSharp functions provided + /// by Death Socket. + /// let determineSKHorizontalLines (width: int) (height: int) (rows: int) = createSKHorizontalLines width height rows @@ -199,61 +241,9 @@ namespace DeathSocket /// The height of the image. /// The number of columns the grid should have. /// - /// This function is part of the SkiaSharp functions provided - /// by Death Socket. - let determineSKVerticalLines (width: int) (height: int) (columns: int) = - createSKVerticalLines width height columns - - /// - /// Uses the information included in spec to create a gridded image. - /// It then asynchronously saves it. Uses .jpg or .png formats only. - /// Use this function when targeting Xamarin/UWP/.Net Core. - /// - /// - /// 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). - /// - /// - /// Make sure the image, which is having the overlay added to it, - /// is not in use or needed by another program/process. - /// This is because it is locked whilst in this function. - /// - [] - let applySkiaGridAsync (spec: SkiaSpec) = - async { - try - validateFilePath spec.originalPath |> ignore - validateSaveFileType spec.savePath |> ignore - drawSkiaGrid spec - with - | :? FileNotFoundException as ex -> - printfn "File could not be found at %s" ex.Message - } - - /// - /// Uses the information included in spec to create a gridded image. - /// It then asynchronously saves it. Uses .jpg or .png formats only. - /// Use this function when targeting Xamarin/UWP/.Net Core. - /// - /// - /// 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). - /// /// - /// Make sure the image, which is having the overlay added to it, - /// is not in use or needed by another program/process. - /// This is because it is locked whilst in this function. + /// This function is part of the SkiaSharp functions provided by Death + /// Socket. /// - [] - let applySkiaRGBGridAsync (spec: SkiaRGBSpec) = - async { - try - validateFilePath spec.originalPath |> ignore - validateSaveFileType spec.savePath |> ignore - drawSkiaRGBGrid spec - with - | :? FileNotFoundException as ex -> - printfn "File could not be found at %s" ex.Message - } \ No newline at end of file + let determineSKVerticalLines (width: int) (height: int) (columns: int) = + createSKVerticalLines width height columns \ No newline at end of file diff --git a/DeathSocket/ImagePrep.fs b/DeathSocket/ImagePrep.fs new file mode 100644 index 0000000..25ec5f0 --- /dev/null +++ b/DeathSocket/ImagePrep.fs @@ -0,0 +1,70 @@ +module internal ImagePrep + + open System.IO + open DeathSocket + open SkiaSharp + open System.Drawing + + let determineLongestDimension pDims aDims = + let width = (fst pDims) - (fst aDims) + let height = (snd pDims) - (snd aDims) + match width > height with + | true -> Width + | false -> Height + + let determineImageScale pDimension aDimension = + (double aDimension) / (double pDimension) + + let determineLineScale (lineWidth: double) (scale: double) = + lineWidth / scale + + let adjustLineThickness pDimensions aDimensions lineWidth = + match (determineLongestDimension pDimensions aDimensions) with + | Width -> + let thickness = + determineImageScale (fst pDimensions) (fst aDimensions) + |> determineLineScale lineWidth + thickness + | Height -> + let thickness = + determineImageScale (snd pDimensions) (snd aDimensions) + |> determineLineScale lineWidth + thickness + + (* SkiaSharp Functions + ======================================================================== *) + let createSKHorizontalLines width height rows = + let interval = float32 (height / rows) + [|for point in 1 .. (rows - 1) -> + [| SKPoint ((float32 0), (interval * (float32 point))) + SKPoint ((float32 width), (interval * (float32 point)))|]|] + + let createSKVerticalLines width height columns = + let interval = float32 (width / columns) + [| for point in 1 .. (columns - 1) -> + [| SKPoint ((interval * (float32 point)), (float32 0)) + SKPoint ((interval * (float32 point)), (float32 height))|]|] + + let determineSkiaDimensions filePath = + use fileStream = File.Open (filePath, FileMode.Open) + use skStream = new SKManagedStream (fileStream) + use bitmap = SKBitmap.Decode (skStream) + (bitmap.Width, bitmap.Height) + + (* System.Drawing Functions + ======================================================================== *) + let createHorizontalLines width height rows = + let interval = height / rows + [| for point in 1 .. (rows - 1) -> + [|Point (0, (interval * point)) + Point (width, (interval * point) )|]|] + + let createVerticalLines width height columns = + let interval = width / columns + [| for point in 1 .. (columns - 1) -> + [| Point ((interval * point), 0) + Point ((interval * point), height)|]|] + + let determineSystemDrawingDimensions filePath = + use bitmap = Bitmap.FromFile filePath + (bitmap.Width, bitmap.Height) \ No newline at end of file diff --git a/DeathSocket/ImageServices.fs b/DeathSocket/ImageServices.fs index c68b950..9949829 100644 --- a/DeathSocket/ImageServices.fs +++ b/DeathSocket/ImageServices.fs @@ -5,23 +5,25 @@ open System.Drawing.Imaging open DeathSocket open ColourServices + open ImagePrep open SkiaSharp - // SkiaSharp Functions - // ======================================================================== - - let createSKHorizontalLines width height rows = - let interval = float32 (height / rows) - [|for point in 1 .. (rows - 1) -> - [| SKPoint ((float32 0), (interval * (float32 point))) - SKPoint ((float32 width), (interval * (float32 point)))|]|] - - let createSKVerticalLines width height columns = - let interval = float32 (width / columns) - [| for point in 1 .. (columns - 1) -> - [| SKPoint ((interval * (float32 point)), (float32 0)) - SKPoint ((interval * (float32 point)), (float32 height))|]|] - + (* Note on 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 grind their computer to a halt. On top of + that, they have no means to make alterations to the code. *) + + (* SkiaSharp Functions + ======================================================================== *) + + // Does not work when using SkiaSharp NuGet v. 1.68 let drawSkiaGrid (spec: SkiaSpec) = use fileStream = File.Open (spec.originalPath, FileMode.Open) use skStream = new SKManagedStream (fileStream) @@ -60,6 +62,7 @@ use saveStream = File.OpenWrite (spec.savePath) data.SaveTo (saveStream) + // Does not work when using SkiaSharp NuGet v. 1.68 let drawSkiaRGBGrid spec = use fileStream = File.Open (spec.originalPath, FileMode.Open) use skStream = new SKManagedStream (fileStream) @@ -99,31 +102,100 @@ 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) - let createHorizontalLines width height rows = - let interval = height / rows - [| for point in 1 .. (rows - 1) -> - [|Point (0, (interval * point)) - Point (width, (interval * point) )|]|] + 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 - let createVerticalLines width height columns = - let interval = width / columns - [| for point in 1 .. (columns - 1) -> - [| Point ((interval * point), 0) - Point ((interval * point), height)|]|] + (* System.Drawing Functions + ======================================================================== *) (* 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)) @@ -138,7 +210,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)) diff --git a/DeathSocket/ScratchPad.fsx b/DeathSocket/ScratchPad.fsx index c719984..399af83 100644 --- a/DeathSocket/ScratchPad.fsx +++ b/DeathSocket/ScratchPad.fsx @@ -1,26 +1,29 @@ -// These two paths need adjusting to match your computer. +// These paths need adjusting to match your computer. #r @"C:/Users/craig/.nuget/packages/skiasharp/1.60.3/lib/netstandard1.3/SkiaSharp.dll" -#r @"C:/Users/craig/.nuget/packages/skiasharp/1.60.3/lib/net45/SkiaSharp.dll" +// Currently not working... +//#r @"C:/Users/craig/.nuget/packages/skiasharp/1.60.3/runtimes/win-x64/native/libSkiaSharp.dll" #load "Domain.fs" -#load "Validation.fs" +#load "ValidationServices.fs" #load "ColourServices.fs" +#load "ImagePrep.fs" #load "ImageServices.fs" #load "GridPainter.fs" -open System.Drawing open System -open DeathSocket -open Validation -open ImageServices +open System.Drawing open SkiaSharp +open ValidationServices +open ImagePrep +open ImageServices +open DeathSocket (* Death Socket Scripts =============================================================================== The code in here can be use to create a gridded image without running the CLI. You can, also, run checks to see what Death Socket is doing when creating -a nerw image. *) +a new image. *) let desktop = Environment.GetFolderPath (Environment.SpecialFolder.Desktop) @@ -40,6 +43,14 @@ let skVerticalLines = createSKVerticalLines 120 450 22 (* You will need to provide the image and specify its load/save location. Death Socket assumes either JPEG or PNG files.*) +let dimensions = + //SkiaSharp (desktop + "/test.jpg") + SystemDrawing (desktop + "/test.jpg") + |> GridPainter.determineImageDimensions + +// Change the line thickness (the last parameter) to whatever you want. +let scaledPen = GridPainter.scaleLineThickness (100, 100) dimensions 8.0 + // Brush Specification (uses System.Drawing) Brush ({ originalPath = desktop + "/test.jpg" savePath = desktop + "/grid.png" diff --git a/DeathSocket/Validation.fs b/DeathSocket/Validation.fs deleted file mode 100644 index edc208a..0000000 --- a/DeathSocket/Validation.fs +++ /dev/null @@ -1,20 +0,0 @@ -module internal Validation - - open System.IO - - let validateFilePath path = - match File.Exists path with - | true -> () - | false -> raise (new FileNotFoundException (path + " could not be found.")) - - let validateSaveFileType file = - match Path.GetExtension file with - | ".jpg" -> () - | ".JPG" -> () - | ".png" -> () - | ".PNG" -> () - | _ -> invalidArg "savePath" "The file type must be a .jpg or .png file." - - let validateIO iPath oPath = - validateFilePath iPath - validateSaveFileType oPath \ No newline at end of file diff --git a/DeathSocket/ValidationServices.fs b/DeathSocket/ValidationServices.fs new file mode 100644 index 0000000..a2f3edc --- /dev/null +++ b/DeathSocket/ValidationServices.fs @@ -0,0 +1,31 @@ +module internal ValidationServices + + open System.IO + + let validateFilePath path = + match File.Exists path with + | true -> () + | false -> raise (new FileNotFoundException ("No file found at " + path)) + + let validateSaveFileType file = + match Path.GetExtension file with + | ".jpg" -> () + | ".JPG" -> () + | ".png" -> () + | ".PNG" -> () + | _ -> invalidArg "savePath" "The file type must be a .jpg or .png file." + + let validateIO iPath oPath = + validateFilePath iPath + validateSaveFileType oPath + + let validateDimensions dimensions = + match dimensions with + | (0, _) -> invalidArg "Width" "Width must be greater than 0." + | (_, 0) -> invalidArg "Height" "Height must be greater than 0." + | (_, _) -> () + + let validateLineThickness thickness = + match thickness with + | thickness when thickness <= 0.0 -> invalidArg "LineThickness" "LineThickness must be greater than 0." + | _ -> () \ No newline at end of file diff --git a/TestCentre/AssemblyInfo.fs b/TestCentre/AssemblyInfo.fs index 301bac2..d033100 100644 --- a/TestCentre/AssemblyInfo.fs +++ b/TestCentre/AssemblyInfo.fs @@ -34,8 +34,8 @@ open System.Runtime.InteropServices // You can specify all the values or you can default the Build and Revision Numbers // by using the '*' as shown below: // [] -[] -[] +[] +[] do () \ No newline at end of file diff --git a/TestCentre/LibraryTests.fs b/TestCentre/LibraryTests.fs index 8e37eb1..17abe0c 100644 --- a/TestCentre/LibraryTests.fs +++ b/TestCentre/LibraryTests.fs @@ -34,6 +34,18 @@ let loadLocation = __SOURCE_DIRECTORY__ + "/LoadingTestArea" let saveLocation = __SOURCE_DIRECTORY__ + "/SavingTestArea" + let validSkiaImagePath = + (loadLocation + "/RequiredInfo/Skia-599x336.png") + let validSystemDrawingImagePath = + (loadLocation + "/RequiredInfo/SystemDrawing-338x307.png") + let validImagePath = + (loadLocation + "/RequiredInfo/ImageTest1.png") + let validImagePath2 = + (loadLocation + "/RequiredInfo/ImageTest2.png") + let validImagePath3 = + (loadLocation + "/RequiredInfo/ImageTest3.png") + let invalidImagePath = "invalid.png" + let allColours = let properties = typeof.GetProperties @@ -99,12 +111,12 @@ module PropertyTests = - open FsCheck.Xunit - open DeathSocket + open System.IO open System.Drawing + open DeathSocket open DeathSocket.GridPainter open TestingHelpers - open System.IO + open FsCheck.Xunit (* With regards to the "saving images" tests, you should end up with one image left over in the SavingTestArea folder. Comment out the @@ -175,6 +187,7 @@ let result = determineVerticalLines (newNum()) (newNum()) (newNum()) result.Length > 0 + // This test fails when using SkiaSharp 1.68 [] let ``Can apply grid to image and save it using SkiaSpec`` () = resetSavingTestArea() @@ -190,6 +203,7 @@ |> Async.RunSynchronously (File.Exists sPath) = true + // This test fails when using SkiaSharp 1.68 [] let ``Can apply grid to image and save it using SkiaRGBSpec`` () = resetSavingTestArea() @@ -217,17 +231,188 @@ let result = determineSKVerticalLines (newNum()) (newNum()) (newNum()) result.Length > 0 + [] + let ``Can create a populated SKData buffer when calling createSKDataAsync with SkiaBuffer`` () = + let result () = + SkiaBuffer ({ filePath = validImagePath + skColour = randomSKColour () + penWidth = float32 (newPenWidth()) + rows = newNum () + columns = newNum () }) + |> createSKDataAsync + |> Async.RunSynchronously + (result ()).IsEmpty = false + + [] + let ``Can create a populated SKData buffer when calling createSKDataAsync with SkiaRGBBuffer`` () = + let result () = + SkiaRGBBuffer ({ filePath = validImagePath3 + red = float32 (newNum()) + green = float32 (newNum()) + blue = float32 (newNum()) + penWidth = float32 (newPenWidth()) + rows = newNum () + columns = newNum () }) + |> createSKDataAsync + |> Async.RunSynchronously + (result ()).IsEmpty = false + module UnitTests = - open TestingHelpers - open Xunit - open DeathSocket open System open System.IO + open System.Drawing + open Xunit + open DeathSocket + open SkiaSharp + open TestingHelpers + + [] + let ``An empty SKData object is returned when createSKDataAsync cannot find file when using SkiaBuffer`` () = + let result = + SkiaBuffer ({ filePath = invalidImagePath + penWidth = float32 1 + skColour = SKColors.Empty + rows = 1 + columns = 1 }) + |> GridPainter.createSKDataAsync + |> Async.RunSynchronously + Assert.Equal(SKData.Empty, result) + + [] + let ``An empty SKData object is returned when createSKDataAsync cannot find file when using SkiaRGBBuffer`` () = + let result = + SkiaRGBBuffer ({ filePath = invalidImagePath + penWidth = float32 1 + red = float32 1 + green = float32 1 + blue = float32 1 + rows = 1 + columns = 1 }) + |> GridPainter.createSKDataAsync + |> Async.RunSynchronously + Assert.Equal(SKData.Empty, result) + + [] + let ``An empty SKData object is returned when calling createSKDataAsync with SkiaRGB`` () = + let result = + SkiaRGB ({ originalPath = validImagePath2 + savePath= validImagePath + red = float32 1 + green = float32 1 + blue = float32 1 + penWidth = float32 1 + rows = 1 + columns = 1}) + |> GridPainter.createSKDataAsync + |> Async.RunSynchronously + Assert.Equal(SKData.Empty, result) + + [] + let ``An empty SKData object is returned when calling createSKDataAsync with Skia`` () = + let result = + Skia ({ originalPath = validImagePath3 + savePath= validImagePath + skColour = SKColors.Empty + penWidth = float32 1 + rows = 1 + columns = 1 }) + |> GridPainter.createSKDataAsync + |> Async.RunSynchronously + Assert.Equal(SKData.Empty, result) + + [] + let ``An empty SKData object is returned when calling createSKDataAsync with Brush`` () = + let result = + Brush ({ originalPath = validImagePath + savePath= validImagePath + colour = Brushes.AliceBlue + penWidth = float32 1 + rows = 1 + columns = 1 }) + |> GridPainter.createSKDataAsync + |> Async.RunSynchronously + Assert.Equal(SKData.Empty, result) + + [] + let ``An empty SKData object is returned when calling createSKDataAsync with RGBA`` () = + let result = + RGBA ({ originalPath = validImagePath + savePath= validImagePath + red = float 1 + green = float 1 + blue = float 1 + alpha = float 1 + penWidth = float32 1 + rows = 1 + columns = 1 }) + |> GridPainter.createSKDataAsync + |> Async.RunSynchronously + Assert.Equal(SKData.Empty, result) + + [] + let ``Argument Exception is thrown when actualDimensions are set to 0 when calling scaleLineThickness`` () = + let result () = + GridPainter.scaleLineThickness (1, 0) (0, 0) 1.0 + Assert.Throws(fun () -> result () |> ignore) + + [] + let ``Argument Exception is thrown when lineThickness is set to 0 when calling scaleLineThickness`` () = + let result () = + GridPainter.scaleLineThickness (1, 0)(1, 0) 0.0 + Assert.Throws(fun () -> result () |> ignore) + + [] + let ``Argument Exception is thrown when previewDimensions are set to 0 when calling scaleLineThickness`` () = + let result () = + GridPainter.scaleLineThickness (0, 0) (1, 0) 1.0 + Assert.Throws(fun () -> result () |> ignore) + + [] + let ``Can determine image width using SkiaSharp`` () = + let dimensions = + SkiaSharp (validSkiaImagePath) + |> GridPainter.determineImageDimensions + let result () = (fst dimensions) = 599 + Assert.True (result ()) + + [] + let ``Can determine image height using SkiaSharp`` () = + let dimensions = + SkiaSharp (validSkiaImagePath) + |> GridPainter.determineImageDimensions + let result () = (snd dimensions) = 336 + Assert.True (result ()) + + [] + let ``Can determine image width using SystemDrawing`` () = + let dimensions = + SystemDrawing (validSystemDrawingImagePath) + |> GridPainter.determineImageDimensions + let result () = (fst dimensions) = 338 + Assert.True (result ()) + + [] + let ``Can determine image height using SystemDrawing`` () = + let dimensions = + SystemDrawing (validSystemDrawingImagePath) + |> GridPainter.determineImageDimensions + let result () = (snd dimensions) = 307 + Assert.True (result ()) + + [] + let ``Divide By Zero Exception is thrown when 0 rows is used when determining horizontal lines`` () = + let result () = GridPainter.determineHorizontalLines 100 100 0 + Assert.Throws(fun () -> result () |> ignore) + + [] + let ``Divide By Zero Exception is thrown when 0 columns is used when determining vertical lines`` () = + let result () = GridPainter.determineVerticalLines 100 100 0 + Assert.Throws(fun () -> result () |> ignore) (* This test is a pre-test test (a test for the tests if you will...). It is here to make sure the property test has what it needs to run. - If the property test fails, here is a good place to start. + If the property tests fails, here is a good place to start. See script.fs (in Test Centre) for information on populating the LoadingTestArea folder. *) [] @@ -236,6 +421,24 @@ let imagesAreThere = if length < 100 then false else true Assert.True imagesAreThere + [] + let ``Pen thickness scales down to match preview image dimensions`` () = + let result () = + GridPainter.scaleLineThickness (100, 100) (200, 200) 10.0 + Assert.Equal (5.0, (result ())) + + [] + let ``Pen thickness scales up to match preview image dimensions`` () = + let result () = + GridPainter.scaleLineThickness (200, 200) (100, 100) 10.0 + Assert.Equal (20.0, (result ())) + + [] + let ``Pen thickness remains the same when preview image matches actual dimensions`` () = + let result () = + GridPainter.scaleLineThickness (100, 100) (100, 100) 10.0 + Assert.Equal (10.0, (result ())) + (* This test is a pre-test test. If the property tests fails, here is a good place to start. The easiest way to get this test to pass is to create a folder called "SavingTestArea" in this projects folder @@ -244,14 +447,4 @@ for extra context. *) [] let ``SavingTestArea folder can be found`` () = - Assert.True (Directory.Exists saveLocation) - - [] - let ``Divide By Zero Exception is thrown when 0 rows is used when determining horizontal lines`` () = - let result () = GridPainter.determineHorizontalLines 100 100 0 - Assert.Throws(fun () -> result () |> ignore) - - [] - let ``Divide By Zero Exception is thrown when 0 columns is used when determining vertical lines`` () = - let result () = GridPainter.determineVerticalLines 100 100 0 - Assert.Throws(fun () -> result () |> ignore) \ No newline at end of file + Assert.True (Directory.Exists saveLocation) \ No newline at end of file diff --git a/TestCentre/LoadingTestArea/RequiredInfo/ImageTest1.png b/TestCentre/LoadingTestArea/RequiredInfo/ImageTest1.png new file mode 100644 index 0000000..dc71c54 Binary files /dev/null and b/TestCentre/LoadingTestArea/RequiredInfo/ImageTest1.png differ diff --git a/TestCentre/LoadingTestArea/RequiredInfo/ImageTest2.png b/TestCentre/LoadingTestArea/RequiredInfo/ImageTest2.png new file mode 100644 index 0000000..dc71c54 Binary files /dev/null and b/TestCentre/LoadingTestArea/RequiredInfo/ImageTest2.png differ diff --git a/TestCentre/LoadingTestArea/RequiredInfo/ImageTest3.png b/TestCentre/LoadingTestArea/RequiredInfo/ImageTest3.png new file mode 100644 index 0000000..dc71c54 Binary files /dev/null and b/TestCentre/LoadingTestArea/RequiredInfo/ImageTest3.png differ diff --git a/TestCentre/LoadingTestArea/RequiredInfo/Skia-599x336.png b/TestCentre/LoadingTestArea/RequiredInfo/Skia-599x336.png new file mode 100644 index 0000000..c737fa0 Binary files /dev/null and b/TestCentre/LoadingTestArea/RequiredInfo/Skia-599x336.png differ diff --git a/TestCentre/LoadingTestArea/RequiredInfo/SystemDrawing-338x307.png b/TestCentre/LoadingTestArea/RequiredInfo/SystemDrawing-338x307.png new file mode 100644 index 0000000..dc71c54 Binary files /dev/null and b/TestCentre/LoadingTestArea/RequiredInfo/SystemDrawing-338x307.png differ