Create a Simple Document Viewer with ExtJS

A document or report viewer is simply a tool that can be used to provide quick, indexed access to various PDF report files, image files or even word documents. You can easily index your documents as you author them but some versions of PDF tools don’t permit this. Another option would be to create a Microsoft Powerpoint slideshow – but this doesn’t give you the left-hand navigation tree capabilities of an indexed Adobe PDF (for example.)

I wanted to create a tool that could be used to provide visual access to many individual documents while presenting an easy-to-navigation tree structure based on the groupings or categories of these documents.

This article will provide the tools and examples necessary to enable you to create a simple, light-weight, portable (zip and go), cross-browser, web-based (serverless) document viewer. The intent is to have an archive that you can mail or carry on a thumb drive and distribute freely to customers, prospects or colleagues to enable quick and easy viewing and access to a collection of PDF or other files.

As I always do… let’s cut to the chase… looking for the downloads or source code?
[download id=”12″]

Before we start, I’ll also point you to the working demo / example.

A west region with a treeview control and a center panel with details that change as you select items from the treeview. This type of interface is ubiquitous and yet creating something like this to simply view documents may seem very difficult for folks that haven’t worked with javascript. The document viewer application was created to provide a mechanism for viewing collections of sample reports.  A design goal for this application was for it to be completely standalone – not requiring any additional software or web server to view the documents.

At the heart of this system is a perl script – parse_csv.pl. This is a perl script that was written to create a javascript object notation assignment (JSON) used to populate a treeview control for the purpose of displaying example pdf reports with previews. In order to use this package to create your own custom file or report viewer, you must create a comma separated value (CSV) file with the following values.

REPORTFILE,PREVIEW,GROUP,TITLE,DESCRIPTION

Column 1 – Report file specifies the name of the PDF report file that will be downloadable from within the viewer.

Column 2 – Preview file specifies the name of the preview or thumbnail image that will be presented in the details pane of the viewer application.

Column 3 – Group specifies the treeview group where this report will be grouped.  You should sort your csv file so that groups are stored together.

Column 4 – Title specifies the title of the report and will be displayed in the details pane of when the user clicks on the report in the treeview.

Column 5 – Description specifies the details of what the report will show.

Example

REPORTFILE,PREVIEW,GROUP,TITLE,DESCRIPTION
reportfile_number_one.pdf,preview_of_report_file_one.png,Group One, Example of a Report File One,This report shows some serious stuff.
reportfile_number_two.pdf,preview_of_report_file_two.png,Group One, Example of a Report File Two,This report shows some more serious stuff.
reportfile_number_three.pdf,preview_of_report_file_three.png,Group Two, Example of a Report File Three,This report shows some more serious stuff.
reportfile_number_four.pdf,preview_of_report_file_four.png,Group Two, Example of a Report File Four,This report shows some more serious stuff.
reportfile_number_five.pdf,preview_of_report_file_five.png,Group Two, Example of a Report File Five,This report shows some more serious stuff.
reportfile_number_six.pdf,preview_of_report_file_six.png,Group Three, Example of a Report File Six,This report shows some more serious stuff.
reportfile_number_seven.pdf,preview_of_report_file_seven.png,Group Three, Example of a Report File Seven,This report shows some more serious stuff.

You may include the column headers in your file – however, ensure that you use the --skipheader flag when executing the parse_csv.pl script.

Requirements
Perl.  The parse_csv.pl script is written in perl so you will need an environment were you can execute perl scripts.  A subrequirement is that I’m using some libraries that you may not have installed in your perl environment.

Checkout http://search.cpan.org if you get some errors about missing libraries.

Here’s an overview of the process to create your own custom document viewer.

  1. Download the [download id=”12″] package.  This will contain everything you need to

create and customize your own self contained document viewer.  Self contained

means that you don’t even need to host these documents and the viewer on a web

server… you can simply open the index.html file from a browser.

  1. Review the directory tree for the example package.
