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;
}
}
}