I have generated a spreadsheet with data and a chart based on the data using C# OpenXML.
I am now wanting to now embed this chart into a word document, so that when these reports are sent out, the charts can be edited if need be (Not an external link).
I have looked around for hours and can't find any consistent documentation for articles which achieve this. Most leads seem to talk about 'EmbeddedPackageParts'.
If anyone has any helpful articles or can provide some clarity it would be appreciated.
Cheers,
Duncan.
I ended up figuring this out!
Steps:
Generate xlsx file with my data in it.
Save the xlsx file locally
In my word document create a new chart part and generate the graph contents
ChartPart wordChartPart = document.MainDocumentPart.AddNewPart<ChartPart>();
string wordChartId = document.MainDocumentPart.GetIdOfPart(wordChartPart);
WordDocumentBuilder.Workflows.SpreadsheetUtils.GenerateBarChartPart(wordChartPart, categories, dataRows);
Embed the spreadsheet in the ChartPart
EmbeddedPackagePart embeddedObjectPart = wordChartPart.AddEmbeddedPackagePart(#"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
using (FileStream stream = new FileStream(file, FileMode.Open))
{
byte[] documentBytes = new byte[stream.Length];
stream.Read(documentBytes, 0, documentBytes.Length);
using (BinaryWriter writer = new BinaryWriter(embeddedObjectPart.GetStream()))
{
writer.Write(documentBytes);
writer.Flush();
}
}
I could then insert an Inline Drawing into my word document which referenced the chart part.
you can open both the new word document and the excel sheet that your charts are included currently.
You can add some rich text content controls to the word document and insert the Chartparts of the excel sheet in those places.
Make sure that your charts have titles. You can read the charts from the titles
To append them to the word document you need to create a paragraph object and append it to a Run object and also you need to define an Inline also in order to append the charts.
I hope you can make the things I said clear by looking at the following example.
private string WordReportGeneration(string docPath, string excelPath)
{
string[] chartTitles = new string[] {"","","","","","",.... };//Chart titles
string[] bookMark = new string[] { "C1", "C2", "C3",..... };//rich text controls of the word doc
for (int i = 0; i < chartTitles.Length; i++) //going through the chart title array
{
using (WordprocessingDocument myWordDoc = WordprocessingDocument.Open(docPath, true))
{
MainDocumentPart mainPart = myWordDoc.MainDocumentPart;
SdtBlock sdt = null;
mainPart.Document.Descendants<SdtBlock>().ToList().ForEach(b => {
var child = b.SdtProperties.GetFirstChild<Tag>();
if (child != null && child.Val.Equals(bookMark[i]))
sdt = b;
});
Paragraph p = sdt.SdtContentBlock.GetFirstChild<Paragraph>();
p.RemoveAllChildren();
Run r = new Run();
p.Append(r);
Drawing drawing = new Drawing();
r.Append(drawing);
Inline inline = new Inline(
new Extent()
{ Cx = 5486400, Cy = 3200400 });
using (SpreadsheetDocument mySpreadsheet = SpreadsheetDocument.Open(excelPath, true))
{
WorkbookPart workbookPart = mySpreadsheet.WorkbookPart;
Sheet theSheet = workbookPart.Workbook.Descendants<Sheet>().FirstOrDefault(s => s.Name == "Report");
WorksheetPart worksheetPart = (WorksheetPart)workbookPart.GetPartById(theSheet.Id);
DrawingsPart drawingPart = worksheetPart.DrawingsPart;
ChartPart chartPart = (ChartPart)drawingPart.ChartParts.FirstOrDefault(x =>
x.ChartSpace.ChildElements[4].FirstChild.InnerText.Trim() == chartTitles[i]);
ChartPart importedChartPart = mainPart.AddPart<ChartPart>(chartPart);
string relId = mainPart.GetIdOfPart(importedChartPart);
DocumentFormat.OpenXml.Drawing.Spreadsheet.GraphicFrame frame = drawingPart.WorksheetDrawing.Descendants<DocumentFormat.OpenXml.Drawing.Spreadsheet.GraphicFrame>().First();
Graphic clonedGraphic = (Graphic)frame.Graphic.CloneNode(true);
ChartReference c = clonedGraphic.GraphicData.GetFirstChild<ChartReference>();
c.Id = relId;
DocProperties docPr = new DocProperties();
docPr.Name = "XXX";
docPr.Id = GetMaxDocPrId(mainPart) + 1;
inline.Append(docPr, clonedGraphic);
drawing.Append(inline);
}
myWordDoc.Save();
myWordDoc.Close();
}
}
return docPath;
}
enter code here
enter code here
Related
I am trying to create an Excel file using Open XML C#, where there are multiple sheets in my excel file that needs to be created. Please find my code below and help me with a solution.
When I open the excel only 1 sheet is present, sheet 2 is getting overwritten in sheet 1.
public void CreateExcelFile(string filePath , List<SheetData> excelSheet)
{
var sheetCount = GetSheetCount(filePath);
using (SpreadsheetDocument spreedDoc = SpreadsheetDocument.Create(filePath,
DocumentFormat.OpenXml.SpreadsheetDocumentType.Workbook))
{
Workbook workbook1 = new Workbook();
workbook1.AddNamespaceDeclaration("r", "http://schemas.openxmlformats.org/officeDocument/2006/relationships");
FileVersion fileVersion1 = new FileVersion() { ApplicationName = "xl", LastEdited = "5", LowestEdited = "4", BuildVersion = "9302" };
WorkbookProperties workbookProperties1 = new WorkbookProperties() { FilterPrivacy = true, DefaultThemeVersion = (UInt32Value)124226U };
BookViews bookViews1 = new BookViews();
WorkbookView workbookView1 = new WorkbookView() { XWindow = 240, YWindow = 105, WindowWidth = (UInt32Value)14805U, WindowHeight = (UInt32Value)8010U, ActiveTab = (UInt32Value)2U };
bookViews1.Append(workbookView1);
Sheets sheetCol = new Sheets();
int ctr = 0;
foreach (var sheetToCreate in excelSheet)
{
ctr++;
//WorksheetPart worksheetPart = null;
//worksheetPart = wbPart.AddNewPart<WorksheetPart>();
//var sheetData = new SheetData();
//worksheetPart.Worksheet = new Worksheet(sheetData);
//worksheetPart.Worksheet = new Worksheet(sheetToCreate);
string sheetName = "Sheet";
sheetName += ctr.ToString();
var sheet = new Sheet()
{
Id = "rId" + ctr,
SheetId = (UInt32)ctr,
Name = sheetName
};
sheet.Append(sheetToCreate);
sheetCol.Append(sheet);
//wbPart.Workbook.Sheets.AppendChild(sheet);
//wbPart.Workbook.Append(sheetCol);
workbook1.Sheets.Append(sheet);
//var workingSheet = ((WorksheetPart)wbPart.GetPartById(sheet.Id)).Worksheet;
}
workbook1.Append(fileVersion1);
workbook1.Append(workbookProperties1);
workbook1.Append(bookViews1);
workbook1.Append(sheetCol);
//Set Border
//wbPark
workbook1.Save();
}
}
When dealing with OpenXML, the "OpenXML Productivity Tool" (downloadable from Microsoft's site) is your friend. When I work with creating documents with OpenXML, I do the following:
Create a document similar to what I want using Excel or Word (in this case, an Excel spreadsheet document with two sheets (I labeled them "First" and "Second")).
Open the document up in the productivity tool
Reflect the code (a button on the Tool's UI)
Copy/paste the code I want.
If I do that, I end up with all the code I need. I've boiled it down to the essentials to answer your question. Excel will complain that the document produced below is corrupt (since all it includes is the code to create the two sheets - and leaves lots of other required code out - all of which is stealable from the tool). However, if you tell Excel "Repair this for me", you will get a document with two sheets, labeled "First" and "Second"
WorkbookPart workbookPart1 = document.AddWorkbookPart();
Workbook workbook1 = new Workbook() { MCAttributes = new MarkupCompatibilityAttributes() { Ignorable = "x15 xr xr6 xr10 xr2" } };
//lots of namespace declarations - copy them from the tool (they are needed!!)
//lots of code for WorkbookProperties, etc (you can get this from Tool)
Sheets sheets1 = new Sheets();
Sheet sheet1 = new Sheet() { Name = "First", SheetId = (UInt32Value)1U, Id = "rId1" };
Sheet sheet2 = new Sheet() { Name = "Second", SheetId = (UInt32Value)2U, Id = "rId2" };
sheets1.Append(sheet1);
sheets1.Append(sheet2);
//More elided code
workbook1.Append(sheets1);
workbookPart1.Workbook = workbook1;
I strongly recommend that you keep the pluralization of things like Sheet and Sheets clear. Your code has Sheets sheetCol = new Sheets();, it's not clear after that that sheetCol is a Sheets and not a Sheet.
Then you have something called sheetToCreate which is a SheetData object. Again, that's confusing - call it something clear like thisSheetData or something. I really strongly recommend that you follow the OpenXML naming as much as you can, otherwise it is extremely hard to follow what's going on.
The Productivity Tool is also handy once you've written out your file. You can see what you created, often spotting what the error is. In addition, there's a validate button that usually (not always) points you to problems in the structure of your documents.
Good luck. Prepare to get better at cussing at your code!
I tried to use Microsoft.Office.Interop.Excel but it's too slow when it comes to reading large excel documents (it was taking over 5 minutes for me). I read that DocumentFormat.OpenXml is faster when it comes to reading large excel documents but in the documentation it doesn't appear that I can't store the columns and row indexes.
For now, I am also only interested in the first row to get the column headers and I will be reading the rest of the document after some logic. I haven't been able to find a way to read only a portion of the excel document. I want to do something similar to this:
int r = 1; //row index
int c = 1; //column index
while (xlRange.Cells[r,c] != null && xlRange.Cells[r, c].Value2 != null)
{
TagListData.Add(new TagClass { IsTagSelected = false, TagName = xlRange[r, c].Value2.toString(), rIndex = r, cIndex = c });
c += 3;
}
Users will be picking excel documents through openFileDialog so there's no fixed number of rows of columns I can use. Is there a way I could make this work?
Thank you
In OpenXML if a cell has no text it might or might not appear in the list of cells (depends on whether it ever had text or not). Therefore the while (...Value2 != null) type of approach isn't really a safe way to do things in OpenXML.
Here is a very simple approach to reading the first row (written using LINQPad hence the Main and the Dump). Note the (simplified) use of the SharedStringTable to get the real text of the cell:
void Main()
{
var fileName = #"c:\temp\openxml-read-row.xlsx";
using (FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
{
using (SpreadsheetDocument doc = SpreadsheetDocument.Open(fs, false))
{
// Get the necessary bits of the doc
WorkbookPart workbookPart = doc.WorkbookPart;
SharedStringTablePart sstpart = workbookPart.GetPartsOfType<SharedStringTablePart>().First();
SharedStringTable sst = sstpart.SharedStringTable;
WorkbookStylesPart ssp = workbookPart.GetPartsOfType<WorkbookStylesPart>().First();
Stylesheet ss = ssp.Stylesheet;
// Get the first worksheet
WorksheetPart worksheetPart = workbookPart.WorksheetParts.First();
Worksheet sheet = worksheetPart.Worksheet;
var rows = sheet.Descendants<Row>();
var row = rows.First();
foreach (var cell in row.Descendants<Cell>())
{
var txt = GetCellText(cell, sst);
// LINQPad specific method .Dump()
$"{cell.CellReference} = {txt}".Dump();
}
}
}
}
// Very basic way to get the text of a cell
private string GetCellText(Cell cell, SharedStringTable sst)
{
if (cell == null)
return "";
if ((cell.DataType != null) && (cell.DataType == CellValues.SharedString))
{
int ssid = int.Parse(cell.CellValue.Text);
string str = sst.ChildElements[ssid].InnerText;
return str;
}
else if (cell.CellValue != null)
{
return cell.CellValue.Text;
}
return "";
}
However... there's potentially a lot of work involved with OpenXML and you'd be well advised to try and use something like ClosedXML or EPPlus instead.
eg using ClosedXML
using (var workbook = new XLWorkbook(fileName))
{
var worksheet = workbook.Worksheets.First();
var row = worksheet.Row(1);
foreach (var cell in row.CellsUsed())
{
var txt = cell.Value.ToString();
// LINQPad specific method .Dump()
$"{cell.Address.ToString()} = {txt}".Dump();
}
}
I'm using ClosedXML to change the values of some cells in a XLSX file. These cells are used to generate a chart on another sheet.
I'm not touching the chart at all with ClosedXML. In fact, I'm not even touching the sheet that contains it, I'm just changing the values of some data cells, like this:
var workbook = new XLWorkbook(#"C:\path\to\my\file.xlsx");
var dataWorksheet = workbook.Worksheet("data");
var cell = dataWorksheet.Cell(1, 2);
cell.Value = 30;
workbook.Save();
However, when I re-open the file in Excel, the chart that was on the other sheet (which I haven't touched at all) simply disappears, leaving the sheet empty.
I know ClosedXML doesn't support the CREATION of charts, but is there a way to avoid LOSING them when saving?
Unfortunately that is a limitation on ClosedXML there are a few "chart" requests on the project page:
https://github.com/ClosedXML/ClosedXML/issues?utf8=%E2%9C%93&q=is%3Aissue+chart
But fear not the XLSX format is very simple! it's just a bunch of files all zipped.
We can still use ClosedXML to edit that Worksheet save it as a temp file and then replace the sheet on the original file, here is my code:
using ClosedXML.Excel;
using Ionic.Zip;
using System.IO;
using System.Linq;
namespace ExcelTest
{
class Program
{
static void Main(string[] args)
{
string file = #"..\..\file.xlsx";
ClosedXMLTest(file);
}
static void ClosedXMLTest(string file)
{
string outFile = "fileClosedXML.xlsx";
var workbook = new XLWorkbook(file);
var dataWorksheet = workbook.Worksheet("data");
for (int i = 1; i < 10; i++)
dataWorksheet.Cell(i, i).Value = i;
workbook.SaveAs(outFile);
ReplaceSheet(outFile, file, "xl/worksheets/sheet1.xml");
}
static void ReplaceSheet(string outputFile, string inputFile, string sheetName)
{
using (var ozip = new ZipFile(outputFile))
using (var izip = new ZipFile(inputFile))
{
var osheet = ozip.Entries.Where(x => x.FileName == sheetName).FirstOrDefault();
var tempS = new MemoryStream();
osheet.Extract(tempS);
var isheet = izip.Entries.Where(x => x.FileName == sheetName).FirstOrDefault();
izip.RemoveEntry(isheet);
izip.AddEntry(isheet.FileName, tempS.ToArray());
izip.Save();
}
}
}
}
I have the full project here:
https://github.com/heldersepu/csharp-proj/tree/master/ExcelTest
I am trying to create reports in my asp.net MVC3 application after a lot of search I found many blog posts talks about ITextSharp to convert my Html/Razor to Pdf I am trying to parse razor view to get PDf as follows
public void Render(ViewContext viewContext, TextWriter writer)
{
var doc = new Document();
// associate output with response stream
var pdfWriter = PdfWriter.GetInstance(doc, viewContext.HttpContext.Response.OutputStream);
pdfWriter.CloseStream = false;
viewContext.HttpContext.Response.ContentType = "application/pdf";
viewContext.HttpContext.Response.ContentEncoding = System.Text.Encoding.UTF8;
// generate view into string
var sb = new System.Text.StringBuilder();
TextWriter tw = new System.IO.StringWriter(sb);
_result.View.Render(viewContext, tw);
var resultCache = sb.ToString();
//Path to our font
string arialuniTff = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Fonts), "ARIALUNI.TTF");
//Register the font with iTextSharp
iTextSharp.text.FontFactory.Register(arialuniTff);
//Create a new stylesheet
iTextSharp.text.html.simpleparser.StyleSheet ST = new iTextSharp.text.html.simpleparser.StyleSheet();
//Set the default body font to our registered font's internal name
ST.LoadTagStyle(HtmlTags.BODY, HtmlTags.FACE, "Arial Unicode MS");
//Set the default encoding to support Unicode characters
ST.LoadTagStyle(HtmlTags.BODY, HtmlTags.ENCODING, BaseFont.IDENTITY_H);
//Parse our HTML using the stylesheet created above
List<IElement> list = HTMLWorker.ParseToList(new StringReader(resultCache), ST);
doc.Open();
//Loop through each element, don't bother wrapping in P tags
foreach (var element in list)
{
doc.Add(element);
}
doc.Close();
pdfWriter.Close();
}
the result of that code is
which is not correct, the arabic word should be "محمد". so what I need is to set document direction to be from right to left
EDIT
Thanks to #Romulus
I made a little changes to his code i just replaced adding element to PdfPCell to looping on my Html and set some attributes
//Loop through each element, don't bother wrapping in P tags
foreach (var element in list)
{
//Create a cell and add text to it
//PdfPCell text = new PdfPCell(new Phrase(element.ToString(), f));
//Ensure that wrapping is on, otherwise Right to Left text will not display
//text.NoWrap = false;
//Add the cell to the table
//table.AddCell(text);
if (element is iTextSharp.text.pdf.PdfPTable)
{
table = (iTextSharp.text.pdf.PdfPTable)element;
table.DefaultCell.NoWrap = false;
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
foreach (PdfPRow row in table.Rows)
{
foreach (PdfPCell cell in row.GetCells())
{
cell.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
cell.NoWrap = false;
}
}
}
}
That's working for me now well Thanks :)
You need to use container elements which support RunDirection, such as ColumnText or PdfPCell and then set their element.RunDirection = PdfWriter.RUN_DIRECTION_RTL
List<IElement> list = HTMLWorker.ParseToList(new StringReader(resultCache), ST);
doc.Open();
//Use a table so that we can set the text direction
PdfPTable table = new PdfPTable(1);
//Ensure that wrapping is on, otherwise Right to Left text will not display
table.DefaultCell.NoWrap = false;
table.RunDirection = PdfWriter.RUN_DIRECTION_RTL;
//Loop through each element, don't bother wrapping in P tags
foreach (var element in list)
{
//Create a cell and add text to it
PdfPCell text = new PdfPCell(new Phrase(element, font));
//Ensure that wrapping is on, otherwise Right to Left text will not display
text.NoWrap = false;
//Add the cell to the table
table.AddCell(text);
}
//Add the table to the document
document.Add(table);
doc.Close();
pdfWriter.Close();
For addition reference, have a look at this sample.
I need to embed an Excel document in a Word document. I used the answer on this SO question -> How can I embed any file type into Microsoft Word using OpenXml 2.0;
Everything works fine except that:
DrawAspect = OVML.OleDrawAspectValues.Icon lets you edit the Excel object by opening a new Excel instance. However, when I edit the data, it is not updated in the Word document.
DrawAspect = OVML.OleDrawAspectValues.Content lets you edit the Excel object directly in the Word document.
My question is, what do I have to change in the code so can I edit the Excel object in the new instance and have it properly reflected in the Word document? I tried everything to no avail.
Something tells me that DrawAspect = OVML.OleDrawAspectValues.Icon suggests that the object acts as an Icon, and changes cannot be properly reflected in this icon.
You could try a way I prorosed here:
How to insert an Image in WORD after a bookmark using OpenXML.
In short, use Open XML SDK 2.0 Productivity Tool (which is a part of Open XML SDK 2.0). Do whatever you need to do with document manually in MS Word. Then open this file in Open XML SDK 2.0 Productivity Tool. Then find the edits you are interested in to see how it is represented in OpenXML format , as well as how to do that programmaticaly.
Hope that helps!
UPDATED:
Okay - I have got better now what's the problem is... So in addition to my advice above I would recommend you to look at this threads on MSDN Forum:
How to modify the imbedded Excel in a Word document supplying chart
data
Embedded excel sheet in word
I let myself to repost the code sample (posted on MSDN Forum by Ji Zhou) just to avoid the deletion of original thread there.
Hope it is helpful enough to retrieve the Excel object from Word, change some cells and embed it back into Word.
public static void Main()
{
using (WordprocessingDocument wDoc = WordprocessingDocument.Open(#"D:\test.docx", true))
{
Stream stream = wDoc.MainDocumentPart.ChartParts.First().EmbeddedPackagePart.GetStream();
using (SpreadsheetDocument ssDoc = SpreadsheetDocument.Open(stream, true))
{
WorkbookPart wbPart = ssDoc.WorkbookPart;
Sheet theSheet = wbPart.Workbook.Descendants<Sheet>().
Where(s => s.Name == "Sheet1").FirstOrDefault();
if (theSheet != null)
{
Worksheet ws = ((WorksheetPart)(wbPart.GetPartById(theSheet.Id))).Worksheet;
Cell theCell = InsertCellInWorksheet("C", 2, ws);
theCell.CellValue = new CellValue("5");
theCell.DataType = new EnumValue<CellValues>(CellValues.Number);
ws.Save();
}
}
}
}
private static Cell InsertCellInWorksheet(string columnName, uint rowIndex, Worksheet worksheet)
{
SheetData sheetData = worksheet.GetFirstChild<SheetData>();
string cellReference = columnName + rowIndex;
Row row;
if (sheetData.Elements<Row>().Where(r => r.RowIndex == rowIndex).Count() != 0)
{
row = sheetData.Elements<Row>().Where(r => r.RowIndex == rowIndex).First();
}
else
{
row = new Row() { RowIndex = rowIndex };
sheetData.Append(row);
}
if (row.Elements<Cell>().Where(c => c.CellReference.Value == columnName + rowIndex).Count() > 0)
{
return row.Elements<Cell>().Where(c => c.CellReference.Value == cellReference).First();
}
else
{
Cell refCell = null;
foreach (Cell cell in row.Elements<Cell>())
{
if (string.Compare(cell.CellReference.Value, cellReference, true) > 0)
{
refCell = cell;
break;
}
}
Cell newCell = new Cell() { CellReference = cellReference };
row.InsertBefore(newCell, refCell);
worksheet.Save();
return newCell;
}
}