2010年6月22日 星期二

[Javascript] EPUB Reader?

原先想在寫一個 EPUB Reader 的,後來被建議乾脆找純 Javascript 的來做做,畢竟 HTML5 也越來越熱囉!隨意地找尋,看到有兩套:



第一套似乎很威,可以直接從 EPUB 格式讀取資料出來,看他的 lib 裡的確也有 zip、base64 等相關字眼,只可惜初步測試好像有點問題,又很順手地找到 Monocle 這套!並且還是 MIT License !他類似只是一個 EBook 的套版,把資料餵給他吃就行了!



Monocle 挺厲害的,在網頁上直接地 show 一本電子書,馬上就可以體驗操作介面。上面那本書則是網站上的一個範例,整個就幾乎跟 iBooks 很像啦!因此就挑他上手吧!我的工作就只剩下把 EPUB 裡頭的東西抽出來組一個資料結構來餵給 Monocle 而已啦!(另外還要先對 EPUB 做 Unzip 囉)


以下是需用到的 Javascript 元件:



  • DOMParser

  • XMLHttpRequest


快速簡單的範例:


@index.html


<html>
    <head>
        <script type="text/javascript" src="src/monocle.js"></script>
        <script type="text/javascript" src="src/book.js"></script>
        <script type="text/javascript" src="src/compat.js"></script>
        <script type="text/javascript" src="src/component.js"></script>
        <script type="text/javascript" src="src/framer.js"></script>
        <script type="text/javascript" src="src/place.js"></script>
        <script type="text/javascript" src="src/reader.js"></script>
        <script type="text/javascript" src="src/styles.js"></script>
        <script type="text/javascript" src="src/controls/contents.js"></script>
        <script type="text/javascript" src="src/controls/magnifier.js"></script>
        <script type="text/javascript" src="src/controls/placesaver.js"></script>
        <script type="text/javascript" src="src/controls/scrubber.js"></script>
        <script type="text/javascript" src="src/controls/spinner.js"></script>
        <script type="text/javascript" src="src/flippers/instant.js"></script>
        <script type="text/javascript" src="src/flippers/legacy.js"></script>
        <script type="text/javascript" src="src/flippers/scroller.js"></script>
        <script type="text/javascript" src="src/flippers/slider.js"></script>

        <script type="text/javascript" src="UnzipEPUBParser.js"></script>

    </head>
    <body>
        <div id="reader" style="width: 300px; height: 400px"></div>
        <script>
            var test = new UnzipEPUBParser();
            test.initWithPath( 'MyTestBook' );
            
            var bookData = {
                getComponents: function () {
                    return test.navList;
                    return [
                        'component1.xhtml',
                        'component2.xhtml',
                        'component3.xhtml',
                        'component4.xhtml'
                    ];
                },
                getContents: function () {
                    return test.navPoint;
                    return [
                        {
                            title: "Chapter 1",
                            src: "component1.xhtml"
                        },
                        {
                            title: "Chapter 2",
                            src: "component3.xhtml#chapter-2"
                        }
                    ];
                },
                getComponent: function (componentId) {
                    var raw = test.getFileContent( test.basePath + '/' + test.navMap[componentId]['src'] );
                    if( !raw.status )
                        return null;
                    return raw.data;

                    return {
                        'component1.xhtml':'<h1>Chapter 1</h1><p>Hello world</p>',
                        'component2.xhtml':'<p>Chapter 1 continued.</p>',
                        'component3.xhtml':'<p>Chapter 1 continued again.</p>' + '<h1 id="chapter-2">Chapter 2</h1>' +'<p>Hello from the second chapter.</p>',
                        'component4.xhtml':'<p>THE END.</p>'
                    }[componentId];
                },
                getMetaData: function(key) {
                    return {
                        title: "A book",
                        creator: "Inventive Labs"
                    }[key];
                }
            }

            // Initialize the reader element.
            var reader = Monocle.Reader('reader');

            // Initialize a book object. (Of course we could have just passed it in
            // as the second argument to the reader constructor, which would also
            // obviate the need for the setBook call below. This is the scenic route.)
            var book = Monocle.Book(bookData);

            // Assign the book to the reader and go to the 3rd page.
            reader.setBook(book);
            //reader.moveTo({ page: 1 });
        </script>
    </body>
</html>


@UnzipEPUBParser.js


