(*.Net Framework Assemblies System.Drawing*) namespace ExpandablePrinter module FsharpPrinting = open System open System.Xml open System.Drawing.Printing open System.Drawing let private Document = new PrintDocument() let private printerSettings = new PrinterSettings() /// Converts a Rectangle to a RectangleF let RectangleToRectangleF(r: Rectangle) = RectangleF(float32(r.X), float32(r.Y), float32(r.Width), float32(r.Height)) /// Converts a RectangleF to a Rectangle let RectangleFtoRectangle(r: RectangleF) = Rectangle(int(r.X), int(r.Y), int(r.Width), int(r.Height)) /// Size of the four margins let Margins = Document.PrinterSettings.DefaultPageSettings.Margins /// RectangleF giving the size of the paper in most cases equal to PageBounds let Bounds = RectangleToRectangleF(Document.PrinterSettings.DefaultPageSettings.Bounds) /// RectangleF giving the bounds of the page including margins. let PageBounds = RectangleF(Bounds.Left, Bounds.Top, Bounds.Right - Bounds.Left, Bounds.Bottom - Bounds.Top) /// RectangleF giving the bounds of the page excluding margins equals the size of the page container. let PageContainer = RectangleF(float32(Margins.Left), float32(Margins.Top), Bounds.Right - float32(Margins.Left + Margins.Right), Bounds.Bottom - float32(Margins.Top + Margins.Bottom)) /// Read the string value from n's attribute with the name "name". /// If "name" is not defined Some(Value) is returns or if defaultValue is None an exception is thrown /// If "name" is not defined and defaultValue is None /// XmlNode /// Attribute name /// Option type. None is used when a value has to be specified. let readString(n: XmlNode, name, defaultValue) = let value = (n :?> XmlElement).GetAttribute(name) match defaultValue with | None when value = "" -> failwith($"Attribute {name} in tag <{(n :?> XmlElement).Name}> missing") | None -> value | Some(v) when value = "" -> v | Some(_) -> value /// Visual Basic and Csharp version of readString /// Read the string value from n's attribute with the name "name". /// If "name" is not defined defaultValue is returns or if defaultValue is (null or Nothing) an exception is thrown /// If "name" is not defined and defaultValue is (null or Nothing) /// XmlNode /// Attribute name /// Single. (null or Nothing) is used when a value has to be specified. let readStringVisualBasicCsharp(n: XmlNode, name, defaultValue) = let value = (n :?> XmlElement).GetAttribute(name) match defaultValue with | null when value = "" -> failwith($"Attribute {name} in tag <{(n :?> XmlElement).Name}> missing") | null -> value | v when value = "" -> v | _ -> value /// Read the string value from string element n let readText(n: XmlNode) = (n :?> XmlElement).InnerText /// Read the float32 (Single) value from n's attribute with the name "name". /// If "name" is not defined Some(Value) is returns or if defaultValue is None an exception is thrown /// If "name" is not defined and defaultValue is None /// If value is not a float number /// XmlNode /// Attribute name /// Option type. None is used when a value has to be specified. let readFloat(n: XmlNode, name, defaultValue) = let value = (n :?> XmlElement).GetAttribute(name) let i = ref 0.0f match (defaultValue, Single.TryParse(value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, i)) with | (_, true) -> !i | (_, false) when value <> "" -> failwith($"Attribute {name} in tag <{(n :?> XmlElement).Name}> not a float number") | (None, false) -> failwith($"Attribute {name} in tag <{(n :?> XmlElement).Name}> not a float number") | (Some(v), false) -> v /// Visual Basic and Csharp version of readFloat /// Read the float32 (Single) value from n's attribute with the name "name". /// If "name" is not defined defaultValue is returns or if defaultValue is (null or Nothing) an exception is thrown /// If "name" is not defined and defaultValue is (null or Nothing) /// If value is not a float number /// XmlNode /// Attribute name /// (null or Nothing) is used when a value has to be specified. let readFloatVisualBasicCsharp(n: XmlNode, name, defaultValue: Nullable) = let value = (n :?> XmlElement).GetAttribute(name) let i = ref 0.0f match (defaultValue, Single.TryParse(value, System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, i)) with | (_, true) -> !i | (_, false) when value <> "" -> failwith($"Attribute {name} in tag <{(n :?> XmlElement).Name}> not a float number") | (v, false) when v = System.Nullable() -> failwith($"Attribute {name} in tag <{(n :?> XmlElement).Name}> not a float number") | (v, false) -> v.Value /// Read the Font from n's attribute with the name "Font", "Size" and "Style". let readFont(n: XmlNode, f: Font) = new Font(readString(n, "Font", Some(f.Name)), readFloat(n, "Size", Some(f.Size)), Enum.Parse(typeof, readString(n, "Style", Some("Regular"))) :?> FontStyle) /// Read the PointF from n's attribute with the names "Tab" and "VerticalTab". let readTab(n: XmlNode, p: PointF) = PointF(readFloat(n, "Tab", Some(p.X)), readFloat(n, "VerticalTab", Some(p.Y))) /// Read the Color from n's attribute with the name "Colour". let readColour(n: XmlNode) = Color.FromName(readString(n, "Colour", Some("Black"))) /// Read the PointF from n's attribute with the names "X" and "Y". let read1PointF(n: XmlNode, xOffset, yOffset) = PointF(readFloat(n, "X", None) + xOffset, readFloat(n, "Y", None) + yOffset) /// Read a second PointF from n's attribute with the names "X2" and "Y2". let read2PointF(n: XmlNode, xOffset, yOffset) = PointF(readFloat(n, "X2", None) + xOffset, readFloat(n, "Y2", None) + yOffset) /// Read a readRRectangleF from n's attribute with the names "X", "Y", "Width" and "Height". let readRRectangleF(n: XmlNode, xOffset, yOffset) = let p = read1PointF(n, 0.0f, 0.0f) RectangleF(p.X + xOffset, p.Y + yOffset, readFloat(n, "Width", None), readFloat(n, "Height", None) ) /// Read a font size from n's attribute with the name "Size" and return Font f with this new size. let setFontSize(n : XmlNode, f : Font) = new Font(f.Name, readFloat(n, "Size", Some(10.0f))) let offsetRectangleF(r: RectangleF, x, y, w, h) = RectangleF(r.X + x, r.Y + y, r.Width + w, r.Height + h) let offsetRectangle(r: RectangleF, x, y, w, h) = Rectangle(int(r.X + x), int(r.Y + y), int(r.Width + w), int(r.Height + h)) let private PahragraphPrint(g: Graphics, container : RectangleF, p : PointF, f: Font, flag: StringFormatFlags, n: XmlNode) = if n.Name <> "Format" then failwith($"Wrong tag <{n.Name}> after , and has to be ") let fStyle = Enum.Parse(typeof, readString(n, "Style", Some("Regular"))) :?> FontStyle use font = readFont(n, f) let tabs = readTab(n, p) let drawRect = offsetRectangleF(container, tabs.X, tabs.Y, -tabs.X, -tabs.Y) let remainingSpace = SizeF(container.Width - tabs.X, container.Height - tabs.Y) let paragraphs = readText(n) let sizeParagraphs = g.MeasureString(paragraphs, font, remainingSpace, new StringFormat(flag)) if remainingSpace.Width < sizeParagraphs.Width || remainingSpace.Height < sizeParagraphs.Height then failwith($"Text in out of container") g.DrawString(paragraphs, font, new SolidBrush(readColour(n)), drawRect, new StringFormat(flag)) sizeParagraphs let rec private runContainers(g: Graphics, printFont: Font, n:XmlNode, functions: Object, container: RectangleF) = let mutable yCurrent = 0.0f let graphicalElements = n.ChildNodes for e in graphicalElements do match e.Name with | "Container" -> let r = readRRectangleF(e, container.X, container.Y) let eWidth = readFloat(e, "Draw", Some(0.0f)) if eWidth > 0.0f then g.DrawRectangle( new Pen(readColour(e), eWidth), offsetRectangle(r, eWidth / 2.0f, eWidth / 2.0f, -eWidth, -eWidth)) let r2 = if eWidth > 0.0f then offsetRectangleF(r, eWidth, eWidth, -eWidth * 2.0f, -eWidth * 2.0f) else r runContainers(g, printFont, e, functions, r2) | "Line" -> let mutable xCurrent = 0.0f let mutable usedHeight = 0.0f for item in e.ChildNodes do let usedSize = PahragraphPrint(g, container, PointF(xCurrent, yCurrent), printFont, StringFormatFlags.NoWrap, item) xCurrent <- xCurrent + usedSize.Width usedHeight <- float32(Math.Max(usedHeight, usedSize.Height)) yCurrent <- yCurrent + usedHeight | "FreeLine" -> let mutable xCurrent = 0.0f for item in e.ChildNodes do xCurrent <- xCurrent + PahragraphPrint(g, Bounds, PointF(xCurrent, 0.0f), printFont, StringFormatFlags.NoWrap, item).Width | "Paragraphs" -> yCurrent <- yCurrent + PahragraphPrint(g, container, PointF(0.0f, yCurrent), setFontSize(e, printFont), StringFormatFlags.NoClip, e.FirstChild).Height | "Point" -> let width = readFloat(e, "Width", Some(1.0f)) let p = read1PointF(e, container.X - width / 2.0f, container.Y - width / 2.0f) g.FillEllipse(new SolidBrush(readColour(e)), RectangleF(p.X , p.Y, width, width)) | "SolidLine" -> g.DrawLine(new Pen(readColour(e), readFloat(e, "Width", Some(2.0f))), read1PointF(e, container.X, container.Y), read2PointF(e, container.X, container.Y)) | "Function" -> if isNull functions then failwith($"Tag name detected, but functions is (null or Nothing)>") let qqq = functions.GetType().GetMethods() let MetodInf = functions.GetType().GetMethod(readString(e, "Name", None)) MetodInf.Invoke(functions, [|g; container; e.Attributes|]) |> ignore | "FunctionXML" -> if isNull functions then failwith($"Tag name detected, but functions is(null or Nothing)>") let MetodInf = functions.GetType().GetMethod(readString(e, "Name", None)) MetodInf.Invoke(functions, [|g; container; e.InnerXml|]) |> ignore | "#comment" -> () | _ -> failwith($"Ilegal tag name in <{e.Name}>") let mutable paragraphCount = 0 let mutable paragraphs = Array.empty let printPages(g: Graphics, font: Font, container: RectangleF, text: string)= if paragraphCount = 0 then paragraphs <- text.Replace("\n", "").Split([|'\r'|], StringSplitOptions.None) let textFit: string = "" let rec addLine (s: string, l : string, index : int) = let textArea = SizeF(container.Width, Single.MaxValue) let sPlus = s + l let z = g.MeasureString(sPlus, font, textArea, new StringFormat(StringFormatFlags.NoClip)) match (z.Height > container.Height, index < paragraphs.Length - 1) with | (false, true) -> addLine(sPlus + "\r\n", paragraphs.[index + 1], index + 1) | (false, false) -> (index, sPlus) | (true, _) -> (index - 1, s) let index, s = addLine(textFit, paragraphs.[paragraphCount], paragraphCount) paragraphCount <- index + 1 g.DrawString(s, font, new SolidBrush(Color.Black), container, new StringFormat(StringFormatFlags.NoClip)) paragraphCount < paragraphs.Length let mutable private pageCount = 0 let private documentPrintPage2 (xmlDoc: XmlDocument, functions: Object) (sender: Object) (ev: PrintPageEventArgs) = let leftMargin = ev.MarginBounds.Left |> float32 let rightMargin = ev.MarginBounds.Right |> float32 let totalWidth = rightMargin - leftMargin |> float32 let topMargin = ev.MarginBounds.Top |> float32 let bottomMargin = ev.MarginBounds.Bottom |> float32 let totalHight = bottomMargin - topMargin |> float32 let print = xmlDoc.FirstChild.NextSibling if print.Name <> "Print" then failwith("Root tag has to be ") let printFont = readFont(print, new Font("Areal", 10.0f)) let pages = print.ChildNodes match pages.[pageCount].Name with | "Page" -> runContainers(ev.Graphics, printFont, pages.[pageCount], functions, RectangleF(leftMargin, topMargin, totalWidth, totalHight)) pageCount <- pageCount + 1 if pageCount < pages.Count then ev.HasMorePages <- true else ev.HasMorePages <- false | "MultiplePages" -> let f = readFont(pages.[pageCount].FirstChild, printFont) let text = readText(pages.[pageCount].FirstChild) let ended = not(printPages(ev.Graphics, f, RectangleF(leftMargin, topMargin, totalWidth, totalHight), text)) if ended then paragraphCount <- 0; pageCount <- pageCount + 1 ev.HasMorePages <- not ended || pageCount < pages.Count | _ -> failwith("After root tag the children tags has to be og ") let private printing2(source: string, functions: Object) = Document.PrinterSettings <- printerSettings let XMLdoc = new XmlDocument() XMLdoc.LoadXml(source) let documentPrintPage = documentPrintPage2(XMLdoc, functions) let printPageEventHandler = new PrintPageEventHandler(documentPrintPage) Document.PrintPage.AddHandler(printPageEventHandler) Document.Print() Document.PrintPage.RemoveHandler(printPageEventHandler) pageCount <- 0; paragraphCount <- 0 /// Start printing the XML document source to the file /// functions can be (null or Nothing) if no special printing functions is used /// functions is a referance to an object O with the special printing functions called by either XML tag /// <Functions Name = "O method name" attr1 = "Value1" attr2 = "Value2" ... attrN = "ValueN"/> or /// <Function2 Name = "O method name" /> /// inner XML tags /// </Function2> /// /// Tag after <Paragraphs> has to be <Format><Format> /// Text starts outside the container /// Function is not existing /// Unknown tag name in container /// Wrong root tag /// description /// XML document defining the print /// object O with the special printing functions /// full path to *.pdf output file let printingPDF(source: string, functions: Object, file) = printerSettings.PrinterName <- "Microsoft Print to PDF" printerSettings.PrintToFile <- true Document.PrinterSettings <- printerSettings printerSettings.PrintFileName <- file printing2(source, functions) /// Start printing the XML document source /// functions can be (null or Nothing) if no special printing functions is used /// functions is a referance to an object O with the special printing functions called by either XML tag /// <Functions Name = "O method name" attr1 = "Value1" attr2 = "Value2" ... attrN = "ValueN"/> or /// <Function2 Name = "O method name" /> /// inner XML tags /// </Function2> /// /// Tag after <Paragraphs> has to be <Format><Format> /// Text starts outside the container /// Function is not existing /// Unknown tag name in container /// Wrong root tag /// description /// XML document defining the print /// object O with the special printing functions /// Selected printers name sting let printingPaper(source: string, functions: Object, printerName) = printerSettings.PrinterName <- printerName printing2(source, functions) /// Split text to multiple containers /// text to split. Keep paragraphs together. /// Array of containers /// Font used in all containers let split(text: string, containers: RectangleF[], font: Font) = let mutable textFitContainers: string[] = Array.empty let splitStringToFit (g: Graphics, font: Font, containers: RectangleF[], text: string) = let paragraphs = text.Split([|'\r'|], StringSplitOptions.None) |> Array.map (fun (item: string) -> item.Replace("\n", "")) let textFit: string[] = Array.zeroCreate containers.Length let rec addLine (s: string, l : string, index : int, contanier : int) = let textArea = SizeF(containers.[contanier].Width, Single.MaxValue) let sPlus = s + l let z = g.MeasureString(sPlus, font, textArea, new StringFormat(StringFormatFlags.NoClip)) match (z.Height > containers.[contanier].Height, index < paragraphs.Length - 1) with | (false, true) -> addLine(sPlus + "\r\n", paragraphs.[index + 1], index + 1, contanier) | (false, false) -> (index, sPlus) | (true, _) -> (index - 1, s) let mutable j = 0 for i in 0 .. containers.Length - 1 do if j < paragraphs.Length - 1 then let index, s = addLine(textFit.[i], paragraphs.[j], j, i) textFit.[i] <- s j <- index + 1 textFit let documentPrintPage (sender: Object) (ev: PrintPageEventArgs) = textFitContainers <- splitStringToFit(ev.Graphics, font, containers, text) ev.HasMorePages <- false // Find ev.Graphics to calculate text size let Document = new PrintDocument() let printerSettings = new PrinterSettings() printerSettings.PrinterName <- "Microsoft Print to PDF" printerSettings.PrintToFile <- true printerSettings.PrintFileName <- __SOURCE_DIRECTORY__ + @"\test.pdf" Document.PrinterSettings <- printerSettings let printPageEventHandler = new PrintPageEventHandler(documentPrintPage) Document.PrintPage.AddHandler(printPageEventHandler) Document.Print() Document.PrintPage.RemoveHandler(printPageEventHandler) textFitContainers