Skip to content

Implement support for Excel EMBED function and object file embedding #2189

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions cell.go
Original file line number Diff line number Diff line change
Expand Up @@ -1745,3 +1745,18 @@ func shiftCell(val string, dCol, dRow int) string {
}
return strings.Join(parts, ":")
}

// SetCellEmbedFormula provides a function to set EMBED formula on the cell.
// This is a convenience function for setting object embedding formulas in Excel cells.
// For example:
//
// err := f.SetCellEmbedFormula("Sheet1", "A1", "Package")
//
// This will set the formula =EMBED("Package","") in cell A1.
func (f *File) SetCellEmbedFormula(sheet, cell, objectType string) error {
if objectType == "" {
objectType = "Package"
}
formula := fmt.Sprintf("EMBED(\"%s\",\"\")", objectType)
return f.SetCellFormula(sheet, cell, formula)
Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing input validation for sheet and cell parameters. The function should validate these inputs before proceeding, similar to other cell functions in the codebase.

Copilot uses AI. Check for mistakes.

}
153 changes: 153 additions & 0 deletions picture.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ package excelize
import (
"bytes"
"encoding/xml"
"fmt"
"image"
"io"
"os"
Expand Down Expand Up @@ -1018,3 +1019,155 @@ func (f *File) getDispImages(sheet, cell string) ([]Picture, error) {
}
return pics, err
}

// EmbeddedObjectOptions defines the format set of embedded object.
type EmbeddedObjectOptions struct {
AltText string
PrintObject *bool
Locked *bool
ObjectType string // Default is "Package"
}

// AddEmbeddedObject provides a method to embed a file as an object in a cell.
// The embedded object will be accessible through Excel's EMBED formula.
// Supported object types include "Package" for general files. For example:
//
// package main
//
// import (
// "fmt"
// "os"
//
// "github.com/xuri/excelize/v2"
// )
//
// func main() {
// f := excelize.NewFile()
// defer func() {
// if err := f.Close(); err != nil {
// fmt.Println(err)
// }
// }()
//
// // Read file to embed
// file, err := os.ReadFile("document.pdf")
// if err != nil {
// fmt.Println(err)
// return
// }
//
// // Add embedded object
// if err := f.AddEmbeddedObject("Sheet1", "A1", "document.pdf", file,
// &excelize.EmbeddedObjectOptions{
// ObjectType: "Package",
// AltText: "Embedded PDF Document",
// }); err != nil {
// fmt.Println(err)
// return
// }
//
// if err := f.SaveAs("Book1.xlsx"); err != nil {
// fmt.Println(err)
// }
// }
func (f *File) AddEmbeddedObject(sheet, cell, filename string, file []byte, opts *EmbeddedObjectOptions) error {
if opts == nil {
opts = &EmbeddedObjectOptions{
ObjectType: "Package",
PrintObject: boolPtr(true),
Locked: boolPtr(true),
}
}
if opts.ObjectType == "" {
opts.ObjectType = "Package"
}
if opts.PrintObject == nil {
opts.PrintObject = boolPtr(true)
}
if opts.Locked == nil {
opts.Locked = boolPtr(true)
}

// Add embedded object to package
objPath := f.addEmbeddedObject(file, filename)

// Add relationships
sheetXMLPath, _ := f.getSheetXMLPath(sheet)
sheetRels := "xl/worksheets/_rels/" + strings.TrimPrefix(sheetXMLPath, "xl/worksheets/") + ".rels"
rID := f.addRels(sheetRels, SourceRelationshipOLEObject, "../"+objPath, "")

// Set EMBED formula in cell
formula := fmt.Sprintf("EMBED(\"%s\",\"\")", opts.ObjectType)
if err := f.SetCellFormula(sheet, cell, formula); err != nil {
return err
}

// Add OLE object to worksheet
ws, err := f.workSheetReader(sheet)
if err != nil {
return err
}

if ws.OleObjects == nil {
ws.OleObjects = &xlsxInnerXML{}
}

// Create OLE object XML content
oleObjectXML := fmt.Sprintf(`<oleObject progId="Package" dvAspect="DVASPECT_ICON" link="false" oleUpdate="OLEUPDATE_ONCALL" autoLoad="false" shapeId="1025" r:id="rId%d"/>`, rID)
Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded shapeId="1025" could cause conflicts if multiple OLE objects are added to the same worksheet. Consider generating unique shape IDs or making this configurable.

Copilot uses AI. Check for mistakes.

Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The progId is hardcoded as "Package" but should use the ObjectType from the options parameter to allow for different object types.

Suggested change
oleObjectXML := fmt.Sprintf(`<oleObject progId="Package" dvAspect="DVASPECT_ICON" link="false" oleUpdate="OLEUPDATE_ONCALL" autoLoad="false" shapeId="1025" r:id="rId%d"/>`, rID)
oleObjectXML := fmt.Sprintf(`<oleObject progId="%s" dvAspect="DVASPECT_ICON" link="false" oleUpdate="OLEUPDATE_ONCALL" autoLoad="false" shapeId="1025" r:id="rId%d"/>`, opts.ObjectType, rID)

Copilot uses AI. Check for mistakes.

if ws.OleObjects.Content == "" {
ws.OleObjects.Content = oleObjectXML
} else {
ws.OleObjects.Content += oleObjectXML
}

// Add content type for embedded object
return f.addContentTypePartEmbeddedObject()
}

