One of the articles I most looked forward to posting here is an article I wrote about getting PDF output from VFP, originally published in FoxTalk. I’ve re-formatted it and posted it here now. I get requests for it all the time.
The article was originally written because PDF output was one of those things we couldn’t put “in the box”. It is something I’ve been doing with various versions of FoxPro for years. I knew it would be possible to do it with ReportListeners even though we hadn’t provided explicitly for it, and wanted to show how I’ve always done this. I wanted to take the opportunity to discuss and expose a whole bunch of new-in-9 reporting concepts, such as some new printing-related behavior and the _ReportListener FFC class’s report collection. That’s how PDFListener was “born”.
But, if I’ve been doing it for years, I’ve obviously not been doing it with ReportListeners for years! In the version posted here, I’ve included some code tested back through VFP 5 in the source, that will give you just as good results but doesn’t use a ReportListener class.
PDFClass has a report collection just like PDFListener’s, and behaves almost exactly the same way. (Now I wonder where _ReportListener’s API came from?!)
One of the things that folks are often confused about with _ReportListener and its ability to run a collection of reports is that the reports it handles don’t necessarily have to run with object references. You can tell it to give each REPORT FORM a reference to itself, you can specify a listener reference separately for each member report in the collection if you like… or you can have _ReportListener run old-style REPORT FORM commands with no object reference at all. This is a particularly good idea with PDF output, for reasons explained in the article, so PDFListener adds some behavior to ensure that old-style REPORT FORM behavior is in fact its default, while still allowing you to specify object references if you want.
Ludek Coufal wrote to me about this, because he didn’t realize why PDFListener didn’t have the same page numbering results as other descendents of _ReportListener. The simple answer is: if you’re not using reportlistener references, you can’t use reportlistener.PageTotal.
But the real answer is a bit more complicated (as is always the case in Reporting, and often the case in Life). A collection of reports can’t necessarily rely on a cumulative .PageTotal even if all REPORT FORM runs are object-assisted, because one has to allow for the possibilitiy that different reports are running to different output targets or using different reportlistener references.
As I told Ludek:
Note that _ReportListener’s ability to figure out cumulative page totals was designed to be overridden or extended y other users. I stubbed out a ReportPages collection to help people get that idea. However I made it PRIVATE because I was not sure how it would be best used by the many different Fox possibilities — look at the comment in the code:IF NOT ("NOWA " $ STRTRAN(" " + m.lcClauses,"IT," ") OR ; "NOWAI " $ " " + m.lcClauses) THIS.ReportPages[m.liIndex] = THIS.SharedPageTotal * TBD: make this a two column array with * output pages (responsive to RANGE clause) * represented as well? ENDIF
To make this work properly in cases like PDFListener, where old-style REPORT FORM commands are more likely to be run during RunReports, a lot more work would have to be done by a subclass. I would have overridden, rather than augmenting, RunReports in PDFListener to do this, if I had thought it worthwhile. I would have had to pay attention to the value of “NORESET”, and I would have adjusted the ReportPages collection differently depending on its value, of course. There are many, many possibilities here and it is impossible to do it totally generically.
I would also have looked at each REPORT FORM command to see whether it was object-assisted or not, if it were I would have checked to see whether a .SharedPageTotal property was available (this is an FFC property, not base) before going to .PageTotal, if not I would have checked to see if _PAGETOTAL was getting updated, which it might not have been , etc. Many, many, many possibilities, even before you start talking about the posited second column of the array, which holds the number(s) of pages that were actually printed, versus the raw total of pages in the report, which were used to calculate various values (including _PAGENO and reportlistener.PageNo).
Why .SharedPageTotal, you ask? .PageTotal, the base property, is readonly. This means, among other things, that a chain of successors cannot be updated with the appropriate value as the “lead” reportlistener gets updated by the engine. .SharedPageTotal exists so that the lead does have a way of updating the successors. But, more significantly for our ppurposes here, .SharedPageTotal belongs to us, and we can manipulate it, just as you can change the value of _PAGETOTAL if you want. In a collection of reports, you might very well be manipulating this value for purposes of your own, and accumulating pages according to some scheme of your own.
6 thoughts on “PDF Power Redux , Redacted, and Reduced”
Very Informative Article. A must Read.
Many Thanks To Auther
Thanks a lot Lisa.
Apart from providing a full PDF solution, I’ve learned some very useful techniques with this article.
Thanks again !
Thanks guys. I should point out on this page that, in SP2, we did adjust the page array referred to in this blog post to make it easier for folks to adjust the array, however they want to, in classes derived from _ReportListener.
You’ll find a brief mention of the change in the relevant TMM page, http://spacefold.com/articles/TMM/_ReportListener.aspx, and I posted more detail in a blog entry, here http://spacefold.com/lisa/A-small-present-for-Ludek-RunReports-revision.
Thanks for sharing your work with the community.
We had trouble when upgrading our installation to GS v8.71, using the “silent install” of GS from a DB table.
VFP adds a .txt extension to filenames with no extension, which made Ghostscript unhappy. Here’s a mod to Lisa’s prg that creates/replaces the installer table directly in the DBC and nixes the bogus .txt extensions.
GS v8.71, incidentally, allows copying text from the generated PDF. GS v7 produced garbled text on copy/paste.
* >L<‘s Q&D program
* for gathering the GhostScript
* files required for installation
* (c) Lisa Slater Nicholls
LOCAL lcTopDir, lcFile
PRIVATE ALL LIKE j*
=MESSAGEBOX(“Open the database and Specify the table in the next dialogs…”)
OPEN DATABASE ?
jcInstall= PUTFILE(“Install dbf…”,”GSinstall”,”dbf”)
DROP TABLE JUSTSTEM(jcInstall)
CREATE TABLE (jcInstall) (filename c(120), DIR c(10), contents m NOCPTRANS)
=MESSAGEBOX( “Find dir for GS Executable in next dialog…”)
lcTopDir = GETDIR()
lcTopDir = ADDBS(lcTopDir)
SET NOCPTRANS TO contents
DO GetFiles WITH lcTopDir, “”
DO GetFiles WITH lcTopDir, “fonts\”
DO GetFiles WITH lcTopDir, “lib\”
PROCEDURE GetFiles(tTopDir, tSubDir)
LOCAL lcFile, lcDir
lcDir = tTopDir + tSubDir
lcFile = SYS(2000,FORCEPATH(“*.*”,lcDir))
DO WHILE NOT EMPTY(lcFile)
INSERT INTO (jcInstall)(filename, DIR) VALUES (JUSTFNAME(lcFile),tSubDir)
APPEND MEMO contents FROM (FORCEPATH(lcFile,lcDir))
*If the filename does not have an extension,
*VFP forces the file extension to “.txt” when
*you COPY MEMO to create the files later. VFP will
*not force a file extension when the name ends in “dot.”
IF !(“.” $ jcName)
REPLACE filename WITH jcName+”.”
lcFile = SYS(2000,FORCEPATH(“*.*”,lcDir),1)