function UnzipEPUBParser() {}
UnzipEPUBParser.prototype.basePath = '';
UnzipEPUBParser.prototype.status = false;
UnzipEPUBParser.prototype.error = null;
UnzipEPUBParser.prototype.navPoint = new Array();
UnzipEPUBParser.prototype.navMap = new Array();
UnzipEPUBParser.prototype.navList = new Array();
UnzipEPUBParser.prototype.metadata = new Array();
UnzipEPUBParser.prototype.getFileContent = function( path ) {
    var ajReq = new XMLHttpRequest();
    try{
        ajReq.open( "GET", path , false);
        ajReq.send(null);
    }catch(err){
        return { 'status':false , 'data':err };
    }
    return { 'status':true , 'data':ajReq.responseText };
};
UnzipEPUBParser.prototype.initWithPath = function( path ) {
    this.basePath = path;

    var parser = new DOMParser();
    var xmlDoc = null ;
    var obj = null;

    var META_INF_CONTAINER = this.getFileContent( path + '/META-INF/container.xml' );
    if( !META_INF_CONTAINER.status )
    {
        this.status = false;
        this.error = "Cannot open the file: "+path+"/META-INF/container.xml";
        return this.status;
    }
    else if( META_INF_CONTAINER.data == null || META_INF_CONTAINER.data == '' )
    {
        this.status = false;
        this.error =  "empty file: "+path+"/META-INF/container.xml";
        return this.status;
    }

    xmlDoc = parser.parseFromString( META_INF_CONTAINER.data , "text/xml" );    
    if( !( obj = xmlDoc.getElementsByTagName( 'rootfile' )[0] ) || !obj.getAttribute( 'full-path' ) ) {
        this.status = false;
        this.error = "Cannot find the <rootfile> or 'full-path'";
        return this.status;
    }

    var CONTENT_OPF = this.getFileContent( path + '/' + obj.getAttribute( 'full-path' ) );
    if( !CONTENT_OPF.status )
    {
        this.status = false;
        this.error = "Cannot open the file: "+ path + '/' + obj.getAttribute( 'full-path' );
        return this.status;
    }
    else if( CONTENT_OPF.data == null || CONTENT_OPF.data == '' )
    {
        this.status = false;
        this.error =  "empty file: "+path + '/' + obj.getAttribute( 'full-path' );
        return this.status;
    }

    var get_base_path = -1;
    if( ( get_base_path = obj.getAttribute( 'full-path' ).lastIndexOf( '/' ) ) > 0 )
    {
        path += '/' + obj.getAttribute( 'full-path' ).substring( 0 , get_base_path );
        this.basePath = path;
    }

    xmlDoc = parser.parseFromString( CONTENT_OPF.data , "text/xml" );    
    if( !( obj = xmlDoc.getElementsByTagName( 'manifest' )[0] ) || !( obj = obj.getElementsByTagName( 'item' ) ) ){
        this.status = false;
        this.error = "Cannot find the <manifest> or <item>";
        return this.status;
    }

    var ncx_path  = null;
    for( var i=0 , cnt=obj.length ; i<cnt ; ++i ) {
        //console.log( obj[i].getAttribute('id') + "\n"  );
        if( obj[i].getAttribute('id') == 'ncx' )
            ncx_path = obj[i].getAttribute('href');
    }
    if( !ncx_path ) {
        this.status = false;
        this.error = "Cannot find the ncx info";
        return this.status;
    }

    var TOC_NCX = this.getFileContent( path + '/' + ncx_path );
    if( !TOC_NCX.status )
    {
        this.status = false;
        this.error = "Cannot open the file: "+ path + '/' + ncx_path ;
        return this.status;
    }
    else if( TOC_NCX.data == null || TOC_NCX.data == '' )
    {
        this.status = false;
        this.error =  "empty file: "+path + '/' + ncx_path;
        return this.status;
    }
    xmlDoc = parser.parseFromString( TOC_NCX.data , "text/xml" );    
    if( !( obj = xmlDoc.getElementsByTagName( 'navMap' )[0] ) || !( obj = obj.getElementsByTagName( 'navPoint' ) ) ){
        this.status = false;
        this.error = "Cannot find the <navMap> or <navPoint>";
        return this.status;
    }

    for( var i=0 , cnt=obj.length ; i<cnt ; ++i ) {
        //console.log( obj[i].getAttribute('id') + "\n"  );
        //console.log( obj[i].getElementsByTagName( 'text' )[0].childNodes[0].nodeValue );
        if(     obj[i].getElementsByTagName( 'content' )[0]
            && obj[i].getElementsByTagName( 'content' )[0].getAttribute( 'src' )
            && obj[i].getElementsByTagName( 'text' )[0]
            && obj[i].getElementsByTagName( 'text' )[0].childNodes[0].nodeValue
            && obj[i].getAttribute( 'id' )
            && obj[i].getAttribute( 'playOrder' )
        )
        {
            this.navMap[ obj[i].getAttribute( 'id' ) ] = {
                'id': obj[i].getAttribute( 'id' ) ,
                'title': obj[i].getElementsByTagName( 'text' )[0].childNodes[0].nodeValue ,
                'src': obj[i].getElementsByTagName( 'content' )[0].getAttribute( 'src' )  ,
                'order': obj[i].getAttribute( 'playOrder' )
            };
            this.navPoint.push( this.navMap[ obj[i].getAttribute( 'id' ) ] );
            this.navList.push( obj[i].getAttribute( 'id' ) );
        }
    }
    //console.log( this.navPoint );
    //console.log( this.navMap );
    //console.log( obj );
    //console.log( xmlDoc.getElementsByTagName( 'rootfile' ) );
    this.status = true;
    this.error = null;
    return this.status;

    //console.log( xmlDoc );
    //alert( xmlDoc );
    //alert( xmlDoc['childNodes'] );
    //console.log( xmlDoc['childNodes'] );
};


如此一來只要擺定好東西就能搞定啦!目錄結構:


MyTestBook/
        META-INF/
                container.xml
        ...
index.html
UnzipEPUBParser.js
src/
        book.js
        compat.js
        component.js
        controls/
                ...
        flippers/
                ...
        framer.js
        monocle.js
        place.js
        reader.js
        styles.js


[教學] 保證學會製作iPad適用的電子書格式 裡頭的 三國演義ePub 作為範例,在 FireFox 瀏覽器上呈現結果:


EPUB Browser EPUB Browser EPUB Browser


沒有留言:

張貼留言