Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

SXW Writer

by benn (Vicar)
on Jun 13, 2003 at 14:56 UTC ( [id://265690]=sourcecode: print w/replies, xml ) Need Help??
Category: Utility Scripts
Author/Contact Info benn
Description: I needed something to write out Open Office .sxw files, and was suprised to find nothing on CPAN, so I spent a couple of days knocking this up. (Typical huh? Just doing it in Open Office would take me 5 minutes :)).

It's very raw, but it works, and may serve somebody as a starting off point for something else. All the documentation is in POD at the bottom. Enjoy.

Cheers,Ben.

Update added add_font example to synopsis

package SXWWriter::Elt;
use XML::Twig;
use base "XML::Twig::Elt";

##########################################################
# Add to element
##########################################################

# 'public' functions
sub add_h { add_text($_[0],'h',$_[1],$_[2]);}    # add a <text:h> elem
+ent
sub add_p { add_text($_[0],'p',$_[1],$_[2]);}    # add a <text:p> elem
+ent
sub add {                # insert an object into another (oo err missu
+s)
    my ($self,$obj) = @_;
    $obj->paste('last_child',$self);
    $obj;
}    
# internals
sub add_text {                                    # add a <text:$type>
+ element
    my ($self,$type,$text,$style) = @_;
    $style ||= "Standard";
    $text  ||= '#EMPTY'; # Ensure an <empty/> tag
    my @t;
    foreach ((ref $text eq 'ARRAY') ? @$text : $text) {
        while (s/^(.*?)\t//) {    #replace tabs with <text:tab-stop />
            push @t,$1;
            push @t,SXWWriter::Elt->new('text:tab-stop','#EMPTY');
        }
        push @t,$_;
    }
    my $t = SXWWriter::Elt->new("text:$type"=>{'text:style-name'=>$sty
+le},@t);
    $t->paste('last_child',$self);
}
sub insert_new { # 'cos insert_new_elt returns an XML::Twig::Elt
    my $s = shift();
    $s->add(SXWWriter::Elt->new(@_));
}

1;

package SXWWriter;

use XML::Twig;
use strict;
use warnings;
use File::Basename;

