2012-10-10

PHP 輸出 OpenDocument Spreadsheet

OpenDocument Format 是一種開放文件格式,廣範於開放辦公室軟件上應用
例如 OpenOffice.org (現為 Apache OpenOffice), LibreOffice, KOffice 等辦公室軟件
閉源的 Microsoft Office 由 2007 開始 及 線上辦公室應用的 Google Docs (現為 Google Drive 的一部分)
亦支援 OpenDocument Format

OpenDocument Format 由於使用 XML ,檔案於資料交換上非常簡單及方便
OpenDocument Format 將文件內容,文件風格,圖像等分開保存,再利用 Zip 壓縮技術將所有檔案包裝成一個獨立檔案
所以以任何文字編輯器觀察文件,會發現文件頭 2 個位元組是 PK (即 Phil Katz Zip 的作者)
在 Windows 環境下強行將副案名改為 zip 便可以將檔案壓縮

這篇文章主要介紹只使用 XML 製作 OpenDocument Spreadsheet (ods) 格式
為甚麼要使用 ods ? 而不使用更簡單的 Comma Separated Values (csv) 或 Plain Text (txt)
預設的 Spreadsheet ,csv 以逗號分隔,而 txt 以 tab 分隔
其他開源辦公室軟件,還可以自訂欄位分隔號及文字分隔符
兩者雖然非常簡單方便,但兩者都不能加入風格,例如文字顏色、欄位顏色、邊界顏色等
雖然 ods 不及 csv 及 txt 簡單,但 ods 仍然是一種不錯的 Spreadsheet 格式

若果只製作純資料的 OpenDocument Spreadsheet 文件
文件配置上只需要有 META-INF/manifest.xml, content.xml 這兩個文件

META-INF/manifest.xml 為指示以哪一種 OpenDocument Format 開啟及處理方式,因此這個檔案非常重要
(注意,META-INF 為全大寫字母)
content.xml 包含文件版面設定、字型、風格,及最主要的內容


META-INF/manifest.xml 內容大致如下,使用最基本的 OpenDocument Format 資訊
<?xml version="1.0" encoding="UTF-8"?><!--
--><manifest:manifest xmlns:manifest="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"><!--
    --><manifest:file-entry manifest:media-type="application/vnd.oasis.opendocument.spreadsheet" manifest:full-path="/"/><!--
    --><manifest:file-entry manifest:media-type="text/xml" manifest:full-path="content.xml"/><!--
--></manifest:manifest>

content.xml 內容大致如下
<?xml version="1.0" encoding="UTF-8"?><!--
--><office:document-content xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0" xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" xmlns:table="urn:oasis:names:tc:opendocument:xmlns:table:1.0" xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0" xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0" office:version="1.0"><!--
    --><office:body><!--
        --><office:spreadsheet><!--
            --><table:table table:name="Sheet1"><!--
                --><table:table-row><!--
                    --><table:table-cell office:value-type="string"><!--
                        --><text:p><![CDATA[hello, world]]></text:p><!--
                    --></table:table-cell><!--
                --></table:table-row><!--
            --></table:table><!--
        --></office:spreadsheet><!--
    --></office:body><!--
--></office:document-content>

當中的
<table:table table:name="Sheet1"><!--
    --><table:table-row><!--
        --><table:table-cell office:value-type="string"><!--
            --><text:p><![CDATA[hello, world]]></text:p><!--
        --></table:table-cell><!--
    --></table:table-row><!--
--></table:table>
就是顯示的資訊部分
大致上與 HTML 的 table tr td 差不多
<![CDATA[hello, world]]> 這種寫法是避免文字包含「&」、「>」、「<」這類 XML 特殊字元而發生問題


另外常用的跨列跨欄屬性 table:number-rows-spanned, table:number-columns-spanned 及 文字屬性 office:value-type
有 string (字串), float (浮點數), boolean (真假值) 等常用文字屬性

*註
由於由開源辦公室軟件所編製的 ods 不會包含 newline 及 tab 而是用單行編寫
使用者閱讀上相當困難,尤其資料非常龐大時,文字編輯器對單行編寫的閱讀能力亦會下降
而在下使用了大量 <!----> 是為了避免 XML 解析 newline 及 tab 卻保留閱讀性
但這種寫法會令文件體積增加 (單是一組 <!----> 已經 7 個位元組,還未計算 newline 及 tab 數量)


然後編製 ZipFile.php
<?php
    
class ZipFile{

    private $datasec = array();
    private $ctrl_dir = array();
    private $eof_ctrl_dir = "\x50\x4b\x05\x06\x00\x00\x00\x00";
    private $old_offset = 0;

    private function unix2DosTime($unixtime = 0){
        $timearray = (($unixtime == 0) ? getdate() : getdate($unixtime));
        if ($timearray['year'] < 1980){
            $timearray['year'] = 1980;
            $timearray['mon'] = 1;
            $timearray['mday'] = 1;
            $timearray['hours'] = 0;
            $timearray['minutes'] = 0;
            $timearray['seconds'] = 0;
        }

        return (($timearray['year'] - 1980) << 25) |
            ($timearray['mon'] << 21) |
            ($timearray['mday'] << 16) |
            ($timearray['hours'] << 11) |
            ($timearray['minutes'] << 5) |
            ($timearray['seconds'] >> 1);
    }

