TechSpoken

"Any ideas?" is the most frequently-asked question in technical forums. My answer is: yes.

PDF Power Redux , Redacted, and Reduced

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.  Howver 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.

Comments (6) -

  • alwy ali

    11/8/2007 9:43:01 PM |

    Very Informative Article. A must Read.
    Many Thanks To Auther

  • Cesar

    6/9/2008 3:37:10 PM |

    Thanks a lot Lisa.
    Apart from providing a full PDF solution, I've learned some very useful techniques with this article.

    Thanks again !

  • adrianh

    6/19/2010 1:23:23 PM |

    Thanks for sharing your work with the community.

  • adrianh

    6/19/2010 4:13:31 PM |

    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.

    * CREATEINSTALLDBF.PRG
    * >L<'s Q&D program
    * for gathering the GhostScript
    * files required for installation
    * (c) Lisa Slater Nicholls

    LOCAL lcTopDir, lcFile
    PRIVATE ALL LIKE j*

    CLOSE DATABASES

    =MESSAGEBOX("Open the database and Specify the table in the next dialogs...")

    OPEN DATABASE ?

    jcInstall= PUTFILE("Install dbf...","GSinstall","dbf")
    IF EMPTY(jcInstall)
    =MESSAGEBOX("User cancelled")
    RETURN
    ENDIF

    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\"

    ??CHR(7)
    =MESSAGEBOX("Mission accomplished...")
    RETURN

    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."
    jcName= JUSTFNAME(lcFile)
    IF !("." $ jcName)
    REPLACE filename WITH jcName+"."
    ENDIF

    lcFile = SYS(2000,FORCEPATH("*.*",lcDir),1)
    ENDDO
    ENDPROC
    *----------------------------------------*

    Thanks Lisa!

  • >L<

    6/19/2010 11:47:11 PM |

    Thanks Adrian!

Pingbacks and trackbacks (1)+

Comments are closed