Some Background
PDFs are probably the most popular digital document exchange format on the planet. Being recently ratified as an ISO standard can’t hurt Adobe’s cause either. Because of its popularity, there are many instances where you run into these documents on the web and viewing them in place is largely faked by our browsers. Clicking on one of these PDFs causes Adobe to open while the document downloads in the background, and eventually, you get to see it in all its glory. Well, I say there’s a better way.
What’s the Point?
It’s simple. PDFs can be large. What’s the point of taking the time to download the whole document and start Adobe Reader if you only want to see the 150th page of our 369 page documentation PDF? There isn’t. ThinDoc achieves the goal of being a zero-footprint PDF Viewer using the WebImageViewer from Atalasoft’s DotImage SDK, and with a little bit of magic, can be integrated into your daily life with no programming knowledge whatsoever. Being “zero-footprint” implies that ThinDoc requires no plugins (like Flash), Acrobat Reader, or Applets. It is pure JavaScript and HTML on the client.
The Design Goals
It needed to be fast, it needed to be full screen (read “full browser”), and it needed to be simple.
- The first of these we get for free. The WebImageViewer loads images in the same tile-based fashion as Google Maps.
- Full Screen is a little tough due to the requirements by W3C in setting height to a percentage being calculated based on its containing block (which is generally the height of the content within it).
- Simple.. hmm, that’s sort of subjective, but we can deal with that later.
Full-screening the WebImageViewer
To make any block level element in an HTML page have 100% height, you must not only set that element's height to 100%, but also the height of its parent… and its parent’s parent… and its parent’s parent… and so on. When you get to the body, it might feel a bit weird and your IDE may bark at you, but do it anyways: <body style=”height:100%”>
. Even more “nails-on-the-chalkboard” is the fact that you must set the height of the root HTML element! After that, the full-screened viewer is so close it’s not funny.
The WebImageViewer is built from an iFrame. To interact with the image and pan around in it, the iFrame has scroll bars. The bottom scroll bar we can deal with -- that’s good to have -- but every browser interprets page height a little differently and therefore you can’t guarantee that the viewer will not stretch beyond the length of the browser window, causing the window's scroll bar to appear. What we need to do is use a few techniques to force each browser to hide scroll bars entirely.
The Everything-but-IE Fix
To remove scroll bars from every browser except for IE, you simply set the overflow CSS property of the body to hidden
. Your CSS file would have something like this in it:
body {
...
overflow:hidden;
}
For the Other 85% of the People Out There…
To remove scroll bars from IE, you need to set the scroll attribute of the body to no
. If you do it in Visual Studio, it will issue you a warning, informing you that it is not valid… go figure. Your body tag should now look like this: <body style=”height:100%” scroll=”no”>
.
Making It Simple
Making this as minimalistic as possible was a goal. There are two sets of controls and neither of them appears unless you are moving your mouse; this lets you see the whole document without anything in your way. The zoom, close, and download controls are fairly straightforward, so I won’t go into them.
Thumbnails
The thumbnail viewer on the bottom uses a bit of JavaScript to be able to page through the frames. Here’s a look at what’s inside:
var ScrollInterval;
var xPos = 0;
var _thumbWidth;
function scrollright() {
clearInterval(ScrollInterval);
ScrollInterval = setInterval("moveright()", 25);
}
function moveright() {
if(xPos > -_thumbWidth * (WebThumbnailViewer1.getCount()-4)) {
xPos = xPos - 50;
WebThumbnailViewer1.setScrollPosition(new atalaPoint(xPos,0));
}
}
function scrollleft() {
clearInterval(ScrollInterval);
ScrollInterval = setInterval("moveleft()", 25);
}
function moveleft() {
if(xPos < 0) {
xPos = xPos + 50;
WebThumbnailViewer1.setScrollPosition(new atalaPoint(xPos,0));
}
}
function snap() {
clearInterval(ScrollInterval);
var numThumbs = Math.round(xPos / _thumbWidth);
xPos = numThumbs * _thumbWidth;
WebThumbnailViewer1.setScrollPosition(new atalaPoint(xPos,0));
}
<!-- the Scroll Right Button -->
<a href="#" onmousedown="scrollright();" onmouseout="snap();" onmouseup="snap();">
<img alt="Move Left Button" src="images/moveRight.png" height="140px" width="30px" />
</a>
The right-arrow button in the thumbnail viewer that controls scrolling to the right has an OnMouseDown
event handler called scrollRight
. By using setInterval
, we are able to decouple the speed that the thumbs whiz by at from the speed of the computer the script is running on. Every 25 milliseconds, the moveRight
method is called and the scroll position of the WebThumbnailViewer
is increased by 50 pixels until the mouse button is released and snap
is called. snap
makes sure that the viewer lands on a set of 4 thumbs always. We don’t want to see half of a thumbnail, two solid ones, and another half.
Hiding and Showing the Controls
When the application starts, the controls are displayed to give the user a hint that they exist. By simply moving the mouse, they will reappear and remain visible until the mouse stops moving for two seconds.
The WebImageViewer provides a MouseMove
event, but one that only fires when the mouse is moved over the image. What about the case when the image has been zoomed out far enough where the mouse can be in the viewer, but not over the image? If we just used that event, the controls would eventually disappear and we could never get them back. Instead, we need to use the onmousemove
event of the WebImageViewer’s parent iFrame.
var iframes =
document.getElementById("WebImageViewer1").getElementsByTagName("iframe");
iframe = iframes[0].contentWindow || iframes[0].contentDocument;
if(iframe.document)
iframe = iframe.document;
iframe.onmousemove = showAll;
First, we grab the document element with the ID of our WebImageViewer. In this case, it is WebImageViewer1
. Next, we need to get all of its child elements that are iFrames. Since there is only one (I know this because I dug through the DOM in FireBug), we can address it with index zero. After making sure we have the right DOM element depending on the browser we’re using, we can set the onmousemove
event to call the showAll
function when it fires.
var HideAllTimeout;
function showAll() {
clearTimeout(HideAllTimeout);
HideAllTimeout = setTimeout("hideAll();", "2000");
WebThumbnailViewer1.setVisibility("visible");
document.getElementById("thumbs").style.visibility = "visible";
document.getElementById("controls").style.visibility = "visible";
}
function hideAll() {
clearTimeout(HideAllTimeout);
WebThumbnailViewer1.setVisibility("hidden");
document.getElementById("thumbs").style.visibility = "hidden";
document.getElementById("controls").style.visibility = "hidden";
}
The showAll
function makes sure the hideAll
function won’t be called by clearing any delayed calls to it, prepares a new delayed call to hideAll
and then ensures that the controls and thumbnails are visible. When the mouse stops moving, the most recent setTimeout
of the hideAll
method would not have been cleared, and after a 2-second delay, the controls will disappear.
Caching
To make sure that any documents requested are loaded quickly, we need to cache it locally for future processing. First, the code-behind grabs the image at the requested URL and saves it to a folder on disk.
url = Server.HtmlDecode(url);
filename = MapPath(FileMapper.GetHashedName(url));
if (!File.Exists(filename))
{
WebClient client = new WebClient();
client.DownloadFile(url, filename);
}
This lets us not only load the subsequent pages of the document more quickly, but also makes the next load of this document blazingly fast. Since I don’t want to eat up more than 50MB of my disk, I remove the n least recently used documents until the cache is smaller than 50MB.
The Bookmarklet
To make this app as usable as possible, I wrote a simple Bookmarklet. Drag this ThinDocify link into your browser’s Bookmarks Bar (for Firefox and Safari) or right click it and add it as a Favorite (for IE). To use it, navigate to your favorite page that contains links to PDF documents, click the ThinDocify Bookmarklet in your Bookmarks Bar, and click the links to the PDFs on that page.
A Bookmarklet is simply a piece of JavaScript that is crammed into a link. When you add that link to your Bookmarks bar and run it, it is run in the context of the page that is currently in the browser. In the code below, we loop through every link on the page and check to see if the URL ends in PDF or TIF. I also do a quick don’t-break-Google check and make sure that none of the URLs contain question marks. Granted, this does limit its functionality on sites that serve PDFs from scripts that take in their names in the query string, but hey, at least it doesn’t break the internet. For each of those URLs, we replace it with a call to the JavaScript function window.open
and set the location to http://thindoc.atalasoft.com/url=http://thedesiredurl.com/file.pdf.
<script type="text/javascript">
<!--
(function() {
var i;
for(i = 0; i < document.links.length; i++) {
var url = document.links[i].href;
if((url.substring(url.length-3, url.length) == 'pdf' ||
url.substring(url.length-3, url.length) == 'tif') &&
url.indexOf('?') == -1) {
document.links[i].href =
'javascript:window.open(\'http: escape(url) +
'\',\'_thindoc\');';document.links[i].target = '_thindoc';
}
}
}
)();
-->
</script>
You can also add this script to the bottom of your own web pages and it will automatically convert all PDF links to open in ThinDoc.
For more information about ThinDoc, please visit its About page.
Sample Documents
Click on the following documents to see ThinDoc in action:
Link to a specific page within a document:
Download the Source
You can get the source for ThinDoc here.