.
|-- css     Style Sheets
|-- extjs    Javascript Library for the treeview and panel components
|-- images    Various supporting image files (like icons, etc.)
|-- js    Javascript source files
|-- reports    Report directory - this is where you'll place the reports, pdfs and preview thumbnail files that will be displayed in the viewer.
|-- scripts    Scripts used to create and customize your viewer
  1. Since you’ll be creating your own document or report viewer, you’ll probably want to delete the contents of the reports directory.
  1. Create the report files that you’ll be displaying in the document viewer.  Report files can be PDF, PowerPoint (ppt) or Word Document (doc) files.  Typically, the report file examples are multi-page pdf files.  Once you have the report files, copy them into the reports directory in the base of the example package.
  1. Create a screenshot, or preview image that will be displayed in the preview section of the details panel when viewing the report.  zScreen is a great, free utility that lets you capture portions of your screen and save them to files.  I prefer ‘png’ format because of smaller size.  You’ll need to create one preview image for each report you plan to display in the document viewer.  Copy or place these preview image files into the reports directory along side the actual pdf or ppt report files.
  1. Now you’ll need to create a comma-separated file that contains the names of your report files, the groups they’ll be displayed in, the document titles and a description of each.  As previously mentioned the format for this file is REPORTFILE, PREVIEW, GROUP, TITLE, and DESCRIPTION.  You can find an example in the scripts directory in a file called example.csv.
  2. Create the documents.js file.  This is the file which contains the Javascript Notation (JSON) which is used to populate the treeview control as seen in this image.  The package contains a script named parse_csv.pl and can be found in the scripts directory of the example package.  This script should be run from the main directory of the archive and must be provided several command line options in order to execute properly.  The following is an example execution of the script:

scripts/parse_csv.pl --csvfile scripts/example.csv --outputfile js/documents.js

This was executed from the base directory of the example archive file.  The –csvfile argument tells parse_csv.pl where to file the csv file that you created in step 5.  The –outputfile argument tells parse_csv.pl where to write the json.

It is important to note that the script assumes that you have not modified the directory layout and that you are storing your reports and previews or thumbnails in the “reports” directory.  You may run the script with “—help” to obtain additional information and options.

$ scripts/parse_csv.pl --help


Usage: scripts/parse_csv.pl --csvfile <filename> [--outputfile <filename.js>]
[--outputfile <outputfilename>] controls the output javascript filename - defaults to stdout
[--reportdir <reportdirectory>] specifies the directory where the pdf files may be stored
[--thumbdir <thumbnailedirectory>] specifies the directory where the thumbnail or preview image files may be stored
[--htmltitle <htmltitle>] specifies the title to be used in the report viewer html file
[[--overwrite] | [--nooverwrite]] - controls whether the output file will be overwritten

The following is an example of the resultant documents.js file that is created.


var json=[{"iconCls":"group","text":"Group One","children":[{"content":"reports/reportfile_number_one.pdf","description":"This report shows some serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_one.png","text":"Example of a Report File One","id":"1","leaf":"true"},{"content":"reports/reportfile_number_two.pdf","description":"This report shows some more serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_two.png","text":"Example of a Report File Two","id":"2","leaf":"true"}],"id":0.0960750988024337},{"iconCls":"group","text":"Group Three","children":[{"content":"reports/reportfile_number_six.pdf","description":"This report shows some more serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_six.png","text":"Example of a Report File Six","id":"6","leaf":"true"},{"content":"reports/reportfile_number_seven.pdf","description":"This report shows some more serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_seven.png","text":"Example of a Report File Seven","id":"7","leaf":"true"}],"id":0.119647523478733},{"iconCls":"group","text":"Group Two","children":[{"content":"reports/reportfile_number_three.pdf","description":"This report shows some more serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_three.png","text":"Example of a Report File Three","id":"3","leaf":"true"},{"content":"reports/reportfile_number_four.pdf","description":"This report shows some more serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_four.png","text":"Example of a Report File Four","id":"4","leaf":"true"},{"content":"reports/reportfile_number_five.pdf","description":"This report shows some more serious stuff.","size":"16.23 KB","published":"01-29-2012","iconCls":"pdf","preview":"reports/preview_of_report_file_five.png","text":"Example of a Report File Five","id":"5","leaf":"true"}],"id":0.423250772097287}]