our $compression = 6;    # default compression level
$XML::Twig::base_ent{'£'}='&pound;';
sub new {
    my ($class) = shift();
    my %doc=(
        'manifest'=>{fname=>"Meta-Inf\\manifest.xml",xml=>make_xml("ma
+nifest:manifest")},
        'meta'=>{fname=>"meta.xml",xml=>make_xml("office:document-meta
+")},
        'styles'=>{fname=>"styles.xml",xml=>make_xml("office:document-
+content")},
        'content'=>{fname=>"content.xml",xml=>make_xml("office:documen
+t-styles")},
        'index'=>1
    );
    my @ns = (['office:version','1.0'],['xmlns:office','http://openoff
+ice.org/2000/office'],
        ['xmlns:number','http://openoffice.org/2000/datastyle'],['xmln
+s:math','http://www.w3.org/1998/Math/MathML'],
        ['xmlns:xlink','http://www.w3.org/1999/xlink'],['xmlns:form','
+http://openoffice.org/2000/form'], 
        ['xmlns:table','http://openoffice.org/2000/table'], ['xmlns:st
+yle','http://openoffice.org/2000/style'],
        ['xmlns:script','http://openoffice.org/2000/script'], ['xmlns:
+draw','http://openoffice.org/2000/drawing'],
        ['xmlns:svg','http://www.w3.org/2000/svg'], ['xmlns:fo','http:
+//www.w3.org/1999/XSL/Format'],
        ['xmlns:text','http://openoffice.org/2000/text'], ['xmlns:dr3d
+','http://openoffice.org/2000/dr3d'],
        ['xmlns:chart','http://openoffice.org/2000/chart']);

    my $mroot = SXWWriter::Elt->new('manifest:manifest'=>{'xmlns:manif
+est'=>"http://openoffice.org/2001/manifest"}); 
    $doc{manifest}{xml}->set_root($mroot);

    my %props = map {$_->[0]=>$_->[1]} @ns[0..1];
    my $nroot = SXWWriter::Elt->new('office:document-meta'=>{%props,
        'xmlns:meta'=> 'http://openoffice.org/2000/meta',
        'xmlns:xlink'=> 'http://www.w3.org/1999/xlink',
        'xmlns:dc'=>'http://purl.org/dc/elements/1.1/'});
    $nroot->insert_new("office:meta");
    $doc{meta}{xml}->set_root($nroot);

    %props = map {$_->[0]=>$_->[1]} @ns;

    my $sroot = SXWWriter::Elt->new('office:document-styles'=>{%props,
+'office:class'=>'text'});

    my $fd = $sroot->insert_new("office:font-decls");
    my $ss = $sroot->insert_new("office:styles");
    my $as = $sroot->insert_new("office:automatic-styles");
    my $pm = $as->insert_new("style:page-master"=>{"style:name"=>"pm1"
+});
    $pm->insert_new("style:header-style",'#EMPTY');
    $pm->insert_new("style:footer-style",'#EMPTY');
    my $psp = $pm->insert_new("style:properties"=>{
            'style:print-orientation'=>'portrait','style:footnote-max-
+height'=>'0cm',
            'fo:page-width'=>"21.001cm",'fo:page-height'=>"29.7cm",
            'fo:margin-right'=>'2cm','fo:margin-bottom'=>'2cm','fo:mar
+gin-top' => '2cm','fo:margin-left'=>'2cm'});
    $psp->insert_new('style:footnote-sep'=>{
        'style:distance-before-sep'=>'0.101cm','style:adjustment'=>'le
+ft','style:width'=>'0.018cm',
        'style:rel-width'=>'25%','style:distance-after-sep'=>'0.101cm'
+,'style:color' =>'#000000'});
    $ss->insert_new("text:linenumbering-configuration"=>
        {'text:increment'=>'5','text:number-lines'=>'false','text:numb
+er-position'=>'left','text:offset'=>'0.499cm','style:num-format'=>'1'
+},
        '#EMPTY');
    $ss->insert_new("text:footnotes-configuration"=>
        {'style:num-format'=>"1",'text:start-value'=>"0",'text:footnot
+es-position'=>"page",'text:start-numbering-at'=>"document"},
        '#EMPTY');
    $ss->insert_new("text:endnotes-configuration"=>{'style:num-format'
+=>"i",'text:start-value'=>"0"},'#EMPTY');
    my $tos = $ss->insert_new("text:outline-style");
    $tos->insert_new('text:outline-level-style'=>{'style:num-format'=>
+'','text:level'=>$_},'#EMPTY') foreach (0..10);
    my $ms = $sroot->insert_new("office:master-styles");
    $ms->insert_new("style:master-page"=>{"style:name"=>"Standard","st
+yle:page-master-name"=>"pm1"});
    $doc{styles}{xml}->set_root($sroot);
    
    my $croot = SXWWriter::Elt->new('office:document-content'=>{%props
+,'office:class'=>'text'});
    $croot->insert_new("office:script",'#EMPTY');
    $croot->insert_new("office:font-decls");
    $croot->insert_new("office:automatic-styles");
    my $body = $croot->insert_new("office:body");
    my $fds = $body->insert_new("text:sequence-decls");
    foreach (qw/Illustration Table Text Drawing/) {
        $fds->insert_new('text:sequence-decl'=>{'text:name'=>$_,'text:
+display-outline-level'=>'0'},'#EMPTY');
    }
    $doc{content}{xml}->set_root($croot);
    $doc{body} = $body;
    $doc{pageprops} = $psp;    # save page props - saves us having to 
+find them again

    my $doc = bless \%doc ,$class;
    
    $doc->add_dc('generator',"SWX Creator");
    $doc->add_manifest('/','application/vnd.sun.xml.writer','Pictures/
+','','content.xml',
        'text/xml','styles.xml','text/xml','meta.xml','text/xml');
    $doc->add_font('font-family-generic'=>'roman','font-pitch'=>'varia
+ble','name' => 'Thorndale','font-family' => 'Thorndale');
    $doc->add_font('font-family-generic'=>'swiss','font-pitch'=>'varia
+ble','name'=>'Arial','font-family' => 'Arial');
    $doc->add_sstyle(family=>'paragraph',name=>'Standard'); 
    $doc->add_sstyle('parent-style-name'=>'Standard','family'=>'paragr
+aph','name'=>'Text body',
            'properties' =>    {'fo:margin-top'=>'0cm','fo:margin-bott
+om'=>'0.212cm'}
    );
    $doc;
}

sub save {
    my ($self,$filename) = @_;
    use Archive::Zip qw( :ERROR_CODES);
    my $zip = Archive::Zip->new();
    my $m = $self->{manifest}{xml}->root;
    foreach ($m->children()) {
        if ($_->att('manifest:media-type') =~/image/) {
            my $file = $_->att('manifest:full-path');
            my $filename = basename($file);
            my $m = $zip->addFile($file,"Pictures\\$filename");
            $_->set_att('manifest:full-path',"Pictures/$filename");
        }
    }
    foreach (keys %$self) {
        next if /index|body|page/;
        my $m = $zip->addString($self->{$_}{xml}->sprint,$self->{$_}{f
+name});
        $m->desiredCompressionLevel($compression);
    }
    die "Can't write file $!" unless $zip->writeToFileNamed($filename)
+ == AZ_OK;
}

##########################################################
# Add to Document
##########################################################
sub add_manifest {
    my $self = shift;
    while (@_) {
        $self->{manifest}{xml}->root->insert_new(
            'manifest:file-entry'=>{"manifest:full-path"=>shift,'manif
+est:media-type'=>shift},'#EMPTY'
        );
    }
}

sub add_meta {add_to_meta($_[0],"meta:".$_[1],$_[2]);}
sub add_dc {add_to_meta($_[0],"dc:".$_[1],$_[2]);}
sub add_to_meta {
    $_[0]->{meta}{xml}->root->first_child->insert_new($_[1],$_[2]);
}

sub add_font {
    my ($self,%properties) = @_;
    my $style = make_style(%properties);
    $self->{content}{xml}->root->first_child('office:font-decls')->ins
+ert_new('style:font-decl',$style,'#EMPTY');
    $self->{styles}{xml}->root->first_child('office:font-decls')->inse
+rt_new('style:font-decl',$style,'#EMPTY');
}

sub add_pstyle {
    my ($self,$name,%props) = @_;
    $self->add_style('name'=>$name,'family'=>"paragraph",'parent-style
+-name'=>"Standard",'properties'=>\%props);
}
sub add_tstyle {
    my ($self,$name,%prop) = @_;
    $self->add_style('name'=>$name,'family'=>"text",'properties'=>\%pr
+op);
}
sub add_fstyle {
    my ($self,$name,%prop) = @_;
    $self->add_style('name'=>$name,'family'=>"frame",'properties'=>\%p
+rop);
}
sub add_style {my $self=shift();$self->add_to_styles("content",'office
+:automatic-styles',@_);}
sub add_sstyle {my $self=shift();$self->add_to_styles("styles",'office
+:styles',@_);}
sub add_to_styles {
    my ($self,$doc,$stylename,%prop) = @_;
    my $prop =  SXWWriter::Elt->new('style:properties'=>$prop{properti
+es},'#EMPTY');
    delete $prop{properties};
    $self->{$doc}{xml}->root->first_child($stylename)->insert_new("sty
+le:style",make_style(%prop),$prop); 
}

###################################################
# Helpers
###################################################
sub make_style {
    my %opts = @_;
    foreach (keys %opts){
        $opts{(($_ eq 'font-family')?"fo":"style").":$_"}=$opts{$_};
        delete $opts{$_};
    }
    \%opts;
}

sub make_xml {
    my ($type) = @_;
    my $dtd = ($type =~ /manifest/) ? "Manifest" : "office";
    my $doc = ($dtd eq "Manifest")  ? $dtd : "Office Document";
    my $t  = XML::Twig->new(pretty_print => 'indented','output_encodin
+g'=>'UTF-8');
    $t->set_xml_version("1.0");
    $t->set_doctype($type,$dtd.".dtd","-//OpenOffice.org//DTD $doc 1.0
+//EN");
    $t;
}

###################################################
# Document Properties
###################################################

sub set_page_margin {$_[0]->set_page_margins($_[1],$_[1],$_[1],$_[1]);
+}
sub set_page_margins {
    my ($self,$l,$r,$t,$b) = @_;
    $self->{pageprops}->set_att("fo:margin-left",$l/10 . "cm");
    $self->{pageprops}->set_att("fo:margin-right",$r/10 . "cm");
    $self->{pageprops}->set_att("fo:margin-top",$t/10 . "cm");
    $self->{pageprops}->set_att("fo:margin-bottom",$b/10 . "cm");
}

##########################################################
# Make Document Elements
##########################################################
sub make_span {
    my ($self,$text,$style) = @_;
    SXWWriter::Elt->new('text:span'=>{'text:style-name'=>$style},$text
+);
}

my %boxprops = ('style:vertical-pos'=>"from-top",'style:vertical-rel'=
+>"paragraph",
        'style:horizontal-pos'=>"from-left",'style:horizontal-rel'=>"p
+aragraph",
        "fo:border"=>"none");

sub make_frame {
    my ($self,$x,$y,$w,$h,%prop) = @_;
    my $index = $self->{'index'}++;
    my %props = (%boxprops,%prop);
    $self->add_style('properties'=>\%props,"name"=>"fr$index","family"
+=>"graphics","parent-style-name"=>"Frame");
    SXWWriter::Elt->new("draw:text-box"=>{_box($x,$y,$w,$h,$index,"Fra
+me")});
}

sub make_picture {
    my ($self,$filename,$x,$y,$w,$h,%prop) = @_;
    my %props = (%boxprops,'draw:luminance'=>"0%",'draw:contrast'=>"0%
+",
        'draw:red'=>"0%",'draw:green'=>"0%",'draw:blue'=>"0%",'draw:ga
+mma'=>"1",
        'draw:color-inversion'=>"false",'draw:transparency'=>"-100%",
        'draw:color-mode'=>"standard",'fo:clip'=>"rect(0cm 0cm 0cm 0cm
+)",%prop);
    use File::MMagic;
    my $m = new File::MMagic;
    my $media = $m->checktype_filename($filename);
    $self->add_manifest($filename,$media);    #will replace pathname w
+hen saving
    $filename = basename($filename);
    my $index = $self->{'index'}++;
    $self->add_style('properties'=>\%props,"name"=>"fr$index","family"
+=>"graphics","parent-style-name"=>"Graphics");
    SXWWriter::Elt->new("draw:image"=>{_box($x,$y,$w,$h,$index,"Graphi
+c"),
        'xlink:href'=>"#Pictures/$filename",'xlink:type'=>"simple",
        'xlink:show'=>"embed",'xlink:actuate'=>"onLoad"});
}

sub _box {
    my ($x,$y,$w,$h,$i,$n) = @_;
    return('svg:x'=>($x/10)."cm",'svg:y'=>($y/10) ."cm",
        'svg:width'=> ($w/10) ."cm",'svg:height' => ($h/10) ."cm",
        'draw:style-name'=>"fr$i",'draw:z-index'=>"1",
        'text:anchor-type'=>"page",'draw:name'=>"$n$i");
}

1;

__END__

=head1 NAME

    SXWWriter -- create simple Open Office .sxw files

=head1 SYNOPSIS

    use SXWWriter;

    my $doc = new SXWWriter();
    $doc->add_font('font-pitch'=>'variable',        # Arial & Thorndal
+e predefined
        'name'=>'MyFont','font-family' => 'Courier New');
    $doc->add_pstyle('P1','style:font-name'=>"Arial",    # a paragraph
+ style
        'fo:color'=>"#0000ff",'fo:font-size'=>"36pt");
    $doc->add_pstyle('P2','style:font-name'=>"MyFont",'fo:font-size'=>
+"11pt");
    $doc->add_tstyle('T1','fo:font-weight'=>"bold");    # inline text 
+style
    $doc->add_dc('title',"My Document");                # dc:field=val
+ue
    $doc->add_meta('initial-creator',"Me");                # meta:fiel
+d=value
    $doc->set_page_margin(10);    # ...or set_page_margins(l,r,t,b)

    my $body = $doc->{body};                    # A subclass of XML::T
+wig::Elt
    my $frame = $doc->make_frame(136,10,57,50);    # x y width height 
+in mm

    $body->add_p("A sentence in the default style");
    $body->add_p();                                # blank paragraph
    $body->add_p("Some big text in style P1",'P1');
    
    my $frame = $doc->make_frame(122,0,57,33);    # x y width height
    $frame->add_p("Text inside the frame",'P2');
    $frame->add_p([$doc->make_span("embedded bold","T1")," Text"],'P2'
+);
    $frame->add_p("Some text \t\twith tabs",'P2');
    $body->add_p($frame);

    my $picture = $doc->make_picture('picture.png',92,30,25,26); #name
+,x,y,w,h
    $body->add_p($picture);

    $s->save('mydocument.sxw');

=head1 DESCRIPTION

    This is about the bare minimum needed to write out Open Office Tex
+t files.
    All the 'public' methods are shown in the synopsis. Many style par
+ameters
    are available, which can be found by downloading the definitive .s
+xw
    specification from http://xml.openoffice.org/xml_specification.pdf
+ (1.4M),
    or more simply by having a look in an existing .sxw :)

    The body of the document, and all inserted elements, are a subclas
+s of
    XML::Twig::Elt, so all its methods are available if needed. Frames
+ and
    pictures need to be placed inside a paragraph to be displayed for 
+reasons
    not yet clear to me (but may be once I finish reading the 1.4M spe
+c :)) - 
    there *is* a simple 'add' method (which will paste() a Twig direct
+ly), but 
    for the moment, elements need to be add_p()'ed instead.
    
    Adding a picture will copy the file into the .sxw file on saving 
    (a zip file, but no compression is done on pictures - it seemed 
    unnecessary). It uses File::MMagic to autodetect the MIME type for
+ 
    simplicity's sake, but it'd be simple to remove this and replace w
+ith
    a passed parameter if so wished.

=head1 TODO

    Lots. It works, but that's about it :) There's a couple of methods
    in there that are waiting for a use - add_h(),add_fstyle() etc. - 
+they
    all work, but I haven't inserted all the right document headers ye
+t to
    utilise them, simply because I didn't need them yet. Damm this laz
+iness.
    It only does single pages too atm...
    Feel free to suggest improvements / send me bits of code / whateve
+r.

=head1 LICENSE

Same terms as Perl itself yada yada yada

=head1 AUTHOR

Ben Daglish

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: sourcecode [id://265690]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others chanting in the Monastery: (9)
As of 2024-03-28 10:07 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found