TweetFollow Us on Twitter

Dynamic PDF

Volume Number: 16 (2000)
Issue Number: 3
Column Tag: Programming Techniques

Dynamic PDF Made Easy

by Kas Thomas

It's simpler than you think to generate custom PDF documents in real time using Perl and CGI

Adobe's Portable Document Format (PDF) has won universal acclaim as document interchange format, because of its ability to deliver graphically rich, structured content consistently across multiple operating environments. Because of its strong support for vector graphics, font embedding, hypertext links, and other advanced features, PDF is a powerful, far-reaching document standard. But that also makes it a relatively complex standard. (For details, see the September 1999 MacTech.) PDF's complexity, in turn, has discouraged many web developers from attempting to write PDF files dynamically, at the server. Automatically generated HTML pages are common; but when was the last time you saw an auto-generated PDF web page?

It turns out that with a little foreknowledge of PDF's internal workings, and a good grasp of CGI fundamentals, it's not that hard to put together server scripts that will generate PDF on demand. There's no question that dynamic PDF can be a challenge to do right. But if your dynamic document needs are modest (for example, if you'd simply like to be able to generate a custom "Thank You" page at run time) and your Perl skills are passable (i.e., you've graduated from writing "Hello World" to generating the much more impressive string "Internal Server Error"), you can automate the serving of custom PDF pages with surprisingly little effort. In fact, a Perl script comprising no more than five or six dozen lines of code will get you started.

Our Strategy

Our strategy will be simple: We want to be able to collect an arbitrary glob of "user text" from a web form, and convert that text to a PDF page served back to the user via HTTP, suitable for viewing in a browser equipped with the free Acrobat Viewer plug-in. For simplicity's sake, we'll start by omitting any attempts at vector graphics, colored text, rotated text, styled text in various point sizes, etc. (But I hasten to add that before this article is finished, we're going to be doing all of those things and more.) For starters, we just want to serve the user's words back to him, at any convenient point size, in black and white, on a letter-sized PDF page. For sake of convenience, we're also going to generate the "data collection" form and our PDF reply from one and the same Perl script. That way, we don't have to keep track of multiple documents: an HTML form, Perl scripts, CSS stylesheets, etc. If you want to add that complexity to the picture yourself later, so be it. Here, we're going to keep things stone-simple.

I might point out that "live" copies of the dynamic-PDF scripts developed below can be found at http://www.acroforms.com/dynamicPDF.html (which is a portal page with links to several working scripts).

I also want to stress that when we're done, the PDF pages we will have generated can be saved to disk using your browser's Save As feature. (Reader won't save the docs, but your browser will.) Thus, we will have achieved the Holy Grail of the PDF world: writing PDF without using Distiller or Acrobat.

Once we've gotten our basic approach nailed down, we'll branch out into fancy features like colored backgrounds, stroked/filled paths, rotated text, styled text, etc. But first, a detour into the land of PDF internal structure.

PDF at the Subatomic Level

Internally, a PDF file consists of plain-ASCII object descriptions, following a notably Postscript-like (i.e., postfix-notation) syntax. All PDF files have a header (containing version information), a cross-reference (or 'xref') table containing offsets to the various objects that make up the file, and a trailer containing references to the root object as well as the byte offset from the start of the file to the beginning of the 'xref' table. (See my story in see the September 1999 MacTech for more information.) The formal PDF specification, available online at http://partners.adobe.com/asn/developer/PDFS/TN/PDFSPEC.PDF, describes the relationships between PDF objects (and the meanings of various dictionary "key" entries) in comprehensive detail, if you're interested. But we're going to skip all that, because most of it is not necessary for what we're doing.

It turns out you can bend (or even break) a lot of the "rules" spelled out in the PDF spec, without making Acrobat Reader unhappy, if you know a thing or two about PDF internals. An instructive exercise in this regard is to make Adobe's Distiller program generate a small "Hello World" type of file, then examine the file in a text editor-and try to find superfluous data in it that can be removed. If you try this, you will quickly discover that /Info objects, for example, contain meta-data about the PDF file (such as the doc's author, subject, producer, keywords, etc.) that can easily be dispensed with. Likewise, Adobe tends to insert long /ID objects (containing a kind of "digital fingerprint" for identification of the file) in PDF files; these aren't strictly needed.

If you keep removing superfluous objects and data items from a PDF file, you may be amazed at how much "data" you can actually get rid of without making Reader complain. A careful re-reading of the PDF Specification usually reveals that items you thought were mandatory are actually optional. Even the all-important 'xref' table isn't strictly needed, since Acrobat Reader can (and will, and does) generate the necessary byte offsets itself, at runtime, if the table is defective or missing.

In late 1999, I sponsored a contest at <www.acroforms.com> in which the goal was to produce the smallest possible PDF file that wrote "Hello World" to the screen without making Acrobat Reader generate any error dialogs. Amazingly, the winner of that contest submitted a file that was only a little more than 200 bytes long:

%PDF-1.
1 0 obj<</Pages 2 0 R>>
2 0 obj<</Kids[<</Parent 2 0
R/Resources<</Font<</R
<</Subtype/Type1/BaseFont/Arial>>
>>>>/Contents<<>>stream
BT/R 14
Tf(Hello World!)Tj
endstream
>>]>>
trailer<</Root 1 0 R>>

Note that the second object (starting with "2 0 obj...") is extra-long, with many nested "dictionary" entries, ending only just before the word "trailer" at the bottom of the file. This particular PDF file uses 14-point Arial as the typeface. No font encodings are embedded in the file, however, because Arial is one of the base-14 fonts that Acrobat Reader ships with. (Arial replaced Helvetica in the version-4.0 release of Reader.) Every machine that has Acrobat Reader is guaranteed to have Arial.

There are many things technically "wrong" with this short PDF file, including the fact that it has no 'xref' table and contains a text stream that is opened with 'BT' (for Begin Text) but is never closed with 'ET' (End Text). In many ways, it's a wonder Reader can even parse and display this file. Yet it does.

There is one small technical difficulty with this file that impacts viewing: Namely, no positioning information is given for the text - which means (since Reader "zeroes out" the current transformation matrix, or CTM, before displaying any block of text) that the starting "pen position" for displaying "Hello World!" in this instance is the origin of user space: i.e., the lower left corner of the page. You have to scroll down to the very bottom of the page to see the single line of text. To fix this requires that we add a transformation matrix of our own to the text stream:

%PDF-1.
1 0 obj<</Pages 2 0 R>>
2 0 obj<</Kids[<</Parent 2 0
R/Resources<</Font<</R
<</Subtype/Type1/BaseFont/Arial>>
>>>>/Contents<<>>stream
BT/R 14
1 0 0 1 72 720 Tm
Tf(Hello World!)Tj
endstream
>>]>>
trailer<</Root 1 0 R>>

Can you spot the change? We've added a line that ends with 'Tm' (the transform-matrix operator). The six-number matrix will look familiar to you if you've studied Postscript. The first four numbers are scaling and skewing factors (signifying no change in those characteristics, in this case); the final two numbers are translation values in 'x' and 'y', respectively. Since the default user space in PDF has 72 units to the inch, using values of 72 and 720 result in the starting position for our text results in our text being drawn one inch from the left edge of the page and ten inches up from the bottom. (In PDF, 'y' units get bigger as you go up.) Now our 14-point text will be drawn where the user can see it.

In the Perl script that follows, we're going to use a variation of the foregoing PDF file as the basis (the "template," if you will) for our dynamically generated PDF page. While some experts may cringe at the thought of using a "broken" PDF file as the starting point for this kind of exercise, the fact is that for illustrative purposes, a "subatomic" template file of this kind is hard to beat. Besides, the proof of the pudding is that Acrobat Reader doesn't in any way "choke" on the end result. What the user sees in his (or her) browser is a fully functional PDF file: one that can be saved to disk (using the browser's Save As feature) for later use.

The Common Gateway

Collecting information from users is a common task in the world of the Web, calling for the use of interactive forms. Sometimes, the form in question is a static HTML file stored on a server; but increasingly, HTML forms themselves are dynamically generated at the server in response to user requests. That is, the form itself doesn't exist until a CGI script, often written in Perl, generates it.

CGI (the Common Gateway Interface) is nothing more than a set of conventions for pushing and pulling information across an HTTP connection. Forms, and the scripts that retrieve information from web forms, use the CGI protocol to get the job done. Learning to write CGI scripts in Perl is an art unto itself, but the job is made tremendously easier if you take advantage of some of the excellent free Perl libraries available for implementing CGI processes.

Arguably the best Perl library for this task is Lincoln Stein's powerful CGI.pm package. This set of routines (which takes the pain out of writing HTML at the server and dealing with web forms) is so useful that it now ships with Perl as part of the "full install" of the language. What this means is that any Unix server that has Perl 5.003, patchlevel 7 or higher (as almost all do) automatically has CGI.pm, waiting to be called from any Perl script.

When you use CGI.pm, you don't have to worry about whether incoming form data is being supplied via GET or POST, where it's being stored on the server, or any of the nasty parsing details involved in extracting urlencoded data from HTTP streams. To get the value of a form field called "UserName" from a Perl script at runtime using CGI.pm, all you have to do is:

$name = param('UserName');

Here, $name refers to the local scalar variable into which we wish to store the value from "UserName." If the user has typed "John Smith" into the relevant form field before Submitting the form data to the server, then $name will contain "John Smith" after this line of code executes. The param() function in CGI.pm takes care of finding the incoming data, decoding it if it's urlencoded, etc.

Dynamic Forms Using CGI.pm

One task CGI.pm excels at is generating HTML code dynamically. For example, to create the HTML form shown in Figure 1, all we have to do is execute the following few lines of Perl:

  print header,
 		start_html(-title=>'PDF Bounceback, by Kas Thomas',
                   -bgcolor=>'#FFFFFF'),		
 		p(h2('PDF Text Entry'));

	print start_form,
		textarea(-name=>'UsersText',
		         -rows=>24,
		         -columns=>60),p,
        submit(-name=>'action',-value=>'Generate PDF');
    
  end_form;


Figure 1.A form created dynamically using CGI.pm.

The function h2('content here') applies an HTML level-2 headline tag around the text "content here"; likewise, p() applies paragraph tags around a block of text, and so forth. Nesting the function calls causes the relevant HTML tags to nest correctly. A common idiom in Perl for getting multiple strings to print to an output stream is to separate the strings with commas and put them between print at the start and a semicolon at the end. Functions like header(), start_form(), and end_form() are part of the CGI.pm package. (In Perl, parentheses after a function name do not constitute a "function call operator." They are therefore optional, if no arguments are given.)

If you write a Perl script containing the foregoing lines of code, name the script "Form.pl", and place it in the "cgi-bin" directory of your web server, then whenever anybody goes to the script's URL, the script will launch automatically and write the form shown in Figure 1 to the caller's browser. This is how many (if not most) dynamic forms work.

A Dynamic PDF Script

At this point, believe it or not, we're in a position to put together a full, working Perl script for producing an HTML form dynamically, retrieving the contents of that (filled-out) form, and auto-generating a PDF reply back to the user's browser. Listing 1 shows such a script, complete.

Listing 1: dPDF.pl


dPDF.pl

A complete Perl script for generating an HTML form, retrieving the

form's contents, and writing the contents back out as a PDF file.


#!/usr/bin/perl
# ------------------------------
# Script: dPDF.pl (simple script to output PDF dynamically)
# © 2000 by Kas Thomas. Updates at www.acroforms.com.
# ------------------------------
use CGI qw/:standard/;

# Check params, and if null, show form...
DrawForm() unless param();

$allText = param('UsersText');
 
print header("application/pdf");   # output PDF header

Send_PDF_Leader();

foreach $line (split /\n/,$allText) {
    
	 print '(' . $line . ')Tj';
     print "\n";   
	 print 'T*';				 # goto start of next line		    
     print "\n";   
	  
}

Send_PDF_Trailer();

# - - - - - - - - - - DrawForm() - - - - - - - - - - -
sub DrawForm {

	print header,
 		start_html(-title=>'PDF Bounceback, by Kas Thomas',
                   -bgcolor=>'#FFFFFF'),		
 		p(h2(font({-color=>'red'},'PDF Text Entry')));

	print start_form,
		textarea(-name=>'UsersText',
		         -rows=>24,
		         -columns=>60,
		         -wrap=>virtual),p,
        submit(-name=>'action',-value=>'Generate PDF');
    
    end_form;
    
    exit;
}

# - - - - - - - - - - Send_PDF_Leader() - - - - - - - -
sub Send_PDF_Leader {

print <<PDF_LEADER;
%PDF-1.
1 0 obj<</Pages 2 0 R>>
2 0 obj<</Kids[<</Parent 2 0 R/Resources<</Font<</R
<</Subtype/Type1/BaseFont/Arial>>
>>>>/Contents<<>>stream
BT/R 14
1 0 0 1 72 700 Tm
15 TL
Tf()Tj
PDF_LEADER
}

# - - - - - - - - - - Send_PDF_Trailer() - - - - - - - -
sub Send_PDF_Trailer {

print <<PDF_TRAILER;
endstream
>>]>>
trailer<</Root 1 0 R>>
PDF_TRAILER
}

After a use directive to make sure Perl knows we need to use the CGI.pm package, we check to see whether the CGI.pm function param() returns nothing, in which case it means the script is being called for the first time. If the script is being called for the first time, it means we need to generate the HTML form of Figure 1 - which is accomplished by the subroutine DrawForm(). Otherwise, if param() is not nil, we're responding to a hit on the "Generate PDF" button (which is the form's equivalent of a Submit button).

If we're responding to a hit on the Generate PDF button, the first order of business is to retrieve the user-entered text, which we do with:

$allText = param('UsersText');

The textarea field of the HTML form is named "UsersText"; hence, we want to retrieve whatever the user typed there. The foregoing line of code captures the user's text into a single big string (a scalar variable, in Perl parlance) called $allText. We'll have occasion to parse that big string into smaller constituents (representing individual lines of text) in a moment.

Before we can write PDF to the user's browser, we have to open an HTTP connection and send an HTTP header with the appropriate MIME type for PDF (which is "application/pdf"). This is the browser's way of knowing what kind of file is about to arrive. The browser can take appropriate action in terms of launching helper apps, handing off control to plug-ins, or handling the transaction itself. In this instance, if the user has the Acrobat browser plug-in installed, the browser will hand control to the plug-in. It's also possible that the user has set the browser up such that incoming PDF files are redirected to the standalone Acrobat Reader app. If the MIME type "application/pdf" has not been registered with the browser, the browser will react to the incoming header by asking the user what to do with the about-to-arrive unknown file type. The user can then cancel out of the transaction, store the incoming file to disk, etc.

Opening the HTTP connection and sending the proper header is done with:

print header("application/pdf");

After that, our script goes to a subroutine called Send_PDF_Leader(). This subroutine simply writes 200 bytes or so of raw PDF to the HTTP connection. Most of that code should look familiar to you from our discussion of minimal PDF files earlier.

Writing the User's Text

Next comes our script's "main loop," you might say, in which we parse the user's text into individual lines and write them out as PDF text-stream data. We use Perl's split() function to divide the $allText string into substrings, splitting at every newline. Then we write a line of user text, in parentheses, followed by the 'Tj' operator (PDF's "writeline" operator), and after every line of text so processed, we write a 'T*', which (in PDF) means to drop down to the start of the next line (using the current leading) and prepare the virtual pen to write more text.

After writing every line of user text, we call Send_PDF_Trailer() to write out the last few bytes of our PDF template. At that point, we're finished.

An example of a dynamically generated PDF page using this script is shown in Figure 2.


Figure 2.A dynamically generated PDF file.

The Great Escape

At this point, we have a script that obediently writes our user's text input as 14-point Arial, in black ink on a white PDF page. Not very interesting-looking. It would be nice if we could jazz things up a bit by drawing our text in color, perhaps in various additional sizes. It would also be nice if we could specify something other than a pure white background. But before we proceed to increase the spiff factor of our PDF, we need to attend to one small detail.

PDF represents strings internally as ASCII character sequences inside parentheses. Thus, whenever a string contains parentheses as part of the text, they need to be "escaped" with a preceding backslash. Otherwise, the PDF interpreter, upon encountering a right-parenthesis inside a block of text, may think the text string has been terminated! The right-paren will be treated as an end-of-string marker. This could lead to strange errors if our user is in the habit (as many of us are) of loading up text with parenthetical remarks.

Fortunately, it's not hard to trap parens and "escape them" automatically for the user. Of course, we also have to trap and escape backslashes in the user's text, because (again) PDF treats all backslashes as "special" in a text stream. To deal with parens and backslashes, we declare an associative array in Perl as follows:

%PDF_ESCAPES = ('\\' => '\\\\', 
                ')'  => '\)', 
                '('  => '\(');

The way this special kind of array (sometimes called a hash) works is that when we want to look up the "replacement" for, say, a bare right-paren, we just need to do:

$PDF_ESCAPES { ')' };

Note the dollar sign preceding the hash name (and the curly braces enclosing the array index). In Perl, the dollar sign means we are coercing the array lookup into a scalar context. When the above statement evaluates, it will evaluate to the lookup value '\)' (i.e., the string representing our properly escaped paren).

Of course, Perl (like PDF) also treats backslashes as special, which means that a string representing a literal backslash needs to be escaped - with a backslash! That is, the string '\\' prints as one backslash (not two) in Perl. Likewise, '\\\\' is treated as two literal backslashes in a row, not four. That's why the first lookup item in our hash, namely '\\\\', is associated with '\\'. (Confused yet?)

Now. We need to check every line of user input before writing it as raw PDF, so that any parens or backslashes contained in the user's text get converted to their properly escaped equivalents, preventing Acrobat Reader from choking. We can accomplish this essential check with the following line of code, inserted in our "main loop":

$line =~ s/([\\()])/$PDF_ESCAPES{$1}/ge;

"Wow," you're probably muttering, "what a weird-looking line of code!" And indeed it is. A Perl fan will recognize it immediately as a powerful regex-based substitution (regex being short for "regular expression"). In Perl, a statement like

$myString =~ s/this/that/g;

means to scan the contents of $myString, looking for the substring 'this', and upon finding a match, substitute (per the leading lowercase 's') the string 'that'; and oh, by the way, do this repeatedly (globally) throughout the entire contents of $myString. (That's what the trailing lowercase 'g' means.)

In our somewhat complicated substitution operation, we're scanning for any match of the regular expression [\\()], which is a character class with three members: the backslash character (which we have to escape with a backslash, for Perl's benefit; hence '\\'), the left-paren, and the right-paren. Square brackets mean that these characters form a class of allowable "match" characters. Any match of any one of these characters (in any order) inside the target string will trigger a "hit." Enclosing the entire character class within parentheses, as in ([\\()]), means we want Perl to store a temporary copy of any match that occurs, in a variable called $1. (If you're not familiar with this idiom, don't spend too much time worrying about it right now. You can always read up on regexes later.) Our replacement value, which Perl consults whenever a "hit" happens, in this case is $PDF_ESCAPES{$1} - that is to say, we use the match value ($1) as an index into our associative array, in order to retrieve the appropriate replacement string. This trick will only work, however, if we remember to put a trailing lowercase 'e' on the end of the expression, which means evaluate the replacement string as a Perl expression. Otherwise, Perl will literally substitute whatever's in the replacement position, unevaluated. That's not what we want.

Upping the Spiff Factor

As I started to say before, it would be nice if we could "spiff up" our PDF output a bit, augmenting it with colored text, styled text, or perhaps some vector graphics or different background colors, etc. - anything except plain old black text on a white background.

Adding extra spiff turns out to be astonishingly easy. All we need to do is let the user "drop down" into low-level PDF whenever he or she wants to. Follow me for a minute. Suppose we invent a special "escape command" of our own, signified by (let's say) the ASCII string <@>. We'll call this the "escape-to-PDF" operator. To get back to normal text-writing mode, we'll require the use of </@>, which we'll call the "escape-to-text" operator. Anything bracketed by <@> and </@> gets treated as raw PDF. While your user is in "raw PDF" mode, he can make use of any of the Postscript-like page operators and vector-drawing commands recognized by the PDF Specification: m for moveto, l for lineto, rg for setting the RGB color, Tm for set matrix, B for fill and stroke path, etc. (See the URL given earlier for the formal PDF Specification, at Adobe's web site.)

Using the same regex-based substitution trick that we used before, we can add one more lookup table (or associative array) to our script, and add one more regex-substitution line to our main loop. Only this time, the hash will return a value of ')Tj ' for any hit on <@>, and a value of ' (' for any hit on </@>. That way, whenever the user types <@> as part of his text stream, our script outputs a PDF end-of-string marker, followed by the PDF "write line" operator (Tj), spoofing Acrobat Reader into letting the user write raw page operators, if he wants to. When the user types </@>, the script outputs a "begin string" marker, and Reader is spoofed into going back into text-stream mode.

What Can You Do With It?

Now we've got quite a bit of power in our hands. If you know a little bit of PDF, you can create almost any kind of display magic you want with your dynamically generated PDF document. Let's try the following code entered into our form's main text area:

<@>0.85 0.85 0.85 RG</@>
<@>1 J 110 w 250 600 m 250 600 l B</@>
<@>.6 .6 .6 RG</@>
<@>18 w 72 72 m 72 720 l 540 720 l 540 72 l s</@>
<@>0 1 -1 0 144 520 Tm</@>

Let's try some rotated text.

The end result is shown in Figure 3. We've created a PDF document containing a large grey box (going around the edges of the page), a filed grey circle, and the words "Let's try some rotated text" running vertically up the page.


Figure 3.Custom-generated PDF containing rotated text and stroked vector graphics.

To understand what's going on, let's look at the above form input, one line at a time. The first five lines are encapsulated by <@> and </@> , which means we're writing raw PDF. The first line of raw PDF contains 0.85 0.85 0.85 RG, which sets the stroke color (in our default RGB color space) to 15% grey. In PDF, RGB channel values run from zero to 1.0, with 1.0 being brightest. Therefore, setting each channel to 0.85 makes for 15% grey (or 85% white).

The second line is more complicated. Basically, it says to set the default line end-cap style to rounded end-caps (that's what '1 J' means); set the default line width to 110 pixels (thus '110 w'); move the pen to page coordinates of 250,600 ('m' means "moveto"); draw a line to 250,600; and finally, stroke the path ('B'). In plain English: "Draw a zero-length line, 110 pixels wide, at 250,600 with rounded end caps." Since the line has zero length, only the end caps are drawn. In this way, we spoof Acrobat into drawing a perfect circle, which looks filled (although really it's only stroked). This is an old Postscript hack for drawing filled circles. The reason we use it here is that PDF doesn't have a "draw circle" operator, just Bézier-curve operators. (I don't know about you, but I find the idea of drawing a circle using linked Bézier segments too painful to think about.)

Next, in the third line of raw PDF, we set the stroke color to 40% grey (60% white) with .6 .6 .6 RG.

In line four, we change the default stroke width to 18 pixels ('18 w'); move the pen to 72,72; draw a line to 72,720; another line to 540,720; a line to 540,72; and then we use the 's' command, which in PDF means "close this path and stroke it." This is how we produced a thick, dark-grey line going around the margins of the page.

Finally, in the fifth line we perform some matrix magic. Transform matrices, in PDF (as in Postscript) consist of six numbers, the first four of which specify scaling and rotation. To rotate the coordinate system, just make sure the first four numbers represent (consecutively) the cosine, sine, negated sine, and cosine of the desired angle. In our example, we used 90 degrees as the angle (hence the matrix values 0, 1, -1, and 0). The final two numbers in the transform matrix represent translation values in x and y. That is, we move the origin of space to whatever these numbers are (in this case, 144,520). Recall that the origin starts life at the lower left corner of the page.

Now when our text is drawn, it begins at 144,520 but runs at 90 degrees up the page. Since text is (by default) filled but not stroked, it gets drawn in black (the default fill color, which we have not changed).

Simple as pi.

Conclusion

We've covered an incredible amount of ground in a relatively small space. Not only have we shown how to serve dynamic PDF pages from a compact (80 lines or so) Perl script, but we've integrated a low-level "raw PDF" editor into the data-input form such that if the user wants to, he can produce sophisticated graphics effects inside the generated PDF page. (A "live" copy of this script, and others similar to it, can be found online at http://www.acroforms.com/dynamicPDF.html.)

Of course, many useful additions could still be made to our little script. A useful exercise would be to add such features as:

  • Additional fonts, and a facility for the user to change font sizes on the fly.
  • Automatic line-wrapping, with or without justification.
  • Custom margin widths.
  • Custom background colors. (This is just a matter of drawing a filled, colored rectangle large enough to cover the entire page.)
  • Stylesheets.

You can probably think of lots of other features that would be nice to have. The good news is, now you're in a position to implement them, because (as you now know) dynamic PDF really isn't that hard.


Kas Thomas (kt@svgcentral.com) consults for Adobe Systems and writes a regular column about PDF-related programming (covering JavaScript, CGI, dynamic PDF, and related issues) for www.acroforms.com. He has been programming in C and assembly on the Mac since MC68000 days and remembers when the MacOS ran in black-and-white.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

FileMaker Pro 19.4.2 - Quickly build cus...
FileMaker Pro is the tool you use to create a custom app. You also use FileMaker Pro to access your app on a computer. Start by importing data from a spreadsheet or using a built-in Starter app to... Read more
Adobe Illustrator 26.0.3 - Professional...
You can download Adobe Illustrator for Mac as a part of Creative Cloud for only $20.99/month. Adobe Illustrator for Mac is the vector graphics classics in the design industry. It is a digital... Read more
WhatRoute 2.4.9 - Geographically trace o...
WhatRoute is designed to find the names of all the routers an IP packet passes through on its way from your Mac to a destination host. It also measures the round-trip time from your Mac to the router... Read more
Notion 2.0.20 - A unified workspace for...
Notion is the unified workspace for modern teams. Notion Features: Integration with Slack Documents Wikis Tasks Release notes were unavailable when this listing was updated. Download Now]]> Read more
Monterey Cache Cleaner 17.0.2 - Clear ca...
Monterey Cache Cleaner is an award-winning general-purpose tool for macOS X. MCC makes system maintenance simple with an easy point-and-click interface to many macOS X functions. Novice and expert... Read more
Firetask Pro 4.6.8 - Innovative task man...
Firetask Pro represents the next generation of easy-to-use, project-oriented task management apps. By combining David Allen's powerful Getting Things Done (GTD®) approach with classical task... Read more
Smultron 13.0.4 - Easy-to-use, powerful...
Smultron 13 is the text editor for all of us. Smultron is powerful and confident without being complicated. Its elegance and simplicity helps everyone being creative and to write and edit all sorts... Read more
Box Sync 4.0.8057 - Online synchronizati...
Box Sync gives you a hard-drive in the Cloud for online storage. Note: You must first sign up to use Box. What if the files you need are on your laptop -- but you're on the road with your iPhone? No... Read more
Audio Hijack 3.8.10 - Record and enhance...
Audio Hijack (was Audio Hijack Pro) drastically changes the way you use audio on your computer, giving you the freedom to listen to audio when you want and how you want. Record and enhance any audio... Read more
Direct Mail 6.0.1 - Create and send grea...
Direct Mail is an easy-to-use, fully-featured email marketing app purpose-built for macOS. Create, send, and track great looking email campaigns that get results. Start your newsletter by selecting... Read more

Latest Forum Discussions

See All

Hopefully Not Jared’s Last Show – The To...
My suspicions from last week were correct, and after my two kids tested positive for Covid last week both my wife and I have now tested positive as well. It seems you just can’t escape this stuff lately. Thankfully the two little ones are pretty... | Read more »
TouchArcade Game of the Week: ‘Micro RPG...
I feel like idle games are one of those perfect fits for the mobile platform. Not that they replace more involved gaming experiences when you’re in the mood for that, but they do fit in alongside other types of games just fine as a “go to" when you... | Read more »
‘Phantom Blade: Executioners’ Closed Bet...
Phantom Blade: Executioners is holding a small-scale technical test that lets players get first dibs on the KungFuPunk action RPG. Offered to selected players only, S-Game’s first Closed Beta Test will provide players with limited edition in-game... | Read more »
New ‘Warhammer 40,000: Tacticus’ Video S...
Back in September Snowprint Studios, who you may know from their previous Legend of Solgard or Rivengard, announced that they’d partnered up with Games Workshop to put out a new tactical game in the Warhammer 40,000 universe titled Warhammer 40,000... | Read more »
SwitchArcade Round-Up: ‘Pokemon Legends:...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for January 28th, 2022. We’ve got a bunch of new releases to look at today, with a few big hitters, a few mid-level diversions, and a healthy supply of compost. Since it’s Friday, we... | Read more »
Phantom Blade: Executioners, S-Game...
S-Game has kicked off its first Closed Beta Test for Phantom Blade: Executioners, inviting a selected few to get first dibs on the upcoming KungFuPunk action RPG on mobile. The CBT officially begins this January 28th, and beta testers will receive... | Read more »
‘Infinite Galaxy’ First Anniversary: Cel...
Cultivating a new generation of valiant commanders across 240 countries worldwide, Infinite Galaxy has quenched players’ thirst to explore the vastness of space – and there are only more intergalactic adventures to embark on from here on out. Camel... | Read more »
War and Order: How to brave the cold in...
War and Order's 6th-anniversary celebrations are underway, and all in good time too - this season not only brings about fabulous festivities, but it also lets players experience the harsh winter in an entirely new way. [Read more] | Read more »
‘Hidden Folks+’ Is This Week’s New Apple...
The original Hidden Folks from Adriaan de Jongh is an excellent hidden objects game featuring hand drawn visuals. It is an absolute joy to play, and it has now released on Apple Arcade in the form of Hidden Folks+ () as an App Store great. If you’... | Read more »
Mini Metro’s First Big Update of 2022 Ad...
Last year saw great updates for Dinosaur Polo Club’s Mini Metro ($3.99) which is also available on Apple Arcade as an App Store Great. | Read more »

Price Scanner via MacPrices.net

Apple has clearance 2020 13″ MacBook Airs ava...
Apple has clearance, Certified Refurbished, 2020 13″ Intel-based MacBook Airs in stock today starting at only $719 and up to $370 off original MSRP. Each MacBook features a new outer case, comes with... Read more
The cheapest iPhones for sale today at Apple...
Apple has restocked Apple Certified Refurbished iPhone 8 models starting at only $359. Each refurbished iPhone comes with a fresh external case, standard Apple 1-year warranty, and free shipping.... Read more
14″ MacBook Pro with Apple M1 Max CPU now in...
Looking for a new 14″ MacBook Pro with an Apple M1 Max CPU? Stock is finally trickling into Apple resellers. B&H has Silver 14″ M1 Max MacBook Pros in stock today for $2899 including free 1-2 day... Read more
14″ MacBook Pros with Apple M1 Pro CPUs are i...
Amazon is reporting stock of 14″ MacBook Pros with M1 Pro CPUs today with a $50 discount. Shipping is free, and delivery is available by February 1st for most configurations. Be sure to make your... Read more
Apple has restocked 13″ M1 MacBook Pros for $...
Apple has restocked a full line of 13″ M1 MacBook Pros available Certified Refurbished, starting at only $1099 and up to $230 off original MSRP. These are the cheapest M1 MacBook Pros for sale today... Read more
Apple’s AirPods Max headphones are on sale fo...
Amazon has Silver, Blue, and Space Gray Apple AirPods Max headphones on sale today for $100 off MSRP. Shipping is free, and all models are in stock today. Their price is the lowest currently... Read more
Open a new line of service at Verizon and get...
Verizon is giving away 64GB Apple iPhone 12 minis or your choice of an iPhone 11 to customers who choose one of these phones and open a new line of service. Offer is available online only, and no... Read more
Open-box 13″ M1 MacBook Airs now available st...
QuickShip Electronics has open-box return 13″ M1 MacBook Airs in stock and on sale for $200-$400 off MSRP on their eBay store right now with free express delivery. According to QuickShip, “The item... Read more
Verizon’s 2022 iPad promo: $100-$310 off any...
Verizon has cellular-capable iPads on sale for $100-$310 off MSRP when purchased with an Unlimited service plan. Sale price is applied to your account monthly over a 24 or 30 month period, depending... Read more
Sunday Sale: Apple AirPods are on sale for up...
Amazon has Apple AirPods on sale for $10-$100 off MSRP today, depending on the model. All are in stock today with free delivery: – AirPods Max headphones (Blue): $449 $100 off MSRP – AirPods Max... Read more

Jobs Board

Registered Nurse (RN) Employee Health PSJH -...
…is calling for a Registered Nurse (RN) Employee Health PSJH to our location in Apple Valley, CA.** We are seeking a Registered Nurse (RN) Employee Health PSJH to be Read more
Systems Administrator - Pearson (United State...
…and troubleshoot Windows operating systems (workstation and server), laptop computers, Apple iPads, Chromebooks and printers** + **Administer and troubleshoot all Read more
IT Assistant Level 1- IT Desktop Support Anal...
…providing tier-1 or better IT help desk support in a large Windows and Apple environment * Experience using IT Service Desk Management Software * Knowledge of IT Read more
Human Resources Business Partner PSJH - Provi...
…**is calling a** **Human Resources Business Partner, PSJH** **to our location in Apple Valley, CA.** **Applicants that meet qualifications will receive a text with Read more
Manager Community Health Investment Programs...
…is calling a Manager Community Health Investment Programs PSJH to our location in Apple Valley, CA.** **Qualified candidates will be invited to do a self-paced video Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.