var htmltitle='Example Report Viewer;'

Notice that the script determines the proper filetype and assigns the appropriate icon class – pdf for pdfs, ppt for ppts, etc.  Also note that the script determines the file size and date created.  These attributes are displayed in the details panel when viewing the documents.

Again… here’s the goods:
[download id=”12″]

Mobile Device Detection and Redirection with Perl and CGI

Last month, I took some time and wrote about Mobile Device Detection and Redirection with PHP. Well, it turns out that some folks can’t (or choose not to) make use of PHP and favor the Perl/CGI approach to web development. This article covers the same bases and presents a set of examples implemented using Perl and Lincoln Stein’s great CGI.pm module.

Download Code and Examples Here:
From Github: http://github.com/mrlynn/MobileBrowserDetectionExample

or locally:
Version 0.3 (Latest – includes perl/cgi AND PHP examples)
[download id=”2″]

Version 0.2 (Older version – PHP examples only)
[download id=”1″]

View the working example using Perl/CGI here: https://mlynn.org/uatest/cgi-bin/index.cgi

Redirecting a user based on the type of device they are using is not rocket science. In fact, it can be accomplished quite simply using an apache redirect in a .htaccess file.

RewriteCond %{HTTP_USER_AGENT} ^.*iPad.*$
RewriteRule ^(.*)$ http://ipad.yourdomain.com [R=301]

The issue with this approach is that users coming to your site using an iPad will never have the ability to see the other versions of your site. for many, that may be a satisfactory solution. However, if you want users to have the ability to view a version of your site designed specifically for their type of mobile device by default – but have the option to click a link and either view or optionally set a preference for another version of the site, this article is for you.

Perl and CGI have been around for many years and are in wide use on the web. Perl is extremely extensible and using the very popular CGI.pm Perl Module written by Lincoln Stein, it’s very easy to write code to dynamically manage your web site.

Where PHP has built-in variables designed to expose the server and apache session environment ($_SERVER, $_SESSION), Perl and CGI.pm make use of %ENV and module variables such as param(). Consider the following table which contrasts PHP and Perl/CGI.

User Agent

PHP

$_SERVER['HTTP_USER_AGENT']

Perl/CGI

$ENV{HTTP_USER_AGENT}

Reading Cookies

PHP

$cookievalue = $_COOKIE['COOKIENAME']

Perl/CGI

use CGI;
$query=new CGI;
$cookievalue = $query-&gt;cookie('COOKIENAME');

Setting Cookies

PHP

setcookie(name, value, expire, path, domain);

Perl/CGI

$cookie = $query-&gt;cookie(-name=&gt;'COOKIENAME',
			 -value=&gt;'COOKIEVALUE',
			 -expires=&gt;'+4h',
			 -path=&gt;'/');
print $query-&gt;header(-cookie=&gt;$cookie);

Now that we have the basics down, the rest is easy. We’ll want to check the users’ device type by querying the user agent, check the value of a cookie to see if they’ve specified a preference for something other than the site designed for their specific device and redirect accordingly.

The initial landing page does most of the heavy lifting.

index.cgi

#!/usr/bin/perl
require 'includes/config.inc.pl';
require 'includes/functions.inc.pl';

use CGI;
$query = new CGI; 

$useragent = $ENV{HTTP_USER_AGENT};
$sitepref = $query->cookie( 'SITEPREF' );

if ( ismobile( $useragent ) ) {
        if ( !$sitepref ) {
          if ( isipad( $useragent ) ) {
                  print $query->redirect( "$IPADURL" );
          } else {
              if ( isiphone( $useragent ) ) {
                          print $query->redirect( "$IPHONEURL" );
                  } else {
                          print $query->redirect( "$MOBILEURL" );
                  }
          }
        } else {
          if ( $sitepref=='MOBILE' ) {
                  print $query->redirect( "$MOBILEURL" );
          } else {
                  if ( $sitepref=='IPHONE' ) {
                          print $query->redirect( "$IPHONEURL" );
                  } else {
                          if ( $sitepref=='IPAD' ) {
                                  print $query->redirect( "$IPADURL" );
                          } else {
                                  print $query->redirect( "$NORMALURL" );
                          }
                  }
            }
        }
} else {
    if ( !$sitepref ) {
          print $query->redirect( "$NORMALURL" );
    } else {
      if ( $sitepref=='MOBILE' ) {
          print $query->redirect( "$MOBILEURL" );
      } else {
          if ( $sitepref=='IPAD' ) {
              print $query->redirect( "$IPADURL" );
          } else {
              if ( $sitepref=='IPHONE' ) {
                  print $query->redirect( "$IPHONEURL" );
              } else {
                  print $query->redirect( "$NORMALURL" );
              }
          }  
      }
    }
}

