NAME
ODF::lpOD::Tutorial - A few basic recipes about lpOD
DESCRIPTION
This tutorial is intended to provide the user with a basic understanding of the lpOD tool set through simple examples that execute various operations in documents. It's not a reference manual; it's just intended to introduce the big picture and allow the user to get started and produce something before going through the man pages for details. The features described here are only a subset of the lpOD functional coverage.
Note that our code examples use Perl 5.10 syntax, so don't forget use 5.010.xxx
if you use them unchanged (and remember that lpOD requires Perl 5.10 or later).
Global document operation
This chapter introduces ways to get a global access to a document, to create a new document, to get or set the global document metadata. It illustrates features that are documented in ODF::lpOD::Document.
Loading documents and parts
Before accessing to anything in a document, you have to create an odf_document
instance through the odf_get_document()
constructor with the path/filename as the only mandatory argument:
my $doc = odf_get_document("olddoc.odt")
or die "Failed to load the document\n";
Note that odf_get_document()
is a wrapper for the get()
method of the odf_document
class, and that the instruction above could be written as shown below:
my $doc = odf_document->get("olddoc.odt")
or die "Failed to load the document";
In a real application, don't forget to check the return value. If something goes wrong for any reason (for example, if the specified file is not available or doesn't contain a consistent ODF document), odf_get_document
returns undef
.
In usual applications, you may have to select a particular workspace in the document. The workspaces are called parts here. The most commonly used are represented by lpOD symbolic constants: CONTENT
, STYLES
, META
, and others. If you need to insert, search, process or delete some document content, you need to select the CONTENT
part, through the get_part()
document method:
my $content = $doc->get_part(CONTENT);
We'll see how to go through document parts and deal with the various components later in this introduction.
The most part of the content-oriented operations are context-based, i.e. they are executed from previously selected elements and not at the global document level.
As long as we just need, say, to deal with the document content, and we have nothing to do with styles, metadata and other stuff, we can use get_body()
that returns the so-called document body, that is the top context for every object that appear in the page bodies:
my $context = $doc->get_body;
This shortcut, which selects the body element of the document content, is a synonym of:
my $context = $doc->get_part(CONTENT)->get_body;
With lpOD, it's often question of context. Almost every operation is executed through a method whose calling object is the context. The document (from a programmatic point of view) looks like a Russian doll; it's the top level of a hierarchy of contexts. It contains parts, that are level 2 contexts. A part contains a main element, so-called root. The root is the top of a hierarchy of elements. Every element, including the root, belongs to the odf_element
class and may be the context for a large set of operations regarding its own features or its sub-elements (so-called children). In the CONTENT
part, the body (that is a particular child of the root), is the most usual context for operations regarding the displayable content of a document. However each element contains, or can contain, one or more elements, and so on; as a consequence, each element, once selected, may become the context for further element searching or processing operations.
The "document body" is the subspace that contains or can contain the various objects that populate the document content (text paragraphs, tables, images, lists, and so on). We can use it as the "context" for subsequent operations regarding such objects.
Saving a document
When you load a document instance through odf_get_document()
, you just get a read-only connector to this resource. If some particular parts are loaded using get_part()
, and if some elements in these parts are created, updated, moved or deleted, all the changes are done in memory. In order to commit the changes and make them persistent, you must activate the save()
document method:
$doc->save;
This method, like a typical office software, replaces the old file with a new one that reflects the changes made in every part.
Note that it's possible to save the changes for some parts and to dismiss the changes made in other parts. As an example, we could want to do some data transformations and extractions in a large document content, then update the document metadata and forget the changes made in the content. We just need to switch off the update flag of the CONTENT
part using the needs_update()
accessor (that is available for each part):
$doc->get_part(CONTENT)->needs_update(FALSE);
$doc->save;
The save()
method is prevented from writing back any part whose update flag is FALSE
. Of course this flag may be reset to TRUE
through at any time through a subsequent use of needs_update()
.
Note that it's a good practice to switch off the update flag of the CONTENT
part as long as you just need it for read only, in order to avoid useless processing.
While the default target of save()
is the source file itself, you may specify an alternative output through a target
optional parameter:
$doc->save(target => "newdoc.odt");
In such a case, the source file remains unchanged. A new one is created, reflecting all the possible changes (with the exception of changes made in parts whose update flag had been set to FALSE
, if any).
Creating a new document
When you want to create a new document, you must use the odf_new_document()
constructor, with a mandatory argument that specifies the document type. The possible document types are 'text'
, 'spreadsheet'
, 'presentation'
, or 'drawing'
. As an example, the following instruction creates a new spreadsheet document:
my $doc = odf_new_document('spreadsheet');
This constructor returns a odf_document
, just like odf_get_document()
. However, there is no associated source file, so when you want to save()
it you must provide the target
parameter.
The same job could be done using another style:
my $doc = odf_document->create('spreadsheet');
The example below could be a one-liner program that creates and saves a new empty ODF presentation:
odf_new_document('presentation')->save(target => "demo.odp");
Leaving a document
When your application no longer needs to do anything with a previously loaded or created document, no particular action is required. However, in a process that handles multiple successive documents, it's strongly recommended to execute an explicit call of an instance destructor, namely the forget()
method, for each document that is no longer used:
$doc->forget;
Unless this explicit instruction, you could be faced with significant memory leaks.
Playing with document metadata
An office document owns a set of so-called "metadata". Metadata is "data about the document". For the end user, it may be got (and sometimes changed) through the "File/Properties" sub menu of a typical desktop software. lpOD allows the programmer to select, read or write any piece of metadata.
Pre-defined metadata
A document may contain some global metadata. The most commonly used ones are the title, the subject, the description, the creation date, the modification date, the creator, and others. All these data belong to the META
document part. We can get access to any of them through the META
context. Note that the META
document part is directly usable as the context for metadata access, so we don't need to look for a particular body element in this part:
my $meta = $doc->get_part(META);
From this context, a set of get/set, self-documented accessors, is available. The first instruction below displays the existing document title, and the second one sets a new title that replaces the old one:
say $meta->get_title;
$meta->set_title("New Title");
The one-liner below displays the document title:
say odf_get_document("mydoc.ods")->get_part(META)->get_title;
Some set_xxx
metadata accessors provide default values when called without argument. As examples, set_modification_date()
, that sets the date/time of last modification, automatically puts the current system date, while set_editing_cycles()
automatically increments the document revision count by one, when called without explicit values:
$meta->set_modification_date;
$meta->set_editing_cycles;
The set_creator()
accessor specifies the author of the last modification. It may be used in order to write any arbitrary string but, without argument, if uses the name of the system user of the current process (provided that Perl can get such an information from the operating system).
Note that lpOD provides such accessors as set_creation_date()
or set_initial_creator()
that allows the programmer to change the date and the author's name of the initial version, that is generally not allowed with an interactive graphical editing software.
A piece of metadata whose data type is date should be locale-neutral; it's stored according to the ISO-8601 format. It's returned as is by the read accessors. However, the write accessors allow the user to provide a numeric date (that is automatically converted). In addition, lpOD provides a numeric_date()
function that translates an ISO date into a numeric date.
lpOD deliberately allows you to provide any arbitrary value for any piece of metadata. If you want to set a creation date that is later than the last modification date, or decrement the editing cycle count, it's your choice.
Document keywords
In the META
context, you can use or change the document keywords. Assuming that $meta
is the result of a previous call to get_part(META)
, an instruction like $meta-
get_keywords()> returns the full list of existing keywords. In scalar context, this list is produced as a single comma-separated string. In list context, get_keywords()
returns one item by keyword. So the following instructions export the keywords as a concatenated string, then in separate lines:
say scalar $meta->get_keywords;
say for $meta->keywords;
Thanks to check_keyword()
, that returns TRUE
if and only if a given keyword is present, the following script displays the name of every ODF document (i.e. every file whose name is like "*.od?") in the current directory whose metadata include a keyword provided through the command line:
foreach my $file (glob "*.od?") {
say "$file is OK !" if
odf_get_document($file)
->get_part(META)
->check_keyword($ARGV[0]);
}
Of course it's possible to add a new keyword:
$meta->set_keyword("ODF");
...as well as a list of keywords in a single instruction:
$meta->set_keywords("ODF", "The lpOD Project", "Perl");
Custom metadata
Besides the standard common document metadata, lpOD allows the user to get or set custom, so called user defined metadata, through get_user_fields()
and set_user_fields()
. The user defined metadata look like a 3-column table whose properties are name, value, and type.
get_user_fields()
returns a list whose each item is 3-element a hash ref containing these informations. So, the example below displays the full set of user-defined metadata (names, values and types):
my $meta = $doc->get_part(META);
foreach my $uf ($meta->get_user_fields) {
say "Name : $uf->{name} " .
"Value : $uf->{value} " .
"Type : $uf->{type}";
}
Symmetrically, set_user_fields()
creates or resets some custom metadata from a list of hash refs with the same structure:
$meta->set_user_fields
(
{
name => 'Author',
value => 'The lpOD team',
},
{
name => 'Production date',
value => time,
type => 'date'
},
{
name => 'Organization',
value => 'The lpOD Consortium'
}
);
As you can see, the type
has not been provided for every field in this last example, because the default type is string
, that is convenient for our "Author" and "Organization" fields.
Of course, it's possible to set an individual custom value using set_user_field()
, with a simpler syntax. This setter requires a field name, a value, and optionally a data type (default is string
), as shown below:
$meta->set_user_field("Classified", FALSE, "boolean");
$meta->set_user_field("Invoice amount", 123.45, "float");
$meta->set_user_field("Project name", "lpOD", "string");
Inserting text
We'll have a look at some basic text component handling. Doing so we'll discover some features that are not text-specific and that introduce more general aspects of the lpOD element management logic.
Beware: the present chapter is not exclusively about text documents. In any ODF file, the paragraph is the basic text container. For example, the visible text content of any table cell is made of paragraphs, so a typical spreadsheet document (that is a set of tables) contains a lot of paragraphs. Similarly, the text content of every item in a bulleted list is a set of one or more paragraphs, so a presentation document, that does a massive use of such lists, is mostly a particular way to organize paragraphs. More generally, almost any displayable or printable text in any kind of ODF document is stored in paragraphs. As a consequence, paragraph handling functions are part of the most essential ones.
The objects and methods introduced in this chapter are mainly documented in ODF::lpOD::TextElement. However, we'll use a few common methods that belong to any document element, whose reference documentation is provided in ODF::lpOD::Element.
Inserting a new paragraph
Any visible text belongs to a paragraph that in turn is attached somewhere in a context.
The most simple recipe is the "Hello World" example that creates and insert a paragraph at the beginning of the document body.
First we need to select a context, i.e. a particular element that will become the "container" of the new paragraph. In our example, we just need the document body:
my $context = $doc->get_part(CONTENT)->get_body;
We assume that $doc
is a document object, previously initialized using odf_get_document()
or odf_new_document()
. Note that we could use a shortcut for the instruction above:
my $context = $doc->get_body;
get_body()
, when directly used as a document method, automatically selects the body of the CONTENT
part.
Now we can create a paragraph with the odf_create_paragraph()
constructor:
my $p = odf_create_paragraph;
According to your personal programming style, you could prefer the following instruction, that is equivalent:
my $p = odf_paragraph->create;
Then we can attach this paragraph (that is initially created "nowhere") at the place of our choice, that is the document body:
$context->insert_element($p);
Note that insert_element()
allows us to insert any kind of element anywhere. Without explicit explicit option, the given object is inserted as the first child of the context, so the new paragraph will appear at the very beginning of our document (whatever the existing content). We could use append_element()
, that puts the new element at the end of the context. In addition, insert_element()
can take optional parameters allowing the user to specify a particular position, before or after another element, already existing in the context.
However, this paragraph is empty, because we created it without text. We can populate it later using the set_text()
method:
$p->set_text("Hello World !");
Note that set_text()
works with any element, but with various effects. In any case, it deletes any previous content and replaces it with the given text string. If set_text()
is used directly from a high level context element, such as the document body, it just erases everything visible. So, the following sequence deletes any previous content before inserting our new paragraph:
$context->set_text("");
$context->insert_element($p);
The odf_create_paragraph()
constructor allows us to create a paragraph and initialize it with a content and/or a style, thanks to appropriate options:
$p = odf_create_paragraph(
text => "Hello World !",
style => "Standard"
);
Using an alternative style, the same job could be done like that:
$p = odf_paragraph->create(
text => "Hello World !",
style => "Standard"
);
The style
option requires the name of a paragraph style (that is, or will be, defined elsewhere). We'll have a look to text styles in another recipe. You can create text element without explicit style name as long as the default presentation of your favorite viewing/editing software is convenient for you.
Of course it's possible to change the text and/or the style later using set_text()
and/or set_style()
. However these optional parameters allow the user to create/populate/insert paragraphs in a more compact way:
$context->insert_element(
odf_create_paragraph(
text => "Hello World !",
style => "Standard"
)
);
Note that, by default, insert_element()
anchors the given object at the first position under the calling context, so after the instruction above the new paragraph becomes the first element of the document body. On the other hand, append_element()
puts the new element at the end of the context. Of course in real applications we need more flexibility. Fortunately there is an alternative use of insert_element()
. We can, for example, insert a new paragraph immediately before or after another paragraph. It's possible thanks to an alternative use of insert_element()
, that may be called from any kind of element instead of the document body. In the next example, we select a particular paragraph (say, the last one) then we insert a new paragraph before it:
my $p = $context->get_paragraph(position => -1);
$p->insert_element(
odf_create_paragraph(
text => "We are before the last paragraph",
style => "Standard"
),
position => PREV_SIBLING
);
Here the calling context is a paragraph. Of course we don't want to insert the new paragraph inside this context (lpOD doesn't prevent you from doing so if you absolutely want, but you would probably get strange results, knowing that nested paragraphs don't work properly with ODF-compliant software). The position
option, whose value is set to PREV_SIBLING
("previous sibling" in conventional XML vocabulary), specifies that the insertion point must be out of the context, just before it and at the same hierarchical level. We could set position
to NEXT_SIBLING
, resulting in an insertion after the context.
Note that insert_element()
allows a before
or after
optional parameter, allowing to insert the element before or after an element that is not the context. So the code example below produces the same result as the previous example:
my $p = $context->get_paragraph(position => -1);
$context->insert_element(
odf_create_paragraph(
text => "We are before the last paragraph",
style => "Standard"
),
before => $p
);
See ODF::lpOD::Element for more details about insert_element()
.
Cloning text elements
Paragraphs and headings are particular ODF elements (elements that can belong to a ODF document). An ODF element is (unsurprisingly) an instance of the odf_element
class (that is a shortcut for ODF::lpOD::Element
).
Every odf_element
owns a clone()
method that produces an exact replicate of itself and all its content and properties. So the sequence below produces a content with 10 copies of the same paragraph:
my $p = odf_create_paragraph(
text => "Hello World !",
style => "Standard"
);
$context->append_element($p->clone) for 1..10;
Note that in this example the paragraph created by odf_create_paragraph()
remains "free": it's not anchored in any context while its copies are appended in the document.
The clone()
constructor allows you to copy a text element (like any other element) in a document for use in another document. The next example appends the copy of the first paragraph of a document at the end of another document:
$p = $doc1->get_body->get_paragraph->clone;
$doc2->get_body->append_element($p);
This example introduced get_paragraph()
. We'll see more details about the text element retrieval methods later. Just remember that this method, without any search parameter, selects the first paragraph of the calling context.
The clone()
method is simple and efficient for element replication in the current process execution, but it's not the appropriate tool as soon as we want to export a particular element for persistent storage or transmission through a network in order to reuse it later or in a remote location. Every odf_element
, including text elements, owns a serialize
method that returns the full XML description of itself and its whole content. This XML export (if stored or piped through any kind of transport service) may be used later as the only argument of a subsequent call odf_create_element()
generic element constructor, that rebuilds a new element that is an exact replicate of the exported one (unless somebody has modified the XML string in the mean time, of course). In the example below, a paragraph XML export is stored in a file, then the export is loaded (supposedly within another process) and inserted as a new element in a document:
# exporting process
$xml = $p->serialize;
open(FH, '>:utf8', "paragraph.xml");
print FH $xml;
close FH;
# importing process
open(FH, '<:utf8', "paragraph.xml");
$xml .= $_ while <$FH>;
close FH;
$p = odf_create_element($xml);
After that, the imported element may be attached anywhere using insert_element()
or append_element()
. (Note that it's strongly recommended to handle the XML exports in utf8
mode.)
Inserting text content with headings
A heading is a special paragraph, whose purpose is to be used as a main or intermediate title, and that may belong to a hierarchy of titles. It's created using odf_create_heading()
, that works like odf_create_paragraph()
but allows the user to specify additional parameters including the heading level
(whose default is 1, i.e. the top level).
The following sequence creates a text document from scratch and populates it with a level 1 heading, followed by a level 2 heading, then by a regular paragraph:
use ODF::lpOD;
my $doc = odf_new_document('text');
my $context = $doc->get_body;
$context->append_element(
odf_create_heading(
level => 1,
text => "Introduction",
style => "Heading 1"
)
);
$context->append_element(
odf_create_heading(
level => 2,
text => "Part One",
style => "Heading 2"
)
);
$context->append_element(
odf_create_paragraph(
text => "This is my text content",
style => "Text body"
)
);
$doc->save(target => "test_doc.odt");
Note that we used arbitrary style names in this examples; such styles may not be available in your documents. Paragraph style creation is introduced later.
Remember (once for all) that every odf_create_xyz
constructor is an alias for the create()
class method or odf_xyz
.
Encoding issues
In some situations, you could be faced with character encoding troubles when importing or exporting text content or attribute values.
Remember that, by default, the character set of your applications is utf8
. However, for any reason, you may need to put non-utf8 content in documents. For example, if you capture some text from a web page whose character set is iso-8859-1
and directly push it in a document through a paragraph creation, non-ASCII accented letters will be misrepresented. This issue is very easy to fix (provided that the source character set is known and supported):
lpod->set_input_charset('iso-8859-1');
Symmetrically, if you want to use the various lpOD content extraction methods in a non-utf8 environment, you can tell that to lpOD:
lpod->set_output_charset('iso-8859-15');
Note that the input and output character sets may be controlled independently, and that you can change each of them several times in a program. To restore the default configuration, you may just use the same methods with 'utf8' as input or output character set.
Retrieving text elements
Any existing text element may be looked for and selected according to various criteria.
The most straightforward is the position in the document sequence. The instruction hereafter retrieves the 3rd paragraph of the document:
$p = $doc->get_body->get_paragraph(position => 2);
Why 2 and not 3 ? Just because the position number of the 1st element is 0 and not 1. The position
parameter accepts negative values, that specify positions counted back from the end. So, the next instruction selects the very last paragraph of the document:
$p = $doc->get_body->get_paragraph(position => -1);
Our search context is always, up to now, the whole document body. Remember that this context may be anything more restrictive (such as a particular section, if any). Generally speaking, the search context may be any element that can directly or indirectly contain text paragraphs. We'll see some of these high level structured containers later.
A paragraph may be searched according to other criteria than its position in the context. The following instruction selects the 5th paragraph that uses a given style and whose content matches a given regular expression (if any):
$p = $context->get_paragraph(
style => "Standard",
content => "ODF",
position => 4
)
or say "Not found !";
Of course, this instruction fails if the context contains less than 5 paragraphs matching the given style
and content
conditions.
If position
is omitted, get_paragraph()
returns the 1st paragraph that matches the other condition(s).
get_paragraph()
returns undef
if unsuccessful.
An other available selector, get_paragraphs()
, retrieves all the paragraphs matching the given content
and/or style
conditions. Of course, the position
option doesn't make sense here. The example below counts the paragraphs that contain "ODF" or "OpenDocument" and whose style is "Text body":
$count++ for $context->get_paragraphs(
style => "Text body",
content => "(ODF|OpenDocument)"
);
Headings may be selected in a similar way using get_heading()
and get_headings()
methods. The options are roughly the same, with the exception of style
that is replaced by level
. As an example, we can get all the level 2 headings that contain "ODF" like that:
@odf_titles = $context->get_headings(
level => 2,
content => "ODF"
);
Once retrieved, a paragraph or a heading may be changed, cloned, exported, or changed by various ways.
Any text element containing a text bookmark may be selected according the bookmark name, as shown below:
$p = $context->get_paragraph_by_bookmark("BM1");
Knowing that headings, like paragraphs, can contain bookmarks we don't know if the returned object (if any) is a flat paragraph or a heading. However it's very easy to check the object class if such an information matters. The last example could be continued like that:
unless ($p)
{
say "Not found !";
}
else
{
if ($p->isa(odf_heading))
{
say "It's a heading";
}
elsif ($p->isa(odf_paragraph))
{
say "It's a paragraph";
}
}
Remember that odf_heading
is a subclass of odf_paragraph
, so the order of the if...elsif
tests matters.
Styling text
In the ODF world, nothing regarding layout or presentation properties is mixed with text. So, every piece of text that is displayed with a particular presentation must be linked to an appropriate style, which is described elsewhere. In previous examples, we create paragraphs with given style names, assuming that the corresponding styles are, or will be available.
The style-related reference documentation is available in ODF::lpOD::Style.
Creating a paragraph style
The user can set the style of a paragraph at any time, not only at the creation time. So, assuming $p
is a paragraph picked up somewhere in a document, the following instruction applies a so-called "Bordered" style:
$p->set_style("Bordered");
(Note that get_style()
would return the current style of the element.)
But we just provide a style name, not a style. The paragraph will not be bordered just because I told it to apply a "Bordered" style.
Assuming that "Bordered" had never been defined for the current document, we have to create it and define its properties. We want to get a 0.5cm thick, dotted blue border line, separated from the content by a 0.5cm space.
The generic style constructor is odf_create_style()
(that is an exported alias for the create
constructor of the odf_style
class). It requires a style family identifier as its first argument, knowing that, say, a table style or a graphic style is not the same thing as a paragraph style. The style family specifies the kind of objects that could use it. Because we want to create a style for a paragraph layout, the family selector is 'paragraph'
. After the family selector, named parameters are allowed in order to describe the style. The most important one is the name
, knowing that a style can't be included in a document without a unique name (the style identifier for the objects that use it).
Once created, the style must be registered in the document style set. To do so, you must use the document-based insert_style()
method (don't use insert_element()
for styles). The following instruction creates the new paragraph style and includes it in the document:
$ps1 = $doc->insert_style(
odf_create_style('paragraph', name => 'Bordered')
);
As you can see, the document itself is (apparently) the context for style insertion. Technically, it's not true: lpOD automatically (and transparently for the user) selects the storage context, that depends on the style family. For some style families, there is more than one possible context and the user may specify particular choices through optional parameters, described in ODF::lpOD::Style, but you can safely ignore such details and work with the default lpOD behavior for a time.
Note that c<insert_style()> returns the inserted style element, so the application can keep it in order to invoke it later. The instruction above produces the same effect as the sequence below:
$ps1 = odf_create_style('paragraph', name => 'Bordered');
$doc->insert_style($ps1);
Now "Bordered" is the name of a really available... but not really useful style, because we didn't specify its features. As a consequence, any paragraph that will use it will be displayed according to the default paragraph style of the document (if any). So we must create the style with the right definition:
$ps1 = $doc->insert_style(
odf_create_style(
'paragraph',
name => 'Bordered',
border => '0.1cm dotted #000080',
padding => '0.5cm'
)
);
The border
option must be set with a 3-part value: the first one is the thickness, the second one is a keyword that specifies the style of line, and the third one is the RGB color code. Note that with ODF (like with other de facto standards) a color code is a 6-digit hexadecimal value prefixed by "#". In this example the blue value is "80" while the red and green are zero. Note that lpOD provides a color_code()
utility that can provide the color codes corresponding to a few hundreds of conventional color names, so the same border
specification could be written like that:
border => '0.1cm dotted ' . color_code('navy blue')
The padding
option specifies the gap between the border and the content.
The "Bordered" style is OK. Now suppose that we want to display some paragraphs with all the default layout but centered, and some others centered and bordered with a blue, 0.1cm thick dotted line, separated from the content by a 0.5cm space. That is, the same properties as those of our "Bordered" style, and only one additional property.
Thanks to a align
option, we could create a new style that with all the needed features:
$ps2 = $doc->insert_style(
odf_create_style(
'paragraph',
name => 'BorderedCentered',
border => '0.1cm dotted #000080',
padding => '0.5cm',
align => 'center'
)
);
However, this method is not very elegant. Thanks to the ODF style inheritance logic, there is a better approach. Knowing that "BorderedCentered" differs from "Bordered" by one property, we can define it as a derivative of "Bordered", through the parent
option:
$ps2 = $doc->insert_style(
odf_create_style(
'paragraph',
name => 'BorderedCentered',
parent => 'Bordered',
align => 'center'
)
);
Once created, a paragraph style may be changed or enriched using various set_xxx()
methods. As an example, we could provide our styles with a pretty yellow background through the following instruction:
$ps1->set_background(color => '#ffff00');
Thanks to a color symbolic naming facility provided by lpOD, this instruction could be written in a more friendly way:
$ps1->set_background(color => 'yellow');
Of course this last form works only if the needed color has a registered name and if you know it.
Note that, due to inheritance, this new background color will affect both $ps1
and $ps2
.
In the examples above, we kept the return values of odf_create_style()
and insert_style()
. But we can retrieve an existing style thanks to the document get_style()
method, provided that we know the family and the name. So the next instruction will allow us to select (or check the existence of) our last created style:
$ps2 = $doc->get_style('paragraph', 'BorderedCentered');
Take care of possible misleading homonymies: the document based get_style()
method, that retrieves an existing style by family and name, is not the same as the element based get_style()
method, that returns the unique name of the style used by the element. So the right way to get the style object that controls the layout of a given paragraph is demonstrated by the following example:
$style_name = $paragraph->get_style;
$style = $doc->get_style('paragraph', $style_name);
The style name (in combination with the family name) is the key to select the style object in the document. We must select the object itself in order to check or change its properties, to clone it and/or to export it from a document to another document.
Creating and using a text style
While a paragraph style applies to a paragraph as a text container, a text style applies to the content, i.e. the text itself. Text styles are about character layout (mainly font-related properties) while paragraph styles are about alignment, background, borders, and so on.
A text style may be created in a similar way as a paragraph style, just replacing 'paragraph' by 'text' as the family name, like in this example:
$ts = odf_create_style('text', name => 'BigText');
or this one (that is the same):
$ts = odf_style->create('text', name => 'BigText');
In order to make this style useful, we have to provide some properties. As an example, we'll create this "BigText" style will set a 24pt bold blue font:
$ts = odf_create_style(
'text',
name => "BigText",
size => "24pt",
weight => "bold",
color => "navy blue"
);
But how apply this style to one or many text segment ? There are two options.
The first option consists of using it as a template to add a text-oriented area to an existing paragraph style. Doing so, we specify that all the properties of this text style will be the default properties of the whole text content of any paragraph using the modified paragraph style. Example:
$ps = $doc->get_style('paragraph', 'BorderedCentered');
$ps->set_properties(area => 'text', clone => $ts);
Now, even if the "BigText" text style is never inserted in the document, its properties (i.e. size
, weight
, color
) are register as the default properties for the text of every paragraph whose style is "BorderedCentered".
The second option consists of specifying a particular text span in a paragraph and to apply the text style for this segment only, not to the whole paragraph text, whatever the style of this paragraph and its possible default text properties. This option requires the use of set_span()
. The (arbitrary) example below shows the way to create and register a text style, then to apply it to any "OpenDocument" substring found in any paragraph whose style is "Standard":
$doc->insert_style(
odf_create_style(
'text',
name => "BigText",
size => "24pt",
weight => "bold",
style => "italic",
color => "navy blue"
)
);
foreach $p ($doc->get_paragraphs(style => "Standard")) {
$p->set_span(
filter => 'OpenDocument',
style => 'BigText'
);
}
set_span()
requires a style
parameter (that should be the name of a text style, existing or to be created). In this example the filter
option (whose value may be a string or a regexp) specifies that the given style must apply to any text span that matches. But other search and positioning options are allowed; see the "Text spans" section in ODF::lpOD::TextElement for details.
Up to now we introduced properties related to the character size, color, style and other details, but not the font choice. Of course, a font
optional parameter is available with odf_create_style()
as well as set_properties()
. Example:
$doc->insert_style(
odf_create_style(
'text',
name => "MyText",
size => "14pt",
font => "Times New Roman"
)
);
However, if you don't take care, your font specification could produce no visible effect in some situations. Of course you have checked the availability of the needed font in your environment. But, in any ODF document, a font must be declared to be used by a style. Fortunately such declaration is very easy to register thanks to the document-bases set_font_declaration()
method:
$doc->set_font_declaration("Times New Roman");
However, you should not systematically set a declaration for any font you borrow in your text style definitions, because your document probably contains previously declared fonts, knowing that set_font_declaration()
deletes and replaces any existing declaration for the same font, and some existing declarations may contain more properties than a single font name.
Back to paragraph style properties
Text styles are mainly intended to be used in text spans. The can be used to add text properties to paragraph styles, too, but creating a text style as a temporary object to be used as a template for the text properties of a paragraph style (and to be immediately forgotten) is a bit complicated and boring. set_properties()
may be used for direct definition of text properties:
$ps = $doc->get_style('paragraph', 'BorderedCentered');
$ps->set_properties(
area => 'text',
size => "24pt",
weight => "bold",
color => "navy blue"
);
The area
parameter, whose value should be 'paragraph' or 'text' with a paragraph style, acts a selector that, in this example, tells that all the other parameters are text style properties.
The default value of area
in set_properties()
depends on the family of the calling style object. Knowing that in our example $ps
is a paragraph style, if set_properties()
without area
(or with area
set to 'paragraph'), the other parameters will be regarded as regular paragraph properties. So it's possible to change the main properties of an existing paragraph style at any time, like in the example below that updates the border
and c<align> properties of our "BorderedCentered" style:
$ps = $doc->get_style('paragraph', 'BorderedCentered');
$ps->set_properties(
border => '0.5mm solid #00ff00',
align => 'justify'
);
Unfortunately, due to the change made in the align
property, the name "BorderedCentered" is no longer significant (for the end-user, not for lpOD and the office software, which are agnostic about the meaning of the style names). set_properties()
doesn't allow us to change the style name. However it's possible to force a new name for this style thanks to set_name()
, that works with styles like with some other kinds of ODF elements:
$ps->set_name("BorderedJustified");
On the other hand, changing a style name may be risky knowing that this name is used as an identifier by all the paragraphs that use it. However, if you absolutely want to rename a paragraph style you can retrieve the affected paragraphs and reconnect them:
$_->set_style("BorderedJustified")
for $doc->get_body
->get_paragraphs(
style => "BorderedCentered"
);
Remember set_style()
, that works with paragraphs and other elements and allows to change the style of the calling element at any time.
Bookmarking text elements
We introduced bookmarks as a way to select a particular element, knowing that every bookmark is identified by a name (unique for the document). Text bookmarks are easily inserted through the graphical interface of a text processor. lpOD provides the same facility for Perl programs, thanks to the set_bookmark()
paragraph method. The instruction below puts a bookmark that designates the 1st paragraph containing "ODF" in a document:
$p = $doc->get_body->get_paragraph(content => "ODF");
$p->set_bookmark("Found") if $p;
So we can later retrieve this paragraph (whatever the changes done in the mean time) like that:
$p = $doc->get_body->get_paragraph_by_bookmark("Found");
While set_bookmark()
requires only a bookmark name, it allows the user to specify positioning options. The default position is the beginning of the calling element (just before the first character). With the offset
optional parameter, it's possible to specify an insert position. The two following instructions puts a bookmark before the 5th character and another bookmark before the last character:
$p->set_bookmark("BM1", offset => 4);
$p->set_bookmark("BM2", offset => -1);
If we want to set a bookmark after the last character, the value of the offset
option must be set to 'end'
:
$p->set_bookmark("BM3", offset => 'end');
A bookmark may be set somewhere in a text element without knowledge of the numeric offset, and according to a search expression. To do so, the user must provide a before
or after
(mutually exclusive) parameter, whose value may be a text string or a regular expression. The following recipe puts a bookmark before the first "ODF" substring in a previously selected paragraph:
$p->set_bookmark("Here", before => "ODF");
Up to now, we introduced a bookmark as a positional mark only (i.e. like a flag set at a given offset in a given paragraph or heading). However, we can create bookmarks that include text, instead of designating a place in the text. To do so, we have to designate a text range instead of a single offset. There are various ways, but the easiest are a particular use of the offset
option, and/or a content
option. In the following example, a first bookmark covers a range starting from before the 4th character and ending before the 8th character, while a second bookmark covers the first occurrence of the "ODF" substring (if any):
$p->set_bookmark("BM1", offset => [3, 7]);
$p->set_bookmark("BM2", content => "ODF");
More sophisticated positioning features are allowed through various parameter combinations. Have a look at the Bookmarks section in ODF::lpOD::TextElement for details.
You should remember the bookmark positioning logic knowing that the same principles works with other objects such as index marks, bibliography marks, hyperlinks and text spans.
Setting hyperlinks in text content
lpOD allows us to easily set navigation links in text documents,thanks to the set_hyperlink()
method that works with paragraphs and headings. Hyperlinks are positioned in the same way as text spans introduced above, and the corresponding reference documentation is available in ODF::lpOD::TextElement.
This method selects the text segment that will support the link according to the same positioning logic as set_span()
(already introduced in this tutorial). It requires an additional parameter, that is the url
of the destination. As an example, the following sequence associates a link aimed at the home page of the ODF Technical Committee to every "ODF" or "OpenDocument" substring in every paragraph in a given document:
$_->set_hyperlink(
filter => 'ODF|OpenDocument',
url => 'http://www.oasis-open.org/committees/office'
)
for $doc->get_body->get_paragraphs;
This example uses an internet link, but it's possible to set navigation links of other kinds, including, of course, links aimed as various entry points in the current document, through the same url
parameter. URL values whose leading character is '#' are generally interpreted as links to somewhere in the current document content (section, title, etc). The following example sets a link from a given substring to a bookmark in the same document:
$paragraph->set_hyperlink(
filter => "jump to the bookmark",
url => "#BM1"
);
If you want to set a hyperlink associated to the whole content of the target paragraph, just use set_hyperlink()
without positioning option:
$paragraph->set_hyperlink(url => 'http://some.where');
Hyperlinks may have optional properties, that can be set through set_hyperlink
using various additional parameters (not introduced here).
Creating an item list
A list is a structured, hierarchical container. It contains a set of items. Each item in turn can contain paragraphs. An item content is generally displayed after either a so-called "bullet" (that is a particular character or an image) or an order number. The list-related functionality is documented in ODF::lpOD::StructuredContainer.
Like a paragraph (and many other objects), a list may be created through an appropriate constructor then inserted in a selected context using insert_element()
. So we can insert a new item list, say, after the first paragraph containing "ODF", like that:
$p = $doc->get_body->get_paragraph(content => "ODF");
$list = $p->insert_element(odf_create_list);
There is no mandatory argument for the odf_create_list()
constructor, but in real applications you should provide a style
parameter whose value is the name of a list style. Of course, it's never too late to set or change the style of an existing list, thanks to the set_style()
accessor, that works for various kinds of objects including paragraphs and lists.
This code inserts nothing more than an empty list. The list is a particular odf_element
whose class is odf_list
. We can populate it using its add_item()
method:
$list->add_item(text => "The first one", style => "Standard");
This sequence appends an items to the list, with an text content and a style. Technically, the given text is not directly the text of the list item. An item is a structural element only; it acts as the context of one or more paragraphs. The given parameters, if any, are used by add_item()
to create a paragraph. So the sequence below produces the same effect as the previous instruction:
my $item = $list->add_item;
$item->append_element(
odf_create_paragraph(
text => "The first one",
style => "Standard"
)
);
The following example takes a paragraph in a given context (possibly in the same document or elsewhere) and appends a copy of it as an item in our list:
$p = $context->get_paragraph(position => 4);
$list->add_item->append_element($p->clone);
A list item can contain a list instead of a paragraph (so we can create a hierarchy of lists). The sequence below creates a main list whose first item is a single paragraph and the second item is a sub-list that contains 3 item, each one being a single paragraph:
$list = $context->insert_element(odf_create_list);
$sublist = odf_create_list;
$list->add_item(text => "Main list, item 1");
$list->add_item->append_element($sublist);
$sublist->add_item(text => "Sub-list, item 1");
$sublist->add_item(text => "Sub-list, item 2");
$sublist->add_item(text => "Sub-list, item 3");
Note that an item can contain more than one object; it may host, for example, several paragraphs or, say, paragraphs and sub-lists.
As text processor end-users, we use to distinguish so-called "bulleted" lists and "numbered" (or "ordered") lists. With ODF (and lpOD), we just know lists. The optional prefixes of the list items, which may be numbers, character strings or images, depend on a list style, introduced in the next chapter.
Creating and using a list style
A list style doesn't specify the same style for every item in the lists that use it. It may specify a particular style for each hierarchical level in a list. So a list style is a set of list item styles. The list style reference documentation may be found in ODF::lpOD::Style.
Regular named list styles
However, a list style definition is very simple if we just want to get classical ordered items starting with a decimal number followed by a dot and a space, with one-level lists. In such case, the appropriate style should be created like that:
my $ls = odf_create_style('list', name => "MyNumberedStyle");
$doc->insert_style($ls);
$ls->set_level_style(
1,
type => 'number',
format => '1',
suffix => '. '
);
If this style (after registration in a document through insert_style()
) is used by a list, all the level 1 items will be numbered according to a sequence like "1. ", "2. ", "3. ", and so on. In addition, we can provide set_level_style()
with a style
option whose value should be the name of a text style (like "BigText", to reuse the text style that we created in a previous section). This option allows us to control the character layout of the item label (font, size, etc).
On the other hand, if we create a level 2 sub-list (i.e. a new list contained in an item of the main list), it will not be affected because we defined a level 1 style only. But it's not too late to define a level 2 item style linked to the same "MyNumberedStyle" list style. The following instruction specifies a level 2 whose numbering will look like "(a) ", "(b) ", "(c) ", and so on:
$ls->set_level_style(
2,
type => 'number',
format => 'a',
prefix => '(',
suffix => ') '
);
The prefix
and suffix
options allow us to specify characters strings (including multiple spaces) before and after the item label. However, we can control more precisely the label spacing and alignment through extended list level style properties which can be set using the level style based set_properties()
method. The example below defines a list level style with the same options as above and additional properties:
$ls->set_level_style(
2,
type => 'number',
format => 'a',
prefix => '(',
suffix => ') '
)
->set_properties(
'space before' => "1.5cm",
'min label width' => "4cm",
'min label distance' => '5mm',
align => 'end'
);
The additional properties for level 2 here specify that every item will be indented by 1.5cm, that a 4cm minimal space will be allocated for the label, that the label itself (prefix, number and suffix) will be located at the end of this 4cm space, and that the text of the item will be separated from the label by a 5mm minimal distance.
The list level style properties shown in the last example may be changed later, like that:
$ls->get_level_style(2)->set_properties(align => 'start');
This last instruction moves the item label from the end to the start of the space whose width is specified by 'min label width'
.
You can retrieve a registered list style using the document-based get_style()
method, with the style family and the style name as usual:
$ls = $doc->get_style('list', 'MyNumberedStyle');
Outline style
The so-called outline style is a particular list style, defined once for all in a text document. It controls the default layout of the title hierarchy, knowing that the titles (or headings) may be numbered and defined at different levels.
The outline style is generally present in every text document. A new document, just created using odf_new_document()
, automatically includes an outline style. However, you are allowed to create a new one through odf_create_style
and insert it using insert_style()
, like any other style. As soon as you insert a new outline style, it automatically replaces the old one (if any).
The first difference between the outline style and other list style is the name: there is only one outline style, so it doesn't need any identifying attribute. In other words, the outline style is the only one member of the outline
style family. As an consequence, you cat get it without name:
$os = $doc->get_style('outline');
Symmetrically, the outline style is created and inserted without name:
$os = $doc->insert_style(odf_create_style('outline'));
The (sometimes really useful) sequence below replicates the outline style of a document and puts it in another document, showing a smart way to normalize the title hierarchy according to a template document:
$os = $source_doc->get_style('outline')->clone;
$target_doc->insert_style($os);
The second difference with named list styles is that you can't select the type
of a list level style; this type is automatically set to 'number'
so you should omit the type
parameter of set_level_style()
when the context is an outline style.
Playing with tables
In the lpOD vocabulary, a table may be a particular sheet (or tab) in a spreadsheet document, as well as a so-called table in a text or presentation document. Spreadsheet tables bring the most powerful functionality, and the way interactive office suites handle and display tables heavily depends on the document type. However, for the lpOD user, the basic concepts and methods related to tables work similarly whatever the document type. In spite of this similarity, we recommend not to insert in a document a table copied from a document of another type; you would probably not get the desired result.
Beware that, while the structure and the content of a table are relatively easy to handle, the layout is very complicated knowing that everything is controlled by styles. A table and its components may depend on a very large set of styles: the table itself has a style, and every individual column, row and cell may have its own style. In addition, the text content of any cell is made of one or more paragraphs, each one associated with a paragraph style. As a consequence, the layout of a single table may depend on dozens of styles. If you need to create tables, we encourage you to use the GUI of your favorite office software in order to create template documents containing tables with the appropriate layout, then to reuse these tables instead of consuming the most part of your programming time in cosmetic stuff.
Regarding the structure and the content of the tables, the reference documentation may be found in ODF::lpOD::Table. The table, cell, column, and row styles are presented in the table-related styles section in ODF::lpOD::Style.
Table creation and expansion
The following example creates a table with a default layout, containing 5 columns and 12 rows:
$t = odf_create_table("MyTable", width => 5, length => 12);
Note that the first argument is a table name, that must be unique. The result is a table object, whose class is odf_table
(that is a synonym of ODF::lpOD::Table
). Then this new table may be inserted in a document as usual:
$doc->get_body->insert_element($t);
If the current document is a spreadsheet, the apparent size is not fixed, so the given table size doesn't produce visible effects. However, the really stored table is limited, so you can't reach any cell, row or column beyond the specified size. On the other hand, if the document type is text, the visible size should be the given one.
Once created, this table may be expanded by, say, 2 columns and 3 rows:
$t->add_column(number => 2);
$t->add_row(number => 3);
Note that, without argument, add_columns()
and add_row()
append respectively one column and one row at the end. The number
option allows us to insert more than one column or row in a single call. The new columns/rows are initialized with the same content and layout as the previous last column/row (you can change them later).
We can insert columns or rows somewhere within the table, not only at the end. To do so, we just have to specify a position. The next instruction inserts 4 new rows before the third existing row (knowing that the number of the first row is zero):
$t->add_row(number => 4, before => 2);
The new inserted rows are replicates of the previous 3rd row.
We can decide that the new rows must be inserted after the 3rd one:
$t->add_row(number => 4, after => 2);
In such case, the insertion point is not the same but the new rows are always initialized as copies of the reference row, that is the 3rd one (or, if you prefer, the row #2).
Table retrieval
From a given context, that may be for example the document body, or a particular section, or a previously selected table cell (knowing that a table cell may contain a table), lpOD allows to get a table through various methods. The simplest one is shown below:
$t = $context->get_table_by_name("MyTable");
The table name is unique, so it's the best possible identifier. However, we can select a table without knowledge of its name, and using its position in the order of the document. The first instruction below selects the first table (or sheet) of the document, while the second on selects the last table:
$t1 = $doc->get_body->get_table_by_position(0);
$t2 = $doc->get_body->get_table_by_position(-1);
Note that get_table()
is available as a shortcut for get_table_by_name()
.
In the worst situations, you could retrieve a table according to a particular content:
$t3 = $context->get_table_by_content("xyz");
This instruction returns the first table in the context whose at least one cell contains "xyz". Needless to say that such a selection method is not the most efficient.
You can get all the tables as a list:
@tables = $context->get_tables;
Table metadata
As soon as you get a table, you can check its global properties and change some of them.
The get_size()
method returns the number of rows and the number of columns:
($h, $w) = $context->get_table("MyTable")->get_size;
say "This table contains $h row and $w columns";
Note that there is no such method as set_size()
; a table may be expanded or reduced by explicit row or column insertion or deletion, using methods introduced later.
While get_name()
returns the present name of the table, set_name()
changes it:
$doc->get_body->get_table_by_position(-1)->set_name("The last");
Some interesting accessors allows you to get or set table protections. The sequence below switches the write protection on for every table in every spreadsheet ("*.ods") document in the current directory:
foreach my $file (<*.ods>) {
my $doc = odf_get_document($file);
foreach my $table ($doc->get_body->get_tables) {
$table->set_protected(TRUE);
}
$doc->save;
$doc->forget;
}
Note that in this example (that could have to run against a lot of documents) an explicit forget()
destructor call it issued for each processed instance in order to avoid memory leaks.
If a table is locked with a lost protection key, you can easily remove such protection:
$table->set_protected(FALSE);
$table->set_protection_key(undef);
You can very easily prevent anybody from unlocking the table when editing the document through an interactive spreadsheet processor:
$table->set_protected(TRUE);
$table->set_protection_key("foo bar");
The "protection key" that is stored this way is not the unlocking password itself; it's only a hash value of this password. So if you set an arbitrary string as the protection key you will probably never find a corresponding password. As a consequence, the table can't be unlocked through a typical office software (but, of course, you can unlock it using lpOD or any tool that can directly update ODF files).
Note that with OpenOffice.org such protection works for spreadsheets but not for tables included in text documents. And, of course, lpOD can't get any access to contents stored with a real cryptographic protection.
Individual cell access, read and update
A table, once selected, allows individual cell selection by coordinates thanks to get_cell()
. This method allows numeric or alphanumeric representation of the coordinates, so the two instructions below are equivalent whatever the document type (i.e. the alphanumeric, "spreadsheet-like" notation works with tables that belong, for example, to text documents as well):
$cell = $table->get_cell(0, 0);
$cell = $table->get_cell("A1");
Note that the numeric notation, which is zero-based, allows negative coordinates. So the following instruction returns the bottom right cell of a table (whatever the size), while the alphanumeric notation doesn't allow to get the last cell of the last row without knowledge of the table size:
$cell = $table->get_cell(-1, -1);
A selected table cell is a odf_cell
object (that is an alias for ODF::lpOD::Cell
). Such object, like a paragraph, provides a get_text()
method allowing to get its content as flat text:
say "The cell contains " . $table->get_cell("B4")->get_text;
We can get or set the text content of a table cell using get_text()
or set_text()
. But doing so we just use the cell as a text container, while it may be a typed data container. If, for example, the data type of a given cell is float
or currency
, the computable, internally stored value of the cell may differ from its text representation. This value, if any, may be read or updated using get_value()
or set_value()
. Thanks to the get_type()
accessor, we can check the data type and process the cell accordingly, like the sequence below which computes the grand total of all the cells whose type is float
or currency
in all the tables of a given document:
my $amount = 0;
my $count = 0;
my $filter = ['float', 'currency'];
foreach my $table ($doc->get_body->get_tables) {
my ($h, $w) = $table->get_size;
for (my $i = 0 ; $i < $h ; $i++) {
for (my $j = 0 ; $j < $w ; $j++) {
my $cell = $table->get_cell($i, $j);
if ($cell->get_type ~~ $filter) {
$count++;
$amount += $cell->get_value;
}
}
}
}
say "Found $amount in $count numeric cells";
This introductory tutorial is not focused on performances. However some code optimization is not useless with large spreadsheets. In the last examples, we get each individual cell from the context of a table, with 2D coordinates. it's interesting to know that cells are contained in rows, while rows are contained in tables (columns don't really keep content, they act as style linking objects). Each table row is a odf_row
object (that is an alias for ODF::lpOD::Row
), that provides its own version of get_cell()
; when the context is a row, get_cell()
needs only the horizontal position of the needed cell, and the search requires less computation. So, we could rewrote the last example as shown:
my $amount = 0;
my $count = 0;
my $filter = ['float', 'currency'];
foreach my $table ($doc->get_body->get_tables) {
my ($h, $w) = $table->get_size;
ROW: for (my $i = 0 ; $i < $h ; $i++) {
my $row = $table->get_row($i);
CELL: for (my $j = 0 ; $j < $w ; $j++) {
my $cell = $row->get_cell($j) or last CELL;
if ($cell->get_type ~~ $filter) {
$count++;
$amount += $cell->get_value;
}
}
}
}
We added loop labels for clarity. In addition, we appended or last
after get_cell($j)
in the inner loop, in order to avoid a crash and directly go to the next table row if there is no cell at the current $j
position. Why such a precaution, knowing that we can't iterate beyond $w
, that is the table width ? Practically, get_size()
returns the number of rows and the number of cells of the largest row and in some (dirty and hopefully exceptional) situations, we could get rows with less cells than the table width.
If you are faced with performance issues, look at the appropriate section in ODF::lpOD::Table. But please don't regard lpOD as a VLDB management system, and don't hope an instant response when you launch a full scan of a table including millions of cells.
In many situations, the user may want to get the value of an individual cells or the values of cell ranges, without updating the cells. As long as there is no update, the application doesn't need to get the cell object; it just need the value, that may be extracted using get_cell_value()
. So, the two instructions below produce the same result, but the second one may be more efficient in a very large table:
$value = $table->get_cell($i, $j)->get_value;
$value = $table->get_cell_value($i, $j);
This method is available with tables, columns and rows; its more efficient version is row-based.
The example below shows the way to extract the value of the bottom right cell of the first table in every text document of a given file list:
my $sum = 0;
my $count = 0;
foreach my $filename (glob('*.odt')) {
my $doc = odf_get_document($filename);
$sum += $doc ->get_body
->get_table_by_position(0)
->get_cell_value(-1, -1);
$doc->forget;
$count++;
}
say "The grand total is $sum in $count documents";
We assume that all the *.odt
files in the current directory are regular ODF documents containing one or more tables, and where the last cell of the last row of the first table is numeric and not empty. Of course a real application should do some checks and handle exceptions.
The get_cell_values()
method allows the user to extract a value list from all the cells corresponding to a specified data type in a specified range. So, the instruction below returns all the float
values from cell 'C' to cell 'X':
@values = $row->get_cell_values('float', 'C:X');
The first argument of get_cell_values()
is a ODF data type (string
, float
, currency
, etc). The second one is either an alphanumeric, spreadsheet-like cell range specification, or the numeric (zero-based) position of the first cell; of course if the second argument is numeric a third argument, specifying the numeric position of the last cell, is required. The result is a list containing only the defined values matching the given type (empty cells and cells whose data type doesn' match are ignored), so the length of the list may be less than the specified cell range.
This method may be used from rows, columns, or tables. See ODF::lpOD::Table for more information about its possible behavior in each context.
When get_cell_values()
is used in scalar context with the float
, currency
, or percentage
data type, it returns an array ref whose items are (in this order), the number of non-empty matching cells, the minimum, the maximum and the sum. So the sequence below provides the average amount of the currency
cells belonging to the 'E2:G10' range:
$r = $table->get_cell_values('float', 'E2:G10');
unless ($r->[0])
{
warn "No value !\n"; $average = undef;
}
else
{
$average = $r->[3] / $r->[0];
}
Note that get_cell_values()
may be used without type restriction; to do so, the user must provide 'all'
as first argument instead of a data type. See ODF::lpOD::Table for details regarding the behavior of this method when, due to the data type, the sum doesn't make sense.
Selecting rows according to the content of a column
"What is the price of the product XYZ ?", that could be reworded as "What contains the cell 'H' in the row where the cell 'B' contains XYZ ?", assuming that, in a spreadsheet, the 2nd cell (column 'B') is occupied by a product ID while the 8th cell (column 'H') contains the price, could be answered like that:
$row = $table->get_row_by_index(B => 'XYZ');
if ($row) {
$price = $row->get_cell('H')->get_value;
} else {
alert "Product XYZ not found";
}
We could select all the rows whose a specified cell matches a given expression or numeric value:
@rows = $table->get_rows_by_index(FF => 'John');
get_row_by_index()
and get_rows_by_index()
take a column number or alphabetical identifier as first argument, and a search value as second argument (allowing a hash syntax). It allows the user to regard a specified column as it was an "index" for row selection. Beware that the search value, if the data type of the "index" column is string
or date
, is processed as a regexp; so "Johnny", "Johnnie" and "Johnson" will match if the search string is "John".
Table layout
Now let's have a look at the way to specify some basic presentation properties of a table. First of all, remember that everything related to layout is controlled by styles. On the other hand, knowing that a table is a compound element, its final layout depends not only on a single table style, but on the styles that are individually used by its columns, rows and cells.
Defining the full layout of a complex table and everyone of its components programmatically is a very tricky business. Each time you need feature-rich and beautiful tables, you should use document templates, previously designed using an interactive, ODF-compliant spreadsheet or text processor, instead of creating everything from scratch with Perl. But, of course, styling a very simple table or adjusting a particular presentation detail in a previously decorated table is not a headache. The present subsection introduces a few simple recipes and illustrates the basic principles.
Table graphic size control
As an illustration, we'll create, in a text document, a 3-column table that will be centered in the page and whose width will be 80% of the page width:
$doc = odf_new_document('text');
$context = $doc->get_body;
$table = $context->insert_element(
odf_create_table(
"MyTable",
width => 3,
length => 12,
style => "MyTableStyle"
)
);
$doc->insert_style(
odf_create_style(
'table',
name => "MyTableStyle",
width => '80%',
align => 'center'
)
);
Note that this style definition wouldn't produce visible effects in a spreadsheet document, where tables are not embedded in pages.
If the result of the sequence above is OK, the global table width and position are fixed, but we said nothing about the width of each column. By default, the space will probably be equally distributed. But we can define an explicit width for each column through a column style. The following sequence adds 3 column styles:
$doc->insert_style(
odf_create_style(
'table column',
name => 'ColumnA'
)
)->set_properties(
width => '500*'
);
$doc->insert_style(
odf_create_style(
'table column',
name => 'ColumnB'
)
)->set_properties(
width => '200*'
);
$doc->insert_style(
odf_create_style(
'table column',
name => 'ColumnC'
)
)->set_properties(
width => '300*'
);
As you can see, the column width values in this example include a trailing star, meaning that they are relative values. Knowing that the sum of these widths is 1000, the respective column width will be 500/1000, 200/1000, and 300/1000. Why not "5*", "2*" and "3*", that would apparently mean the same relative values ? The explanation is probably a bit complicated, but some ODF applications prefer multiple digit figures.
The systematic use of set_properties()
in this example could be omitted, knowing that the two following examples are equivalent:
# ex 1
$cs = odf_create_style(
'table column',
name => 'ColumnA'
);
$cs->set_properties(width => '500*');
# ex 2
$cs = odf_create_style(
'table column',
name => 'ColumnA',
width => '500*'
);
Of course you may prefer the second form. It works knowing that some optional parameters of odf_create_style
are automatically recognized as properties of the default area (remember that a column style, like a paragraph style, may have more than one property area).
You should set absolute values instead relative ones. lpOD regards a column width value as absolute as soon as it's terminated by any ODF-compliant length unit (such as mm
, cm
, and so on). It's the recommended option with spreadsheets, because in such documents the number of columns is (apparently) unlimited. On the other hand, beware that, in text documents, absolute widths are regarded as relative in some situations. In addition, you may provide both (comma-separated) absolute and relative values through the width
parameter, as described in ODF::lpOD::Style... but this tutorial is not the right place to discuss the ODF rules and options related to absolute and relative sizing.
Once our column styles are registered in the document, we must link each column to the appropriate style. Like many other objects, a column owns a set_style()
method, and we can select any column by a letter ("A", "B"...). We deliberately chose column style names that differ by the last letter only, and this last letter is the corresponding column letter, so the loop below links each column to its style:
$table->get_column($_)->set_style("Column$_") for ('A'..'C');
In a similar way, we can define row styles that specify a non-default height (knowing that the default height may be dynamically adjusted according to the content). The sequence hereafter provides the 4th row with 8cm height:
$doc->insert_style(
odf_create_style(
'table row',
name => "VeryHighRow"
)
)->set_properties(
height => "8cm"
);
$table->get_row(3)->set_style("VeryHighRow");
Decorating cells
A cell style can control a largest property set than a row or column style.
First of all, it supports almost all the common layout options as any other rectangular object style (such as frames, introduced later). As an example, the instruction below creates and registers a cell style providing a 1mm thick blue border with a 1.25mm grey shadow:
$cs = $doc->insert_style(
odf_create_style(
'table cell',
name => "YellowBox"
)
);
$cs->set_properties(
border => "1mm solid #000080",
shadow => "#808080 1.25mm 1.25mm"
);
$cs->set_background(color => "yellow");
In this example, we introduced the set_background()
method, that is available with any lpOD rectangular object, like frames or cells. However, table cell styles own a background color
property that may be directly set without using set_background()
, so the instruction below produces the same result as the previous one:
$doc->insert_style(
odf_create_style(
'table cell',
name => "YellowBox"
)
)->set_properties(
border => "1mm solid #000080",
shadow => "#808080 1.25mm 1.25mm",
'background color' => "yellow"
);
(Note that the 'background color'
option could be written background_color
(without quote), knowing that spaces and underscore characters are equivalent in most lpOD method options.)
Because all the properties in this example are properties of the default area of a cell style, a more compact form is allowed:
$doc->insert_style(
odf_create_style(
'table cell',
name => "YellowBox"
border => "1mm solid #000080",
shadow => "#808080 1.25mm 1.25mm",
'background color' => "yellow"
)
);
Unsurprisingly, this new style may be applied to one or more cells in our table using the usual set_style()
method:
$table->get_cell("B4")->set_style("YellowBox");
$table->get_cell("C8")->set_style("YellowBox");
A cell style may be declared as the default for every cell in a whole row or column. Knowing that the default style applies only for cells without style, the following code sets a default light blue background for all the cells in a row but one cell that uses the previously defined "YellowBox" style:
$doc->insert_style(
odf_create_style(
'table cell',
name => "LightBlue"
)
)->set_background(
color => "light blue"
);
$row = $table->get_row(6);
$row->set_default_cell_style("LightBlue");
$row->get_cell("C")->set_style("YellowBox");
Beware that default cell styles are not interpreted according to clear and uniform rules by every ODF spreadsheet processor. So this example, that should always work with a table belonging to a text document, may produce surprising and apparently counter-intuitive results with spreadsheets. But this tutorial is not the right place to discuss the issue and describe the workarounds.
Now remember that a table cell, like a paragraph, is a text container, so such options as font name, font size, font style, font color and so on can matter. All that is controlled through the text area of the used cell style. So you can set the text properties of a cell style in the same way as the text properties of a paragraph style, through set_properties()
:
$cell_style = odf_create_style(
'table cell',
name => "YetAnother"
);
$cell_style->set_properties(
area => 'table cell',
border => "1mm solid #000080"
);
$cell_style->set_properties(
area => 'text',
size => '12pt',
weight => 'bold',
style => 'italic'
);
$doc->insert_style($cell_style);
Note that the default value of the <area> parameter of set_properties()
is the family of the calling style instance, that is 'table cell' here, so we could omit this parameter in the first call of set_properties()
of the example above.
Another important question, for numeric cells, is the display format of the figures. As an example, the same amount, say 123.4, stored in a cell whose type is currency
, could be displayed as "0123.400", "123.40 €", "$123.40", or according to an unlimited set of other formats. These number formats are not directly described in cell styles, but a cell style may be provided with a 'data style'
parameter, whose value is the identifier of a numeric style registered elsewhere. Creating a new, ODF-compliant numeric format programmatically with the present version of lpOD requires some knowledge of the ODF XML grammar (unfortunately the sprintf
patterns don't work). So the best option consists of reusing number formats, previously registered in the document by your office software.
Before leaving this section, remember that the styling is the most complicated issue for any application that builds tables from scratch or that imports tables from other documents, because a single table may directly or indirectly depend on dozens of styles. However, lpOD provides you with some facilities for individual or bulk style importation (see ODF::lpOD::Style).
Inserting frames, images and boxes
A frame is a generic rectangular area that may contain, for example, an image or a simple or complex text element.
Inserting an image box
As a quick practical exercise, let's choose a small image file in a popular format such as PNG, JPEG, BMP (not limitatively) available in our local file system, and try the following example (where you can replace the given file name by the full path of your real image file). In this example, we assume that $doc
is a text document.
$context = $doc->get_body;
$p = $context->append_element(odf_create_paragraph);
$p->append_element(odf_create_image_frame("logo.png"));
This code sequence creates and appends a new paragraph without text, then appends a now image frame as the content of this paragraph. You can execute a save
later and, if you are really lucky, you will see the image at the end of the document content. "Lucky" means, among other conditions that Image::Size
is installed in your Perl environment, because you didn't specify any display size for the image and, in such case, lpOD tries to load Image::Size
in order to get the original size of the resource. However, an application-provided size
parameter is more safe, knowing that the desired display size is not always the original one (not to say that the given path to the image resource may be an internet link, and in such case lpOD will not try to calculate the original size).
In our example the image frame is appended as a character is a paragraph. So if we want to center the image we just have to center the paragraph using a regular paragraph style:
$doc->insert_style(
odf_create_style(
'paragraph',
name => "Centered",
align => 'center'
)
);
$p = $context->append_element(
odf_create_paragraph(style => "Centered")
);
$p->append_element(
odf_create_image_frame(
"logo.jpg",
size => "6cm, 5cm"
);
We have probably introduced the simplest way to insert an frame; it consists of using a text container to host the frame. But it's not convenient in any situation.
In a text document, we could need to put an image at given coordinates in a given page, whatever the text content. This result can't be obtained by attachment to a paragraph. The frame must be attached to the document body (and not to a particular element) and it's positioning scheme must be specified. Assuming we want to put it at a given position in the first page, the following instruction could be right:
$doc->get_body->append_element(
odf_create_image_frame(
"logo.jpg",
size => "6cm, 5cm",
position => "4cm, 8cm",
page => 1
)
);
However, a few more work is needed, because this frame has no style. The way the coordinates are interpreted depends on some properties of the associated graphic style. So we'll get a better result with the following snippet:
$doc->insert_style(
odf_create_style('graphic', name => "Classic")
);
$doc->get_body->append_element(
odf_create_image_frame(
"logo.jpg",
style => "Classic",
size => "6cm, 5cm",
position => "4cm, 8cm",
page => 1
)
);
The so-named "Classic"
style apparently doesn't contain any property, so what is its utility ? The answer is simple: when you create a graphic style without specifying any option, lpOD attempts to set reasonably convenient default parameters for you. As an example, with this default style, the origin of the coordinates is the top-left corner of the page editable area. Of course an advanced user can easily define another behavior.
Positioning is not all. A graphic style allows you to control some other presentation features. As an example, you may want to apply some visual correction parameters, like a 10% adjustment on the blue color and a 50% opacity:
$doc->insert_style(
odf_create_style(
'graphic',
name => "Customized",
blue => "10%",
'image opacity' => "50%"
),
automatic => TRUE
);
Note: This last style insertion was made with the automatic
parameter. This parameter (if TRUE
) instructs insert_style()
to register the style in the so-called "automatic" category (see ODF::lpOD::Style for explanations about automatic and common styles). We prefer to register customized graphic styles as automatic because (for some mysterious reasons) we noticed that at least one popular ODF text processor appears to ignore some properties when set in common styles.
Just a minute before leaving the image frames. Up to now we just inserted an image available as an external resource through a file path/name. The image should be properly embedded in the document as long as you launch you ODF text processor from the same location. But the image resource will no longer be reachable if the document is viewed elsewhere. So it could be useful to include the image in the physical ODF file. To do so, you may use the add_file()
document-based method, that allows you to store almost anything in your office document files. Example:
$doc->add_file("logo.png");
The looks simple, but it's not OK. When we created the image frame, we provided an external file path/name. We must tell the image frame that the resource is an internal one. To do so, we must capture the return value of add_file()
and use it as the image resource identifier in place of the file name:
$link = $doc->add_file("logo.png");
$doc->get_body->append_element(
odf_create_image_frame(
$link,
style => "Classic",
size => "6cm, 5cm",
position => "4cm, 8cm",
page => 1
)
);
Some details about other possible uses of add_file()
are provided in ODF::lpOD::Document.
Inserting a text box
A text frame is about the same as an image frame, but it contains one or more text elements instead of an image.
Like an image frame, a text frame needs a graphic style.
The instruction below creates a text box containing a single text paragraph whose content is the first argument:
$tb = odf_create_text_frame("The text in the frame");
The following sequence attaches this box to the first page at a given position and associates it to a graphic style:
$doc->get_body->append_element(
odf_create_text_frame(
"The text in the frame",
style => "TextBox",
size => "6cm, 5cm",
position => "4cm, 8cm",
page => 1
)
);
Note that we could reuse our previously created "Classic" style for this frame but we prefer to define a new one in order, say, to set a green solid background and to center the text in the box:
$doc->insert_style(
odf_create_style(
'graphic',
name => "TextBox",
fill => 'solid',
'fill color' => 'pale green',
'textarea vertical align' => 'middle',
'textarea horizontal align' => 'center'
),
automatic => TRUE
);
Note that we introduced the text box creation with a literal text content as the first argument. However, we can fill a text frame with more than a single line of text. As usual, TIMTOWTDI. One of them consists of creating a basic, empty non-specialized frame, then filling it with a list of text elements using set_text_box()
. This last method takes a list of text literals and/or ODF text elements (such as paragraphs) as arguments and puts all that stuff sequentially in the frame:
$fr = odf_create_frame(
style => "TextBox",
size => "6cm, 5cm",
position => "4cm, 8cm",
page => 1
);
$p = odf_create_paragraph(
text => "Item 3",
style => "RedBold"
);
$ps = odf_create_style(
'paragraph',
name => "RedBold"
);
$ps->set_properties(
area => 'text',
color => 'red',
weight => 'bold'
);
$fr->set_text_box("Item 1", "Item 2", $p, "Item 4");
$doc->get_body->append_element($fr);
$doc->insert_style($ps, automatic => TRUE);
Here we create a frame and a paragraph (with its own style); then we turn the frame as a text box with set_text_box()
and, in the same time, we put a mixed list of items (one of them is our paragraph, the others are literals) in this box.
The main documentation about frames is in ODF::lpOD::StructuredContainer.
Controlling text page styles
Before reading this section, we recommend you to read the beginning of the Page styles section in ODF::lpOD::Style in order to understand the relationship between pages, master pages and page layouts.
Here we'll introduce the topic with a simple use case: defining and using a special custom page whose particular properties are a 30cm width, a 16cm height, and a footer displaying the page number.
First, let's create the page layout that controls the size:
$doc->insert_style(
odf_create_style(
'page layout',
name => "SpecialLayout",
size => "30cm, 16cm"
)
);
Then create the "master" (i.e. the main page style) that uses the new layout:
$mp = $doc->insert_style(
odf_create_style(
'master page',
name => "CustomPage",
layout => "SpecialLayout"
)
);
Now append a footer definition to our new master page. This footer will contain a single paragraph that in turn will contain a single text field:
$p = odf_create_paragraph;
$p->set_field('page number');
$footer = $mp->set_footer;
$footer->insert_element($p);
In this example, we created an empty paragraph; we called the set_field()
paragraph method with 'page number' as field type, and without positioning argument because the page number field is intended to be the only content of the paragraph. Then we extended the previously created master page by set_footer()
, whose return value was the page footer. Finally we inserted the paragraph (containing the page number field) in the footer.
A page header may be created from a page master using set_header()
, which returns an object that may be used in a similar way as a footer, i.e. as a container for various text or graphic objects. Of course, you can insert much more than a single paragraph in a page footer or header. You can use insert_element()
and/or append_element()
repeatedly with these two page style components.
Using variables and text fields
While illustrating a page style creation use case, we introduced a page number field. Such special elements and many others, intended to host dynamic content, may be used in the text body and not only in page footers or headers.
A field may be put anywhere within the text of a paragraph or a heading, using set_field()
, according to the same positioning parameters as set_bookmark()
previously introduced. The first argument of set_field()
is the field type, that specifies the kind of dynamic data to display. The following function returns the list of allowed types:
odf_text_field->types;
While lpOD internally implements fields as objects whose classes are odf_field
(alias ODF::lpOD::Field
) and odf_text_field
(alias ODF::lpOD::TextField
), you will not use any constructor such as odf_create_xxx
, because set_field()
creates the object and puts it in place.
Most object types are linked to dynamic data coming from the external environment (ex: date
), from the general document metadata (ex: title
), or from the local position in the document (ex: chapter
). In addition, a field may be associated to a user-defined variable.
This first example, assuming that the calling object is a previously selected paragraph, the following instruction inserts a the file name at the default position, i.e. the beginning of the paragraph:
$p->set_field('file name');
The instruction below puts the same field at the end:
$p->set_field('file name', offset => 'end');
The thing may turn more complicated as soon as you want to display formatted fields. In order to illustrate the topic, the instruction below puts a date field which will be displayed according to the default date format:
$p->set_field('date');
However, like table numeric table cells, numeric fields may be associated with number styles, thanks to an optional style
parameter:
$p->set_field('date', style => 'MyDateFormat');
The variable
field type allows the user to link a field to a given user- defined variable, defined at the document level. If this type is selected, a name
parameter must specify the variable (unique) name:
$p->set_field('variable', name => 'InvoiceAmount');
Many fields may be linked to the same variable: their role may be to display in several places a value stored once.
lpOD allows you to define new variables, and not only to use existing variables in fields. Unlike fields, a variable is not located at a particular place in the document content, because it's not visible. So it must be created at the document level, using the odf_document
set_variable()
method:
$doc->set_variable(
"InvoiceAmount",
type => "float",
value => 123
);
Left apart the so-called simple
(but not so simple) variables, an existing user
variable (i.e. the default variable type), once selected by name, may be updated at any time:
$var = $doc->get_variable("InvoiceAmount");
$var->set_value($new_value);
If a user-defined variable is updated, all the linked fields are updated accordingly when the document is displayed by an ODF viewer.
For other more information about the fields and variables, see ODF::lpOD::TextElement.
A few words about presentations and draw pages
While a spreadsheet document is a document whose content is made of tables, a presentation is a document whose content is made of draw pages.
Draw pages, like frames, are documented in ODF::lpOD::StructuredContainer.
The most complicated business in presentations is the page styling, so, in this introduction we'll not try to introduce the making of a draw page style. In a realistic application, it's probably the best option: use already decorated presentation templates with luxurious backgrounds, and keep focused on the content.
First, open your document and select the document body, just as usual:
$doc = odf_get_document("template.odp");
$context = $doc->get_body;
Then, say, remove any existing draw page in order (just to show the way):
$_->delete for $context->get_draw_pages;
As any odf_element
, a draw page owns a delete()
method, that removes the page itself and its whole content. Issued from the document body element, get_draw_pages()
returns all the existing pages as a list, so our document is new empty, but its styles remain available.
Let's create and append a new draw page:
$page = $context->append_element(
odf_create_draw_page("P1", name => "The First One")
);
Now a first page in present in our document. Note that the draw page constructor odf_create_draw_page()
is used with a mandatory argument which is the page identifier (must be unique) and an optional, but strongly recommended parameter which is the visible name, and that must be unique if set. (Note that the ID and the name are redundant as identifiers, so one of them may disappear in future versions of the ODF standard; if so, lpOD will evolve accordingly.) The identifier may be used later to retrieve the page using get_draw_page()
.
This new page will be displayed according to the default style of our template document. But we have to populate it. Good news: we already know the way to create text and image frames, and such objects provide the usual content of a draw page. So, let's create an image frame in the same way as previously:
$page->append_element(
odf_create_image_frame(
"logo.jpg",
style => "Classic",
size => "6cm, 5cm",
position => "4cm, 8cm"
)
);
Note that the calling context of append_element()
is now the draw page, not the document body, because everything that may be displayed in a presentation document must be enclosed in a draw page. The given position
parameter is, of course, relative to the area of the draw page. While the explicit use of a graphic style is not always required by every presentation viewer, it's recommended to provide it (or, as a probably better option, to reuse an external style library). The creation and registration of a graphic style work exactly like with text documents.
Of course, you can/must include the image file in your ODF package using add_file()
as previously shown with another kind of document.
As a consequence, as soon as you got a good presentation template you can easily generate new presentations according to any kind of content.
With a lot of coding (and ODF awareness), you could control a lot of funny presentation gadgets (such as animations) from Perl programs, but it's clearly not the top priority of lpOD.
In a typical long running conference, we often use pages that are replicates of other pages with small variants. Its very easy with lpOD to copy an existing page, change some details (including of course the name and the identifier), and insert the copy somewhere. As an illustration, the following sequence takes a copy of a page whose ID is "P1", changes its ID and name, then appends it at the end of the presentation:
my $new_page = $context->get_draw_page("P1")->clone;
$new_page->set_id("P2");
$new_page->set_name("The Last One");
$context->append_element($new_page);
In some situations we know the name of a page (because it's visible through a typical presentation editor) but we ignore its ID. Fortunately, lpOD provides a solution to select this page anyway:
$page = $context->get_draw_page_by_name("Introduction");
And, if we know nothing but the sequential position in the presentation order, we can always select a page by position, like in this example which provides a name to the last page:
$context->get_draw_page_by_position(-1)->set_name("The End");
1 POD Error
The following errors were encountered while parsing the POD:
- Around line 1927:
Non-ASCII character seen before =encoding in '€",'. Assuming UTF-8