    public function addFile($data, $name, $time = 0){
        $name = str_replace('\\', '/', $name);

        $dtime = substr('00000000' . dechex($this->unix2DosTime($time)), -8);
        $hexdtime = '\x' . $dtime[6] . $dtime[7] . '\x' . $dtime[4] . $dtime[5]
            . '\x' . $dtime[2] . $dtime[3]
            . '\x' . $dtime[0] . $dtime[1];
        eval('$hexdtime = "' . $hexdtime . '";');

        $fr = "\x50\x4b\x03\x04";
        $fr .= "\x14\x00";
        $fr .= "\x00\x00";
        $fr .= "\x08\x00";
        $fr .= $hexdtime;

        $unc_len = strlen($data);
        $crc = crc32($data);
        $zdata = gzcompress($data);
        $zdata = substr(substr($zdata, 0, strlen($zdata) - 4), 2);
        $c_len = strlen($zdata);
        $fr .= pack('V', $crc);
        $fr .= pack('V', $c_len);
        $fr .= pack('V', $unc_len);
        $fr .= pack('v', strlen($name));
        $fr .= pack('v', 0);
        $fr .= $name;
        $fr .= $zdata;

        $this->datasec[] = $fr;

        $cdrec = "\x50\x4b\x01\x02";
        $cdrec .= "\x00\x00";
        $cdrec .= "\x14\x00";
        $cdrec .= "\x00\x00";
        $cdrec .= "\x08\x00";
        $cdrec .= $hexdtime;
        $cdrec .= pack('V', $crc);
        $cdrec .= pack('V', $c_len);
        $cdrec .= pack('V', $unc_len);
        $cdrec .= pack('v', strlen($name));
        $cdrec .= pack('v', 0);
        $cdrec .= pack('v', 0);
        $cdrec .= pack('v', 0);
        $cdrec .= pack('v', 0);
        $cdrec .= pack('V', 32);

        $cdrec .= pack('V', $this->old_offset);
        $this->old_offset += strlen($fr);

        $cdrec .= $name;
        $this->ctrl_dir[] = $cdrec;
    }

    public function file(){
        $data = implode('', $this->datasec);
        $ctrldir = implode('', $this->ctrl_dir);

        return $data . $ctrldir . $this->eof_ctrl_dir .
        pack('v', sizeof($this->ctrl_dir)) . pack('v', sizeof($this->ctrl_dir)) .
        pack('V', strlen($ctrldir)) . pack('V', strlen($data)) . "\x00\x00";
    }
}

?>

這個類別可能閣下已經見過,這個類別其實來自 phpMyAdmin
由於減少多餘的資料,在下將原來的 comment 全部刪除,若果想查看原文件,請到 phpmyadmin/libraries/zip.lib.php
(注意:不同的 phpmyadmin 設定有機會不同,如 NAS, Windows, Linux 等,本文使用 LinuxMint)
這個類別的好處是不需要創建額外檔案便可以直接輸出 zip 檔



<?php

// require('ZipFile.php');
header('Content-Type: application/zip; charset=UTF-8');
header('Content-Disposition: attachment; filename="myzip.zip"');
$zip = new ZipFile();
$zip->addFile('my content', 'content.txt');
echo $zip->file();

?>




這樣便可以輸出 zip 檔

同樣道理,由於 ods 只是 zip
只要將輸出 MIME 改寫為 application/vnd.oasis.opendocument.spreadsheet
及加載檔案名為 META-INF/manifest.xml 及 content.xml 即可



<?php

// require('ZipFile.php');
header('Content-Type: application/vnd.oasis.opendocument.spreadsheet; charset=UTF-8');
header('Content-Disposition: attachment; filename="myods.ods"');
$zip = new ZipFile();
$zip->addFile($manifest_string, 'META-INF/manifest.xml');
$zip->addFile($content_string, 'content.xml');
echo $zip->file();

?>





另外還可以透過各類壓縮軟件,直接將檔案壓縮為 zip
以 7-Zip 為例子,由於 7-Zip 為跨平台軟件,利用 7-Zip 以 zip 壓縮文件可以無視不同平台的問題
7z a -tzip -mx9 myods.ods content.xml META-INF/
利用 7-Zip 指令將 content.xml 及 META-INF/ 壓縮為 zip 同時直接將檔案輸出為 ods
關於 7-Zip指令 請查看在下另一篇文章



在 GUI 的操作下,使用 Windows 內建的 zip 或 Linux / Unix / Mac 的 Archive Manager 選擇 zip
亦可以達至相同效果

透過具 OpenDocument Spreadsheet 分析功能既辦公室軟件開啟

沒有留言 :

張貼留言