<?php
/*
OpenDocument.php, utility functions for working with OpenDocument files and
converting them to XHTML.
Copyright (C) 2003-2005 Arend van Beelen, Auton Rijnsburg
This program is free software; you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation; either version 2 of the License, or (at your option)
any later version.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
For any questions, comments or whatever, you may mail me at: hide@address.com
*/
require_once('Config.php');
require_once('URI.php');
/**
* @brief Class with utility functions for working with OpenDocument files.
*/
class OpenDocument
{
/**
* Destructor.
*
* Performs cleanup operations.
*/
public function __destruct()
{
if($this->fileOpened == true)
{
$this->close();
}
}
/**
* Loads the specified OpenDocument for use by the other utility
* functions in this class.
*
* If the document has already been loaded, you will receive a cached
* reference to it. This reference contains all changes made to the
* already loaded document, and any changes you make on this reference
* will reflect on the original and vice versa.
*
* @param openDocument URI to the OpenDocument file to load.
*
* @return @p true if the document was successfully loaded, @p false
* otherwise.
*/
public function load($openDocument)
{
if($this->fileOpened == true)
{
$this->close();
}
if((URI::permissions($openDocument) & PERMISSION_READ) == 0)
{
return false;
}
$simplifiedFilename = String::simplifyPath($openDocument);
if(isset(self::$openedDocuments[$simplifiedFilename]))
{
$this->fileOpened = true;
$this->document = $openDocument;
$this->tmpdir = self::$openedDocuments[$simplifiedFilename]->tmpdir;
$this->metaDoc = self::$openedDocuments[$simplifiedFilename]->metaDoc;
$this->metaNodes = self::$openedDocuments[$simplifiedFilename]->metaNodes;
$this->changed = self::$openedDocuments[$simplifiedFilename]->changed;
$this->cached = true;
return true;
}
// create a temporary directory and extract the document to it
$this->tmpdir = tempnam('/tmp', 'OpenDocument');
unlink($this->tmpdir);
if(mkdir($this->tmpdir) == false)
{
return false;
}
if(copy($openDocument, "{$this->tmpdir}/document.od") == false)
{
exec("rm -R {$this->tmpdir}");
trigger_error('Could not copy OpenDocument file');
return false;
}
exec("unzip {$this->tmpdir}/document.od -d {$this->tmpdir}", $output, $return);
if($return != 0)
{
exec("rm -R {$this->tmpdir}");
return false;
}
$this->loadMeta();
$this->fileOpened = true;
$this->document = $openDocument;
$this->cached = false;
self::$openedDocuments[$simplifiedFilename] = $this;
return true;
}
/**
* Unloads the loaded OpenDocument file. If the file has been changed, it
* will be saved automatically. Any cached references to it will become
* useless and using them after this call will result in undefined
* behavior.
*/
public function unload()
{
$simplifiedFilename = String::simplifyPath($this->document);
if(isset(self::$openedDocuments[$simplifiedFilename]))
{
unset(self::$openedDocuments[$simplifiedFilename]);
}
else
{
trigger_error('Document was never loaded');
}
}
/**
* Returns the title of the currently opened OpenDocument file.
*
* @return The title of the document as given by the document's author.
*
* @sa setTitle()
*/
public function title()
{
if($this->fileOpened == false)
{
trigger_error('Trying to get title when no document is loaded');
return '';
}
return $this->metaNodes['title']->nodeValue;
}
/**
* Sets the title of the currently opened OpenDocument file.
*
* @param title The new title to use for the document.
*
* @sa title()
*/
public function setTitle($title)
{
if($this->fileOpened == false)
{
trigger_error('Cannot set title when no document is loaded');
return;
}
$this->metaNodes['title']->nodeValue = $title;
$this->setChanged();
}
/**
* Returns the subject of the currently opened OpenDocument file.
*
* @return The subject of the document as given by the document's author.
*
* @sa setSubject()
*/
public function subject()
{
if($this->fileOpened == false)
{
trigger_error('Trying to get subject when no document is loaded');
return '';
}
return $this->metaNodes['subject']->nodeValue;
}
/**
* Sets the subject of the currently opened OpenDocument file.
*
* @param subject The new subject to set for the document.
*
* @sa subject()
*/
public function setSubject($subject)
{
if($this->fileOpened == false)
{
trigger_error('Cannot set subject when no document is loaded');
return;
}
$this->metaNodes['subject']->nodeValue = $subject;
$this->setChanged();
}
/**
* Returns some keywords about the currently opened OpenDocument file.
*
* @return Some keywords about the document as given by the document's author.
*
* @sa setKeywords()
*/
public function keywords()
{
if($this->fileOpened == false)
{
trigger_error('Trying to get keywords when no document is loaded');
return '';
}
return $this->metaNodes['keywords']->nodeValue;
}
/**
* Sets keywords about the currently opened OpenDocument file.
*
* @param keywords The new keywords to set for the document.
*
* @sa keywords()
*/
public function setKeywords($keywords)
{
if($this->fileOpened == false)
{
trigger_error('Cannot set keywords when no document is loaded');
return;
}
$this->metaNodes['keywords']->nodeValue = $keywords;
$this->setChanged();
}
/**
* Returns comments about the currently opened OpenDocument file.
*
* @return The comments about the document as given by the document's author.
*
* @sa setComments()
*/
public function comments()
{
if($this->fileOpened == false)
{
trigger_error('Trying to get comments when no document is loaded');
return '';
}
return $this->metaNodes['comments']->nodeValue;
}
/**
* Sets comments about the currently opened OpenDocument file.
*
* @param comments The new comments to set for the document.
*
* @sa comments()
*/
public function setComments($comments)
{
if($this->fileOpened == false)
{
trigger_error('Cannot set comments when no document is loaded');
return;
}
$this->metaNodes['comments']->nodeValue = $comments;
$this->setChanged();
}
/**
* Returns the name of the author of the currently opened OpenDocument file.
*
* @return The name of the document's author.
*
* @sa setAuthor()
*/
public function author()
{
if($this->fileOpened == false)
{
trigger_error('Trying to get author when no document is loaded');
return '';
}
return $this->metaNodes['author']->nodeValue;
}
/**
* Sets the name of the author of the currently opened OpenDocument file.
*
* @param author The new name to use as the document's author.
*
* @sa author()
*/
public function setAuthor($author)
{
if($this->fileOpened == false)
{
trigger_error('Cannot set author when no document is loaded');
return;
}
$this->metaNodes['author']->nodeValue = $author;
$this->setChanged();
}
/**
* Converts the currently opened OpenDocument file to XHTML.
*
* This function does not modify the original document.
*
* @param xhtmlDocument URI to the resulting XHTML document.
*
* @return @p true if the document was successfully converted, @p false
* otherwise.
*/
public function convertToXHTML($xhtmlDocument)
{
if($this->fileOpened == false)
{
trigger_error('No document was loaded for conversion to XHTML');
return false;
}
if(((URI::permissions(dirname($xhtmlDocument)) & PERMISSION_APPEND) &&
(URI::fileExists($xhtmlDocument) == false ||
URI::permissions($xhtmlDocument) & PERMISSION_MODIFY)) == false)
{
return false;
}
// copy images
$images = glob("{$this->tmpdir}/Pictures/*");
if($images !== false)
{
$picturesDir = dirname($xhtmlDocument).'/Pictures';
if(URI::fileExists($picturesDir) == false)
{
URI::mkdir($picturesDir);
}
foreach($images as $image)
{
$basename = basename($image);
if($basename == '.' || $basename == '..')
{
continue;
}
copy($image, "$picturesDir/$basename");
}
}
// convert the OpenDocument XML file using the XSL stylesheets into an XHTML file
OpenDocument2XHTML::reset();
$stylesDoc = new DOMDocument();
$contentDoc = new DOMDocument();
if($stylesDoc->load("{$this->tmpdir}/styles.xml") == false ||
$contentDoc->load("{$this->tmpdir}/content.xml") == false)
{
return false;
}
$stylesXSL = new DOMDocument();
$stylesXSL->load(AUKYLA_DIR.'/data/base/OpenDocument/Styles.xsl');
$contentXSL = new DOMDocument();
$contentXSL->load(AUKYLA_DIR.'/data/base/OpenDocument/Content2XHTML.xsl');
$xsltProcessor = new XSLTProcessor();
if($xsltProcessor->hasExsltSupport() == false)
{
die('EXSLT support in PHP is required for converting OpenDocument files!');
}
$xsltProcessor->registerPHPFunctions();
$xsltProcessor->importStyleSheet($stylesXSL);
$xsltProcessor->transformToXML($stylesDoc);
$xsltProcessor = new XSLTProcessor();
$xsltProcessor->registerPHPFunctions();
$xsltProcessor->importStyleSheet($contentXSL);
$xsltProcessor->setParameter('', 'imagePrefix', Config::globals('downloadURL').'?file='.dirname($xhtmlDocument).'/Pictures/');
$xsltProcessor->setParameter('', 'title', $this->title());
if($xsltProcessor->transformToURI($contentDoc, $xhtmlDocument) == false)
{
return false;
}
return true;
}
/**
* Scales all images in the currently opened OpenDocument file to a maximum
* resolution.
*
* @param maxWidth The maximum width of the images.
* @param maxHeight The maximum height of the images.
*
* @return @p true on success, @p false on error.
*
* @note This function requires ImageMagick to be installed to be able to work.
*/
public function scaleImages($maxWidth, $maxHeight)
{
if($this->fileOpened == false)
{
trigger_error('No document was loaded for scaling images');
return false;
}
if((URI::permissions($this->document) & PERMISSION_MODIFY) == 0)
{
return false;
}
$images = glob("{$this->tmpdir}/Pictures/*");
if($images !== false && sizeof($images) > 0)
{
foreach($images as $image)
{
unset($output);
exec("identify $image", $output, $return);
if($return != 0)
{
trigger_error("Failed to run 'identify', is ImageMagick installed?");
return false;
}
$properties = explode(' ', $output[0]);
$geometry = $properties[2];
list($width, $height) = explode('x', $geometry);
if($width > $maxWidth || $height > $maxHeight)
{
exec("convert -size {$maxWidth}x{$maxHeight} $image -scale {$maxWidth}x{$maxHeight} $image", $output, $return);
if($return != 0)
{
trigger_error("Failed to run 'convert', is ImageMagick installed?");
return false;
}
}
}
$this->setChanged();
}
return true;
}
/**
* Returns the temporary directory in which you can find the unzipped
* version of a loaded OpenDocument file.
*
* If you change the contents of the temporary directory, be sure to
* call setChanged() so your changes will be saved.
*
* @return The path to the temporary directory where the OpenDocument
* file has been unzipped to, or an empty string if no file was
* loaded.
*
* @sa setChanged()
*/
public function temporaryDirectory()
{
if($this->fileOpened == false)
{
trigger_error('Requesting temporary directory with no document loaded');
return '';
}
return $this->tmpdir;
}
private function loadMeta()
{
$this->metaNodes = array('title' => false,
'subject' => false,
'keywords' => false,
'comments' => false,
'author' => false);
$this->metaDoc = new DOMDocument();
if($this->metaDoc->load("{$this->tmpdir}/meta.xml") == false)
{
return false;
}
$dcNS = 'http://purl.org/dc/elements/1.1/';
$metaNS = 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0';
$officeNS = 'urn:oasis:names:tc:opendocument:xmlns:office:1.0';
$xpath = new DOMXPath($this->metaDoc);
$xpath->registerNamespace('dc', $dcNS);
$xpath->registerNamespace('meta', $metaNS);
$xpath->registerNamespace('office', $officeNS);
$metaNodes = $xpath->query('//office:document-meta/office:meta/*');
foreach($metaNodes as $metaNode)
{
if($metaNode->namespaceURI == $dcNS)
{
if($metaNode->localName == 'title')
{
$this->metaNodes['title'] = $metaNode;
}
if($metaNode->localName == 'subject')
{
$this->metaNodes['subject'] = $metaNode;
}
if($metaNode->localName == 'description')
{
$this->metaNodes['comments'] = $metaNode;
}
if($metaNode->localName == 'author')
{
$this->metaNodes['creator'] = $metaNode;
}
}
elseif($metaNode->namespaceURI == $metaNS)
{
if($metaNode->localName == 'keyword')
{
$this->metaNodes['keywords'] = $metaNode;
}
}
}
$officeMetaNode = $xpath->query('//office:document-meta/office:meta')->item(0);
foreach(array('title',
'subject',
'keywords',
'comments',
'author') as $key)
{
if($this->metaNodes[$key] === false)
{
if($key == 'keywords')
{
$ns = $metaNS;
}
else
{
$ns = $dcNS;
}
$node = $this->metaDoc->createElementNS($ns, $key, '');
$officeMetaNode->appendChild($node);
$this->metaNodes[$key] = $node;
}
}
}
/**
* Marks the document as changed so it will be saved when you unload it.
* Call this function if you have changed the document directly.
*
* @sa unload()
*/
public function setChanged()
{
if($this->cached == true)
{
$simplifiedFilename = String::simplifyPath($this->document);
self::$openedDocuments[$simplifiedFilename]->changed = true;
}
$this->changed = true;
}
/**
* Closes the currently opened OpenDocument file.
*
* @sa open()
*/
private function close()
{
if($this->cached == false)
{
if($this->changed == true)
{
$this->metaDoc->save("{$this->tmpdir}/meta.xml");
if(URI::permissions($this->document) & PERMISSION_MODIFY)
{
exec("cd {$this->tmpdir}; rm document.od; zip -r - * > document.od", $output, $return);
if($return == 0)
{
copy("{$this->tmpdir}/document.od", $this->document);
}
}
else
{
trigger_error("Not allowed to save {$this->document}");
}
}
// clean up
exec("rm -R {$this->tmpdir}");
$this->fileOpened = false;
$this->document = '';
unset($this->meta);
}
}
private static $openedDocuments = array();
private $fileOpened = false;
private $document = '';
private $tmpdir = '';
private $metaDoc;
private $metaNodes;
private $changed = false;
private $cached = false;
}
/**
* @internal
*/
class OpenDocument2XHTML
{
public static function reset()
{
self::$styles = array();
self::$fontFaces = array();
self::$listStyles = array();
}
/**
* @internal
*/
public static function setFontFace($name, $family)
{
self::$fontFaces[$name] = $family;
return '';
}
/**
* @internal
*/
public static function fontFaceFamily($name)
{
return (isset(self::$fontFaces[$name]) ? self::$fontFaces[$name] : '');
}
/**
* @internal
*/
public static function createStyle($family, $styleName, $parentStyleName = '')
{
if(isset(self::$styles[$family]) == false)
{
self::$styles[$family] = array();
}
if(isset(self::$styles[$family][$styleName]) == false)
{
if(isset(self::$styles[$family][$parentStyleName]))
{
self::$styles[$family][$styleName] = self::$styles[$family][$parentStyleName];
}
else
{
self::$styles[$family][$styleName] = array();
if($family == 'paragraph')
{
self::$styles[$family][$styleName]['margin-top'] = '0cm';
self::$styles[$family][$styleName]['margin-bottom'] = '0cm';
}
elseif($family == 'paragraph')
{
self::$styles[$family][$styleName]['margin'] = '0cm';
self::$styles[$family][$styleName]['padding'] = '0cm';
}
}
}
return '';
}
/**
* @internal
*/
public static function createListStyle($styleName)
{
if(isset(self::$listStyles[$styleName]) == false)
{
self::$listStyles[$styleName] = array();
}
return '';
}
/**
* @internal
*/
public static function setStyleTextProperties($family,
$styleName,
$fontName,
$fontSize,
$fontStyle,
$fontWeight,
$textUnderlineStyle,
$color,
$backgroundColor)
{
self::createStyle($family, $styleName);
self::$styles[$family][$styleName] = array_merge(self::$styles[$family][$styleName],
self::cssStyleTextProperties($fontName,
$fontSize,
$fontStyle,
$fontWeight,
$textUnderlineStyle,
$color,
$backgroundColor));
return '';
}
/**
* @internal
*/
public static function setStyleParagraphProperties($family,
$styleName,
$marginTop,
$marginBottom,
$textAlign)
{
self::createStyle($family, $styleName);
self::$styles[$family][$styleName] = array_merge(self::$styles[$family][$styleName],
self::cssStyleParagraphProperties($marginTop,
$marginBottom,
$textAlign));
return '';
}
/**
* @internal
*/
public static function setStyleTableProperties($family,
$styleName,
$width)
{
self::createStyle($family, $styleName);
self::$styles[$family][$styleName] = array_merge(self::$styles[$family][$styleName],
self::cssStyleTableProperties($width));
return '';
}
/**
* @internal
*/
public static function setStyleTableCellProperties($family,
$styleName,
$border,
$borderTop,
$borderRight,
$borderBottom,
$borderLeft,
$backgroundColor,
$padding)
{
self::createStyle($family, $styleName);
self::$styles[$family][$styleName] = array_merge(self::$styles[$family][$styleName],
self::cssStyleTableCellProperties($border,
$borderTop,
$borderRight,
$borderBottom,
$borderLeft,
$backgroundColor,
$padding));
return '';
}
/**
* @internal
*/
public static function setStyleListProperties($styleName,
$listLevel,
$bulletChar,
$numFormat)
{
self::createListStyle($styleName);
self::$listStyles[$styleName][$listLevel] = self::cssStyleListProperties($bulletChar,
$numFormat);
return '';
}
/**
* @internal
*/
public static function style($family, $styleName)
{
$styleArray = (isset(self::$styles[$family][$styleName]) ? self::$styles[$family][$styleName] :
(isset(self::$styles[$family]['']) ? self::$styles[$family][''] : array()));
$style = '';
foreach($styleArray as $key => $value)
{
if($style != '')
{
$style .= '; ';
}
$style .= "$key: $value";
}
return $style;
}
/**
* @internal
*/
public static function listStyle($styleName, $listLevel)
{
$styleArray = (isset(self::$listStyles[$styleName][$listLevel]) ?
self::$listStyles[$styleName][$listLevel] : array());
$style = '';
foreach($styleArray as $key => $value)
{
if($style != '')
{
$style .= '; ';
}
$style .= "$key: $value";
}
return $style;
}
/**
* @internal
*/
private static function cssStyleTextProperties($fontName,
$fontSize,
$fontStyle,
$fontWeight,
$textUnderlineStyle,
$color,
$backgroundColor)
{
$style = array();
if($fontName != '')
{
// fonts families are ignored purposefully, with the exception of
// seperating monospace/non-monospace
//$style['font-family'] = self::fontFaceFamily($fontName);
$font = self::fontFaceFamily($fontName);
if(strstr($font, 'Courier') !== false ||
strstr($font, 'Cumberland') !== false)
{
$style['font-family'] = 'Courier New';
}
}
if($fontSize != '')
{
// font sizes are ignored purposefully
//$style['font-size'] = $fontSize;
}
if($fontStyle != '')
{
$style['font-style'] = $fontStyle;
}
if($fontWeight != '')
{
$style['font-weight'] = $fontWeight;
}
if($textUnderlineStyle != '')
{
$style['text-decoration'] = 'underline';
}
if($color != '')
{
$style['color'] = $color;
}
if($backgroundColor != '')
{
$style['background-color'] = $backgroundColor;
}
return $style;
}
/**
* @internal
*/
private static function cssStyleParagraphProperties($marginTop,
$marginBottom,
$textAlign)
{
$style = array();
if($marginTop != '')
{
$style['margin-top'] = $marginTop;
}
if($marginBottom != '')
{
$style['margin-bottom'] = $marginBottom;
}
if($textAlign != '')
{
$style['text-align'] = $textAlign;
}
return $style;
}
/**
* @internal
*/
private static function cssStyleTableProperties($width)
{
$style = array();
if($width != '')
{
$style['width'] = $width;
}
return $style;
}
/**
* @internal
*/
private static function cssStyleTableCellProperties($border,
$borderTop,
$borderRight,
$borderBottom,
$borderLeft,
$backgroundColor,
$padding)
{
$style = array();
if($border != '')
{
$style['border'] = $border;
}
if($borderTop != '')
{
$style['border-top'] = $borderTop;
}
if($borderRight != '')
{
$style['border-right'] = $borderRight;
}
if($borderBottom != '')
{
$style['border-bottom'] = $borderBottom;
}
if($borderLeft != '')
{
$style['border-left'] = $borderLeft;
}
if($backgroundColor != '')
{
$style['background-color'] = $backgroundColor;
}
if($padding != '')
{
$style['padding'] = $padding;
}
return $style;
}
/**
* @internal
*/
private static function cssStyleListProperties($bulletChar,
$numFormat)
{
$style = array();
if($bulletChar != '')
{
if($bulletChar == 'â¢')
{
$style['list-style'] = 'disc';
}
elseif($bulletChar == 'î')
{
$style['list-style'] = 'square';
}
}
else
{
if($numFormat == '1')
{
$style['list-style'] = 'decimal';
}
elseif($numFormat == 'a')
{
$style['list-style'] = 'lower-latin';
}
elseif($numFormat == 'A')
{
$style['list-style'] = 'upper-latin';
}
elseif($numFormat == 'i')
{
$style['list-style'] = 'lower-roman';
}
elseif($numFormat == 'I')
{
$style['list-style'] = 'upper-roman';
}
}
return $style;
}
private static $styles;
private static $fontFaces;
private static $listStyles;
}
?>