And the included files… which set the URL values and contain many of the functions.

functions.inc.pl

sub setcookiealive() {
        my ($name,$value,$expires,$redirect) = @_;
        $cookie = $query->cookie(-name=>"$name",
                         -value=>"$value",
                         -expires=>'+5d',
                         -path=>'/');
        if ($redirect) {
                print $query->redirect("$redirect","-cookie=>$cookie");
        } else {
                print $query->header(-cookie=>$cookie);
        }
}

sub in_array {
     my ($arr,$search_for) = @_;
     my %items = map {$_ => 1} @$arr; # create a hash out of the array values
     return (exists($items{$search_for}))?1:0;
}

sub ismobile {
        $useragent=lc(@_);
        $is_mobile = '0';

        if($useragent =~ m/(android|up.browser|up.link|mmp|symbian|smartphone|midp|wap|phone)/i) {
                $is_mobile=1;
        }

        if((index($ENV{HTTP_ACCEPT},'application/vnd.wap.xhtml+xml')>0) || ($ENV{HTTP_X_WAP_PROFILE} || $ENV{HTTP_PROFILE})) {
                $is_mobile=1;
        }

        $mobile_ua = lc(substr $ENV{HTTP_USER_AGENT},0,4);
        @mobile_agents = ('w3c ','acs-','alav','alca','amoi','andr','audi','avan','benq','bird','blac','blaz','brew','cell','cldc','cmd-','dang','doco','eric','hipt','inno','ipaq','java','jigs','kddi','keji','leno','lg-c','lg-d','lg-g','lge-','maui','maxo','midp','mits','mmef','mobi','mot-','moto','mwbp','nec-','newt','noki','oper','palm','pana','pant','phil','play','port','prox','qwap','sage','sams','sany','sch-','sec-','send','seri','sgh-','shar','sie-','siem','smal','smar','sony','sph-','symb','t-mo','teli','tim-','tosh','tsm-','upg1','upsi','vk-v','voda','wap-','wapa','wapi','wapp','wapr','webc','winw','winw','xda','xda-');

        if(in_array(@mobile_agents,$mobile_ua)) {
                $is_mobile=1;
        }

        if ($ENV{ALL_HTTP}) {
                if (index(lc($ENV{ALL_HTTP}),'OperaMini')>0) {
                        $is_mobile=1;
                }
        }

        if (index(lc($ENV{HTTP_USER_AGENT}),'windows')>0) {
                $is_mobile=0;
        }
    return $is_mobile;
}

sub isiphone {

        $useragent = @_;
        $iphone=0;
        if (lc($useragent) =~ m/iphone/) {
                $iphone=1;
        }
        return $iphone;
}

sub isipad {

        $useragent = @_;
        $ipad=0;
        if (lc($useragent) =~ m/ipad/) {
                $ipad=1;
        }
        return $ipad;
}
1;

config.inc.pl

$SITEURL='https://mlynn.org/uatest/cgi-bin/index.cgi';
$LOGFILE='debug.log';
$MOBILEURL='https://mlynn.org/uatest/cgi-bin/mobile/index.cgi';
$IPHONEURL='https://mlynn.org/uatest/cgi-bin/iphone/index.cgi';
$IPADURL='https://mlynn.org/uatest/cgi-bin/ipad/index.cgi';
$NORMALURL='https://mlynn.org/uatest/cgi-bin/normal/index.cgi';
1;

Related Sites and Interesting Links

Have a site you’d like to add to this list? Send me an email or submit a comment.