using DocumentFormat.OpenXml.Packaging; using DocumentFormat.OpenXml.Spreadsheet; using DocumentFormat.OpenXml; using System.Data; using System.Text.RegularExpressions; using Font = DocumentFormat.OpenXml.Spreadsheet.Font; using Color = DocumentFormat.OpenXml.Spreadsheet.Color; namespace qdr.app.studiou.orders2printpack.Labels { /// /// Generates Excel documents for printing labels using OpenXML format /// without relying on Microsoft Office libraries. /// public class ExcelLabelGenerator { // Default row height in points private const double DEFAULT_ROW_HEIGHT = 15.0; // Border styles private const int BORDER_STYLE_ID = 1; // Template pattern to match column names private static readonly Regex TemplatePattern = new Regex(@"\{([^{}]+)\}", RegexOptions.Compiled); // Formatting patterns private static readonly Regex BoldPattern = new Regex(@"\*\*(.+?)\*\*", RegexOptions.Compiled); private static readonly Regex ItalicPattern = new Regex(@"__(.+?)__", RegexOptions.Compiled); private static readonly Regex UnderlinePattern = new Regex(@"_(.+?)_", RegexOptions.Compiled); private static readonly Regex FontSizePattern = new Regex(@"\^\^(\d+)\^(.*?)\^\^", RegexOptions.Compiled); // Style indexes for different formatting private const int STYLE_NORMAL = 0; private const int STYLE_BORDER = 1; private const int STYLE_BOLD = 2; private const int STYLE_ITALIC = 3; private const int STYLE_UNDERLINE = 4; private const int STYLE_BOLD_BORDER = 5; private const int STYLE_ITALIC_BORDER = 6; private const int STYLE_UNDERLINE_BORDER = 7; // Font size style indexes start at 8 and go up private const int STYLE_FONT_SIZE_START = 8; /// /// Label format template that will be used to generate the content of each label. /// Use {columnName} placeholders to reference DataTable columns. /// Example: "ID: {ID}\nName: {ProductName}\nPrice: ${Price}" /// private readonly string _labelTemplate; /// /// Initializes a new instance of the ExcelLabelGenerator class with a specific label template. /// /// Template string with {columnName} placeholders public ExcelLabelGenerator(string labelTemplate) { _labelTemplate = labelTemplate ?? throw new ArgumentNullException(nameof(labelTemplate)); } /// /// Initializes a new instance of the ExcelLabelGenerator class with a default template. /// The default template will simply concatenate all column values. /// public ExcelLabelGenerator() { _labelTemplate = string.Empty; } /// /// Generates an Excel document with labels formatted according to specified layout /// /// DataTable containing the label data /// Path where the Excel file will be saved /// Number of label columns per A4 page /// Number of label rows per A4 page /// Width of each label in points /// Height of each label in points /// True if generation was successful, otherwise false public bool GenerateLabels( DataTable data, string outputPath, int columnsPerPage, int rowsPerPage, double labelWidthPt = 200.0, double labelHeightPt = 120.0) { if (data == null || data.Rows.Count == 0) throw new ArgumentException("Data table cannot be null or empty"); if (columnsPerPage <= 0 || rowsPerPage <= 0) throw new ArgumentException("Columns and rows per page must be greater than zero"); try { // Create the Excel document using (SpreadsheetDocument document = SpreadsheetDocument.Create(outputPath, SpreadsheetDocumentType.Workbook)) { // Add a WorkbookPart to the document WorkbookPart workbookPart = document.AddWorkbookPart(); workbookPart.Workbook = new Workbook(); // Add styles to the workbook WorkbookStylesPart stylesPart = workbookPart.AddNewPart(); stylesPart.Stylesheet = CreateStylesheet(); stylesPart.Stylesheet.Save(); // Add a WorksheetPart to the WorkbookPart WorksheetPart worksheetPart = workbookPart.AddNewPart(); worksheetPart.Worksheet = new Worksheet(new SheetData()); // Add Sheets to the Workbook Sheets sheets = workbookPart.Workbook.AppendChild(new Sheets()); // Add a Sheet to the Sheets collection Sheet sheet = new Sheet() { Id = workbookPart.GetIdOfPart(worksheetPart), SheetId = 1, Name = "Labels" }; sheets.Append(sheet); // Get the SheetData from the WorksheetPart var sheetData = worksheetPart.Worksheet.GetFirstChild(); // Configure page layout for A4 PageSetup pageSetup = new PageSetup { PaperSize = (UInt32Value)9U, // A4 paper size Orientation = OrientationValues.Portrait, FitToWidth = 1, FitToHeight = 0 // Auto height }; PageMargins pageMargins = new PageMargins { Top = 0.75D, Bottom = 0.75D, Left = 0.7D, Right = 0.7D, Header = 0.3D, Footer = 0.3D }; // Add page setup and margins worksheetPart.Worksheet.AppendChild(pageSetup); worksheetPart.Worksheet.AppendChild(pageMargins); // Set column widths based on the fixed label width Columns columns = new Columns(); // Convert points to Excel column width // Excel column width units are based on the width of the "0" (zero) character in the default font // The conversion is approximate but works for most cases double excelColumnWidth = labelWidthPt / 7.0; // Approximate conversion for (int i = 1; i <= columnsPerPage; i++) { columns.Append(new Column { Min = (uint)i, Max = (uint)i, Width = excelColumnWidth, CustomWidth = true }); } worksheetPart.Worksheet.InsertBefore(columns, sheetData); // Process each row of data Dictionary rows = new Dictionary(); for (int dataIndex = 0; dataIndex < data.Rows.Count; dataIndex++) { // Calculate the label position on the page int pageCol = dataIndex % columnsPerPage + 1; int pageRow = dataIndex / columnsPerPage % rowsPerPage + 1; // Calculate actual row in Excel - one cell per label int excelRowIndex = dataIndex / columnsPerPage / rowsPerPage * rowsPerPage + pageRow; uint rowIndex = (uint)excelRowIndex; // Ensure row exists if (!rows.ContainsKey(rowIndex)) { Row row = new Row { RowIndex = rowIndex, Height = labelHeightPt, CustomHeight = true }; rows.Add(rowIndex, row); } string columnLetter = ColumnIndexToLetters(pageCol); // Create content for the label using the template with newlines string labelContent = GenerateLabelContent(data.Rows[dataIndex]); // Add cell with border style and content Cell cell = CreateCell(columnLetter, rowIndex, labelContent, BORDER_STYLE_ID); rows[rowIndex].Append(cell); } // Add all rows to the sheet foreach (var row in rows.Values.OrderBy(r => r.RowIndex)) { sheetData?.Append(row); } // Save the worksheet worksheetPart.Worksheet.Save(); // Save the workbook workbookPart.Workbook.Save(); } return true; } catch (Exception ex) { Console.WriteLine($"Error generating Excel labels: {ex.Message}"); return false; } } /// /// Gets or creates a style for a specific font size /// /// Font size in points /// Whether to include a border /// Style index private uint GetFontSizeStyle(int fontSize, bool withBorder) { // We'll use a simple formula to calculate the style index // STYLE_FONT_SIZE_START + (fontSize * 2) for no border // STYLE_FONT_SIZE_START + (fontSize * 2) + 1 for with border int styleOffset = fontSize * 2 + (withBorder ? 1 : 0); uint styleIndex = (uint)(STYLE_FONT_SIZE_START + styleOffset); // Return the style index - we'll ensure the style exists when creating the stylesheet return styleIndex; } /// /// Parses text for style markers and removes them from the text /// Handles nested formatting (e.g. _**text**_ for bold and underlined) /// /// Text to parse, will be modified to remove markers /// TextStyle object with detected formatting private TextStyle ParseTextStyle(ref string text) { TextStyle style = new TextStyle(); bool madeChanges; // We need to repeat the pattern matching until no more changes are made // This handles nested formatting like _**text**_ do { madeChanges = false; // Check for bold if (BoldPattern.IsMatch(text)) { style.IsBold = true; string newText = BoldPattern.Replace(text, "$1"); if (newText != text) { text = newText; madeChanges = true; } } // Check for italic if (ItalicPattern.IsMatch(text)) { style.IsItalic = true; string newText = ItalicPattern.Replace(text, "$1"); if (newText != text) { text = newText; madeChanges = true; } } // Check for underline if (UnderlinePattern.IsMatch(text)) { style.IsUnderlined = true; string newText = UnderlinePattern.Replace(text, "$1"); if (newText != text) { text = newText; madeChanges = true; } } } while (madeChanges); return style; } /// /// Represents text formatting style /// private class TextStyle { public bool IsBold { get; set; } public bool IsItalic { get; set; } public bool IsUnderlined { get; set; } public bool HasFormatting => IsBold || IsItalic || IsUnderlined; } /// /// The key update is to the CreateCell method and ensuring all cell formats /// have WrapText = true properly set. Also ensuring the alignment is properly /// applied to each cell format. /// private Cell CreateCell(string columnName, uint rowIndex, string text, uint? styleIndex = null) { // Create the cell Cell cell = new Cell { CellReference = $"{columnName}{rowIndex}", DataType = CellValues.InlineString, StyleIndex = styleIndex ?? STYLE_NORMAL }; // Create the inline string that will hold our text with preserved formatting InlineString inlineString = new InlineString(); // We need to handle each line of text separately while preserving the newlines string[] lines = text.Split(new[] { '\n' }, StringSplitOptions.None); for (int lineIndex = 0; lineIndex < lines.Length; lineIndex++) { string line = lines[lineIndex]; // Check if this is not the first line, we need to add a line break if (lineIndex > 0) { // For line breaks in Excel, we need to add a line feed character // within the same text run, not as a separate Break element Run lineBreakRun = new Run(); lineBreakRun.AppendChild(new Text { Text = "\n", Space = SpaceProcessingModeValues.Preserve }); inlineString.AppendChild(lineBreakRun); } // Process the current line // This can contain formatting markers like bold, italic, etc. string remainingText = line; // First check for font size formatting while (FontSizePattern.IsMatch(remainingText)) { Match match = FontSizePattern.Match(remainingText); // Add text before the font size marker if any if (match.Index > 0) { string beforeText = remainingText.Substring(0, match.Index); TextStyle beforeStyle = ParseTextStyle(ref beforeText); AddFormattedRun(inlineString, beforeText, beforeStyle); } // Handle the font-sized text int fontSize; if (int.TryParse(match.Groups[1].Value, out fontSize)) { string fontSizeText = match.Groups[2].Value; TextStyle fontSizeStyle = ParseTextStyle(ref fontSizeText); // Create a run with font size and any other formatting Run fontSizeRun = new Run(); RunProperties fontSizeProps = new RunProperties(); fontSizeProps.AppendChild(new FontSize { Val = fontSize }); if (fontSizeStyle.IsBold) fontSizeProps.AppendChild(new Bold()); if (fontSizeStyle.IsItalic) fontSizeProps.AppendChild(new Italic()); if (fontSizeStyle.IsUnderlined) fontSizeProps.AppendChild(new Underline { Val = UnderlineValues.Single }); fontSizeRun.AppendChild(fontSizeProps); fontSizeRun.AppendChild(new Text { Text = fontSizeText, Space = SpaceProcessingModeValues.Preserve }); inlineString.AppendChild(fontSizeRun); } // Move past the processed part remainingText = remainingText.Substring(match.Index + match.Length); } // Process any remaining text in this line if any if (!string.IsNullOrEmpty(remainingText)) { TextStyle remainingStyle = ParseTextStyle(ref remainingText); AddFormattedRun(inlineString, remainingText, remainingStyle); } } cell.AppendChild(inlineString); return cell; } /// /// Helper method to add a run with the specified formatting /// private void AddFormattedRun(InlineString inlineString, string text, TextStyle style) { if (string.IsNullOrEmpty(text)) return; Run run = new Run(); if (style.HasFormatting) { RunProperties props = new RunProperties(); if (style.IsBold) props.AppendChild(new Bold()); if (style.IsItalic) props.AppendChild(new Italic()); if (style.IsUnderlined) props.AppendChild(new Underline { Val = UnderlineValues.Single }); run.AppendChild(props); } run.AppendChild(new Text { Text = text, Space = SpaceProcessingModeValues.Preserve }); inlineString.AppendChild(run); } /// /// This is a key modification to ensure ALL cell formats have WrapText=true /// private Stylesheet CreateStylesheet() { Stylesheet stylesheet = new Stylesheet(); // Create Fonts Fonts fonts = new Fonts(); // Font 0: Normal (11pt) Font normalFont = new Font(); normalFont.AppendChild(new FontSize { Val = 11D }); fonts.Append(normalFont); // Font 1: Bold (11pt) Font boldFont = new Font(); boldFont.AppendChild(new FontSize { Val = 11D }); boldFont.Append(new Bold()); fonts.Append(boldFont); // Font 2: Italic (11pt) Font italicFont = new Font(); italicFont.AppendChild(new FontSize { Val = 11D }); italicFont.Append(new Italic()); fonts.Append(italicFont); // Font 3: Underline (11pt) Font underlineFont = new Font(); underlineFont.AppendChild(new FontSize { Val = 11D }); underlineFont.Append(new Underline() { Val = UnderlineValues.Single }); fonts.Append(underlineFont); // Add font sizes from 8pt to 72pt for (int size = 8; size <= 72; size++) { // Regular font with custom size Font sizedFont = new Font(); sizedFont.AppendChild(new FontSize { Val = (double)size }); fonts.Append(sizedFont); } fonts.Count = (uint)fonts.ChildElements.Count; // Create Fills Fills fills = new Fills(); Fill fill1 = new Fill(); fill1.PatternFill = new PatternFill { PatternType = PatternValues.None }; Fill fill2 = new Fill(); fill2.PatternFill = new PatternFill { PatternType = PatternValues.Gray125 }; fills.Append(fill1); fills.Append(fill2); fills.Count = (uint)fills.ChildElements.Count; // Create Borders Borders borders = new Borders(); // Border 0: No border Border noBorder = new Border(); borders.Append(noBorder); // Border 1: All sides Border allBorder = new Border(); // Left Border LeftBorder leftBorder = new LeftBorder { Style = BorderStyleValues.Thin }; leftBorder.Append(new Color { Auto = true }); allBorder.Append(leftBorder); // Right Border RightBorder rightBorder = new RightBorder { Style = BorderStyleValues.Thin }; rightBorder.Append(new Color { Auto = true }); allBorder.Append(rightBorder); // Top Border TopBorder topBorder = new TopBorder { Style = BorderStyleValues.Thin }; topBorder.Append(new Color { Auto = true }); allBorder.Append(topBorder); // Bottom Border BottomBorder bottomBorder = new BottomBorder { Style = BorderStyleValues.Thin }; bottomBorder.Append(new Color { Auto = true }); allBorder.Append(bottomBorder); // Diagonal Border (blank) allBorder.Append(new DiagonalBorder()); borders.Append(allBorder); borders.Count = (uint)borders.ChildElements.Count; // Create CellFormats CellFormats cellFormats = new CellFormats(); // *** CRITICALLY IMPORTANT: All cell formats must have WrapText=true *** // Format 0: Default / Normal CellFormat format0 = new CellFormat { NumberFormatId = 0, FontId = 0, FillId = 0, BorderId = 0, FormatId = 0, ApplyAlignment = true }; format0.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format0); // Format 1: Border only CellFormat format1 = new CellFormat { NumberFormatId = 0, FontId = 0, FillId = 0, BorderId = 1, FormatId = 0, ApplyBorder = true, ApplyAlignment = true }; format1.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format1); // Format 2: Bold CellFormat format2 = new CellFormat { NumberFormatId = 0, FontId = 1, FillId = 0, BorderId = 0, FormatId = 0, ApplyFont = true, ApplyAlignment = true }; format2.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format2); // Format 3: Italic CellFormat format3 = new CellFormat { NumberFormatId = 0, FontId = 2, FillId = 0, BorderId = 0, FormatId = 0, ApplyFont = true, ApplyAlignment = true }; format3.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format3); // Format 4: Underline CellFormat format4 = new CellFormat { NumberFormatId = 0, FontId = 3, FillId = 0, BorderId = 0, FormatId = 0, ApplyFont = true, ApplyAlignment = true }; format4.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format4); // Format 5: Bold + Border CellFormat format5 = new CellFormat { NumberFormatId = 0, FontId = 1, FillId = 0, BorderId = 1, FormatId = 0, ApplyFont = true, ApplyBorder = true, ApplyAlignment = true }; format5.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format5); // Format 6: Italic + Border CellFormat format6 = new CellFormat { NumberFormatId = 0, FontId = 2, FillId = 0, BorderId = 1, FormatId = 0, ApplyFont = true, ApplyBorder = true, ApplyAlignment = true }; format6.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format6); // Format 7: Underline + Border CellFormat format7 = new CellFormat { NumberFormatId = 0, FontId = 3, FillId = 0, BorderId = 1, FormatId = 0, ApplyFont = true, ApplyBorder = true, ApplyAlignment = true }; format7.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, // Left indent for text margin JustifyLastLine = false }); cellFormats.Append(format7); cellFormats.Count = (uint)cellFormats.ChildElements.Count; // Add additional cell formats for font sizes 8pt to 72pt // Both with and without borders for (int size = 8; size <= 72; size++) { int fontIndex = 4 + (size - 8); // Font indexes start after our 4 basic fonts // Create style without border CellFormat formatSize = new CellFormat { NumberFormatId = 0, FontId = (uint)fontIndex, FillId = 0, BorderId = 0, FormatId = 0, ApplyFont = true, ApplyAlignment = true }; formatSize.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, JustifyLastLine = false }); cellFormats.Append(formatSize); // Create style with border CellFormat formatSizeBorder = new CellFormat { NumberFormatId = 0, FontId = (uint)fontIndex, FillId = 0, BorderId = 1, FormatId = 0, ApplyFont = true, ApplyBorder = true, ApplyAlignment = true }; formatSizeBorder.AppendChild(new Alignment { Vertical = VerticalAlignmentValues.Center, Horizontal = HorizontalAlignmentValues.Left, WrapText = true, Indent = 1, JustifyLastLine = false }); cellFormats.Append(formatSizeBorder); } cellFormats.Count = (uint)cellFormats.ChildElements.Count; stylesheet.Append(fonts); stylesheet.Append(fills); stylesheet.Append(borders); stylesheet.Append(cellFormats); return stylesheet; } /// /// Generates the content for a label based on the template and data row /// /// The DataRow containing the label data /// String representing the label content with newlines private string GenerateLabelContent(DataRow dataRow) { string result; if (string.IsNullOrEmpty(_labelTemplate)) { // Default behavior: use column name and value for each column List lines = new List(); foreach (DataColumn column in dataRow.Table.Columns) { object value = dataRow[column.ColumnName]; string lineContent = $"{column.ColumnName}: {(value != DBNull.Value ? value?.ToString() : string.Empty)}"; lines.Add(lineContent); } result = string.Join("\n", lines); } else { // Use the specified template result = _labelTemplate; // Replace all column placeholders with actual values result = TemplatePattern.Replace(result, match => { string columnName = match.Groups[1].Value; if (dataRow.Table.Columns.Contains(columnName)) { object value = dataRow[columnName]; return value != DBNull.Value ? value?.ToString() ?? string.Empty : string.Empty; } return match.Value; // Keep the placeholder if column not found }); } return result; } /// /// Converts a column index to Excel column letters (e.g., 1 = A, 27 = AA) /// private string ColumnIndexToLetters(int columnIndex) { string columnLetters = string.Empty; while (columnIndex > 0) { int remainder = (columnIndex - 1) % 26; columnLetters = (char)('A' + remainder) + columnLetters; columnIndex = (columnIndex - 1) / 26; } return columnLetters; } } }