From a3fb09440d941ce525442764fc29805027aaf228 Mon Sep 17 00:00:00 2001 From: Gary Sharp Date: Thu, 12 Oct 2023 15:51:33 +1100 Subject: [PATCH] feature: document template QR Code image extensions --- .../Extensions/DocumentTemplateExtensions.cs | 1 - Disco.BI/BI/Interop/Pdf/PdfGenerator.cs | 26 +++- Disco.BI/BI/Interop/Pdf/Utilities.cs | 7 + Disco.Models/Disco.Models.csproj | 3 +- .../Extensions}/IImageExpressionResult.cs | 8 +- .../Extensions/ImageExpressionFormat.cs | 9 ++ Disco.Services/Disco.Services.csproj | 1 + .../Documents/AttachmentImport/ImportPage.cs | 38 +++++- .../Documents/QRCodeBinaryEncoder.cs | 47 +++++-- Disco.Services/Expressions/Expression.cs | 2 +- .../Expressions/Extensions/ImageExt.cs | 17 ++- .../BaseImageExpressionResult.cs | 36 ++++-- .../BitmapImageExpressionResult.cs | 13 +- .../FileImageExpressionResult.cs | 16 ++- .../FileMontageImageExpressionResult.cs | 17 +-- .../QrCodeImageExpressionResult.cs | 122 ++++++++++++++++++ Disco.Web/App_Start/AppConfig.cs | 1 + 17 files changed, 299 insertions(+), 65 deletions(-) rename Disco.Models/{BI/Expressions => Services/Expressions/Extensions}/IImageExpressionResult.cs (53%) create mode 100644 Disco.Models/Services/Expressions/Extensions/ImageExpressionFormat.cs create mode 100644 Disco.Services/Expressions/Extensions/ImageResultImplementations/QrCodeImageExpressionResult.cs diff --git a/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs b/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs index b7c4dda1..ab338651 100644 --- a/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs +++ b/Disco.BI/BI/Extensions/DocumentTemplateExtensions.cs @@ -9,7 +9,6 @@ using Disco.Services.Interop.ActiveDirectory; using iTextSharp.text.pdf; using System; using System.Collections.Generic; -using System.Data.Entity; using System.Drawing; using System.IO; using System.Linq; diff --git a/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs b/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs index 6ed71c57..80bf7e06 100644 --- a/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs +++ b/Disco.BI/BI/Interop/Pdf/PdfGenerator.cs @@ -1,8 +1,8 @@ using Disco.BI.Extensions; using Disco.Data.Repository; -using Disco.Models.BI.Expressions; using Disco.Models.Repository; using Disco.Models.Services.Documents; +using Disco.Models.Services.Expressions.Extensions; using Disco.Services; using Disco.Services.Documents; using Disco.Services.Expressions; @@ -270,14 +270,15 @@ namespace Disco.BI.Interop.Pdf // Encode to QRCode byte array var pageUniqueIdWidth = (int)pdfFieldPosition.position.Width; var pageUniqueIdHeight = (int)pdfFieldPosition.position.Height; - var pageUniqueIdEncoded = QRCodeBinaryEncoder.Encode(pageUniqueIdBytes, pageUniqueIdWidth, pageUniqueIdHeight); + var pageUniqueIdEncoded = QRCodeBinaryEncoder.Encode(pageUniqueIdBytes, out var qrWidth, out var qrHeight); // Encode byte array to Image - var pageUniqueIdImageData = CCITTG4Encoder.Compress(pageUniqueIdEncoded, pageUniqueIdWidth, pageUniqueIdHeight); - var pageUniqueIdImage = iTextSharp.text.Image.GetInstance(pageUniqueIdWidth, pageUniqueIdHeight, false, 256, 1, pageUniqueIdImageData, null); + var pageUniqueIdImageData = CCITTG4Encoder.Compress(pageUniqueIdEncoded, qrWidth, qrHeight); + var pageUniqueIdImage = iTextSharp.text.Image.GetInstance(qrWidth, qrHeight, false, 256, 1, pageUniqueIdImageData, null); // Add to the pdf page pageUniqueIdImage.SetAbsolutePosition(pdfFieldPosition.position.Left, pdfFieldPosition.position.Bottom); + pageUniqueIdImage.ScaleToFit(pdfFieldPosition.position.Width, pdfFieldPosition.position.Height); pdfStamper.GetOverContent(pdfFieldPosition.page).AddImage(pageUniqueIdImage); } // Hide Fields @@ -315,7 +316,22 @@ namespace Disco.BI.Interop.Pdf for (int pdfFieldOrdinal = 0; pdfFieldOrdinal < fields.Size; pdfFieldOrdinal++) { AcroFields.FieldPosition pdfFieldPosition = pdfFieldPositions[pdfFieldOrdinal]; - iTextSharp.text.Image pdfImage = iTextSharp.text.Image.GetInstance(imageResult.GetImage((int)(pdfFieldPosition.position.Width * 1.6), (int)(pdfFieldPosition.position.Height * 1.6))); + + iTextSharp.text.Image pdfImage; + var imageWidth = (int)(pdfFieldPosition.position.Width * 1.6); + var imageHeight = (int)(pdfFieldPosition.position.Height * 1.6); + if (imageResult.Format == ImageExpressionFormat.Jpeg || imageResult.Format == ImageExpressionFormat.Png) + { + pdfImage = iTextSharp.text.Image.GetInstance(imageResult.GetImage(imageWidth, imageHeight)); + } + else if (imageResult.Format == ImageExpressionFormat.CcittG4) + { + var imageData = imageResult.GetImage(out imageWidth, out imageHeight); + pdfImage = iTextSharp.text.Image.GetInstance(imageWidth, imageHeight, false, 256, 1, imageData.GetBuffer(), null); + } + else + throw new NotSupportedException($"Unexpected image format {imageResult.Format}"); + pdfImage.SetAbsolutePosition(pdfFieldPosition.position.Left, pdfFieldPosition.position.Bottom); pdfImage.ScaleToFit(pdfFieldPosition.position.Width, pdfFieldPosition.position.Height); pdfStamper.GetOverContent(pdfFieldPosition.page).AddImage(pdfImage); diff --git a/Disco.BI/BI/Interop/Pdf/Utilities.cs b/Disco.BI/BI/Interop/Pdf/Utilities.cs index 5b144981..06274bf4 100644 --- a/Disco.BI/BI/Interop/Pdf/Utilities.cs +++ b/Disco.BI/BI/Interop/Pdf/Utilities.cs @@ -1,5 +1,7 @@ using iTextSharp.text; using iTextSharp.text.pdf; +using iTextSharp.text.pdf.codec; +using System; using System.Collections.Generic; using System.IO; @@ -7,6 +9,11 @@ namespace Disco.BI.Interop.Pdf { public static class Utilities { + public static Func GetCCITTG4EncoderCompressDelegate() + { + return CCITTG4Encoder.Compress; + } + public static Stream JoinPdfs(bool InsertBlankPages, List Pdfs) { if (Pdfs.Count == 0) diff --git a/Disco.Models/Disco.Models.csproj b/Disco.Models/Disco.Models.csproj index aff8334d..6b904b7a 100644 --- a/Disco.Models/Disco.Models.csproj +++ b/Disco.Models/Disco.Models.csproj @@ -49,6 +49,7 @@ + @@ -86,7 +87,7 @@ - + diff --git a/Disco.Models/BI/Expressions/IImageExpressionResult.cs b/Disco.Models/Services/Expressions/Extensions/IImageExpressionResult.cs similarity index 53% rename from Disco.Models/BI/Expressions/IImageExpressionResult.cs rename to Disco.Models/Services/Expressions/Extensions/IImageExpressionResult.cs index 2fbc6a8e..5b6bbd1d 100644 --- a/Disco.Models/BI/Expressions/IImageExpressionResult.cs +++ b/Disco.Models/Services/Expressions/Extensions/IImageExpressionResult.cs @@ -1,13 +1,13 @@ using System.IO; -namespace Disco.Models.BI.Expressions +namespace Disco.Models.Services.Expressions.Extensions { public interface IImageExpressionResult { - Stream GetImage(int Width, int Height); - Stream GetImage(); + MemoryStream GetImage(int width, int height); + MemoryStream GetImage(out int width, out int height); byte Quality { get; set; } - bool LosslessFormat { get; set; } + ImageExpressionFormat Format { get; set; } bool ShowField { get; set; } string BackgroundColour { get; set; } bool BackgroundPreferTransparent { get; set; } diff --git a/Disco.Models/Services/Expressions/Extensions/ImageExpressionFormat.cs b/Disco.Models/Services/Expressions/Extensions/ImageExpressionFormat.cs new file mode 100644 index 00000000..e2f48ed4 --- /dev/null +++ b/Disco.Models/Services/Expressions/Extensions/ImageExpressionFormat.cs @@ -0,0 +1,9 @@ +namespace Disco.Models.Services.Expressions.Extensions +{ + public enum ImageExpressionFormat + { + Jpeg, + Png, + CcittG4, + } +} diff --git a/Disco.Services/Disco.Services.csproj b/Disco.Services/Disco.Services.csproj index 9cc1052d..8971a704 100644 --- a/Disco.Services/Disco.Services.csproj +++ b/Disco.Services/Disco.Services.csproj @@ -317,6 +317,7 @@ + diff --git a/Disco.Services/Documents/AttachmentImport/ImportPage.cs b/Disco.Services/Documents/AttachmentImport/ImportPage.cs index 53cf8252..5de57b49 100644 --- a/Disco.Services/Documents/AttachmentImport/ImportPage.cs +++ b/Disco.Services/Documents/AttachmentImport/ImportPage.cs @@ -113,18 +113,42 @@ namespace Disco.Services.Documents.AttachmentImport { var qrReader = new QRCodeMultiReader(); - var qrReaderHints = new Dictionary() { - { DecodeHintType.TRY_HARDER, true } - }; - var qrImageSource = new BitmapLuminanceSource((Bitmap)Image); var qrBinarizer = new HybridBinarizer(qrImageSource); var qrBinaryBitmap = new BinaryBitmap(qrBinarizer); + Result DetectDocumentUniqueIdentifier(BinaryBitmap bitmap) + { + var qrReaderHints = new Dictionary() { + { DecodeHintType.TRY_HARDER, true } + }; + + var qrCodeResults = qrReader.decodeMultiple(bitmap, qrReaderHints); + + if (qrCodeResults != null && qrCodeResults.Length > 0) + { + if (qrCodeResults.Length > 1) + { + // multiple qr codes on page, test for byte-mark + foreach (var qr in qrCodeResults) + { + if (qr.ResultMetadata.TryGetValue(ResultMetadataType.BYTE_SEGMENTS, out var byteSegments)) + { + var qrBytes = ((List)byteSegments)[0]; + if (DocumentUniqueIdentifier.IsDocumentUniqueIdentifier(qrBytes)) + return qr; + } + } + } + return qrCodeResults[0]; + } + return null; + } + try { - qrCodeResult = qrReader.decodeMultiple(qrBinaryBitmap, qrReaderHints)?.FirstOrDefault(); + qrCodeResult = DetectDocumentUniqueIdentifier(qrBinaryBitmap); qrCodeResultScale = 1F; } catch (ReaderException) @@ -148,7 +172,7 @@ namespace Disco.Services.Documents.AttachmentImport try { - qrCodeResult = qrReader.decodeMultiple(qrBinaryBitmap, qrReaderHints)?.FirstOrDefault(); + qrCodeResult = DetectDocumentUniqueIdentifier(qrBinaryBitmap); qrCodeResultScale = 1.75F; } catch (ReaderException) @@ -171,7 +195,7 @@ namespace Disco.Services.Documents.AttachmentImport try { - qrCodeResult = qrReader.decodeMultiple(qrBinaryBitmap, qrReaderHints)?.FirstOrDefault(); + qrCodeResult = DetectDocumentUniqueIdentifier(qrBinaryBitmap); qrCodeResultScale = 2F; } catch (ReaderException) diff --git a/Disco.Services/Documents/QRCodeBinaryEncoder.cs b/Disco.Services/Documents/QRCodeBinaryEncoder.cs index 74b663a6..b457682f 100644 --- a/Disco.Services/Documents/QRCodeBinaryEncoder.cs +++ b/Disco.Services/Documents/QRCodeBinaryEncoder.cs @@ -8,8 +8,24 @@ namespace Disco.Services.Documents { public static class QRCodeBinaryEncoder { + public static byte[] Encode(string content, ErrorCorrectionLevel ecLevel, out int width, out int height) + { + var code = Encoder.encode(content, ecLevel, null); - public static byte[] Encode(byte[] Content, int Width, int Height) + var array = code.Matrix.Array; + width = code.Matrix.Width; + height = code.Matrix.Height; + + return scaleMatrix(code.Matrix.Array, width, height); + } + + public static byte[] Encode(string content, ErrorCorrectionLevel ecLevel, int width, int height) + { + var code = Encoder.encode(content, ecLevel, null); + return scaleMatrix(code.Matrix.Array, width, height); + } + + public static byte[] Encode(byte[] content, out int width, out int height) { var ecLevel = ErrorCorrectionLevel.L; var bits = new BitArray(); @@ -17,7 +33,7 @@ namespace Disco.Services.Documents var mode = Mode.BYTE; var bitsNeeded = 4 + mode.getCharacterCountBits(ZXing.QrCode.Internal.Version.getVersionForNumber(1)) + - (Content.Length * 8); + (content.Length * 8); var version = ChooseVersion(bitsNeeded, out ecLevel); var ecBlocks = version.getECBlocksForLevel(ecLevel); @@ -28,12 +44,12 @@ namespace Disco.Services.Documents bits.appendBits(mode.Bits, 4); // Write the number of bytes - bits.appendBits(Content.Length, mode.getCharacterCountBits(version)); + bits.appendBits(content.Length, mode.getCharacterCountBits(version)); // Write the bytes - for (int i = 0; i < Content.Length; i++) + for (int i = 0; i < content.Length; i++) { - bits.appendBits(Content[i], 8); + bits.appendBits(content[i], 8); } // Terminate the bit stream @@ -72,7 +88,10 @@ namespace Disco.Services.Documents MatrixUtil.buildMatrix(finalBits, ecLevel, version, maskPattern, matrix); // Render matrix to bytes - return scaleMatrix(matrix.Array, Width, Height); + width = matrix.Width; + height = matrix.Height; + return scaleMatrix(matrix.Array, width, height); + //return FlattenMatrix(matrix.Array, width, height); } private static ZXing.QrCode.Internal.Version ChooseVersion(int RequiredBits, out ErrorCorrectionLevel ECLevel) @@ -242,17 +261,17 @@ namespace Disco.Services.Documents return bestMaskPattern; } - private static byte[] scaleMatrix(byte[][] matrix, int Width, int Height) + private static byte[] scaleMatrix(byte[][] matrix, int width, int height) { var matrixWidth = matrix[0].Length; var matrixHeight = matrix.Length; - Width = Math.Max(Width, matrixWidth); - Height = Math.Max(Height, matrixHeight); - var byteColumns = (Width + 7) / 8; - var outputBytes = new byte[byteColumns * Height]; - var scale = Math.Min(Width / (matrixWidth + 1), Height / (matrixHeight + 1)); - var offsetX = (Width - (matrixWidth * scale)) / 2; - var offsetY = (Height - (matrixHeight * scale)) / 2; + width = Math.Max(width, matrixWidth); + height = Math.Max(height, matrixHeight); + var byteColumns = (width + 7) / 8; + var outputBytes = new byte[byteColumns * height]; + var scale = Math.Max(1, Math.Min(width / (matrixWidth + 1), height / (matrixHeight + 1))); + var offsetX = (width - (matrixWidth * scale)) / 2; + var offsetY = (height - (matrixHeight * scale)) / 2; // initialize output bytes for (int i = 0; i < outputBytes.Length; i++) { diff --git a/Disco.Services/Expressions/Expression.cs b/Disco.Services/Expressions/Expression.cs index 251a2a64..99c958b1 100644 --- a/Disco.Services/Expressions/Expression.cs +++ b/Disco.Services/Expressions/Expression.cs @@ -1,7 +1,7 @@ using Disco.Data.Repository; -using Disco.Models.BI.Expressions; using Disco.Models.Repository; using Disco.Models.Services.Documents; +using Disco.Models.Services.Expressions.Extensions; using Disco.Services.Plugins.Features.DetailsProvider; using Spring.Core.TypeResolution; using Spring.Expressions; diff --git a/Disco.Services/Expressions/Extensions/ImageExt.cs b/Disco.Services/Expressions/Extensions/ImageExt.cs index a9f5c517..c119ff7f 100644 --- a/Disco.Services/Expressions/Extensions/ImageExt.cs +++ b/Disco.Services/Expressions/Extensions/ImageExt.cs @@ -118,15 +118,28 @@ namespace Disco.Services.Expressions.Extensions } public static BitmapImageExpressionResult OrganisationLogo() { - var configCache = new Disco.Data.Configuration.SystemConfiguration(null); + var configCache = new Data.Configuration.SystemConfiguration(null); BitmapImageExpressionResult result; using (var orgLogo = configCache.OrganisationLogo) { result = ImageFromStream(orgLogo); } - result.LosslessFormat = true; + result.Format = Models.Services.Expressions.Extensions.ImageExpressionFormat.Png; return result; } + public static QrCodeImageExpressionResult QrCode(string content) + { + return new QrCodeImageExpressionResult(content, 'M'); + } + + public static QrCodeImageExpressionResult QrCode(string content, string errorCorrectionLevel) + { + if (string.IsNullOrWhiteSpace(errorCorrectionLevel)) + errorCorrectionLevel = "M"; + + return new QrCodeImageExpressionResult(content, errorCorrectionLevel[0]); + } + } } diff --git a/Disco.Services/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs b/Disco.Services/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs index 0d35aa17..0465205c 100644 --- a/Disco.Services/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs +++ b/Disco.Services/Expressions/Extensions/ImageResultImplementations/BaseImageExpressionResult.cs @@ -1,4 +1,4 @@ -using Disco.Models.BI.Expressions; +using Disco.Models.Services.Expressions.Extensions; using System; using System.Drawing; using System.IO; @@ -8,33 +8,35 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations public abstract class BaseImageExpressionResult : IImageExpressionResult { public byte Quality { get; set; } - public bool LosslessFormat { get; set; } + public ImageExpressionFormat Format { get; set; } public bool ShowField { get; set; } public string BackgroundColour { get; set; } public bool BackgroundPreferTransparent { get; set; } public BaseImageExpressionResult() { - LosslessFormat = false; + Format = ImageExpressionFormat.Jpeg; Quality = 90; ShowField = false; BackgroundPreferTransparent = true; } - public abstract Stream GetImage(int Width, int Height); - public abstract Stream GetImage(); + public abstract MemoryStream GetImage(int width, int height); + public abstract MemoryStream GetImage(out int width, out int height); - protected Stream RenderImage(Image SourceImage, int Width, int Height) + protected MemoryStream RenderBitmapImage(Image SourceImage, int Width, int Height) { if (SourceImage == null) - throw new ArgumentNullException("SourceImage"); + throw new ArgumentNullException(nameof(SourceImage)); if (Width <= 0) - throw new ArgumentOutOfRangeException("Width", "Width must be > 0"); + throw new ArgumentOutOfRangeException(nameof(Width), "Width must be > 0"); if (Height <= 0) - throw new ArgumentOutOfRangeException("Height", "Height must be > 0"); + throw new ArgumentOutOfRangeException(nameof(Height), "Height must be > 0"); + if (Format != ImageExpressionFormat.Jpeg && Format != ImageExpressionFormat.Png) + throw new NotSupportedException($"The format {Format} is not supported by this method"); Brush backgroundBrush = null; - if (!LosslessFormat || !BackgroundPreferTransparent) + if (Format == ImageExpressionFormat.Jpeg || !BackgroundPreferTransparent) { if (string.IsNullOrEmpty(BackgroundColour)) backgroundBrush = Brushes.White; @@ -44,22 +46,28 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations using (Image resizedImage = SourceImage.ResizeImage(Width, Height, backgroundBrush)) { - return OutputImage(resizedImage); + return OutputBitmapImage(resizedImage); } } - protected Stream OutputImage(Image SourceImage) + protected MemoryStream OutputBitmapImage(Image SourceImage) { + if (Format != ImageExpressionFormat.Jpeg && Format != ImageExpressionFormat.Png) + throw new NotSupportedException($"The format {Format} is not supported by this method"); + MemoryStream imageStream = new MemoryStream(); - if (LosslessFormat) + if (Format == ImageExpressionFormat.Png) { // Lossless Format - PNG SourceImage.SavePng(imageStream); } - else + else if (Format == ImageExpressionFormat.Jpeg) { // Lossy Format - JPG var quality = Math.Min(100, Math.Max(1, (int)Quality)); SourceImage.SaveJpg(quality, imageStream); } + else + throw new NotSupportedException($"Unexpected format {Format}"); + imageStream.Position = 0; return imageStream; } diff --git a/Disco.Services/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs b/Disco.Services/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs index d82ec9ee..e75a5d8a 100644 --- a/Disco.Services/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs +++ b/Disco.Services/Expressions/Extensions/ImageResultImplementations/BitmapImageExpressionResult.cs @@ -16,14 +16,19 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations this.Image = Image; } - public override Stream GetImage(int Width, int Height) + public override MemoryStream GetImage(int width, int height) { - return RenderImage(Image, Width, Height); + return RenderBitmapImage(Image, width, height); } - public override Stream GetImage() + public override MemoryStream GetImage(out int width, out int height) { - return OutputImage(Image); + var image = Image; + + width = image.Width; + height = image.Height; + + return OutputBitmapImage(image); } } } diff --git a/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs b/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs index 42eef4d6..8192df07 100644 --- a/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs +++ b/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileImageExpressionResult.cs @@ -18,20 +18,28 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations this.AbsoluteFilePath = AbsoluteFilePath; } - public override Stream GetImage(int Width, int Height) + public override MemoryStream GetImage(int width, int height) { - using (Image SourceImage = Bitmap.FromFile(AbsoluteFilePath)) + using (var sourceImage = Image.FromFile(AbsoluteFilePath)) { - return RenderImage(SourceImage, Width, Height); + return RenderBitmapImage(sourceImage, width, height); } } - public override Stream GetImage() + public override MemoryStream GetImage(out int width, out int height) { var stream = new MemoryStream(); using (var fileStream = File.OpenRead(AbsoluteFilePath)) fileStream.CopyTo(stream); stream.Position = 0; + + using (var sourceImage = Image.FromStream(stream)) + { + width = sourceImage.Width; + height = sourceImage.Height; + } + stream.Position = 0; + return stream; } } diff --git a/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs b/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs index f1725d23..c9235b87 100644 --- a/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs +++ b/Disco.Services/Expressions/Extensions/ImageResultImplementations/FileMontageImageExpressionResult.cs @@ -27,17 +27,17 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations Padding = 4; } - public override Stream GetImage(int Width, int Height) + public override MemoryStream GetImage(int width, int height) { - return DoLayout(Width, Height); + return DoLayout(width, height, out _, out _); } - public override Stream GetImage() + public override MemoryStream GetImage(out int width, out int height) { - return DoLayout(width: null, height: null); + return DoLayout(width: null, height: null, out width, out height); } - private Stream DoLayout(int? width, int? height) + private MemoryStream DoLayout(int? width, int? height, out int resultWidth, out int resultHeight) { List images = new List(); try @@ -81,7 +81,7 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations montageGraphics.SmoothingMode = SmoothingMode.HighQuality; // Draw Background - if (!LosslessFormat || !BackgroundPreferTransparent) + if (Format == Models.Services.Expressions.Extensions.ImageExpressionFormat.Jpeg || !BackgroundPreferTransparent) { Brush backgroundBrush = Brushes.White; if (!string.IsNullOrEmpty(BackgroundColour)) @@ -96,7 +96,9 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations else DoTableLayout(images, montageGraphics); } - return OutputImage(montageImage); + resultWidth = width.Value; + resultHeight = height.Value; + return OutputBitmapImage(montageImage); } } catch (Exception) { throw; } @@ -111,7 +113,6 @@ namespace Disco.Services.Expressions.Extensions.ImageResultImplementations private void DoHorizontalLayout(List Images, Graphics MontageGraphics) { - float imageScale; float imagePosition = 0; int imagesWidthTotal = Images.Sum(i => i.Width); diff --git a/Disco.Services/Expressions/Extensions/ImageResultImplementations/QrCodeImageExpressionResult.cs b/Disco.Services/Expressions/Extensions/ImageResultImplementations/QrCodeImageExpressionResult.cs new file mode 100644 index 00000000..035a99aa --- /dev/null +++ b/Disco.Services/Expressions/Extensions/ImageResultImplementations/QrCodeImageExpressionResult.cs @@ -0,0 +1,122 @@ +using Disco.Models.Services.Expressions.Extensions; +using Disco.Services.Documents; +using System; +using System.Drawing; +using System.Drawing.Imaging; +using System.IO; +using System.Runtime.InteropServices; +using ZXing.QrCode.Internal; + +namespace Disco.Services.Expressions.Extensions.ImageResultImplementations +{ + public class QrCodeImageExpressionResult : BaseImageExpressionResult + { + public static Func CCITTG4EncoderCompressDelegate; + public string Data { get; } + public char ErrorCorrectionLevel { get; } + private readonly ErrorCorrectionLevel ecLevel; + + public QrCodeImageExpressionResult(string data, char errorCorrectionLevel) + { + Data = data; + ErrorCorrectionLevel = errorCorrectionLevel; + + switch (errorCorrectionLevel) + { + case 'l': + case 'L': + ecLevel = ZXing.QrCode.Internal.ErrorCorrectionLevel.L; + break; + case 'm': + case 'M': + ecLevel = ZXing.QrCode.Internal.ErrorCorrectionLevel.M; + break; + case 'q': + case 'Q': + ecLevel = ZXing.QrCode.Internal.ErrorCorrectionLevel.Q; + break; + case 'h': + case 'H': + ecLevel = ZXing.QrCode.Internal.ErrorCorrectionLevel.H; + break; + default: + throw new ArgumentException("Error correction level must be L, M, Q or H"); + } + + Format = ImageExpressionFormat.CcittG4; + Quality = 90; + ShowField = false; + BackgroundPreferTransparent = true; + } + + public override MemoryStream GetImage(int width, int height) + { + var rawImage = QRCodeBinaryEncoder.Encode(Data, ecLevel, width, height); + return RenderStream(rawImage, width, height); + } + + public override MemoryStream GetImage(out int width, out int height) + { + var rawImage = QRCodeBinaryEncoder.Encode(Data, ecLevel, out width, out height); + return RenderStream(rawImage, width, height); + } + + private MemoryStream RenderStream(byte[] data, int width, int height) + { + if (Format == ImageExpressionFormat.CcittG4) + { + if (CCITTG4EncoderCompressDelegate == null) + throw new InvalidOperationException($"The {CCITTG4EncoderCompressDelegate} delegate has not been initialized"); + + var result = CCITTG4EncoderCompressDelegate(data, width, height); + return new MemoryStream(result, 0, result.Length, false, true); + } + else + { + using (var bitmap = RenderBitmap(data, width, height)) + { + return OutputBitmapImage(bitmap); + } + } + } + + private Image RenderBitmap(byte[] data, int width, int height) + { + var bitmap = new Bitmap(width, height, PixelFormat.Format1bppIndexed); + bitmap.Palette.Entries[0] = Color.Black; + bitmap.Palette.Entries[0] = Color.White; + + var pixelData = bitmap.LockBits(new Rectangle(Point.Empty, bitmap.Size), ImageLockMode.WriteOnly, PixelFormat.Format1bppIndexed); + try + { + for (int y = 0; y < height; y++) + { + var offset = y * width; + var buffer = new byte[(width + 7) / 8]; + var bufferOffset = 0; + var bitOffset = 0; + for (int x = 0; x < width; x++) + { + var pixel = data[offset + x]; + if (pixel != 0) + { + buffer[bufferOffset] = (byte)(buffer[bufferOffset] | (byte)((128) >> bitOffset)); + } + if (bitOffset == 8) + { + bufferOffset++; + bitOffset = 0; + } + } + Marshal.Copy(buffer, 0, pixelData.Scan0, buffer.Length); + } + } + finally + { + bitmap.UnlockBits(pixelData); + } + + return bitmap; + } + } +} diff --git a/Disco.Web/App_Start/AppConfig.cs b/Disco.Web/App_Start/AppConfig.cs index 52460e4a..54dc5c09 100644 --- a/Disco.Web/App_Start/AppConfig.cs +++ b/Disco.Web/App_Start/AppConfig.cs @@ -53,6 +53,7 @@ namespace Disco.Web InitalizeCoreEnvironment(Database); // Initialize Expressions + Disco.Services.Expressions.Extensions.ImageResultImplementations.QrCodeImageExpressionResult.CCITTG4EncoderCompressDelegate = Disco.BI.Interop.Pdf.Utilities.GetCCITTG4EncoderCompressDelegate(); Disco.Services.Expressions.Expression.InitializeExpressions(); // Initialize Job Queues