// addEmbeddedObject adds embedded object file to the package and returns the path.
func (f *File) addEmbeddedObject(file []byte, filename string) string {
count := f.countEmbeddedObjects()
objPath := "embeddings/oleObject" + strconv.Itoa(count+1) + ".bin"
Copy link
Preview

Copilot AI Jul 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filename parameter is not used in the function implementation. Either remove it or use it for better object identification in the generated path.

Suggested change
objPath := "embeddings/oleObject" + strconv.Itoa(count+1) + ".bin"
// Sanitize filename to remove invalid characters and extensions
baseName := strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename))
baseName = strings.ReplaceAll(baseName, " ", "_") // Replace spaces with underscores
objPath := "embeddings/" + baseName + "_" + strconv.Itoa(count+1) + ".bin"

Copilot uses AI. Check for mistakes.

f.Pkg.Store("xl/"+objPath, file)
return objPath
}

// countEmbeddedObjects counts the number of embedded objects in the package.
func (f *File) countEmbeddedObjects() int {
count := 0
f.Pkg.Range(func(k, v interface{}) bool {
if strings.Contains(k.(string), "xl/embeddings/oleObject") {
count++
}
return true
})
return count
}

// addContentTypePartEmbeddedObject adds content type for embedded objects.
func (f *File) addContentTypePartEmbeddedObject() error {
content, err := f.contentTypesReader()
if err != nil {
return err
}
content.mu.Lock()
defer content.mu.Unlock()

// Check if bin extension already exists
var binExists bool
for _, v := range content.Defaults {
if v.Extension == "bin" {
binExists = true
break
}
}

if !binExists {
content.Defaults = append(content.Defaults, xlsxDefault{
Extension: "bin",
ContentType: ContentTypeOLEObject,
})
}

return nil
}
67 changes: 67 additions & 0 deletions picture_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,3 +615,70 @@ func TestGetImageCells(t *testing.T) {
assert.EqualError(t, err, "XML syntax error on line 1: invalid UTF-8")
assert.NoError(t, f.Close())
}

func TestAddEmbeddedObject(t *testing.T) {
f := NewFile()
defer func() {
assert.NoError(t, f.Close())
}()

// Test data for embedding
testData := []byte("This is a test document content")

// Test adding embedded object with default options
err := f.AddEmbeddedObject("Sheet1", "A1", "test.txt", testData, nil)
assert.NoError(t, err)

// Verify the EMBED formula was set
formula, err := f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, `EMBED("Package","")`, formula)

// Test adding embedded object with custom options
err = f.AddEmbeddedObject("Sheet1", "B1", "document.pdf", testData,
&EmbeddedObjectOptions{
ObjectType: "Package",
AltText: "Embedded PDF Document",
})
assert.NoError(t, err)

// Verify the second EMBED formula
formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, `EMBED("Package","")`, formula)

// Test with invalid sheet name
err = f.AddEmbeddedObject("", "A1", "test.txt", testData, nil)
assert.EqualError(t, err, ErrSheetNameBlank.Error())

// Test with invalid cell reference
err = f.AddEmbeddedObject("Sheet1", "", "test.txt", testData, nil)
assert.EqualError(t, err, `cannot convert cell "" to coordinates: invalid cell name ""`)
}

func TestSetCellEmbedFormula(t *testing.T) {
f := NewFile()
defer func() {
assert.NoError(t, f.Close())
}()

// Test setting EMBED formula with default Package type
err := f.SetCellEmbedFormula("Sheet1", "A1", "")
assert.NoError(t, err)

formula, err := f.GetCellFormula("Sheet1", "A1")
assert.NoError(t, err)
assert.Equal(t, `EMBED("Package","")`, formula)

// Test setting EMBED formula with custom object type
err = f.SetCellEmbedFormula("Sheet1", "B1", "Document")
assert.NoError(t, err)

formula, err = f.GetCellFormula("Sheet1", "B1")
assert.NoError(t, err)
assert.Equal(t, `EMBED("Document","")`, formula)

// Test with invalid sheet name
err = f.SetCellEmbedFormula("", "A1", "Package")
assert.EqualError(t, err, ErrSheetNameBlank.Error())
}
3 changes: 3 additions & 0 deletions templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const (
ContentTypeTemplateMacro = "application/vnd.ms-excel.template.macroEnabled.main+xml"
ContentTypeVBA = "application/vnd.ms-office.vbaProject"
ContentTypeVML = "application/vnd.openxmlformats-officedocument.vmlDrawing"
ContentTypeOLEObject = "application/vnd.openxmlformats-officedocument.oleObject"
NameSpaceDrawingMLMain = "http://schemas.openxmlformats.org/drawingml/2006/main"
NameSpaceDublinCore = "http://purl.org/dc/elements/1.1/"
NameSpaceDublinCoreMetadataInitiative = "http://purl.org/dc/dcmitype/"
Expand All @@ -88,6 +89,8 @@ const (
SourceRelationshipTable = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table"
SourceRelationshipVBAProject = "http://schemas.microsoft.com/office/2006/relationships/vbaProject"
SourceRelationshipWorkSheet = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheet"
SourceRelationshipOLEObject = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject"
SourceRelationshipPackage = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package"
StrictNameSpaceDocumentPropertiesVariantTypes = "http://purl.oclc.org/ooxml/officeDocument/docPropsVTypes"
StrictNameSpaceDrawingMLMain = "http://purl.oclc.org/ooxml/drawingml/main"
StrictNameSpaceExtendedProperties = "http://purl.oclc.org/ooxml/officeDocument/extendedProperties"
Expand Down