Custom Properties
Custom properties are name and value pairs an author or a program attaches to a
document. Unlike the built-in properties, the names
are yours: “Reviewer”, “ContractID”, “ExpiryDate”. Word shows them under Custom
in the advanced properties dialog and can insert them into the body as
DOCPROPERTY fields, which makes them a lightweight way to carry structured data
with a document.
They live in docProps/custom.xml, a part that many documents simply do not
have. Document.GetOrCreateCustomProperties creates the part, its content type
override and its relationship on first use, so it is the call to reach for
whether the document already has properties or not.
Every value is typed, and the type is part of the name of the setter you call.
| Setter | Go type | Stored as |
|---|---|---|
SetPropertyAsLpwstr | string | vt:lpwstr |
SetPropertyAsI4 | int32 | vt:i4 |
SetPropertyAsR8 | float64 | vt:r8 |
SetPropertyAsBool | bool | vt:bool |
SetPropertyAsDate | time.Time | vt:filetime |
Those five cover nearly everything. SetPropertyAsLpwstr is the right one for
text, since lpwstr is the Unicode string Word writes; SetPropertyAsLpstr
exists but produces the narrow variant. SetPropertyAsDate deliberately stores
a vt:filetime rather than a vt:date, because Word does not display vt:date
at all. The full set covers the rest of the variant types, including
SetPropertyAsVector and SetPropertyAsArray for compound values.
Reading and writing
doc, err := document.Open("document.docx")
if err != nil {
log.Fatalf("error opening document: %s", err)
}
defer doc.Close()
cp := doc.GetOrCreateCustomProperties()
if p := cp.GetPropertyByName("Company"); p.X() != nil {
fmt.Println("Company:", *p.X().PropertyChoice.Lpwstr)
}
cp.SetPropertyAsLpwstr("Reviewer", "Jane Smith")
cp.SetPropertyAsDate("Reviewed", time.Now())
doc.SaveToFile("document_customized.docx")Reading is where this API asks the most of you. GetPropertyByName returns a
CustomProperty, and the value hangs off X().PropertyChoice as one non-nil
pointer among the variant fields, so you have to know which type was stored and
pick the matching field. There is no AsString() or similar.
PropertiesList() returns every property as []*custom_properties.CT_Property
when you want to enumerate rather than look up, which is also the way to discover
what types a document actually uses.
Setting a name that already exists replaces its value in place; setting a new one appends. The same call does both, so there is no add-or-update decision to make.
Nothing about this is specific to documents opened from disk. On a document from
document.New() the first GetOrCreateCustomProperties creates the part and the
rest is identical.
Limitations
GetPropertyByName returns a zero-value CustomProperty when the name is not
found, and its X() is nil. The dereference chain that reads a value therefore
panics on a missing name rather than returning an error, which is why the guard
above checks X() != nil first. The example prints the zero value for a
deliberately absent name to show what it looks like.
Names match exactly, including case. “Company” and “company” are two different properties.
CustomProperty, the type GetPropertyByName returns, is marked deprecated in
its own doc comment while remaining the only way to read a single property by
name. Treat the deprecation as a signal that the read side may change, not as a
reason to avoid it.
Replacing an existing property renumbers it. The replacement is written with a
fresh pid one past the highest in use rather than keeping the property’s own,
so the document ends up with a gap in the pid sequence. Word tolerates this and
the property continues to work, but a document round-tripped repeatedly will
drift its pid values upward.
SetPropertyAsDate converts to UTC and truncates to whole seconds before
writing, on the grounds that local times and fractional seconds both upset
Office. SetPropertyAsFiletime writes what you give it, if you need the value
preserved exactly.
Run the example
The example reads four properties out of document.docx, prints the zero value
returned for a name that does not exist, replaces Company, adds four new
properties of four different types and saves to document_customized.docx. It
then does the same on a document from document.New() to show that the flow does
not differ.
git clone https://github.com/unidoc/unioffice-examples.git
cd unioffice-examples/document/doc-custom-properties
go run main.goIf this is your first time using UniOffice, follow the getting started guide to create an API key and set up your development environment.
View the full source
Sample output
AppVersion 00.500
Company Unidoc
DocSecurity 0
LinksUpToDate false
Non-existent {<nil>}
Company Another company{<nil>} is the zero CustomProperty printed for the missing name. Reading a
value off it would have panicked.