diff --git a/bin/reportlab/MANIFEST.in b/bin/reportlab/MANIFEST.in new file mode 100644 index 00000000000..86f3442fd92 --- /dev/null +++ b/bin/reportlab/MANIFEST.in @@ -0,0 +1,6 @@ +global-include *.dtd *.txt *.xml *.yml +global-include *.c *.h *.in *.mashed +global-include *.gif *.png *.jpg .a85 +global-include *.AFM *.PFB *.ttf +global-include README +include changes diff --git a/bin/reportlab/README b/bin/reportlab/README new file mode 100644 index 00000000000..40b00be75f6 --- /dev/null +++ b/bin/reportlab/README @@ -0,0 +1,35 @@ +#copyright ReportLab Inc. 2000-2001 +#see license.txt for license details +#history http://cvs.sourceforge.net/cgi-bin/cvsweb.cgi/reportlab/README?cvsroot=reportlab +#$Header: /tmp/reportlab/reportlab/README,v 1.9 2002/03/05 17:21:44 rgbecker Exp $ + +This is the ReportLab PDF library. + +Licensing +========= +BSD license. See license.txt for details + +Installation +============ +Either unpack reportlab.zip or reportlab.tgz to some directory say +d:\ReportLab. If you can, ensure that the line terminator style is +correct for your OS (man zip programs have a text mode option eg -a). + +Create a .pth file, say reportlab.pth in your Python +home directory. It should have one line: +d:/ReportLab. + +Alternatively unpack the archive into a directory which is already on your +python path. + +Documentation +============= +Full documentation is included in PDF format in the docs directory. +If you are working with a CVS version, run the script genAll.py +in that directory to generate the documentation. + +Acknowledgements and Thanks +=========================== +lib/normalDate.py originally by Jeff Bauer +(please let us know of other acknowledgements - we just + started this section - 13/2/2002) diff --git a/bin/reportlab/__init__.py b/bin/reportlab/__init__.py new file mode 100644 index 00000000000..5a381ed9b26 --- /dev/null +++ b/bin/reportlab/__init__.py @@ -0,0 +1,33 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/__init__.py +__version__=''' $Id: __init__.py 2877 2006-05-18 15:11:23Z andy $ ''' +__doc__="""The Reportlab PDF generation library.""" +Version = "2.0" + +import sys + +if sys.version_info[0:2] < (2, 3): + warning = """The trunk of reportlab requires Python 2.3 or higher. + Any older applications should either use released versions beginning + with 1.x (e.g. 1.21), or snapshots or checkouts from our 'version1' + branch. + """ + raise ImportError("reportlab needs Python 2.3 or higher", warning) + +def getStory(context): + if context.target == 'UserGuide': + # parse some local file + import os + myDir = os.path.split(__file__)[0] + import yaml + return yaml.parseFile(myDir + os.sep + 'mydocs.yaml') + else: + # this signals that it should revert to default processing + return None + + +def getMonitor(): + import reportlab.monitor + mon = reportlab.monitor.ReportLabToolkitMonitor() + return mon diff --git a/bin/reportlab/changes b/bin/reportlab/changes new file mode 100644 index 00000000000..f22b174ddb8 --- /dev/null +++ b/bin/reportlab/changes @@ -0,0 +1,7788 @@ +################################################################################# +#################### RELEASE 2.0 at 15:00 GMT 23/May/2006 ################# +################################################################################# +r2905 | andy | 2006-05-23 15:49:28 +0100 (Tue, 23 May 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/cidfonts.py + disable loading of CMAPS +r2904 | andy | 2006-05-23 15:19:54 +0100 (Tue, 23 May 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/ch2a_fonts.py + removed CMap references +r2903 | andy | 2006-05-23 15:04:08 +0100 (Tue, 23 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_multibyte_kor.py + M /reportlab/trunk/reportlab/test/test_multibyte_cht.py + M /reportlab/trunk/reportlab/test/test_multibyte_chs.py + proper samples added for CJK languages +r2902 | andy | 2006-05-23 14:08:04 +0100 (Tue, 23 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_multibyte_kor.py + M /reportlab/trunk/reportlab/test/test_multibyte_cht.py + M /reportlab/trunk/reportlab/test/test_multibyte_chs.py + M /reportlab/trunk/reportlab/lib/codecharts.py + partial silencing of cmap tests +r2901 | andy | 2006-05-23 00:02:22 +0100 (Tue, 23 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/pdfbase/cidfonts.py + M /reportlab/trunk/reportlab/pdfbase/_cidfontdata.py + A /reportlab/trunk/reportlab/pdfbase/_can_cmap_data.py + added canned width data for CID fonts +r2900 | andy | 2006-05-22 22:49:00 +0100 (Mon, 22 May 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/genuserguide.py + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + D /reportlab/trunk/reportlab/docs/userguide/ch0_whatsnewinrl2.py + updated user guide +r2899 | rgbecker | 2006-05-22 13:42:07 +0100 (Mon, 22 May 2006) | 1 line + M /reportlab/trunk/reportlab/rl_config.py + rl_config.py: fix missing comma in CMap exemplars list +r2893 | rgbecker | 2006-05-21 10:12:50 +0100 (Sun, 21 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/runAll.py + runAll.py: add support for external running +r2892 | rgbecker | 2006-05-19 15:16:02 +0100 (Fri, 19 May 2006) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + A /reportlab/trunk/reportlab/lib/rltempfile.py + reportlab.lib: break out rltempfile.py to avoid rl_accel imports +r2883 | andy | 2006-05-19 12:34:32 +0100 (Fri, 19 May 2006) | 1 line + M /reportlab/trunk/reportlab/tools/docco/stylesheet.py + M /reportlab/trunk/reportlab/tools/docco/rltemplate.py + M /reportlab/trunk/reportlab/test/runAll.py + manual style changes +r2882 | andy | 2006-05-18 22:54:38 +0100 (Thu, 18 May 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + manual introduction amendments +r2881 | andy | 2006-05-18 17:56:05 +0100 (Thu, 18 May 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/ch2_graphics.py + amended John's encoding paragraph +r2880 | oualid | 2006-05-18 17:45:05 +0100 (Thu, 18 May 2006) | 1 line + M /reportlab/trunk/reportlab/tools/docco/stylesheet.py + added extra paragraphstyle +r2879 | oualid | 2006-05-18 17:43:07 +0100 (Thu, 18 May 2006) | 1 line + A /reportlab/trunk/reportlab/docs/userguide/ch0_whatsnewinrl2.py + chapter What's new in RL 2 of user guide +r2878 | andy | 2006-05-18 16:13:01 +0100 (Thu, 18 May 2006) | 1 line + M /reportlab/trunk/reportlab/lib/codecharts.py + M /reportlab/trunk/reportlab/docs/userguide/ch2a_fonts.py + updated codecharts and ch3 of user guide +r2877 | andy | 2006-05-18 16:11:23 +0100 (Thu, 18 May 2006) | 1 line + M /reportlab/trunk/reportlab/__init__.py + changed version and added warning for old Python users +r2875 | andy | 2006-05-18 08:00:16 +0100 (Thu, 18 May 2006) | 1 line + M /reportlab/trunk/reportlab/lib/validators.py + codec validator for diagra +r2874 | jjlee | 2006-05-17 14:07:13 +0100 (Wed, 17 May 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/ch2_graphics.py + Correct docs in introduction re encoding for V2 +r2873 | rgbecker | 2006-05-17 11:59:59 +0100 (Wed, 17 May 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + pdfmetrics.py: fix rl_accel --> _rl_accel typo +r2872 | rgbecker | 2006-05-17 11:58:04 +0100 (Wed, 17 May 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/rl_codecs.py + rl_codecs: slight speedup +r2871 | rgbecker | 2006-05-17 10:39:29 +0100 (Wed, 17 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_rl_accel.py + test_rl_accel.py: fix ref count checks +r2868 | jjlee | 2006-05-16 15:58:47 +0100 (Tue, 16 May 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + Fix typos +r2867 | rgbecker | 2006-05-16 14:19:40 +0100 (Tue, 16 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_rl_accel.py + test_rl_accel: added some testsi for new functionality +r2865 | rgbecker | 2006-05-15 17:37:44 +0100 (Mon, 15 May 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + ttfonts.py: allow for _rl_accel._instanceStringWidthTTF +r2861 | rgbecker | 2006-05-15 08:44:00 +0100 (Mon, 15 May 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + pdfmetrics.py: _rl_accel._instanceStringWidthU +r2860 | rgbecker | 2006-05-12 18:30:55 +0100 (Fri, 12 May 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + pdfmetrics.py: more version2 accelerator stuff +r2857 | rgbecker | 2006-05-11 14:06:52 +0100 (Thu, 11 May 2006) | 1 line + M /reportlab/trunk/reportlab/platypus/paragraph.py + paragraph: fix misuse of style before setting in cjk breaklines +r2856 | rgbecker | 2006-05-11 10:48:13 +0100 (Thu, 11 May 2006) | 1 line + M /reportlab/trunk/reportlab/demos/odyssey/dodyssey.py + reportlab: add hotshot analysis to dodyssey.py +r2854 | rgbecker | 2006-05-10 13:57:21 +0100 (Wed, 10 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_hello.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + reportlab: PDFString vs Unicode changes +r2853 | rgbecker | 2006-05-10 13:56:39 +0100 (Wed, 10 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + reportlab: fix up links in paragraphs +r2852 | rgbecker | 2006-05-08 16:04:15 +0100 (Mon, 08 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/utils.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/test/runAll.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + reportlab: minor fixes to platypus, tests and fix jap splitting bug +r2851 | rgbecker | 2006-05-08 15:34:45 +0100 (Mon, 08 May 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/barcode/widgets.py + M /reportlab/trunk/reportlab/graphics/barcode/test.py + barcode: fix to import from reportlab +r2850 | rgbecker | 2006-05-08 14:40:06 +0100 (Mon, 08 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/pythonpowered.gif + we shouldn't ever need to change this +r2849 | andy | 2006-05-06 09:25:23 +0100 (Sat, 06 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/utils.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/test/pythonpowered.gif + M /reportlab/trunk/reportlab/platypus/tables.py + friendlified way test suite runs +r2848 | andy | 2006-05-05 00:45:29 +0100 (Fri, 05 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/platypus/tables.py + added japanese splitting test, and table preprocessor to convert unicode->utf8 +r2847 | andy | 2006-05-05 00:07:51 +0100 (Fri, 05 May 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + expanded Japanese para tests +r2846 | rgbecker | 2006-05-04 11:48:36 +0100 (Thu, 04 May 2006) | 1 line + A /reportlab/trunk/reportlab/graphics/barcode/widgets.py + A /reportlab/trunk/reportlab/graphics/barcode/usps.py + A /reportlab/trunk/reportlab/graphics/barcode/test.py + A /reportlab/trunk/reportlab/graphics/barcode/fourstate.py + A /reportlab/trunk/reportlab/graphics/barcode/eanbc.py + A /reportlab/trunk/reportlab/graphics/barcode/common.py + A /reportlab/trunk/reportlab/graphics/barcode/code93.py + A /reportlab/trunk/reportlab/graphics/barcode/code39.py + A /reportlab/trunk/reportlab/graphics/barcode/code128.py + A /reportlab/trunk/reportlab/graphics/barcode/__init__.py + A /reportlab/trunk/reportlab/graphics/barcode/VERSION + A /reportlab/trunk/reportlab/graphics/barcode/TODO + A /reportlab/trunk/reportlab/graphics/barcode/README + A /reportlab/trunk/reportlab/graphics/barcode + reportlab: barcode moved to reportlab/graphics +r2845 | rgbecker | 2006-05-03 13:24:35 +0100 (Wed, 03 May 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + shapes.py: merged stable 2843:2844 minor fix to outDir handling in Drawing.save +r2843 | rgbecker | 2006-04-28 18:33:32 +0100 (Fri, 28 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + reportlab: added pound sign +r2842 | rgbecker | 2006-04-28 15:08:58 +0100 (Fri, 28 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + reportlab: fixed added to paragraph +r2841 | rgbecker | 2006-04-27 18:42:12 +0100 (Thu, 27 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/rl_codecs.py + rl_codecs: eliminate decorators +r2840 | rgbecker | 2006-04-20 15:37:13 +0100 (Thu, 20 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + reportlab: wastefully allow TTFont to use ttc files +r2839 | rgbecker | 2006-04-19 17:55:46 +0100 (Wed, 19 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + ttfonts.py: fix up to use struct properly +r2838 | rgbecker | 2006-04-18 18:47:54 +0100 (Tue, 18 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/axes.py + reportlab: minor fix to axes.py +r2834 | rgbecker | 2006-04-05 18:42:59 +0100 (Wed, 05 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + reportlab: fix bad test test_pdfgen_links.py +r2833 | rgbecker | 2006-04-05 17:01:20 +0100 (Wed, 05 Apr 2006) | 1 line + A /reportlab/trunk/reportlab/test/test_platypus_paraparser.py + A /reportlab/trunk/reportlab/pdfbase/rl_codecs.py + A /reportlab/trunk/reportlab/lib/textsplit.py + reportlab: add files from utf8 branch +r2830 | rgbecker | 2006-04-05 16:18:32 +0100 (Wed, 05 Apr 2006) | 1 line + M /reportlab/trunk/reportlab/tools/pythonpoint/stdparser.py + M /reportlab/trunk/reportlab/tools/pythonpoint/pythonpoint.py + M /reportlab/trunk/reportlab/tools/docco/rl_doc_utils.py + M /reportlab/trunk/reportlab/tools/docco/graphdocpy.py + M /reportlab/trunk/reportlab/test/test_tools_pythonpoint.py + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/test/test_pdfbase_pdfmetrics.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/rl_config.py + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/para.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + M /reportlab/trunk/reportlab/pdfbase/cidfonts.py + M /reportlab/trunk/reportlab/pdfbase/_cidfontdata.py + M /reportlab/trunk/reportlab/lib/validators.py + M /reportlab/trunk/reportlab/lib/styles.py + M /reportlab/trunk/reportlab/lib/rparsexml.py + M /reportlab/trunk/reportlab/graphics/testshapes.py + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderPDF.py + M /reportlab/trunk/reportlab/docs/userguide/ch5_paragraphs.py + M /reportlab/trunk/reportlab/docs/userguide/ch2a_fonts.py + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + M /reportlab/trunk/reportlab/demos/stdfonts/stdfonts.py + reportlab-utf8 moved to trunk +r2808 | rgbecker | 2006-03-15 16:47:27 +0000 (Wed, 15 Mar 2006) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/graphics/renderSVG.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + M /reportlab/trunk/reportlab/graphics/renderPM.py + reportlab: changes to accomodate barcodes +r2807 | andy | 2006-03-15 12:22:10 +0000 (Wed, 15 Mar 2006) | 1 line + M /reportlab/trunk/reportlab/lib/validators.py + new pattern matcher - partially done +r2806 | andy | 2006-03-13 23:33:51 +0000 (Mon, 13 Mar 2006) | 1 line + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + updated _imaging import +r2803 | rgbecker | 2006-03-09 13:29:26 +0000 (Thu, 09 Mar 2006) | 1 line + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + reportlab: added FreeTextAnnotation and minor improvements to other annotations +r2802 | rgbecker | 2006-03-09 13:21:50 +0000 (Thu, 09 Mar 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/graphics/renderbase.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + reportlab/graphics cosmetics and adding drawImage to renderPS +r2799 | rgbecker | 2006-03-02 13:54:39 +0000 (Thu, 02 Mar 2006) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: attempt to fix up keepInFrame for one more case +r2794 | rgbecker | 2006-03-01 10:48:40 +0000 (Wed, 01 Mar 2006) | 1 line + A /reportlab/trunk/reportlab/tools/utils/dumpttf.py + reportlab/tools/utils: added dumpttf.py +r2792 | rgbecker | 2006-02-28 18:37:06 +0000 (Tue, 28 Feb 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + ttfonts.py: fix index bug in name table read +r2789 | rgbecker | 2006-02-28 15:04:57 +0000 (Tue, 28 Feb 2006) | 1 line + M /reportlab/trunk/reportlab/pdfbase/cidfonts.py + M /reportlab/trunk/reportlab/lib/utils.py + reportlab: remove cidfonts print, fix get_rl_tempdir +r2786 | rgbecker | 2006-02-27 21:26:46 +0000 (Mon, 27 Feb 2006) | 1 line + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + reportlab: minor changes to annotation support +r2775 | rgbecker | 2006-02-15 12:17:24 +0000 (Wed, 15 Feb 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/__init__.py + platypus: ImagesAndFlowables in final form +r2774 | rgbecker | 2006-02-14 18:07:27 +0000 (Tue, 14 Feb 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/__init__.py + reportlab: added FlowablesAndImage +r2765 | rgbecker | 2006-02-02 18:48:12 +0000 (Thu, 02 Feb 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/pdfbase/pdfutils.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfgen: fix badly drawn jpegs which are actually gifs +r2762 | rgbecker | 2006-01-30 16:29:07 +0000 (Mon, 30 Jan 2006) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/flowables.py + added _offsets handling to paragraph for flow around on left side +r2759 | rgbecker | 2006-01-25 11:25:29 +0000 (Wed, 25 Jan 2006) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + utils.py: annotate ImageReader read errors +r2755 | rgbecker | 2006-01-25 11:21:10 +0000 (Wed, 25 Jan 2006) | 1 line + M /reportlab/trunk/reportlab/platypus/para.py + para.py: minor layout changes to example etc +r2752 | rgbecker | 2006-01-09 13:29:53 +0000 (Mon, 09 Jan 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + shapes.py: fix wrap for Drawings to take account of renderScale +r2749 | rgbecker | 2006-01-09 11:22:03 +0000 (Mon, 09 Jan 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/graphics/renderSVG.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderPDF.py + reprotlab/graphics: fix so renderScale is used properly +r2746 | rgbecker | 2006-01-05 15:17:03 +0000 (Thu, 05 Jan 2006) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + shapes.py: make copy use derived class if not overridden +r2745 | rgbecker | 2005-12-20 12:43:24 +0000 (Tue, 20 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + shapes.py: allow override for CWD +r2744 | rgbecker | 2005-12-15 12:28:18 +0000 (Thu, 15 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/pdfgen/pdfimages.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + reportlab: fix test_graphics_charts and eliminate 0 x 0 image bug in Acrobat 7.0 +r2743 | rgbecker | 2005-12-12 15:51:29 +0000 (Mon, 12 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + piecharts: now have orderMode 'alternate' and proper vertical spreading +r2742 | oualid | 2005-12-12 11:58:08 +0000 (Mon, 12 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/lib/normalDate.py + normalDate: add initialize support for datetime +r2741 | andy | 2005-12-07 21:52:33 +0000 (Wed, 07 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/lib/validators.py + M /reportlab/trunk/reportlab/lib/attrmap.py + M /reportlab/trunk/reportlab/graphics/testshapes.py + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/graphics/renderbase.py + datacharts synchronised +r2740 | rgbecker | 2005-12-07 13:30:14 +0000 (Wed, 07 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/rl_config.py + rl_config: fix typo +r2739 | rgbecker | 2005-12-07 13:25:25 +0000 (Wed, 07 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/rl_config.py + M /reportlab/trunk/reportlab/graphics/shapes.py + reportlab: add defaultGraphicsFontName, svg saving +r2738 | rgbecker | 2005-12-07 11:39:53 +0000 (Wed, 07 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/graphics/renderbase.py + M /reportlab/trunk/reportlab/graphics/renderSVG.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderPDF.py + graphics: added Drawing.renderScale hack for renderer terminal drawing scales +r2737 | rgbecker | 2005-12-06 18:36:58 +0000 (Tue, 06 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + piecharts: initial version of pointer labels +r2736 | andy | 2005-12-01 23:03:45 +0000 (Thu, 01 Dec 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + removed a 2.4-ism +r2733 | rgbecker | 2005-11-22 11:24:22 +0000 (Tue, 22 Nov 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc.py: more optimisations +r2732 | rgbecker | 2005-11-21 19:39:42 +0000 (Mon, 21 Nov 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc.py: speed improvements +r2727 | rgbecker | 2005-10-31 11:20:18 +0000 (Mon, 31 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + pdfgen: abstract color setting ops to textobject._PDFColorSetter +r2724 | rgbecker | 2005-10-30 11:52:47 +0000 (Sun, 30 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/axes.py + axes.py: factor out _allInt +r2723 | rgbecker | 2005-10-30 11:52:10 +0000 (Sun, 30 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + piecharts.py: fix checkLabelOverlap for 3d pie +r2719 | rgbecker | 2005-10-20 14:19:59 +0100 (Thu, 20 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_graphics_layout.py + M /reportlab/trunk/reportlab/graphics/shapes.py + reportlab: fix small angle addArc & Wedge.asPolygon +r2715 | rgbecker | 2005-10-10 12:56:30 +0100 (Mon, 10 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: fixes to PTOContainer +r2711 | rgbecker | 2005-10-06 11:04:37 +0100 (Thu, 06 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_pto.py + M /reportlab/trunk/reportlab/platypus/flowables.py + platypus: fixup KeepInFrame error handling +r2707 | rgbecker | 2005-10-04 13:59:21 +0100 (Tue, 04 Oct 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: make KeepInFrame wrapping more robust +r2699 | rgbecker | 2005-09-29 13:35:38 +0100 (Thu, 29 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/frames.py + frames.py: logger only when _debug +r2698 | rgbecker | 2005-09-29 12:49:23 +0100 (Thu, 29 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + platypus: changes to make identity more useful +r2697 | rgbecker | 2005-09-28 18:22:53 +0100 (Wed, 28 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_pto.py + test_platypus_pto.py: changed FrameFlowable to KeepInFrame +r2696 | rgbecker | 2005-09-28 14:22:53 +0100 (Wed, 28 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + flowables.py: keepInFrame now truncates etc properly, doctemplate.py: fix handle_frameEnd +r2695 | andy | 2005-09-27 22:12:08 +0100 (Tue, 27 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + renamed FrameFlowable to KeepInFrame +r2694 | rgbecker | 2005-09-27 18:31:51 +0100 (Tue, 27 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + flowablse.py: minor fix and hack to FrameFlowable +r2693 | rgbecker | 2005-09-27 16:29:16 +0100 (Tue, 27 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: minor fixes revealed by rml2pdf use +r2692 | rgbecker | 2005-09-27 14:26:12 +0100 (Tue, 27 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_pto.py + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + platypus: initial FrameFlowable implementation +r2689 | rgbecker | 2005-09-22 17:53:18 +0100 (Thu, 22 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + merge stable 2687:2688 pdfdoc.py: attempt to robustify escape attribute access +r2680 | rgbecker | 2005-09-21 17:17:56 +0100 (Wed, 21 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/platypus/flowables.py + platypus: fix up None defaults for table line commands, add HRFlowable dash arg +r2679 | rgbecker | 2005-09-20 16:59:39 +0100 (Tue, 20 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/axes.py + axes.py: make YCategoryAxis accept less than n labels +r2676 | rgbecker | 2005-09-06 11:25:00 +0100 (Tue, 06 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/samples/radar.py + M /reportlab/trunk/reportlab/graphics/samples/filled_radar.py + M /reportlab/trunk/reportlab/graphics/charts/spider.py + merge stable 2674:2675 graphics: fix buglet in spider and non-conforming samples +r2674 | rgbecker | 2005-09-05 17:20:26 +0100 (Mon, 05 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/spider.py + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + merged utf8 2666:2671 improved spider plots +r2668 | rgbecker | 2005-09-05 11:23:51 +0100 (Mon, 05 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/widgetbase.py + merge utf8 2666:2667 widgetbase: make line[3] back up for line[(3,x)] +r2663 | rgbecker | 2005-09-02 10:44:17 +0100 (Fri, 02 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/utils3d.py + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + reportlab/graphics/charts: better piechart labels +r2662 | rgbecker | 2005-09-01 17:18:33 +0100 (Thu, 01 Sep 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc: allow special escaping in Annotations and PDFString +r2659 | rgbecker | 2005-08-18 11:28:12 +0100 (Thu, 18 Aug 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + M /reportlab/trunk/reportlab/graphics/charts/axes.py + merge stable 2657:2658 axes/lineplots.py: fix inFill behaviour +r2655 | rgbecker | 2005-08-16 10:40:10 +0100 (Tue, 16 Aug 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_lib_utils.py + merge stable 2653:2654 test_lib_utils.py: ensure we get an incremented count +r2652 | rgbecker | 2005-08-15 12:21:53 +0100 (Mon, 15 Aug 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_lib_utils.py + nerge stable 2649:2650 test_lib_utils.py: fix race in temp creation +r2649 | andy | 2005-07-31 23:47:00 +0100 (Sun, 31 Jul 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/para.py + Chad Miller's table fix +r2648 | rgbecker | 2005-07-28 16:10:18 +0100 (Thu, 28 Jul 2005) | 1 line + M /reportlab/trunk/reportlab/pdfgen/canvas.py + canvas.py: fix probable bug in setdash +r2647 | rgbecker | 2005-07-26 14:47:51 +0100 (Tue, 26 Jul 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/textlabels.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + merge 2643:2644 from stable textlabels.py, barcharts.py: added boxTarget support +r2630 | rgbecker | 2005-07-08 10:18:22 +0100 (Fri, 08 Jul 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_pdfbase_ttfonts.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + merge 2628:2629: ttfonts: altered subset naming +r2623 | rgbecker | 2005-06-27 16:34:29 +0100 (Mon, 27 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + pdfbase.pdfmetrics: fix glob to be zip friendly +r2619 | rgbecker | 2005-06-24 15:49:15 +0100 (Fri, 24 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/test/utils.py + M /reportlab/trunk/reportlab/test/test_widgets_grids.py + M /reportlab/trunk/reportlab/test/test_widgetbase_tpc.py + M /reportlab/trunk/reportlab/test/test_utils.py + M /reportlab/trunk/reportlab/test/test_tools_pythonpoint.py + M /reportlab/trunk/reportlab/test/test_table_layout.py + M /reportlab/trunk/reportlab/test/test_source_chars.py + M /reportlab/trunk/reportlab/test/test_rl_accel.py + M /reportlab/trunk/reportlab/test/test_renderSVG.py + M /reportlab/trunk/reportlab/test/test_pyfiles.py + M /reportlab/trunk/reportlab/test/test_platypus_xref.py + M /reportlab/trunk/reportlab/test/test_platypus_toc.py + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/test/test_platypus_pto.py + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/test/test_platypus_leftright.py + M /reportlab/trunk/reportlab/test/test_platypus_indents.py + M /reportlab/trunk/reportlab/test/test_platypus_general.py + M /reportlab/trunk/reportlab/test/test_platypus_breaking.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pycanvas.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pagemodes.py + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/test/test_pdfgen_callback.py + M /reportlab/trunk/reportlab/test/test_pdfbase_ttfonts.py + M /reportlab/trunk/reportlab/test/test_pdfbase_postscript.py + M /reportlab/trunk/reportlab/test/test_pdfbase_pdfutils.py + M /reportlab/trunk/reportlab/test/test_pdfbase_pdfmetrics.py + M /reportlab/trunk/reportlab/test/test_pdfbase_fontembed.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + M /reportlab/trunk/reportlab/test/test_paragraphs.py + M /reportlab/trunk/reportlab/test/test_multibyte_kor.py + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/test/test_multibyte_cht.py + M /reportlab/trunk/reportlab/test/test_multibyte_chs.py + M /reportlab/trunk/reportlab/test/test_lib_validators.py + M /reportlab/trunk/reportlab/test/test_lib_utils.py + M /reportlab/trunk/reportlab/test/test_lib_sequencer.py + M /reportlab/trunk/reportlab/test/test_lib_colors.py + M /reportlab/trunk/reportlab/test/test_invariant.py + M /reportlab/trunk/reportlab/test/test_images.py + M /reportlab/trunk/reportlab/test/test_hello.py + M /reportlab/trunk/reportlab/test/test_graphics_speed.py + M /reportlab/trunk/reportlab/test/test_graphics_layout.py + M /reportlab/trunk/reportlab/test/test_graphics_images.py + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/test/test_extra.py + M /reportlab/trunk/reportlab/test/test_docstrings.py + M /reportlab/trunk/reportlab/test/test_docs_build.py + M /reportlab/trunk/reportlab/test/test_charts_textlabels.py + M /reportlab/trunk/reportlab/test/runAll.py + reportlab/test: fix so individual tests print output folder location +r2617 | andy | 2005-06-15 21:23:07 +0100 (Wed, 15 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + generalized a rarely-used error message handler which changed in 2.3 +r2613 | rgbecker | 2005-06-15 14:46:10 +0100 (Wed, 15 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/spider.py + spider.py: fix all zero bug (inspired by Max M) +r2611 | rgbecker | 2005-06-13 11:13:47 +0100 (Mon, 13 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + utils.py: fix argument reference +r2608 | rgbecker | 2005-06-12 12:53:56 +0100 (Sun, 12 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_pdfbase_ttfonts.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + ttfonts.py: apply Albertas Agejevas' spaces patch to fix para splitting +r2606 | andy | 2005-06-09 23:07:02 +0100 (Thu, 09 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + added a platypus logger +r2604 | rgbecker | 2005-06-08 11:12:46 +0100 (Wed, 08 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + legends.py: fix 2.1 compatibility problem +r2599 | rgbecker | 2005-06-06 17:07:54 +0100 (Mon, 06 Jun 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + utils.py: raise RuntimeError if no image handling available in ImageReader +r2595 | rgbecker | 2005-05-27 16:40:20 +0100 (Fri, 27 May 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/legends.py + fundsdemo: added support for special legending +r2592 | rgbecker | 2005-05-16 13:18:46 +0100 (Mon, 16 May 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: allow for h/vAlign constructor agruments to be used +r2591 | andy | 2005-05-13 20:22:10 +0100 (Fri, 13 May 2005) | 1 line + M /reportlab/trunk/reportlab/tools/pythonpoint/pythonpoint.py + added a check for missing files +r2590 | rgbecker | 2005-05-10 14:09:40 +0100 (Tue, 10 May 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/renderPS.py + renderPS.py: attempt to fix setFont error +r2589 | rgbecker | 2005-05-04 12:33:30 +0100 (Wed, 04 May 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/doctemplate.py + doctemplate.py: add rotation argument to BaseDocTemplate +r2588 | rgbecker | 2005-05-03 12:56:56 +0100 (Tue, 03 May 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/doctemplate.py + doctemplate.py: fix syntax error +r2587 | rgbecker | 2005-05-03 12:41:54 +0100 (Tue, 03 May 2005) | 1 line + M /reportlab/trunk/reportlab/lib/colors.py + colors.py: add a fidred +r2586 | rgbecker | 2005-05-03 12:41:17 +0100 (Tue, 03 May 2005) | 1 line + M /reportlab/trunk/reportlab/lib/attrmap.py + attrmap: fix initialization of _initial +r2585 | rgbecker | 2005-05-03 12:40:11 +0100 (Tue, 03 May 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/doctemplate.py + doctemplate.py: support for canvasmaker and rotation +r2584 | rgbecker | 2005-05-03 12:39:22 +0100 (Tue, 03 May 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + M /reportlab/trunk/reportlab/lib/utils.py + reportlab: better support for PIL image +r2583 | rgbecker | 2005-04-30 17:31:55 +0100 (Sat, 30 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc.py: fix misspelling of DeviceGray +r2581 | rgbecker | 2005-04-27 13:09:43 +0100 (Wed, 27 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + utils.py: use normpath in _archive etc etc +r2579 | rgbecker | 2005-04-25 11:38:14 +0100 (Mon, 25 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfutils.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + M /reportlab/trunk/reportlab/lib/utils.py + CMYK image improvements +r2576 | rgbecker | 2005-04-20 11:39:42 +0100 (Wed, 20 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: fix vertical height bug +r2570 | jjlee | 2005-04-19 13:44:21 +0100 (Tue, 19 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_utils.py + Merge to trunk the fix to a FmtSelfDict test that I accidentally conmitted to a tag (svn merge -r 2568:2569 https://www.reportlab.co.uk/svn/public/reportlab/tags/fidreleases/fidelity-20050310T103800/reportlab) +r2566 | jjlee | 2005-04-18 14:25:25 +0100 (Mon, 18 Apr 2005) | 1 line + A /reportlab/trunk/reportlab/test/test_utils.py + M /reportlab/trunk/reportlab/lib/utils.py + Allow overriding parameters in _fmt utility method +r2561 | rgbecker | 2005-04-15 15:04:49 +0100 (Fri, 15 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/pdfgen/canvas.py + canvas.py: minor changes to bring into line with TBlatter's early serializer version +r2560 | rgbecker | 2005-04-15 15:04:10 +0100 (Fri, 15 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc.py: minor changes to bring into line with TBlatter's early serializer version +r2551 | rgbecker | 2005-04-14 15:53:04 +0100 (Thu, 14 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc.py: minor changes to simplify PDFInfoL/U in other apps +r2550 | rgbecker | 2005-04-12 18:23:17 +0100 (Tue, 12 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: fixes to split + spans +r2549 | rgbecker | 2005-04-01 10:38:18 +0100 (Fri, 01 Apr 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: eliminate use of enumerate +r2548 | rgbecker | 2005-03-31 16:35:20 +0100 (Thu, 31 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + pdfmetrics.py: fix minor buglet discovered by Ron Peleg +r2547 | rgbecker | 2005-03-29 14:54:51 +0100 (Tue, 29 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_table_layout.py + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: added in Gary Poster's gary@zope.com latest patch +r2546 | rgbecker | 2005-03-23 13:35:58 +0000 (Wed, 23 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: be evn more robust about width/drawWidth etc +r2545 | rgbecker | 2005-03-23 11:35:10 +0000 (Wed, 23 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: fix for case when minWidth fails du eot attribute error +r2544 | rgbecker | 2005-03-18 18:51:22 +0000 (Fri, 18 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: improve join/cap conversion +r2543 | rptlab | 2005-03-18 15:15:23 +0000 (Fri, 18 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: 2.1 compatibility changes +r2542 | rgbecker | 2005-03-18 14:37:49 +0000 (Fri, 18 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + tables.py, paragraph.py: Gary Poster's layout improvement patch +r2541 | rgbecker | 2005-03-18 14:37:01 +0000 (Fri, 18 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/doctemplate.py + doctemplate.py: fix cgitb recursion problem +r2540 | rgbecker | 2005-03-17 12:01:27 +0000 (Thu, 17 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: fix Gary's patch (broken by Robin) +r2539 | rgbecker | 2005-03-16 18:46:16 +0000 (Wed, 16 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: fixes to join/cap/dash from Gary Poster +r2538 | rgbecker | 2005-03-16 09:47:07 +0000 (Wed, 16 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: empty cellval patch from Jean-Francois Gosset +r2537 | rgbecker | 2005-03-15 14:19:29 +0000 (Tue, 15 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/pdfgen/pathobject.py + pathobject: speed improvements and initial assertion +r2536 | andy | 2005-03-14 20:04:55 +0000 (Mon, 14 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + enhanced decimal alignment +r2535 | rgbecker | 2005-03-11 10:34:29 +0000 (Fri, 11 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/doctemplate.py + doctemplate.py: fix buglet reported by Steve Halasz +r2530 | jjlee | 2005-03-02 14:27:57 +0000 (Wed, 02 Mar 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + Add process environment to logged traceback +r2528 | rgbecker | 2005-02-28 15:55:58 +0000 (Mon, 28 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + canvas.py: added in Andrew Mercer's linkURL fix +r2526 | rgbecker | 2005-02-21 15:40:08 +0000 (Mon, 21 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_paragraphs.py + test_paragraphs.py: added font size change test for underlining test +r2525 | rgbecker | 2005-02-21 14:55:11 +0000 (Mon, 21 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_paragraphs.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + paragraph.py: added proper undeerline color +r2524 | rgbecker | 2005-02-17 08:43:01 +0000 (Thu, 17 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/lib/units.py + units.py: fix pica conversion thanks to Paul McNett +r2523 | rgbecker | 2005-02-07 17:59:09 +0000 (Mon, 07 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + utils.py: added FmtSelfDict class +r2522 | rgbecker | 2005-02-07 11:26:31 +0000 (Mon, 07 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/shapes.py + shapes.py: added save renderer specific argument handling +r2521 | rgbecker | 2005-02-07 10:29:15 +0000 (Mon, 07 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/pdfgen/canvas.py + canvas.py: added setFontSize method +r2520 | rgbecker | 2005-02-05 10:19:05 +0000 (Sat, 05 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: fix 2.1 breakage +r2519 | rgbecker | 2005-02-03 17:14:58 +0000 (Thu, 03 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + platypus: make LCActionFlowable the bad ones +r2518 | rgbecker | 2005-02-03 11:30:21 +0000 (Thu, 03 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + platypus: attempt to make KeepTogether/keepWithNext more robust +r2517 | rgbecker | 2005-02-02 14:43:28 +0000 (Wed, 02 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + reportlab/lib/utils.py: remove bad checkin +r2516 | rgbecker | 2005-02-02 14:40:15 +0000 (Wed, 02 Feb 2005) | 1 line + M /reportlab/trunk/reportlab/lib/utils.py + fix reportlab_functional.txt +r2515 | andy | 2005-01-31 16:47:03 +0000 (Mon, 31 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/paraparser.py + allow solid para background +r2514 | andy | 2005-01-27 21:44:35 +0000 (Thu, 27 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/platypus/paragraph.py + minor fix to allow getPlaintext on more paragraphs +r2512 | rgbecker | 2005-01-14 16:06:57 +0000 (Fri, 14 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: added span rows split handling & test contributed by Andre Reitz +r2511 | rgbecker | 2005-01-14 15:42:12 +0000 (Fri, 14 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/setup.py + setup.py: fix missing comma, contributed by Andre Reitz +r2510 | rgbecker | 2005-01-13 11:50:57 +0000 (Thu, 13 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/axes.py + axes.py: finally make abf/rr work properly +r2509 | rgbecker | 2005-01-10 18:29:17 +0000 (Mon, 10 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/renderPS.py + renderPS.py: fix buglet in color setting +r2507 | rgbecker | 2005-01-10 10:04:09 +0000 (Mon, 10 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/legends.py + legends.py: fix for empty legend prob +r2506 | rgbecker | 2005-01-06 12:21:25 +0000 (Thu, 06 Jan 2005) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_leftright.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + added canvas.textAnnotation and tests for it +r2504 | rgbecker | 2004-12-31 12:49:02 +0000 (Fri, 31 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/legends.py + legends.py: fix callout bug +r2502 | rgbecker | 2004-12-31 11:20:06 +0000 (Fri, 31 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/rl_config.py + M /reportlab/trunk/reportlab/platypus/tables.py + added longTableOptimize config variable +r2501 | rgbecker | 2004-12-31 11:13:44 +0000 (Fri, 31 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + test_graphics_charts.py: added new legend testing +r2500 | rgbecker | 2004-12-30 09:51:30 +0000 (Thu, 30 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + barcharts.py: fix missing getattr +r2499 | rgbecker | 2004-12-29 17:12:34 +0000 (Wed, 29 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/doughnut.py + Fix up missing initialization +r2498 | rgbecker | 2004-12-29 17:11:02 +0000 (Wed, 29 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + M /reportlab/trunk/reportlab/graphics/charts/doughnut.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + barcharts.py: NoneOr was miscapitalized +r2497 | rgbecker | 2004-12-29 14:08:38 +0000 (Wed, 29 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + charts: minor adjustments to legends/swatches for pies/barcharts +r2496 | rgbecker | 2004-12-23 15:38:16 +0000 (Thu, 23 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/legends.py + legends.py: added variColumn attribute +r2495 | rgbecker | 2004-12-22 18:25:37 +0000 (Wed, 22 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + graphics: minor legend tweaks +r2494 | rgbecker | 2004-12-22 16:50:08 +0000 (Wed, 22 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/axes.py + axes.py: added categegorAxis tickShift +r2493 | rgbecker | 2004-12-22 16:14:25 +0000 (Wed, 22 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/graphics/charts/spider.py + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + M /reportlab/trunk/reportlab/graphics/charts/linecharts.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + charts: autolegending in place, legend now has boxAnchor +r2492 | rgbecker | 2004-12-21 18:02:28 +0000 (Tue, 21 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/lib/validators.py + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + M /reportlab/trunk/reportlab/graphics/charts/linecharts.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + M /reportlab/trunk/reportlab/graphics/charts/axes.py + Auto Legends +r2491 | andy | 2004-12-21 05:29:55 +0000 (Tue, 21 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + missing isString import +r2490 | rgbecker | 2004-12-20 19:00:02 +0000 (Mon, 20 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/lib/validators.py + validators.py: better auto support +r2489 | rgbecker | 2004-12-20 18:59:35 +0000 (Mon, 20 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/legends.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + legends.py: more smart changes +r2488 | rgbecker | 2004-12-19 09:19:07 +0000 (Sun, 19 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/legends.py + legends.py: fix missing import +r2487 | rgbecker | 2004-12-19 09:15:56 +0000 (Sun, 19 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/lib/validators.py + validators.py: forgot to check in with AutoOr +r2486 | rgbecker | 2004-12-17 16:58:32 +0000 (Fri, 17 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/widgetbase.py + M /reportlab/trunk/reportlab/graphics/charts/spider.py + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + M /reportlab/trunk/reportlab/graphics/charts/doughnut.py + graphics: changes for SmartLegend +r2485 | rgbecker | 2004-12-16 16:42:12 +0000 (Thu, 16 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + lineplots.py: allow filled/unfilled plotting +r2484 | andy | 2004-12-10 07:27:50 +0000 (Fri, 10 Dec 2004) | 1 line + A /reportlab/trunk/reportlab/test/test_platypus_leftright.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + M /reportlab/trunk/reportlab/lib/corp.py + left/right template support +r2483 | rgbecker | 2004-12-06 17:00:44 +0000 (Mon, 06 Dec 2004) | 1 line + M /reportlab/trunk/reportlab/test/runAll.py + reportlab.test.utils.py: fix 2.4 problem +r2480 | rgbecker | 2004-11-25 14:27:07 +0000 (Thu, 25 Nov 2004) | 1 line + M /reportlab/trunk/reportlab/changes + reportlab: prparing for 1.20 re-release +r2478 | rgbecker | 2004-11-25 14:18:03 +0000 (Thu, 25 Nov 2004) | 1 line + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: extend span line blocking to multi lines +################################################################################# +#################### RELEASE 1.20 at 18:00 GMT 25/Nov/2004 ################# +################################################################################# +r2478 | rgbecker | 2004-11-25 14:18:03 +0000 (Thu, 25 Nov 2004) + tables.py: extend span line blocking to multi lines +r2477 | rgbecker | 2004-11-24 18:29:57 +0000 (Wed, 24 Nov 2004) + setup.py: add backin the missing sub-packages +r2476 | rgbecker | 2004-11-24 16:42:12 +0000 (Wed, 24 Nov 2004) + reportlab: table improvments and better documentation +r2475 | rgbecker | 2004-11-23 17:08:33 +0000 (Tue, 23 Nov 2004) + reportlab.setup.py: now works with and without rl_accel +r2462 | rgbecker | 2004-11-03 20:48:51 +0000 (Wed, 03 Nov 2004) + M /reportlab/trunk/reportlab/lib/utils.py + reportlab lib utils.py: add find_locals +r2461 | rgbecker | 2004-11-03 15:38:07 +0000 (Wed, 03 Nov 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + reportlab graphics: changes for canvasadapter +r2460 | rgbecker | 2004-11-02 10:18:55 +0000 (Tue, 02 Nov 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: fix no PIL jpg problem reported by Martin.Zohlhuber@tttech.com +r2459 | rgbecker | 2004-10-23 16:46:56 +0100 (Sat, 23 Oct 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/platypus/flowables.py + canvasadapter.py fixes +r2458 | rgbecker | 2004-10-22 17:37:01 +0100 (Fri, 22 Oct 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/lib/colors.py + minor changes for pdf canvas compatibility and tests +r2457 | rgbecker | 2004-10-20 20:27:07 +0100 (Wed, 20 Oct 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderbase.py + M /reportlab/trunk/reportlab/platypus/tables.py + CanvasAdapter related changes +r2456 | rgbecker | 2004-10-19 18:52:20 +0100 (Tue, 19 Oct 2004) + M /reportlab/trunk/reportlab/platypus/paragraph.py + paragraph.py: use canvas.setFillColor +r2455 | rgbecker | 2004-10-07 18:51:31 +0100 (Thu, 07 Oct 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfutils.py + pdfutils.py: added _fusc +r2454 | rgbecker | 2004-10-07 10:36:36 +0100 (Thu, 07 Oct 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/stdparser.py + stdparser.py: fix 2.1 brokenness +r2453 | andy | 2004-10-05 23:34:14 +0100 (Tue, 05 Oct 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + fixed col/row shading bug where last row or col never got shaded - reported R Revill +r2452 | andy | 2004-10-04 21:52:20 +0100 (Mon, 04 Oct 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Tim Roberts' fix to vertical table cell alignment +r2451 | andy | 2004-10-04 08:03:42 +0100 (Mon, 04 Oct 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/stdparser.py + Francesco Pierfederici [fpierfed@noao.edu] fix to incorrect path argument +r2450 | rgbecker | 2004-10-01 13:28:12 +0100 (Fri, 01 Oct 2004) + M /reportlab/trunk/reportlab/rl_config.py + rl_config.py: use import for local_rl_config +r2449 | rgbecker | 2004-10-01 11:40:15 +0100 (Fri, 01 Oct 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: fix buglet in command adding +r2448 | andy | 2004-09-29 11:18:32 +0100 (Wed, 29 Sep 2004) + M /reportlab/trunk/reportlab/lib/colors.py + M /reportlab/trunk/reportlab/lib/corp.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + table color cycle changes +r2447 | andy | 2004-09-29 10:17:52 +0100 (Wed, 29 Sep 2004) + M /reportlab/trunk/reportlab/license.txt + Updated to 2004 +r2446 | rgbecker | 2004-09-23 17:52:19 +0100 (Thu, 23 Sep 2004) + D /reportlab/trunk/reportlab/lib/Makefile.pre.in + D /reportlab/trunk/reportlab/lib/README.extensions + D /reportlab/trunk/reportlab/lib/Setup.in + D /reportlab/trunk/reportlab/lib/__BUILD.dsw + D /reportlab/trunk/reportlab/lib/_rl_accel.c + D /reportlab/trunk/reportlab/lib/_rl_accel.dsp + D /reportlab/trunk/reportlab/lib/_rl_accel.java + D /reportlab/trunk/reportlab/lib/hnjalloc.c + D /reportlab/trunk/reportlab/lib/hnjalloc.h + D /reportlab/trunk/reportlab/lib/hyphen.c + D /reportlab/trunk/reportlab/lib/hyphen.h + D /reportlab/trunk/reportlab/lib/hyphen.mashed + D /reportlab/trunk/reportlab/lib/pyHnj.dsp + D /reportlab/trunk/reportlab/lib/pyHnjmodule.c + D /reportlab/trunk/reportlab/lib/setup.py + D /reportlab/trunk/reportlab/lib/sgmlop.c + D /reportlab/trunk/reportlab/lib/sgmlop.dsp + M /reportlab/trunk/reportlab/setup.py + A /reportlab/trunk/rl_addons/rl_accel + A /reportlab/trunk/rl_addons/rl_accel/Makefile.pre.in (from /reportlab/trunk/reportlab/lib/Makefile.pre.in:2444) + A /reportlab/trunk/rl_addons/rl_accel/README.extensions (from /reportlab/trunk/reportlab/lib/README.extensions:2444) + A /reportlab/trunk/rl_addons/rl_accel/Setup.in (from /reportlab/trunk/reportlab/lib/Setup.in:2444) + A /reportlab/trunk/rl_addons/rl_accel/__BUILD.dsw (from /reportlab/trunk/reportlab/lib/__BUILD.dsw:2444) + A /reportlab/trunk/rl_addons/rl_accel/_rl_accel.c (from /reportlab/trunk/reportlab/lib/_rl_accel.c:2444) + A /reportlab/trunk/rl_addons/rl_accel/_rl_accel.dsp (from /reportlab/trunk/reportlab/lib/_rl_accel.dsp:2444) + A /reportlab/trunk/rl_addons/rl_accel/_rl_accel.java (from /reportlab/trunk/reportlab/lib/_rl_accel.java:2444) + A /reportlab/trunk/rl_addons/rl_accel/hnjalloc.c (from /reportlab/trunk/reportlab/lib/hnjalloc.c:2444) + A /reportlab/trunk/rl_addons/rl_accel/hnjalloc.h (from /reportlab/trunk/reportlab/lib/hnjalloc.h:2444) + A /reportlab/trunk/rl_addons/rl_accel/hyphen.c (from /reportlab/trunk/reportlab/lib/hyphen.c:2444) + A /reportlab/trunk/rl_addons/rl_accel/hyphen.h (from /reportlab/trunk/reportlab/lib/hyphen.h:2444) + A /reportlab/trunk/rl_addons/rl_accel/hyphen.mashed (from /reportlab/trunk/reportlab/lib/hyphen.mashed:2444) + A /reportlab/trunk/rl_addons/rl_accel/pyHnj.dsp (from /reportlab/trunk/reportlab/lib/pyHnj.dsp:2444) + A /reportlab/trunk/rl_addons/rl_accel/pyHnjmodule.c (from /reportlab/trunk/reportlab/lib/pyHnjmodule.c:2444) + A /reportlab/trunk/rl_addons/rl_accel/setup.py (from /reportlab/trunk/reportlab/lib/setup.py:2444) + A /reportlab/trunk/rl_addons/rl_accel/sgmlop.c (from /reportlab/trunk/reportlab/lib/sgmlop.c:2444) + A /reportlab/trunk/rl_addons/rl_accel/sgmlop.dsp (from /reportlab/trunk/reportlab/lib/sgmlop.dsp:2444) + reportlab/lib: moved _rl_accel and friends to rl_addons/rl_accel +r2445 | rgbecker | 2004-09-23 13:36:41 +0100 (Thu, 23 Sep 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: robustify repr +r2443 | rgbecker | 2004-09-22 09:57:45 +0100 (Wed, 22 Sep 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + flowables.py: remove 2.1 incompatibility +r2442 | rgbecker | 2004-09-20 17:31:22 +0100 (Mon, 20 Sep 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/test/test_platypus_pto.py + platypus/test: working pto (& inner pto) +r2441 | rgbecker | 2004-09-17 11:46:56 +0100 (Fri, 17 Sep 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + renderPM.py: backward compatibility fix for _renderPM version <=0.98 +r2440 | rgbecker | 2004-09-15 10:40:03 +0100 (Wed, 15 Sep 2004) + M /reportlab/trunk/reportlab/setup.py + setup.py: fix get_version using patch from michael@stroeder.com +r2439 | rgbecker | 2004-09-14 12:08:13 +0100 (Tue, 14 Sep 2004) + M /reportlab/trunk/reportlab/platypus/__init__.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/tables.py + A /reportlab/trunk/reportlab/test/test_platypus_pto.py + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + platypus: added ptocontainer, moved tables test +r2438 | rgbecker | 2004-09-13 09:41:41 +0100 (Mon, 13 Sep 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + test_platypus_general.py: remove another 2.1 scope problem +r2437 | rgbecker | 2004-09-12 11:06:45 +0100 (Sun, 12 Sep 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + test_platypus_general.py: eliminate 2.1 scope problem +r2436 | rgbecker | 2004-09-11 15:18:33 +0100 (Sat, 11 Sep 2004) + M /reportlab/trunk/reportlab/lib/randomtext.py + randomtext.py: robustify textwrap import +r2435 | rgbecker | 2004-09-10 10:54:15 +0100 (Fri, 10 Sep 2004) + M /reportlab/trunk/reportlab/lib/randomtext.py + randomtext.py: fix argument handling +r2434 | rgbecker | 2004-09-09 18:42:18 +0100 (Thu, 09 Sep 2004) + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/rl_config.py + move _FUZZ to rl_config +r2433 | rgbecker | 2004-09-09 18:01:57 +0100 (Thu, 09 Sep 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc.py: improve image masking given by Colors +r2432 | rgbecker | 2004-09-09 12:56:24 +0100 (Thu, 09 Sep 2004) + M /reportlab/trunk/reportlab/lib/randomtext.py + randomtext.py: some improvements +r2431 | rgbecker | 2004-09-08 18:41:10 +0100 (Wed, 08 Sep 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + flowables.py: minor change to PTOContainer +r2430 | rgbecker | 2004-09-08 18:37:26 +0100 (Wed, 08 Sep 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/frames.py + platypus: almost finished PTOContainer +r2429 | rgbecker | 2004-09-08 12:02:58 +0100 (Wed, 08 Sep 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + tables.py: implement Eric Stephen's border drawing suggestion +r2428 | rgbecker | 2004-09-08 11:47:55 +0100 (Wed, 08 Sep 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + test_platypus_general.py: fix so all output seen +r2427 | rgbecker | 2004-09-03 10:53:17 +0100 (Fri, 03 Sep 2004) + M /reportlab/trunk/rl_addons/pyRXP/pyRXP.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/xmlparser.c + pyRXP: fix buf bug in xmlparser.c -->1.05 +r2426 | rgbecker | 2004-09-02 12:52:56 +0100 (Thu, 02 Sep 2004) + M /reportlab/trunk/reportlab/platypus/xpreformatted.py + XPreformatted: make __init__ lead args same as for paragraph +r2425 | rgbecker | 2004-08-27 17:08:32 +0100 (Fri, 27 Aug 2004) + M /reportlab/trunk/reportlab/graphics/charts/axes.py + axes.py: fix format bug +r2424 | rgbecker | 2004-08-25 15:38:48 +0100 (Wed, 25 Aug 2004) + M /reportlab/trunk/reportlab/platypus/paraparser.py + paraparser.py: minor speedup +r2423 | rgbecker | 2004-08-24 12:36:22 +0100 (Tue, 24 Aug 2004) + M /reportlab/trunk/reportlab/lib/sequencer.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/test/test_paragraphs.py + added seqchain/format tags +r2422 | rgbecker | 2004-08-23 16:54:25 +0100 (Mon, 23 Aug 2004) + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/test/test_paragraphs.py + fix backcolour bug +r2421 | rgbecker | 2004-08-20 17:12:33 +0100 (Fri, 20 Aug 2004) + M /reportlab/trunk/reportlab/platypus/__init__.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/test/test_paragraphs.py + minor twitching on Indenter +r2420 | rgbecker | 2004-08-19 23:17:05 +0100 (Thu, 19 Aug 2004) + M /reportlab/trunk/reportlab/lib/utils.py + reportlab/lib/utils.py: added flatten utility function +r2419 | rgbecker | 2004-08-19 10:58:05 +0100 (Thu, 19 Aug 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + _rl_accel.c: patch from Yves Bastide +r2418 | rgbecker | 2004-08-18 14:11:15 +0100 (Wed, 18 Aug 2004) + M /reportlab/trunk/reportlab/graphics/charts/textlabels.py + M /reportlab/trunk/reportlab/graphics/widgetbase.py + remove 2.2 isms +r2417 | rgbecker | 2004-08-18 12:17:54 +0100 (Wed, 18 Aug 2004) + M /reportlab/trunk/reportlab/graphics/charts/textlabels.py + textlabels.py: added keyword arguments +r2416 | rgbecker | 2004-08-17 14:52:21 +0100 (Tue, 17 Aug 2004) + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + lineplots.py: added filler handling +r2415 | rgbecker | 2004-08-07 16:00:28 +0100 (Sat, 07 Aug 2004) + M /reportlab/trunk/reportlab/graphics/renderPDF.py + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + M /reportlab/trunk/reportlab/graphics/renderSVG.py + M /reportlab/trunk/reportlab/graphics/renderbase.py + M /reportlab/trunk/reportlab/rl_config.py + rl_config add _unset_, graphics.renderxxx refactoring +r2414 | rgbecker | 2004-07-28 08:53:41 +0100 (Wed, 28 Jul 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + pdfdoc: fix rare bug (exposed in tinyrml) +r2413 | rgbecker | 2004-07-26 09:56:04 +0100 (Mon, 26 Jul 2004) + M /reportlab/trunk/rl_addons/pyRXP/rxp/system.h + Attempt to fix LONG_LONG defn problem +r2412 | rgbecker | 2004-07-23 17:57:11 +0100 (Fri, 23 Jul 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/rl_addons/renderPM/_renderPM.c + M /reportlab/trunk/rl_addons/renderPM/gt1/gt1-parset1.c + M /reportlab/trunk/rl_addons/renderPM/gt1/gt1-parset1.h + Add reader callback arg to _renderPM.makeT1Font +r2411 | rgbecker | 2004-07-23 14:48:21 +0100 (Fri, 23 Jul 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Minor fixes to lib.utils.rl_get_module +r2410 | rgbecker | 2004-07-23 10:43:00 +0100 (Fri, 23 Jul 2004) + M /reportlab/trunk/reportlab/test/test_pdfbase_ttfonts.py + test_pdfbase_ttfonts.py add multipage check +r2409 | rgbecker | 2004-07-22 11:59:29 +0100 (Thu, 22 Jul 2004) + M /reportlab/trunk/reportlab/lib/utils.py + reportlab.lib.utils fix rl_isdir +r2408 | rgbecker | 2004-07-17 20:43:44 +0100 (Sat, 17 Jul 2004) + M /reportlab/trunk/rl_addons/pyRXP/patches/differ.bat + D /reportlab/trunk/rl_addons/pyRXP/patches/rl-1.2.5.patch + A /reportlab/trunk/rl_addons/pyRXP/patches/rl-1.4.0-patch + M /reportlab/trunk/rl_addons/pyRXP/pyRXP.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/COPYRIGHT + M /reportlab/trunk/rl_addons/pyRXP/rxp/Makefile + M /reportlab/trunk/rl_addons/pyRXP/rxp/Manual + A /reportlab/trunk/rl_addons/pyRXP/rxp/catalog.c + A /reportlab/trunk/rl_addons/pyRXP/rxp/catalog.h + A /reportlab/trunk/rl_addons/pyRXP/rxp/catalog_dtd.c + A /reportlab/trunk/rl_addons/pyRXP/rxp/catutil.c + A /reportlab/trunk/rl_addons/pyRXP/rxp/catutil.h + M /reportlab/trunk/rl_addons/pyRXP/rxp/ctype16.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/ctype16.h + M /reportlab/trunk/rl_addons/pyRXP/rxp/dtd.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/dtd.h + A /reportlab/trunk/rl_addons/pyRXP/rxp/entityopener.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/infoset-print.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/input.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/input.h + M /reportlab/trunk/rl_addons/pyRXP/rxp/namespaces.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/namespaces.h + A /reportlab/trunk/rl_addons/pyRXP/rxp/nf16check.c + A /reportlab/trunk/rl_addons/pyRXP/rxp/nf16check.h + A /reportlab/trunk/rl_addons/pyRXP/rxp/nf16data.c + A /reportlab/trunk/rl_addons/pyRXP/rxp/nf16data.h + A /reportlab/trunk/rl_addons/pyRXP/rxp/resolve.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/rxp.1 + M /reportlab/trunk/rl_addons/pyRXP/rxp/rxp.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/rxputil.h + M /reportlab/trunk/rl_addons/pyRXP/rxp/stdio16.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/system.h + M /reportlab/trunk/rl_addons/pyRXP/rxp/url.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/version.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/xmlparser.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/xmlparser.h + M /reportlab/trunk/rl_addons/pyRXP/setup.py + Synchronize with rxp-1.4.0 --> version 1.04 +r2407 | rgbecker | 2004-07-15 11:22:38 +0100 (Thu, 15 Jul 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Another know location +r2406 | rgbecker | 2004-07-09 17:29:34 +0100 (Fri, 09 Jul 2004) + M /reportlab/trunk/reportlab/rl_config.py + Add user home to search paths +r2405 | kirpal | 2004-07-09 15:34:52 +0100 (Fri, 09 Jul 2004) + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + changes to refrences of CVS +r2404 | rgbecker | 2004-07-02 14:37:32 +0100 (Fri, 02 Jul 2004) + A /reportlab/trunk/reportlab/tools/utils + A /reportlab/trunk/reportlab/tools/utils/add_bleed.py + Initial checkin +r2402 | kirpal | 2004-06-29 14:28:59 +0100 (Tue, 29 Jun 2004) + D /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml + moved to users/kirpals +r2401 | kirpal | 2004-06-29 13:04:15 +0100 (Tue, 29 Jun 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml + changes to presentation +r2400 | kirpal | 2004-06-29 10:52:08 +0100 (Tue, 29 Jun 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml +r2399 | kirpal | 2004-06-29 10:37:59 +0100 (Tue, 29 Jun 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml + modified presentation +r2398 | kirpal | 2004-06-29 10:19:21 +0100 (Tue, 29 Jun 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml + modified presentation +r2397 | kirpal | 2004-06-28 17:09:41 +0100 (Mon, 28 Jun 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml + changes to presentation +r2396 | kirpal | 2004-06-28 15:40:16 +0100 (Mon, 28 Jun 2004) + A /reportlab/trunk/reportlab/tools/pythonpoint/demos/confKit.xml + python point presentation for Confkit +r2395 | rgbecker | 2004-06-25 19:10:50 +0100 (Fri, 25 Jun 2004) + M /reportlab/trunk/reportlab/demos/stdfonts/stdfonts.py + M /reportlab/trunk/reportlab/graphics/renderPDF.py + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/lib/validators.py + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + D /reportlab/trunk/reportlab/pdfbase/rl_codecs.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/rl_config.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + D /reportlab/trunk/reportlab/test/test_platypus_paraparser.py + Removed unicode changes +r2393 | rgbecker | 2004-06-23 15:56:24 +0100 (Wed, 23 Jun 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Fix 2.2 mktemp argument mismatch +r2392 | rgbecker | 2004-06-23 14:56:44 +0100 (Wed, 23 Jun 2004) + M /reportlab/trunk/reportlab/lib/utils.py + M /reportlab/trunk/reportlab/pdfbase/_fontdata.py + M /reportlab/trunk/reportlab/rl_config.py + More changes related to compact distro path recognition +r2391 | rgbecker | 2004-06-23 09:52:43 +0100 (Wed, 23 Jun 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + Changes to bruteforce search, non-invasive changes to rl_accel stuff +r2390 | rgbecker | 2004-06-23 09:49:10 +0100 (Wed, 23 Jun 2004) + M /reportlab/trunk/reportlab/lib/codecharts.py + Fix for unicode difficulties +r2389 | rgbecker | 2004-06-22 18:00:41 +0100 (Tue, 22 Jun 2004) + A /reportlab/trunk/reportlab/pdfbase/rl_codecs.py + First try at Adobe T1 encodings +r2388 | rgbecker | 2004-06-21 15:07:46 +0100 (Mon, 21 Jun 2004) + M /reportlab/trunk/reportlab/demos/stdfonts/stdfonts.py + Attempt to fix up for unicode +r2387 | jjlee | 2004-06-18 11:39:08 +0100 (Fri, 18 Jun 2004) + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + Fixed zero-length string case in stringWidth +r2386 | rgbecker | 2004-06-17 16:26:26 +0100 (Thu, 17 Jun 2004) + M /reportlab/trunk/rl_addons/pyRXP/pyRXP.c + M /reportlab/trunk/rl_addons/pyRXP/rxp/url.h + M /reportlab/trunk/rl_addons/pyRXP/setup.py + Remove $Header:, fix CopyRight & history +r2385 | rgbecker | 2004-06-17 16:26:05 +0100 (Thu, 17 Jun 2004) + M /reportlab/trunk/reportlab/__init__.py + M /reportlab/trunk/reportlab/demos/colors/colortest.py + M /reportlab/trunk/reportlab/demos/gadflypaper/gfe.py + M /reportlab/trunk/reportlab/demos/odyssey/dodyssey.py + M /reportlab/trunk/reportlab/demos/odyssey/fodyssey.py + M /reportlab/trunk/reportlab/demos/odyssey/odyssey.py + M /reportlab/trunk/reportlab/demos/rlzope/rlzope.py + M /reportlab/trunk/reportlab/demos/stdfonts/stdfonts.py + M /reportlab/trunk/reportlab/demos/tests/testdemos.py + M /reportlab/trunk/reportlab/docs/graphguide/ch1_intro.py + M /reportlab/trunk/reportlab/docs/graphguide/ch2_concepts.py + M /reportlab/trunk/reportlab/docs/graphguide/ch3_shapes.py + M /reportlab/trunk/reportlab/docs/graphguide/ch4_widgets.py + M /reportlab/trunk/reportlab/docs/graphguide/ch5_charts.py + M /reportlab/trunk/reportlab/docs/graphguide/gengraphguide.py + M /reportlab/trunk/reportlab/docs/reference/genreference.py + M /reportlab/trunk/reportlab/docs/userguide/app_demos.py + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + M /reportlab/trunk/reportlab/docs/userguide/ch2_graphics.py + M /reportlab/trunk/reportlab/docs/userguide/ch2a_fonts.py + M /reportlab/trunk/reportlab/docs/userguide/ch3_pdffeatures.py + M /reportlab/trunk/reportlab/docs/userguide/ch4_platypus_concepts.py + M /reportlab/trunk/reportlab/docs/userguide/ch5_paragraphs.py + M /reportlab/trunk/reportlab/docs/userguide/ch6_tables.py + M /reportlab/trunk/reportlab/docs/userguide/ch7_custom.py + M /reportlab/trunk/reportlab/docs/userguide/ch9_future.py + M /reportlab/trunk/reportlab/docs/userguide/genuserguide.py + M /reportlab/trunk/reportlab/extensions/__init__.py + M /reportlab/trunk/reportlab/graphics/__init__.py + M /reportlab/trunk/reportlab/graphics/charts/__init__.py + M /reportlab/trunk/reportlab/graphics/charts/areas.py + M /reportlab/trunk/reportlab/graphics/charts/axes.py + M /reportlab/trunk/reportlab/graphics/charts/barcharts.py + M /reportlab/trunk/reportlab/graphics/charts/doughnut.py + M /reportlab/trunk/reportlab/graphics/charts/legends.py + M /reportlab/trunk/reportlab/graphics/charts/linecharts.py + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + M /reportlab/trunk/reportlab/graphics/charts/markers.py + M /reportlab/trunk/reportlab/graphics/charts/piecharts.py + M /reportlab/trunk/reportlab/graphics/charts/spider.py + M /reportlab/trunk/reportlab/graphics/charts/textlabels.py + M /reportlab/trunk/reportlab/graphics/charts/utils.py + M /reportlab/trunk/reportlab/graphics/renderPDF.py + M /reportlab/trunk/reportlab/graphics/renderPM.py + M /reportlab/trunk/reportlab/graphics/renderPS.py + M /reportlab/trunk/reportlab/graphics/renderbase.py + M /reportlab/trunk/reportlab/graphics/samples/__init__.py + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/graphics/testdrawings.py + M /reportlab/trunk/reportlab/graphics/testshapes.py + M /reportlab/trunk/reportlab/graphics/widgetbase.py + M /reportlab/trunk/reportlab/graphics/widgets/__init__.py + M /reportlab/trunk/reportlab/graphics/widgets/eventcal.py + M /reportlab/trunk/reportlab/graphics/widgets/flags.py + M /reportlab/trunk/reportlab/graphics/widgets/grids.py + M /reportlab/trunk/reportlab/graphics/widgets/markers.py + M /reportlab/trunk/reportlab/graphics/widgets/signsandsymbols.py + M /reportlab/trunk/reportlab/lib/__init__.py + M /reportlab/trunk/reportlab/lib/_rl_accel.c + M /reportlab/trunk/reportlab/lib/abag.py + M /reportlab/trunk/reportlab/lib/attrmap.py + M /reportlab/trunk/reportlab/lib/codecharts.py + M /reportlab/trunk/reportlab/lib/colors.py + M /reportlab/trunk/reportlab/lib/corp.py + M /reportlab/trunk/reportlab/lib/enums.py + M /reportlab/trunk/reportlab/lib/extformat.py + M /reportlab/trunk/reportlab/lib/fonts.py + M /reportlab/trunk/reportlab/lib/formatters.py + M /reportlab/trunk/reportlab/lib/logger.py + M /reportlab/trunk/reportlab/lib/pagesizes.py + M /reportlab/trunk/reportlab/lib/randomtext.py + M /reportlab/trunk/reportlab/lib/sequencer.py + M /reportlab/trunk/reportlab/lib/set_ops.py + M /reportlab/trunk/reportlab/lib/setup.py + M /reportlab/trunk/reportlab/lib/styles.py + M /reportlab/trunk/reportlab/lib/tocindex.py + M /reportlab/trunk/reportlab/lib/units.py + M /reportlab/trunk/reportlab/lib/utils.py + M /reportlab/trunk/reportlab/lib/validators.py + M /reportlab/trunk/reportlab/lib/yaml.py + M /reportlab/trunk/reportlab/pdfbase/__init__.py + M /reportlab/trunk/reportlab/pdfbase/_cidfontdata.py + M /reportlab/trunk/reportlab/pdfbase/_fontdata.py + M /reportlab/trunk/reportlab/pdfbase/cidfonts.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/pdfbase/pdfutils.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/pdfgen/__init__.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/pathobject.py + M /reportlab/trunk/reportlab/pdfgen/pdfgeom.py + M /reportlab/trunk/reportlab/pdfgen/pdfimages.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/platypus/__init__.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + M /reportlab/trunk/reportlab/platypus/figures.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/platypus/tableofcontents.py + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/platypus/xpreformatted.py + M /reportlab/trunk/reportlab/rl_config.py + M /reportlab/trunk/reportlab/setup.py + M /reportlab/trunk/reportlab/test/__init__.py + M /reportlab/trunk/reportlab/test/runAll.py + M /reportlab/trunk/reportlab/test/test_charts_textlabels.py + M /reportlab/trunk/reportlab/test/test_docstrings.py + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/test/test_graphics_images.py + M /reportlab/trunk/reportlab/test/test_graphics_layout.py + M /reportlab/trunk/reportlab/test/test_graphics_speed.py + M /reportlab/trunk/reportlab/test/test_hello.py + M /reportlab/trunk/reportlab/test/test_images.py + M /reportlab/trunk/reportlab/test/test_invariant.py + M /reportlab/trunk/reportlab/test/test_lib_colors.py + M /reportlab/trunk/reportlab/test/test_lib_sequencer.py + M /reportlab/trunk/reportlab/test/test_multibyte_chs.py + M /reportlab/trunk/reportlab/test/test_multibyte_cht.py + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/test/test_paragraphs.py + M /reportlab/trunk/reportlab/test/test_pdfbase_pdfmetrics.py + M /reportlab/trunk/reportlab/test/test_pdfbase_pdfutils.py + M /reportlab/trunk/reportlab/test/test_pdfbase_postscript.py + M /reportlab/trunk/reportlab/test/test_pdfgen_callback.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pagemodes.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pycanvas.py + M /reportlab/trunk/reportlab/test/test_platypus_breaking.py + M /reportlab/trunk/reportlab/test/test_platypus_general.py + M /reportlab/trunk/reportlab/test/test_platypus_indents.py + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/test/test_platypus_paraparser.py + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/test/test_platypus_toc.py + M /reportlab/trunk/reportlab/test/test_platypus_xref.py + M /reportlab/trunk/reportlab/test/test_pyfiles.py + M /reportlab/trunk/reportlab/test/test_source_chars.py + M /reportlab/trunk/reportlab/test/test_widgetbase_tpc.py + M /reportlab/trunk/reportlab/tools/__init__.py + M /reportlab/trunk/reportlab/tools/docco/__init__.py + M /reportlab/trunk/reportlab/tools/docco/codegrab.py + M /reportlab/trunk/reportlab/tools/docco/docpy.py + M /reportlab/trunk/reportlab/tools/docco/examples.py + M /reportlab/trunk/reportlab/tools/docco/graphdocpy.py + M /reportlab/trunk/reportlab/tools/docco/rl_doc_utils.py + M /reportlab/trunk/reportlab/tools/docco/rltemplate.py + M /reportlab/trunk/reportlab/tools/docco/stylesheet.py + M /reportlab/trunk/reportlab/tools/docco/t_parse.py + M /reportlab/trunk/reportlab/tools/docco/yaml.py + M /reportlab/trunk/reportlab/tools/docco/yaml2pdf.py + M /reportlab/trunk/reportlab/tools/py2pdf/__init__.py + M /reportlab/trunk/reportlab/tools/py2pdf/idle_print.py + M /reportlab/trunk/reportlab/tools/pythonpoint/__init__.py + M /reportlab/trunk/reportlab/tools/pythonpoint/customshapes.py + M /reportlab/trunk/reportlab/tools/pythonpoint/styles/__init__.py + M /reportlab/trunk/reportlab/tools/pythonpoint/styles/horrible.py + M /reportlab/trunk/reportlab/tools/pythonpoint/styles/modern.py + Remove $Header:, fix CopyRight & history +r2384 | mike | 2004-06-17 15:50:51 +0100 (Thu, 17 Jun 2004) + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + Change back to native eol to fix line endings +r2383 | mike | 2004-06-17 15:44:11 +0100 (Thu, 17 Jun 2004) + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + Temporary change to binary to fix line endings +r2382 | rgbecker | 2004-06-17 15:23:21 +0100 (Thu, 17 Jun 2004) + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + Attempt to fix lineendings +r2381 | rgbecker | 2004-06-17 13:01:26 +0100 (Thu, 17 Jun 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + Fix encoding for zap & symbol +r2380 | rgbecker | 2004-06-16 12:45:56 +0100 (Wed, 16 Jun 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Forgot to check the exactly equal case for _startswith_rl +r2379 | rgbecker | 2004-06-15 14:10:37 +0100 (Tue, 15 Jun 2004) + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + Moved encoding checks into splitString +r2378 | rgbecker | 2004-06-15 12:34:47 +0100 (Tue, 15 Jun 2004) + M /reportlab/trunk/reportlab/test/test_platypus_paraparser.py + Fix properties and possibly line-endings +r2377 | rgbecker | 2004-06-15 11:33:32 +0100 (Tue, 15 Jun 2004) + M /reportlab/trunk/reportlab/graphics/charts/axes.py + remove use of False (invalid in 2.2) +r2376 | andy | 2004-06-14 23:13:04 +0100 (Mon, 14 Jun 2004) + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + fixed widths bug for tt fonts +r2375 | rgbecker | 2004-06-14 18:51:15 +0100 (Mon, 14 Jun 2004) + M /reportlab/trunk/reportlab/graphics/charts/axes.py +r2374 | andy | 2004-06-14 17:41:25 +0100 (Mon, 14 Jun 2004) + M /reportlab/trunk/reportlab/graphics/renderPDF.py + M /reportlab/trunk/reportlab/graphics/shapes.py + M /reportlab/trunk/reportlab/lib/validators.py + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/paragraph.py + M /reportlab/trunk/reportlab/platypus/paraparser.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + A /reportlab/trunk/reportlab/test/test_platypus_paraparser.py + Unicode and UTF8 support changes +r2373 | rgbecker | 2004-06-14 17:29:04 +0100 (Mon, 14 Jun 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + Fix import of logger +r2372 | mike | 2004-06-11 18:11:22 +0100 (Fri, 11 Jun 2004) + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + Added more accurate checking of UTF8 output +r2371 | mike | 2004-06-11 17:15:45 +0100 (Fri, 11 Jun 2004) + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + Made some corrections and got all TextEncodingTestCase tests working +r2370 | mike | 2004-06-11 16:25:50 +0100 (Fri, 11 Jun 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + Added support for mac_roman encoding and changed to always convert unicode strings to font encoding +r2369 | robin | 2004-06-11 11:17:18 +0100 (Fri, 11 Jun 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + Early 2.x has no UnicodeDecodeError and no ''.decode +r2368 | andy_robinson | 2004-06-10 01:43:21 +0100 (Thu, 10 Jun 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + Now has runnable encoding-conversion +r2367 | rgbecker | 2004-06-07 17:30:37 +0100 (Mon, 07 Jun 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Add xpre example +r2366 | rgbecker | 2004-06-03 12:26:44 +0100 (Thu, 03 Jun 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Another rework of the open for read code +r2365 | rgbecker | 2004-06-03 11:38:25 +0100 (Thu, 03 Jun 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + Attempt to fix the file: form +r2364 | andy_robinson | 2004-05-29 00:41:18 +0100 (Sat, 29 May 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + M /reportlab/trunk/reportlab/platypus/figures.py + M /reportlab/trunk/reportlab/rl_config.py + M /reportlab/trunk/reportlab/tools/pythonpoint/pythonpoint.dtd + M /reportlab/trunk/reportlab/tools/pythonpoint/pythonpoint.py + M /reportlab/trunk/reportlab/tools/pythonpoint/stdparser.py + Prelim support for unicode conversion (disabled by + rl_config.autoConvertEncodings at first); + borders to drawings in pythonpoint and for figure + objects; +r2363 | rgbecker | 2004-05-28 15:39:42 +0100 (Fri, 28 May 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + Attempt to test http urls for images +r2362 | rgbecker | 2004-05-28 15:04:25 +0100 (Fri, 28 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Attempt to fix open_for_read again +r2361 | rgbecker | 2004-05-28 15:02:14 +0100 (Fri, 28 May 2004) + M /reportlab/trunk/reportlab/test/test_lib_utils.py + Added open_and_read tests +r2360 | rgbecker | 2004-05-26 19:44:55 +0100 (Wed, 26 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + More compact distro changes +r2359 | jjlee | 2004-05-26 10:37:07 +0100 (Wed, 26 May 2004) + M /reportlab/trunk/reportlab/graphics/charts/lineplots.py + M /reportlab/trunk/reportlab/graphics/shapes.py + Make a couple of files emacs font-lock friendly +r2358 | rgbecker | 2004-05-25 22:12:31 +0100 (Tue, 25 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Use more advanced __loader__ friendly code +r2357 | rgbecker | 2004-05-25 17:33:56 +0100 (Tue, 25 May 2004) + M /reportlab/trunk/reportlab/docs/reference/genreference.py + Fix missing module +r2356 | rgbecker | 2004-05-24 13:19:53 +0100 (Mon, 24 May 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + Attempt to fix 1.5 problems +r2355 | rgbecker | 2004-05-23 11:39:30 +0100 (Sun, 23 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + rl_get_module now seems to work as intended +r2354 | rgbecker | 2004-05-23 10:27:52 +0100 (Sun, 23 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + rl_get_module added +r2353 | rgbecker | 2004-05-20 11:10:27 +0100 (Thu, 20 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Attempt to robustify the memo +r2352 | rgbecker | 2004-05-18 18:16:55 +0100 (Tue, 18 May 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Fix proposed by Derik Barclay +r2351 | rgbecker | 2004-05-08 18:19:54 +0100 (Sat, 08 May 2004) + M /reportlab/trunk/reportlab/tools/docco/docpy.py + M /reportlab/trunk/reportlab/tools/docco/graphdocpy.py + D /reportlab/trunk/reportlab/tools/docco/inspect.py + Removed private version of inspect +r2350 | rgbecker | 2004-05-07 17:56:29 +0100 (Fri, 07 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added some more stuff and script name to DebugMemo output +r2349 | rgbecker | 2004-05-07 16:28:33 +0100 (Fri, 07 May 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added capture_traceback=1 to DebugMemo init +r2348 | rgbecker | 2004-05-03 09:13:37 +0100 (Mon, 03 May 2004) + M /reportlab/trunk/reportlab/graphics/widgets/grids.py + Make stripe strokeColor same as fill +r2347 | rgbecker | 2004-05-03 09:11:48 +0100 (Mon, 03 May 2004) + M /reportlab/trunk/reportlab/test/test_docstrings.py + Minor changes +r2346 | rgbecker | 2004-05-03 09:09:52 +0100 (Mon, 03 May 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + Fix flagged and penalty values +r2345 | rgbecker | 2004-04-28 16:27:31 +0100 (Wed, 28 Apr 2004) + M /reportlab/trunk/reportlab/tools/docco/graphdocpy.py + Move to using inspect for getsource (actually works) +r2344 | rgbecker | 2004-04-28 15:41:56 +0100 (Wed, 28 Apr 2004) + M /reportlab/trunk/reportlab/tools/docco/rl_doc_utils.py + Added setStory +r2343 | rgbecker | 2004-04-28 15:40:21 +0100 (Wed, 28 Apr 2004) + M /reportlab/trunk/reportlab/docs/graphguide/gengraphguide.py + M /reportlab/trunk/reportlab/docs/reference/genreference.py + M /reportlab/trunk/reportlab/docs/userguide/ch1_intro.py + M /reportlab/trunk/reportlab/docs/userguide/ch2a_fonts.py + M /reportlab/trunk/reportlab/docs/userguide/ch6_tables.py + M /reportlab/trunk/reportlab/docs/userguide/genuserguide.py + Fixing up to not use folder relative import +r2342 | rgbecker | 2004-04-22 18:36:41 +0100 (Thu, 22 Apr 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Attempt to fix the Mike Spanning example +r2341 | rgbecker | 2004-04-21 17:39:36 +0100 (Wed, 21 Apr 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Added Mike's counter span example +r2340 | rgbecker | 2004-04-14 14:10:51 +0100 (Wed, 14 Apr 2004) + M /reportlab/trunk/reportlab/graphics/shapes.py + Fix problem with empty charts +r2339 | rgbecker | 2004-04-08 17:02:52 +0100 (Thu, 08 Apr 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + remove Box stuff prior to 2.3 +r2338 | rgbecker | 2004-04-08 15:17:42 +0100 (Thu, 08 Apr 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + removed PyDoc_STR for now; changed to do hex version checks +r2337 | rgbecker | 2004-04-05 19:07:42 +0100 (Mon, 05 Apr 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + M /reportlab/trunk/reportlab/platypus/tables.py + Minor changes +r2336 | rgbecker | 2004-04-05 15:17:29 +0100 (Mon, 05 Apr 2004) + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + Another attempt to fix up wanrings +r2335 | rgbecker | 2004-04-05 13:59:56 +0100 (Mon, 05 Apr 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + Forgot to bump the version +r2334 | rgbecker | 2004-04-05 08:47:18 +0100 (Mon, 05 Apr 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + Added in BoxList, removed AttrDict +r2333 | rgbecker | 2004-04-01 18:00:33 +0100 (Thu, 01 Apr 2004) + M /reportlab/trunk/reportlab/lib/extformat.py + Added header lines +r2332 | rgbecker | 2004-04-01 17:59:22 +0100 (Thu, 01 Apr 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added get_rl_tempfile +r2331 | rgbecker | 2004-04-01 17:58:02 +0100 (Thu, 01 Apr 2004) + A /reportlab/trunk/reportlab/lib/extformat.py + Added for better formatting +r2330 | rgbecker | 2004-04-01 12:17:02 +0100 (Thu, 01 Apr 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Ensure _colWidths is defined +r2329 | rgbecker | 2004-03-30 15:58:35 +0100 (Tue, 30 Mar 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + Added HRFlowable +r2328 | rgbecker | 2004-03-27 09:42:24 +0000 (Sat, 27 Mar 2004) + M /reportlab/trunk/reportlab/test/runAll.py + Indicate output folder +r2327 | rgbecker | 2004-03-27 09:41:37 +0000 (Sat, 27 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + Ensure external mask gets through in a85 cases +r2326 | rgbecker | 2004-03-26 21:34:03 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + M /reportlab/trunk/reportlab/pdfbase/pdfutils.py + Fix bug introduced during transparency/ImageReader changes +r2325 | rgbecker | 2004-03-26 16:54:37 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/test/test_tools_pythonpoint.py + Fix buglet in compact testing +r2324 | rgbecker | 2004-03-26 15:44:24 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Added percentages +r2323 | rgbecker | 2004-03-26 14:27:24 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/test/test_pyfiles.py + Move to having all test output in a temp folder +r2322 | rgbecker | 2004-03-26 14:26:50 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/test/runAll.py + Add test_*.txt cleanup +r2321 | rgbecker | 2004-03-26 14:20:44 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/test/runAll.py + M /reportlab/trunk/reportlab/test/test_charts_textlabels.py + M /reportlab/trunk/reportlab/test/test_docstrings.py + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/test/test_graphics_images.py + M /reportlab/trunk/reportlab/test/test_graphics_speed.py + M /reportlab/trunk/reportlab/test/test_hello.py + M /reportlab/trunk/reportlab/test/test_invariant.py + M /reportlab/trunk/reportlab/test/test_lib_colors.py + M /reportlab/trunk/reportlab/test/test_multibyte_chs.py + M /reportlab/trunk/reportlab/test/test_multibyte_cht.py + M /reportlab/trunk/reportlab/test/test_multibyte_jpn.py + M /reportlab/trunk/reportlab/test/test_multibyte_kor.py + M /reportlab/trunk/reportlab/test/test_paragraphs.py + M /reportlab/trunk/reportlab/test/test_pdfbase_encodings.py + M /reportlab/trunk/reportlab/test/test_pdfbase_fontembed.py + M /reportlab/trunk/reportlab/test/test_pdfbase_pdfmetrics.py + M /reportlab/trunk/reportlab/test/test_pdfbase_postscript.py + M /reportlab/trunk/reportlab/test/test_pdfbase_ttfonts.py + M /reportlab/trunk/reportlab/test/test_pdfgen_callback.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + M /reportlab/trunk/reportlab/test/test_pdfgen_links.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pagemodes.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pycanvas.py + M /reportlab/trunk/reportlab/test/test_platypus_breaking.py + M /reportlab/trunk/reportlab/test/test_platypus_general.py + M /reportlab/trunk/reportlab/test/test_platypus_indents.py + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/test/test_platypus_tables.py + M /reportlab/trunk/reportlab/test/test_platypus_toc.py + M /reportlab/trunk/reportlab/test/test_platypus_xref.py + M /reportlab/trunk/reportlab/test/test_renderSVG.py + M /reportlab/trunk/reportlab/test/test_source_chars.py + M /reportlab/trunk/reportlab/test/test_table_layout.py + M /reportlab/trunk/reportlab/test/test_tools_pythonpoint.py + M /reportlab/trunk/reportlab/test/test_widgets_grids.py + M /reportlab/trunk/reportlab/test/utils.py + Move to having all test output in a temp folder +r2320 | rgbecker | 2004-03-26 11:37:09 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/test/test_platypus_indents.py + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + M /reportlab/trunk/reportlab/test/test_widgetbase_tpc.py + Remove unwanted import +r2319 | rgbecker | 2004-03-26 11:34:11 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/reportlab/test/test_charts_textlabels.py + M /reportlab/trunk/reportlab/test/test_graphics_charts.py + M /reportlab/trunk/reportlab/test/test_lib_utils.py + M /reportlab/trunk/reportlab/test/test_platypus_toc.py + Eliminate bad tempdir setting +r2318 | rgbecker | 2004-03-26 10:48:43 +0000 (Fri, 26 Mar 2004) + M /reportlab/trunk/rl_addons/pyRXP/pyRXP.c + Fix up bug introduced with new CB API +r2317 | rgbecker | 2004-03-24 17:17:05 +0000 (Wed, 24 Mar 2004) + M /reportlab/trunk/reportlab/tools/pythonpoint/pythonpoint.py + Add eoCB for compactdistro +r2316 | rgbecker | 2004-03-24 17:16:22 +0000 (Wed, 24 Mar 2004) + M /reportlab/trunk/reportlab/test/test_tools_pythonpoint.py + Fix test_tools_pythonpoint for compact distro +r2315 | rgbecker | 2004-03-24 17:14:53 +0000 (Wed, 24 Mar 2004) + M /reportlab/trunk/rl_addons/pyRXP/pyRXP.c + Allow eoCB to return (URI,contents) for compact distro +r2314 | rgbecker | 2004-03-24 14:04:51 +0000 (Wed, 24 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/cidfonts.py + Switch to using get_rl_tempdir +r2313 | rgbecker | 2004-03-24 14:03:31 +0000 (Wed, 24 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added get_rl_tempdir +r2312 | rgbecker | 2004-03-24 09:42:22 +0000 (Wed, 24 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + True/False aren't defined for Python < 2.3 +r2311 | rgbecker | 2004-03-23 18:57:33 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + M /reportlab/trunk/reportlab/test/utils.py + Remove things that python 2.1 fails on +r2310 | rgbecker | 2004-03-23 17:35:42 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + Compact distro friendly +r2309 | rgbecker | 2004-03-23 17:34:11 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added rl_glob +r2308 | rgbecker | 2004-03-23 17:32:20 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/rl_config.py + Use rl_isdir +r2307 | rgbecker | 2004-03-23 16:18:29 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + Now works in compact distro +r2306 | rgbecker | 2004-03-23 15:43:13 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_invariant.py + Now works in compact distro +r2305 | rgbecker | 2004-03-23 15:37:53 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + Fix for compact distro +r2304 | rgbecker | 2004-03-23 15:20:50 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + We prefer absolute RL DIR +r2303 | rgbecker | 2004-03-23 15:19:21 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_platypus_paragraphs.py + Now works in compact distro +r2302 | rgbecker | 2004-03-23 15:18:34 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/utils.py + Switch to using rl_isdir +r2301 | rgbecker | 2004-03-23 14:46:35 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/runAll.py + Allow specifying the pattern +r2300 | rgbecker | 2004-03-23 14:30:01 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added open_and_readlines +r2299 | rgbecker | 2004-03-23 14:28:47 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_lib_utils.py + M /reportlab/trunk/reportlab/test/test_pyfiles.py + Fix up for compact distro +r2298 | rgbecker | 2004-03-23 14:15:47 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_source_chars.py + Fix up for compact distro (leave Zapping alone) +r2297 | rgbecker | 2004-03-23 13:54:42 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Improvements for compact running +r2296 | rgbecker | 2004-03-23 13:50:40 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + Ensure runnable in compact distro +r2295 | rgbecker | 2004-03-23 12:55:44 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/test/test_lib_utils.py + Allow to run in a compact distro +r2294 | rgbecker | 2004-03-23 12:20:08 +0000 (Tue, 23 Mar 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + Make Image to use open_for_read +r2293 | rgbecker | 2004-03-22 18:09:51 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added rl_isfile +r2292 | rgbecker | 2004-03-22 18:08:50 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/test/runAll.py + M /reportlab/trunk/reportlab/test/test_images.py + M /reportlab/trunk/reportlab/test/test_pdfgen_pycanvas.py + M /reportlab/trunk/reportlab/test/test_platypus_general.py + Small changes to make tests work compactly +r2291 | rgbecker | 2004-03-22 15:59:22 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/test/utils.py + Remove spurious print +r2290 | rgbecker | 2004-03-22 14:06:12 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/platypus/flowables.py + Fix up more Image cases +r2289 | rgbecker | 2004-03-22 14:01:09 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/test/test_platypus_general.py + Add new Image flowable exemplars +r2288 | rgbecker | 2004-03-22 13:21:06 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/test/runAll.py + M /reportlab/trunk/reportlab/test/test_pyfiles.py + M /reportlab/trunk/reportlab/test/utils.py + Minor changes preparing for zip comapct distros +r2287 | rgbecker | 2004-03-22 13:02:43 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + Get the right fp +r2286 | rgbecker | 2004-03-22 12:54:30 +0000 (Mon, 22 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + Attempt to fix the epc bug +r2285 | andy_robinson | 2004-03-19 22:18:37 +0000 (Fri, 19 Mar 2004) + M /reportlab/trunk/reportlab/docs/userguide/ch4_platypus_concepts.py + Documentation about hAlign +r2284 | rgbecker | 2004-03-19 18:00:56 +0000 (Fri, 19 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + First attempt at reading from the zip archive +r2283 | rgbecker | 2004-03-19 10:55:55 +0000 (Fri, 19 Mar 2004) + M /reportlab/trunk/reportlab/rl_config.py + Sys_version was being used in older python +r2282 | rgbecker | 2004-03-18 15:55:50 +0000 (Thu, 18 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + M /reportlab/trunk/reportlab/pdfbase/pdfdoc.py + M /reportlab/trunk/reportlab/pdfbase/pdfutils.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/pdfgen/pdfimages.py + Fix transparency & jpeg behaviour +r2281 | rgbecker | 2004-03-18 12:05:22 +0000 (Thu, 18 Mar 2004) + M /reportlab/trunk/reportlab/pdfgen/textobject.py + TextObject fixes from Ian Millington +r2280 | rgbecker | 2004-03-17 18:54:06 +0000 (Wed, 17 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Move ImageReader open wrapper to where it should be +r2279 | rgbecker | 2004-03-17 16:29:30 +0000 (Wed, 17 Mar 2004) + M /reportlab/trunk/reportlab/demos/rlzope/rlzope.py + Attempt to fix to match ImageReader +r2278 | rgbecker | 2004-03-17 14:35:51 +0000 (Wed, 17 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Improve the fs distro check +r2277 | rgbecker | 2004-03-17 14:34:41 +0000 (Wed, 17 Mar 2004) + M /reportlab/trunk/reportlab/rl_config.py + Added rlhome/../fonts +r2276 | rgbecker | 2004-03-17 00:21:39 +0000 (Wed, 17 Mar 2004) + M /reportlab/trunk/reportlab/pdfgen/pdfimages.py + Remove spurious print reported by Joel Pearson +r2275 | rgbecker | 2004-03-16 15:28:26 +0000 (Tue, 16 Mar 2004) + M /reportlab/trunk/rl_addons/pyRXP/pyRXP.c + Fix BOM removal buglet & valid/ext-sa/014 +r2274 | andy_robinson | 2004-03-15 23:49:48 +0000 (Mon, 15 Mar 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + corrected erroneous comment +r2273 | rgbecker | 2004-03-14 09:47:14 +0000 (Sun, 14 Mar 2004) + M /reportlab/trunk/reportlab/platypus/tables.py + Slightly more efficient linedrawing +r2272 | andy_robinson | 2004-03-12 23:54:11 +0000 (Fri, 12 Mar 2004) + M /reportlab/trunk/reportlab/lib/corp.py + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/platypus/tables.py + M /reportlab/trunk/reportlab/test/test_pdfgen_general.py + Multiple lines and decimal alignments now supported +r2271 | rgbecker | 2004-03-11 10:57:59 +0000 (Thu, 11 Mar 2004) + M /reportlab/trunk/utils/README + Fix missing command instruction +r2270 | andy_robinson | 2004-03-09 23:24:34 +0000 (Tue, 09 Mar 2004) + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/pdfgen/textobject.py + Truetype fonts now convert latin-1 to utf8 if fed non-utf8 +r2269 | andy_robinson | 2004-03-09 22:22:26 +0000 (Tue, 09 Mar 2004) + M /reportlab/trunk/reportlab/__init__.py + M /reportlab/trunk/reportlab/lib/fonts.py + M /reportlab/trunk/reportlab/pdfbase/_fontdata.py + M /reportlab/trunk/reportlab/pdfbase/pdfmetrics.py + M /reportlab/trunk/reportlab/pdfbase/ttfonts.py + M /reportlab/trunk/reportlab/platypus/doctemplate.py + Font reregistration cleanup +r2268 | rgbecker | 2004-03-08 18:36:05 +0000 (Mon, 08 Mar 2004) + M /reportlab/trunk/reportlab/lib/utils.py + Added _findFiles & CIDict +r2267 | rgbecker | 2004-02-22 17:35:43 +0000 (Sun, 22 Feb 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + URL change from Aleksander Piotrowski +r2266 | rgbecker | 2004-02-17 16:10:18 +0000 (Tue, 17 Feb 2004) + M /reportlab/trunk/reportlab/platypus/frames.py + M /reportlab/trunk/reportlab/rl_config.py + Added overlapAttachedSpace +r2265 | andy_robinson | 2004-02-10 00:12:21 +0000 (Tue, 10 Feb 2004) + M /reportlab/trunk/reportlab/pdfgen/canvas.py + M /reportlab/trunk/reportlab/test/test_pdfbase_postscript.py + Added finegrained control of where postscript commands appear +r2264 | rgbecker | 2004-02-05 18:31:35 +0000 (Thu, 05 Feb 2004) + M /reportlab/trunk/reportlab/platypus/paragraph.py + Fix to from Ulrich Screiner +r2263 | rgbecker | 2004-02-05 18:21:50 +0000 (Thu, 05 Feb 2004) + M /reportlab/trunk/reportlab/pdfgen/pdfimages.py + Fix missing import found by Ulrich Schreiner +r2262 | rgbecker | 2004-02-02 08:43:51 +0000 (Mon, 02 Feb 2004) + M /reportlab/trunk/reportlab/lib/_rl_accel.c + Start on tex lib in C +r2261 | rgbecker | 2004-01-29 17:06:24 +0000 (Thu, 29 Jan 2004) + M /reportlab/trunk/reportlab/graphics/renderPS.py + Attempt to get delayed font setting and proper psNames +r2260 | rgbecker | 2004-01-28 13:03:47 +0000 (Wed, 28 Jan 2004) + M /reportlab/trunk/reportlab/graphics/testshapes.py + Allow for bad fonts in getDrawing13 +r2259 | rgbecker | 2004-01-28 13:03:19 +0000 (Wed, 28 Jan 2004) + M /reportlab/trunk/reportlab/graphics/renderPM.py + Allow for no eps preview in tests +r2258 | rgbecker | 2004-01-23 00:04:40 +0000 (Fri, 23 Jan 2004) + M /reportlab/trunk/rl_addons/pyRXP/rxp/url.c + Attempt to fix UNC opening +r2257 | rgbecker | 2004-01-21 18:04:05 +0000 (Wed, 21 Jan 2004) + M /reportlab/trunk/utils/README +################################################################################# +#################### RELEASE 1.19 at 18:00 GMT 21/Jan/2004 ################# +################################################################################# +##### 2004/01/21 ##### + pdfbase/ttfonts.py 1.18 rgbecker Improve error handling & naming +##### 2004/01/14 ##### + rl_addons/renderPM/pfm.py 1.3 rgbecker Changes for 2.3 compatibility +##### 2004/01/13 ##### + pdfgen/canvas.py 1.118 rgbecker Allow for possible restarts + platypus/doctemplate.py 1.71 rgbecker Allow for simple loop detection +##### 2004/01/10 ##### + platypus/figures.py 1.16 rgbecker Added optional memory caching +##### 2004/01/09 ##### + platypus/tables.py 1.71 rgbecker Modified version of Henning von Bargen's LongTables optimisation +##### 2004/01/08 ##### + platypus/doctemplate.py 1.70 andy_robinson Upped empty page limit to 10 +##### 2004/01/07 ##### + platypus/flowables.py 1.44 andy_robinson Candidate fix for infinite looping + platypus/doctemplate.py 1.69 andy_robinson Candidate fix for infinite looping + lib/codecharts.py 1.8 andy_robinson Candidate fix for infinite looping + platypus/figures.py 1.15 rgbecker Fix up caption and split methods etc + platypus/tables.py 1.70 dragan1 merging Jython-branch + platypus/tables.py 1.69 rgbecker Make _listCellGeom more specific + platypus/__init__.py 1.16.2.2 dragan1 added conditional import linee for jython compatilibity + platypus/figures.py 1.14 rgbecker Improved FlexFigure scaling, in memory PageCatcherFigures +##### 2004/01/05 ##### + rl_addons/renderPM/_renderPM.c 1.31 rgbecker Fix bad long field value +##### 2003/12/23 ##### + lib/_rl_accel.c 1.37 rgbecker Added hex32 +##### 2003/12/19 ##### + platypus/tables.py 1.68 rgbecker Added splitfirst/last handling +##### 2003/12/18 ##### + tools/pythonpoint/styles/standard.py 1.3.4.1 dragan1 Platypus import structure changes + tools/pythonpoint/styles/htu.py 1.1.4.1 dragan1 Platypus import structure changes + tools/pythonpoint/pythonpoint.py 1.27.2.1 dragan1 Platypus import structure changes + tools/pythonpoint/demos/examples.py 1.3.4.1 dragan1 Platypus import structure changes + tools/docco/yaml2pdf.py 1.2.4.1 dragan1 Platypus import structure changes + tools/docco/rltemplate.py 1.3.4.1 dragan1 Platypus import structure changes + tools/docco/rl_doc_utils.py 1.6.4.1 dragan1 Platypus import structure changes + tools/docco/examples.py 1.4.4.1 dragan1 Platypus import structure changes + test/test_table_layout.py 1.1.4.1 dragan1 Platypus import structure changes + test/test_rl_accel.py 1.3.2.1 dragan1 Platypus import structure changes + test/test_platypus_xref.py 1.3.4.1 dragan1 Platypus import structure changes + test/test_platypus_tables.py 1.3.4.1 dragan1 Platypus import structure changes + test/test_platypus_paragraphs.py 1.11.4.1 dragan1 Platypus import structure changes + test/test_platypus_indents.py 1.1.4.1 dragan1 Platypus import structure changes + test/test_platypus_general.py 1.13.2.1 dragan1 Platypus import structure changes + test/test_platypus_breaking.py 1.6.4.1 dragan1 Platypus import structure changes + test/test_paragraphs.py 1.17.2.1 dragan1 Platypus import structure changes + test/test_graphics_speed.py 1.12.4.1 dragan1 Platypus import structure changes + test/test_graphics_charts.py 1.14.4.1 dragan1 Platypus import structure changes + platypus/tables.py 1.67.2.1 dragan1 Platypus import structure changes + platypus/paraparser.py 1.53.2.1 dragan1 Platypus import structure changes + platypus/figures.py 1.13.2.1 dragan1 Platypus import structure changes + platypus/__init__.py 1.16.2.1 dragan1 Platypus import structure changes + lib/tocindex.py 1.10.2.1 dragan1 Platypus import structure changes + lib/codecharts.py 1.7.4.1 dragan1 Platypus import structure changes + graphics/shapes.py 1.100.2.1 dragan1 Platypus import structure changes + graphics/renderPDF.py 1.24.2.1 dragan1 Platypus import structure changes + docs/userguide/ch7_custom.py 1.4.4.1 dragan1 Platypus import structure changes + docs/userguide/ch6_tables.py 1.6.4.1 dragan1 Platypus import structure changes + docs/userguide/ch4_platypus_concepts.py 1.4.4.1 dragan1 Platypus import structure changes + docs/userguide/ch2a_fonts.py 1.10.2.1 dragan1 Platypus import structure changes + demos/rlzope/rlzope.py 1.5.4.1 dragan1 Platypus import structure changes + demos/odyssey/fodyssey.py 1.17.4.1 dragan1 Platypus import structure changes + demos/gadflypaper/gfe.py 1.15.4.1 dragan1 Platypus import structure changes +##### 2003/12/17 ##### + graphics/testshapes.py 1.20 rgbecker Attempt to control ttf usage + graphics/renderPM.py 1.44 rgbecker Attempt to control ttf usage + platypus/paragraph.py 1.71 rgbecker Fix missing return +##### 2003/12/16 ##### + platypus/paragraph.py 1.70 rgbecker Underline alignment fix from Marc Stober +##### 2003/12/15 ##### + test/test_lib_sequencer.py 1.7 dragan1 changed method call from this to _this - java keyword + lib/sequencer.py 1.14 dragan1 renamed methid this to _this - java keyword +##### 2003/12/14 ##### + tools/pythonpoint/styles/modern.py 1.4 andy_robinson Permits style file to live in local directory + tools/pythonpoint/stdparser.py 1.20 andy_robinson Permits style file to live in local directory +##### 2003/12/12 ##### + lib/rparsexml.py 1.6 rgbecker Allow for both to have the fake entityReplacer thing + rl_addons/pyRXP/examples/benchmarks.py 1.2 rgbecker -->reportlab.lib.rparsexml +##### 2003/12/11 ##### + lib/rparsexml.py 1.5 rgbecker Add some support for slices etc +################################################################################# +#################### RELEASE 1.08 at 12:00 BST 19/June/2001 ##################### +################################################################################# +##### 2001/06/19 ##### + test/test_graphics_speed.py 1.10 dinu_gherman + Renamed chart attribute names to bars/lines/slices. + test/test_graphics_charts.py 1.12 dinu_gherman + Renamed chart attribute names to bars/lines/slices. + graphics/charts/piecharts.py 1.17 dinu_gherman + Renamed chart attribute names to bars/lines/slices. + graphics/charts/lineplots.py 1.17 dinu_gherman + Renamed chart attribute names to bars/lines/slices. + graphics/charts/linecharts.py 1.12 dinu_gherman + Renamed chart attribute names to bars/lines/slices. + graphics/charts/barcharts.py 1.18 dinu_gherman + Renamed chart attribute names to bars/lines/slices. +##### 2001/06/18 ##### + docs/graphguide/ch2_graphics.py 1.21 rgbecker + Changed from defaultStyles to pieStyles + graphics/widgetbase.py 1.25 rgbecker + Added recur arg to allow non-recurring + graphics/shapes.py 1.32 rgbecker + Added recur arg to allow non-recurring + test/test_graphics_speed.py 1.9 dinu_gherman + Renamed defaultStyles attribute with barStyles, lineStyles, pieStyles. + test/test_graphics_charts.py 1.11 dinu_gherman + Renamed defaultStyles attribute with barStyles, lineStyles, pieStyles. + graphics/charts/piecharts.py 1.16 dinu_gherman + Renamed defaultStyles attribute with barStyles, lineStyles, pieStyles. + graphics/charts/lineplots.py 1.16 dinu_gherman + Renamed defaultStyles attribute with barStyles, lineStyles, pieStyles. + graphics/charts/linecharts.py 1.11 dinu_gherman + Renamed defaultStyles attribute with barStyles, lineStyles, pieStyles. + graphics/charts/barcharts.py 1.17 dinu_gherman + Renamed defaultStyles attribute with barStyles, lineStyles, pieStyles. +##### 2001/06/16 ##### + graphics/shapes.py 1.31 rgbecker + Added Null _DrawingEditorMixin class +##### 2001/06/15 ##### + pdfbase/pdfdoc.py 1.45 aaron_watters + added missing PDFText() function for consistency (needed by pagecatcher) +##### 2001/06/13 ##### + rl_addons/renderPM/setup.py 1.4 jvr + some changes to make it potentially work under MacOS with the CodeWarrior compiler. Unfortunately it still doesn't :-( + lib/setup.py 1.8 jvr + minor patch: building the sgmlop, _rl_accel and pyHnj now simply *works* with distutils under MacOS with codeWarrior. Good Stuff. (haven't added any install smartness yet) + graphics/renderPM.py 1.8 jvr + Test program: use os.path.join() for path manipulation instead of things like 'pmout%s%s'%(os.sep,filename) as this gets it wrong under MacOS. + rl_addons/renderPM/libart_lgpl/art_misc.c 1.2 jvr + extended #ifndef _WIN32 to also check for undefined-ness of 'macintosh' + rl_addons/renderPM/gt1/gt1-parset1.c 1.2 jvr + change to make it compile under MacOS: added strdup() hack for Mac + rl_addons/renderPM/_renderPM.c 1.3 jvr + changes to make it compile under MacOS: - added strdup() hack for Mac - changed size_t fields in gstateObject to int -- otherwise I'd have to add casts to each and every bpath_add_point() call (can't implicitly go from size_t to int) - repaired "illegal constant expression" in gstate like Robin suggested + lib/_rl_accel.c 1.18 rgbecker + Just's Mac patch + graphics/widgetbase.py 1.24 andy_robinson + Changes to support customer project: allow nulls in bar charts, and added some missing but obvious properties e.g. strokeColor for bar and legend borders. + graphics/charts/piecharts.py 1.15 andy_robinson + Changes to support customer project: allow nulls in bar charts, and added some missing but obvious properties e.g. strokeColor for bar and legend borders. + graphics/charts/legends.py 1.9 andy_robinson + Changes to support customer project: allow nulls in bar charts, and added some missing but obvious properties e.g. strokeColor for bar and legend borders. + graphics/charts/barcharts.py 1.16 andy_robinson + Changes to support customer project: allow nulls in bar charts, and added some missing but obvious properties e.g. strokeColor for bar and legend borders. + graphics/charts/axes.py 1.27 andy_robinson + Changes to support customer project: allow nulls in bar charts, and added some missing but obvious properties e.g. strokeColor for bar and legend borders. +##### 2001/06/11 ##### + pdfgen/canvas.py 1.80 rgbecker + Made canvas default to rl_config.pageCompression + rl_config.py 1.9 rgbecker + Added pageCompression + graphics/renderPDF.py 1.9 andy_robinson + Added autoSize option to rendderPDF, and allowed "no border" around bars in bar charts. To support QIR project. + graphics/charts/barcharts.py 1.15 andy_robinson + Added autoSize option to rendderPDF, and allowed "no border" around bars in bar charts. To support QIR project. +##### 2001/06/09 ##### + platypus/frames.py 1.13 rgbecker + added _FUZZ and changed frame add test +##### 2001/06/07 ##### + platypus/paragraph.py 1.55 rgbecker + Remove explicit £ character to stop Dinu moaning + test/test_pdfgen_pagemodes.py 1.5 rgbecker + Try to shorten filenames + test/test_widgetbase_tpc.py 1.1 rgbecker + Shortened names + test/test_graphics_widgetbase_tpc.py 1.2 rgbecker + Shortened names + test/test_graphics_charts_textlabels.py 1.4 rgbecker + Shortened names + test/test_charts_textlabels.py 1.1 rgbecker + Shortened names + lib/utils.py 1.16 rgbecker + Imported PIL_WARNINGS + graphics/widgetbase.py 1.23 rgbecker + Allow TypedPropertyCollection exemplar class to have getattr etc +##### 2001/06/06 ##### + graphics/widgetbase.py 1.22 rgbecker + Fbot's suggested fix + lib/setup.py 1.7 rgbecker + Fixed up MovePYDs + rl_addons/renderPM/setup.py 1.3 rgbecker + Fixed up MovePYDs + rl_addons/renderPM/_renderPM.c 1.2 rgbecker + Niki Spahiev's PyMem_New patch for 1.5.2 +##### 2001/05/30 ##### + test/test_pdfbase_pdfmetrics.py 1.7 rgbecker + Fixes for 2.1 whining + test/runAll.py 1.6 rgbecker + Fixes for 2.1 whining + rl_addons/renderPM/setup.py 1.2 rgbecker + Moved pyds to DLLs + lib/setup.py 1.6 rgbecker + Moved pyds to DLLs +##### 2001/05/29 ##### + docs/graphguide/ch2_graphics.py 1.20 johnprecedo + A number of small changes made - mainly changing import statements in the examples so that they actually work as given without producing an errors. Done up to page 15. + pdfgen/canvas.py 1.79 rgbecker + Fix the 2.1 _escape fix + rl_config.py 1.8 rgbecker + Changes for 2.1 escape fix + pdfgen/canvas.py 1.78 rgbecker + Changes for 2.1 escape fix + lib/_rl_accel.c 1.17 rgbecker + Changes for 2.1 escape fix + test/test_pyfiles.py 1.6 dinu_gherman + Added a test for first lines containing #!...python... +##### 2001/05/28 ##### + platypus/paragraph.py 1.54 rgbecker + Add some pound signs to test 5 +##### 2001/05/26 ##### + utils/daily.py 1.43 rgbecker + Fixed precedence + utils/daily.py 1.42 rgbecker + Fixed error print' daily.py +##### 2001/05/25 ##### + graphics/charts/legends.py 1.8 dinu_gherman + Added font attributes for text in legends. + lib/_rl_accel.c 1.16 rgbecker + Fix special case ',' + lib/_rl_accel.c 1.15 rgbecker + Added fix for comma decimal point + lib/utils.py 1.15 rgbecker + Attempt to fix the locale mismatch problem + lib/graphdocpy.py 1.8 rgbecker + removed use of renderGD + pdfgen/pathobject.py 1.9 rgbecker + Changed a few %f formats to use %s and fp_str + pdfgen/canvas.py 1.77 rgbecker + Changed a few %f formats to use %s and fp_str +##### 2001/05/23 ##### + graphics/charts/textlabels.py 1.7 rgbecker + Standardize nattribute names + graphics/renderPS.py 1.7 rgbecker + Synchronize backend to PDF standard + graphics/renderPM.py 1.7 rgbecker + Synchronize backend to PDF standard + graphics/renderPDF.py 1.8 rgbecker + Synchronize backend to PDF standard + graphics/widgetbase.py 1.21 rgbecker + Correct names and factorise some behaviour + graphics/shapes.py 1.30 rgbecker + Correct names and factorise some behaviour +##### 2001/05/22 ##### + graphics/charts/barcharts.py 1.14 dinu_gherman + Made strokeColor attribute for the bars be really used. + graphics/charts/textlabels.py 1.6 dinu_gherman + Added missing fillColor attribute for the string itself. + graphics/charts/piecharts.py 1.14 rgbecker + Fixes to piechart label handling + lib/__BUILD.dsw 1.4 rgbecker + Indicated usage of setup.py + lib/README.extensions 1.3 rgbecker + Indicated usage of setup.py +##### 2001/05/21 ##### + graphics/widgetbase.py 1.20 rgbecker + Fixed _ItemWrapper name and made it really cache + graphics/widgetbase.py 1.19 rgbecker + Improved element wrapping seems to work + test/test_graphics_widgetbase_tpc.py 1.1 dinu_gherman + Initial checkin. + graphics/widgetbase.py 1.18 rgbecker + Added element wrapper and fixed getitem so collections work better + test/test_docstrings.py 1.7 dinu_gherman + Substantial rewrite. +##### 2001/05/20 ##### + pdfbase/_fontdata.py 1.10 rgbecker + Fix typo + rl_config.py 1.7 rgbecker + Better Font handling for linux2 + pdfbase/_fontdata.py 1.9 rgbecker + Better Font handling for linux2 + rl_config.py 1.6 rgbecker + Font handling patches for linux2 from L Catucci + pdfbase/_fontdata.py 1.8 rgbecker + Font handling patches for linux2 from L Catucci + test/unittest.py 1.4 rgbecker + Fix linesep for non-JPython + rl_config.py 1.5 rgbecker + Added PIL_WARNINGS/ZLIB_WARNINGS + lib/utils.py 1.14 rgbecker + Added PIL_WARNINGS/ZLIB_WARNINGS +##### 2001/05/18 ##### + test/test_docstrings.py 1.6 dinu_gherman + Applied Robin's patch. + graphics/testshapes.py 1.12 rgbecker + Added locals & globals to the eval + graphics/charts/piecharts.py 1.13 rgbecker + Need some real strokeWidths + rl_addons/renderPM/pfm.py 1.2 rgbecker + Remove above 127 characters + test/runAll.py 1.5 dinu_gherman + Sorted order in which test module are executed (using sort()). + test/test_docstrings.py 1.5 dinu_gherman + Fixed bug where files got written everywhere by using new SecureTestCase. + test/utils.py 1.1 dinu_gherman + Initial checkin. + platypus/paragraph.py 1.53 rgbecker + added minWidth method to Flowable, Paragraph + platypus/flowables.py 1.19 rgbecker + added minWidth method to Flowable, Paragraph + graphics/widgetbase.py 1.17 rgbecker + Make setVector more friendly + graphics/shapes.py 1.29 rgbecker + Fixed expandUserNode method + graphics/widgetbase.py 1.16 rgbecker + Added TypedPropertyCollection.setVector +##### 2001/05/17 ##### + docs/graphguide/ch2_graphics.py 1.19 rgbecker + Pies don't seem to have a wedges collection any longer + graphics/shapes.py 1.28 rgbecker + Use canned routine validateSetattr + lib/validators.py 1.6 rgbecker + New AttrMap emplacement + lib/attrmap.py 1.2 rgbecker + New AttrMap emplacement + graphics/widgets/signsandsymbols.py 1.12 rgbecker + New AttrMap emplacement + graphics/widgets/flags.py 1.5 rgbecker + New AttrMap emplacement + graphics/charts/textlabels.py 1.5 rgbecker + New AttrMap emplacement + graphics/charts/piecharts.py 1.12 rgbecker + New AttrMap emplacement + graphics/charts/lineplots.py 1.15 rgbecker + New AttrMap emplacement + graphics/charts/linecharts.py 1.10 rgbecker + New AttrMap emplacement + graphics/charts/legends.py 1.7 rgbecker + New AttrMap emplacement + graphics/charts/barcharts.py 1.13 rgbecker + New AttrMap emplacement + graphics/charts/axes.py 1.26 rgbecker + New AttrMap emplacement + graphics/widgetbase.py 1.15 rgbecker + New AttrMap emplacement + graphics/shapes.py 1.27 rgbecker + New AttrMap emplacement + graphics/shapes.py 1.26 rgbecker + Removal of the Auto Thing + graphics/charts/linecharts.py 1.9 rgbecker + Removal of the Auto Thing + graphics/charts/barcharts.py 1.12 rgbecker + Removal of the Auto Thing + graphics/charts/axes.py 1.25 rgbecker + Removal of the Auto Thing + docs/graphguide/ch2_graphics.py 1.18 rgbecker + Removal of the Auto Thing + lib/attrmap.py 1.1 rgbecker + Initial version + lib/validators.py 1.5 rgbecker + New Scheme Validators + test/test_lib_validators.py 1.4 rgbecker + New Scheme Validators +##### 2001/05/16 ##### + graphics/renderPM.py 1.6 rgbecker + Added drawToString + test/test_graphics_charts.py 1.10 rgbecker + Renamed Legend0 to Legend, fixed _attrmap typo + graphics/charts/legends.py 1.6 rgbecker + Renamed Legend0 to Legend, fixed _attrmap typo +##### 2001/05/15 ##### + graphics/charts/lineplots.py 1.14 dinu_gherman + Added strokeDashArray attribute to LinePlotProperties class. + test/test_lib_validators.py 1.3 dinu_gherman + Removed isNumberOrAuto validator. + lib/validators.py 1.4 dinu_gherman + Removed isNumberOrAuto validator. + graphics/charts/axes.py 1.24 dinu_gherman + Removed isNumberOrAuto validator. + graphics/widgetbase.py 1.14 dinu_gherman + Adapted to use Validator instances in validators.py. + graphics/shapes.py 1.25 dinu_gherman + Adapted to use Validator instances in validators.py. + graphics/testshapes.py 1.11 rgbecker + Improvements to testshapse.py + graphics/renderPM.py 1.5 rgbecker + Improvements to testshapse.py + platypus/paragraph.py 1.52 rgbecker + Added find to imports + test/test_paragraphs.py 1.10 rgbecker + Added in line font changes +##### 2001/05/11 ##### + platypus/doctemplate.py 1.40 rgbecker + Dynamic page sizes + lib/graphdocpy.py 1.7 dinu_gherman + Fixed buglet. + graphics/widgets/signsandsymbols.py 1.11 dinu_gherman + Adapted to using vlaidator classes. + graphics/widgets/flags.py 1.4 dinu_gherman + Adapted to using vlaidator classes. + lib/graphdocpy.py 1.6 dinu_gherman + Added display of documented public widget attributes. + graphics/charts/textlabels.py 1.4 dinu_gherman + Adapted to using validator classes in attribute maps. + graphics/charts/piecharts.py 1.11 dinu_gherman + Adapted to using validator classes in attribute maps. + graphics/charts/lineplots.py 1.13 dinu_gherman + Adapted to using validator classes in attribute maps. + graphics/charts/linecharts.py 1.8 dinu_gherman + Adapted to using validator classes in attribute maps. + graphics/charts/legends.py 1.5 dinu_gherman + Adapted to using validator classes in attribute maps. + graphics/charts/barcharts.py 1.11 dinu_gherman + Adapted to using validator classes in attribute maps. + graphics/charts/axes.py 1.23 dinu_gherman + Adapted to using validator classes in attribute maps. + test/test_lib_validators.py 1.2 dinu_gherman + Changed validator functions into classes. Adapted test cases. Added a tiny change to widgetbase.py. + lib/validators.py 1.3 dinu_gherman + Changed validator functions into classes. Adapted test cases. Added a tiny change to widgetbase.py. + graphics/widgetbase.py 1.13 dinu_gherman + Changed validator functions into classes. Adapted test cases. Added a tiny change to widgetbase.py. + test/test_coordtracking.py 1.2 rgbecker + Removed for for later correction +##### 2001/05/10 ##### + test/test_coordtracking.py 1.1 aaron_watters + test file for coordinate tracking and links + pdfgen/canvas.py 1.76 aaron_watters + canvas coordinate matrix tracking and related linkage support + graphics/widgetbase.py 1.12 dinu_gherman + Switched to using lib.validators module. + graphics/widgets/signsandsymbols.py 1.10 dinu_gherman + Switched to using lib.validators module. + graphics/widgets/flags.py 1.3 dinu_gherman + Switched to using lib.validators module. +##### 2001/05/09 ##### + lib/setup.py 1.5 rgbecker + Compatibility fixes + lib/_rl_accel.c 1.14 rgbecker + Compatibility fixes + graphics/widgetbase.py 1.11 dinu_gherman + Code and docstring enhancements. + test/test_graphics_speed.py 1.8 dinu_gherman + Switched to using defaultStyles attribute. + test/test_graphics_charts.py 1.9 dinu_gherman + Switched to using defaultStyles attribute. + graphics/charts/axes.py 1.22 dinu_gherman + Added visibleAxis/visibleTicks attributes. + graphics/charts/piecharts.py 1.10 dinu_gherman + Broken draw method apart (well...). + graphics/charts/piecharts.py 1.9 dinu_gherman + Done some slight code enhancements. + graphics/charts/lineplots.py 1.12 dinu_gherman + Switched to using PropHolder superclass for LinePlotProperties. Added strokeWidth attribute to LinePlotProperties. + test/test_lib_validators.py 1.1 dinu_gherman + Initial checkin. +##### 2001/05/08 ##### + graphics/charts/barcharts.py 1.10 dinu_gherman + Switched to using defaultStyles attribute. + graphics/charts/linecharts.py 1.7 dinu_gherman + Switched to using typed collection styles. + graphics/charts/piecharts.py 1.8 dinu_gherman + Made more consistent use of typed collections. + graphics/charts/lineplots.py 1.11 dinu_gherman + Removed dead code. Removed references to usedSymbol attribute. Made defaultStyles attribute a typed collection. + graphics/widgetbase.py 1.10 dinu_gherman + Added a __len__ method to TypedPropertyCOllection. + graphics/charts/lineplots.py 1.10 dinu_gherman + Removed references to defaultColors. + lib/graphdocpy.py 1.5 dinu_gherman + Opening now showing PDF outline (with closed function sections). Minor fixes. +##### 2001/05/07 ##### + graphics/renderPDF.py 1.7 aaron_watters + get rid of automatic boundary box option + graphics/charts/axes.py 1.21 dinu_gherman + Reduced number of arguments for joinToAxis() methods. + lib/validators.py 1.2 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/shapes.py 1.24 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/textlabels.py 1.3 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/piecharts.py 1.7 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/lineplots.py 1.9 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/linecharts.py 1.6 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/legends.py 1.4 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/barcharts.py 1.9 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + graphics/charts/axes.py 1.20 dinu_gherman + Extracted validator functions into lib.validators. Modified import statements. + lib/validators.py 1.1 dinu_gherman + Added future validators module (stripped off from shapes.py). + graphics/charts/piecharts.py 1.6 dinu_gherman + Various minor changes. + graphics/charts/lineplots.py 1.8 dinu_gherman + Various minor changes. + graphics/charts/linecharts.py 1.5 dinu_gherman + Various minor changes. + graphics/charts/legends.py 1.3 dinu_gherman + Various minor changes. + graphics/charts/barcharts.py 1.8 dinu_gherman + Various minor changes. + graphics/charts/axes.py 1.19 dinu_gherman + Various minor changes. +##### 2001/05/05 ##### + platypus/tables.py 1.39 aaron_watters + hacky fix to nudge bug + pdfgen/canvas.py 1.75 aaron_watters + fix for major font metrics state tracking bug +##### 2001/05/04 ##### + platypus/tables.py 1.38 aaron_watters + use round caps for miter limit + graphics/renderPM.py 1.4 rgbecker + Readded import checks and improved messages + graphics/renderPM.py 1.3 rgbecker + Added fix for png files +##### 2001/05/03 ##### + graphics/renderPM.py 1.2 rgbecker + ImportError checks for _renderPM + graphics/renderPM.py 1.1 rgbecker + Moved from rlextra/graphics/Csrc/renderPM +################################################################################# +#################### RELEASE 1.07 at 11:54 BST ################################## +################################################################################# +##### 2001/05/02 ##### + lib/units.py 1.3 rgbecker + Added toLength function +##### 2001/05/01 ##### + lib/colors.py 1.18 johnprecedo + Added special color - ReportLabBlue. + graphics/charts/axes.py 1.18 rgbecker + makeLabels/Ticks changed + graphics/testshapes.py 1.10 rgbecker + strokeDashArray fixes + graphics/shapes.py 1.23 rgbecker + strokeDashArray fixes + graphics/charts/axes.py 1.17 rgbecker + strokeDashArray fixes +##### 2001/04/30 ##### + pdfbase/pdfmetrics.py 1.37 rgbecker + Added some imports for compatibility/efficiency +##### 2001/04/28 ##### + graphics/widgets/signsandsymbols.py 1.9 rgbecker + try and ensure float division +##### 2001/04/26 ##### + graphics/charts/lineplots.py 1.7 dinu_gherman + Changed defaultColors to defaultStyles. + pdfgen/pdfimages.py 1.12 rgbecker + Fixed typo zlib (not lib) + lib/randomtext.py 1.7 johnprecedo + Minor changes. Now allows you to specify the maximum number of sentences for the randomtext as an argument. Also added an example usage to the docstring. +##### 2001/04/25 ##### + license.txt 1.2 andy_robinson + Trivial change to test CVS write access + graphics/charts/piecharts.py 1.5 dinu_gherman + Added more samples. + graphics/charts/piecharts.py 1.4 johnprecedo + Added a new example. + graphics/charts/piecharts.py 1.3 dinu_gherman + Removed sector line for single sliced pie charts. +##### 2001/04/24 ##### + graphics/charts/axes.py 1.16 dinu_gherman + Made joining of axes possible via axes attributes. +##### 2001/04/23 ##### + pdfbase/pdfmetrics.py 1.36 rgbecker + Added findT1File to base Face, and _fontdata + pdfbase/_fontdata.py 1.7 rgbecker + Added findT1File to base Face, and _fontdata + lib/fonts.py 1.7 rgbecker + Moved T1 search stuff into pdfbase.fontdata + pdfbase/pdfmetrics.py 1.35 rgbecker + Remove debug prints +##### 2001/04/20 ##### + test/test_pdfbase_fontembed.py 1.2 rgbecker + Merged pdfgen.fonts into pdfmetrics again + test/test_pdfbase_encodings.py 1.2 rgbecker + Merged pdfgen.fonts into pdfmetrics again + pdfgen/fonts.py 1.2 rgbecker + Merged pdfgen.fonts into pdfmetrics again + pdfbase/pdfmetrics.py 1.34 rgbecker + Merged pdfgen.fonts into pdfmetrics again + pdfbase/_fontdata.py 1.6 rgbecker + Merged pdfgen.fonts into pdfmetrics again + lib/_rl_accel.c 1.13 rgbecker + Merged pdfgen.fonts into pdfmetrics again + utils/daily.py 1.41 rgbecker + Fix graphguide generation + utils/daily.py 1.40 rgbecker + Add graphguide generation + pdfgen/canvas.py 1.74 rgbecker + pageCompression fix from Mark Charlebois (was undefined if zlib not present); removed stray print statements on font registration. + pdfbase/pdfmetrics.py 1.33 rgbecker + pageCompression fix from Mark Charlebois (was undefined if zlib not present); removed stray print statements on font registration. +##### 2001/04/18 ##### + test/test_pdfbase_fontembed.py 1.1 rgbecker + New font embedding mechanism + test/test_pdfbase_encodings.py 1.1 rgbecker + New font embedding mechanism + pdfgen/fonts.py 1.1 rgbecker + New font embedding mechanism + test/test_pdfgen_general.py 1.4 rgbecker + New font embedding stuff. + test/test_pdfbase_pdfmetrics.py 1.6 rgbecker + New font embedding stuff. + pdfgen/canvas.py 1.73 rgbecker + New font embedding stuff. + pdfbase/pdfmetrics.py 1.32 rgbecker + New font embedding stuff. + pdfbase/pdfdoc.py 1.44 rgbecker + New font embedding stuff. + README 1.5 rgbecker + Testing write access to CVS, only whitespace changed + testfile.txt 1.2 johnprecedo + removed test file + testfile.txt 1.1 johnprecedo + nitial checkin to test CVS write access +##### 2001/04/17 ##### + rl_config.py 1.4 rgbecker + Fix typo in T1SearchPath name + pdfbase/afm2w.py 1.2 rgbecker + Removed file added for test + pdfbase/afm2w.py 1.1 rgbecker + Adding afm2w as a test + docs/userguide/testfile.txt 1.1 andy_robinson + Testing checkin rights + docs/userguide/ch9_future.py 1.3 andy_robinson + Testing SF commit ability +##### 2001/04/16 ##### + __init__.py 1.13 rgbecker + Changed copyright dates + README 1.4 andy_robinson + Whitespace change to test CVS write access +##### 2001/04/13 ##### + test/test_pdfgen_general.py 1.3 rgbecker + Added sausages + test/test_pdfgen_general.py 1.2 andy_robinson + filename case change for an embedded GIF + test/test_platypus_general.py 1.2 andy_robinson + General pdfgen test moved to here + test/test_pdfgen_general.py 1.1 andy_robinson + General pdfgen test moved to here + test/test_platypus_tables.py 1.1 andy_robinson + Added tests formerly under platypus/test + test/test_platypus_general.py 1.1 andy_robinson + Added tests formerly under platypus/test + test/pythonpowered.gif 1.1 andy_robinson + Added tests formerly under platypus/test +##### 2001/04/12 ##### + graphics/charts/barcharts.py 1.7 rgbecker + Moved functionality into the base Barchart class + graphics/renderPDF.py 1.6 rgbecker + Added showBoundary arg +##### 2001/04/11 ##### + graphics/charts/axes.py 1.15 rgbecker + Move common value axis bits into base class + graphics/charts/axes.py 1.14 rgbecker + Try and make min/max finding as complicated as possible + graphics/charts/axes.py 1.13 rgbecker + Moved some common stuff into ValueAxis, added maximumTicks + graphics/shapes.py 1.22 rgbecker + Improved comment somewhat + graphics/charts/axes.py 1.12 rgbecker + Added _findMax/Min funcs + graphics/charts/axes.py 1.11 rgbecker + Added _calcValueStep and _rangeAdjust methods + graphics/widgetbase.py 1.9 rgbecker + Added StyleProeprties class +##### 2001/04/10 ##### + graphics/widgetbase.py 1.8 andy_robinson + Robin's refactoring to separate propholder and Widget + demos/pythonpoint/pythonpoint.py 1.32 andy_robinson + Removing pingo dependency + graphics/charts/markers.py 1.2 dinu_gherman + Added two funcions and tidied-up. + lib/normalDate.py 1.1 dinu_gherman + Initial checkin. + lib/pagesizes.py 1.7 andy_robinson + Fixed size of legal +##### 2001/04/09 ##### + graphics/charts/lineplots.py 1.6 dinu_gherman + Fixed import buglet. + test/test_graphics_charts.py 1.8 dinu_gherman + Adapted to new LineChart class names. + graphics/charts/markers.py 1.1 dinu_gherman + Initial checkin. + graphics/charts/lineplots.py 1.5 dinu_gherman + Splitted draw() method. Removed marker functions. Removed usage of XTimeSeriesAxis. Lots of tiny changes. + graphics/charts/linecharts.py 1.4 dinu_gherman + Aplitted draw method. Moved marker functions into markers.py. Many tiny modifications. + graphics/charts/barcharts.py 1.6 dinu_gherman + Minimal doc string changes. + graphics/charts/axes.py 1.10 dinu_gherman + Added attribute valueSteps to X- and YValueAxis. Commented XTimeSeriesAxes. + graphics/charts/barcharts.py 1.5 dinu_gherman + Added abstract BarChart base class. Applied various code simplifications. Removed 'import *'. + graphics/charts/barcharts.py 1.4 dinu_gherman + Tiny changes (to verify CVS works). + graphics/charts/barcharts.py 1.3 dinu_gherman + Split draw() methods into makeBars() and makeBackground(). Fixed one possible bug (reverted order of configure and setPosition()). + graphics/charts/utils.py 1.2 dinu_gherman + Added Robin's stuff from rgb_ticks.py. + graphics/charts/axes.py 1.9 dinu_gherman + Factorized two more methods into ValueAxis. + graphics/charts/axes.py 1.8 dinu_gherman + Factorized some more methods into abstract base classes. + graphics/charts/axes.py 1.7 dinu_gherman + Added new abstract base classes CategoryAxis and ValueAxis. Factorized some methods in X/Y axes and moved to these base classes. Split draw methods into three new methods. + graphics/charts/lineplots.py 1.4 dinu_gherman + Removed commented code. + graphics/charts/utils.py 1.1 dinu_gherman + Moved utility functions into new module utils.py. + graphics/charts/lineplots.py 1.3 dinu_gherman + Moved utility functions into new module utils.py. + graphics/charts/axes.py 1.6 dinu_gherman + Moved utility functions into new module utils.py. + graphics/charts/axes.py 1.5 dinu_gherman + Removed 'import *'. +##### 2001/04/06 ##### + lib/graphdocpy.py 1.4 andy_robinson + Now adds the current directory to the path; makes it easier to run it over other modules. + graphics/charts/axes.py 1.4 andy_robinson + Changes received from Dinu - required for demo, not sure what's in it (Andy) + graphics/shapes.py 1.21 rgbecker + Fix indent + graphics/shapes.py 1.20 rgbecker + _updater was silly + lib/utils.py 1.13 rgbecker + _updater was silly + lib/utils.py 1.12 rgbecker + Added dict _updater func + graphics/shapes.py 1.19 rgbecker + Group/Drawing slightly cleaner python +##### 2001/04/05 ##### + graphics/shapes.py 1.18 rgbecker + Added Group insert method + graphics/shapes.py 1.17 rgbecker + Added stringWidth import + utils/cvs_status.py 1.2 rgbecker + License text changes + test/test_pyfiles.py 1.5 rgbecker + License text changes + test/test_platypus_toc.py 1.6 rgbecker + License text changes + test/test_platypus_paragraphs.py 1.6 rgbecker + License text changes + test/test_pdfgen_pagemodes.py 1.4 rgbecker + License text changes + test/test_pdfbase_pdfutils.py 1.3 rgbecker + License text changes + test/test_pdfbase_pdfmetrics.py 1.5 rgbecker + License text changes + test/test_paragraphs.py 1.9 rgbecker + License text changes + test/test_lib_sequencer.py 1.4 rgbecker + License text changes + test/test_lib_colors.py 1.6 rgbecker + License text changes + test/test_graphics_speed.py 1.7 rgbecker + License text changes + test/test_graphics_charts_textlabels.py 1.3 rgbecker + License text changes + test/test_graphics_charts.py 1.7 rgbecker + License text changes + test/test_docstrings.py 1.4 rgbecker + License text changes + test/runAll.py 1.4 rgbecker + License text changes + test/__init__.py 1.2 rgbecker + License text changes + platypus/tableofcontents.py 1.2 rgbecker + License text changes + pdfbase/pdfmetrics.py 1.31 rgbecker + License text changes + pdfbase/_fontdata.py 1.5 rgbecker + License text changes + lib/yaml.py 1.2 rgbecker + License text changes + lib/setup.py 1.4 rgbecker + License text changes + lib/graphdocpy.py 1.3 rgbecker + License text changes + rl_config.py 1.3 rgbecker + License text changes + lib/docpy.py 1.4 rgbecker + License text changes + graphics/widgets/signsandsymbols.py 1.8 rgbecker + License text changes + graphics/widgets/flags.py 1.2 rgbecker + License text changes + graphics/widgets/__init__.py 1.2 rgbecker + License text changes + graphics/widgetbase.py 1.7 rgbecker + License text changes + graphics/testshapes.py 1.9 rgbecker + License text changes + graphics/testdrawings.py 1.2 rgbecker + License text changes + graphics/renderbase.py 1.9 rgbecker + License text changes + graphics/renderPS.py 1.6 rgbecker + License text changes + graphics/renderPDF.py 1.5 rgbecker + License text changes + graphics/charts/textlabels.py 1.2 rgbecker + License text changes + graphics/charts/piecharts.py 1.2 rgbecker + License text changes + graphics/charts/lineplots.py 1.2 rgbecker + License text changes + graphics/charts/linecharts.py 1.3 rgbecker + License text changes + graphics/charts/legends.py 1.2 rgbecker + License text changes + graphics/charts/barcharts.py 1.2 rgbecker + License text changes + graphics/charts/axes.py 1.3 rgbecker + License text changes + graphics/charts/__init__.py 1.2 rgbecker + License text changes + graphics/__init__.py 1.2 rgbecker + License text changes + docs/userguide/ch9_future.py 1.2 rgbecker + License text changes + docs/graphguide/t_parse.py 1.2 rgbecker + License text changes + docs/graphguide/platdemos.py 1.2 rgbecker + License text changes + docs/graphguide/gengraphguide.py 1.2 rgbecker + License text changes + docs/graphguide/examples.py 1.2 rgbecker + License text changes + docs/graphguide/ch2_graphics.py 1.17 rgbecker + License text changes + docs/graphguide/ch1_intro.py 1.7 rgbecker + License text changes + graphics/shapes.py 1.16 rgbecker + Fixed indentation + graphics/shapes.py 1.15 rgbecker + Added missing comment + graphics/shapes.py 1.14 rgbecker + Made Drawings inherit from Group + utils/copyr.txt 1.2 rgbecker + Changed copyright period + utils/copyrite.py 1.2 rgbecker + Removed spurious assignnments +##### 2001/04/04 ##### + lib/logger.py 1.3 rgbecker + Added enable attribute to the WarnOnce class + graphics/shapes.py 1.13 rgbecker + Added _textBoxLimits +##### 2001/04/03 ##### + docs/graphguide/ch2_graphics.py 1.16 johnprecedo + Spellchecked - fixed 15 typos. + docs/graphguide/ch2_graphics.py 1.15 johnprecedo + Minor changes - resolved a conflict. + docs/graphguide/ch2_graphics.py 1.14 johnprecedo + Added another sample Pie. +##### 2001/04/02 ##### + docs/graphguide/ch1_intro.py 1.6 johnprecedo + Corrected a couple of typoes. + docs/graphguide/ch2_graphics.py 1.13 johnprecedo + Corrected a couple of typoes. +################################################################################# +#################### RELEASE 1.06 at 14:00 BST ################################## +################################################################################# +##### 2001/03/30 ##### + docs/graphguide/ch2_graphics.py 1.11 dinu_gherman + Minor changes. + docs/graphguide/ch2_graphics.py 1.10 dinu_gherman + Updated text. + docs/graphguide/ch2_graphics.py 1.9 dinu_gherman + Added two bar charts. + docs/graphguide/ch2_graphics.py 1.8 dinu_gherman + Added samples for signsandsymbols. + docs/graphguide/ch2_graphics.py 1.7 dinu_gherman + Added minor changes. + docs/graphguide/ch1_intro.py 1.5 dinu_gherman + Added minor changes. + docs/graphguide/ch1_intro.py 1.4 dinu_gherman + Retrofitted a few changes to the content. + docs/userguide/genuserguide.py 1.46 dinu_gherman + Removed two commented imports (to see if CVS works). + utils/daily.py 1.39 rgbecker + Changed to account for graphdocpy and dos/graphguide + docs/userguide/genuserguide.py 1.45 rgbecker + Cross-refrenced to graphics guide, removed spurious ch8 + docs/userguide/ch8_graphics.py 1.15 rgbecker + Cross-refrenced to graphics guide, removed spurious ch8 + docs/userguide/ch2_graphics.py 1.12 rgbecker + Cross-refrenced to graphics guide, removed spurious ch8 + docs/userguide/ch1_intro.py 1.13 rgbecker + Cross-refrenced to graphics guide, removed spurious ch8 + README 1.3 rgbecker + Touch Test +##### 2001/03/29 ##### + docs/userguide/ch8_graphics.py 1.14 dinu_gherman + Added further minor corrections. + docs/graphguide/ch2_graphics.py 1.6 dinu_gherman + Further changes. + docs/graphguide/ch2_graphics.py 1.5 dinu_gherman + Added a table. + docs/graphguide/ch2_graphics.py 1.4 dinu_gherman + Further changes. + docs/userguide/ch8_graphics.py 1.13 dinu_gherman + Shortened even further. + docs/graphguide/ch2_graphics.py 1.3 dinu_gherman + Minor changes. + docs/graphguide/ch1_intro.py 1.3 dinu_gherman + Minor changes. + docs/graphguide/ch2_graphics.py 1.2 dinu_gherman + Changed granularity of chapters. + docs/graphguide/ch1_intro.py 1.2 dinu_gherman + Changed granularity of chapters. + docs/userguide/genuserguide.py 1.44 dinu_gherman + Removed reference to original graphics chapter. + docs/userguide/ch8_graphics_long.py 1.2 dinu_gherman + Removed graphics chapter into graphics guide. + docs/graphguide/t_parse.py 1.1 dinu_gherman + Initial checkin, mostly copied from user guide. + docs/graphguide/platdemos.py 1.1 dinu_gherman + Initial checkin, mostly copied from user guide. + docs/graphguide/gengraphguide.py 1.1 dinu_gherman + Initial checkin, mostly copied from user guide. + docs/graphguide/examples.py 1.1 dinu_gherman + Initial checkin, mostly copied from user guide. + docs/graphguide/ch2_graphics.py 1.1 dinu_gherman + Initial checkin, mostly copied from user guide. + docs/graphguide/ch1_intro.py 1.1 dinu_gherman + Initial checkin, mostly copied from user guide. +##### 2001/03/28 ##### + docs/userguide/ch8_graphics.py 1.12 dinu_gherman + Further changes. + docs/userguide/ch8_graphics.py 1.11 dinu_gherman + Shortened from previous version. + docs/userguide/genuserguide.py 1.43 dinu_gherman + Added import of short graphics chapter. + docs/userguide/ch8_graphics_long.py 1.1 dinu_gherman + Copyied graphics chapter file (to be extracted into a seperate document). + docs/userguide/ch8_graphics.py 1.10 johnprecedo + More images. + docs/userguide/ch8_graphics.py 1.9 johnprecedo + Added more illustrations. + graphics/charts/linecharts.py 1.2 dinu_gherman + Added to docstrings and tidied-up a bit. +##### 2001/03/27 ##### + docs/userguide/ch8_graphics.py 1.8 andy_robinson + Drawings and tables bug fixed. + docs/tools/rltemplate.py 1.11 andy_robinson + Drawings and tables bug fixed. + docs/userguide/ch8_graphics.py 1.7 johnprecedo + More markup. Added todo comments for missing images + docs/userguide/genuserguide.py 1.42 johnprecedo + Added Andy's changes to include illustrations (from code snippets). + docs/userguide/ch8_graphics.py 1.6 johnprecedo + Added Andy's changes to include illustrations (from code snippets). + docs/userguide/ch8_graphics.py 1.5 johnprecedo + More markup changes. Code snippets changed so that they match with current code. + docs/userguide/ch8_graphics.py 1.4 johnprecedo + Lots of small markup changes. Removed reference to paths - they aren't implemented yet in reportlab\graphics. changed references to 'widgets.py' to 'widgetbase.py'. + docs/userguide/genuserguide.py 1.41 johnprecedo + Moved chapter 8 to chapter 9 - new chapter 8 inserted for reportlab\graphics. +##### 2001/03/26 ##### + docs/userguide/ch8_graphics.py 1.3 johnprecedo + Finished markup of reportlab\graphics tutorial. Only one table inserted so far. Any markup other that split into headings/paragraphs/code examples is also missing. + docs/userguide/ch8_graphics.py 1.2 johnprecedo + First partial checkin on new chapter on "Platform Independent Graphics". Little markup done. Later part is still commented out (awaiting markup). + utils/runtests.py 1.16 rgbecker + Improved pattern searches and added better inhibiting + test/unittest.py 1.3 rgbecker + Added utils\runtests.py inhibitor comment + test/test_pyfiles.py 1.4 rgbecker + Added utils\runtests.py inhibitor comment + test/test_platypus_toc.py 1.5 rgbecker + Added utils\runtests.py inhibitor comment + test/test_platypus_paragraphs.py 1.5 rgbecker + Added utils\runtests.py inhibitor comment + test/test_pdfgen_pagemodes.py 1.3 rgbecker + Added utils\runtests.py inhibitor comment + test/test_pdfbase_pdfutils.py 1.2 rgbecker + Added utils\runtests.py inhibitor comment + test/test_pdfbase_pdfmetrics.py 1.4 rgbecker + Added utils\runtests.py inhibitor comment + test/test_paragraphs.py 1.8 rgbecker + Added utils\runtests.py inhibitor comment + test/test_lib_sequencer.py 1.3 rgbecker + Added utils\runtests.py inhibitor comment + test/test_lib_colors.py 1.5 rgbecker + Added utils\runtests.py inhibitor comment + test/test_graphics_speed.py 1.6 rgbecker + Added utils\runtests.py inhibitor comment + test/test_graphics_charts_textlabels.py 1.2 rgbecker + Added utils\runtests.py inhibitor comment + test/test_graphics_charts.py 1.6 rgbecker + Added utils\runtests.py inhibitor comment + test/test_docstrings.py 1.3 rgbecker + Added utils\runtests.py inhibitor comment + test/runAll.py 1.3 rgbecker + Added utils\runtests.py inhibitor comment + docs/userguide/ch9_future.py 1.1 johnprecedo + Existing chapter 8 renamed to chapter 9. Stub for new chapter 8 on graphics added. + docs/userguide/ch8_graphics.py 1.1 johnprecedo + Existing chapter 8 renamed to chapter 9. Stub for new chapter 8 on graphics added. + docs/userguide/ch8_future.py 1.4 johnprecedo + Existing chapter 8 renamed to chapter 9. Stub for new chapter 8 on graphics added. + lib/utils.py 1.11 rgbecker + Changed to prefer PIL as package + platypus/flowables.py 1.18 rgbecker + Changed to PIL_Image + pdfgen/test/testpdfgen.py 1.17 rgbecker + Changed to PIL_Image + pdfgen/pdfimages.py 1.11 rgbecker + Changed to PIL_Image + pdfgen/canvas.py 1.72 rgbecker + Changed to PIL_Image + pdfbase/pdfutils.py 1.21 rgbecker + Changed to PIL_Image + lib/utils.py 1.10 rgbecker + Changed to PIL_Image + graphics/renderPS.py 1.5 rgbecker + Changed to PIL_Image +##### 2001/03/25 ##### + graphics/shapes.py 1.12 dinu_gherman + Improved Auto class. Changed formatting slightly. + test/test_graphics_charts.py 1.5 dinu_gherman + Fixed a buglet with graphdocpy.py running over this file. + lib/docpy.py 1.3 dinu_gherman + Fixed argument bug for UmlPdfDocumentBuilder.begin(). Added bullets to 'imported modules' section. Added cosmetic changes to MyTemplate.afterFlowable(). +##### 2001/03/23 ##### + test/test_pyfiles.py 1.3 dinu_gherman + Changed trailing digit test to really fail. + graphics/widgets/flags0.py 1.5 dinu_gherman + Replaced trailing digit filename. + graphics/widgets/flags.py 1.1 dinu_gherman + Replaced trailing digit filename. + platypus/tableofcontents0.py 1.3 dinu_gherman + Replaced trailing digit file. + platypus/tableofcontents.py 1.1 dinu_gherman + Replaced trailing digit file. + test/test_platypus_toc.py 1.4 dinu_gherman + Fixed imports for non-trailing digit modules. + test/test_platypus_paragraphs.py 1.4 dinu_gherman + Fixed imports for non-trailing digit modules. + test/test_pyfiles.py 1.2 dinu_gherman + Added test for trailing digits in filenames. + lib/graphdocpy.py 1.2 dinu_gherman + Fixed doc strings and imports. + lib/docpy.py 1.2 dinu_gherman + Fixed doc strings and imports. + lib/graphdocpy.py 1.1 dinu_gherman + Initial checkin of non-trailing digit files replacing previous ones. + lib/docpy.py 1.1 dinu_gherman + Initial checkin of non-trailing digit files replacing previous ones. + lib/graphicsdoc0.py 1.12 dinu_gherman + Removed trailing digit file to be replaced with others. + lib/docpy1.py 1.2 dinu_gherman + Removed trailing digit files to be replaced with others. + lib/docpy0.py 1.8 dinu_gherman + Removed trailing digit files to be replaced with others. + graphics/shapes.py 1.11 dinu_gherman + Added isNumberOrAuto() function. + graphics/charts/axes.py 1.2 dinu_gherman + Added missing attribute maps. +##### 2001/03/22 ##### + lib/_rl_accel.c 1.12 rgbecker + Fixed up a better lookup for _AttrDict + platypus/paraparser.py 1.41 andy_robinson + Accepts seqdefault/seqDefault and seqreset/seqReset + lib/_rl_accel.c 1.11 rgbecker + Renamed AttrDict to _AttrDict +##### 2001/03/21 ##### + pdfbase/pdfdoc.py 1.43 aaron_watters + fixed minor bug in forms handling + lib/_rl_accel.c 1.10 rgbecker + AttrDict improvements not yet primetime + lib/setup.py 1.3 rgbecker + avoid import errors for Dinu, no runtests for Robin + lib/_rl_accel.c 1.9 rgbecker + avoid import errors for Dinu, no runtests for Robin + test/test_graphics_charts_textlabel0.py 1.4 dinu_gherman + Removed trailing digit filename to be replaced. + test/test_graphics_speed.py 1.5 dinu_gherman + Replaced one file with a non-trailing digit filename. Modified files using previous trailing digit modules. + test/test_graphics_charts_textlabels.py 1.1 dinu_gherman + Replaced one file with a non-trailing digit filename. Modified files using previous trailing digit modules. + test/test_graphics_charts.py 1.4 dinu_gherman + Replaced one file with a non-trailing digit filename. Modified files using previous trailing digit modules. + graphics/charts/textlabels.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/piecharts.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/lineplots.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/linecharts.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/legends.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/barcharts.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/axes.py 1.1 dinu_gherman + Initial checkin, replacing previous trailing digit filenames. + graphics/charts/textlabel0.py 1.7 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + graphics/charts/piechart0.py 1.6 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + graphics/charts/lineplot.py 1.2 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + graphics/charts/linechart0.py 1.3 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + graphics/charts/legends0.py 1.9 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + graphics/charts/barchart1.py 1.19 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + graphics/charts/axes0.py 1.18 dinu_gherman + Removed file names to be replaced with non-trailing digit ones. + rl_config.py 1.2 rgbecker + Made imageCaching a positive quantity and proeprty of the canvas + platypus/tables.py 1.37 rgbecker + Made imageCaching a positive quantity and proeprty of the canvas + pdfgen/pdfimages.py 1.10 rgbecker + Made imageCaching a positive quantity and proeprty of the canvas + pdfgen/canvas.py 1.71 rgbecker + Made imageCaching a positive quantity and proeprty of the canvas + docs/userguide/ch7_custom.py 1.7 rgbecker + Made imageCaching a positive quantity and proeprty of the canvas + docs/userguide/ch6_tables.py 1.17 rgbecker + Made imageCaching a positive quantity and proeprty of the canvas +##### 2001/03/20 ##### + graphics/charts/lineplot.py 1.1 dinu_gherman + Initial checkin. Added new experimental time series x value axis. + graphics/charts/axes0.py 1.17 dinu_gherman + Initial checkin. Added new experimental time series x value axis. + graphics/charts/legends0.py 1.8 dinu_gherman + Fixed import buglet. + graphics/charts/axes0.py 1.16 dinu_gherman + Fixed buglet when used with bar charts. + graphics/charts/barchart1.py 1.18 dinu_gherman + Added horizontal bar charts plus samples. +##### 2001/03/19 ##### + platypus/tables.py 1.36 aaron_watters + changed handling of table element styles for better space/time. old code left commented + test/test_paragraphs.py 1.7 andy_robinson + Proving you get leading whitespace in print if you have leading whitespace inside the para tag. +##### 2001/03/17 ##### + lib/styles.py 1.14 rgbecker + Removed spurious CellStyle + pdfbase/pdfmetrics.py 1.29 rgbecker + Fixed up self test +##### 2001/03/16 ##### + graphics/charts/barchart1.py 1.17 dinu_gherman + Fixed bug for bar charts using attribute 'useAbsolute'. + pdfbase/pdfmetrics.py 1.28 rgbecker + Fix bad name error(s) + pdfbase/_fontdata.py 1.4 rgbecker + Fix bad name error(s) + test/test_paragraphs.py 1.6 rgbecker + Renamed config.py to rl_config.py + test/test_graphics_speed.py 1.4 rgbecker + Renamed config.py to rl_config.py + platypus/test/testplatypus.py 1.23 rgbecker + Renamed config.py to rl_config.py + platypus/flowables.py 1.17 rgbecker + Renamed config.py to rl_config.py + platypus/doctemplate.py 1.39 rgbecker + Renamed config.py to rl_config.py + pdfgen/canvas.py 1.70 rgbecker + Renamed config.py to rl_config.py + pdfbase/pdfdoc.py 1.42 rgbecker + Renamed config.py to rl_config.py + lib/tocindex.py 1.7 rgbecker + Renamed config.py to rl_config.py + lib/fonts.py 1.6 rgbecker + Renamed config.py to rl_config.py + graphics/widgetbase.py 1.6 rgbecker + Renamed config.py to rl_config.py + graphics/shapes.py 1.10 rgbecker + Renamed config.py to rl_config.py + docs/userguide/genuserguide.py 1.40 rgbecker + Renamed config.py to rl_config.py + docs/userguide/examples.py 1.18 rgbecker + Renamed config.py to rl_config.py + docs/userguide/ch2_graphics.py 1.11 rgbecker + Renamed config.py to rl_config.py + docs/tools/rltemplate.py 1.10 rgbecker + Renamed config.py to rl_config.py + demos/pythonpoint/pythonpoint.py 1.31 rgbecker + Renamed config.py to rl_config.py + demos/gadflypaper/gfe.py 1.14 rgbecker + Renamed config.py to rl_config.py + utils/simpledoc.py 1.6 rgbecker + Fixed typo added point 8 (upload to SF) + utils/README 1.7 rgbecker + Fixed typo added point 8 (upload to SF) + pdfgen/pdfimages.py 1.9 rgbecker + fixed noImageCaching + pdfbase/_fontdata.py 1.3 rgbecker + Changed in line with _rl_accel 0.3 + rl_config.py 1.1 rgbecker + Renamed config.py to rl_config.py + config.py 1.4 rgbecker + Renamed config.py to rl_config.py + pdfbase/pdfmetrics.py 1.27 rgbecker + Added support for _rl_accel 0.3 + lib/_rl_accel.c 1.8 rgbecker + Added _instanceStringWidth, imroved fontSize arg handling +##### 2001/03/15 ##### + graphics/charts/barchart1.py 1.16 dinu_gherman + Fixed baseline bug and added test sample. + pdfbase/pdfmetrics.py 1.26 rgbecker + addStandardFonts was spurious + pdfgen/canvas.py 1.69 rgbecker + pdfmetrics.addStandardFonts was spurious + pdfbase/pdfmetrics.py 1.25 rgbecker + Stuff for adding standardFonts + pdfgen/canvas.py 1.68 rgbecker + Use pdfmetrics addFonts +##### 2001/03/14 ##### + graphics/charts/barchart1.py 1.15 dinu_gherman + Added more samples. +##### 2001/03/13 ##### + changes 1.7 dinu_gherman + New release 1.05. + __init__.py 1.11 dinu_gherman + New release 1.05. + test/test_graphics_charts.py 1.3 dinu_gherman + Changed to use Legend0 instead of Swatches0. + graphics/charts/legends0.py 1.7 dinu_gherman + Renamed Swatches0 to Legend0. Stripped away horizontal bar charts temporyrily. + graphics/charts/barchart1.py 1.14 dinu_gherman + Renamed Swatches0 to Legend0. Stripped away horizontal bar charts temporyrily. + graphics/charts/axes0.py 1.15 dinu_gherman + Renamed Swatches0 to Legend0. Stripped away horizontal bar charts temporyrily. + lib/setup.py 1.2 rgbecker + Added sgmlop pyHnj extensions + lib/_rl_accel.c 1.7 rgbecker + Fixed up the defaultEncoding thing and added a version number + lib/setup.py 1.1 rgbecker + Initial version of distutils script + lib/_rl_accel.c 1.6 rgbecker + Initial version of distutils script + graphics/charts/barchart1.py 1.13 dinu_gherman + Enabled mono-bar charts and no-bar charts(!) plus more samples. + graphics/charts/axes0.py 1.14 dinu_gherman + Enabled mono-bar charts and no-bar charts(!) plus more samples. + graphics/charts/barchart1.py 1.12 dinu_gherman + Added new samples. + graphics/charts/axes0.py 1.13 dinu_gherman + Set minimum number of items for X/Y axes to 1 instead of 2. + graphics/charts/axes0.py 1.12 dinu_gherman + Fixed bug refusing exactly two data items. + graphics/charts/axes0.py 1.11 dinu_gherman + Improved axes. + graphics/charts/axes0.py 1.10 dinu_gherman + Improved X/Y axes. + graphics/charts/axes0.py 1.9 dinu_gherman + Improved YCategoryAxis slightly. + test/test_graphics_charts.py 1.2 dinu_gherman + Changed attribute postponed to _postponed in doctemplate.py. Added minor changes in shapes.py. Changed test_graphics_charts.py to no longer use page breaks. + platypus/doctemplate.py 1.38 dinu_gherman + Changed attribute postponed to _postponed in doctemplate.py. Added minor changes in shapes.py. Changed test_graphics_charts.py to no longer use page breaks. + graphics/shapes.py 1.9 dinu_gherman + Changed attribute postponed to _postponed in doctemplate.py. Added minor changes in shapes.py. Changed test_graphics_charts.py to no longer use page breaks. + graphics/charts/legends0.py 1.6 andy_robinson + Fixed broken cross reference to another example + test/test_lib_colors.py 1.4 andy_robinson + Added CMYK support to core + pdfgen/canvas.py 1.67 andy_robinson + Added CMYK support to core + lib/colors.py 1.17 andy_robinson + Added CMYK support to core +##### 2001/03/12 ##### + test/test_lib_colors.py 1.3 andy_robinson + Initial checkin of CMYKColor class + lib/colors.py 1.16 andy_robinson + Initial checkin of CMYKColor class + graphics/charts/barchart1.py 1.11 dinu_gherman + Added initial crude version of HorizontalBarChart class. + graphics/charts/axes0.py 1.8 dinu_gherman + Added dual versions for category and value axes. (not all tested) + graphics/charts/piechart0.py 1.5 andy_robinson + Fixed bug with defaultColors changes being erroneouslty banned + graphics/charts/legends0.py 1.5 andy_robinson + Fixed bug with defaultColors changes being erroneouslty banned + test/test_graphics_charts.py 1.1 dinu_gherman + Initial checkin. + graphics/charts/legends0.py 1.4 dinu_gherman + Fixed import error. + test/test_pdfbase_pdfmetrics.py 1.3 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + test/test_paragraphs.py 1.5 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfgen/canvas.py 1.66 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfbase/pdfmetrics.py 1.24 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfbase/pdfdoc.py 1.41 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfbase/_fontdata.py 1.2 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfgen/fonts0.py 1.4 rgbecker + Removed fonts0 stuff to pdfbase.pdfmetrics +##### 2001/03/08 ##### + test/test_pdfbase_pdfmetrics.py 1.2 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfgen/fonts0.py 1.3 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfgen/canvas.py 1.65 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfbase/pdfmetrics.py 1.23 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfbase/_fontdata.py 1.1 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + demos/stdfonts/stdfonts.py 1.10 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + test/test_platypus_paragraphs.py 1.3 dinu_gherman + Added a multi-page paragraph test. + docs/userguide/ch1_intro.py 1.12 rgbecker + Changed bad character +##### 2001/03/07 ##### + utils/simpledoc.py 1.5 rgbecker + Changed things to use config.py for defaults + test/test_paragraphs.py 1.4 rgbecker + Changed things to use config.py for defaults + platypus/test/testplatypus.py 1.22 rgbecker + Changed things to use config.py for defaults + platypus/tables.py 1.35 rgbecker + Changed things to use config.py for defaults + platypus/flowables.py 1.16 rgbecker + Changed things to use config.py for defaults + platypus/doctemplate.py 1.37 rgbecker + Changed things to use config.py for defaults + pdfgen/fonts0.py 1.2 rgbecker + Changed things to use config.py for defaults + pdfgen/canvas.py 1.64 rgbecker + Changed things to use config.py for defaults + pdfbase/pdfdoc.py 1.40 rgbecker + Changed things to use config.py for defaults + lib/tocindex.py 1.6 rgbecker + Changed things to use config.py for defaults + lib/pagesizes.py 1.6 rgbecker + Changed things to use config.py for defaults + lib/fonts.py 1.5 rgbecker + Changed things to use config.py for defaults + docs/userguide/genuserguide.py 1.39 rgbecker + Changed things to use config.py for defaults + docs/userguide/examples.py 1.17 rgbecker + Changed things to use config.py for defaults + docs/userguide/ch4_platypus_concepts.py 1.10 rgbecker + Changed things to use config.py for defaults + docs/userguide/ch2_graphics.py 1.10 rgbecker + Changed things to use config.py for defaults + docs/tools/rltemplate.py 1.9 rgbecker + Changed things to use config.py for defaults + demos/pythonpoint/pythonpoint.py 1.30 rgbecker + Changed things to use config.py for defaults + demos/odyssey/fodyssey.py 1.15 rgbecker + Changed things to use config.py for defaults + demos/gadflypaper/gfe.py 1.13 rgbecker + Changed things to use config.py for defaults + config.py 1.3 rgbecker + Changed things to use config.py for defaults + platypus/paragraph.py 1.51 rgbecker + Added test 5 +##### 2001/03/06 ##### + lib/yaml.py 1.1 andy_robinson + Added + test/test_pdfbase_pdfmetrics.py 1.1 andy_robinson + Added explicit font and encoding support + platypus/paragraph.py 1.50 andy_robinson + Added explicit font and encoding support + platypus/doctemplate.py 1.36 andy_robinson + Added explicit font and encoding support + pdfgen/fonts0.py 1.1 andy_robinson + Added explicit font and encoding support + pdfgen/canvas.py 1.63 andy_robinson + Added explicit font and encoding support + pdfbase/pdfmetrics.py 1.22 andy_robinson + Added explicit font and encoding support + pdfbase/pdfdoc.py 1.39 andy_robinson + Added explicit font and encoding support + lib/graphicsdoc0.py 1.11 andy_robinson + Added explicit font and encoding support + demos/stdfonts/stdfonts.py 1.9 andy_robinson + Added explicit font and encoding support + config.py 1.2 andy_robinson + Added explicit font and encoding support + __init__.py 1.10 andy_robinson + Added explicit font and encoding support +##### 2001/03/02 ##### + lib/fonts.py 1.4 rgbecker + Removed spurious import + lib/fonts.py 1.3 rgbecker + Added standard filename roots and findPFB +##### 2001/03/01 ##### + pdfbase/pdfmetrics.py 1.21 rgbecker + Fixed _rl_accel import +##### 2001/02/28 ##### + platypus/paraparser.py 1.40 rgbecker + Exception tests need str() + pdfbase/pdfutils.py 1.20 rgbecker + Exception tests need str() + pdfbase/pdfmetrics.py 1.20 rgbecker + Exception tests need str() + lib/xmllib.py 1.6 rgbecker + Exception tests need str() + lib/utils.py 1.9 rgbecker + Exception tests need str() + lib/graphicsdoc0.py 1.10 rgbecker + Exception tests need str() + demos/odyssey/dodyssey.py 1.10 rgbecker + slight twitch to onDraw test + pdfbase/pdfutils.py 1.19 rgbecker + Fixed typos + graphics/renderPS.py 1.4 rgbecker + Improved ImportError handling + lib/xmllib.py 1.5 rgbecker + Improved ImportError handling + lib/utils.py 1.8 rgbecker + Improved ImportError handling + lib/graphicsdoc0.py 1.9 rgbecker + Improved ImportError handling + lib/_rl_accel.dsp 1.3 rgbecker + Improved ImportError handling + pdfbase/pdfutils.py 1.18 rgbecker + Improved ImportError handling + pdfbase/pdfmetrics.py 1.19 rgbecker + Improved ImportError handling + pdfbase/pdfdoc.py 1.38 rgbecker + Improved ImportError handling + pdfgen/pdfimages.py 1.8 rgbecker + Imporved ImportError handling + pdfgen/canvas.py 1.62 rgbecker + Imporved ImportError handling + platypus/paraparser.py 1.39 rgbecker + Imporved ImportError handling + graphics/charts/barchart1.py 1.10 dinu_gherman + Changed bar chart labels to have always a boxAttribute of 'c'. + test/test_graphics_charts_textlabel0.py 1.3 dinu_gherman + Added meaningful tests for textAnchor attribute. +##### 2001/02/27 ##### + graphics/charts/textlabel0.py 1.6 rgbecker + Added auto height, width and leading + test/test_graphics_charts_textlabel0.py 1.2 dinu_gherman + Thoroughly changed test file. + graphics/charts/barchart1.py 1.9 dinu_gherman + Splitted function sample2 in sample2a and sample2b. + test/test_graphics_charts_textlabel0.py 1.1 dinu_gherman + Initial checkin. + graphics/charts/textlabel0.py 1.5 dinu_gherman + Removed commented test function. +##### 2001/02/26 ##### + lib/docpy1.py 1.1 andy_robinson + Initial checkin of new experiments +##### 2001/02/24 ##### + graphics/widgets/signsandsymbols.py 1.7 dinu_gherman + Added color attribute to SmileyFace0. + graphics/charts/linechart0.py 1.2 dinu_gherman + General improvements. + graphics/charts/linechart0.py 1.1 dinu_gherman + Initial checkin. +##### 2001/02/23 ##### + test/test_pyfiles.py 1.1 dinu_gherman + Initial checkin. +##### 2001/02/22 ##### + pdfbase/pdfutils.py 1.17 dinu_gherman + Removed commented encoding test functions. + test/test_pdfbase_pdfutils.py 1.1 dinu_gherman + Initial checkin. + pdfbase/pdfutils.py 1.16 dinu_gherman + Commented another test function. + pdfbase/pdfutils.py 1.15 dinu_gherman + Commented one test function. + graphics/charts/textlabel0.py 1.4 dinu_gherman + Minor corrections. + graphics/charts/axes0.py 1.7 dinu_gherman + Minor corrections. +##### 2001/02/21 ##### + pdfgen/canvas.py 1.61 aaron_watters + bugfix: code before of begin/end form should not appear in the form but in page +##### 2001/02/19 ##### + graphics/charts/axes0.py 1.6 dinu_gherman + Added different modes for joined x- and y-axes. + graphics/charts/barchart1.py 1.8 dinu_gherman + Added attribute mappings. + graphics/charts/axes0.py 1.5 dinu_gherman + Added attribute mappings. + graphics/charts/axes0.py 1.4 dinu_gherman + Changed test function to sample function. + graphics/charts/legends0.py 1.3 dinu_gherman + Added combined barchart sample. + graphics/charts/legends0.py 1.2 dinu_gherman + Minor changes. + graphics/charts/legends0.py 1.1 dinu_gherman + Initial checkin. + test/test_pdfgen_pagemodes.py 1.2 dinu_gherman + Added doc string. + test/test_platypus_paragraphs.py 1.2 dinu_gherman + Added doc strings. + test/test_lib_sequencer.py 1.2 dinu_gherman + Added doc strings. + test/test_lib_colors.py 1.2 dinu_gherman + Added doc strings. + test/test_docstrings.py 1.2 dinu_gherman + Fixed filename bug. + test/test_docstrings.py 1.1 dinu_gherman + Initial checkin. +##### 2001/02/18 ##### + test/unittest.py 1.2 andy_robinson + Resynched graphics test with Pie class; runAll now deletes previous PDF files incorporated latest unittest.py + test/test_graphics_speed.py 1.3 andy_robinson + Resynched graphics test with Pie class; runAll now deletes previous PDF files incorporated latest unittest.py + test/runAll.py 1.2 andy_robinson + Resynched graphics test with Pie class; runAll now deletes previous PDF files incorporated latest unittest.py + test/00readme.txt 1.4 andy_robinson + Resynched graphics test with Pie class; runAll now deletes previous PDF files incorporated latest unittest.py +##### 2001/02/16 ##### + test/test_lib_colors.py 1.1 dinu_gherman + Initil checkin. + lib/colors.py 1.15 dinu_gherman + Added color2bw function. +##### 2001/02/15 ##### + graphics/charts/piechart0.py 1.4 andy_robinson + Slimmed down and added demo methods + graphics/charts/barchart1.py 1.7 andy_robinson + Slimmed down and added demo methods + graphics/charts/axes0.py 1.3 andy_robinson + Slimmed down and added demo methods + test/test_platypus_paragraphs.py 1.1 dinu_gherman + Initial checkin. +##### 2001/02/14 ##### + graphics/charts/barchart.py 1.4 dinu_gherman + File has become obsolete. See barchart1.py. + lib/graphicsdoc0.py 1.8 dinu_gherman + Improved displaying widget properties (using pprint). + pdfgen/test/testPageMode.py 1.3 dinu_gherman + Moved to reportlab.test. + test/test_pdfgen_pagemodes.py 1.1 dinu_gherman + Initial checkin, moved from reportlab.pdfgen.test. + test/test_lib_sequencer.py 1.1 dinu_gherman + Initial checkin. + test/runAll.py 1.1 dinu_gherman + Initial checkin. + test/test_platypus_toc.py 1.3 dinu_gherman + Restructured entire test suite to make clean use of the unittest module. + test/test_paragraphs.py 1.3 dinu_gherman + Restructured entire test suite to make clean use of the unittest module. + test/test_graphics_speed.py 1.2 dinu_gherman + Restructured entire test suite to make clean use of the unittest module. + test/00readme.txt 1.3 dinu_gherman + Restructured entire test suite to make clean use of the unittest module. +##### 2001/02/13 ##### + graphics/widgets/flags0.py 1.4 johnprecedo + - Removed white borders from inside edges of flags. - added the word "sample" to the demo flag produced by Flag0 - dummy __init__ and demo methods added for Portugal0 - no example flag or properties are produced. +##### 2001/03/13 ##### + test/test_graphics_charts.py 1.3 dinu_gherman + Changed to use Legend0 instead of Swatches0. + graphics/charts/legends0.py 1.7 dinu_gherman + Renamed Swatches0 to Legend0. Stripped away horizontal bar charts temporyrily. + graphics/charts/barchart1.py 1.14 dinu_gherman + Renamed Swatches0 to Legend0. Stripped away horizontal bar charts temporyrily. + graphics/charts/axes0.py 1.15 dinu_gherman + Renamed Swatches0 to Legend0. Stripped away horizontal bar charts temporyrily. + lib/setup.py 1.2 rgbecker + Added sgmlop pyHnj extensions + lib/_rl_accel.c 1.7 rgbecker + Fixed up the defaultEncoding thing and added a version number + lib/setup.py 1.1 rgbecker + Initial version of distutils script + lib/_rl_accel.c 1.6 rgbecker + Initial version of distutils script + graphics/charts/barchart1.py 1.13 dinu_gherman + Enabled mono-bar charts and no-bar charts(!) plus more samples. + graphics/charts/axes0.py 1.14 dinu_gherman + Enabled mono-bar charts and no-bar charts(!) plus more samples. + graphics/charts/barchart1.py 1.12 dinu_gherman + Added new samples. + graphics/charts/axes0.py 1.13 dinu_gherman + Set minimum number of items for X/Y axes to 1 instead of 2. + graphics/charts/axes0.py 1.12 dinu_gherman + Fixed bug refusing exactly two data items. + graphics/charts/axes0.py 1.11 dinu_gherman + Improved axes. + graphics/charts/axes0.py 1.10 dinu_gherman + Improved X/Y axes. + graphics/charts/axes0.py 1.9 dinu_gherman + Improved YCategoryAxis slightly. + test/test_graphics_charts.py 1.2 dinu_gherman + Changed attribute postponed to _postponed in doctemplate.py. Added minor changes in shapes.py. Changed test_graphics_charts.py to no longer use page breaks. + platypus/doctemplate.py 1.38 dinu_gherman + Changed attribute postponed to _postponed in doctemplate.py. Added minor changes in shapes.py. Changed test_graphics_charts.py to no longer use page breaks. + graphics/shapes.py 1.9 dinu_gherman + Changed attribute postponed to _postponed in doctemplate.py. Added minor changes in shapes.py. Changed test_graphics_charts.py to no longer use page breaks. + graphics/charts/legends0.py 1.6 andy_robinson + Fixed broken cross reference to another example + test/test_lib_colors.py 1.4 andy_robinson + Added CMYK support to core + pdfgen/canvas.py 1.67 andy_robinson + Added CMYK support to core + lib/colors.py 1.17 andy_robinson + Added CMYK support to core +##### 2001/03/12 ##### + test/test_lib_colors.py 1.3 andy_robinson + Initial checkin of CMYKColor class + lib/colors.py 1.16 andy_robinson + Initial checkin of CMYKColor class + graphics/charts/barchart1.py 1.11 dinu_gherman + Added initial crude version of HorizontalBarChart class. + graphics/charts/axes0.py 1.8 dinu_gherman + Added dual versions for category and value axes. (not all tested) + graphics/charts/piechart0.py 1.5 andy_robinson + Fixed bug with defaultColors changes being erroneouslty banned + graphics/charts/legends0.py 1.5 andy_robinson + Fixed bug with defaultColors changes being erroneouslty banned + test/test_graphics_charts.py 1.1 dinu_gherman + Initial checkin. + graphics/charts/legends0.py 1.4 dinu_gherman + Fixed import error. + test/test_pdfbase_pdfmetrics.py 1.3 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + test/test_paragraphs.py 1.5 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfgen/canvas.py 1.66 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfbase/pdfmetrics.py 1.24 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfbase/pdfdoc.py 1.41 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfbase/_fontdata.py 1.2 rgbecker + Changes to basic Font stuff, moved it to pdfmetrics + pdfgen/fonts0.py 1.4 rgbecker + Removed fonts0 stuff to pdfbase.pdfmetrics +##### 2001/03/08 ##### + test/test_pdfbase_pdfmetrics.py 1.2 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfgen/fonts0.py 1.3 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfgen/canvas.py 1.65 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfbase/pdfmetrics.py 1.23 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + pdfbase/_fontdata.py 1.1 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + demos/stdfonts/stdfonts.py 1.10 rgbecker + Initial split of pdfmetrics into code + database; some minor name adjustments + test/test_platypus_paragraphs.py 1.3 dinu_gherman + Added a multi-page paragraph test. +##### 2001/03/07 ##### + test/test_paragraphs.py 1.4 rgbecker + Changed things to use config.py for defaults + platypus/test/testplatypus.py 1.22 rgbecker + Changed things to use config.py for defaults + platypus/tables.py 1.35 rgbecker + Changed things to use config.py for defaults + platypus/flowables.py 1.16 rgbecker + Changed things to use config.py for defaults + platypus/doctemplate.py 1.37 rgbecker + Changed things to use config.py for defaults + pdfgen/fonts0.py 1.2 rgbecker + Changed things to use config.py for defaults + pdfgen/canvas.py 1.64 rgbecker + Changed things to use config.py for defaults + pdfbase/pdfdoc.py 1.40 rgbecker + Changed things to use config.py for defaults + lib/tocindex.py 1.6 rgbecker + Changed things to use config.py for defaults + lib/pagesizes.py 1.6 rgbecker + Changed things to use config.py for defaults + lib/fonts.py 1.5 rgbecker + Changed things to use config.py for defaults + demos/pythonpoint/pythonpoint.py 1.30 rgbecker + Changed things to use config.py for defaults + demos/odyssey/fodyssey.py 1.15 rgbecker + Changed things to use config.py for defaults + demos/gadflypaper/gfe.py 1.13 rgbecker + Changed things to use config.py for defaults + config.py 1.3 rgbecker + Changed things to use config.py for defaults + platypus/paragraph.py 1.51 rgbecker + Added test 5 +##### 2001/03/06 ##### + lib/yaml.py 1.1 andy_robinson + Added + test/test_pdfbase_pdfmetrics.py 1.1 andy_robinson + Added explicit font and encoding support + platypus/paragraph.py 1.50 andy_robinson + Added explicit font and encoding support + platypus/doctemplate.py 1.36 andy_robinson + Added explicit font and encoding support + pdfgen/fonts0.py 1.1 andy_robinson + Added explicit font and encoding support + pdfgen/canvas.py 1.63 andy_robinson + Added explicit font and encoding support + pdfbase/pdfmetrics.py 1.22 andy_robinson + Added explicit font and encoding support + pdfbase/pdfdoc.py 1.39 andy_robinson + Added explicit font and encoding support + lib/graphicsdoc0.py 1.11 andy_robinson + Added explicit font and encoding support + demos/stdfonts/stdfonts.py 1.9 andy_robinson + Added explicit font and encoding support + config.py 1.2 andy_robinson + Added explicit font and encoding support + __init__.py 1.10 andy_robinson + Added explicit font and encoding support +##### 2001/feb/09 release 1.03 +##### 2001/02/09 ##### + platypus/tableofcontents0.py 1.2 dinu_gherman + Fixed wrapped line indentation. + lib/graphicsdoc0.py 1.7 dinu_gherman + Entirely reworked PDF and HTML doc builders. + lib/docpy0.py 1.7 dinu_gherman + Entirely reworked PDF and HTML doc builders. + graphics/widgetbase.py 1.5 dinu_gherman + Shortened two lines that might appear in doc tools. + platypus/tableofcontents0.py 1.1 dinu_gherman + Initial checkin. +##### 2001/02/08 ##### + platypus/doctemplate.py 1.35 dinu_gherman + Commented some disturbing print statements. + lib/graphicsdoc0.py 1.6 dinu_gherman + UmlPdfDocBuilder moved to docpy0.py. Changed some defaults and variable names. Fixed handling of packages with dotted names. Changed output of GraphPdfDocBuilder0. + lib/docpy0.py 1.6 dinu_gherman + UmlPdfDocBuilder moved here from graphicsdoc0.py. Changed some defaults and variable names. Fixed handling of packages with dotted names. + graphics/charts/barchart1.py 1.6 dinu_gherman + One long line reduced in length. +##### 2001/02/07 ##### + lib/randomtext.py 1.6 johnprecedo + fixed a typo + lib/randomtext.py 1.5 johnprecedo + Added a PYTHON theme (it had to be done...). + lib/graphicsdoc0.py 1.5 dinu_gherman + Added displaying drawings returned by functions. + graphics/widgets/signsandsymbols.py 1.5 johnprecedo + Corrected mistake in the doctsring for flags0.py and signsandsymbols.py. + graphics/widgets/flags0.py 1.3 johnprecedo + Corrected mistake in the doctsring for flags0.py and signsandsymbols.py. + lib/graphicsdoc0.py 1.4 dinu_gherman + Fixed import error. + lib/graphicsdoc0.py 1.3 dinu_gherman + Added an experimental PlatypusDocBuilder0. Uncommented UmlPdfDocBuilder0. + lib/graphicsdoc0.py 1.2 dinu_gherman + Moved utility functions from docpy0.py to graphicsdoc0.py. + lib/docpy0.py 1.5 dinu_gherman + Moved utility functions from docpy0.py to graphicsdoc0.py. +##### 2001/02/06 ##### + lib/graphicsdoc0.py 1.1 dinu_gherman + Initial checkin. + lib/docpy0.py 1.4 dinu_gherman + Stripped-off everything widget/graphics-specific. + graphics/charts/barchart1.py 1.5 andy_robinson + Added barLabelBudge property to support a customer + graphics/renderbase.py 1.8 andy_robinson + Fixes for QIR barchart presentation + graphics/renderPS.py 1.3 andy_robinson + Fixes for QIR barchart presentation + graphics/charts/barchart1.py 1.4 andy_robinson + Fixes for QIR barchart presentation +##### 2001/02/05 ##### + pdfbase/pdfdoc.py 1.36 aaron_watters + forms in forms now work + graphics/widgets/signsandsymbols.py 1.4 johnprecedo + bugfixes for the various demo() methods for widgets. + lib/docpy0.py 1.3 dinu_gherman + Fixed string joining for base class names. + graphics/widgets/signsandsymbols.py 1.3 johnprecedo + changes all demo() methods to centre graphics in boxes 200x100 boxes.. + lib/docpy0.py 1.2 dinu_gherman + Modified inspect import. + lib/docpy0.py 1.1 dinu_gherman + Initial checkin. + graphics/widgets/flags0.py 1.2 johnprecedo + tightened up code for stripes in US and Greek flags. + graphics/widgets/flags0.py 1.1 johnprecedo + First checkin of file that used to be users/john/fwidgets.py. 20 flags as widgets - inlcuding UK, US, most EU members and a few non-EU European countries. + pdfbase/pdfdoc.py 1.35 aaron_watters + pdfdictionary has no has_key method bugfix. sorry. +##### 2001/02/04 ##### + graphics/renderbase.py 1.7 andy_robinson + Continued work on bar charts and support + graphics/charts/textlabel0.py 1.3 andy_robinson + Continued work on bar charts and support + graphics/charts/barchart1.py 1.3 andy_robinson + Continued work on bar charts and support + graphics/charts/axes0.py 1.2 andy_robinson + Continued work on bar charts and support + graphics/charts/axes0.py 1.1 andy_robinson + Broke out axes to separate module + graphics/widgetbase.py 1.4 andy_robinson + Fixed user node rendering bug, added barchart1 + graphics/shapes.py 1.8 andy_robinson + Fixed user node rendering bug, added barchart1 + graphics/renderbase.py 1.6 andy_robinson + Fixed user node rendering bug, added barchart1 + graphics/charts/textlabel0.py 1.2 andy_robinson + Fixed user node rendering bug, added barchart1 + graphics/charts/barchart1.py 1.2 andy_robinson + Fixed user node rendering bug, added barchart1 +##### 2001/02/02 ##### + pdfbase/pdfdoc.py 1.34 aaron_watters + minor changes to support pdfparse +##### 2001/02/01 ##### + graphics/testshapes.py 1.8 andy_robinson + Working on chart framework + graphics/shapes.py 1.7 andy_robinson + Working on chart framework + graphics/renderbase.py 1.5 andy_robinson + Working on chart framework + graphics/charts/textlabel0.py 1.1 andy_robinson + Working on chart framework + graphics/charts/barchart1.py 1.1 andy_robinson + Working on chart framework +##### 2001/01/30 ##### + lib/inspect.py 1.1 dinu_gherman + Initial checkin. + graphics/charts/barchart.py 1.3 aaron_watters + more stuff, bug fixes etcetera.... + graphics/testshapes.py 1.7 andy_robinson + Fixed group transform bug and added tests + graphics/renderbase.py 1.4 andy_robinson + Fixed group transform bug and added tests + graphics/renderPS.py 1.2 andy_robinson + Fixed group transform bug and added tests + graphics/renderPDF.py 1.4 andy_robinson + Fixed group transform bug and added tests +##### 2001/01/29 ##### + graphics/widgets/signsandsymbols.py 1.2 johnprecedo + Changed classnames to use the '0' convention for experimental. Changed way test print out is carried out. (all calls to classes wrapped up in a new function 'test()') some tidying up. +##### 2001/01/28 ##### + graphics/testshapes.py 1.6 andy_robinson + Fixes to CTM to support bitmap renderer; extra string rotation and group tests. + graphics/renderbase.py 1.3 andy_robinson + Fixes to CTM to support bitmap renderer; extra string rotation and group tests. +##### 2001/01/26 ##### + graphics/widgetbase.py 1.3 andy_robinson + Added barchart compatibility + graphics/shapes.py 1.6 andy_robinson + Added barchart compatibility + graphics/charts/piechart0.py 1.3 andy_robinson + Added barchart compatibility + graphics/charts/barchart.py 1.2 aaron_watters + added scales +##### 2001/01/25 ##### + graphics/widgets/signsandsymbols.py 1.1 johnprecedo + First checkin of file that used to be users/john/jwidgets.py. 12 new widgets. + graphics/charts/barchart.py 1.1 aaron_watters + A initial go at Aarons last rewrite of chart widgets :) + graphics/testshapes.py 1.5 andy_robinson + Added postscript renderer and tests, fixed renderer bugs + graphics/shapes.py 1.5 andy_robinson + Added postscript renderer and tests, fixed renderer bugs + graphics/renderbase.py 1.2 andy_robinson + Added postscript renderer and tests, fixed renderer bugs + graphics/renderPS.py 1.1 andy_robinson + Added postscript renderer and tests, fixed renderer bugs + test/test_graphics_speed.py 1.1 andy_robinson + added a crude benchmarking script +##### 2001/01/24 ##### + graphics/testshapes.py 1.4 andy_robinson + Added group tests, fixed font bug, resynched with Dinu + graphics/shapes.py 1.4 andy_robinson + Added group tests, fixed font bug, resynched with Dinu + graphics/renderPDF.py 1.3 andy_robinson + Added group tests, fixed font bug, resynched with Dinu + graphics/testshapes.py 1.3 dinu_gherman + Added unit testing plus error drawing. + graphics/testshapes.py 1.2 dinu_gherman + Added some String shapes. + graphics/testshapes.py 1.1 dinu_gherman + Initial checkin. + graphics/widgetbase.py 1.2 andy_robinson + Collection based piechart amd support for it. + graphics/shapes.py 1.3 andy_robinson + Collection based piechart amd support for it. + graphics/renderPDF.py 1.2 andy_robinson + Collection based piechart amd support for it. + graphics/charts/piechart0.py 1.2 andy_robinson + Collection based piechart amd support for it. +##### 2001/01/23 ##### + config.py 1.1 johnprecedo + Whoops, forgot + graphics/shapes.py 1.2 johnprecedo + typos fixed +##### 2001/01/22 ##### + graphics/charts/piechart0.py 1.1 andy_robinson + Experimental pie module + graphics/widgets/__init__.py 1.1 andy_robinson + Added widgets subpackage + graphics/charts/__init__.py 1.1 andy_robinson + Added charts package + graphics/widgetbase.py 1.1 andy_robinson + Added graphics module + graphics/testdrawings.py 1.1 andy_robinson + Added graphics module + graphics/shapes.py 1.1 andy_robinson + Added graphics module + graphics/renderbase.py 1.1 andy_robinson + Added graphics module + graphics/renderPDF.py 1.1 andy_robinson + Added graphics module + graphics/__init__.py 1.1 andy_robinson + Added graphics module + pdfbase/pdfutils.py 1.14 dinu_gherman + Modified docstrings. +##### 2001/01/19 ##### + lib/abag.py 1.4 rgbecker + Changed comment to be slightly more meaningful + platypus/frames.py 1.12 dinu_gherman + Minor changes. + platypus/doctemplate.py 1.34 dinu_gherman + Minor changes. + lib/randomtext.py 1.4 dinu_gherman + Minor changes. +##### 2001/01/18 ##### + lib/colors.py 1.14 rgbecker + Syncing with pingo +##### 2001/01/12 ##### + pdfgen/textobject.py 1.22 dinu_gherman + Minor neglectable changes. + pdfgen/pdfimages.py 1.7 dinu_gherman + Minor neglectable changes. + pdfgen/pdfgeom.py 1.7 dinu_gherman + Minor neglectable changes. + pdfgen/pathobject.py 1.8 dinu_gherman + Minor neglectable changes. + pdfgen/canvas.py 1.60 dinu_gherman + Minor neglectable changes. + lib/utils.py 1.7 dinu_gherman + Minor neglectable changes. + lib/sequencer.py 1.10 dinu_gherman + Minor neglectable changes. +##### 2001/01/10 ##### + lib/abag.py 1.3 dinu_gherman + Docstring modified. +##### 2001/01/02 ##### + lib/randomtext.py 1.3 johnprecedo + Minor tweaks (removing unneccesary capital letters etc). + lib/randomtext.py 1.2 johnprecedo + Added 5 new themes. +##### 2000/12/29 ##### + test/test_paragraphs.py 1.2 andy_robinson + Added a random text module + platypus/flowables.py 1.15 andy_robinson + Added a random text module + lib/randomtext.py 1.1 andy_robinson + Added a random text module +##### 2000/12/25 ##### + platypus/flowables.py 1.14 rgbecker + Allow for non string file names in Image.__init__ +##### 2000/12/20 ##### + pdfgen/pdfimages.py 1.6 andy_robinson + death to two tabs! +##### 2000/12/19 ##### + pdfgen/pdfimages.py 1.5 rgbecker + Fix typo + pdfgen/pdfimages.py 1.4 rgbecker + Fix to PIL_image, changed to warnOnce +##### 2000/12/18 ##### + platypus/paragraph.py 1.49 rgbecker + More Dinu related fixes/edits + pdfgen/canvas.py 1.59 andy_robinson + Stripped a couple of tabs in an otherwise-space-delimited module +##### 2000/12/17 ##### + platypus/paragraph.py 1.48 rgbecker + Hack to fix Dinu's problem + platypus/doctemplate.py 1.33 rgbecker + Hack to fix Dinu's problem +##### 2000/12/15 ##### + platypus/doctemplate.py 1.32 rgbecker + Better error message + platypus/paragraph.py 1.47 rgbecker + Start on new simpler line splitter + lib/abag.py 1.2 aaron_watters + added a __repr__ for debug purposes + platypus/paragraph.py 1.46 rgbecker + Further Dinuistic fixes for rare cbDefn bugs +##### 2000/12/14 ##### + platypus/test/testplatypus.py 1.21 rgbecker + relativisation of firstLineIndent related code + lib/tocindex.py 1.5 rgbecker + relativisation of firstLineIndent related code + lib/styles.py 1.13 rgbecker + relativisation of firstLineIndent related code + demos/pythonpoint/styles_modern.py 1.8 rgbecker + relativisation of firstLineIndent related code + demos/pythonpoint/styles_horrible.py 1.7 rgbecker + relativisation of firstLineIndent related code + demos/pythonpoint/pythonpoint.py 1.29 rgbecker + relativisation of firstLineIndent related code + lib/__init__.py 1.3 rgbecker + Added RL_DEBUG environ flag + lib/__BUILD.dsw 1.3 rgbecker + Added RL_DEBUG environ flag + pdfbase/pdfmetrics.py 1.18 rgbecker + Added extension debug code + platypus/paragraph.py 1.45 rgbecker + Delete fix + platypus/paragraph.py 1.44 rgbecker + Fixed Dinu's tagmadness4 problem +##### 2000/12/13 ##### + platypus/tables.py 1.34 aaron_watters + added __repr__s and enhanced exception messages for debugging + platypus/paragraph.py 1.43 aaron_watters + added __repr__s and enhanced exception messages for debugging + platypus/frames.py 1.11 aaron_watters + added __repr__s and enhanced exception messages for debugging + platypus/flowables.py 1.13 aaron_watters + added __repr__s and enhanced exception messages for debugging + platypus/test/testplatypus.py 1.20 rgbecker + Try harder than ever for line cleaning and add bg alias + platypus/paraparser.py 1.38 rgbecker + Try harder than ever for line cleaning and add bg alias + platypus/paragraph.py 1.42 rgbecker + Try harder than ever for line cleaning and add bg alias + platypus/paragraph.py 1.41 rgbecker + Layout fixes; the text objects needed moves before & after and the bullet width was calculated after maxWidth was set. Try harder for space/tab removal. + pdfgen/textobject.py 1.21 rgbecker + Layout fixes; Td was absolute not relative to the beginning of the line + platypus/tables.py 1.33 rgbecker + Fixed confusing error message +########### 1.02 Released 2000/12/11 +##### 2000/12/11 ##### + utils/README 1.5 rgbecker + z7-->z9 + platypus/paraparser.py 1.37 rgbecker + Comments and cosmetic changes + platypus/paragraph.py 1.40 rgbecker + Comments and cosmetic changes +##### 2000/12/10 ##### + pdfgen/textobject.py 1.20 andy_robinson + Fixed obscure stringwidth bugs in textobject and bullets + pdfgen/canvas.py 1.58 andy_robinson + Fixed obscure stringwidth bugs in textobject and bullets + pdfbase/pdfmetrics.py 1.17 andy_robinson + Fixed obscure stringwidth bugs in textobject and bullets + lib/styles.py 1.12 andy_robinson + Fixed obscure stringwidth bugs in textobject and bullets + platypus/paraparser.py 1.36 andy_robinson + Added a text background color attribute + platypus/paragraph.py 1.39 andy_robinson + Added a text background color attribute + lib/styles.py 1.11 andy_robinson + Added a text background color attribute + test/test_paragraphs.py 1.1 andy_robinson + Added some paragraph style tests + lib/styles.py 1.10 andy_robinson + Added a title style to support rml2pdf +##### 2000/12/08 ##### + platypus/paragraph.py 1.38 rgbecker + Clean up one word justified paragraph fix. +##### 2000/12/07 ##### + platypus/paragraph.py 1.37 aaron_watters + divide by zero problem for one word paragraphs +##### 2000/12/06 ##### + docs/userguide/ch4_platypus_concepts.py 1.9 aaron_watters + less than should be less than or equal in example + platypus/paragraph.py 1.36 rgbecker + Fixed line wrap problem +##### 2000/12/05 ##### + demos/pythonpoint/pythonpoint.xml 1.12 andy_robinson + Changed link to Michael Wiedmann's DTD + platypus/paraparser.py 1.35 rgbecker + Fixed typo +##### 2000/12/04 ##### + extensions/README 1.2 rgbecker + Spelling corrections + platypus/paraparser.py 1.34 rgbecker + More cosmetic changes + platypus/paragraph.py 1.35 rgbecker + Cosmetic changes + lib/tocindex.py 1.4 rgbecker + Minor fixes to appends +##### 2000/12/01 ##### + platypus/paragraph.py 1.34 aaron_watters + mods permitting late string conversions + pdfgen/canvas.py 1.57 aaron_watters + mods permitting late string conversions + lib/utils.py 1.6 aaron_watters + mods permitting late string conversions +##### 2000/11/29 ##### + platypus/xpreformatted.py 1.12 rgbecker + Semantic Name changes + platypus/paraparser.py 1.33 rgbecker + Semantic Name changes + platypus/paragraph.py 1.33 rgbecker + Semantic Name changes + platypus/__init__.py 1.12 rgbecker + Semantic Name changes + lib/abag.py 1.1 rgbecker + Initial version split from paraparser.py +##### 2000/11/24 ##### + platypus/paragraph.py 1.32 rgbecker + Fixed missing linefeed bug for cbDefn frag at end of line + lib/_rl_accel.c 1.5 rgbecker + Fixed non-working cast, added getInfo +##### 2000/11/23 ##### + platypus/paraparser.py 1.32 rgbecker + Slight optimisation in handle_data for cbdefn frags + platypus/paragraph.py 1.31 rgbecker + Force cbDefn frags to be not Same + platypus/flowables.py 1.12 andy_robinson + Added working table of contents framework + platypus/doctemplate.py 1.31 andy_robinson + Added working table of contents framework + lib/tocindex.py 1.3 andy_robinson + Added working table of contents framework + lib/styles.py 1.9 andy_robinson + Added working table of contents framework +##### 2000/11/21 ##### + platypus/paragraph.py 1.30 rgbecker + Fixed call back handling in _getFragWords + lib/tocindex.py 1.2 andy_robinson + Now formats correctly, no dynamic build stuff yet. + lib/tocindex.py 1.1 andy_robinson + Initial checkin +##### 2000/11/13 ##### + platypus/paraparser.py 1.31 rgbecker + More hacks + platypus/paragraph.py 1.29 rgbecker + More hacks + platypus/paraparser.py 1.30 rgbecker + Fixes to stupid parser + platypus/paraparser.py 1.29 rgbecker + Added onDraw tag to paragraphs + platypus/paragraph.py 1.28 rgbecker + Added onDraw tag to paragraphs + pdfbase/pdfmetrics.py 1.16 rgbecker + Added onDraw tag to paragraphs + demos/odyssey/dodyssey.py 1.9 rgbecker + Added onDraw tag to paragraphs +##### 2000/11/08 ##### + test/unittest.py 1.1 andy_robinson + Added unittest.py and packagized + test/__init__.py 1.1 andy_robinson + Added unittest.py and packagized + pdfbase/pdfdoc.py 1.33 andy_robinson + Changes to fix font encodings + demos/stdfonts/stdfonts.py 1.8 andy_robinson + Changes to fix font encodings + utils/runtests.py 1.15 andy_robinson + Added an exclusion for 'test/unittest.py' in the test suite. + test/00readme.txt 1.2 andy_robinson + Fixed font encoding lookup, metrics for JPython, created test directory + pdfbase/pdfmetrics.py 1.15 andy_robinson + Fixed font encoding lookup, metrics for JPython, created test directory + pdfbase/pdfdoc.py 1.32 andy_robinson + Fixed font encoding lookup, metrics for JPython, created test directory +##### 2000/11/06 ##### + demos/pythonpoint/pythonpoint.xml 1.11 andy_robinson + compression on by default. + demos/pythonpoint/pythonpoint.py 1.28 andy_robinson + compression on by default. +##### 2000/11/05 ##### + platypus/doctemplate.py 1.30 andy_robinson + Fixed page transitions; extended Pythonpoint + pdfgen/canvas.py 1.56 andy_robinson + Fixed page transitions; extended Pythonpoint + pdfbase/pdfdoc.py 1.31 andy_robinson + Fixed page transitions; extended Pythonpoint + demos/pythonpoint/stdparser.py 1.13 andy_robinson + Fixed page transitions; extended Pythonpoint + demos/pythonpoint/pythonpoint.xml 1.10 andy_robinson + Fixed page transitions; extended Pythonpoint + demos/pythonpoint/pythonpoint.py 1.27 andy_robinson + Fixed page transitions; extended Pythonpoint + test/00readme.txt 1.1 andy_robinson + initial checkin + docs/userguide/genuserguide.py 1.38 andy_robinson + UserGuide now resizes template header to fit page size + docs/tools/rltemplate.py 1.8 andy_robinson + UserGuide now resizes template header to fit page size +##### 2000/11/01 ##### + platypus/paragraph.py 1.27 andy_robinson + Added experimental method getActualLineWidths0 +##### 2000/10/26 ##### + platypus/tables.py 1.32 rgbecker + Layout error fixes + platypus/paragraph.py 1.26 rgbecker + Layout error fixes + platypus/flowables.py 1.11 rgbecker + Layout error fixes + docs/userguide/genuserguide.py 1.37 rgbecker + Make page size an optional argument +##### 2000/10/25 ##### + utils/simpledoc.py 1.4 rgbecker + Changed to indirect copyright + utils/runtests.py 1.14 rgbecker + Changed to indirect copyright + utils/license.py 1.4 rgbecker + Changed to indirect copyright + utils/license.c 1.4 rgbecker + Changed to indirect copyright + utils/daily.py 1.38 rgbecker + Changed to indirect copyright + utils/cvslh.py 1.4 rgbecker + Changed to indirect copyright + utils/cvs_check.py 1.7 rgbecker + Changed to indirect copyright + utils/compress_images.py 1.2 rgbecker + Changed to indirect copyright + platypus/test/testtables.py 1.12 rgbecker + Changed to indirect copyright + platypus/xpreformatted.py 1.11 rgbecker + Changed to indirect copyright + platypus/test/testplatypus.py 1.19 rgbecker + Changed to indirect copyright + platypus/tables.py 1.31 rgbecker + Changed to indirect copyright + platypus/paraparser.py 1.28 rgbecker + Changed to indirect copyright + platypus/paragraph.py 1.25 rgbecker + Changed to indirect copyright + platypus/frames.py 1.10 rgbecker + Changed to indirect copyright + platypus/flowables.py 1.10 rgbecker + Changed to indirect copyright + platypus/doctemplate.py 1.29 rgbecker + Changed to indirect copyright + platypus/__init__.py 1.11 rgbecker + Changed to indirect copyright + pdfgen/textobject.py 1.19 rgbecker + Changed to indirect copyright + pdfgen/test/testpdfgen.py 1.16 rgbecker + Changed to indirect copyright + pdfgen/test/testPageMode.py 1.2 rgbecker + Changed to indirect copyright + pdfgen/pdfimages.py 1.3 rgbecker + Changed to indirect copyright + pdfgen/pdfgeom.py 1.6 rgbecker + Changed to indirect copyright + pdfgen/pathobject.py 1.7 rgbecker + Changed to indirect copyright + pdfgen/canvas.py 1.55 rgbecker + Changed to indirect copyright + pdfgen/__init__.py 1.5 rgbecker + Changed to indirect copyright + pdfbase/pdfutils.py 1.13 rgbecker + Changed to indirect copyright + pdfbase/pdfmetrics.py 1.14 rgbecker + Changed to indirect copyright + pdfbase/pdfdoc.py 1.30 rgbecker + Changed to indirect copyright + pdfbase/__init__.py 1.5 rgbecker + Changed to indirect copyright + lib/utils.py 1.5 rgbecker + Changed to indirect copyright + lib/units.py 1.2 rgbecker + Changed to indirect copyright + lib/styles.py 1.8 rgbecker + Changed to indirect copyright + lib/sequencer.py 1.9 rgbecker + Changed to indirect copyright + lib/pagesizes.py 1.5 rgbecker + Changed to indirect copyright + lib/logger.py 1.2 rgbecker + Changed to indirect copyright + lib/fonts.py 1.2 rgbecker + Changed to indirect copyright + lib/enums.py 1.2 rgbecker + Changed to indirect copyright + lib/corp.py 1.2 rgbecker + Changed to indirect copyright + lib/colors.py 1.13 rgbecker + Changed to indirect copyright + lib/_rl_accel.c 1.4 rgbecker + Changed to indirect copyright + lib/__init__.py 1.2 rgbecker + Changed to indirect copyright + extensions/__init__.py 1.2 rgbecker + Changed to indirect copyright + docs/userguide/t_parse.py 1.2 rgbecker + Changed to indirect copyright + docs/userguide/platdemos.py 1.4 rgbecker + Changed to indirect copyright + docs/userguide/genuserguide.py 1.36 rgbecker + Changed to indirect copyright + docs/userguide/examples.py 1.16 rgbecker + Changed to indirect copyright + docs/userguide/ch8_future.py 1.3 rgbecker + Changed to indirect copyright + docs/userguide/ch7_custom.py 1.6 rgbecker + Changed to indirect copyright + docs/userguide/ch6_tables.py 1.16 rgbecker + Changed to indirect copyright + docs/userguide/ch5_paragraphs.py 1.6 rgbecker + Changed to indirect copyright + docs/userguide/ch4_platypus_concepts.py 1.8 rgbecker + Changed to indirect copyright + docs/userguide/ch3_pdffeatures.py 1.7 rgbecker + Changed to indirect copyright + docs/userguide/ch2_graphics.py 1.9 rgbecker + Changed to indirect copyright + docs/userguide/ch1_intro.py 1.11 rgbecker + Changed to indirect copyright + docs/userguide/app_demos.py 1.7 rgbecker + Changed to indirect copyright + docs/tools/yaml2pdf.py 1.8 rgbecker + Changed to indirect copyright + docs/tools/yaml.py 1.9 rgbecker + Changed to indirect copyright + docs/tools/stylesheet.py 1.7 rgbecker + Changed to indirect copyright + docs/tools/rltemplate.py 1.7 rgbecker + Changed to indirect copyright + docs/tools/codegrab.py 1.5 rgbecker + Changed to indirect copyright + demos/tests/testdemos.py 1.5 rgbecker + Changed to indirect copyright + demos/stdfonts/stdfonts.py 1.7 rgbecker + Changed to indirect copyright + demos/pythonpoint/styles_modern.py 1.7 rgbecker + Changed to indirect copyright + demos/pythonpoint/styles_horrible.py 1.6 rgbecker + Changed to indirect copyright + demos/pythonpoint/stdparser.py 1.12 rgbecker + Changed to indirect copyright + demos/pythonpoint/pythonpoint.py 1.26 rgbecker + Changed to indirect copyright + demos/pythonpoint/customshapes.py 1.3 rgbecker + Changed to indirect copyright + demos/py2pdf/idle_print.py 1.7 rgbecker + Changed to indirect copyright + demos/odyssey/odyssey.py 1.9 rgbecker + Changed to indirect copyright + demos/odyssey/fodyssey.py 1.14 rgbecker + Changed to indirect copyright + demos/odyssey/dodyssey.py 1.8 rgbecker + Changed to indirect copyright + demos/gadflypaper/gfe.py 1.12 rgbecker + Changed to indirect copyright + demos/colors/colortest.py 1.2 rgbecker + Changed to indirect copyright + __init__.py 1.7 rgbecker + Changed to indirect copyright + README 1.2 rgbecker + Changed to indirect copyright +##### 2000/10/24 ##### + license.txt 1.1 rgbecker + Initial separate license file + utils/copyrite.py 1.1 rgbecker + Copyright changes + utils/copyr.txt 1.1 rgbecker + Copyright changes + pdfgen/pdfimages.py 1.2 rgbecker + Fix zlib import bug + pdfgen/pdfimages.py 1.1 aaron_watters + image functionality factored out of canvas.py initial checkin. tests pass + pdfgen/canvas.py 1.54 aaron_watters + sliced out image functionality (oops adding in pdfimages.py next) tests pass +##### 2000/10/23 ##### + platypus/paragraph.py 1.24 rgbecker + Added J Alet's bug case + demos/pythonpoint/pythonpoint.xml 1.9 andy_robinson + Corrected bad end tags and added links to Michael Wiedmann's DTD +##### 2000/10/22 ##### + demos/pythonpoint/pythonpoint.xml 1.8 andy_robinson + Modified the documentation to correct Michael Wiedmann's error reports. +##### 2000/10/21 ##### + lib/sgmlop.dsp 1.2 rgbecker + hyphen.c includes substrings.pl in comment + lib/pyHnj.dsp 1.2 rgbecker + hyphen.c includes substrings.pl in comment + lib/hyphen.c 1.2 rgbecker + hyphen.c includes substrings.pl in comment + lib/_rl_accel.dsp 1.2 rgbecker + hyphen.c includes substrings.pl in comment + lib/README.extensions 1.2 rgbecker + hyphen.c includes substrings.pl in comment +##### 2000/10/19 ##### + pdfbase/pdfdoc.py 1.29 rgbecker + Aaron's latest update from the newslist +##### 2000/10/18 ##### + pdfbase/pdfdoc.py 1.28 aaron_watters + undid last checkin and added an option for a default outline (different fix) + pdfbase/pdfdoc.py 1.27 aaron_watters + moved the outline preprocessing step into the format method (fixes testing error) + pdfgen/canvas.py 1.53 aaron_watters + complete revision of pdfdoc. Not finished (compression missing, testing needed) I got Robin's last change in at the last moment :) + pdfbase/pdfdoc.py 1.26 aaron_watters + complete revision of pdfdoc. Not finished (compression missing, testing needed) I got Robin's last change in at the last moment :) +##### 2000/10/15 ##### + pdfgen/test/testPageMode.py 1.1 andy_robinson + Test of pageMode functions. + pdfgen/canvas.py 1.52 andy_robinson + Added showFullScreen0 + pdfbase/pdfdoc.py 1.25 andy_robinson + Added showFullScreen0 +########### 1.01 Released 2000/10/10 +##### 2000/10/05 ##### + pdfbase/pdfutils.py 1.12 rgbecker + Fix indentation + pdfbase/pdfutils.py 1.11 rgbecker + Changes to make freezing easier + pdfbase/pdfmetrics.py 1.13 rgbecker + Changes to make freezing easier + lib/xmllib.py 1.4 rgbecker + Changes to make freezing easier + lib/utils.py 1.4 rgbecker + Changes to make freezing easier +##### 2000/10/04 ##### + platypus/xpreformatted.py 1.10 rgbecker + Fixed copying bug +##### 2000/10/03 ##### + lib/styles.py 1.7 rgbecker + Code needs a firstLineIndent same as normal lineIndent + platypus/paragraph.py 1.23 rgbecker + Justified XPreformatteds are OK +##### 2000/10/02 ##### + platypus/frames.py 1.9 rgbecker + Fixed the atTop test again + platypus/frames.py 1.8 rgbecker + Fixed the atTop test + platypus/xpreformatted.py 1.9 rgbecker + Added license + platypus/xpreformatted.py 1.8 rgbecker + Splitting fixes. Mostly caused by XPreformatted not doing it right. + platypus/paragraph.py 1.22 rgbecker + Splitting fixes. Mostly caused by XPreformatted not doing it right. +##### 2000/10/01 ##### + platypus/frames.py 1.7 rgbecker + Fixed atTop bugs thanks to the effbot + platypus/xpreformatted.py 1.7 rgbecker + Cleaning up XPreformatted + platypus/xpreformatted.py 1.6 rgbecker + Removed space split and readding +##### 2000/09/27 ##### + pdfbase/pdfmetrics.py 1.12 andy_robinson + more work on metrics and encodings for custom fonts + pdfbase/pdfmetrics.py 1.11 andy_robinson + Begun work on loading new AFM files + lib/colors.py 1.12 rgbecker + Fix for sun compiler + lib/_rl_accel.c 1.3 rgbecker + Fix for sun compiler +##### 2000/09/25 ##### + lib/styles.py 1.6 andy_robinson + added has_key method to stylesheet1 + demos/stdfonts/stdfonts.py 1.6 andy_robinson + Allows you to generate hex, decimal or octal labels +##### 2000/09/08 ##### + pdfbase/pdfdoc.py 1.24 rgbecker + Paul Eddington's unix tell() returns a LongIntType bugfix +##### 2000/09/04 ##### + pdfgen/canvas.py 1.51 rgbecker + Fix spurious comment reference to layout +##### 2000/09/01 ##### + docs/userguide/ch2_graphics.py 1.8 rgbecker + Correct size setting thanks to Werner Louche + pdfgen/textobject.py 1.18 rgbecker + Improved optimisation checks + pdfgen/canvas.py 1.50 rgbecker + Improved optimisation checks + pdfgen/textobject.py 1.17 rgbecker + Fixed potential 'Td' bug +##### 2000/08/31 ##### + pdfgen/canvas.py 1.49 rgbecker + Fix transform optimisation +##### 2000/08/29 ##### + lib/pyHnj.dsp 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/utils.py 1.3 rgbecker + Initial version of D Yoo's pyHnj + lib/pyHnjmodule.c 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/hyphen.mashed 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/hyphen.h 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/hyphen.c 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/hnjalloc.h 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/hnjalloc.c 1.1 rgbecker + Initial version of D Yoo's pyHnj + lib/__BUILD.dsw 1.2 rgbecker + Initial version of D Yoo's pyHnj + lib/Setup.in 1.2 rgbecker + Initial version of D Yoo's pyHnj + lib/Makefile.pre.in 1.3 rgbecker + Initial version of D Yoo's pyHnj + extensions/__init__.py 1.1 rgbecker + Initial version + extensions/README 1.1 rgbecker + Initial version +##### 2000/08/25 ##### + platypus/xpreformatted.py 1.5 rgbecker + Further tweaks of XPreformatted + platypus/paragraph.py 1.21 rgbecker + Further tweaks of XPreformatted + docs/userguide/ch6_tables.py 1.15 rgbecker + Further tweaks of XPreformatted +##### 2000/08/24 ##### + platypus/xpreformatted.py 1.4 rgbecker + XPreformatted fixed empty lines and leading space + platypus/paragraph.py 1.20 rgbecker + XPreformatted fixed empty lines and leading space + lib/utils.py 1.2 rgbecker + Added _className func + docs/userguide/ch6_tables.py 1.14 rgbecker + harder test of XPreformatted + docs/userguide/ch6_tables.py 1.13 rgbecker + Added blank lines to XPreformatted sample + platypus/xpreformatted.py 1.3 rgbecker + Fixes to _dedenter + platypus/flowables.py 1.9 rgbecker + Fixes to _dedenter + docs/userguide/genuserguide.py 1.35 rgbecker + Added Mac Intro + docs/userguide/ch1_intro.py 1.10 rgbecker + Added Mac Intro + docs/images/Python_1.6a2_HINT.gif 1.1 rgbecker + Initial versions for Mac intro + docs/images/Python_1.6a2.gif 1.1 rgbecker + Initial versions for Mac intro + docs/images/Edit_Prefs.gif 1.1 rgbecker + Initial versions for Mac intro + platypus/xpreformatted.py 1.2 rgbecker + XPreformatted first fixes; now runs + platypus/flowables.py 1.8 rgbecker + XPreformatted first fixes; now runs + platypus/__init__.py 1.10 rgbecker + XPreformatted first fixes; now runs + docs/userguide/genuserguide.py 1.34 rgbecker + Preformatted docco added + docs/userguide/ch6_tables.py 1.12 rgbecker + Preformatted docco added + pdfbase/pdfdoc.py 1.23 aaron_watters + change to PDFLiteral to support "lazy string conversions" (to support lazy crosslinks) +##### 2000/08/23 ##### + platypus/xpreformatted.py 1.1 rgbecker + Initial semi working version + platypus/paragraph.py 1.19 rgbecker + Preparing for cleanup +##### 2000/08/20 ##### + pdfgen/canvas.py 1.48 andy_robinson + Changed an argument syntax for clarity + demos/pythonpoint/leftlogo.a85 1.3 andy_robinson + Added as binary file + demos/pythonpoint/leftlogo.a85 1.2 andy_robinson + Removed ASCII file as Mac was corrupting it +##### 2000/08/17 ##### + platypus/paragraph.py 1.18 rgbecker + Various brutal changes to paragraph, canvas and textobject for speed/size + pdfgen/textobject.py 1.16 rgbecker + Various brutal changes to paragraph, canvas and textobject for speed/size + pdfgen/canvas.py 1.47 rgbecker + Various brutal changes to paragraph, canvas and textobject for speed/size + demos/odyssey/odyssey.txt 1.8 rgbecker + Various brutal changes to paragraph, canvas and textobject for speed/size + platypus/paraparser.py 1.27 rgbecker + Changed test formatting +##### 2000/08/16 ##### + pdfbase/pdfmetrics.py 1.10 rgbecker + Start using _rl_accel +##### 2000/08/12 ##### + lib/colors.py 1.11 rgbecker + Fix missing parens +##### 2000/08/11 ##### + lib/colors.py 1.10 rgbecker + added rgb2cmyk + docs/userguide/ch7_custom.py 1.5 rgbecker + Added rotated image derived class example +##### 2000/08/09 ##### + pdfbase/pdfdoc.py 1.22 rgbecker + Andy's Symbol/Zapf font fix +##### 2000/08/03 ##### + pdfbase/pdfutils.py 1.10 rgbecker + Robin's take on Bernhard Herzog's fix to A85 decode + platypus/frames.py 1.6 rgbecker + Changing to packer led positioning + platypus/flowables.py 1.7 rgbecker + Changing to packer led positioning + lib/_rl_accel.c 1.2 rgbecker + Linux/GnuC fixes + lib/Makefile.pre.in 1.2 rgbecker + Linux/GnuC fixes +##### 2000/08/02 ##### + docs/userguide/genuserguide.py 1.33 rgbecker + Table cell updates + docs/userguide/ch6_tables.py 1.11 rgbecker + Table cell updates + docs/images/replogo.gif 1.3 rgbecker + Readded with -kb flag + docs/images/replogo.gif 1.2 rgbecker + Removed bad replogo.gif + platypus/tables.py 1.30 rgbecker + Flowable cell fixes/changes +##### 2000/08/01 ##### + platypus/tables.py 1.29 rgbecker + Fixed flowable valign bottom/top middle maybe + platypus/tables.py 1.28 rgbecker + Additions/Improvements to LINE CMD Splitting + pdfgen/textobject.py 1.15 rgbecker + Converted to using fp_str + pdfgen/canvas.py 1.46 rgbecker + Converted to using fp_str + utils/runtests.py 1.13 rgbecker + Added -time flag + lib/README.extensions 1.1 rgbecker + Initial version + lib/Makefile.pre.in 1.1 rgbecker + Initial version + pdfbase/pdfutils.py 1.9 rgbecker + moved accelerators to lib + pdfbase/pdfmetrics.py 1.9 rgbecker + moved accelerators to lib + pdfbase/_streams.c 1.2 rgbecker + moved accelerators to lib + pdfbase/_pdfmetrics.c 1.3 rgbecker + moved accelerators to lib + pdfbase/Setup.in 1.2 rgbecker + moved accelerators to lib + lib/Setup.in 1.1 rgbecker + Initial version + lib/utils.py 1.1 rgbecker + Initial version + lib/sgmlop.dsp 1.1 rgbecker + Initial version + lib/sgmlop.c 1.1 rgbecker + Initial version + lib/_rl_accel.dsp 1.1 rgbecker + Initial version + lib/_rl_accel.c 1.1 rgbecker + Initial version + lib/__BUILD.dsw 1.1 rgbecker + Initial version + pdfgen/textobject.py 1.14 andy_robinson + Updated old doc string +##### 2000/07/31 ##### + pdfgen/canvas.py 1.45 rgbecker + B Herzog fix to dimension formats +##### 2000/07/30 ##### + platypus/frames.py 1.5 rgbecker + geometry changing attributes now work +##### 2000/07/28 ##### + pdfgen/textobject.py 1.13 rgbecker + Bernhard herzog inspired fixes + pdfgen/canvas.py 1.44 rgbecker + Bernhard herzog inspired fixes +##### 2000/07/26 ##### + pdfbase/pdfmetrics.py 1.8 rgbecker + Accelerator rearrangements + pdfbase/_pdfmetrics.c 1.2 rgbecker + Accelerator rearrangements + lib/logger.py 1.1 rgbecker + Initial version +##### 2000/07/25 ##### + docs/userguide/ch3_pdffeatures.py 1.6 rgbecker + Bruce Eckel spotted these typos. + docs/userguide/ch2_graphics.py 1.7 rgbecker + Bruce Eckel spotted these typos. + docs/userguide/ch1_intro.py 1.9 rgbecker + Bruce Eckel spotted these typos. +##### 2000/07/20 ##### + platypus/doctemplate.py 1.28 rgbecker + Added pagesize to canvasmaker call + README.pdfgen.txt 1.8 rgbecker + Release 1.00 Administration + README 1.1 rgbecker + Release 1.00 Administration +########### 1.00 Released 2000/07/20 +demos/pythonpoint/pythonpoint.py + 1.25 2000/07/15 21:13:29 Minor additions to styles etc. andy_robinson +docs/userguide/app_demos.py + 1.6 2000/07/18 13:27:47 fixed typos. dinu_gherman + 1.5 2000/07/18 10:03:45 fixed typos, minimal changes to py2pdf. dinu_gherman +docs/userguide/ch1_intro.py + 1.8 2000/07/18 09:58:11 fixed typos. dinu_gherman +docs/userguide/ch2_graphics.py + 1.6 2000/07/18 09:59:25 fixed typos. dinu_gherman +docs/userguide/ch3_pdffeatures.py + 1.5 2000/07/18 10:00:43 fixed typos. dinu_gherman +docs/userguide/ch4_platypus_concepts.py + 1.7 2000/07/18 10:02:06 fixed typos. dinu_gherman +docs/userguide/ch5_paragraphs.py + 1.5 2000/07/18 13:24:38 fixed typos. dinu_gherman +docs/userguide/ch5_tables.py + 1.2 2000/07/16 14:01:44 ch5_tables.py was a hangover and should have been deleted before. Note to AR if and when we do this again never give the chapters numbers as that is extremely confusing. rgbecker +docs/userguide/ch6_tables.py + 1.10 2000/07/18 13:25:38 fixed typos. dinu_gherman + 1.9 2000/07/14 14:07:48 Image probs again rgbecker +docs/userguide/ch7_custom.py + 1.4 2000/07/18 13:26:17 fixed typos. dinu_gherman + 1.3 2000/07/16 14:07:56 most_chapters.py was also irrelevant rgbecker +docs/userguide/genuserguide.py + 1.32 2000/07/18 13:29:19 removed header dashes. dinu_gherman +docs/userguide/lj8100.jpg + 1.2 2000/07/14 14:07:48 Image probs again rgbecker +docs/userguide/most_chapters.py + 1.6 2000/07/16 14:07:56 most_chapters.py was also irrelevant rgbecker + 1.5 2000/07/16 14:01:44 ch5_tables.py was a hangover and should have been deleted before. Note to AR if and when we do this again never give the chapters numbers as that is extremely confusing. rgbecker +lib/xmllib.py + 1.3 2000/07/18 09:31:53 Uncommented methods in fast parser rgbecker +pdfbase/Setup.in + 1.1 2000/07/19 19:06:39 Added _pdfmetrics.c rgbecker +pdfbase/_pdfmetrics.c + 1.1 2000/07/19 19:06:39 Added _pdfmetrics.c rgbecker +pdfbase/pdfmetrics.py + 1.7 2000/07/19 19:06:39 Added _pdfmetrics.c rgbecker +platypus/tables.py + 1.27 2000/07/20 13:32:33 Started debugging Table split rgbecker +utils/daily.py + 1.37 2000/07/14 14:27:26 Wasn't doing docs for releases rgbecker +utils/runtests.py + 1.12 2000/07/19 13:44:54 Added -prof option rgbecker +########### 0.95 Released 2000/07/14 +demos/py2pdf/demo.py + 1.5 2000/06/27 08:58:19 Try to ensure test file removal rgbecker +demos/py2pdf/py2pdf.py + 1.10 2000/06/28 11:12:40 Stop auto testing rgbecker +demos/pythonpoint/pythonpoint.py + 1.24 2000/07/12 14:23:12 Table argument order changed rgbecker + 1.23 2000/07/12 06:33:43 Added Speaker Notes facility andy_robinson + 1.22 2000/07/10 15:25:47 Added tables to PythonPoint andy_robinson +demos/pythonpoint/pythonpoint.xml + 1.7 2000/07/12 06:33:43 Added Speaker Notes facility andy_robinson + 1.6 2000/07/10 15:25:47 Added tables to PythonPoint andy_robinson +demos/pythonpoint/stdparser.py + 1.10 2000/07/11 15:00:39 Added end_spacer rgbecker + 1.9 2000/07/10 15:25:47 Added tables to PythonPoint andy_robinson +lib/sequencer.py + 1.8 2000/07/10 11:58:35 Pre-incrementing bug fixed andy_robinson + 1.7 2000/07/08 07:11:21 Changed to pre-increment andy_robinson + 1.6 2000/06/21 19:46:43 Added Roman formatters rgbecker +pdfbase/_streams.c + 1.1 2000/07/05 12:20:27 Ascii85 fixes/additions rgbecker +pdfbase/pdfdoc.py + 1.21 2000/06/26 15:58:22 Simple fix to widths problem rgbecker + 1.20 2000/06/23 17:51:22 /Producer (ReportLab http://www.reportlab.com) in document aaron_watters +pdfbase/pdfmetrics.py + 1.6 2000/06/26 15:58:22 Simple fix to widths problem rgbecker +pdfbase/pdfutils.py + 1.8 2000/07/05 12:20:27 Ascii85 fixes/additions rgbecker + 1.7 2000/06/30 15:29:59 Allow for non-caching of images rgbecker +pdfgen/canvas.py + 1.43 2000/06/30 15:27:55 Allow for non-caching of images rgbecker + 1.42 2000/06/26 15:58:22 Simple fix to widths problem rgbecker +pdfgen/pathobject.py + 1.6 2000/07/12 16:07:09 fixed nameerror and path.circle bug aaron_watters +platypus/__init__.py + 1.9 2000/07/13 11:41:00 Added KeepTogether rgbecker + 1.8 2000/06/27 10:07:55 Added CondPageBreak rgbecker + 1.7 2000/06/21 12:27:42 remove UserDocTemplate, but add Andy's hook methods rgbecker +platypus/doctemplate.py + 1.27 2000/07/10 11:58:35 Pre-incrementing bug fixed andy_robinson + 1.26 2000/07/07 16:21:12 Cosmetics rgbecker + 1.25 2000/07/06 12:40:37 Push canvas into flowables during wrap/split rgbecker + 1.24 2000/07/05 12:22:21 Force _calc in SimpleDocTemplae.build rgbecker + 1.23 2000/07/03 15:39:51 Documentation fixes rgbecker + 1.22 2000/06/28 14:52:43 Documentation changes rgbecker + 1.21 2000/06/26 15:58:22 Simple fix to widths problem rgbecker + 1.20 2000/06/21 12:27:42 remove UserDocTemplate, but add Andy's hook methods rgbecker + 1.19 2000/06/20 21:56:17 re-synching after sourceforge went weird andy_robinson +platypus/flowables.py + 1.6 2000/07/13 11:42:10 removed debug prints rgbecker + 1.5 2000/07/13 11:41:00 Added KeepTogether rgbecker + 1.4 2000/06/27 10:07:55 Added CondPageBreak rgbecker +platypus/frames.py + 1.4 2000/07/07 16:21:12 Cosmetics rgbecker + 1.3 2000/07/06 12:40:38 Push canvas into flowables during wrap/split rgbecker +platypus/paragraph.py + 1.17 2000/07/14 10:29:50 The Paragraph.split method was wrongly assuming that the firstLineIndent should reset to zero. It should always reset to leftIndent! rgbecker + 1.16 2000/07/03 15:39:51 Documentation fixes rgbecker + 1.15 2000/06/23 13:13:54 Fixes to splitting code rgbecker +platypus/paraparser.py + 1.26 2000/07/10 23:53:46 Changed base of seqdefault tag to 0 andy_robinson + 1.25 2000/07/04 10:50:33 Sequencer fixes rgbecker +platypus/tables.py + 1.26 2000/07/12 15:36:56 Allow automatic leading in FONT command rgbecker + 1.25 2000/07/12 15:26:46 INNERGRID was dumb rgbecker + 1.24 2000/07/12 15:25:42 INNERGRID was dumb rgbecker + 1.23 2000/07/12 15:18:16 Leading changes fixed rgbecker + 1.22 2000/07/12 14:23:12 Table argument order changed rgbecker + 1.21 2000/07/12 09:05:17 Fixed tuple size bug rgbecker + 1.20 2000/07/11 14:29:45 Table splitting start rgbecker + 1.19 2000/07/10 15:25:47 Added tables to PythonPoint andy_robinson + 1.18 2000/07/08 15:30:04 Cosmetics and error testing rgbecker + 1.17 2000/07/07 16:22:10 Fix auto hieght stuff rgbecker + 1.16 2000/07/07 10:23:36 First attempt at VALIGN rgbecker + 1.15 2000/07/06 14:05:55 Adjusted doc string rgbecker + 1.14 2000/07/06 12:41:47 First try at auto sizing rgbecker + 1.13 2000/06/29 17:55:19 support explicit \n line splitting in cells aaron_watters +platypus/test/testplatypus.py + 1.18 2000/07/12 14:23:12 Table argument order changed rgbecker +platypus/test/testtables.py + 1.11 2000/07/12 14:23:12 Table argument order changed rgbecker +utils/README + 1.2 2000/07/13 14:55:30 Added detailed release rgbecker +########### 0.94 Released 2000/06/20 +demos/gadflypaper/gfe.py + 1.11 2000/06/01 16:27:56 pageSize is wrong at present rgbecker + 1.10 2000/06/01 15:23:06 Platypus re-organisation rgbecker +demos/odyssey/dodyssey.py + 1.7 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.6 2000/06/01 09:41:11 test filename case fix rgbecker +demos/odyssey/fodyssey.py + 1.13 2000/06/01 16:27:56 pageSize is wrong at present rgbecker + 1.12 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.11 2000/06/01 09:41:12 test filename case fix rgbecker +demos/odyssey/odyssey.py + 1.8 2000/06/01 09:41:12 test filename case fix rgbecker +demos/odyssey/odyssey.txt + 1.7 2000/05/31 10:12:45 xml tag added rgbecker +demos/py2pdf/demo.py + 1.4 2000/06/19 11:14:03 Global sequencer put in the 'story builder'. andy_robinson +demos/py2pdf/py2pdf.py + 1.9 2000/06/19 11:14:03 Global sequencer put in the 'story builder'. andy_robinson +demos/pythonpoint/pythonpoint.py + 1.21 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.20 2000/05/23 14:06:45 Preformatted objects now know how to split themselves. andy_robinson +demos/pythonpoint/stdparser.py + 1.8 2000/06/01 15:23:06 Platypus re-organisation rgbecker +demos/pythonpoint/styles_horrible.py + 1.5 2000/06/01 15:23:06 Platypus re-organisation rgbecker +demos/pythonpoint/styles_modern.py + 1.6 2000/06/01 15:23:06 Platypus re-organisation rgbecker +docs/00readme.txt + 1.1 2000/06/05 16:38:54 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:38:54 initial import andy_robinson +docs/images/replogo.a85 + 1.1 2000/06/05 16:39:04 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:39:04 initial import andy_robinson +docs/images/replogo.gif + 1.1 2000/06/05 16:39:04 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:39:04 initial import andy_robinson +docs/reference/build.bat + 1.4 2000/06/19 23:52:31 rltemplate now simple, based on UserDocTemplate andy_robinson + 1.3 2000/06/17 07:46:45 Small text changes andy_robinson + 1.2 2000/06/12 11:13:09 Added sequencer tags to paragraph parser andy_robinson + 1.1 2000/06/07 13:39:22 Added some text to the first page of reference, and a build batch file andy_robinson +docs/reference/reference.yml + 1.8 2000/06/19 23:52:31 rltemplate now simple, based on UserDocTemplate andy_robinson + 1.7 2000/06/17 07:46:45 Small text changes andy_robinson + 1.6 2000/06/14 21:22:52 Added docs for library andy_robinson + 1.5 2000/06/12 11:26:34 Numbered list added andy_robinson + 1.4 2000/06/12 11:13:09 Added sequencer tags to paragraph parser andy_robinson + 1.3 2000/06/09 01:44:24 added automatic generation for pathobject and textobject modules. aaron_watters + 1.2 2000/06/07 13:39:22 Added some text to the first page of reference, and a build batch file andy_robinson + 1.1 2000/06/05 16:39:04 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:39:04 initial import andy_robinson +docs/tools/codegrab.py + 1.3 2000/06/20 11:35:08 Python 1.5.1 compatibility fixes rgbecker + 1.2 2000/06/14 21:22:52 Added docs for library andy_robinson + 1.1 2000/06/05 16:38:57 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:38:57 initial import andy_robinson +docs/tools/rltemplate.py + 1.2 2000/06/19 23:52:31 rltemplate now simple, based on UserDocTemplate andy_robinson + 1.1 2000/06/17 07:48:41 Added a separate rltemplate.py file to contain our standard document template. andy_robinson +docs/tools/stylesheet.py + 1.2 2000/06/14 21:22:52 Added docs for library andy_robinson + 1.1 2000/06/13 20:03:30 Docs is now a package; stylesheet separated out andy_robinson +docs/tools/yaml.py + 1.5 2000/06/20 11:34:47 Python 1.5.1 compatibility fixes rgbecker + 1.4 2000/06/19 15:54:21 Changes to imports again sigh mumble rgbecker + 1.3 2000/06/14 21:22:52 Added docs for library andy_robinson + 1.2 2000/06/13 20:03:30 Docs is now a package; stylesheet separated out andy_robinson + 1.1 2000/06/05 16:38:56 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:38:56 initial import andy_robinson +docs/tools/yaml2pdf.py + 1.6 2000/06/19 15:54:21 Changes to imports again sigh mumble rgbecker + 1.5 2000/06/17 07:47:20 Factored out rltemplate andy_robinson + 1.4 2000/06/13 20:03:30 Docs is now a package; stylesheet separated out andy_robinson + 1.3 2000/06/12 11:26:34 Numbered list added andy_robinson + 1.2 2000/06/07 21:08:12 Change to style for definition lists andy_robinson + 1.1 2000/06/05 16:38:55 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:38:55 initial import andy_robinson +docs/userguide/examples.py + 1.3 2000/06/19 21:13:02 2nd try. more text aaron_watters + 1.2 2000/06/17 06:54:54 Example had a reference to layout.py, removed andy_robinson + 1.1 2000/06/17 02:59:05 initial checkin. examples file for userguide generation aaron_watters +docs/userguide/genuserguide.py + 1.2 2000/06/19 21:13:02 2nd try. more text aaron_watters + 1.1 2000/06/17 02:57:56 initial checkin. user guide generation framework. aaron_watters +docs/userguide/platdemos.py + 1.1 2000/06/05 16:39:04 Initial revision andy_robinson + 1.1.1.1 2000/06/05 16:39:04 initial import andy_robinson +lib/colors.py + 1.9 2000/06/14 21:17:30 Some relative imports fixed andy_robinson + 1.8 2000/05/26 09:43:44 stringToColor-->toColor rgbecker +lib/pagesizes.py + 1.4 2000/06/14 21:17:30 Some relative imports fixed andy_robinson +lib/sequencer.py + 1.5 2000/06/19 11:14:03 Global sequencer put in the 'story builder'. andy_robinson + 1.4 2000/06/12 11:27:17 Added Sequencer and associated XML tags andy_robinson + 1.3 2000/06/11 21:34:01 Largely complete class for numbering lists, figures and chapters andy_robinson + 1.2 2000/06/09 16:18:19 Doc strings, sequencer andy_robinson + 1.1 2000/06/01 15:23:06 Platypus re-organisation rgbecker +pdfbase/pdfdoc.py + 1.19 2000/06/01 09:44:26 SaveToFile: only close the file if we opened it. Aggregated from types imports to module level. rgbecker +pdfgen/canvas.py + 1.41 2000/06/09 16:18:19 Doc strings, sequencer andy_robinson + 1.40 2000/06/09 01:45:22 Lots of documentation additions and changes. aaron_watters + 1.39 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.38 2000/05/26 09:44:40 generalised colors slightly rgbecker + 1.37 2000/05/23 14:06:45 Preformatted objects now know how to split themselves. andy_robinson +pdfgen/pathobject.py + 1.5 2000/06/09 08:17:43 Full qualified a local import in pdfgen package andy_robinson +platypus/__init__.py + 1.6 2000/06/19 23:51:23 Added UserDocTemplate class, and paragraph.getPlainText() andy_robinson + 1.5 2000/06/01 15:23:06 Platypus re-organisation rgbecker +platypus/doctemplate.py + 1.18 2000/06/19 23:51:23 Added UserDocTemplate class, and paragraph.getPlainText() andy_robinson + 1.17 2000/06/19 11:14:03 Global sequencer put in the 'story builder'. andy_robinson + 1.16 2000/06/16 13:49:20 new build parameters to allow alternate filename and canvas implementation (in order to support slideshow summary mode, for example, or embedding one document in another). aaron_watters + 1.15 2000/06/13 13:03:31 more documentation changes aaron_watters + 1.14 2000/06/01 16:27:56 pageSize is wrong at present rgbecker + 1.13 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.12 2000/05/26 10:27:37 Fixed infinite recursion bug rgbecker +platypus/flowables.py + 1.3 2000/06/13 13:03:31 more documentation changes aaron_watters + 1.2 2000/06/01 16:27:56 pageSize is wrong at present rgbecker + 1.1 2000/06/01 15:23:06 Platypus re-organisation rgbecker +platypus/frames.py + 1.2 2000/06/13 13:03:31 more documentation changes aaron_watters + 1.1 2000/06/01 15:23:06 Platypus re-organisation rgbecker +platypus/layout.py + 1.31 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.30 2000/05/23 14:08:02 Preformatted objects now splitting andy_robinson +platypus/paragraph.py + 1.14 2000/06/19 23:51:23 Added UserDocTemplate class, and paragraph.getPlainText() andy_robinson + 1.13 2000/06/19 11:14:03 Global sequencer put in the 'story builder'. andy_robinson + 1.12 2000/06/13 13:03:31 more documentation changes aaron_watters + 1.11 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.10 2000/05/31 10:12:20 xml tag added rgbecker +platypus/paraparser.py + 1.24 2000/06/19 11:14:03 Global sequencer put in the 'story builder'. andy_robinson + 1.23 2000/06/13 04:11:49 noted replication of XML markup comment between paraparser.py and paragraph.py aaron_watters + 1.22 2000/06/12 11:27:17 Added Sequencer and associated XML tags andy_robinson + 1.21 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.20 2000/05/31 10:12:20 xml tag added rgbecker + 1.19 2000/05/26 09:49:23 Color fixes; thanks to J Alet rgbecker + 1.18 2000/05/20 15:36:42 Removed 1.5.2-style getattr call andy_robinson +platypus/tables.py + 1.12 2000/06/13 13:03:31 more documentation changes aaron_watters + 1.11 2000/06/01 15:23:06 Platypus re-organisation rgbecker + 1.10 2000/05/26 09:49:23 Color fixes; thanks to J Alet rgbecker +platypus/test/testplatypus.py + 1.17 2000/06/01 15:23:06 Platypus re-organisation rgbecker +platypus/test/testtables.py + 1.10 2000/06/01 16:27:56 pageSize is wrong at present rgbecker + 1.9 2000/06/01 15:23:06 Platypus re-organisation rgbecker diff --git a/bin/reportlab/demos/colors/colortest.py b/bin/reportlab/demos/colors/colortest.py new file mode 100644 index 00000000000..7845c2cd620 --- /dev/null +++ b/bin/reportlab/demos/colors/colortest.py @@ -0,0 +1,105 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/colors/colortest.py +import reportlab.pdfgen.canvas +from reportlab.lib import colors +from reportlab.lib.units import inch + + +def run(): + c = reportlab.pdfgen.canvas.Canvas('colortest.pdf') + + #do a test of CMYK interspersed with RGB + + #first do RGB values + framePage(c, 'Color Demo - RGB Space and CMYK spaces interspersed' ) + + y = 700 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'cyan') + c.setFillColorCMYK(1,0,0,0) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'red') + c.setFillColorRGB(1,0,0) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'magenta') + c.setFillColorCMYK(0,1,0,0) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'green') + c.setFillColorRGB(0,1,0) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'yellow') + c.setFillColorCMYK(0,0,1,0) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'blue') + c.setFillColorRGB(0,0,1) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + c.setFillColorRGB(0,0,0) + c.drawString(100, y, 'black') + c.setFillColorCMYK(0,0,0,1) + c.rect(200, y, 300, 30, fill=1) + y = y - 40 + + + c.showPage() + + #do all named colors + framePage(c, 'Color Demo - RGB Space - page %d' % c.getPageNumber()) + + all_colors = reportlab.lib.colors.getAllNamedColors().items() + all_colors.sort() # alpha order by name + c.setFont('Times-Roman', 12) + c.drawString(72,730, 'This shows all the named colors in the HTML standard.') + y = 700 + for (name, color) in all_colors: + c.setFillColor(colors.black) + c.drawString(100, y, name) + c.setFillColor(color) + c.rect(200, y-10, 300, 30, fill=1) + y = y - 40 + if y < 100: + c.showPage() + framePage(c, 'Color Demo - RGB Space - page %d' % c.getPageNumber()) + y = 700 + + + + + c.save() + +def framePage(canvas, title): + canvas.setFont('Times-BoldItalic',20) + canvas.drawString(inch, 10.5 * inch, title) + + canvas.setFont('Times-Roman',10) + canvas.drawCentredString(4.135 * inch, 0.75 * inch, + 'Page %d' % canvas.getPageNumber()) + + #draw a border + canvas.setStrokeColorRGB(1,0,0) + canvas.setLineWidth(5) + canvas.line(0.8 * inch, inch, 0.8 * inch, 10.75 * inch) + #reset carefully afterwards + canvas.setLineWidth(1) + canvas.setStrokeColorRGB(0,0,0) + +if __name__ == '__main__': + run() diff --git a/bin/reportlab/demos/gadflypaper/00readme.txt b/bin/reportlab/demos/gadflypaper/00readme.txt new file mode 100644 index 00000000000..f50a9966846 --- /dev/null +++ b/bin/reportlab/demos/gadflypaper/00readme.txt @@ -0,0 +1,3 @@ +This is Aaron Watters' first script; +it renders his paper for IPC8 into +PDF. A fascinating read, as well. \ No newline at end of file diff --git a/bin/reportlab/demos/gadflypaper/gfe.py b/bin/reportlab/demos/gadflypaper/gfe.py new file mode 100644 index 00000000000..dfc1259d5a8 --- /dev/null +++ b/bin/reportlab/demos/gadflypaper/gfe.py @@ -0,0 +1,903 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/gadflypaper/gfe.py +__version__=''' $Id: gfe.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__='' + +#REPORTLAB_TEST_SCRIPT +import sys +from reportlab.platypus import * +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.rl_config import defaultPageSize +PAGE_HEIGHT=defaultPageSize[1] + +styles = getSampleStyleSheet() + +Title = "Integrating Diverse Data Sources with Gadfly 2" + +Author = "Aaron Watters" + +URL = "http://www.chordate.com/" + +email = "arw@ifu.net" + +Abstract = """This paper describes the primative methods underlying the implementation +of SQL query evaluation in Gadfly 2, a database management system implemented +in Python [Van Rossum]. The major design goals behind +the architecture described here are to simplify the implementation +and to permit flexible and efficient extensions to the gadfly +engine. Using this architecture and its interfaces programmers +can add functionality to the engine such as alternative disk based +indexed table implementations, dynamic interfaces to remote data +bases or or other data sources, and user defined computations.""" + +from reportlab.lib.units import inch + +pageinfo = "%s / %s / %s" % (Author, email, Title) + +def myFirstPage(canvas, doc): + canvas.saveState() + #canvas.setStrokeColorRGB(1,0,0) + #canvas.setLineWidth(5) + #canvas.line(66,72,66,PAGE_HEIGHT-72) + canvas.setFont('Times-Bold',16) + canvas.drawString(108, PAGE_HEIGHT-108, Title) + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo) + canvas.restoreState() + +def myLaterPages(canvas, doc): + #canvas.drawImage("snkanim.gif", 36, 36) + canvas.saveState() + #canvas.setStrokeColorRGB(1,0,0) + #canvas.setLineWidth(5) + #canvas.line(66,72,66,PAGE_HEIGHT-72) + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo)) + canvas.restoreState() + +def go(): + Elements.insert(0,Spacer(0,inch)) + doc = SimpleDocTemplate('gfe.pdf') + doc.build(Elements,onFirstPage=myFirstPage, onLaterPages=myLaterPages) + +Elements = [] + +HeaderStyle = styles["Heading1"] # XXXX + +def header(txt, style=HeaderStyle, klass=Paragraph, sep=0.3): + s = Spacer(0.2*inch, sep*inch) + Elements.append(s) + para = klass(txt, style) + Elements.append(para) + +ParaStyle = styles["Normal"] + +def p(txt): + return header(txt, style=ParaStyle, sep=0.1) + +#pre = p # XXX + +PreStyle = styles["Code"] + +def pre(txt): + s = Spacer(0.1*inch, 0.1*inch) + Elements.append(s) + p = Preformatted(txt, PreStyle) + Elements.append(p) + +#header(Title, sep=0.1. style=ParaStyle) +header(Author, sep=0.1, style=ParaStyle) +header(URL, sep=0.1, style=ParaStyle) +header(email, sep=0.1, style=ParaStyle) +header("ABSTRACT") +p(Abstract) + +header("Backgrounder") + +p("""\ +The term "database" usually refers to a persistent +collection of data. Data is persistent if it continues +to exist whether or not it is associated with a running +process on the computer, or even if the computer is +shut down and restarted at some future time. Database +management systems provide support for constructing databases, +maintaining databases, and extracting information from databases.""") +p("""\ +Relational databases manipulate and store persistent +table structures called relations, such as the following +three tables""") + +pre("""\ + -- drinkers who frequent bars (this is a comment) + select * from frequents + + DRINKER | PERWEEK | BAR + ============================ + adam | 1 | lolas + woody | 5 | cheers + sam | 5 | cheers + norm | 3 | cheers + wilt | 2 | joes + norm | 1 | joes + lola | 6 | lolas + norm | 2 | lolas + woody | 1 | lolas + pierre | 0 | frankies +) +""") +pre("""\ + -- drinkers who like beers + select * from likes + + DRINKER | PERDAY | BEER + =============================== + adam | 2 | bud + wilt | 1 | rollingrock + sam | 2 | bud + norm | 3 | rollingrock + norm | 2 | bud + nan | 1 | sierranevada + woody | 2 | pabst + lola | 5 | mickies + +""") +pre("""\ + -- beers served from bars + select * from serves + + BAR | QUANTITY | BEER + ================================= + cheers | 500 | bud + cheers | 255 | samadams + joes | 217 | bud + joes | 13 | samadams + joes | 2222 | mickies + lolas | 1515 | mickies + lolas | 333 | pabst + winkos | 432 | rollingrock + frankies | 5 | snafu +""") +p(""" +The relational model for database structures makes +the simplifying assumption that all data in a database +can be represented in simple table structures +such as these. Although this assumption seems extreme +it provides a good foundation for defining solid and +well defined database management systems and some +of the most successful software companies in the +world, such as Oracle, Sybase, IBM, and Microsoft, +have marketed database management systems based on +the relational model quite successfully. +""") +p(""" +SQL stands for Structured Query Language. +The SQL language defines industry standard +mechanisms for creating, querying, and modified +relational tables. Several years ago SQL was one +of many Relational Database Management System +(RDBMS) query languages in use, and many would +argue not the best on. Now, largely due +to standardization efforts and the +backing of IBM, SQL is THE standard way to talk +to database systems. +""") +p(""" +There are many advantages SQL offers over other +database query languages and alternative paradigms +at this time (please see [O'Neill] or [Korth and Silberschatz] +for more extensive discussions and comparisons between the +SQL/relational approach and others.) +""") +p(""" +The chief advantage over all contenders at this time +is that SQL and the relational model are now widely +used as interfaces and back end data stores to many +different products with different performance characteristics, +user interfaces, and other qualities: Oracle, Sybase, +Ingres, SQL Server, Access, Outlook, +Excel, IBM DB2, Paradox, MySQL, MSQL, POSTgres, and many +others. For this reason a program designed to use +an SQL database as its data storage mechanism can +easily be ported from one SQL data manager to another, +possibly on different platforms. In fact the same +program can seamlessly use several backends and/or +import/export data between different data base platforms +with trivial ease. +No other paradigm offers such flexibility at the moment. +""") +p(""" +Another advantage which is not as immediately +obvious is that the relational model and the SQL +query language are easily understood by semi-technical +and non-technical professionals, such as business +people and accountants. Human resources managers +who would be terrified by an object model diagram +or a snippet of code that resembles a conventional +programming language will frequently feel quite at +ease with a relational model which resembles the +sort of tabular data they deal with on paper in +reports and forms on a daily basis. With a little training the +same HR managers may be able to translate the request +"Who are the drinkers who like bud and frequent cheers?" +into the SQL query +""") +pre(""" + select drinker + from frequents + where bar='cheers' + and drinker in ( + select drinker + from likes + where beer='bud') +""") +p(""" +(or at least they have some hope of understanding +the query once it is written by a technical person +or generated by a GUI interface tool). Thus the use +of SQL and the relational model enables communication +between different communities which must understand +and interact with stored information. In contrast +many other approaches cannot be understood easily +by people without extensive programming experience. +""") +p(""" +Furthermore the declarative nature of SQL +lends itself to automatic query optimization, +and engines such as Gadfly can automatically translate a user query +into an optimized query plan which takes +advantage of available indices and other data characteristics. +In contrast more navigational techniques require the application +program itself to optimize the accesses to the database and +explicitly make use of indices. +""") + +# HACK +Elements.append(PageBreak()) + +p(""" +While it must be admitted that there are application +domains such as computer aided engineering design where +the relational model is unnatural, it is also important +to recognize that for many application domains (such +as scheduling, accounting, inventory, finance, personal +information management, electronic mail) the relational +model is a very natural fit and the SQL query language +make most accesses to the underlying data (even sophisticated +ones) straightforward. """) + +p("""For an example of a moderately +sophisticated query using the tables given above, +the following query lists the drinkers who frequent lolas bar +and like at least two beers not served by lolas +""") + +if 0: + go() + sys.exit(1) + +pre(""" + select f.drinker + from frequents f, likes l + where f.drinker=l.drinker and f.bar='lolas' + and l.beer not in + (select beer from serves where bar='lolas') + group by f.drinker + having count(distinct beer)>=2 +""") +p(""" +yielding the result +""") +pre(""" + DRINKER + ======= + norm +""") +p(""" +Experience shows that queries of this sort are actually +quite common in many applications, and are often much more +difficult to formulate using some navigational database +organizations, such as some "object oriented" database +paradigms. +""") +p(""" +Certainly, +SQL does not provide all you need to interact with +databases -- in order to do "real work" with SQL you +need to use SQL and at least one other language +(such as C, Pascal, C++, Perl, Python, TCL, Visual Basic +or others) to do work (such as readable formatting a report +from raw data) that SQL was not designed to do. +""") + +header("Why Gadfly 1?") + +p("""Gadfly 1.0 is an SQL based relational database implementation +implemented entirely in the Python programming language, with +optional fast data structure accellerators implemented in the +C programming language. Gadfly is relatively small, highly portable, +very easy to use (especially for programmers with previous experience +with SQL databases such as MS Access or Oracle), and reasonably +fast (especially when the kjbuckets C accellerators are used). +For moderate sized problems Gadfly offers a fairly complete +set of features such as transaction semantics, failure recovery, +and a TCP/IP based client/server mode (Please see [Gadfly] for +detailed discussion).""") + + +header("Why Gadfly 2?") + +p("""Gadfly 1.0 also has significant limitations. An active Gadfly +1.0 database keeps all data in (virtual) memory, and hence a Gadfly +1.0 database is limited in size to available virtual memory. Important +features such as date/time/interval operations, regular expression +matching and other standard SQL features are not implemented in +Gadfly 1.0. The optimizer and the query evaluator perform optimizations +using properties of the equality predicate but do not optimize +using properties of inequalities such as BETWEEN or less-than. +It is possible to add "extension views" to a Gadfly +1.0 database, but the mechanism is somewhat clumsy and indices +over extension views are not well supported. The features of Gadfly +2.0 discussed here attempt to address these deficiencies by providing +a uniform extension model that permits addition of alternate table, +function, and predicate implementations.""") + +p("""Other deficiencies, such as missing constructs like "ALTER +TABLE" and the lack of outer joins and NULL values are not +addressed here, although they may be addressed in Gadfly 2.0 or +a later release. This paper also does not intend to explain +the complete operations of the internals; it is intended to provide +at least enough information to understand the basic mechanisms +for extending gadfly.""") + + + + +p("""Some concepts and definitions provided next help with the description +of the gadfly interfaces. [Note: due to the terseness of this +format the ensuing is not a highly formal presentation, but attempts +to approach precision where precision is important.]""") + +header("The semilattice of substitutions") + +p("""Underlying the gadfly implementation are the basic concepts +associated with substitutions. A substitution is a mapping +of attribute names to values (implemented in gadfly using kjbuckets.kjDict +objects). Here an attribute refers to some sort of "descriptive +variable", such as NAME and a value is an assignment for that variable, +like "Dave Ascher". In Gadfly a table is implemented as a sequence +of substitutions, and substitutions are used in many other ways as well. +""") +p(""" +For example consider the substitutions""") + +pre(""" + A = [DRINKER=>'sam'] + B = [DRINKER=>'sam', BAR=>'cheers'] + C = [DRINKER=>'woody', BEER=>'bud'] + D = [DRINKER=>'sam', BEER=>'mickies'] + E = [DRINKER=>'sam', BAR=>'cheers', BEER=>'mickies'] + F = [DRINKER=>'sam', BEER=>'mickies'] + G = [BEER=>'bud', BAR=>'lolas'] + H = [] # the empty substitution + I = [BAR=>'cheers', CAPACITY=>300]""") + +p("""A trivial but important observation is that since substitutions +are mappings, no attribute can assume more than one value in a +substitution. In the operations described below whenever an operator +"tries" to assign more than one value to an attribute +the operator yields an "overdefined" or "inconsistent" +result.""") + +header("Information Semi-order:") + +p("""Substitution B is said to be +more informative than A because B agrees with all assignments +in A (in addition to providing more information as well). Similarly +we say that E is more informative than A, B, D, F. and H but E +is not more informative than the others since, for example G disagrees +with E on the value assigned to the BEER attribute and I provides +additional CAPACITY information not provided in E.""") + +header("Joins and Inconsistency:") + +p("""A join of two substitutions +X and Y is the least informative substitution Z such that Z is +more informative (or equally informative) than both X and Y. For +example B is the join of B with A, E is the join of B with D and""") + +pre(""" + E join I = + [DRINKER=>'sam', BAR=>'cheers', BEER=>'mickies', CAPACITY=>300]""") + +p("""For any two substitutions either (1) they disagree on the value +assigned to some attribute and have no join or (2) they agree +on all common attributes (if there are any) and their join is +the union of all (name, value) assignments in both substitutions. +Written in terms of kjbucket.kjDict operations two kjDicts X and +Y have a join Z = (X+Y) if and only if Z.Clean() is not None. +Two substitutions that have no join are said to be inconsistent. +For example I and G are inconsistent since they disagree on +the value assigned to the BAR attribute and therefore have no +join. The algebra of substitutions with joins technically defines +an abstract algebraic structure called a semilattice.""") + +header("Name space remapping") + +p("""Another primitive operation over substitutions is the remap +operation S2 = S.remap(R) where S is a substitution and R is a +graph of attribute names and S2 is a substitution. This operation +is defined to produce the substitution S2 such that""") + +pre(""" + Name=>Value in S2 if and only if + Name1=>Value in S and Name<=Name1 in R +""") + +p("""or if there is no such substitution S2 the remap value is said +to be overdefined.""") + +p("""For example the remap operation may be used to eliminate attributes +from a substitution. For example""") + +pre(""" + E.remap([DRINKER<=DRINKER, BAR<=BAR]) + = [DRINKER=>'sam', BAR=>'cheers'] +""") + +p("""Illustrating that remapping using the [DRINKER<=DRINKER, +BAR<=BAR] graph eliminates all attributes except DRINKER and +BAR, such as BEER. More generally remap can be used in this way +to implement the classical relational projection operation. (See [Korth and Silberschatz] +for a detailed discussion of the projection operator and other relational +algebra operators such as selection, rename, difference and joins.)""") + +p("""The remap operation can also be used to implement "selection +on attribute equality". For example if we are interested +in the employee names of employees who are their own bosses we +can use the remapping graph""") + +pre(""" + R1 = [NAME<=NAME, NAME<=BOSS] +""") + +p("""and reject substitutions where remapping using R1 is overdefined. +For example""") + +pre(""" + S1 = [NAME=>'joe', BOSS=>'joe'] + S1.remap(R1) = [NAME=>'joe'] + S2 = [NAME=>'fred', BOSS=>'joe'] + S2.remap(R1) is overdefined. +""") + +p("""The last remap is overdefined because the NAME attribute cannot +assume both the values 'fred' and 'joe' in a substitution.""") + +p("""Furthermore, of course, the remap operation can be used to +"rename attributes" or "copy attribute values" +in substitutions. Note below that the missing attribute CAPACITY +in B is effectively ignored in the remapping operation.""") + +pre(""" + B.remap([D<=DRINKER, B<=BAR, B2<=BAR, C<=CAPACITY]) + = [D=>'sam', B=>'cheers', B2=>'cheers'] +""") + +p("""More interestingly, a single remap operation can be used to +perform a combination of renaming, projection, value copying, +and attribute equality selection as one operation. In kjbuckets the remapper +graph is implemented using a kjbuckets.kjGraph and the remap operation +is an intrinsic method of kjbuckets.kjDict objects.""") + +header("Generalized Table Joins and the Evaluator Mainloop""") + +p("""Strictly speaking the Gadfly 2.0 query evaluator only uses +the join and remap operations as its "basic assembly language" +-- all other computations, including inequality comparisons and +arithmetic, are implemented externally to the evaluator as "generalized +table joins." """) + +p("""A table is a sequence of substitutions (which in keeping with +SQL semantics may contain redundant entries). The join between +two tables T1 and T2 is the sequence of all possible defined joins +between pairs of elements from the two tables. Procedurally we +might compute the join as""") + +pre(""" + T1JoinT2 = empty + for t1 in T1: + for t2 in T2: + if t1 join t2 is defined: + add t1 join t2 to T1joinT2""") + +p("""In general circumstances this intuitive implementation is a +very inefficient way to compute the join, and Gadfly almost always +uses other methods, particularly since, as described below, a +"generalized table" can have an "infinite" +number of entries.""") + +p("""For an example of a table join consider the EMPLOYEES table +containing""") + +pre(""" + [NAME=>'john', JOB=>'executive'] + [NAME=>'sue', JOB=>'programmer'] + [NAME=>'eric', JOB=>'peon'] + [NAME=>'bill', JOB=>'peon'] +""") + +p("""and the ACTIVITIES table containing""") + +pre(""" + [JOB=>'peon', DOES=>'windows'] + [JOB=>'peon', DOES=>'floors'] + [JOB=>'programmer', DOES=>'coding'] + [JOB=>'secretary', DOES=>'phone']""") + +p("""then the join between EMPLOYEES and ACTIVITIES must containining""") + +pre(""" + [NAME=>'sue', JOB=>'programmer', DOES=>'coding'] + [NAME=>'eric', JOB=>'peon', DOES=>'windows'] + [NAME=>'bill', JOB=>'peon', DOES=>'windows'] + [NAME=>'eric', JOB=>'peon', DOES=>'floors'] + [NAME=>'bill', JOB=>'peon', DOES=>'floors']""") + +p("""A compiled gadfly subquery ultimately appears to the evaluator +as a sequence of generalized tables that must be joined (in combination +with certain remapping operations that are beyond the scope of +this discussion). The Gadfly mainloop proceeds following the very +loose pseudocode:""") + +pre(""" + Subs = [ [] ] # the unary sequence containing "true" + While some table hasn't been chosen yet: + Choose an unchosen table with the least cost join estimate. + Subs = Subs joined with the chosen table + return Subs""") + +p("""[Note that it is a property of the join operation that the +order in which the joins are carried out will not affect the result, +so the greedy strategy of evaluating the "cheapest join next" +will not effect the result. Also note that the treatment of logical +OR and NOT as well as EXIST, IN, UNION, and aggregation and so +forth are not discussed here, even though they do fit into this +approach.]""") + +p("""The actual implementation is a bit more complex than this, +but the above outline may provide some useful intuition. The "cost +estimation" step and the implementation of the join operation +itself are left up to the generalized table object implementation. +A table implementation has the ability to give an "infinite" +cost estimate, which essentially means "don't join me in +yet under any circumstances." """) + +header("Implementing Functions") + +p("""As mentioned above operations such as arithmetic are implemented +using generalized tables. For example the arithmetic Add operation +is implemented in Gadfly internally as an "infinite generalized +table" containing all possible substitutions""") + +pre(""" + ARG0=>a, ARG1=>b, RESULT=>a+b] +""") + +p("""Where a and b are all possible values which can be summed. +Clearly, it is not possible to enumerate this table, but given +a sequence of substitutions with defined values for ARG0 and ARG1 +such as""") + +pre(""" + [ARG0=>1, ARG1=-4] + [ARG0=>2.6, ARG1=50] + [ARG0=>99, ARG1=1] +""") + +p("""it is possible to implement a "join operation" against +this sequence that performs the same augmentation as a join with +the infinite table defined above:""") + +pre(""" + [ARG0=>1, ARG1=-4, RESULT=-3] + [ARG0=>2.6, ARG1=50, RESULT=52.6] + [ARG0=>99, ARG1=1, RESULT=100] +""") + +p("""Furthermore by giving an "infinite estimate" for +all attempts to evaluate the join where ARG0 and ARG1 are not +available the generalized table implementation for the addition +operation can refuse to compute an "infinite join." """) + +p("""More generally all functions f(a,b,c,d) are represented in +gadfly as generalized tables containing all possible relevant +entries""") + +pre(""" + [ARG0=>a, ARG1=>b, ARG2=>c, ARG3=>d, RESULT=>f(a,b,c,d)]""") + +p("""and the join estimation function refuses all attempts to perform +a join unless all the arguments are provided by the input substitution +sequence.""") + +header("Implementing Predicates") + +p("""Similarly to functions, predicates such as less-than and BETWEEN +and LIKE are implemented using the generalized table mechanism. +For example the "x BETWEEN y AND z" predicate is implemented +as a generalized table "containing" all possible""") + +pre(""" + [ARG0=>a, ARG1=>b, ARG2=>c]""") + +p("""where b<a<c. Furthermore joins with this table are not +permitted unless all three arguments are available in the sequence +of input substitutions.""") + +header("Some Gadfly extension interfaces") + +p("""A gadfly database engine may be extended with user defined +functions, predicates, and alternative table and index implementations. +This section snapshots several Gadfly 2.0 interfaces, currently under +development and likely to change before the package is released.""") + +p("""The basic interface for adding functions and predicates (logical tests) +to a gadfly engine are relatively straightforward. For example to add the +ability to match a regular expression within a gadfly query use the +following implementation.""") + +pre(""" + from re import match + + def addrematch(gadflyinstance): + gadflyinstance.add_predicate("rematch", match) +""") +p(""" +Then upon connecting to the database execute +""") +pre(""" + g = gadfly(...) + ... + addrematch(g) +""") +p(""" +In this case the "semijoin operation" associated with the new predicate +"rematch" is automatically generated, and after the add_predicate +binding operation the gadfly instance supports queries such as""") +pre(""" + select drinker, beer + from likes + where rematch('b*', beer) and drinker not in + (select drinker from frequents where rematch('c*', bar)) +""") +p(""" +By embedding the "rematch" operation within the query the SQL +engine can do "more work" for the programmer and reduce or eliminate the +need to process the query result externally to the engine. +""") +p(""" +In a similar manner functions may be added to a gadfly instance,""") +pre(""" + def modulo(x,y): + return x % y + + def addmodulo(gadflyinstance): + gadflyinstance.add_function("modulo", modulo) + + ... + g = gadfly(...) + ... + addmodulo(g) +""") +p(""" +Then after the binding the modulo function can be used whereever +an SQL expression can occur. +""") +p(""" +Adding alternative table implementations to a Gadfly instance +is more interesting and more difficult. An "extension table" implementation +must conform to the following interface:""") + +pre(""" + # get the kjbuckets.kjSet set of attribute names for this table + names = table.attributes() + + # estimate the difficulty of evaluating a join given known attributes + # return None for "impossible" or n>=0 otherwise with larger values + # indicating greater difficulty or expense + estimate = table.estimate(known_attributes) + + # return the join of the rows of the table with + # the list of kjbuckets.kjDict mappings as a list of mappings. + resultmappings = table.join(listofmappings) +""") +p(""" +In this case add the table to a gadfly instance using""") +pre(""" + gadflyinstance.add_table("table_name", table) +""") +p(""" +For example to add a table which automatically queries filenames +in the filesystems of the host computer a gadfly instance could +be augmented with a GLOB table implemented using the standard +library function glob.glob as follows:""") +pre(""" + import kjbuckets + + class GlobTable: + def __init__(self): pass + + def attributes(self): + return kjbuckets.kjSet("PATTERN", "NAME") + + def estimate(self, known_attributes): + if known_attributes.member("PATTERN"): + return 66 # join not too difficult + else: + return None # join is impossible (must have PATTERN) + + def join(self, listofmappings): + from glob import glob + result = [] + for m in listofmappings: + pattern = m["PATTERN"] + for name in glob(pattern): + newmapping = kjbuckets.kjDict(m) + newmapping["NAME"] = name + if newmapping.Clean(): + result.append(newmapping) + return result + + ... + gadfly_instance.add_table("GLOB", GlobTable()) +""") +p(""" +Then one could formulate queries such as "list the files in directories +associated with packages installed by guido" +""") +pre(""" + select g.name as filename + from packages p, glob g + where p.installer = 'guido' and g.pattern=p.root_directory +""") +p(""" +Note that conceptually the GLOB table is an infinite table including +all filenames on the current computer in the "NAME" column, paired with +a potentially infinite number of patterns. +""") +p(""" +More interesting examples would allow queries to remotely access +data served by an HTTP server, or from any other resource. +""") +p(""" +Furthermore an extension table can be augmented with update methods +""") +pre(""" + table.insert_rows(listofmappings) + table.update_rows(oldlist, newlist) + table.delete_rows(oldlist) +""") +p(""" +Note: at present the implementation does not enforce recovery or +transaction semantics for updates to extension tables, although this +may change in the final release. +""") +p(""" +The table implementation is free to provide its own implementations of +indices which take advantage of data provided by the join argument. +""") + +header("Efficiency Notes") + +p("""The following thought experiment attempts to explain why the +Gadfly implementation is surprisingly fast considering that it +is almost entirely implemented in Python (an interpreted programming +language which is not especially fast when compared to alternatives). +Although Gadfly is quite complex, at an abstract level the process +of query evaluation boils down to a series of embedded loops. +Consider the following nested loops:""") + +pre(""" + iterate 1000: + f(...) # fixed cost of outer loop + iterate 10: + g(...) # fixed cost of middle loop + iterate 10: + # the real work (string parse, matrix mul, query eval...) + h(...)""") + +p("""In my experience many computations follow this pattern where +f, g, are complex, dynamic, special purpose and h is simple, general +purpose, static. Some example computations that follow this pattern +include: file massaging (perl), matrix manipulation (python, tcl), +database/cgi page generation, and vector graphics/imaging.""") + +p("""Suppose implementing f, g, h in python is easy but result in +execution times10 times slower than a much harder implementation +in C, choosing arbitrary and debatable numbers assume each function +call consumes 1 tick in C, 5 ticks in java, 10 ticks in python +for a straightforward implementation of each function f, g, and +h. Under these conditions we get the following cost analysis, +eliminating some uninteresting combinations, of implementing the +function f, g, and h in combinations of Python, C and java:""") + +pre(""" +COST | FLANG | GLANG | HLANG +================================== +111000 | C | C | C +115000 | java | C | C +120000 | python | C | C +155000 | java | java | C +210000 | python | python | C +555000 | java | java | java +560000 | python | java | java +610000 | python | python | java +1110000 | python | python | python +""") + +p("""Note that moving only the innermost loop to C (python/python/C) +speeds up the calculation by half an order of magnitude compared +to the python-only implementation and brings the speed to within +a factor of 2 of an implementation done entirely in C.""") + +p("""Although this artificial and contrived thought experiment is +far from conclusive, we may be tempted to draw the conclusion +that generally programmers should focus first on obtaining a working +implementation (because as John Ousterhout is reported to have +said "the biggest performance improvement is the transition +from non-working to working") using the methodology that +is most likely to obtain a working solution the quickest (Python). Only then if the performance +is inadequate should the programmer focus on optimizing +the inner most loops, perhaps moving them to a very efficient +implementation (C). Optimizing the outer loops will buy little +improvement, and should be done later, if ever.""") + +p("""This was precisely the strategy behind the gadfly implementations, +where most of the inner loops are implemented in the kjbuckets +C extension module and the higher level logic is all in Python. +This also explains why gadfly appears to be "slower" +for simple queries over small data sets, but seems to be relatively +"faster" for more complex queries over larger data sets, +since larger queries and data sets take better advantage of the +optimized inner loops.""") + +header("A Gadfly variant for OLAP?") + +p("""In private correspondence Andy Robinson points out that the +basic logical design underlying Gadfly could be adapted to provide +Online Analytical Processing (OLAP) and other forms of data warehousing +and data mining. Since SQL is not particularly well suited for +the kinds of requests common in these domains the higher level +interfaces would require modification, but the underlying logic +of substitutions and name mappings seems to be appropriate.""") + +header("Conclusion") + +p("""The revamped query engine design in Gadfly 2 supports +a flexible and general extension methodology that permits programmers +to extend the gadfly engine to include additional computations +and access to remote data sources. Among other possibilities this +will permit the gadfly engine to make use of disk based indexed +tables and to dynamically retrieve information from remote data +sources (such as an Excel spreadsheet or an Oracle database). +These features will make gadfly a very useful tool for data manipulation +and integration.""") + +header("References") + +p("""[Van Rossum] Van Rossum, Python Reference Manual, Tutorial, and Library Manuals, +please look to http://www.python.org +for the latest versions, downloads and links to printed versions.""") + +p("""[O'Neill] O'Neill, P., Data Base Principles, Programming, Performance, +Morgan Kaufmann Publishers, San Francisco, 1994.""") + +p("""[Korth and Silberschatz] Korth, H. and Silberschatz, A. and Sudarshan, S. +Data Base System Concepts, McGraw-Hill Series in Computer Science, Boston, +1997""") + +p("""[Gadfly]Gadfly: SQL Relational Database in Python, +http://www.chordate.com/kwParsing/gadfly.html""") + +go() \ No newline at end of file diff --git a/bin/reportlab/demos/odyssey/00readme.txt b/bin/reportlab/demos/odyssey/00readme.txt new file mode 100644 index 00000000000..95c622ac279 --- /dev/null +++ b/bin/reportlab/demos/odyssey/00readme.txt @@ -0,0 +1,56 @@ +This contains a number of benchmarks and demos +based on Homer's Odyssey (which is widely available +in plain, line-oriented text format). There are a large +selection of online books at: + http://classics.mit.edu/ + + +Our distribution ships with just the first chapter +in odyssey.txt. For a more meaningful speed test, +download the full copy from + http://www.reportlab.com/ftp/odyssey.full.zip +or + ftp://ftp.reportlab.com/odyssey.full.zip +and unzip to extract odyssey.full.txt (608kb). + +Benchmark speed depends quite critically +on the presence of our accelerator module, +_rl_accel, which is a C (or Java) extension. +Serious users ought to compile or download this! + +The times quoted are from one machine (Andy Robinson's +home PC, approx 1.2Ghz 128Mb Ram, Win2k in Sep 2003) +in order to give a rough idea of what features cost +what performance. + + +The tests are as follows: + +(1) odyssey.py (produces odyssey.pdf) +This demo takes a large volume of text and prints it +in the simplest way possible. It is a demo of the +basic technique of looping down a page manually and +breaking at the bottom. On my 1.2 Ghz machine this takes +1.91 seconds (124 pages per second) + +(2) fodyssey.py (produces fodyssey.pdf) +This is a 'flowing document' we parse the file and +throw away line breaks to make proper paragraphs. +The Platypus framework renders these. This necessitates +measuring the width of every word in every paragraph +for wrapping purposes. + +This takes 3.27 seconds on the same machine. Paragraph +wrapping basically doubles the work. The text is more +compact with about 50% more words per page. Very roughly, +we can wrap 40 pages of ten-point text per second and save +to PDF. + +(3) dodyssey.py (produced dodyssey.pdf) +This is a slightly fancier version which uses different +page templates (one column for first page in a chapter, +two column for body poages). The additional layout logic +adds about 15%, going up to 3.8 seconds. This is probably +a realistic benchmark for a simple long text document +with a single pass. Documents doing cross-references +and a table of contents might need twice as long. diff --git a/bin/reportlab/demos/odyssey/dodyssey.py b/bin/reportlab/demos/odyssey/dodyssey.py new file mode 100644 index 00000000000..edc24354eca --- /dev/null +++ b/bin/reportlab/demos/odyssey/dodyssey.py @@ -0,0 +1,254 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/odyssey/dodyssey.py +__version__=''' $Id: dodyssey.py 2856 2006-05-11 09:48:13Z rgbecker $ ''' +__doc__='' + +#REPORTLAB_TEST_SCRIPT +import sys, copy, string, os +from reportlab.platypus import * +_NEW_PARA=os.environ.get('NEW_PARA','0')[0] in ('y','Y','1') +_REDCAP=int(os.environ.get('REDCAP','0')) +_CALLBACK=os.environ.get('CALLBACK','0')[0] in ('y','Y','1') +if _NEW_PARA: + def Paragraph(s,style): + from rlextra.radxml.para import Paragraph as PPPP + return PPPP(s,style) + +from reportlab.lib.units import inch +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY + +import reportlab.rl_config +reportlab.rl_config.invariant = 1 + +styles = getSampleStyleSheet() + +Title = "The Odyssey" +Author = "Homer" + +def myTitlePage(canvas, doc): + canvas.saveState() + canvas.restoreState() + +def myLaterPages(canvas, doc): + canvas.saveState() + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "Page %d" % doc.page) + canvas.restoreState() + +def go(): + def myCanvasMaker(fn,**kw): + from reportlab.pdfgen.canvas import Canvas + canv = apply(Canvas,(fn,),kw) + # attach our callback to the canvas + canv.myOnDrawCB = myOnDrawCB + return canv + + doc = BaseDocTemplate('dodyssey.pdf',showBoundary=0) + + #normal frame as for SimpleFlowDocument + frameT = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id='normal') + + #Two Columns + frame1 = Frame(doc.leftMargin, doc.bottomMargin, doc.width/2-6, doc.height, id='col1') + frame2 = Frame(doc.leftMargin+doc.width/2+6, doc.bottomMargin, doc.width/2-6, + doc.height, id='col2') + doc.addPageTemplates([PageTemplate(id='First',frames=frameT, onPage=myTitlePage), + PageTemplate(id='OneCol',frames=frameT, onPage=myLaterPages), + PageTemplate(id='TwoCol',frames=[frame1,frame2], onPage=myLaterPages), + ]) + doc.build(Elements,canvasmaker=myCanvasMaker) + +Elements = [] + +ChapterStyle = copy.deepcopy(styles["Heading1"]) +ChapterStyle.alignment = TA_CENTER +ChapterStyle.fontsize = 14 +InitialStyle = copy.deepcopy(ChapterStyle) +InitialStyle.fontsize = 16 +InitialStyle.leading = 20 +PreStyle = styles["Code"] + +def newPage(): + Elements.append(PageBreak()) + +chNum = 0 +def myOnDrawCB(canv,kind,label): + print 'myOnDrawCB(%s)'%kind, 'Page number=', canv.getPageNumber(), 'label value=', label + +def chapter(txt, style=ChapterStyle): + global chNum + Elements.append(NextPageTemplate('OneCol')) + newPage() + chNum = chNum + 1 + if _NEW_PARA or not _CALLBACK: + Elements.append(Paragraph(('chap %d'%chNum)+txt, style)) + else: + Elements.append(Paragraph(('foo '%chNum)+txt, style)) + Elements.append(Spacer(0.2*inch, 0.3*inch)) + if useTwoCol: + Elements.append(NextPageTemplate('TwoCol')) + +def fTitle(txt,style=InitialStyle): + Elements.append(Paragraph(txt, style)) + +ParaStyle = copy.deepcopy(styles["Normal"]) +ParaStyle.spaceBefore = 0.1*inch +if 'right' in sys.argv: + ParaStyle.alignment = TA_RIGHT +elif 'left' in sys.argv: + ParaStyle.alignment = TA_LEFT +elif 'justify' in sys.argv: + ParaStyle.alignment = TA_JUSTIFY +elif 'center' in sys.argv or 'centre' in sys.argv: + ParaStyle.alignment = TA_CENTER +else: + ParaStyle.alignment = TA_JUSTIFY + +useTwoCol = 'notwocol' not in sys.argv + +def spacer(inches): + Elements.append(Spacer(0.1*inch, inches*inch)) + +def p(txt, style=ParaStyle): + if _REDCAP: + fs, fe = '', '' + n = len(txt) + for i in xrange(n): + if 'a'<=txt[i]<='z' or 'A'<=txt[i]<='Z': + txt = (txt[:i]+(fs+txt[i]+fe))+txt[i+1:] + break + if _REDCAP>=2 and n>20: + j = i+len(fs)+len(fe)+1+int((n-1)/2) + while not ('a'<=txt[j]<='z' or 'A'<=txt[j]<='Z'): j += 1 + txt = (txt[:j]+(''+txt[j]+''))+txt[j+1:] + + if _REDCAP==3 and n>20: + n = len(txt) + fs = '' + for i in xrange(n-1,-1,-1): + if 'a'<=txt[i]<='z' or 'A'<=txt[i]<='Z': + txt = txt[:i]+((fs+txt[i]+fe)+txt[i+1:]) + break + + Elements.append(Paragraph(txt, style)) + +firstPre = 1 +def pre(txt, style=PreStyle): + global firstPre + if firstPre: + Elements.append(NextPageTemplate('OneCol')) + newPage() + firstPre = 0 + + spacer(0.1) + p = Preformatted(txt, style) + Elements.append(p) + +def parseOdyssey(fn): + from time import time + E = [] + t0=time() + L = open(fn,'r').readlines() + t1 = time() + print "open(%s,'r').readlines() took %.4f seconds" %(fn,t1-t0) + for i in xrange(len(L)): + if L[i][-1]=='\012': + L[i] = L[i][:-1] + t2 = time() + print "Removing all linefeeds took %.4f seconds" %(t2-t1) + L.append('') + L.append('-----') + + def findNext(L, i): + while 1: + if string.strip(L[i])=='': + del L[i] + kind = 1 + if i%s' % Title, InitialStyle]) + E.append([fTitle,'by %s' % Author, InitialStyle]) + + while 1: + if f>=len(L): break + + if string.upper(L[f][0:5])=='BOOK ': + E.append([chapter,L[f]]) + f=f+1 + while string.strip(L[f])=='': del L[f] + style = ParaStyle + func = p + else: + style = PreStyle + func = pre + + while 1: + s=f + f, k=findNext(L,s) + sep= (func is pre) and '\012' or ' ' + E.append([func,string.join(L[s:f],sep),style]) + if k: break + t3 = time() + print "Parsing into memory took %.4f seconds" %(t3-t2) + del L + t4 = time() + print "Deleting list of lines took %.4f seconds" %(t4-t3) + for i in xrange(len(E)): + apply(E[i][0],E[i][1:]) + t5 = time() + print "Moving into platypus took %.4f seconds" %(t5-t4) + del E + t6 = time() + print "Deleting list of actions took %.4f seconds" %(t6-t5) + go() + t7 = time() + print "saving to PDF took %.4f seconds" %(t7-t6) + print "Total run took %.4f seconds"%(t7-t0) + + import md5 + print 'file digest: %s' % md5.md5(open('dodyssey.pdf','rb').read()).hexdigest() + +def run(): + for fn in ('odyssey.full.txt','odyssey.txt'): + if os.path.isfile(fn): + parseOdyssey(fn) + break + +def doProf(profname,func,*args,**kwd): + import hotshot, hotshot.stats + prof = hotshot.Profile(profname) + prof.runcall(func) + prof.close() + stats = hotshot.stats.load(profname) + stats.strip_dirs() + stats.sort_stats('time', 'calls') + stats.print_stats(20) + +if __name__=='__main__': + if '--prof' in sys.argv: + doProf('dodyssey.prof',run) + else: + run() diff --git a/bin/reportlab/demos/odyssey/fodyssey.py b/bin/reportlab/demos/odyssey/fodyssey.py new file mode 100644 index 00000000000..bbb6797e7a7 --- /dev/null +++ b/bin/reportlab/demos/odyssey/fodyssey.py @@ -0,0 +1,165 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/odyssey/fodyssey.py +__version__=''' $Id: fodyssey.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__='' + +#REPORTLAB_TEST_SCRIPT +import sys, copy, string, os +from reportlab.platypus import * +from reportlab.lib.units import inch +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY + +styles = getSampleStyleSheet() + +Title = "The Odyssey" +Author = "Homer" + +def myFirstPage(canvas, doc): + canvas.saveState() + canvas.restoreState() + +def myLaterPages(canvas, doc): + canvas.saveState() + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "Page %d" % doc.page) + canvas.restoreState() + +def go(): + doc = SimpleDocTemplate('fodyssey.pdf',showBoundary='showboundary' in sys.argv) + doc.allowSplitting = not 'nosplitting' in sys.argv + doc.build(Elements,myFirstPage,myLaterPages) + +Elements = [] + +ChapterStyle = copy.copy(styles["Heading1"]) +ChapterStyle.alignment = TA_CENTER +ChapterStyle.fontsize = 16 +InitialStyle = copy.deepcopy(ChapterStyle) +InitialStyle.fontsize = 16 +InitialStyle.leading = 20 +PreStyle = styles["Code"] + +def newPage(): + Elements.append(PageBreak()) + +def chapter(txt, style=ChapterStyle): + newPage() + Elements.append(Paragraph(txt, style)) + Elements.append(Spacer(0.2*inch, 0.3*inch)) + +def fTitle(txt,style=InitialStyle): + Elements.append(Paragraph(txt, style)) + +ParaStyle = copy.deepcopy(styles["Normal"]) +ParaStyle.spaceBefore = 0.1*inch +if 'right' in sys.argv: + ParaStyle.alignment = TA_RIGHT +elif 'left' in sys.argv: + ParaStyle.alignment = TA_LEFT +elif 'justify' in sys.argv: + ParaStyle.alignment = TA_JUSTIFY +elif 'center' in sys.argv or 'centre' in sys.argv: + ParaStyle.alignment = TA_CENTER +else: + ParaStyle.alignment = TA_JUSTIFY + +def spacer(inches): + Elements.append(Spacer(0.1*inch, inches*inch)) + +def p(txt, style=ParaStyle): + Elements.append(Paragraph(txt, style)) + +def pre(txt, style=PreStyle): + spacer(0.1) + p = Preformatted(txt, style) + Elements.append(p) + +def parseOdyssey(fn): + from time import time + E = [] + t0=time() + L = open(fn,'r').readlines() + t1 = time() + print "open(%s,'r').readlines() took %.4f seconds" %(fn,t1-t0) + for i in xrange(len(L)): + if L[i][-1]=='\012': + L[i] = L[i][:-1] + t2 = time() + print "Removing all linefeeds took %.4f seconds" %(t2-t1) + L.append('') + L.append('-----') + + def findNext(L, i): + while 1: + if string.strip(L[i])=='': + del L[i] + kind = 1 + if i%s' % Title, InitialStyle]) + E.append([fTitle,'by %s' % Author, InitialStyle]) + + while 1: + if f>=len(L): break + + if string.upper(L[f][0:5])=='BOOK ': + E.append([chapter,L[f]]) + f=f+1 + while string.strip(L[f])=='': del L[f] + style = ParaStyle + func = p + else: + style = PreStyle + func = pre + + while 1: + s=f + f, k=findNext(L,s) + sep= (func is pre) and '\012' or ' ' + E.append([func,string.join(L[s:f],sep),style]) + if k: break + t3 = time() + print "Parsing into memory took %.4f seconds" %(t3-t2) + del L + t4 = time() + print "Deleting list of lines took %.4f seconds" %(t4-t3) + for i in xrange(len(E)): + apply(E[i][0],E[i][1:]) + t5 = time() + print "Moving into platypus took %.4f seconds" %(t5-t4) + del E + t6 = time() + print "Deleting list of actions took %.4f seconds" %(t6-t5) + go() + t7 = time() + print "saving to PDF took %.4f seconds" %(t7-t6) + print "Total run took %.4f seconds"%(t7-t0) + +for fn in ('odyssey.full.txt','odyssey.txt'): + if os.path.isfile(fn): + break +if __name__=='__main__': + parseOdyssey(fn) \ No newline at end of file diff --git a/bin/reportlab/demos/odyssey/odyssey.py b/bin/reportlab/demos/odyssey/odyssey.py new file mode 100644 index 00000000000..ae8d9d6188c --- /dev/null +++ b/bin/reportlab/demos/odyssey/odyssey.py @@ -0,0 +1,151 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/odyssey/odyssey.py +__version__=''' $Id: odyssey.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +___doc__='' +#odyssey.py +# +#Demo/benchmark of PDFgen rendering Homer's Odyssey. + + + +#results on my humble P266 with 64MB: +# Without page compression: +# 239 pages in 3.76 seconds = 77 pages per second + +# With textOut rather than textLine, i.e. computing width +# of every word as we would for wrapping: +# 239 pages in 10.83 seconds = 22 pages per second + +# With page compression and textLine(): +# 239 pages in 39.39 seconds = 6 pages per second + +from reportlab.pdfgen import canvas +import time, os, sys + +#find out what platform we are on and whether accelerator is +#present, in order to print this as part of benchmark info. +try: + import _rl_accel + ACCEL = 1 +except ImportError: + ACCEL = 0 + + + + +from reportlab.lib.units import inch, cm +from reportlab.lib.pagesizes import A4 + +#precalculate some basics +top_margin = A4[1] - inch +bottom_margin = inch +left_margin = inch +right_margin = A4[0] - inch +frame_width = right_margin - left_margin + + +def drawPageFrame(canv): + canv.line(left_margin, top_margin, right_margin, top_margin) + canv.setFont('Times-Italic',12) + canv.drawString(left_margin, top_margin + 2, "Homer's Odyssey") + canv.line(left_margin, top_margin, right_margin, top_margin) + + + canv.line(left_margin, bottom_margin, right_margin, bottom_margin) + canv.drawCentredString(0.5*A4[0], 0.5 * inch, + "Page %d" % canv.getPageNumber()) + + + +def run(verbose=1): + if sys.platform[0:4] == 'java': + impl = 'Jython' + else: + impl = 'Python' + verStr = '%d.%d' % (sys.version_info[0:2]) + if ACCEL: + accelStr = 'with _rl_accel' + else: + accelStr = 'without _rl_accel' + print 'Benchmark of %s %s %s' % (impl, verStr, accelStr) + + started = time.time() + canv = canvas.Canvas('odyssey.pdf', invariant=1) + canv.setPageCompression(1) + drawPageFrame(canv) + + #do some title page stuff + canv.setFont("Times-Bold", 36) + canv.drawCentredString(0.5 * A4[0], 7 * inch, "Homer's Odyssey") + + canv.setFont("Times-Bold", 18) + canv.drawCentredString(0.5 * A4[0], 5 * inch, "Translated by Samuel Burton") + + canv.setFont("Times-Bold", 12) + tx = canv.beginText(left_margin, 3 * inch) + tx.textLine("This is a demo-cum-benchmark for PDFgen. It renders the complete text of Homer's Odyssey") + tx.textLine("from a text file. On my humble P266, it does 77 pages per secondwhile creating a 238 page") + tx.textLine("document. If it is asked to computer text metrics, measuring the width of each word as ") + tx.textLine("one would for paragraph wrapping, it still manages 22 pages per second.") + tx.textLine("") + tx.textLine("Andy Robinson, Robinson Analytics Ltd.") + canv.drawText(tx) + + canv.showPage() + #on with the text... + drawPageFrame(canv) + + canv.setFont('Times-Roman', 12) + tx = canv.beginText(left_margin, top_margin - 0.5*inch) + + for fn in ('odyssey.full.txt','odyssey.txt'): + if os.path.isfile(fn): + break + + data = open(fn,'r').readlines() + for line in data: + #this just does it the fast way... + tx.textLine(line) + #this forces it to do text metrics, which would be the slow + #part if we were wrappng paragraphs. + #canv.textOut(line) + #canv.textLine('') + + #page breaking + y = tx.getY() #get y coordinate + if y < bottom_margin + 0.5*inch: + canv.drawText(tx) + canv.showPage() + drawPageFrame(canv) + canv.setFont('Times-Roman', 12) + tx = canv.beginText(left_margin, top_margin - 0.5*inch) + + #page + pg = canv.getPageNumber() + if verbose and pg % 10 == 0: + print 'formatted page %d' % canv.getPageNumber() + + if tx: + canv.drawText(tx) + canv.showPage() + drawPageFrame(canv) + + if verbose: + print 'about to write to disk...' + + canv.save() + + finished = time.time() + elapsed = finished - started + pages = canv.getPageNumber()-1 + speed = pages / elapsed + fileSize = os.stat('odyssey.pdf')[6] / 1024 + print '%d pages in %0.2f seconds = %0.2f pages per second, file size %d kb' % ( + pages, elapsed, speed, fileSize) + import md5 + print 'file digest: %s' % md5.md5(open('odyssey.pdf','rb').read()).hexdigest() + +if __name__=='__main__': + quiet = ('-q' in sys.argv) + run(verbose = not quiet) \ No newline at end of file diff --git a/bin/reportlab/demos/odyssey/odyssey.txt b/bin/reportlab/demos/odyssey/odyssey.txt new file mode 100644 index 00000000000..f5e4b7f9d60 --- /dev/null +++ b/bin/reportlab/demos/odyssey/odyssey.txt @@ -0,0 +1,207 @@ +Provided by The Internet Classics Archive. +See bottom for copyright. Available online at + http://classics.mit.edu//Homer/odyssey.html + +The Odyssey +By Homer + + +Translated by Samuel Butler + +---------------------------------------------------------------------- + +BOOK I +ITell me, O muse, of that ingenious hero who travelled far and wide + a b c &| & | A' A ' after he had sacked the famous town of Troy. Many cities did he visit, +and many were the nations with whose manners and customs he was acquainted; +moreover he suffered much by sea while trying to save his own life +and bring his men safely home; but do what he might he could not +save1 +his men, for they perished through their own sheer folly in eating +the cattle of the Sun-god Hyperion; so the god prevented them from +ever reaching home. Tell me, too, about all these things, O daughter +of Jove, from whatsoever source you may know them. + +So now all who escaped death in battle or by shipwreck had got safely +home except Ulysses, and he, though he was longing to return to his +wife and country, was detained by the goddess Calypso, who had got +him into a large cave and wanted to marry him. But as years went by, +there came a time when the gods settled that he should go back to +Ithaca; even then, however, when he was among his own people, his +troubles were not yet over; nevertheless all the gods had now begun +to pity him except Neptune, who still persecuted him without ceasing +and would not let him get home. + +Now Neptune had gone off to the Ethiopians, who are at the world's +end, and lie in two halves, the one looking West and the other East. +He had gone there to accept a hecatomb of sheep and oxen, and was +enjoying himself at his festival; but the other gods met in the house +of Olympian Jove, and the sire of gods and men spoke first. At that +moment he was thinking of Aegisthus, who had been killed by Agamemnon's +son Orestes; so he said to the other gods: + +"See now, how men lay blame upon us gods for what is after all nothing +but their own folly. Look at Aegisthus; he must needs make love to +Agamemnon's wife unrighteously and then kill Agamemnon, though he +knew it would be the death of him; for I sent Mercury to warn him +not to do either of these things, inasmuch as Orestes would be sure +to take his revenge when he grew up and wanted to return home. Mercury +told him this in all good will but he would not listen, and now he +has paid for everything in full." + +Then Minerva said, "Father, son of Saturn, King of kings, it served +Aegisthus right, and so it would any one else who does as he did; +but Aegisthus is neither here nor there; it is for Ulysses that my +heart bleeds, when I think of his sufferings in that lonely sea-girt +island, far away, poor man, from all his friends. It is an island +covered with forest, in the very middle of the sea, and a goddess +lives there, daughter of the magician Atlas, who looks after the bottom +of the ocean, and carries the great columns that keep heaven and earth +asunder. This daughter of Atlas has got hold of poor unhappy Ulysses, +and keeps trying by every kind of blandishment to make him forget +his home, so that he is tired of life, and thinks of nothing but how +he may once more see the smoke of his own chimneys. You, sir, take +no heed of this, and yet when Ulysses was before Troy did he not propitiate +you with many a burnt sacrifice? Why then should you keep on being +so angry with him?" + +And Jove said, "My child, what are you talking about? How can I forget +Ulysses than whom there is no more capable man on earth, nor more +liberal in his offerings to the immortal gods that live in heaven? +Bear in mind, however, that Neptune is still furious with Ulysses +for having blinded an eye of Polyphemus king of the Cyclopes. Polyphemus +is son to Neptune by the nymph Thoosa, daughter to the sea-king Phorcys; +therefore though he will not kill Ulysses outright, he torments him +by preventing him from getting home. Still, let us lay our heads together +and see how we can help him to return; Neptune will then be pacified, +for if we are all of a mind he can hardly stand out against us." + +And Minerva said, "Father, son of Saturn, King of kings, if, then, +the gods now mean that Ulysses should get home, we should first send +Mercury to the Ogygian island to tell Calypso that we have made up +our minds and that he is to return. In the meantime I will go to Ithaca, +to put heart into Ulysses' son Telemachus; I will embolden him to +call the Achaeans in assembly, and speak out to the suitors of his +mother Penelope, who persist in eating up any number of his sheep +and oxen; I will also conduct him to Sparta and to Pylos, to see if +he can hear anything about the return of his dear father- for this +will make people speak well of him." + +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. +Ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis ellipsis. + +"Men of Ithaca, it is all your own fault that things have turned out +as they have; you would not listen to me, nor yet to Mentor, when +we bade you check the folly of your sons who were doing much wrong +in the wantonness of their hearts- wasting the substance and dishonouring +the wife of a chieftain who they thought would not return. Now, however, +let it be as I say, and do as I tell you. Do not go out against Ulysses, +or you may find that you have been drawing down evil on your own heads." + +This was what he said, and more than half raised a loud shout, and +at once left the assembly. But the rest stayed where they were, for +the speech of Halitherses displeased them, and they sided with Eupeithes; +they therefore hurried off for their armour, and when they had armed +themselves, they met together in front of the city, and Eupeithes +led them on in their folly. He thought he was going to avenge the +murder of his son, whereas in truth he was never to return, but was +himself to perish in his attempt. + +Then Minerva said to Jove, "Father, son of Saturn, king of kings, +answer me this question- What do you propose to do? Will you set them +fighting still further, or will you make peace between them?" + +And Jove answered, "My child, why should you ask me? Was it not by +your own arrangement that Ulysses came home and took his revenge upon +the suitors? Do whatever you like, but I will tell you what I think +will be most reasonable arrangement. Now that Ulysses is revenged, +let them swear to a solemn covenant, in virtue of which he shall continue +to rule, while we cause the others to forgive and forget the massacre +of their sons and brothers. Let them then all become friends as heretofore, +and let peace and plenty reign." + +This was what Minerva was already eager to bring about, so down she +darted from off the topmost summits of Olympus. + +Now when Laertes and the others had done dinner, Ulysses began by +saying, "Some of you go out and see if they are not getting close +up to us." So one of Dolius's sons went as he was bid. Standing on +the threshold he could see them all quite near, and said to Ulysses, +"Here they are, let us put on our armour at once." + +They put on their armour as fast as they could- that is to say Ulysses, +his three men, and the six sons of Dolius. Laertes also and Dolius +did the same- warriors by necessity in spite of their grey hair. When +they had all put on their armour, they opened the gate and sallied +forth, Ulysses leading the way. + +Then Jove's daughter Minerva came up to them, having assumed the form +and voice of Mentor. Ulysses was glad when he saw her, and said to +his son Telemachus, "Telemachus, now that are about to fight in an +engagement, which will show every man's mettle, be sure not to disgrace +your ancestors, who were eminent for their strength and courage all +the world over." + +"You say truly, my dear father," answered Telemachus, "and you shall +see, if you will, that I am in no mind to disgrace your family." + +Laertes was delighted when he heard this. "Good heavens, he exclaimed, +"what a day I am enjoying: I do indeed rejoice at it. My son and grandson +are vying with one another in the matter of valour." + +On this Minerva came close up to him and said, "Son of Arceisius- +best friend I have in the world- pray to the blue-eyed damsel, and +to Jove her father; then poise your spear and hurl it." + +As she spoke she infused fresh vigour into him, and when he had prayed +to her he poised his spear and hurled it. He hit Eupeithes' helmet, +and the spear went right through it, for the helmet stayed it not, +and his armour rang rattling round him as he fell heavily to the ground. +Meantime Ulysses and his son fell the front line of the foe and smote +them with their swords and spears; indeed, they would have killed +every one of them, and prevented them from ever getting home again, +only Minerva raised her voice aloud, and made every one pause. "Men +of Ithaca," she cried, cease this dreadful war, and settle the matter +at once without further bloodshed." + +On this pale fear seized every one; they were so frightened that their +arms dropped from their hands and fell upon the ground at the sound +of the goddess's voice, and they fled back to the city for their lives. +But Ulysses gave a great cry, and gathering himself together swooped +down like a soaring eagle. Then the son of Saturn sent a thunderbolt +of fire that fell just in front of Minerva, so she said to Ulysses, +"Ulysses, noble son of Laertes, stop this warful strife, or Jove will +be angry with you." + +Thus spoke Minerva, and Ulysses obeyed her gladly. Then Minerva assumed +the form and voice of Mentor, and presently made a covenant of peace +between the two contending parties. + +THE END + +---------------------------------------------------------------------- + +Copyright statement: +The Internet Classics Archive by Daniel C. Stevenson, Web Atomics. +World Wide Web presentation is copyright (C) 1994-1998, Daniel +C. Stevenson, Web Atomics. +All rights reserved under international and pan-American copyright +conventions, including the right of reproduction in whole or in part +in any form. Direct permission requests to classics@classics.mit.edu. +Translation of "The Deeds of the Divine Augustus" by Augustus is +copyright (C) Thomas Bushnell, BSG. + + +To really test that reportlab can produce pages quickly download the +complete version of the test from http://classics.mit.edu//Homer/odyssey.html +and copy it to this directory as odyssey.full.txt. + +A zipped version of the full text is available for download at +ftp://ftp.reportlab.com/odyssey.full.zip diff --git a/bin/reportlab/demos/rlzope/readme.txt b/bin/reportlab/demos/rlzope/readme.txt new file mode 100644 index 00000000000..4bd60a5bb64 --- /dev/null +++ b/bin/reportlab/demos/rlzope/readme.txt @@ -0,0 +1,73 @@ +# rlzope : an external Zope method to show people how to use +# the ReportLab toolkit from within Zope. +# +# this method searches an image named 'logo' in the +# ZODB then prints it at the top of a simple PDF +# document made with ReportLab +# +# the resulting PDF document is returned to the +# user's web browser and, if possible, it is +# simultaneously saved into the ZODB. +# +# this method illustrates how to use both the platypus +# and canvas frameworks. +# +# License : The ReportLab Toolkit's license (similar to BSD) +# +# Author : Jerome Alet - alet@unice.fr +# + +Installation instructions : +=========================== + + 0 - If not installed then install Zope. + + 1 - Install reportlab in the Zope/lib/python/Shared directory by unpacking + the tarball and putting a reportlabs.pth file in site-packages for the Zope + used with Python. The path value in the reportlabs.pth file must be + relative. For a typical Zope installation, the path is "../../python/Shared". + Remember to restart Zope so the new path is instantiated. + + 2 - Install PIL in the Zope/lib/python/Shared directory. You need to + ensure that the _imaging.so or .pyd is also installed appropriately. + It should be compatible with the python running the zope site. + + 3 - Copy rlzope.py to your Zope installation's "Extensions" + subdirectory, e.g. /var/lib/zope/Extensions/ under Debian GNU/Linux. + + 4 - From within Zope's management interface, add an External Method with + these parameters : + + Id : rlzope + Title : rlzope + Module Name : rlzope + Function Name : rlzope + + 5 - From within Zope's management interface, add an image called "logo" + in the same Folder than rlzope, or somewhere above in the Folder + hierarchy. For example you can use ReportLab's logo which you + can find in reportlab/docs/images/replogo.gif + + 6 - Point your web browser to rlzope, e.g. on my laptop under + Debian GNU/Linux : + + http://localhost:9673/rlzope + + This will send a simple PDF document named 'dummy.pdf' to your + web browser, and if possible save it as a File object in the + Zope Object DataBase, with this name. Note, however, that if + an object with the same name already exists then it won't + be replaced for security reasons. + + You can optionally add a parameter called 'name' with + a filename as the value, to specify another filename, + e.g. : +logo + http://localhost:9673/rlzope?name=sample.pdf + + 7 - Adapt it to your own needs. + + 8 - Enjoy ! + +Send comments or bug reports at : alet@unice.fr + diff --git a/bin/reportlab/demos/rlzope/rlzope.py b/bin/reportlab/demos/rlzope/rlzope.py new file mode 100644 index 00000000000..867d8466759 --- /dev/null +++ b/bin/reportlab/demos/rlzope/rlzope.py @@ -0,0 +1,169 @@ +# +# Using the ReportLab toolkit from within Zope +# +# WARNING : The MyPDFDoc class deals with ReportLab's platypus framework, +# while the MyPageTemplate class directly deals with ReportLab's +# canvas, this way you know how to do with both... +# +# License : the ReportLab Toolkit's one +# see : http://www.reportlab.com +# +# Author : Jerome Alet - alet@unice.fr +# +# + +import string, cStringIO +try : + from Shared.reportlab.platypus.paragraph import Paragraph + from Shared.reportlab.platypus.doctemplate import * + from Shared.reportlab.lib.units import inch + from Shared.reportlab.lib import styles + from Shared.reportlab.lib.utils import ImageReader +except ImportError : + from reportlab.platypus.paragraph import Paragraph + from reportlab.platypus.doctemplate import * + from reportlab.lib.units import inch + from reportlab.lib import styles + from reportlab.lib.utils import ImageReader + +class MyPDFDoc : + class MyPageTemplate(PageTemplate) : + """Our own page template.""" + def __init__(self, parent) : + """Initialise our page template.""" + # + # we must save a pointer to our parent somewhere + self.parent = parent + + # Our doc is made of a single frame + content = Frame(0.75 * inch, 0.5 * inch, parent.document.pagesize[0] - 1.25 * inch, parent.document.pagesize[1] - (1.5 * inch)) + PageTemplate.__init__(self, "MyTemplate", [content]) + + # get all the images we need now, in case we've got + # several pages this will save some CPU + self.logo = self.getImageFromZODB("logo") + + def getImageFromZODB(self, name) : + """Retrieves an Image from the ZODB, converts it to PIL, + and makes it 0.75 inch high. + """ + try : + # try to get it from ZODB + logo = getattr(self.parent.context, name) + except AttributeError : + # not found ! + return None + + # Convert it to PIL + image = ImageReader(cStringIO.StringIO(str(logo.data))) + (width, height) = image.getSize() + + # scale it to be 0.75 inch high + multi = ((height + 0.0) / (0.75 * inch)) + width = int(width / multi) + height = int(height / multi) + + return ((width, height), image) + + def beforeDrawPage(self, canvas, doc) : + """Draws a logo and an contribution message on each page.""" + canvas.saveState() + if self.logo is not None : + # draws the logo if it exists + ((width, height), image) = self.logo + canvas.drawImage(image, inch, doc.pagesize[1] - inch, width, height) + canvas.setFont('Times-Roman', 10) + canvas.drawCentredString(inch + (doc.pagesize[0] - (1.5 * inch)) / 2, 0.25 * inch, "Contributed by Jerome Alet - alet@unice.fr") + canvas.restoreState() + + def __init__(self, context, filename) : + # save some datas + self.context = context + self.built = 0 + self.objects = [] + + # we will build an in-memory document + # instead of creating an on-disk file. + self.report = cStringIO.StringIO() + + # initialise a PDF document using ReportLab's platypus + self.document = BaseDocTemplate(self.report) + + # add our page template + # (we could add more than one, but I prefer to keep it simple) + self.document.addPageTemplates(self.MyPageTemplate(self)) + + # get the default style sheets + self.StyleSheet = styles.getSampleStyleSheet() + + # then build a simple doc with ReportLab's platypus + sometext = "A sample script to show how to use ReportLab from within Zope" + url = self.escapexml(context.absolute_url()) + urlfilename = self.escapexml(context.absolute_url() + '/%s' % filename) + self.append(Paragraph("Using ReportLab from within Zope", self.StyleSheet["Heading3"])) + self.append(Spacer(0, 10)) + self.append(Paragraph("You launched it from : %s" % url, self.StyleSheet['Normal'])) + self.append(Spacer(0, 40)) + self.append(Paragraph("If possible, this report will be automatically saved as : %s" % urlfilename, self.StyleSheet['Normal'])) + + # generation du document PDF + self.document.build(self.objects) + self.built = 1 + + def __str__(self) : + """Returns the PDF document as a string of text, or None if it's not ready yet.""" + if self.built : + return self.report.getvalue() + else : + return None + + def append(self, object) : + """Appends an object to our platypus "story" (using ReportLab's terminology).""" + self.objects.append(object) + + def escapexml(self, s) : + """Escape some xml entities.""" + s = string.strip(s) + s = string.replace(s, "&", "&") + s = string.replace(s, "<", "<") + return string.replace(s, ">", ">") + +def rlzope(self) : + """A sample external method to show people how to use ReportLab from within Zope.""" + try: + # + # which file/object name to use ? + # append ?name=xxxxx to rlzope's url to + # choose another name + filename = self.REQUEST.get("name", "dummy.pdf") + if filename[-4:] != '.pdf' : + filename = filename + '.pdf' + + # tell the browser we send some PDF document + # with the requested filename + + # get the document's content itself as a string of text + content = str(MyPDFDoc(self, filename)) + + # we will return it to the browser, but before that we also want to + # save it into the ZODB into the current folder + try : + self.manage_addFile(id = filename, file = content, title = "A sample PDF document produced with ReportLab", precondition = '', content_type = "application/pdf") + except : + # it seems an object with this name already exists in the ZODB: + # it's more secure to not replace it, since we could possibly + # destroy an important PDF document of this name. + pass + self.REQUEST.RESPONSE.setHeader('Content-Type', 'application/pdf') + self.REQUEST.RESPONSE.setHeader('Content-Disposition', 'attachment; filename=%s' % filename) + except: + import traceback, sys, cgi + content = sys.stdout = sys.stderr = cStringIO.StringIO() + self.REQUEST.RESPONSE.setHeader('Content-Type', 'text/html') + traceback.print_exc() + sys.stdout = sys.__stdout__ + sys.stderr = sys.__stderr__ + content = '
%s
' % cgi.escape(content.getvalue()) + + # then we also return the PDF content to the browser + return content diff --git a/bin/reportlab/demos/stdfonts/00readme.txt b/bin/reportlab/demos/stdfonts/00readme.txt new file mode 100644 index 00000000000..9803d8e0c9d --- /dev/null +++ b/bin/reportlab/demos/stdfonts/00readme.txt @@ -0,0 +1,7 @@ +This lists out the standard 14 fonts +in a very plain and simple fashion. + +Notably, the output is huge - it makes +two separate text objects for each glyph. +Smarter programming would make tighter +PDF, but more lines of Python! diff --git a/bin/reportlab/demos/stdfonts/stdfonts.py b/bin/reportlab/demos/stdfonts/stdfonts.py new file mode 100644 index 00000000000..44ce94f4739 --- /dev/null +++ b/bin/reportlab/demos/stdfonts/stdfonts.py @@ -0,0 +1,74 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/stdfonts/stdfonts.py +__version__=''' $Id: stdfonts.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' +__doc__=""" +This generates tables showing the 14 standard fonts in both +WinAnsi and MacRoman encodings, and their character codes. +Supply an argument of 'hex' or 'oct' to get code charts +in those encodings; octal is what you need for \\n escape +sequences in Python literals. + +usage: standardfonts.py [dec|hex|oct] +""" +import sys +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen import canvas +import string + +label_formats = {'dec':('%d=', 'Decimal'), + 'oct':('%o=','Octal'), + 'hex':('0x%x=', 'Hexadecimal')} + +def run(mode): + + label_formatter, caption = label_formats[mode] + + for enc in ['MacRoman', 'WinAnsi']: + canv = canvas.Canvas( + 'StandardFonts_%s.pdf' % enc, + ) + canv.setPageCompression(0) + + for faceName in pdfmetrics.standardFonts: + if faceName in ['Symbol', 'ZapfDingbats']: + encLabel = faceName+'Encoding' + else: + encLabel = enc + 'Encoding' + + fontName = faceName + '-' + encLabel + pdfmetrics.registerFont(pdfmetrics.Font(fontName, + faceName, + encLabel) + ) + + canv.setFont('Times-Bold', 18) + canv.drawString(80, 744, fontName) + canv.setFont('Times-BoldItalic', 12) + canv.drawRightString(515, 744, 'Labels in ' + caption) + + + #for dingbats, we need to use another font for the numbers. + #do two parallel text objects. + for byt in range(32, 256): + col, row = divmod(byt - 32, 32) + x = 72 + (66*col) + y = 720 - (18*row) + canv.setFont('Helvetica', 14) + canv.drawString(x, y, label_formatter % byt) + canv.setFont(fontName, 14) + canv.drawString(x+44, y, chr(byt).decode(encLabel,'ignore').encode('utf8')) + canv.showPage() + canv.save() + +if __name__ == '__main__': + if len(sys.argv)==2: + mode = string.lower(sys.argv[1]) + if mode not in ['dec','oct','hex']: + print __doc__ + + elif len(sys.argv) == 1: + mode = 'dec' + run(mode) + else: + print __doc__ diff --git a/bin/reportlab/demos/tests/testdemos.py b/bin/reportlab/demos/tests/testdemos.py new file mode 100644 index 00000000000..3a215bb2269 --- /dev/null +++ b/bin/reportlab/demos/tests/testdemos.py @@ -0,0 +1,14 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/demos/tests/testdemos.py +__version__=''' $Id: testdemos.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__='Test all demos' +_globals=globals().copy() +import os, sys +from reportlab import pdfgen + +for p in ('pythonpoint/pythonpoint.py','stdfonts/stdfonts.py','odyssey/odyssey.py', 'gadflypaper/gfe.py'): + fn = os.path.normcase(os.path.normpath(os.path.join(os.path.dirname(pdfgen.__file__),'..','demos',p))) + os.chdir(os.path.dirname(fn)) + execfile(fn,_globals.copy()) \ No newline at end of file diff --git a/bin/reportlab/docs/00readme.txt b/bin/reportlab/docs/00readme.txt new file mode 100644 index 00000000000..d1810ae46d0 --- /dev/null +++ b/bin/reportlab/docs/00readme.txt @@ -0,0 +1,7 @@ +Thid directory holds documentation. For end users, +it should contain a number of PDF manuals. For +people working with the source, this directory will +be the destination for any manuals built. + +If you don't see the pdf manual you expected or you wich to +ensure an up to date copy run the script tools/genAll.py! diff --git a/bin/reportlab/docs/genAll.py b/bin/reportlab/docs/genAll.py new file mode 100644 index 00000000000..0776903c9a2 --- /dev/null +++ b/bin/reportlab/docs/genAll.py @@ -0,0 +1,37 @@ +#!/bin/env python +import os, sys +def _genAll(d=None,verbose=1): + if not d: d = '.' + if not os.path.isabs(d): + d = os.path.normpath(os.path.join(os.getcwd(),d)) + L = ['reference/genreference.py', + 'userguide/genuserguide.py', + 'graphguide/gengraphguide.py', + '../tools/docco/graphdocpy.py', + ] + if os.path.isdir('../rl_addons'): + L = L + ['../rl_addons/pyRXP/docs/PyRXP_Documentation.rml'] + elif os.path.isdir('../../rl_addons'): + L = L + ['../../rl_addons/pyRXP/docs/PyRXP_Documentation.rml'] + for p in L: + os.chdir(d) + os.chdir(os.path.dirname(p)) + if p[-4:]=='.rml': + try: + from rlextra.rml2pdf.rml2pdf import main + main(exe=0,fn=[os.path.basename(p)], quiet=not verbose, outDir=d) + except: + pass + else: + cmd = '%s %s %s' % (sys.executable,os.path.basename(p), not verbose and '-s' or '') + if verbose: print cmd + os.system(cmd) + +"""Runs the manual-building scripts""" +if __name__=='__main__': + #need a quiet mode for the test suite + if '-s' in sys.argv: # 'silent + verbose = 0 + else: + verbose = 1 + _genAll(os.path.dirname(sys.argv[0]),verbose) \ No newline at end of file diff --git a/bin/reportlab/docs/graphguide/ch1_intro.py b/bin/reportlab/docs/graphguide/ch1_intro.py new file mode 100644 index 00000000000..794f4a769fc --- /dev/null +++ b/bin/reportlab/docs/graphguide/ch1_intro.py @@ -0,0 +1,105 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/graphguide/ch1_intro.py +from reportlab.tools.docco.rl_doc_utils import * +import reportlab + +title("Graphics Guide") +centred('ReportLab Version ' + reportlab.Version) + +nextTemplate("Normal") + +######################################################################## +# +# Chapter 1 +# +######################################################################## + + +heading1("Introduction") + + +heading2("About this document") +disc(""" +This document is intended to be a helpful and reasonably full +introduction to the use of the ReportLab Graphics sub-package. +Starting with simple drawings and shapes, we will take you through the +slightly more complex reusable widgets all the way through to our +powerful and flexible chart library. You will see examples of using +reportlab/graphics to make bar charts, line charts, line plots, pie +charts... and a smiley face. +""") + +disc(""" +We presume that you have already installed both the Python programming +language and the core ReportLab library. If you have not done either +of these, look in the ReportLab User Guide where chapter one +talks you through all the required steps. +""") + +disc(""" +We recommend that you read some or all of the User Guide and have at +least a basic understanding of how the ReportLab library works before +you start getting to grips with ReportLab Graphics. +""") + +disc("") +todo(""" +Be warned! This document is in a very preliminary form. We need +your help to make sure it is complete and helpful. Please send any +feedback to our user mailing list, reportlab-users@reportlab.com. +""") + +heading2("What is ReportLab?") +disc("""ReportLab is a software library that lets you directly +create documents in Adobe's Portable Document Format (PDF) using +the Python programming language. """) + +disc("""The ReportLab library directly creates PDF based on +your graphics commands. There are no intervening steps. Your applications +can generate reports extremely fast - sometimes orders +of magnitude faster than traditional report-writing +tools.""") + +heading2("What is ReportLab Graphics?") +disc(""" +ReportLab Graphics is one of the sub-packages to the ReportLab +library. It started off as a stand-alone set of programs, but is now a +fully integrated part of the ReportLab toolkit that allows you to use +its powerful charting and graphics features to improve your PDF forms +and reports. +""") + +heading2("Getting Involved") +disc("""ReportLab is an Open Source project. Although we are +a commercial company we provide the core PDF generation +sources freely, even for commercial purposes, and we make no income directly +from these modules. We also welcome help from the community +as much as any other Open Source project. There are many +ways in which you can help:""") + +bullet("""General feedback on the core API. Does it work for you? +Are there any rough edges? Does anything feel clunky and awkward?""") + +bullet("""New objects to put in reports, or useful utilities for the library. +We have an open standard for report objects, so if you have written a nice +chart or table class, why not contribute it?""") + +bullet("""Demonstrations and Case Studies: If you have produced some nice +output, send it to us (with or without scripts). If ReportLab solved a +problem for you at work, write a little 'case study' and send it in. +And if your web site uses our tools to make reports, let us link to it. +We will be happy to display your work (and credit it with your name +and company) on our site!""") + +bullet("""Working on the core code: we have a long list of things +to refine or to implement. If you are missing some features or +just want to help out, let us know!""") + +disc("""The first step for anyone wanting to learn more or +get involved is to join the mailing list. To Subscribe visit +$http://two.pairlist.net/mailman/listinfo/reportlab-users$. +From there you can also browse through the group's archives +and contributions. The mailing list is +the place to report bugs and get support. """) + diff --git a/bin/reportlab/docs/graphguide/ch2_concepts.py b/bin/reportlab/docs/graphguide/ch2_concepts.py new file mode 100644 index 00000000000..28dab45fece --- /dev/null +++ b/bin/reportlab/docs/graphguide/ch2_concepts.py @@ -0,0 +1,376 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/graphguide/ch2_concepts.py +from reportlab.tools.docco.rl_doc_utils import * + +heading1("General Concepts") + +disc(""" +In this chapter we will present some of the more fundamental principles of +the graphics library, which will show-up later in various places. +""") + + +heading2("Drawings and Renderers") + +disc(""" +A Drawing is a platform-independent description of a collection of +shapes. +It is not directly associated with PDF, Postscript or any other output +format. +Fortunately, most vector graphics systems have followed the Postscript +model and it is possible to describe shapes unambiguously. +""") + +disc(""" +A drawing contains a number of primitive Shapes. +Normal shapes are those widely known as rectangles, circles, lines, +etc. +One special (logic) shape is a Group, which can hold other +shapes and apply a transformation to them. +Groups represent composites of shapes and allow to treat the +composite as if it were a single shape. +Just about anything can be built up from a small number of basic +shapes. +""") + +disc(""" +The package provides several Renderers which know how to draw a +drawing into different formats. +These include PDF (of course), Postscript, and bitmap output. +The bitmap renderer uses Raph Levien's libart rasterizer +and Fredrik Lundh's Python Imaging Library (PIL). +Very recently, an experimental SVG renderer was also added. +It makes use of Python's standard library XML modules, so you don't +need to install the XML-SIG's additional package named PyXML. +If you have the right extensions installed, you can generate drawings +in bitmap form for the web as well as vector form for PDF documents, +and get "identical output". +""") + +disc(""" +The PDF renderer has special "privileges" - a Drawing object is also +a Flowable and, hence, can be placed directly in the story +of any Platypus document, or drawn directly on a Canvas with +one line of code. +In addition, the PDF renderer has a utility function to make +a one-page PDF document quickly. +""") + +disc(""" +The SVG renderer is special as it is still pretty experimental. +The SVG code it generates is not really optimised in any way and +maps only the features available in ReportLab Graphics (RLG) to +SVG. This means there is no support for SVG animation, interactivity, +scripting or more sophisticated clipping, masking or graduation +shapes. +So, be careful, and please report any bugs you find! +""") + +disc(""" +We expect to add both input and output filters for many vector +graphics formats in future. +SVG was the most prominent first one to start with for which there +is now an output filter in the graphics package. +An SVG input filter will probably become available in Summer 2002 +as an additional module. +GUIs will be able to obtain screen images from the bitmap output +filter working with PIL, so a chart could appear in a Tkinter +GUI window. +""") + + +heading2("Coordinate System") + +disc(""" +The Y-direction in our X-Y coordinate system points from the +bottom up. +This is consistent with PDF, Postscript and mathematical notation. +It also appears to be more natural for people, especially when +working with charts. +Note that in other graphics models (such as SVG) the Y-coordinate +points down. +For the SVG renderer this is actually no problem as it will take +your drawings and flip things as needed, so your SVG output looks +just as expected. +""") + +disc(""" +The X-coordinate points, as usual, from left to right. +So far there doesn't seem to be any model advocating the opposite +direction - at least not yet (with interesting exceptions, as it +seems, for Arabs looking at time series charts...). +""") + + +heading2("Getting Started") + +disc(""" +Let's create a simple drawing containing the string "Hello World", +displayed on top of a coloured rectangle. +After creating it we will save the drawing to a standalone PDF file. +""") + +eg(""" + from reportlab.lib import colors + from reportlab.graphics.shapes import * + + d = Drawing(400, 200) + d.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) + d.add(String(150,100, 'Hello World', + fontSize=18, fillColor=colors.red)) + + from reportlab.graphics import renderPDF + renderPDF.drawToFile(d, 'example1.pdf', 'My First Drawing') +""") + +disc("This will produce a PDF file containing the following graphic:") + +from reportlab.graphics.shapes import * +from reportlab.graphics import testshapes +t = testshapes.getDrawing01() +draw(t, "'Hello World'") + +disc(""" +Each renderer is allowed to do whatever is appropriate for its format, +and may have whatever API is needed. +If it refers to a file format, it usually has a $drawToFile$ function, +and that's all you need to know about the renderer. +Let's save the same drawing in Encapsulated Postscript format: +""") + +##eg(""" +## from reportlab.graphics import renderPS +## renderPS.drawToFile(D, 'example1.eps', 'My First Drawing') +##""") +eg(""" + from reportlab.graphics import renderPS + renderPS.drawToFile(d, 'example1.eps') +""") + +disc(""" +This will produce an EPS file with the identical drawing, which +may be imported into publishing tools such as Quark Express. +If we wanted to generate the same drawing as a bitmap file for +a website, say, all we need to do is write code like this: +""") + +eg(""" + from reportlab.graphics import renderPM + renderPM.saveToFile(d, 'example1.png', 'PNG') +""") + +disc(""" +Many other bitmap formats, like GIF, JPG, TIFF, BMP and PPN are +genuinely available, making it unlikely you'll need to add external +postprocessing steps to convert to the final format you need. +""") + +disc(""" +To produce an SVG file containing the identical drawing, which +may be imported into graphical editing tools such as Illustrator +all we need to do is write code like this: +""") + +eg(""" + from reportlab.graphics import renderSVG + renderSVG.drawToFile(d, 'example1.svg') +""") + + +heading2("Attribute Verification") + +disc(""" +Python is very dynamic and lets us execute statements at run time that +can easily be the source for unexpected behaviour. +One subtle 'error' is when assigning to an attribute that the framework +doesn't know about because the used attribute's name contains a typo. +Python lets you get away with it (adding a new attribute to an object, +say), but the graphics framework will not detect this 'typo' without +taking special counter-measures. +""") + +disc(""" +There are two verification techniques to avoid this situation. +The default is for every object to check every assignment at run +time, such that you can only assign to 'legal' attributes. +This is what happens by default. +As this imposes a small performance penalty, this behaviour can +be turned off when you need it to be. +""") + +eg(""" +>>> r = Rect(10,10,200,100, fillColor=colors.red) +>>> +>>> r.fullColor = colors.green # note the typo +>>> r.x = 'not a number' # illegal argument type +>>> del r.width # that should confuse it +""") + +disc(""" +These statements would be caught by the compiler in a statically +typed language, but Python lets you get away with it. +The first error could leave you staring at the picture trying to +figure out why the colors were wrong. +The second error would probably become clear only later, when +some back-end tries to draw the rectangle. +The third, though less likely, results in an invalid object that +would not know how to draw itself. +""") + +eg(""" +>>> r = shapes.Rect(10,10,200,80) +>>> r.fullColor = colors.green +Traceback (most recent call last): + File "", line 1, in ? + File "C:\code\users\andy\graphics\shapes.py", line 254, in __setattr__ + validateSetattr(self,attr,value) #from reportlab.lib.attrmap + File "C:\code\users\andy\lib\attrmap.py", line 74, in validateSetattr + raise AttributeError, "Illegal attribute '%s' in class %s" % (name, obj.__class__.__name__) +AttributeError: Illegal attribute 'fullColor' in class Rect +>>> +""") + +disc(""" +This imposes a performance penalty, so this behaviour can be turned +off when you need it to be. +To do this, you should use the following lines of code before you +first import reportlab.graphics.shapes: +""") + +eg(""" +>>> import reportlab.rl_config +>>> reportlab.rl_config.shapeChecking = 0 +>>> from reportlab.graphics import shapes +>>> +""") + +disc(""" +Once you turn off $shapeChecking$, the classes are actually built +without the verification hook; code should get faster, then. +Currently the penalty seems to be about 25% on batches of charts, +so it is hardly worth disabling. +However, if we move the renderers to C in future (which is eminently +possible), the remaining 75% would shrink to almost nothing and +the saving from verification would be significant. +""") + +disc(""" +Each object, including the drawing itself, has a $verify()$ method. +This either succeeds, or raises an exception. +If you turn off automatic verification, then you should explicitly +call $verify()$ in testing when developing the code, or perhaps +once in a batch process. +""") + + +heading2("Property Editing") + +disc(""" +A cornerstone of the reportlab/graphics which we will cover below is +that you can automatically document widgets. +This means getting hold of all of their editable properties, +including those of their subcomponents. +""") + +disc(""" +Another goal is to be able to create GUIs and config files for +drawings. +A generic GUI can be built to show all editable properties +of a drawing, and let you modify them and see the results. +The Visual Basic or Delphi development environment are good +examples of this kind of thing. +In a batch charting application, a file could list all the +properties of all the components in a chart, and be merged +with a database query to make a batch of charts. +""") + +disc(""" +To support these applications we have two interfaces, $getProperties$ +and $setProperties$, as well as a convenience method $dumpProperties$. +The first returns a dictionary of the editable properties of an +object; the second sets them en masse. +If an object has publicly exposed 'children' then one can recursively +set and get their properties too. +This will make much more sense when we look at Widgets later on, +but we need to put the support into the base of the framework. +""") + +eg(""" +>>> r = shapes.Rect(0,0,200,100) +>>> import pprint +>>> pprint.pprint(r.getProperties()) +{'fillColor': Color(0.00,0.00,0.00), + 'height': 100, + 'rx': 0, + 'ry': 0, + 'strokeColor': Color(0.00,0.00,0.00), + 'strokeDashArray': None, + 'strokeLineCap': 0, + 'strokeLineJoin': 0, + 'strokeMiterLimit': 0, + 'strokeWidth': 1, + 'width': 200, + 'x': 0, + 'y': 0} +>>> r.setProperties({'x':20, 'y':30, 'strokeColor': colors.red}) +>>> r.dumpProperties() +fillColor = Color(0.00,0.00,0.00) +height = 100 +rx = 0 +ry = 0 +strokeColor = Color(1.00,0.00,0.00) +strokeDashArray = None +strokeLineCap = 0 +strokeLineJoin = 0 +strokeMiterLimit = 0 +strokeWidth = 1 +width = 200 +x = 20 +y = 30 +>>> """) + +disc(""" +Note: $pprint$ is the standard Python library module that allows +you to 'pretty print' output over multiple lines rather than having +one very long line. +""") + +disc(""" +These three methods don't seem to do much here, but as we will see +they make our widgets framework much more powerful when dealing with +non-primitive objects. +""") + + +heading2("Naming Children") + +disc(""" +You can add objects to the $Drawing$ and $Group$ objects. +These normally go into a list of contents. +However, you may also give objects a name when adding them. +This allows you to refer to and possibly change any element +of a drawing after constructing it. +""") + +eg(""" +>>> d = shapes.Drawing(400, 200) +>>> s = shapes.String(10, 10, 'Hello World') +>>> d.add(s, 'caption') +>>> s.caption.text +'Hello World' +>>> +""") + +disc(""" +Note that you can use the same shape instance in several contexts +in a drawing; if you choose to use the same $Circle$ object in many +locations (e.g. a scatter plot) and use different names to access +it, it will still be a shared object and the changes will be +global. +""") + +disc(""" +This provides one paradigm for creating and modifying interactive +drawings. +""") \ No newline at end of file diff --git a/bin/reportlab/docs/graphguide/ch3_shapes.py b/bin/reportlab/docs/graphguide/ch3_shapes.py new file mode 100644 index 00000000000..375513a037e --- /dev/null +++ b/bin/reportlab/docs/graphguide/ch3_shapes.py @@ -0,0 +1,416 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/graphguide/ch3_shapes.py + +from reportlab.tools.docco.rl_doc_utils import * +from reportlab.graphics.shapes import * + +heading1("Shapes") + +disc(""" +This chapter describes the concept of shapes and their importance +as building blocks for all output generated by the graphics library. +Some properties of existing shapes and their relationship to +diagrams are presented and the notion of having different renderers +for different output formats is briefly introduced. +""") + +heading2("Available Shapes") + +disc(""" +Drawings are made up of Shapes. +Absolutely anything can be built up by combining the same set of +primitive shapes. +The module $shapes.py$ supplies a number of primitive shapes and +constructs which can be added to a drawing. +They are: +""") + +bullet("Rect") +bullet("Circle") +bullet("Ellipse") +bullet("Wedge (a pie slice)") +bullet("Polygon") +bullet("Line") +bullet("PolyLine") +bullet("String") +bullet("Group") +bullet("Path (not implemented yet, but will be added in the future)") + +disc(""" +The following drawing, taken from our test suite, shows most of the +basic shapes (except for groups). +Those with a filled green surface are also called solid shapes +(these are $Rect$, $Circle$, $Ellipse$, $Wedge$ and $Polygon$). +""") + +from reportlab.graphics import testshapes + +t = testshapes.getDrawing06() +draw(t, "Basic shapes") + + +heading2("Shape Properties") + +disc(""" +Shapes have two kinds of properties - some to define their geometry +and some to define their style. +Let's create a red rectangle with 3-point thick green borders: +""") + +eg(""" +>>> from reportlab.graphics.shapes import Rect +>>> from reportlab.lib.colors import red, green +>>> r = Rect(5, 5, 200, 100) +>>> r.fillColor = red +>>> r.strokeColor = green +>>> r.strokeWidth = 3 +>>> +""") + +from reportlab.graphics.shapes import Rect +from reportlab.lib.colors import red, green +d = Drawing(220, 120) +r = Rect(5, 5, 200, 100) +r.fillColor = red +r.strokeColor = green +r.strokeWidth = 3 +d.add(r) +draw(d, "red rectangle with green border") + +disc(""" +Note: In future examples we will omit the import statements. +""") + +disc(""" +All shapes have a number of properties which can be set. +At an interactive prompt, we can use their dumpProperties() +method to list these. +Here's what you can use to configure a Rect: +""") + +eg(""" +>>> r.dumpProperties() +fillColor = Color(1.00,0.00,0.00) +height = 100 +rx = 0 +ry = 0 +strokeColor = Color(0.00,0.50,0.00) +strokeDashArray = None +strokeLineCap = 0 +strokeLineJoin = 0 +strokeMiterLimit = 0 +strokeWidth = 3 +width = 200 +x = 5 +y = 5 +>>> +""") + +disc(""" +Shapes generally have style properties and geometry +properties. +$x$, $y$, $width$ and $height$ are part of the geometry and must +be provided when creating the rectangle, since it does not make +much sense without those properties. +The others are optional and come with sensible defaults. +""") + +disc(""" +You may set other properties on subsequent lines, or by passing them +as optional arguments to the constructor. +We could also have created our rectangle this way: +""") + +eg(""" +>>> r = Rect(5, 5, 200, 100, + fillColor=red, + strokeColor=green, + strokeWidth=3) +""") + +disc(""" +Let's run through the style properties. $fillColor$ is obvious. +$stroke$ is publishing terminology for the edge of a shape; +the stroke has a color, width, possibly a dash pattern, and +some (rarely used) features for what happens when a line turns +a corner. +$rx$ and $ry$ are optional geometric properties and are used to +define the corner radius for a rounded rectangle. +""") + +disc("All the other solid shapes share the same style properties.") + + +heading2("Lines") + +disc(""" +We provide single straight lines, PolyLines and curves. +Lines have all the $stroke*$ properties, but no $fillColor$. +Here are a few Line and PolyLine examples and the corresponding +graphics output: +""") + +eg(""" + Line(50,50, 300,100, + strokeColor=colors.blue, strokeWidth=5) + Line(50,100, 300,50, + strokeColor=colors.red, + strokeWidth=10, + strokeDashArray=[10, 20]) + PolyLine([120,110, 130,150, 140,110, 150,150, 160,110, + 170,150, 180,110, 190,150, 200,110], + strokeWidth=2, + strokeColor=colors.purple) +""") + +d = Drawing(400, 200) +d.add(Line(50,50, 300,100,strokeColor=colors.blue, strokeWidth=5)) +d.add(Line(50,100, 300,50, + strokeColor=colors.red, + strokeWidth=10, + strokeDashArray=[10, 20])) +d.add(PolyLine([120,110, 130,150, 140,110, 150,150, 160,110, + 170,150, 180,110, 190,150, 200,110], + strokeWidth=2, + strokeColor=colors.purple)) +draw(d, "Line and PolyLine examples") + + +heading2("Strings") + +disc(""" +The ReportLab Graphics package is not designed for fancy text +layout, but it can place strings at desired locations and with +left/right/center alignment. +Let's specify a $String$ object and look at its properties: +""") + +eg(""" +>>> s = String(10, 50, 'Hello World') +>>> s.dumpProperties() +fillColor = Color(0.00,0.00,0.00) +fontName = Times-Roman +fontSize = 10 +text = Hello World +textAnchor = start +x = 10 +y = 50 +>>> +""") + +disc(""" +Strings have a textAnchor property, which may have one of the +values 'start', 'middle', 'end'. +If this is set to 'start', x and y relate to the start of the +string, and so on. +This provides an easy way to align text. +""") + +disc(""" +Strings use a common font standard: the Type 1 Postscript fonts +present in Acrobat Reader. +We can thus use the basic 14 fonts in ReportLab and get accurate +metrics for them. +We have recently also added support for extra Type 1 fonts +and the renderers all know how to render Type 1 fonts. +""") + +##Until now we have worked with bitmap renderers which have to use +##TrueType fonts and which make some substitutions; this could lead +##to differences in text wrapping or even the number of labels on +##a chart between renderers. + +disc(""" +Here is a more fancy example using the code snippet below. +Please consult the ReportLab User Guide to see how non-standard +like 'LettErrorRobot-Chrome' fonts are being registered! +""") + +eg(""" + d = Drawing(400, 200) + for size in range(12, 36, 4): + d.add(String(10+size*2, 10+size*2, 'Hello World', + fontName='Times-Roman', + fontSize=size)) + + d.add(String(130, 120, 'Hello World', + fontName='Courier', + fontSize=36)) + + d.add(String(150, 160, 'Hello World', + fontName='LettErrorRobot-Chrome', + fontSize=36)) +""") + +from reportlab.pdfbase import pdfmetrics +from reportlab import rl_config +rl_config.warnOnMissingFontGlyphs = 0 +afmFile, pfbFile = getJustFontPaths() +T1face = pdfmetrics.EmbeddedType1Face(afmFile, pfbFile) +T1faceName = 'LettErrorRobot-Chrome' +pdfmetrics.registerTypeFace(T1face) +T1font = pdfmetrics.Font(T1faceName, T1faceName, 'WinAnsiEncoding') +pdfmetrics.registerFont(T1font) + +d = Drawing(400, 200) +for size in range(12, 36, 4): + d.add(String(10+size*2, 10+size*2, 'Hello World', + fontName='Times-Roman', + fontSize=size)) + +d.add(String(130, 120, 'Hello World', + fontName='Courier', + fontSize=36)) + +d.add(String(150, 160, 'Hello World', + fontName='LettErrorRobot-Chrome', + fontSize=36)) + +draw(d, 'fancy font example') + + +heading2("""Paths""") + +disc(""" +Postscript paths are a widely understood concept in graphics. +They are not implemented in $reportlab/graphics$ as yet, but they +will be, soon. +""") + +# NB This commented out section is for 'future compatibility' - paths haven't +# been implemented yet, but when they are we can uncomment this back in. + + ##disc("""Postscript paths are a widely understood concept in graphics. A Path + ## is a way of defining a region in space. You put an imaginary pen down, + ## draw straight and curved segments, and even pick the pen up and move + ## it. At the end of this you have described a region, which may consist + ## of several distinct close shapes or unclosed lines. At the end, this + ## 'path' is 'stroked and filled' according to its properties. A Path has + ## the same style properties as a solid shape. It can be used to create + ## any irregular shape.""") + ## + ##disc("""In Postscript-based imaging models such as PDF, Postscript and SVG, + ## everything is done with paths. All the specific shapes covered above + ## are instances of paths; even text strings (which are shapes in which + ## each character is an outline to be filled). Here we begin creating a + ## path with a straight line and a bezier curve:""") + ## + ##eg(""" + ##>>> P = Path(0,0, strokeWidth=3, strokeColor=red) + ##>>> P.lineTo(0, 50) + ##>>> P.curveTo(10,50,80,80,100,30) + ##>>> + ##""") + + ##disc("""As well as being the only way to draw complex shapes, paths offer some + ## performance advantages in renderers which support them. If you want to + ## create a scatter plot with 5000 blue circles of different sizes, you + ## can create 5000 circles, or one path object. With the latter, you only + ## need to set the color and line width once. PINGO just remembers the + ## drawing sequence, and writes it out into the file. In renderers which + ## do not support paths, the renderer will still have to decompose it + ## into 5000 circles so you won't save anything.""") + ## + ##disc("""Note that our current path implementation is an approximation; it + ## should be finished off accurately for PDF and PS.""") + + +heading2("Groups") + +disc(""" +Finally, we have Group objects. +A group has a list of contents, which are other nodes. +It can also apply a transformation - its contents can be rotated, +scaled or shifted. +If you know the math, you can set the transform directly. +Otherwise it provides methods to rotate, scale and so on. +Here we make a group which is rotated and translated: +""") + +eg(""" +>>> g =Group(shape1, shape2, shape3) +>>> g.rotate(30) +>>> g.translate(50, 200) +""") + +disc(""" +Groups provide a tool for reuse. +You can make a bunch of shapes to represent some component - say, +a coordinate system - and put them in one group called "Axis". +You can then put that group into other groups, each with a different +translation and rotation, and you get a bunch of axis. +It is still the same group, being drawn in different places. +""") + +disc("""Let's do this with some only slightly more code:""") + +eg(""" + d = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), # x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), # ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), # ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + d.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.rotate(15) + + d.add(secondAxisGroup) + + thirdAxisGroup = Group(Axis, + transform=mmult(translate(300,10), + rotate(30))) + d.add(thirdAxisGroup) +""") + +d = Drawing(400, 200) +Axis = Group( + Line(0,0,100,0), # x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), # ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), # ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) +firstAxisGroup = Group(Axis) +firstAxisGroup.translate(10,10) +d.add(firstAxisGroup) +secondAxisGroup = Group(Axis) +secondAxisGroup.translate(150,10) +secondAxisGroup.rotate(15) +d.add(secondAxisGroup) +thirdAxisGroup = Group(Axis, + transform=mmult(translate(300,10), + rotate(30))) +d.add(thirdAxisGroup) +draw(d, "Groups examples") \ No newline at end of file diff --git a/bin/reportlab/docs/graphguide/ch4_widgets.py b/bin/reportlab/docs/graphguide/ch4_widgets.py new file mode 100644 index 00000000000..eed115d6a86 --- /dev/null +++ b/bin/reportlab/docs/graphguide/ch4_widgets.py @@ -0,0 +1,422 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/graphguide/ch4_widgets.py + +from reportlab.tools.docco.rl_doc_utils import * +from reportlab.graphics.shapes import * +from reportlab.graphics.widgets import signsandsymbols + +heading1("Widgets") + +disc(""" +We now describe widgets and how they relate to shapes. +Using many examples it is shown how widgets make reusable +graphics components. +""") + + +heading2("Shapes vs. Widgets") + +disc("""Up until now, Drawings have been 'pure data'. There is no code in them + to actually do anything, except assist the programmer in checking and + inspecting the drawing. In fact, that's the cornerstone of the whole + concept and is what lets us achieve portability - a renderer only + needs to implement the primitive shapes.""") + +disc("""We want to build reusable graphic objects, including a powerful chart + library. To do this we need to reuse more tangible things than + rectangles and circles. We should be able to write objects for other + to reuse - arrows, gears, text boxes, UML diagram nodes, even fully + fledged charts.""") + +disc(""" +The Widget standard is a standard built on top of the shapes module. +Anyone can write new widgets, and we can build up libraries of them. +Widgets support the $getProperties()$ and $setProperties()$ methods, +so you can inspect and modify as well as document them in a uniform +way. +""") + +bullet("A widget is a reusable shape ") +bullet("""it can be initialized with no arguments + when its $draw()$ method is called it creates a primitive Shape or a + Group to represent itself""") +bullet("""It can have any parameters you want, and they can drive the way it is + drawn""") +bullet("""it has a $demo()$ method which should return an attractively drawn + example of itself in a 200x100 rectangle. This is the cornerstone of + the automatic documentation tools. The $demo()$ method should also have + a well written docstring, since that is printed too!""") + +disc("""Widgets run contrary to the idea that a drawing is just a bundle of + shapes; surely they have their own code? The way they work is that a + widget can convert itself to a group of primitive shapes. If some of + its components are themselves widgets, they will get converted too. + This happens automatically during rendering; the renderer will not see + your chart widget, but just a collection of rectangles, lines and + strings. You can also explicitly 'flatten out' a drawing, causing all + widgets to be converted to primitives.""") + + +heading2("Using a Widget") + +disc(""" +Let's imagine a simple new widget. +We will use a widget to draw a face, then show how it was implemented.""") + +eg(""" +>>> from reportlab.lib import colors +>>> from reportlab.graphics import shapes +>>> from reportlab.graphics import widgetbase +>>> from reportlab.graphics import renderPDF +>>> d = shapes.Drawing(200, 100) +>>> f = widgetbase.Face() +>>> f.skinColor = colors.yellow +>>> f.mood = "sad" +>>> d.add(f) +>>> renderPDF.drawToFile(d, 'face.pdf', 'A Face') +""") + +from reportlab.graphics import widgetbase +d = Drawing(200, 120) +f = widgetbase.Face() +f.x = 50 +f.y = 10 +f.skinColor = colors.yellow +f.mood = "sad" +d.add(f) +draw(d, 'A sample widget') + +disc(""" +Let's see what properties it has available, using the $setProperties()$ +method we have seen earlier: +""") + +eg(""" +>>> f.dumpProperties() +eyeColor = Color(0.00,0.00,1.00) +mood = sad +size = 80 +skinColor = Color(1.00,1.00,0.00) +x = 10 +y = 10 +>>> +""") + +disc(""" +One thing which seems strange about the above code is that we did not +set the size or position when we made the face. +This is a necessary trade-off to allow a uniform interface for +constructing widgets and documenting them - they cannot require +arguments in their $__init__()$ method. +Instead, they are generally designed to fit in a 200 x 100 +window, and you move or resize them by setting properties such as +x, y, width and so on after creation. +""") + +disc(""" +In addition, a widget always provides a $demo()$ method. +Simple ones like this always do something sensible before setting +properties, but more complex ones like a chart would not have any +data to plot. +The documentation tool calls $demo()$ so that your fancy new chart +class can create a drawing showing what it can do. +""") + +disc(""" +Here are a handful of simple widgets available in the module +signsandsymbols.py: +""") + +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.widgets import signsandsymbols + +d = Drawing(230, 230) + +ne = signsandsymbols.NoEntry() +ds = signsandsymbols.DangerSign() +fd = signsandsymbols.FloppyDisk() +ns = signsandsymbols.NoSmoking() + +ne.x, ne.y = 10, 10 +ds.x, ds.y = 120, 10 +fd.x, fd.y = 10, 120 +ns.x, ns.y = 120, 120 + +d.add(ne) +d.add(ds) +d.add(fd) +d.add(ns) + +draw(d, 'A few samples from signsandsymbols.py') + +disc(""" +And this is the code needed to generate them as seen in the drawing above: +""") + +eg(""" +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.widgets import signsandsymbols + +d = Drawing(230, 230) + +ne = signsandsymbols.NoEntry() +ds = signsandsymbols.DangerSign() +fd = signsandsymbols.FloppyDisk() +ns = signsandsymbols.NoSmoking() + +ne.x, ne.y = 10, 10 +ds.x, ds.y = 120, 10 +fd.x, fd.y = 10, 120 +ns.x, ns.y = 120, 120 + +d.add(ne) +d.add(ds) +d.add(fd) +d.add(ns) +""") + + +heading2("Compound Widgets") + +disc("""Let's imagine a compound widget which draws two faces side by side. + This is easy to build when you have the Face widget.""") + +eg(""" +>>> tf = widgetbase.TwoFaces() +>>> tf.faceOne.mood +'happy' +>>> tf.faceTwo.mood +'sad' +>>> tf.dumpProperties() +faceOne.eyeColor = Color(0.00,0.00,1.00) +faceOne.mood = happy +faceOne.size = 80 +faceOne.skinColor = None +faceOne.x = 10 +faceOne.y = 10 +faceTwo.eyeColor = Color(0.00,0.00,1.00) +faceTwo.mood = sad +faceTwo.size = 80 +faceTwo.skinColor = None +faceTwo.x = 100 +faceTwo.y = 10 +>>> +""") + +disc("""The attributes 'faceOne' and 'faceTwo' are deliberately exposed so you + can get at them directly. There could also be top-level attributes, + but there aren't in this case.""") + + +heading2("Verifying Widgets") + +disc("""The widget designer decides the policy on verification, but by default + they work like shapes - checking every assignment - if the designer + has provided the checking information.""") + + +heading2("Implementing Widgets") + +disc("""We tried to make it as easy to implement widgets as possible. Here's + the code for a Face widget which does not do any type checking:""") + +eg(""" +class Face(Widget): + \"\"\"This draws a face with two eyes, mouth and nose.\"\"\" + + def __init__(self): + self.x = 10 + self.y = 10 + self.size = 80 + self.skinColor = None + self.eyeColor = colors.blue + self.mood = 'happy' + + def draw(self): + s = self.size # abbreviate as we will use this a lot + g = shapes.Group() + g.transform = [1,0,0,1,self.x, self.y] + # background + g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5, + fillColor=self.skinColor)) + # CODE OMITTED TO MAKE MORE SHAPES + return g +""") + +disc("""We left out all the code to draw the shapes in this document, but you + can find it in the distribution in $widgetbase.py$.""") + +disc("""By default, any attribute without a leading underscore is returned by + setProperties. This is a deliberate policy to encourage consistent + coding conventions.""") + +disc("""Once your widget works, you probably want to add support for + verification. This involves adding a dictionary to the class called + $_verifyMap$, which map from attribute names to 'checking functions'. + The $widgetbase.py$ module defines a bunch of checking functions with names + like $isNumber$, $isListOfShapes$ and so on. You can also simply use $None$, + which means that the attribute must be present but can have any type. + And you can and should write your own checking functions. We want to + restrict the "mood" custom attribute to the values "happy", "sad" or + "ok". So we do this:""") + +eg(""" +class Face(Widget): + \"\"\"This draws a face with two eyes. It exposes a + couple of properties to configure itself and hides + all other details\"\"\" + def checkMood(moodName): + return (moodName in ('happy','sad','ok')) + _verifyMap = { + 'x': shapes.isNumber, + 'y': shapes.isNumber, + 'size': shapes.isNumber, + 'skinColor':shapes.isColorOrNone, + 'eyeColor': shapes.isColorOrNone, + 'mood': checkMood + } +""") + +disc("""This checking will be performed on every attribute assignment; or, if + $config.shapeChecking$ is off, whenever you call $myFace.verify()$.""") + + +heading2("Documenting Widgets") + +disc(""" +We are working on a generic tool to document any Python package or +module; this is already checked into ReportLab and will be used to +generate a reference for the ReportLab package. +When it encounters widgets, it adds extra sections to the +manual including:""") + +bullet("the doc string for your widget class ") +bullet("the code snippet from your demo() method, so people can see how to use it") +bullet("the drawing produced by the demo() method ") +bullet("the property dump for the widget in the drawing. ") + +disc(""" +This tool will mean that we can have guaranteed up-to-date +documentation on our widgets and charts, both on the web site +and in print; and that you can do the same for your own widgets, +too! +""") + + +heading2("Widget Design Strategies") + +disc("""We could not come up with a consistent architecture for designing + widgets, so we are leaving that problem to the authors! If you do not + like the default verification strategy, or the way + $setProperties/getProperties$ works, you can override them yourself.""") + +disc("""For simple widgets it is recommended that you do what we did above: + select non-overlapping properties, initialize every property on + $__init__$ and construct everything when $draw()$ is called. You can + instead have $__setattr__$ hooks and have things updated when certain + attributes are set. Consider a pie chart. If you want to expose the + individual wedges, you might write code like this:""") + +eg(""" +from reportlab.graphics.charts import piecharts +pc = piecharts.Pie() +pc.defaultColors = [navy, blue, skyblue] #used in rotation +pc.data = [10,30,50,25] +pc.slices[7].strokeWidth = 5 +""") +#removed 'pc.backColor = yellow' from above code example + +disc("""The last line is problematic as we have only created four wedges - in + fact we might not have created them yet. Does $pc.wedges[7]$ raise an + error? Is it a prescription for what should happen if a seventh wedge + is defined, used to override the default settings? We dump this + problem squarely on the widget author for now, and recommend that you + get a simple one working before exposing 'child objects' whose + existence depends on other properties' values :-)""") + +disc("""We also discussed rules by which parent widgets could pass properties + to their children. There seems to be a general desire for a global way + to say that 'all wedges get their lineWidth from the lineWidth of + their parent' without a lot of repetitive coding. We do not have a + universal solution, so again leave that to widget authors. We hope + people will experiment with push-down, pull-down and pattern-matching + approaches and come up with something nice. In the meantime, we + certainly can write monolithic chart widgets which work like the ones + in, say, Visual Basic and Delphi.""") + +disc("""For now have a look at the following sample code using an early + version of a pie chart widget and the output it generates:""") + +eg(""" +from reportlab.lib.colors import * +from reportlab.graphics import shapes,renderPDF +from reportlab.graphics.charts.piecharts import Pie + +d = Drawing(400,200) +d.add(String(100,175,"Without labels", textAnchor="middle")) +d.add(String(300,175,"With labels", textAnchor="middle")) + +pc = Pie() +pc.x = 25 +pc.y = 50 +pc.data = [10,20,30,40,50,60] +pc.slices[0].popout = 5 +d.add(pc, 'pie1') + +pc2 = Pie() +pc2.x = 150 +pc2.y = 50 +pc2.data = [10,20,30,40,50,60] +pc2.labels = ['a','b','c','d','e','f'] +d.add(pc2, 'pie2') + +pc3 = Pie() +pc3.x = 275 +pc3.y = 50 +pc3.data = [10,20,30,40,50,60] +pc3.labels = ['a','b','c','d','e','f'] +pc3.wedges.labelRadius = 0.65 +pc3.wedges.fontName = "Helvetica-Bold" +pc3.wedges.fontSize = 16 +pc3.wedges.fontColor = colors.yellow +d.add(pc3, 'pie3') +""") + +# Hack to force a new paragraph before the todo() :-( +disc("") + +from reportlab.lib.colors import * +from reportlab.graphics import shapes,renderPDF +from reportlab.graphics.charts.piecharts import Pie + +d = Drawing(400,200) +d.add(String(100,175,"Without labels", textAnchor="middle")) +d.add(String(300,175,"With labels", textAnchor="middle")) + +pc = Pie() +pc.x = 25 +pc.y = 50 +pc.data = [10,20,30,40,50,60] +pc.slices[0].popout = 5 +d.add(pc, 'pie1') + +pc2 = Pie() +pc2.x = 150 +pc2.y = 50 +pc2.data = [10,20,30,40,50,60] +pc2.labels = ['a','b','c','d','e','f'] +d.add(pc2, 'pie2') + +pc3 = Pie() +pc3.x = 275 +pc3.y = 50 +pc3.data = [10,20,30,40,50,60] +pc3.labels = ['a','b','c','d','e','f'] +pc3.slices.labelRadius = 0.65 +pc3.slices.fontName = "Helvetica-Bold" +pc3.slices.fontSize = 16 +pc3.slices.fontColor = colors.yellow +d.add(pc3, 'pie3') + +draw(d, 'Some sample Pies') \ No newline at end of file diff --git a/bin/reportlab/docs/graphguide/ch5_charts.py b/bin/reportlab/docs/graphguide/ch5_charts.py new file mode 100644 index 00000000000..d37df78d094 --- /dev/null +++ b/bin/reportlab/docs/graphguide/ch5_charts.py @@ -0,0 +1,1229 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/graphguide/ch5_charts.py + +from reportlab.tools.docco.rl_doc_utils import * +from reportlab.graphics.shapes import * + +heading1("Charts") + +disc(""" +The motivation for much of this is to create a flexible chart +package. +This chapter presents a treatment of the ideas behind our charting +model, what the design goals are and what components of the chart +package already exist. +""") + + +heading2("Design Goals") + +disc("Here are some of the design goals: ") + +disc("Make simple top-level use really simple ") +disc("""It should be possible to create a simple chart with minimum lines of + code, yet have it 'do the right things' with sensible automatic + settings. The pie chart snippets above do this. If a real chart has + many subcomponents, you still should not need to interact with them + unless you want to customize what they do.""") + +disc("Allow precise positioning ") +disc("""An absolute requirement in publishing and graphic design is to control + the placing and style of every element. We will try to have properties + that specify things in fixed sizes and proportions of the drawing, + rather than having automatic resizing. Thus, the 'inner plot + rectangle' will not magically change when you make the font size of + the y labels bigger, even if this means your labels can spill out of + the left edge of the chart rectangle. It is your job to preview the + chart and choose sizes and spaces which will work.""") + +disc("""Some things do need to be automatic. For example, if you want to fit N + bars into a 200 point space and don't know N in advance, we specify + bar separation as a percentage of the width of a bar rather than a + point size, and let the chart work it out. This is still deterministic + and controllable.""") + +disc("Control child elements individually or as a group") +disc("""We use smart collection classes that let you customize a group of + things, or just one of them. For example you can do this in our + experimental pie chart:""") + +eg(""" +d = Drawing(400,200) +pc = Pie() +pc.x = 150 +pc.y = 50 +pc.data = [10,20,30,40,50,60] +pc.labels = ['a','b','c','d','e','f'] +pc.slices.strokeWidth=0.5 +pc.slices[3].popout = 20 +pc.slices[3].strokeWidth = 2 +pc.slices[3].strokeDashArray = [2,2] +pc.slices[3].labelRadius = 1.75 +pc.slices[3].fontColor = colors.red +d.add(pc, '') +""") + +disc("""pc.slices[3] actually lazily creates a little object which holds + information about the slice in question; this will be used to format a + fourth slice at draw-time if there is one.""") + +disc("Only expose things you should change ") +disc("""It would be wrong from a statistical viewpoint to let you directly + adjust the angle of one of the pie wedges in the above example, since + that is determined by the data. So not everything will be exposed + through the public properties. There may be 'back doors' to let you + violate this when you really need to, or methods to provide advanced + functionality, but in general properties will be orthogonal.""") + +disc("Composition and component based ") +disc("""Charts are built out of reusable child widgets. A Legend is an + easy-to-grasp example. If you need a specialized type of legend (e.g. + circular colour swatches), you should subclass the standard Legend + widget. Then you could either do something like...""") + +eg(""" +c = MyChartWithLegend() +c.legend = MyNewLegendClass() # just change it +c.legend.swatchRadius = 5 # set a property only relevant to the new one +c.data = [10,20,30] # and then configure as usual... +""") + +disc("""...or create/modify your own chart or drawing class which creates one + of these by default. This is also very relevant for time series + charts, where there can be many styles of x axis.""") + +disc("""Top level chart classes will create a number of such components, and + then either call methods or set private properties to tell them their + height and position - all the stuff which should be done for you and + which you cannot customise. We are working on modelling what the + components should be and will publish their APIs here as a consensus + emerges.""") + +disc("Multiples ") +disc("""A corollary of the component approach is that you can create diagrams + with multiple charts, or custom data graphics. Our favourite example + of what we are aiming for is the weather report in our gallery + contributed by a user; we'd like to make it easy to create such + drawings, hook the building blocks up to their legends, and feed that + data in a consistent way.""") +disc("""(If you want to see the image, it is available on our website at +http://www.reportlab.com/demos/provencio.pdf)""") + + +##heading2("Key Concepts and Components") +heading2("Overview") + +disc("""A chart or plot is an object which is placed on a drawing; it is not + itself a drawing. You can thus control where it goes, put several on + the same drawing, or add annotations.""") + +disc("""Charts have two axes; axes may be Value or Category axes. Axes in turn + have a Labels property which lets you configure all text labels or + each one individually. Most of the configuration details which vary + from chart to chart relate to axis properties, or axis labels.""") + +disc("""Objects expose properties through the interfaces discussed in the + previous section; these are all optional and are there to let the end + user configure the appearance. Things which must be set for a chart to + work, and essential communication between a chart and its components, + are handled through methods.""") + +disc("""You can subclass any chart component and use your replacement instead + of the original provided you implement the essential methods and + properties.""") + + +heading2("Labels") + +disc(""" +A label is a string of text attached to some chart element. +They are used on axes, for titles or alongside axes, or attached +to individual data points. +Labels may contain newline characters, but only one font. +""") + +disc("""The text and 'origin' of a label are typically set by its parent + object. They are accessed by methods rather than properties. Thus, the + X axis decides the 'reference point' for each tickmark label and the + numeric or date text for each label. However, the end user can set + properties of the label (or collection of labels) directly to affect + its position relative to this origin and all of its formatting.""") + +eg(""" +from reportlab.graphics import shapes +from reportlab.graphics.charts.textlabels import Label + +d = Drawing(200, 100) + +# mark the origin of the label +d.add(Circle(100,90, 5, fillColor=colors.green)) + +lab = Label() +lab.setOrigin(100,90) +lab.boxAnchor = 'ne' +lab.angle = 45 +lab.dx = 0 +lab.dy = -20 +lab.boxStrokeColor = colors.green +lab.setText('Some\nMulti-Line\nLabel') + +d.add(lab) +""") + + +from reportlab.graphics import shapes +from reportlab.graphics.charts.textlabels import Label + +d = Drawing(200, 100) + +# mark the origin of the label +d.add(Circle(100,90, 5, fillColor=colors.green)) + +lab = Label() +lab.setOrigin(100,90) +lab.boxAnchor = 'ne' +lab.angle = 45 +lab.dx = 0 +lab.dy = -20 +lab.boxStrokeColor = colors.green +lab.setText('Some\nMulti-Line\nLabel') + +d.add(lab) + +draw(d, 'Label example') + + + +disc(""" +In the drawing above, the label is defined relative to the green blob. +The text box should have its north-east corner ten points down from +the origin, and be rotated by 45 degrees about that corner. +""") + +disc(""" +At present labels have the following properties, which we believe are +sufficient for all charts we have seen to date: +""") + +disc("") + +data=[["Property", "Meaning"], + ["dx", """The label's x displacement."""], + ["dy", """The label's y displacement."""], + ["angle", """The angle of rotation (counterclockwise) applied to the label."""], + ["boxAnchor", "The label's box anchor, one of 'n', 'e', 'w', 's', 'ne', 'nw', 'se', 'sw'."], + ["textAnchor", """The place where to anchor the label's text, one of 'start', 'middle', 'end'."""], + ["boxFillColor", """The fill color used in the label's box."""], + ["boxStrokeColor", "The stroke color used in the label's box."], + ["boxStrokeWidth", """The line width of the label's box."""], + ["fontName", """The label's font name."""], + ["fontSize", """The label's font size."""], + ["leading", """The leading value of the label's text lines."""], + ["x", """The X-coordinate of the reference point."""], + ["y", """The Y-coordinate of the reference point."""], + ["width", """The label's width."""], + ["height", """The label's height."""] + ] +t=Table(data, colWidths=(100,330)) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,0),'Times-Bold',10,12), + ('FONT',(0,1),(0,-1),'Courier',8,8), + ('FONT',(1,1),(1,-1),'Times-Roman',10,12), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +getStory().append(t) +caption("""Table - Label properties""") + +disc(""" +To see many more examples of $Label$ objects with different +combinations of properties, please have a look into the +ReportLab test suite in the folder $reportlab/test$, run the +script $test_charts_textlabels.py$ and look at the PDF document +it generates! +""") + + + +heading2("Axes") + +disc(""" +We identify two basic kinds of axes - Value and Category +ones. +Both come in horizontal and vertical flavors. +Both can be subclassed to make very specific kinds of axis. +For example, if you have complex rules for which dates to display +in a time series application, or want irregular scaling, you override +the axis and make a new one. +""") + +disc(""" +Axes are responsible for determining the mapping from data to image +coordinates; transforming points on request from the chart; drawing +themselves and their tickmarks, gridlines and axis labels. +""") + +disc(""" +This drawing shows two axes, one of each kind, which have been created +directly without reference to any chart: +""") + + +from reportlab.graphics import shapes +from reportlab.graphics.charts.axes import XCategoryAxis,YValueAxis + +drawing = Drawing(400, 200) + +data = [(10, 20, 30, 40), (15, 22, 37, 42)] + +xAxis = XCategoryAxis() +xAxis.setPosition(75, 75, 300) +xAxis.configure(data) +xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +xAxis.labels.boxAnchor = 'n' +xAxis.labels[3].dy = -15 +xAxis.labels[3].angle = 30 +xAxis.labels[3].fontName = 'Times-Bold' + +yAxis = YValueAxis() +yAxis.setPosition(50, 50, 125) +yAxis.configure(data) + +drawing.add(xAxis) +drawing.add(yAxis) + +draw(drawing, 'Two isolated axes') + + +disc("Here is the code that created them: ") + +eg(""" +from reportlab.graphics import shapes +from reportlab.graphics.charts.axes import XCategoryAxis,YValueAxis + +drawing = Drawing(400, 200) + +data = [(10, 20, 30, 40), (15, 22, 37, 42)] + +xAxis = XCategoryAxis() +xAxis.setPosition(75, 75, 300) +xAxis.configure(data) +xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +xAxis.labels.boxAnchor = 'n' +xAxis.labels[3].dy = -15 +xAxis.labels[3].angle = 30 +xAxis.labels[3].fontName = 'Times-Bold' + +yAxis = YValueAxis() +yAxis.setPosition(50, 50, 125) +yAxis.configure(data) + +drawing.add(xAxis) +drawing.add(yAxis) +""") + +disc(""" +Remember that, usually, you won't have to create axes directly; +when using a standard chart, it comes with ready-made axes. +The methods are what the chart uses to configure it and take care +of the geometry. +However, we will talk through them in detail below. +The orthogonally dual axes to those we describe have essentially +the same properties, except for those refering to ticks. +""") + + +heading3("XCategoryAxis class") + +disc(""" +A Category Axis doesn't really have a scale; it just divides itself +into equal-sized buckets. +It is simpler than a value axis. +The chart (or programmer) sets its location with the method +$setPosition(x, y, length)$. +The next stage is to show it the data so that it can configure +itself. +This is easy for a category axis - it just counts the number of +data points in one of the data series. The $reversed$ attribute (if 1) +indicates that the categories should be reversed. +When the drawing is drawn, the axis can provide some help to the +chart with its $scale()$ method, which tells the chart where +a given category begins and ends on the page. +We have not yet seen any need to let people override the widths +or positions of categories. +""") + +disc("An XCategoryAxis has the following editable properties:") + +disc("") + +data=[["Property", "Meaning"], + ["visible", """Should the axis be drawn at all? Sometimes you don't want +to display one or both axes, but they still need to be there as +they manage the scaling of points."""], + ["strokeColor", "Color of the axis"], + ["strokeDashArray", """Whether to draw axis with a dash and, if so, what kind. +Defaults to None"""], + ["strokeWidth", "Width of axis in points"], + ["tickUp", """How far above the axis should the tick marks protrude? +(Note that making this equal to chart height gives you a gridline)"""], + ["tickDown", """How far below the axis should the tick mark protrude?"""], + ["categoryNames", """Either None, or a list of strings. This should have the +same length as each data series."""], + ["labels", """A collection of labels for the tick marks. By default the 'north' +of each text label (i.e top centre) is positioned 5 points down +from the centre of each category on the axis. You may redefine +any property of the whole label group or of any one label. If +categoryNames=None, no labels are drawn."""], + ["title", """Not Implemented Yet. This needs to be like a label, but also +lets you set the text directly. It would have a default +location below the axis."""]] +t=Table(data, colWidths=(100,330)) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,0),'Times-Bold',10,12), + ('FONT',(0,1),(0,-1),'Courier',8,8), + ('FONT',(1,1),(1,-1),'Times-Roman',10,12), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +getStory().append(t) +caption("""Table - XCategoryAxis properties""") + + +heading3("YValueAxis") + +disc(""" +The left axis in the diagram is a YValueAxis. +A Value Axis differs from a Category Axis in that each point along +its length corresponds to a y value in chart space. +It is the job of the axis to configure itself, and to convert Y values +from chart space to points on demand to assist the parent chart in +plotting. +""") + +disc(""" +$setPosition(x, y, length)$ and $configure(data)$ work exactly as +for a category axis. +If you have not fully specified the maximum, minimum and tick +interval, then $configure()$ results in the axis choosing suitable +values. +Once configured, the value axis can convert y data values to drawing +space with the $scale()$ method. +Thus: +""") + +eg(""" +>>> yAxis = YValueAxis() +>>> yAxis.setPosition(50, 50, 125) +>>> data = [(10, 20, 30, 40),(15, 22, 37, 42)] +>>> yAxis.configure(data) +>>> yAxis.scale(10) # should be bottom of chart +50.0 +>>> yAxis.scale(40) # should be near the top +167.1875 +>>> +""") + +disc("""By default, the highest data point is aligned with the top of the + axis, the lowest with the bottom of the axis, and the axis choose + 'nice round numbers' for its tickmark points. You may override these + settings with the properties below. """) + +disc("") + +data=[["Property", "Meaning"], + ["visible", """Should the axis be drawn at all? Sometimes you don't want +to display one or both axes, but they still need to be there as +they manage the scaling of points."""], + ["strokeColor", "Color of the axis"], + ["strokeDashArray", """Whether to draw axis with a dash and, if so, what kind. +Defaults to None"""], + ["strokeWidth", "Width of axis in points"], + ["tickLeft", """How far to the left of the axis should the tick marks protrude? +(Note that making this equal to chart height gives you a gridline)"""], + ["tickRight", """How far to the right of the axis should the tick mark protrude?"""], + + ["valueMin", """The y value to which the bottom of the axis should correspond. +Default value is None in which case the axis sets it to the lowest +actual data point (e.g. 10 in the example above). It is common to set +this to zero to avoid misleading the eye."""], + ["valueMax", """The y value to which the top of the axis should correspond. +Default value is None in which case the axis sets it to the highest +actual data point (e.g. 42 in the example above). It is common to set +this to a 'round number' so data bars do not quite reach the top."""], + ["valueStep", """The y change between tick intervals. By default this is +None, and the chart tries to pick 'nice round numbers' which are +just wider than the minimumTickSpacing below."""], + + ["valueSteps", """A list of numbers at which to place ticks."""], + + ["minimumTickSpacing", """This is used when valueStep is set to None, and ignored +otherwise. The designer specified that tick marks should be no +closer than X points apart (based, presumably, on considerations +of the label font size and angle). The chart tries values of the +type 1,2,5,10,20,50,100... (going down below 1 if necessary) until +it finds an interval which is greater than the desired spacing, and +uses this for the step."""], + ["labelTextFormat", """This determines what goes in the labels. Unlike a category +axis which accepts fixed strings, the labels on a ValueAxis are +supposed to be numbers. You may provide either a 'format string' +like '%0.2f' (show two decimal places), or an arbitrary function +which accepts a number and returns a string. One use for the +latter is to convert a timestamp to a readable year-month-day +format."""], + ["title", """Not Implemented Yet. This needs to be like a label, but also +lets you set the text directly. It would have a default +location below the axis."""]] +t=Table(data, colWidths=(100,330)) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,0),'Times-Bold',10,12), + ('FONT',(0,1),(0,-1),'Courier',8,8), + ('FONT',(1,1),(1,-1),'Times-Roman',10,12), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +getStory().append(t) +caption("""Table - YValueAxis properties""") + +disc(""" +The $valueSteps$ property lets you explicitly specify the +tick mark locations, so you don't have to follow regular intervals. +Hence, you can plot month ends and month end dates with a couple of +helper functions, and without needing special time series chart +classes. +The following code show how to create a simple $XValueAxis$ with special +tick intervals. Make sure to set the $valueSteps$ attribute before calling +the configure method! +""") + +eg(""" +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.charts.axes import XValueAxis + +drawing = Drawing(400, 100) + +data = [(10, 20, 30, 40)] + +xAxis = XValueAxis() +xAxis.setPosition(75, 50, 300) +xAxis.valueSteps = [10, 15, 20, 30, 35, 40] +xAxis.configure(data) +xAxis.labels.boxAnchor = 'n' + +drawing.add(xAxis) +""") + + +from reportlab.graphics import shapes +from reportlab.graphics.charts.axes import XValueAxis + +drawing = Drawing(400, 100) + +data = [(10, 20, 30, 40)] + +xAxis = XValueAxis() +xAxis.setPosition(75, 50, 300) +xAxis.valueSteps = [10, 15, 20, 30, 35, 40] +xAxis.configure(data) +xAxis.labels.boxAnchor = 'n' + +drawing.add(xAxis) + +draw(drawing, 'An axis with non-equidistant tick marks') + + +disc(""" +In addition to these properties, all axes classes have three +properties describing how to join two of them to each other. +Again, this is interesting only if you define your own charts +or want to modify the appearance of an existing chart using +such axes. +These properties are listed here only very briefly for now, +but you can find a host of sample functions in the module +$reportlab/graphics/axes.py$ which you can examine... +""") + +disc(""" +One axis is joined to another, by calling the method +$joinToAxis(otherAxis, mode, pos)$ on the first axis, +with $mode$ and $pos$ being the properties described by +$joinAxisMode$ and $joinAxisPos$, respectively. +$'points'$ means to use an absolute value, and $'value'$ +to use a relative value (both indicated by the the +$joinAxisPos$ property) along the axis. +""") + +disc("") + +data=[["Property", "Meaning"], + ["joinAxis", """Join both axes if true."""], + ["joinAxisMode", """Mode used for connecting axis ('bottom', 'top', 'left', 'right', 'value', 'points', None)."""], + ["joinAxisPos", """Position at which to join with other axis."""], + ] +t=Table(data, colWidths=(100,330)) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,0),'Times-Bold',10,12), + ('FONT',(0,1),(0,-1),'Courier',8,8), + ('FONT',(1,1),(1,-1),'Times-Roman',10,12), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +getStory().append(t) +caption("""Table - Axes joining properties""") + + +heading2("Bar Charts") + +disc(""" +This describes our current $VerticalBarChart$ class, which uses the +axes and labels above. +We think it is step in the right direction but is is +far from final. +Note that people we speak to are divided about 50/50 on whether to +call this a 'Vertical' or 'Horizontal' bar chart. +We chose this name because 'Vertical' appears next to 'Bar', so +we take it to mean that the bars rather than the category axis +are vertical. +""") + +disc(""" +As usual, we will start with an example: +""") + +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.charts.barcharts import VerticalBarChart + +drawing = Drawing(400, 200) + +data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + +bc = VerticalBarChart() +bc.x = 50 +bc.y = 50 +bc.height = 125 +bc.width = 300 +bc.data = data +bc.strokeColor = colors.black + +bc.valueAxis.valueMin = 0 +bc.valueAxis.valueMax = 50 +bc.valueAxis.valueStep = 10 + +bc.categoryAxis.labels.boxAnchor = 'ne' +bc.categoryAxis.labels.dx = 8 +bc.categoryAxis.labels.dy = -2 +bc.categoryAxis.labels.angle = 30 +bc.categoryAxis.categoryNames = ['Jan-99','Feb-99','Mar-99', + 'Apr-99','May-99','Jun-99','Jul-99','Aug-99'] + +drawing.add(bc) + +draw(drawing, 'Simple bar chart with two data series') + + +eg(""" + # code to produce the above chart + + from reportlab.graphics.shapes import Drawing + from reportlab.graphics.charts.barcharts import VerticalBarChart + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 10 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99','Feb-99','Mar-99', + 'Apr-99','May-99','Jun-99','Jul-99','Aug-99'] + + drawing.add(bc) +""") + +disc(""" +Most of this code is concerned with setting up the axes and +labels, which we have already covered. +Here are the top-level properties of the $VerticalBarChart$ class: +""") + +disc("") + +data=[["Property", "Meaning"], + ["data", """This should be a "list of lists of numbers" or "list of +tuples of numbers". If you have just one series, write it as +data = [(10,20,30,42),]"""], + ["x, y, width, height", """These define the inner 'plot rectangle'. We +highlighted this with a yellow border above. Note that it is +your job to place the chart on the drawing in a way which leaves +room for all the axis labels and tickmarks. We specify this 'inner +rectangle' because it makes it very easy to lay out multiple charts +in a consistent manner."""], + ["strokeColor", """Defaults to None. This will draw a border around the +plot rectangle, which may be useful in debugging. Axes will +overwrite this."""], + ["fillColor", """Defaults to None. This will fill the plot rectangle with +a solid color. (Note that we could implement dashArray etc. +as for any other solid shape)"""], + ["barLabelFormat", """This is a format string or function used for displaying +labels above each bar. They are positioned automatically +above the bar for positive values and below for negative ones."""], + ["useAbsolute", """Defaults to 0. If 1, the three properties below are +absolute values in points (which means you can make a chart +where the bars stick out from the plot rectangle); if 0, +they are relative quantities and indicate the proportional +widths of the elements involved."""], + ["barWidth", """As it says. Defaults to 10."""], + ["groupSpacing", """Defaults to 5. This is the space between each group of +bars. If you have only one series, use groupSpacing and not +barSpacing to split them up. Half of the groupSpacing is used +before the first bar in the chart, and another half at the end."""], + ["barSpacing", """Defaults to 0. This is the spacing between bars in each +group. If you wanted a little gap between green and red bars in +the example above, you would make this non-zero."""], + ["barLabelFormat", """Defaults to None. As with the YValueAxis, if you supply +a function or format string then labels will be drawn next +to each bar showing the numeric value."""], + ["barLabels", """A collection of labels used to format all bar labels. Since +this is a two-dimensional array, you may explicitly format the +third label of the second series using this syntax: + chart.barLabels[(1,2)].fontSize = 12"""], + ["valueAxis", """The value axis, which may be formatted as described +previously."""], + ["categoryAxis", """The category axis, which may be formatted as described +previously."""], + + ["title", """Not Implemented Yet. This needs to be like a label, but also +lets you set the text directly. It would have a default +location below the axis."""]] +t=Table(data, colWidths=(100,330)) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,0),'Times-Bold',10,12), + ('FONT',(0,1),(0,-1),'Courier',8,8), + ('FONT',(1,1),(1,-1),'Times-Roman',10,12), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +getStory().append(t) +caption("""Table - VerticalBarChart properties""") + + +disc(""" +From this table we deduce that adding the following lines to our code +above should double the spacing between bar groups (the $groupSpacing$ +attribute has a default value of five points) and we should also see +some tiny space between bars of the same group ($barSpacing$). +""") + +eg(""" + bc.groupSpacing = 10 + bc.barSpacing = 2.5 +""") + +disc(""" +And, in fact, this is exactly what we can see after adding these +lines to the code above. +Notice how the width of the individual bars has changed as well. +This is because the space added between the bars has to be 'taken' +from somewhere as the total chart width stays unchanged. +""") + +from reportlab.graphics.shapes import Drawing +from reportlab.graphics.charts.barcharts import VerticalBarChart + +drawing = Drawing(400, 200) + +data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + +bc = VerticalBarChart() +bc.x = 50 +bc.y = 50 +bc.height = 125 +bc.width = 300 +bc.data = data +bc.strokeColor = colors.black + +bc.groupSpacing = 10 +bc.barSpacing = 2.5 + +bc.valueAxis.valueMin = 0 +bc.valueAxis.valueMax = 50 +bc.valueAxis.valueStep = 10 + +bc.categoryAxis.labels.boxAnchor = 'ne' +bc.categoryAxis.labels.dx = 8 +bc.categoryAxis.labels.dy = -2 +bc.categoryAxis.labels.angle = 30 +bc.categoryAxis.categoryNames = ['Jan-99','Feb-99','Mar-99', + 'Apr-99','May-99','Jun-99','Jul-99','Aug-99'] + +drawing.add(bc) + +draw(drawing, 'Like before, but with modified spacing') + +disc(""" +Bars labels are automatically displayed for negative values +below the lower end of the bar for positive values +above the upper end of the other ones. +""") + + +##Property Value +##data This should be a "list of lists of numbers" or "list of tuples of numbers". If you have just one series, write it as +##data = [(10,20,30,42),] +## +##x, y, width, height These define the inner 'plot rectangle'. We highlighted this with a yellow border above. Note that it is your job to place the chart on the drawing in a way which leaves room for all the axis labels and tickmarks. We specify this 'inner rectangle' because it makes it very easy to lay out multiple charts in a consistent manner. +##strokeColor Defaults to None. This will draw a border around the plot rectangle, which may be useful in debugging. Axes will overwrite this. +##fillColor Defaults to None. This will fill the plot rectangle with a solid color. (Note that we could implement dashArray etc. as for any other solid shape) +##barLabelFormat This is a format string or function used for displaying labels above each bar. We're working on ways to position these labels so that they work for positive and negative bars. +##useAbsolute Defaults to 0. If 1, the three properties below are absolute values in points (which means you can make a chart where the bars stick out from the plot rectangle); if 0, they are relative quantities and indicate the proportional widths of the elements involved. +##barWidth As it says. Defaults to 10. +##groupSpacing Defaults to 5. This is the space between each group of bars. If you have only one series, use groupSpacing and not barSpacing to split them up. Half of the groupSpacing is used before the first bar in the chart, and another half at the end. +##barSpacing Defaults to 0. This is the spacing between bars in each group. If you wanted a little gap between green and red bars in the example above, you would make this non-zero. +##barLabelFormat Defaults to None. As with the YValueAxis, if you supply a function or format string then labels will be drawn next to each bar showing the numeric value. +##barLabels A collection of labels used to format all bar labels. Since this is a two-dimensional array, you may explicitly format the third label of the second series using this syntax: +## chart.barLabels[(1,2)].fontSize = 12 +## +##valueAxis The value axis, which may be formatted as described previously +##categoryAxis The categoryAxis, which may be formatted as described previously +##title, subTitle Not implemented yet. These would be label-like objects whose text could be set directly and which would appear in sensible locations. For now, you can just place extra strings on the drawing. + + +heading2("Line Charts") + +disc(""" +We consider "Line Charts" to be essentially the same as +"Bar Charts", but with lines instead of bars. +Both share the same pair of Category/Value axes pairs. +This is in contrast to "Line Plots", where both axes are +Value axes. +""") + +disc(""" +The following code and its output shall serve as a simple +example. +More explanation will follow. +For the time being you can also study the output of running +the tool $reportlab/lib/graphdocpy.py$ withough any arguments +and search the generated PDF document for examples of +Line Charts. +""") + +eg(""" +from reportlab.graphics.charts.linecharts import HorizontalLineChart + +drawing = Drawing(400, 200) + +data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) +] + +lc = HorizontalLineChart() +lc.x = 50 +lc.y = 50 +lc.height = 125 +lc.width = 300 +lc.data = data +lc.joinedLines = 1 +catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') +lc.categoryAxis.categoryNames = catNames +lc.categoryAxis.labels.boxAnchor = 'n' +lc.valueAxis.valueMin = 0 +lc.valueAxis.valueMax = 60 +lc.valueAxis.valueStep = 15 +lc.lines[0].strokeWidth = 2 +lc.lines[1].strokeWidth = 1.5 +drawing.add(lc) +""") + +from reportlab.graphics.charts.linecharts import HorizontalLineChart + +drawing = Drawing(400, 200) + +data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) +] + +lc = HorizontalLineChart() +lc.x = 50 +lc.y = 50 +lc.height = 125 +lc.width = 300 +lc.data = data +lc.joinedLines = 1 +catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') +lc.categoryAxis.categoryNames = catNames +lc.categoryAxis.labels.boxAnchor = 'n' +lc.valueAxis.valueMin = 0 +lc.valueAxis.valueMax = 60 +lc.valueAxis.valueStep = 15 +lc.lines[0].strokeWidth = 2 +lc.lines[1].strokeWidth = 1.5 +drawing.add(lc) + +draw(drawing, 'HorizontalLineChart sample') + + +disc("") +todo("Add properties table.") + + +heading2("Line Plots") + +disc(""" +Below we show a more complex example of a Line Plot that +also uses some experimental features like line markers +placed at each data point. +""") + +eg(""" +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.widgets.markers import makeMarker + +drawing = Drawing(400, 200) + +data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) +] + +lp = LinePlot() +lp.x = 50 +lp.y = 50 +lp.height = 125 +lp.width = 300 +lp.data = data +lp.joinedLines = 1 +lp.lines[0].symbol = makeMarker('FilledCircle') +lp.lines[1].symbol = makeMarker('Circle') +lp.lineLabelFormat = '%2.0f' +lp.strokeColor = colors.black +lp.xValueAxis.valueMin = 0 +lp.xValueAxis.valueMax = 5 +lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] +lp.xValueAxis.labelTextFormat = '%2.1f' +lp.yValueAxis.valueMin = 0 +lp.yValueAxis.valueMax = 7 +lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6] + +drawing.add(lp) +""") + + +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.widgets.markers import makeMarker + +drawing = Drawing(400, 200) + +data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) +] + +lp = LinePlot() +lp.x = 50 +lp.y = 50 +lp.height = 125 +lp.width = 300 +lp.data = data +lp.joinedLines = 1 +lp.lines[0].symbol = makeMarker('FilledCircle') +lp.lines[1].symbol = makeMarker('Circle') +lp.lineLabelFormat = '%2.0f' +lp.strokeColor = colors.black +lp.xValueAxis.valueMin = 0 +lp.xValueAxis.valueMax = 5 +lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] +lp.xValueAxis.labelTextFormat = '%2.1f' +lp.yValueAxis.valueMin = 0 +lp.yValueAxis.valueMax = 7 +lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6] + +drawing.add(lp) + +draw(drawing, 'LinePlot sample') + + + +disc("") +todo("Add properties table.") + + + +heading2("Pie Charts") + +disc(""" +We've already seen a pie chart example above. +This is provisional but seems to do most things. +At the very least we need to change the name. +For completeness we will cover it here. +""") + +eg(""" +from reportlab.graphics.charts.piecharts import Pie + +d = Drawing(200, 100) + +pc = Pie() +pc.x = 65 +pc.y = 15 +pc.width = 70 +pc.height = 70 +pc.data = [10,20,30,40,50,60] +pc.labels = ['a','b','c','d','e','f'] + +pc.slices.strokeWidth=0.5 +pc.slices[3].popout = 10 +pc.slices[3].strokeWidth = 2 +pc.slices[3].strokeDashArray = [2,2] +pc.slices[3].labelRadius = 1.75 +pc.slices[3].fontColor = colors.red + +d.add(pc) +""") + +from reportlab.graphics.charts.piecharts import Pie + +d = Drawing(200, 100) + +pc = Pie() +pc.x = 65 +pc.y = 15 +pc.width = 70 +pc.height = 70 +pc.data = [10,20,30,40,50,60] +pc.labels = ['a','b','c','d','e','f'] + +pc.slices.strokeWidth=0.5 +pc.slices[3].popout = 10 +pc.slices[3].strokeWidth = 2 +pc.slices[3].strokeDashArray = [2,2] +pc.slices[3].labelRadius = 1.75 +pc.slices[3].fontColor = colors.red + +d.add(pc) + +draw(d, 'A bare bones pie chart') + +disc(""" +Properties are covered below. +The pie has a 'wedges' collection and we document wedge properties +in the same table. +This was invented before we finished the $Label$ class and will +probably be reworked to use such labels shortly. +""") + +disc("") +todo("Add properties table.") + +##Property Value +##data a list or tuple of numbers +##x, y, width, height Bounding box of the pie. Note that x and y do NOT specify the centre but the bottom left corner, and that width and height do not have to be equal; pies may be elliptical and wedges will be drawn correctly. +##labels None, or a list of strings. Make it None if you don't want labels around the edge of the pie. Since it is impossible to know the size of slices, we generally discourage placing labels in or around pies; it is much better to put them in a legend alongside. +##startAngle Where is the start angle of the first pie slice? The default is '90' which is twelve o'clock. +##direction Which direction do slices progress in? The default is 'clockwise'. +##wedges Collection of wedges. This lets you customise each wedge, or individual ones. See below +##wedges.strokeWidth Border width for wedge +##wedges.strokeColor Border color +##wedges.strokeDashArray Solid or dashed line configuration for +##wedges.popout How far out should the slice(s) stick from the centre of +##the pie? default is zero. +##wedges.fontName +##wedges.fontSize +##wedges.fontColor Used for text labels +##wedges.labelRadius This controls the anchor point for a text label. It +##is a fraction of the radius; 0.7 will place the text inside the pie, +##1.2 will place it slightly outside. (note that if we add labels, we +##will keep this to specify their anchor point) +## + + +heading2("Legends") + +disc(""" +Various preliminary legend classes can be found but need a +cleanup to be consistent with the rest of the charting +model. +Legends are the natural place to specify the colors and line +styles of charts; we propose that each chart is created with +a $legend$ attribute which is invisible. +One would then do the following to specify colors: +""") + +eg(""" +myChart.legend.defaultColors = [red, green, blue] +""") + +disc(""" +One could also define a group of charts sharing the same legend: +""") + +eg(""" +myLegend = Legend() +myLegend.defaultColor = [red, green.....] #yuck! +myLegend.columns = 2 +# etc. +chart1.legend = myLegend +chart2.legend = myLegend +chart3.legend = myLegend +""") + +# Hack to force a new paragraph before the todo() :-( +disc("") + +todo("""Does this work? Is it an acceptable complication over specifying chart +colors directly?""") + + + +heading2("Remaining Issues") + +disc(""" +There are several issues that are almost solved, but for which +is is a bit too early to start making them really public. +Nevertheless, here is a list of things that are under way: +""") + +list(""" +Color specification - right now the chart has an undocumented property +$defaultColors$, which provides a list of colors to cycle through, +such that each data series gets its own color. +Right now, if you introduce a legend, you need to make sure it shares +the same list of colors. +Most likely, this will be replaced with a scheme to specify a kind +of legend containing attributes with different values for each data +series. +This legend can then also be shared by several charts, but need not +be visible itself. +""") + +list(""" +Additional chart types - when the current design will have become +more stable, we expect to add variants of bar charts to deal with stacked +and percentile bars as well as the side-by-side variant seen here. +""") + + +heading2("Outlook") + +disc(""" +It will take some time to deal with the full range of chart types. +We expect to finalize bars and pies first and to produce trial +implementations of more general plots, thereafter. +""") + + +heading3("X-Y Plots") + +disc(""" +Most other plots involve two value axes and directly plotting +x-y data in some form. +The series can be plotted as lines, marker symbols, both, or +custom graphics such as open-high-low-close graphics. +All share the concepts of scaling and axis/title formatting. +At a certain point, a routine will loop over the data series and +'do something' with the data points at given x-y locations. +Given a basic line plot, it should be very easy to derive a +custom chart type just by overriding a single method - say, +$drawSeries()$. +""") + + +heading3("Marker customisation and custom shapes") + +disc(""" +Well known plotting packages such as excel, Mathematica and Excel +offer ranges of marker types to add to charts. +We can do better - you can write any kind of chart widget you +want and just tell the chart to use it as an example. +""") + + +heading4("Combination plots") + +disc(""" +Combining multiple plot types is really easy. +You can just draw several charts (bar, line or whatever) in +the same rectangle, suppressing axes as needed. +So a chart could correlate a line with Scottish typhoid cases +over a 15 year period on the left axis with a set of bars showing +inflation rates on the right axis. +If anyone can remind us where this example came from we'll +attribute it, and happily show the well-known graph as an +example. +""") + + +heading3("Interactive editors") + +disc(""" +One principle of the Graphics package is to make all 'interesting' +properties of its graphic components accessible and changeable by +setting apropriate values of corresponding public attributes. +This makes it very tempting to build a tool like a GUI editor that +that helps you with doing that interactively. +""") + +disc(""" +ReportLab has built such a tool using the Tkinter toolkit that +loads pure Python code describing a drawing and records your +property editing operations. +This "change history" is then used to create code for a subclass +of that chart, say, that can be saved and used instantly just +like any other chart or as a new starting point for another +interactive editing session. +""") + +disc(""" +This is still work in progress, though, and the conditions for +releasing this need to be further elaborated. +""") + + +heading3("Misc.") + +disc(""" +This has not been an exhaustive look at all the chart classes. +Those classes are constantly being worked on. +To see exactly what is in the current distribution, use the +$graphdocpy.py$ utility. +By default, it will run on reportlab/graphics, and produce a full +report. +(If you want to run it on other modules or packages, +$graphdocpy.py -h$ prints a help message that will tell you +how.) +""") + +disc(""" +This is the tool that was mentioned in the section on 'Documenting +Widgets'. +""") \ No newline at end of file diff --git a/bin/reportlab/docs/graphguide/gengraphguide.py b/bin/reportlab/docs/graphguide/gengraphguide.py new file mode 100644 index 00000000000..541bbcedbac --- /dev/null +++ b/bin/reportlab/docs/graphguide/gengraphguide.py @@ -0,0 +1,59 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/graphguide/gengraphguide.py +__version__=''' $Id: gengraphguide.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__ = """ +This module contains the script for building the graphics guide. +""" +def run(pagesize=None, verbose=1, outDir=None): + import os + from reportlab.tools.docco.rl_doc_utils import setStory, getStory, RLDocTemplate, defaultPageSize + from reportlab.tools.docco import rl_doc_utils + from reportlab.lib.utils import open_and_read, _RL_DIR + if not outDir: outDir = os.path.join(_RL_DIR,'docs') + destfn = os.path.join(outDir,'graphguide.pdf') + doc = RLDocTemplate(destfn,pagesize = pagesize or defaultPageSize) + + #this builds the story + setStory() + G = {} + exec 'from reportlab.tools.docco.rl_doc_utils import *' in G, G + doc = RLDocTemplate(destfn,pagesize = pagesize or defaultPageSize) + for f in ( + 'ch1_intro', + 'ch2_concepts', + 'ch3_shapes', + 'ch4_widgets', + 'ch5_charts', + ): + exec open_and_read(f+'.py',mode='t') in G, G + del G + + story = getStory() + if verbose: print 'Built story contains %d flowables...' % len(story) + doc.build(story) + if verbose: print 'Saved "%s"' % destfn + +def makeSuite(): + "standard test harness support - run self as separate process" + from reportlab.test.utils import ScriptThatMakesFileTest + return ScriptThatMakesFileTest('../docs/graphguide', 'gengraphguide.py', 'graphguide.pdf') + +def main(): + import sys + verbose = '-s' not in sys.argv + if not verbose: sys.argv.remove('-s') + if len(sys.argv) > 1: + try: + pagesize = eval(sys.argv[1]) + except: + print 'Expected page size in argument 1', sys.argv[1] + raise + print 'set page size to',sys.argv[1] + else: + pagesize = None + run(pagesize,verbose) + +if __name__=="__main__": + main() diff --git a/bin/reportlab/docs/images/Edit_Prefs.gif b/bin/reportlab/docs/images/Edit_Prefs.gif new file mode 100644 index 00000000000..ebcfa34987d Binary files /dev/null and b/bin/reportlab/docs/images/Edit_Prefs.gif differ diff --git a/bin/reportlab/docs/images/Python_21.gif b/bin/reportlab/docs/images/Python_21.gif new file mode 100644 index 00000000000..1d6892e074f Binary files /dev/null and b/bin/reportlab/docs/images/Python_21.gif differ diff --git a/bin/reportlab/docs/images/Python_21_HINT.gif b/bin/reportlab/docs/images/Python_21_HINT.gif new file mode 100644 index 00000000000..967a14b6899 Binary files /dev/null and b/bin/reportlab/docs/images/Python_21_HINT.gif differ diff --git a/bin/reportlab/docs/images/fileExchange.gif b/bin/reportlab/docs/images/fileExchange.gif new file mode 100644 index 00000000000..bb5184a833d Binary files /dev/null and b/bin/reportlab/docs/images/fileExchange.gif differ diff --git a/bin/reportlab/docs/images/jpn.gif b/bin/reportlab/docs/images/jpn.gif new file mode 100644 index 00000000000..d1ecbf0f487 Binary files /dev/null and b/bin/reportlab/docs/images/jpn.gif differ diff --git a/bin/reportlab/docs/images/jpnchars.jpg b/bin/reportlab/docs/images/jpnchars.jpg new file mode 100644 index 00000000000..07c8ba6d276 Binary files /dev/null and b/bin/reportlab/docs/images/jpnchars.jpg differ diff --git a/bin/reportlab/docs/images/lj8100.jpg b/bin/reportlab/docs/images/lj8100.jpg new file mode 100644 index 00000000000..be3c6183d3b Binary files /dev/null and b/bin/reportlab/docs/images/lj8100.jpg differ diff --git a/bin/reportlab/docs/images/replogo.a85 b/bin/reportlab/docs/images/replogo.a85 new file mode 100644 index 00000000000..92653e977ec --- /dev/null +++ b/bin/reportlab/docs/images/replogo.a85 @@ -0,0 +1,439 @@ +BI +/W 283 /H 181 /BPC 8 /CS /RGB /F [/A85 /Fl] +ID +Gb"/lGEc',,kl8"&2NBd%Kaogum165&NYZBITmkcQDo:\lEnA +GJETphXmY&(BXdG&0O5g!!*-(#S8+DJ,fTO":,P]5_&h8!X&eu$f\(VI^5_q +jm)jbosgIBN01+GE&G/,0E`$J,.ogcq?_*e\?bYbO$E4*_M&A1cC@I!9Dd\' ++4?4QDr'3BbF=2qLTa,V3(b)tC;HhK!DHiEs= +#e!]_gC"9\WeUZ]%d_rZDm+pZHhMjerV*pJn(tTrkH8+&G'_fGA&n<1gUDI\ +)T%dl>;gEVr7uSSD=[2`\)2)Y\8d.Vp$:55>ISM;g9k_IXBE5_odUJ&4fhbj ++t4u*CCeUS/R,f.kn4Mb(GB+%B[KmgIC4%Q_Cr\%1,1M'_1Dj^kKfbZDr.!E +F(WairP]R[bk1DXB:hnj5(2_GWLnY9BgP/'PUTNpnM"tB`!I)(N#O]nba:,[ +7un]KC=L=e,=d\00l5LHTgOSF))>aLTKrYb%NI015l^i^qt9:L_$;(Sl-nh/ +pYP5l]D"DU][No_Y;b?do<#LYHN!Ng-V^$t+XoX`J[UDW6NstgiU^:#aXJYf +@T/-h#kbk9#UMApV<$^um2e#j'7`g:&J1#=2rAubQ**M!it6f3,*@pVZY%I6 +6o/H!J4.k`9lkR_-;U4ln3a&Bj2[3$3u/IFT]*eDlR2scpu.!iLu\C$^CPtn +H@SNS^AI?QDnk,W"$D00h*&FIlDn4&G3't(Uo5[]m!,=p/AQ+U,=dd\o,oLh +`[7$Kifm(afHdLC\5iol9&c'@AXlrl+e2TcVCt*"D43P1/tBd4IK''*Z:Y1g +&tsE#;eD`AXDo:8;HP?kPAJ]<++5EP`)3&A0_Gc=bj+0GNAo^=jcrnRI.>2- +?@.[$o*RI.f9aJ`&3E/CRVaf0!I//;.6.N@dhWJ":#_Dm4P7SLk/f_K3$fY" +@lB&>=ZT!#RpKMq"Z6@j)ODfZ2)o?9+0oW[2)q0V"P +i5SJT2Aq@18&2N,MORue((.$!o41l.*at,LGSoZG'&i)k+_>o\;RS\8iUnSbf.@["h4W^daDC< +[+g5DH18l\"e-&NZV?P&#\?tUg#Q>8N;^H/+c'&47a2<,ZndtDk/m0)o'Y9I +"Af)p(^a#(U5qJ$"NO<.;GoitP^.[:Han@R-"EJk':6\TnrLq,.c]R(i1M-HV6>$DWiJnWu`pbXf8^#tsp$Blf[o2f0&&E8Fj;UkH;2KPeTN'9<-` +_E>O(dS$eNYO@/]?Jm>IbNhuC)(3i7QGjt[o_n[]],>Y46C;2%ZOfU,n6RL6 +pQXGh.Ts48AWn&r=TE#APblWI!jfTH5qs'iZ7I([Rcm#)4]s3/IWG@.!hl8% +k"5Ai2hk31"fITC&?Wl%R3r7DZT<9r#jE`;DgM?VnOPTHJ6NEF=DX0oV=;jR +N"ee-'gN(,,1IMA,*\r!P!#ZJb/Yi%P$?h-bS)ApoF1$2S`.D2UXU#ifa/5t +gE^EM\-Jt.FMpZj7&UVY^aA_J@B/D`\QM+D5?Y%:r8SkQX)mILmG#,5Qa$@m +8\`%IN+JM>F2lkn-6bE9+'C"P4$:%88J]Hko.1njrgkG16n$[KbII#_p%Qu7 +AE+ku0`TXT)a,W-TbqN<#jU93V[&HNckm'+eCt(+$2Wka!3/NpDgDtsPjuY! +&Osl;iABPJ;B\[/"UG*'SNM&U8$BW">.&+qW`H6*^OH,o4LAeL +6:XCa%o6PiB$Hd`4)!?TBp0l=C<`"Pp/ +m\$/,\foSn:tn@>>.&+rXN:c=IYfebFG+3NbiMdW>Wh$%Tb9lJ,Z=l?0N&Gh +%hOq[K8PLeJg*I1A&jUDW1GZEB0I@H?K&bTn$qD<]C*82Gk'c!.Um,ORPB1S +X.(7fr^"6`JoT&6a&>eUf-b?fn#),:EYL950ZtXh\U7\pG(iO\9lOq# +XrZT?%Us\o*&sSo7H!XR3"?ul#`4f!Zb])99lfrHjSo#-^]+6,^OPUTa&(g] +,XjL"\_WnqMA+tu)_%J0.!LUQk"/iU(.j?`YBf'ePuh98R"@p`^A)0l[V`R2 +gPKuukg6#2UgK&_!SG#.Y9G=NE!Qjc;%hIeB-:05)Dr'p+snP*bJ%1&i@((p'?IgAp1[a)GE\GTFH2IL`l^S2o(A6m?6E<2l$L +;Y'R(,(@?ZKiF0"q<+@tT&PK,F%g#(oSZil5CQj_]O[q`Br_?MX^AjBUQ7T" +@`4'cHhKk=]U=,@T<#7>mE&],fEsJ/^mkC9$&OG7q&6HTG\$(lqac@H(VP3,j+0.uF2VMDhWa371^%^Af^3o`WX]fl=THUV8 +;J-N15UCZnZ$lNCS@!1iRd72#ac[r!.4H&c>e#3_/&6:Xk!k\T$ULAU46i)h +KH%IBj[)gcEr,Fm8/F9"JG-bqWJ=P0'Sa_a8O+\a'e<<9=gB:gLMs_TH*TS32)&7f!:AMCG&[J)1AofJl$\;g^ZSq!^Cknt;Dc +rq^D)=Ld1@AECt!d)V]q@7Ekg^RdT8Q7,V)DV_mO20\KDKS0K@4tMr8-me_+ +6ALSS;34Fo$;PSrC"&qrfWZNdMAT:eiJ/N9h/.>n:)McoCDR=g&EbB?cmQGOOBGCXu1L3)ObU2-@^S6cq05 +nc5;/mQ%?`p.hBGdaHQ]G81hMC-X9k5CORVD$e$*2eaH?-X<`[^HO@dZM4=l +hab9!R=Or%o^Ut^'0p7ELITD^<^"+ +Y!l5XU?A.u?="`9krB9aJ"#6+F6Cj4:db>5(9+:4@Z;6SRum`F3IA4r#nnWs&Oa4CR=Gqu:(/!q"AuW/k>2KVoY(R4ecMKT7N&8fNoIBR'&aTu +_%a8nlP$gt1hjg.A&aJ$sW.\U= +KOa]?e`5]MM*PY*DnCl6E32BB-7:0ra%\H4"(mN22(EhnjW*?28pfK@J`[[!MH!B8[HQK"!jf +o&R]D%nHJm"5FeBE%?@jo3S$g"8jar0l.N@RCY?!>0pJlFiV":, +&g1[(4*Jhq4I[qd=t#2]@%_3GZgHMV8%Q>l$=k!K4DE4hF[E))8cGMmOmSJk?qlY!GSG +'_ngSBXGW%WF4CmbBfH-3VQK-:?/N?Na1r%YB8a/6fP63Waj9$Pl!n ++l7j>8A\fLp2K7U;6T>E8cDG6!4WXc7"?Hb4Wk7fh,^l +Gu-2k?3!Y7cm+Skj[`5QE>n)0T9M@!8U^1;abaC7_SidkoJoUefH/9?E +m[7^Z+i%f8.H9CJ_lri9)WY8p75:$OAAe5ELDN`>?bUp7P;"RLL(age=oA'T +O+^%JPcaC(_^&i&\OMW$?*I^&jCHZ47FAA,!\WBYV+\4^X;sfD#_(f_`)u%D +ioZYkE\3NqppFke.Pj4>CKh1_F!5H&-r[]0L18NU;5m^/LH4?c?Q`Oc+.f> +e'lc1r:!(0,K&T&5#R9!O*0a;.9*5W'_;b]%M6T=E8Z8c8n+!r1V8tF-R:'I ++$4X^F::4AG[id#s#kMi'IFbKBbr[3@`!p_(!L,Yf9&7*"3cmV!eZ2bigph@OdFSrfZ>_)pa+!F0[?c:Jha3e$ +Wb9YLH1U1ceu]n+M;lM9Y4]K4f5O5c]&>)%37YT_leK$l5PsfR1M=uAFuG9k +S2bN*&eV9`ZAkZR>FRNf$5=o8Vb`p:>IHrQH3h7\I94<8qod?[6Y2su'8LFG +A#!QR2/8@+="a7_215o7IMTF;cpp)Z910VdA>cG-X&c)k)<>H!hS%ARq=Ku>CCY/RHP>@o_)Usb^#_d/YQ.l\.UBW-ac[tIp$9)0ldcnS +7'Q`g\Mef2ib4WcY?R`Dh*LdI@Sfe +'.EM&=(sh7GC/YXT4glkDrBMHTQ5bpueo<4-Jk7ZJI6i4snO\K1c? +oYqhgpuXipp4\#IjiWi=;P_CgDniu2oB(rc?XlqA[q3$bf`Gq?"eh]Y"fM.j +l)109b*/G:ZL[;4B'K.OcM)%!,a1qTu2iPK0LQMp$BO&AkR +/d?ud!>#gE>IRAG0akrjfr;0hqML2`"qA_A0-B`WJcuO6Y"?Od\T>#Gm\6$d +cGr?W:`VT=aI+hsh7@aLN>lMG0*e#)8m"T5H@!.X'&cYVJo?#.k^fU"@(uX: +lQ?`MW4E9K-9rP&dNX0:*P9Hb5c^2&>@u-K(!AdN`f,_TFEH?aGskaV*NJ8_ +acl#'21JE1amp(+#hr=aABE]AHY^aP8/M(<`0K13r&T_6?b_'DD+tcQ._2#c +o!)N7HFV*<4*ID<,IQUdD*b#^J?&%QJHP[iQI+[i9+RQQi,QW!3d'^eXBRT]Uk^G:S0iH +o&W5ZLp3V&4To+BADdNcV57$RIJ`_dl`IXBHI-,gm`DtYj)C0""m(6!\CCq3 +i@8gGr-=;eL(N$>3.:Y1m#fWIRhrr),tg)R[!1uqfWqG_nl-8MBbrl$iF5@C +NZC3iG3rI-+1#t+2O05B^=p>O^Up#Z`Q1X;6G,+9:,YQ +j,\EPgj,*AG8$(0Sj%Adh!*:cc^6EhkB;`8:HV=@30E-=bEaa7[r3X3;R5`7 +R58(f4aQaf0k3\3(kH$Q1^)QTJ3Rr+[Z,e(h7GW*].20N4aQaFrUndSGOM[u +BYh"Q0>-ekW)9=7_;s\Y7VGG1>&==R1G$g9n!4?>eAt*TB,# +jN'oV->hJDaXhe7gqRRPVJ16(:s-\.*_MYF;7q-n^;?q#m^_LoAV=2P?_iVp +:/O>E_@-qdlLW=ds5Jqi+OjX,MFZGE`PR+IS*G[1THSel[VQhbF8Yh'S)[bU +?b`1!;RQ`=.'j9,3I3Z'hRnge2s.e1kic&gZYpDE2fI9eC4`kuE;9[k[Vboc +1XGPBIYD+#'3"Ji%Yi\`6YP>h3IetT7EVpJR1Lnro[/!OO$uh@C\l>b5[ub)Ft?"Pa)^W6[TA$TKFo,u/Hu&4g%\!A:pA +@aL!>i[8[qI)UO2pANF:$>#.)cU3-+Sk@9YE8J?Zru=%>Zu=ini^MrU/8-`) +@XApFT'o-$9GJaQZY%HJK*GI;*3BO4ZtZqmKX/#1TiK-n*O,cBRP_iI7$Qa$S +X`VMIn3,=hshgs0.:8h45VC=NY[UjX;d +Y9?emeEPG[)`26$AA5J[JQ&jK!(([Qi.2,Rk0/Z'5S>9>3Urpn9UINUK*[0Z +JQ!QY!+i8H9"8C\;1=fB:]Bh+V.jiWkVW`,>B(2-I^H:gJB0&'oi +U-/,7f.U+%&2H_&*V!*/VLpfo',!ZHB.2C%':WAFd1Ff@e#!Qh,&/6=r8(^< +9P_X_W>;T\JH2aS!=Kb7^4oq*G#b_P*?Jqp*F(eL,uXuL0Me3Lc^qghiPK74 +c.e+T'5u[9ER^paY&0>IZ?V8%B_An@R4SHeQ!LW877>&l)_bY"TeV$BT).OQ +UN1Dj'dJWA&M>N929>m$:pk&g0k855HU^C4TqKE0@DfIiRd9+X6I>p`Ctc71 +JH?"J$,JCHi8"nb4S[K3Dn0Y;Bm3kM!=JUZBEe4m5-jMk,c)g-Nh:#!a +TBm94\@=MYf]Z&!2Vn9D)K=+oN#7,$L-\-'OA&e*V.""_0]12>](l\nEWS+- +!V#iM?@$>Ti5*>kmUcorgDi_D*20=L7ocQgOZu\A;Gb8,4RiQnYeL*mO7gY# +Uo(#(QYLA?BXCYS"e3#"]68&.Gu6peHGbIHBQ?`Jg.o7^/,=NGMi\'-Vrg00.2G^[*0]]nqE_tWNNF#2#%!mQAY;hhPea$M1s=X-9,IC)KVG`]rlZUg+VY*tq7"U!Q2&mO*aKaGEocZE +]Cl%L@$"bLo%eNIaktPon)BIib4Tb=1+0:`C7k[:)jL$U\D+`-TjE!&"oQ'& +2/=BKiQZRfWmQ7;7j!0OcpJ?>`^cH?#P]'$2/CbOdBtC_ju54b4oQLU6L*JC +[>2LbOlZKjOcD/jjs_l/'qISlRH#Z,5oIkN><%87U<.'bmUKa.I)$K8V'c`s +NoN(q4OAK&K^7UQ`8NZ6a&k\fBQ?B^.FQM444Sn2m+ATAqtKDBr/p063-!r5 +++C^MCTmbPM8mpW)-=XmJP_A1I+Enb$ISZrRV[V_Kk +CY&:UGkEq!P@Wt!4H#ktE+[Y#hR:C[H0(pTP_,\,hisWP5?b[YE^epm9ZqHjCn:o4n`c3rk#fQt@#VkiF-;9f"(drO> +[%^q;)`G@hN]Z[?l`MF#MELR];lC'_N0OdW:QY+nFK&$!SQDVfPZr"3"KL^n=&,J$7"O) +nB_rmGCrD03[>6d=;3@+Bi)o3iU=E@omid3JT0lY%NO-9It*oDVt7XfUu7S% +g:"$dg@1X`U):lX-\3*Albe/?aU]@N^n$=$D;?o!.@[B=/0BA`Pq,qJG3sOs +2,/%eEG7CWrTpl:@P=>D^^6a\.o861[VXpiW.-oR.SX,"Nq>"L"H&'^:fQLS +$A@tXOt;+rVi4c+otUY?'J!4CVmL?tRZJBtOH651Q,7bhKL:J&J_IK%`9EVp +>\hroV:n05puA&8L[lY%eEG;T,pD7a@Y=/Hk.C)rRhnBTnOrdp)cInqjb?jr +"`="nM_?u%d"!)++sI^&L@cYor.Vmi5F84YAaF#S^sD'ja?9$MIR=TnofuW4 +%.fs&*orQ7l9R$QD_u%J'Q0aXKi*[K9_O"2k"WBWOgnU"X']H>1Z!eB5 +56lus/g"=c6h`m1 +>dD.s:7XFC$-3+XQYNU&X]d"]qLNFXFMnN-;5O]pYB9$WYXo5Z(+N:^@)7GJ +=3q;nMfud7RsXhD5gU:R(IlOQ29(9F.C]HXGt4)?6A89e23M;-eWm`X$Y6]a +'IrhH`ujJFl:f.U,I_QJ@nm;W'Urp[PIS`ZjbTYQ0f5h$f1p2<\5@tbLj_=A +@N5tuatEt@i7b`$G2ihK.TOaBnZ4@tIt86j0@Z%_No9>).lAj?=XW!gB2N0; +=kCaSKM^Vp085dlImFbC`G[`fa?%_+qF@][H0,un8XV;;>i@+&Uja`F.A\B@ ++iT:?"&5A9qIp(t]CAb3:1fu3%5CT2Ppl4\/+!NUStNmc[VXV0qXB^b/QuNu +["C8c,:?2J4Se'%>j(>_%8PID:tl*B>)aFXGer[qX)irH'U8lsd8A1(4_TcW +-^Jk[6m$qR%soug4o!rf]EK"u^a+A&Fj\R7kf`\thB:"g4NK"PN2P#fU6W/L +5IlenYmLp[/XUJXpBU+Oh9&0CRBf)l58i.;ml +r7mfGKeI>\gkHl8%7.[)lJ^jC`qsJk+.SLnW`?)8Q&iLGXbCu%h&rDIa(Z@> +QTPMA7_ZH=&[a'HldDV&[VXVn>kn5A01:1#:(pnf9@c]u8nC4 +FrOV33A)pZ':c7?l#7V\3lPn*o`"" +Fo80CW@o1/fL0R,9d01#gJQqZ!uJ4=K_];(%HnO3EE++`^\DB\NZGH:ab&&t +"ZEp,r'O>BF8hnRK4^qG/,MJ7UDW3-,SPr3]ioX^r'c[te#am)QCCt'7:"kb +@Zq.;IDPK4GTr0?U)p$YAK)=V$K@;q'g7t%)LEoDXcRhP!VaW#,t@_%+sF'T^p5)iE!d5o.IICs6m$qf +&o4/$lk/EjI%9-o@6q+rLje7!KddiG%CM7`#_T-XX_M:2`Dc&]&q,^8Z;rks +)#UQ!Ie2"U.h1+!(kX0HeS..?Y['G9R<`[BJ7qt4e5N9.\odU5h+Fmk +L$g(a,W;X#^2XiPSHT9NoLEdR`39suS$1>2+MPAEP"pB=0Gi$O."+K.+lmm\ +Qrh4Y)p5QHlXgpTG[sc/6?,EG&/_@a\$XQ'*]S^O>l;qNA8tm=BLSc>=s6Wh +C460/j0*h``GXkL_:O:KA6rX5il.T@V5!7@ +/R*u.c,AIRoFHs%Q)QgBo$u8#Xd!pJj(1j`MFXD(Gtf'ME9+H,l(*%TM.u*b +E("_!,NWqQ'>r'&'I`I_hu2u7Hg\^m(T1f5P\Mn-ORh=2:k86)f.1&28)4$6 +SbS^9;+Wc2Yts6qS+Ja*Y:#(7M +b3$MSbS,Ol/IRW(b-TJ-2?9^tkK^Se80``,cYTb>%sF([PFGrG=upmd"JmI+ +rY\*Y@oe3nPQh7)&r.,5;:VR>SK\-;i^#],*.m9#6n%AedVr?KSUaG#nIr%_ +'SPIH]H+gZ-LT/HS^?.K1aZP+?d,65Uth1fOqT6rA`=u'O]e+9!4de.[-ugp +hu/SnosAn]3%`_9Kanm;I4(EQ>84rG*mYD;2K)][&SP!BK=_\.]R5 +)TM)q<]j6)#'U?FSVU#2(qGLL,`,#,'Bodr&3M$1h07cHXhC]:,s)@%lWKaV +B$=G8H?N%(.M]16p$-\ceu:F\)6tlmP]#$XUr7?s=p1mW5k7d\$#6E^M$iDo +HXm/A#c50U5NBG=;C%Ypas>)"a[3Od;U>b/+fH)Z]..&WIP-@1HO?5"NWLU9 +agjMs=X?gf2#a'YVnNB>9lfsgmr="3pYUHbIn+="$8i5dWd:lLb(^sO*f$R@ +D#ChgRj!]7iJ(X0Bfi9Gm^6%u)#1oH^u'q;:K6QVmoQ.!;l +2C;_\=+um5q2nnRgL'sN(H=`=,!Q@2H"@b+=aCVq"W^%ne(i`B]C?i.,g4ni +)?MjKEG/=a^=:Y3JkG?4ZI$^d.m+`FL/WZ3Pl1D8J7rNWgn2BSIk=d,J/>Sr +XNS]o&/V=6kF[6qpmi'W.:KbNX!k!.![-G0(G8+n+OV#InWg\[e@Wa]-iAZ* +_&/kIX&lKb)PL$,=9O8BU)s(=V?GkUBuDGZVe8Z(d&+mal#!5PB&tVUKciH' +$Y@cqnTtWZ/4cUCG/JNrK1*gi&G8<(e_1+D.Z2WT@MXtGi>Y2CT$YWc&-d4R +A>(D$/g&5qlGDN@V'$7S."mbGP.tYRi^h:Y2/?6Qr)XSrKp(J/S._r#(S"=& +e2*OLZUSi1UnjZABK37g/ftOXkFeDjUj[auAc/>aJ`O`Y_\G,X=M,PO_V%<, +Cu;sTfT.d'T>4>]]Di/=]B9oJ3#8GPq5U +]Q@Hh*[qoa=t=lBYUOMI]G(@FaIPE%fOWj0W8o;L\Y>#cKj:rr%DaU'JQu@A +Uu](\-_71Z5dkL^po#a9AL*45ErQhI!N8^[CtuNTZf[N_51%:f+__]a?0>l] +V.9U[;N=(`Ym)YfrA)G&3fUVDm=+`/?h&:RFj$(&(VM!d;W\bib?A'F_RO4) +=-c1[n40-oBKO<.mC1uaSt>CX4:[K@0PEl>\QH+G-t+Mh@4W=;j^0B_X%Y1M +7VR:`E.<`BUVjVuTc^:e3lFo)%mdrb"/H2:)+,-8Ec/$pe.55=M5m)=oC'Be +?:t4[5Q;TMV>a>0MM@5^_hSb420a>)Dc'\6,:1j3CW#s-U?&iqKu=ee/%eU3 +&tTJHCr:Hoan"VF)T\6u+8Y0$YJ0QR_.9-R,TbD/S-'.i`%55_,P%p:i1A8A +BG:M&PWV38>uSt!J5Cbt5URX9GYhm"WdBhi13*D5X8?PUSm<@OmUTX!e0/E& +T%J]en2MnZ=3)p#779Ul;/]Xl)G[9@6A1MmOpf#HcFCCX6 +0D2!\a1[V1Ab7\nc=S^:DHbA8i!sH4I:r\N8b;8YX`&n8H]"Cc*E"A%O4&LU +p"*:N,F$k"=-@hFPUGJOVAI6W$L%dFME;J!;BG.GPKm#P_-K5qYp["LQ8!0q +15hOTKUrf$h#'rtqNEu>@)9-[b;?h0&N!"\'1cjlP4e+o:ik;@c?UG;*4hRh +P+8nd1tn;YQCi(/F_rL&:2nO@2\[eI,3/H-rquSj+Cu_?a0X!Bn6c4g?bB-b +!X0tn9gG,3Mm+bD]KT!bTqF)ME/1HG9l&@`jW+,1pk0h?QVdG4G9?S*R564J +Ab"d!Zp$t%VT,*pB1""j23*n+]2)inDr.!e(5c.Y#=Wa3)4F_/7k7lg"6>sh +62=W*Hq6Rt>pF\%(mX^Ihu4B9`>)@NlC[3k-C?L=d's.\5s[E7cF$8:*V_62 +bambCR9f%OW61(]8$ll^g7d:7XF7dtfI56ElCO(HI%F1`93- +X2u*`,_Kl/\2baTf3NUZT:j)_*'&U`9C\fs$mL`.O#TEH+;FS"5R7fF;?5ga +O&DZZ@0+mg"d%ngkK]V\H1Jb%"'2\i4D`c\N>@XJP!'Gf,!p:$B]6Ut[$-Z9 +Ukb.tPI6eRAQ*u6Up(>J&=Y2hGZgsj<'EKu3IB>XCBGi[;pYJ>XnlAA4ZMXNN.5Pd]WHBMMcZJ4>;jLleEV63ra%BLRjFASF=0E%*2n +9Pm1Tgh^7aL1qP-aY1F,dUe3%%uWiK)![6udj!\!s#lOE1q3]WD@EHWA=EqX +^`Eku/73&J>H.rDa'3%;&6(W#_d\&nSd:$2im(LY[hYB3H^5g6Rmm"uf!IUD +caY&=o7>2R6YKtkA?aRg9+.JOO[bp[agr3;AFUpV(!)GnN1dqZnjK386(E]d?rY7(m*00&l+s]*, +e]WE#l)3fC.9$JY#H_ZW@U9M&PY6OmiuS0[5Q1cRh1gS3#O!Ke?l?h3V*dmP +D^d+ane4hQW$@]a>I22>.)7SBe;WZ.h$"12Xp.Hqq5S(fZiZ<8GQSDDPs)DR +F`.,o'hX/aV>sd8e[gVh$Xk.Q0t6/_Zjr@O\Zp,^EcKp'"aU_EUE11'>e"qB +<\1-=2T?,KKEL>S!Q^lt58DB%1euB\k&s15B9Z)U/6Pg'G`ir*hF+0q!IMOH +jtmjT5r);']\refqt2V"C/+V@#SoR;mY6b7+I]aP#:-*^gU:tQ[,Hfq_P?/j +Zmdker,C%Q8#RM%VFk?XC=]K.I;nnaj&?iHBh(,0B0&u&'L.TP<82X@6F]f& +VeudSOgMWGE/2K]7^Or$BPDN"b/PG>Tf@)ms8CjTV/2AG;\lsB;5dn,P114O +E6XU!KS#qdjF0?QotP:RNTM7kOoR1N8u$h3mC.s`:S0h10;i?m0+>*_VF?@V +_.6JAq"#CWb!h=Q!Z%hnq24jB9;;FN"Y1^,NX#A_L +L+)9+L^,MifP\9\=\KMbh)\tfoi,F7fdJDXd[i"@(DUOTBn-=9d +X::cBqk?2ePRM.IaYSKfEpse+qA*dr>VI?S/e2BB;_@W$Z[]i!,u2jIIA.]B'X?[P__CIr_IX,:#)4.]3BaAH"saW:%%R6a.^)AS93 +i*!&T1tKrM)8-+!;4d:)k)=7J@]t_IJ)kL7"5&:0F(\1*E^jjA +7e2@'iPSK/(k[q\mUXY8SbhO(%3%3]K5Qa5%$MVPa[:-?,!42.+PCYnSA:Q4 +Q!:1>-Q+T7Z&Y'(Wm7(m0%YG`jR6,GR$:I??sZUu.NV_27Lfmkr#1ZZZi +WH,Q>k(iF\?bUnU7T9Yr%UfrV)4N0oY,NC;8SN5b,GBB\[ap*9pfVdW-?T1M +:Ob^G;>@hOpKeqi"Eu1)4su&^&uZ6g)>!\U39?YGTnIed1V)@r#1F'$JFL?H +>)J[NJ_/s`g_+/;bs/hGD;u1<36$WK%(hhE685!e!+YSujL&dTg9`(`Rk8>5 +Id-"[06UhOG?g7/"aL]=hu2te8A_ZDb75LOJCeGIDs"e`]carRNYE*H1br=u +gO]p,70W\a\%bAa:^EqJ`26t0JtaGO-o#:l1XMGdDB<>`ok`#0%4h`h6km0H +@W-$8'4mg^h0!1DGe%'CnVDAb&Z%EPr=#qsM)l08A4%pNq=sao4a-o*Vd"e*?r8"'e/OWt)a:8>=o +_UO1Ora01Be&Hft$FD="U9p:eP8:K'1,ku@\N^u`-Z`YtHc,t'#giZg8-@8F +#iUmj7kfN5X_g2njhShXRr.,-k.7.U$9#bjXS4sZo07a#b?oM+K*Mo"]\lVS +B.M-W\Tm1=-'#9$k.IZs-k)C:lX0Z1O.Dn?Cp#:hj+dA^6K"A'7m-QI)=f;c +1=U7J*P%r+C]G)GL\tO%[qs_A06-[MeJKW-KJ>[IYM& +V5[AB)mr)/LhM;P=Y]`!`\0uYbAnb'@[/aBLJ,-%Z@lDhDIIS6HVfpR7>!>ibS7@or+$PWaeC6TK`EYS] +CLe:+^0!UdA7!.d:g=Mnn,RiG+E+>63=TV#@W@O2HQX!#+:B8(,=LP5$9D4c/!gs +BSGNB_g1IoV+YM..S;/(C*EAb5'PsFXb3Ddb#/mjQ4J"Uo/lERZZ+m?";a!P +EPiT6V5C,)pkSDoW"CLICdR)WDq#q1c&38h?5sSlre5G45(dN[S<>8'@C5rp +Atl;Lm+@GlSrT815(,K'g'uI^ql5R?*-7;DAoo_)#T/hWV1r,dX]),mBm>Ap +<7-]kQ#Xc0)i`ElZi)L6\.q>Nf3NSLRAlTTfN,j;DGJ<5I=h"@HB)5H:2^8* +Pc;H4VrVebqA)!oU7$.F8!WBbFc3?L*/'MiBk\GAk^isGW-MU.`Hn:q[9/@t +.L8VCiNVQ]G:JeGe1,i&V(HDafnCW4ki;:2d-'d`J-Bq4EC^IKTTsdOacFG4 +fRS#hcY]=+Os\poaZURc@JN&M,&[Vl'L97.J)gi;Jifnm%$S8s1g!)D$&fHd +S%pOVkiAm+Te#U7Wpprp%';O8K43:_O<9r/> +s8)&bjXQXgd[1C;m2Th435p#Weq!0Lj^H]>-a$<6Srj0$AP.&'$ZKVgA::\R8/F/-,A:uDj?NY=,7uXR +%hFp`csJt[TFP*tJ.H\9UCh5<,4u3aEYlDa"Hj +4F-Me@p0YJ''A\-V=I#&Rh)?'asIrLC%uLbo5$UTfVF(Gb",o4W,m>i*,jn: +Cm*DJ'C4D,#BEq[$2[&i9_opc"BBmTG=1LhbP`LneG+f(H"ql-*q[U:-:AeRbta)]$@/, +?,Ifm^a:+XZI6N`=r)DY.C9D>1<_?m/M>LS3!*;j"MU8HkCu:^ef.W*j4*1$>=$q1$oni%QCPruZ!rEQaIkP,25QcV +3n_A$[bZ\^P8l\%UaSLa:HF&6G`maf(#j3mZ"M@PiCM4'TqG3V!$iF/H$tJ^ +A5puQrgng@10_D?[VXW9lF>gCj2X)rg^&`ZNfQeQr+*?OB_7%BkI1#Z)(A\k +fYjOB8/OPr%?S,:!ji$Z6?tEV=NodlG]CtBUF)rX'G2'H@!M%jYsX9V+ff:* +R_T3*G`',TF]Y"u".?#[R6jme7\OHqY$?fQjd3=mUiQdqR%H9![*g:dM5@!H +n%%U&C>drqmmU71?7Lc^Z=V57`O/qe:+dE6s35J2K/u=EXQR[qhTBt+!T%!I +TM(S%l)S?Kn9aot?*n8Ws&JR6D)G/.2+El6Q]oP;LjX1beTUlC`fY/mZq8G< +C-7,C=1<6gY3A(H-@F$ub:,!AL,%F,@+MNWc47"MY)=#F7WWtOh01=N"pLkP +FQsc2k7F;Lndc_fV6W__8FnTQC'K4ZLb#BN[OcPVNj_%1S7/_!fgT%6)8H8X0j3a8c(`s.m*^Md/>: +S'1Pi[GoT@numN8.rIOro&NJO4cC=X"/mFmYKT*u<\m6V+-#!=)1fU^8RpdC +5ROM*9u9tX4"6.-+](TU0j$e>:K;0P?;?Sa-jJoMTYX5'5_D)I\&DE>,L+8' +rqqoj/%%<>,WioGm +#@X9Ar.V"9ltI%?)<*rM-#tn4>sa?Z6`t)L\RYg]W!F6*um +,ndbj]?WFOjBGlC]C3HQ=+M0=Vn^S^HrtkT2HiWPj3qG0GZZBHUeu(I]f>uF +Dnd;WIQWn;=7Hd],R]ttfWfDS6bQp]*85ptL/!gKj<'MZeL7t3QACABP)R6q'3^1< +D*Bf)e>Z@i<)X$>2*uBF#cR-TnI(o#XtNX:rEZD +6#'(OrfXI)1CI:oW1ieu]]dn?6-0G/8thQ`fc?%Yl"b9od,Q +@?Q"`76$!&N97s56E\\cBQujRUqN<$Zn=ZK6(k$.TD@nBI'(3hQ6eb,GQM19P%6bj,gKBkpQe"Ojn#79*6M@WP>` +Rl@'BM7bYr_$=:ZqXj#iSQk9_eXj`A#%-&4$EY1@Lh_cChq^+!"N0NZnH9U$ +`>;n6-tTl[cGo&%+I:F#68\*js+9gUKJQ5]+qaD(q/@";!KBJ4i.6d&PV6=R +'ELHV\[OG!dSXdtQjQ9I_%_*6NDJCZ$q*:(n/u04S2kZ#8@-`$3"3/%mOMkA +l"A;3W>46=$"r_$-efQhJYSfa"_cM+o2tY*UIUC)!epZt]u"VBfR96Di7fhf +[Nm.F+eRsT>LuMhI^t9.Vi&?X)-7\(Q+B\,#b:GfqP]3>3?Q=24]=6WOkR73 +^JljMPq>)^NZP`/NU]&C9?]He#SESm7'e=G*2+t921<:>kdrXYLOuhCo$sOJ +2hs1D]O5!\Mt9r\misd#J[IDr"52.HP>nhG8XUM!kAUm2LYgs%1kET_$H\,G +Z$Z65bGkrM#[N3iM`u\m[&bTN;c2>9lP+t9L)8SOXQ=6-O4>2@X]o;omu&-8 +7.EoL#?M2]GnHXH=[J@(7rr=8`P:qOHbR,6gpepWNlu>3VsD'hKn$c%6f"[3 +K#@rKEC5XPEIn6^jD[sOhR%mC[e9,q=Bs/YhP6(S7jh]!0JV9+K.C-@s#MW. +;j1T)2bA#)7sC^/X4ZpV>K)>hr93+FQ$+lU"r*L%Rd?ctPi;X/>hs,)QI\u\ +*2(^b$lGZ)'sqjMpha;N^[+$[Z*Z&MTNZ#0g/Kq>H7?MZG%Z"GUO1B9PJ46U +7mee+Z&'-TcCgl76NAmU_iXs?pM)MrBU;d8ARB-T(C,mVNQIa:#bsNuOk08eg`bE2NXcGd+kUO+m$`+>< +%5EH5*$J:8$5HaQCl)5`nYi>c;#kS:-XV2hWD]Ps%NK08f)S49]mB@W%UA%V7RTKcY10CIUN#2/\9t>XXnq.&(_:RY +@K$&q]mKM)gdW6h4YR$:4m8cg+9DR&FW]Wd:B0KAs(V96GOhF'ft"&\%@ZU& +Xo!5F\4o7kUi!s[!6p4Fku+m,hnD@,!c?Yu=h&FE"rd]TkcL.e;_p7h>XpSP +*BJ;hEo/*3NkE2"X9!2`*'Sapo&Rp;Rl@g1!o3U=3bd0HR4Y+OnI!D$=Ms6q0iiPUF<-o:'FJ'\U)I!g=mXBAc8C,Kqd]YLhn%e)q'04(09 +R&FM:l_M$^@?f&Qe>SKkGAd%Npt2JEd>I#G +AF1>7;.jB+Rr[5WA>;VR;D18LZlq[8cpF+bo9cCb@ +if!NtgY39'X_4K<=d1ISh7@a3>e+V#XGQ1+FQcPsb8]AOi9].'++3R/(-&^e +:3A0NKtGdGH1R1F9;6QcUk5a@5oC!R0%&s'3jCQ&+B3`9HsBo]-E]u]i78.% +'/S^p]@Rt<2YF\+n(s<[="[UZ+CD6X^HNJ=.A2Kq2l6:cd#gClJeMr6(5h"B +Lk:RRN*tDKD6J<5RS35DCh45J;FK*tb*D;M^V=\"DCj9%^juTL8?"_R)I_rq +gQHPfbEa_ZW6hTK^8fSAOY1[)Rt6hTDh+;[/U;*N8.5FL_HGTj +,@A04/R#NK5(.a=1H3OI2c%&dBK9Wi+1:rSX&c?CkK[A8>.(%kLP@=R=>oSE +%/c2!,=ZeBg/ZmZ=Qj;K#sPe8h)^&br2AV/$Q!,`(RS;,F;QoP(c>erG*p$W ++.n#Ykq#uC1i:rgb*4=mmnXt^G!ri4r5i0$!..+9D2U5c!F)[VWqA7lJ,Ji2 +7>l36G+&\uSt8]j6(C]:_UdTH[;4C>Sio/9D!p)hQX)-d5SV"(X*sm=C&FPl +le-.f>4n,<=K%)l/!_eng3/p@<69&-*'8>F9:%:!16o9cm^(5'X.:caQS4D2 +V](mT?QLVi(r<4PWt5^,$u>0L)m][k'7ms_dA)?I#Gt:[PERS!ZHSn(En%\C +b"02Bm^jDsD@@4VN'O][?4K97PI@ta=7?E8jiU"7TN.`h_%;cmP&0;.gB5<[ +"Y`gX$PogI[Hc,46[.qO($Y8npncnY"HbRQeD7K&4L,unEF\"fiXhKq`5H'1 +OT@n(G=U14@WGs,*M7W>)>q0;N>bN]3&,e:oDha=E8u!4lrcY#/O6>pB=:a+ +3/%T>[r1$W\T?g+X^&F-qUXG[8rOTAJlkSk8\:$'N_ +,7qA^'cNhdLCYIrs/<7qCpK09(.hA;,taEceZ7DfY@F^lNi#aV3D(W/q3,;j5cW92?o_]Ua5Z;X`"1(mRh5,qO2X-aSKNXgYd#g6- +p?hSg+Bl$beLI@]4)!AVBnb1`'^%oC(4G8+8,DoE[r^`X8=0'O!O3n>>U_iU +)=+k`S=k]/kKf25>.8BT0^'oSnC_HH`Q-$B0H"\"APNdIr&p;3Y[PF#&;XIN +AM:c3djA1,Z')C(mZt$nW=g79pu$d4&bt"pi5(%cooY-p6BJf'^aZD4a>"h] ++UA2ZoB+:BJ84,;[0,\*MWN$JYB&aI,=ddL[r5WLb8]AOi7tka0>IFj'"g+@ +9bp>Z$\,?\X]egT4MV+.('N*Z%+(eQZY#LT[tM<3R=ch\RUjO'V.%8re##hI +k$pkDIL9rV.pXX*,YhKQXhiFsb*4>WY[FqXh3*qa@Q_VBTH8FQm&<.s73_$D +[S>q.!K\%mAF?:EQYoB,Go[Hi\b.fr-R,?VU'0qO4- +,pT7*TN)9=$0J3j[;&'`%,&=@GGNE/X'j\jD43mA7:\b1TT[M7(JBu@JM)97 +Xg_+p5t3%fOH>RnjHE@`$mlqQO`aS8-KN:4#LZ><4?;&I`@uf@r2*2XWno;G +f/s1"Em0k'hkQ5r3cO#m4aZlj]"A&mEQIJ3gUHVb:HT42*tN?B"99&IbYs,u +%3SW^G4"G;SSNk`$e;b,j)!DSTrc7o*NQT7K1_&J +S3(qt9:%7f3j"IWKiZc[,2#4\W`5rqW)00h??p,Xo&[^S"%3+frRKLBgLN!) +4oY6:EH-!FD8qMP_)/)-!La\>e>Q4h,EuuAA>)V\.TNcbakj^XJ,AWB_31_5 +KaS^.FmDMVM*'^ZC/!eg7(@VJE\/,M'I-.@9#<]I:ddl!P[LAg5_0!%.THi, +6Z@&;kYZ_8ji2][^E?M/Z=Sq//(UIcO4C:4#Dsqp+b)[!+e_Ab)FR0ildp(- +YT_5*kb56dIE\9G45Wf%1eARA+:nMl2f=kgOZbJ+DVd.m_Q9(S=0>oFITs:! +NHBFJr3EQK:jr67Khqp(cprcK]TEHHMEp,!qsT%&\`[SNSfIq8!qhkHQq704 +nR+4DFZgH@[Q]3?r3^=l*]7\38kM^OM]CYei;EEBGke[]eWPUf%6PaP10@(< +`JPCOjN,`F9:)JJQNe<6_oRrbW@3Zch7us4$;OH:>b='"C+N/"NY0?!*BJ9J +imar+LG6jsPkfd,hr;V5UM8V7qe^IM#6*IJYP*d2Pi?aS7p+oD,IRnFFm@H_ +>8FW/*'AK8aH7]op?ZMFY+9,l3Q?D2s1gCM>U4VF:nU)JmbLsH=qs_` +M@nd+i6aiha$YrdB>G;[JnUZ)5AVcX2bU6G!mcVZNjWihJCoZ)_+"ab%SG&S +$JSh-5$e]$oG;^)N6D4W*P88T_&5qhi!!*qb48[f_SB6lMA=SsW*g54fKnE/ +I4J9qJ9+Vl?G3pmlZ0r_;a0SLo1$DYWQ'4sT'I=nOMp-&u8NWFlC@hq2_%W\/Lk7U3o! +7:j`4PtF\?8-gs`0`n'X-r[Zsjm!n%G]9&s:-g;=JqBInfAI"4Ngkcr_+QE" +>QM8F9dZ1o(akq)nT!#(RZWE$H"EE_l$@78ML"bQD3l+^>CGZe+6D4e4l(#5 +\(DrI)9YiKEH-!rGM]uX_3/RgK%6o'=8#IuC=R(LWejP9*W\&mn-J$P\(=<= +6-N5NE4?Q0Qb(4?:>_g[s9OYrj-i"a1mUc07AZ+!9q]DB+82o)fusgpqKMmbBfO.4Np=4[q,RqXcOSb2DI#W@Ar1HgEJSAHo3$ +#=VahQ)3'L]7<2"&+'M0$t@f1JhR(#*u1BWGn46^AOaVPEnP)4 +\HB]#S`4\7kU+k"f3FApK:Rfs7Oqu7jX]*l.pNdR`3>;SfejF9Y*eSa+06FLqjHR: +)5cnZa\@Nb7b#&dTX*Ip*IAeAVF>/q,9PTuVc"$#oB/VH;8Y[h,'M>@CY+=_ +H:VA7b`dE==X]YkOfIdL:732;ek0mAjdu@*-?J<1LU'E6V_S0KR)s6ujE%0$gaL:26ZDFTBP%3d$g&5TCo*95[BmT`q<%-C +7JG\h5NgLm9fmL_<2?!E^s0Zg6\hm9\0ZgSO*Z=QYXuPN8$U1;Q0aa)VNC90 +H@VNt,;(O='#dP"9.%1dIQ,+0)F$$ajk;A[9lVaX3(GODYrngN6bYh3jgb1D +[m*V8ekbJc8hB"`.&`XrF1,l9DS^o._d[4=N,gQ,KZe +%[9VQOg8@%(`-F=(![(DO>jAhkNVdHQhkab^4'D?a(p.gCjhXTc(+SDQGX6Z +Hi&Q+\i4`[(XG),7CWYFZ\HJ,ISd=\aT;VQ]_^ooX&c?[F?9`;8_CCae&sDZ +$mSB2/mYhtd:+/SH[ocU0?&Pb8qg)&KSMmER"'`lR,Oe2~> +EI diff --git a/bin/reportlab/docs/images/replogo.gif b/bin/reportlab/docs/images/replogo.gif new file mode 100644 index 00000000000..e91e1ca03e0 Binary files /dev/null and b/bin/reportlab/docs/images/replogo.gif differ diff --git a/bin/reportlab/docs/reference/build.bat b/bin/reportlab/docs/reference/build.bat new file mode 100755 index 00000000000..483af7b9b6c --- /dev/null +++ b/bin/reportlab/docs/reference/build.bat @@ -0,0 +1,3 @@ +del reference.pdf +python ..\tools\yaml2pdf.py reference.yml +start reference.pdf diff --git a/bin/reportlab/docs/reference/genreference.py b/bin/reportlab/docs/reference/genreference.py new file mode 100644 index 00000000000..c2b6afd7598 --- /dev/null +++ b/bin/reportlab/docs/reference/genreference.py @@ -0,0 +1,29 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/reference/genreference.py +__version__=''' $Id: genreference.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__ = """ +This module contains the script for building the reference. +""" +def run(verbose=None, outDir=None): + import os, sys, shutil + from reportlab.tools.docco import yaml2pdf + from reportlab.lib.utils import _RL_DIR + if verbose is None: verbose=('-s' not in sys.argv) + yaml2pdf.run('reference.yml','reference.pdf') + if verbose: print 'Saved reference.pdf' + docdir = os.path.join(_RL_DIR,'docs') + if outDir: docDir = outDir + destfn = docdir + os.sep + 'reference.pdf' + shutil.copyfile('reference.pdf', destfn) + if verbose: print 'copied to %s' % destfn + +def makeSuite(): + "standard test harness support - run self as separate process" + from reportlab.test.utils import ScriptThatMakesFileTest + return ScriptThatMakesFileTest('../docs/reference', 'genreference.py', 'reference.pdf') + + +if __name__=='__main__': + run() diff --git a/bin/reportlab/docs/reference/reference.yml b/bin/reportlab/docs/reference/reference.yml new file mode 100644 index 00000000000..65e4091af5c --- /dev/null +++ b/bin/reportlab/docs/reference/reference.yml @@ -0,0 +1,217 @@ +.t ReportLab API Reference + +.nextPageTemplate Normal + +.h1 Introduction + +This is the API reference for the ReportLab library. All public +classes, functions and methods are documented here. + +Most of the reference text is built automatically from the +documentation strings in each class, method and function. +That's why it uses preformatted text and doesn't look very +pretty. + +Please note the following points: +.bu ()Items with one leading underscore are considered private +to the modules they are defined in; they are not documented +here and we make no commitment to their maintenance. +.bu ()Items ending in a digit (usually zero) are experimental; +they are released to allow widespread testing, but are +guaranteed to be broken in future (if only by dropping the +zero). By all means play with these and give feedback, but +do not use them in production scripts. + +.h2 Package Architecture + +The reportlab package is broken into a number of subpackages. +These are as follows: + +.df reportlab.pdfgen +- this is the programming +interface to the PDF file format. The Canvas (and its co-workers, +TextObject and PathObject) provide everything you need to +create PDF output working at a low level - individual shapes +and lines of text. Internally, it constructs blocks of +page marking operators which match your drawing commands, +and hand them over to the pdfbase +package for drawing. + +.df reportlab.pdfbase +- this is not part of the +public interface. It contains code to handle the 'outer +structure' of PDF files, and utilities to handle text metrics +and compressed streams. + +.df reportlab.platypus +- PLATYPUS stands for +"Page Layout and Typography Using Scripts". It provides a +higher level of abstraction dealing with paragraphs, frames +on the page, and document templates. This is used for multi- +page documents such as this reference. + +.df reportlab.lib +- this contains code of +interest to application developers which cuts across +both of our libraries, such as standard colors, units, and +page sizes. It will also contain more drawable and flowable +objects in future. + +There is also a demos directory containing various demonstrations, +and a docs directory. These can be accessed with package +notation but should not be thought of as packages. + +Each package is documented in turn. + +.pageBreak +.h1 reportlab.pdfgen subpackage + +This package contains three modules, canvas.py, textobject.py +and pathobject.py, which define three classes of corresponding +names. The only class users should construct directly is +the Canvas, defined in reportlab.pdfgen.canvas; it provides +methods to obtain PathObjects and TextObjects. + + +.getClassDoc reportlab.pdfgen.canvas Canvas +.pageBreak +The method Canvas.beginPath allows users to construct +a PDFPathObject, which is defined in reportlab/pdfgen/pathobject.py. + +.getClassDoc reportlab.pdfgen.pathobject PDFPathObject +.pageBreak +The method Canvas.beginText allows users to construct +a PDFTextObject, which is defined in reportlab/pdfgen/textobject.py. + +.getClassDoc reportlab.pdfgen.textobject PDFTextObject + +.pageBreak +.h1 reportlab.platypus subpackage + +The platypus package defines our high-level page layout API. +The division into modules is far from final and has been +based more on balancing the module lengths than on any +particular programming interface. The __init__ module +imports the key classes into the top level of the package. + +.h2 Overall Structure + +Abstractly Platypus currently can be thought of has having four +levels: documents, pages, frames and flowables (things which can fit into frames in some way). +In practice there is a fifth level, the canvas, so that if you want +you can do anything that pdfgen's canvas allows. + +.h2 Document Templates +.h3 BaseDocTemplate + +The basic document template class; it provides for initialisation and +rendering of documents. A whole bunch of methods +handle_XXX handle document +rendering events. These event routines all contain some +significant semantics so while these may be overridden that +may require some detailed knowledge. Some other methods are +completely virtual and are designed to be overridden. +.h3 BaseDocTemplate +.getClassDoc reportlab.platypus.doctemplate BaseDocTemplate + +.pageBreak +A simple document processor can be made using derived class, +SimpleDocTemplate. + +.pageBreak +.h3 SimpleDocTemplate +.getClassDoc reportlab.platypus.doctemplate SimpleDocTemplate + +.pageBreak +.h2 Flowables +.getClassDoc reportlab.platypus.paragraph Paragraph +.getClassDoc reportlab.platypus.flowables Flowable +.getClassDoc reportlab.platypus.flowables XBox +.getClassDoc reportlab.platypus.flowables Preformatted +.getClassDoc reportlab.platypus.flowables Image +.getClassDoc reportlab.platypus.flowables Spacer +.getClassDoc reportlab.platypus.flowables PageBreak +.getClassDoc reportlab.platypus.flowables CondPageBreak +.getClassDoc reportlab.platypus.flowables KeepTogether +.getClassDoc reportlab.platypus.flowables Macro +.getClassDoc reportlab.platypus.xpreformatted XPreformatted +.getClassDoc reportlab.platypus.xpreformatted PythonPreformatted + +.pageBreak +.h1 reportlab.lib subpackage + +This package contains a number of modules which either add utility +to pdfgen and platypus, or which are of general use in graphics +applications. + +.h2 reportlab.lib.colors module +.getModuleDoc reportlab.lib.colors + +.h2 reportlab.lib.corp module +.getModuleDoc reportlab.lib.corp + +.h2 reportlab.lib.enums module +.getModuleDoc reportlab.lib.enums + +.h2 reportlab.lib.fonts module +.getModuleDoc reportlab.lib.fonts + +.h2 reportlab.lib.pagesizes module +.getModuleDoc reportlab.lib.pagesizes + +.h2 reportlab.lib.sequencer module +.getModuleDoc reportlab.lib.sequencer + + + + +.pageBreak +.h1 Appendix A - CVS Revision History +.beginPre Code +$Log: reference.yml,v $ +Revision 1.1 2001/10/05 12:33:33 rgbecker +Moved from original project docs, history lost + +Revision 1.13 2001/08/30 10:32:38 dinu_gherman +Added missing flowables. + +Revision 1.12 2001/07/11 09:21:27 rgbecker +Typo fix from Jerome Alet + +Revision 1.11 2000/07/10 23:56:09 andy_robinson +Paragraphs chapter pretty much complete. Fancy cover. + +Revision 1.10 2000/07/03 15:39:51 rgbecker +Documentation fixes + +Revision 1.9 2000/06/28 14:52:43 rgbecker +Documentation changes + +Revision 1.8 2000/06/19 23:52:31 andy_robinson +rltemplate now simple, based on UserDocTemplate + +Revision 1.7 2000/06/17 07:46:45 andy_robinson +Small text changes + +Revision 1.6 2000/06/14 21:22:52 andy_robinson +Added docs for library + +Revision 1.5 2000/06/12 11:26:34 andy_robinson +Numbered list added + +Revision 1.4 2000/06/12 11:13:09 andy_robinson +Added sequencer tags to paragraph parser + +Revision 1.3 2000/06/09 01:44:24 aaron_watters +added automatic generation for pathobject and textobject modules. + +Revision 1.2 2000/06/07 13:39:22 andy_robinson +Added some text to the first page of reference, and a build batch file + +Revision 1.1.1.1 2000/06/05 16:39:04 andy_robinson +initial import + +.endPre + + + diff --git a/bin/reportlab/docs/userguide/app_demos.py b/bin/reportlab/docs/userguide/app_demos.py new file mode 100644 index 00000000000..6c1c156477c --- /dev/null +++ b/bin/reportlab/docs/userguide/app_demos.py @@ -0,0 +1,113 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/app_demos.py +from reportlab.tools.docco.rl_doc_utils import * + +Appendix1("ReportLab Demos") +disc("""In the subdirectories of $reportlab/demos$ there are a number of working examples showing +almost all aspects of reportlab in use.""") + +heading2("""Odyssey""") +disc(""" +The three scripts odyssey.py, dodyssey.py and fodyssey.py all take the file odyssey.txt +and produce PDF documents. The included odyssey.txt is short; a longer and more testing version +can be found at ftp://ftp.reportlab.com/odyssey.full.zip. +""") +eg(""" +Windows +cd reportlab\\demos\\odyssey +python odyssey.py +start odyssey.pdf + +Linux +cd reportlab/demos/odyssey +python odyssey.py +acrord odyssey.pdf +""") +disc("""Simple formatting is shown by the odyssey.py script. It runs quite fast, +but all it does is gather the text and force it onto the canvas pages. It does no paragraph +manipulation at all so you get to see the XML < & > tags. +""") +disc("""The scripts fodyssey.py and dodyssey.py handle paragraph formatting so you get +to see colour changes etc. Both scripts +use the document template class and the dodyssey.py script shows the ability to do dual column +layout and uses multiple page templates. +""") + +heading2("""Standard Fonts and Colors""") +disc("""In $reportlab/demos/stdfonts$ the script stdfonts.py can be used to illustrate +ReportLab's standard fonts. Run the script using""") +eg(""" +cd reportlab\\demos\\stdfonts +python stdfonts.py +""") +disc(""" +to produce two PDF documents, StandardFonts_MacRoman.pdf & +StandardFonts_WinAnsi.pdf which show the two most common built in +font encodings. +""") +disc("""The colortest.py script in $reportlab/demos/colors$ demonstrates the different ways in which +reportlab can set up and use colors.""") +disc("""Try running the script and viewing the output document, colortest.pdf. This shows +different color spaces and a large selection of the colors which are named +in the $reportlab.lib.colors$ module. +""") +heading2("""Py2pdf""") +disc("""Dinu Gherman (<gherman@europemail.com>) contributed this useful script +which uses reportlab to produce nicely colorized PDF documents from Python +scripts including bookmarks for classes, methods and functions. +To get a nice version of the main script try""") +eg(""" +cd reportlab/demos/py2pdf +python py2pdf.py py2pdf.py +acrord py2pdf.pdf +""") +disc("""i.e. we used py2pdf to produce a nice version of py2pdf.py in +the document with the same rootname and a .pdf extension. +""") +disc(""" +The py2pdf.py script has many options which are beyond the scope of this +simple introduction; consult the comments at the start of the script. +""") +heading2("Gadflypaper") +disc(""" +The Python script, gfe.py, in $reportlab/demos/gadflypaper$ uses an inline style of +document preparation. The script almost entirely produced by Aaron Watters produces a document +describing Aaron's $gadfly$ in memory database for Python. To generate the document use +""") +eg(""" +cd reportlab\\gadflypaper +python gfe.py +start gfe.pdf +""") +disc(""" +everything in the PDF document was produced by the script which is why this is an inline style +of document production. So, to produce a header followed by some text the script uses functions +$header$ and $p$ which take some text and append to a global story list. +""") +eg(''' +header("Conclusion") + +p("""The revamped query engine design in Gadfly 2 supports +.......... +and integration.""") +''') +heading2("""Pythonpoint""") +disc("""Andy Robinson has refined the pythonpoint.py script (in $reportlab\\demos\\pythonpoint$) +until it is a really useful script. It takes an input file containing an XML markup +and uses an xmllib style parser to map the tags into PDF slides. When run in its own directory +pythonpoint.py takes as a default input the file pythonpoint.xml and produces pythonpoint.pdf +which is documentation for Pythonpoint! You can also see it in action with an older paper +""") +eg(""" +cd reportlab\\demos\\pythonpoint +python pythonpoint.py monterey.xml +start monterey.pdf +""") +disc(""" +Not only is pythonpoint self documenting, but it also demonstrates reportlab and PDF. It uses +many features of reportlab (document templates, tables etc). +Exotic features of PDF such as fadeins and bookmarks are also shown to good effect. The use of +an XML document can be contrasted with the inline style of the gadflypaper demo; the +content is completely separate from the formatting +""") \ No newline at end of file diff --git a/bin/reportlab/docs/userguide/ch1_intro.py b/bin/reportlab/docs/userguide/ch1_intro.py new file mode 100644 index 00000000000..1aa99314d99 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch1_intro.py @@ -0,0 +1,693 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch1_intro.py +from reportlab.tools.docco.rl_doc_utils import * +import reportlab + +title("ReportLab PDF Library") +title("User Guide") +centred('ReportLab Version ' + reportlab.Version) + +nextTemplate("Normal") + +######################################################################## +# +# Chapter 1 +# +######################################################################## + + +heading1("Introduction") + + +heading2("About this document") +disc("""This document is an introduction to the ReportLab PDF library. +Some previous programming experience +is presumed and familiarity with the Python Programming language is +recommended. If you are new to Python, we tell you in the next section +where to go for orientation. +""") + +disc(""" +This manual does not cover 100% of the features, but should explain all +the main concepts and help you get started, and point you at other +learning resources. +After working your way through this, you should be ready to begin +writing programs to produce sophisticated reports. +""") + +disc("""In this chapter, we will cover the groundwork:""") +bullet("What is ReportLab all about, and why should I use it?") +bullet("What is Python?") +bullet("How do I get everything set up and running?") + +todo(""" +We need your help to make sure this manual is complete and helpful. +Please send any feedback to our user mailing list, +which is signposted from www.reportlab.org. +""") + +heading2("What is the ReportLab PDF Library?") +disc("""This is a software library that lets you directly +create documents in Adobe's Portable Document Format (PDF) using +the Python programming language. It also creates charts and data graphics +in various bitmap and vector formats as well as PDF.""") + +disc("""PDF is the global standard for electronic documents. It +supports high-quality printing yet is totally portable across +platforms, thanks to the freely available Acrobat Reader. Any +application which previously generated hard copy reports or driving a printer +can benefit from making PDF documents instead; these can be archived, +emailed, placed on the web, or printed out the old-fashioned way. +However, the PDF file format is a complex +indexed binary format which is impossible to type directly. +The PDF format specification is more than 600 pages long and +PDF files must provide precise byte offsets -- a single extra +character placed anywhere in a valid PDF document can render it +invalid. This makes it harder to generate than HTML.""") + +disc("""Most of the world's PDF documents have been produced +by Adobe's Acrobat tools, or rivals such as JAWS PDF Creator, which act +as 'print drivers'. Anyone wanting to automate PDF production would +typically use a product like Quark, Word or Framemaker running in a loop +with macros or plugins, connected to Acrobat. Pipelines of several +languages and products can be slow and somewhat unwieldy. +""") + + +disc("""The ReportLab library directly creates PDF based on +your graphics commands. There are no intervening steps. Your applications +can generate reports extremely fast - sometimes orders +of magnitude faster than traditional report-writing +tools. This approach is shared by several other libraries - PDFlib for C, +iText for Java, iTextSharp for .NET and others. However, The ReportLab library +differs in that it can work at much higher levels, with a full featured engine +for laying out documents complete with tables and charts. """) + + +disc("""In addition, because you are writing a program +in a powerful general purpose language, there are no +restrictions at all on where you get your data from, +how you transform it, and the kind of output +you can create. And you can reuse code across +whole families of reports.""") + +disc("""The ReportLab library is expected to be useful +in at least the following contexts:""") +bullet("Dynamic PDF generation on the web") +bullet("High-volume corporate reporting and database publishing") +bullet("""An embeddable print engine for other applications, including +a 'report language' so that users can customize their own reports. +This is particularly relevant to cross-platform apps which cannot +rely on a consistent printing or previewing API on each operating +system.""") +bullet("""A 'build system' for complex documents with charts, tables +and text such as management accounts, statistical reports and +scientific papers """) +bullet("""Going from XML to PDF in one step!""") + + + + +heading2("What is Python?") +disc(""" +Python is an interpreted, interactive, object-oriented programming language. It is often compared to Tcl, Perl, +Scheme or Java. +""") + +disc(""" +Python combines remarkable power with very clear syntax. It has modules, classes, exceptions, very high level +dynamic data types, and dynamic typing. There are interfaces to many system calls and libraries, as well as to +various windowing systems (X11, Motif, Tk, Mac, MFC). New built-in modules are easily written in C or C++. +Python is also usable as an extension language for applications that need a programmable interface. +""") + + +disc(""" +Python is as old as Java and has been growing steadily in popularity for 13 years; since our +library first came out it has entered the mainstream. Many ReportLab library users are +already Python devotees, but if you are not, we feel that the language is an excellent +choice for document-generation apps because of its expressiveness and ability to get +data from anywhere. +""") + +disc(""" +Python is copyrighted but freely usable and distributable, even for commercial use. +""") + +heading2("Acknowledgements") +disc("""Many people have contributed to ReportLab. We would like to thank +in particular (in approximately chronological order) Chris Lee, Magnus Lie Hetland, +Robert Kern, Jeff Bauer (who contributed normalDate.py); Jerome Alet (numerous patches +and the rlzope demo), Andre Reitz, Max M, Albertas Agejevas, T Blatter, Ron Peleg, +Gary Poster, Steve Halasz, Andrew Mercer, Paul McNett, Chad Miller, Tim Roberts, +Jorge Godoy and Benn B.""") + +disc("""Special thanks go to Just van Rossum for his valuable assistance with +font technicalities and the LettErrorRobot-Chrome type 1 font.""") + +disc("""Marius Gedminas deserves a big hand for contributing the work on TrueType fonts and we +are glad to include these in the toolkit. Finally we thank Bigelow & Holmes Inc ($design@bigelowandholmes.com$) +for Luxi Serif Regular and Ray Larabie ($http://www.larabiefonts.com$) for the Rina TrueType font.""") + +heading2("Installation and Setup") + +disc(""" +Below we provide an abbreviated setup procedure for Python experts and a more +verbose procedure for people who are new to Python. +""") + +heading3("Installation for experts") +disc("""First of all, we'll give you the high-speed version for experienced +Python developers:""") +list("""Install Python 2.3 or later (2.4 recommended). ReportLab 2.x uses + Python 2.3 features and will use 2.4 going forwards. We also maintain + a 1.x branch which works back to Python 2.1. + """) +list("""If you want to produce compressed PDF files (recommended), +check that zlib is installed.""") +list("""If you want to work with bitmap images, install and +test the Python Imaging Library""") +list("""Unpack the reportlab package (reportlab.zip +or reportlab.tgz) into a directory on your path. (You can also use ^python setup.py install^ if you wish)""") +list("""Unpack the rl_addons package and build the C extensions with distutils; or grab the +corresponding .pyd files from our download page. """) + + +list("""$cd$ to ^reportlab/test^ and execute $runAll.py$. +This will create many PDF files. """) +list("""You may also want to download and run the ^rl_check.py^ on our site, which +health-checks an installation and reports on any missing options. """) +disc(" ") +disc("""If you have any problems, check the 'Detailed Instructions' section below.""") + +heading3("A note on available versions") +disc("""The $reportlab$ library can be found at $ftp.reportlab.com$ in +the top-level directory or at http://www.reportlab.com/ftp/. +Each successive version is stored in both zip +and tgz format, but the contents are identical apart from line endings. +Versions are numbered: $ReportLab_1_00.zip$, $ReportLab_1_01.zip$ and so on. The +latest stable version is also available as just $reportlab.zip$ (or +$reportlab.tgz$), which is actually a symbolic link to the latest +numbered version. Finally, daily snapshots off the trunk are available as +$current.zip$ (or $current.tgz$). +""") + + +heading3("Instructions for novices: Windows") + + + +disc("""This section assumes you +don't know much about Python. We cover all of the steps for three +common platforms, including how to verify that each one is complete. +While this may seem like a long list, everything takes 5 minutes if +you have the binaries at hand.""") + + +restartList() + +list("""Get and install Python from $http://www.python.org/.$ +Reportlab 2.x works with Python 2.3 upwards but we strongly recommend to use +the latest stable version of Python (2.4.3 at the time of writing). +Follow the links to 'Download' and get the latest +official version. This will install itself into $C:\Python24$ +After installing, you should be able to run the +'Python (command line)' option from the Start Menu.""") + +list("""If on Windows, we strongly recommend installing the Python Windows +Extensions, which let you use access all the Windows data sources, and provide +a very nice IDE. This can be found at ^http://sourceforge.net/projects/pywin32/^. +Once this is installed, you can start +Pythonwin from the Start Menu and get a GUI application.""") + +list("""The next step is optional and only necessary if you want to +include images in your reports; it can also be carried out later. However +we always recommend a full installation if time permits.""") + +list("""Install the Python Imaging Library ($PIL$) from $http://www.pythonware.com/products/pil/$. +""") + + +list("""Now you are ready to install reportlab itself. Unzip the archive straight into +your Python directory; it creates a subdirectory named +$reportlab$. You should now be able to go to a Python +command line interpreter and type $import reportlab$ without getting +an error message.""") + +list("""Download the zip file of precompiled DLLs for your Python version from +the bottom of the ^http://www.reportlab.org/downloads.html^ downloads page, and unzip +them into ^C:\Python24\lib\site-packages^ (or its equivalent for other Python versions""") + +list("""Open up a $MS-DOS$ command prompt and CD to +"$..\\reportlab\\test$". Enter "$runAll.py$". You should see lots of dots +and no error messages. This will also create many PDF files and generate +the manuals in ^reportlab/docs^ (including this one). """) + +list(""" +Finally, we recommend you download and run the script ^rl_check.py^ from +^^http://www.reportlab.org/ftp/^. This will health-check all the above +steps and warn you if anything is missing or mismatched.""") + +heading3("Instructions for Python novices: Unix") + +restartList() +list("""On a large number of Unix and Linux distributions, Python is already installed, +or is avaialable as a standard package you can install with the relevant package manager.""") + +list("""If you want to compile from +source download the latest +sources from http://www.python.org (currently the latest source is +in http://www.python.org/ftp/python/2.4.3/Python-2.4.3.tgz). If you wish to use +binaries +get the latest RPM or DEB or whatever package and install (or get your +super user (system administrator) to do the work).""") + +list("""If you are building Python yourself, unpack the sources into a +temporary directory using a tar command e.g. $tar xzvf Python-2.4.3.tgz$; +this will create a subdirectory called Python-2.4.3 (or whatever). cd +into this directory. Then read the file $README$! It contains the +latest information on how to install Python.""") + +list("""If your system has the gzip libz library installed +check that the zlib extension will be installed by default by editing +the file Modules/Setup.in and ensuring that (near line 405) the line +containing zlib zlibmodule.c is uncommented i.e. has no hash '#' character at the +beginning. You also need to decide if you will be installing in the default location +(/usr/local/) or in some other place. +The zlib module is needed if you want compressed PDF and for some images.""") + +list("""Invoke the command $./configure --prefix=/usr/local$ this should configure +the source directory for building. Then you can build the binaries with +a $make$ command. If your $make$ command is not up to it try building +with $make MAKE=make$. If all goes well install with $make install$.""") + +list("""If all has gone well and python is in the execution search path +you should now be able to type $python$ and see a Python prompt.""") + +list(""" +Once you can do that it's time to try and install ReportLab. +First get the latest reportlab.tgz. +If ReportLab is to be available to all then the reportlab archive should be unpacked in +the lib/site-python directory (typically /usr/local/lib/site-python) if necessary by +a superuser. +Otherwise unpack in a directory of your choice and arrange for that directory to be on your +$PYTHONPATH$ variable. +""") +eg(""" +#put something like this in your +#shell rcfile +PYTHONPATH=$HOME/mypythonpackages +export PYTHONPATH +""",after=0.1) + +list("""You should now be able to run python and execute the python statement +""",doBullet=0) +eg("""import reportlab""",after=0.1) +list("""If you want to use images you should certainly consider +getting & installing the Python Imaging Library - follow the +directions from +$http://www.python.org/sigs/image-sig/index.html$ or get it directly from +$http://www.pythonware.com/products/pil/$.""") + + +heading3("Instructions for Python novices: Mac") +disc(""" +This is much, much easier with Mac OS X since Python (usually 2.3) is installed on your +system as standard. Just follow the instructions for installing the ReportLab archive +above. +""") + + +heading3("Instructions for Jython (Java implementation of Python) users") + +disc(""" +A port to Java was done in 2004. This involved some changes to the framework +and creating Java equivalents of the C extensions. At the end of this work +the entire output of the test suite produced byte-for-byte identical output. +However, we have not been testng against Jython since, because (a) as far as +we know no one used it, and (b) Jython has not kept up with Python features +which we need to use. We suggest you use ReportLab v1.19 or v1.20 which +were Python-2.1 compatible. We'd welcome test reports and/or a volunteer to +refresh things now that Jython is progressing.""") + +disc(""" +The Jython version was tested under Sun's J2SDK 1.3.1. It is known that under +J2SDK 1.4.0_01 $test_pdfbase_ttfonts.py$ fails horribly with an outOfMemory +exception, probably caused by a JVM bug. +""") + + + + +restartList() + +list(""" +Before installing Jython, make sure you have a supported version of +Java Virtual Machine installed. For the list of supported JVM's see +$http://www.jython.org/platform.html$ +""") + +list(""" +To install Jython, download the setup package from $www.jython.org$ and +follow installation instructions. +""") + +list(""" +To set ReportLab toolkit under Jython PATH, edit $JYTHON_HOME/registry$ file +and include line that tells Jython where to look for packages. To include +ReportLab toolkit under Jython PATH, directory that contains Reportlab +should be included: $python.path=REPORTLAB_HOME_PARENT_DIR$ +For example, if your Reportlab toolkit is installed under $C:\code\\reportlab$ +the path line should be: $python.path=C:\\\\code$ (note two backslashes!) +""") + +heading3("Instructions for IronPython (Python for .NET) users") + +disc(""" +We haven't tackled this yet officially, but IronPython can apparently +run much of our code. We do need to go through the same exercises we did for Jython +- finding the .NET equivalents of _rl_accel, pyRXP, _renderPM and PIL - +to get 100% managed code. Hopefully this will happen soon and we'd be +delighted to work with anyone on this. +""") + + +heading2("Getting Involved") +disc("""ReportLab is an Open Source project. Although we are +a commercial company we provide the core PDF generation +sources freely, even for commercial purposes, and we make no income directly +from these modules. We also welcome help from the community +as much as any other Open Source project. There are many +ways in which you can help:""") + +bullet("""General feedback on the core API. Does it work for you? +Are there any rough edges? Does anything feel clunky and awkward?""") + +bullet("""New objects to put in reports, or useful utilities for the library. +We have an open standard for report objects, so if you have written a nice +chart or table class, why not contribute it?""") + +bullet("""Demonstrations and Case Studies: If you have produced some nice +output, send it to us (with or without scripts). If ReportLab solved a +problem for you at work, write a little 'case study' and send it in. +And if your web site uses our tools to make reports, let us link to it. +We will be happy to display your work (and credit it with your name +and company) on our site!""") + +bullet("""Working on the core code: we have a long list of things +to refine or to implement. If you are missing some features or +just want to help out, let us know!""") + +disc("""The first step for anyone wanting to learn more or +get involved is to join the mailing list. To Subscribe visit +$http://two.pairlist.net/mailman/listinfo/reportlab-users$. +From there you can also browse through the group's archives +and contributions. The mailing list is +the place to report bugs and get support. """) + + +heading2("Site Configuration") +disc("""There are a number of options which most likely need to be configured globally for a site. +The python script module $reportlab/rl_config.py$ may be edited to change the values of several +important sitewide properties.""") +bullet("""verbose: set to integer values to control diagnostic output.""") +bullet("""shapeChecking: set this to zero to turn off a lot of error checking in the graphics modules""") +bullet("""defaultEncoding: set this to WinAnsiEncoding or MacRomanEncoding.""") +bullet("""defaultPageSize: set this to one of the values defined in reportlab/lib/pagesizes.py; as delivered +it is set to pagesizes.A4; other values are pagesizes.letter etc.""") +bullet("""defaultImageCaching: set to zero to inhibit the creation of .a85 files on your +hard-drive. The default is to create these preprocessed PDF compatible image files for faster loading""") +bullet("""T1SearchPath: this is a python list of strings representing directories that +may be queried for information on Type 1 fonts""") +bullet("""TTFSearchPath: this is a python list of strings representing directories that +may be queried for information on TrueType fonts""") +bullet("""CMapSearchPath: this is a python list of strings representing directories that +may be queried for information on font code maps.""") +bullet("""showBoundary: set to non-zero to get boundary lines drawn.""") +bullet("""ZLIB_WARNINGS: set to non-zero to get warnings if the Python compression extension is not found.""") +bullet("""pageComression: set to non-zero to try and get compressed PDF.""") +bullet("""allowtableBoundsErrors: set to 0 to force an error on very large Platypus table elements""") +bullet("""emptyTableAction: Controls behaviour for empty tables, can be 'error' (default), 'indicate' or 'ignore'.""") + + + +heading2("Learning More About Python") + +disc(""" +If you are a total beginner to Python, you should check out one or more from the +growing number of resources on Python programming. The following are freely +available on the web: +""") + + +bullet("""Introductory Material on Python. +A list of tutorials on the Python.org web site. +$http://www.python.org/doc/Intros.html$ +""") + + +bullet("""Python Tutorial. +The official Python Tutorial by Guido van Rossum (edited by Fred L. Drake, Jr.) +$http://www.python.org/doc/tut/$ +""") + + +bullet("""Learning to Program. +A tutorial on programming by Alan Gauld. Has a heavy emphasis on +Python, but also uses other languages. +$http://www.freenetpages.co.uk/hp/alan.gauld/$ +""") + + +bullet("""How to think like a computer scientist (Python version). +$http://www.ibiblio.org/obp/thinkCSpy/$ +""") + + +bullet("""Instant Python. +A 6-page minimal crash course by Magnus Lie Hetland. +$http://www.hetland.org/python/instant-python.php$ +""") + + +bullet("""Dive Into Python. +A free Python tutorial for experienced programmers. +$http://diveintopython.org/$ +""") + + +from reportlab.lib.codecharts import SingleByteEncodingChart +from reportlab.tools.docco.stylesheet import getStyleSheet +styles = getStyleSheet() +indent0_style = styles['Indent0'] +indent1_style = styles['Indent1'] + +heading2("What's New in ReportLab 2.0") +disc(""" +Many new features have been added, foremost amongst which is the support +for unicode. This page documents what has changed since version 1.20.""") + +disc(""" +Adding full unicode support meant that we had to break backwards-compatibility, +so old code written for ReportLab 1 will sometimes need changes before it will +run correctly with ReportLab 2. Now that we have made the clean break to +introduce this important new feature, we intend to keep the API +backwards-compatible throughout the 2.* series. +""") +heading3("Goals for the 2.x series") +disc(""" +The main rationale for 2.0 was an incompatible change at the character level: +to properly support Unicode input. Now that it's out we will maintain compatibility +with 2.0. There are no pressing feature wishlists and new features will be driven, +as always, by contributions and the demands of projects.""") + +disc(""" +Our 1.x code base is still Python 2.1 compatible. The new version lets us move forwards +with a baseline of Python 2.4 (2.3 will work too, for the moment, but we don't promise +that going forwards) so we can use newer language features freely in our development.""") + +disc(""" +One area where we do want to make progress from release to release is with documentation +and installability. We'll be looking into better support for distutils, setuptools, +eggs and so on; and into better examples and tools to help people learn what's in the +(substantial) code base.""") + +disc(""" +Bigger ideas and more substantial rewrites are deferred to Version 3.0, with no particular +target dates. +""") + +heading3("Contributions") +disc("""Thanks to everybody who has contributed to the open-source toolkit in the run-up +to the 2.0 release, whether by reporting bugs, sending patches, or contributing to the +reportlab-users mailing list. Thanks especially to the following people, who contributed +code that has gone into 2.0: Andre Reitz, Max M, Albertas Agejevas, T Blatter, Ron Peleg, +Gary Poster, Steve Halasz, Andrew Mercer, Paul McNett, Chad Miller. +""") +todo("""If we missed you, please let us know!""") + +heading3("Unicode support") +disc(""" +This is the Big One, and the reason some apps may break. You must now pass in text either +in UTF-8 or as unicode string objects. The library will handle everything to do with output +encoding. There is more information on this below. +Since this is the biggest change, we'll start by reviewing how it worked in the past.""") + +disc(""" +In ReportLab 1.x, any string input you passed to our APIs was supposed to be in the same +encoding as the font you selected for output. If using the default fonts in Acrobat Reader +(Helvetica/Times/Courier), you would have implicitly used WinAnsi encoding, which is almost +exactly the same as Latin-1. However, if using TrueType fonts, you would have been using UTF-8.""") + +disc("""For Asian fonts, you had a wide choice of encodings but had to specify which one +(e.g Shift-JIS or EUC for Japanese). This state of affairs meant that you had +to make sure that every piece of text input was in the same encoding as the font used +to display it.""") + + + +disc("""Input text encoding is UTF-8 or Python Unicode strings""") +disc(""" +Any text you pass to a canvas API (drawString etc.), Paragraph or other flowable +constructor, into a table cell, or as an attribute of a graphic (e.g. chart.title.text), +is supposed to be unicode. If you use a traditional Python string, it is assumed to be UTF-8. +If you pass a Unicode object, we know it's unicode.""", style=indent1_style) + +disc("""Font encodings""") +disc(""" +Fonts still work in different ways, and the built-in ones will still use WinAnsi or MacRoman +internally while TrueType will use UTF-8. However, the library hides this from you; it converts +as it writes out the PDF file. As before, it's still your job to make sure the font you use has +the characters you need, or you may get either a traceback or a visible error character.""",style=indent1_style) + +disc("""Asian CID fonts""") +disc(""" +You no longer need to specify the encoding for the built-in Asian fonts, just the face name. +ReportLab knows about the standard fonts in Adobe's Asian Language Packs +""", style=indent1_style) + +disc("""Asian Truetype fonts""") +disc(""" +The standard Truetype fonts differ slightly for Asian languages (e.g msmincho.ttc). +These can now be read and used, albeit somewhat inefficiently. +""", style=indent1_style) + +disc("""Asian word wrapping""") +disc(""" +Previously we could display strings in Asian languages, but could not properly +wrap paragraphs as there are no gaps between the words. We now have a basic word wrapping +algorithm. +""", style=indent1_style) + +disc("""unichar tag""") +disc(""" +A convenience tag, <unichar/> has also been added. You can now do +or <unichar name='LATIN SMALL LETTER U WITH DIAERESIS'/> and +get a lowercase u umlaut. Names should be those in the Unicode Character Database. +""", style=indent1_style) + +disc("""Accents, greeks and symbols""") +disc(""" +The correct way to refer to all non-ASCII characters is to use their unicode representation. +This can be literal Unicode or UTF-8. Special symbols and Greek letters (collectively, "greeks") +inserted in paragraphs using the greek tag (e.g. <greek>lambda</greek>) or using the entity +references (e.g. λ) are now processed in a different way than in version 1.""", style=indent1_style) +disc(""" +Previously, these were always rendered using the Zapf Dingbats font. Now they are always output +in the font you specified, unless that font does not support that character. If the font does +not support the character, and the font you specified was an Adobe Type 1 font, Zapf Dingbats +is used as a fallback. However, at present there is no fallback in the case of TTF fonts. +Note that this means that documents that contain greeks and specify a TTF font may need +changing to explicitly specify the font to use for the greek character, or you will see a black +square in place of that character when you view your PDF output in Acrobat Reader. +""", style=indent1_style) + +# Other New Features Section ####################### +heading3("Other New Features") +disc("""PDF""") +disc("""Improved low-level annotation support for PDF "free text annotations" +""", style=indent0_style) +disc("""FreeTextAnnotation allows showing and hiding of an arbitrary PDF "form" +(reusable chunk of PDF content) depending on whether the document is printed or +viewed on-screen, or depending on whether the mouse is hovered over the content, etc. +""", style=indent1_style) + +disc("""TTC font collection files are now readable" +""", style=indent0_style) +disc("""ReportLab now supports using TTF fonts packaged in .TTC files""", style=indent1_style) + +disc("""East Asian font support (CID and TTF)""", style=indent0_style) +disc("""You no longer need to specify the encoding for the built-in Asian fonts, +just the face name. ReportLab knows about the standard fonts in Adobe's Asian Language Packs. +""", style=indent1_style) + +disc("""Native support for JPEG CMYK images""", style=indent0_style) +disc("""ReportLab now takes advantage of PDF's native JPEG CMYK image support, +so that JPEG CMYK images are no longer (lossily) converted to RGB format before including +them in PDF.""", style=indent1_style) + + +disc("""Platypus""") +disc("""Link support in paragraphs""", style=indent0_style) +disc(""" +Platypus paragraphs can now contain link elements, which support both internal links +to the same PDF document, links to other local PDF documents, and URL links to pages on +the web. Some examples:""", style=indent1_style) +disc("""Web links:""", style=indent1_style) +disc("""<link href="http://www.reportlab.com/">ReportLab<link>""", style=styles['Link']) + +disc("""Internal link to current PDF document:""", style=indent1_style) +disc("""<link href="summary">ReportLab<link>""", style=styles['Link']) + +disc("""External link to a PDF document on the local filesystem:""", style=indent1_style) +disc("""<link href="pdf:C:/john/report.pdf">ReportLab<link>""", style=styles['Link']) + +disc("""Improved wrapping support""", style=indent0_style) +disc("""Support for wrapping arbitrary sequence of flowables around an image, using +reportlab.platypus.flowables.ImageAndFlowables (similar to ParagraphAndImage).""" +,style=indent1_style) + +disc("""KeepInFrame""", style=indent0_style) +disc("""Sometimes the length of a piece of text you'd like to include in a fixed piece +of page "real estate" is not guaranteed to be constrained to a fixed maximum length. +In these cases, KeepInFrame allows you to specify an appropriate action to take when +the text is too long for the space allocated for it. In particular, it can shrink the text +to fit, mask (truncate) overflowing text, allow the text to overflow into the rest of the document, +or raise an error.""",style=indent1_style) + + +disc("""Improved convenience features for inserting unicode symbols and other characters +""", style=indent0_style) +disc(""" lets you conveniently insert unicode characters using the standard long name +or code point. Characters inserted with the <greek> tags (e.g. lambda) or corresponding +entity references (e.g. λ) support arbitrary fonts (rather than only Zapf Dingbats).""",style=indent1_style) + +disc("""Improvements to Legending""", style=indent0_style) +disc("""Instead of manual placement, there is now a attachment point (N, S, E, W, etc.), so that +the legend is always automatically positioned correctly relative to the chart. Swatches (the small +sample squares of colour / pattern fill sometimes displayed in the legend) can now be automatically +created from the graph data. Legends can now have automatically-computed totals (useful for +financial applications).""",style=indent1_style) + +disc("""More and better ways to place piechart labels""", style=indent0_style) +disc("""New smart algorithms for automatic pie chart label positioning have been added. +You can now produce nice-looking labels without manual positioning even for awkward cases in +big runs of charts.""",style=indent1_style) + +disc("""Adjustable piechart slice ordering""", style=indent0_style) +disc("""For example. pie charts with lots of small slices can be configured to alternate thin and +thick slices to help the lagel placememt algorithm work better.""",style=indent1_style) +disc("""Improved spiderplots""", style=indent0_style) + + +# Noteworthy bug fixes Section ####################### +heading3("Noteworthy bug fixes") +disc("""Fixes to TTF splitting (patch from Albertas Agejevas)""") +disc("""This affected some documents using font subsetting""", style=indent0_style) + +disc("""Tables with spans improved splitting""") +disc("""Splitting of tables across pages did not work correctly when the table had +row/column spans""", style=indent0_style) + +disc("""Fix runtime error affecting keepWithNext""") diff --git a/bin/reportlab/docs/userguide/ch2_graphics.py b/bin/reportlab/docs/userguide/ch2_graphics.py new file mode 100644 index 00000000000..083b756db5b --- /dev/null +++ b/bin/reportlab/docs/userguide/ch2_graphics.py @@ -0,0 +1,1180 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch2_graphics.py +from reportlab.tools.docco.rl_doc_utils import * +from reportlab.lib.codecharts import SingleByteEncodingChart + +heading1("Graphics and Text with $pdfgen$") + +heading2("Basic Concepts") +disc(""" +The $pdfgen$ package is the lowest level interface for +generating PDF documents. A $pdfgen$ program is essentially +a sequence of instructions for "painting" a document onto +a sequence of pages. The interface object which provides the +painting operations is the $pdfgen canvas$. +""") + +disc(""" +The canvas should be thought of as a sheet of white paper +with points on the sheet identified using Cartesian ^(X,Y)^ coordinates +which by default have the ^(0,0)^ origin point at the lower +left corner of the page. Furthermore the first coordinate ^x^ +goes to the right and the second coordinate ^y^ goes up, by +default.""") + +disc(""" +A simple example +program that uses a canvas follows. +""") + +eg(""" + from reportlab.pdfgen import canvas + def hello(c): + c.drawString(100,100,"Hello World") + c = canvas.Canvas("hello.pdf") + hello(c) + c.showPage() + c.save() +""") + +disc(""" +The above code creates a $canvas$ object which will generate +a PDF file named $hello.pdf$ in the current working directory. +It then calls the $hello$ function passing the $canvas$ as an argument. +Finally the $showPage$ method saves the current page of the canvas +and the $save$ method stores the file and closes the canvas.""") + +disc(""" +The $showPage$ method causes the $canvas$ to stop drawing on the +current page and any further operations will draw on a subsequent +page (if there are any further operations -- if not no +new page is created). The $save$ method must be called after the +construction of the document is complete -- it generates the PDF +document, which is the whole purpose of the $canvas$ object. +""") + +heading2("More about the Canvas") +disc(""" +Before describing the drawing operations, we will digress to cover +some of the things which can be done to configure a canvas. There +are many different settings available. If you are new to Python +or can't wait to produce some output, you can skip ahead, but +come back later and read this!""") + +disc("""First of all, we will look at the constructor +arguments for the canvas:""") +eg(""" def __init__(self,filename, + pagesize=(595.27,841.89), + bottomup = 1, + pageCompression=0, + encoding=rl_config.defaultEncoding, + verbosity=0): + """) + +disc("""The $filename$ argument controls the +name of the final PDF file. You +may also pass in any open file object (such as $sys.stdout$, the python process standard output) +and the PDF document will be written to that. Since PDF +is a binary format, you should take care when writing other +stuff before or after it; you can't deliver PDF documents +inline in the middle of an HTML page!""") + +disc("""The $pagesize$ argument is a tuple of two numbers +in points (1/72 of an inch). The canvas defaults to $A4$ (an international standard +page size which differs from the American standard page size of $letter$), +but it is better to explicitly specify it. Most common page +sizes are found in the library module $reportlab.lib.pagesizes$, +so you can use expressions like""") + +eg("""from reportlab.lib.pagesizes import letter, A4 +myCanvas = Canvas('myfile.pdf', pagesize=letter) +width, height = letter #keep for later +""") + +pencilnote() + +disc("""If you have problems printing your document make sure you +are using the right page size (usually either $A4$ or $letter$). +Some printers do not work well with pages that are too large or too small.""") + +disc("""Very often, you will want to calculate things based on +the page size. In the example above we extracted the width and +height. Later in the program we may use the $width$ variable to +define a right margin as $width - inch$ rather than using +a constant. By using variables the margin will still make sense even +if the page size changes.""") + +disc("""The $bottomup$ argument +switches coordinate systems. Some graphics systems (like PDF +and PostScript) place (0,0) at the bottom left of the page +others (like many graphical user interfaces [GUI's]) place the origen at the top left. The +$bottomup$ argument is deprecated and may be dropped in future""") + +todo("""Need to see if it really works for all tasks, and if not + then get rid of it""") + +disc("""The $pageCompression$ option determines whether the stream +of PDF operations for each page is compressed. By default +page streams are not compressed, because the compression slows the file generation process. +If output size is important set $pageCompression=1$, but remember that, compressed documents will +be smaller, but slower to generate. Note that images are always compressed, and this option +will only save space if you have a very large amount of text and vector graphics +on each page.""") + +disc("""The $encoding$ argument is largely obsolete in version 2.0 and can +probably be omitted by 99% of users. Its default value is fine unless you +very specifically need to use one of the 25 or so characters which are present +in MacRoman and not in Winansi. A useful reference to these is here: +http://www.alanwood.net/demos/charsetdiffs.html. + +The parameter determines which font encoding is used for the +standard Type 1 fonts; this should correspond to the encoding on your system. +Note that this is the encoding used internally by the font; text you +pass to the ReportLab toolkit for rendering should always either be a Python +unicode string object or a UTF-8 encoded byte string (see the next chapter)! +The font encoding has two values at present: $'WinAnsiEncoding'$ or +$'MacRomanEncoding'$. The variable $rl_config.defaultEncoding$ above points +to the former, which is standard on Windows, Mac OS X and many Unices +(including Linux). If you are Mac user and don't have OS X, you may want to +make a global change: modify the line at the top of +reportlab/pdfbase/pdfdoc.py to switch it over. Otherwise, you can +probably just ignore this argument completely and never pass it. For all TTF +and the commonly-used CID fonts, the encoding you pass in here is ignored, +since the reportlab library itself knows the right encodings in those +cases.""") + +disc("""The demo script $reportlab/demos/stdfonts.py$ +will print out two test documents showing all code points +in all fonts, so you can look up characters. Special +characters can be inserted into string commands with +the usual Python escape sequences; for example \\101 = 'A'.""") + +disc("""The $verbosity$ argument determines how much log +information is printed. By default, it is zero to assist +applications which want to capture PDF from standard output. +With a value of 1, you will get a confirmation message +each time a document is generated. Higher numbers may +give more output in future.""") + +todo("to do - all the info functions and other non-drawing stuff") + + + + + +todo("""Cover all constructor arguments, and setAuthor etc.""") + +heading2("Drawing Operations") +disc(""" +Suppose the $hello$ function referenced above is implemented as +follows (we will not explain each of the operations in detail +yet). +""") + +eg(examples.testhello) + +disc(""" +Examining this code notice that there are essentially two types +of operations performed using a canvas. The first type draws something +on the page such as a text string or a rectangle or a line. The second +type changes the state of the canvas such as +changing the current fill or stroke color or changing the current font +type and size. +""") + +disc(""" +If we imagine the program as a painter working on +the canvas the "draw" operations apply paint to the canvas using +the current set of tools (colors, line styles, fonts, etcetera) +and the "state change" operations change one of the current tools +(changing the fill color from whatever it was to blue, or changing +the current font to $Times-Roman$ in 15 points, for example). +""") + +disc(""" +The document generated by the "hello world" program listed above would contain +the following graphics. +""") + +illust(examples.hello, '"Hello World" in pdfgen') + +heading3("About the demos in this document") + +disc(""" +This document contains demonstrations of the code discussed like the one shown +in the rectangle above. These demos are drawn on a "tiny page" embedded +within the real pages of the guide. The tiny pages are %s inches wide +and %s inches tall. The demo displays show the actual output of the demo code. +For convenience the size of the output has been reduced slightly. +""" % (examplefunctionxinches, examplefunctionyinches)) + +heading2('The tools: the "draw" operations') + +disc(""" +This section briefly lists the tools available to the program +for painting information onto a page using the canvas interface. +These will be discussed in detail in later sections. They are listed +here for easy reference and for summary purposes. +""") + +heading3("Line methods") + +eg("""canvas.line(x1,y1,x2,y2)""") +eg("""canvas.lines(linelist)""") + +disc(""" +The line methods draw straight line segments on the canvas. +""") + +heading3("Shape methods") + +eg("""canvas.grid(xlist, ylist) """) +eg("""canvas.bezier(x1, y1, x2, y2, x3, y3, x4, y4)""") +eg("""canvas.arc(x1,y1,x2,y2) """) +eg("""canvas.rect(x, y, width, height, stroke=1, fill=0) """) +eg("""canvas.ellipse(x1,y1, x2,y2, stroke=1, fill=0)""") +eg("""canvas.wedge(x1,y1, x2,y2, startAng, extent, stroke=1, fill=0) """) +eg("""canvas.circle(x_cen, y_cen, r, stroke=1, fill=0)""") +eg("""canvas.roundRect(x, y, width, height, radius, stroke=1, fill=0) """) + +disc(""" +The shape methods draw common complex shapes on the canvas. +""") + +heading3("String drawing methods") + +eg("""canvas.drawString(x, y, text):""") +eg("""canvas.drawRightString(x, y, text) """) +eg("""canvas.drawCentredString(x, y, text)""") + +disc(""" +The draw string methods draw single lines of text on the canvas. +""") + +heading3("The text object methods") +eg("""textobject = canvas.beginText(x, y) """) +eg("""canvas.drawText(textobject) """) + +disc(""" +Text objects are used to format text in ways that +are not supported directly by the $canvas$ interface. +A program creates a text object from the $canvas$ using $beginText$ +and then formats text by invoking $textobject$ methods. +Finally the $textobject$ is drawn onto the canvas using +$drawText$. +""") + +heading3("The path object methods") + +eg("""path = canvas.beginPath() """) +eg("""canvas.drawPath(path, stroke=1, fill=0) """) +eg("""canvas.clipPath(path, stroke=1, fill=0) """) + +disc(""" +Path objects are similar to text objects: they provide dedicated control +for performing complex graphical drawing not directly provided by the +canvas interface. A program creates a path object using $beginPath$ +populates the path with graphics using the methods of the path object +and then draws the path on the canvas using $drawPath$.""") + +disc("""It is also possible +to use a path as a "clipping region" using the $clipPath$ method -- for example a circular path +can be used to clip away the outer parts of a rectangular image leaving +only a circular part of the image visible on the page. +""") + +heading3("Image methods") +pencilnote() +disc(""" +You need the Python Imaging Library (PIL) to use images with the ReportLab package. +Examnples of the techniques below can be found by running the script $test_pdfgen_general.py$ +in our $test$ subdirectory and looking at page 7 of the output. +""") + +disc(""" +There are two similar-sounding ways to draw images. The preferred one is +the $drawImage$ method. This implements a caching system so you can +define an image once and draw it many times; it will only be +stored once in the PDF file. $drawImage$ also exposes one advanced parameter, +a transparency mask, and will expose more in future. The older technique, +$drawInlineImage$, stores bitmaps within the page stream and is thus very +inefficient if you use the same image more than once in a document; but can result +in PDFs which render faster if the images are very small and not repeated. We'll +discuss the oldest one first: +""") + +eg("""canvas.drawInlineImage(self, image, x,y, width=None,height=None) """) + +disc(""" +The $drawInlineImage$ method places an image on the canvas. The $image$ +parameter may be either a PIL Image object or an image filename. Many common +file formats are accepted including GIF and JPEG. It returns the size of the actual +image in pixels as a (width, height) tuple. +""") + +eg("""canvas.drawImage(self, image, x,y, width=None,height=None,mask=None) """) +disc(""" +The arguments and return value work as for $drawInlineImage$. However, we use a caching +system; a given image will only be stored the first time it is used, and just referenced +on subsequent use. If you supply a filename, it assumes that the same filename +means the same image. If you supply a PIL image, it tests if the content +has actually changed before re-embedding.""") + +disc(""" +The $mask$ parameter lets you create transparent images. It takes 6 numbers +and defines the range of RGB values which will be masked out or treated as +transparent. For example with [0,2,40,42,136,139], it will mask out any pixels +with a Red value from 0 or 1, Green from 40 or 41 and Blue of 136, 137 or 138 +(on a scale of 0-255). It's currently your job to know which color is the +'transparent' or background one.""") + +disc("""PDF allows for many image features and we will expose more of the over +time, probably with extra keyword arguments to $drawImage$.""") + +heading3("Ending a page") + +eg("""canvas.showPage()""") + +disc("""The $showPage$ method finishes the current page. All additional drawing will +be done on another page.""") + +pencilnote() + +disc("""Warning! All state changes (font changes, color settings, geometry transforms, etcetera) +are FORGOTTEN when you advance to a new page in $pdfgen$. Any state settings you wish to preserve +must be set up again before the program proceeds with drawing!""") + +heading2('The toolbox: the "state change" operations') + +disc(""" +This section briefly lists the ways to switch the tools used by the +program +for painting information onto a page using the $canvas$ interface. +These too will be discussed in detail in later sections. +""") + +heading3("Changing Colors") +eg("""canvas.setFillColorCMYK(c, m, y, k) """) +eg("""canvas.setStrikeColorCMYK(c, m, y, k) """) +eg("""canvas.setFillColorRGB(r, g, b) """) +eg("""canvas.setStrokeColorRGB(r, g, b) """) +eg("""canvas.setFillColor(acolor) """) +eg("""canvas.setStrokeColor(acolor) """) +eg("""canvas.setFillGray(gray) """) +eg("""canvas.setStrokeGray(gray) """) + +disc(""" +PDF supports three different color models: gray level, additive (red/green/blue or RGB), and +subtractive with darkness parameter (cyan/magenta/yellow/darkness or CMYK). +The ReportLab packages also provide named colors such as $lawngreen$. There are two +basic color parameters in the graphics state: the $Fill$ color for the interior of graphic +figures and the $Stroke$ color for the boundary of graphic figures. The above methods +support setting the fill or stroke color using any of the four color specifications. +""") + +heading3("Changing Fonts") +eg("""canvas.setFont(psfontname, size, leading = None) """) + +disc(""" +The $setFont$ method changes the current text font to a given type and size. +The $leading$ parameter specifies the distance down to move when advancing from +one text line to the next. +""") + +heading3("Changing Graphical Line Styles") + +eg("""canvas.setLineWidth(width) """) +eg("""canvas.setLineCap(mode) """) +eg("""canvas.setLineJoin(mode) """) +eg("""canvas.setMiterLimit(limit) """) +eg("""canvas.setDash(self, array=[], phase=0) """) + +disc(""" +Lines drawn in PDF can be presented in a number of graphical styles. +Lines can have different widths, they can end in differing cap styles, +they can meet in different join styles, and they can be continuous or +they can be dotted or dashed. The above methods adjust these various parameters.""") + +heading3("Changing Geometry") + +eg("""canvas.setPageSize(pair) """) +eg("""canvas.transform(a,b,c,d,e,f): """) +eg("""canvas.translate(dx, dy) """) +eg("""canvas.scale(x, y) """) +eg("""canvas.rotate(theta) """) +eg("""canvas.skew(alpha, beta) """) + +disc(""" +All PDF drawings fit into a specified page size. Elements drawn outside of the specified +page size are not visible. Furthermore all drawn elements are passed through an affine +transformation which may adjust their location and/or distort their appearence. The +$setPageSize$ method adjusts the current page size. The $transform$, $translate$, $scale$, +$rotate$, and $skew$ methods add additional transformations to the current transformation. +It is important to remember that these transformations are incremental -- a new +transform modifies the current transform (but does not replace it). +""") + +heading3("State control") + +eg("""canvas.saveState() """) +eg("""canvas.restoreState() """) + +disc(""" +Very often it is important to save the current font, graphics transform, line styles and +other graphics state in order to restore them later. The $saveState$ method marks the +current graphics state for later restoration by a matching $restoreState$. Note that +the save and restore method invokation must match -- a restore call restores the state to +the most recently saved state which hasn't been restored yet. +You cannot save the state on one page and restore +it on the next, however -- no state is preserved between pages.""") + +heading2("Other $canvas$ methods.") + +disc(""" +Not all methods of the $canvas$ object fit into the "tool" or "toolbox" +categories. Below are some of the misfits, included here for completeness. +""") + +eg(""" + canvas.setAuthor() + canvas.addOutlineEntry(title, key, level=0, closed=None) + canvas.setTitle(title) + canvas.setSubject(subj) + canvas.pageHasData() + canvas.showOutline() + canvas.bookmarkPage(name) + canvas.bookmarkHorizontalAbsolute(name, yhorizontal) + canvas.doForm() + canvas.beginForm(name, lowerx=0, lowery=0, upperx=None, uppery=None) + canvas.endForm() + canvas.linkAbsolute(contents, destinationname, Rect=None, addtopage=1, name=None, **kw) + canvas.getPageNumber() + canvas.addLiteral() + canvas.getAvailableFonts() + canvas.stringWidth(self, text, fontName, fontSize, encoding=None) + canvas.setPageCompression(onoff=1) + canvas.setPageTransition(self, effectname=None, duration=1, + direction=0,dimension='H',motion='I') +""") + + +heading2('Coordinates (default user space)') + +disc(""" +By default locations on a page are identified by a pair of numbers. +For example the pair $(4.5*inch, 1*inch)$ identifies the location +found on the page by starting at the lower left corner and moving to +the right 4.5 inches and up one inch. +""") + +disc("""For example, the following function draws +a number of elements on a $canvas$.""") + +eg(examples.testcoords) + +disc("""In the default user space the "origin" ^(0,0)^ point is at the lower +left corner. Executing the $coords$ function in the default user space +(for the "demo minipage") we obtain the following.""") + +illust(examples.coords, 'The Coordinate System') + +heading3("Moving the origin: the $translate$ method") + +disc("""Often it is useful to "move the origin" to a new point off +the lower left corner. The $canvas.translate(^x,y^)$ method moves the origin +for the current page to the point currently identified by ^(x,y)^.""") + +disc("""For example the following translate function first moves +the origin before drawing the same objects as shown above.""") + +eg(examples.testtranslate) + +disc("""This produces the following.""") + +illust(examples.translate, "Moving the origin: the $translate$ method") + + +#illust(NOP) # execute some code + +pencilnote() + + +disc(""" +Note: As illustrated in the example it is perfectly possible to draw objects +or parts of objects "off the page". +In particular a common confusing bug is a translation operation that translates the +entire drawing off the visible area of the page. If a program produces a blank page +it is possible that all the drawn objects are off the page. +""") + +heading3("Shrinking and growing: the scale operation") + +disc("""Another important operation is scaling. The scaling operation $canvas.scale(^dx,dy^)$ +stretches or shrinks the ^x^ and ^y^ dimensions by the ^dx^, ^dy^ factors respectively. Often +^dx^ and ^dy^ are the same -- for example to reduce a drawing by half in all dimensions use +$dx = dy = 0.5$. However for the purposes of illustration we show an example where +$dx$ and $dy$ are different. +""") + +eg(examples.testscale) + +disc("""This produces a "short and fat" reduced version of the previously displayed operations.""") + +illust(examples.scale, "Scaling the coordinate system") + + +#illust(NOP) # execute some code + +pencilnote() + + +disc("""Note: scaling may also move objects or parts of objects off the page, +or may cause objects to "shrink to nothing." """) + +disc("""Scaling and translation can be combined, but the order of the +operations are important.""") + +eg(examples.testscaletranslate) + +disc("""This example function first saves the current $canvas$ state +and then does a $scale$ followed by a $translate$. Afterward the function +restores the state (effectively removing the effects of the scaling and +translation) and then does the same operations in a different order. +Observe the effect below.""") + +illust(examples.scaletranslate, "Scaling and Translating") + + +#illust(NOP) # execute some code + +pencilnote() + + +disc("""Note: scaling shrinks or grows everything including line widths +so using the canvas.scale method to render a microscopic drawing in +scaled microscopic units +may produce a blob (because all line widths will get expanded a huge amount). +Also rendering an aircraft wing in meters scaled to centimeters may cause the lines +to shrink to the point where they disappear. For engineering or scientific purposes +such as these scale and translate +the units externally before rendering them using the canvas.""") + +heading3("Saving and restoring the $canvas$ state: $saveState$ and $restoreState$") + +disc(""" +The $scaletranslate$ function used an important feature of the $canvas$ object: +the ability to save and restore the current parameters of the $canvas$. +By enclosing a sequence of operations in a matching pair of $canvas.saveState()$ +an $canvas.restoreState()$ operations all changes of font, color, line style, +scaling, translation, or other aspects of the $canvas$ graphics state can be +restored to the state at the point of the $saveState()$. Remember that the save/restore +calls must match: a stray save or restore operation may cause unexpected +and undesirable behavior. Also, remember that no $canvas$ state is +preserved across page breaks, and the save/restore mechanism does not work +across page breaks. +""") + +heading3("Mirror image") + +disc(""" +It is interesting although perhaps not terribly useful to note that +scale factors can be negative. For example the following function +""") + +eg(examples.testmirror) + +disc(""" +creates a mirror image of the elements drawn by the $coord$ function. +""") + +illust(examples.mirror, "Mirror Images") + +disc(""" +Notice that the text strings are painted backwards. +""") + +heading2("Colors") + +disc(""" +There are four ways to specify colors in $pdfgen$: by name (using the $color$ +module, by red/green/blue (additive, $RGB$) value, +by cyan/magenta/yellow/darkness (subtractive, $CMYK$), or by gray level. +The $colors$ function below exercises each of the four methods. +""") + +eg(examples.testcolors) + +disc(""" +The $RGB$ or additive color specification follows the way a computer +screen adds different levels of the red, green, or blue light to make +any color, where white is formed by turning all three lights on full +$(1,1,1)$.""") + +disc("""The $CMYK$ or subtractive method follows the way a printer +mixes three pigments (cyan, magenta, and yellow) to form colors. +Because mixing chemicals is more difficult than combining light there +is a fourth parameter for darkness. For example a chemical +combination of the $CMY$ pigments generally never makes a perfect +black -- instead producing a muddy color -- so, to get black printers +don't use the $CMY$ pigments but use a direct black ink. Because +$CMYK$ maps more directly to the way printer hardware works it may +be the case that colors specified in $CMYK$ will provide better fidelity +and better control when printed. +""") + +illust(examples.colors, "Color Models") + +heading2('Painting back to front') + +disc(""" +Objects may be painted over other objects to good effect in $pdfgen$. As +in painting with oils the object painted last will show up on top. For +example, the $spumoni$ function below paints up a base of colors and then +paints a white text over the base. +""") + +eg(examples.testspumoni) + +disc(""" +The word "SPUMONI" is painted in white over the colored rectangles, +with the apparent effect of "removing" the color inside the body of +the word. +""") + +illust(examples.spumoni, "Painting over colors") + +disc(""" +The last letters of the word are not visible because the default $canvas$ +background is white and painting white letters over a white background +leaves no visible effect. +""") + +disc(""" +This method of building up complex paintings in layers can be done +in very many layers in $pdfgen$ -- there are fewer physical limitations +than there are when dealing with physical paints. +""") + +eg(examples.testspumoni2) + +disc(""" +The $spumoni2$ function layers an ice cream cone over the +$spumoni$ drawing. Note that different parts of the cone +and scoops layer over eachother as well. +""") +illust(examples.spumoni2, "building up a drawing in layers") + + +heading2('Standard fonts and text objects') + +disc(""" +Text may be drawn in many different colors, fonts, and sizes in $pdfgen$. +The $textsize$ function demonstrates how to change the color and font and +size of text and how to place text on the page. +""") + +eg(examples.testtextsize) + +disc(""" +The $textsize$ function generates the following page. +""") + +illust(examples.textsize, "text in different fonts and sizes") + +disc(""" +A number of different fonts are always available in $pdfgen$. +""") + +eg(examples.testfonts) + +disc(""" +The $fonts$ function lists the fonts that are always available. +These don't need to be stored in a PDF document, since they +are guaranteed to be present in Acrobat Reader. +""") + +illust(examples.fonts, "the 14 standard fonts") + +disc(""" +For information on how to use arbitrary fonts, see the next chapter. +""") + + +heading2("Text object methods") + +disc(""" +For the dedicated presentation of text in a PDF document, use a text object. +The text object interface provides detailed control of text layout parameters +not available directly at the $canvas$ level. In addition, it results in smaller +PDF that will render faster than many separate calls to the $drawString$ methods. +""") + +eg("""textobject.setTextOrigin(x,y)""") +eg("""textobject.setTextTransform(a,b,c,d,e,f)""") +eg("""textobject.moveCursor(dx, dy) # from start of current LINE""") +eg("""(x,y) = textobject.getCursor()""") +eg("""x = textobject.getX(); y = textobject.getY()""") +eg("""textobject.setFont(psfontname, size, leading = None)""") +eg("""textobject.textOut(text)""") +eg("""textobject.textLine(text='')""") +eg("""textobject.textLines(stuff, trim=1)""") + +disc(""" +The text object methods shown above relate to basic text geometry. +""") + +disc(""" +A text object maintains a text cursor which moves about the page when +text is drawn. For example the $setTextOrigin$ places the cursor +in a known position and the $textLine$ and $textLines$ methods move +the text cursor down past the lines that have been missing. +""") + +eg(examples.testcursormoves1) + +disc(""" +The $cursormoves$ function relies on the automatic +movement of the text cursor for placing text after the origin +has been set. +""") + +illust(examples.cursormoves1, "How the text cursor moves") + +disc(""" +It is also possible to control the movement of the cursor +more explicitly by using the $moveCursor$ method (which moves +the cursor as an offset from the start of the current line +NOT the current cursor, and which also has positive ^y^ offsets +move down (in contrast to the normal geometry where +positive ^y^ usually moves up. +""") + +eg(examples.testcursormoves2) + +disc(""" +Here the $textOut$ does not move the down a line in contrast +to the $textLine$ function which does move down. +""") + +illust(examples.cursormoves2, "How the text cursor moves again") + +heading3("Character Spacing") + +eg("""textobject.setCharSpace(charSpace)""") + +disc("""The $setCharSpace$ method adjusts one of the parameters of text -- the inter-character +spacing.""") + +eg(examples.testcharspace) + +disc("""The +$charspace$ function exercises various spacing settings. +It produces the following page.""") + +illust(examples.charspace, "Adjusting inter-character spacing") + +heading3("Word Spacing") + +eg("""textobject.setWordSpace(wordSpace)""") + +disc("The $setWordSpace$ method adjusts the space between words.") + +eg(examples.testwordspace) + +disc("""The $wordspace$ function shows what various word space settings +look like below.""") + +illust(examples.wordspace, "Adjusting word spacing") + +heading3("Horizontal Scaling") + +eg("""textobject.setHorizScale(horizScale)""") + +disc("""Lines of text can be stretched or shrunken horizontally by the +$setHorizScale$ method.""") + +eg(examples.testhorizontalscale) + +disc("""The horizontal scaling parameter ^horizScale^ +is given in percentages (with 100 as the default), so the 80 setting +shown below looks skinny. +""") +illust(examples.horizontalscale, "adjusting horizontal text scaling") + +heading3("Interline spacing (Leading)") + +eg("""textobject.setLeading(leading)""") + +disc("""The vertical offset between the point at which one +line starts and where the next starts is called the leading +offset. The $setLeading$ method adjusts the leading offset. +""") + +eg(examples.testleading) + +disc("""As shown below if the leading offset is set too small +characters of one line my write over the bottom parts of characters +in the previous line.""") + +illust(examples.leading, "adjusting the leading") + +heading3("Other text object methods") + +eg("""textobject.setTextRenderMode(mode)""") + +disc("""The $setTextRenderMode$ method allows text to be used +as a forground for clipping background drawings, for example.""") + +eg("""textobject.setRise(rise)""") + +disc(""" +The $setRise$ method raises or lowers text on the line +(for creating superscripts or subscripts, for example). +""") + +eg("""textobject.setFillColor(aColor); +textobject.setStrokeColor(self, aColor) +# and similar""") + +disc(""" +These color change operations change the color of the text and are otherwise +similar to the color methods for the $canvas$ object.""") + +heading2('Paths and Lines') + +disc("""Just as textobjects are designed for the dedicated presentation +of text, path objects are designed for the dedicated construction of +graphical figures. When path objects are drawn onto a $canvas$ they +are drawn as one figure (like a rectangle) and the mode of drawing +for the entire figure can be adjusted: the lines of the figure can +be drawn (stroked) or not; the interior of the figure can be filled or +not; and so forth.""") + +disc(""" +For example the $star$ function uses a path object +to draw a star +""") + +eg(examples.teststar) + +disc(""" +The $star$ function has been designed to be useful in illustrating +various line style parameters supported by $pdfgen$. +""") + +illust(examples.star, "line style parameters") + +heading3("Line join settings") + +disc(""" +The $setLineJoin$ method can adjust whether line segments meet in a point +a square or a rounded vertex. +""") + +eg(examples.testjoins) + +disc(""" +The line join setting is only really of interest for thick lines because +it cannot be seen clearly for thin lines. +""") + +illust(examples.joins, "different line join styles") + +heading3("Line cap settings") + +disc("""The line cap setting, adjusted using the $setLineCap$ method, +determines whether a terminating line +ends in a square exactly at the vertex, a square over the vertex +or a half circle over the vertex. +""") + +eg(examples.testcaps) + +disc("""The line cap setting, like the line join setting, is only clearly +visible when the lines are thick.""") + +illust(examples.caps, "line cap settings") + +heading3("Dashes and broken lines") + +disc(""" +The $setDash$ method allows lines to be broken into dots or dashes. +""") + +eg(examples.testdashes) + +disc(""" +The patterns for the dashes or dots can be in a simple on/off repeating pattern +or they can be specified in a complex repeating pattern. +""") + +illust(examples.dashes, "some dash patterns") + +heading3("Creating complex figures with path objects") + +disc(""" +Combinations of lines, curves, arcs and other figures +can be combined into a single figure using path objects. +For example the function shown below constructs two path +objects using lines and curves. +This function will be used later on as part of a +pencil icon construction. +""") + +eg(examples.testpenciltip) + +disc(""" +Note that the interior of the pencil tip is filled +as one object even though it is constructed from +several lines and curves. The pencil lead is then +drawn over it using a new path object. +""") + +illust(examples.penciltip, "a pencil tip") + +heading2('Rectangles, circles, ellipses') + +disc(""" +The $pdfgen$ module supports a number of generally useful shapes +such as rectangles, rounded rectangles, ellipses, and circles. +Each of these figures can be used in path objects or can be drawn +directly on a $canvas$. For example the $pencil$ function below +draws a pencil icon using rectangles and rounded rectangles with +various fill colors and a few other annotations. +""") + +eg(examples.testpencil) + +pencilnote() + +disc(""" +Note that this function is used to create the "margin pencil" to the left. +Also note that the order in which the elements are drawn are important +because, for example, the white rectangles "erase" parts of a black rectangle +and the "tip" paints over part of the yellow rectangle. +""") + +illust(examples.pencil, "a whole pencil") + +heading2('Bezier curves') + +disc(""" +Programs that wish to construct figures with curving borders +generally use Bezier curves to form the borders. +""") + +eg(examples.testbezier) + +disc(""" +A Bezier curve is specified by four control points +$(x1,y1)$, $(x2,y2)$, $(x3,y3)$, $(x4,y4)$. +The curve starts at $(x1,y1)$ and ends at $(x4,y4)$ +and the line segment from $(x1,y1)$ to $(x2,y2)$ +and the line segment from $(x3,y3)$ to $(x4,y4)$ +both form tangents to the curve. Furthermore the +curve is entirely contained in the convex figure with vertices +at the control points. +""") + +illust(examples.bezier, "basic bezier curves") + +disc(""" +The drawing above (the output of $testbezier$) shows +a bezier curves, the tangent lines defined by the control points +and the convex figure with vertices at the control points. +""") + +heading3("Smoothly joining bezier curve sequences") + +disc(""" +It is often useful to join several bezier curves to form a +single smooth curve. To construct a larger smooth curve from +several bezier curves make sure that the tangent lines to adjacent +bezier curves that join at a control point lie on the same line. +""") + +eg(examples.testbezier2) + +disc(""" +The figure created by $testbezier2$ describes a smooth +complex curve because adjacent tangent lines "line up" as +illustrated below. +""") + +illust(examples.bezier2, "bezier curves") + +heading2("Path object methods") + +disc(""" +Path objects build complex graphical figures by setting +the "pen" or "brush" at a start point on the canvas and drawing +lines or curves to additional points on the canvas. Most operations +apply paint on the canvas starting at the end point of the last +operation and leave the brush at a new end point. +""") + +eg("""pathobject.moveTo(x,y)""") + +disc(""" +The $moveTo$ method lifts the brush (ending any current sequence +of lines or curves if there is one) and replaces the brush at the +new ^(x,y)^ location on the canvas to start a new path sequence. +""") + +eg("""pathobject.lineTo(x,y)""") + +disc(""" +The $lineTo$ method paints straight line segment from the current brush +location to the new ^(x,y)^ location. +""") + +eg("""pathobject.curveTo(x1, y1, x2, y2, x3, y3) """) + +disc(""" +The $curveTo$ method starts painting a Bezier curve beginning at +the current brush location, using ^(x1,y1)^, ^(x2,y2)^, and ^(x3,y3)^ +as the other three control points, leaving the brush on ^(x3,y3)^. +""") + +eg("""pathobject.arc(x1,y1, x2,y2, startAng=0, extent=90) """) + +eg("""pathobject.arcTo(x1,y1, x2,y2, startAng=0, extent=90) """) + +disc(""" +The $arc$ and $arcTo$ methods paint partial ellipses. The $arc$ method first "lifts the brush" +and starts a new shape sequence. The $arcTo$ method joins the start of +the partial ellipse to the current +shape sequence by line segment before drawing the partial ellipse. The points +^(x1,y1)^ and ^(x2,y2)^ define opposite corner points of a rectangle enclosing +the ellipse. The $startAng$ is an angle (in degrees) specifying where to begin +the partial ellipse where the 0 angle is the midpoint of the right border of the enclosing +rectangle (when ^(x1,y1)^ is the lower left corner and ^(x2,y2)^ is the upper +right corner). The $extent$ is the angle in degrees to traverse on the ellipse. +""") + +eg(examples.testarcs) + +disc("""The $arcs$ function above exercises the two partial ellipse methods. +It produces the following drawing.""") + +illust(examples.arcs, "arcs in path objects") + +eg("""pathobject.rect(x, y, width, height) """) + +disc("""The $rect$ method draws a rectangle with lower left corner +at ^(x,y)^ of the specified ^width^ and ^height^.""") + +eg("""pathobject.ellipse(x, y, width, height)""") + +disc("""The $ellipse$ method +draws an ellipse enclosed in the rectange with lower left corner +at ^(x,y)^ of the specified ^width^ and ^height^. +""") + +eg("""pathobject.circle(x_cen, y_cen, r) """) + +disc("""The $circle$ method +draws a circle centered at ^(x_cen, y_cen)^ with radius ^r^. +""") + +eg(examples.testvariousshapes) + +disc(""" +The $variousshapes$ function above shows a rectangle, circle and ellipse +placed in a frame of reference grid. +""") + +illust(examples.variousshapes, "rectangles, circles, ellipses in path objects") + +eg("""pathobject.close() """) + +disc(""" +The $close$ method closes the current graphical figure +by painting a line segment from the last point of the figure +to the starting point of the figure (the the most +recent point where the brush was placed on the paper by $moveTo$ +or $arc$ or other placement operations). +""") + +eg(examples.testclosingfigures) + +disc(""" +The $closingfigures$ function illustrates the +effect of closing or not closing figures including a line +segment and a partial ellipse. +""") + +illust(examples.closingfigures, "closing and not closing pathobject figures") + +disc(""" +Closing or not closing graphical figures effects only the stroked outline +of a figure, not the filling of the figure as illustrated above. +""") + + +disc(""" +For a more extensive example of drawing using a path object +examine the $hand$ function. +""") + +eg(examples.testhand) + +disc(""" +In debug mode (the default) the $hand$ function shows the tangent line segments +to the bezier curves used to compose the figure. Note that where the segments +line up the curves join smoothly, but where they do not line up the curves show +a "sharp edge". +""") + +illust(examples.hand, "an outline of a hand using bezier curves") + +disc(""" +Used in non-debug mode the $hand$ function only shows the +Bezier curves. With the $fill$ parameter set the figure is +filled using the current fill color. +""") + +eg(examples.testhand2) + +disc(""" +Note that the "stroking" of the border draws over the interior fill where +they overlap. +""") + +illust(examples.hand2, "the finished hand, filled") + + + +heading2("Further Reading: The ReportLab Graphics Library") + +disc(""" +So far the graphics we have seen was created on a fairly low level. +It should be noted, though, that there is another way of creating +much more sophisticated graphics using the emerging dedicated +high-level ReportLab Graphics Library. +""") + +disc(""" +It can be used to produce high-quality, platform-independant, +reusable graphics for different output formats (vector and bitmap) +like PDF, EPS and soon others like SVG. +""") + +disc(""" +A thorough description of its philsophy and features is beyond the +scope of this general user guide and the reader is recommended to +continue with the "ReportLab Graphics Guide". +There she will find information about the existing components and +how to create customized ones. +""") + +disc(""" +Also, the graphics guide contains a presentation of an emerging +charting package and its components (labels, axes, legends and +different types of charts like bar, line and pie charts) that +builds directly on the graphics library. +""") + + +##### FILL THEM IN diff --git a/bin/reportlab/docs/userguide/ch2a_fonts.py b/bin/reportlab/docs/userguide/ch2a_fonts.py new file mode 100644 index 00000000000..460305d42a1 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch2a_fonts.py @@ -0,0 +1,513 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch2a_fonts.py +from reportlab.tools.docco.rl_doc_utils import * +from reportlab.lib.codecharts import SingleByteEncodingChart +from reportlab.platypus import Image +import reportlab + +heading1("Fonts and encodings") + +disc(""" +This chapter covers fonts, encodings and Asian language capabilities. +If you are purely concerned with generating PDFs for Western +European languages, you can just read the "Unicode is the default" section +below and skip the rest on a first reading. +We expect this section to grow considerably over time. We +hope that Open Source will enable us to give better support for +more of the world's languages than other tools, and we welcome +feedback and help in this area. +""") + +heading2("Unicode and UTF8 are the default input encodings") + +disc(""" +Starting with reportlab Version 2.0 (May 2006), all text input you +provide to our APIs should be in UTF8 or as Python Unicode objects. +This applies to arguments to canvas.drawString and related APIs, +table cell content, drawing object parameters, and paragraph source +text. +""") + + +disc(""" +We considered making the input encoding configurable or even locale-dependent, +but decided that "explicit is better than implicit".""") + +disc(""" +This simplifies many things we used to do previously regarding greek +letters, symbols and so on. To display any character, find out its +unicode code point, and make sure the font you are using is able +to display it.""") + +disc(""" +If you are adapting a ReportLab 1.x application, or reading data from +another source which contains single-byte data (e.g. latin-1 or WinAnsi), +you need to do a conversion into Unicode. The Python codecs package now +includes converters for all the common encodings, including Asian ones. +""") + + + +disc(u""" +If your data is not encoded as UTF8, you will get a UnicodeDecodeError as +soon as you feed in a non-ASCII character. For example, this snippet below is +attempting to read in and print a series of names, including one with a French +accent: ^Marc-Andr\u00e9 Lemburg^. The standard error is quite helpful and tells you +what character it doesn't like: +""") + +eg(u""" +>>> from reportlab.pdfgen.canvas import Canvas +>>> c = Canvas('temp.pdf') +>>> y = 700 +>>> for line in file('latin_python_gurus.txt','r'): +... c.drawString(100, y, line.strip()) +... +Traceback (most recent call last): +... +UnicodeDecodeError: 'utf8' codec can't decode bytes in position 9-11: invalid data +-->\u00e9 L<--emburg +>>> +""") + + +disc(""" +The simplest fix is just to convert your data to unicode, saying which encoding +it comes from, like this:""") + +eg(""" +>>> for line in file('latin_input.txt','r'): +... uniLine = unicode(line, 'latin-1') +... c.drawString(100, y, uniLine.strip()) +>>> +>>> c.save() +""") + + +heading2("Changing the built-in fonts output encoding") + +disc(""" +There are still a number of places in the code, including the rl_config +defaultEncoding parameter, and arguments passed to various Font constructors. +These generally relate to the OUTPUT encoding used when we write data in the font +file. This affects which characters are actually available in the font if +you are using Type 1 fonts, since only 256 glyphs can be available at +one time. Unless you have a very specific need for +MacRoman or MacExpert encoding characters, we advise you to ignore +this. By default the standard fonts (Helvetica, Courier, Times Roman) +will offer the glyphs available in Latin-1. If you try to print a non-Latin-1 +character using the built-in Helvetica, you'll see a rectangle or blob. + +""") + + +heading2("Using non-standard Type 1 fonts") + +disc(""" +As discussed in the previous chapter, every copy of Acrobat Reader +comes with 14 standard fonts built in. Therefore, the ReportLab +PDF Library only needs to refer to these by name. If you want +to use other fonts, they must be available to your code and +will be embedded in the PDF document.""") + +disc(""" +You can use the mechanism described below to include arbitrary +fonts in your documents. Just van Rossum has kindly donated a Type 1 +font named LettErrorRobot-Chrome which we may +use for testing and/or documenting purposes (and which you may +use as well). It comes bundled with the ReportLab distribution in the +directory $reportlab/fonts$. +""") + +disc(""" +Right now font-embedding relies on font description files in the Adobe +AFM ('Adobe Font Metrics') and PFB ('Printer Font Binary') format. The +former is an ASCII file and contains information about the characters +('glyphs') in the font such as height, width, bounding box info and +other 'metrics', while the latter is a binary file that describes the +shapes of the font. The $reportlab/fonts$ directory contains the files +$'LeERC___.AFM'$ and $'LeERC___.PFB'$ that are used as an example +font. +""") + +disc(""" +In the following example locate the folder containing the test font and +register it for future use with the $pdfmetrics$ module, +after which we can use it like any other standard font. +""") + + +eg(""" +import os +import reportlab +folder = os.path.dirname(reportlab.__file__) + os.sep + 'fonts' +afmFile = os.path.join(folder, 'LeERC___.AFM') +pfbFile = os.path.join(folder, 'LeERC___.PFB') + +from reportlab.pdfbase import pdfmetrics +justFace = pdfmetrics.EmbeddedType1Face(afmFile, pfbFile) +faceName = 'LettErrorRobot-Chrome' # pulled from AFM file +pdfmetrics.registerTypeFace(justFace) +justFont = pdfmetrics.Font('LettErrorRobot-Chrome', + faceName, + 'WinAnsiEncoding') +pdfmetrics.registerFont(justFont) + +canvas.setFont('LettErrorRobot-Chrome', 32) +canvas.drawString(10, 150, 'This should be in') +canvas.drawString(10, 100, 'LettErrorRobot-Chrome') +""") + + +disc(""" +Note that the argument "WinAnsiEncoding" has nothing to do with the input; +it's to say which set of characters within the font file will be active +and available. +""") + +illust(examples.customfont1, "Using a very non-standard font") + +disc(""" +The font's facename comes from the AFM file's $FontName$ field. +In the example above we knew the name in advance, but quite +often the names of font description files are pretty cryptic +and then you might want to retrieve the name from an AFM file +automatically. +When lacking a more sophisticated method you can use some +code as simple as this: +""") + +eg(""" +class FontNameNotFoundError(Exception): + pass + + +def findFontName(path): + "Extract a font name from an AFM file." + + f = open(path) + + found = 0 + while not found: + line = f.readline()[:-1] + if not found and line[:16] == 'StartCharMetrics': + raise FontNameNotFoundError, path + if line[:8] == 'FontName': + fontName = line[9:] + found = 1 + + return fontName +""") + +disc(""" +In the LettErrorRobot-Chrome example we explicitely specified +the place of the font description files to be loaded. +In general, you'll prefer to store your fonts in some canonic +locations and make the embedding mechanism aware of them. +Using the same configuration mechanism we've already seen at the +beginning of this section we can indicate a default search path +for Type-1 fonts. +""") + +disc(""" +Unfortunately, there is no reliable standard yet for such +locations (not even on the same platform) and, hence, you might +have to edit the file $reportlab/rl_config.py$ to modify the +value of the $T1SearchPath$ identifier to contain additional +directories. Our own recommendation is to use the ^reportlab/fonts^ +folder in development; and to have any needed fonts as packaged parts of +your application in any kind of controlled server deployment. This insulates +you from fonts being installed and uninstalled by other software or system +administrator. +""") + +heading3("Warnings about missing glyphs") +disc("""If you specify an encoding, it is generally assumed that +the font designer has provided all the needed glyphs. However, +this is not always true. In the case of our example font, +the letters of the alphabet are present, but many symbols and +accents are missing. The default behaviour is for the font to +print a 'notdef' character - typically a blob, dot or space - +when passed a character it cannot draw. However, you can ask +the library to warn you instead; the code below (executed +before loading a font) will cause warnings to be generated +for any glyphs not in the font when you register it.""") + +eg(""" +import reportlab.rl_config +reportlab.rl_config.warnOnMissingFontGlyphs = 0 +""") + + + +heading2("Standard Single-Byte Font Encodings") +disc(""" +This section shows you the glyphs available in the common encodings. +""") + + +disc("""The code chart below shows the characters in the $WinAnsiEncoding$. +This is the standard encoding on Windows and many Unix systems in America +and Western Europe. It is also knows as Code Page 1252, and is practically +identical to ISO-Latin-1 (it contains one or two extra characters). This +is the default encoding used by the Reportlab PDF Library. It was generated from +a standard routine in $reportlab/lib$, $codecharts.py$, +which can be used to display the contents of fonts. The index numbers +along the edges are in hex.""") + +cht1 = SingleByteEncodingChart(encodingName='WinAnsiEncoding',charsPerRow=32, boxSize=12) +illust(lambda canv: cht1.drawOn(canv, 0, 0), "WinAnsi Encoding", cht1.width, cht1.height) + +disc("""The code chart below shows the characters in the $MacRomanEncoding$. +as it sounds, this is the standard encoding on Macintosh computers in +America and Western Europe. As usual with non-unicode encodings, the first +128 code points (top 4 rows in this case) are the ASCII standard and agree +with the WinAnsi code chart above; but the bottom 4 rows differ.""") +cht2 = SingleByteEncodingChart(encodingName='MacRomanEncoding',charsPerRow=32, boxSize=12) +illust(lambda canv: cht2.drawOn(canv, 0, 0), "MacRoman Encoding", cht2.width, cht2.height) + +disc("""These two encodings are available for the standard fonts (Helvetica, +Times-Roman and Courier and their variants) and will be available for most +commercial fonts including those from Adobe. However, some fonts contain non- +text glyphs and the concept does not really apply. For example, ZapfDingbats +and Symbol can each be treated as having their own encoding.""") + +cht3 = SingleByteEncodingChart(faceName='ZapfDingbats',encodingName='ZapfDingbatsEncoding',charsPerRow=32, boxSize=12) +illust(lambda canv: cht3.drawOn(canv, 0, 0), "ZapfDingbats and its one and only encoding", cht3.width, cht3.height) + +cht4 = SingleByteEncodingChart(faceName='Symbol',encodingName='SymbolEncoding',charsPerRow=32, boxSize=12) +illust(lambda canv: cht4.drawOn(canv, 0, 0), "Symbol and its one and only encoding", cht4.width, cht4.height) + + +CPage(5) +heading2("TrueType Font Support") +disc(""" +Marius Gedminas ($mgedmin@delfi.lt$) with the help of Viktorija Zaksiene ($vika@pov.lt$) +have contributed support for embedded TrueType fonts. TrueType fonts work in Unicode/UTF8 +and are not limited to 256 characters.""") + + +CPage(3) +disc("""We use $reportlab.pdfbase.ttfonts.TTFont$ to create a true type +font object and register using $reportlab.pdfbase.pdfmetrics.registerFont$. +In pdfgen drawing directly to the canvas we can do""") +eg(""" +# we know some glyphs are missing, suppress warnings +import reportlab.rl_config +reportlab.rl_config.warnOnMissingFontGlyphs = 0 + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +pdfmetrics.registerFont(TTFont('Rina', 'rina.ttf')) +canvas.setFont(Rina, 32) +canvas.drawString(10, 150, "Some text encoded in UTF-8") +canvas.drawString(10, 100, "In the Rina TT Font!") +""") +illust(examples.ttffont1, "Using a the Rina TrueType Font") +disc("""In the above example the true type font object is created using""") +eg(""" + TTFont(name,filename) +""") +disc("""so that the ReportLab internal name is given by the first argument and the second argument +is a string(or file like object) denoting the font's TTF file. In Marius' original patch the filename +was supposed to be exactly correct, but we have modified things so that if the filename is relative +then a search for the corresponding file is done in the current directory and then in directories +specified by $reportlab.rl_config.TTFSearchpath$!""") + +from reportlab.lib.styles import ParagraphStyle + +from reportlab.lib.fonts import addMapping +addMapping('Rina', 0, 0, 'Rina') +addMapping('Rina', 0, 1, 'Rina') +addMapping('Rina', 1, 0, 'Rina') +addMapping('Rina', 1, 1, 'Rina') + +disc("""Before using the TT Fonts in Platypus we should add a mapping from the family name to the +individual font names that describe the behaviour under the $$ and $$ attributes.""") + +eg(""" +from reportlab.lib.fonts import addMapping +addMapping('Rina', 0, 0, 'Rina') #normal +addMapping('Rina', 0, 1, 'Rina') #italic +addMapping('Rina', 1, 0, 'Rina') #bold +addMapping('Rina', 1, 1, 'Rina') #italic and bold +""") + +disc("""We only have a Rina regular font, no bold or italic, so we must map all to the +same internal fontname. ^<b>^ and ^<i>^ tags may now be used safely, but +have no effect. +After registering and mapping +the Rina font as above we can use paragraph text like""") +parabox2("""This is in Times-Roman +and this is in magenta Rina!""","Using TTF fonts in paragraphs") + + + + +heading2("Asian Font Support") +disc("""The Reportlab PDF Library aims to expose full support for Asian fonts. +PDF is the first really portable solution for Asian text handling. There are +two main approaches for this: Adobe's Asian Language Packs, or TrueType fonts. +""") + +heading3("Asian Language Packs") +disc(""" +This approach offers the best performance since nothing needs embedding in the PDF file; +as with the standard fonts, everything is on the reader.""") + +disc(""" +Adobe makes available add-ons for each main language. In Adobe Reader 6.0 and 7.0, you +will be prompted to download and install these as soon as you try to open a document +using them. In earlier versions, you would see an error message on opening an Asian document +and had to know what to do. +""") + +disc(""" +Japanese, Traditional Chinese (Taiwan/Hong Kong), Simplified Chinese (mainland China) +and Korean are all supported and our software knows about the following fonts: +""") +bullet(""" +$chs$ = Chinese Simplified (mainland): '$STSong-Light$' +""") +bullet(""" +$cht$ = Chinese Traditional (Taiwan): '$MSung-Light$', '$MHei-Medium$' +""") +bullet(""" +$kor$ = Korean: '$HYSMyeongJoStd-Medium$','$HYGothic-Medium$' +""") +bullet(""" +$jpn$ = Japanese: '$HeiseiMin-W3$', '$HeiseiKakuGo-W5$' +""") + + +disc("""Since many users will not have the font packs installed, we have included +a rather grainy ^bitmap^ of some Japanese characters. We will discuss below what is needed to +generate them.""") +# include a bitmap of some Asian text +I=os.path.join(os.path.dirname(reportlab.__file__),'docs','images','jpnchars.jpg') +try: + getStory().append(Image(I)) +except: + disc("""An image should have appeared here.""") + +disc("""Prior to Version 2.0, you had to specify one of many native encodings +when registering a CID Font. In version 2.0 you should a new UnicodeCIDFont +class.""") + +eg(""" +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.cidfonts import UnicodeCIDFont +pdfmetrics.registerFont(UnicodeCIDFont('HeiseiMin-W3')) +canvas.setFont('HeiseiMin-W3', 16) + +# the two unicode characters below are "Tokyo" +msg = u'\u6771\u4EAC : Unicode font, unicode input' +canvas.drawString(100, 675, msg) +""") +#had to double-escape the slashes above to get escapes into the PDF + +disc("""The old coding style with explicit encodings should still work, but is now +only relevant if you need to construct vertical text. We aim to add more readable options +for horizontal and vertical text to the UnicodeCIDFont constructor in future. +The following four test scripts generate samples in the corresponding languages:""") +eg("""reportlab/test/test_multibyte_jpn.py +reportlab/test/test_multibyte_kor.py +reportlab/test/test_multibyte_chs.py +reportlab/test/test_multibyte_cht.py""") + +## put back in when we have vertical text... +##disc("""The illustration below shows part of the first page +##of the Japanese output sample. It shows both horizontal and vertical +##writing, and illustrates the ability to mix variable-width Latin +##characters in Asian sentences. The choice of horizontal and vertical +##writing is determined by the encoding, which ends in 'H' or 'V'. +##Whether an encoding uses fixed-width or variable-width versions +##of Latin characters also depends on the encoding used; see the definitions +##below.""") +## +##Illustration(image("../images/jpn.gif", width=531*0.50, +##height=435*0.50), 'Output from test_multibyte_jpn.py') +## +##caption(""" +##Output from test_multibyte_jpn.py +##""") + + + + +disc("""In previous versions of the ReportLab PDF Library, we had to make +use of Adobe's CMap files (located near Acrobat Reader if the Asian Language +packs were installed). Now that we only have one encoding to deal with, the +character width data is embedded in the package, and CMap files are not needed +for generation. The CMap search path in ^rl_config.py^ is now deprecated +and has no effect if you restrict yourself to UnicodeCIDFont. +""") + + +heading3("TrueType fonts with Asian characters") +disc(""" +This is the easy way to do it. No special handling at all is needed to +work with Asian TrueType fonts. Windows users who have installed, for example, +Japanese as an option in Control Panel, will have a font "msmincho.ttf" which +can be used. However, be aware that it takes time to parse the fonts, and that +quite large subsets may need to be embedded in your PDFs. We can also now parse +files ending in .ttc, which are a slight variation of .ttf. + +""") + + +heading3("To Do") +disc("""We expect to be developing this area of the package for some time.accept2dyear +Here is an outline of the main priorities. We welcome help!""") + +bullet(""" +Ensure that we have accurate character metrics for all encodings in horizontal and +vertical writing.""") + +bullet(""" +Add options to ^UnicodeCIDFont^ to allow vertical and proportional variants where the font permits it.""") + + +bullet(""" +Improve the word wrapping code in paragraphs and allow vertical writing.""") + + + +CPage(5) +heading2("RenderPM tests") + +disc("""This may also be the best place to mention the test function of $reportlab/graphics/renderPM.py$, +which can be considered the cannonical place for tests which exercise renderPM (the "PixMap Renderer", +as opposed to renderPDF, renderPS or renderSVG).""") + +disc("""If you run this from the command line, you should see lots of output like the following.""") + +eg("""C:\\code\\reportlab\\graphics>renderPM.py +wrote pmout\\renderPM0.gif +wrote pmout\\renderPM0.tif +wrote pmout\\renderPM0.png +wrote pmout\\renderPM0.jpg +wrote pmout\\renderPM0.pct +... +wrote pmout\\renderPM12.gif +wrote pmout\\renderPM12.tif +wrote pmout\\renderPM12.png +wrote pmout\\renderPM12.jpg +wrote pmout\\renderPM12.pct +wrote pmout\\index.html""") + +disc("""This runs a number of tests progressing from a "Hello World" test, through various tests of +Lines; text strings in a number of sizes, fonts, colours and alignments; the basic shapes; translated +and rotated groups; scaled coordinates; rotated strings; nested groups; anchoring and non-standard fonts.""") + +disc("""It creates a subdirectory called $pmout$, writes the image files into it, and writes an +$index.html$ page which makes it easy to refer to all the results.""") + +disc("""The font-related tests which you may wish to look at are test #11 ('Text strings in a non-standard font') +and test #12 ('Test Various Fonts').""") + + + + +##### FILL THEM IN diff --git a/bin/reportlab/docs/userguide/ch3_pdffeatures.py b/bin/reportlab/docs/userguide/ch3_pdffeatures.py new file mode 100644 index 00000000000..36fe8b8be07 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch3_pdffeatures.py @@ -0,0 +1,271 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch3_pdffeatures.py +from reportlab.tools.docco.rl_doc_utils import * + + +heading1("Exposing PDF Special Capabilities") +disc("""PDF provides a number of features to make electronic + document viewing more efficient and comfortable, and + our library exposes a number of these.""") + +heading2("Forms") +disc("""The Form feature lets you create a block of graphics and text + once near the start of a PDF file, and then simply refer to it on + subsequent pages. If you are dealing with a run of 5000 repetitive + business forms - for example, one-page invoices or payslips - you + only need to store the backdrop once and simply draw the changing + text on each page. Used correctly, forms can dramatically cut + file size and production time, and apparently even speed things + up on the printer. + """) +disc("""Forms do not need to refer to a whole page; anything which + might be repeated often should be placed in a form.""") +disc("""The example below shows the basic sequence used. A real + program would probably define the forms up front and refer to + them from another location.""") + + +eg(examples.testforms) + +heading2("Links and Destinations") +disc("""PDF supports internal hyperlinks. There is a very wide + range of link types, destination types and events which + can be triggered by a click. At the moment we just + support the basic ability to jump from one part of a document + to another, and to control the zoom level of the window after + the jump. The bookmarkPage method defines a destination that + is the endpoint of a jump.""") +#todo("code example here...") + +eg(""" + canvas.bookmarkPage(name, + fitType="Fit", + left=None, + top=None, + bottom=None, + right=None, + zoom=None + ) +""") +disc(""" +By default the $bookmarkPage$ method defines the page itself as the +destination. After jumping to an endpoint defined by bookmarkPage, +the PDF browser will display the whole page, scaling it to fit the +screen:""") + +eg("""canvas.bookmarkPage(name)""") + +disc("""The $bookmarkPage$ method can be instructed to display the +page in a number of different ways by providing a $fitType$ +parameter.""") + +eg("") + +t = Table([ + ['fitType','Parameters Required','Meaning'], + ['Fit',None,'Entire page fits in window (the default)'], + ['FitH','top','Top coord at top of window, width scaled to fit'], + ['FitV','left','Left coord at left of window, height scaled to fit'], + ['FitR','left bottom right top','Scale window to fit the specified rectangle'], + ['XYZ','left top zoom','Fine grained control. If you omit a parameter\nthe PDF browser interprets it as "leave as is"'] + ]) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,1),'Times-Bold',10,12), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) + +getStory().append(t) +caption("""Table - Required attributes for different fit types""") + +disc(""" +Note : $fitType$ settings are case-sensitive so $fitType="FIT"$ is invalid$ +""") + + +disc(""" +Sometimes you want the destination of a jump to be some part of a page. +The $FitR$ fitType allows you to identify a particular rectangle, scaling +the area to fit the entire page. +""") + +disc(""" +To set the display to a particular x and y coordinate of the page and to +control the zoom directly use fitType="XYZ". +""") + +eg(""" +canvas.bookmarkPage('my_bookmark',fitType="XYZ",left=0,top=200) +""") + + + +disc(""" +This destination is at the leftmost of the page with the top of the screen +at position 200. Because $zoom$ was not set the zoom remains at whatever the +user had it set to. +""") + +eg(""" +canvas.bookmarkPage('my_bookmark',fitType="XYZ",left=0,top=200,zoom=2) +""") + +disc("""This time zoom is set to expand the page 2X its normal size.""") + +disc(""" +Note : Both $XYZ$ and $FitR$ fitTypes require that their positional parameters +($top, bottom, left, right$) be specified in terms of the default user space. +They ignore any geometric transform in effect in the canvas graphic state. +""") + + + +pencilnote() + +disc(""" +Note: Two previous bookmark methods are supported but deprecated now +that bookmarkPage is so general. These are $bookmarkHorizontalAbsolute$ +and $bookmarkHorizontal$. +""") + +heading3("Defining internal links") +eg(""" + canvas.linkAbsolute(contents, destinationname, Rect=None, addtopage=1, name=None, **kw) + """) + +disc(""" + The $linkAbsolute$ method defines a starting point for a jump. When the user + is browsing the generated document using a dynamic viewer (such as Acrobat Reader) + when the mouse is clicked when the pointer is within the rectangle specified + by $Rect$ the viewer will jump to the endpoint associated with $destinationname$. + As in the case with $bookmarkHorizontalAbsolute$ the rectangle $Rect$ must be + specified in terms of the default user space. The $contents$ parameter specifies + a chunk of text which displays in the viewer if the user left-clicks on the region. +""") + +disc(""" +The rectangle $Rect$ must be specified in terms of a tuple ^(x1,y1,x2,y2)^ identifying +the lower left and upper right points of the rectangle in default user space. +""") + +disc(""" +For example the code +""") + +eg(""" + canvas.bookmarkPage("Meaning_of_life") +""") + +disc(""" +defines a location as the whole of the current page with the identifier +$Meaning_of_life$. To create a rectangular link to it while drawing a possibly +different page, we would use this code: +""") + +eg(""" + canvas.linkAbsolute("Find the Meaning of Life", "Meaning_of_life", + (inch, inch, 6*inch, 2*inch)) +""") + +disc(""" +By default during interactive viewing a rectangle appears around the +link. Use the keyword argument $Border='[0 0 0]'$ to +suppress the visible rectangle around the during viewing link. +For example +""") + +eg(""" + canvas.linkAbsolute("Meaning of Life", "Meaning_of_life", + (inch, inch, 6*inch, 2*inch), Border='[0 0 0]') +""") + + +heading2("Outline Trees") +disc("""Acrobat Reader has a navigation page which can hold a + document outline; it should normally be visible when you + open this guide. We provide some simple methods to add + outline entries. Typically, a program to make a document + (such as this user guide) will call the method + $canvas.addOutlineEntry(^self, title, key, level=0, + closed=None^)$ as it reaches each heading in the document. + """) + +disc("""^title^ is the caption which will be displayed in + the left pane. The ^key^ must be a string which is + unique within the document and which names a bookmark, + as with the hyperlinks. The ^level^ is zero - the + uppermost level - unless otherwise specified, and + it is an error to go down more than one level at a time + (for example to follow a level 0 heading by a level 2 + heading). Finally, the ^closed^ argument specifies + whether the node in the outline pane is closed + or opened by default.""") + +disc("""The snippet below is taken from the document template + that formats this user guide. A central processor looks + at each paragraph in turn, and makes a new outline entry + when a new chapter occurs, taking the chapter heading text + as the caption text. The key is obtained from the + chapter number (not shown here), so Chapter 2 has the + key 'ch2'. The bookmark to which the + outline entry points aims at the whole page, but it could + as easily have been an individual paragraph. + """) + +eg(""" +#abridged code from our document template +if paragraph.style == 'Heading1': + self.chapter = paragraph.getPlainText() + key = 'ch%d' % self.chapterNo + self.canv.bookmarkPage(key) + self.canv.addOutlineEntry(paragraph.getPlainText(), + key, 0, 0) + """) + +heading2("Page Transition Effects") + + +eg(""" + canvas.setPageTransition(self, effectname=None, duration=1, + direction=0,dimension='H',motion='I') + """) + +disc(""" +The $setPageTransition$ method specifies how one page will be replaced with +the next. By setting the page transition effect to "dissolve" for example +the current page will appear to melt away when it is replaced by the next +page during interactive viewing. These effects are useful in spicing up +slide presentations, among other places. +Please see the reference manual for more detail on how to use this method. +""") + +heading2("Internal File Annotations") + +eg(""" + canvas.setAuthor(name) + canvas.setTitle(title) + canvas.setSubject(subj) + """) + +disc(""" +These methods have no automatically seen visible effect on the document. +They add internal annotations to the document. These annotations can be +viewed using the "Document Info" menu item of the browser and they also can +be used as a simple standard way of providing basic information about the +document to archiving software which need not parse the entire +file. To find the annotations view the $*.pdf$ output file using a standard +text editor (such as $notepad$ on MS/Windows or $vi$ or $emacs$ on unix) and look +for the string $/Author$ in the file contents. +""") + +eg(examples.testannotations) + +disc(""" +If you want the subject, title, and author to automatically display +in the document when viewed and printed you must paint them onto the +document like any other text. +""") + +illust(examples.annotations, "Setting document internal annotations") diff --git a/bin/reportlab/docs/userguide/ch4_platypus_concepts.py b/bin/reportlab/docs/userguide/ch4_platypus_concepts.py new file mode 100644 index 00000000000..b3b6d17a748 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch4_platypus_concepts.py @@ -0,0 +1,509 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch4_platypus_concepts.py +from reportlab.tools.docco.rl_doc_utils import * + +#####################################################################################################3 + + +heading1("PLATYPUS - Page Layout and Typography Using Scripts") + +heading2("Design Goals") + +disc(""" +Platypus stands for "Page Layout and Typography Using Scripts". It is a high +level page layout library which lets you programmatically create complex +documents with a minimum of effort. +""") + + + +disc(""" +The design of Platypus seeks to separate "high level" layout decisions +from the document content as much as possible. Thus, for example, paragraphs +are constructed using paragraph styles and pages are constructed +using page templates with the intention that hundreds of +documents with thousands of pages can be reformatted to different +style specifications with the modifications of a few lines in a single +shared file which contains the paragraph styles and page layout specifications. +""") + + +disc(""" +The overall design of Platypus can be thought of has having +several layers, top down, these are""") + +disc("$DocTemplates$ the outermost container for the document;") + +disc("$PageTemplates$ specifications for layouts of pages of various kinds;") + +disc("$Frames$ specifications of regions in pages that can contain flowing text or graphics.") + +disc("""$Flowables$ text or graphic elements that should be "flowed + into the document (i.e. things like images, paragraphs and tables, but not things + like page footers or fixed page graphics).""") + +disc("""$pdfgen.Canvas$ the lowest level which ultimately receives the painting of the + document from the other layers.""") + +illust(examples.doctemplateillustration, "Illustration of DocTemplate structure") + +disc(""" + The illustration above graphically illustrates the concepts of $DocTemplates$, + $PageTemplates$ and $Flowables$. It is deceptive, however, because each + of the $PageTemplates$ actually may specify the format for any number of pages + (not just one as might be inferred from the diagram). +""") + +disc(""" +$DocTemplates$ contain one or more $PageTemplates$ each of which contain one or more +$Frames$. $Flowables$ are things which can be flowed into a $Frame$ e.g. +a $Paragraph$ or a $Table$. +""") + +disc(""" +To use platypus you create a document from a $DocTemplate$ class and pass +a list of $Flowable$s to its $build$ method. The document +$build$ method knows how to process the list of flowables +into something reasonable. +""") + +disc(""" +Internally the $DocTemplate$ class implements page layout and formatting +using various events. Each of the events has a corresponding handler method +called $handle_XXX$ where $XXX$ is the event name. A typical event is +$frameBegin$ which occurs when the machinery begins to use a frame for the +first time. +""") + +disc(""" +A Platypus story consists of a sequence of basic elements called $Flowables$ +and these elements drive the data driven Platypus formatting engine. +To modify the behavior of the engine +a special kind of flowable, $ActionFlowables$, tell the layout engine to, +for example, skip to the next +column or change to another $PageTemplate$. +""") + + +heading2("""Getting started""") + +disc("""Consider the following code sequence which provides +a very simple "hello world" example for Platypus.""") + +eg(examples.platypussetup) + +disc("""First we import some constructors, some paragraph styles +and other conveniences from other modules.""") + +eg(examples.platypusfirstpage) + +disc("""We define the fixed features of the first page of the document +with the function above.""") + +eg(examples.platypusnextpage) + +disc("""Since we want pages after the first to look different from the +first we define an alternate layout for the fixed features +of the other pages. Note that the two functions above use +the $pdfgen$ level canvas operations to paint the annotations for +the pages. +""") + +eg(examples.platypusgo) + +disc(""" +Finally, we create a story and build the document. +Note that we are using a "canned" document template here which +comes pre-built with page templates. We are also using a pre-built +paragraph style. We are only using two types of flowables here +-- $Spacers$ and $Paragraphs$. The first $Spacer$ ensures that the +Paragraphs skip past the title string. +""") + +disc(""" +To see the output of this example program run the module +$docs/userguide/examples.py$ (from the ReportLab $docs$ distribution) +as a "top level script". The script interpretation $python examples.py$ will +generate the Platypus output $phello.pdf$. +""") + + +heading2("$Flowables$") +disc(""" +$Flowables$ are things which can be drawn and which have $wrap$, $draw$ and perhaps $split$ methods. +$Flowable$ is an abstract base class for things to be drawn and an instance knows its size +and draws in its own coordinate system (this requires the base API to provide an absolute coordinate +system when the $Flowable.draw$ method is called). To get an instance use $f=Flowable()$. +""") +disc(""" +It should be noted that the $Flowable$ class is an abstract class and is normally +only used as a base class. +""") +k=startKeep() +disc(""" +To illustrate the general way in which $Flowables$ are used we show how a derived class $Paragraph$ +is used and drawn on a canvas. $Paragraphs$ are so important they will get a whole chapter +to themselves. +""") +eg(""" + from reportlab.lib.styles import getSampleStyleSheet + from reportlab.platypus import Paragraph + from reportlab.pdfgen.canvas import Canvas + styleSheet = getSampleStyleSheet() + style = styleSheet['BodyText'] + P=Paragraph('This is a very silly example',style) + canv = Canvas('doc.pdf') + aW = 460 # available width and height + aH = 800 + w,h = P.wrap(aW, aH) # find required space + if w<=aW and h<=aH: + P.drawOn(canv,0,aH) + aH = aH - h # reduce the available height + canv.save() + else: + raise ValueError, "Not enough room" +""") +endKeep(k) +heading3("$Flowable$ User Methods") +eg(""" + Flowable.draw() +""") +disc("""This will be called to ask the flowable to actually render itself. +The $Flowable$ class does not implement $draw$. +The calling code should ensure that the flowable has an attribute $canv$ +which is the $pdfgen.Canvas$ which should be drawn to an that the $Canvas$ +is in an appropriate state (as regards translations rotations, etc). Normally +this method will only be called internally by the $drawOn$ method. Derived classes +must implement this method. +""") +eg(""" + Flowable.drawOn(canvas,x,y) +""") +disc(""" +This is the method which controlling programs use to render the flowable to a particular +canvas. It handles the translation to the canvas coordinate (x,y) and ensuring that +the flowable has a $canv$ attribute so that the +$draw$ method (which is not implemented in the base class) can render in an +absolute coordinate frame. +""") +eg(""" + Flowable.wrap(availWidth, availHeight) +""") +disc("""This will be called by the enclosing frame before objects +are asked their size, drawn or whatever. It returns the +size actually used.""") +eg(""" + Flowable.split(self, availWidth, availheight): +""") +disc("""This will be called by more sophisticated frames when + wrap fails. Stupid flowables should return [] meaning that they are unable to split. +Clever flowables should split themselves and return a list of flowables. It is up to +the client code to ensure that repeated attempts to split are avoided. +If the space is sufficient the split method should return [self]. +Otherwise +the flowable should rearrange itself and return a list $[f0,...]$ of flowables +which will be considered in order. The implemented split method should avoid +changing $self$ as this will allow sophisticated layout mechanisms to do multiple +passes over a list of flowables. +""") + +heading2("Guidelines for flowable positioning") + +disc("""Two methods, which by default return zero, provide guidance on vertical +spacing of flowables: +""") + +eg(""" + Flowable.getSpaceAfter(self): + Flowable.getSpaceBefore(self): +""") +disc("""These methods return how much space should follow or precede +the flowable. The space doesn't belong to the flowable itself i.e. the flowable's +$draw$ method shouldn't consider it when rendering. Controlling programs +will use the values returned in determining how much space is required by +a particular flowable in context. +""") + +disc("""All flowables have an $hAlign$ property: $('LEFT', 'RIGHT', 'CENTER' or 'CENTRE')$. +For paragraphs, which fill the full width of the frame, this has no effect. For tables, +images or other objects which are less than the width of the frame, this determines their +horizontal placement. + +""") + + +disc("""The chapters which follow will cover the most important +specific types of flowables: Paragraphs and Tables.""") + + +heading2("Frames") +disc(""" +$Frames$ are active containers which are themselves contained in $PageTemplates$. +$Frames$ have a location and size and maintain a concept of remaining drawable +space. The command +""") + +eg(""" + Frame(x1, y1, width,height, leftPadding=6, bottomPadding=6, + rightPadding=6, topPadding=6, id=None, showBoundary=0) +""") +disc("""creates a $Frame$ instance with lower left hand corner at coordinate $(x1,y1)$ +(relative to the canvas at use time) and with dimensions $width$ x $height$. The $Padding$ +arguments are positive quantities used to reduce the space available for drawing. +The $id$ argument is an identifier for use at runtime e.g. 'LeftColumn' or 'RightColumn' etc. +If the $showBoundary$ argument is non-zero then the boundary of the frame will get drawn +at run time (this is useful sometimes). +""") +heading3("$Frame$ User Methods") +eg(""" + Frame.addFromList(drawlist, canvas) +""") +disc("""consumes $Flowables$ from the front of $drawlist$ until the + frame is full. If it cannot fit one object, raises + an exception.""") + +eg(""" + Frame.split(flowable,canv) +""") +disc('''Asks the flowable to split using up the available space and return +the list of flowables. +''') + +eg(""" + Frame.drawBoundary(canvas) +""") +disc("draws the frame boundary as a rectangle (primarily for debugging).") +heading3("Using $Frames$") +disc(""" +$Frames$ can be used directly with canvases and flowables to create documents. +The $Frame.addFromList$ method handles the $wrap$ & $drawOn$ calls for you. +You don't need all of the Platypus machinery to get something useful into +PDF. +""") +eg(""" +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.units import inch +from reportlab.platypus import Paragraph, Frame +styles = getSampleStyleSheet() +styleN = styles['Normal'] +styleH = styles['Heading1'] +story = [] + +#add some flowables +story.append(Paragraph("This is a Heading",styleH)) +story.append(Paragraph("This is a paragraph in Normal style.", + styleN)) +c = Canvas('mydoc.pdf') +f = Frame(inch, inch, 6*inch, 9*inch, showBoundary=1) +f.addFromList(story,c) +c.save() +""") + +heading2("Documents and Templates") + +disc(""" +The $BaseDocTemplate$ class implements the basic machinery for document +formatting. An instance of the class contains a list of one or more +$PageTemplates$ that can be used to describe the layout of information +on a single page. The $build$ method can be used to process +a list of $Flowables$ to produce a PDF document. +""") + +CPage(3.0) +heading3("The $BaseDocTemplate$ class") + +eg(""" + BaseDocTemplate(self, filename, + pagesize=defaultPageSize, + pageTemplates=[], + showBoundary=0, + leftMargin=inch, + rightMargin=inch, + topMargin=inch, + bottomMargin=inch, + allowSplitting=1, + title=None, + author=None, + _pageBreakQuick=1) +""") + +disc(""" +creates a document template suitable for creating a basic document. It comes with quite a lot +of internal machinery, but no default page templates. The required $filename$ can be a string, +the name of a file to receive the created PDF document; alternatively it +can be an object which has a $write$ method such as a $StringIO$ or $file$ or $socket$. +""") + +disc(""" +The allowed arguments should be self explanatory, but $showBoundary$ controls whether or +not $Frame$ boundaries are drawn which can be useful for debugging purposes. The +$allowSplitting$ argument determines whether the builtin methods should try to split +individual $Flowables$ across $Frames$. The $_pageBreakQuick$ argument determines whether +an attempt to do a page break should try to end all the frames on the page or not, before ending +the page. +""") + +heading4("User $BaseDocTemplate$ Methods") + +disc("""These are of direct interest to client programmers +in that they are normally expected to be used. +""") +eg(""" + BaseDocTemplate.addPageTemplates(self,pageTemplates) +""") +disc(""" +This method is used to add one or a list of $PageTemplates$ to an existing documents. +""") +eg(""" + BaseDocTemplate.build(self, flowables, filename=None, canvasmaker=canvas.Canvas) +""") +disc(""" +This is the main method which is of interest to the application +programmer. Assuming that the document instance is correctly set up the +$build$ method takes the story in the shape of the list of flowables +(the $flowables$ argument) and loops through the list forcing the flowables +one at a time through the formatting machinery. Effectively this causes +the $BaseDocTemplate$ instance to issue calls to the instance $handle_XXX$ methods +to process the various events. +""") +heading4("User Virtual $BaseDocTemplate$ Methods") +disc(""" +These have no semantics at all in the base class. They are intended as pure virtual hooks +into the layout machinery. Creators of immediately derived classes can override these +without worrying about affecting the properties of the layout engine. +""") +eg(""" + BaseDocTemplate.afterInit(self) +""") +disc(""" +This is called after initialisation of the base class; a derived class could overide +the method to add default $PageTemplates$. +""") + +eg(""" + BaseDocTemplate.afterPage(self) +""") +disc("""This is called after page processing, and + immediately after the afterDrawPage method + of the current page template. A derived class could +use this to do things which are dependent on information in the page +such as the first and last word on the page of a dictionary. +""") + +eg(""" + BaseDocTemplate.beforeDocument(self) +""") + +disc("""This is called before any processing is +done on the document, but after the processing machinery +is ready. It can therefore be used to do things to the instance\'s +$pdfgen.canvas$ and the like. +""") + +eg(""" + BaseDocTemplate.beforePage(self) +""") + +disc("""This is called at the beginning of page + processing, and immediately before the + beforeDrawPage method of the current page + template. It could be used to reset page specific + information holders.""") + +eg(""" + BaseDocTemplate.filterFlowables(self,flowables) +""") + +disc("""This is called to filter flowables at the start of the main handle_flowable method. + Upon return if flowables[0] has been set to None it is discarded and the main + method returns immediately. + """) + +eg(""" + BaseDocTemplate.afterFlowable(self, flowable) +""") + +disc("""Called after a flowable has been rendered. An interested class could use this +hook to gather information about what information is present on a particular page or frame.""") + +heading4("$BaseDocTemplate$ Event handler Methods") +disc(""" +These methods constitute the greater part of the layout engine. Programmers shouldn't +have to call or override these methods directly unless they are trying to modify the layout engine. +Of course, the experienced programmer who wants to intervene at a particular event, $XXX$, +which does not correspond to one of the virtual methods can always override and +call the base method from the drived class version. We make this easy by providing +a base class synonym for each of the handler methods with the same name prefixed by an underscore '_'. +""") + +eg(""" + def handle_pageBegin(self): + doStuff() + BaseDocTemplate.handle_pageBegin(self) + doMoreStuff() + + #using the synonym + def handle_pageEnd(self): + doStuff() + self._handle_pageEnd() + doMoreStuff() +""") +disc(""" +Here we list the methods only as an indication of the events that are being +handled. +Interested programmers can take a look at the source. +""") +eg(""" + handle_currentFrame(self,fx) + handle_documentBegin(self) + handle_flowable(self,flowables) + handle_frameBegin(self,*args) + handle_frameEnd(self) + handle_nextFrame(self,fx) + handle_nextPageTemplate(self,pt) + handle_pageBegin(self) + handle_pageBreak(self) + handle_pageEnd(self) +""") + +disc(""" +Using document templates can be very easy; $SimpleDoctemplate$ is a class derived from +$BaseDocTemplate$ which provides its own $PageTemplate$ and $Frame$ setup. +""") + +eg(""" +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.pagesizes import letter +from reportlab.platypus import Paragraph, SimpleDocTemplate +styles = getSampleStyleSheet() +styleN = styles['Normal'] +styleH = styles['Heading1'] +story = [] + +#add some flowables +story.append(Paragraph("This is a Heading",styleH)) +story.append(Paragraph("This is a paragraph in Normal style.", + styleN)) +doc = SimpleDocTemplate('mydoc.pdf',pagesize = letter) +doc.build(story) +""") +heading3("$PageTemplates$") +disc(""" +The $PageTemplate$ class is a container class with fairly minimal semantics. Each instance +contains a list of $Frames$ and has methods which should be called at the start and end +of each page. +""") +eg("PageTemplate(id=None,frames=[],onPage=_doNothing,onPageEnd=_doNothing)") +disc(""" +is used to initialize an instance, the $frames$ argument should be a list of $Frames$ +whilst the optional $onPage$ and $onPageEnd$ arguments are callables which should have signature +$def XXX(canvas,document)$ where $canvas$ and $document$ +are the canvas and document being drawn. These routines are intended to be used to paint non-flowing (i.e. standard) +parts of pages. These attribute functions are exactly parallel to the pure virtual methods +$PageTemplate.beforPage$ and $PageTemplate.afterPage$ which have signature +$beforPage(self,canvas,document)$. The methods allow class derivation to be used to define +standard behaviour, whilst the attributes allow instance changes. The $id$ argument is used at +run time to perform $PageTemplate$ switching so $id='FirstPage'$ or $id='TwoColumns'$ are typical. +""") diff --git a/bin/reportlab/docs/userguide/ch5_paragraphs.py b/bin/reportlab/docs/userguide/ch5_paragraphs.py new file mode 100644 index 00000000000..3191f1f1d55 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch5_paragraphs.py @@ -0,0 +1,319 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch5_paragraphs.py +from reportlab.tools.docco.rl_doc_utils import * + +#begin chapter oon paragraphs +heading1("Paragraphs") +disc(""" +The $reportlab.platypus.Paragraph$ class is one of the most useful of the Platypus $Flowables$; +it can format fairly arbitrary text and provides for inline font style and colour changes using +an XML style markup. The overall shape of the formatted text can be justified, right or left ragged +or centered. The XML markup can even be used to insert greek characters or to do subscripts. +""") +disc("""The following text creates an instance of the $Paragraph$ class:""") +eg("""Paragraph(text, style, bulletText=None)""") +disc("""The $text$ argument contains the text of the +paragraph; excess white space is removed from the text at the ends and internally after +linefeeds. This allows easy use of indented triple quoted text in Python scripts. +The $bulletText$ argument provides the text of a default bullet for the paragraph. +The font and other properties for the paragraph text and bullet are set using the style argument. +""") +disc(""" +The $style$ argument should be an instance of class $ParagraphStyle$ obtained typically +using""") +eg(""" +from reportlab.lib.styles import ParagraphStyle +""") +disc(""" +this container class provides for the setting of multiple default paragraph attributes +in a structured way. The styles are arranged in a dictionary style object called a $stylesheet$ +which allows for the styles to be accessed as $stylesheet['BodyText']$. A sample style +sheet is provided. +""") +eg(""" +from reportlab.lib.styles import getSampleStyleSheet +stylesheet=getSampleStyleSheet() +normalStyle = stylesheet['Normal'] +""") +disc(""" +The options which can be set for a $Paragraph$ can be seen from the $ParagraphStyle$ defaults. +""") +heading4("$class ParagraphStyle$") +eg(""" +class ParagraphStyle(PropertySet): + defaults = { + 'fontName':'Times-Roman', + 'fontSize':10, + 'leading':12, + 'leftIndent':0, + 'rightIndent':0, + 'firstLineIndent':0, + 'alignment':TA_LEFT, + 'spaceBefore':0, + 'spaceAfter':0, + 'bulletFontName':'Times-Roman', + 'bulletFontSize':10, + 'bulletIndent':0, + 'textColor': black + } +""") + +heading2("Using Paragraph Styles") + +#this will be used in the ParaBox demos. +sample = """You are hereby charged that on the 28th day of May, 1970, you did +willfully, unlawfully, and with malice of forethought, publish an +alleged English-Hungarian phrase book with intent to cause a breach +of the peace. How do you plead?""" + + +disc("""The $Paragraph$ and $ParagraphStyle$ classes together +handle most common formatting needs. The following examples +draw paragraphs in various styles, and add a bounding box +so that you can see exactly what space is taken up.""") + +s1 = ParagraphStyle('Normal') +parabox(sample, s1, 'The default $ParagraphStyle$') + +disc("""The two attributes $spaceBefore$ and $spaceAfter$ do what they +say, except at the top or bottom of a frame. At the top of a frame, +$spaceBefore$ is ignored, and at the bottom, $spaceAfter$ is ignored. +This means that you could specify that a 'Heading2' style had two +inches of space before when it occurs in mid-page, but will not +get acres of whitespace at the top of a page. These two attributes +should be thought of as 'requests' to the Frame and are not part +of the space occupied by the Paragraph itself.""") + +disc("""The $fontSize$ and $fontName$ tags are obvious, but it is +important to set the $leading$. This is the spacing between +adjacent lines of text; a good rule of thumb is to make this +20% larger than the point size. To get double-spaced text, +use a high $leading$.""") + +disc("""The figure below shows space before and after and an +increased leading:""") + +parabox(sample, + ParagraphStyle('Spaced', + spaceBefore=6, + spaceAfter=6, + leading=16), + 'Space before and after and increased leading' + ) + +disc("""The $leftIndent$ and $rightIndent$ attributes do exactly +what you would expect; $firstLineIndent$ is added to the $leftIndent$ of the +first line. If you want a straight left edge, remember +to set $firstLineIndent$ equal to 0.""") + +parabox(sample, + ParagraphStyle('indented', + firstLineIndent=+24, + leftIndent=24, + rightIndent=24), + 'one third inch indents at left and right, two thirds on first line' + ) + +disc("""Setting $firstLineIndent$ equal to a negative number, $leftIndent$ +much higher, and using a +different font (we'll show you how later!) can give you a +definition list:.""") + +parabox('Judge Pickles: ' + sample, + ParagraphStyle('dl', + leftIndent=36), + 'Definition Lists' + ) + +disc("""There are four possible values of $alignment$, defined as +constants in the module reportlab.lib.enums. These are +TA_LEFT, TA_CENTER or TA_CENTRE, TA_RIGHT and +TA_JUSTIFY, with values of 0, 1, 2 and 4 respectively. These +do exactly what you would expect.""") + + +heading2("Paragraph XML Markup Tags") +disc("""XML markup can be used to modify or specify the +overall paragraph style, and also to specify intra- +paragraph markup.""") + +heading3("The outermost < para > tag") + + +disc(""" +The paragraph text may optionally be surrounded by +<para attributes....> +</para> +tags. The attributes if any of the opening <para> tag affect the style that is used +with the $Paragraph$ $text$ and/or $bulletText$. +""") +disc(" ") + +from reportlab.platypus.paraparser import _addAttributeNames, _paraAttrMap, _bulletAttrMap + +def getAttrs(A): + _addAttributeNames(A) + S={} + for k, v in A.items(): + a = v[0] + if not S.has_key(a): + S[a] = k + else: + S[a] = "%s, %s" %(S[a],k) + + K = S.keys() + K.sort() + D=[('Attribute','Synonyms')] + for k in K: + D.append((k,S[k])) + cols=2*[None] + rows=len(D)*[None] + return D,cols,rows + +t=apply(Table,getAttrs(_paraAttrMap)) +t.setStyle(TableStyle([ + ('FONT',(0,0),(-1,1),'Times-Bold',10,12), + ('FONT',(0,1),(-1,-1),'Courier',8,8), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +getStory().append(t) +caption("""Table - Synonyms for style attributes""") + +disc("""Some useful synonyms have been provided for our Python attribute +names, including lowercase versions, and the equivalent properties +from the HTML standard where they exist. These additions make +it much easier to build XML-printing applications, since +much intra-paragraph markup may not need translating. The +table below shows the allowed attributes and synonyms in the +outermost paragraph tag.""") + + +heading2("Intra-paragraph markup") +disc("""'...) +and italic (...). It is also legal to use an underline +tag (... but it has no effect; PostScript fonts don't +support underlining, and neither do we, yet.]]>""") + +parabox2("""You are hereby charged that on the 28th day of May, 1970, you did +willfully, unlawfully, and with malice of forethought, publish an +alleged English-Hungarian phrase book with intent to cause a breach +of the peace. How do you plead?""", "Simple bold and italic tags") + +heading3("The $<font>$ tag") +disc("""The $<font>$ tag can be used to change the font name, +size and text color for any substring within the paragraph. +Legal attributes are $size$, $face$, $name$ (which is the same as $face$), +$color$, and $fg$ (which is the same as $color$). The $name$ is +the font family name, without any 'bold' or 'italic' suffixes. +Colors may be +HTML color names or a hex string encoded in a variety of ways; +see ^reportlab.lib.colors^ for the formats allowed.""") + +parabox2(""" +You are hereby charged that on the 28th day of May, 1970, you did +willfully, unlawfully, and with malice of forethought, +publish an +alleged English-Hungarian phrase book with intent to cause a breach +of the peace. How do you plead?""", "The $font$ tag") + +heading3("Superscripts and Subscripts") +disc("""Superscripts and subscripts are supported with the + and tags, which work exactly +as you might expect. In addition, most greek letters +can be accessed by using the +tag, or with mathML entity names.]]>""") + +##parabox2("""epsiloniota +##pi = -1""", "Greek letters and subscripts") + +parabox2("""Equation (α): e ip = -1""", + "Greek letters and superscripts") + +heading3("Numbering Paragraphs and Lists") +disc("""The $<seq>$ tag provides comprehensive support +for numbering lists, chapter headings and so on. It acts as +an interface to the $Sequencer$ class in ^reportlab.lib.sequencer^. +These are used to number headings and figures throughout this +document. +You may create as many separate 'counters' as you wish, accessed +with the $id$ attribute; these will be incremented by one each +time they are accessed. The $seqreset$ tag resets a counter. +If you want it to resume from a number other than 1, use +the syntax <seqreset id="mycounter" base="42">. +Let's have a go:""") + +parabox2(""", , . +Reset. , , +.""", "Basic sequences") + +disc("""You can save specifying an ID by designating a counter ID +as the default using the <seqdefault id="Counter"> +tag; it will then be used whenever a counter ID +is not specified. This saves some typing, especially when +doing multi-level lists; you just change counter ID when +stepping in or out a level.""") + +parabox2("""Continued... , +, , , , , .""", +"The default sequence") + +disc("""Finally, one can access multi-level sequences using +a variation of Python string formatting and the $template$ +attribute in a <seq> tags. This is used to do the +captions in all of the figures, as well as the level two +headings. The substring $%(counter)s$ extracts the current +value of a counter without incrementing it; appending a +plus sign as in $%(counter)s$ increments the counter. +The figure captions use a pattern like the one below:""") + +parabox2("""Figure - Multi-level templates""", +"Multi-level templates") + +disc("""We cheated a little - the real document used 'Figure', +but the text above uses 'FigureNo' - otherwise we would have +messed up our numbering!""") + +heading2("Bullets and Paragraph Numbering") +disc("""In addition to the three indent properties, some other +parameters are needed to correctly handle bulleted and numbered +lists. We discuss this here because you have now seen how +to handle numbering. A paragraph may have an optional +^bulletText^ argument passed to its constructor; alternatively, +bullet text may be placed in a $..
]]>$ +tag at its head. The text will be drawn on the first line of +the paragraph, with its x origin determined by the $bulletIndent$ +attribute of the style, and in the font given in the +$bulletFontName$ attribute. For genuine bullets, a good +idea is to select the Times-Roman font in the style, and +use a character such as $\\xe2\\x80\\xa2)$:""") + +t=apply(Table,getAttrs(_bulletAttrMap)) +t.setStyle([ + ('FONT',(0,0),(-1,1),'Times-Bold',10,12), + ('FONT',(0,1),(-1,-1),'Courier',8,8), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ]) +getStory().append(t) + +caption("""Table - <bullet> attributes & synonyms""") +disc("""The <bullet> tag is only allowed once in a given paragraph and its use +overrides the implied bullet style and ^bulletText^ specified in the ^Paragraph^ +creation. +""") +parabox("""\xe2\x80\xa2this is a bullet point. Spam +spam spam spam spam spam spam spam spam spam spam spam +spam spam spam spam spam spam spam spam spam spam """, + styleSheet['Bullet'], + 'Basic use of bullet points') + +disc("""Exactly the same technique is used for numbers, +except that a sequence tag is used. It is also possible +to put a multi-character string in the bullet; with a deep +indent and bold bullet font, you can make a compact +definition list.""") diff --git a/bin/reportlab/docs/userguide/ch6_tables.py b/bin/reportlab/docs/userguide/ch6_tables.py new file mode 100644 index 00000000000..011423367ba --- /dev/null +++ b/bin/reportlab/docs/userguide/ch6_tables.py @@ -0,0 +1,419 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch6_tables.py +from reportlab.tools.docco.rl_doc_utils import * +from reportlab.platypus import Image +import reportlab + +heading1("Tables and TableStyles") +disc(""" +The $Table$ and $LongTable$ classes derive from the $Flowable$ class and are intended +as a simple textual gridding mechanisms. The $longTable$ class uses a greedy algorithm +when calculating column widths and is intended for long tables where speed counts. +$Table$ cells can hold anything which can be converted to +a Python $string$ or $Flowables$ (or lists of $Flowables$). +""") + +disc(""" +Our present tables are a trade-off between efficient drawing and specification +and functionality. We assume the reader has some familiarity with HTML tables. +In brief, they have the following characteristics: +""") + +bullet("""They can contain anything convertible to a string; flowable +objects such as other tables; or entire sub-stories""") + +bullet("""They can work out the row heights to fit the data if you don't supply +the row height. (They can also work out the widths, but generally it is better +for a designer to set the width manually, and it draws faster).""") + +bullet("""They can split across pages if needed (see the canSplit attribute). +You can specify that a number of rows at the top and bottom should be +repeated after the split (e.g. show the headers again on page 2,3,4...)""") + +bullet("""For very wide tables, they can also split 'by column'. You can choose +whether tou want to split down-and-across or across-and-down""") + +bullet("""They have a simple and powerful notation for specifying shading and +gridlines which works well with financial or database tables, where you +don't know the number of rows up front. You can easily say 'make the last row +bold and put a line above it'""") + +bullet("""The style and data are separated, so you can declare a handful of table +styles and use them for a family of reports. Styes can also 'inherit', as with +paragraphs.""") + +disc("""There is however one main limitation compared to an HTML table. +They define a simple rectangular grid. There is no simple row or column +spanning; if you need to span cells, you must nest tables inside table cells instead or use a more +complex scheme in which the lead cell of a span contains the actual contents.""") + +disc(""" +$Tables$ are created by passing the constructor an optional sequence of column widths, +an optional sequence of row heights, and the data in row order. +Drawing of the table can be controlled by using a $TableStyle$ instance. This allows control of the +color and weight of the lines (if any), and the font, alignment and padding of the text. +A primitive automatic row height and or column width calculation mechanism is provided for. +""") + +heading2('$Table$ User Methods') +disc("""These are the main methods which are of interest to the client programmer.""") + +heading4("""$Table(data, colWidths=None, rowHeights=None, style=None, splitByRow=1, +repeatRows=0, repeatCols=0)$""") + +disc("""The $data$ argument is a sequence of sequences of cell values each of which +should be convertible to a string value using the $str$ function or should be a Flowable instance (such as a $Paragraph$) or a list (or tuple) of such instances. +If a cell value is a $Flowable$ or list of $Flowables$ these must either have a determined width +or the containing column must have a fixed width. +The first row of cell values +is in $data[0]$ i.e. the values are in row order. The $i$, $j$th. cell value is in +$data[i][j]$. Newline characters $'\\n'$ in cell values are treated as line split characters and +are used at draw time to format the cell into lines. +""") +disc("""The other arguments are fairly obvious, the $colWidths$ argument is a sequence +of numbers or possibly $None$, representing the widths of the columns. The number of elements +in $colWidths$ determines the number of columns in the table. +A value of $None$ means that the corresponding column width should be calculated automatically.""") + +disc("""The $rowHeights$ argument is a sequence +of numbers or possibly $None$, representing the heights of the rows. The number of elements +in $rowHeights$ determines the number of rows in the table. +A value of $None$ means that the corresponding row height should be calculated automatically.""") + +disc("""The $style$ argument can be an initial style for the table.""") +disc("""The $splitByRow$ argument is only needed for tables both too tall and too wide +to fit in the current context. In this case you must decide whether to 'tile' +down and across, or across and then down. This parameter is a Boolean indicating that the +$Table$ should split itself +by row before attempting to split itself by column when too little space is available in +the current drawing area and the caller wants the $Table$ to split.""") + +disc("""The $repeatRows$ and $repeatCols$ arguments specify the number of leading rows and columns +that should be repeated when the $Table$ is asked to split itself.""") +heading4('$Table.setStyle(tblStyle)$') +disc(""" +This method applies a particular instance of class $TableStyle$ (discussed below) +to the $Table$ instance. This is the only way to get $tables$ to appear +in a nicely formatted way. +""") +disc(""" +Successive uses of the $setStyle$ method apply the styles in an additive fashion. +That is, later applications override earlier ones where they overlap. +""") + +heading2('$TableStyle$') +disc(""" +This class is created by passing it a sequence of commands, each command +is a tuple identified by its first element which is a string; the remaining +elements of the command tuple represent the start and stop cell coordinates +of the command and possibly thickness and colors, etc. +""") +heading2("$TableStyle$ User Methods") +heading3("$TableStyle(commandSequence)$") +disc("""The creation method initializes the $TableStyle$ with the argument +command sequence as an example:""") +eg(""" + LIST_STYLE = TableStyle( + [('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +""") +heading3("$TableStyle.add(commandSequence)$") +disc("""This method allows you to add commands to an existing +$TableStyle$, i.e. you can build up $TableStyles$ in multiple statements. +""") +eg(""" + LIST_STYLE.add('BACKGROUND', (0,0), (-1,0), colors.Color(0,0.7,0.7)) +""") +heading3("$TableStyle.getCommands()$") +disc("""This method returns the sequence of commands of the instance.""") +eg(""" + cmds = LIST_STYLE.getCommands() +""") +heading2("$TableStyle$ Commands") +disc("""The commands passed to $TableStyles$ come in three main groups +which affect the table background, draw lines, or set cell styles. +""") +disc("""The first element of each command is its identifier, +the second and third arguments determine the cell coordinates of +the box of cells which are affected with negative coordinates +counting backwards from the limit values as in Python +indexing. The coordinates are given as +(column, row) which follows the spreadsheet 'A1' model, but not +the more natural (for mathematicians) 'RC' ordering. +The top left cell is (0, 0) the bottom right is (-1, -1). Depending on +the command various extra (???) occur at indices beginning at 3 on. +""") +heading3("""$TableStyle$ Cell Formatting Commands""") +disc("""The cell formatting commands all begin with an identifier, followed by +the start and stop cell definitions and the perhaps other arguments. +the cell formatting commands are:""") +eg(""" +FONT - takes fontname, optional fontsize and optional leading. +FONTNAME (or FACE) - takes fontname. +FONTSIZE (or SIZE) - takes fontsize in points; leading may get out of sync. +LEADING - takes leading in points. +TEXTCOLOR - takes a color name or (R,G,B) tuple. +ALIGNMENT (or ALIGN) - takes one of LEFT, RIGHT and CENTRE (or CENTER) or DECIMAL. +LEFTPADDING - takes an integer, defaults to 6. +RIGHTPADDING - takes an integer, defaults to 6. +BOTTOMPADDING - takes an integer, defaults to 3. +TOPPADDING - takes an integer, defaults to 3. +BACKGROUND - takes a color. +ROWBACKGROUNDS - takes a list of colors to be used cyclically. +COLBACKGROUNDS - takes a list of colors to be used cyclically. +VALIGN - takes one of TOP, MIDDLE or the default BOTTOM +""") +disc("""This sets the background cell color in the relevant cells. +The following example shows the $BACKGROUND$, and $TEXTCOLOR$ commands in action:""") +EmbeddedCode(""" +data= [['00', '01', '02', '03', '04'], + ['10', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] +t=Table(data) +t.setStyle(TableStyle([('BACKGROUND',(1,1),(-2,-2),colors.green), + ('TEXTCOLOR',(0,0),(1,-1),colors.red)])) +""") +disc("""To see the effects of the alignment styles we need some widths +and a grid, but it should be easy to see where the styles come from.""") +EmbeddedCode(""" +data= [['00', '01', '02', '03', '04'], + ['10', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] +t=Table(data,5*[0.4*inch], 4*[0.4*inch]) +t.setStyle(TableStyle([('ALIGN',(1,1),(-2,-2),'RIGHT'), + ('TEXTCOLOR',(1,1),(-2,-2),colors.red), + ('VALIGN',(0,0),(0,-1),'TOP'), + ('TEXTCOLOR',(0,0),(0,-1),colors.blue), + ('ALIGN',(0,-1),(-1,-1),'CENTER'), + ('VALIGN',(0,-1),(-1,-1),'MIDDLE'), + ('TEXTCOLOR',(0,-1),(-1,-1),colors.green), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ])) +""") +heading3("""$TableStyle$ Line Commands""") +disc(""" + Line commands begin with the identifier, the start and stop cell coordinates + and always follow this with the thickness (in points) and color of the desired lines. Colors can be names, + or they can be specified as a (R, G, B) tuple, where R, G and B are floats and (0, 0, 0) is black. The line + command names are: GRID, BOX, OUTLINE, INNERGRID, LINEBELOW, LINEABOVE, LINEBEFORE + and LINEAFTER. BOX and OUTLINE are equivalent, and GRID is the equivalent of applying both BOX and + INNERGRID. +""") +CPage(4.0) +disc("""We can see some line commands in action with the following example. +""") +EmbeddedCode(""" +data= [['00', '01', '02', '03', '04'], + ['10', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] +t=Table(data,style=[('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ]) +""") +disc("""Line commands cause problems for tables when they split; the following example +shows a table being split in various positions""") +EmbeddedCode(""" +data= [['00', '01', '02', '03', '04'], + ['10', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] +t=Table(data,style=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('BOX',(0,0),(-1,-1),2,colors.black), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ('BACKGROUND', (0, 0), (0, 1), colors.pink), + ('BACKGROUND', (1, 1), (1, 2), colors.lavender), + ('BACKGROUND', (2, 2), (2, 3), colors.orange), + ]) +""") +t=getStory()[-1] +getStory().append(Spacer(0,6)) +for s in t.split(4*inch,30): + getStory().append(s) + getStory().append(Spacer(0,6)) +getStory().append(Spacer(0,6)) +for s in t.split(4*inch,36): + getStory().append(s) + getStory().append(Spacer(0,6)) + +disc("""When unsplit and split at the first or second row.""") + +CPage(4.0) +heading3("""Complex Cell Values""") +disc(""" +As mentioned above we can have complicated cell values including $Paragraphs$, $Images$ and other $Flowables$ +or lists of the same. To see this in operation consider the following code and the table it produces. +Note that the $Image$ has a white background which will obscure any background you choose for the cell. +To get better results you should use a transparent background. +""") +import os, reportlab.platypus +I = '../images/replogo.gif' +EmbeddedCode(""" +I = Image('%s') +I.drawHeight = 1.25*inch*I.drawHeight / I.drawWidth +I.drawWidth = 1.25*inch +P0 = Paragraph(''' + A paragraph + 1''', + styleSheet["BodyText"]) +P = Paragraph(''' + The ReportLab Left + Logo + Image''', + styleSheet["BodyText"]) +data= [['A', 'B', 'C', P0, 'D'], + ['00', '01', '02', [I,P], '04'], + ['10', '11', '12', [P,I], '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] + +t=Table(data,style=[('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ('BACKGROUND', (0, 0), (0, 1), colors.pink), + ('BACKGROUND', (1, 1), (1, 2), colors.lavender), + ('BACKGROUND', (2, 2), (2, 3), colors.orange), + ('BOX',(0,0),(-1,-1),2,colors.black), + ('GRID',(0,0),(-1,-1),0.5,colors.black), + ('VALIGN',(3,0),(3,0),'BOTTOM'), + ('BACKGROUND',(3,0),(3,0),colors.limegreen), + ('BACKGROUND',(3,1),(3,1),colors.khaki), + ('ALIGN',(3,1),(3,1),'CENTER'), + ('BACKGROUND',(3,2),(3,2),colors.beige), + ('ALIGN',(3,2),(3,2),'LEFT'), + ]) + +t._argW[3]=1.5*inch +"""%I) +heading3("""$TableStyle$ Span Commands""") +disc("""Our $Table$ classes support the concept of spanning, but it isn't specified in the same way +as html. The style specification +""") +eg(""" +SPAN, (sc,sr), (ec,er) +""") +disc("""indicates that the cells in columns $sc$ - $ec$ and rows $sr$ - $er$ should be combined into a super cell +with contents determined by the cell $(sc, sr)$. The other cells should be present, but should contain empty strings +or you may get unexpected results. +""") +EmbeddedCode(""" +data= [['Top\\nLeft', '', '02', '03', '04'], + ['', '', '12', '13', '14'], + ['20', '21', '22', 'Bottom\\nRight', ''], + ['30', '31', '32', '', '']] +t=Table(data,style=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('BACKGROUND',(0,0),(1,1),colors.palegreen), + ('SPAN',(0,0),(1,1)), + ('BACKGROUND',(-2,-2),(-1,-1), colors.pink), + ('SPAN',(-2,-2),(-1,-1)), + ]) +""") + +disc("""notice that we don't need to be conservative with our $GRID$ command. The spanned cells are not drawn through. +""") +heading3("""Special $TableStyle$ Indeces""") +disc("""In any style command the first row index may be set to one of the special strings +$'splitlast'$ or $'splitfirst'$ to indicate that the style should be used only for the last row of +a split table, or the first row of a continuation. This allows splitting tables with nicer effects around the split.""") + +heading1("""Other Useful $Flowables$""") +heading2("""$Preformatted(text, style, bulletText = None, dedent=0)$""") +disc(""" +Creates a preformatted paragraph which does no wrapping, line splitting or other manipulations. +No $XML$ style tags are taken account of in the text. +If dedent is non zero $dedent$ common leading +spaces will be removed from the front of each line. +""") +heading2("""$XPreformatted(text, style, bulletText = None, dedent=0, frags=None)$""") +disc(""" +This is a non rearranging form of the $Paragraph$ class; $XML$ tags are allowed in +$text$ and have the same meanings as for the $Paragraph$ class. +As for $Preformatted$, if dedent is non zero $dedent$ common leading +spaces will be removed from the front of each line. +""") +EmbeddedCode(""" +from reportlab.lib.styles import getSampleStyleSheet +stylesheet=getSampleStyleSheet() +normalStyle = stylesheet['Normal'] +text=''' + + This is a non rearranging form of the Paragraph class; + XML tags are allowed in text and have the same + + meanings as for the Paragraph class. + As for Preformatted, if dedent is non zero dedent + common leading spaces will be removed from the + front of each line. + You can have &amp; style entities as well for & < > and ". + +''' +t=XPreformatted(text,normalStyle,dedent=3) +""") +heading2("""$Image(filename, width=None, height=None)$""") +disc("""Create a flowable which will contain the image defined by the data in file $filename$. +The default PDF image type jpeg is supported and if the PIL extension to Python +is installed the other image types can also be handled. If $width$ and or $height$ are specified +then they determine the dimension of the displayed image in points. If either dimension is +not specified (or specified as $None$) then the corresponding pixel dimension of the image is assumed +to be in points and used. +""") +I=os.path.join(os.path.dirname(reportlab.__file__),'docs','images','lj8100.jpg') +eg(""" +Image("lj8100.jpg") +""",after=0.1) +disc("""will display as""") +try: + getStory().append(Image(I)) +except: + disc("""An image should have appeared here.""") +disc("""whereas""") +eg(""" +im = Image("lj8100.jpg", width=2*inch, height=2*inch) +im.hAlign = 'CENTER' +""", after=0.1) +disc('produces') +try: + im = Image(I, width=2*inch, height=2*inch) + im.hAlign = 'CENTER' + getStory().append(Image(I, width=2*inch, height=2*inch)) +except: + disc("""An image should have appeared here.""") +heading2("""$Spacer(width, height)$""") +disc("""This does exactly as would be expected; it adds a certain amount of space into the story. +At present this only works for vertical space. +""") +CPage(1) +heading2("""$PageBreak()$""") +disc("""This $Flowable$ represents a page break. It works by effectively consuming all vertical +space given to it. This is sufficient for a single $Frame$ document, but would only be a +frame break for multiple frames so the $BaseDocTemplate$ mechanism +detects $pageBreaks$ internally and handles them specially. +""") +CPage(1) +heading2("""$CondPageBreak(height)$""") +disc("""This $Flowable$ attempts to force a $Frame$ break if insufficient vertical space remains +in the current $Frame$. It is thus probably wrongly named and should probably be renamed as +$CondFrameBreak$. +""") +CPage(1) +heading2("""$KeepTogether(flowables)$""") +disc(""" +This compound $Flowable$ takes a list of $Flowables$ and attempts to keep them in the same $Frame$. +If the total height of the $Flowables$ in the list $flowables$ exceeds the current frame's available +space then all the space is used and a frame break is forced. +""") diff --git a/bin/reportlab/docs/userguide/ch7_custom.py b/bin/reportlab/docs/userguide/ch7_custom.py new file mode 100644 index 00000000000..176d042cd85 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch7_custom.py @@ -0,0 +1,81 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch7_custom.py +from reportlab.tools.docco.rl_doc_utils import * + +heading1("Writing your own $Flowable$ Objects") +disc(""" +Flowables are intended to be an open standard for creating +reusable report content, and you can easily create your +own objects. We hope that over time we will build up +a library of contributions, giving reportlab users a +rich selection of charts, graphics and other "report +widgets" they can use in their own reports. This section +shows you how to create your own flowables.""") + +todo("""we should put the Figure class in the +standard library, as it is a very useful base.""") + + + + +heading2("A very simple $Flowable$") + +disc(""" +Recall the $hand$ function from the $pdfgen$ section of this user guide which +generated a drawing of a hand as a closed figure composed from Bezier curves. +""") +illust(examples.hand, "a hand") +disc(""" +To embed this or any other drawing in a Platypus flowable we must define a +subclass of $Flowable$ +with at least a $wrap$ method and a $draw$ method. +""") +eg(examples.testhandannotation) +disc(""" +The $wrap$ method must provide the size of the drawing -- it is used by +the Platypus mainloop to decide whether this element fits in the space remaining +on the current frame. The $draw$ method performs the drawing of the object after +the Platypus mainloop has translated the $(0,0)$ origin to an appropriate location +in an appropriate frame. +""") +disc(""" +Below are some example uses of the $HandAnnotation$ flowable. +""") + +from reportlab.lib.colors import blue, pink, yellow, cyan, brown +from reportlab.lib.units import inch + +handnote() + +disc("""The default.""") + +handnote(size=inch) + +disc("""Just one inch high.""") + +handnote(xoffset=3*inch, size=inch, strokecolor=blue, fillcolor=cyan) + +disc("""One inch high and shifted to the left with blue and cyan.""") + + +heading2("Modifying a Built in $Flowable$") +disc("""To modify an existing flowable, you should create a derived class +and override the methods you need to change to get the desired behaviour""") +disc("""As an example to create a rotated image you need to override the wrap +and draw methods of the existing Image class""") +import os +from reportlab.platypus import * +I = '../images/replogo.gif' + +EmbeddedCode(""" +class RotatedImage(Image): + def wrap(self,availWidth,availHeight): + h, w = Image.wrap(self,availHeight,availWidth) + return w, h + def draw(self): + self.canv.rotate(90) + Image.draw(self) +I = RotatedImage('%s') +I.hAlign = 'CENTER' +""" % I,'I') \ No newline at end of file diff --git a/bin/reportlab/docs/userguide/ch9_future.py b/bin/reportlab/docs/userguide/ch9_future.py new file mode 100644 index 00000000000..6ac5b94e502 --- /dev/null +++ b/bin/reportlab/docs/userguide/ch9_future.py @@ -0,0 +1,35 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/ch9_future.py +from reportlab.tools.docco.rl_doc_utils import * + +heading1("Future Directions") + +disc("""We have a very long list of things we plan to do +and what we do first will most likely be inspired by customer +or user interest. +""") + +disc(""" +We plan to provide a large number of pre-designed Platypus example +document types -- brochure, newsletter, business letter, thesis, memo, +etcetera, to give our users a better boost towards the solutions they +desire. +""") + +disc(""" +We plan to fully support adding fonts and internationalization, which are +not well supported in the current release.""") + +disc(""" +We plan to fully support some of the more obscure features of PDF +such as general hyperlinks, which are not yet well supported. +""") + +disc(""" +We are also open for suggestions. Please let us know what you think +is missing. You can also offer patches or contributions. Please +look to $http://www.reportlab.com$ for the latest mailing list and +contact information.""") + +# this comment is a trivial test of SF checkin rights - delete it some time! AR 2001-04-17 \ No newline at end of file diff --git a/bin/reportlab/docs/userguide/genuserguide.py b/bin/reportlab/docs/userguide/genuserguide.py new file mode 100644 index 00000000000..3166a6a6036 --- /dev/null +++ b/bin/reportlab/docs/userguide/genuserguide.py @@ -0,0 +1,85 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/docs/userguide/genuserguide.py +__version__=''' $Id: genuserguide.py 2900 2006-05-22 21:49:00Z andy $ ''' +__doc__ = """ +This module contains the script for building the user guide. +""" +def run(pagesize=None, verbose=0, outDir=None): + import os + from reportlab.tools.docco.rl_doc_utils import setStory, getStory, RLDocTemplate, defaultPageSize + from reportlab.tools.docco import rl_doc_utils + from reportlab.lib.utils import open_and_read, _RL_DIR + if not outDir: outDir = os.path.join(_RL_DIR,'docs') + destfn = os.path.join(outDir,'userguide.pdf') + doc = RLDocTemplate(destfn,pagesize = pagesize or defaultPageSize) + + #this builds the story + setStory() + G = {} + exec 'from reportlab.tools.docco.rl_doc_utils import *' in G, G + for f in ( + 'ch1_intro', + 'ch2_graphics', + 'ch2a_fonts', + 'ch3_pdffeatures', + 'ch4_platypus_concepts', + 'ch5_paragraphs', + 'ch6_tables', + 'ch7_custom', + 'ch9_future', + 'app_demos', + ): + exec open_and_read(f+'.py',mode='t') in G, G + del G + + story = getStory() + if verbose: print 'Built story contains %d flowables...' % len(story) + doc.build(story) + if verbose: print 'Saved "%s"' % destfn + +def makeSuite(): + "standard test harness support - run self as separate process" + from reportlab.test.utils import ScriptThatMakesFileTest + return ScriptThatMakesFileTest('../docs/userguide', 'genuserguide.py', 'userguide.pdf') + +def main(): + import sys + outDir = filter(lambda x: x[:9]=='--outdir=',sys.argv) + if outDir: + outDir = outDir[0] + sys.argv.remove(outDir) + outDir = outDir[9:] + else: + outDir = None + verbose = '-s' not in sys.argv + if not verbose: sys.argv.remove('-s') + timing = '-timing' in sys.argv + if timing: sys.argv.remove('-timing') + prof = '-prof' in sys.argv + if prof: sys.argv.remove('-prof') + + if len(sys.argv) > 1: + try: + pagesize = (w,h) = eval(sys.argv[1]) + except: + print 'Expected page size in argument 1', sys.argv[1] + raise + if verbose: + print 'set page size to',sys.argv[1] + else: + pagesize = None + if timing: + from time import time + t0 = time() + run(pagesize, verbose,outDir) + if verbose: + print 'Generation of userguide took %.2f seconds' % (time()-t0) + elif prof: + import profile + profile.run('run(pagesize,verbose,outDir)','genuserguide.stats') + else: + run(pagesize, verbose,outDir) +if __name__=="__main__": + main() diff --git a/bin/reportlab/docs/userguide/testfile.txt b/bin/reportlab/docs/userguide/testfile.txt new file mode 100644 index 00000000000..020f3d7cbaf --- /dev/null +++ b/bin/reportlab/docs/userguide/testfile.txt @@ -0,0 +1,2 @@ +Test of ability to create new files in sourceforge CVS, which +seems not to be working. Can be removed any time. \ No newline at end of file diff --git a/bin/reportlab/extensions/README b/bin/reportlab/extensions/README new file mode 100644 index 00000000000..a0d9fc5ca28 --- /dev/null +++ b/bin/reportlab/extensions/README @@ -0,0 +1,25 @@ +This directory is intended to act as a placeholder for extending ReportLab +with extra extensions. It has been packagised with an empty __init__.py. + +So a typical extender should add his package extension as + + reportlab/extensions/great_extension/__init__.py + /dingo.py + /etc etc + +and single modules as + + reportlab/extensions/my_module.py + +Then client code can do + + from reportlab.extensions.great_extension import dingo + +if you extend with just a single module it might be simpler to add that +into extensions so that you could do + + from reportlab.extensions import my_module. + + +ReportLab can take no responsibility for name clashes and problems caused by +modules and packages in reportlab/extensions. diff --git a/bin/reportlab/extensions/__init__.py b/bin/reportlab/extensions/__init__.py new file mode 100644 index 00000000000..7e0ffa63afd --- /dev/null +++ b/bin/reportlab/extensions/__init__.py @@ -0,0 +1,7 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/extensions/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__=""" +""" +# No usable content. Do not add to this file! diff --git a/bin/reportlab/fonts/00readme.txt b/bin/reportlab/fonts/00readme.txt new file mode 100644 index 00000000000..08d545a8666 --- /dev/null +++ b/bin/reportlab/fonts/00readme.txt @@ -0,0 +1,8 @@ +This directory is a convenient place to put +fonts where Reportlab will find them. If running +in a server environment, this is a good place to +put Type 1 fonts needed by your application. If +in a desktop environment, you might prefer to add +your font directories to the T1SearchPath in +reportlab/rl_config.py instead. + diff --git a/bin/reportlab/fonts/LeERC___.AFM b/bin/reportlab/fonts/LeERC___.AFM new file mode 100644 index 00000000000..fe491e473a2 --- /dev/null +++ b/bin/reportlab/fonts/LeERC___.AFM @@ -0,0 +1,93 @@ +StartFontMetrics 2.0 +Comment Generated by RoboFog 08-06-2001 4:26:40 PM +FontName LettErrorRobot-Chrome +FullName LettErrorRobot-Chrome +FamilyName LettErrorRobot +Weight Medium +Notice (C) 1998-2001 LettError, Just van Rossum, Erik van Blokland, http://www.letterror.com/ +ItalicAngle 0 +IsFixedPitch false +UnderlinePosition -133 +UnderlineThickness 20 +Version 001.000 +EncodingScheme AdobeStandardEncoding +FontBBox -53 -252 1047 752 +CapHeight 647 +XHeight 548 +Descender -252 +Ascender 747 +StartCharMetrics 68 +C 32 ; WX 300 ; N space ; B 0 0 0 0 ; +C 33 ; WX 300 ; N exclam ; B 48 -52 253 652 ; +C 38 ; WX 700 ; N ampersand ; B 48 -47 653 647 ; +C 42 ; WX 700 ; N asterisk ; B 48 148 653 752 ; +C 48 ; WX 700 ; N zero ; B 53 -47 648 548 ; +C 49 ; WX 500 ; N one ; B 48 -47 453 553 ; +C 50 ; WX 600 ; N two ; B 48 -47 553 548 ; +C 51 ; WX 600 ; N three ; B 48 -147 553 548 ; +C 52 ; WX 700 ; N four ; B 48 -152 653 553 ; +C 53 ; WX 600 ; N five ; B 48 -147 553 548 ; +C 54 ; WX 600 ; N six ; B 53 -47 553 647 ; +C 55 ; WX 600 ; N seven ; B 48 -152 548 548 ; +C 56 ; WX 600 ; N eight ; B 48 -47 553 647 ; +C 57 ; WX 600 ; N nine ; B 48 -147 548 548 ; +C 63 ; WX 500 ; N question ; B 48 -52 448 647 ; +C 64 ; WX 800 ; N at ; B 53 -47 748 647 ; +C 65 ; WX 700 ; N A ; B 53 -52 648 652 ; +C 66 ; WX 600 ; N B ; B 53 -47 553 647 ; +C 67 ; WX 600 ; N C ; B 53 -47 553 647 ; +C 68 ; WX 700 ; N D ; B 53 -47 648 647 ; +C 69 ; WX 600 ; N E ; B 53 -47 553 647 ; +C 70 ; WX 600 ; N F ; B 53 -52 553 647 ; +C 71 ; WX 700 ; N G ; B 53 -47 653 647 ; +C 72 ; WX 700 ; N H ; B 53 -52 648 652 ; +C 73 ; WX 300 ; N I ; B 53 -52 248 652 ; +C 74 ; WX 300 ; N J ; B -53 -252 248 652 ; +C 75 ; WX 700 ; N K ; B 53 -52 653 652 ; +C 76 ; WX 600 ; N L ; B 53 -47 553 652 ; +C 77 ; WX 900 ; N M ; B 53 -52 848 652 ; +C 78 ; WX 700 ; N N ; B 53 -52 648 652 ; +C 79 ; WX 700 ; N O ; B 53 -47 648 647 ; +C 80 ; WX 600 ; N P ; B 53 -52 548 647 ; +C 81 ; WX 700 ; N Q ; B 53 -252 653 647 ; +C 82 ; WX 600 ; N R ; B 53 -52 653 647 ; +C 83 ; WX 600 ; N S ; B 48 -47 553 647 ; +C 84 ; WX 700 ; N T ; B 48 -52 653 647 ; +C 85 ; WX 700 ; N U ; B 53 -47 648 652 ; +C 86 ; WX 700 ; N V ; B 53 -52 648 652 ; +C 87 ; WX 1100 ; N W ; B 53 -52 1047 652 ; +C 88 ; WX 700 ; N X ; B 48 -52 653 652 ; +C 89 ; WX 700 ; N Y ; B 53 -52 648 652 ; +C 90 ; WX 700 ; N Z ; B 48 -47 653 647 ; +C 97 ; WX 600 ; N a ; B 48 -47 548 548 ; +C 98 ; WX 600 ; N b ; B 53 -47 548 752 ; +C 99 ; WX 600 ; N c ; B 53 -47 553 548 ; +C 100 ; WX 600 ; N d ; B 53 -47 548 752 ; +C 101 ; WX 600 ; N e ; B 53 -47 553 548 ; +C 102 ; WX 400 ; N f ; B -53 -52 453 747 ; +C 103 ; WX 600 ; N g ; B 48 -247 548 548 ; +C 104 ; WX 600 ; N h ; B 53 -52 548 752 ; +C 105 ; WX 300 ; N i ; B 48 -52 253 752 ; +C 106 ; WX 300 ; N j ; B -53 -252 253 752 ; +C 107 ; WX 600 ; N k ; B 53 -52 553 752 ; +C 108 ; WX 300 ; N l ; B 53 -52 248 752 ; +C 109 ; WX 900 ; N m ; B 53 -52 848 548 ; +C 110 ; WX 600 ; N n ; B 53 -52 548 548 ; +C 111 ; WX 600 ; N o ; B 53 -47 548 548 ; +C 112 ; WX 600 ; N p ; B 53 -252 548 548 ; +C 113 ; WX 600 ; N q ; B 53 -252 548 548 ; +C 114 ; WX 400 ; N r ; B 53 -52 453 553 ; +C 115 ; WX 600 ; N s ; B 48 -47 553 548 ; +C 116 ; WX 400 ; N t ; B -53 -47 453 652 ; +C 117 ; WX 600 ; N u ; B 53 -47 548 553 ; +C 118 ; WX 600 ; N v ; B 53 -52 548 553 ; +C 119 ; WX 900 ; N w ; B 53 -52 848 553 ; +C 120 ; WX 700 ; N x ; B 48 -52 653 553 ; +C 121 ; WX 600 ; N y ; B 48 -252 548 553 ; +C 122 ; WX 500 ; N z ; B -53 -47 553 548 ; +EndCharMetrics +StartKernData +StartKernPairs 0 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/bin/reportlab/fonts/LeERC___.PFB b/bin/reportlab/fonts/LeERC___.PFB new file mode 100644 index 00000000000..c1cf7ec189a Binary files /dev/null and b/bin/reportlab/fonts/LeERC___.PFB differ diff --git a/bin/reportlab/fonts/luxiserif.ttf b/bin/reportlab/fonts/luxiserif.ttf new file mode 100644 index 00000000000..daa8ad8cccd Binary files /dev/null and b/bin/reportlab/fonts/luxiserif.ttf differ diff --git a/bin/reportlab/fonts/luxiserif_license.txt b/bin/reportlab/fonts/luxiserif_license.txt new file mode 100644 index 00000000000..e8b344b8542 --- /dev/null +++ b/bin/reportlab/fonts/luxiserif_license.txt @@ -0,0 +1,40 @@ +Bigelow & Holmes Inc and URW++ GmbH Luxi font license + +Luxi fonts copyright (c) 2001 by Bigelow & Holmes Inc. Luxi font instruction +code copyright (c) 2001 by URW++ GmbH. All Rights Reserved. Luxi is a +registered trademark of Bigelow & Holmes Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of these Fonts and associated documentation files (the "Font Software"), to +deal in the Font Software, including without limitation the rights to use, +copy, merge, publish, distribute, sublicense, and/or sell copies of the Font +Software, and to permit persons to whom the Font Software is furnished to do +so, subject to the following conditions: + +The above copyright and trademark notices and this permission notice shall be +included in all copies of one or more of the Font Software. + +The Font Software may not be modified, altered, or added to, and in +particular the designs of glyphs or characters in the Fonts may not be +modified nor may additional glyphs or characters be added to the Fonts. This +License becomes null and void when the Fonts or Font Software have been +modified. + +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, +TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BIGELOW & HOLMES INC. OR URW++ +GMBH. BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY +GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN +AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR +INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT +SOFTWARE. + +Except as contained in this notice, the names of Bigelow & Holmes Inc. and +URW++ GmbH. shall not be used in advertising or otherwise to promote the +sale, use or other dealings in this Font Software without prior written +authorization from Bigelow & Holmes Inc. and URW++ GmbH. + +For further information, contact: + +info@urwpp.de or design@bigelowandholmes.com diff --git a/bin/reportlab/fonts/rina.ttf b/bin/reportlab/fonts/rina.ttf new file mode 100644 index 00000000000..f21f0bae3a3 Binary files /dev/null and b/bin/reportlab/fonts/rina.ttf differ diff --git a/bin/reportlab/fonts/rina_license.txt b/bin/reportlab/fonts/rina_license.txt new file mode 100644 index 00000000000..3a82e7a1f48 --- /dev/null +++ b/bin/reportlab/fonts/rina_license.txt @@ -0,0 +1,93 @@ +The fonts contained in this archive are Freeware. +No payment is required for the use of these fonts. They're free! +Commercial use? Sure, but a donation or a product sample would be +appreciated. + +$40 US is the usual amount per font for commercial use but any amount is appreciated. + +I make all the fonts (around 370 of them) on my web page and they're all free. +I offer Deluxe versions of some of my fonts for sale. They contain several + weights and styles of each font. Just click on the Deluxe button at Larabie Fonts to see them. + +The page is called Larabie Fonts +It can be found at various mirror sites. + +try: +http://www.larabiefonts.com +http://uk.zarcrom.com/font/ +http://swankarmy.net/larabiefonts/ +http://come.to/larabiefonts + +I've provided the world with about 300 free fonts, +so if you'd like to make a donation I'd be more than happy to accept it. + +No donation is too small! Music and artwork are good too. +If you have some CD's you're not listening to anymore, send 'em along! + +Send anything at all to + +Ray Larabie +61 Wesley Ave. +Port Credit +Ontario, CANADA +L5H 2M8 + +If you decide to send a cheque (that's how we spell it in Canada) +make it payable to Ray Larabie. If you want to double check the address +have a look at the donation section on any of my webpages. + +Canadian or US funds? Any funds are fine with me. +Whatever's easy for you. + + +Ray Larabie +drowsy@cheerful.com +or... +rlarabie@hotmail.com + +------------------------------- +Larabie Fonts End-user license agreement software product from Larabie Fonts +--------------------------------------------------- + +SOFTWARE PRODUCT LICENSE + +The SOFTWARE PRODUCT is protected by copyright laws and international copyright treaties, +as well as other intellectual property laws and treaties. The SOFTWARE PRODUCT is licensed, +not sold. + +1. GRANT OF LICENSE. This document grants you the following rights: + +- Installation and Use. You may install and use an unlimited number of copies of the +SOFTWARE PRODUCT. + +- Reproduction and Distribution. You may reproduce and distribute an unlimited number of +copies of the SOFTWARE PRODUCT; provided that each copy shall be a true and complete copy, +including all copyright and trademark notices (if applicable) , and shall be accompanied by +a copy of this text file. Copies of the SOFTWARE PRODUCT may not be distributed for profit +either on a standalone basis or included as part of your own product unless by prior +permission of Larabie Fonts. + +2. DESCRIPTION OF OTHER RIGHTS AND LIMITATIONS. + +- Restrictions on Alteration. You may not rename, edit or create any derivative works from +the SOFTWARE PRODUCT, other than subsetting when embedding them in documents unless you have +permission from Larabie Fonts. + +LIMITED WARRANTY +NO WARRANTIES. Larabie Fonts expressly disclaims any warranty for the SOFTWARE PRODUCT. The +SOFTWARE PRODUCT and any related documentation is provided "as is" without warranty of any +kind, either express or implied, including, without limitation, the implied warranties or +merchantability, fitness for a particular purpose, or noninfringement. The entire risk +arising out of use or performance of the SOFTWARE PRODUCT remains with you. + +NO LIABILITY FOR CONSEQUENTIAL DAMAGES. In no event shall Larabie Fonts be liable for any +damages whatsoever (including, without limitation, damages for loss of business profits, +business interruption, loss of business information, or any other pecuniary loss) arising +out of the use of or inability to use this product, even if Larabie Fonts has been advised +of the possibility of such damages. + +3. MISCELLANEOUS + +Should you have any questions concerning this document, or if you desire to contact +Larabie Fonts for any reason, please contact rlarabie@hotmail.com , or write: Ray Larabie, +61 Wesley Ave. Mississauga, ON Canada L5H 2M8 \ No newline at end of file diff --git a/bin/reportlab/graphics/__init__.py b/bin/reportlab/graphics/__init__.py new file mode 100644 index 00000000000..0e4597012a4 --- /dev/null +++ b/bin/reportlab/graphics/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' \ No newline at end of file diff --git a/bin/reportlab/graphics/barcode/README b/bin/reportlab/graphics/barcode/README new file mode 100644 index 00000000000..33cf57b7a66 --- /dev/null +++ b/bin/reportlab/graphics/barcode/README @@ -0,0 +1,59 @@ +Symbologies Currently Supported +=============================== + +The following have, at a minimum, been verified to scan with a WASP +CCD barcode scanner (found one bug in my code, two in the scanner!). +Some have had more extensive testing: + + Interleaved 2 of 5 + MSI + Codabar + Code 39 (Standard Character Set) + Code 39 (Extended Character Set) + Code 93 (Standard Character Set) + Code 93 (Extended Character Set) + Code 128 (Automatic use of A, B, C, with some optimizations -- + more coming) + +The following have been tested by sending a fair number of mailpieces +with them: + + USPS FIM + USPS POSTNET + +The following have not been tested, as none of the scanners I have +access to support them: + + Code 11 + + +Future Plans, Consulting +======================== + +Soon: + +I plan to implement the following linear codes soon: + + UPC/EAN(/JAN) + +The following are in progress, but I lack a way to verify them +(scanners I have access to don't read them), and I don't have complete +specs for the UK style. + + Royal Mail 4-State (UK/NL/etc style, and Australian style) + +Down the road, I'd like to do some 2D symbologies. Likely first candidate +is PDF417. MaxiCode, Aztec Code, and some of the stacked symbologies are +also good candidates. + +I am available to do implementation of additional symbologies for hire. +Because I enjoy hacking barcodes, my rates for work in this particular +area are very low and are mainly to help offset costs associated with +obtaining related documents and/or to buy or gain access to scanning +equipment for symbologies if I don't already have a scanner that +supports them. Loans of equipment are also accepted. + +For more information, contact: + +Ty Sarna +tsarna@sarna.org diff --git a/bin/reportlab/graphics/barcode/TODO b/bin/reportlab/graphics/barcode/TODO new file mode 100644 index 00000000000..5f2ffcb6699 --- /dev/null +++ b/bin/reportlab/graphics/barcode/TODO @@ -0,0 +1,24 @@ +See also README for some plans and info on consulting. + +- Overall framework docs + +- Finish Aussie Rules 4-State, for which I have complete docs now (yay + USPS and aupost.com.au for putting specs online. Too bad UKPost doesn't.) + +- Investigate USPS PLANET stuff + +- Higher-level objects that handle barcoded address blocks with correct + spacings and such (US, AU, UK/etc?) + +- Even higher-level objects that represent mailpieces and place the + above-style address block objects, FIM codes, "place stamp here" blocks, + etc, correctly? + +- Framework for laying out labels on various styles of n-up label + sheets, like Avery labels, etc? + +- Decide if Plessey is worth doing. MSI-like (MSI is actually derived from + it), but specs were never formalized. Probably only useful for legacy + applications. If you need it, mail me. + +- Get someone to test Code 11, or find a scanner that handles it diff --git a/bin/reportlab/graphics/barcode/VERSION b/bin/reportlab/graphics/barcode/VERSION new file mode 100644 index 00000000000..33ad517e76a --- /dev/null +++ b/bin/reportlab/graphics/barcode/VERSION @@ -0,0 +1 @@ +0.9 diff --git a/bin/reportlab/graphics/barcode/__init__.py b/bin/reportlab/graphics/barcode/__init__.py new file mode 100644 index 00000000000..bde067f504a --- /dev/null +++ b/bin/reportlab/graphics/barcode/__init__.py @@ -0,0 +1,126 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +__version__ = '0.9' + +def getCodes(): + """Returns a dict mapping code names to widgets""" + + from widgets import (BarcodeI2of5, + BarcodeCode128, + BarcodeStandard93, + BarcodeExtended93, + BarcodeStandard39, + BarcodeExtended39, + BarcodeMSI, + BarcodeCodabar, + BarcodeCode11, + BarcodeFIM, + BarcodePOSTNET) + + #newer codes will typically get their own module + from eanbc import Ean13BarcodeWidget, Ean8BarcodeWidget + + + #the module exports a dictionary of names to widgets, to make it easy for + #apps and doc tools to display information about them. + codes = {} + for widget in ( + BarcodeI2of5, + BarcodeCode128, + BarcodeStandard93, + BarcodeExtended93, + BarcodeStandard39, + BarcodeExtended39, + BarcodeMSI, + BarcodeCodabar, + BarcodeCode11, + BarcodeFIM, + BarcodePOSTNET, + Ean13BarcodeWidget, + Ean8BarcodeWidget, + ): + codeName = widget.codeName + codes[codeName] = widget + + return codes + +def getCodeNames(): + """Returns sorted list of supported bar code names""" + return sorted(getCodes().keys()) + +def createBarcodeDrawing(codeName, **options): + """This creates and returns a drawing with a barcode. + """ + from reportlab.graphics.shapes import Drawing, Group + + codes = getCodes() + bcc = codes[codeName] + width = options.pop('width',None) + height = options.pop('height',None) + isoScale = options.pop('isoScale',0) + kw = {} + for k,v in options.iteritems(): + if k.startswith('_') or k in bcc._attrMap: kw[k] = v + bc = bcc(**kw) + + #size it after setting the data + x1, y1, x2, y2 = bc.getBounds() + w = float(x2 - x1) + h = float(y2 - y1) + sx = width not in ('auto',None) + sy = height not in ('auto',None) + if sx or sy: + sx = sx and width/w or 1.0 + sy = sy and height/h or 1.0 + if isoScale: + if sx<1.0 and sy<1.0: + sx = sy = max(sx,sy) + else: + sx = sy = min(sx,sy) + + w *= sx + h *= sy + else: + sx = sy = 1 + + #bc.x = -sx*x1 + #bc.y = -sy*y1 + d = Drawing(width=w,height=h,transform=[sx,0,0,sy,-sx*x1,-sy*y1]) + d.add(bc, "_bc") + return d + +def createBarcodeImageInMemory(codeName, **options): + """This creates and returns barcode as an image in memory. + """ + d = createBarcodeDrawing(codeName, **options) + return d.asString(format) diff --git a/bin/reportlab/graphics/barcode/code128.py b/bin/reportlab/graphics/barcode/code128.py new file mode 100644 index 00000000000..443edb06c84 --- /dev/null +++ b/bin/reportlab/graphics/barcode/code128.py @@ -0,0 +1,322 @@ +# +# Copyright (c) 2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import MultiWidthBarcode +from string import digits + +_patterns = { + 0 : 'BaBbBb', 1 : 'BbBaBb', 2 : 'BbBbBa', + 3 : 'AbAbBc', 4 : 'AbAcBb', 5 : 'AcAbBb', + 6 : 'AbBbAc', 7 : 'AbBcAb', 8 : 'AcBbAb', + 9 : 'BbAbAc', 10 : 'BbAcAb', 11 : 'BcAbAb', + 12 : 'AaBbCb', 13 : 'AbBaCb', 14 : 'AbBbCa', + 15 : 'AaCbBb', 16 : 'AbCaBb', 17 : 'AbCbBa', + 18 : 'BbCbAa', 19 : 'BbAaCb', 20 : 'BbAbCa', + 21 : 'BaCbAb', 22 : 'BbCaAb', 23 : 'CaBaCa', + 24 : 'CaAbBb', 25 : 'CbAaBb', 26 : 'CbAbBa', + 27 : 'CaBbAb', 28 : 'CbBaAb', 29 : 'CbBbAa', + 30 : 'BaBaBc', 31 : 'BaBcBa', 32 : 'BcBaBa', + 33 : 'AaAcBc', 34 : 'AcAaBc', 35 : 'AcAcBa', + 36 : 'AaBcAc', 37 : 'AcBaAc', 38 : 'AcBcAa', + 39 : 'BaAcAc', 40 : 'BcAaAc', 41 : 'BcAcAa', + 42 : 'AaBaCc', 43 : 'AaBcCa', 44 : 'AcBaCa', + 45 : 'AaCaBc', 46 : 'AaCcBa', 47 : 'AcCaBa', + 48 : 'CaCaBa', 49 : 'BaAcCa', 50 : 'BcAaCa', + 51 : 'BaCaAc', 52 : 'BaCcAa', 53 : 'BaCaCa', + 54 : 'CaAaBc', 55 : 'CaAcBa', 56 : 'CcAaBa', + 57 : 'CaBaAc', 58 : 'CaBcAa', 59 : 'CcBaAa', + 60 : 'CaDaAa', 61 : 'BbAdAa', 62 : 'DcAaAa', + 63 : 'AaAbBd', 64 : 'AaAdBb', 65 : 'AbAaBd', + 66 : 'AbAdBa', 67 : 'AdAaBb', 68 : 'AdAbBa', + 69 : 'AaBbAd', 70 : 'AaBdAb', 71 : 'AbBaAd', + 72 : 'AbBdAa', 73 : 'AdBaAb', 74 : 'AdBbAa', + 75 : 'BdAbAa', 76 : 'BbAaAd', 77 : 'DaCaAa', + 78 : 'BdAaAb', 79 : 'AcDaAa', 80 : 'AaAbDb', + 81 : 'AbAaDb', 82 : 'AbAbDa', 83 : 'AaDbAb', + 84 : 'AbDaAb', 85 : 'AbDbAa', 86 : 'DaAbAb', + 87 : 'DbAaAb', 88 : 'DbAbAa', 89 : 'BaBaDa', + 90 : 'BaDaBa', 91 : 'DaBaBa', 92 : 'AaAaDc', + 93 : 'AaAcDa', 94 : 'AcAaDa', 95 : 'AaDaAc', + 96 : 'AaDcAa', 97 : 'DaAaAc', 98 : 'DaAcAa', + 99 : 'AaCaDa', 100 : 'AaDaCa', 101 : 'CaAaDa', + 102 : 'DaAaCa', 103 : 'BaAdAb', 104 : 'BaAbAd', + 105 : 'BaAbCb', 106 : 'BcCaAaB' +} + +starta, startb, startc, stop = 103, 104, 105, 106 + +seta = { + ' ' : 0, '!' : 1, '"' : 2, '#' : 3, + '$' : 4, '%' : 5, '&' : 6, '\'' : 7, + '(' : 8, ')' : 9, '*' : 10, '+' : 11, + ',' : 12, '-' : 13, '.' : 14, '/' : 15, + '0' : 16, '1' : 17, '2' : 18, '3' : 19, + '4' : 20, '5' : 21, '6' : 22, '7' : 23, + '8' : 24, '9' : 25, ':' : 26, ';' : 27, + '<' : 28, '=' : 29, '>' : 30, '?' : 31, + '@' : 32, 'A' : 33, 'B' : 34, 'C' : 35, + 'D' : 36, 'E' : 37, 'F' : 38, 'G' : 39, + 'H' : 40, 'I' : 41, 'J' : 42, 'K' : 43, + 'L' : 44, 'M' : 45, 'N' : 46, 'O' : 47, + 'P' : 48, 'Q' : 49, 'R' : 50, 'S' : 51, + 'T' : 52, 'U' : 53, 'V' : 54, 'W' : 55, + 'X' : 56, 'Y' : 57, 'Z' : 58, '[' : 59, + '\\' : 60, ']' : 61, '^' : 62, '_' : 63, + '\x00' : 64, '\x01' : 65, '\x02' : 66, '\x03' : 67, + '\x04' : 68, '\x05' : 69, '\x06' : 70, '\x07' : 71, + '\x08' : 72, '\x09' : 73, '\x0a' : 74, '\x0b' : 75, + '\x0c' : 76, '\x0d' : 77, '\x0e' : 78, '\x0f' : 79, + '\x10' : 80, '\x11' : 81, '\x12' : 82, '\x13' : 83, + '\x14' : 84, '\x15' : 85, '\x16' : 86, '\x17' : 87, + '\x18' : 88, '\x19' : 89, '\x1a' : 90, '\x1b' : 91, + '\x1c' : 92, '\x1d' : 93, '\x1e' : 94, '\x1f' : 95, + '\xf3' : 96, '\xf2' : 97, 'SHIFT' : 98, 'TO_C' : 99, + 'TO_B' : 100, '\xf4' : 101, '\xf1' : 102 +} + +setb = { + ' ' : 0, '!' : 1, '"' : 2, '#' : 3, + '$' : 4, '%' : 5, '&' : 6, '\'' : 7, + '(' : 8, ')' : 9, '*' : 10, '+' : 11, + ',' : 12, '-' : 13, '.' : 14, '/' : 15, + '0' : 16, '1' : 17, '2' : 18, '3' : 19, + '4' : 20, '5' : 21, '6' : 22, '7' : 23, + '8' : 24, '9' : 25, ':' : 26, ';' : 27, + '<' : 28, '=' : 29, '>' : 30, '?' : 31, + '@' : 32, 'A' : 33, 'B' : 34, 'C' : 35, + 'D' : 36, 'E' : 37, 'F' : 38, 'G' : 39, + 'H' : 40, 'I' : 41, 'J' : 42, 'K' : 43, + 'L' : 44, 'M' : 45, 'N' : 46, 'O' : 47, + 'P' : 48, 'Q' : 49, 'R' : 50, 'S' : 51, + 'T' : 52, 'U' : 53, 'V' : 54, 'W' : 55, + 'X' : 56, 'Y' : 57, 'Z' : 58, '[' : 59, + '\\' : 60, ']' : 61, '^' : 62, '_' : 63, + '`' : 64, 'a' : 65, 'b' : 66, 'c' : 67, + 'd' : 68, 'e' : 69, 'f' : 70, 'g' : 71, + 'h' : 72, 'i' : 73, 'j' : 74, 'k' : 75, + 'l' : 76, 'm' : 77, 'n' : 78, 'o' : 79, + 'p' : 80, 'q' : 81, 'r' : 82, 's' : 83, + 't' : 84, 'u' : 85, 'v' : 86, 'w' : 87, + 'x' : 88, 'y' : 89, 'z' : 90, '{' : 91, + '|' : 92, '}' : 93, '~' : 94, '\x7f' : 95, + '\xf3' : 96, '\xf2' : 97, 'SHIFT' : 98, 'TO_C' : 99, + '\xf4' : 100, 'TO_A' : 101, '\xf1' : 102 +} + +setc = { + '00': 0, '01': 1, '02': 2, '03': 3, '04': 4, + '05': 5, '06': 6, '07': 7, '08': 8, '09': 9, + '10':10, '11':11, '12':12, '13':13, '14':14, + '15':15, '16':16, '17':17, '18':18, '19':19, + '20':20, '21':21, '22':22, '23':23, '24':24, + '25':25, '26':26, '27':27, '28':28, '29':29, + '30':30, '31':31, '32':32, '33':33, '34':34, + '35':35, '36':36, '37':37, '38':38, '39':39, + '40':40, '41':41, '42':42, '43':43, '44':44, + '45':45, '46':46, '47':47, '48':48, '49':49, + '50':50, '51':51, '52':52, '53':53, '54':54, + '55':55, '56':56, '57':57, '58':58, '59':59, + '60':60, '61':61, '62':62, '63':63, '64':64, + '65':65, '66':66, '67':67, '68':68, '69':69, + '70':70, '71':71, '72':72, '73':73, '74':74, + '75':75, '76':76, '77':77, '78':78, '79':79, + '80':80, '81':81, '82':82, '83':83, '84':84, + '85':85, '86':86, '87':87, '88':88, '89':89, + '90':90, '91':91, '92':92, '93':93, '94':94, + '95':95, '96':96, '97':97, '98':98, '99':99, + + 'TO_B' : 100, 'TO_A' : 101, '\xf1' : 102 +} + +setmap = { + 'TO_A' : (seta, setb), + 'TO_B' : (setb, seta), + 'TO_C' : (setc, None), + 'START_A' : (starta, seta, setb), + 'START_B' : (startb, setb, seta), + 'START_C' : (startc, setc, None), +} +tos = setmap.keys() + +class Code128(MultiWidthBarcode): + """ + Code 128 is a very compact symbology that can encode the entire + 128 character ASCII set, plus 4 special control codes, + (FNC1-FNC4, expressed in the input string as \xf1 to \xf4). + Code 128 can also encode digits at double density (2 per byte) + and has a mandatory checksum. Code 128 is well supported and + commonly used -- for example, by UPS for tracking labels. + + Because of these qualities, Code 128 is probably the best choice + for a linear symbology today (assuming you have a choice). + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + Sources of Information on Code 128: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/code_128.html + http://www.adams1.com/pub/russadam/128code.html + http://www.barcodeman.com/c128.html + + Official Spec, "ANSI/AIM BC4-1999, ISS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + barWidth = inch * 0.0075 + lquiet = None + rquiet = None + quiet = 1 + barHeight = None + def __init__(self, value='', **args): + + if type(value) is type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + if self.rquiet is None: + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + MultiWidthBarcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if ord(c) > 127 and c not in '\xf1\xf2\xf3\xf4': + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def _trailingDigitsToC(self, l): + # Optimization: trailing digits -> set C double-digits + c = 1 + savings = -1 # the TO_C costs one character + rl = ['STOP'] + while c < len(l): + i = (-c - 1) + if l[i] == '\xf1': + c = c + 1 + rl.insert(0, '\xf1') + continue + elif len(l[i]) == 1 and l[i] in digits \ + and len(l[i-1]) == 1 and l[i-1] in digits: + c += 2 + savings += 1 + rl.insert(0, l[i-1] + l[i]) + continue + else: + break + if savings > 0: + return l[:-c] + ['TO_C'] + rl + else: + return l + + def encode(self): + # First, encode using only B + s = self.validated + l = ['START_B'] + for c in s: + if not setb.has_key(c): + l = l + ['TO_A', c, 'TO_B'] + else: + l.append(c) + l.append('STOP') + + l = self._trailingDigitsToC(l) + + # Finally, replace START_X,TO_Y with START_Y + if l[1] in tos: + l[:2] = ['START_' + l[1][-1]] + +# print `l` + + # encode into numbers + start, set, shset = setmap[l[0]] + e = [start] + + l = l[1:-1] + while l: + c = l[0] + if c == 'SHIFT': + e = e + [set[c], shset[l[1]]] + l = l[2:] + elif c in tos: + e.append(set[c]) + set, shset = setmap[c] + l = l[1:] + else: + e.append(set[c]) + l = l[1:] + + c = e[0] + for i in range(1, len(e)): + c = c + i * e[i] + self.encoded = e + [c % 103, stop] + return self.encoded + + def decompose(self): + self.decomposed = ''.join([_patterns[c] for c in self.encoded]) + return self.decomposed + + def _humanText(self): + return self.value diff --git a/bin/reportlab/graphics/barcode/code39.py b/bin/reportlab/graphics/barcode/code39.py new file mode 100644 index 00000000000..697b54045b7 --- /dev/null +++ b/bin/reportlab/graphics/barcode/code39.py @@ -0,0 +1,248 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import Barcode +import string + +_patterns = { + '0': ("bsbSBsBsb", 0), '1': ("BsbSbsbsB", 1), + '2': ("bsBSbsbsB", 2), '3': ("BsBSbsbsb", 3), + '4': ("bsbSBsbsB", 4), '5': ("BsbSBsbsb", 5), + '6': ("bsBSBsbsb", 6), '7': ("bsbSbsBsB", 7), + '8': ("BsbSbsBsb", 8), '9': ("bsBSbsBsb", 9), + 'A': ("BsbsbSbsB", 10), 'B': ("bsBsbSbsB", 11), + 'C': ("BsBsbSbsb", 12), 'D': ("bsbsBSbsB", 13), + 'E': ("BsbsBSbsb", 14), 'F': ("bsBsBSbsb", 15), + 'G': ("bsbsbSBsB", 16), 'H': ("BsbsbSBsb", 17), + 'I': ("bsBsbSBsb", 18), 'J': ("bsbsBSBsb", 19), + 'K': ("BsbsbsbSB", 20), 'L': ("bsBsbsbSB", 21), + 'M': ("BsBsbsbSb", 22), 'N': ("bsbsBsbSB", 23), + 'O': ("BsbsBsbSb", 24), 'P': ("bsBsBsbSb", 25), + 'Q': ("bsbsbsBSB", 26), 'R': ("BsbsbsBSb", 27), + 'S': ("bsBsbsBSb", 28), 'T': ("bsbsBsBSb", 29), + 'U': ("BSbsbsbsB", 30), 'V': ("bSBsbsbsB", 31), + 'W': ("BSBsbsbsb", 32), 'X': ("bSbsBsbsB", 33), + 'Y': ("BSbsBsbsb", 34), 'Z': ("bSBsBsbsb", 35), + '-': ("bSbsbsBsB", 36), '.': ("BSbsbsBsb", 37), + ' ': ("bSBsbsBsb", 38), '*': ("bSbsBsBsb", 39), + '$': ("bSbSbSbsb", 40), '/': ("bSbSbsbSb", 41), + '+': ("bSbsbSbSb", 42), '%': ("bsbSbSbSb", 43) +} + +_valchars = [ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', + 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', + 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', + 'X', 'Y', 'Z', '-', '.', ' ', '*', '$', '/', '+', '%' +] + +_extended = { + '\0': "%U", '\01': "$A", '\02': "$B", '\03': "$C", + '\04': "$D", '\05': "$E", '\06': "$F", '\07': "$G", + '\010': "$H", '\011': "$I", '\012': "$J", '\013': "$K", + '\014': "$L", '\015': "$M", '\016': "$N", '\017': "$O", + '\020': "$P", '\021': "$Q", '\022': "$R", '\023': "$S", + '\024': "$T", '\025': "$U", '\026': "$V", '\027': "$W", + '\030': "$X", '\031': "$Y", '\032': "$Z", '\033': "%A", + '\034': "%B", '\035': "%C", '\036': "%D", '\037': "%E", + '!': "/A", '"': "/B", '#': "/C", '$': "/D", + '%': "/E", '&': "/F", '\'': "/G", '(': "/H", + ')': "/I", '*': "/J", '+': "/K", ',': "/L", + '/': "/O", ':': "/Z", ';': "%F", '<': "%G", + '=': "%H", '>': "%I", '?': "%J", '@': "%V", + '[': "%K", '\\': "%L", ']': "%M", '^': "%N", + '_': "%O", '`': "%W", 'a': "+A", 'b': "+B", + 'c': "+C", 'd': "+D", 'e': "+E", 'f': "+F", + 'g': "+G", 'h': "+H", 'i': "+I", 'j': "+J", + 'k': "+K", 'l': "+L", 'm': "+M", 'n': "+N", + 'o': "+O", 'p': "+P", 'q': "+Q", 'r': "+R", + 's': "+S", 't': "+T", 'u': "+U", 'v': "+V", + 'w': "+W", 'x': "+X", 'y': "+Y", 'z': "+Z", + '{': "%P", '|': "%Q", '}': "%R", '~': "%S", + '\177': "%T" +} + + +_stdchrs = string.digits + string.uppercase + "-. *$/+%" +_extchrs = _stdchrs + string.lowercase + \ + "\000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017" + \ + "\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037" + \ + "!'#&\"(),:;<=>?@[\\]^_`{|}~\177" + +def _encode39(value, cksum, stop): + v = sum([_patterns[c][1] for c in value]) % 43 + if cksum: + value += _valchars[v] + if stop: value = '*'+value+'*' + return value + +class _Code39Base(Barcode): + barWidth = inch * 0.0075 + lquiet = None + rquiet = None + quiet = 1 + gap = None + barHeight = None + ratio = 2.2 + checksum = 1 + bearers = 0.0 + stop = 1 + def __init__(self, value = "", **args): + for k, v in args.iteritems(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def decompose(self): + dval = "" + for c in self.encoded: + dval = dval + _patterns[c][0] + 'i' + self.decomposed = dval[:-1] + return self.decomposed + + def _humanText(self): + return self.stop and self.encoded[1:-1] or self.encoded + +class Standard39(_Code39Base): + """ + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the + barWidth is greater than 20 mils (.02 inch)) + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 1): + Wether to compute and include the check digit + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length. + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on Code 39: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/code_39.html + http://www.adams1.com/pub/russadam/39code.html + http://www.barcodeman.com/c39_1.html + + Official Spec, "ANSI/AIM BC1-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if c in string.lowercase: + c = string.upper(c) + if c not in _stdchrs: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + self.encoded = _encode39(self.validated, self.checksum, self.stop) + return self.encoded + +class Extended39(_Code39Base): + """ + Extended Code 39 is a convention for encoding additional characters + not present in stanmdard Code 39 by using pairs of characters to + represent the characters missing in Standard Code 39. + + See Standard39 for arguments. + + Sources of Information on Extended Code 39: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/xcode_39.html + http://www.barcodeman.com/c39_ext.html + """ + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if c not in _extchrs: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + self.encoded = "" + for c in self.validated: + if _extended.has_key(c): + self.encoded = self.encoded + _extended[c] + elif c in _stdchrs: + self.encoded = self.encoded + c + else: + raise ValueError + self.encoded = _encode39(self.encoded, self.checksum,self.stop) + return self.encoded diff --git a/bin/reportlab/graphics/barcode/code93.py b/bin/reportlab/graphics/barcode/code93.py new file mode 100644 index 00000000000..0ab52e945e3 --- /dev/null +++ b/bin/reportlab/graphics/barcode/code93.py @@ -0,0 +1,236 @@ +# +# Copyright (c) 2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import MultiWidthBarcode +import string + +_patterns = { + '0' : ('AcAaAb', 0), '1' : ('AaAbAc', 1), '2' : ('AaAcAb', 2), + '3' : ('AaAdAa', 3), '4' : ('AbAaAc', 4), '5' : ('AbAbAb', 5), + '6' : ('AbAcAa', 6), '7' : ('AaAaAd', 7), '8' : ('AcAbAa', 8), + '9' : ('AdAaAa', 9), 'A' : ('BaAaAc', 10), 'B' : ('BaAbAb', 11), + 'C' : ('BaAcAa', 12), 'D' : ('BbAaAb', 13), 'E' : ('BbAbAa', 14), + 'F' : ('BcAaAa', 15), 'G' : ('AaBaAc', 16), 'H' : ('AaBbAb', 17), + 'I' : ('AaBcAa', 18), 'J' : ('AbBaAb', 19), 'K' : ('AcBaAa', 20), + 'L' : ('AaAaBc', 21), 'M' : ('AaAbBb', 22), 'N' : ('AaAcBa', 23), + 'O' : ('AbAaBb', 24), 'P' : ('AcAaBa', 25), 'Q' : ('BaBaAb', 26), + 'R' : ('BaBbAa', 27), 'S' : ('BaAaBb', 28), 'T' : ('BaAbBa', 29), + 'U' : ('BbAaBa', 30), 'V' : ('BbBaAa', 31), 'W' : ('AaBaBb', 32), + 'X' : ('AaBbBa', 33), 'Y' : ('AbBaBa', 34), 'Z' : ('AbCaAa', 35), + '-' : ('AbAaCa', 36), '.' : ('CaAaAb', 37), ' ' : ('CaAbAa', 38), + '$' : ('CbAaAa', 39), '/' : ('AaBaCa', 40), '+' : ('AaCaBa', 41), + '%' : ('BaAaCa', 42), '#' : ('AbAbBa', 43), '!' : ('CaBaAa', 44), + '=' : ('CaAaBa', 45), '&' : ('AbBbAa', 46), + 'start' : ('AaAaDa', -1), 'stop' : ('AaAaDaA', -2) +} + +_charsbyval = {} +for k, v in _patterns.items(): + _charsbyval[v[1]] = k + +_extended = { + '\x00' : '!U', '\x01' : '#A', '\x02' : '#B', '\x03' : '#C', + '\x04' : '#D', '\x05' : '#E', '\x06' : '#F', '\x07' : '#G', + '\x08' : '#H', '\x09' : '#I', '\x0a' : '#J', '\x0b' : '#K', + '\x0c' : '#L', '\x0d' : '#M', '\x0e' : '#N', '\x0f' : '#O', + '\x10' : '#P', '\x11' : '#Q', '\x12' : '#R', '\x13' : '#S', + '\x14' : '#T', '\x15' : '#U', '\x16' : '#V', '\x17' : '#W', + '\x18' : '#X', '\x19' : '#Y', '\x1a' : '#Z', '\x1b' : '!A', + '\x1c' : '!B', '\x1d' : '!C', '\x1e' : '!D', '\x1f' : '!E', + '!' : '=A', '"' : '=B', '#' : '=C', '$' : '=D', + '%' : '=E', '&' : '=F', '\'' : '=G', '(' : '=H', + ')' : '=I', '*' : '=J', '+' : '=K', ',' : '=L', + '/' : '=O', ':' : '=Z', ';' : '!F', '<' : '!G', + '=' : '!H', '>' : '!I', '?' : '!J', '@' : '!V', + '[' : '!K', '\\' : '!L', ']' : '!M', '^' : '!N', + '_' : '!O', '`' : '!W', 'a' : '&A', 'b' : '&B', + 'c' : '&C', 'd' : '&D', 'e' : '&E', 'f' : '&F', + 'g' : '&G', 'h' : '&H', 'i' : '&I', 'j' : '&J', + 'k' : '&K', 'l' : '&L', 'm' : '&M', 'n' : '&N', + 'o' : '&O', 'p' : '&P', 'q' : '&Q', 'r' : '&R', + 's' : '&S', 't' : '&T', 'u' : '&U', 'v' : '&V', + 'w' : '&W', 'x' : '&X', 'y' : '&Y', 'z' : '&Z', + '{' : '!P', '|' : '!Q', '}' : '!R', '~' : '!S', + '\x7f' : '!T' +} + +def _encode93(str): + s = map(None, str) + s.reverse() + + # compute 'C' checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * _patterns[s[i]][1] + i = i + 1; v = v + 1 + if v > 20: + v = 1 + s.insert(0, _charsbyval[c % 47]) + + # compute 'K' checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * _patterns[s[i]][1] + i = i + 1; v = v + 1 + if v > 15: + v = 1 + s.insert(0, _charsbyval[c % 47]) + + s.reverse() + + return string.join(s, '') + +class _Code93Base(MultiWidthBarcode): + barWidth = inch * 0.0075 + lquiet = None + rquiet = None + quiet = 1 + barHeight = None + stop = 1 + def __init__(self, value='', **args): + + if type(value) is type(1): + value = str(value) + + for (k, v) in args.iteritems(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + MultiWidthBarcode.__init__(self, value) + + def decompose(self): + dval = self.stop and [_patterns['start'][0]] or [] + dval += [_patterns[c][0] for c in self.encoded] + if self.stop: dval.append(_patterns['stop'][0]) + self.decomposed = ''.join(dval) + return self.decomposed + +class Standard93(_Code93Base): + """ + Code 93 is a Uppercase alphanumeric symbology with some punctuation. + See Extended Code 93 for a variant that can represent the entire + 128 characrter ASCII set. + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on Code 93: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/code_93.html + + Official Spec, "NSI/AIM BC5-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + def validate(self): + vval = "" + self.valid = 1 + for c in self.value: + if c in string.lowercase: + c = string.upper(c) + if not _patterns.has_key(c): + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + self.encoded = _encode93(self.validated) + return self.encoded + + +class Extended93(_Code93Base): + """ + Extended Code 93 is a convention for encoding the entire 128 character + set using pairs of characters to represent the characters missing in + Standard Code 93. It is very much like Extended Code 39 in that way. + + See Standard93 for arguments. + """ + + def validate(self): + vval = [] + self.valid = 1 + a = vval.append + for c in self.value: + if not _patterns.has_key(c) and not _extended.has_key(c): + self.valid = 0 + continue + a(c) + self.validated = ''.join(vval) + return self.validated + + def encode(self): + self.encoded = "" + for c in self.validated: + if _patterns.has_key(c): + self.encoded = self.encoded + c + elif _extended.has_key(c): + self.encoded = self.encoded + _extended[c] + else: + raise ValueError + self.encoded = _encode93(self.encoded) + return self.encoded + + def _humanText(self): + return self.validated+self.encoded[-2:] diff --git a/bin/reportlab/graphics/barcode/common.py b/bin/reportlab/graphics/barcode/common.py new file mode 100644 index 00000000000..70dbcba72b6 --- /dev/null +++ b/bin/reportlab/graphics/barcode/common.py @@ -0,0 +1,748 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.platypus.flowables import Flowable +from reportlab.lib.units import inch +import string + +class Barcode(Flowable): + """Abstract Base for barcodes. Includes implementations of + some methods suitable for the more primitive barcode types""" + + fontName = 'Courier' + fontSize = 12 + humanReadable = 0 + + def _humanText(self): + return self.encoded + + def __init__(self, value='',**args): + self.value = value + + for (k, v) in args.items(): + setattr(self, k, v) + + + if not hasattr(self, 'gap'): + self.gap = None + + self.validate() + self.encode() + self.decompose() + self.computeSize() + + def validate(self): + self.valid = 1 + self.validated = self.value + + def encode(self): + self.encoded = self.validated + + def decompose(self): + self.decomposed = self.encoded + + def computeSize(self, *args): + barWidth = self.barWidth + wx = barWidth * self.ratio + + if self.gap == None: + self.gap = barWidth + + w = 0.0 + + for c in self.decomposed: + if c in 'sb': + w = w + barWidth + elif c in 'SB': + w = w + wx + else: # 'i' + w = w + self.gap + + if self.barHeight is None: + self.barHeight = w * 0.15 + self.barHeight = max(0.25 * inch, self.barHeight) + if self.bearers: + self.barHeight = self.barHeight + self.bearers * 2.0 * barWidth + + if self.quiet: + w += self.lquiet + self.rquiet + + self.height = self.barHeight + self.width = w + + def draw(self): + barWidth = self.barWidth + wx = barWidth * self.ratio + + left = self.quiet and self.lquiet or 0 + b = self.bearers * barWidth + bb = b * 0.5 + tb = self.barHeight - (b * 1.5) + + for c in self.decomposed: + if c == 'i': + left = left + self.gap + elif c == 's': + left = left + barWidth + elif c == 'S': + left = left + wx + elif c == 'b': + self.rect(left, bb, barWidth, tb) + left = left + barWidth + elif c == 'B': + self.rect(left, bb, wx, tb) + left = left + wx + + if self.bearers: + self.rect(self.lquiet, 0, \ + self.width - (self.lquiet + self.rquiet), b) + self.rect(self.lquiet, self.barHeight - b, \ + self.width - (self.lquiet + self.rquiet), b) + + self.drawHumanReadable() + + def drawHumanReadable(self): + if self.humanReadable: + #we have text + from reportlab.pdfbase.pdfmetrics import getAscent, stringWidth + s = str(self._humanText()) + fontSize = self.fontSize + fontName = self.fontName + w = stringWidth(s,fontName,fontSize) + width = self.width + if self.quiet: + width -= self.lquiet+self.rquiet + x = self.lquiet + else: + x = 0 + if w>width: fontSize *= width/float(w) + y = 1.07*getAscent(fontName)*fontSize/1000. + self.annotate(x+width/2.,-y,s,fontName,fontSize) + + def rect(self, x, y, w, h): + self.canv.rect(x, y, w, h, stroke=0, fill=1) + + def annotate(self,x,y,text,fontName,fontSize,anchor='middle'): + canv = self.canv + canv.saveState() + canv.setFont(self.fontName,fontSize) + if anchor=='middle': func = 'drawCentredString' + elif anchor=='end': func = 'drawRightString' + else: func = 'drawString' + getattr(canv,func)(text,x,y) + canv.restoreState() + +class MultiWidthBarcode(Barcode): + """Base for variable-bar-width codes like Code93 and Code128""" + + def computeSize(self, *args): + barWidth = self.barWidth + oa, oA = ord('a') - 1, ord('A') - 1 + + w = 0.0 + + for c in self.decomposed: + oc = ord(c) + if c in string.lowercase: + w = w + barWidth * (oc - oa) + elif c in string.uppercase: + w = w + barWidth * (oc - oA) + + if self.barHeight is None: + self.barHeight = w * 0.15 + self.barHeight = max(0.25 * inch, self.barHeight) + + if self.quiet: + w += self.lquiet + self.rquiet + + self.height = self.barHeight + self.width = w + + def draw(self): + oa, oA = ord('a') - 1, ord('A') - 1 + barWidth = self.barWidth + left = self.quiet and self.lquiet or 0 + + for c in self.decomposed: + oc = ord(c) + if c in string.lowercase: + left = left + (oc - oa) * barWidth + elif c in string.uppercase: + w = (oc - oA) * barWidth + self.rect(left, 0, w, self.barHeight) + left += w + self.drawHumanReadable() + +class I2of5(Barcode): + """ + Interleaved 2 of 5 is a numeric-only barcode. It encodes an even + number of digits; if an odd number is given, a 0 is prepended. + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils). + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the + barWidth is greater than 20 mils (.02 inch)) + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 1): + Whether to compute and include the check digit + + bearers (float, in units of barWidth. default 3.0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 3 x-dimensions. + Set to zero for no bearer bars. (Bearer bars help detect + misscans, so it is suggested to leave them on). + + quiet (bool, default 1): + Whether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length. + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on Interleaved 2 of 5: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/i_25.html + http://www.adams1.com/pub/russadam/i25code.html + + Official Spec, "ANSI/AIM BC2-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + + patterns = { + 'start' : 'bsbs', + 'stop' : 'Bsb', + + 'B0' : 'bbBBb', 'S0' : 'ssSSs', + 'B1' : 'BbbbB', 'S1' : 'SsssS', + 'B2' : 'bBbbB', 'S2' : 'sSssS', + 'B3' : 'BBbbb', 'S3' : 'SSsss', + 'B4' : 'bbBbB', 'S4' : 'ssSsS', + 'B5' : 'BbBbb', 'S5' : 'SsSss', + 'B6' : 'bBBbb', 'S6' : 'sSSss', + 'B7' : 'bbbBB', 'S7' : 'sssSS', + 'B8' : 'BbbBb', 'S8' : 'SssSs', + 'B9' : 'bBbBb', 'S9' : 'sSsSs' + } + + barHeight = None + barWidth = inch * 0.0075 + ratio = 2.2 + checksum = 1 + bearers = 3.0 + quiet = 1 + lquiet = None + rquiet = None + stop = 1 + + def __init__(self, value='', **args): + + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = min(inch * 0.25, self.barWidth * 10.0) + self.rquiet = min(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + for c in string.strip(self.value): + if c not in string.digits: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + s = self.validated + + # make sure result will be a multiple of 2 digits long, + # checksum included + if ((len(self.validated) % 2 == 0) and self.checksum) \ + or ((len(self.validated) % 2 == 1) and not self.checksum): + s = '0' + s + + if self.checksum: + c = 0 + cm = 3 + + for d in s: + c = c + cm * int(d) + if cm == 3: + cm = 1 + else: + cm = 3 + + d = 10 - (int(d) % 10) + s = s + `d` + + self.encoded = s + + def decompose(self): + dval = self.stop and [self.patterns['start']] or [] + a = dval.append + + for i in xrange(0, len(self.encoded), 2): + b = self.patterns['B' + self.encoded[i]] + s = self.patterns['S' + self.encoded[i+1]] + + for i in range(0, len(b)): + a(b[i] + s[i]) + + if self.stop: a(self.patterns['stop']) + self.decomposed = ''.join(dval) + return self.decomposed + +class MSI(Barcode): + """ + MSI is a numeric-only barcode. + + Options that may be passed to constructor: + + value (int, or numeric string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 1): + Wether to compute and include the check digit + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidths. + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + stop (bool, default 1): + Whether to include start/stop symbols. + + Sources of Information on MSI Bar Code: + + http://www.semiconductor.agilent.com/barcode/sg/Misc/msi_code.html + http://www.adams1.com/pub/russadam/plessy.html + """ + + patterns = { + 'start' : 'Bs', 'stop' : 'bSb', + + '0' : 'bSbSbSbS', '1' : 'bSbSbSBs', + '2' : 'bSbSBsbS', '3' : 'bSbSBsBs', + '4' : 'bSBsbSbS', '5' : 'bSBsbSBs', + '6' : 'bSBsBsbS', '7' : 'bSBsBsBs', + '8' : 'BsbSbSbS', '9' : 'BsbSbSBs' + } + + stop = 1 + barHeight = None + barWidth = inch * 0.0075 + ratio = 2.2 + checksum = 1 + bearers = 0.0 + quiet = 1 + lquiet = None + rquiet = None + + def __init__(self, value="", **args): + + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = max(inch * 0.25, self.barWidth * 10.0) + self.rquiet = max(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + for c in string.strip(self.value): + if c not in string.digits: + self.valid = 0 + continue + vval = vval + c + self.validated = vval + return vval + + def encode(self): + s = self.validated + + if self.checksum: + c = '' + for i in range(1, len(s), 2): + c = c + s[i] + d = str(int(c) * 2) + t = 0 + for c in d: + t = t + int(c) + for i in range(0, len(s), 2): + t = t + int(s[i]) + c = 10 - (t % 10) + + s = s + str(c) + + self.encoded = s + + def decompose(self): + dval = self.stop and [self.patterns['start']] or [] + dval += [self.patterns[c] for c in self.encoded] + if self.stop: dval.append(self.patterns['stop']) + self.decomposed = ''.join(dval) + return self.decomposed + +class Codabar(Barcode): + """ + Codabar is a numeric plus some puntuation ("-$:/.+") barcode + with four start/stop characters (A, B, C, and D). + + Options that may be passed to constructor: + + value (string. required.): + The value to encode. + + barWidth (float, default .0065): + X-Dimension, or width of the smallest element + minimum is 6.5 mils (.0065 inch) + + ratio (float, default 2.0): + The ratio of wide elements to narrow elements. + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (bool, default 0): + Whether to compute and include the check digit + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + quiet (bool, default 1): + Whether to include quiet zones in the symbol. + + stop (bool, default 1): + Whether to include start/stop symbols. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + Sources of Information on Codabar + + http://www.semiconductor.agilent.com/barcode/sg/Misc/codabar.html + http://www.barcodeman.com/codabar.html + + Official Spec, "ANSI/AIM BC3-1995, USS" is available for US$45 from + http://www.aimglobal.org/aimstore/ + """ + + patterns = { + '0': 'bsbsbSB', '1': 'bsbsBSb', '2': 'bsbSbsB', + '3': 'BSbsbsb', '4': 'bsBsbSb', '5': 'BsbsbSb', + '6': 'bSbsbsB', '7': 'bSbsBsb', '8': 'bSBsbsb', + '9': 'BsbSbsb', '-': 'bsbSBsb', '$': 'bsBSbsb', + ':': 'BsbsBsB', '/': 'BsBsbsB', '.': 'BsBsBsb', + '+': 'bsBsBsB', 'A': 'bsBSbSb', 'B': 'bSbSbsB', + 'C': 'bsbSbSB', 'D': 'bsbSBSb' + } + + values = { + '0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4, + '5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9, + '-' : 10, '$' : 11, ':' : 12, '/' : 13, '.' : 14, + '+' : 15, 'A' : 16, 'B' : 17, 'C' : 18, 'D' : 19 + } + + chars = string.digits + "-$:/.+" + + stop = 1 + barHeight = None + barWidth = inch * 0.0065 + ratio = 2.0 # XXX ? + checksum = 0 + bearers = 0.0 + quiet = 1 + lquiet = None + rquiet = None + + def __init__(self, value='', **args): + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = min(inch * 0.25, self.barWidth * 10.0) + self.rquiet = min(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + s = string.strip(self.value) + for i in range(0, len(s)): + c = s[i] + if c not in self.chars: + if ((i != 0) and (i != len(s) - 1)) or (c not in 'ABCD'): + self.Valid = 0 + continue + vval = vval + c + + if self.stop: + if vval[0] not in 'ABCD': + vval = 'A' + vval + if vval[-1] not in 'ABCD': + vval = vval + vval[0] + + self.validated = vval + return vval + + def encode(self): + s = self.validated + + if self.checksum: + v = sum([self.values[c] for c in s]) + s += self.chars[v % 16] + + self.encoded = s + + def decompose(self): + dval = ''.join([self.patterns[c]+'i' for c in self.encoded]) + self.decomposed = dval[:-1] + return self.decomposed + +class Code11(Barcode): + """ + Code 11 is an almost-numeric barcode. It encodes the digits 0-9 plus + dash ("-"). 11 characters total, hence the name. + + value (int or string. required.): + The value to encode. + + barWidth (float, default .0075): + X-Dimension, or width of the smallest element + + ratio (float, default 2.2): + The ratio of wide elements to narrow elements. + + gap (float or None, default None): + width of intercharacter gap. None means "use barWidth". + + barHeight (float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length. + + checksum (0 none, 1 1-digit, 2 2-digit, -1 auto, default -1): + How many checksum digits to include. -1 ("auto") means + 1 if the number of digits is 10 or less, else 2. + + bearers (float, in units of barWidth. default 0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 0 (no bearers). + + quiet (bool, default 1): + Wether to include quiet zones in the symbol. + + lquiet (float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or 10 barWidth + + rquiet (float, defaults as above): + Quiet zone size to right left of code, if quiet is true. + + Sources of Information on Code 11: + + http://www.cwi.nl/people/dik/english/codes/barcodes.html + """ + + chars = string.digits + '-' + + patterns = { + '0' : 'bsbsB', '1' : 'BsbsB', '2' : 'bSbsB', + '3' : 'BSbsb', '4' : 'bsBsB', '5' : 'BsBsb', + '6' : 'bSBsb', '7' : 'bsbSB', '8' : 'BsbSb', + '9' : 'Bsbsb', '-' : 'bsBsb', 'S' : 'bsBSb' # Start/Stop + } + + values = { + '0' : 0, '1' : 1, '2' : 2, '3' : 3, '4' : 4, + '5' : 5, '6' : 6, '7' : 7, '8' : 8, '9' : 9, + '-' : 10, + } + + stop = 1 + barHeight = None + barWidth = inch * 0.0075 + ratio = 2.2 # XXX ? + checksum = -1 # Auto + bearers = 0.0 + quiet = 1 + lquiet = None + rquiet = None + def __init__(self, value='', **args): + if type(value) == type(1): + value = str(value) + + for (k, v) in args.items(): + setattr(self, k, v) + + if self.quiet: + if self.lquiet is None: + self.lquiet = min(inch * 0.25, self.barWidth * 10.0) + self.rquiet = min(inch * 0.25, self.barWidth * 10.0) + else: + self.lquiet = self.rquiet = 0.0 + + Barcode.__init__(self, value) + + def validate(self): + vval = "" + self.valid = 1 + s = string.strip(self.value) + for i in range(0, len(s)): + c = s[i] + if c not in self.chars: + self.Valid = 0 + continue + vval = vval + c + + self.validated = vval + return vval + + def encode(self): + s = self.validated + + if self.checksum == -1: + if len(s) <= 10: + self.checksum = 1 + else: + self.checksum = 2 + + if self.checksum > 0: + # compute first checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * string.index(self.chars, s[-(i+1)]) + i = i + 1; v = v + 1 + if v > 10: + v = 1 + s = s + self.chars[c % 11] + + if self.checksum > 1: + # compute second checksum + i = 0; v = 1; c = 0 + while i < len(s): + c = c + v * string.index(self.chars, s[-(i+1)]) + i = i + 1; v = v + 1 + if v > 9: + v = 1 + s = s + self.chars[c % 10] + + self.encoded = self.stop and ('S' + s + 'S') or s + + def decompose(self): + dval = [self.patterns[c]+'i' for c in self.encoded] + self.decomposed = ''.join(dval[:-1]) + return self.decomposed + + def _humanText(self): + return self.stop and self.encoded[1:-1] or self.encoded diff --git a/bin/reportlab/graphics/barcode/eanbc.py b/bin/reportlab/graphics/barcode/eanbc.py new file mode 100644 index 00000000000..f9fbc73b61c --- /dev/null +++ b/bin/reportlab/graphics/barcode/eanbc.py @@ -0,0 +1,339 @@ +__all__=( + 'Ean13BarcodeWidget','isEanString', + ) +from reportlab.graphics.shapes import Group, String, Rect +from reportlab.lib import colors +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.validators import isNumber, isColor, isString, Validator, isBoolean +from reportlab.lib.attrmap import * +from reportlab.graphics.charts.areas import PlotArea +from reportlab.lib.units import mm + +#work out a list of manufacturer codes.... +_eanNumberSystems = [ + ('00-13', 'USA & Canada'), + ('20-29', 'In-Store Functions'), + ('30-37', 'France'), + ('40-44', 'Germany'), + ('45', 'Japan (also 49)'), + ('46', 'Russian Federation'), + ('471', 'Taiwan'), + ('474', 'Estonia'), + ('475', 'Latvia'), + ('477', 'Lithuania'), + ('479', 'Sri Lanka'), + ('480', 'Philippines'), + ('482', 'Ukraine'), + ('484', 'Moldova'), + ('485', 'Armenia'), + ('486', 'Georgia'), + ('487', 'Kazakhstan'), + ('489', 'Hong Kong'), + ('49', 'Japan (JAN-13)'), + ('50', 'United Kingdom'), + ('520', 'Greece'), + ('528', 'Lebanon'), + ('529', 'Cyprus'), + ('531', 'Macedonia'), + ('535', 'Malta'), + ('539', 'Ireland'), + ('54', 'Belgium & Luxembourg'), + ('560', 'Portugal'), + ('569', 'Iceland'), + ('57', 'Denmark'), + ('590', 'Poland'), + ('594', 'Romania'), + ('599', 'Hungary'), + ('600-601', 'South Africa'), + ('609', 'Mauritius'), + ('611', 'Morocco'), + ('613', 'Algeria'), + ('619', 'Tunisia'), + ('622', 'Egypt'), + ('625', 'Jordan'), + ('626', 'Iran'), + ('64', 'Finland'), + ('690-692', 'China'), + ('70', 'Norway'), + ('729', 'Israel'), + ('73', 'Sweden'), + ('740', 'Guatemala'), + ('741', 'El Salvador'), + ('742', 'Honduras'), + ('743', 'Nicaragua'), + ('744', 'Costa Rica'), + ('746', 'Dominican Republic'), + ('750', 'Mexico'), + ('759', 'Venezuela'), + ('76', 'Switzerland'), + ('770', 'Colombia'), + ('773', 'Uruguay'), + ('775', 'Peru'), + ('777', 'Bolivia'), + ('779', 'Argentina'), + ('780', 'Chile'), + ('784', 'Paraguay'), + ('785', 'Peru'), + ('786', 'Ecuador'), + ('789', 'Brazil'), + ('80-83', 'Italy'), + ('84', 'Spain'), + ('850', 'Cuba'), + ('858', 'Slovakia'), + ('859', 'Czech Republic'), + ('860', 'Yugloslavia'), + ('869', 'Turkey'), + ('87', 'Netherlands'), + ('880', 'South Korea'), + ('885', 'Thailand'), + ('888', 'Singapore'), + ('890', 'India'), + ('893', 'Vietnam'), + ('899', 'Indonesia'), + ('90-91', 'Austria'), + ('93', 'Australia'), + ('94', 'New Zealand'), + ('955', 'Malaysia'), + ('977', 'International Standard Serial Number for Periodicals (ISSN)'), + ('978', 'International Standard Book Numbering (ISBN)'), + ('979', 'International Standard Music Number (ISMN)'), + ('980', 'Refund receipts'), + ('981-982', 'Common Currency Coupons'), + ('99', 'Coupons') + ] + +manufacturerCodes = {} +for (k, v) in _eanNumberSystems: + words = k.split('-') + if len(words)==2: + fromCode = int(words[0]) + toCode = int(words[1]) + for code in range(fromCode, toCode+1): + manufacturerCodes[code] = v + else: + manufacturerCodes[int(k)] = v + +class isEan13String(Validator): + def test(self,x): + return type(x) is str and len(x)<=12 and len([c for c in x if c in "0123456789"])==12 +isEan13String = isEan13String() + +class Ean13BarcodeWidget(PlotArea): + codeName = "EAN13" + _attrMap = AttrMap(BASE=PlotArea, + value = AttrMapValue(isEan13String, desc='the number'), + fontName = AttrMapValue(isString, desc='fontName'), + fontSize = AttrMapValue(isNumber, desc='font size'), + x = AttrMapValue(isNumber, desc='x-coord'), + y = AttrMapValue(isNumber, desc='y-coord'), + barFillColor = AttrMapValue(isColor, desc='bar color'), + barHeight = AttrMapValue(isNumber, desc='Height of bars.'), + barWidth = AttrMapValue(isNumber, desc='Width of bars.'), + barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'), + barStrokeColor = AttrMapValue(isColor, desc='Color of bar borders.'), + textColor = AttrMapValue(isColor, desc='human readable text color'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + quiet = AttrMapValue(isBoolean, desc='if quiet zone to be used'), + lquiet = AttrMapValue(isBoolean, desc='left quiet zone length'), + rquiet = AttrMapValue(isBoolean, desc='right quiet zone length'), + ) + _digits=12 + _start_right = 7 #for ean-13 left = [0:7] right=[7:13] + _nbars = 113 + barHeight = 25.93*mm #millimeters + barWidth = (37.29/_nbars)*mm + humanReadable = 1 + _0csw = 1 + _1csw = 3 + + #Left Hand Digits. + _left = ( ("0001101", "0011001", "0010011", "0111101", + "0100011", "0110001", "0101111", "0111011", + "0110111", "0001011", + ), #odd left hand digits + ("0100111", "0110011", "0011011", "0100001", + "0011101", "0111001", "0000101", "0010001", + "0001001", "0010111"), #even left hand digits + ) + + _right = ("1110010", "1100110", "1101100", "1000010", + "1011100", "1001110", "1010000", "1000100", + "1001000", "1110100") + + quiet = 1 + rquiet = lquiet = None + _tail = "101" + _sep = "01010" + + _lhconvert={ + "0": (0,0,0,0,0,0), + "1": (0,0,1,0,1,1), + "2": (0,0,1,1,0,1), + "3": (0,0,1,1,1,0), + "4": (0,1,0,0,1,1), + "5": (0,1,1,0,0,1), + "6": (0,1,1,1,0,0), + "7": (0,1,0,1,0,1), + "8": (0,1,0,1,1,0), + "9": (0,1,1,0,1,0) + } + fontSize = 8 #millimeters + fontName = 'Helvetica' + textColor = barFillColor = barStrokeColor = colors.black + barStrokeWidth = 0 + x = 0 + y = 0 + def __init__(self,value='123456789012',**kw): + self.value=max(self._digits-len(value),0)*'0'+value[:self._digits] + for k, v in kw.iteritems(): + setattr(self, k, v) + + width = property(lambda self: self.barWidth*(self._nbars-18+self._calc_quiet(self.lquiet)+self._calc_quiet(self.rquiet))) + + def wrap(self,aW,aH): + return self.width,self.barHeight + + def _encode_left(self,s,a): + cp = self._lhconvert[s[0]] #convert the left hand numbers + _left = self._left + z = ord('0') + for i,c in enumerate(s[1:self._start_right]): + a(_left[cp[i]][ord(c)-z]) + + def _short_bar(self,i): + i += 9 - self._lquiet + return self.humanReadable and ((120: v += 1 + else: + v = 0 + return v + + def draw(self): + g = Group() + gAdd = g.add + barWidth = self.barWidth + width = self.width + barHeight = self.barHeight + x = self.x + y = self.y + gAdd(Rect(x,y,width,barHeight,fillColor=None,strokeColor=None,strokeWidth=0)) + s = self.value+self._checkdigit(self.value) + self._lquiet = lquiet = self._calc_quiet(self.lquiet) + rquiet = self._calc_quiet(self.rquiet) + b = [lquiet*'0',self._tail] #the signal string + a = b.append + self._encode_left(s,a) + a(self._sep) + + z = ord('0') + _right = self._right + for c in s[self._start_right:]: + a(_right[ord(c)-z]) + a(self._tail) + a(rquiet*'0') + + fontSize = self.fontSize + barFillColor = self.barFillColor + barStrokeWidth = self.barStrokeWidth + + fth = fontSize*1.2 + b = ''.join(b) + + lrect = None + for i,c in enumerate(b): + if c=="1": + dh = self._short_bar(i) and fth or 0 + yh = y+dh + if lrect and lrect.y==yh: + lrect.width += barWidth + else: + lrect = Rect(x,yh,barWidth,barHeight-dh,fillColor=barFillColor,strokeWidth=barStrokeWidth,strokeColor=barFillColor) + gAdd(lrect) + else: + lrect = None + x += barWidth + + if self.humanReadable: self._add_human_readable(s,gAdd) + return g + + def _add_human_readable(self,s,gAdd): + barWidth = self.barWidth + fontSize = self.fontSize + textColor = self.textColor + fontName = self.fontName + fth = fontSize*1.2 + # draw the num below the line. + c = s[0] + w = stringWidth(c,fontName,fontSize) + x = self.x+barWidth*(self._lquiet-8) + y = self.y + 0.2*fth + + gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor)) + x = self.x + (33-9+self._lquiet)*barWidth + + c = s[1:7] + gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle')) + + x += 47*barWidth + c = s[7:] + gAdd(String(x,y,c,fontName=fontName,fontSize=fontSize,fillColor=textColor,textAnchor='middle')) + + @classmethod + def _checkdigit(cls,num): + z = ord('0') + iSum = cls._0csw*sum([(ord(x)-z) for x in num[::2]]) \ + + cls._1csw*sum([(ord(x)-z) for x in num[1::2]]) + return chr(z+((10-(iSum%10))%10)) + +class isEan8String(Validator): + def test(self,x): + return type(x) is str and len(x)<=7 and len([c for c in x if c in "0123456789"])==7 +isEan8String = isEan8String() + +class Ean8BarcodeWidget(Ean13BarcodeWidget): + codeName = "EAN8" + _attrMap = AttrMap(BASE=Ean13BarcodeWidget, + value = AttrMapValue(isEan8String, desc='the number'), + ) + _start_right = 4 #for ean-13 left = [0:7] right=[7:13] + _nbars = 85 + _digits=7 + _0csw = 3 + _1csw = 1 + + def _encode_left(self,s,a): + cp = self._lhconvert[s[0]] #convert the left hand numbers + _left = self._left[0] + z = ord('0') + for i,c in enumerate(s[0:self._start_right]): + a(_left[ord(c)-z]) + + def _short_bar(self,i): + i += 9 - self._lquiet + return self.humanReadable and ((12 +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import Barcode +import string + +# . 3 T Tracker +# , 2 D Descender +# ' 1 A Ascender +# | 0 H Ascender/Descender + +_rm_patterns = { + "0" : "--||", "1" : "-',|", "2" : "-'|,", "3" : "'-,|", + "4" : "'-|,", "5" : "'',,", "6" : "-,'|", "7" : "-|-|", + "8" : "-|',", "9" : "',-|", "A" : "',',", "B" : "'|-,", + "C" : "-,|'", "D" : "-|,'", "E" : "-||-", "F" : "',,'", + "G" : "',|-", "H" : "'|,-", "I" : ",-'|", "J" : ",'-|", + "K" : ",'',", "L" : "|--|", "M" : "|-',", "N" : "|'-,", + "O" : ",-|'", "P" : ",','", "Q" : ",'|-", "R" : "|-,'", + "S" : "|-|-", "T" : "|',-", "U" : ",,''", "V" : ",|-'", + "W" : ",|'-", "X" : "|,-'", "Y" : "|,'-", "Z" : "||--", + + # start, stop + "(" : "'-,'", ")" : "'|,|" +} + +_ozN_patterns = { + "0" : "||", "1" : "|'", "2" : "|,", "3" : "'|", "4" : "''", + "5" : "',", "6" : ",|", "7" : ",'", "8" : ",,", "9" : ".|" +} + +_ozC_patterns = { + "A" : "|||", "B" : "||'", "C" : "||,", "D" : "|'|", + "E" : "|''", "F" : "|',", "G" : "|,|", "H" : "|,'", + "I" : "|,,", "J" : "'||", "K" : "'|'", "L" : "'|,", + "M" : "''|", "N" : "'''", "O" : "'',", "P" : "',|", + "Q" : "','", "R" : "',,", "S" : ",||", "T" : ",|'", + "U" : ",|,", "V" : ",'|", "W" : ",''", "X" : ",',", + "Y" : ",,|", "Z" : ",,'", "a" : "|,.", "b" : "|.|", + "c" : "|.'", "d" : "|.,", "e" : "|..", "f" : "'|.", + "g" : "''.", "h" : "',.", "i" : "'.|", "j" : "'.'", + "k" : "'.,", "l" : "'..", "m" : ",|.", "n" : ",'.", + "o" : ",,.", "p" : ",.|", "q" : ",.'", "r" : ",.,", + "s" : ",..", "t" : ".|.", "u" : ".'.", "v" : ".,.", + "w" : "..|", "x" : "..'", "y" : "..,", "z" : "...", + "0" : ",,,", "1" : ".||", "2" : ".|'", "3" : ".|,", + "4" : ".'|", "5" : ".''", "6" : ".',", "7" : ".,|", + "8" : ".,'", "9" : ".,,", " " : "||.", "#" : "|'.", +} + +#http://www.auspost.com.au/futurepost/ diff --git a/bin/reportlab/graphics/barcode/test.py b/bin/reportlab/graphics/barcode/test.py new file mode 100644 index 00000000000..3342947672e --- /dev/null +++ b/bin/reportlab/graphics/barcode/test.py @@ -0,0 +1,185 @@ +#!/usr/pkg/bin/python + +import os, sys, time + +from reportlab.graphics.barcode.common import * +from reportlab.graphics.barcode.code39 import * +from reportlab.graphics.barcode.code93 import * +from reportlab.graphics.barcode.code128 import * +from reportlab.graphics.barcode.usps import * + + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.platypus import Spacer, SimpleDocTemplate, Table, TableStyle, Preformatted, PageBreak +from reportlab.lib.units import inch, cm +from reportlab.lib import colors + +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.platypus.flowables import XBox, KeepTogether +from reportlab.graphics.shapes import Drawing + +from reportlab.graphics.barcode import getCodes, getCodeNames, createBarcodeDrawing +def run(): + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleH = styles['Heading1'] + story = [] + + #for codeNames in code + story.append(Paragraph('I2of5', styleN)) + story.append(I2of5(1234, barWidth = inch*0.02, checksum=0)) + story.append(Paragraph('MSI', styleN)) + story.append(MSI(1234)) + story.append(Paragraph('Codabar', styleN)) + story.append(Codabar("A012345B", barWidth = inch*0.02)) + story.append(Paragraph('Code 11', styleN)) + story.append(Code11("01234545634563")) + story.append(Paragraph('Code 39', styleN)) + story.append(Standard39("A012345B%R")) + story.append(Paragraph('Extended Code 39', styleN)) + story.append(Extended39("A012345B}")) + story.append(Paragraph('Code93', styleN)) + story.append(Standard93("CODE 93")) + story.append(Paragraph('Extended Code93', styleN)) + story.append(Extended93("L@@K! Code 93 :-)")) #, barWidth=0.005 * inch)) + story.append(Paragraph('Code 128', styleN)) + c=Code128("AB-12345678") #, barWidth=0.005 * inch) + #print 'WIDTH =', (c.width / inch), 'barWidth =', (c.barWidth / inch) + #print 'LQ =', (c.lquiet / inch), 'RQ =', (c.rquiet / inch) + story.append(c) + story.append(Paragraph('USPS FIM', styleN)) + story.append(FIM("A")) + story.append(Paragraph('USPS POSTNET', styleN)) + story.append(POSTNET('78247-1043')) + + from reportlab.graphics.barcode import createBarcodeDrawing + story.append(Paragraph('EAN13', styleN)) + bcd = createBarcodeDrawing('EAN13', value='123456789012') + story.append(bcd) + story.append(Paragraph('EAN8', styleN)) + bcd = createBarcodeDrawing('EAN8', value='1234567') + story.append(bcd) + + story.append(Paragraph('Label Size', styleN)) + story.append(XBox((2.0 + 5.0/8.0)*inch, 1 * inch, '1x2-5/8"')) + story.append(Paragraph('Label Size', styleN)) + story.append(XBox((1.75)*inch, .5 * inch, '1/2x1-3/4"')) + c = Canvas('out.pdf') + f = Frame(inch, inch, 6*inch, 9*inch, showBoundary=1) + f.addFromList(story, c) + c.save() + print 'saved out.pdf' + +def fullTest(fileName="test_full.pdf"): + """Creates large-ish test document with a variety of parameters""" + + story = [] + + styles = getSampleStyleSheet() + styleN = styles['Normal'] + styleH = styles['Heading1'] + styleH2 = styles['Heading2'] + story = [] + + story.append(Paragraph('ReportLab Barcode Test Suite - full output', styleH)) + story.append(Paragraph('Generated on %s' % time.ctime(time.time()), styleN)) + + story.append(Paragraph('', styleN)) + story.append(Paragraph('Repository information for this build:', styleN)) + #see if we can figure out where it was built, if we're running in source + if os.path.split(os.getcwd())[-1] == 'barcode' and os.path.isdir('.svn'): + #runnning in a filesystem svn copy + infoLines = os.popen('svn info').read() + story.append(Preformatted(infoLines, styles["Code"])) + + story.append(Paragraph('About this document', styleH2)) + story.append(Paragraph('History and Status', styleH2)) + + story.append(Paragraph(""" + This is the test suite and docoumentation for the ReportLab open source barcode API, + being re-released as part of the forthcoming ReportLab 2.0 release. + """, styleN)) + + story.append(Paragraph(""" + Several years ago Ty Sarna contributed a barcode module to the ReportLab community. + Several of the codes were used by him in hiw work and to the best of our knowledge + this was correct. These were written as flowable objects and were available in PDFs, + but not in our graphics framework. However, we had no knowledge of barcodes ourselves + and did not advertise or extend the package. + """, styleN)) + + story.append(Paragraph(""" + We "wrapped" the barcodes to be usable within our graphics framework; they are now available + as Drawing objects which can be rendered to EPS files or bitmaps. For the last 2 years this + has been available in our Diagra and Report Markup Language products. However, we did not + charge separately and use was on an "as is" basis. + """, styleN)) + + story.append(Paragraph(""" + A major licensee of our technology has kindly agreed to part-fund proper productisation + of this code on an open source basis in Q1 2006. This has involved addition of EAN codes + as well as a proper testing program. Henceforth we intend to publicise the code more widely, + gather feedback, accept contributions of code and treat it as "supported". + """, styleN)) + + story.append(Paragraph(""" + This involved making available both downloads and testing resources. This PDF document + is the output of the current test suite. It contains codes you can scan (if you use a nice sharp + laser printer!), and will be extended over coming weeks to include usage examples and notes on + each barcode and how widely tested they are. This is being done through documentation strings in + the barcode objects themselves so should always be up to date. + """, styleN)) + + story.append(Paragraph('Usage examples', styleH2)) + story.append(Paragraph(""" + To be completed + """, styleN)) + + story.append(Paragraph('The codes', styleH2)) + story.append(Paragraph(""" + Below we show a scannable code from each barcode, with and without human-readable text. + These are magnified about 2x from the natural size done by the original author to aid + inspection. This will be expanded to include several test cases per code, and to add + explanations of checksums. Be aware that (a) if you enter numeric codes which are too + short they may be prefixed for you (e.g. "123" for an 8-digit code becomes "00000123"), + and that the scanned results and readable text will generally include extra checksums + at the end. + """, styleN)) + + codeNames = getCodeNames() + from reportlab.lib.utils import flatten + width = [float(x[8:]) for x in sys.argv if x.startswith('--width=')] + height = [float(x[9:]) for x in sys.argv if x.startswith('--height=')] + isoScale = [int(x[11:]) for x in sys.argv if x.startswith('--isoscale=')] + options = {} + if width: options['width'] = width[0] + if height: options['height'] = height[0] + if isoScale: options['isoScale'] = isoScale[0] + scales = [x[8:].split(',') for x in sys.argv if x.startswith('--scale=')] + scales = map(float,scales and flatten(scales) or [1]) + scales = map(float,scales and flatten(scales) or [1]) + for scale in scales: + story.append(PageBreak()) + story.append(Paragraph('Scale = %.1f'%scale, styleH2)) + story.append(Spacer(36, 12)) + for codeName in codeNames: + s = [Paragraph('Code: ' + codeName, styleH2)] + for hr in (0,1): + s.append(Spacer(36, 12)) + dr = createBarcodeDrawing(codeName, humanReadable=hr,**options) + dr.renderScale = scale + s.append(dr) + s.append(Spacer(36, 12)) + s.append(Paragraph('Barcode should say: ' + dr._bc.value, styleN)) + story.append(KeepTogether(s)) + + SimpleDocTemplate(fileName).build(story) + print 'created', fileName + +if __name__=='__main__': + run() + fullTest() diff --git a/bin/reportlab/graphics/barcode/usps.py b/bin/reportlab/graphics/barcode/usps.py new file mode 100644 index 00000000000..999dd9ca340 --- /dev/null +++ b/bin/reportlab/graphics/barcode/usps.py @@ -0,0 +1,228 @@ +# +# Copyright (c) 1996-2000 Tyler C. Sarna +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# 3. All advertising materials mentioning features or use of this software +# must display the following acknowledgement: +# This product includes software developed by Tyler C. Sarna. +# 4. Neither the name of the author nor the names of contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS +# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +# + +from reportlab.lib.units import inch +from common import Barcode +import string + +_fim_patterns = { + 'A' : "|| | ||", + 'B' : "| || || |", + 'C' : "|| | | ||", + 'D' : "||| | |||", + # XXX There is an E. + # The below has been seen, but dunno if it is E or not: + # 'E' : '|||| ||||' +} + +_postnet_patterns = { + '1' : "...||", '2' : "..|.|", '3' : "..||.", '4' : ".|..|", + '5' : ".|.|.", '6' : ".||..", '7' : "|...|", '8' : "|..|.", + '9' : "|.|..", '0' : "||...", 'S' : "|", +} + +class FIM(Barcode): + """" + FIM (Facing ID Marks) encode only one letter. + There are currently four defined: + + A Courtesy reply mail with pre-printed POSTNET + B Business reply mail without pre-printed POSTNET + C Business reply mail with pre-printed POSTNET + D OCR Readable mail without pre-printed POSTNET + + Options that may be passed to constructor: + + value (single character string from the set A - D. required.): + The value to encode. + + quiet (bool, default 0): + Whether to include quiet zones in the symbol. + + The following may also be passed, but doing so will generate nonstandard + symbols which should not be used. This is mainly documented here to + show the defaults: + + barHeight (float, default 5/8 inch): + Height of the code. This might legitimately be overriden to make + a taller symbol that will 'bleed' off the edge of the paper, + leaving 5/8 inch remaining. + + lquiet (float, default 1/4 inch): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length. + + rquiet (float, default 15/32 inch): + Quiet zone size to right left of code, if quiet is true. + + Sources of information on FIM: + + USPS Publication 25, A Guide to Business Mail Preparation + http://new.usps.com/cpim/ftp/pubs/pub25.pdf + """ + barWidth = inch * (1.0/32.0) + spaceWidth = inch * (1.0/16.0) + barHeight = inch * (5.0/8.0) + rquiet = inch * (0.25) + lquiet = inch * (15.0/32.0) + quiet = 0 + def __init__(self, value='', **args): + for (k, v) in args.items(): + setattr(self, k, v) + + Barcode.__init__(self, value) + + def validate(self): + self.valid = 1 + self.validated = '' + for c in self.value: + if c in string.whitespace: + continue + elif c in "abcdABCD": + self.validated = self.validated + string.upper(c) + else: + self.valid = 0 + + if len(self.validated) != 1: + raise ValueError, "Input must be exactly one character" + + return self.validated + + def decompose(self): + self.decomposed = '' + for c in self.encoded: + self.decomposed = self.decomposed + _fim_patterns[c] + + return self.decomposed + + def computeSize(self): + self.width = (len(self.decomposed) - 1) * self.spaceWidth + self.barWidth + if self.quiet: + self.width += self.lquiet + self.rquiet + self.height = self.barHeight + + def draw(self): + left = self.quiet and self.lquiet or 0 + for c in self.decomposed: + if c == '|': + self.rect(left, 0.0, self.barWidth, self.barHeight) + left += self.spaceWidth + self.drawHumanReadable() + + def _humanText(self): + return self.value + +class POSTNET(Barcode): + """" + POSTNET is used in the US to encode "zip codes" (postal codes) on + mail. It can encode 5, 9, or 11 digit codes. I've read that it's + pointless to do 5 digits, since USPS will just have to re-print + them with 9 or 11 digits. + + Sources of information on POSTNET: + + USPS Publication 25, A Guide to Business Mail Preparation + http://new.usps.com/cpim/ftp/pubs/pub25.pdf + """ + quiet = 0 + shortHeight = inch * 0.050 + barHeight = inch * 0.125 + barWidth = inch * 0.018 + spaceWidth = inch * 0.0275 + def __init__(self, value='', **args): + + for (k, v) in args.items(): + setattr(self, k, v) + + Barcode.__init__(self, value) + + def validate(self): + self.validated = '' + self.valid = 1 + count = 0 + for c in self.value: + if c in (string.whitespace + '-'): + pass + elif c in string.digits: + count = count + 1 + if count == 6: + self.validated = self.validated + '-' + self.validated = self.validated + c + else: + self.valid = 0 + + if len(self.validated) not in [5, 10, 12]: + self.valid = 0 + + return self.validated + + def encode(self): + self.encoded = "S" + check = 0 + for c in self.validated: + if c in string.digits: + self.encoded = self.encoded + c + check = check + string.atoi(c) + elif c == '-': + pass + else: + raise ValueError, "Invalid character in input" + check = (10 - (check % 10)) % 10 + self.encoded = self.encoded + `check` + 'S' + return self.encoded + + def decompose(self): + self.decomposed = '' + for c in self.encoded: + self.decomposed = self.decomposed + _postnet_patterns[c] + return self.decomposed + + def computeSize(self): + self.width = len(self.decomposed) * self.barWidth + (len(self.decomposed) - 1) * self.spaceWidth + self.height = self.barHeight + + def draw(self): + sdown = self.barHeight - self.shortHeight + left = 0 + + for c in self.decomposed: + if c == '.': + h = self.shortHeight + else: + h = self.barHeight + self.rect(left, 0.0, self.barWidth, h) + left = left + self.barWidth + self.spaceWidth + self.drawHumanReadable() + + def _humanText(self): + return self.encoded[1:-1] diff --git a/bin/reportlab/graphics/barcode/widgets.py b/bin/reportlab/graphics/barcode/widgets.py new file mode 100644 index 00000000000..b350d513157 --- /dev/null +++ b/bin/reportlab/graphics/barcode/widgets.py @@ -0,0 +1,304 @@ +#copyright ReportLab Europe Limited. 2000-2006 +#see license.txt for license details +__version__=''' $Id: widgets.py 2851 2006-05-08 14:34:45Z rgbecker $ ''' +__all__= ( + 'BarcodeI2of5', + 'BarcodeCode128', + 'BarcodeStandard93', + 'BarcodeExtended93', + 'BarcodeStandard39', + 'BarcodeExtended39', + 'BarcodeMSI', + 'BarcodeCodabar', + 'BarcodeCode11', + 'BarcodeFIM', + 'BarcodePOSTNET', + ) + +from reportlab.lib.validators import isInt, isNumber, isColor, isString, isColorOrNone, OneOf, isBoolean, EitherOr, isNumberOrNone +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.lib.colors import black +from reportlab.graphics.shapes import Line, Rect, Group, NotImplementedError, String +from reportlab.graphics.charts.areas import PlotArea + +''' +#snippet + +#first make your Drawing +from reportlab.graphics.shapes import Drawing +d= Drawing(100,50) + +#create and set up the widget +from reportlab.graphics.barcode.widgets import BarcodeStandard93 +bc = BarcodeStandard93() +bc.value = 'RGB-123456' + +#add to the drawing and save +d.add(bc) +# d.save(formats=['gif','pict'],fnRoot='bc_sample') +''' + +class _BarcodeWidget(PlotArea): + _attrMap = AttrMap(BASE=PlotArea, + barStrokeColor = AttrMapValue(isColorOrNone, desc='Color of bar borders.'), + barFillColor = AttrMapValue(isColorOrNone, desc='Color of bar interior areas.'), + barStrokeWidth = AttrMapValue(isNumber, desc='Width of bar borders.'), + value = AttrMapValue(EitherOr((isString,isNumber)), desc='Value.'), + textColor = AttrMapValue(isColorOrNone, desc='Color of human readable text.'), + valid = AttrMapValue(isBoolean), + validated = AttrMapValue(isString,desc="validated form of input"), + encoded = AttrMapValue(None,desc="encoded form of input"), + decomposed = AttrMapValue(isString,desc="decomposed form of input"), + canv = AttrMapValue(None,desc="temporarily used for internal methods"), + gap = AttrMapValue(isNumberOrNone, desc='Width of inter character gaps.'), + ) + + barStrokeColor = barFillColor = textColor = black + barStrokeWidth = 0 + _BCC = None + def __init__(self,BCC=None,_value='',**kw): + self._BCC = BCC + class Combiner(self.__class__,BCC): + __name__ = self.__class__.__name__ + self.__class__ = Combiner + PlotArea.__init__(self) + self.x = self.y = 0 + kw.setdefault('value',_value) + BCC.__init__(self,**kw) + + def rect(self,x,y,w,h,**kw): + self._Gadd(Rect(self.x+x,self.y+y,w,h, + strokeColor=self.barStrokeColor,strokeWidth=self.barStrokeWidth, fillColor=self.barFillColor)) + + def draw(self): + if not self._BCC: raise NotImplementedError("Abstract class %s cannot be drawn" % self.__class__.__name__) + self.canv = self + G = Group() + self._Gadd = G.add + self._Gadd(Rect(self.x,self.y,self.width,self.height,fillColor=None,strokeColor=None,strokeWidth=0.0001)) + self._BCC.draw(self) + del self.canv, self._Gadd + return G + + def annotate(self,x,y,text,fontName,fontSize,anchor='middle'): + self._Gadd(String(self.x+x,self.y+y,text,fontName=fontName,fontSize=fontSize, + textAnchor=anchor,fillColor=self.textColor)) + +class BarcodeI2of5(_BarcodeWidget): + """Interleaved 2 of 5 is used in distribution and warehouse industries. + + It encodes an even-numbered sequence of numeric digits. There is an optional + module 10 check digit; if including this, the total length must be odd so that + it becomes even after including the check digit. Otherwise the length must be + even. Since the check digit is optional, our library does not check it. + """ + + _tests = [ + '12', + '1234', + '123456', + '12345678', + '1234567890' + ] + codeName = "I2of5" + _attrMap = AttrMap(BASE=_BarcodeWidget, + barWidth = AttrMapValue(isNumber,'''(float, default .0075): + X-Dimension, or width of the smallest element + Minumum is .0075 inch (7.5 mils).'''), + ratio = AttrMapValue(isNumber,'''(float, default 2.2): + The ratio of wide elements to narrow elements. + Must be between 2.0 and 3.0 (or 2.2 and 3.0 if the + barWidth is greater than 20 mils (.02 inch))'''), + gap = AttrMapValue(isNumberOrNone,'''(float or None, default None): + width of intercharacter gap. None means "use barWidth".'''), + barHeight = AttrMapValue(isNumber,'''(float, see default below): + Height of the symbol. Default is the height of the two + bearer bars (if they exist) plus the greater of .25 inch + or .15 times the symbol's length.'''), + checksum = AttrMapValue(isBoolean,'''(bool, default 1): + Whether to compute and include the check digit'''), + bearers = AttrMapValue(isNumber,'''(float, in units of barWidth. default 3.0): + Height of bearer bars (horizontal bars along the top and + bottom of the barcode). Default is 3 x-dimensions. + Set to zero for no bearer bars. (Bearer bars help detect + misscans, so it is suggested to leave them on).'''), + quiet = AttrMapValue(isBoolean,'''(bool, default 1): + Whether to include quiet zones in the symbol.'''), + + lquiet = AttrMapValue(isNumber,'''(float, see default below): + Quiet zone size to left of code, if quiet is true. + Default is the greater of .25 inch, or .15 times the symbol's + length.'''), + + rquiet = AttrMapValue(isNumber,'''(float, defaults as above): + Quiet zone size to right left of code, if quiet is true.'''), + fontName = AttrMapValue(isString, desc='human readable font'), + fontSize = AttrMapValue(isNumber, desc='human readable font size'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'), + ) + _bcTransMap = {} + + def __init__(self,**kw): + from reportlab.graphics.barcode.common import I2of5 + _BarcodeWidget.__init__(self,I2of5,1234,**kw) + +class BarcodeCode128(BarcodeI2of5): + """Code 128 encodes any number of characters in the ASCII character set. + """ + _tests = [ + 'ReportLab Rocks!' + ] + codeName = "Code128" + _attrMap = AttrMap(BASE=BarcodeI2of5,UNWANTED=('bearers','checksum','ratio','checksum','stop')) + def __init__(self,**kw): + from reportlab.graphics.barcode.code128 import Code128 + _BarcodeWidget.__init__(self,Code128,"AB-12345678",**kw) + +class BarcodeStandard93(BarcodeCode128): + """This is a compressed form of Code 39""" + codeName = "Standard93" + _attrMap = AttrMap(BASE=BarcodeCode128, + stop = AttrMapValue(isBoolean, desc='if we use start/stop symbols (default 1)'), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.code93 import Standard93 + _BarcodeWidget.__init__(self,Standard93,"CODE 93",**kw) + +class BarcodeExtended93(BarcodeStandard93): + """This is a compressed form of Code 39, allowing the full ASCII charset""" + codeName = "Extended93" + def __init__(self,**kw): + from reportlab.graphics.barcode.code93 import Extended93 + _BarcodeWidget.__init__(self,Extended93,"L@@K! Code 93 ;-)",**kw) + +class BarcodeStandard39(BarcodeI2of5): + """Code39 is widely used in non-retail, especially US defence and health. + Allowed characters are 0-9, A-Z (caps only), space, and -.$/+%*. + """ + + codeName = "Standard39" + def __init__(self,**kw): + from reportlab.graphics.barcode.code39 import Standard39 + _BarcodeWidget.__init__(self,Standard39,"A012345B%R",**kw) + +class BarcodeExtended39(BarcodeI2of5): + """Extended 39 encodes the full ASCII character set by encoding + characters as pairs of Code 39 characters; $, /, % and + are used as + shift characters.""" + + codeName = "Extended39" + def __init__(self,**kw): + from reportlab.graphics.barcode.code39 import Extended39 + _BarcodeWidget.__init__(self,Extended39,"A012345B}",**kw) + +class BarcodeMSI(BarcodeI2of5): + """MSI is used for inventory control in retail applications. + + There are several methods for calculating check digits so we + do not implement one. + """ + codeName = "MSI" + def __init__(self,**kw): + from reportlab.graphics.barcode.common import MSI + _BarcodeWidget.__init__(self,MSI,1234,**kw) + +class BarcodeCodabar(BarcodeI2of5): + """Used in blood banks, photo labs and FedEx labels. + Encodes 0-9, -$:/.+, and four start/stop characters A-D. + """ + codeName = "Codabar" + def __init__(self,**kw): + from reportlab.graphics.barcode.common import Codabar + _BarcodeWidget.__init__(self,Codabar,"A012345B",**kw) + +class BarcodeCode11(BarcodeI2of5): + """Used mostly for labelling telecommunications equipment. + It encodes numeric digits. + """ + codeName = "Code11" + _attrMap = AttrMap(BASE=BarcodeI2of5, + checksum = AttrMapValue(isInt,'''(integer, default 2): + Whether to compute and include the check digit(s). + (0 none, 1 1-digit, 2 2-digit, -1 auto, default -1): + How many checksum digits to include. -1 ("auto") means + 1 if the number of digits is 10 or less, else 2.'''), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.common import Code11 + _BarcodeWidget.__init__(self,Code11,"01234545634563",**kw) + +class BarcodeFIM(_BarcodeWidget): + """ + FIM was developed as part of the POSTNET barcoding system. FIM (Face Identification Marking) is used by the cancelling machines to sort mail according to whether or not they have bar code and their postage requirements. There are four types of FIM called FIM A, FIM B, FIM C, and FIM D. + + The four FIM types have the following meanings: + FIM A- Postage required pre-barcoded + FIM B - Postage pre-paid, no bar code exists + FIM C- Postage prepaid prebarcoded + FIM D- Postage required, no bar code exists + """ + codeName = "FIM" + _attrMap = AttrMap(BASE=_BarcodeWidget, + barWidth = AttrMapValue(isNumber,'''(float, default 1/32in): the bar width.'''), + spaceWidth = AttrMapValue(isNumber,'''(float or None, default 1/16in): + width of intercharacter gap. None means "use barWidth".'''), + barHeight = AttrMapValue(isNumber,'''(float, default 5/8in): The bar height.'''), + quiet = AttrMapValue(isBoolean,'''(bool, default 0): + Whether to include quiet zones in the symbol.'''), + lquiet = AttrMapValue(isNumber,'''(float, default: 15/32in): + Quiet zone size to left of code, if quiet is true.'''), + rquiet = AttrMapValue(isNumber,'''(float, default 1/4in): + Quiet zone size to right left of code, if quiet is true.'''), + fontName = AttrMapValue(isString, desc='human readable font'), + fontSize = AttrMapValue(isNumber, desc='human readable font size'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.usps import FIM + _BarcodeWidget.__init__(self,FIM,"A",**kw) + +class BarcodePOSTNET(_BarcodeWidget): + codeName = "POSTNET" + _attrMap = AttrMap(BASE=_BarcodeWidget, + barWidth = AttrMapValue(isNumber,'''(float, default 0.018*in): the bar width.'''), + spaceWidth = AttrMapValue(isNumber,'''(float or None, default 0.0275in): width of intercharacter gap.'''), + shortHeight = AttrMapValue(isNumber,'''(float, default 0.05in): The short bar height.'''), + barHeight = AttrMapValue(isNumber,'''(float, default 0.125in): The full bar height.'''), + fontName = AttrMapValue(isString, desc='human readable font'), + fontSize = AttrMapValue(isNumber, desc='human readable font size'), + humanReadable = AttrMapValue(isBoolean, desc='if human readable'), + ) + def __init__(self,**kw): + from reportlab.graphics.barcode.usps import POSTNET + _BarcodeWidget.__init__(self,POSTNET,"78247-1043",**kw) + +if __name__=='__main__': + import os, sys, glob + from reportlab.graphics.shapes import Drawing + os.chdir(os.path.dirname(sys.argv[0])) + if not os.path.isdir('out'): + os.mkdir('out') + map(os.remove,glob.glob(os.path.join('out','*'))) + html = [''] + a = html.append + for C in (BarcodeI2of5, + BarcodeCode128, + BarcodeStandard93, + BarcodeExtended93, + BarcodeStandard39, + BarcodeExtended39, + BarcodeMSI, + BarcodeCodabar, + BarcodeCode11, + BarcodeFIM, + BarcodePOSTNET, + ): + name = C.__name__ + i = C() + D = Drawing(100,50) + D.add(i) + D.save(formats=['gif','pict'],outDir='out',fnRoot=name) + a('

%s


' % (name, name)) + a('') + open(os.path.join('out','index.html'),'w').write('\n'.join(html)) diff --git a/bin/reportlab/graphics/charts/__init__.py b/bin/reportlab/graphics/charts/__init__.py new file mode 100644 index 00000000000..6b2a42748cc --- /dev/null +++ b/bin/reportlab/graphics/charts/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/areas.py b/bin/reportlab/graphics/charts/areas.py new file mode 100644 index 00000000000..7588d924009 --- /dev/null +++ b/bin/reportlab/graphics/charts/areas.py @@ -0,0 +1,92 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/areas.py +"""This module defines a Area mixin classes +""" +__version__=''' $Id: areas.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isNoneOrShape +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics.shapes import Rect, Group, Line, Polygon +from reportlab.lib.attrmap import AttrMap, AttrMapValue + +class PlotArea(Widget): + "Abstract base class representing a chart's plot area, pretty unusable by itself." + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc='X position of the lower-left corner of the chart.'), + y = AttrMapValue(isNumber, desc='Y position of the lower-left corner of the chart.'), + width = AttrMapValue(isNumber, desc='Width of the chart.'), + height = AttrMapValue(isNumber, desc='Height of the chart.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of the plot area border.'), + strokeWidth = AttrMapValue(isNumber, desc='Width plot area border.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color of the plot area interior.'), + background = AttrMapValue(isNoneOrShape, desc='Handle to background object.'), + debug = AttrMapValue(isNumber, desc='Used only for debugging.'), + ) + + def __init__(self): + self.x = 20 + self.y = 10 + self.height = 85 + self.width = 180 + self.strokeColor = None + self.strokeWidth = 1 + self.fillColor = None + self.background = None + self.debug = 0 + + def makeBackground(self): + if self.background is not None: + BG = self.background + if isinstance(BG,Group): + g = BG + for bg in g.contents: + bg.x = self.x + bg.y = self.y + bg.width = self.width + bg.height = self.height + else: + g = Group() + if type(BG) not in (type(()),type([])): BG=(BG,) + for bg in BG: + bg.x = self.x + bg.y = self.y + bg.width = self.width + bg.height = self.height + g.add(bg) + return g + else: + strokeColor,strokeWidth,fillColor=self.strokeColor, self.strokeWidth, self.fillColor + if (strokeWidth and strokeColor) or fillColor: + g = Group() + _3d_dy = getattr(self,'_3d_dy',None) + x = self.x + y = self.y + h = self.height + w = self.width + if _3d_dy is not None: + _3d_dx = self._3d_dx + if fillColor and not strokeColor: + from reportlab.lib.colors import Blacker + c = Blacker(fillColor, getattr(self,'_3d_blacken',0.7)) + else: + c = strokeColor + if not strokeWidth: strokeWidth = 0.5 + if fillColor or strokeColor or c: + bg = Polygon([x,y,x,y+h,x+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+h+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y], + strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fillColor) + g.add(bg) + g.add(Line(x,y,x+_3d_dx,y+_3d_dy, strokeWidth=0.5, strokeColor=c)) + g.add(Line(x+_3d_dx,y+_3d_dy, x+_3d_dx,y+h+_3d_dy,strokeWidth=0.5, strokeColor=c)) + fc = Blacker(c, getattr(self,'_3d_blacken',0.8)) + g.add(Polygon([x,y,x+_3d_dx,y+_3d_dy,x+w+_3d_dx,y+_3d_dy,x+w,y], + strokeColor=strokeColor or c or grey, strokeWidth=strokeWidth, fillColor=fc)) + bg = Line(x+_3d_dx,y+_3d_dy, x+w+_3d_dx,y+_3d_dy,strokeWidth=0.5, strokeColor=c) + else: + bg = None + else: + bg = Rect(x, y, w, h, + strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor) + if bg: g.add(bg) + return g + else: + return None diff --git a/bin/reportlab/graphics/charts/axes.py b/bin/reportlab/graphics/charts/axes.py new file mode 100644 index 00000000000..ab0be2ad060 --- /dev/null +++ b/bin/reportlab/graphics/charts/axes.py @@ -0,0 +1,1979 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/axes.py +"""Collection of axes for charts. + +The current collection comprises axes for charts using cartesian +coordinate systems. All axes might have tick marks and labels. +There are two dichotomies for axes: one of X and Y flavours and +another of category and value flavours. + +Category axes have an ordering but no metric. They are divided +into a number of equal-sized buckets. Their tick marks or labels, +if available, go BETWEEN the buckets, and the labels are placed +below to/left of the X/Y-axis, respectively. + + Value axes have an ordering AND metric. They correspond to a nu- + meric quantity. Value axis have a real number quantity associated + with it. The chart tells it where to go. + The most basic axis divides the number line into equal spaces + and has tickmarks and labels associated with each; later we + will add variants where you can specify the sampling + interval. + +The charts using axis tell them where the labels should be placed. + +Axes of complementary X/Y flavours can be connected to each other +in various ways, i.e. with a specific reference point, like an +x/value axis to a y/value (or category) axis. In this case the +connection can be either at the top or bottom of the former or +at any absolute value (specified in points) or at some value of +the former axes in its own coordinate system. +""" +__version__=''' $Id: axes.py 2838 2006-04-18 17:47:54Z rgbecker $ ''' + +import string +from types import FunctionType, StringType, TupleType, ListType + +from reportlab.lib.validators import isNumber, isNumberOrNone, isListOfStringsOrNone, isListOfNumbers, \ + isListOfNumbersOrNone, isColorOrNone, OneOf, isBoolean, SequenceOf, \ + isString, EitherOr +from reportlab.lib.attrmap import * +from reportlab.lib import normalDate +from reportlab.graphics.shapes import Drawing, Line, PolyLine, Group, STATE_DEFAULTS, _textBoxLimits, _rotatedBoxLimits +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.charts.utils import nextRoundNumber + + +# Helpers. + +def _findMinMaxValue(V, x, default, func, special=None): + if type(V[0][0]) in (TupleType,ListType): + if special: + f=lambda T,x=x,special=special,func=func: special(T,x,func) + else: + f=lambda T,x=x: T[x] + V=map(lambda e,f=f: map(f,e),V) + V = filter(len,map(lambda x: filter(lambda x: x is not None,x),V)) + if len(V)==0: return default + return func(map(func,V)) + +def _findMin(V, x, default,special=None): + '''find minimum over V[i][x]''' + return _findMinMaxValue(V,x,default,min,special=special) + +def _findMax(V, x, default,special=None): + '''find maximum over V[i][x]''' + return _findMinMaxValue(V,x,default,max,special=special) + +def _allInt(values): + '''true if all values are int''' + for v in values: + try: + if int(v)!=v: return 0 + except: + return 0 + return 1 + +class _AxisG(Widget): + def _get_line_pos(self,v): + v = self.scale(v) + try: + v = v[0] + except: + pass + return v + + def _cxLine(self,x,start,end): + x = self._get_line_pos(x) + return Line(x, self._y + start, x, self._y + end) + + def _cyLine(self,y,start,end): + y = self._get_line_pos(y) + return Line(self._x + start, y, self._x + end, y) + + def _cxLine3d(self,x,start,end,_3d_dx,_3d_dy): + x = self._get_line_pos(x) + y0 = self._y + start + y1 = self._y + end + y0, y1 = min(y0,y1),max(y0,y1) + x1 = x + _3d_dx + return PolyLine([x,y0,x1,y0+_3d_dy,x1,y1+_3d_dy],strokeLineJoin=1) + + def _cyLine3d(self,y,start,end,_3d_dx,_3d_dy): + y = self._get_line_pos(y) + x0 = self._x + start + x1 = self._x + end + x0, x1 = min(x0,x1),max(x0,x1) + y1 = y + _3d_dy + return PolyLine([x0,y,x0+_3d_dx,y1,x1+_3d_dx,y1],strokeLineJoin=1) + + def _getLineFunc(self, start, end, parent=None): + _3d_dx = getattr(parent,'_3d_dx',None) + if _3d_dx is not None: + _3d_dy = getattr(parent,'_3d_dy',None) + f = self._dataIndex and self._cyLine3d or self._cxLine3d + return lambda v, s=start, e=end, f=f,_3d_dx=_3d_dx,_3d_dy=_3d_dy: f(v,s,e,_3d_dx=_3d_dx,_3d_dy=_3d_dy) + else: + f = self._dataIndex and self._cyLine or self._cxLine + return lambda v, s=start, e=end, f=f: f(v,s,e) + + def _makeLines(self,g,start,end,strokeColor,strokeWidth,strokeDashArray,parent=None): + func = self._getLineFunc(start,end,parent) + for t in self._tickValues: + L = func(t) + L.strokeColor = strokeColor + L.strokeWidth = strokeWidth + L.strokeDashArray = strokeDashArray + g.add(L) + + def makeGrid(self,g,dim=None,parent=None): + '''this is only called by a container object''' + s = self.gridStart + e = self.gridEnd + if dim: + s = s is None and dim[0] + e = e is None and dim[0]+dim[1] + c = self.gridStrokeColor + if self.visibleGrid and (s or e) and c is not None: + if self._dataIndex: offs = self._x + else: offs = self._y + self._makeLines(g,s-offs,e-offs,c,self.gridStrokeWidth,self.gridStrokeDashArray,parent=parent) + +# Category axes. +class CategoryAxis(_AxisG): + "Abstract category axis, unusable in itself." + _nodoc = 1 + _attrMap = AttrMap( + visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'), + visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'), + visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'), + visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'), + visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'), + gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'), + gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'), + gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'), + gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'), + gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'), + labels = AttrMapValue(None, desc='Handle of the axis labels.'), + categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'), + joinAxis = AttrMapValue(None, desc='Join both axes if true.'), + joinAxisPos = AttrMapValue(isNumberOrNone, desc='Position at which to join with other axis.'), + reverseDirection = AttrMapValue(isBoolean, desc='If true reverse category direction.'), + style = AttrMapValue(OneOf('parallel','stacked','parallel_3d'),"How common category bars are plotted"), + labelAxisMode = AttrMapValue(OneOf('high','low','axis'), desc="Like joinAxisMode, but for the axis labels"), + tickShift = AttrMapValue(isBoolean, desc='Tick shift typically'), + ) + + def __init__(self): + assert self.__class__.__name__!='CategoryAxis', "Abstract Class CategoryAxis Instantiated" + # private properties set by methods. The initial values + # here are to make demos easy; they would always be + # overridden in real life. + self._x = 50 + self._y = 50 + self._length = 100 + self._catCount = 0 + + # public properties + self.visible = 1 + self.visibleAxis = 1 + self.visibleTicks = 1 + self.visibleLabels = 1 + self.visibleGrid = 0 + + self.strokeWidth = 1 + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStrokeWidth = 0.25 + self.gridStrokeColor = STATE_DEFAULTS['strokeColor'] + self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStart = self.gridEnd = 0 + self.labels = TypedPropertyCollection(Label) + # if None, they don't get labels. If provided, + # you need one name per data point and they are + # used for label text. + self.categoryNames = None + self.joinAxis = None + self.joinAxisPos = None + self.joinAxisMode = None + self.labelAxisMode = 'axis' + self.reverseDirection = 0 + self.style = 'parallel' + + #various private things which need to be initialized + self._labelTextFormat = None + self.tickShift = 0 + + def setPosition(self, x, y, length): + # ensure floating point + self._x = x + self._y = y + self._length = length + + + def configure(self, multiSeries,barWidth=None): + self._catCount = max(map(len,multiSeries)) + self._barWidth = barWidth or (self._length/float(self._catCount or 1)) + self._calcTickmarkPositions() + + def _calcTickmarkPositions(self): + n = self._catCount + if self.tickShift: + self._tickValues = [t+0.5 for t in xrange(n)] + else: + self._tickValues = range(n+1) + + def draw(self): + g = Group() + + if not self.visible: + return g + + g.add(self.makeAxis()) + g.add(self.makeTicks()) + g.add(self.makeTickLabels()) + + return g + + def _scale(self,idx): + if self.reverseDirection: idx = self._catCount-idx-1 + return idx + +def _assertYAxis(axis): + acn = axis.__class__.__name__ + assert acn[0]=='Y' or acn[:4]=='AdjY', "Cannot connect to other axes (%s), but Y- ones." % acn +def _assertXAxis(axis): + acn = axis.__class__.__name__ + assert acn[0]=='X' or acn[:11]=='NormalDateX', "Cannot connect to other axes (%s), but X- ones." % acn + +class XCategoryAxis(CategoryAxis): + "X/category axis" + + _attrMap = AttrMap(BASE=CategoryAxis, + tickUp = AttrMapValue(isNumber, + desc='Tick length up the axis.'), + tickDown = AttrMapValue(isNumber, + desc='Tick length down the axis.'), + joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None), + desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."), + ) + + _dataIndex = 0 + + def __init__(self): + CategoryAxis.__init__(self) + self.labels.boxAnchor = 'n' #north - top edge + self.labels.dy = -5 + # ultra-simple tick marks for now go between categories + # and have same line style as axis - need more + self.tickUp = 0 # how far into chart does tick go? + self.tickDown = 5 # how far below axis does tick go? + + + def demo(self): + self.setPosition(30, 70, 140) + self.configure([(10,20,30,40,50)]) + + self.categoryNames = ['One','Two','Three','Four','Five'] + # all labels top-centre aligned apart from the last + self.labels.boxAnchor = 'n' + self.labels[4].boxAnchor = 'e' + self.labels[4].angle = 90 + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, yAxis, mode='bottom', pos=None): + "Join with y-axis using some mode." + + _assertYAxis(yAxis) + if mode == 'bottom': + self._x = yAxis._x + self._y = yAxis._y + elif mode == 'top': + self._x = yAxis._x + self._y = yAxis._y + yAxis._length + elif mode == 'value': + self._x = yAxis._x + self._y = yAxis.scale(pos) + elif mode == 'points': + self._x = yAxis._x + self._y = pos + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('bottom', 'top'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, idx): + """returns the x position and width in drawing units of the slice""" + return (self._x + self._scale(idx)*self._barWidth, self._barWidth) + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x + self._length, self._y) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if not self.visibleTicks: return g + if self.tickUp or self.tickDown: + self._makeLines(g,self.tickUp,-self.tickDown,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + + def _labelAxisPos(self): + axis = self.joinAxis + if axis: + mode = self.labelAxisMode + if mode == 'low': + return axis._y + elif mode == 'high': + return axis._y + axis._length + return self._y + + def makeTickLabels(self): + g = Group() + + if not self.visibleLabels: return g + + categoryNames = self.categoryNames + if categoryNames is not None: + catCount = self._catCount + n = len(categoryNames) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + _y = self._labelAxisPos() + _x = self._x + + for i in xrange(catCount): + if reverseDirection: ic = catCount-i-1 + else: ic = i + if ic>=n: continue + x = _x + (i+0.5) * barWidth + label = self.labels[i] + label.setOrigin(x, _y) + label.setText(categoryNames[ic] or '') + g.add(label) + + return g + + +class YCategoryAxis(CategoryAxis): + "Y/category axis" + + _attrMap = AttrMap(BASE=CategoryAxis, + tickLeft = AttrMapValue(isNumber, + desc='Tick length left of the axis.'), + tickRight = AttrMapValue(isNumber, + desc='Tick length right of the axis.'), + joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)), + desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."), + ) + + _dataIndex = 1 + + + def __init__(self): + CategoryAxis.__init__(self) + self.labels.boxAnchor = 'e' #east - right edge + self.labels.dx = -5 + # ultra-simple tick marks for now go between categories + # and have same line style as axis - need more + self.tickLeft = 5 # how far left of axis does tick go? + self.tickRight = 0 # how far right of axis does tick go? + + + def demo(self): + self.setPosition(50, 10, 80) + self.configure([(10,20,30)]) + self.categoryNames = ['One','Two','Three'] + # all labels top-centre aligned apart from the last + self.labels.boxAnchor = 'e' + self.labels[2].boxAnchor = 's' + self.labels[2].angle = 90 + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, xAxis, mode='left', pos=None): + "Join with x-axis using some mode." + + _assertXAxis(xAxis) + + if mode == 'left': + self._x = xAxis._x * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'right': + self._x = (xAxis._x + xAxis._length) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'value': + self._x = xAxis.scale(pos) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'points': + self._x = pos * 1.0 + self._y = xAxis._y * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('left', 'right'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, idx): + "Returns the y position and width in drawing units of the slice." + return (self._y + self._scale(idx)*self._barWidth, self._barWidth) + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x, self._y + self._length) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if not self.visibleTicks: return g + if self.tickLeft or self.tickRight: + self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + + def _labelAxisPos(self): + axis = self.joinAxis + if axis: + mode = self.labelAxisMode + if mode == 'low': + return axis._x + elif mode == 'high': + return axis._x + axis._length + return self._x + + def makeTickLabels(self): + g = Group() + + if not self.visibleTicks: return g + + categoryNames = self.categoryNames + if categoryNames is not None: + catCount = self._catCount + n = len(categoryNames) + reverseDirection = self.reverseDirection + barWidth = self._barWidth + labels = self.labels + _x = self._labelAxisPos() + _y = self._y + for i in xrange(catCount): + if reverseDirection: ic = catCount-i-1 + else: ic = i + if ic>=n: continue + y = _y + (i+0.5) * barWidth + label = labels[i] + label.setOrigin(_x, y) + label.setText(categoryNames[ic] or '') + g.add(label) + return g + + +# Value axes. +class ValueAxis(_AxisG): + "Abstract value axis, unusable in itself." + + _attrMap = AttrMap( + forceZero = AttrMapValue(EitherOr((isBoolean,OneOf('near'))), desc='Ensure zero in range if true.'), + visible = AttrMapValue(isBoolean, desc='Display entire object, if true.'), + visibleAxis = AttrMapValue(isBoolean, desc='Display axis line, if true.'), + visibleLabels = AttrMapValue(isBoolean, desc='Display axis labels, if true.'), + visibleTicks = AttrMapValue(isBoolean, desc='Display axis ticks, if true.'), + visibleGrid = AttrMapValue(isBoolean, desc='Display axis grid, if true.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of axis line and ticks.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of axis line and ticks.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for axis line.'), + gridStrokeWidth = AttrMapValue(isNumber, desc='Width of grid lines.'), + gridStrokeColor = AttrMapValue(isColorOrNone, desc='Color of grid lines.'), + gridStrokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array used for grid lines.'), + gridStart = AttrMapValue(isNumberOrNone, desc='Start of grid lines wrt axis origin'), + gridEnd = AttrMapValue(isNumberOrNone, desc='End of grid lines wrt axis origin'), + minimumTickSpacing = AttrMapValue(isNumber, desc='Minimum value for distance between ticks.'), + maximumTicks = AttrMapValue(isNumber, desc='Maximum number of ticks.'), + labels = AttrMapValue(None, desc='Handle of the axis labels.'), + labelTextFormat = AttrMapValue(None, desc='Formatting string or function used for axis labels.'), + labelTextPostFormat = AttrMapValue(None, desc='Extra Formatting string.'), + labelTextScale = AttrMapValue(isNumberOrNone, desc='Scaling for label tick values.'), + valueMin = AttrMapValue(isNumberOrNone, desc='Minimum value on axis.'), + valueMax = AttrMapValue(isNumberOrNone, desc='Maximum value on axis.'), + valueStep = AttrMapValue(isNumberOrNone, desc='Step size used between ticks.'), + valueSteps = AttrMapValue(isListOfNumbersOrNone, desc='List of step sizes used between ticks.'), + avoidBoundFrac = AttrMapValue(EitherOr((isNumberOrNone,SequenceOf(isNumber,emptyOK=0,lo=2,hi=2))), desc='Fraction of interval to allow above and below.'), + rangeRound=AttrMapValue(OneOf('none','both','ceiling','floor'),'How to round the axis limits'), + zrangePref = AttrMapValue(isNumberOrNone, desc='Zero range axis limit preference.'), + style = AttrMapValue(OneOf('normal','stacked','parallel_3d'),"How values are plotted!"), + ) + + def __init__(self): + assert self.__class__.__name__!='ValueAxis', 'Abstract Class ValueAxis Instantiated' + self._configured = 0 + # private properties set by methods. The initial values + # here are to make demos easy; they would always be + # overridden in real life. + self._x = 50 + self._y = 50 + self._length = 100 + + # public properties + self.visible = 1 + self.visibleAxis = 1 + self.visibleLabels = 1 + self.visibleTicks = 1 + self.visibleGrid = 0 + self.forceZero = 0 + + self.strokeWidth = 1 + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStrokeWidth = 0.25 + self.gridStrokeColor = STATE_DEFAULTS['strokeColor'] + self.gridStrokeDashArray = STATE_DEFAULTS['strokeDashArray'] + self.gridStart = self.gridEnd = 0 + + self.labels = TypedPropertyCollection(Label) + self.labels.angle = 0 + + # how close can the ticks be? + self.minimumTickSpacing = 10 + self.maximumTicks = 7 + + # a format string like '%0.2f' + # or a function which takes the value as an argument and returns a string + self._labelTextFormat = self.labelTextFormat = self.labelTextPostFormat = self.labelTextScale = None + + # if set to None, these will be worked out for you. + # if you override any or all of them, your values + # will be used. + self.valueMin = None + self.valueMax = None + self.valueStep = None + self.avoidBoundFrac = None + self.rangeRound = 'none' + self.zrangePref = 0 + self.style = 'normal' + + def setPosition(self, x, y, length): + # ensure floating point + self._x = x * 1.0 + self._y = y * 1.0 + self._length = length * 1.0 + + def configure(self, dataSeries): + """Let the axis configure its scale and range based on the data. + + Called after setPosition. Let it look at a list of lists of + numbers determine the tick mark intervals. If valueMin, + valueMax and valueStep are configured then it + will use them; if any of them are set to None it + will look at the data and make some sensible decision. + You may override this to build custom axes with + irregular intervals. It creates an internal + variable self._values, which is a list of numbers + to use in plotting. + """ + self._setRange(dataSeries) + self._calcTickmarkPositions() + self._calcScaleFactor() + self._configured = 1 + + def _getValueStepAndTicks(self, valueMin, valueMax,cache={}): + try: + K = (valueMin,valueMax) + r = cache[K] + except: + self._valueMin = valueMin + self._valueMax = valueMax + T = self._calcTickPositions() + if len(T)>1: + valueStep = T[1]-T[0] + else: + oVS = self.valueStep + self.valueStep = None + T = self._calcTickPositions() + self.valueStep = oVS + if len(T)>1: + valueStep = T[1]-T[0] + else: + valueStep = self._valueStep + r = cache[K] = valueStep, T, valueStep*1e-8 + return r + + def _setRange(self, dataSeries): + """Set minimum and maximum axis values. + + The dataSeries argument is assumed to be a list of data + vectors. Each vector is itself a list or tuple of numbers. + + Returns a min, max tuple. + """ + + oMin = valueMin = self.valueMin + oMax = valueMax = self.valueMax + rangeRound = self.rangeRound + if valueMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0) + if valueMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0) + if valueMin == valueMax: + if valueMax==0: + if oMin is None and oMax is None: + zrp = getattr(self,'zrangePref',0) + if zrp>0: + valueMax = zrp + valueMin = 0 + elif zrp<0: + valueMax = 0 + valueMin = zrp + else: + valueMax = 0.01 + valueMin = -0.01 + elif self.valueMin is None: + valueMin = -0.01 + else: + valueMax = 0.01 + else: + if valueMax>0: + valueMax = 1.2*valueMax + valueMin = 0.0 + else: + valueMax = 0.0 + valueMin = 1.2*valueMin + + if getattr(self,'_bubblePlot',None): + bubbleMax = float(_findMax(dataSeries,2,0)) + frac=.25 + bubbleV=frac*(valueMax-valueMin) + self._bubbleV = bubbleV + self._bubbleMax = bubbleMax + self._bubbleRadius = frac*self._length + def special(T,x,func,bubbleV=bubbleV,bubbleMax=bubbleMax): + try: + v = T[2] + except IndexError: + v = bubbleMAx*0.1 + bubbleV *= (v/bubbleMax)**0.5 + return func(T[x]+bubbleV,T[x]-bubbleV) + if oMin is None: valueMin = self._cValueMin = _findMin(dataSeries,self._dataIndex,0,special=special) + if oMax is None: valueMax = self._cValueMax = _findMax(dataSeries,self._dataIndex,0,special=special) + + forceZero = self.forceZero + if forceZero: + if forceZero=='near': + forceZero = min(abs(valueMin),abs(valueMax)) <= 5*(valueMax-valueMin) + if forceZero: + if valueMax<0: valueMax=0 + elif valueMin>0: valueMin = 0 + + abf = self.avoidBoundFrac + do_rr = not getattr(self,'valueSteps',None) + do_abf = abf and do_rr + if type(abf) not in (TupleType,ListType): + abf = abf, abf + do_rr = rangeRound is not 'none' and do_rr + if do_rr: + rrn = rangeRound in ['both','floor'] + rrx = rangeRound in ['both','ceiling'] + else: + rrn = rrx = 0 + + go = do_rr or do_abf + cache = {} + cMin = valueMin + cMax = valueMax + iter = 0 + while go and iter<=10: + iter += 1 + go = 0 + if do_abf: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + i0 = valueStep*abf[0] + i1 = valueStep*abf[1] + if rrn: v = T[0] + else: v = valueMin + u = cMin-i0 + if abs(v)>fuzz and v>=u+fuzz: + valueMin = u + go = 1 + if rrx: v = T[-1] + else: v = valueMax + u = cMax+i1 + if abs(v)>fuzz and v<=u-fuzz: + valueMax = u + go = 1 + + if do_rr: + valueStep, T, fuzz = self._getValueStepAndTicks(valueMin, valueMax, cache) + if rrn: + if valueMin=T[0]+fuzz + valueMin = T[0] + if rrx: + if valueMax>T[-1]+fuzz: + valueMax = T[-1]+valueStep + go = 1 + else: + go = valueMax<=T[-1]-fuzz + valueMax = T[-1] + + self._valueMin, self._valueMax = valueMin, valueMax + self._rangeAdjust() + + def _rangeAdjust(self): + """Override this if you want to alter the calculated range. + + E.g. if want a minumamum range of 30% or don't want 100% + as the first point. + """ + pass + + def _adjustAxisTicks(self): + '''Override if you want to put slack at the ends of the axis + eg if you don't want the last tick to be at the bottom etc + ''' + pass + + def _calcScaleFactor(self): + """Calculate the axis' scale factor. + This should be called only *after* the axis' range is set. + Returns a number. + """ + self._scaleFactor = self._length / float(self._valueMax - self._valueMin) + return self._scaleFactor + + def _calcTickPositions(self): + self._calcValueStep() + valueMin, valueMax, valueStep = self._valueMin, self._valueMax, self._valueStep + fuzz = 1e-8*valueStep + rangeRound = self.rangeRound + i0 = int(float(valueMin)/valueStep) + v = i0*valueStep + if rangeRound in ('both','floor'): + if v>valueMin+fuzz: i0 -= 1 + elif vvalueMax+fuzz: i1 -= 1 + return [i*valueStep for i in xrange(i0,i1+1)] + + def _calcTickmarkPositions(self): + """Calculate a list of tick positions on the axis. Returns a list of numbers.""" + self._tickValues = getattr(self,'valueSteps',None) + if self._tickValues: return self._tickValues + self._tickValues = self._calcTickPositions() + self._adjustAxisTicks() + return self._tickValues + + def _calcValueStep(self): + '''Calculate _valueStep for the axis or get from valueStep.''' + if self.valueStep is None: + rawRange = self._valueMax - self._valueMin + rawInterval = rawRange / min(float(self.maximumTicks-1),(float(self._length)/self.minimumTickSpacing)) + self._valueStep = nextRoundNumber(rawInterval) + else: + self._valueStep = self.valueStep + + def _allIntTicks(self): + return _allInt(self._tickValues) + + def makeTickLabels(self): + g = Group() + if not self.visibleLabels: return g + + f = self._labelTextFormat # perhaps someone already set it + if f is None: + f = self.labelTextFormat or (self._allIntTicks() and '%.0f' or str) + elif f is str and self._allIntTicks(): f = '%.0f' + post = self.labelTextPostFormat + scl = self.labelTextScale + pos = [self._x, self._y] + d = self._dataIndex + labels = self.labels + + i = 0 + for tick in self._tickValues: + if f and labels[i].visible: + v = self.scale(tick) + if scl is not None: + t = tick*scl + else: + t = tick + if type(f) is StringType: txt = f % t + elif type(f) in (TupleType,ListType): + #it's a list, use as many items as we get + if i < len(f): + txt = f[i] + else: + txt = '' + elif callable(f): + txt = f(t) + else: + raise ValueError, 'Invalid labelTextFormat %s' % f + if post: txt = post % txt + label = labels[i] + pos[d] = v + apply(label.setOrigin,pos) + label.setText(txt) + g.add(label) + i = i + 1 + + return g + + def draw(self): + g = Group() + + if not self.visible: + return g + + g.add(self.makeAxis()) + g.add(self.makeTicks()) + g.add(self.makeTickLabels()) + + return g + + +class XValueAxis(ValueAxis): + "X/value axis" + + _attrMap = AttrMap(BASE=ValueAxis, + tickUp = AttrMapValue(isNumber, + desc='Tick length up the axis.'), + tickDown = AttrMapValue(isNumber, + desc='Tick length down the axis.'), + joinAxis = AttrMapValue(None, + desc='Join both axes if true.'), + joinAxisMode = AttrMapValue(OneOf('bottom', 'top', 'value', 'points', None), + desc="Mode used for connecting axis ('bottom', 'top', 'value', 'points', None)."), + joinAxisPos = AttrMapValue(isNumberOrNone, + desc='Position at which to join with other axis.'), + ) + + # Indicate the dimension of the data we're interested in. + _dataIndex = 0 + + def __init__(self): + ValueAxis.__init__(self) + + self.labels.boxAnchor = 'n' + self.labels.dx = 0 + self.labels.dy = -5 + + self.tickUp = 0 + self.tickDown = 5 + + self.joinAxis = None + self.joinAxisMode = None + self.joinAxisPos = None + + + def demo(self): + self.setPosition(20, 50, 150) + self.configure([(10,20,30,40,50)]) + + d = Drawing(200, 100) + d.add(self) + return d + + + def joinToAxis(self, yAxis, mode='bottom', pos=None): + "Join with y-axis using some mode." + _assertYAxis(yAxis) + if mode == 'bottom': + self._x = yAxis._x * 1.0 + self._y = yAxis._y * 1.0 + elif mode == 'top': + self._x = yAxis._x * 1.0 + self._y = (yAxis._y + yAxis._length) * 1.0 + elif mode == 'value': + self._x = yAxis._x * 1.0 + self._y = yAxis.scale(pos) * 1.0 + elif mode == 'points': + self._x = yAxis._x * 1.0 + self._y = pos * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('bottom', 'top'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, value): + """Converts a numeric value to a Y position. + + The chart first configures the axis, then asks it to + work out the x value for each point when plotting + lines or bars. You could override this to do + logarithmic axes. + """ + + msg = "Axis cannot scale numbers before it is configured" + assert self._configured, msg + if value is None: + value = 0 + return self._x + self._scaleFactor * (value - self._valueMin) + + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x + self._length, self._y) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if self.visibleTicks and (self.tickUp or self.tickDown): + self._makeLines(g,-self.tickDown,self.tickUp,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + +class NormalDateXValueAxis(XValueAxis): + """An X axis applying additional rules. + + Depending on the data and some built-in rules, the axis + displays normalDate values as nicely formatted dates. + + The client chart should have NormalDate X values. + """ + + _attrMap = AttrMap(BASE = XValueAxis, + bottomAxisLabelSlack = AttrMapValue(isNumber, desc="Fractional amount used to adjust label spacing"), + niceMonth = AttrMapValue(isBoolean, desc="Flag for displaying months 'nicely'."), + forceEndDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of last date value.'), + forceFirstDate = AttrMapValue(isBoolean, desc='Flag for enforced displaying of first date value.'), + xLabelFormat = AttrMapValue(None, desc="Label format string (e.g. '{mm}/{yy}') or function."), + dayOfWeekName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=7,hi=7), desc='Weekday names.'), + monthName = AttrMapValue(SequenceOf(isString,emptyOK=0,lo=12,hi=12), desc='Month names.'), + dailyFreq = AttrMapValue(isBoolean, desc='True if we are to assume daily data to be ticked at end of month.'), + ) + + _valueClass = normalDate.ND + + def __init__(self, **kw): + apply(XValueAxis.__init__, (self,)) + + # some global variables still used... + self.bottomAxisLabelSlack = 0.1 + self.niceMonth = 1 + self.forceEndDate = 0 + self.forceFirstDate = 0 + self.dailyFreq = 0 + self.xLabelFormat = "{mm}/{yy}" + self.dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + self.monthName = ['January', 'February', 'March', 'April', 'May', 'June', 'July', + 'August', 'September', 'October', 'November', 'December'] + self.valueSteps = None + + def _scalar2ND(self, x): + "Convert a scalar to a NormalDate value." + d = self._valueClass() + d.normalize(x) + return d + + def _dateFormatter(self, v): + "Create a formatted label for some value." + if not isinstance(v,normalDate.NormalDate): + v = self._scalar2ND(v) + d, m = normalDate._dayOfWeekName, normalDate._monthName + try: + normalDate._dayOfWeekName, normalDate._monthName = self.dayOfWeekName, self.monthName + return v.formatMS(self.xLabelFormat) + finally: + normalDate._dayOfWeekName, normalDate._monthName = d, m + + def _xAxisTicker(self, xVals): + """Complex stuff... + + Needs explanation... + """ + axisLength = self._length + formatter = self._dateFormatter + labels = self.labels + fontName, fontSize, leading = labels.fontName, labels.fontSize, labels.leading + textAnchor, boxAnchor, angle = labels.textAnchor, labels.boxAnchor, labels.angle + RBL = _textBoxLimits(string.split(formatter(xVals[0]),'\n'),fontName, + fontSize,leading or 1.2*fontSize,textAnchor,boxAnchor) + RBL = _rotatedBoxLimits(RBL[0],RBL[1],RBL[2],RBL[3], angle) + xLabelW = RBL[1]-RBL[0] + xLabelH = RBL[3]-RBL[2] + w = max(xLabelW,labels.width,self.minimumTickSpacing) + + W = w+w*self.bottomAxisLabelSlack + n = len(xVals) + ticks = [] + labels = [] + maximumTicks = self.maximumTicks + + def addTick(i, xVals=xVals, formatter=formatter, ticks=ticks, labels=labels): + ticks.insert(0,xVals[i]) + labels.insert(0,formatter(xVals[i])) + + for d in (1,2,3,6,12,24,60,120): + k = n/d + if k<=maximumTicks and k*W <= axisLength: + i = n-1 + if self.niceMonth: + j = xVals[-1].month() % (d<=12 and d or 12) + if j: + if self.forceEndDate: addTick(i) + i = i - j + + #weird first date ie not at end of month + try: + wfd = xVals[0].month() == xVals[1].month() + except: + wfd = 0 + + while i>=wfd: + addTick(i) + i = i - d + + if self.forceFirstDate and ticks[0] != xVals[0]: + addTick(0) + if (axisLength/(ticks[-1]-ticks[0]))*(ticks[1]-ticks[0])<=w: + del ticks[1], labels[1] + if self.forceEndDate and self.niceMonth and j: + if (axisLength/(ticks[-1]-ticks[0]))*(ticks[-1]-ticks[-2])<=w: + del ticks[-2], labels[-2] + try: + if labels[0] and labels[0]==labels[1]: + del ticks[1], labels[1] + except IndexError: + pass + + return ticks, labels + + def _convertXV(self,data): + '''Convert all XValues to a standard normalDate type''' + + VC = self._valueClass + for D in data: + for i in xrange(len(D)): + x, y = D[i] + if not isinstance(x,VC): + D[i] = (VC(x),y) + + def _getStepsAndLabels(self,xVals): + if self.dailyFreq: + xEOM = [] + pm = 0 + px = xVals[0] + for x in xVals: + m = x.month() + if pm!=m: + if pm: xEOM.append(px) + pm = m + px = x + px = xVals[-1] + if xEOM[-1]!=x: xEOM.append(px) + steps, labels = self._xAxisTicker(xEOM) + else: + steps, labels = self._xAxisTicker(xVals) + return steps, labels + + def configure(self, data): + self._convertXV(data) + from reportlab.lib.set_ops import union + xVals = reduce(union,map(lambda x: map(lambda dv: dv[0],x),data),[]) + xVals.sort() + steps,labels = self._getStepsAndLabels(xVals) + valueMin, valueMax = self.valueMin, self.valueMax + if valueMin is None: valueMin = xVals[0] + if valueMax is None: valueMax = xVals[-1] + self._valueMin, self._valueMax = valueMin, valueMax + self._tickValues = steps + self._labelTextFormat = labels + + self._scaleFactor = self._length / float(valueMax - valueMin) + self._tickValues = steps + self._configured = 1 + +class YValueAxis(ValueAxis): + "Y/value axis" + + _attrMap = AttrMap(BASE=ValueAxis, + tickLeft = AttrMapValue(isNumber, + desc='Tick length left of the axis.'), + tickRight = AttrMapValue(isNumber, + desc='Tick length right of the axis.'), + joinAxis = AttrMapValue(None, + desc='Join both axes if true.'), + joinAxisMode = AttrMapValue(OneOf(('left', 'right', 'value', 'points', None)), + desc="Mode used for connecting axis ('left', 'right', 'value', 'points', None)."), + joinAxisPos = AttrMapValue(isNumberOrNone, + desc='Position at which to join with other axis.'), + ) + + # Indicate the dimension of the data we're interested in. + _dataIndex = 1 + + def __init__(self): + ValueAxis.__init__(self) + + self.labels.boxAnchor = 'e' + self.labels.dx = -5 + self.labels.dy = 0 + + self.tickRight = 0 + self.tickLeft = 5 + + self.joinAxis = None + self.joinAxisMode = None + self.joinAxisPos = None + + + def demo(self): + data = [(10, 20, 30, 42)] + self.setPosition(100, 10, 80) + self.configure(data) + + drawing = Drawing(200, 100) + drawing.add(self) + return drawing + + + + def joinToAxis(self, xAxis, mode='left', pos=None): + "Join with x-axis using some mode." + _assertXAxis(xAxis) + if mode == 'left': + self._x = xAxis._x * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'right': + self._x = (xAxis._x + xAxis._length) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'value': + self._x = xAxis.scale(pos) * 1.0 + self._y = xAxis._y * 1.0 + elif mode == 'points': + self._x = pos * 1.0 + self._y = xAxis._y * 1.0 + + def _joinToAxis(self): + ja = self.joinAxis + if ja: + jam = self.joinAxisMode + jap = self.joinAxisPos + jta = self.joinToAxis + if jam in ('left', 'right'): + jta(ja, mode=jam) + elif jam in ('value', 'points'): + jta(ja, mode=jam, pos=jap) + + def scale(self, value): + """Converts a numeric value to a Y position. + + The chart first configures the axis, then asks it to + work out the x value for each point when plotting + lines or bars. You could override this to do + logarithmic axes. + """ + + msg = "Axis cannot scale numbers before it is configured" + assert self._configured, msg + + if value is None: + value = 0 + return self._y + self._scaleFactor * (value - self._valueMin) + + + def makeAxis(self): + g = Group() + self._joinToAxis() + if not self.visibleAxis: return g + + axis = Line(self._x, self._y, self._x, self._y + self._length) + axis.strokeColor = self.strokeColor + axis.strokeWidth = self.strokeWidth + axis.strokeDashArray = self.strokeDashArray + g.add(axis) + + return g + + def makeTicks(self): + g = Group() + if self.visibleTicks and (self.tickLeft or self.tickRight): + self._makeLines(g,-self.tickLeft,self.tickRight,self.strokeColor,self.strokeWidth,self.strokeDashArray) + return g + +class AdjYValueAxis(YValueAxis): + """A Y-axis applying additional rules. + + Depending on the data and some built-in rules, the axis + may choose to adjust its range and origin. + """ + _attrMap = AttrMap(BASE = YValueAxis, + requiredRange = AttrMapValue(isNumberOrNone, desc='Minimum required value range.'), + leftAxisPercent = AttrMapValue(isBoolean, desc='When true add percent sign to label values.'), + leftAxisOrigShiftIPC = AttrMapValue(isNumber, desc='Lowest label shift interval ratio.'), + leftAxisOrigShiftMin = AttrMapValue(isNumber, desc='Minimum amount to shift.'), + leftAxisSkipLL0 = AttrMapValue(EitherOr((isBoolean,isListOfNumbers)), desc='Skip/Keep lowest tick label when true/false.\nOr skiplist') + ) + + def __init__(self): + apply(YValueAxis.__init__, (self,)) + self.requiredRange = 30 + self.leftAxisPercent = 1 + self.leftAxisOrigShiftIPC = 0.15 + self.leftAxisOrigShiftMin = 12 + self.leftAxisSkipLL0 = 0 + self.valueSteps = None + + def _rangeAdjust(self): + "Adjusts the value range of the axis." + + from reportlab.graphics.charts.utils import find_good_grid, ticks + y_min, y_max = self._valueMin, self._valueMax + m = self.maximumTicks + n = filter(lambda x,m=m: x<=m,[4,5,6,7,8,9]) + if not n: n = [m] + + valueStep, requiredRange = self.valueStep, self.requiredRange + if requiredRange and y_max - y_min < requiredRange: + y1, y2 = find_good_grid(y_min, y_max,n=n,grid=valueStep)[:2] + if y2 - y1 < requiredRange: + ym = (y1+y2)*0.5 + y1 = min(ym-requiredRange*0.5,y_min) + y2 = max(ym+requiredRange*0.5,y_max) + if y_min>=100 and y1<100: + y2 = y2 + 100 - y1 + y1 = 100 + elif y_min>=0 and y1<0: + y2 = y2 - y1 + y1 = 0 + self._valueMin, self._valueMax = y1, y2 + + T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep) + abf = self.avoidBoundFrac + if abf: + i1 = (T[1]-T[0]) + if type(abf) not in (TupleType,ListType): + i0 = i1 = i1*abf + else: + i0 = i1*abf[0] + i1 = i1*abf[1] + _n = getattr(self,'_cValueMin',T[0]) + _x = getattr(self,'_cValueMax',T[-1]) + if _n - T[0] < i0: self._valueMin = self._valueMin - i0 + if T[-1]-_x < i1: self._valueMax = self._valueMax + i1 + T, L = ticks(self._valueMin, self._valueMax, split=1, n=n, percent=self.leftAxisPercent,grid=valueStep) + + self._valueMin = T[0] + self._valueMax = T[-1] + self._tickValues = self.valueSteps = T + if self.labelTextFormat is None: + self._labelTextFormat = L + else: + self._labelTextFormat = self.labelTextFormat + + if abs(self._valueMin-100)<1e-6: + self._calcValueStep() + vMax, vMin = self._valueMax, self._valueMin + m = max(self.leftAxisOrigShiftIPC*self._valueStep, + (vMax-vMin)*self.leftAxisOrigShiftMin/self._length) + self._valueMin = self._valueMin - m + + if self.leftAxisSkipLL0: + if type(self.leftAxisSkipLL0) in (ListType,TupleType): + for x in self.leftAxisSkipLL0: + try: + L[x] = '' + except IndexError: + pass + L[0] = '' + +# Sample functions. +def sample0a(): + "Sample drawing with one xcat axis and two buckets." + + drawing = Drawing(400, 200) + + data = [(10, 20)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Ying', 'Yang'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + + return drawing + + +def sample0b(): + "Sample drawing with one xcat axis and one bucket only." + + drawing = Drawing(400, 200) + + data = [(10,)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Ying'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + + return drawing + + +def sample1(): + "Sample drawing containing two unconnected axes." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XCategoryAxis() + xAxis.setPosition(75, 75, 300) + xAxis.configure(data) + xAxis.categoryNames = ['Beer','Wine','Meat','Cannelloni'] + xAxis.labels.boxAnchor = 'n' + xAxis.labels[3].dy = -15 + xAxis.labels[3].angle = 30 + xAxis.labels[3].fontName = 'Times-Bold' + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +##def sample2a(): +## "Make sample drawing with two axes, x connected at top of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='top') +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2b(): +## "Make two axes, x connected at bottom of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='bottom') +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2c(): +## "Make two axes, x connected at fixed value (in points) of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='points', pos=100) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample2d(): +## "Make two axes, x connected at fixed value (of y-axes) of y." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='value', pos=20) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3a(): +## "Make sample drawing with two axes, y connected at left of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## yAxis.joinToAxis(xAxis, mode='left') +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3b(): +## "Make sample drawing with two axes, y connected at right of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## xAxis = XCategoryAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] +## xAxis.labels.boxAnchor = 'n' +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## yAxis.joinToAxis(xAxis, mode='right') +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing +## +## +##def sample3c(): +## "Make two axes, y connected at fixed value (in points) of x." +## +## drawing = Drawing(400, 200) +## +## data = [(10, 20, 30, 42)] +## +## yAxis = YValueAxis() +## yAxis.setPosition(50, 50, 125) +## yAxis.configure(data) +## +## xAxis = XValueAxis() +## xAxis._length = 300 +## xAxis.configure(data) +## xAxis.joinToAxis(yAxis, mode='points', pos=100) +## +## drawing.add(xAxis) +## drawing.add(yAxis) +## +## return drawing + + +def sample4a(): + "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'points' + xAxis.joinAxisPos = 100 + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4b(): + "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'value' + xAxis.joinAxisPos = 35 + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4c(): + "Sample drawing, xvalue/yvalue axes, y connected to bottom of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4c1(): + "xvalue/yvalue axes, without drawing axis lines/ticks." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + yAxis.visibleAxis = 0 + yAxis.visibleTicks = 0 + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.configure(data) + xAxis.visibleAxis = 0 + xAxis.visibleTicks = 0 + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample4d(): + "Sample drawing, xvalue/yvalue axes, y connected to top of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'top' + xAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5a(): + "Sample drawing, xvalue/yvalue axes, y connected at 100 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'points' + yAxis.joinAxisPos = 100 + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5b(): + "Sample drawing, xvalue/yvalue axes, y connected at value 35 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'value' + yAxis.joinAxisPos = 35 + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5c(): + "Sample drawing, xvalue/yvalue axes, y connected at right of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'right' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample5d(): + "Sample drawing, xvalue/yvalue axes, y connected at left of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis.setPosition(50, 50, 300) + xAxis.configure(data) + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'left' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6a(): + "Sample drawing, xcat/yvalue axes, x connected at top of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'top' + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6b(): + "Sample drawing, xcat/yvalue axes, x connected at bottom of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'bottom' + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6c(): + "Sample drawing, xcat/yvalue axes, x connected at 100 pts to y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'points' + xAxis.joinAxisPos = 100 + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample6d(): + "Sample drawing, xcat/yvalue axes, x connected at value 20 of y." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + yAxis = YValueAxis() + yAxis.setPosition(50, 50, 125) + yAxis.configure(data) + + xAxis = XCategoryAxis() + xAxis._length = 300 + xAxis.configure(data) + xAxis.joinAxis = yAxis + xAxis.joinAxisMode = 'value' + xAxis.joinAxisPos = 20 + xAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + xAxis.labels.boxAnchor = 'n' + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7a(): + "Sample drawing, xvalue/ycat axes, y connected at right of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'right' + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7b(): + "Sample drawing, xvalue/ycat axes, y connected at left of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'left' + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7c(): + "Sample drawing, xvalue/ycat axes, y connected at value 30 of x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'value' + yAxis.joinAxisPos = 30 + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing + + +def sample7d(): + "Sample drawing, xvalue/ycat axes, y connected at 200 pts to x." + + drawing = Drawing(400, 200) + + data = [(10, 20, 30, 42)] + + xAxis = XValueAxis() + xAxis._length = 300 + xAxis.configure(data) + + yAxis = YCategoryAxis() + yAxis.setPosition(50, 50, 125) + yAxis.joinAxis = xAxis + yAxis.joinAxisMode = 'points' + yAxis.joinAxisPos = 200 + yAxis.categoryNames = ['Beer', 'Wine', 'Meat', 'Cannelloni'] + yAxis.labels.boxAnchor = 'e' + yAxis.configure(data) + + drawing.add(xAxis) + drawing.add(yAxis) + + return drawing diff --git a/bin/reportlab/graphics/charts/barcharts.py b/bin/reportlab/graphics/charts/barcharts.py new file mode 100644 index 00000000000..0ca1ae3ed57 --- /dev/null +++ b/bin/reportlab/graphics/charts/barcharts.py @@ -0,0 +1,1972 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/barcharts.py +"""This module defines a variety of Bar Chart components. + +The basic flavors are Side-by-side, available in horizontal and +vertical versions. + +Stacked and percentile bar charts to follow... +""" +__version__=''' $Id: barcharts.py 2647 2005-07-26 13:47:51Z rgbecker $ ''' + +import string, copy +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isString,\ + isListOfStrings, SequenceOf, isBoolean, isNoneOrShape, isStringOrNone,\ + NoneOr +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.formatters import Formatter +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Line, Rect, Group, Drawing, NotImplementedError +from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis, YCategoryAxis, XValueAxis +from reportlab.graphics.charts.textlabels import BarChartLabel, NA_Label, NoneOrInstanceOfNA_Label +from reportlab.graphics.charts.areas import PlotArea + +class BarChartProperties(PropHolder): + _attrMap = AttrMap( + strokeColor = AttrMapValue(isColorOrNone, desc='Color of the bar border.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color of the bar interior area.'), + strokeWidth = AttrMapValue(isNumber, desc='Width of the bar border.'), + symbol = AttrMapValue(None, desc='A widget to be used instead of a normal bar.'), + name = AttrMapValue(isString, desc='Text to be associated with a bar (eg seriesname)'), + swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ..."), + ) + + def __init__(self): + self.strokeColor = None + self.fillColor = colors.blue + self.strokeWidth = 0.5 + self.symbol = None + +# Bar chart classes. +class BarChart(PlotArea): + "Abstract base class, unusable by itself." + + _attrMap = AttrMap(BASE=PlotArea, + useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'), + barWidth = AttrMapValue(isNumber, desc='The width of an individual bar.'), + groupSpacing = AttrMapValue(isNumber, desc='Width between groups of bars.'), + barSpacing = AttrMapValue(isNumber, desc='Width between individual bars.'), + bars = AttrMapValue(None, desc='Handle of the individual bars.'), + valueAxis = AttrMapValue(None, desc='Handle of the value axis.'), + categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + barLabels = AttrMapValue(None, desc='Handle to the list of bar labels.'), + barLabelFormat = AttrMapValue(None, desc='Formatting string or function used for bar labels.'), + barLabelCallOut = AttrMapValue(None, desc='Callout function(label)\nlabel._callOutInfo = (self,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0)'), + barLabelArray = AttrMapValue(None, desc='explicit array of bar label values, must match size of data if present.'), + reversePlotOrder = AttrMapValue(isBoolean, desc='If true, reverse common category plot order.'), + naLabel = AttrMapValue(NoneOrInstanceOfNA_Label, desc='Label to use for N/A values.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.bars + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None)) + fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None)) + strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth = getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None)) + swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) + if swatchMarker: + return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) + return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return getattr(self.bars[i],'name',default) + + def __init__(self): + assert self.__class__.__name__ not in ('BarChart','BarChart3D'), 'Abstract Class %s Instantiated' % self.__class__.__name__ + + if self._flipXY: + self.categoryAxis = YCategoryAxis() + self.valueAxis = XValueAxis() + else: + self.categoryAxis = XCategoryAxis() + self.valueAxis = YValueAxis() + + PlotArea.__init__(self) + self.barSpacing = 0 + self.reversePlotOrder = 0 + + + # this defines two series of 3 points. Just an example. + self.data = [(100,110,120,130), + (70, 80, 85, 90)] + + # control bar spacing. is useAbsolute = 1 then + # the next parameters are in points; otherwise + # they are 'proportions' and are normalized to + # fit the available space. Half a barSpacing + # is allocated at the beginning and end of the + # chart. + self.useAbsolute = 0 #- not done yet + self.barWidth = 10 + self.groupSpacing = 5 + self.barSpacing = 0 + + self.barLabels = TypedPropertyCollection(BarChartLabel) + self.barLabels.boxAnchor = 'c' + self.barLabels.textAnchor = 'middle' + self.barLabelFormat = None + self.barLabelArray = None + # this says whether the origin is inside or outside + # the bar - +10 means put the origin ten points + # above the tip of the bar if value > 0, or ten + # points inside if bar value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.barLabels.nudge = 0 + + # if you have multiple series, by default they butt + # together. + + # we really need some well-designed default lists of + # colors e.g. from Tufte. These will be used in a + # cycle to set the fill color of each series. + self.bars = TypedPropertyCollection(BarChartProperties) +## self.bars.symbol = None + self.bars.strokeWidth = 1 + self.bars.strokeColor = colors.black + + self.bars[0].fillColor = colors.red + self.bars[1].fillColor = colors.green + self.bars[2].fillColor = colors.blue + self.naLabel = None#NA_Label() + + + def demo(self): + """Shows basic use of a bar chart""" + if self.__class__.__name__=='BarChart': + raise NotImplementedError, 'Abstract Class BarChart has no demo' + drawing = Drawing(200, 100) + bc = self.__class__() + drawing.add(bc) + return drawing + + def _getConfigureData(self): + cA = self.categoryAxis + data = self.data + if cA.style not in ('parallel','parallel_3d'): + _data = data + data = max(map(len,_data))*[0] + for d in _data: + for i in xrange(len(d)): + data[i] = data[i] + (d[i] or 0) + data = list(_data) + [data] + self._configureData = data + + def _getMinMax(self): + '''Attempt to return the data range''' + self._getConfigureData() + self.valueAxis._setRange(self._configureData) + return self.valueAxis._valueMin, self.valueAxis._valueMax + + def _drawBegin(self,org,length): + '''Position and configure value axis, return crossing value''' + vA = self.valueAxis + vA.setPosition(self.x, self.y, length) + self._getConfigureData() + vA.configure(self._configureData) + + # if zero is in chart, put the other axis there, otherwise use low + crossesAt = vA.scale(0) + if crossesAt > org+length or crossesAt=0 and 1 or -1)*nudge, y + 0.5*height + else: + value = height + if anti: value = 0 + return x + 0.5*width, y + value + (height>=0 and 1 or -1)*nudge + + def _addBarLabel(self, g, rowNo, colNo, x, y, width, height): + text = self._getLabelText(rowNo,colNo) + if text: + self._addLabel(text, self.barLabels[(rowNo, colNo)], g, rowNo, colNo, x, y, width, height) + + def _addNABarLabel(self, g, rowNo, colNo, x, y, width, height): + na = self.naLabel + if na and na.text: + na = copy.copy(na) + v = self.valueAxis._valueMax<=0 and -1e-8 or 1e-8 + if width is None: width = v + if height is None: height = v + self._addLabel(na.text, na, g, rowNo, colNo, x, y, width, height) + + def _addLabel(self, text, label, g, rowNo, colNo, x, y, width, height): + if label.visible: + labelWidth = stringWidth(text, label.fontName, label.fontSize) + x0, y0 = self._labelXY(label,x,y,width,height) + flipXY = self._flipXY + if flipXY: + pm = width + else: + pm = height + label._pmv = pm #the plus minus val + fixedEnd = getattr(label,'fixedEnd', None) + if fixedEnd is not None: + v = fixedEnd._getValue(self,pm) + x00, y00 = x0, y0 + if flipXY: + x0 = v + else: + y0 = v + else: + if flipXY: + x00 = x0 + y00 = y+height/2.0 + else: + x00 = x+width/2.0 + y00 = y0 + fixedStart = getattr(label,'fixedStart', None) + if fixedStart is not None: + v = fixedStart._getValue(self,pm) + if flipXY: + x00 = v + else: + y00 = v + + if pm<0: + if flipXY: + dx = -2*label.dx + dy = 0 + else: + dy = -2*label.dy + dx = 0 + else: + dy = dx = 0 + label.setOrigin(x0+dx, y0+dy) + label.setText(text) + sC, sW = label.lineStrokeColor, label.lineStrokeWidth + if sC and sW: g.insert(0,Line(x00,y00,x0,y0, strokeColor=sC, strokeWidth=sW)) + g.add(label) + alx = getattr(self,'barLabelCallOut',None) + if alx: + label._callOutInfo = (self,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0) + alx(label) + del label._callOutInfo + + def _makeBar(self,g,x,y,width,height,rowNo,style): + r = Rect(x, y, width, height) + r.strokeWidth = style.strokeWidth + r.fillColor = style.fillColor + r.strokeColor = style.strokeColor + g.add(r) + + def _makeBars(self,g,lg): + lenData = len(self.data) + bars = self.bars + for rowNo in range(lenData): + row = self._barPositions[rowNo] + styleCount = len(bars) + styleIdx = rowNo % styleCount + rowStyle = bars[styleIdx] + for colNo in range(len(row)): + barPos = row[colNo] + style = bars.has_key((styleIdx,colNo)) and bars[(styleIdx,colNo)] or rowStyle + (x, y, width, height) = barPos + if None in (width,height): + self._addNABarLabel(lg,rowNo,colNo,x,y,width,height) + continue + + # Draw a rectangular symbol for each data item, + # or a normal colored rectangle. + symbol = None + if hasattr(style, 'symbol'): + symbol = copy.deepcopy(style.symbol) + elif hasattr(self.bars, 'symbol'): + symbol = self.bars.symbol + + if symbol: + symbol.x = x + symbol.y = y + symbol.width = width + symbol.height = height + g.add(symbol) + elif abs(width)>1e-7 and abs(height)>=1e-7 and (style.fillColor is not None or style.strokeColor is not None): + self._makeBar(g,x,y,width,height,rowNo,style) + + self._addBarLabel(lg,rowNo,colNo,x,y,width,height) + + def makeBars(self): + g = Group() + lg = Group() + self._makeBars(g,lg) + g.add(lg) + return g + + def _desiredCategoryAxisLength(self): + '''for dynamically computing the desired category axis length''' + style = self.categoryAxis.style + data = self.data + n = len(data) + m = max(map(len,data)) + if style=='parallel': + groupWidth = (n-1)*self.barSpacing+n*self.barWidth + else: + groupWidth = self.barWidth + return m*(self.groupSpacing+groupWidth) + + def draw(self): + cA, vA = self.categoryAxis, self.valueAxis + if vA: ovAjA, vA.joinAxis = vA.joinAxis, cA + if cA: ocAjA, cA.joinAxis = cA.joinAxis, vA + if self._flipXY: + cA.setPosition(self._drawBegin(self.x,self.width), self.y, self.height) + else: + cA.setPosition(self.x, self._drawBegin(self.y,self.height), self.width) + return self._drawFinish() + +class VerticalBarChart(BarChart): + "Vertical bar chart with multiple side-by-side bars." + _flipXY = 0 + +class HorizontalBarChart(BarChart): + "Horizontal bar chart with multiple side-by-side bars." + _flipXY = 1 + +class _FakeGroup: + def __init__(self, cmp=None): + self._data = [] + self._cmp = cmp + + def add(self,what): + self._data.append(what) + + def value(self): + return self._data + + def sort(self): + self._data.sort(self._cmp) + +class BarChart3D(BarChart): + _attrMap = AttrMap(BASE=BarChart, + theta_x = AttrMapValue(isNumber, desc='dx/dz'), + theta_y = AttrMapValue(isNumber, desc='dy/dz'), + zDepth = AttrMapValue(isNumber, desc='depth of an individual series'), + zSpace = AttrMapValue(isNumber, desc='z gap around series'), + ) + theta_x = .5 + theta_y = .5 + zDepth = None + zSpace = None + + def calcBarPositions(self): + BarChart.calcBarPositions(self) + seriesCount = self._seriesCount + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + if self.categoryAxis.style=='parallel_3d': + _3d_depth = seriesCount*zDepth+(seriesCount+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + _3d_depth *= self._normFactor + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + if self.categoryAxis.style=='parallel_3d': + z0 = self._normFactor*(rowNo*(zDepth+zSpace)+zSpace) + else: + z0 = self._normFactor*zSpace + return z0 + + def _makeBar(self,g,x,y,width,height,rowNo,style): + zDepth = self.zDepth + if zDepth is None: zDepth = self.barWidth + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth*self._normFactor + if width<0: + x += width + width = -width + x += z0*self.theta_x + y += z0*self.theta_y + if self._flipXY: + y += zSpace + else: + x += zSpace + g.add((0,z0,z1,x,y,width,height,rowNo,style)) + + def _addBarLabel(self, g, rowNo, colNo, x, y, width, height): + z0 = self._calc_z0(rowNo) + zSpace = self.zSpace + if zSpace is None: zSpace = self.barSpacing + z1 = z0 + x += z0*self.theta_x + y += z0*self.theta_y + if self._flipXY: + y += zSpace + else: + x += zSpace + g.add((1,z0,z1,x,y,width,height,rowNo,colNo)) + + def makeBars(self): + from utils3d import _draw_3d_bar + fg = _FakeGroup(cmp=self._cmpZ) + self._makeBars(fg,fg) + fg.sort() + g = Group() + theta_x = self.theta_x + theta_y = self.theta_y + for t in fg.value(): + if t[0]==1: + z0,z1,x,y,width,height,rowNo,colNo = t[1:] + BarChart._addBarLabel(self,g,rowNo,colNo,x,y,width,height) + elif t[0]==0: + z0,z1,x,y,width,height,rowNo,style = t[1:] + dz = z1 - z0 + _draw_3d_bar(g, x, x+width, y, y+height, dz*theta_x, dz*theta_y, + fillColor=style.fillColor, fillColorShaded=None, + strokeColor=style.strokeColor, strokeWidth=style.strokeWidth, + shading=0.45) + return g + +class VerticalBarChart3D(BarChart3D,VerticalBarChart): + _cmpZ=lambda self,a,b:cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[4])) + +class HorizontalBarChart3D(BarChart3D,HorizontalBarChart): + _cmpZ = lambda self,a,b: cmp((-a[1],a[4],a[0],-a[3]),(-b[1],b[4],b[0],-b[3])) #t, z0, z1, x, y = a[:5] + +# Vertical samples. +def sampleV0a(): + "A slightly pathologic bar chart with only TWO data items." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV0b(): + "A pathologic bar chart with only ONE data item." + + drawing = Drawing(400, 200) + + data = [(42,)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99'] + + drawing.add(bc) + + return drawing + + +def sampleV0c(): + "A really pathologic bar chart with NO data items at all!" + + drawing = Drawing(400, 200) + + data = [()] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.categoryNames = [] + + drawing.add(bc) + + return drawing + + +def sampleV1(): + "Sample of multi-series bar chart." + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc) + + return drawing + + +def sampleV2a(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 0 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dy = -60 + + drawing.add(bc) + + return drawing + + +def sampleV2b(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 5 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dy = -60 + + drawing.add(bc) + + return drawing + + +def sampleV2c(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.99), + (0.6, -4.9, -3, 4, 9.99) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 2 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' + bc.valueAxis.labels.textAnchor = 'middle' + bc.categoryAxis.labels.dy = -60 + + bc.barLabels.nudge = 10 + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + + drawing.add(bc) + + return drawing + + +def sampleV3(): + "Faked horizontal bar chart using a vertical real one (deprecated)." + + names = ("UK Equities", "US Equities", "European Equities", "Japanese Equities", + "Pacific (ex Japan) Equities", "Emerging Markets Equities", + "UK Bonds", "Overseas Bonds", "UK Index-Linked", "Cash") + + series1 = (-1.5, 0.3, 0.5, 1.0, 0.8, 0.7, 0.4, 0.1, 1.0, 0.3) + series2 = (0.0, 0.33, 0.55, 1.1, 0.88, 0.77, 0.44, 0.11, 1.10, 0.33) + + assert len(names) == len(series1), "bad data" + assert len(names) == len(series2), "bad data" + + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 0 + bc.y = 0 + bc.height = 100 + bc.width = 150 + bc.data = (series1,) + bc.bars.fillColor = colors.green + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'w' # irrelevant (becomes 'c') + bc.barLabels.angle = 90 + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + bc.barLabels.nudge = 10 + + bc.valueAxis.visible = 0 + bc.valueAxis.valueMin = -2 + bc.valueAxis.valueMax = +2 + bc.valueAxis.valueStep = 1 + + bc.categoryAxis.tickUp = 0 + bc.categoryAxis.tickDown = 0 + bc.categoryAxis.categoryNames = names + bc.categoryAxis.labels.angle = 90 + bc.categoryAxis.labels.boxAnchor = 'w' + bc.categoryAxis.labels.dx = 0 + bc.categoryAxis.labels.dy = -125 + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 6 + + g = Group(bc) + g.translate(100, 175) + g.rotate(-90) + + drawing.add(g) + + return drawing + + +def sampleV4a(): + "A bar chart showing value axis region starting at *exactly* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4b(): + "A bar chart showing value axis region starting *below* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4c(): + "A bar chart showing value axis region staring *above* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV4d(): + "A bar chart showing value axis region entirely *below* zero." + + drawing = Drawing(400, 200) + + data = [(-13, -20)] + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -30 + bc.valueAxis.valueMax = -10 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +### + +##dataSample5 = [(10, 20), (20, 30), (30, 40), (40, 50), (50, 60)] +##dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30), (50, 20)] +dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30)] + +def sampleV5a(): + "A simple bar chart with no expressed spacing attributes." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5b(): + "A simple bar chart with proportional spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 0 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c1(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 0 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c2(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c3(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 0 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleV5c4(): + "Make sampe simple bar chart but with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'n' + bc.categoryAxis.labels.dy = -5 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +# Horizontal samples + +def sampleH0a(): + "Make a slightly pathologic bar chart with only TWO data items." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH0b(): + "Make a pathologic bar chart with only ONE data item." + + drawing = Drawing(400, 200) + + data = [(42,)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 50 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = ['Jan-99'] + + drawing.add(bc) + + return drawing + + +def sampleH0c(): + "Make a really pathologic bar chart with NO data items at all!" + + drawing = Drawing(400, 200) + + data = [()] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'se' + bc.categoryAxis.labels.angle = 30 + bc.categoryAxis.categoryNames = [] + + drawing.add(bc) + + return drawing + + +def sampleH1(): + "Sample of multi-series bar chart." + + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5) + ] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc, 'barchart') + + return drawing + + +def sampleH2a(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 0 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + bc.valueAxis.configure(bc.data) + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + drawing.add(bc) + + return drawing + + +def sampleH2b(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.2), + (0.6, -4.9, -3, 4, 6.8) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 5 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + drawing.add(bc) + + return drawing + + +def sampleH2c(): + "Sample of multi-series bar chart." + + data = [(2.4, -5.7, 2, 5, 9.99), + (0.6, -4.9, -3, 4, 9.99) + ] + + labels = ("Q3 2000", "Year to Date", "12 months", + "Annualised\n3 years", "Since 07.10.99") + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 80 + bc.y = 50 + bc.height = 120 + bc.width = 300 + bc.data = data + + bc.barSpacing = 2 + bc.groupSpacing = 10 + bc.barWidth = 10 + + bc.valueAxis.valueMin = -15 + bc.valueAxis.valueMax = +15 + bc.valueAxis.valueStep = 5 + bc.valueAxis.labels.fontName = 'Helvetica' + bc.valueAxis.labels.fontSize = 8 + bc.valueAxis.labels.boxAnchor = 'n' + bc.valueAxis.labels.textAnchor = 'middle' + + bc.categoryAxis.categoryNames = labels + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 8 + bc.categoryAxis.labels.dx = -150 + + bc.barLabels.nudge = 10 + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'n' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + + drawing.add(bc) + + return drawing + + +def sampleH3(): + "A really horizontal bar chart (compared to the equivalent faked one)." + + names = ("UK Equities", "US Equities", "European Equities", "Japanese Equities", + "Pacific (ex Japan) Equities", "Emerging Markets Equities", + "UK Bonds", "Overseas Bonds", "UK Index-Linked", "Cash") + + series1 = (-1.5, 0.3, 0.5, 1.0, 0.8, 0.7, 0.4, 0.1, 1.0, 0.3) + series2 = (0.0, 0.33, 0.55, 1.1, 0.88, 0.77, 0.44, 0.11, 1.10, 0.33) + + assert len(names) == len(series1), "bad data" + assert len(names) == len(series2), "bad data" + + drawing = Drawing(400, 200) + + bc = HorizontalBarChart() + bc.x = 100 + bc.y = 20 + bc.height = 150 + bc.width = 250 + bc.data = (series1,) + bc.bars.fillColor = colors.green + + bc.barLabelFormat = '%0.2f' + bc.barLabels.dx = 0 + bc.barLabels.dy = 0 + bc.barLabels.boxAnchor = 'w' # irrelevant (becomes 'c') + bc.barLabels.fontName = 'Helvetica' + bc.barLabels.fontSize = 6 + bc.barLabels.nudge = 10 + + bc.valueAxis.visible = 0 + bc.valueAxis.valueMin = -2 + bc.valueAxis.valueMax = +2 + bc.valueAxis.valueStep = 1 + + bc.categoryAxis.tickLeft = 0 + bc.categoryAxis.tickRight = 0 + bc.categoryAxis.categoryNames = names + bc.categoryAxis.labels.boxAnchor = 'w' + bc.categoryAxis.labels.dx = -170 + bc.categoryAxis.labels.fontName = 'Helvetica' + bc.categoryAxis.labels.fontSize = 6 + + g = Group(bc) + drawing.add(g) + + return drawing + + +def sampleH4a(): + "A bar chart showing value axis region starting at *exactly* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4b(): + "A bar chart showing value axis region starting *below* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4c(): + "A bar chart showing value axis region starting *above* zero." + + drawing = Drawing(400, 200) + + data = [(13, 20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 10 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH4d(): + "A bar chart showing value axis region entirely *below* zero." + + drawing = Drawing(400, 200) + + data = [(-13, -20)] + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = -30 + bc.valueAxis.valueMax = -10 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +dataSample5 = [(10, 60), (20, 50), (30, 40), (40, 30)] + +def sampleH5a(): + "A simple bar chart with no expressed spacing attributes." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5b(): + "A simple bar chart with proportional spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 0 + bc.barWidth = 40 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c1(): + "A simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 0 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c2(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 0 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c3(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 20 + bc.height = 155 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 0 + bc.barSpacing = 2 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + + +def sampleH5c4(): + "Simple bar chart with absolute spacing." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + drawing.add(bc) + + return drawing + +def sampleSymbol1(): + "Simple bar chart using symbol attribute." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.barWidth = 10 + bc.groupSpacing = 15 + bc.barSpacing = 3 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + from reportlab.graphics.widgets.grids import ShadedRect + sym1 = ShadedRect() + sym1.fillColorStart = colors.black + sym1.fillColorEnd = colors.blue + sym1.orientation = 'horizontal' + sym1.strokeWidth = 0 + + sym2 = ShadedRect() + sym2.fillColorStart = colors.black + sym2.fillColorEnd = colors.pink + sym2.orientation = 'horizontal' + sym2.strokeWidth = 0 + + sym3 = ShadedRect() + sym3.fillColorStart = colors.blue + sym3.fillColorEnd = colors.white + sym3.orientation = 'vertical' + sym3.cylinderMode = 1 + sym3.strokeWidth = 0 + + bc.bars.symbol = sym1 + bc.bars[2].symbol = sym2 + bc.bars[3].symbol = sym3 + + drawing.add(bc) + + return drawing + +def sampleStacked1(): + "Simple bar chart using symbol attribute." + + drawing = Drawing(400, 200) + + data = dataSample5 + + bc = VerticalBarChart() + bc.categoryAxis.style = 'stacked' + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + bc.strokeColor = colors.black + + bc.barWidth = 10 + bc.groupSpacing = 15 + bc.valueAxis.valueMin = 0 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + from reportlab.graphics.widgets.grids import ShadedRect + bc.bars.symbol = ShadedRect() + bc.bars.symbol.fillColorStart = colors.red + bc.bars.symbol.fillColorEnd = colors.white + bc.bars.symbol.orientation = 'vertical' + bc.bars.symbol.cylinderMode = 1 + bc.bars.symbol.strokeWidth = 0 + + bc.bars[1].symbol = ShadedRect() + bc.bars[1].symbol.fillColorStart = colors.magenta + bc.bars[1].symbol.fillColorEnd = colors.white + bc.bars[1].symbol.orientation = 'vertical' + bc.bars[1].symbol.cylinderMode = 1 + bc.bars[1].symbol.strokeWidth = 0 + + bc.bars[2].symbol = ShadedRect() + bc.bars[2].symbol.fillColorStart = colors.green + bc.bars[2].symbol.fillColorEnd = colors.white + bc.bars[2].symbol.orientation = 'vertical' + bc.bars[2].symbol.cylinderMode = 1 + bc.bars[2].symbol.strokeWidth = 0 + + bc.bars[3].symbol = ShadedRect() + bc.bars[3].symbol.fillColorStart = colors.blue + bc.bars[3].symbol.fillColorEnd = colors.white + bc.bars[3].symbol.orientation = 'vertical' + bc.bars[3].symbol.cylinderMode = 1 + bc.bars[3].symbol.strokeWidth = 0 + + drawing.add(bc) + + return drawing + +#class version of function sampleH5c4 above +class SampleH5c4(Drawing): + "Simple bar chart with absolute spacing." + + def __init__(self,width=400,height=200,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + bc = HorizontalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = dataSample5 + bc.strokeColor = colors.black + + bc.useAbsolute = 1 + bc.barWidth = 10 + bc.groupSpacing = 20 + bc.barSpacing = 10 + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'e' + bc.categoryAxis.categoryNames = ['Ying', 'Yang'] + + self.add(bc,name='HBC') diff --git a/bin/reportlab/graphics/charts/dotbox.py b/bin/reportlab/graphics/charts/dotbox.py new file mode 100644 index 00000000000..6962d278822 --- /dev/null +++ b/bin/reportlab/graphics/charts/dotbox.py @@ -0,0 +1,165 @@ +from reportlab.lib.colors import blue, _PCMYK_black +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.shapes import Circle, Drawing, Group, Line, Rect, String +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.attrmap import * +from reportlab.lib.validators import * +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import getFont +from reportlab.graphics.charts.lineplots import _maxWidth + +class DotBox(Widget): + """Returns a dotbox widget.""" + + #Doesn't use TypedPropertyCollection for labels - this can be a later improvement + _attrMap = AttrMap( + xlabels = AttrMapValue(isNoneOrListOfNoneOrStrings, + desc="List of text labels for boxes on left hand side"), + ylabels = AttrMapValue(isNoneOrListOfNoneOrStrings, + desc="Text label for second box on left hand side"), + labelFontName = AttrMapValue(isString, + desc="Name of font used for the labels"), + labelFontSize = AttrMapValue(isNumber, + desc="Size of font used for the labels"), + labelOffset = AttrMapValue(isNumber, + desc="Space between label text and grid edge"), + strokeWidth = AttrMapValue(isNumber, + desc='Width of the grid and dot outline'), + gridDivWidth = AttrMapValue(isNumber, + desc="Width of each 'box'"), + gridColor = AttrMapValue(isColor, + desc='Colour for the box and gridding'), + dotDiameter = AttrMapValue(isNumber, + desc="Diameter of the circle used for the 'dot'"), + dotColor = AttrMapValue(isColor, + desc='Colour of the circle on the box'), + dotXPosition = AttrMapValue(isNumber, + desc='X Position of the circle'), + dotYPosition = AttrMapValue(isNumber, + desc='X Position of the circle'), + x = AttrMapValue(isNumber, + desc='X Position of dotbox'), + y = AttrMapValue(isNumber, + desc='Y Position of dotbox'), + ) + + def __init__(self): + self.xlabels=["Value", "Blend", "Growth"] + self.ylabels=["Small", "Medium", "Large"] + self.labelFontName = "Helvetica" + self.labelFontSize = 6 + self.labelOffset = 5 + self.strokeWidth = 0.5 + self.gridDivWidth=0.5*cm + self.gridColor=colors.Color(25/255.0,77/255.0,135/255.0) + self.dotDiameter=0.4*cm + self.dotColor=colors.Color(232/255.0,224/255.0,119/255.0) + self.dotXPosition = 1 + self.dotYPosition = 1 + self.x = 30 + self.y = 5 + + + def _getDrawingDimensions(self): + leftPadding=rightPadding=topPadding=bottomPadding=5 + #find width of grid + tx=len(self.xlabels)*self.gridDivWidth + #add padding (and offset) + tx=tx+leftPadding+rightPadding+self.labelOffset + #add in maximum width of text + tx=tx+_maxWidth(self.xlabels, self.labelFontName, self.labelFontSize) + #find height of grid + ty=len(self.ylabels)*self.gridDivWidth + #add padding (and offset) + ty=ty+topPadding+bottomPadding+self.labelOffset + #add in maximum width of text + ty=ty+_maxWidth(self.ylabels, self.labelFontName, self.labelFontSize) + #print (tx, ty) + return (tx,ty) + + def demo(self,drawing=None): + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + g = Group() + + #box + g.add(Rect(self.x,self.y,len(self.xlabels)*self.gridDivWidth,len(self.ylabels)*self.gridDivWidth, + strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + fillColor=None)) + + #internal gridding + for f in range (1,len(self.ylabels)): + #horizontal + g.add(Line(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + x1 = self.x, + y1 = self.y+f*self.gridDivWidth, + x2 = self.x+len(self.xlabels)*self.gridDivWidth, + y2 = self.y+f*self.gridDivWidth)) + for f in range (1,len(self.xlabels)): + #vertical + g.add(Line(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + x1 = self.x+f*self.gridDivWidth, + y1 = self.y, + x2 = self.x+f*self.gridDivWidth, + y2 = self.y+len(self.ylabels)*self.gridDivWidth)) + + # draw the 'dot' + g.add(Circle(strokeColor=self.gridColor, + strokeWidth=self.strokeWidth, + fillColor=self.dotColor, + cx = self.x+(self.dotXPosition*self.gridDivWidth), + cy = self.y+(self.dotYPosition*self.gridDivWidth), + r = self.dotDiameter/2.0)) + + #used for centering y-labels (below) + ascent=getFont(self.labelFontName).face.ascent + if ascent==0: + ascent=0.718 # default (from helvetica) + ascent=ascent*self.labelFontSize # normalize + + #do y-labels + if self.ylabels != None: + for f in range (len(self.ylabels)-1,-1,-1): + if self.ylabels[f]!= None: + g.add(String(strokeColor=self.gridColor, + text = self.ylabels[f], + fontName = self.labelFontName, + fontSize = self.labelFontSize, + fillColor=_PCMYK_black, + x = self.x-self.labelOffset, + y = self.y+(f*self.gridDivWidth+(self.gridDivWidth-ascent)/2.0), + textAnchor = 'end')) + + #do x-labels + if self.xlabels != None: + for f in range (0,len(self.xlabels)): + if self.xlabels[f]!= None: + l=Label() + l.x=self.x+(f*self.gridDivWidth)+(self.gridDivWidth+ascent)/2.0 + l.y=self.y+(len(self.ylabels)*self.gridDivWidth)+self.labelOffset + l.angle=90 + l.textAnchor='start' + l.fontName = self.labelFontName + l.fontSize = self.labelFontSize + l.fillColor = _PCMYK_black + l.setText(self.xlabels[f]) + l.boxAnchor = 'sw' + l.draw() + g.add(l) + + return g + + + + +if __name__ == "__main__": + d = DotBox() + d.demo().save(fnRoot="dotbox") \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/doughnut.py b/bin/reportlab/graphics/charts/doughnut.py new file mode 100644 index 00000000000..f365f9a3fb8 --- /dev/null +++ b/bin/reportlab/graphics/charts/doughnut.py @@ -0,0 +1,349 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/doughnut.py +# doughnut chart + +"""Doughnut chart + +Produces a circular chart like the doughnut charts produced by Excel. +Can handle multiple series (which produce concentric 'rings' in the chart). + +""" +__version__=''' $Id: doughnut.py 2499 2004-12-29 17:12:34Z rgbecker $ ''' + +import copy +from math import sin, cos, pi +from types import ListType, TupleType +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors,\ + isNoneOrListOfNoneOrStrings,\ + isNoneOrListOfNoneOrNumbers,\ + isNumberOrNone +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, Ellipse, \ + Wedge, String, SolidShape, UserNode, STATE_DEFAULTS +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.piecharts import AbstractPieChart, WedgeProperties, _addWedgeLabel +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import Marker + +class SectorProperties(WedgeProperties): + """This holds descriptive information about the sectors in a doughnut chart. + + It is not to be confused with the 'sector itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Sector object for you with its + format method. + """ + _attrMap = AttrMap(BASE=WedgeProperties, + ) + +class Doughnut(AbstractPieChart): + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc='X position of the chart within its container.'), + y = AttrMapValue(isNumber, desc='Y position of the chart within its container.'), + width = AttrMapValue(isNumber, desc='width of doughnut bounding box. Need not be same as width.'), + height = AttrMapValue(isNumber, desc='height of doughnut bounding box. Need not be same as height.'), + data = AttrMapValue(None, desc='list of numbers defining sector sizes; need not sum to 1'), + labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), + startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), + direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), + slices = AttrMapValue(None, desc="collection of sector descriptor objects"), + simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.data = [1,1] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + + self.slices = TypedPropertyCollection(SectorProperties) + self.slices[0].fillColor = colors.darkcyan + self.slices[1].fillColor = colors.blueviolet + self.slices[2].fillColor = colors.blue + self.slices[3].fillColor = colors.cyan + + def demo(self): + d = Drawing(200, 100) + + dn = Doughnut() + dn.x = 50 + dn.y = 10 + dn.width = 100 + dn.height = 80 + dn.data = [10,20,30,40,50,60] + dn.labels = ['a','b','c','d','e','f'] + + dn.slices.strokeWidth=0.5 + dn.slices[3].popout = 10 + dn.slices[3].strokeWidth = 2 + dn.slices[3].strokeDashArray = [2,2] + dn.slices[3].labelRadius = 1.75 + dn.slices[3].fontColor = colors.red + dn.slices[0].fillColor = colors.darkcyan + dn.slices[1].fillColor = colors.blueviolet + dn.slices[2].fillColor = colors.blue + dn.slices[3].fillColor = colors.cyan + dn.slices[4].fillColor = colors.aquamarine + dn.slices[5].fillColor = colors.cadetblue + dn.slices[6].fillColor = colors.lightcoral + + d.add(dn) + return d + + def normalizeData(self, data=None): + from operator import add + sum = float(reduce(add,data,0)) + return abs(sum)>=1e-8 and map(lambda x,f=360./sum: f*x, data) or len(data)*[0] + + def makeSectors(self): + # normalize slice data + if type(self.data) in (ListType, TupleType) and type(self.data[0]) in (ListType, TupleType): + #it's a nested list, more than one sequence + normData = [] + n = [] + for l in self.data: + t = self.normalizeData(l) + normData.append(t) + n.append(len(t)) + self._seriesCount = max(n) + else: + normData = self.normalizeData(self.data) + n = len(normData) + self._seriesCount = n + + #labels + if self.labels is None: + labels = [] + if type(n) not in (ListType,TupleType): + labels = [''] * n + else: + for m in n: + labels = list(labels) + [''] * m + else: + labels = self.labels + #there's no point in raising errors for less than enough labels if + #we silently create all for the extreme case of no labels. + if type(n) not in (ListType,TupleType): + i = n-len(labels) + if i>0: + labels = list(labels) + [''] * i + else: + tlab = 0 + for m in n: + tlab = tlab+m + i = tlab-len(labels) + if i>0: + labels = list(labels) + [''] * i + + xradius = self.width/2.0 + yradius = self.height/2.0 + centerx = self.x + xradius + centery = self.y + yradius + + if self.direction == "anticlockwise": + whichWay = 1 + else: + whichWay = -1 + + g = Group() + sn = 0 + + startAngle = self.startAngle #% 360 + styleCount = len(self.slices) + if type(self.data[0]) in (ListType, TupleType): + #multi-series doughnut + iradius = (self.height/5.0)/len(self.data) + for series in normData: + i = 0 + for angle in series: + endAngle = (startAngle + (angle * whichWay)) #% 360 + if abs(startAngle-endAngle)>=1e-5: + if startAngle < endAngle: + a1 = startAngle + a2 = endAngle + else: + a1 = endAngle + a2 = startAngle + + #if we didn't use %stylecount here we'd end up with the later sectors + #all having the default style + sectorStyle = self.slices[i%styleCount] + + # is it a popout? + cx, cy = centerx, centery + if sectorStyle.popout != 0: + # pop out the sector + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle * pi/180.0 + popdistance = sectorStyle.popout + cx = centerx + popdistance * cos(aveAngleRadians) + cy = centery + popdistance * sin(aveAngleRadians) + + if type(n) in (ListType,TupleType): + theSector = Wedge(cx, cy, xradius+(sn*iradius)-iradius, a1, a2, yradius=yradius+(sn*iradius)-iradius, radius1=yradius+(sn*iradius)-(2*iradius)) + else: + theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=iradius) + + theSector.fillColor = sectorStyle.fillColor + theSector.strokeColor = sectorStyle.strokeColor + theSector.strokeWidth = sectorStyle.strokeWidth + theSector.strokeDashArray = sectorStyle.strokeDashArray + + g.add(theSector) + startAngle = endAngle + + text = self.getSeriesName(i,'') + if text: + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle*pi/180.0 + labelRadius = sectorStyle.labelRadius + labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) + labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) + _addWedgeLabel(self,text,g.add,averageAngle,labelX,labelY,sectorStyle) + i = i + 1 + sn = sn + 1 + + else: + i = 0 + #single series doughnut + iradius = self.height/5.0 + for angle in normData: + endAngle = (startAngle + (angle * whichWay)) #% 360 + if abs(startAngle-endAngle)>=1e-5: + if startAngle < endAngle: + a1 = startAngle + a2 = endAngle + else: + a1 = endAngle + a2 = startAngle + + #if we didn't use %stylecount here we'd end up with the later sectors + #all having the default style + sectorStyle = self.slices[i%styleCount] + + # is it a popout? + cx, cy = centerx, centery + if sectorStyle.popout != 0: + # pop out the sector + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle * pi/180.0 + popdistance = sectorStyle.popout + cx = centerx + popdistance * cos(aveAngleRadians) + cy = centery + popdistance * sin(aveAngleRadians) + + if n > 1: + theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, radius1=iradius) + elif n==1: + theSector = Wedge(cx, cy, xradius, a1, a2, yradius=yradius, iradius=iradius) + + theSector.fillColor = sectorStyle.fillColor + theSector.strokeColor = sectorStyle.strokeColor + theSector.strokeWidth = sectorStyle.strokeWidth + theSector.strokeDashArray = sectorStyle.strokeDashArray + + g.add(theSector) + + # now draw a label + if labels[i] != "": + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle*pi/180.0 + labelRadius = sectorStyle.labelRadius + labelX = centerx + (0.5 * self.width * cos(aveAngleRadians) * labelRadius) + labelY = centery + (0.5 * self.height * sin(aveAngleRadians) * labelRadius) + + theLabel = String(labelX, labelY, labels[i]) + theLabel.textAnchor = "middle" + theLabel.fontSize = sectorStyle.fontSize + theLabel.fontName = sectorStyle.fontName + theLabel.fillColor = sectorStyle.fontColor + + g.add(theLabel) + + startAngle = endAngle + i = i + 1 + + return g + + def draw(self): + g = Group() + g.add(self.makeSectors()) + return g + + +def sample1(): + "Make up something from the individual Sectors" + + d = Drawing(400, 400) + g = Group() + + s1 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=0, endangledegrees=120, radius1=100) + s1.fillColor=colors.red + s1.strokeColor=None + d.add(s1) + s2 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=120, endangledegrees=240, radius1=100) + s2.fillColor=colors.green + s2.strokeColor=None + d.add(s2) + s3 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=240, endangledegrees=260, radius1=100) + s3.fillColor=colors.blue + s3.strokeColor=None + d.add(s3) + s4 = Wedge(centerx=200, centery=200, radius=150, startangledegrees=260, endangledegrees=360, radius1=100) + s4.fillColor=colors.gray + s4.strokeColor=None + d.add(s4) + + return d + +def sample2(): + "Make a simple demo" + + d = Drawing(400, 400) + + dn = Doughnut() + dn.x = 50 + dn.y = 50 + dn.width = 300 + dn.height = 300 + dn.data = [10,20,30,40,50,60] + + d.add(dn) + + return d + +def sample3(): + "Make a more complex demo" + + d = Drawing(400, 400) + dn = Doughnut() + dn.x = 50 + dn.y = 50 + dn.width = 300 + dn.height = 300 + dn.data = [[10,20,30,40,50,60], [10,20,30,40]] + dn.labels = ['a','b','c','d','e','f'] + + d.add(dn) + + return d + +if __name__=='__main__': + + from reportlab.graphics.renderPDF import drawToFile + d = sample1() + drawToFile(d, 'doughnut1.pdf') + d = sample2() + drawToFile(d, 'doughnut2.pdf') + d = sample3() + drawToFile(d, 'doughnut3.pdf') diff --git a/bin/reportlab/graphics/charts/legends.py b/bin/reportlab/graphics/charts/legends.py new file mode 100644 index 00000000000..fae4d2b4fec --- /dev/null +++ b/bin/reportlab/graphics/charts/legends.py @@ -0,0 +1,611 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/legends.py +"""This will be a collection of legends to be used with charts. +""" +__version__=''' $Id: legends.py 2604 2005-06-08 10:12:46Z rgbecker $ ''' + +import copy, operator + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, OneOf, isString, isColorOrNone,\ + isNumberOrNone, isListOfNumbersOrNone, isStringOrNone, isBoolean,\ + NoneOr, AutoOr, isAuto, Auto, isBoxAnchor, SequenceOf +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Drawing, Group, String, Rect, Line, STATE_DEFAULTS +from reportlab.graphics.charts.areas import PlotArea +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.utils import isSeqType + + +def _getStr(s): + if isSeqType(s): + return map(str,s) + else: + return str(s) + +def _getLines(s): + if isSeqType(s): + return tuple([(x or '').split('\n') for x in s]) + else: + return (s or '').split('\n') + +def _getLineCount(s): + T = _getLines(s) + if isSeqType(s): + return max([len(x) for x in T]) + else: + return len(T) + +def _getWidth(s,fontName, fontSize, sepSpace=0): + if isSeqType(s): + sum = 0 + for t in s: + m = [stringWidth(x, fontName, fontSize) for x in t.split('\n')] + sum += m and max(m) or 0 + sum += (len(s)-1)*sepSpace + return sum + m = [stringWidth(x, fontName, fontSize) for x in s.split('\n')] + return m and max(m) or 0 + +class Legend(Widget): + """A simple legend containing rectangular swatches and strings. + + The swatches are filled rectangles whenever the respective + color object in 'colorNamePairs' is a subclass of Color in + reportlab.lib.colors. Otherwise the object passed instead is + assumed to have 'x', 'y', 'width' and 'height' attributes. + A legend then tries to set them or catches any error. This + lets you plug-in any widget you like as a replacement for + the default rectangular swatches. + + Strings can be nicely aligned left or right to the swatches. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="x-coordinate of upper-left reference point"), + y = AttrMapValue(isNumber, desc="y-coordinate of upper-left reference point"), + deltax = AttrMapValue(isNumberOrNone, desc="x-distance between neighbouring swatches"), + deltay = AttrMapValue(isNumberOrNone, desc="y-distance between neighbouring swatches"), + dxTextSpace = AttrMapValue(isNumber, desc="Distance between swatch rectangle and text"), + autoXPadding = AttrMapValue(isNumber, desc="x Padding between columns if deltax=None"), + autoYPadding = AttrMapValue(isNumber, desc="y Padding between rows if deltay=None"), + yGap = AttrMapValue(isNumber, desc="Additional gap between rows"), + dx = AttrMapValue(isNumber, desc="Width of swatch rectangle"), + dy = AttrMapValue(isNumber, desc="Height of swatch rectangle"), + columnMaximum = AttrMapValue(isNumber, desc="Max. number of items per column"), + alignment = AttrMapValue(OneOf("left", "right"), desc="Alignment of text with respect to swatches"), + colorNamePairs = AttrMapValue(None, desc="List of color/name tuples (color can also be widget)"), + fontName = AttrMapValue(isString, desc="Font name of the strings"), + fontSize = AttrMapValue(isNumber, desc="Font size of the strings"), + fillColor = AttrMapValue(isColorOrNone, desc=""), + strokeColor = AttrMapValue(isColorOrNone, desc="Border color of the swatches"), + strokeWidth = AttrMapValue(isNumber, desc="Width of the border color of the swatches"), + swatchMarker = AttrMapValue(NoneOr(AutoOr(isSymbol)), desc="None, Auto() or makeMarker('Diamond') ..."), + callout = AttrMapValue(None, desc="a user callout(self,g,x,y,(color,text))"), + boxAnchor = AttrMapValue(isBoxAnchor,'Anchor point for the legend area'), + variColumn = AttrMapValue(isBoolean,'If true column widths may vary (default is false)'), + dividerLines = AttrMapValue(OneOf(0,1,2,3,4,5,6,7),'If 1 we have dividers between the rows | 2 for extra top | 4 for bottom'), + dividerWidth = AttrMapValue(isNumber, desc="dividerLines width"), + dividerColor = AttrMapValue(isColorOrNone, desc="dividerLines color"), + dividerDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array for dividerLines.'), + dividerOffsX = AttrMapValue(SequenceOf(isNumber,emptyOK=0,lo=2,hi=2), desc='divider lines X offsets'), + dividerOffsY = AttrMapValue(isNumber, desc="dividerLines Y offset"), + sepSpace = AttrMapValue(isNumber, desc="separator spacing"), + colEndCallout = AttrMapValue(None, desc="a user callout(self,g, x, xt, y,width, lWidth)"), + ) + + def __init__(self): + # Upper-left reference point. + self.x = 0 + self.y = 0 + + # Alginment of text with respect to swatches. + self.alignment = "left" + + # x- and y-distances between neighbouring swatches. + self.deltax = 75 + self.deltay = 20 + self.autoXPadding = 5 + self.autoYPadding = 2 + + # Size of swatch rectangle. + self.dx = 10 + self.dy = 10 + + # Distance between swatch rectangle and text. + self.dxTextSpace = 10 + + # Max. number of items per column. + self.columnMaximum = 3 + + # Color/name pairs. + self.colorNamePairs = [ (colors.red, "red"), + (colors.blue, "blue"), + (colors.green, "green"), + (colors.pink, "pink"), + (colors.yellow, "yellow") ] + + # Font name and size of the labels. + self.fontName = STATE_DEFAULTS['fontName'] + self.fontSize = STATE_DEFAULTS['fontSize'] + self.fillColor = STATE_DEFAULTS['fillColor'] + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeWidth = STATE_DEFAULTS['strokeWidth'] + self.swatchMarker = None + self.boxAnchor = 'nw' + self.yGap = 0 + self.variColumn = 0 + self.dividerLines = 0 + self.dividerWidth = 0.5 + self.dividerDashArray = None + self.dividerColor = colors.black + self.dividerOffsX = (0,0) + self.dividerOffsY = 0 + self.sepSpace = 0 + self.colEndCallout = None + + def _getChartStyleName(self,chart): + for a in 'lines', 'bars', 'slices', 'strands': + if hasattr(chart,a): return a + return None + + def _getChartStyle(self,chart): + return getattr(chart,self._getChartStyleName(chart),None) + + def _getTexts(self,colorNamePairs): + if not isAuto(colorNamePairs): + texts = [_getStr(p[1]) for p in colorNamePairs] + else: + chart = colorNamePairs.chart + texts = [str(chart.getSeriesName(i,'series %d' % i)) for i in xrange(chart._seriesCount)] + return texts + + def _calculateMaxWidth(self, colorNamePairs): + "Calculate the maximum width of some given strings." + M = [] + a = M.append + for t in self._getTexts(colorNamePairs): + M.append(_getWidth(t, self.fontName, self.fontSize,self.sepSpace)) + if not M: return 0 + if self.variColumn: + columnMaximum = self.columnMaximum + return [max(M[r:r+columnMaximum]) for r in range(0,len(M),self.columnMaximum)] + else: + return max(M) + + def _calcHeight(self): + dy = self.dy + yGap = self.yGap + thisy = upperlefty = self.y - dy + fontSize = self.fontSize + ascent=getFont(self.fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent *= fontSize + leading = fontSize*1.2 + deltay = self.deltay + if not deltay: deltay = max(dy,leading)+self.autoYPadding + columnCount = 0 + count = 0 + lowy = upperlefty + lim = self.columnMaximum - 1 + for name in self._getTexts(self.colorNamePairs): + y0 = thisy+(dy-ascent)*0.5 + y = y0 - _getLineCount(name)*leading + leadingMove = 2*y0-y-thisy + newy = thisy-max(deltay,leadingMove)-yGap + lowy = min(y,newy,lowy) + if count==lim: + count = 0 + thisy = upperlefty + columnCount = columnCount + 1 + else: + thisy = newy + count = count+1 + return upperlefty - lowy + + def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor): + return Rect(x, thisy, dx, dy, + fillColor = fillColor, + strokeColor = strokeColor, + strokeWidth = strokeWidth, + ) + + def draw(self): + colorNamePairs = self.colorNamePairs + autoCP = isAuto(colorNamePairs) + if autoCP: + chart = getattr(colorNamePairs,'chart',getattr(colorNamePairs,'obj',None)) + swatchMarker = None + autoCP = Auto(obj=chart) + n = chart._seriesCount + chartTexts = self._getTexts(colorNamePairs) + else: + swatchMarker = getattr(self,'swatchMarker',None) + if isAuto(swatchMarker): + chart = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)) + swatchMarker = Auto(obj=chart) + n = len(colorNamePairs) + dx = self.dx + dy = self.dy + alignment = self.alignment + columnMaximum = self.columnMaximum + deltax = self.deltax + deltay = self.deltay + dxTextSpace = self.dxTextSpace + fontName = self.fontName + fontSize = self.fontSize + fillColor = self.fillColor + strokeWidth = self.strokeWidth + strokeColor = self.strokeColor + leading = fontSize*1.2 + yGap = self.yGap + if not deltay: + deltay = max(dy,leading)+self.autoYPadding + ba = self.boxAnchor + baw = ba not in ('nw','w','sw','autox') + maxWidth = self._calculateMaxWidth(colorNamePairs) + nCols = int((n+columnMaximum-1)/columnMaximum) + xW = dx+dxTextSpace+self.autoXPadding + variColumn = self.variColumn + if variColumn: + width = reduce(operator.add,maxWidth,0)+xW*nCols + else: + deltax = max(maxWidth+xW,deltax) + width = nCols*deltax + maxWidth = nCols*[maxWidth] + + thisx = self.x + thisy = self.y - self.dy + if ba not in ('ne','n','nw','autoy'): + height = self._calcHeight() + if ba in ('e','c','w'): + thisy += height/2. + else: + thisy += height + if baw: + if ba in ('n','c','s'): + thisx -= width/2 + else: + thisx -= width + upperlefty = thisy + + g = Group() + def gAdd(t,g=g,fontName=fontName,fontSize=fontSize,fillColor=fillColor): + t.fontName = fontName + t.fontSize = fontSize + t.fillColor = fillColor + return g.add(t) + + ascent=getFont(fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent *= fontSize # normalize + + lim = columnMaximum - 1 + callout = getattr(self,'callout',None) + dividerLines = self.dividerLines + if dividerLines: + dividerWidth = self.dividerWidth + dividerColor = self.dividerColor + dividerDashArray = self.dividerDashArray + dividerOffsX = self.dividerOffsX + dividerOffsY = self.dividerOffsY + + for i in xrange(n): + if autoCP: + col = autoCP + col.index = i + name = chartTexts[i] + else: + col, name = colorNamePairs[i] + if isAuto(swatchMarker): + col = swatchMarker + col.index = i + if isAuto(name): + name = getattr(swatchMarker,'chart',getattr(swatchMarker,'obj',None)).getSeriesName(i,'series %d' % i) + T = _getLines(name) + S = [] + j = int(i/columnMaximum) + + # thisy+dy/2 = y+leading/2 + y = y0 = thisy+(dy-ascent)*0.5 + + if callout: callout(self,g,thisx,y,(col,name)) + if alignment == "left": + if isSeqType(name): + for t in T[0]: + S.append(String(thisx,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "start")) + y -= leading + yd = y + y = y0 + for t in T[1]: + S.append(String(thisx+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "end")) + y -= leading + y = min(yd,y) + else: + for t in T: + # align text to left + S.append(String(thisx+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "end")) + y -= leading + x = thisx+maxWidth[j]+dxTextSpace + elif alignment == "right": + if isSeqType(name): + y0 = y + for t in T[0]: + S.append(String(thisx+dx+dxTextSpace,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "start")) + y -= leading + yd = y + y = y0 + for t in T[1]: + S.append(String(thisx+dx+dxTextSpace+maxWidth[j],y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "end")) + y -= leading + y = min(yd,y) + else: + for t in T: + # align text to right + S.append(String(thisx+dx+dxTextSpace,y,t,fontName=fontName,fontSize=fontSize,fillColor=fillColor, + textAnchor = "start")) + y -= leading + x = thisx + else: + raise ValueError, "bad alignment" + leadingMove = 2*y0-y-thisy + + if dividerLines: + xd = thisx+dx+dxTextSpace+maxWidth[j]+dividerOffsX[1] + yd = thisy+dy*0.5+dividerOffsY + if ((dividerLines&1) and i%columnMaximum) or ((dividerLines&2) and not i%columnMaximum): + g.add(Line(thisx+dividerOffsX[0],yd,xd,yd, + strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray)) + + if (dividerLines&4) and (i%columnMaximum==lim or i==(n-1)): + yd -= max(deltay,leadingMove)+yGap + g.add(Line(thisx+dividerOffsX[0],yd,xd,yd, + strokeColor=dividerColor, strokeWidth=dividerWidth, strokeDashArray=dividerDashArray)) + + # Make a 'normal' color swatch... + if isAuto(col): + chart = getattr(col,'chart',getattr(col,'obj',None)) + g.add(chart.makeSwatchSample(getattr(col,'index',i),x,thisy,dx,dy)) + elif isinstance(col, colors.Color): + if isSymbol(swatchMarker): + g.add(uSymbol2Symbol(swatchMarker,x+dx/2.,thisy+dy/2.,col)) + else: + g.add(self._defaultSwatch(x,thisy,dx,dy,fillColor=col,strokeWidth=strokeWidth,strokeColor=strokeColor)) + else: + try: + c = copy.deepcopy(col) + c.x = x + c.y = thisy + c.width = dx + c.height = dy + g.add(c) + except: + pass + + map(gAdd,S) + if self.colEndCallout and (i%columnMaximum==lim or i==(n-1)): + if alignment == "left": + xt = thisx + else: + xt = thisx+dx+dxTextSpace + yd = thisy+dy*0.5+dividerOffsY - (max(deltay,leadingMove)+yGap) + self.colEndCallout(self, g, thisx, xt, yd, maxWidth[j], maxWidth[j]+dx+dxTextSpace) + + if i%columnMaximum==lim: + if variColumn: + thisx += maxWidth[j]+xW + else: + thisx = thisx+deltax + thisy = upperlefty + else: + thisy = thisy-max(deltay,leadingMove)-yGap + + return g + + def demo(self): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'left' + legend.x = 0 + legend.y = 100 + legend.dxTextSpace = 5 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + +class TotalAnnotator: + def __init__(self, lText='Total', rText='0.0', fontName='Times-Roman', fontSize=10, + fillColor=colors.black, strokeWidth=0.5, strokeColor=colors.black, strokeDashArray=None, + dx=0, dy=0, dly=0, dlx=(0,0)): + self.lText = lText + self.rText = rText + self.fontName = fontName + self.fontSize = fontSize + self.fillColor = fillColor + self.dy = dy + self.dx = dx + self.dly = dly + self.dlx = dlx + self.strokeWidth = strokeWidth + self.strokeColor = strokeColor + self.strokeDashArray = strokeDashArray + + def __call__(self,legend, g, x, xt, y, width, lWidth): + from reportlab.graphics.shapes import String, Line + fontSize = self.fontSize + fontName = self.fontName + fillColor = self.fillColor + strokeColor = self.strokeColor + strokeWidth = self.strokeWidth + ascent=getFont(fontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent *= fontSize + leading = fontSize*1.2 + yt = y+self.dy-ascent*1.3 + if self.lText and fillColor: + g.add(String(xt,yt,self.lText, + fontName=fontName, + fontSize=fontSize, + fillColor=fillColor, + textAnchor = "start")) + if self.rText: + g.add(String(xt+width,yt,self.rText, + fontName=fontName, + fontSize=fontSize, + fillColor=fillColor, + textAnchor = "end")) + if strokeWidth and strokeColor: + yL = y+self.dly-leading + g.add(Line(x+self.dlx[0],yL,x+self.dlx[1]+lWidth,yL, + strokeColor=strokeColor, strokeWidth=strokeWidth, + strokeDashArray=self.strokeDashArray)) + +class LineSwatch(Widget): + """basically a Line with properties added so it can be used in a LineLegend""" + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="x-coordinate for swatch line start point"), + y = AttrMapValue(isNumber, desc="y-coordinate for swatch line start point"), + width = AttrMapValue(isNumber, desc="length of swatch line"), + height = AttrMapValue(isNumber, desc="used for line strokeWidth"), + strokeColor = AttrMapValue(isColorOrNone, desc="color of swatch line"), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc="dash array for swatch line"), + ) + + def __init__(self): + from reportlab.lib.colors import red + from reportlab.graphics.shapes import Line + self.x = 0 + self.y = 0 + self.width = 20 + self.height = 1 + self.strokeColor = red + self.strokeDashArray = None + + def draw(self): + l = Line(self.x,self.y,self.x+self.width,self.y) + l.strokeColor = self.strokeColor + l.strokeDashArray = self.strokeDashArray + l.strokeWidth = self.height + return l + +class LineLegend(Legend): + """A subclass of Legend for drawing legends with lines as the + swatches rather than rectangles. Useful for lineCharts and + linePlots. Should be similar in all other ways the the standard + Legend class. + """ + + def __init__(self): + Legend.__init__(self) + + # Size of swatch rectangle. + self.dx = 10 + self.dy = 2 + + def _defaultSwatch(self,x,thisy,dx,dy,fillColor,strokeWidth,strokeColor): + l = LineSwatch() + l.x = x + l.y = thisy + l.width = dx + l.height = dy + l.strokeColor = fillColor + return l + +def sample1c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + legend.x = 0 + legend.y = 100 + legend.dxTextSpace = 5 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + + +def sample2c(): + "Make sample legend." + + d = Drawing(200, 100) + + legend = Legend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + + d.add(legend, 'legend') + + return d + +def sample3(): + "Make sample legend with line swatches." + + d = Drawing(200, 100) + + legend = LineLegend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + items = map(lambda i:(getattr(colors, i), i), items) + legend.colorNamePairs = items + d.add(legend, 'legend') + + return d + + +def sample3a(): + "Make sample legend with line swatches and dasharrays on the lines." + + d = Drawing(200, 100) + + legend = LineLegend() + legend.alignment = 'right' + legend.x = 20 + legend.y = 90 + legend.deltax = 60 + legend.dxTextSpace = 10 + legend.columnMaximum = 4 + items = 'red green blue yellow pink black white'.split() + darrays = ([2,1], [2,5], [2,2,5,5], [1,2,3,4], [4,2,3,4], [1,2,3,4,5,6], [1]) + cnp = [] + for i in range(0, len(items)): + l = LineSwatch() + l.strokeColor = getattr(colors, items[i]) + l.strokeDashArray = darrays[i] + cnp.append((l, items[i])) + legend.colorNamePairs = cnp + d.add(legend, 'legend') + + return d diff --git a/bin/reportlab/graphics/charts/linecharts.py b/bin/reportlab/graphics/charts/linecharts.py new file mode 100644 index 00000000000..99ef982a644 --- /dev/null +++ b/bin/reportlab/graphics/charts/linecharts.py @@ -0,0 +1,695 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/linecharts.py +""" +This modules defines a very preliminary Line Chart example. +""" +__version__=''' $Id: linecharts.py 2493 2004-12-22 16:14:25Z rgbecker $ ''' + +import string +from types import FunctionType, StringType + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColor, isColorOrNone, isListOfStrings, \ + isListOfStringsOrNone, SequenceOf, isBoolean, NoneOr, \ + isListOfNumbersOrNone, isStringOrNone +from reportlab.lib.attrmap import * +from reportlab.lib.formatters import Formatter +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.shapes import Line, Rect, Group, Drawing, Polygon, PolyLine +from reportlab.graphics.widgets.signsandsymbols import NoEntry +from reportlab.graphics.charts.axes import XCategoryAxis, YValueAxis +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker +from reportlab.graphics.charts.areas import PlotArea + +class LineChartProperties(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), + symbol = AttrMapValue(NoneOr(isSymbol), desc='Widget placed at data points.'), + shader = AttrMapValue(None, desc='Shader Class.'), + filler = AttrMapValue(None, desc='Filler Class.'), + name = AttrMapValue(isStringOrNone, desc='Name of the line.'), + ) + +class AbstractLineChart(PlotArea): + + def makeSwatchSample(self,rowNo, x, y, width, height): + baseStyle = self.lines + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + color = style.strokeColor + y = y+height/2. + if self.joinedLines: + dash = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth= getattr(style, 'strokeWidth', getattr(style, 'strokeWidth',None)) + L = Line(x,y,x+width,y,strokeColor=color,strokeLineCap=0) + if strokeWidth: L.strokeWidth = strokeWidth + if dash: L.strokeDashArray = dash + else: + L = None + + if hasattr(style, 'symbol'): + S = style.symbol + elif hasattr(baseStyle, 'symbol'): + S = baseStyle.symbol + else: + S = None + + if S: S = uSymbol2Symbol(S,x+width/2.,y,color) + if S and L: + g = Group() + g.add(L) + g.add(S) + return g + return S or L + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return getattr(self.lines[i],'name',default) + +class LineChart(AbstractLineChart): + pass + +# This is conceptually similar to the VerticalBarChart. +# Still it is better named HorizontalLineChart... :-/ + +class HorizontalLineChart(LineChart): + """Line chart with multiple lines. + + A line chart is assumed to have one category and one value axis. + Despite its generic name this particular line chart class has + a vertical value axis and a horizontal category one. It may + evolve into individual horizontal and vertical variants (like + with the existing bar charts). + + Available attributes are: + + x: x-position of lower-left chart origin + y: y-position of lower-left chart origin + width: chart width + height: chart height + + useAbsolute: disables auto-scaling of chart elements (?) + lineLabelNudge: distance of data labels to data points + lineLabels: labels associated with data values + lineLabelFormat: format string or callback function + groupSpacing: space between categories + + joinedLines: enables drawing of lines + + strokeColor: color of chart lines (?) + fillColor: color for chart background (?) + lines: style list, used cyclically for data series + + valueAxis: value axis object + categoryAxis: category axis object + categoryNames: category names + + data: chart data, a list of data series of equal length + """ + + _attrMap = AttrMap(BASE=LineChart, + useAbsolute = AttrMapValue(isNumber, desc='Flag to use absolute spacing values.'), + lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'), + lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'), + lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'), + lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'), + groupSpacing = AttrMapValue(isNumber, desc='? - Likely to disappear.'), + joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'), + lines = AttrMapValue(None, desc='Handle of the lines.'), + valueAxis = AttrMapValue(None, desc='Handle of the value axis.'), + categoryAxis = AttrMapValue(None, desc='Handle of the category axis.'), + categoryNames = AttrMapValue(isListOfStringsOrNone, desc='List of category names.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + inFill = AttrMapValue(isBoolean, desc='Whether infilling should be done.'), + reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def __init__(self): + LineChart.__init__(self) + + # Allow for a bounding rectangle. + self.strokeColor = None + self.fillColor = None + + # Named so we have less recoding for the horizontal one :-) + self.categoryAxis = XCategoryAxis() + self.valueAxis = YValueAxis() + + # This defines two series of 3 points. Just an example. + self.data = [(100,110,120,130), + (70, 80, 80, 90)] + self.categoryNames = ('North','South','East','West') + + self.lines = TypedPropertyCollection(LineChartProperties) + self.lines.strokeWidth = 1 + self.lines[0].strokeColor = colors.red + self.lines[1].strokeColor = colors.green + self.lines[2].strokeColor = colors.blue + + # control spacing. if useAbsolute = 1 then + # the next parameters are in points; otherwise + # they are 'proportions' and are normalized to + # fit the available space. + self.useAbsolute = 0 #- not done yet + self.groupSpacing = 1 #5 + + self.lineLabels = TypedPropertyCollection(Label) + self.lineLabelFormat = None + self.lineLabelArray = None + + # This says whether the origin is above or below + # the data point. +10 means put the origin ten points + # above the data point if value > 0, or ten + # points below if data value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.lineLabelNudge = 10 + # If you have multiple series, by default they butt + # together. + + # New line chart attributes. + self.joinedLines = 1 # Connect items with straight lines. + self.inFill = 0 + self.reversePlotOrder = 0 + + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(200, 100) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 10, 21, 28, 38, 46, 25, 5) + ] + + lc = HorizontalLineChart() + + lc.x = 20 + lc.y = 10 + lc.height = 85 + lc.width = 170 + lc.data = data + lc.lines.symbol = makeMarker('Circle') + + drawing.add(lc) + + return drawing + + + def calcPositions(self): + """Works out where they go. + + Sets an attribute _positions which is a list of + lists of (x, y) matching the data. + """ + + self._seriesCount = len(self.data) + self._rowLength = max(map(len,self.data)) + + if self.useAbsolute: + # Dimensions are absolute. + normFactor = 1.0 + else: + # Dimensions are normalized to fit. + normWidth = self.groupSpacing + availWidth = self.categoryAxis.scale(0)[1] + normFactor = availWidth / normWidth + + self._positions = [] + for rowNo in range(len(self.data)): + lineRow = [] + for colNo in range(len(self.data[rowNo])): + datum = self.data[rowNo][colNo] + if datum is not None: + (groupX, groupWidth) = self.categoryAxis.scale(colNo) + x = groupX + (0.5 * self.groupSpacing * normFactor) + y = self.valueAxis.scale(0) + height = self.valueAxis.scale(datum) - y + lineRow.append((x, y+height)) + self._positions.append(lineRow) + + + def _innerDrawLabel(self, rowNo, colNo, x, y): + "Draw a label for a given item in the list." + + labelFmt = self.lineLabelFormat + labelValue = self.data[rowNo][colNo] + + if labelFmt is None: + labelText = None + elif type(labelFmt) is StringType: + if labelFmt == 'values': + labelText = self.lineLabelArray[rowNo][colNo] + else: + labelText = labelFmt % labelValue + elif type(labelFmt) is FunctionType: + labelText = labelFmt(labelValue) + elif isinstance(labelFmt, Formatter): + labelText = labelFmt(labelValue) + else: + msg = "Unknown formatter type %s, expected string or function" + raise Exception, msg % labelFmt + + if labelText: + label = self.lineLabels[(rowNo, colNo)] + # Make sure labels are some distance off the data point. + if y > 0: + label.setOrigin(x, y + self.lineLabelNudge) + else: + label.setOrigin(x, y - self.lineLabelNudge) + label.setText(labelText) + else: + label = None + return label + + def drawLabel(self, G, rowNo, colNo, x, y): + '''Draw a label for a given item in the list. + G must have an add method''' + G.add(self._innerDrawLabel(rowNo,colNo,x,y)) + + def makeLines(self): + g = Group() + + labelFmt = self.lineLabelFormat + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = self.inFill + if inFill: + inFillY = self.categoryAxis._y + inFillX0 = self.valueAxis._x + inFillX1 = inFillX0 + self.categoryAxis._length + inFillG = getattr(self,'_inFillG',g) + + # Iterate over data rows. + for rowNo in P: + row = self._positions[rowNo] + styleCount = len(self.lines) + styleIdx = rowNo % styleCount + rowStyle = self.lines[styleIdx] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + + if hasattr(self.lines[styleIdx], 'strokeWidth'): + strokeWidth = self.lines[styleIdx].strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + strokeWidth = self.lines.strokeWidth + else: + strokeWidth = None + + # Iterate over data columns. + if self.joinedLines: + points = [] + for colNo in range(len(row)): + points += row[colNo] + if inFill: + points = points + [inFillX1,inFillY,inFillX0,inFillY] + inFillG.add(Polygon(points,fillColor=rowColor,strokeColor=rowColor,strokeWidth=0.1)) + else: + line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1) + if strokeWidth: + line.strokeWidth = strokeWidth + if dash: + line.strokeDashArray = dash + g.add(line) + + if hasattr(self.lines[styleIdx], 'symbol'): + uSymbol = self.lines[styleIdx].symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for colNo in range(len(row)): + x1, y1 = row[colNo] + symbol = uSymbol2Symbol(uSymbol,x1,y1,rowStyle.strokeColor) + if symbol: g.add(symbol) + + # Draw item labels. + for colNo in range(len(row)): + x1, y1 = row[colNo] + self.drawLabel(g, rowNo, colNo, x1, y1) + + return g + + def draw(self): + "Draws itself." + + vA, cA = self.valueAxis, self.categoryAxis + vA.setPosition(self.x, self.y, self.height) + if vA: vA.joinAxis = cA + if cA: cA.joinAxis = vA + vA.configure(self.data) + + # If zero is in chart, put x axis there, otherwise + # use bottom. + xAxisCrossesAt = vA.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + cA.setPosition(self.x, y, self.width) + cA.configure(self.data) + + self.calcPositions() + + g = Group() + g.add(self.makeBackground()) + if self.inFill: + self._inFillG = Group() + g.add(self._inFillG) + + g.add(cA) + g.add(vA) + vA.gridStart = cA._x + vA.gridEnd = cA._x+cA._length + cA.gridStart = vA._y + cA.gridEnd = vA._y+vA._length + cA.makeGrid(g,parent=self) + vA.makeGrid(g,parent=self) + g.add(self.makeLines()) + for a in getattr(self,'annotations',()): g.add(a(self,cA.scale,vA.scale)) + return g + +def _cmpFakeItem(a,b): + '''t, z0, z1, x, y = a[:5]''' + return cmp((-a[1],a[3],a[0],-a[4]),(-b[1],b[3],b[0],-b[4])) + +class _FakeGroup: + def __init__(self): + self._data = [] + + def add(self,what): + if what: self._data.append(what) + + def value(self): + return self._data + + def sort(self): + self._data.sort(_cmpFakeItem) + #for t in self._data: print t + +class HorizontalLineChart3D(HorizontalLineChart): + _attrMap = AttrMap(BASE=HorizontalLineChart, + theta_x = AttrMapValue(isNumber, desc='dx/dz'), + theta_y = AttrMapValue(isNumber, desc='dy/dz'), + zDepth = AttrMapValue(isNumber, desc='depth of an individual series'), + zSpace = AttrMapValue(isNumber, desc='z gap around series'), + ) + theta_x = .5 + theta_y = .5 + zDepth = 10 + zSpace = 3 + + def calcPositions(self): + HorizontalLineChart.calcPositions(self) + nSeries = self._seriesCount + zSpace = self.zSpace + zDepth = self.zDepth + if self.categoryAxis.style=='parallel_3d': + _3d_depth = nSeries*zDepth+(nSeries+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zSpace = self.zSpace + if self.categoryAxis.style=='parallel_3d': + z0 = rowNo*(self.zDepth+zSpace)+zSpace + else: + z0 = zSpace + return z0 + + def _zadjust(self,x,y,z): + return x+z*self.theta_x, y+z*self.theta_y + + def makeLines(self): + labelFmt = self.lineLabelFormat + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = self.inFill + assert not inFill, "inFill not supported for 3d yet" + #if inFill: + #inFillY = self.categoryAxis._y + #inFillX0 = self.valueAxis._x + #inFillX1 = inFillX0 + self.categoryAxis._length + #inFillG = getattr(self,'_inFillG',g) + zDepth = self.zDepth + _zadjust = self._zadjust + theta_x = self.theta_x + theta_y = self.theta_y + F = _FakeGroup() + from utils3d import _make_3d_line_info + tileWidth = getattr(self,'_3d_tilewidth',None) + if not tileWidth and self.categoryAxis.style!='parallel_3d': tileWidth = 1 + + # Iterate over data rows. + for rowNo in P: + row = self._positions[rowNo] + n = len(row) + styleCount = len(self.lines) + styleIdx = rowNo % styleCount + rowStyle = self.lines[styleIdx] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth + + if hasattr(self.lines[styleIdx], 'strokeWidth'): + strokeWidth = self.lines[styleIdx].strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + strokeWidth = self.lines.strokeWidth + else: + strokeWidth = None + + # Iterate over data columns. + if self.joinedLines: + if n: + x0, y0 = row[0] + for colNo in xrange(1,n): + x1, y1 = row[colNo] + _make_3d_line_info( F, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + rowColor, fillColorShaded=None, tileWidth=tileWidth, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1) + x0, y0 = x1, y1 + + if hasattr(self.lines[styleIdx], 'symbol'): + uSymbol = self.lines[styleIdx].symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for colNo in xrange(n): + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + symbol = uSymbol2Symbol(uSymbol,x1,y1,rowColor) + if symbol: F.add((2,z0,z0,x1,y1,symbol)) + + # Draw item labels. + for colNo in xrange(n): + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + L = self._innerDrawLabel(rowNo, colNo, x1, y1) + if L: F.add((2,z0,z0,x1,y1,L)) + + F.sort() + g = Group() + map(lambda x,a=g.add: a(x[-1]),F.value()) + return g + +class VerticalLineChart(LineChart): + pass + + +def sample1(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lines.symbol = makeMarker('FilledDiamond') + lc.lineLabelFormat = '%2.0f' + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing + + +class SampleHorizontalLineChart(HorizontalLineChart): + "Sample class overwriting one method to draw additional horizontal lines." + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(200, 100) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (14, 10, 21, 28, 38, 46, 25, 5) + ] + + lc = SampleHorizontalLineChart() + + lc.x = 20 + lc.y = 10 + lc.height = 85 + lc.width = 170 + lc.data = data + lc.strokeColor = colors.white + lc.fillColor = colors.HexColor(0xCCCCCC) + + drawing.add(lc) + + return drawing + + + def makeBackground(self): + g = Group() + + g.add(HorizontalLineChart.makeBackground(self)) + + valAxis = self.valueAxis + valTickPositions = valAxis._tickValues + + for y in valTickPositions: + y = valAxis.scale(y) + g.add(Line(self.x, y, self.x+self.width, y, + strokeColor = self.strokeColor)) + + return g + + + +def sample1a(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = SampleHorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.strokeColor = colors.white + lc.fillColor = colors.HexColor(0xCCCCCC) + lc.lines.symbol = makeMarker('FilledDiamond') + lc.lineLabelFormat = '%2.0f' + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing + + +def sample2(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lines.symbol = makeMarker('Smiley') + lc.lineLabelFormat = '%2.0f' + lc.strokeColor = colors.black + lc.fillColor = colors.lightblue + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing + + +def sample3(): + drawing = Drawing(400, 200) + + data = [ + (13, 5, 20, 22, 37, 45, 19, 4), + (5, 20, 46, 38, 23, 21, 6, 14) + ] + + lc = HorizontalLineChart() + + lc.x = 50 + lc.y = 50 + lc.height = 125 + lc.width = 300 + lc.data = data + lc.joinedLines = 1 + lc.lineLabelFormat = '%2.0f' + lc.strokeColor = colors.black + + lc.lines[0].symbol = makeMarker('Smiley') + lc.lines[1].symbol = NoEntry + lc.lines[0].strokeWidth = 2 + lc.lines[1].strokeWidth = 4 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + lc.categoryAxis.categoryNames = catNames + lc.categoryAxis.labels.boxAnchor = 'n' + + lc.valueAxis.valueMin = 0 + lc.valueAxis.valueMax = 60 + lc.valueAxis.valueStep = 15 + + drawing.add(lc) + + return drawing diff --git a/bin/reportlab/graphics/charts/lineplots.py b/bin/reportlab/graphics/charts/lineplots.py new file mode 100644 index 00000000000..42732dddde3 --- /dev/null +++ b/bin/reportlab/graphics/charts/lineplots.py @@ -0,0 +1,1083 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/lineplots.py +"""This module defines a very preliminary Line Plot example. +""" +__version__=''' $Id: lineplots.py 2659 2005-08-18 10:28:12Z rgbecker $ ''' + +import string, time +from types import FunctionType + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Drawing, Group, Rect, Line, PolyLine, Polygon, _SetKeyWordArgs +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.charts.axes import XValueAxis, YValueAxis, AdjYValueAxis, NormalDateXValueAxis +from reportlab.graphics.charts.utils import * +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol, makeMarker +from reportlab.graphics.widgets.grids import Grid, DoubleGrid, ShadedRect, ShadedPolygon +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.charts.areas import PlotArea + +# This might be moved again from here... +class LinePlotProperties(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber, desc='Width of a line.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color of a line.'), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone, desc='Dash array of a line.'), + symbol = AttrMapValue(None, desc='Widget placed at data points.'), + shader = AttrMapValue(None, desc='Shader Class.'), + filler = AttrMapValue(None, desc='Filler Class.'), + name = AttrMapValue(isStringOrNone, desc='Name of the line.'), + ) + +class Shader(_SetKeyWordArgs): + _attrMap = AttrMap(BASE=PlotArea, + vertical = AttrMapValue(isBoolean, desc='If true shade to x axis'), + colors = AttrMapValue(SequenceOf(isColorOrNone,lo=2,hi=2), desc='(AxisColor, LineColor)'), + ) + + def shade(self, lp, g, rowNo, rowColor, row): + c = [None,None] + c = getattr(self,'colors',c) or c + if not c[0]: c[0] = getattr(lp,'fillColor',colors.white) + if not c[1]: c[1] = rowColor + +class NoFiller: + def fill(self, lp, g, rowNo, rowColor, points): + pass + +class Filler: + '''mixin providing simple polygon fill''' + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone, desc='filler interior color'), + strokeColor = AttrMapValue(isColorOrNone, desc='filler edge color'), + strokeWidth = AttrMapValue(isNumberOrNone, desc='filler edge width'), + ) + def __init__(self,**kw): + self.__dict__ = kw + + def fill(self, lp, g, rowNo, rowColor, points): + g.add(Polygon(points, + fillColor=getattr(self,'fillColor',rowColor), + strokeColor=getattr(self,'strokeColor',rowColor), + strokeWidth=getattr(self,'strokeWidth',0.1))) + +class ShadedPolyFiller(Filler,ShadedPolygon): + pass + +class PolyFiller(Filler,Polygon): + pass + +from linecharts import AbstractLineChart +class LinePlot(AbstractLineChart): + """Line plot with multiple lines. + + Both x- and y-axis are value axis (so there are no seperate + X and Y versions of this class). + """ + _attrMap = AttrMap(BASE=PlotArea, + reversePlotOrder = AttrMapValue(isBoolean, desc='If true reverse plot order.'), + lineLabelNudge = AttrMapValue(isNumber, desc='Distance between a data point and its label.'), + lineLabels = AttrMapValue(None, desc='Handle to the list of data point labels.'), + lineLabelFormat = AttrMapValue(None, desc='Formatting string or function used for data point labels.'), + lineLabelArray = AttrMapValue(None, desc='explicit array of line label values, must match size of data if present.'), + joinedLines = AttrMapValue(isNumber, desc='Display data points joined with lines if true.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for background border of plot area.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'), + lines = AttrMapValue(None, desc='Handle of the lines.'), + xValueAxis = AttrMapValue(None, desc='Handle of the x axis.'), + yValueAxis = AttrMapValue(None, desc='Handle of the y axis.'), + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) x/y tuples.'), + annotations = AttrMapValue(None, desc='list of callables, will be called with self, xscale, yscale.'), + ) + + def __init__(self): + PlotArea.__init__(self) + self.reversePlotOrder = 0 + + self.xValueAxis = XValueAxis() + self.yValueAxis = YValueAxis() + + # this defines two series of 3 points. Just an example. + self.data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3,4), (4,6)) + ] + + self.lines = TypedPropertyCollection(LinePlotProperties) + self.lines.strokeWidth = 1 + self.lines[0].strokeColor = colors.red + self.lines[1].strokeColor = colors.blue + + self.lineLabels = TypedPropertyCollection(Label) + self.lineLabelFormat = None + self.lineLabelArray = None + + # this says whether the origin is inside or outside + # the bar - +10 means put the origin ten points + # above the tip of the bar if value > 0, or ten + # points inside if bar value < 0. This is different + # to label dx/dy which are not dependent on the + # sign of the data. + self.lineLabelNudge = 10 + # if you have multiple series, by default they butt + # together. + + # New line chart attributes. + self.joinedLines = 1 # Connect items with straight lines. + + #private attributes + self._inFill = None + + def demo(self): + """Shows basic use of a line chart.""" + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.lines[0].strokeColor = colors.red + lp.lines[0].symbol = makeMarker('FilledCircle') + lp.lines[1].strokeColor = colors.blue + lp.lines[1].symbol = makeMarker('FilledDiamond') + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueStep = 1 + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + + def calcPositions(self): + """Works out where they go. + + Sets an attribute _positions which is a list of + lists of (x, y) matching the data. + """ + + self._seriesCount = len(self.data) + self._rowLength = max(map(len,self.data)) + + self._positions = [] + for rowNo in range(len(self.data)): + line = [] + for colNo in range(len(self.data[rowNo])): + datum = self.data[rowNo][colNo] # x,y value + if type(datum[0]) == type(''): + x = self.xValueAxis.scale(mktime(mkTimeTuple(datum[0]))) + else: + x = self.xValueAxis.scale(datum[0]) + y = self.yValueAxis.scale(datum[1]) + line.append((x, y)) + self._positions.append(line) + + def _innerDrawLabel(self, rowNo, colNo, x, y): + "Draw a label for a given item in the list." + + labelFmt = self.lineLabelFormat + labelValue = self.data[rowNo][colNo][1] ### + + if labelFmt is None: + labelText = None + elif type(labelFmt) is StringType: + if labelFmt == 'values': + labelText = self.lineLabelArray[rowNo][colNo] + else: + labelText = labelFmt % labelValue + elif type(labelFmt) is FunctionType: + labelText = labelFmt(labelValue) + elif isinstance(labelFmt, Formatter): + labelText = labelFmt(labelValue) + else: + msg = "Unknown formatter type %s, expected string or function" + raise Exception, msg % labelFmt + + if labelText: + label = self.lineLabels[(rowNo, colNo)] + #hack to make sure labels are outside the bar + if y > 0: + label.setOrigin(x, y + self.lineLabelNudge) + else: + label.setOrigin(x, y - self.lineLabelNudge) + label.setText(labelText) + else: + label = None + return label + + def drawLabel(self, G, rowNo, colNo, x, y): + '''Draw a label for a given item in the list. + G must have an add method''' + G.add(self._innerDrawLabel(rowNo,colNo,x,y)) + + def makeLines(self): + g = Group() + bubblePlot = getattr(self,'_bubblePlot',None) + if bubblePlot: + yA = self.yValueAxis + xA = self.xValueAxis + bubbleR = min(yA._bubbleRadius,xA._bubbleRadius) + bubbleMax = xA._bubbleMax + + labelFmt = self.lineLabelFormat + + P = range(len(self._positions)) + if self.reversePlotOrder: P.reverse() + inFill = getattr(self,'_inFill',None) + if inFill: + inFillY = self.xValueAxis._y + inFillX0 = self.yValueAxis._x + inFillX1 = inFillX0 + self.xValueAxis._length + inFillG = getattr(self,'_inFillG',g) + # Iterate over data rows. + styleCount = len(self.lines) + for rowNo in P: + row = self._positions[rowNo] + rowStyle = self.lines[rowNo % styleCount] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + + if hasattr(rowStyle, 'strokeWidth'): + width = rowStyle.strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + width = self.lines.strokeWidth + else: + width = None + + # Iterate over data columns. + if self.joinedLines: + points = [] + for xy in row: + points = points + [xy[0], xy[1]] + if inFill: + fpoints = [inFillX0,inFillY] + points + [inFillX1,inFillY] + filler = getattr(rowStyle, 'filler', None) + if filler: + filler.fill(self,inFillG,rowNo,rowColor,fpoints) + else: + inFillG.add(Polygon(fpoints,fillColor=rowColor,strokeColor=rowColor,strokeWidth=width or 0.1)) + if inFill in (None,0,2): + line = PolyLine(points,strokeColor=rowColor,strokeLineCap=0,strokeLineJoin=1) + if width: + line.strokeWidth = width + if dash: + line.strokeDashArray = dash + g.add(line) + + if hasattr(rowStyle, 'symbol'): + uSymbol = rowStyle.symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + j = -1 + if bubblePlot: drow = self.data[rowNo] + for xy in row: + j += 1 + symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor) + if symbol: + if bubblePlot: + symbol.size = bubbleR*(drow[j][2]/bubbleMax)**0.5 + g.add(symbol) + + # Draw data labels. + for colNo in range(len(row)): + x1, y1 = row[colNo] + self.drawLabel(g, rowNo, colNo, x1, y1) + + shader = getattr(rowStyle, 'shader', None) + if shader: shader.shade(self,g,rowNo,rowColor,row) + + return g + + def draw(self): + yA = self.yValueAxis + xA = self.xValueAxis + if getattr(self,'_bubblePlot',None): + yA._bubblePlot = xA._bubblePlot = 1 + yA.setPosition(self.x, self.y, self.height) + if yA: yA.joinAxis = xA + if xA: xA.joinAxis = yA + yA.configure(self.data) + + # if zero is in chart, put x axis there, otherwise use bottom. + xAxisCrossesAt = yA.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + xA.setPosition(self.x, y, self.width) + xA.configure(self.data) + self.calcPositions() + g = Group() + g.add(self.makeBackground()) + if self._inFill: + xA._joinToAxis() + self._inFillG = Group() + g.add(self._inFillG) + g.add(xA) + g.add(yA) + yA.gridStart = xA._x + yA.gridEnd = xA._x+xA._length + xA.gridStart = yA._y + xA.gridEnd = yA._y+yA._length + xA.makeGrid(g,parent=self) + yA.makeGrid(g,parent=self) + g.add(self.makeLines()) + for a in getattr(self,'annotations',()): g.add(a(self,xA.scale,yA.scale)) + return g + +class LinePlot3D(LinePlot): + _attrMap = AttrMap(BASE=LinePlot, + theta_x = AttrMapValue(isNumber, desc='dx/dz'), + theta_y = AttrMapValue(isNumber, desc='dy/dz'), + zDepth = AttrMapValue(isNumber, desc='depth of an individual series'), + zSpace = AttrMapValue(isNumber, desc='z gap around series'), + ) + theta_x = .5 + theta_y = .5 + zDepth = 10 + zSpace = 3 + + def calcPositions(self): + LinePlot.calcPositions(self) + nSeries = self._seriesCount + zSpace = self.zSpace + zDepth = self.zDepth + if self.xValueAxis.style=='parallel_3d': + _3d_depth = nSeries*zDepth+(nSeries+1)*zSpace + else: + _3d_depth = zDepth + 2*zSpace + self._3d_dx = self.theta_x*_3d_depth + self._3d_dy = self.theta_y*_3d_depth + + def _calc_z0(self,rowNo): + zSpace = self.zSpace + if self.xValueAxis.style=='parallel_3d': + z0 = rowNo*(self.zDepth+zSpace)+zSpace + else: + z0 = zSpace + return z0 + + def _zadjust(self,x,y,z): + return x+z*self.theta_x, y+z*self.theta_y + + def makeLines(self): + bubblePlot = getattr(self,'_bubblePlot',None) + assert not bubblePlot, "_bubblePlot not supported for 3d yet" + #if bubblePlot: + # yA = self.yValueAxis + # xA = self.xValueAxis + # bubbleR = min(yA._bubbleRadius,xA._bubbleRadius) + # bubbleMax = xA._bubbleMax + + labelFmt = self.lineLabelFormat + positions = self._positions + + P = range(len(positions)) + if self.reversePlotOrder: P.reverse() + inFill = getattr(self,'_inFill',None) + assert not inFill, "inFill not supported for 3d yet" + #if inFill: + # inFillY = self.xValueAxis._y + # inFillX0 = self.yValueAxis._x + # inFillX1 = inFillX0 + self.xValueAxis._length + # inFillG = getattr(self,'_inFillG',g) + zDepth = self.zDepth + _zadjust = self._zadjust + theta_x = self.theta_x + theta_y = self.theta_y + from linecharts import _FakeGroup + F = _FakeGroup() + + from utils3d import _make_3d_line_info, find_intersections + if self.xValueAxis.style!='parallel_3d': + tileWidth = getattr(self,'_3d_tilewidth',1) + if getattr(self,'_find_intersections',None): + from copy import copy + fpositions = map(copy,positions) + I = find_intersections(fpositions,small=tileWidth) + ic = None + for i,j,x,y in I: + if ic!=i: + ic = i + jc = 0 + else: + jc+=1 + fpositions[i].insert(j+jc,(x,y)) + tileWidth = None + else: + fpositions = positions + else: + tileWidth = None + fpositions = positions + + # Iterate over data rows. + styleCount = len(self.lines) + for rowNo in P: + row = positions[rowNo] + n = len(row) + rowStyle = self.lines[rowNo % styleCount] + rowColor = rowStyle.strokeColor + dash = getattr(rowStyle, 'strokeDashArray', None) + z0 = self._calc_z0(rowNo) + z1 = z0 + zDepth + + if hasattr(rowStyle, 'strokeWidth'): + width = rowStyle.strokeWidth + elif hasattr(self.lines, 'strokeWidth'): + width = self.lines.strokeWidth + else: + width = None + + # Iterate over data columns. + if self.joinedLines: + if n: + frow = fpositions[rowNo] + x0, y0 = frow[0] + for colNo in xrange(1,len(frow)): + x1, y1 = frow[colNo] + _make_3d_line_info( F, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + rowColor, fillColorShaded=None, tileWidth=tileWidth, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1) + x0, y0 = x1, y1 + + if hasattr(rowStyle, 'symbol'): + uSymbol = rowStyle.symbol + elif hasattr(self.lines, 'symbol'): + uSymbol = self.lines.symbol + else: + uSymbol = None + + if uSymbol: + for xy in row: + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + symbol = uSymbol2Symbol(uSymbol,xy[0],xy[1],rowColor) + if symbol: F.add((1,z0,z0,x1,y1,symbol)) + + # Draw data labels. + for colNo in xrange(n): + x1, y1 = row[colNo] + x1, y1 = _zadjust(x1,y1,z0) + L = self._innerDrawLabel(rowNo, colNo, x1, y1) + if L: F.add((2,z0,z0,x1,y1,L)) + + F.sort() + g = Group() + map(lambda x,a=g.add: a(x[-1]),F.value()) + return g + +_monthlyIndexData = [[(19971202, 100.0), + (19971231, 100.1704367), + (19980131, 101.5639577), + (19980228, 102.1879927), + (19980331, 101.6337257), + (19980430, 102.7640446), + (19980531, 102.9198038), + (19980630, 103.25938789999999), + (19980731, 103.2516421), + (19980831, 105.4744329), + (19980930, 109.3242705), + (19981031, 111.9859291), + (19981130, 110.9184642), + (19981231, 110.9184642), + (19990131, 111.9882532), + (19990228, 109.7912614), + (19990331, 110.24189629999999), + (19990430, 110.4279321), + (19990531, 109.33955469999999), + (19990630, 108.2341748), + (19990731, 110.21294469999999), + (19990831, 110.9683062), + (19990930, 112.4425371), + (19991031, 112.7314032), + (19991130, 112.3509645), + (19991231, 112.3660659), + (20000131, 110.9255248), + (20000229, 110.5266306), + (20000331, 113.3116101), + (20000430, 111.0449133), + (20000531, 111.702717), + (20000630, 113.5832178)], + [(19971202, 100.0), + (19971231, 100.0), + (19980131, 100.8), + (19980228, 102.0), + (19980331, 101.9), + (19980430, 103.0), + (19980531, 103.0), + (19980630, 103.1), + (19980731, 103.1), + (19980831, 102.8), + (19980930, 105.6), + (19981031, 108.3), + (19981130, 108.1), + (19981231, 111.9), + (19990131, 113.1), + (19990228, 110.2), + (19990331, 111.8), + (19990430, 112.3), + (19990531, 110.1), + (19990630, 109.3), + (19990731, 111.2), + (19990831, 111.7), + (19990930, 112.6), + (19991031, 113.2), + (19991130, 113.9), + (19991231, 115.4), + (20000131, 112.7), + (20000229, 113.9), + (20000331, 115.8), + (20000430, 112.2), + (20000531, 112.6), + (20000630, 114.6)]] + +class GridLinePlot(LinePlot): + """A customized version of LinePlot. + It uses NormalDateXValueAxis() and AdjYValueAxis() for the X and Y axes. + The chart has a default grid background with thin horizontal lines + aligned with the tickmarks (and labels). You can change the back- + ground to be any Grid or ShadedRect, or scale the whole chart. + If you do provide a background, you can specify the colours of the + stripes with 'background.stripeColors'. + """ + + _attrMap = AttrMap(BASE=LinePlot, + background = AttrMapValue(None, desc='Background for chart area (now Grid or ShadedRect).'), + scaleFactor = AttrMapValue(isNumberOrNone, desc='Scalefactor to apply to whole drawing.'), + ) + + def __init__(self): + from reportlab.lib import colors + LinePlot.__init__(self) + self.xValueAxis = NormalDateXValueAxis() + self.yValueAxis = AdjYValueAxis() + self.scaleFactor = None + self.background = Grid() + self.background.orientation = 'horizontal' + self.background.useRects = 0 + self.background.useLines = 1 + self.background.strokeWidth = 0.5 + self.background.strokeColor = colors.black + self.data = _monthlyIndexData + + def demo(self,drawing=None): + from reportlab.lib import colors + if not drawing: + drawing = Drawing(400, 200) + lp = AdjLinePlot() + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = _monthlyIndexData + lp.joinedLines = 1 + lp.strokeColor = colors.black + c0 = colors.PCMYKColor(100,65,0,30, spotName='PANTONE 288 CV', density=100) + lp.lines[0].strokeColor = c0 + lp.lines[0].strokeWidth = 2 + lp.lines[0].strokeDashArray = None + c1 = colors.PCMYKColor(0,79,91,0, spotName='PANTONE Wm Red CV', density=100) + lp.lines[1].strokeColor = c1 + lp.lines[1].strokeWidth = 1 + lp.lines[1].strokeDashArray = [3,1] + lp.xValueAxis.labels.fontSize = 10 + lp.xValueAxis.labels.textAnchor = 'start' + lp.xValueAxis.labels.boxAnchor = 'w' + lp.xValueAxis.labels.angle = -45 + lp.xValueAxis.labels.dx = 0 + lp.xValueAxis.labels.dy = -8 + lp.xValueAxis.xLabelFormat = '{mm}/{yy}' + lp.yValueAxis.labelTextFormat = '%5d%% ' + lp.yValueAxis.tickLeft = 5 + lp.yValueAxis.labels.fontSize = 10 + lp.background = Grid() + lp.background.stripeColors = [colors.pink, colors.lightblue] + lp.background.orientation = 'vertical' + drawing.add(lp,'plot') + return drawing + + def draw(self): + xva, yva = self.xValueAxis, self.yValueAxis + if xva: xva.joinAxis = yva + if yva: yva.joinAxis = xva + + yva.setPosition(self.x, self.y, self.height) + yva.configure(self.data) + + # if zero is in chart, put x axis there, otherwise + # use bottom. + xAxisCrossesAt = yva.scale(0) + if ((xAxisCrossesAt > self.y + self.height) or (xAxisCrossesAt < self.y)): + y = self.y + else: + y = xAxisCrossesAt + + xva.setPosition(self.x, y, self.width) + xva.configure(self.data) + + back = self.background + if isinstance(back, Grid): + if back.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.deltaSteps = steps + elif back.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.deltaSteps = steps + elif isinstance(back, DoubleGrid): + # Ideally, these lines would not be needed... + back.grid0.x = self.x + back.grid0.y = self.y + back.grid0.width = self.width + back.grid0.height = self.height + back.grid1.x = self.x + back.grid1.y = self.y + back.grid1.width = self.width + back.grid1.height = self.height + + # some room left for optimization... + if back.grid0.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.grid0.deltaSteps = steps + elif back.grid0.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.grid0.deltaSteps = steps + if back.grid1.orientation == 'vertical' and xva._tickValues: + xpos = map(xva.scale, [xva._valueMin] + xva._tickValues) + steps = [] + for i in range(len(xpos)-1): + steps.append(xpos[i+1] - xpos[i]) + back.grid1.deltaSteps = steps + elif back.grid1.orientation == 'horizontal' and yva._tickValues: + ypos = map(yva.scale, [yva._valueMin] + yva._tickValues) + steps = [] + for i in range(len(ypos)-1): + steps.append(ypos[i+1] - ypos[i]) + back.grid1.deltaSteps = steps + + self.calcPositions() + + width, height, scaleFactor = self.width, self.height, self.scaleFactor + if scaleFactor and scaleFactor!=1: + #g = Drawing(scaleFactor*width, scaleFactor*height) + g.transform = (scaleFactor, 0, 0, scaleFactor,0,0) + else: + g = Group() + + g.add(self.makeBackground()) + g.add(self.xValueAxis) + g.add(self.yValueAxis) + g.add(self.makeLines()) + + return g + +class AreaLinePlot(LinePlot): + '''we're given data in the form [(X1,Y11,..Y1M)....(Xn,Yn1,...YnM)]'''#' + def __init__(self): + LinePlot.__init__(self) + self._inFill = 1 + self.reversePlotOrder = 1 + self.data = [(1,20,100,30),(2,11,50,15),(3,15,70,40)] + + def draw(self): + try: + odata = self.data + n = len(odata) + m = len(odata[0]) + S = n*[0] + self.data = [] + for i in xrange(1,m): + D = [] + for j in xrange(n): + S[j] = S[j] + odata[j][i] + D.append((odata[j][0],S[j])) + self.data.append(D) + return LinePlot.draw(self) + finally: + self.data = odata + +class SplitLinePlot(AreaLinePlot): + def __init__(self): + AreaLinePlot.__init__(self) + self.xValueAxis = NormalDateXValueAxis() + self.yValueAxis = AdjYValueAxis() + self.data=[(20030601,0.95,0.05,0.0),(20030701,0.95,0.05,0.0),(20030801,0.95,0.05,0.0),(20030901,0.95,0.05,0.0),(20031001,0.95,0.05,0.0),(20031101,0.95,0.05,0.0),(20031201,0.95,0.05,0.0),(20040101,0.95,0.05,0.0),(20040201,0.95,0.05,0.0),(20040301,0.95,0.05,0.0),(20040401,0.95,0.05,0.0),(20040501,0.95,0.05,0.0),(20040601,0.95,0.05,0.0),(20040701,0.95,0.05,0.0),(20040801,0.95,0.05,0.0),(20040901,0.95,0.05,0.0),(20041001,0.95,0.05,0.0),(20041101,0.95,0.05,0.0),(20041201,0.95,0.05,0.0),(20050101,0.95,0.05,0.0),(20050201,0.95,0.05,0.0),(20050301,0.95,0.05,0.0),(20050401,0.95,0.05,0.0),(20050501,0.95,0.05,0.0),(20050601,0.95,0.05,0.0),(20050701,0.95,0.05,0.0),(20050801,0.95,0.05,0.0),(20050901,0.95,0.05,0.0),(20051001,0.95,0.05,0.0),(20051101,0.95,0.05,0.0),(20051201,0.95,0.05,0.0),(20060101,0.95,0.05,0.0),(20060201,0.95,0.05,0.0),(20060301,0.95,0.05,0.0),(20060401,0.95,0.05,0.0),(20060501,0.95,0.05,0.0),(20060601,0.95,0.05,0.0),(20060701,0.95,0.05,0.0),(20060801,0.95,0.05,0.0),(20060901,0.95,0.05,0.0),(20061001,0.95,0.05,0.0),(20061101,0.95,0.05,0.0),(20061201,0.95,0.05,0.0),(20070101,0.95,0.05,0.0),(20070201,0.95,0.05,0.0),(20070301,0.95,0.05,0.0),(20070401,0.95,0.05,0.0),(20070501,0.95,0.05,0.0),(20070601,0.95,0.05,0.0),(20070701,0.95,0.05,0.0),(20070801,0.95,0.05,0.0),(20070901,0.95,0.05,0.0),(20071001,0.95,0.05,0.0),(20071101,0.95,0.05,0.0),(20071201,0.95,0.05,0.0),(20080101,0.95,0.05,0.0),(20080201,0.95,0.05,0.0),(20080301,0.95,0.05,0.0),(20080401,0.95,0.05,0.0),(20080501,0.95,0.05,0.0),(20080601,0.95,0.05,0.0),(20080701,0.95,0.05,0.0),(20080801,0.95,0.05,0.0),(20080901,0.95,0.05,0.0),(20081001,0.95,0.05,0.0),(20081101,0.95,0.05,0.0),(20081201,0.95,0.05,0.0),(20090101,0.95,0.05,0.0),(20090201,0.91,0.09,0.0),(20090301,0.91,0.09,0.0),(20090401,0.91,0.09,0.0),(20090501,0.91,0.09,0.0),(20090601,0.91,0.09,0.0),(20090701,0.91,0.09,0.0),(20090801,0.91,0.09,0.0),(20090901,0.91,0.09,0.0),(20091001,0.91,0.09,0.0),(20091101,0.91,0.09,0.0),(20091201,0.91,0.09,0.0),(20100101,0.91,0.09,0.0),(20100201,0.81,0.19,0.0),(20100301,0.81,0.19,0.0),(20100401,0.81,0.19,0.0),(20100501,0.81,0.19,0.0),(20100601,0.81,0.19,0.0),(20100701,0.81,0.19,0.0),(20100801,0.81,0.19,0.0),(20100901,0.81,0.19,0.0),(20101001,0.81,0.19,0.0),(20101101,0.81,0.19,0.0),(20101201,0.81,0.19,0.0),(20110101,0.81,0.19,0.0),(20110201,0.72,0.28,0.0),(20110301,0.72,0.28,0.0),(20110401,0.72,0.28,0.0),(20110501,0.72,0.28,0.0),(20110601,0.72,0.28,0.0),(20110701,0.72,0.28,0.0),(20110801,0.72,0.28,0.0),(20110901,0.72,0.28,0.0),(20111001,0.72,0.28,0.0),(20111101,0.72,0.28,0.0),(20111201,0.72,0.28,0.0),(20120101,0.72,0.28,0.0),(20120201,0.53,0.47,0.0),(20120301,0.53,0.47,0.0),(20120401,0.53,0.47,0.0),(20120501,0.53,0.47,0.0),(20120601,0.53,0.47,0.0),(20120701,0.53,0.47,0.0),(20120801,0.53,0.47,0.0),(20120901,0.53,0.47,0.0),(20121001,0.53,0.47,0.0),(20121101,0.53,0.47,0.0),(20121201,0.53,0.47,0.0),(20130101,0.53,0.47,0.0),(20130201,0.44,0.56,0.0),(20130301,0.44,0.56,0.0),(20130401,0.44,0.56,0.0),(20130501,0.44,0.56,0.0),(20130601,0.44,0.56,0.0),(20130701,0.44,0.56,0.0),(20130801,0.44,0.56,0.0),(20130901,0.44,0.56,0.0),(20131001,0.44,0.56,0.0),(20131101,0.44,0.56,0.0),(20131201,0.44,0.56,0.0),(20140101,0.44,0.56,0.0),(20140201,0.36,0.5,0.14),(20140301,0.36,0.5,0.14),(20140401,0.36,0.5,0.14),(20140501,0.36,0.5,0.14),(20140601,0.36,0.5,0.14),(20140701,0.36,0.5,0.14),(20140801,0.36,0.5,0.14),(20140901,0.36,0.5,0.14),(20141001,0.36,0.5,0.14),(20141101,0.36,0.5,0.14),(20141201,0.36,0.5,0.14),(20150101,0.36,0.5,0.14),(20150201,0.3,0.41,0.29),(20150301,0.3,0.41,0.29),(20150401,0.3,0.41,0.29),(20150501,0.3,0.41,0.29),(20150601,0.3,0.41,0.29),(20150701,0.3,0.41,0.29),(20150801,0.3,0.41,0.29),(20150901,0.3,0.41,0.29),(20151001,0.3,0.41,0.29),(20151101,0.3,0.41,0.29),(20151201,0.3,0.41,0.29),(20160101,0.3,0.41,0.29),(20160201,0.26,0.36,0.38),(20160301,0.26,0.36,0.38),(20160401,0.26,0.36,0.38),(20160501,0.26,0.36,0.38),(20160601,0.26,0.36,0.38),(20160701,0.26,0.36,0.38),(20160801,0.26,0.36,0.38),(20160901,0.26,0.36,0.38),(20161001,0.26,0.36,0.38),(20161101,0.26,0.36,0.38),(20161201,0.26,0.36,0.38),(20170101,0.26,0.36,0.38),(20170201,0.2,0.3,0.5),(20170301,0.2,0.3,0.5),(20170401,0.2,0.3,0.5),(20170501,0.2,0.3,0.5),(20170601,0.2,0.3,0.5),(20170701,0.2,0.3,0.5),(20170801,0.2,0.3,0.5),(20170901,0.2,0.3,0.5),(20171001,0.2,0.3,0.5),(20171101,0.2,0.3,0.5),(20171201,0.2,0.3,0.5),(20180101,0.2,0.3,0.5),(20180201,0.13,0.37,0.5),(20180301,0.13,0.37,0.5),(20180401,0.13,0.37,0.5),(20180501,0.13,0.37,0.5),(20180601,0.13,0.37,0.5),(20180701,0.13,0.37,0.5),(20180801,0.13,0.37,0.5),(20180901,0.13,0.37,0.5),(20181001,0.13,0.37,0.5),(20181101,0.13,0.37,0.5),(20181201,0.13,0.37,0.5),(20190101,0.13,0.37,0.5),(20190201,0.1,0.4,0.5),(20190301,0.1,0.4,0.5),(20190401,0.1,0.4,0.5),(20190501,0.1,0.4,0.5),(20190601,0.1,0.4,0.5),(20190701,0.1,0.4,0.5),(20190801,0.1,0.4,0.5),(20190901,0.1,0.4,0.5),(20191001,0.1,0.4,0.5),(20191101,0.1,0.4,0.5),(20191201,0.1,0.4,0.5),(20200101,0.1,0.4,0.5)] + self.yValueAxis.requiredRange = None + self.yValueAxis.leftAxisPercent = 0 + self.yValueAxis.leftAxisOrigShiftMin = 0 + self.yValueAxis.leftAxisOrigShiftIPC = 0 + self.lines[0].strokeColor = colors.toColor(0x0033cc) + self.lines[1].strokeColor = colors.toColor(0x99c3ff) + self.lines[2].strokeColor = colors.toColor(0xCC0033) + +def _maxWidth(T, fontName, fontSize): + '''return max stringWidth for the list of strings T''' + if type(T) not in (type(()),type([])): T = (T,) + T = filter(None,T) + return T and max(map(lambda t,sW=stringWidth,fN=fontName, fS=fontSize: sW(t,fN,fS),T)) or 0 + +class ScatterPlot(LinePlot): + """A scatter plot widget""" + + _attrMap = AttrMap(BASE=LinePlot, + width = AttrMapValue(isNumber, desc="Width of the area inside the axes"), + height = AttrMapValue(isNumber, desc="Height of the area inside the axes"), + outerBorderOn = AttrMapValue(isBoolean, desc="Is there an outer border (continuation of axes)"), + outerBorderColor = AttrMapValue(isColorOrNone, desc="Color of outer border (if any)"), + background = AttrMapValue(isColorOrNone, desc="Background color (if any)"), + labelOffset = AttrMapValue(isNumber, desc="Space between label and Axis (or other labels)"), + axisTickLengths = AttrMapValue(isNumber, desc="Lenth of the ticks on both axes"), + axisStrokeWidth = AttrMapValue(isNumber, desc="Stroke width for both axes"), + xLabel = AttrMapValue(isString, desc="Label for the whole X-Axis"), + yLabel = AttrMapValue(isString, desc="Label for the whole Y-Axis"), + data = AttrMapValue(isAnything, desc='Data points - a list of x/y tuples.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border of plot area.'), + fillColor = AttrMapValue(isColorOrNone, desc='Color used for background interior of plot area.'), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + ) + + def __init__(self): + LinePlot.__init__(self) + self.width = 142 + self.height = 77 + self.outerBorderOn = 1 + self.outerBorderColor = colors.black + self.background = None + + _labelOffset = 3 + _axisTickLengths = 2 + _axisStrokeWidth = 0.5 + + self.yValueAxis.valueMin = None + self.yValueAxis.valueMax = None + self.yValueAxis.valueStep = None + self.yValueAxis.labelTextFormat = '%s' + + self.xLabel="X Lable" + self.xValueAxis.labels.fontSize = 6 + + self.yLabel="Y Lable" + self.yValueAxis.labels.fontSize = 6 + + self.data =[((0.030, 62.73), + (0.074, 54.363), + (1.216, 17.964)), + + ((1.360, 11.621), + (1.387, 50.011), + (1.428, 68.953)), + + ((1.444, 86.888), + (1.754, 35.58), + (1.766, 36.05))] + + #values for lineplot + self.joinedLines = 0 + self.fillColor = self.background + + self.leftPadding=5 + self.rightPadding=10 + self.topPadding=5 + self.bottomPadding=5 + + self.x = self.leftPadding+_axisTickLengths+(_labelOffset*2) + self.x=self.x+_maxWidth(str(self.yValueAxis.valueMax), self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize) + self.y = self.bottomPadding+_axisTickLengths+_labelOffset+self.xValueAxis.labels.fontSize + + self.xValueAxis.labels.dy = -_labelOffset + self.xValueAxis.tickDown = _axisTickLengths + self.xValueAxis.strokeWidth = _axisStrokeWidth + self.xValueAxis.rangeRound='both' + self.yValueAxis.labels.dx = -_labelOffset + self.yValueAxis.tickLeft = _axisTickLengths + self.yValueAxis.strokeWidth = _axisStrokeWidth + self.yValueAxis.rangeRound='both' + + self.lineLabelFormat="%.2f" + self.lineLabels.fontSize = 5 + self.lineLabels.boxAnchor = 'e' + self.lineLabels.dx = -2 + self.lineLabelNudge = 0 + self.lines.symbol=makeMarker('FilledCircle',size=3) + self.lines[1].symbol=makeMarker('FilledDiamond',size=3) + self.lines[2].symbol=makeMarker('FilledSquare',size=3) + self.lines[2].strokeColor = colors.green + + def _getDrawingDimensions(self): + tx = self.leftPadding+self.yValueAxis.tickLeft+(self.yValueAxis.labels.dx*2)+self.xValueAxis.labels.fontSize + tx=tx+(5*_maxWidth(str(self.yValueAxis.valueMax), self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize)) + tx=tx+self.width+self.rightPadding + t=('%.2f%%'%self.xValueAxis.valueMax) + tx=tx+(_maxWidth(t, self.yValueAxis.labels.fontName, self.yValueAxis.labels.fontSize)) + ty = self.bottomPadding+self.xValueAxis.tickDown+(self.xValueAxis.labels.dy*2)+(self.xValueAxis.labels.fontSize*2) + ty=ty+self.yValueAxis.labels.fontSize+self.height+self.topPadding + #print (tx, ty) + return (tx,ty) + + def demo(self,drawing=None): + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + ascent=getFont(self.xValueAxis.labels.fontName).face.ascent + if ascent==0: + ascent=0.718 # default (from helvetica) + ascent=ascent*self.xValueAxis.labels.fontSize # normalize + + #basic LinePlot - does the Axes, Ticks etc + lp = LinePlot.draw(self) + + xLabel = self.xLabel + if xLabel: #Overall label for the X-axis + xl=Label() + xl.x = (self.x+self.width)/2.0 + xl.y = 0 + xl.fontName = self.xValueAxis.labels.fontName + xl.fontSize = self.xValueAxis.labels.fontSize + xl.setText(xLabel) + lp.add(xl) + + yLabel = self.yLabel + if yLabel: #Overall label for the Y-axis + yl=Label() + yl.angle = 90 + yl.x = 0 + yl.y = (self.y+self.height/2.0) + yl.fontName = self.yValueAxis.labels.fontName + yl.fontSize = self.yValueAxis.labels.fontSize + yl.setText(yLabel) + lp.add(yl) + + # do a bounding box - in the same style as the axes + if self.outerBorderOn: + lp.add(Rect(self.x, self.y, self.width, self.height, + strokeColor = self.outerBorderColor, + strokeWidth = self.yValueAxis.strokeWidth, + fillColor = None)) + + lp.shift(self.leftPadding, self.bottomPadding) + + return lp + +def sample1a(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.strokeColor = colors.black + + lp.lines.symbol = makeMarker('UK_Flag') + + lp.lines[0].strokeWidth = 2 + lp.lines[1].strokeWidth = 4 + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueStep = 1 + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + +def sample1b(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines.symbol = makeMarker('Circle') + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] + lp.xValueAxis.labelTextFormat = '%2.1f' + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueStep = 1 + + drawing.add(lp) + + return drawing + + +def sample1c(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + ((1,1), (2,2), (2.5,1), (3,3), (4,5)), + ((1,2), (2,3), (2.5,2), (3.5,5), (4,6)) + ] + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines[0].symbol = makeMarker('FilledCircle') + lp.lines[1].symbol = makeMarker('Circle') + lp.lineLabelFormat = '%2.0f' + lp.strokeColor = colors.black + + lp.xValueAxis.valueMin = 0 + lp.xValueAxis.valueMax = 5 + lp.xValueAxis.valueSteps = [1, 2, 2.5, 3, 4, 5] + lp.xValueAxis.labelTextFormat = '%2.1f' + + lp.yValueAxis.valueMin = 0 + lp.yValueAxis.valueMax = 7 + lp.yValueAxis.valueSteps = [1, 2, 3, 5, 6] + + drawing.add(lp) + + return drawing + + +def preprocessData(series): + "Convert date strings into seconds and multiply values by 100." + + return map(lambda x: (str2seconds(x[0]), x[1]*100), series) + + +def sample2(): + "A line plot with non-equidistant points in x-axis." + + drawing = Drawing(400, 200) + + data = [ + (('25/11/1991',1), + ('30/11/1991',1.000933333), + ('31/12/1991',1.0062), + ('31/01/1992',1.0112), + ('29/02/1992',1.0158), + ('31/03/1992',1.020733333), + ('30/04/1992',1.026133333), + ('31/05/1992',1.030266667), + ('30/06/1992',1.034466667), + ('31/07/1992',1.038733333), + ('31/08/1992',1.0422), + ('30/09/1992',1.045533333), + ('31/10/1992',1.049866667), + ('30/11/1992',1.054733333), + ('31/12/1992',1.061), + ), + ] + + data[0] = preprocessData(data[0]) + + lp = LinePlot() + + lp.x = 50 + lp.y = 50 + lp.height = 125 + lp.width = 300 + lp.data = data + lp.joinedLines = 1 + lp.lines.symbol = makeMarker('FilledDiamond') + lp.strokeColor = colors.black + + start = mktime(mkTimeTuple('25/11/1991')) + t0 = mktime(mkTimeTuple('30/11/1991')) + t1 = mktime(mkTimeTuple('31/12/1991')) + t2 = mktime(mkTimeTuple('31/03/1992')) + t3 = mktime(mkTimeTuple('30/06/1992')) + t4 = mktime(mkTimeTuple('30/09/1992')) + end = mktime(mkTimeTuple('31/12/1992')) + lp.xValueAxis.valueMin = start + lp.xValueAxis.valueMax = end + lp.xValueAxis.valueSteps = [start, t0, t1, t2, t3, t4, end] + lp.xValueAxis.labelTextFormat = seconds2str + lp.xValueAxis.labels[1].dy = -20 + lp.xValueAxis.labels[2].dy = -35 + + lp.yValueAxis.labelTextFormat = '%4.2f' + lp.yValueAxis.valueMin = 100 + lp.yValueAxis.valueMax = 110 + lp.yValueAxis.valueStep = 2 + + drawing.add(lp) + + return drawing diff --git a/bin/reportlab/graphics/charts/markers.py b/bin/reportlab/graphics/charts/markers.py new file mode 100644 index 00000000000..cb3493cb36b --- /dev/null +++ b/bin/reportlab/graphics/charts/markers.py @@ -0,0 +1,81 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/markers.py +""" +This modules defines a collection of markers used in charts. + +The make* functions return a simple shape or a widget as for +the smiley. +""" +__version__=''' $Id: markers.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from reportlab.lib import colors +from reportlab.graphics.shapes import Rect, Line, Circle, Polygon +from reportlab.graphics.widgets.signsandsymbols import SmileyFace + + +def makeEmptySquare(x, y, size, color): + "Make an empty square marker." + + d = size/2.0 + rect = Rect(x-d, y-d, 2*d, 2*d) + rect.strokeColor = color + rect.fillColor = None + + return rect + + +def makeFilledSquare(x, y, size, color): + "Make a filled square marker." + + d = size/2.0 + rect = Rect(x-d, y-d, 2*d, 2*d) + rect.strokeColor = color + rect.fillColor = color + + return rect + + +def makeFilledDiamond(x, y, size, color): + "Make a filled diamond marker." + + d = size/2.0 + poly = Polygon((x-d,y, x,y+d, x+d,y, x,y-d)) + poly.strokeColor = color + poly.fillColor = color + + return poly + + +def makeEmptyCircle(x, y, size, color): + "Make a hollow circle marker." + + d = size/2.0 + circle = Circle(x, y, d) + circle.strokeColor = color + circle.fillColor = colors.white + + return circle + + +def makeFilledCircle(x, y, size, color): + "Make a hollow circle marker." + + d = size/2.0 + circle = Circle(x, y, d) + circle.strokeColor = color + circle.fillColor = color + + return circle + + +def makeSmiley(x, y, size, color): + "Make a smiley marker." + + d = size + s = SmileyFace() + s.fillColor = color + s.x = x-d + s.y = y-d + s.size = d*2 + + return s \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/piecharts.py b/bin/reportlab/graphics/charts/piecharts.py new file mode 100644 index 00000000000..f8646086b8e --- /dev/null +++ b/bin/reportlab/graphics/charts/piecharts.py @@ -0,0 +1,1279 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/piecharts.py +# experimental pie chart script. Two types of pie - one is a monolithic +#widget with all top-level properties, the other delegates most stuff to +#a wedges collection whic lets you customize the group or every individual +#wedge. + +"""Basic Pie Chart class. + +This permits you to customize and pop out individual wedges; +supports elliptical and circular pies. +""" +__version__=''' $Id: piecharts.py 2743 2005-12-12 15:51:29Z rgbecker $ ''' + +import copy +from math import sin, cos, pi + +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors, isNumberOrNone,\ + isNoneOrListOfNoneOrStrings, isTextAnchor,\ + isNoneOrListOfNoneOrNumbers, isBoxAnchor,\ + isStringOrNone, NoneOr +from reportlab.graphics.widgets.markers import uSymbol2Symbol, isSymbol +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, ArcPath, Polygon, Rect, PolyLine +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.areas import PlotArea +from textlabels import Label + +_ANGLE2BOXANCHOR={0:'w', 45:'sw', 90:'s', 135:'se', 180:'e', 225:'ne', 270:'n', 315: 'nw', -45: 'nw'} +_ANGLE2RBOXANCHOR={0:'e', 45:'ne', 90:'n', 135:'nw', 180:'w', 225:'sw', 270:'s', 315: 'se', -45: 'se'} +class WedgeLabel(Label): + def _checkDXY(self,ba): + pass + def _getBoxAnchor(self): + na = (int((self._pmv%360)/45.)*45)%360 + if not (na % 90): # we have a right angle case + da = (self._pmv - na) % 360 + if abs(da)>5: + na = na + (da>0 and 45 or -45) + ba = (getattr(self,'_anti',None) and _ANGLE2RBOXANCHOR or _ANGLE2BOXANCHOR)[na] + self._checkDXY(ba) + return ba + +class WedgeProperties(PropHolder): + """This holds descriptive information about the wedges in a pie chart. + + It is not to be confused with the 'wedge itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Wedge object for you with its + format method. + """ + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + popout = AttrMapValue(isNumber), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + fontColor = AttrMapValue(isColorOrNone), + labelRadius = AttrMapValue(isNumber), + label_dx = AttrMapValue(isNumber), + label_dy = AttrMapValue(isNumber), + label_angle = AttrMapValue(isNumber), + label_boxAnchor = AttrMapValue(isBoxAnchor), + label_boxStrokeColor = AttrMapValue(isColorOrNone), + label_boxStrokeWidth = AttrMapValue(isNumber), + label_boxFillColor = AttrMapValue(isColorOrNone), + label_strokeColor = AttrMapValue(isColorOrNone), + label_strokeWidth = AttrMapValue(isNumber), + label_text = AttrMapValue(isStringOrNone), + label_leading = AttrMapValue(isNumberOrNone), + label_width = AttrMapValue(isNumberOrNone), + label_maxWidth = AttrMapValue(isNumberOrNone), + label_height = AttrMapValue(isNumberOrNone), + label_textAnchor = AttrMapValue(isTextAnchor), + label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + label_topPadding = AttrMapValue(isNumber,'padding at top of box'), + label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), + label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), + label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + label_pointer_strokeColor = AttrMapValue(isColorOrNone,desc='Color of indicator line'), + label_pointer_strokeWidth = AttrMapValue(isNumber,desc='StrokeWidth of indicator line'), + label_pointer_elbowLength = AttrMapValue(isNumber,desc='length of final indicator line segment'), + label_pointer_edgePad = AttrMapValue(isNumber,desc='pad between pointer label and box'), + label_pointer_piePad = AttrMapValue(isNumber,desc='pad between pointer label and pie'), + swatchMarker = AttrMapValue(NoneOr(isSymbol), desc="None or makeMarker('Diamond') ..."), + ) + + def __init__(self): + self.strokeWidth = 0 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.popout = 0 + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.label_dx = self.label_dy = self.label_angle = 0 + self.label_text = None + self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 + self.label_boxAnchor = 'c' + self.label_boxStrokeColor = None #boxStroke + self.label_boxStrokeWidth = 0.5 #boxStrokeWidth + self.label_boxFillColor = None + self.label_strokeColor = None + self.label_strokeWidth = 0.1 + self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None + self.label_textAnchor = 'start' + self.label_visible = 1 + self.label_pointer_strokeColor = colors.black + self.label_pointer_strokeWidth = 0.5 + self.label_pointer_elbowLength = 3 + self.label_pointer_edgePad = 2 + self.label_pointer_piePad = 3 + +def _addWedgeLabel(self,text,add,angle,labelX,labelY,wedgeStyle,labelClass=WedgeLabel): + # now draw a label + if self.simpleLabels: + theLabel = String(labelX, labelY, text) + theLabel.textAnchor = "middle" + theLabel._pmv = angle + else: + theLabel = labelClass() + theLabel._pmv = angle + theLabel.x = labelX + theLabel.y = labelY + theLabel.dx = wedgeStyle.label_dx + theLabel.dy = wedgeStyle.label_dy + theLabel.angle = wedgeStyle.label_angle + theLabel.boxAnchor = wedgeStyle.label_boxAnchor + theLabel.boxStrokeColor = wedgeStyle.label_boxStrokeColor + theLabel.boxStrokeWidth = wedgeStyle.label_boxStrokeWidth + theLabel.boxFillColor = wedgeStyle.label_boxFillColor + theLabel.strokeColor = wedgeStyle.label_strokeColor + theLabel.strokeWidth = wedgeStyle.label_strokeWidth + _text = wedgeStyle.label_text + if _text is None: _text = text + theLabel._text = _text + theLabel.leading = wedgeStyle.label_leading + theLabel.width = wedgeStyle.label_width + theLabel.maxWidth = wedgeStyle.label_maxWidth + theLabel.height = wedgeStyle.label_height + theLabel.textAnchor = wedgeStyle.label_textAnchor + theLabel.visible = wedgeStyle.label_visible + theLabel.topPadding = wedgeStyle.label_topPadding + theLabel.leftPadding = wedgeStyle.label_leftPadding + theLabel.rightPadding = wedgeStyle.label_rightPadding + theLabel.bottomPadding = wedgeStyle.label_bottomPadding + theLabel.fontSize = wedgeStyle.fontSize + theLabel.fontName = wedgeStyle.fontName + theLabel.fillColor = wedgeStyle.fontColor + add(theLabel) + +def _fixLabels(labels,n): + if labels is None: + labels = [''] * n + else: + i = n-len(labels) + if i>0: labels = labels + ['']*i + return labels + +class AbstractPieChart(PlotArea): + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.slices + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None)) + fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None)) + strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',None)) + swatchMarker = getattr(style, 'swatchMarker', getattr(baseStyle, 'swatchMarker',None)) + if swatchMarker: + return uSymbol2Symbol(swatchMarker,x+width/2.,y+height/2.,fillColor) + return Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + try: + text = str(self.labels[i]) + except: + text = default + if not self.simpleLabels: + _text = getattr(self.slices[i],'label_text','') + if _text is not None: text = _text + return text + +def boundsOverlap(P,Q): + return not(P[0]>Q[2]-1e-2 or Q[0]>P[2]-1e-2 or P[1]>Q[3]-1e-2 or Q[1]>P[3]-1e-2) + +def _findOverlapRun(B,i,wrap): + '''find overlap run containing B[i]''' + n = len(B) + R = [i] + while 1: + i = R[-1] + j = (i+1)%n + if j in R or not boundsOverlap(B[i],B[j]): break + R.append(j) + while 1: + i = R[0] + j = (i-1)%n + if j in R or not boundsOverlap(B[i],B[j]): break + R.insert(0,j) + return R + +def findOverlapRun(B,wrap=1): + '''determine a set of overlaps in bounding boxes B or return None''' + n = len(B) + if n>1: + for i in xrange(n-1): + R = _findOverlapRun(B,i,wrap) + if len(R)>1: return R + return None + +def fixLabelOverlaps(L): + nL = len(L) + if nL<2: return + B = [l._origdata['bounds'] for l in L] + OK = 1 + RP = [] + iter = 0 + mult = 1. + + while iter<30: + R = findOverlapRun(B) + if not R: break + nR = len(R) + if nR==nL: break + if not [r for r in RP if r in R]: + mult = 1.0 + da = 0 + r0 = R[0] + rL = R[-1] + bi = B[r0] + taa = aa = _360(L[r0]._pmv) + for r in R[1:]: + b = B[r] + da = max(da,min(b[3]-bi[1],bi[3]-b[1])) + bi = b + aa += L[r]._pmv + aa = aa/float(nR) + utaa = abs(L[rL]._pmv-taa) + ntaa = _360(utaa) + da *= mult*(nR-1)/ntaa + + for r in R: + l = L[r] + orig = l._origdata + angle = l._pmv = _360(l._pmv+da*(_360(l._pmv)-aa)) + rad = angle/_180_pi + l.x = orig['cx'] + orig['rx']*cos(rad) + l.y = orig['cy'] + orig['ry']*sin(rad) + B[r] = l.getBounds() + RP = R + mult *= 1.05 + iter += 1 + +def intervalIntersection(A,B): + x,y = max(min(A),min(B)),min(max(A),max(B)) + if x>=y: return None + return x,y + +def _makeSideArcDefs(sa,direction): + sa %= 360 + if 90<=sa<270: + if direction=='clockwise': + a = (0,90,sa),(1,-90,90),(0,-360+sa,-90) + else: + a = (0,sa,270),(1,270,450),(0,450,360+sa) + else: + offs = sa>=270 and 360 or 0 + if direction=='clockwise': + a = (1,offs-90,sa),(0,offs-270,offs-90),(1,-360+sa,offs-270) + else: + a = (1,sa,offs+90),(0,offs+90,offs+270),(1,offs+270,360+sa) + return tuple([a for a in a if a[1]1: a.sort(lambda x,y: cmp(y[1]-y[0],x[1]-x[0])) + return a[0] + +def _fPLSide(l,width,side=None): + data = l._origdata + if side is None: + li = data['li'] + ri = data['ri'] + if li is None: + side = 1 + i = ri + elif ri is None: + side = 0 + i = li + elif li[1]-li[0]>ri[1]-ri[0]: + side = 0 + i = li + else: + side = 1 + i = ri + w = data['width'] + edgePad = data['edgePad'] + if not side: #on left + l._pmv = 180 + l.x = edgePad+w + i = data['li'] + else: + l._pmv = 0 + l.x = width - w - edgePad + i = data['ri'] + mid = data['mid'] = (i[0]+i[1])*0.5 + data['smid'] = sin(mid/_180_pi) + data['cmid'] = cos(mid/_180_pi) + data['side'] = side + return side,w + +def _fPLCF(a,b): + return cmp(b._origdata['smid'],a._origdata['smid']) + +def _arcCF(a,b): + return cmp(a[1],b[1]) + +def _fixPointerLabels(n,L,x,y,width,height,side=None): + LR = [],[] + mlr = [0,0] + for l in L: + i,w = _fPLSide(l,width,side) + LR[i].append(l) + mlr[i] = max(w,mlr[i]) + mul = 1 + G = n*[None] + mel = 0 + hh = height*0.5 + yhh = y+hh + m = max(mlr) + for i in (0,1): + T = LR[i] + if T: + B = [] + aB = B.append + S = [] + aS = S.append + T.sort(_fPLCF) + p = 0 + yh = y+height + for l in T: + data = l._origdata + inc = x+mul*(m-data['width']) + l.x += inc + G[data['index']] = l + ly = yhh+data['smid']*hh + b = data['bounds'] + b2 = (b[3]-b[1])*0.5 + if ly+b2>yh: ly = yh-b2 + if ly-b2sFree: break + yh = B[j0][3]+sAbove*sNeed/sFree + for r in R: + l = T[r] + data = l._origdata + b = data['bounds'] + b2 = (b[3]-b[1])*0.5 + yh -= 0.5 + ly = l.y = yh-b2 + B[r] = data['bounds'] = (b[0],ly-b2,b[2],yh) + yh = ly - b2 - 0.5 + mlr[i] = m+p + mul = -1 + return G, mlr[0], mlr[1], mel + +class Pie(AbstractPieChart): + _attrMap = AttrMap(BASE=AbstractPieChart, + data = AttrMapValue(isListOfNumbers, desc='list of numbers defining wedge sizes; need not sum to 1'), + labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), + startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), + direction = AttrMapValue(OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), + slices = AttrMapValue(None, desc="collection of wedge descriptor objects"), + simpleLabels = AttrMapValue(isBoolean, desc="If true(default) use String not super duper WedgeLabel"), + other_threshold = AttrMapValue(isNumber, desc='A value for doing threshholding, not used yet.'), + checkLabelOverlap = AttrMapValue(isBoolean, desc="If true check and attempt to fix standard label overlaps(default off)"), + pointerLabelMode = AttrMapValue(OneOf(None,'LeftRight','LeftAndRight'), desc=""), + sameRadii = AttrMapValue(isBoolean, desc="If true make x/y radii the same(default off)"), + orderMode = AttrMapValue(OneOf('fixed','alternate')), + xradius = AttrMapValue(isNumberOrNone, desc="X direction Radius"), + yradius = AttrMapValue(isNumberOrNone, desc="Y direction Radius"), + ) + other_threshold=None + + def __init__(self,**kwd): + PlotArea.__init__(self) + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.data = [1,2.3,1.7,4.2] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + self.checkLabelOverlap = 0 + self.pointerLabelMode = None + self.sameRadii = False + self.orderMode = 'fixed' + self.xradius = self.yradius = None + + self.slices = TypedPropertyCollection(WedgeProperties) + self.slices[0].fillColor = colors.darkcyan + self.slices[1].fillColor = colors.blueviolet + self.slices[2].fillColor = colors.blue + self.slices[3].fillColor = colors.cyan + self.slices[4].fillColor = colors.pink + self.slices[5].fillColor = colors.magenta + self.slices[6].fillColor = colors.yellow + + def demo(self): + d = Drawing(200, 100) + + pc = Pie() + pc.x = 50 + pc.y = 10 + pc.width = 100 + pc.height = 80 + pc.data = [10,20,30,40,50,60] + pc.labels = ['a','b','c','d','e','f'] + + pc.slices.strokeWidth=0.5 + pc.slices[3].popout = 10 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + pc.slices[0].fillColor = colors.darkcyan + pc.slices[1].fillColor = colors.blueviolet + pc.slices[2].fillColor = colors.blue + pc.slices[3].fillColor = colors.cyan + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + + d.add(pc) + return d + + def makePointerLabels(self,angles,plMode): + class PL: + def __init__(self,centerx,centery,xradius,yradius,data,lu=0,ru=0): + self.centerx = centerx + self.centery = centery + self.xradius = xradius + self.yradius = yradius + self.data = data + self.lu = lu + self.ru = ru + + labelX = self.width-2 + labelY = self.height + n = nr = nl = maxW = sumH = 0 + styleCount = len(self.slices) + L=[] + L_add = L.append + refArcs = _makeSideArcDefs(self.startAngle,self.direction) + for i, A in angles: + if A[1] is None: continue + sn = self.getSeriesName(i,'') + if not sn: continue + n += 1 + style = self.slices[i%styleCount] + _addWedgeLabel(self,sn,L_add,180,labelX,labelY,style,labelClass=WedgeLabel) + l = L[-1] + b = l.getBounds() + w = b[2]-b[0] + h = b[3]-b[1] + ri = [(a[0],intervalIntersection(A,(a[1],a[2]))) for a in refArcs] + li = _findLargestArc(ri,0) + ri = _findLargestArc(ri,1) + if li and ri: + if plMode=='LeftAndRight': + if li[1]-li[0]ri[1]-ri[0]: + ri = None + if ri: nr += 1 + if li: nl += 1 + l._origdata = dict(bounds=b,width=w,height=h,li=li,ri=ri,index=i,edgePad=style.label_pointer_edgePad,piePad=style.label_pointer_piePad,elbowLength=style.label_pointer_elbowLength) + maxW = max(w,maxW) + sumH += h+2 + + if not n: #we have no labels + xradius = self.width*0.5 + yradius = self.height*0.5 + centerx = self.x+xradius + centery = self.y+yradius + if self.xradius: xradius = self.xradius + if self.yradius: yradius = self.yradius + if self.sameRadii: xradius=yradius=min(xradius,yradius) + return PL(centerx,centery,xradius,yradius,[]) + + aonR = nr==n + if sumH=1e-8 and map(lambda x,f=360./sum: f*x, data) or len(data)*[0] + + def makeAngles(self): + startAngle = self.startAngle % 360 + whichWay = self.direction == "clockwise" and -1 or 1 + D = [a for a in enumerate(self.normalizeData())] + if self.orderMode=='alternate': + W = [a for a in D if abs(a[1])>=1e-5] + W.sort(_arcCF) + T = [[],[]] + i = 0 + while W: + if i<2: + a = W.pop(0) + else: + a = W.pop(-1) + T[i%2].append(a) + i += 1 + i %= 4 + T[1].reverse() + D = T[0]+T[1] + [a for a in D if abs(a[1])<1e-5] + A = [] + a = A.append + for i, angle in D: + endAngle = (startAngle + (angle * whichWay)) + if abs(angle)>=1e-5: + if startAngle >= endAngle: + aa = endAngle,startAngle + else: + aa = startAngle,endAngle + else: + aa = startAngle, None + startAngle = endAngle + a((i,aa)) + return A + + def makeWedges(self): + angles = self.makeAngles() + n = len(angles) + labels = _fixLabels(self.labels,n) + + self._seriesCount = n + styleCount = len(self.slices) + + plMode = self.pointerLabelMode + if plMode: + checkLabelOverlap = False + PL=self.makePointerLabels(angles,plMode) + xradius = PL.xradius + yradius = PL.yradius + centerx = PL.centerx + centery = PL.centery + PL_data = PL.data + gSN = lambda i: '' + else: + xradius = self.width*0.5 + yradius = self.height*0.5 + centerx = self.x + xradius + centery = self.y + yradius + if self.xradius: xradius = self.xradius + if self.yradius: yradius = self.yradius + if self.sameRadii: xradius=yradius=min(xradius,yradius) + checkLabelOverlap = self.checkLabelOverlap + gSN = lambda i: self.getSeriesName(i,'') + + g = Group() + g_add = g.add + if checkLabelOverlap: + L = [] + L_add = L.append + else: + L_add = g_add + + for i,(a1,a2) in angles: + if a2 is None: continue + #if we didn't use %stylecount here we'd end up with the later wedges + #all having the default style + wedgeStyle = self.slices[i%styleCount] + + # is it a popout? + cx, cy = centerx, centery + text = gSN(i) + popout = wedgeStyle.popout + if text or popout: + averageAngle = (a1+a2)/2.0 + aveAngleRadians = averageAngle/_180_pi + cosAA = cos(aveAngleRadians) + sinAA = sin(aveAngleRadians) + if popout: + # pop out the wedge + cx = centerx + popout*cosAA + cy = centery + popout*sinAA + + if n > 1: + theWedge = Wedge(cx, cy, xradius, a1, a2, yradius=yradius) + elif n==1: + theWedge = Ellipse(cx, cy, xradius, yradius) + + theWedge.fillColor = wedgeStyle.fillColor + theWedge.strokeColor = wedgeStyle.strokeColor + theWedge.strokeWidth = wedgeStyle.strokeWidth + theWedge.strokeDashArray = wedgeStyle.strokeDashArray + + g_add(theWedge) + if text: + labelRadius = wedgeStyle.labelRadius + rx = xradius*labelRadius + ry = yradius*labelRadius + labelX = cx + rx*cosAA + labelY = cy + ry*sinAA + _addWedgeLabel(self,text,L_add,averageAngle,labelX,labelY,wedgeStyle) + if checkLabelOverlap: + l = L[-1] + l._origdata = { 'x': labelX, 'y':labelY, 'angle': averageAngle, + 'rx': rx, 'ry':ry, 'cx':cx, 'cy':cy, + 'bounds': l.getBounds(), + } + elif plMode and PL_data: + l = PL_data[i] + if l: + data = l._origdata + sinM = data['smid'] + cosM = data['cmid'] + lX = cx + xradius*cosM + lY = cy + yradius*sinM + lpel = wedgeStyle.label_pointer_elbowLength + lXi = lX + lpel*cosM + lYi = lY + lpel*sinM + L_add(PolyLine((lX,lY,lXi,lYi,l.x,l.y), + strokeWidth=wedgeStyle.label_pointer_strokeWidth, + strokeColor=wedgeStyle.label_pointer_strokeColor)) + L_add(l) + + if checkLabelOverlap: + fixLabelOverlaps(L) + map(g_add,L) + + return g + + def draw(self): + G = self.makeBackground() + w = self.makeWedges() + if G: return Group(G,w) + return w + +class LegendedPie(Pie): + """Pie with a two part legend (one editable with swatches, one hidden without swatches).""" + + _attrMap = AttrMap(BASE=Pie, + drawLegend = AttrMapValue(isBoolean, desc="If true then create and draw legend"), + legend1 = AttrMapValue(None, desc="Handle to legend for pie"), + legendNumberFormat = AttrMapValue(None, desc="Formatting routine for number on right hand side of legend."), + legendNumberOffset = AttrMapValue(isNumber, desc="Horizontal space between legend and numbers on r/hand side"), + pieAndLegend_colors = AttrMapValue(isListOfColors, desc="Colours used for both swatches and pie"), + legend_names = AttrMapValue(isNoneOrListOfNoneOrStrings, desc="Names used in legend (or None)"), + legend_data = AttrMapValue(isNoneOrListOfNoneOrNumbers, desc="Numbers used on r/hand side of legend (or None)"), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + ) + + def __init__(self): + Pie.__init__(self) + self.x = 0 + self.y = 0 + self.height = 100 + self.width = 100 + self.data = [38.4, 20.7, 18.9, 15.4, 6.6] + self.labels = None + self.direction = 'clockwise' + PCMYKColor, black = colors.PCMYKColor, colors.black + self.pieAndLegend_colors = [PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV'), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV'), + PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=75), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=75), + PCMYKColor(11,11,72,0,spotName='PANTONE 458 CV',density=50), + PCMYKColor(100,65,0,30,spotName='PANTONE 288 CV',density=50)] + + #Allows us up to six 'wedges' to be coloured + self.slices[0].fillColor=self.pieAndLegend_colors[0] + self.slices[1].fillColor=self.pieAndLegend_colors[1] + self.slices[2].fillColor=self.pieAndLegend_colors[2] + self.slices[3].fillColor=self.pieAndLegend_colors[3] + self.slices[4].fillColor=self.pieAndLegend_colors[4] + self.slices[5].fillColor=self.pieAndLegend_colors[5] + + self.slices.strokeWidth = 0.75 + self.slices.strokeColor = black + + legendOffset = 17 + self.legendNumberOffset = 51 + self.legendNumberFormat = '%.1f%%' + self.legend_data = self.data + + #set up the legends + from reportlab.graphics.charts.legends import Legend + self.legend1 = Legend() + self.legend1.x = self.width+legendOffset + self.legend1.y = self.height + self.legend1.deltax = 5.67 + self.legend1.deltay = 14.17 + self.legend1.dxTextSpace = 11.39 + self.legend1.dx = 5.67 + self.legend1.dy = 5.67 + self.legend1.columnMaximum = 7 + self.legend1.alignment = 'right' + self.legend_names = ['AAA:','AA:','A:','BBB:','NR:'] + for f in range(0,len(self.data)): + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) + self.legend1.fontName = "Helvetica-Bold" + self.legend1.fontSize = 6 + self.legend1.strokeColor = black + self.legend1.strokeWidth = 0.5 + + self._legend2 = Legend() + self._legend2.dxTextSpace = 0 + self._legend2.dx = 0 + self._legend2.alignment = 'right' + self._legend2.fontName = "Helvetica-Oblique" + self._legend2.fontSize = 6 + self._legend2.strokeColor = self.legend1.strokeColor + + self.leftPadding = 5 + self.rightPadding = 5 + self.topPadding = 5 + self.bottomPadding = 5 + self.drawLegend = 1 + + def draw(self): + if self.drawLegend: + self.legend1.colorNamePairs = [] + self._legend2.colorNamePairs = [] + for f in range(0,len(self.data)): + if self.legend_names == None: + self.slices[f].fillColor = self.pieAndLegend_colors[f] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], None)) + else: + try: + self.slices[f].fillColor = self.pieAndLegend_colors[f] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f], self.legend_names[f])) + except IndexError: + self.slices[f].fillColor = self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)] + self.legend1.colorNamePairs.append((self.pieAndLegend_colors[f%len(self.pieAndLegend_colors)], self.legend_names[f])) + if self.legend_data != None: + ldf = self.legend_data[f] + lNF = self.legendNumberFormat + from types import StringType + if ldf is None or lNF is None: + pass + elif type(lNF) is StringType: + ldf = lNF % ldf + elif callable(lNF): + ldf = lNF(ldf) + else: + p = self.legend_names[f] + if self.legend_data != None: + ldf = self.legend_data[f] + lNF = self.legendNumberFormat + if ldf is None or lNF is None: + pass + elif type(lNF) is StringType: + ldf = lNF % ldf + elif callable(lNF): + ldf = lNF(ldf) + else: + msg = "Unknown formatter type %s, expected string or function" % self.legendNumberFormat + raise Exception, msg + self._legend2.colorNamePairs.append((None,ldf)) + p = Pie.draw(self) + if self.drawLegend: + p.add(self.legend1) + #hide from user - keeps both sides lined up! + self._legend2.x = self.legend1.x+self.legendNumberOffset + self._legend2.y = self.legend1.y + self._legend2.deltax = self.legend1.deltax + self._legend2.deltay = self.legend1.deltay + self._legend2.dy = self.legend1.dy + self._legend2.columnMaximum = self.legend1.columnMaximum + p.add(self._legend2) + p.shift(self.leftPadding, self.bottomPadding) + return p + + def _getDrawingDimensions(self): + tx = self.rightPadding + if self.drawLegend: + tx = tx+self.legend1.x+self.legendNumberOffset #self._legend2.x + tx = tx + self._legend2._calculateMaxWidth(self._legend2.colorNamePairs) + ty = self.bottomPadding+self.height+self.topPadding + return (tx,ty) + + def demo(self, drawing=None): + if not drawing: + tx,ty = self._getDrawingDimensions() + drawing = Drawing(tx, ty) + drawing.add(self.draw()) + return drawing + +from utils3d import _getShaded, _2rad, _360, _pi_2, _2pi, _180_pi +class Wedge3dProperties(PropHolder): + """This holds descriptive information about the wedges in a pie chart. + + It is not to be confused with the 'wedge itself'; this just holds + a recipe for how to format one, and does not allow you to hack the + angles. It can format a genuine Wedge object for you with its + format method. + """ + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone), + fillColorShaded = AttrMapValue(isColorOrNone), + fontColor = AttrMapValue(isColorOrNone), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + label_angle = AttrMapValue(isNumber), + label_bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + label_boxAnchor = AttrMapValue(isBoxAnchor), + label_boxFillColor = AttrMapValue(isColorOrNone), + label_boxStrokeColor = AttrMapValue(isColorOrNone), + label_boxStrokeWidth = AttrMapValue(isNumber), + label_dx = AttrMapValue(isNumber), + label_dy = AttrMapValue(isNumber), + label_height = AttrMapValue(isNumberOrNone), + label_leading = AttrMapValue(isNumberOrNone), + label_leftPadding = AttrMapValue(isNumber,'padding at left of box'), + label_maxWidth = AttrMapValue(isNumberOrNone), + label_rightPadding = AttrMapValue(isNumber,'padding at right of box'), + label_strokeColor = AttrMapValue(isColorOrNone), + label_strokeWidth = AttrMapValue(isNumber), + label_text = AttrMapValue(isStringOrNone), + label_textAnchor = AttrMapValue(isTextAnchor), + label_topPadding = AttrMapValue(isNumber,'padding at top of box'), + label_visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + label_width = AttrMapValue(isNumberOrNone), + labelRadius = AttrMapValue(isNumber), + popout = AttrMapValue(isNumber), + shading = AttrMapValue(isNumber), + strokeColor = AttrMapValue(isColorOrNone), + strokeColorShaded = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + strokeWidth = AttrMapValue(isNumber), + visible = AttrMapValue(isBoolean,'set to false to skip displaying'), + ) + + def __init__(self): + self.strokeWidth = 0 + self.shading = 0.3 + self.visible = 1 + self.strokeColorShaded = self.fillColorShaded = self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.popout = 0 + self.fontName = STATE_DEFAULTS["fontName"] + self.fontSize = STATE_DEFAULTS["fontSize"] + self.fontColor = STATE_DEFAULTS["fillColor"] + self.labelRadius = 1.2 + self.label_dx = self.label_dy = self.label_angle = 0 + self.label_text = None + self.label_topPadding = self.label_leftPadding = self.label_rightPadding = self.label_bottomPadding = 0 + self.label_boxAnchor = 'c' + self.label_boxStrokeColor = None #boxStroke + self.label_boxStrokeWidth = 0.5 #boxStrokeWidth + self.label_boxFillColor = None + self.label_strokeColor = None + self.label_strokeWidth = 0.1 + self.label_leading = self.label_width = self.label_maxWidth = self.label_height = None + self.label_textAnchor = 'start' + self.label_visible = 1 + +class _SL3D: + def __init__(self,lo,hi): + if lo<0: + lo += 360 + hi += 360 + self.lo = lo + self.hi = hi + self.mid = (lo+hi)*0.5 + + def __str__(self): + return '_SL3D(%.2f,%.2f)' % (self.lo,self.hi) + +_270r = _2rad(270) +class Pie3d(Pie): + _attrMap = AttrMap(BASE=Pie, + perspective = AttrMapValue(isNumber, desc='A flattening parameter.'), + depth_3d = AttrMapValue(isNumber, desc='depth of the pie.'), + angle_3d = AttrMapValue(isNumber, desc='The view angle.'), + ) + perspective = 70 + depth_3d = 25 + angle_3d = 180 + + def _popout(self,i): + return self.slices[i].popout or 0 + + def CX(self, i,d ): + return self._cx+(d and self._xdepth_3d or 0)+self._popout(i)*cos(_2rad(self._sl3d[i].mid)) + def CY(self,i,d): + return self._cy+(d and self._ydepth_3d or 0)+self._popout(i)*sin(_2rad(self._sl3d[i].mid)) + def OX(self,i,o,d): + return self.CX(i,d)+self._radiusx*cos(_2rad(o)) + def OY(self,i,o,d): + return self.CY(i,d)+self._radiusy*sin(_2rad(o)) + + def rad_dist(self,a): + _3dva = self._3dva + return min(abs(a-_3dva),abs(a-_3dva+360)) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 300 + self.height = 200 + self.data = [12.50,20.10,2.00,22.00,5.00,18.00,13.00] + self.labels = None # or list of strings + self.startAngle = 90 + self.direction = "clockwise" + self.simpleLabels = 1 + self.slices = TypedPropertyCollection(Wedge3dProperties) + self.slices[0].fillColor = colors.darkcyan + self.slices[1].fillColor = colors.blueviolet + self.slices[2].fillColor = colors.blue + self.slices[3].fillColor = colors.cyan + self.slices[4].fillColor = colors.azure + self.slices[5].fillColor = colors.crimson + self.slices[6].fillColor = colors.darkviolet + self.checkLabelOverlap = 0 + + def _fillSide(self,L,i,angle,strokeColor,strokeWidth,fillColor): + rd = self.rad_dist(angle) + if rd0: angle0, angle1 = angle1, angle0 + _sl3d.append(_SL3D(angle0,angle1)) + + labels = _fixLabels(self.labels,n) + a0 = _3d_angle + a1 = _3d_angle+180 + T = [] + S = [] + L = [] + + class WedgeLabel3d(WedgeLabel): + _ydepth_3d = self._ydepth_3d + def _checkDXY(self,ba): + if ba[0]=='n': + if not hasattr(self,'_ody'): + self._ody = self.dy + self.dy = -self._ody + self._ydepth_3d + + checkLabelOverlap = self.checkLabelOverlap + + for i in xrange(n): + style = slices[i] + if not style.visible: continue + sl = _sl3d[i] + lo = angle0 = sl.lo + hi = angle1 = sl.hi + if abs(hi-lo)<=1e-7: continue + fillColor = _getShaded(style.fillColor,style.fillColorShaded,style.shading) + strokeColor = _getShaded(style.strokeColor,style.strokeColorShaded,style.shading) or fillColor + strokeWidth = style.strokeWidth + cx0 = CX(i,0) + cy0 = CY(i,0) + cx1 = CX(i,1) + cy1 = CY(i,1) + #background shaded pie bottom + g.add(Wedge(cx1,cy1,radiusx, lo, hi,yradius=radiusy, + strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor, + strokeLineJoin=1)) + #connect to top + if lo < a0 < hi: angle0 = a0 + if lo < a1 < hi: angle1 = a1 + if 1: + p = ArcPath(strokeColor=strokeColor, fillColor=fillColor,strokeWidth=strokeWidth,strokeLineJoin=1) + p.addArc(cx1,cy1,radiusx,angle0,angle1,yradius=radiusy,moveTo=1) + p.lineTo(OX(i,angle1,0),OY(i,angle1,0)) + p.addArc(cx0,cy0,radiusx,angle0,angle1,yradius=radiusy,reverse=1) + p.closePath() + if angle0<=_3dva and angle1>=_3dva: + rd = 0 + else: + rd = min(rad_dist(angle0),rad_dist(angle1)) + S.append((rd,p)) + _fillSide(S,i,lo,strokeColor,strokeWidth,fillColor) + _fillSide(S,i,hi,strokeColor,strokeWidth,fillColor) + + #bright shaded top + fillColor = style.fillColor + strokeColor = style.strokeColor or fillColor + T.append(Wedge(cx0,cy0,radiusx,lo,hi,yradius=radiusy, + strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor,strokeLineJoin=1)) + + text = labels[i] + if text: + rat = style.labelRadius + self._radiusx *= rat + self._radiusy *= rat + mid = sl.mid + labelX = OX(i,mid,0) + labelY = OY(i,mid,0) + _addWedgeLabel(self,text,L.append,mid,labelX,labelY,style,labelClass=WedgeLabel3d) + if checkLabelOverlap: + l = L[-1] + l._origdata = { 'x': labelX, 'y':labelY, 'angle': mid, + 'rx': self._radiusx, 'ry':self._radiusy, 'cx':CX(i,0), 'cy':CY(i,0), + 'bounds': l.getBounds(), + } + self._radiusx = radiusx + self._radiusy = radiusy + + S.sort(lambda a,b: -cmp(a[0],b[0])) + if checkLabelOverlap: + fixLabelOverlaps(L) + map(g.add,map(lambda x:x[1],S)+T+L) + return g + + def demo(self): + d = Drawing(200, 100) + + pc = Pie() + pc.x = 50 + pc.y = 10 + pc.width = 100 + pc.height = 80 + pc.data = [10,20,30,40,50,60] + pc.labels = ['a','b','c','d','e','f'] + + pc.slices.strokeWidth=0.5 + pc.slices[3].popout = 10 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + pc.slices[0].fillColor = colors.darkcyan + pc.slices[1].fillColor = colors.blueviolet + pc.slices[2].fillColor = colors.blue + pc.slices[3].fillColor = colors.cyan + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + self.slices[1].visible = 0 + self.slices[3].visible = 1 + self.slices[4].visible = 1 + self.slices[5].visible = 1 + self.slices[6].visible = 0 + + d.add(pc) + return d + + +def sample0a(): + "Make a degenerated pie chart with only one slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10] + pc.labels = ['a'] + pc.slices.strokeWidth=1#0.5 + + d.add(pc) + + return d + + +def sample0b(): + "Make a degenerated pie chart with only one slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.width = 120 + pc.height = 100 + pc.data = [10] + pc.labels = ['a'] + pc.slices.strokeWidth=1#0.5 + + d.add(pc) + + return d + + +def sample1(): + "Make a typical pie chart with with one slice treated in a special way." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10, 20, 30, 40, 50, 60] + pc.labels = ['a', 'b', 'c', 'd', 'e', 'f'] + + pc.slices.strokeWidth=1#0.5 + pc.slices[3].popout = 20 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + + d.add(pc) + + return d + + +def sample2(): + "Make a pie chart with nine slices." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + pc.data = [0.31, 0.148, 0.108, + 0.076, 0.033, 0.03, + 0.019, 0.126, 0.15] + pc.labels = ['1', '2', '3', '4', '5', '6', '7', '8', 'X'] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + pc.slices[3].fillColor = colors.lightsteelblue + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + pc.slices[6].fillColor = colors.lightcoral + pc.slices[7].fillColor = colors.tan + pc.slices[8].fillColor = colors.darkseagreen + + d.add(pc) + + return d + + +def sample3(): + "Make a pie chart with a very slim slice." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + + pc.data = [74, 1, 25] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + + d.add(pc) + + return d + + +def sample4(): + "Make a pie chart with several very slim slices." + + d = Drawing(400, 200) + + pc = Pie() + pc.x = 125 + pc.y = 25 + + pc.data = [74, 1, 1, 1, 1, 22] + + pc.width = 150 + pc.height = 150 + pc.slices.strokeWidth=1#0.5 + pc.slices[0].fillColor = colors.steelblue + pc.slices[1].fillColor = colors.thistle + pc.slices[2].fillColor = colors.cornflower + pc.slices[3].fillColor = colors.lightsteelblue + pc.slices[4].fillColor = colors.aquamarine + pc.slices[5].fillColor = colors.cadetblue + + d.add(pc) + + return d diff --git a/bin/reportlab/graphics/charts/slidebox.py b/bin/reportlab/graphics/charts/slidebox.py new file mode 100644 index 00000000000..309fb07ba0d --- /dev/null +++ b/bin/reportlab/graphics/charts/slidebox.py @@ -0,0 +1,186 @@ +from reportlab.lib.colors import Color, white, black +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.shapes import Polygon, Line, Circle, String, Drawing, PolyLine, Group, Rect +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection +from reportlab.lib.attrmap import * +from reportlab.lib.validators import * +from reportlab.lib.units import cm +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.graphics.widgets.grids import ShadedRect, Grid + +class SlideBox(Widget): + """Returns a slidebox widget""" + _attrMap = AttrMap( + labelFontName = AttrMapValue(isString, desc="Name of font used for the labels"), + labelFontSize = AttrMapValue(isNumber, desc="Size of font used for the labels"), + labelStrokeColor = AttrMapValue(isColorOrNone, desc="Colour for for number outlines"), + labelFillColor = AttrMapValue(isColorOrNone, desc="Colour for number insides"), + startColor = AttrMapValue(isColor, desc='Color of first box'), + endColor = AttrMapValue(isColor, desc='Color of last box'), + numberOfBoxes = AttrMapValue(isInt, desc='How many boxes there are'), + trianglePosition = AttrMapValue(isInt, desc='Which box is highlighted by the triangles'), + triangleHeight = AttrMapValue(isNumber, desc="Height of indicator triangles"), + triangleWidth = AttrMapValue(isNumber, desc="Width of indicator triangles"), + triangleFillColor = AttrMapValue(isColor, desc="Colour of indicator triangles"), + triangleStrokeColor = AttrMapValue(isColorOrNone, desc="Colour of indicator triangle outline"), + triangleStrokeWidth = AttrMapValue(isNumber, desc="Colour of indicator triangle outline"), + boxHeight = AttrMapValue(isNumber, desc="Height of the boxes"), + boxWidth = AttrMapValue(isNumber, desc="Width of the boxes"), + boxSpacing = AttrMapValue(isNumber, desc="Space between the boxes"), + boxOutlineColor = AttrMapValue(isColorOrNone, desc="Colour used to outline the boxes (if any)"), + boxOutlineWidth = AttrMapValue(isNumberOrNone, desc="Width of the box outline (if any)"), + leftPadding = AttrMapValue(isNumber, desc='Padding on left of drawing'), + rightPadding = AttrMapValue(isNumber, desc='Padding on right of drawing'), + topPadding = AttrMapValue(isNumber, desc='Padding at top of drawing'), + bottomPadding = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + background = AttrMapValue(isColorOrNone, desc='Colour of the background to the drawing (if any)'), + sourceLabelText = AttrMapValue(isNoneOrString, desc="Text used for the 'source' label (can be empty)"), + sourceLabelOffset = AttrMapValue(isNumber, desc='Padding at bottom of drawing'), + sourceLabelFontName = AttrMapValue(isString, desc="Name of font used for the 'source' label"), + sourceLabelFontSize = AttrMapValue(isNumber, desc="Font size for the 'source' label"), + sourceLabelFillColor = AttrMapValue(isColorOrNone, desc="Colour ink for the 'source' label (bottom right)"), + ) + + def __init__(self): + self.labelFontName = "Helvetica-Bold" + self.labelFontSize = 10 + self.labelStrokeColor = black + self.labelFillColor = white + self.startColor = colors.Color(232/255.0,224/255.0,119/255.0) + self.endColor = colors.Color(25/255.0,77/255.0,135/255.0) + self.numberOfBoxes = 7 + self.trianglePosition = 7 + self.triangleHeight = 0.12*cm + self.triangleWidth = 0.38*cm + self.triangleFillColor = white + self.triangleStrokeColor = black + self.triangleStrokeWidth = 0.58 + self.boxHeight = 0.55*cm + self.boxWidth = 0.73*cm + self.boxSpacing = 0.075*cm + self.boxOutlineColor = black + self.boxOutlineWidth = 0.58 + self.leftPadding=5 + self.rightPadding=5 + self.topPadding=5 + self.bottomPadding=5 + self.background=None + self.sourceLabelText = "Source: ReportLab" + self.sourceLabelOffset = 0.2*cm + self.sourceLabelFontName = "Helvetica-Oblique" + self.sourceLabelFontSize = 6 + self.sourceLabelFillColor = black + + def _getDrawingDimensions(self): + tx=(self.numberOfBoxes*self.boxWidth) + if self.numberOfBoxes>1: tx=tx+((self.numberOfBoxes-1)*self.boxSpacing) + tx=tx+self.leftPadding+self.rightPadding + ty=self.boxHeight+self.triangleHeight + ty=ty+self.topPadding+self.bottomPadding+self.sourceLabelOffset+self.sourceLabelFontSize + return (tx,ty) + + def _getColors(self): + # for calculating intermediate colors... + numShades = self.numberOfBoxes+1 + fillColorStart = self.startColor + fillColorEnd = self.endColor + colorsList =[] + + for i in range(0,numShades): + colorsList.append(colors.linearlyInterpolatedColor(fillColorStart, fillColorEnd, 0, numShades-1, i)) + return colorsList + + def demo(self,drawing=None): + from reportlab.lib import colors + if not drawing: + tx,ty=self._getDrawingDimensions() + drawing = Drawing(tx,ty) + drawing.add(self.draw()) + return drawing + + def draw(self): + g = Group() + ys = self.bottomPadding+(self.triangleHeight/2)+self.sourceLabelOffset+self.sourceLabelFontSize + if self.background: + x,y = self._getDrawingDimensions() + g.add(Rect(-self.leftPadding,-ys,x,y, + strokeColor=None, + strokeWidth=0, + fillColor=self.background)) + + ascent=getFont(self.labelFontName).face.ascent/1000. + if ascent==0: ascent=0.718 # default (from helvetica) + ascent=ascent*self.labelFontSize # normalize + + colorsList = self._getColors() + + # Draw the boxes - now uses ShadedRect from grids + x=0 + for f in range (0,self.numberOfBoxes): + sr=ShadedRect() + sr.x=x + sr.y=0 + sr.width=self.boxWidth + sr.height=self.boxHeight + sr.orientation = 'vertical' + sr.numShades = 30 + sr.fillColorStart = colorsList[f] + sr.fillColorEnd = colorsList[f+1] + sr.strokeColor = None + sr.strokeWidth = 0 + + g.add(sr) + + g.add(Rect(x,0,self.boxWidth,self.boxHeight, + strokeColor=self.boxOutlineColor, + strokeWidth=self.boxOutlineWidth, + fillColor=None)) + + g.add(String(x+self.boxWidth/2.,(self.boxHeight-ascent)/2., + text = str(f+1), + fillColor = self.labelFillColor, + strokeColor=self.labelStrokeColor, + textAnchor = 'middle', + fontName = self.labelFontName, + fontSize = self.labelFontSize)) + x=x+self.boxWidth+self.boxSpacing + + #do triangles + xt = (self.trianglePosition*self.boxWidth) + if self.trianglePosition>1: + xt = xt+(self.trianglePosition-1)*self.boxSpacing + xt = xt-(self.boxWidth/2) + g.add(Polygon( + strokeColor = self.triangleStrokeColor, + strokeWidth = self.triangleStrokeWidth, + fillColor = self.triangleFillColor, + points=[xt,self.boxHeight-(self.triangleHeight/2), + xt-(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2), + xt+(self.triangleWidth/2),self.boxHeight+(self.triangleHeight/2), + xt,self.boxHeight-(self.triangleHeight/2)])) + g.add(Polygon( + strokeColor = self.triangleStrokeColor, + strokeWidth = self.triangleStrokeWidth, + fillColor = self.triangleFillColor, + points=[xt,0+(self.triangleHeight/2), + xt-(self.triangleWidth/2),0-(self.triangleHeight/2), + xt+(self.triangleWidth/2),0-(self.triangleHeight/2), + xt,0+(self.triangleHeight/2)])) + + #source label + if self.sourceLabelText != None: + g.add(String(x-self.boxSpacing,0-(self.triangleHeight/2)-self.sourceLabelOffset-(self.sourceLabelFontSize), + text = self.sourceLabelText, + fillColor = self.sourceLabelFillColor, + textAnchor = 'end', + fontName = self.sourceLabelFontName, + fontSize = self.sourceLabelFontSize)) + + g.shift(self.leftPadding, ys) + + return g + + +if __name__ == "__main__": + d = SlideBox() + d.demo().save(fnRoot="slidebox") diff --git a/bin/reportlab/graphics/charts/spider.py b/bin/reportlab/graphics/charts/spider.py new file mode 100644 index 00000000000..6de92724390 --- /dev/null +++ b/bin/reportlab/graphics/charts/spider.py @@ -0,0 +1,407 @@ + #Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/spider.py +# spider chart, also known as radar chart + +"""Spider Chart + +Normal use shows variation of 5-10 parameters against some 'norm' or target. +When there is more than one series, place the series with the largest +numbers first, as it will be overdrawn by each successive one. +""" +__version__=''' $Id: spider.py 2676 2005-09-06 10:25:00Z rgbecker $ ''' + +import copy +from math import sin, cos, pi + +from reportlab.lib import colors +from reportlab.lib.validators import isColor, isNumber, isListOfNumbersOrNone,\ + isListOfNumbers, isColorOrNone, isString,\ + isListOfStringsOrNone, OneOf, SequenceOf,\ + isBoolean, isListOfColors, isNumberOrNone,\ + isNoneOrListOfNoneOrStrings, isTextAnchor,\ + isNoneOrListOfNoneOrNumbers, isBoxAnchor,\ + isStringOrNone, isStringOrNone, EitherOr,\ + isCallable +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Line, Rect, Polygon, PolyLine, Ellipse, \ + Wedge, String, STATE_DEFAULTS +from reportlab.graphics.widgetbase import Widget, TypedPropertyCollection, PropHolder +from reportlab.graphics.charts.areas import PlotArea +from piecharts import WedgeLabel +from reportlab.graphics.widgets.markers import makeMarker, uSymbol2Symbol, isSymbol + +class StrandProperty(PropHolder): + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + symbol = AttrMapValue(EitherOr((isStringOrNone,isSymbol)), desc='Widget placed at data points.'), + symbolSize= AttrMapValue(isNumber, desc='Symbol size.'), + name = AttrMapValue(isStringOrNone, desc='Name of the strand.'), + ) + + def __init__(self): + self.strokeWidth = 1 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.symbol = None + self.symbolSize = 5 + self.name = None + +class SpokeProperty(PropHolder): + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + labelRadius = AttrMapValue(isNumber), + visible = AttrMapValue(isBoolean,desc="True if the spoke line is to be drawn"), + ) + + def __init__(self,**kw): + self.strokeWidth = 0.5 + self.fillColor = None + self.strokeColor = STATE_DEFAULTS["strokeColor"] + self.strokeDashArray = STATE_DEFAULTS["strokeDashArray"] + self.visible = 1 + self.labelRadius = 1.05 + +class SpokeLabel(WedgeLabel): + def __init__(self,**kw): + WedgeLabel.__init__(self,**kw) + if '_text' not in kw.keys(): self._text = '' + +class StrandLabel(SpokeLabel): + _attrMap = AttrMap(BASE=SpokeLabel, + format = AttrMapValue(EitherOr((isStringOrNone,isCallable)),"Format for the label"), + dR = AttrMapValue(isNumberOrNone,"radial shift for label"), + ) + def __init__(self,**kw): + self.format = '' + self.dR = 0 + SpokeLabel.__init__(self,**kw) + +def _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty): + L = labelClass() + L._text = text + L.x = cx + radius*car + L.y = cy + radius*sar + L._pmv = angle*180/pi + L.boxAnchor = sty.boxAnchor + L.dx = sty.dx + L.dy = sty.dy + L.angle = sty.angle + L.boxAnchor = sty.boxAnchor + L.boxStrokeColor = sty.boxStrokeColor + L.boxStrokeWidth = sty.boxStrokeWidth + L.boxFillColor = sty.boxFillColor + L.strokeColor = sty.strokeColor + L.strokeWidth = sty.strokeWidth + L.leading = sty.leading + L.width = sty.width + L.maxWidth = sty.maxWidth + L.height = sty.height + L.textAnchor = sty.textAnchor + L.visible = sty.visible + L.topPadding = sty.topPadding + L.leftPadding = sty.leftPadding + L.rightPadding = sty.rightPadding + L.bottomPadding = sty.bottomPadding + L.fontName = sty.fontName + L.fontSize = sty.fontSize + L.fillColor = sty.fillColor + return L + +class SpiderChart(PlotArea): + _attrMap = AttrMap(BASE=PlotArea, + data = AttrMapValue(None, desc='Data to be plotted, list of (lists of) numbers.'), + labels = AttrMapValue(isListOfStringsOrNone, desc="optional list of labels to use for each data point"), + startAngle = AttrMapValue(isNumber, desc="angle of first slice; like the compass, 0 is due North"), + direction = AttrMapValue( OneOf('clockwise', 'anticlockwise'), desc="'clockwise' or 'anticlockwise'"), + strands = AttrMapValue(None, desc="collection of strand descriptor objects"), + spokes = AttrMapValue(None, desc="collection of spoke descriptor objects"), + strandLabels = AttrMapValue(None, desc="collection of strand label descriptor objects"), + spokeLabels = AttrMapValue(None, desc="collection of spoke label descriptor objects"), + ) + + def makeSwatchSample(self, rowNo, x, y, width, height): + baseStyle = self.strands + styleIdx = rowNo % len(baseStyle) + style = baseStyle[styleIdx] + strokeColor = getattr(style, 'strokeColor', getattr(baseStyle,'strokeColor',None)) + fillColor = getattr(style, 'fillColor', getattr(baseStyle,'fillColor',None)) + strokeDashArray = getattr(style, 'strokeDashArray', getattr(baseStyle,'strokeDashArray',None)) + strokeWidth = getattr(style, 'strokeWidth', getattr(baseStyle, 'strokeWidth',0)) + symbol = getattr(style, 'symbol', getattr(baseStyle, 'symbol',None)) + ym = y+height/2.0 + if fillColor is None and strokeColor is not None and strokeWidth>0: + bg = Line(x,ym,x+width,ym,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray) + elif fillColor is not None: + bg = Rect(x,y,width,height,strokeWidth=strokeWidth,strokeColor=strokeColor, + strokeDashArray=strokeDashArray,fillColor=fillColor) + else: + bg = None + if symbol: + symbol = uSymbol2Symbol(symbol,x+width/2.,ym,color) + if bg: + g = Group() + g.add(bg) + g.add(symbol) + return g + return symbol or bg + + def getSeriesName(self,i,default=None): + '''return series name i or default''' + return getattr(self.strands[i],'name',default) + + def __init__(self): + PlotArea.__init__(self) + + self.data = [[10,12,14,16,14,12], [6,8,10,12,9,11]] + self.labels = None # or list of strings + self.labels = ['a','b','c','d','e','f'] + self.startAngle = 90 + self.direction = "clockwise" + + self.strands = TypedPropertyCollection(StrandProperty) + self.spokes = TypedPropertyCollection(SpokeProperty) + self.spokeLabels = TypedPropertyCollection(SpokeLabel) + self.spokeLabels._text = None + self.strandLabels = TypedPropertyCollection(StrandLabel) + self.x = 10 + self.y = 10 + self.width = 180 + self.height = 180 + + def demo(self): + d = Drawing(200, 200) + d.add(SpiderChart()) + return d + + def normalizeData(self, outer = 0.0): + """Turns data into normalized ones where each datum is < 1.0, + and 1.0 = maximum radius. Adds 10% at outside edge by default""" + data = self.data + assert min(map(min,data)) >=0, "Cannot do spider plots of negative numbers!" + norm = max(map(max,data)) + norm *= (1.0+outer) + if norm<1e-9: norm = 1.0 + self._norm = norm + return [[e/norm for e in row] for row in data] + + def _innerDrawLabel(self, sty, radius, cx, cy, angle, car, sar, labelClass=StrandLabel): + "Draw a label for a given item in the list." + fmt = sty.format + value = radius*self._norm + if not fmt: + text = None + elif isinstance(fmt,str): + if fmt == 'values': + text = sty._text + else: + text = fmt % value + elif callable(fmt): + text = fmt(value) + else: + raise ValueError("Unknown formatter type %s, expected string or function" % fmt) + + if text: + dR = sty.dR + if dR: + radius += dR/self._radius + L = _setupLabel(labelClass, text, radius, cx, cy, angle, car, sar, sty) + if dR<0: L._anti = 1 + else: + L = None + return L + + def draw(self): + # normalize slice data + g = self.makeBackground() or Group() + + xradius = self.width/2.0 + yradius = self.height/2.0 + self._radius = radius = min(xradius, yradius) + cx = self.x + xradius + cy = self.y + yradius + + data = self.normalizeData() + + self._seriesCount = len(data) + n = len(data[0]) + + #labels + if self.labels is None: + labels = [''] * n + else: + labels = self.labels + #there's no point in raising errors for less than enough errors if + #we silently create all for the extreme case of no labels. + i = n-len(labels) + if i>0: + labels = labels + ['']*i + + S = [] + STRANDS = [] + STRANDAREAS = [] + syms = [] + labs = [] + csa = [] + angle = self.startAngle*pi/180 + direction = self.direction == "clockwise" and -1 or 1 + angleBetween = direction*(2 * pi)/float(n) + spokes = self.spokes + spokeLabels = self.spokeLabels + for i in xrange(n): + car = cos(angle)*radius + sar = sin(angle)*radius + csa.append((car,sar,angle)) + si = self.spokes[i] + if si.visible: + spoke = Line(cx, cy, cx + car, cy + sar, strokeWidth = si.strokeWidth, strokeColor=si.strokeColor, strokeDashArray=si.strokeDashArray) + S.append(spoke) + sli = spokeLabels[i] + text = sli._text + if not text: text = labels[i] + if text: + S.append(_setupLabel(WedgeLabel, text, si.labelRadius, cx, cy, angle, car, sar, sli)) + angle += angleBetween + + # now plot the polygons + rowIdx = 0 + strands = self.strands + strandLabels = self.strandLabels + for row in data: + # series plot + rsty = strands[rowIdx] + points = [] + car, sar = csa[-1][:2] + r = row[-1] + points.append(cx+car*r) + points.append(cy+sar*r) + for i in xrange(n): + car, sar, angle = csa[i] + r = row[i] + points.append(cx+car*r) + points.append(cy+sar*r) + L = self._innerDrawLabel(strandLabels[(rowIdx,i)], r, cx, cy, angle, car, sar, labelClass=StrandLabel) + if L: labs.append(L) + sty = strands[(rowIdx,i)] + uSymbol = sty.symbol + + # put in a marker, if it needs one + if uSymbol: + s_x = cx+car*r + s_y = cy+sar*r + s_fillColor = sty.fillColor + s_strokeColor = sty.strokeColor + s_strokeWidth = sty.strokeWidth + s_angle = 0 + s_size = sty.symbolSize + if type(uSymbol) is type(''): + symbol = makeMarker(uSymbol, + size = s_size, + x = s_x, + y = s_y, + fillColor = s_fillColor, + strokeColor = s_strokeColor, + strokeWidth = s_strokeWidth, + angle = s_angle, + ) + else: + symbol = uSymbol2Symbol(uSymbol,s_x,s_y,s_fillColor) + for k,v in (('size', s_size), ('fillColor', s_fillColor), + ('x', s_x), ('y', s_y), + ('strokeColor',s_strokeColor), ('strokeWidth',s_strokeWidth), + ('angle',s_angle),): + if getattr(symbol,k,None) is None: + try: + setattr(symbol,k,v) + except: + pass + syms.append(symbol) + + # make up the 'strand' + if rsty.fillColor: + strand = Polygon(points) + strand.fillColor = rsty.fillColor + strand.strokeColor = None + strand.strokeWidth = 0 + STRANDAREAS.append(strand) + if rsty.strokeColor and rsty.strokeWidth: + strand = PolyLine(points) + strand.strokeColor = rsty.strokeColor + strand.strokeWidth = rsty.strokeWidth + strand.strokeDashArray = rsty.strokeDashArray + STRANDS.append(strand) + rowIdx += 1 + + map(g.add,STRANDAREAS+STRANDS+syms+S+labs) + return g + +def sample1(): + "Make a simple spider chart" + d = Drawing(400, 400) + sp = SpiderChart() + sp.x = 50 + sp.y = 50 + sp.width = 300 + sp.height = 300 + sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]] + sp.labels = ['a','b','c','d','e','f'] + sp.strands[0].strokeColor = colors.cornsilk + sp.strands[1].strokeColor = colors.cyan + sp.strands[2].strokeColor = colors.palegreen + sp.strands[0].fillColor = colors.cornsilk + sp.strands[1].fillColor = colors.cyan + sp.strands[2].fillColor = colors.palegreen + sp.spokes.strokeDashArray = (2,2) + d.add(sp) + return d + + +def sample2(): + "Make a spider chart with markers, but no fill" + d = Drawing(400, 400) + sp = SpiderChart() + sp.x = 50 + sp.y = 50 + sp.width = 300 + sp.height = 300 + sp.data = [[10,12,14,16,14,12], [6,8,10,12,9,15],[7,8,17,4,12,8]] + sp.labels = ['U','V','W','X','Y','Z'] + sp.strands.strokeWidth = 1 + sp.strands[0].fillColor = colors.pink + sp.strands[1].fillColor = colors.lightblue + sp.strands[2].fillColor = colors.palegreen + sp.strands[0].strokeColor = colors.red + sp.strands[1].strokeColor = colors.blue + sp.strands[2].strokeColor = colors.green + sp.strands.symbol = "FilledDiamond" + sp.strands[1].symbol = makeMarker("Circle") + sp.strands[1].symbol.strokeWidth = 0.5 + sp.strands[1].symbol.fillColor = colors.yellow + sp.strands.symbolSize = 6 + sp.strandLabels[0,3]._text = 'special' + sp.strandLabels[0,1]._text = 'one' + sp.strandLabels[0,0]._text = 'zero' + sp.strandLabels[1,0]._text = 'Earth' + sp.strandLabels[2,2]._text = 'Mars' + sp.strandLabels.format = 'values' + sp.strandLabels.dR = -5 + d.add(sp) + return d + + +if __name__=='__main__': + d = sample1() + from reportlab.graphics.renderPDF import drawToFile + drawToFile(d, 'spider.pdf') + d = sample2() + drawToFile(d, 'spider2.pdf') diff --git a/bin/reportlab/graphics/charts/textlabels.py b/bin/reportlab/graphics/charts/textlabels.py new file mode 100644 index 00000000000..5bcd0250e79 --- /dev/null +++ b/bin/reportlab/graphics/charts/textlabels.py @@ -0,0 +1,442 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/textlabels.py +__version__=''' $Id: textlabels.py 2647 2005-07-26 13:47:51Z rgbecker $ ''' +import string + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isNumberOrNone, OneOf, isColorOrNone, isString, \ + isTextAnchor, isBoxAnchor, isBoolean, NoneOr, isInstanceOf, isNoneOrString +from reportlab.lib.attrmap import * +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.graphics.shapes import Drawing, Group, Circle, Rect, String, STATE_DEFAULTS +from reportlab.graphics.shapes import _PATH_OP_ARG_COUNT, _PATH_OP_NAMES, definePath +from reportlab.graphics.widgetbase import Widget, PropHolder + +_gs = None +_A2BA= { + 'x': {0:'n', 45:'ne', 90:'e', 135:'se', 180:'s', 225:'sw', 270:'w', 315: 'nw', -45: 'nw'}, + 'y': {0:'e', 45:'se', 90:'s', 135:'sw', 180:'w', 225:'nw', 270:'n', 315: 'ne', -45: 'ne'}, + } +def _simpleSplit(txt,mW,SW): + L = [] + ws = SW(' ') + O = [] + w = -ws + for t in string.split(txt): + lt = SW(t) + if w+ws+lt<=mW or O==[]: + O.append(t) + w = w + ws + lt + else: + L.append(string.join(O,' ')) + O = [t] + w = lt + if O!=[]: L.append(string.join(O,' ')) + return L + +def _pathNumTrunc(n): + if int(n)==n: return int(n) + return round(n,5) + +def _processGlyph(G, truncate=1, pathReverse=0): + O = [] + P = [] + R = [] + for g in G+(('end',),): + op = g[0] + if O and op in ['moveTo', 'moveToClosed','end']: + if O[0]=='moveToClosed': + O = O[1:] + if pathReverse: + for i in xrange(0,len(P),2): + P[i+1], P[i] = P[i:i+2] + P.reverse() + O.reverse() + O.insert(0,'moveTo') + O.append('closePath') + i = 0 + if truncate: P = map(_pathNumTrunc,P) + for o in O: + j = i + _PATH_OP_ARG_COUNT[_PATH_OP_NAMES.index(o)] + if o=='closePath': + R.append(o) + else: + R.append((o,)+ tuple(P[i:j])) + i = j + O = [] + P = [] + O.append(op) + P.extend(g[1:]) + return R + +def _text2PathDescription(text, x=0, y=0, fontName='Times-Roman', fontSize=1000, + anchor='start', truncate=1, pathReverse=0): + global _gs + if not _gs: + import _renderPM + _gs = _renderPM.gstate(1,1) + from reportlab.graphics import renderPM + renderPM._setFont(_gs,fontName,fontSize) + P = [] + if not anchor =='start': + textLen = stringWidth(text, fontName,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2. + for g in _gs._stringPath(text,x,y): + P.extend(_processGlyph(g,truncate=truncate,pathReverse=pathReverse)) + return P + +def _text2Path(text, x=0, y=0, fontName='Times-Roman', fontSize=1000, + anchor='start', truncate=1, pathReverse=0): + return definePath(_text2PathDescription(text,x=x,y=y,fontName=fontName, + fontSize=fontSize,anchor=anchor,truncate=truncate,pathReverse=pathReverse)) + +_BA2TA={'w':'start','nw':'start','sw':'start','e':'end', 'ne': 'end', 'se':'end', 'n':'middle','s':'middle','c':'middle'} +class Label(Widget): + """A text label to attach to something else, such as a chart axis. + + This allows you to specify an offset, angle and many anchor + properties relative to the label's origin. It allows, for example, + angled multiline axis labels. + """ + # fairly straight port of Robin Becker's textbox.py to new widgets + # framework. + + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + dx = AttrMapValue(isNumber), + dy = AttrMapValue(isNumber), + angle = AttrMapValue(isNumber), + boxAnchor = AttrMapValue(isBoxAnchor), + boxStrokeColor = AttrMapValue(isColorOrNone), + boxStrokeWidth = AttrMapValue(isNumber), + boxFillColor = AttrMapValue(isColorOrNone), + boxTarget = AttrMapValue(isString), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + text = AttrMapValue(isString), + fontName = AttrMapValue(isString), + fontSize = AttrMapValue(isNumber), + leading = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + maxWidth = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + textAnchor = AttrMapValue(isTextAnchor), + visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + topPadding = AttrMapValue(isNumber,'padding at top of box'), + leftPadding = AttrMapValue(isNumber,'padding at left of box'), + rightPadding = AttrMapValue(isNumber,'padding at right of box'), + bottomPadding = AttrMapValue(isNumber,'padding at bottom of box'), + ) + + def __init__(self,**kw): + self._setKeywords(**kw) + self._setKeywords( + _text = 'Multi-Line\nString', + boxAnchor = 'c', + angle = 0, + x = 0, + y = 0, + dx = 0, + dy = 0, + topPadding = 0, + leftPadding = 0, + rightPadding = 0, + bottomPadding = 0, + boxStrokeWidth = 0.5, + boxStrokeColor = None, + boxTarget = 'normal', + strokeColor = None, + boxFillColor = None, + leading = None, + width = None, + maxWidth = None, + height = None, + fillColor = STATE_DEFAULTS['fillColor'], + fontName = STATE_DEFAULTS['fontName'], + fontSize = STATE_DEFAULTS['fontSize'], + strokeWidth = 0.1, + textAnchor = 'start', + visible = 1, + ) + + def setText(self, text): + """Set the text property. May contain embedded newline characters. + Called by the containing chart or axis.""" + self._text = text + + + def setOrigin(self, x, y): + """Set the origin. This would be the tick mark or bar top relative to + which it is defined. Called by the containing chart or axis.""" + self.x = x + self.y = y + + + def demo(self): + """This shows a label positioned with its top right corner + at the top centre of the drawing, and rotated 45 degrees.""" + + d = Drawing(200, 100) + + # mark the origin of the label + d.add(Circle(100,90, 5, fillColor=colors.green)) + + lab = Label() + lab.setOrigin(100,90) + lab.boxAnchor = 'ne' + lab.angle = 45 + lab.dx = 0 + lab.dy = -20 + lab.boxStrokeColor = colors.green + lab.setText('Another\nMulti-Line\nString') + d.add(lab) + + return d + + def _getBoxAnchor(self): + '''hook for allowing special box anchor effects''' + ba = self.boxAnchor + if ba in ('autox', 'autoy'): + angle = self.angle + na = (int((angle%360)/45.)*45)%360 + if not (na % 90): # we have a right angle case + da = (angle - na) % 360 + if abs(da)>5: + na = na + (da>0 and 45 or -45) + ba = _A2BA[ba[-1]][na] + return ba + + def computeSize(self): + # the thing will draw in its own coordinate system + self._lines = string.split(self._text, '\n') + self._lineWidths = [] + topPadding = self.topPadding + leftPadding = self.leftPadding + rightPadding = self.rightPadding + bottomPadding = self.bottomPadding + SW = lambda text, fN=self.fontName, fS=self.fontSize: stringWidth(text, fN, fS) + if self.maxWidth: + L = [] + for l in self._lines: + L[-1:-1] = _simpleSplit(l,self.maxWidth,SW) + self._lines = L + if not self.width: + w = 0 + for line in self._lines: + thisWidth = SW(line) + self._lineWidths.append(thisWidth) + w = max(w,thisWidth) + self._width = w+leftPadding+rightPadding + else: + self._width = self.width + self._height = self.height or ((self.leading or 1.2*self.fontSize) * len(self._lines)+topPadding+bottomPadding) + self._ewidth = (self._width-leftPadding-rightPadding) + self._eheight = (self._height-topPadding-bottomPadding) + boxAnchor = self._getBoxAnchor() + if boxAnchor in ['n','ne','nw']: + self._top = -topPadding + elif boxAnchor in ['s','sw','se']: + self._top = self._height-topPadding + else: + self._top = 0.5*self._eheight + self._bottom = self._top - self._eheight + + if boxAnchor in ['ne','e','se']: + self._left = leftPadding - self._width + elif boxAnchor in ['nw','w','sw']: + self._left = leftPadding + else: + self._left = -self._ewidth*0.5 + self._right = self._left+self._ewidth + + def _getTextAnchor(self): + '''This can be overridden to allow special effects''' + ta = self.textAnchor + if ta=='boxauto': ta = _BA2TA[self._getBoxAnchor()] + return ta + + def draw(self): + _text = self._text + self._text = _text or '' + self.computeSize() + self._text = _text + g = Group() + g.translate(self.x + self.dx, self.y + self.dy) + g.rotate(self.angle) + + y = self._top - self.fontSize + textAnchor = self._getTextAnchor() + if textAnchor == 'start': + x = self._left + elif textAnchor == 'middle': + x = self._left + self._ewidth*0.5 + else: + x = self._right + + # paint box behind text just in case they + # fill it + if self.boxFillColor or (self.boxStrokeColor and self.boxStrokeWidth): + g.add(Rect( self._left-self.leftPadding, + self._bottom-self.bottomPadding, + self._width, + self._height, + strokeColor=self.boxStrokeColor, + strokeWidth=self.boxStrokeWidth, + fillColor=self.boxFillColor) + ) + + fillColor, fontName, fontSize = self.fillColor, self.fontName, self.fontSize + strokeColor, strokeWidth, leading = self.strokeColor, self.strokeWidth, (self.leading or 1.2*fontSize) + if strokeColor: + for line in self._lines: + s = _text2Path(line, x, y, fontName, fontSize, textAnchor) + s.fillColor = fillColor + s.strokeColor = strokeColor + s.strokeWidth = strokeWidth + g.add(s) + y = y - leading + else: + for line in self._lines: + s = String(x, y, line) + s.textAnchor = textAnchor + s.fontName = fontName + s.fontSize = fontSize + s.fillColor = fillColor + g.add(s) + y = y - leading + + return g + +class LabelDecorator: + _attrMap = AttrMap( + x = AttrMapValue(isNumberOrNone), + y = AttrMapValue(isNumberOrNone), + dx = AttrMapValue(isNumberOrNone), + dy = AttrMapValue(isNumberOrNone), + angle = AttrMapValue(isNumberOrNone), + boxAnchor = AttrMapValue(isBoxAnchor), + boxStrokeColor = AttrMapValue(isColorOrNone), + boxStrokeWidth = AttrMapValue(isNumberOrNone), + boxFillColor = AttrMapValue(isColorOrNone), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumberOrNone), + fontName = AttrMapValue(isNoneOrString), + fontSize = AttrMapValue(isNumberOrNone), + leading = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + maxWidth = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + textAnchor = AttrMapValue(isTextAnchor), + visible = AttrMapValue(isBoolean,desc="True if the label is to be drawn"), + ) + + def __init__(self): + self.textAnchor = 'start' + self.boxAnchor = 'w' + for a in self._attrMap.keys(): + if not hasattr(self,a): setattr(self,a,None) + + def decorate(self,l,L): + chart,g,rowNo,colNo,x,y,width,height,x00,y00,x0,y0 = l._callOutInfo + L.setText(chart.categoryAxis.categoryNames[colNo]) + g.add(L) + + def __call__(self,l): + from copy import deepcopy + L = Label() + for a,v in self.__dict__.items(): + if v is None: v = getattr(l,a,None) + setattr(L,a,v) + self.decorate(l,L) + +isOffsetMode=OneOf('high','low','bar','axis') +class LabelOffset(PropHolder): + _attrMap = AttrMap( + posMode = AttrMapValue(isOffsetMode,desc="Where to base +ve offset"), + pos = AttrMapValue(isNumber,desc='Value for positive elements'), + negMode = AttrMapValue(isOffsetMode,desc="Where to base -ve offset"), + neg = AttrMapValue(isNumber,desc='Value for negative elements'), + ) + def __init__(self): + self.posMode=self.negMode='axis' + self.pos = self.neg = 0 + + def _getValue(self, chart, val): + flipXY = chart._flipXY + A = chart.categoryAxis + jA = A.joinAxis + if val>=0: + mode = self.posMode + delta = self.pos + else: + mode = self.negMode + delta = self.neg + if flipXY: + v = A._x + else: + v = A._y + if jA: + if flipXY: + _v = jA._x + else: + _v = jA._y + if mode=='high': + v = _v + jA._length + elif mode=='low': + v = _v + elif mode=='bar': + v = _v+val + return v+delta + +NoneOrInstanceOfLabelOffset=NoneOr(isInstanceOf(LabelOffset)) + +class BarChartLabel(Label): + """ + An extended Label allowing for nudging, lines visibility etc + """ + _attrMap = AttrMap( + BASE=Label, + lineStrokeWidth = AttrMapValue(isNumberOrNone, desc="Non-zero for a drawn line"), + lineStrokeColor = AttrMapValue(isColorOrNone, desc="Color for a drawn line"), + fixedEnd = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw ends +/-"), + fixedStart = AttrMapValue(NoneOrInstanceOfLabelOffset, desc="None or fixed draw starts +/-"), + nudge = AttrMapValue(isNumber, desc="Non-zero sign dependent nudge"), + ) + + def __init__(self): + Label.__init__(self) + self.lineStrokeWidth = 0 + self.lineStrokeColor = None + self.nudge = 0 + self.fixedStart = self.fixedEnd = None + self._pmv = 0 + + def _getBoxAnchor(self): + a = self.boxAnchor + if self._pmv<0: a = {'nw':'se','n':'s','ne':'sw','w':'e','c':'c','e':'w','sw':'ne','s':'n','se':'nw'}[a] + return a + + def _getTextAnchor(self): + a = self.textAnchor + if self._pmv<0: a = {'start':'end', 'middle':'middle', 'end':'start'}[a] + return a + +class NA_Label(BarChartLabel): + """ + An extended Label allowing for nudging, lines visibility etc + """ + _attrMap = AttrMap( + BASE=BarChartLabel, + text = AttrMapValue(isNoneOrString, desc="Text to be used for N/A values"), + ) + def __init__(self): + BarChartLabel.__init__(self) + self.text = 'n/a' +NoneOrInstanceOfNA_Label=NoneOr(isInstanceOf(NA_Label)) diff --git a/bin/reportlab/graphics/charts/utils.py b/bin/reportlab/graphics/charts/utils.py new file mode 100644 index 00000000000..c654428f5c1 --- /dev/null +++ b/bin/reportlab/graphics/charts/utils.py @@ -0,0 +1,191 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/charts/utils.py +"Utilities used here and there." +__version__=''' $Id: utils.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from time import mktime, gmtime, strftime +import string + + +### Dinu's stuff used in some line plots (likely to vansih). + +def mkTimeTuple(timeString): + "Convert a 'dd/mm/yyyy' formatted string to a tuple for use in the time module." + + list = [0] * 9 + dd, mm, yyyy = map(int, string.split(timeString, '/')) + list[:3] = [yyyy, mm, dd] + + return tuple(list) + + +def str2seconds(timeString): + "Convert a number of seconds since the epoch into a date string." + + return mktime(mkTimeTuple(timeString)) + + +def seconds2str(seconds): + "Convert a date string into the number of seconds since the epoch." + + return strftime('%Y-%m-%d', gmtime(seconds)) + + +### Aaron's rounding function for making nice values on axes. + +from math import log10 + +def nextRoundNumber(x): + """Return the first 'nice round number' greater than or equal to x + + Used in selecting apropriate tick mark intervals; we say we want + an interval which places ticks at least 10 points apart, work out + what that is in chart space, and ask for the nextRoundNumber(). + Tries the series 1,2,5,10,20,50,100.., going up or down as needed. + """ + + #guess to nearest order of magnitude + if x in (0, 1): + return x + + if x < 0: + return -1.0 * nextRoundNumber(-x) + else: + lg = int(log10(x)) + + if lg == 0: + if x < 1: + base = 0.1 + else: + base = 1.0 + elif lg < 0: + base = 10.0 ** (lg - 1) + else: + base = 10.0 ** lg # e.g. base(153) = 100 + # base will always be lower than x + + if base >= x: + return base * 1.0 + elif (base * 2) >= x: + return base * 2.0 + elif (base * 5) >= x: + return base * 5.0 + else: + return base * 10.0 + + +### Robin's stuff from rgb_ticks. + +from math import log10, floor + +_intervals=(.1, .2, .25, .5) +_j_max=len(_intervals)-1 + + +def find_interval(lo,hi,I=5): + 'determine tick parameters for range [lo, hi] using I intervals' + + if lo >= hi: + if lo==hi: + if lo==0: + lo = -.1 + hi = .1 + else: + lo = 0.9*lo + hi = 1.1*hi + else: + raise ValueError, "lo>hi" + x=(hi - lo)/float(I) + b= (x>0 and (x<1 or x>10)) and 10**floor(log10(x)) or 1 + b = b + while 1: + a = x/b + if a<=_intervals[-1]: break + b = b*10 + + j = 0 + while a>_intervals[j]: j = j + 1 + + while 1: + ss = _intervals[j]*b + n = lo/ss + l = int(n)-(n<0) + n = ss*l + x = ss*(l+I) + a = I*ss + if n>0: + if a>=hi: + n = 0.0 + x = a + elif hi<0: + a = -a + if lo>a: + n = a + x = 0 + if hi<=x and n<=lo: break + j = j + 1 + if j>_j_max: + j = 0 + b = b*10 + return n, x, ss, lo - n + x - hi + + +def find_good_grid(lower,upper,n=(4,5,6,7,8,9), grid=None): + if grid: + t = divmod(lower,grid)[0] * grid + hi, z = divmod(upper,grid) + if z>1e-8: hi = hi+1 + hi = hi*grid + else: + try: + n[0] + except TypeError: + n = xrange(max(1,n-2),max(n+3,2)) + + w = 1e308 + for i in n: + z=find_interval(lower,upper,i) + if z[3] 3 or power < -3: + format = '%+'+`w+7`+'.0e' + else: + if power >= 0: + digits = int(power)+w + format = '%' + `digits`+'.0f' + else: + digits = w-int(power) + format = '%'+`digits+2`+'.'+`digits`+'f' + + if percent: format=format+'%%' + T = [] + n = int(float(hi-t)/grid+0.1)+1 + if split: + labels = [] + for i in xrange(n): + v = t+grid*i + T.append(v) + labels.append(format % v) + return T, labels + else: + for i in xrange(n): + v = t+grid*i + T.append((v, format % v)) + return T \ No newline at end of file diff --git a/bin/reportlab/graphics/charts/utils3d.py b/bin/reportlab/graphics/charts/utils3d.py new file mode 100644 index 00000000000..ca6d0f8d3d5 --- /dev/null +++ b/bin/reportlab/graphics/charts/utils3d.py @@ -0,0 +1,233 @@ +from reportlab.lib import colors +from reportlab.lib.attrmap import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import Group, Drawing, Ellipse, Wedge, String, STATE_DEFAULTS, Polygon, Line + +def _getShaded(col,shd=None,shading=0.1): + if shd is None: + from reportlab.lib.colors import Blacker + if col: shd = Blacker(col,1-shading) + return shd + +def _getLit(col,shd=None,lighting=0.1): + if shd is None: + from reportlab.lib.colors import Whiter + if col: shd = Whiter(col,1-lighting) + return shd + + +def _draw_3d_bar(G, x1, x2, y0, yhigh, xdepth, ydepth, + fillColor=None, fillColorShaded=None, + strokeColor=None, strokeWidth=1, shading=0.1): + fillColorShaded = _getShaded(fillColor,None,shading) + fillColorShadedTop = _getShaded(fillColor,None,shading/2.0) + + def _add_3d_bar(x1, x2, y1, y2, xoff, yoff, + G=G,strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor): + G.add(Polygon((x1,y1, x1+xoff,y1+yoff, x2+xoff,y2+yoff, x2,y2), + strokeWidth=strokeWidth, strokeColor=strokeColor, fillColor=fillColor,strokeLineJoin=1)) + + usd = max(y0, yhigh) + if xdepth or ydepth: + if y0!=yhigh: #non-zero height + _add_3d_bar( x2, x2, y0, yhigh, xdepth, ydepth, fillColor=fillColorShaded) #side + + _add_3d_bar(x1, x2, usd, usd, xdepth, ydepth, fillColor=fillColorShadedTop) #top + + G.add(Polygon((x1,y0,x2,y0,x2,yhigh,x1,yhigh), + strokeColor=strokeColor, strokeWidth=strokeWidth, fillColor=fillColor,strokeLineJoin=1)) #front + + if xdepth or ydepth: + G.add(Line( x1, usd, x2, usd, strokeWidth=strokeWidth, strokeColor=strokeColor or fillColorShaded)) + +class _YStrip: + def __init__(self,y0,y1, slope, fillColor, fillColorShaded, shading=0.1): + self.y0 = y0 + self.y1 = y1 + self.slope = slope + self.fillColor = fillColor + self.fillColorShaded = _getShaded(fillColor,fillColorShaded,shading) + +def _ystrip_poly( x0, x1, y0, y1, xoff, yoff): + return [x0,y0,x0+xoff,y0+yoff,x1+xoff,y1+yoff,x1,y1] + + +def _make_3d_line_info( G, x0, x1, y0, y1, z0, z1, + theta_x, theta_y, + fillColor, fillColorShaded=None, tileWidth=1, + strokeColor=None, strokeWidth=None, strokeDashArray=None, + shading=0.1): + zwidth = abs(z1-z0) + xdepth = zwidth*theta_x + ydepth = zwidth*theta_y + depth_slope = xdepth==0 and 1e150 or -ydepth/float(xdepth) + + x = float(x1-x0) + slope = x==0 and 1e150 or (y1-y0)/x + + c = slope>depth_slope and _getShaded(fillColor,fillColorShaded,shading) or fillColor + zy0 = z0*theta_y + zx0 = z0*theta_x + + tileStrokeWidth = 0.6 + if tileWidth is None: + D = [(x1,y1)] + else: + T = ((y1-y0)**2+(x1-x0)**2)**0.5 + tileStrokeWidth *= tileWidth + if Tself.x1: return 1 + if o.s==self.s and o.i in (self.i-1,self.i+1): return + a = self.a + b = self.b + oa = o.a + ob = o.b + det = ob*a - oa*b + if -1e-81 or ou<0 or ou>1: return + x = x0 + u*a + y = self.y0 + u*b + if _ZERO=small: a(seg) + S.sort(_segCmp) + I = [] + n = len(S) + for i in xrange(0,n-1): + s = S[i] + for j in xrange(i+1,n): + if s.intersect(S[j],I)==1: break + I.sort() + return I + +if __name__=='__main__': + from reportlab.graphics.shapes import Drawing + from reportlab.lib.colors import lightgrey, pink + D = Drawing(300,200) + _draw_3d_bar(D, 10, 20, 10, 50, 5, 5, fillColor=lightgrey, strokeColor=pink) + _draw_3d_bar(D, 30, 40, 10, 45, 5, 5, fillColor=lightgrey, strokeColor=pink) + + D.save(formats=['pdf'],outDir='.',fnRoot='_draw_3d_bar') + + print find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(.2666666667,0.4),(0.1,0.4),(0.1,0.2),(0,0),(1,1)],[(0,1),(0.4,0.1),(1,0.1)]]) + print find_intersections([[(0.1, 0.2), (0.1, 0.4)], [(0, 1), (0.4, 0.1)]]) + print find_intersections([[(0.2, 0.4), (0.1, 0.4)], [(0.1, 0.8), (0.4, 0.1)]]) + print find_intersections([[(0,0),(1,1)],[(0.4,0.1),(1,0.1)]]) + print find_intersections([[(0,0.5),(1,0.5),(0.5,0),(0.5,1)],[(0,0),(1,1)],[(0.1,0.8),(0.4,0.1),(1,0.1)]]) diff --git a/bin/reportlab/graphics/renderPDF.py b/bin/reportlab/graphics/renderPDF.py new file mode 100644 index 00000000000..e1da60c0896 --- /dev/null +++ b/bin/reportlab/graphics/renderPDF.py @@ -0,0 +1,360 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderPDF.py +# renderPDF - draws Drawings onto a canvas +"""Usage: + import renderpdf + renderpdf.draw(drawing, canvas, x, y) +Execute the script to see some test drawings. +changed +""" +__version__=''' $Id: renderPDF.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' + +from reportlab.graphics.shapes import * +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.utils import getStringIO +from reportlab import rl_config +from renderbase import Renderer, StateTracker, getStateDelta, renderScaledDrawing + +# the main entry point for users... +def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_): + """As it says""" + R = _PDFRenderer() + R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + +class _PDFRenderer(Renderer): + """This draws onto a PDF document. It needs to be a class + rather than a function, as some PDF-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._stroke = 0 + self._fill = 0 + self._tracker = StateTracker() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + #print "pdf:drawNode", self + #if node.__class__ is Wedge: stop + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.restoreState() + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.width, rect.height, + stroke=self._stroke, + fill=self._fill + ) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.width, rect.height, rect.rx, + fill=self._fill, + stroke=self._stroke + ) + + def drawImage(self, image): + # currently not implemented in other renderers + if image.path and os.path.exists(image.path): + self._canvas.drawInlineImage( + image.path, + image.x, image.y, + image.width, image.height + ) + + def drawLine(self, line): + if self._stroke: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + def drawCircle(self, circle): + self._canvas.circle( + circle.cx, circle.cy, circle.r, + fill=self._fill, + stroke=self._stroke + ) + + def drawPolyLine(self, polyline): + if self._stroke: + assert len(polyline.points) >= 2, 'Polyline must have 2 or more points' + head, tail = polyline.points[0:2], polyline.points[2:], + path = self._canvas.beginPath() + path.moveTo(head[0], head[1]) + for i in range(0, len(tail), 2): + path.lineTo(tail[i], tail[i+1]) + self._canvas.drawPath(path) + + def drawWedge(self, wedge): + centerx, centery, radius, startangledegrees, endangledegrees = \ + wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees + yradius, radius1, yradius1 = wedge._xtraRadii() + if yradius is None: yradius = radius + angle = endangledegrees-startangledegrees + path = self._canvas.beginPath() + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + path.moveTo(centerx, centery) + path.arcTo(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, angle) + else: + path.arc(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, angle) + path.arcTo(centerx-radius1, centery-yradius1, centerx+radius1, centery+yradius1, + endangledegrees, -angle) + path.close() + self._canvas.drawPath(path, + fill=self._fill, + stroke=self._stroke) + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2,fill=self._fill,stroke=self._stroke) + + def drawPolygon(self, polygon): + assert len(polygon.points) >= 2, 'Polyline must have 2 or more points' + head, tail = polygon.points[0:2], polygon.points[2:], + path = self._canvas.beginPath() + path.moveTo(head[0], head[1]) + for i in range(0, len(tail), 2): + path.lineTo(tail[i], tail[i+1]) + path.close() + self._canvas.drawPath( + path, + stroke=self._stroke, + fill=self._fill + ) + + def drawString(self, stringObj): + if self._fill: + S = self._tracker.getState() + text_anchor, x, y, text, enc = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text, stringObj.encoding + if not text_anchor in ['start','inherited']: + font, font_size = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font, font_size, enc) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for textAnchor '+str(text_anchor) + t = self._canvas.beginText(x,y) + t.textLine(text) + self._canvas.drawText(t) + + def drawPath(self, path): + from reportlab.graphics.shapes import _renderPath + pdfPath = self._canvas.beginPath() + drawFuncs = (pdfPath.moveTo, pdfPath.lineTo, pdfPath.curveTo, pdfPath.close) + isClosed = _renderPath(path, drawFuncs) + if isClosed: + fill = self._fill + else: + fill = 0 + if path.isClipPath: + self._canvas.clipPath(pdfPath, fill=fill, stroke=self._stroke) + else: + self._canvas.drawPath(pdfPath, + fill=fill, + stroke=self._stroke) + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the PDF operators + needed to set those properties""" + for key, value in delta.items(): + if key == 'transform': + self._canvas.transform(value[0], value[1], value[2], + value[3], value[4], value[5]) + elif key == 'strokeColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + if value is None: + self._stroke = 0 + else: + self._stroke = 1 + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) +# elif key == 'stroke_dasharray': +# self._canvas.setDash(array=value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() + elif key == 'fillColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + if value is None: + self._fill = 0 + else: + self._fill = 1 + self._canvas.setFillColor(value) + elif key in ['fontSize', 'fontName']: + # both need setting together in PDF + # one or both might be in the deltas, + # so need to get whichever is missing + fontname = delta.get('fontName', self._canvas._fontname) + fontsize = delta.get('fontSize', self._canvas._fontsize) + self._canvas.setFont(fontname, fontsize) + +from reportlab.platypus import Flowable +class GraphicsFlowable(Flowable): + """Flowable wrapper around a Pingo drawing""" + def __init__(self, drawing): + self.drawing = drawing + self.width = self.drawing.width + self.height = self.drawing.height + + def draw(self): + draw(self.drawing, self.canv, 0, 0) + +def drawToFile(d, fn, msg="", showBoundary=rl_config._unset_, autoSize=1): + """Makes a one-page PDF with just the drawing. + + If autoSize=1, the PDF will be the same size as + the drawing; if 0, it will place the drawing on + an A4 page with a title above it - possibly overflowing + if too big.""" + d = renderScaledDrawing(d) + c = Canvas(fn) + c.setFont('Times-Roman', 36) + c.drawString(80, 750, msg) + c.setTitle(msg) + + if autoSize: + c.setPageSize((d.width, d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + else: + #show with a title + c.setFont('Times-Roman', 12) + y = 740 + i = 1 + y = y - d.height + draw(d, c, 80, y, showBoundary=showBoundary) + + c.showPage() + c.save() + if sys.platform=='mac' and not hasattr(fn, "write"): + try: + import macfs, macostools + macfs.FSSpec(fn).SetCreatorType("CARO", "PDF ") + macostools.touched(fn) + except: + pass + + +def drawToString(d, msg="", showBoundary=rl_config._unset_,autoSize=1): + "Returns a PDF as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, msg=msg, showBoundary=showBoundary,autoSize=autoSize) + return s.getvalue() + + +######################################################### +# +# test code. First, defin a bunch of drawings. +# Routine to draw them comes at the end. +# +######################################################### + + +def test(): + c = Canvas('renderPDF.pdf') + c.setFont('Times-Roman', 36) + c.drawString(80, 750, 'Graphics Test') + + # print all drawings and their doc strings from the test + # file + + #grab all drawings from the test module + from reportlab.graphics import testshapes + drawings = [] + for funcname in dir(testshapes): + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') #execute it + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + #print in a loop, with their doc strings + c.setFont('Times-Roman', 12) + y = 740 + i = 1 + for (drawing, docstring) in drawings: + assert (docstring is not None), "Drawing %d has no docstring!" % i + if y < 300: #allows 5-6 lines of text + c.showPage() + y = 740 + # draw a title + y = y - 30 + c.setFont('Times-BoldItalic',12) + c.drawString(80, y, 'Drawing %d' % i) + c.setFont('Times-Roman',12) + y = y - 14 + textObj = c.beginText(80, y) + textObj.textLines(docstring) + c.drawText(textObj) + y = textObj.getY() + y = y - drawing.height + draw(drawing, c, 80, y) + i = i + 1 + if y!=740: c.showPage() + + c.save() + print 'saved renderPDF.pdf' + +##def testFlowable(): +## """Makes a platypus document""" +## from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer +## from reportlab.lib.styles import getSampleStyleSheet +## styles = getSampleStyleSheet() +## styNormal = styles['Normal'] +## +## doc = SimpleDocTemplate('test_flowable.pdf') +## story = [] +## story.append(Paragraph("This sees is a drawing can work as a flowable", styNormal)) +## +## import testdrawings +## drawings = [] +## +## for funcname in dir(testdrawings): +## if funcname[0:10] == 'getDrawing': +## drawing = eval('testdrawings.' + funcname + '()') #execute it +## docstring = eval('testdrawings.' + funcname + '.__doc__') +## story.append(Paragraph(docstring, styNormal)) +## story.append(Spacer(18,18)) +## story.append(drawing) +## story.append(Spacer(36,36)) +## +## doc.build(story) +## print 'saves test_flowable.pdf' + +if __name__=='__main__': + test() + #testFlowable() diff --git a/bin/reportlab/graphics/renderPM.py b/bin/reportlab/graphics/renderPM.py new file mode 100644 index 00000000000..35960a25b75 --- /dev/null +++ b/bin/reportlab/graphics/renderPM.py @@ -0,0 +1,663 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/graphics/Csrc/renderPM/renderP.py +__version__=''' $Id: renderPM.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' +"""Usage: + from reportlab.graphics import renderPM + renderPM.drawToFile(drawing,filename,fmt='GIF',configPIL={....}) +Other functions let you create a PM drawing as string or into a PM buffer. +Execute the script to see some test drawings.""" + +from reportlab.graphics.shapes import * +from reportlab.graphics.renderbase import StateTracker, getStateDelta, renderScaledDrawing +from reportlab.pdfbase.pdfmetrics import getFont, unicode2T1 +from math import sin, cos, pi, ceil +from reportlab.lib.utils import getStringIO, open_and_read +from reportlab import rl_config + +class RenderPMError(Exception): + pass + +import string, os, sys + +try: + import _renderPM +except ImportError, errMsg: + raise ImportError, "No module named _renderPM\n" + \ + (str(errMsg)!='No module named _renderPM' and "it may be the wrong version or badly installed!" or + "see http://www.reportlab.org/rl_addons.html") + +from types import TupleType, ListType +_SeqTypes = (TupleType,ListType) + +def _getImage(): + try: + from PIL import Image + except ImportError: + import Image + return Image + +def Color2Hex(c): + #assert isinstance(colorobj, colors.Color) #these checks don't work well RGB + if c: return ((0xFF&int(255*c.red)) << 16) | ((0xFF&int(255*c.green)) << 8) | (0xFF&int(255*c.blue)) + return c + +# the main entry point for users... +def draw(drawing, canvas, x, y, showBoundary=rl_config._unset_): + """As it says""" + R = _PMRenderer() + R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + +from reportlab.graphics.renderbase import Renderer +class _PMRenderer(Renderer): + """This draws onto a pix map image. It needs to be a class + rather than a function, as some image-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._tracker = StateTracker() + + def pop(self): + self._tracker.pop() + self.applyState() + + def push(self,node): + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyState() + + def applyState(self): + s = self._tracker.getState() + self._canvas.ctm = s['ctm'] + self._canvas.strokeWidth = s['strokeWidth'] + self._canvas.strokeColor = Color2Hex(s['strokeColor']) + self._canvas.lineCap = s['strokeLineCap'] + self._canvas.lineJoin = s['strokeLineJoin'] + da = s['strokeDashArray'] + da = da and (0,da) or None + self._canvas.dashArray = da + self._canvas.fillColor = Color2Hex(s['fillColor']) + self._canvas.setFont(s['fontName'], s['fontSize']) + + def initState(self,x,y): + deltas = STATE_DEFAULTS.copy() + deltas['transform'] = self._canvas._baseCTM[0:4]+(x,y) + self._tracker.push(deltas) + self.applyState() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + + #apply state changes + self.push(node) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + # restore the state + self.pop() + + def drawRect(self, rect): + c = self._canvas + if rect.rx == rect.ry == 0: + #plain old rectangle, draw clockwise (x-axis to y-axis) direction + c.rect(rect.x,rect.y, rect.width, rect.height) + else: + c.roundRect(rect.x,rect.y, rect.width, rect.height, rect.rx, rect.ry) + + def drawLine(self, line): + self._canvas.line(line.x1,line.y1,line.x2,line.y2) + + def drawImage(self, image): + if image.path and os.path.exists(image.path): + if type(image.path) is type(''): + im = _getImage().open(image.path).convert('RGB') + else: + im = image.path.convert('RGB') + srcW, srcH = im.size + dstW, dstH = image.width, image.height + if dstW is None: dstW = srcW + if dstH is None: dstH = srcH + self._canvas._aapixbuf( + image.x, image.y, dstW, dstH, + im.tostring(), srcW, srcH, 3, + ) + + def drawCircle(self, circle): + c = self._canvas + c.circle(circle.cx,circle.cy, circle.r) + c.fillstrokepath() + + def drawPolyLine(self, polyline, _doClose=0): + P = polyline.points + assert len(P) >= 2, 'Polyline must have 1 or more points' + c = self._canvas + c.pathBegin() + c.moveTo(P[0], P[1]) + for i in range(2, len(P), 2): + c.lineTo(P[i], P[i+1]) + if _doClose: + c.pathClose() + c.pathFill() + c.pathStroke() + + def drawEllipse(self, ellipse): + c=self._canvas + c.ellipse(ellipse.cx, ellipse.cy, ellipse.rx,ellipse.ry) + c.fillstrokepath() + + def drawPolygon(self, polygon): + self.drawPolyLine(polygon,_doClose=1) + + def drawString(self, stringObj): + canv = self._canvas + fill = canv.fillColor + if fill is not None: + S = self._tracker.getState() + text_anchor = S['textAnchor'] + fontName = S['fontName'] + fontSize = S['fontSize'] + font = getFont(fontName) + text = stringObj.text + x = stringObj.x + y = stringObj.y + if not text_anchor in ['start','inherited']: + textLen = stringWidth(text, fontName,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for textAnchor '+str(text_anchor) + if getattr(font,'_dynamicFont',None): + if isinstance(text,unicode): text = text.encode('utf8') + canv.drawString(x,y,text) + else: + fc = font + if not isinstance(text,unicode): + try: + text = text.decode('utf8') + except UnicodeDecodeError,e: + i,j = e.args[2:4] + raise UnicodeDecodeError(*(e.args[:4]+('%s\n%s-->%s<--%s' % (e.args[4],text[i-10:i],text[i:j],text[j:j+10]),))) + + FT = unicode2T1(text,[font]+font.substitutionFonts) + n = len(FT) + nm1 = n-1 + wscale = 0.001*fontSize + for i in xrange(n): + f, t = FT[i] + if f!=fc: + canv.setFont(f.fontName,fontSize) + fc = f + canv.drawString(x,y,t) + if i!=nm1: + x += wscale*sum(map(f.widths.__getitem__,map(ord,t))) + if font!=fc: + canv.setFont(fontName,fontSize) + + def drawPath(self, path): + c = self._canvas + if path is EmptyClipPath: + del c._clipPaths[-1] + if c._clipPaths: + P = c._clipPaths[-1] + icp = P.isClipPath + P.isClipPath = 1 + self.drawPath(P) + P.isClipPath = icp + else: + c.clipPathClear() + return + c.pathBegin() + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.pathClose) + from reportlab.graphics.shapes import _renderPath + isClosed = _renderPath(path, drawFuncs) + if path.isClipPath: + c.clipPathSet() + c._clipPaths.append(path) + else: + if isClosed: c.pathFill() + c.pathStroke() + +def _setFont(gs,fontName,fontSize): + try: + gs.setFont(fontName,fontSize) + except _renderPM.Error, errMsg: + if errMsg.args[0]!="Can't find font!": raise + #here's where we try to add a font to the canvas + try: + f = getFont(fontName) + if _renderPM._version<='0.98': #added reader arg in 0.99 + _renderPM.makeT1Font(fontName,f.face.findT1File(),f.encoding.vector) + else: + _renderPM.makeT1Font(fontName,f.face.findT1File(),f.encoding.vector,open_and_read) + except: + s1, s2 = map(str,sys.exc_info()[:2]) + raise RenderPMError, "Can't setFont(%s) missing the T1 files?\nOriginally %s: %s" % (fontName,s1,s2) + gs.setFont(fontName,fontSize) + +def _convert2pilp(im): + Image = _getImage() + return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE) + +def _saveAsPICT(im,fn,fmt,transparent=None): + im = _convert2pilp(im) + cols, rows = im.size + #s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette(),transparent is not None and Color2Hex(transparent) or -1) + s = _renderPM.pil2pict(cols,rows,im.tostring(),im.im.getpalette()) + if not hasattr(fn,'write'): + open(os.path.splitext(fn)[0]+'.'+string.lower(fmt),'wb').write(s) + if os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(fn,ext='PICT') + else: + fn.write(s) + +BEZIER_ARC_MAGIC = 0.5522847498 #constant for drawing circular arcs w/ Beziers +class PMCanvas: + def __init__(self,w,h,dpi=72,bg=0xffffff,configPIL=None): + '''configPIL dict is passed to image save method''' + scale = dpi/72.0 + w = int(w*scale+0.5) + h = int(h*scale+0.5) + self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg) + self.__dict__['_bg'] = bg + self.__dict__['_baseCTM'] = (scale,0,0,scale,0,0) + self.__dict__['_clipPaths'] = [] + self.__dict__['configPIL'] = configPIL + self.__dict__['_dpi'] = dpi + self.ctm = self._baseCTM + + def _drawTimeResize(self,w,h,bg=None): + if bg is None: bg = self._bg + self._drawing.width, self._drawing.height = w, h + A = {'ctm':None, 'strokeWidth':None, 'strokeColor':None, 'lineCap':None, 'lineJoin':None, 'dashArray':None, 'fillColor':None} + gs = self._gs + fN,fS = gs.fontName, gs.fontSize + for k in A.keys(): + A[k] = getattr(gs,k) + del gs, self._gs + gs = self.__dict__['_gs'] = _renderPM.gstate(w,h,bg=bg) + for k in A.keys(): + setattr(self,k,A[k]) + gs.setFont(fN,fS) + + def toPIL(self): + im = _getImage().new('RGB', size=(self._gs.width, self._gs.height)) + im.fromstring(self._gs.pixBuf) + return im + + def saveToFile(self,fn,fmt=None): + im = self.toPIL() + if fmt is None: + if type(fn) is not StringType: + raise ValueError, "Invalid type '%s' for fn when fmt is None" % type(fn) + fmt = os.path.splitext(fn)[1] + if fmt.startswith('.'): fmt = fmt[1:] + configPIL = self.configPIL or {} + fmt = string.upper(fmt) + if fmt in ['GIF']: + im = _convert2pilp(im) + elif fmt in ['PCT','PICT']: + return _saveAsPICT(im,fn,fmt,transparent=configPIL.get('transparent',None)) + elif fmt in ['PNG','TIFF','BMP', 'PPM', 'TIF']: + if fmt=='TIF': fmt = 'TIFF' + if fmt=='PNG': + try: + from PIL import PngImagePlugin + except ImportError: + import PngImagePlugin + elif fmt=='BMP': + try: + from PIL import BmpImagePlugin + except ImportError: + import BmpImagePlugin + elif fmt in ('JPG','JPEG'): + fmt = 'JPEG' + else: + raise RenderPMError,"Unknown image kind %s" % fmt + if fmt=='TIFF': + tc = configPIL.get('transparent',None) + if tc: + from PIL import ImageChops, Image + T = 768*[0] + for o, c in zip((0,256,512), tc.bitmap_rgb()): + T[o+c] = 255 + #if type(fn) is type(''): ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])).save(fn+'_mask.gif','GIF') + im = Image.merge('RGBA', im.split()+(ImageChops.invert(im.point(T).convert('L').point(255*[0]+[255])),)) + #if type(fn) is type(''): im.save(fn+'_masked.gif','GIF') + for a,d in ('resolution',self._dpi),('resolution unit','inch'): + configPIL[a] = configPIL.get(a,d) + apply(im.save,(fn,fmt),configPIL) + if not hasattr(fn,'write') and os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(fn,ext=fmt) + + def saveToString(self,fmt='GIF'): + s = getStringIO() + self.saveToFile(s,fmt=fmt) + return s.getvalue() + + def _saveToBMP(self,f): + ''' + Niki Spahiev, , asserts that this is a respectable way to get BMP without PIL + f is a file like object to which the BMP is written + ''' + import struct + gs = self._gs + pix, width, height = gs.pixBuf, gs.width, gs.height + f.write(struct.pack('=2sLLLLLLhh24x','BM',len(pix)+54,0,54,40,width,height,1,24)) + rowb = width * 3 + for o in range(len(pix),0,-rowb): + f.write(pix[o-rowb:o]) + f.write( '\0' * 14 ) + + def setFont(self,fontName,fontSize,leading=None): + _setFont(self._gs,fontName,fontSize) + + def __setattr__(self,name,value): + setattr(self._gs,name,value) + + def __getattr__(self,name): + return getattr(self._gs,name) + + def fillstrokepath(self,stroke=1,fill=1): + if fill: self.pathFill() + if stroke: self.pathStroke() + + def _bezierArcSegmentCCW(self, cx,cy, rx,ry, theta0, theta1): + """compute the control points for a bezier arc with theta1-theta0 <= 90. + Points are computed for an arc with angle theta increasing in the + counter-clockwise (CCW) direction. returns a tuple with starting point + and 3 control points of a cubic bezier curve for the curvto opertator""" + + # Requires theta1 - theta0 <= 90 for a good approximation + assert abs(theta1 - theta0) <= 90 + cos0 = cos(pi*theta0/180.0) + sin0 = sin(pi*theta0/180.0) + x0 = cx + rx*cos0 + y0 = cy + ry*sin0 + + cos1 = cos(pi*theta1/180.0) + sin1 = sin(pi*theta1/180.0) + + x3 = cx + rx*cos1 + y3 = cy + ry*sin1 + + dx1 = -rx * sin0 + dy1 = ry * cos0 + + #from pdfgeom + halfAng = pi*(theta1-theta0)/(2.0 * 180.0) + k = abs(4.0 / 3.0 * (1.0 - cos(halfAng) ) /(sin(halfAng)) ) + x1 = x0 + dx1 * k + y1 = y0 + dy1 * k + + dx2 = -rx * sin1 + dy2 = ry * cos1 + + x2 = x3 - dx2 * k + y2 = y3 - dy2 * k + return ((x0,y0), ((x1,y1), (x2,y2), (x3,y3)) ) + + def bezierArcCCW(self, cx,cy, rx,ry, theta0, theta1): + """return a set of control points for Bezier approximation to an arc + with angle increasing counter clockwise. No requirement on |theta1-theta0| <= 90 + However, it must be true that theta1-theta0 > 0.""" + + # I believe this is also clockwise + # pretty much just like Robert Kern's pdfgeom.BezierArc + angularExtent = theta1 - theta0 + # break down the arc into fragments of <=90 degrees + if abs(angularExtent) <= 90.0: # we just need one fragment + angleList = [(theta0,theta1)] + else: + Nfrag = int( ceil( abs(angularExtent)/90.) ) + fragAngle = float(angularExtent)/ Nfrag # this could be negative + angleList = [] + for ii in range(Nfrag): + a = theta0 + ii * fragAngle + b = a + fragAngle # hmm.. is I wonder if this is precise enought + angleList.append((a,b)) + + ctrlpts = [] + for (a,b) in angleList: + if not ctrlpts: # first time + [(x0,y0), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b) + ctrlpts.append(pts) + else: + [(tmpx,tmpy), pts] = self._bezierArcSegmentCCW(cx,cy, rx,ry, a,b) + ctrlpts.append(pts) + return ((x0,y0), ctrlpts) + + def addEllipsoidalArc(self, cx,cy, rx, ry, ang1, ang2): + """adds an ellisesoidal arc segment to a path, with an ellipse centered + on cx,cy and with radii (major & minor axes) rx and ry. The arc is + drawn in the CCW direction. Requires: (ang2-ang1) > 0""" + + ((x0,y0), ctrlpts) = self.bezierArcCCW(cx,cy, rx,ry,ang1,ang2) + + self.lineTo(x0,y0) + for ((x1,y1), (x2,y2),(x3,y3)) in ctrlpts: + self.curveTo(x1,y1,x2,y2,x3,y3) + + def drawCentredString(self, x, y, text, text_anchor='middle'): + if self.fillColor is not None: + textLen = stringWidth(text, self.fontName,self.fontSize) + if text_anchor=='end': + x -= textLen + elif text_anchor=='middle': + x -= textLen/2. + self.drawString(x,y,text) + + def drawRightString(self, text, x, y): + self.drawCentredString(text,x,y,text_anchor='end') + + def line(self,x1,y1,x2,y2): + if self.strokeColor is not None: + self.pathBegin() + self.moveTo(x1,y1) + self.lineTo(x2,y2) + self.pathStroke() + + def rect(self,x,y,width,height,stroke=1,fill=1): + self.pathBegin() + self.moveTo(x, y) + self.lineTo(x+width, y) + self.lineTo(x+width, y + height) + self.lineTo(x, y + height) + self.pathClose() + self.fillstrokepath(stroke=stroke,fill=fill) + + def roundRect(self, x, y, width, height, rx,ry): + """rect(self, x, y, width, height, rx,ry): + Draw a rectangle if rx or rx and ry are specified the corners are + rounded with ellipsoidal arcs determined by rx and ry + (drawn in the counter-clockwise direction)""" + if rx==0: rx = ry + if ry==0: ry = rx + x2 = x + width + y2 = y + height + self.pathBegin() + self.moveTo(x+rx,y) + self.addEllipsoidalArc(x2-rx, y+ry, rx, ry, 270, 360 ) + self.addEllipsoidalArc(x2-rx, y2-ry, rx, ry, 0, 90) + self.addEllipsoidalArc(x+rx, y2-ry, rx, ry, 90, 180) + self.addEllipsoidalArc(x+rx, y+ry, rx, ry, 180, 270) + self.pathClose() + self.fillstrokepath() + + def circle(self, cx, cy, r): + "add closed path circle with center cx,cy and axes r: counter-clockwise orientation" + self.ellipse(cx,cy,r,r) + + def ellipse(self, cx,cy,rx,ry): + """add closed path ellipse with center cx,cy and axes rx,ry: counter-clockwise orientation + (remember y-axis increases downward) """ + self.pathBegin() + # first segment + x0 = cx + rx # (x0,y0) start pt + y0 = cy + + x3 = cx # (x3,y3) end pt of arc + y3 = cy-ry + + x1 = cx+rx + y1 = cy-ry*BEZIER_ARC_MAGIC + + x2 = x3 + rx*BEZIER_ARC_MAGIC + y2 = y3 + self.moveTo(x0, y0) + self.curveTo(x1,y1,x2,y2,x3,y3) + # next segment + x0 = x3 + y0 = y3 + + x3 = cx-rx + y3 = cy + + x1 = cx-rx*BEZIER_ARC_MAGIC + y1 = cy-ry + + x2 = x3 + y2 = cy- ry*BEZIER_ARC_MAGIC + self.curveTo(x1,y1,x2,y2,x3,y3) + # next segment + x0 = x3 + y0 = y3 + + x3 = cx + y3 = cy+ry + + x1 = cx-rx + y1 = cy+ry*BEZIER_ARC_MAGIC + + x2 = cx -rx*BEZIER_ARC_MAGIC + y2 = cy+ry + self.curveTo(x1,y1,x2,y2,x3,y3) + #last segment + x0 = x3 + y0 = y3 + + x3 = cx+rx + y3 = cy + + x1 = cx+rx*BEZIER_ARC_MAGIC + y1 = cy+ry + + x2 = cx+rx + y2 = cy+ry*BEZIER_ARC_MAGIC + self.curveTo(x1,y1,x2,y2,x3,y3) + self.pathClose() + + def saveState(self): + '''do nothing for compatibility''' + pass + + def setFillColor(self,aColor): + self.fillColor = Color2Hex(aColor) + + def setStrokeColor(self,aColor): + self.strokeColor = Color2Hex(aColor) + + restoreState = saveState + + # compatibility routines + def setLineCap(self,cap): + self.lineCap = cap + + def setLineWidth(self,width): + self.strokeWidth = width + +def drawToPMCanvas(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + d = renderScaledDrawing(d) + c = PMCanvas(d.width, d.height, dpi=dpi, bg=bg, configPIL=configPIL) + draw(d, c, 0, 0, showBoundary=showBoundary) + return c + +def drawToPIL(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + return drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary).toPIL() + +def drawToPILP(d, dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + Image = _getImage() + im = drawToPIL(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary) + return im.convert("P", dither=Image.NONE, palette=Image.ADAPTIVE) + +def drawToFile(d,fn,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + '''create a pixmap and draw drawing, d to it then save as a file + configPIL dict is passed to image save method''' + c = drawToPMCanvas(d, dpi=dpi, bg=bg, configPIL=configPIL, showBoundary=showBoundary) + c.saveToFile(fn,fmt) + +def drawToString(d,fmt='GIF', dpi=72, bg=0xffffff, configPIL=None, showBoundary=rl_config._unset_): + s = getStringIO() + drawToFile(d,s,fmt=fmt, dpi=dpi, bg=bg, configPIL=configPIL) + return s.getvalue() + +save = drawToFile + +def test(): + def ext(x): + if x=='tiff': x='tif' + return x + #grab all drawings from the test module and write out. + #make a page of links in HTML to assist viewing. + import os + from reportlab.graphics import testshapes + getAllTestDrawings = testshapes.getAllTestDrawings + drawings = [] + if not os.path.isdir('pmout'): + os.mkdir('pmout') + htmlTop = """renderPM output results + +

renderPM results of output

+ """ + htmlBottom = """ + + """ + html = [htmlTop] + + i = 0 + #print in a loop, with their doc strings + for (drawing, docstring, name) in getAllTestDrawings(doTTF=hasattr(_renderPM,'ft_get_face')): + fnRoot = 'renderPM%d' % i + if 1 or i==10: + w = int(drawing.width) + h = int(drawing.height) + html.append('

Drawing %s %d

\n
%s
' % (name, i, docstring)) + + for k in ['gif','tiff', 'png', 'jpg', 'pct']: + if k in ['gif','png','jpg','pct']: + html.append('

%s format

\n' % string.upper(k)) + try: + filename = '%s.%s' % (fnRoot, ext(k)) + fullpath = os.path.join('pmout', filename) + if os.path.isfile(fullpath): + os.remove(fullpath) + if k=='pct': + from reportlab.lib.colors import white + drawToFile(drawing,fullpath,fmt=k,configPIL={'transparent':white}) + else: + drawToFile(drawing,fullpath,fmt=k) + if k in ['gif','png','jpg']: + html.append('
\n' % filename) + print 'wrote',fullpath + except AttributeError: + print 'Problem drawing %s file'%k + raise + if os.environ.get('RL_NOEPSPREVIEW','0')=='1': drawing.__dict__['preview'] = 0 + drawing.save(formats=['eps','pdf'],outDir='pmout',fnRoot=fnRoot) + i = i + 1 + #if i==10: break + html.append(htmlBottom) + htmlFileName = os.path.join('pmout', 'index.html') + open(htmlFileName, 'w').writelines(html) + if sys.platform=='mac': + from reportlab.lib.utils import markfilename + markfilename(htmlFileName,ext='HTML') + print 'wrote %s' % htmlFileName + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/renderPS.py b/bin/reportlab/graphics/renderPS.py new file mode 100644 index 00000000000..b6e11828fc3 --- /dev/null +++ b/bin/reportlab/graphics/renderPS.py @@ -0,0 +1,871 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderPS.py +__version__=''' $Id: renderPS.py 2808 2006-03-15 16:47:27Z rgbecker $ ''' +import string, types +from reportlab.pdfbase.pdfmetrics import getFont, stringWidth # for font info +from reportlab.lib.utils import fp_str, getStringIO +from reportlab.lib.colors import black +from reportlab.graphics.renderbase import Renderer, StateTracker, getStateDelta, renderScaledDrawing +from reportlab.graphics.shapes import STATE_DEFAULTS +import math +from types import StringType +from operator import getitem +from reportlab import rl_config + + +# we need to create encoding vectors for each font we use, or they will +# come out in Adobe's old StandardEncoding, which NOBODY uses. +PS_WinAnsiEncoding=""" +/RE { %def + findfont begin + currentdict dup length dict begin + { %forall + 1 index /FID ne { def } { pop pop } ifelse + } forall + /FontName exch def dup length 0 ne { %if + /Encoding Encoding 256 array copy def + 0 exch { %forall + dup type /nametype eq { %ifelse + Encoding 2 index 2 index put + pop 1 add + }{ %else + exch pop + } ifelse + } forall + } if pop + currentdict dup end end + /FontName get exch definefont pop +} bind def + +/WinAnsiEncoding [ + 39/quotesingle 96/grave 128/euro 130/quotesinglbase/florin/quotedblbase + /ellipsis/dagger/daggerdbl/circumflex/perthousand + /Scaron/guilsinglleft/OE 145/quoteleft/quoteright + /quotedblleft/quotedblright/bullet/endash/emdash + /tilde/trademark/scaron/guilsinglright/oe/dotlessi + 159/Ydieresis 164/currency 166/brokenbar 168/dieresis/copyright + /ordfeminine 172/logicalnot 174/registered/macron/ring + 177/plusminus/twosuperior/threesuperior/acute/mu + 183/periodcentered/cedilla/onesuperior/ordmasculine + 188/onequarter/onehalf/threequarters 192/Agrave/Aacute + /Acircumflex/Atilde/Adieresis/Aring/AE/Ccedilla + /Egrave/Eacute/Ecircumflex/Edieresis/Igrave/Iacute + /Icircumflex/Idieresis/Eth/Ntilde/Ograve/Oacute + /Ocircumflex/Otilde/Odieresis/multiply/Oslash + /Ugrave/Uacute/Ucircumflex/Udieresis/Yacute/Thorn + /germandbls/agrave/aacute/acircumflex/atilde/adieresis + /aring/ae/ccedilla/egrave/eacute/ecircumflex + /edieresis/igrave/iacute/icircumflex/idieresis + /eth/ntilde/ograve/oacute/ocircumflex/otilde + /odieresis/divide/oslash/ugrave/uacute/ucircumflex + /udieresis/yacute/thorn/ydieresis +] def + +""" + +class PSCanvas: + def __init__(self,size=(300,300), PostScriptLevel=2): + self.width, self.height = size + xtraState = [] + self._xtraState_push = xtraState.append + self._xtraState_pop = xtraState.pop + self.comments = 0 + self.code = [] + self._sep = '\n' + self._strokeColor = self._fillColor = self._lineWidth = \ + self._font = self._fontSize = self._lineCap = \ + self._lineJoin = self._color = None + + + self._fontsUsed = [] # track them as we go + + self.setFont(STATE_DEFAULTS['fontName'],STATE_DEFAULTS['fontSize']) + self.setStrokeColor(STATE_DEFAULTS['strokeColor']) + self.setLineCap(2) + self.setLineJoin(0) + self.setLineWidth(1) + + self.PostScriptLevel=PostScriptLevel + + def comment(self,msg): + if self.comments: self.code.append('%'+msg) + + def drawImage(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version + # select between postscript level 1 or level 2 + if PostScriptLevel==1: + self._drawImageLevel1(image, x1,y1, x2=None,y2=None) + elif PostScriptLevel == 2 : + self._drawImageLevel2(image, x1,y1, x2=None,y2=None) + else : + raise 'PostScriptLevelException' + + def clear(self): + self.code.append('showpage') # ugh, this makes no sense oh well. + + def save(self,f=None): + if not hasattr(f,'write'): + file = open(f,'wb') + else: + file = f + if self.code[-1]!='showpage': self.clear() + self.code.insert(0,'''\ +%%!PS-Adobe-3.0 EPSF-3.0 +%%%%BoundingBox: 0 0 %d %d +%%%% Initialization: +/m {moveto} bind def +/l {lineto} bind def +/c {curveto} bind def + +%s +''' % (self.width,self.height, PS_WinAnsiEncoding)) + + # for each font used, reencode the vectors + fontReencode = [] + for fontName in self._fontsUsed: + fontReencode.append('WinAnsiEncoding /%s /%s RE' % (fontName, fontName)) + self.code.insert(1, string.join(fontReencode, self._sep)) + + file.write(string.join(self.code,self._sep)) + if file is not f: + file.close() + from reportlab.lib.utils import markfilename + markfilename(f,creatorcode='XPR3',filetype='EPSF') + + def saveState(self): + self._xtraState_push((self._fontCodeLoc,)) + self.code.append('gsave') + + def restoreState(self): + self.code.append('grestore') + self._fontCodeLoc, = self._xtraState_pop() + + def stringWidth(self, s, font=None, fontSize=None): + """Return the logical width of the string if it were drawn + in the current font (defaults to self.font).""" + font = font or self._font + fontSize = fontSize or self._fontSize + return stringWidth(s, font, fontSize) + + def setLineCap(self,v): + if self._lineCap!=v: + self._lineCap = v + self.code.append('%d setlinecap'%v) + + def setLineJoin(self,v): + if self._lineJoin!=v: + self._lineJoin = v + self.code.append('%d setlinejoin'%v) + + def setDash(self, array=[], phase=0): + """Two notations. pass two numbers, or an array and phase""" + # copied and modified from reportlab.canvas + psoperation = "setdash" + if type(array) == types.IntType or type(array) == types.FloatType: + self._code.append('[%s %s] 0 %s' % (array, phase, psoperation)) + elif type(array) == types.ListType or type(array) == types.TupleType: + assert phase >= 0, "phase is a length in user space" + textarray = string.join(map(str, array)) + self.code.append('[%s] %s %s' % (textarray, phase, psoperation)) + + def setStrokeColor(self, color): + self._strokeColor = color + self.setColor(color) + + def setColor(self, color): + if self._color!=color: + self._color = color + if color: + if hasattr(color, "cyan"): + self.code.append('%s setcmykcolor' % fp_str(color.cyan, color.magenta, color.yellow, color.black)) + else: + self.code.append('%s setrgbcolor' % fp_str(color.red, color.green, color.blue)) + + def setFillColor(self, color): + self._fillColor = color + self.setColor(color) + + def setLineWidth(self, width): + if width != self._lineWidth: + self._lineWidth = width + self.code.append('%s setlinewidth' % width) + + def setFont(self,font,fontSize,leading=None): + if self._font!=font or self._fontSize!=fontSize: + self._fontCodeLoc = len(self.code) + self._font = font + self._fontSize = fontSize + self.code.append('') + + def line(self, x1, y1, x2, y2): + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.code.append('%s m %s l stroke' % (fp_str(x1, y1), fp_str(x2, y2))) + + def _escape(self, s): + ''' + return a copy of string s with special characters in postscript strings + escaped with backslashes. + Have not handled characters that are converted normally in python strings + i.e. \n -> newline + ''' + str = string.replace(s, chr(0x5C), r'\\' ) + str = string.replace(str, '(', '\(' ) + str = string.replace(str, ')', '\)') + return str + + def drawString(self, x, y, s, angle=0): + if self._fillColor != None: + if not self.code[self._fontCodeLoc]: + psName = getFont(self._font).face.name + self.code[self._fontCodeLoc]='(%s) findfont %s scalefont setfont' % (psName,fp_str(self._fontSize)) + if psName not in self._fontsUsed: + self._fontsUsed.append(psName) + self.setColor(self._fillColor) + s = self._escape(s) +## before inverting... +## if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops +## self.code.append('%s m 1 -1 scale (%s) show 1 -1 scale' % (fp_str(x,y),s)) +## else : # general case, rotated text +## self.code.append('gsave %s %s translate %s rotate' % (x,y,angle)) +## self.code.append('0 0 m 1 -1 scale (%s) show' % s) +## self.code.append('grestore') + if angle == 0 : # do special case of angle = 0 first. Avoids a bunch of gsave/grestore ops + self.code.append('%s m (%s) show ' % (fp_str(x,y),s)) + else : # general case, rotated text + self.code.append('gsave %s %s translate %s rotate' % (x,y,angle)) + self.code.append('0 0 m (%s) show' % s) + self.code.append('grestore') + + def drawCentredString(self, x, y, text, text_anchor='middle'): + if self._fillColor is not None: + textLen = stringWidth(text, self._font,self._fontSize) + if text_anchor=='end': + x -= textLen + elif text_anchor=='middle': + x -= textLen/2. + self.drawString(x,y,text) + + def drawRightString(self, text, x, y): + self.drawCentredString(text,x,y,text_anchor='end') + + def drawCurve(self, x1, y1, x2, y2, x3, y3, x4, y4, closed=0): + codeline = '%s m %s curveto' + data = (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4)) + if self._fillColor != None: + self.setColor(self._fillColor) + self.code.append((codeline % data) + ' eofill') + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.code.append((codeline % data) + + ((closed and ' closepath') or '') + + ' stroke') + + ######################################################################################## + + def rect(self, x1,y1, x2,y2, stroke=1, fill=1): + "Draw a rectangle between x1,y1, and x2,y2" + # Path is drawn in counter-clockwise direction" + + x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py + y1, y2 = min(y1,y2), max(y1, y2) + self.polygon(((x1,y1),(x2,y1),(x2,y2),(x1,y2)), closed=1, stroke=stroke, fill = fill) + + def roundRect(self, x1,y1, x2,y2, rx=8, ry=8): + """Draw a rounded rectangle between x1,y1, and x2,y2, + with corners inset as ellipses with x radius rx and y radius ry. + These should have x10, and ry>0.""" + # Path is drawn in counter-clockwise direction + + x1, x2 = min(x1,x2), max(x1, x2) # from piddle.py + y1, y2 = min(y1,y2), max(y1, y2) + + # Note: arcto command draws a line from current point to beginning of arc + # save current matrix, translate to center of ellipse, scale by rx ry, and draw + # a circle of unit radius in counterclockwise dir, return to original matrix + # arguments are (cx, cy, rx, ry, startAngle, endAngle) + ellipsePath = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s arc setmatrix' + + # choice between newpath and moveTo beginning of arc + # go with newpath for precision, does this violate any assumptions in code??? + rrCode = ['newpath'] # Round Rect code path + # upper left corner ellipse is first + rrCode.append(ellipsePath % (x1+rx, y1+ry, rx, -ry, 90, 180)) + rrCode.append(ellipsePath % (x1+rx, y2-ry, rx, -ry, 180, 270)) + rrCode.append(ellipsePath % (x2-rx, y2-ry, rx, -ry, 270, 360)) + rrCode.append(ellipsePath % (x2-rx, y1+ry, rx, -ry, 0, 90) ) + rrCode.append('closepath') + + self._fillAndStroke(rrCode) + + def ellipse(self, x1,y1, x2,y2): + """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. + These should have x1= 0: + arc='arc' + else: + arc='arcn' + data = (x,y, xScale, yScale, startAng, startAng+extent, arc) + + return codeline % data + + def polygon(self, p, closed=0, stroke=1, fill=1): + assert len(p) >= 2, 'Polygon must have 2 or more points' + + start = p[0] + p = p[1:] + + polyCode = [] + polyCode.append("%s m" % fp_str(start)) + for point in p: + polyCode.append("%s l" % fp_str(point)) + if closed: + polyCode.append("closepath") + + self._fillAndStroke(polyCode,stroke=stroke,fill=fill) + + def lines(self, lineList, color=None, width=None): + if self._strokeColor != None: + self._setColor(self._strokeColor) + codeline = '%s m %s l stroke' + for line in lineList: + self.code.append(codeline % (fp_str(line[0]),fp_str(line[1]))) + + def moveTo(self,x,y): + self.code.append('%s m' % fp_str(x, y)) + + def lineTo(self,x,y): + self.code.append('%s l' % fp_str(x, y)) + + def curveTo(self,x1,y1,x2,y2,x3,y3): + self.code.append('%s c' % fp_str(x1,y1,x2,y2,x3,y3)) + + def closePath(self): + self.code.append('closepath') + + def polyLine(self, p): + assert len(p) >= 1, 'Polyline must have 1 or more points' + if self._strokeColor != None: + self.setColor(self._strokeColor) + self.moveTo(p[0][0], p[0][1]) + for t in p[1:]: + self.lineTo(t[0], t[1]) + self.code.append('stroke') + + + def drawFigure(self, partList, closed=0): + figureCode = [] + first = 1 + + for part in partList: + op = part[0] + args = list(part[1:]) + + if op == figureLine: + if first: + first = 0 + figureCode.append("%s m" % fp_str(args[:2])) + else: + figureCode.append("%s l" % fp_str(args[:2])) + figureCode.append("%s l" % fp_str(args[2:])) + + elif op == figureArc: + first = 0 + x1,y1,x2,y2,startAngle,extent = args[:6] + figureCode.append(self._genArcCode(x1,y1,x2,y2,startAngle,extent)) + + elif op == figureCurve: + if first: + first = 0 + figureCode.append("%s m" % fp_str(args[:2])) + else: + figureCode.append("%s l" % fp_str(args[:2])) + figureCode.append("%s curveto" % fp_str(args[2:])) + else: + raise TypeError, "unknown figure operator: "+op + + if closed: + figureCode.append("closepath") + self._fillAndStroke(figureCode) + + def _fillAndStroke(self,code,clip=0,fill=1,stroke=1): + fill = self._fillColor and fill + stroke = self._strokeColor and stroke + if fill or stroke or clip: + self.code.extend(code) + if fill: + if stroke or clip: self.code.append("gsave") + self.setColor(self._fillColor) + self.code.append("eofill") + if stroke or clip: self.code.append("grestore") + if stroke: + if clip: self.code.append("gsave") + self.setColor(self._strokeColor) + self.code.append("stroke") + if clip: self.code.append("grestore") + if clip: + self.code.append("clip") + self.code.append("newpath") + + + def translate(self,x,y): + self.code.append('%s translate' % fp_str(x,y)) + + def scale(self,x,y): + self.code.append('%s scale' % fp_str(x,y)) + + def transform(self,a,b,c,d,e,f): + self.code.append('[%s] concat' % fp_str(a,b,c,d,e,f)) + + def _drawTimeResize(self,w,h): + '''if this is used we're probably in the wrong world''' + self.width, self.height = w, h + + ############################################################################################ + # drawImage(self. image, x1, y1, x2=None, y2=None) is now defined by either _drawImageLevel1 + # ._drawImageLevel2, the choice is made in .__init__ depending on option + def _drawImageLevel1(self, image, x1, y1, x2=None,y2=None): + # Postscript Level1 version available for fallback mode when Level2 doesn't work + """drawImage(self,image,x1,y1,x2=None,y2=None) : If x2 and y2 are ommitted, they are + calculated from image size. (x1,y1) is upper left of image, (x2,y2) is lower right of + image in piddle coordinates.""" + # For now let's start with 24 bit RGB images (following piddlePDF again) + component_depth = 8 + myimage = image.convert('RGB') + imgwidth, imgheight = myimage.size + if not x2: + x2 = imgwidth + x1 + if not y2: + y2 = y1 + imgheight + drawwidth = x2 - x1 + drawheight = y2 - y1 + #print 'Image size (%d, %d); Draw size (%d, %d)' % (imgwidth, imgheight, drawwidth, drawheight) + # now I need to tell postscript how big image is + + # "image operators assume that they receive sample data from + # their data source in x-axis major index order. The coordinate + # of the lower-left corner of the first sample is (0,0), of the + # second (1,0) and so on" -PS2 ref manual p. 215 + # + # The ImageMatrix maps unit squre of user space to boundary of the source image + # + + # The CurrentTransformationMatrix (CTM) maps the unit square of + # user space to the rect...on the page that is to receive the + # image. A common ImageMatrix is [width 0 0 -height 0 height] + # (for a left to right, top to bottom image ) + + # first let's map the user coordinates start at offset x1,y1 on page + + self.code.extend([ + 'gsave', + '%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image + '%s %s scale' % (drawwidth,drawheight), + '/scanline %d 3 mul string def' % imgwidth # scanline by multiples of image width + ]) + + # now push the dimensions and depth info onto the stack + # and push the ImageMatrix to map the source to the target rectangle (see above) + # finally specify source (PS2 pp. 225 ) and by exmample + self.code.extend([ + '%s %s %s' % (imgwidth, imgheight, component_depth), + '[%s %s %s %s %s %s]' % (imgwidth, 0, 0, -imgheight, 0, imgheight), + '{ currentfile scanline readhexstring pop } false 3', + 'colorimage ' + ]) + + # data source output--now we just need to deliver a hex encode + # series of lines of the right overall size can follow + # piddlePDF again + + rawimage = myimage.tostring() + assert(len(rawimage) == imgwidth*imgheight, 'Wrong amount of data for image') + #compressed = zlib.compress(rawimage) # no zlib at moment + hex_encoded = self._AsciiHexEncode(rawimage) + + # write in blocks of 78 chars per line + outstream = getStringIO(hex_encoded) + + dataline = outstream.read(78) + while dataline <> "": + self.code.append(dataline) + dataline= outstream.read(78) + self.code.append('% end of image data') # for clarity + self.code.append('grestore') # return coordinates to normal + + # end of drawImage + def _AsciiHexEncode(self, input): # also based on piddlePDF + "Helper function used by images" + output = getStringIO() + for char in input: + output.write('%02x' % ord(char)) + return output.getvalue() + + def _drawImageLevel2(self, image, x1,y1, x2=None,y2=None): # Postscript Level2 version + '''At present we're handling only PIL''' + ### what sort of image are we to draw + if image.mode=='L' : + imBitsPerComponent = 8 + imNumComponents = 1 + myimage = image + elif image.mode == '1': + myimage = image.convert('L') + imNumComponents = 1 + myimage = image + else : + myimage = image.convert('RGB') + imNumComponents = 3 + imBitsPerComponent = 8 + + imwidth, imheight = myimage.size + if not x2: + x2 = imwidth + x1 + if not y2: + y2 = y1 + imheight + drawwidth = x2 - x1 + drawheight = y2 - y1 + self.code.extend([ + 'gsave', + '%s %s translate' % (x1,-y1 - drawheight), # need to start are lower left of image + '%s %s scale' % (drawwidth,drawheight)]) + + if imNumComponents == 3 : + self.code.append('/DeviceRGB setcolorspace') + elif imNumComponents == 1 : + self.code.append('/DeviceGray setcolorspace') + # create the image dictionary + self.code.append(""" +<< +/ImageType 1 +/Width %d /Height %d %% dimensions of source image +/BitsPerComponent %d""" % (imwidth, imheight, imBitsPerComponent) ) + + if imNumComponents == 1: + self.code.append('/Decode [0 1]') + if imNumComponents == 3: + self.code.append('/Decode [0 1 0 1 0 1] %% decode color values normally') + + self.code.extend([ '/ImageMatrix [%s 0 0 %s 0 %s]' % (imwidth, -imheight, imheight), + '/DataSource currentfile /ASCIIHexDecode filter', + '>> % End image dictionary', + 'image']) + # after image operator just need to dump image dat to file as hexstring + rawimage = myimage.tostring() + assert(len(rawimage) == imwidth*imheight, 'Wrong amount of data for image') + #compressed = zlib.compress(rawimage) # no zlib at moment + hex_encoded = self._AsciiHexEncode(rawimage) + + # write in blocks of 78 chars per line + outstream = getStringIO(hex_encoded) + + dataline = outstream.read(78) + while dataline <> "": + self.code.append(dataline) + dataline= outstream.read(78) + self.code.append('> % end of image data') # > is EOD for hex encoded filterfor clarity + self.code.append('grestore') # return coordinates to normal + +# renderpdf - draws them onto a canvas +"""Usage: + from reportlab.graphics import renderPS + renderPS.draw(drawing, canvas, x, y) +Execute the script to see some test drawings.""" +from shapes import * + +# hack so we only get warnings once each +#warnOnce = WarnOnce() + +# the main entry point for users... +def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary): + """As it says""" + R = _PSRenderer() + R.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + +def _pointsFromList(L): + ''' + given a list of coordinates [x0, y0, x1, y1....] + produce a list of points [(x0,y0), (y1,y0),....] + ''' + P=[] + for i in range(0,len(L),2): + P.append((L[i],L[i+1])) + return P + +class _PSRenderer(Renderer): + """This draws onto a EPS document. It needs to be a class + rather than a function, as some EPS-specific state tracking is + needed outside of the state info in the SVG model.""" + + def __init__(self): + self._tracker = StateTracker() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + self._canvas.comment('begin node %s'%`node`) + color = self._canvas._color + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + rDeltas = self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + self._canvas.restoreState() + self._canvas.comment('end node %s'%`node`) + self._canvas._color = color + + #restore things we might have lost (without actually doing anything). + for k, v in rDeltas.items(): + if self._restores.has_key(k): + setattr(self._canvas,self._restores[k],v) + +## _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap', +## 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font', +## 'font_size':'_fontSize'} + _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap', + 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font', + 'fontSize':'_fontSize'} + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height, rect.rx, rect.ry + ) + + def drawLine(self, line): + if self._canvas._strokeColor: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + def drawCircle(self, circle): + self._canvas.circle( circle.cx, circle.cy, circle.r) + + def drawWedge(self, wedge): + yradius, radius1, yradius1 = wedge._xtraRadii() + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + startangledegrees = wedge.startangledegrees + endangledegrees = wedge.endangledegrees + centerx= wedge.centerx + centery = wedge.centery + radius = wedge.radius + extent = endangledegrees - startangledegrees + self._canvas.drawArc(centerx-radius, centery-yradius, centerx+radius, centery+yradius, + startangledegrees, extent, fromcenter=1) + else: + self.drawPolygon(wedge.asPolygon()) + + def drawPolyLine(self, p): + if self._canvas._strokeColor: + self._canvas.polyLine(_pointsFromList(p.points)) + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2) + + def drawPolygon(self, p): + self._canvas.polygon(_pointsFromList(p.points), closed=1) + + def drawString(self, stringObj): + if self._canvas._fillColor: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x,stringObj.y,stringObj.text + if not text_anchor in ['start','inherited']: + font, fontSize = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for text_anchor '+str(text_anchor) + self._canvas.drawString(x,y,text) + + def drawPath(self, path): + from reportlab.graphics.shapes import _renderPath + c = self._canvas + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath) + isClosed = _renderPath(path, drawFuncs) + if not isClosed: + c._fillColor = None + c._fillAndStroke([], clip=path.isClipPath) + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + for key, value in delta.items(): + if key == 'transform': + self._canvas.transform(value[0], value[1], value[2], + value[3], value[4], value[5]) + elif key == 'strokeColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() +## elif key == 'stroke_opacity': +## warnOnce('Stroke Opacity not supported yet') + elif key == 'fillColor': + #this has different semantics in PDF to SVG; + #we always have a color, and either do or do + #not apply it; in SVG one can have a 'None' color + self._canvas.setFillColor(value) +## elif key == 'fill_rule': +## warnOnce('Fill rules not done yet') +## elif key == 'fill_opacity': +## warnOnce('Fill opacity not done yet') + elif key in ['fontSize', 'fontName']: + # both need setting together in PDF + # one or both might be in the deltas, + # so need to get whichever is missing + fontname = delta.get('fontName', self._canvas._font) + fontsize = delta.get('fontSize', self._canvas._fontSize) + self._canvas.setFont(fontname, fontsize) + + def drawImage(self, image): + from reportlab.lib.utils import ImageReader + im = ImageReader(image.path) + x0 = image.x + y0 = image.y + x1 = image.width + if x1 is not None: x1 += x0 + y1 = image.height + if y1 is not None: y1 += y0 + self._canvas.drawImage(im._image,x0,y0,x1,y1) + +def drawToFile(d,fn, showBoundary=rl_config.showBoundary): + d = renderScaledDrawing(d) + c = PSCanvas((d.width,d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + c.save(fn) + +def drawToString(d, showBoundary=rl_config.showBoundary): + "Returns a PS as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, showBoundary=showBoundary) + return s.getvalue() + +######################################################### +# +# test code. First, defin a bunch of drawings. +# Routine to draw them comes at the end. +# +######################################################### +def test(outdir='epsout'): + import os + # print all drawings and their doc strings from the test + # file + if not os.path.isdir(outdir): + os.mkdir(outdir) + #grab all drawings from the test module + import testshapes + drawings = [] + + for funcname in dir(testshapes): + #if funcname[0:11] == 'getDrawing2': + # print 'hacked to only show drawing 2' + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') #execute it + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + i = 0 + for (d, docstring) in drawings: + filename = outdir + os.sep + 'renderPS_%d.eps'%i + drawToFile(d,filename) + print 'saved', filename + i = i + 1 + +if __name__=='__main__': + import sys + if len(sys.argv)>1: + outdir = sys.argv[1] + else: + outdir = 'epsout' + test(outdir) diff --git a/bin/reportlab/graphics/renderSVG.py b/bin/reportlab/graphics/renderSVG.py new file mode 100644 index 00000000000..73aaddfc16d --- /dev/null +++ b/bin/reportlab/graphics/renderSVG.py @@ -0,0 +1,828 @@ +"""An experimental SVG renderer for the ReportLab graphics framework. + +This will create SVG code from the ReportLab Graphics API (RLG). +To read existing SVG code and convert it into ReportLab graphics +objects download the svglib module here: + + http://python.net/~gherman/#svglib +""" + +import math, string, types, sys, os +from types import StringType +from operator import getitem + +from reportlab.pdfbase.pdfmetrics import stringWidth # for font info +from reportlab.lib.utils import fp_str +from reportlab.lib.colors import black +from reportlab.graphics.renderbase import StateTracker, getStateDelta, Renderer, renderScaledDrawing +from reportlab.graphics.shapes import STATE_DEFAULTS, Path, UserNode +from reportlab.graphics.shapes import * # (only for test0) +from reportlab import rl_config +from reportlab.lib.utils import getStringIO + +from xml.dom import getDOMImplementation + + +### some constants ### + +sin = math.sin +cos = math.cos +pi = math.pi + +LINE_STYLES = 'stroke-width stroke-linecap stroke fill stroke-dasharray' +TEXT_STYLES = 'font-family font-size' + + +### top-level user function ### + +def drawToString(d, showBoundary=rl_config.showBoundary): + "Returns a SVG as a string in memory, without touching the disk" + s = getStringIO() + drawToFile(d, s, showBoundary=showBoundary) + return s.getvalue() + +def drawToFile(d, fn, showBoundary=rl_config.showBoundary): + d = renderScaledDrawing(d) + c = SVGCanvas((d.width, d.height)) + draw(d, c, 0, 0, showBoundary=showBoundary) + c.save(fn) + + +def draw(drawing, canvas, x=0, y=0, showBoundary=rl_config.showBoundary): + """As it says.""" + r = _SVGRenderer() + r.draw(renderScaledDrawing(drawing), canvas, x, y, showBoundary=showBoundary) + + +### helper functions ### + +def _pointsFromList(L): + """ + given a list of coordinates [x0, y0, x1, y1....] + produce a list of points [(x0,y0), (y1,y0),....] + """ + + P=[] + for i in range(0,len(L), 2): + P.append((L[i], L[i+1])) + + return P + + +def transformNode(doc, newTag, node=None, **attrDict): + """Transform a DOM node into new node and copy selected attributes. + + Creates a new DOM node with tag name 'newTag' for document 'doc' + and copies selected attributes from an existing 'node' as provided + in 'attrDict'. The source 'node' can be None. Attribute values will + be converted to strings. + + E.g. + + n = transformNode(doc, "node1", x="0", y="1") + -> DOM node for + + n = transformNode(doc, "node1", x=0, y=1+1) + -> DOM node for + + n = transformNode(doc, "node1", node0, x="x0", y="x0", zoo=bar()) + -> DOM node for + """ + + newNode = doc.createElement(newTag) + for newAttr, attr in attrDict.items(): + sattr = str(attr) + if not node: + newNode.setAttribute(newAttr, sattr) + else: + attrVal = node.getAttribute(sattr) + newNode.setAttribute(newAttr, attrVal or sattr) + + return newNode + + +### classes ### + +class SVGCanvas: + def __init__(self, size=(300,300)): + self.verbose = 0 + self.width, self.height = self.size = size + # self.height = size[1] + self.code = [] + self.style = {} + self.path = '' + self._strokeColor = self._fillColor = self._lineWidth = \ + self._font = self._fontSize = self._lineCap = \ + self._lineJoin = self._color = None + + implementation = getDOMImplementation('minidom') + self.doc = implementation.createDocument(None, "svg", None) + self.svg = self.doc.documentElement + self.svg.setAttribute("width", str(size[0])) + self.svg.setAttribute("height", str(self.height)) + + title = self.doc.createElement('title') + text = self.doc.createTextNode('...') + title.appendChild(text) + self.svg.appendChild(title) + + desc = self.doc.createElement('desc') + text = self.doc.createTextNode('...') + desc.appendChild(text) + self.svg.appendChild(desc) + + self.setFont(STATE_DEFAULTS['fontName'], STATE_DEFAULTS['fontSize']) + self.setStrokeColor(STATE_DEFAULTS['strokeColor']) + self.setLineCap(2) + self.setLineJoin(0) + self.setLineWidth(1) + + # Add a rectangular clipping path identical to view area. + clipPath = transformNode(self.doc, "clipPath", id="clip") + clipRect = transformNode(self.doc, "rect", x=0, y=0, + width=self.width, height=self.height) + clipPath.appendChild(clipRect) + self.svg.appendChild(clipPath) + + self.groupTree = transformNode(self.doc, "g", + id="group", + transform="scale(1,-1) translate(0,-%d)" % self.height, + style="clip-path: url(#clip)") + self.svg.appendChild(self.groupTree) + self.currGroup = self.groupTree + + + def save(self, f=None): + if type(f) is StringType: + file = open(f, 'w') + else: + file = f + + file.write("""\ + +\n""") + + # use = self.doc.createElement('use') + # use.setAttribute("xlink:href", "#group") + # use.setAttribute("transform", "scale(1, -1)") + # self.svg.appendChild(use) + + result = self.svg.toprettyxml(indent=" ") + file.write(result) + + if file is not f: + file.close() + + + ### helpers ### + + def NOTUSED_stringWidth(self, s, font=None, fontSize=None): + """Return the logical width of the string if it were drawn + in the current font (defaults to self.font). + """ + + font = font or self._font + fontSize = fontSize or self._fontSize + + return stringWidth(s, font, fontSize) + + + def _formatStyle(self, include=''): + str = '' + include = string.split(include) + keys = self.style.keys() + if include: + #2.1-safe version of the line below follows: + #keys = filter(lambda k: k in include, keys) + tmp = [] + for word in keys: + if word in include: + tmp.append(word) + keys = tmp + + items = [] + for k in keys: + items.append((k, self.style[k])) + items = map(lambda i: "%s: %s"%(i[0], i[1]), items) + str = string.join(items, '; ') + ';' + + return str + + + def _escape(self, s): + """ + return a copy of string s with special characters in postscript strings + escaped with backslashes. + Have not handled characters that are converted normally in python strings + i.e. \n -> newline + """ + + str = string.replace(s, chr(0x5C), r'\\' ) + str = string.replace(str, '(', '\(' ) + str = string.replace(str, ')', '\)') + return str + + + def _genArcCode(self, x1, y1, x2, y2, startAng, extent): + """Calculate the path for an arc inscribed in rectangle defined + by (x1,y1),(x2,y2).""" + + return + + #calculate semi-minor and semi-major axes of ellipse + xScale = abs((x2-x1)/2.0) + yScale = abs((y2-y1)/2.0) + #calculate centre of ellipse + x, y = (x1+x2)/2.0, (y1+y2)/2.0 + + codeline = 'matrix currentmatrix %s %s translate %s %s scale 0 0 1 %s %s %s setmatrix' + + if extent >= 0: + arc='arc' + else: + arc='arcn' + data = (x,y, xScale, yScale, startAng, startAng+extent, arc) + + return codeline % data + + + def _fillAndStroke(self, code, clip=0): + path = transformNode(self.doc, "path", + d=self.path, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(path) + self.path = '' + + return + + """ + if self._fillColor or self._strokeColor or clip: + self.code.extend(code) + if self._fillColor: + if self._strokeColor or clip: + self.code.append("gsave") + self.setColor(self._fillColor) + self.code.append("eofill") + if self._strokeColor or clip: + self.code.append("grestore") + if self._strokeColor != None: + if clip: self.code.append("gsave") + self.setColor(self._strokeColor) + self.code.append("stroke") + if clip: self.code.append("grestore") + if clip: + self.code.append("clip") + self.code.append("newpath") + """ + + + ### styles ### + + def setLineCap(self, v): + vals = {0:'butt', 1:'round', 2:'square'} + if self._lineCap != v: + self._lineCap = v + self.style['stroke-linecap'] = vals[v] + + + def setLineJoin(self, v): + vals = {0:'miter', 1:'round', 2:'bevel'} + if self._lineJoin != v: + self._lineJoin = v + self.style['stroke-linecap'] = vals[v] + + + def setDash(self, array=[], phase=0): + """Two notations. Pass two numbers, or an array and phase.""" + + join = string.join + if type(array) in (types.IntType, types.FloatType): + self.style['stroke-dasharray'] = join(map(str, ([array, phase])), ', ') + elif type(array) in (types.ListType, types.TupleType) and len(array) > 0: + assert phase >= 0, "phase is a length in user space" + self.style['stroke-dasharray'] = join(map(str, (array+[phase])), ', ') + + + def setStrokeColor(self, color): + self._strokeColor = color + self.setColor(color) + if color == None: + self.style['stroke'] = 'none' + else: + r, g, b = color.red, color.green, color.blue + self.style['stroke'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100) + + + def setColor(self, color): + if self._color != color: + self._color = color + + + def setFillColor(self, color): + self._fillColor = color + self.setColor(color) + if color == None: + self.style['fill'] = 'none' + else: + r, g, b = color.red, color.green, color.blue + self.style['fill'] = 'rgb(%d%%,%d%%,%d%%)' % (r*100, g*100, b*100) + + + def setLineWidth(self, width): + if width != self._lineWidth: + self._lineWidth = width + self.style['stroke-width'] = width + + + def setFont(self, font, fontSize): + if self._font != font or self._fontSize != fontSize: + self._font, self._fontSize = (font, fontSize) + self.style['font-family'] = font + self.style['font-size'] = fontSize + + + ### shapes ### + + def rect(self, x1,y1, x2,y2, rx=8, ry=8): + "Draw a rectangle between x1,y1 and x2,y2." + + if self.verbose: print "+++ SVGCanvas.rect" + + rect = transformNode(self.doc, "rect", + x=x1, y=y1, width=x2-x1, height=y2-y1, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(rect) + + + def roundRect(self, x1,y1, x2,y2, rx=8, ry=8): + """Draw a rounded rectangle between x1,y1 and x2,y2. + + Corners inset as ellipses with x-radius rx and y-radius ry. + These should have x10, and ry>0. + """ + + rect = transformNode(self.doc, "rect", + x=x1, y=y1, width=x2-x1, height=y2-y1, rx=rx, ry=ry, + style=self._formatStyle(LINE_STYLES)) + + self.currGroup.appendChild(rect) + + + def drawString(self, s, x, y, angle=0): + if self.verbose: print "+++ SVGCanvas.drawString" + + if self._fillColor != None: + self.setColor(self._fillColor) + s = self._escape(s) + st = self._formatStyle(TEXT_STYLES) + if angle != 0: + st = st + " rotate(%f %f %f);" % (angle, x, y) + st = st + " fill: %s;" % self.style['fill'] + text = transformNode(self.doc, "text", + x=x, y=y, style=st, + transform="translate(0,%d) scale(1,-1)" % (2*y)) + content = self.doc.createTextNode(s) + text.appendChild(content) + + self.currGroup.appendChild(text) + + def drawCentredString(self, s, x, y, angle=0,text_anchor='middle'): + if self.verbose: print "+++ SVGCanvas.drawCentredString" + + if self._fillColor != None: + if not text_anchor in ['start', 'inherited']: + textLen = stringWidth(s,self._font,self._fontSize) + if text_anchor=='end': + x -= textLen + elif text_anchor=='middle': + x -= textLen/2. + else: + raise ValueError, 'bad value for text_anchor ' + str(text_anchor) + self.drawString(x,y,text,angle=angle) + + def drawRightString(self, text, x, y, angle=0): + self.drawCentredString(text,x,y,angle=angle,text_anchor='end') + + def comment(self, data): + "Add a comment." + + comment = self.doc.createComment(data) + # self.currGroup.appendChild(comment) + + + def drawImage(self, image, x1, y1, x2=None, y2=None): + pass + + + def line(self, x1, y1, x2, y2): + if self._strokeColor != None: + if 0: # something is wrong with line in my SVG viewer... + line = transformNode(self.doc, "line", + x=x1, y=y1, x2=x2, y2=y2, + style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(line) + path = transformNode(self.doc, "path", + d="M %f,%f L %f,%f Z" % (x1,y1,x2,y2), + style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(path) + + + def ellipse(self, x1, y1, x2, y2): + """Draw an orthogonal ellipse inscribed within the rectangle x1,y1,x2,y2. + + These should have x1=180, 0, mx, my) + else: + str = str + "M %f, %f A %f, %f %d %d %d %f, %f Z " % \ + (mx, my, rx, ry, 0, extent>=180, 0, mx, my) + + if fromcenter: + str = str + "L %f, %f Z " % (cx, cy) + + path = transformNode(self.doc, "path", + d=str, style=self._formatStyle()) + self.currGroup.appendChild(path) + + + def polygon(self, points, closed=0): + assert len(points) >= 2, 'Polygon must have 2 or more points' + + if self._strokeColor != None: + self.setColor(self._strokeColor) + pairs = [] + for i in xrange(len(points)): + pairs.append("%f %f" % (points[i])) + pts = string.join(pairs, ', ') + polyline = transformNode(self.doc, "polygon", + points=pts, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(polyline) + + # self._fillAndStroke(polyCode) + + + def lines(self, lineList, color=None, width=None): + # print "### lineList", lineList + return + + if self._strokeColor != None: + self._setColor(self._strokeColor) + codeline = '%s m %s l stroke' + for line in lineList: + self.code.append(codeline % (fp_str(line[0]), fp_str(line[1]))) + + + def polyLine(self, points): + assert len(points) >= 1, 'Polyline must have 1 or more points' + + if self._strokeColor != None: + self.setColor(self._strokeColor) + pairs = [] + for i in xrange(len(points)): + pairs.append("%f %f" % (points[i])) + pts = string.join(pairs, ', ') + polyline = transformNode(self.doc, "polyline", + points=pts, style=self._formatStyle(LINE_STYLES)) + self.currGroup.appendChild(polyline) + + + ### groups ### + + def startGroup(self): + if self.verbose: print "+++ begin SVGCanvas.startGroup" + currGroup, group = self.currGroup, transformNode(self.doc, "g", transform="") + currGroup.appendChild(group) + self.currGroup = group + if self.verbose: print "+++ end SVGCanvas.startGroup" + return currGroup + + def endGroup(self,currGroup): + if self.verbose: print "+++ begin SVGCanvas.endGroup" + self.currGroup = currGroup + if self.verbose: print "+++ end SVGCanvas.endGroup" + + + def transform(self, a, b, c, d, e, f): + if self.verbose: print "!!! begin SVGCanvas.transform", a, b, c, d, e, f + tr = self.currGroup.getAttribute("transform") + t = 'matrix(%f, %f, %f, %f, %f, %f)' % (a,b,c,d,e,f) + if (a, b, c, d, e, f) != (1, 0, 0, 1, 0, 0): + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + def translate(self, x, y): + # probably never used + print "!!! begin SVGCanvas.translate" + return + + tr = self.currGroup.getAttribute("transform") + t = 'translate(%f, %f)' % (x, y) + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + def scale(self, x, y): + # probably never used + print "!!! begin SVGCanvas.scale" + return + + tr = self.groups[-1].getAttribute("transform") + t = 'scale(%f, %f)' % (x, y) + self.currGroup.setAttribute("transform", "%s %s" % (tr, t)) + + + ### paths ### + + def moveTo(self, x, y): + self.path = self.path + 'M %f %f ' % (x, y) + + + def lineTo(self, x, y): + self.path = self.path + 'L %f %f ' % (x, y) + + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self.path = self.path + 'C %f %f %f %f %f %f ' % (x1, y1, x2, y2, x3, y3) + + + def closePath(self): + self.path = self.path + 'Z ' + + def saveState(self): + pass + + def restoreState(self): + pass + +class _SVGRenderer(Renderer): + """This draws onto an SVG document. + """ + + def __init__(self): + self._tracker = StateTracker() + self.verbose = 0 + + def drawNode(self, node): + """This is the recursive method called for each node in the tree. + """ + + if self.verbose: print "### begin _SVGRenderer.drawNode" + + self._canvas.comment('begin node %s'%`node`) + color = self._canvas._color + if not (isinstance(node, Path) and node.isClipPath): + pass # self._canvas.saveState() + + #apply state changes + deltas = getStateDelta(node) + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + #draw the object, or recurse + self.drawNodeDispatcher(node) + + rDeltas = self._tracker.pop() + if not (isinstance(node, Path) and node.isClipPath): + pass # self._canvas.restoreState() + self._canvas.comment('end node %s'%`node`) + self._canvas._color = color + + #restore things we might have lost (without actually doing anything). + for k, v in rDeltas.items(): + if self._restores.has_key(k): + setattr(self._canvas,self._restores[k],v) + + if self.verbose: print "### end _SVGRenderer.drawNode" + + _restores = {'strokeColor':'_strokeColor','strokeWidth': '_lineWidth','strokeLineCap':'_lineCap', + 'strokeLineJoin':'_lineJoin','fillColor':'_fillColor','fontName':'_font', + 'fontSize':'_fontSize'} + + + def drawGroup(self, group): + if self.verbose: print "### begin _SVGRenderer.drawGroup" + + currGroup = self._canvas.startGroup() + a, b, c, d, e, f = self._tracker.getCTM() + for childNode in group.getContents(): + if isinstance(childNode, UserNode): + node2 = childNode.provideNode() + else: + node2 = childNode + self.drawNode(node2) + self._canvas.transform(a, b, c, d, e, f) + self._canvas.endGroup(currGroup) + + if self.verbose: print "### end _SVGRenderer.drawGroup" + + + def drawRect(self, rect): + if rect.rx == rect.ry == 0: + #plain old rectangle + self._canvas.rect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height) + else: + #cheat and assume ry = rx; better to generalize + #pdfgen roundRect function. TODO + self._canvas.roundRect( + rect.x, rect.y, + rect.x+rect.width, rect.y+rect.height, + rect.rx, rect.ry + ) + + + def drawString(self, stringObj): + if self._canvas._fillColor: + S = self._tracker.getState() + text_anchor, x, y, text = S['textAnchor'], stringObj.x, stringObj.y, stringObj.text + if not text_anchor in ['start', 'inherited']: + font, fontSize = S['fontName'], S['fontSize'] + textLen = stringWidth(text, font,fontSize) + if text_anchor=='end': + x = x-textLen + elif text_anchor=='middle': + x = x - textLen/2 + else: + raise ValueError, 'bad value for text_anchor ' + str(text_anchor) + self._canvas.drawString(text,x,y) + + + def drawLine(self, line): + if self._canvas._strokeColor: + self._canvas.line(line.x1, line.y1, line.x2, line.y2) + + + def drawCircle(self, circle): + self._canvas.circle( circle.cx, circle.cy, circle.r) + + + def drawWedge(self, wedge): + centerx, centery, radius, startangledegrees, endangledegrees = \ + wedge.centerx, wedge.centery, wedge.radius, wedge.startangledegrees, wedge.endangledegrees + yradius = wedge.yradius or wedge.radius + (x1, y1) = (centerx-radius, centery-yradius) + (x2, y2) = (centerx+radius, centery+yradius) + extent = endangledegrees - startangledegrees + self._canvas.drawArc(x1, y1, x2, y2, startangledegrees, extent, fromcenter=1) + + + def drawPolyLine(self, p): + if self._canvas._strokeColor: + self._canvas.polyLine(_pointsFromList(p.points)) + + + def drawEllipse(self, ellipse): + #need to convert to pdfgen's bounding box representation + x1 = ellipse.cx - ellipse.rx + x2 = ellipse.cx + ellipse.rx + y1 = ellipse.cy - ellipse.ry + y2 = ellipse.cy + ellipse.ry + self._canvas.ellipse(x1,y1,x2,y2) + + + def drawPolygon(self, p): + self._canvas.polygon(_pointsFromList(p.points), closed=1) + + + def drawPath(self, path): + # print "### drawPath", path.points + from reportlab.graphics.shapes import _renderPath + c = self._canvas + drawFuncs = (c.moveTo, c.lineTo, c.curveTo, c.closePath) + isClosed = _renderPath(path, drawFuncs) + if not isClosed: + c._fillColor = None + c._fillAndStroke([], clip=path.isClipPath) + + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + + for key, value in delta.items(): + if key == 'transform': + pass + #self._canvas.transform(value[0], value[1], value[2], value[3], value[4], value[5]) + elif key == 'strokeColor': + self._canvas.setStrokeColor(value) + elif key == 'strokeWidth': + self._canvas.setLineWidth(value) + elif key == 'strokeLineCap': #0,1,2 + self._canvas.setLineCap(value) + elif key == 'strokeLineJoin': + self._canvas.setLineJoin(value) + elif key == 'strokeDashArray': + if value: + self._canvas.setDash(value) + else: + self._canvas.setDash() + elif key == 'fillColor': + self._canvas.setFillColor(value) + elif key in ['fontSize', 'fontName']: + fontname = delta.get('fontName', self._canvas._font) + fontsize = delta.get('fontSize', self._canvas._fontSize) + self._canvas.setFont(fontname, fontsize) + + + + +def test0(outdir='svgout'): + # print all drawings and their doc strings from the test + # file + if not os.path.isdir(outdir): + os.mkdir(outdir) + #grab all drawings from the test module + from reportlab.graphics import testshapes + drawings = [] + + for funcname in dir(testshapes): + #if funcname[0:11] == 'getDrawing2': + # print 'hacked to only show drawing 2' + if funcname[0:10] == 'getDrawing': + drawing = eval('testshapes.' + funcname + '()') + docstring = eval('testshapes.' + funcname + '.__doc__') + drawings.append((drawing, docstring)) + + # return + + i = 0 + for (d, docstring) in drawings: + filename = outdir + os.sep + 'renderSVG_%d.svg' % i + drawToFile(d, filename) + # print 'saved', filename + i = i + 1 + + +def test1(): + from reportlab.graphics.testshapes import getDrawing01 + d = getDrawing01() + drawToFile(d, "svgout/test.svg") + + +def test2(): + from reportlab.lib.corp import RL_CorpLogo + from reportlab.graphics.shapes import Drawing + + rl = RL_CorpLogo() + d = Drawing(rl.width,rl.height) + d.add(rl) + drawToFile(d, "svgout/corplogo.svg") + + +if __name__=='__main__': + test0() + test1() + test2() diff --git a/bin/reportlab/graphics/renderbase.py b/bin/reportlab/graphics/renderbase.py new file mode 100644 index 00000000000..10282e15af4 --- /dev/null +++ b/bin/reportlab/graphics/renderbase.py @@ -0,0 +1,351 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/renderbase.py +""" +Superclass for renderers to factor out common functionality and default implementations. +""" + + +__version__=''' $Id $ ''' + +from reportlab.graphics.shapes import * +from reportlab.lib.validators import DerivedValue +from reportlab import rl_config + +def inverse(A): + "For A affine 2D represented as 6vec return 6vec version of A**(-1)" + # I checked this RGB + det = float(A[0]*A[3] - A[2]*A[1]) + R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det] + return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]]) + +def mmult(A, B): + "A postmultiplied by B" + # I checked this RGB + # [a0 a2 a4] [b0 b2 b4] + # [a1 a3 a5] * [b1 b3 b5] + # [ 1 ] [ 1 ] + # + return (A[0]*B[0] + A[2]*B[1], + A[1]*B[0] + A[3]*B[1], + A[0]*B[2] + A[2]*B[3], + A[1]*B[2] + A[3]*B[3], + A[0]*B[4] + A[2]*B[5] + A[4], + A[1]*B[4] + A[3]*B[5] + A[5]) + + +def getStateDelta(shape): + """Used to compute when we need to change the graphics state. + For example, if we have two adjacent red shapes we don't need + to set the pen color to red in between. Returns the effect + the given shape would have on the graphics state""" + delta = {} + for (prop, value) in shape.getProperties().items(): + if STATE_DEFAULTS.has_key(prop): + delta[prop] = value + return delta + + +class StateTracker: + """Keeps a stack of transforms and state + properties. It can contain any properties you + want, but the keys 'transform' and 'ctm' have + special meanings. The getCTM() + method returns the current transformation + matrix at any point, without needing to + invert matrixes when you pop.""" + def __init__(self, defaults=None): + # one stack to keep track of what changes... + self._deltas = [] + + # and another to keep track of cumulative effects. Last one in + # list is the current graphics state. We put one in to simplify + # loops below. + self._combined = [] + if defaults is None: + defaults = STATE_DEFAULTS.copy() + #ensure that if we have a transform, we have a CTM + if defaults.has_key('transform'): + defaults['ctm'] = defaults['transform'] + self._combined.append(defaults) + + def push(self,delta): + """Take a new state dictionary of changes and push it onto + the stack. After doing this, the combined state is accessible + through getState()""" + + newstate = self._combined[-1].copy() + for (key, value) in delta.items(): + if key == 'transform': #do cumulative matrix + newstate['transform'] = delta['transform'] + newstate['ctm'] = mmult(self._combined[-1]['ctm'], delta['transform']) + #print 'statetracker transform = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['transform']) + #print 'statetracker ctm = (%0.2f, %0.2f, %0.2f, %0.2f, %0.2f, %0.2f)' % tuple(newstate['ctm']) + + else: #just overwrite it + newstate[key] = value + + self._combined.append(newstate) + self._deltas.append(delta) + + def pop(self): + """steps back one, and returns a state dictionary with the + deltas to reverse out of wherever you are. Depending + on your back end, you may not need the return value, + since you can get the complete state afterwards with getState()""" + del self._combined[-1] + newState = self._combined[-1] + lastDelta = self._deltas[-1] + del self._deltas[-1] + #need to diff this against the last one in the state + reverseDelta = {} + #print 'pop()...' + for key, curValue in lastDelta.items(): + #print ' key=%s, value=%s' % (key, curValue) + prevValue = newState[key] + if prevValue <> curValue: + #print ' state popping "%s"="%s"' % (key, curValue) + if key == 'transform': + reverseDelta[key] = inverse(lastDelta['transform']) + else: #just return to previous state + reverseDelta[key] = prevValue + return reverseDelta + + def getState(self): + "returns the complete graphics state at this point" + return self._combined[-1] + + def getCTM(self): + "returns the current transformation matrix at this point""" + return self._combined[-1]['ctm'] + + def __getitem__(self,key): + "returns the complete graphics state value of key at this point" + return self._combined[-1][key] + + def __setitem__(self,key,value): + "sets the complete graphics state value of key to value" + self._combined[-1][key] = value + +def testStateTracker(): + print 'Testing state tracker' + defaults = {'fillColor':None, 'strokeColor':None,'fontName':None, 'transform':[1,0,0,1,0,0]} + deltas = [ + {'fillColor':'red'}, + {'fillColor':'green', 'strokeColor':'blue','fontName':'Times-Roman'}, + {'transform':[0.5,0,0,0.5,0,0]}, + {'transform':[0.5,0,0,0.5,2,3]}, + {'strokeColor':'red'} + ] + + st = StateTracker(defaults) + print 'initial:', st.getState() + print + for delta in deltas: + print 'pushing:', delta + st.push(delta) + print 'state: ',st.getState(),'\n' + + for delta in deltas: + print 'popping:',st.pop() + print 'state: ',st.getState(),'\n' + + +def _expandUserNode(node,canvas): + if isinstance(node, UserNode): + try: + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + onode = node + node = node.provideNode() + finally: + if not ocanvas: del onode._canvas + return node + +def renderScaledDrawing(d): + renderScale = d.renderScale + if renderScale!=1.0: + d = d.copy() + d.width *= renderScale + d.height *= renderScale + d.scale(renderScale,renderScale) + d.renderScale = 1.0 + return d + +class Renderer: + """Virtual superclass for graphics renderers.""" + + def __init__(self): + self._tracker = StateTracker() + self._nodeStack = [] #track nodes visited + + def undefined(self, operation): + raise ValueError, "%s operation not defined at superclass class=%s" %(operation, self.__class__) + + def draw(self, drawing, canvas, x=0, y=0, showBoundary=rl_config._unset_): + """This is the top level function, which draws the drawing at the given + location. The recursive part is handled by drawNode.""" + #stash references for ease of communication + if showBoundary is rl_config._unset_: showBoundary=rl_config.showBoundary + self._canvas = canvas + canvas.__dict__['_drawing'] = self._drawing = drawing + drawing._parent = None + try: + #bounding box + if showBoundary: canvas.rect(x, y, drawing.width, drawing.height) + canvas.saveState() + self.initState(x,y) #this is the push() + self.drawNode(drawing) + self.pop() + canvas.restoreState() + finally: + #remove any circular references + del self._canvas, self._drawing, canvas._drawing, drawing._parent + + def initState(self,x,y): + deltas = STATE_DEFAULTS.copy() + deltas['transform'] = [1,0,0,1,x,y] + self._tracker.push(deltas) + self.applyStateChanges(deltas, {}) + + def pop(self): + self._tracker.pop() + + def drawNode(self, node): + """This is the recursive method called for each node + in the tree""" + # Undefined here, but with closer analysis probably can be handled in superclass + self.undefined("drawNode") + + def getStateValue(self, key): + """Return current state parameter for given key""" + currentState = self._tracker._combined[-1] + return currentState[key] + + def fillDerivedValues(self, node): + """Examine a node for any values which are Derived, + and replace them with their calculated values. + Generally things may look at the drawing or their + parent. + + """ + for (key, value) in node.__dict__.items(): + if isinstance(value, DerivedValue): + #just replace with default for key? + #print ' fillDerivedValues(%s)' % key + newValue = value.getValue(self, key) + #print ' got value of %s' % newValue + node.__dict__[key] = newValue + + def drawNodeDispatcher(self, node): + """dispatch on the node's (super) class: shared code""" + + canvas = getattr(self,'_canvas',None) + # replace UserNode with its contents + + try: + node = _expandUserNode(node,canvas) + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + + self.fillDerivedValues(node) + #draw the object, or recurse + if isinstance(node, Line): + self.drawLine(node) + elif isinstance(node, Image): + self.drawImage(node) + elif isinstance(node, Rect): + self.drawRect(node) + elif isinstance(node, Circle): + self.drawCircle(node) + elif isinstance(node, Ellipse): + self.drawEllipse(node) + elif isinstance(node, PolyLine): + self.drawPolyLine(node) + elif isinstance(node, Polygon): + self.drawPolygon(node) + elif isinstance(node, Path): + self.drawPath(node) + elif isinstance(node, String): + self.drawString(node) + elif isinstance(node, Group): + self.drawGroup(node) + elif isinstance(node, Wedge): + self.drawWedge(node) + else: + print 'DrawingError','Unexpected element %s in drawing!' % str(node) + finally: + if not ocanvas: del node._canvas + + _restores = {'stroke':'_stroke','stroke_width': '_lineWidth','stroke_linecap':'_lineCap', + 'stroke_linejoin':'_lineJoin','fill':'_fill','font_family':'_font', + 'font_size':'_fontSize'} + + def drawGroup(self, group): + # just do the contents. Some renderers might need to override this + # if they need a flipped transform + canvas = getattr(self,'_canvas',None) + for node in group.getContents(): + node = _expandUserNode(node,canvas) + + #here is where we do derived values - this seems to get everything. Touch wood. + self.fillDerivedValues(node) + try: + if hasattr(node,'_canvas'): + ocanvas = 1 + else: + node._canvas = canvas + ocanvas = None + node._parent = group + self.drawNode(node) + finally: + del node._parent + if not ocanvas: del node._canvas + + def drawWedge(self, wedge): + # by default ask the wedge to make a polygon of itself and draw that! + #print "drawWedge" + polygon = wedge.asPolygon() + self.drawPolygon(polygon) + + def drawPath(self, path): + polygons = path.asPolygons() + for polygon in polygons: + self.drawPolygon(polygon) + + def drawRect(self, rect): + # could be implemented in terms of polygon + self.undefined("drawRect") + + def drawLine(self, line): + self.undefined("drawLine") + + def drawCircle(self, circle): + self.undefined("drawCircle") + + def drawPolyLine(self, p): + self.undefined("drawPolyLine") + + def drawEllipse(self, ellipse): + self.undefined("drawEllipse") + + def drawPolygon(self, p): + self.undefined("drawPolygon") + + def drawString(self, stringObj): + self.undefined("drawString") + + def applyStateChanges(self, delta, newState): + """This takes a set of states, and outputs the operators + needed to set those properties""" + self.undefined("applyStateChanges") + +if __name__=='__main__': + print "this file has no script interpretation" + print __doc__ diff --git a/bin/reportlab/graphics/samples/__init__.py b/bin/reportlab/graphics/samples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/bin/reportlab/graphics/samples/bubble.py b/bin/reportlab/graphics/samples/bubble.py new file mode 100644 index 00000000000..64bf3137912 --- /dev/null +++ b/bin/reportlab/graphics/samples/bubble.py @@ -0,0 +1,73 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class Bubble(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines.symbol.kind ='Circle' + self.chart.lines.symbol.size = 15 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (350,450))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + + + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + Bubble().save(formats=['pdf'],outDir=None,fnRoot='bubble') diff --git a/bin/reportlab/graphics/samples/clustered_bar.py b/bin/reportlab/graphics/samples/clustered_bar.py new file mode 100644 index 00000000000..4d8c363eb29 --- /dev/null +++ b/bin/reportlab/graphics/samples/clustered_bar.py @@ -0,0 +1,84 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.barcharts import HorizontalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ClusteredBar(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,HorizontalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 6 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 80 + self.chart.valueAxis.tickDown = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickLeft = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 6 + self.chart.categoryAxis.labels.dx = -3 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ClusteredBar().save(formats=['pdf'],outDir=None,fnRoot='clustered_bar') \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/clustered_column.py b/bin/reportlab/graphics/samples/clustered_column.py new file mode 100644 index 00000000000..8ea9542eadc --- /dev/null +++ b/bin/reportlab/graphics/samples/clustered_column.py @@ -0,0 +1,83 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ClusteredColumn(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,VerticalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 7 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 115 + self.chart.valueAxis.tickLeft = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickDown = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ClusteredColumn().save(formats=['pdf'],outDir=None,fnRoot='clustered_column') diff --git a/bin/reportlab/graphics/samples/excelcolors.py b/bin/reportlab/graphics/samples/excelcolors.py new file mode 100644 index 00000000000..f3f4f5b6950 --- /dev/null +++ b/bin/reportlab/graphics/samples/excelcolors.py @@ -0,0 +1,45 @@ +# define standard colors to mimic those used by Microsoft Excel +from reportlab.lib.colors import CMYKColor, PCMYKColor + +#colour names as comments at the end of each line are as a memory jogger ONLY +#NOT HTML named colours! + +#Main colours as used for bars etc +color01 = PCMYKColor(40,40,0,0) # Lavender +color02 = PCMYKColor(0,66,33,39) # Maroon +color03 = PCMYKColor(0,0,20,0) # Yellow +color04 = PCMYKColor(20,0,0,0) # Cyan +color05 = PCMYKColor(0,100,0,59) # Purple +color06 = PCMYKColor(0,49,49,0) # Salmon +color07 = PCMYKColor(100,49,0,19) # Blue +color08 = PCMYKColor(20,20,0,0) # PaleLavender +color09 = PCMYKColor(100,100,0,49) # NavyBlue +color10 = PCMYKColor(0,100,0,0) # Purple + +#Highlight colors - eg for the tops of bars +color01Light = PCMYKColor(39,39,0,25) # Light Lavender +color02Light = PCMYKColor(0,66,33,54) # Light Maroon +color03Light = PCMYKColor(0,0,19,25) # Light Yellow +color04Light = PCMYKColor(19,0,0,25) # Light Cyan +color05Light = PCMYKColor(0,100,0,69) # Light Purple +color06Light = PCMYKColor(0,49,49,25) # Light Salmon +color07Light = PCMYKColor(100,49,0,39) # Light Blue +color08Light = PCMYKColor(19,19,0,25) # Light PaleLavender +color09Light = PCMYKColor(100,100,0,62) # Light NavyBlue +color10Light = PCMYKColor(0,100,0,25) # Light Purple + +#Lowlight colors - eg for the sides of bars +color01Dark = PCMYKColor(39,39,0,49) # Dark Lavender +color02Dark = PCMYKColor(0,66,33,69) # Dark Maroon +color03Dark = PCMYKColor(0,0,20,49) # Dark Yellow +color04Dark = PCMYKColor(20,0,0,49) # Dark Cyan +color05Dark = PCMYKColor(0,100,0,80) # Dark Purple +color06Dark = PCMYKColor(0,50,50,49) # Dark Salmon +color07Dark = PCMYKColor(100,50,0,59) # Dark Blue +color08Dark = PCMYKColor(20,20,0,49) # Dark PaleLavender +color09Dark = PCMYKColor(100,100,0,79) # Dark NavyBlue +color10Dark = PCMYKColor(0,100,0,49) # Dark Purple + +#for standard grey backgrounds +backgroundGrey = PCMYKColor(0,0,0,24) + diff --git a/bin/reportlab/graphics/samples/exploded_pie.py b/bin/reportlab/graphics/samples/exploded_pie.py new file mode 100644 index 00000000000..8076493bd82 --- /dev/null +++ b/bin/reportlab/graphics/samples/exploded_pie.py @@ -0,0 +1,65 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.piecharts import Pie +from excelcolors import * +from reportlab.graphics.widgets.grids import ShadedRect +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class ExplodedPie(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,Pie(),name='chart',validate=None,desc="The main chart") + self.chart.width = 100 + self.chart.height = 100 + self.chart.x = 25 + self.chart.y = 25 + self.chart.slices[0].fillColor = color01 + self.chart.slices[1].fillColor = color02 + self.chart.slices[2].fillColor = color03 + self.chart.slices[3].fillColor = color04 + self.chart.slices[4].fillColor = color05 + self.chart.slices[5].fillColor = color06 + self.chart.slices[6].fillColor = color07 + self.chart.slices[7].fillColor = color08 + self.chart.slices[8].fillColor = color09 + self.chart.slices[9].fillColor = color10 + self.chart.data = (100, 150, 180) + self.chart.startAngle = -90 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'North'), (color02, 'South'), (color03, 'Central')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 160 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.Legend.columnMaximum = 10 + self.chart.slices.strokeWidth = 1 + self.chart.slices.fontName = 'Helvetica' + self.background = ShadedRect() + self.background.fillColorStart = backgroundGrey + self.background.fillColorEnd = backgroundGrey + self.background.numShades = 1 + self.background.strokeWidth = 0.5 + self.background.x = 20 + self.background.y = 20 + self.chart.slices.popout = 5 + self.background.height = 110 + self.background.width = 110 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ExplodedPie().save(formats=['pdf'],outDir=None,fnRoot='exploded_pie') \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/filled_radar.py b/bin/reportlab/graphics/samples/filled_radar.py new file mode 100644 index 00000000000..8439aa235c2 --- /dev/null +++ b/bin/reportlab/graphics/samples/filled_radar.py @@ -0,0 +1,54 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class FilledRadarChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,SpiderChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 90 + self.chart.height = 90 + self.chart.x = 45 + self.chart.y = 25 + self.chart.strands[0].fillColor = color01 + self.chart.strands[1].fillColor = color02 + self.chart.strands[2].fillColor = color03 + self.chart.strands[3].fillColor = color04 + self.chart.strands[4].fillColor = color05 + self.chart.strands[5].fillColor = color06 + self.chart.strands[6].fillColor = color07 + self.chart.strands[7].fillColor = color08 + self.chart.strands[8].fillColor = color09 + self.chart.strands[9].fillColor = color10 + self.chart.strandLabels.fontName = 'Helvetica' + self.chart.strandLabels.fontSize = 6 + self.chart.fillColor = backgroundGrey + self.chart.data = [(125, 180, 200), (100, 150, 180)] + self.chart.labels = ['North', 'South', 'Central'] + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + FilledRadarChart().save(formats=['pdf'],outDir=None,fnRoot='filled_radar') diff --git a/bin/reportlab/graphics/samples/line_chart.py b/bin/reportlab/graphics/samples/line_chart.py new file mode 100644 index 00000000000..49563a94766 --- /dev/null +++ b/bin/reportlab/graphics/samples/line_chart.py @@ -0,0 +1,83 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class LineChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,LinePlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500)), ((0, 150), (100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + LineChart().save(formats=['pdf'],outDir=None,fnRoot='line_chart') diff --git a/bin/reportlab/graphics/samples/linechart_with_markers.py b/bin/reportlab/graphics/samples/linechart_with_markers.py new file mode 100644 index 00000000000..2875cecb5d7 --- /dev/null +++ b/bin/reportlab/graphics/samples/linechart_with_markers.py @@ -0,0 +1,94 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.widgets.markers import makeMarker +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class LineChartWithMarkers(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,LinePlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines[0].symbol = makeMarker('FilledSquare') + self.chart.lines[1].symbol = makeMarker('FilledDiamond') + self.chart.lines[2].symbol = makeMarker('FilledStarFive') + self.chart.lines[3].symbol = makeMarker('FilledTriangle') + self.chart.lines[4].symbol = makeMarker('FilledCircle') + self.chart.lines[5].symbol = makeMarker('FilledPentagon') + self.chart.lines[6].symbol = makeMarker('FilledStarSix') + self.chart.lines[7].symbol = makeMarker('FilledHeptagon') + self.chart.lines[8].symbol = makeMarker('FilledOctagon') + self.chart.lines[9].symbol = makeMarker('FilledCross') + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((0, 50), (100,100), (200,200), (250,210), (300,300), (400,500)), ((0, 150), (100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + LineChartWithMarkers().save(formats=['pdf'],outDir=None,fnRoot='linechart_with_markers') diff --git a/bin/reportlab/graphics/samples/radar.py b/bin/reportlab/graphics/samples/radar.py new file mode 100644 index 00000000000..6d4d8d7b8e7 --- /dev/null +++ b/bin/reportlab/graphics/samples/radar.py @@ -0,0 +1,66 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from excelcolors import * +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label + +class RadarChart(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,SpiderChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 90 + self.chart.height = 90 + self.chart.x = 45 + self.chart.y = 25 + self.chart.strands[0].strokeColor= color01 + self.chart.strands[1].strokeColor= color02 + self.chart.strands[2].strokeColor= color03 + self.chart.strands[3].strokeColor= color04 + self.chart.strands[4].strokeColor= color05 + self.chart.strands[5].strokeColor= color06 + self.chart.strands[6].strokeColor= color07 + self.chart.strands[7].strokeColor= color08 + self.chart.strands[8].strokeColor= color09 + self.chart.strands[9].strokeColor= color10 + self.chart.strands[0].fillColor = None + self.chart.strands[1].fillColor = None + self.chart.strands[2].fillColor = None + self.chart.strands[3].fillColor = None + self.chart.strands[4].fillColor = None + self.chart.strands[5].fillColor = None + self.chart.strands[6].fillColor = None + self.chart.strands[7].fillColor = None + self.chart.strands[8].fillColor = None + self.chart.strands[9].fillColor = None + self.chart.strands.strokeWidth = 1 + self.chart.strandLabels.fontName = 'Helvetica' + self.chart.strandLabels.fontSize = 6 + self.chart.fillColor = backgroundGrey + self.chart.data = [(125, 180, 200), (100, 150, 180)] + self.chart.labels = ['North', 'South', 'Central'] + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.strands.strokeWidth = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + RadarChart().save(formats=['pdf'],outDir=None,fnRoot='radar') diff --git a/bin/reportlab/graphics/samples/runall.py b/bin/reportlab/graphics/samples/runall.py new file mode 100644 index 00000000000..938ab8fec42 --- /dev/null +++ b/bin/reportlab/graphics/samples/runall.py @@ -0,0 +1,59 @@ +# runs all the GUIedit charts in this directory - +# makes a PDF sample for eaxh existing chart type +import sys +import glob +import string +import inspect +import types + +def moduleClasses(mod): + def P(obj, m=mod.__name__, CT=types.ClassType): + return (type(obj)==CT and obj.__module__==m) + try: + return inspect.getmembers(mod, P)[0][1] + except: + return None + +def getclass(f): + return moduleClasses(__import__(f)) + +def run(format, VERBOSE=0): + formats = string.split(format, ',') + for i in range(0, len(formats)): + formats[i] == string.lower(string.strip(formats[i])) + allfiles = glob.glob('*.py') + allfiles.sort() + for fn in allfiles: + f = string.split(fn, '.')[0] + c = getclass(f) + if c != None: + print c.__name__ + try: + for fmt in formats: + if fmt: + c().save(formats=[fmt],outDir='.',fnRoot=c.__name__) + if VERBOSE: + print " %s.%s" % (c.__name__, fmt) + except: + print " COULDN'T CREATE '%s.%s'!" % (c.__name__, format) + +if __name__ == "__main__": + if len(sys.argv) == 1: + run('pdf,pict,png') + else: + try: + if sys.argv[1] == "-h": + print 'usage: runall.py [FORMAT] [-h]' + print ' if format is supplied is should be one or more of pdf,gif,eps,png etc' + print ' if format is missing the following formats are assumed: pdf,pict,png' + print ' -h prints this message' + else: + t = sys.argv[1:] + for f in t: + run(f) + except: + print 'usage: runall.py [FORMAT][-h]' + print ' if format is supplied is should be one or more of pdf,gif,eps,png etc' + print ' if format is missing the following formats are assumed: pdf,pict,png' + print ' -h prints this message' + raise diff --git a/bin/reportlab/graphics/samples/scatter.py b/bin/reportlab/graphics/samples/scatter.py new file mode 100644 index 00000000000..ea1a9991d3e --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter.py @@ -0,0 +1,71 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class Scatter(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + + + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + Scatter().save(formats=['pdf'],outDir=None,fnRoot='scatter') diff --git a/bin/reportlab/graphics/samples/scatter_lines.py b/bin/reportlab/graphics/samples/scatter_lines.py new file mode 100644 index 00000000000..16ed718b2fd --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter_lines.py @@ -0,0 +1,82 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class ScatterLines(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.lines[0].symbol = None + self.chart.lines[1].symbol = None + self.chart.lines[2].symbol = None + self.chart.lines[3].symbol = None + self.chart.lines[4].symbol = None + self.chart.lines[5].symbol = None + self.chart.lines[6].symbol = None + self.chart.lines[7].symbol = None + self.chart.lines[8].symbol = None + self.chart.lines[9].symbol = None + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.gridEnd = 115 + self.chart.yValueAxis.visibleGrid = 1 + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self.chart.joinedLines = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ScatterLines().save(formats=['pdf'],outDir=None,fnRoot='scatter_lines') diff --git a/bin/reportlab/graphics/samples/scatter_lines_markers.py b/bin/reportlab/graphics/samples/scatter_lines_markers.py new file mode 100644 index 00000000000..34f8ff220f5 --- /dev/null +++ b/bin/reportlab/graphics/samples/scatter_lines_markers.py @@ -0,0 +1,72 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.lineplots import ScatterPlot +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class ScatterLinesMarkers(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,ScatterPlot(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.lines[0].strokeColor = color01 + self.chart.lines[1].strokeColor = color02 + self.chart.lines[2].strokeColor = color03 + self.chart.lines[3].strokeColor = color04 + self.chart.lines[4].strokeColor = color05 + self.chart.lines[5].strokeColor = color06 + self.chart.lines[6].strokeColor = color07 + self.chart.lines[7].strokeColor = color08 + self.chart.lines[8].strokeColor = color09 + self.chart.lines[9].strokeColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.lineLabels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontName = 'Helvetica' + self.chart.xValueAxis.labels.fontSize = 7 + self.chart.xValueAxis.forceZero = 0 + self.chart.data = [((100,100), (200,200), (250,210), (300,300), (400,500)), ((100,200), (200,300), (250,200), (300,400), (400, 600))] + self.chart.xValueAxis.avoidBoundFrac = 1 + self.chart.xValueAxis.gridEnd = 115 + self.chart.xValueAxis.tickDown = 3 + self.chart.xValueAxis.visibleGrid = 1 + self.chart.yValueAxis.tickLeft = 3 + self.chart.yValueAxis.labels.fontName = 'Helvetica' + self.chart.yValueAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.lineLabelFormat = None + self.chart.xLabel = 'X Axis' + self.chart.y = 30 + self.chart.yLabel = 'Y Axis' + self.chart.yValueAxis.gridEnd = 115 + self.chart.yValueAxis.visibleGrid = 1 + self.chart.yValueAxis.labelTextFormat = '%d' + self.chart.yValueAxis.forceZero = 1 + self.chart.xValueAxis.forceZero = 1 + self.chart.joinedLines = 1 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + ScatterLinesMarkers().save(formats=['pdf'],outDir=None,fnRoot='scatter_lines_markers') diff --git a/bin/reportlab/graphics/samples/simple_pie.py b/bin/reportlab/graphics/samples/simple_pie.py new file mode 100644 index 00000000000..7542607f6ae --- /dev/null +++ b/bin/reportlab/graphics/samples/simple_pie.py @@ -0,0 +1,61 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.piecharts import Pie +from reportlab.graphics.widgets.grids import ShadedRect +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class SimplePie(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,Pie(),name='chart',validate=None,desc="The main chart") + self.chart.width = 100 + self.chart.height = 100 + self.chart.x = 25 + self.chart.y = 25 + self.chart.slices[0].fillColor = color01 + self.chart.slices[1].fillColor = color02 + self.chart.slices[2].fillColor = color03 + self.chart.slices[3].fillColor = color04 + self.chart.slices[4].fillColor = color05 + self.chart.slices[5].fillColor = color06 + self.chart.slices[6].fillColor = color07 + self.chart.slices[7].fillColor = color08 + self.chart.slices[8].fillColor = color09 + self.chart.slices[9].fillColor = color10 + self.chart.data = (100, 150, 180) + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'North'), (color02, 'South'),(color03, 'Central')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 160 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self.chart.slices.strokeWidth = 1 + self.chart.slices.fontName = 'Helvetica' + self.background = ShadedRect() + self.background.fillColorStart = backgroundGrey + self.background.fillColorEnd = backgroundGrey + self.background.numShades = 1 + self.background.strokeWidth = 0.5 + self.background.x = 25 + self.background.y = 25 + self.Legend.columnMaximum = 10 + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + SimplePie().save(formats=['pdf'],outDir=None,fnRoot=None) \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/stacked_bar.py b/bin/reportlab/graphics/samples/stacked_bar.py new file mode 100644 index 00000000000..9ba2a962f82 --- /dev/null +++ b/bin/reportlab/graphics/samples/stacked_bar.py @@ -0,0 +1,85 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.barcharts import HorizontalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class StackedBar(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,HorizontalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 6 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 80 + self.chart.valueAxis.tickDown = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickLeft = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 6 + self.chart.categoryAxis.labels.dx = -3 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.categoryAxis.style='stacked' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + StackedBar().save(formats=['pdf'],outDir=None,fnRoot='stacked_bar') \ No newline at end of file diff --git a/bin/reportlab/graphics/samples/stacked_column.py b/bin/reportlab/graphics/samples/stacked_column.py new file mode 100644 index 00000000000..a50a1598c90 --- /dev/null +++ b/bin/reportlab/graphics/samples/stacked_column.py @@ -0,0 +1,84 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.shapes import Drawing, _DrawingEditorMixin, String +from reportlab.graphics.charts.textlabels import Label +from excelcolors import * + +class StackedColumn(_DrawingEditorMixin,Drawing): + def __init__(self,width=200,height=150,*args,**kw): + apply(Drawing.__init__,(self,width,height)+args,kw) + self._add(self,VerticalBarChart(),name='chart',validate=None,desc="The main chart") + self.chart.width = 115 + self.chart.height = 80 + self.chart.x = 30 + self.chart.y = 40 + self.chart.bars[0].fillColor = color01 + self.chart.bars[1].fillColor = color02 + self.chart.bars[2].fillColor = color03 + self.chart.bars[3].fillColor = color04 + self.chart.bars[4].fillColor = color05 + self.chart.bars[5].fillColor = color06 + self.chart.bars[6].fillColor = color07 + self.chart.bars[7].fillColor = color08 + self.chart.bars[8].fillColor = color09 + self.chart.bars[9].fillColor = color10 + self.chart.fillColor = backgroundGrey + self.chart.barLabels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontName = 'Helvetica' + self.chart.valueAxis.labels.fontSize = 7 + self.chart.valueAxis.forceZero = 1 + self.chart.data = [(100, 150, 180), (125, 180, 200)] + self.chart.groupSpacing = 15 + self.chart.valueAxis.avoidBoundFrac = 1 + self.chart.valueAxis.gridEnd = 115 + self.chart.valueAxis.tickLeft = 3 + self.chart.valueAxis.visibleGrid = 1 + self.chart.categoryAxis.categoryNames = ['North', 'South', 'Central'] + self.chart.categoryAxis.tickDown = 3 + self.chart.categoryAxis.labels.fontName = 'Helvetica' + self.chart.categoryAxis.labels.fontSize = 7 + self._add(self,Label(),name='Title',validate=None,desc="The title at the top of the chart") + self.Title.fontName = 'Helvetica-Bold' + self.Title.fontSize = 7 + self.Title.x = 100 + self.Title.y = 135 + self.Title._text = 'Chart Title' + self.Title.maxWidth = 180 + self.Title.height = 20 + self.Title.textAnchor ='middle' + self._add(self,Legend(),name='Legend',validate=None,desc="The legend or key for the chart") + self.Legend.colorNamePairs = [(color01, 'Widgets'), (color02, 'Sprockets')] + self.Legend.fontName = 'Helvetica' + self.Legend.fontSize = 7 + self.Legend.x = 153 + self.Legend.y = 85 + self.Legend.dxTextSpace = 5 + self.Legend.dy = 5 + self.Legend.dx = 5 + self.Legend.deltay = 5 + self.Legend.alignment ='right' + self._add(self,Label(),name='XLabel',validate=None,desc="The label on the horizontal axis") + self.XLabel.fontName = 'Helvetica' + self.XLabel.fontSize = 7 + self.XLabel.x = 85 + self.XLabel.y = 10 + self.XLabel.textAnchor ='middle' + self.XLabel.maxWidth = 100 + self.XLabel.height = 20 + self.XLabel._text = "X Axis" + self._add(self,Label(),name='YLabel',validate=None,desc="The label on the vertical axis") + self.YLabel.fontName = 'Helvetica' + self.YLabel.fontSize = 7 + self.YLabel.x = 12 + self.YLabel.y = 80 + self.YLabel.angle = 90 + self.YLabel.textAnchor ='middle' + self.YLabel.maxWidth = 100 + self.YLabel.height = 20 + self.YLabel._text = "Y Axis" + self.chart.categoryAxis.style='stacked' + self._add(self,0,name='preview',validate=None,desc=None) + +if __name__=="__main__": #NORUNTESTS + StackedColumn().save(formats=['pdf'],outDir=None,fnRoot='stacked_column') diff --git a/bin/reportlab/graphics/shapes.py b/bin/reportlab/graphics/shapes.py new file mode 100644 index 00000000000..896385e8bee --- /dev/null +++ b/bin/reportlab/graphics/shapes.py @@ -0,0 +1,1289 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/shapes.py +""" +core of the graphics library - defines Drawing and Shapes +""" +__version__=''' $Id: shapes.py 2845 2006-05-03 12:24:35Z rgbecker $ ''' + +import string, os, sys +from math import pi, cos, sin, tan +from types import FloatType, IntType, ListType, TupleType, StringType, InstanceType +from pprint import pprint + +from reportlab.platypus import Flowable +from reportlab.rl_config import shapeChecking, verbose, defaultGraphicsFontName, _unset_ +from reportlab.lib import logger +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.lib.utils import fp_str +from reportlab.pdfbase.pdfmetrics import stringWidth + +class NotImplementedError(Exception): + pass + +# two constants for filling rules +NON_ZERO_WINDING = 'Non-Zero Winding' +EVEN_ODD = 'Even-Odd' + +## these can be overridden at module level before you start +#creating shapes. So, if using a special color model, +#this provides support for the rendering mechanism. +#you can change defaults globally before you start +#making shapes; one use is to substitute another +#color model cleanly throughout the drawing. + +STATE_DEFAULTS = { # sensible defaults for all + 'transform': (1,0,0,1,0,0), + + # styles follow SVG naming + 'strokeColor': colors.black, + 'strokeWidth': 1, + 'strokeLineCap': 0, + 'strokeLineJoin': 0, + 'strokeMiterLimit' : 'TBA', # don't know yet so let bomb here + 'strokeDashArray': None, + 'strokeOpacity': 1.0, #100% + + 'fillColor': colors.black, #...or text will be invisible + #'fillRule': NON_ZERO_WINDING, - these can be done later + #'fillOpacity': 1.0, #100% - can be done later + + 'fontSize': 10, + 'fontName': defaultGraphicsFontName, + 'textAnchor': 'start' # can be start, middle, end, inherited + } + + +#################################################################### +# math utilities. These could probably be moved into lib +# somewhere. +#################################################################### + +# constructors for matrices: +def nullTransform(): + return (1, 0, 0, 1, 0, 0) + +def translate(dx, dy): + return (1, 0, 0, 1, dx, dy) + +def scale(sx, sy): + return (sx, 0, 0, sy, 0, 0) + +def rotate(angle): + a = angle * pi/180 + return (cos(a), sin(a), -sin(a), cos(a), 0, 0) + +def skewX(angle): + a = angle * pi/180 + return (1, 0, tan(a), 1, 0, 0) + +def skewY(angle): + a = angle * pi/180 + return (1, tan(a), 0, 1, 0, 0) + +def mmult(A, B): + "A postmultiplied by B" + # I checked this RGB + # [a0 a2 a4] [b0 b2 b4] + # [a1 a3 a5] * [b1 b3 b5] + # [ 1 ] [ 1 ] + # + return (A[0]*B[0] + A[2]*B[1], + A[1]*B[0] + A[3]*B[1], + A[0]*B[2] + A[2]*B[3], + A[1]*B[2] + A[3]*B[3], + A[0]*B[4] + A[2]*B[5] + A[4], + A[1]*B[4] + A[3]*B[5] + A[5]) + +def inverse(A): + "For A affine 2D represented as 6vec return 6vec version of A**(-1)" + # I checked this RGB + det = float(A[0]*A[3] - A[2]*A[1]) + R = [A[3]/det, -A[1]/det, -A[2]/det, A[0]/det] + return tuple(R+[-R[0]*A[4]-R[2]*A[5],-R[1]*A[4]-R[3]*A[5]]) + +def zTransformPoint(A,v): + "Apply the homogenous part of atransformation a to vector v --> A*v" + return (A[0]*v[0]+A[2]*v[1],A[1]*v[0]+A[3]*v[1]) + +def transformPoint(A,v): + "Apply transformation a to vector v --> A*v" + return (A[0]*v[0]+A[2]*v[1]+A[4],A[1]*v[0]+A[3]*v[1]+A[5]) + +def transformPoints(matrix, V): + return map(transformPoint, V) + +def zTransformPoints(matrix, V): + return map(lambda x,matrix=matrix: zTransformPoint(matrix,x), V) + +def _textBoxLimits(text, font, fontSize, leading, textAnchor, boxAnchor): + w = 0 + for t in text: + w = max(w,stringWidth(t,font, fontSize)) + + h = len(text)*leading + yt = fontSize + if boxAnchor[0]=='s': + yb = -h + yt = yt - h + elif boxAnchor[0]=='n': + yb = 0 + else: + yb = -h/2.0 + yt = yt + yb + + if boxAnchor[-1]=='e': + xb = -w + if textAnchor=='end': xt = 0 + elif textAnchor=='start': xt = -w + else: xt = -w/2.0 + elif boxAnchor[-1]=='w': + xb = 0 + if textAnchor=='end': xt = w + elif textAnchor=='start': xt = 0 + else: xt = w/2.0 + else: + xb = -w/2.0 + if textAnchor=='end': xt = -xb + elif textAnchor=='start': xt = xb + else: xt = 0 + + return xb, yb, w, h, xt, yt + +def _rotatedBoxLimits( x, y, w, h, angle): + ''' + Find the corner points of the rotated w x h sized box at x,y + return the corner points and the min max points in the original space + ''' + C = zTransformPoints(rotate(angle),((x,y),(x+w,y),(x+w,y+h),(x,y+h))) + X = map(lambda x: x[0], C) + Y = map(lambda x: x[1], C) + return min(X), max(X), min(Y), max(Y), C + + +class _DrawTimeResizeable: + '''Addin class to provide the horribleness of _drawTimeResize''' + def _drawTimeResize(self,w,h): + if hasattr(self,'_canvas'): + canvas = self._canvas + drawing = canvas._drawing + drawing.width, drawing.height = w, h + if hasattr(canvas,'_drawTimeResize'): + canvas._drawTimeResize(w,h) + +class _SetKeyWordArgs: + def __init__(self, keywords={}): + """In general properties may be supplied to the constructor.""" + for key, value in keywords.items(): + setattr(self, key, value) + + +################################################################# +# +# Helper functions for working out bounds +# +################################################################# + +def getRectsBounds(rectList): + # filter out any None objects, e.g. empty groups + L = filter(lambda x: x is not None, rectList) + if not L: return None + + xMin, yMin, xMax, yMax = L[0] + for (x1, y1, x2, y2) in L[1:]: + if x1 < xMin: + xMin = x1 + if x2 > xMax: + xMax = x2 + if y1 < yMin: + yMin = y1 + if y2 > yMax: + yMax = y2 + return (xMin, yMin, xMax, yMax) + +def getPathBounds(points): + n = len(points) + f = lambda i,p = points: p[i] + xs = map(f,xrange(0,n,2)) + ys = map(f,xrange(1,n,2)) + return (min(xs), min(ys), max(xs), max(ys)) + +def getPointsBounds(pointList): + "Helper function for list of points" + first = pointList[0] + if type(first) in (ListType, TupleType): + xs = map(lambda xy: xy[0],pointList) + ys = map(lambda xy: xy[1],pointList) + return (min(xs), min(ys), max(xs), max(ys)) + else: + return getPathBounds(pointList) + +################################################################# +# +# And now the shapes themselves.... +# +################################################################# +class Shape(_SetKeyWordArgs,_DrawTimeResizeable): + """Base class for all nodes in the tree. Nodes are simply + packets of data to be created, stored, and ultimately + rendered - they don't do anything active. They provide + convenience methods for verification but do not + check attribiute assignments or use any clever setattr + tricks this time.""" + _attrMap = AttrMap() + + def copy(self): + """Return a clone of this shape.""" + + # implement this in the descendants as they need the right init methods. + raise NotImplementedError, "No copy method implemented for %s" % self.__class__.__name__ + + def getProperties(self,recur=1): + """Interface to make it easy to extract automatic + documentation""" + + #basic nodes have no children so this is easy. + #for more complex objects like widgets you + #may need to override this. + props = {} + for key, value in self.__dict__.items(): + if key[0:1] <> '_': + props[key] = value + return props + + def setProperties(self, props): + """Supports the bulk setting if properties from, + for example, a GUI application or a config file.""" + + self.__dict__.update(props) + #self.verify() + + def dumpProperties(self, prefix=""): + """Convenience. Lists them on standard output. You + may provide a prefix - mostly helps to generate code + samples for documentation.""" + + propList = self.getProperties().items() + propList.sort() + if prefix: + prefix = prefix + '.' + for (name, value) in propList: + print '%s%s = %s' % (prefix, name, value) + + def verify(self): + """If the programmer has provided the optional + _attrMap attribute, this checks all expected + attributes are present; no unwanted attributes + are present; and (if a checking function is found) + checks each attribute. Either succeeds or raises + an informative exception.""" + + if self._attrMap is not None: + for key in self.__dict__.keys(): + if key[0] <> '_': + assert self._attrMap.has_key(key), "Unexpected attribute %s found in %s" % (key, self) + for (attr, metavalue) in self._attrMap.items(): + assert hasattr(self, attr), "Missing attribute %s from %s" % (attr, self) + value = getattr(self, attr) + assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % (value, attr, self.__class__.__name__) + + if shapeChecking: + """This adds the ability to check every attribute assignment as it is made. + It slows down shapes but is a big help when developing. It does not + get defined if rl_config.shapeChecking = 0""" + def __setattr__(self, attr, value): + """By default we verify. This could be off + in some parallel base classes.""" + validateSetattr(self,attr,value) #from reportlab.lib.attrmap + + def getBounds(self): + "Returns bounding rectangle of object as (x1,y1,x2,y2)" + raise NotImplementedError("Shapes and widgets must implement getBounds") + +class Group(Shape): + """Groups elements together. May apply a transform + to its contents. Has a publicly accessible property + 'contents' which may be used to iterate over contents. + In addition, child nodes may be given a name in which + case they are subsequently accessible as properties.""" + + _attrMap = AttrMap( + transform = AttrMapValue(isTransform,desc="Coordinate transformation to apply"), + contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), + ) + + def __init__(self, *elements, **keywords): + """Initial lists of elements may be provided to allow + compact definitions in literal Python code. May or + may not be useful.""" + + # Groups need _attrMap to be an instance rather than + # a class attribute, as it may be extended at run time. + self._attrMap = self._attrMap.clone() + self.contents = [] + self.transform = (1,0,0,1,0,0) + for elt in elements: + self.add(elt) + # this just applies keywords; do it at the end so they + #don;t get overwritten + _SetKeyWordArgs.__init__(self, keywords) + + def _addNamedNode(self,name,node): + 'if name is not None add an attribute pointing to node and add to the attrMap' + if name: + if name not in self._attrMap.keys(): + self._attrMap[name] = AttrMapValue(isValidChild) + setattr(self, name, node) + + def add(self, node, name=None): + """Appends non-None child node to the 'contents' attribute. In addition, + if a name is provided, it is subsequently accessible by name + """ + # propagates properties down + if node is not None: + assert isValidChild(node), "Can only add Shape or UserNode objects to a Group" + self.contents.append(node) + self._addNamedNode(name,node) + + def _nn(self,node): + self.add(node) + return self.contents[-1] + + def insert(self, i, n, name=None): + 'Inserts sub-node n in contents at specified location' + if n is not None: + assert isValidChild(n), "Can only insert Shape or UserNode objects in a Group" + if i<0: + self.contents[i:i] =[n] + else: + self.contents.insert(i,n) + self._addNamedNode(name,n) + + def expandUserNodes(self): + """Return a new object which only contains primitive shapes.""" + + # many limitations - shared nodes become multiple ones, + obj = isinstance(self,Drawing) and Drawing(self.width,self.height) or Group() + obj._attrMap = self._attrMap.clone() + if hasattr(obj,'transform'): obj.transform = self.transform[:] + + self_contents = self.contents + a = obj.contents.append + for child in self_contents: + if isinstance(child, UserNode): + newChild = child.provideNode() + elif isinstance(child, Group): + newChild = child.expandUserNodes() + else: + newChild = child.copy() + a(newChild) + + self._copyNamedContents(obj) + return obj + + def _explode(self): + ''' return a fully expanded object''' + from reportlab.graphics.widgetbase import Widget + obj = Group() + if hasattr(obj,'transform'): obj.transform = self.transform[:] + P = self.contents[:] # pending nodes + while P: + n = P.pop(0) + if isinstance(n, UserNode): + P.append(n.provideNode()) + elif isinstance(n, Group): + n = n._explode() + if n.transform==(1,0,0,1,0,0): + obj.contents.extend(n.contents) + else: + obj.add(n) + else: + obj.add(n) + return obj + + def _copyContents(self,obj): + for child in self.contents: + obj.contents.append(child) + + def _copyNamedContents(self,obj,aKeys=None,noCopy=('contents',)): + from copy import copy + self_contents = self.contents + if not aKeys: aKeys = self._attrMap.keys() + for (k, v) in self.__dict__.items(): + if v in self_contents: + pos = self_contents.index(v) + setattr(obj, k, obj.contents[pos]) + elif k in aKeys and k not in noCopy: + setattr(obj, k, copy(v)) + + def _copy(self,obj): + """copies to obj""" + obj._attrMap = self._attrMap.clone() + self._copyContents(obj) + self._copyNamedContents(obj) + return obj + + def copy(self): + """returns a copy""" + return self._copy(self.__class__()) + + def rotate(self, theta): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, rotate(theta)) + + def translate(self, dx, dy): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, translate(dx, dy)) + + def scale(self, sx, sy): + """Convenience to help you set transforms""" + self.transform = mmult(self.transform, scale(sx, sy)) + + + def skew(self, kx, ky): + """Convenience to help you set transforms""" + self.transform = mmult(mmult(self.transform, skewX(kx)),skewY(ky)) + + def shift(self, x, y): + '''Convenience function to set the origin arbitrarily''' + self.transform = self.transform[:-2]+(x,y) + + def asDrawing(self, width, height): + """ Convenience function to make a drawing from a group + After calling this the instance will be a drawing! + """ + self.__class__ = Drawing + self._attrMap.update(self._xtraAttrMap) + self.width = width + self.height = height + + def getContents(self): + '''Return the list of things to be rendered + override to get more complicated behaviour''' + b = getattr(self,'background',None) + C = self.contents + if b and b not in C: C = [b]+C + return C + + def getBounds(self): + if self.contents: + b = [] + for elem in self.contents: + b.append(elem.getBounds()) + x1 = getRectsBounds(b) + if x1 is None: return None + x1, y1, x2, y2 = x1 + trans = self.transform + corners = [[x1,y1], [x1, y2], [x2, y1], [x2,y2]] + newCorners = [] + for corner in corners: + newCorners.append(transformPoint(trans, corner)) + return getPointsBounds(newCorners) + else: + #empty group needs a sane default; this + #will happen when interactively creating a group + #nothing has been added to yet. The alternative is + #to handle None as an allowed return value everywhere. + return None + +def _addObjImport(obj,I,n=None): + '''add an import of obj's class to a dictionary of imports''' #' + from inspect import getmodule + c = obj.__class__ + m = getmodule(c).__name__ + n = n or c.__name__ + if not I.has_key(m): + I[m] = [n] + elif n not in I[m]: + I[m].append(n) + +def _repr(self,I=None): + '''return a repr style string with named fixed args first, then keywords''' + if type(self) is InstanceType: + if self is EmptyClipPath: + _addObjImport(self,I,'EmptyClipPath') + return 'EmptyClipPath' + if I: _addObjImport(self,I) + if isinstance(self,Shape): + from inspect import getargs + args, varargs, varkw = getargs(self.__init__.im_func.func_code) + P = self.getProperties() + s = self.__class__.__name__+'(' + for n in args[1:]: + v = P[n] + del P[n] + s = s + '%s,' % _repr(v,I) + for n,v in P.items(): + v = P[n] + s = s + '%s=%s,' % (n, _repr(v,I)) + return s[:-1]+')' + else: + return repr(self) + elif type(self) is FloatType: + return fp_str(self) + elif type(self) in (ListType,TupleType): + s = '' + for v in self: + s = s + '%s,' % _repr(v,I) + if type(self) is ListType: + return '[%s]' % s[:-1] + else: + return '(%s%s)' % (s[:-1],len(self)==1 and ',' or '') + else: + return repr(self) + +def _renderGroupPy(G,pfx,I,i=0,indent='\t\t'): + s = '' + C = getattr(G,'transform',None) + if C: s = s + ('%s%s.transform = %s\n' % (indent,pfx,_repr(C))) + C = G.contents + for n in C: + if isinstance(n, Group): + npfx = 'v%d' % i + i = i + 1 + s = s + '%s%s=%s._nn(Group())\n' % (indent,npfx,pfx) + s = s + _renderGroupPy(n,npfx,I,i,indent) + i = i - 1 + else: + s = s + '%s%s.add(%s)\n' % (indent,pfx,_repr(n,I)) + return s + +def _extraKW(self,pfx,**kw): + kw.update(self.__dict__) + R = {} + n = len(pfx) + for k in kw.keys(): + if k.startswith(pfx): + R[k[n:]] = kw[k] + return R + +class Drawing(Group, Flowable): + """Outermost container; the thing a renderer works on. + This has no properties except a height, width and list + of contents.""" + + _xtraAttrMap = AttrMap( + width = AttrMapValue(isNumber,desc="Drawing width in points."), + height = AttrMapValue(isNumber,desc="Drawing height in points."), + canv = AttrMapValue(None), + background = AttrMapValue(isValidChildOrNone,desc="Background widget for the drawing"), + hAlign = AttrMapValue(OneOf("LEFT", "RIGHT", "CENTER", "CENTRE"), desc="Horizontal alignment within parent document"), + vAlign = AttrMapValue(OneOf("TOP", "BOTTOM", "CENTER", "CENTRE"), desc="Vertical alignment within parent document"), + #AR temporary hack to track back up. + #fontName = AttrMapValue(isStringOrNone), + renderScale = AttrMapValue(isNumber,desc="Global scaling for rendering"), + ) + + _attrMap = AttrMap(BASE=Group) + _attrMap.update(_xtraAttrMap) + + def __init__(self, width=400, height=200, *nodes, **keywords): + self.background = None + apply(Group.__init__,(self,)+nodes,keywords) + self.width = width + self.height = height + self.hAlign = 'LEFT' + self.vAlign = 'BOTTOM' + self.renderScale = 1.0 + + def _renderPy(self): + I = {'reportlab.graphics.shapes': ['_DrawingEditorMixin','Drawing','Group']} + G = _renderGroupPy(self._explode(),'self',I) + n = 'ExplodedDrawing_' + self.__class__.__name__ + s = '#Autogenerated by ReportLab guiedit do not edit\n' + for m, o in I.items(): + s = s + 'from %s import %s\n' % (m,string.replace(str(o)[1:-1],"'","")) + s = s + '\nclass %s(_DrawingEditorMixin,Drawing):\n' % n + s = s + '\tdef __init__(self,width=%s,height=%s,*args,**kw):\n' % (self.width,self.height) + s = s + '\t\tapply(Drawing.__init__,(self,width,height)+args,kw)\n' + s = s + G + s = s + '\n\nif __name__=="__main__": #NORUNTESTS\n\t%s().save(formats=[\'pdf\'],outDir=\'.\',fnRoot=None)\n' % n + return s + + def draw(self,showBoundary=_unset_): + """This is used by the Platypus framework to let the document + draw itself in a story. It is specific to PDF and should not + be used directly.""" + import renderPDF + renderPDF.draw(self, self.canv, 0, 0, showBoundary=showBoundary) + + def wrap(self, availWidth, availHeight): + width = self.width + height = self.height + renderScale = self.renderScale + if renderScale!=1.0: + width *= renderScale + height *= renderScale + return width, height + + def expandUserNodes(self): + """Return a new drawing which only contains primitive shapes.""" + obj = Group.expandUserNodes(self) + obj.width = self.width + obj.height = self.height + return obj + + def copy(self): + """Returns a copy""" + return self._copy(self.__class__(self.width, self.height)) + + def asGroup(self,*args,**kw): + return self._copy(apply(Group,args,kw)) + + def save(self, formats=None, verbose=None, fnRoot=None, outDir=None, title='', **kw): + """Saves copies of self in desired location and formats. + Multiple formats can be supported in one call + + the extra keywords can be of the form + _renderPM_dpi=96 (which passes dpi=96 to renderPM) + """ + from reportlab import rl_config + ext = '' + if not fnRoot: + fnRoot = getattr(self,'fileNamePattern',(self.__class__.__name__+'%03d')) + chartId = getattr(self,'chartId',0) + if callable(fnRoot): + fnRoot = fnRoot(chartId) + else: + try: + fnRoot = fnRoot % getattr(self,'chartId',0) + except TypeError, err: + #the exact error message changed from 2.2 to 2.3 so we need to + #check a substring + if str(err).find('not all arguments converted') < 0: raise + + if os.path.isabs(fnRoot): + outDir, fnRoot = os.path.split(fnRoot) + else: + outDir = outDir or getattr(self,'outDir','.') + outDir = outDir.rstrip().rstrip(os.sep) + if not outDir: outDir = '.' + if not os.path.isabs(outDir): outDir = os.path.join(getattr(self,'_override_CWD',os.path.dirname(sys.argv[0])),outDir) + if not os.path.isdir(outDir): os.makedirs(outDir) + fnroot = os.path.normpath(os.path.join(outDir,fnRoot)) + plotMode = os.path.splitext(fnroot) + if string.lower(plotMode[1][1:]) in ['pdf','ps','eps','gif','png','jpg','jpeg','pct','pict','tiff','tif','py','bmp','svg']: + fnroot = plotMode[0] + + plotMode, verbose = formats or getattr(self,'formats',['pdf']), (verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),))[0] + _saved = logger.warnOnce.enabled, logger.infoOnce.enabled + logger.warnOnce.enabled = logger.infoOnce.enabled = verbose + if 'pdf' in plotMode: + from reportlab.graphics import renderPDF + filename = fnroot+'.pdf' + if verbose: print "generating PDF file %s" % filename + renderPDF.drawToFile(self, filename, title, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPDF_',**kw)) + ext = ext + '/.pdf' + if sys.platform=='mac': + import macfs, macostools + macfs.FSSpec(filename).SetCreatorType("CARO", "PDF ") + macostools.touched(filename) + + for bmFmt in ['gif','png','tif','jpg','tiff','pct','pict', 'bmp']: + if bmFmt in plotMode: + from reportlab.graphics import renderPM + filename = '%s.%s' % (fnroot,bmFmt) + if verbose: print "generating %s file %s" % (bmFmt,filename) + renderPM.drawToFile(self, filename,fmt=bmFmt,showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPM_',**kw)) + ext = ext + '/.' + bmFmt + + if 'eps' in plotMode: + from rlextra.graphics import renderPS_SEP + filename = fnroot+'.eps' + if verbose: print "generating EPS file %s" % filename + renderPS_SEP.drawToFile(self, + filename, + title = fnroot, + dept = getattr(self,'EPS_info',['Testing'])[0], + company = getattr(self,'EPS_info',['','ReportLab'])[1], + preview = getattr(self,'preview',1), + showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw)) + ext = ext + '/.eps' + + if 'svg' in plotMode: + from reportlab.graphics import renderSVG + filename = fnroot+'.svg' + if verbose: print "generating EPS file %s" % filename + renderSVG.drawToFile(self, + filename, + showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderSVG_',**kw)) + ext = ext + '/.svg' + + if 'ps' in plotMode: + from reportlab.graphics import renderPS + filename = fnroot+'.ps' + if verbose: print "generating EPS file %s" % filename + renderPS.drawToFile(self, filename, showBoundary=getattr(self,'showBorder',rl_config.showBoundary),**_extraKW(self,'_renderPS_',**kw)) + ext = ext + '/.ps' + + if 'py' in plotMode: + filename = fnroot+'.py' + if verbose: print "generating py file %s" % filename + open(filename,'w').write(self._renderPy()) + ext = ext + '/.py' + + logger.warnOnce.enabled, logger.infoOnce.enabled = _saved + if hasattr(self,'saveLogger'): + self.saveLogger(fnroot,ext) + return ext and fnroot+ext[1:] or '' + + + def asString(self, format, verbose=None, preview=0): + """Converts to an 8 bit string in given format.""" + assert format in ['pdf','ps','eps','gif','png','jpg','jpeg','bmp','ppm','tiff','tif','py','pict','pct'], 'Unknown file format "%s"' % format + from reportlab import rl_config + #verbose = verbose is not None and (verbose,) or (getattr(self,'verbose',verbose),)[0] + if format == 'pdf': + from reportlab.graphics import renderPDF + return renderPDF.drawToString(self) + elif format in ['gif','png','tif','jpg','pct','pict','bmp','ppm']: + from reportlab.graphics import renderPM + return renderPM.drawToString(self, fmt=format) + elif format == 'eps': + from rlextra.graphics import renderPS_SEP + return renderPS_SEP.drawToString(self, + preview = preview, + showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + elif format == 'ps': + from reportlab.graphics import renderPS + return renderPS.drawToString(self, showBoundary=getattr(self,'showBorder',rl_config.showBoundary)) + elif format == 'py': + return self._renderPy() + +class _DrawingEditorMixin: + '''This is a mixin to provide functionality for edited drawings''' + def _add(self,obj,value,name=None,validate=None,desc=None,pos=None): + ''' + effectively setattr(obj,name,value), but takes care of things with _attrMaps etc + ''' + ivc = isValidChild(value) + if name and hasattr(obj,'_attrMap'): + if not obj.__dict__.has_key('_attrMap'): + obj._attrMap = obj._attrMap.clone() + if ivc and validate is None: validate = isValidChild + obj._attrMap[name] = AttrMapValue(validate,desc) + if hasattr(obj,'add') and ivc: + if pos: + obj.insert(pos,value,name) + else: + obj.add(value,name) + elif name: + setattr(obj,name,value) + else: + raise ValueError, "Can't add, need name" + +class LineShape(Shape): + # base for types of lines + + _attrMap = AttrMap( + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + strokeLineCap = AttrMapValue(None), + strokeLineJoin = AttrMapValue(None), + strokeMiterLimit = AttrMapValue(isNumber), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + ) + + def __init__(self, kw): + self.strokeColor = STATE_DEFAULTS['strokeColor'] + self.strokeWidth = 1 + self.strokeLineCap = 0 + self.strokeLineJoin = 0 + self.strokeMiterLimit = 0 + self.strokeDashArray = None + self.setProperties(kw) + + +class Line(LineShape): + _attrMap = AttrMap(BASE=LineShape, + x1 = AttrMapValue(isNumber), + y1 = AttrMapValue(isNumber), + x2 = AttrMapValue(isNumber), + y2 = AttrMapValue(isNumber), + ) + + def __init__(self, x1, y1, x2, y2, **kw): + LineShape.__init__(self, kw) + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + + def getBounds(self): + "Returns bounding rectangle of object as (x1,y1,x2,y2)" + return (self.x1, self.y1, self.x2, self.y2) + + +class SolidShape(LineShape): + # base for anything with outline and content + + _attrMap = AttrMap(BASE=LineShape, + fillColor = AttrMapValue(isColorOrNone), + ) + + def __init__(self, kw): + self.fillColor = STATE_DEFAULTS['fillColor'] + # do this at the end so keywords overwrite + #the above settings + LineShape.__init__(self, kw) + + +# path operator constants +_MOVETO, _LINETO, _CURVETO, _CLOSEPATH = range(4) +_PATH_OP_ARG_COUNT = (2, 2, 6, 0) # [moveTo, lineTo, curveTo, closePath] +_PATH_OP_NAMES=['moveTo','lineTo','curveTo','closePath'] + +def _renderPath(path, drawFuncs): + """Helper function for renderers.""" + # this could be a method of Path... + points = path.points + i = 0 + hadClosePath = 0 + hadMoveTo = 0 + for op in path.operators: + nArgs = _PATH_OP_ARG_COUNT[op] + func = drawFuncs[op] + j = i + nArgs + apply(func, points[i:j]) + i = j + if op == _CLOSEPATH: + hadClosePath = hadClosePath + 1 + if op == _MOVETO: + hadMoveTo = hadMoveTo + 1 + return hadMoveTo == hadClosePath + +class Path(SolidShape): + """Path, made up of straight lines and bezier curves.""" + + _attrMap = AttrMap(BASE=SolidShape, + points = AttrMapValue(isListOfNumbers), + operators = AttrMapValue(isListOfNumbers), + isClipPath = AttrMapValue(isBoolean), + ) + + def __init__(self, points=None, operators=None, isClipPath=0, **kw): + SolidShape.__init__(self, kw) + if points is None: + points = [] + if operators is None: + operators = [] + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + self.operators = operators + self.isClipPath = isClipPath + + def copy(self): + new = self.__class__(self.points[:], self.operators[:]) + new.setProperties(self.getProperties()) + return new + + def moveTo(self, x, y): + self.points.extend([x, y]) + self.operators.append(_MOVETO) + + def lineTo(self, x, y): + self.points.extend([x, y]) + self.operators.append(_LINETO) + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self.points.extend([x1, y1, x2, y2, x3, y3]) + self.operators.append(_CURVETO) + + def closePath(self): + self.operators.append(_CLOSEPATH) + + def getBounds(self): + return getPathBounds(self.points) + +EmptyClipPath=Path() #special path + +def getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, reverse=None): + if yradius is None: yradius = radius + points = [] + from math import sin, cos, pi + degreestoradians = pi/180.0 + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle.001: + degreedelta = min(angle,degreedelta or 1.) + radiansdelta = degreedelta*degreestoradians + n = max(int(angle/radiansdelta+0.5),1) + radiansdelta = angle/n + n += 1 + else: + n = 1 + radiansdelta = 0 + + for angle in xrange(n): + angle = startangle+angle*radiansdelta + a((centerx+radius*cos(angle),centery+yradius*sin(angle))) + + if reverse: points.reverse() + return points + +class ArcPath(Path): + '''Path with an addArc method''' + def addArc(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, degreedelta=None, moveTo=None, reverse=None): + P = getArcPoints(centerx, centery, radius, startangledegrees, endangledegrees, yradius=yradius, degreedelta=degreedelta, reverse=reverse) + if moveTo or not len(self.operators): + self.moveTo(P[0][0],P[0][1]) + del P[0] + for x, y in P: self.lineTo(x,y) + +def definePath(pathSegs=[],isClipPath=0, dx=0, dy=0, **kw): + O = [] + P = [] + for seg in pathSegs: + if type(seg) not in [ListType,TupleType]: + opName = seg + args = [] + else: + opName = seg[0] + args = seg[1:] + if opName not in _PATH_OP_NAMES: + raise ValueError, 'bad operator name %s' % opName + op = _PATH_OP_NAMES.index(opName) + if len(args)!=_PATH_OP_ARG_COUNT[op]: + raise ValueError, '%s bad arguments %s' % (opName,str(args)) + O.append(op) + P.extend(list(args)) + for d,o in (dx,0), (dy,1): + for i in xrange(o,len(P),2): + P[i] = P[i]+d + return apply(Path,(P,O,isClipPath),kw) + +class Rect(SolidShape): + """Rectangle, possibly with rounded corners.""" + + _attrMap = AttrMap(BASE=SolidShape, + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + width = AttrMapValue(isNumber), + height = AttrMapValue(isNumber), + rx = AttrMapValue(isNumber), + ry = AttrMapValue(isNumber), + ) + + def __init__(self, x, y, width, height, rx=0, ry=0, **kw): + SolidShape.__init__(self, kw) + self.x = x + self.y = y + self.width = width + self.height = height + self.rx = rx + self.ry = ry + + def copy(self): + new = self.__class__(self.x, self.y, self.width, self.height) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.x, self.y, self.x + self.width, self.y + self.height) + + +class Image(SolidShape): + """Bitmap image.""" + + _attrMap = AttrMap(BASE=SolidShape, + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + width = AttrMapValue(isNumberOrNone), + height = AttrMapValue(isNumberOrNone), + path = AttrMapValue(None), + ) + + def __init__(self, x, y, width, height, path, **kw): + SolidShape.__init__(self, kw) + self.x = x + self.y = y + self.width = width + self.height = height + self.path = path + + def copy(self): + new = self.__class__(self.x, self.y, self.width, self.height, self.path) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.x, self.y, self.x + width, self.y + width) + +class Circle(SolidShape): + + _attrMap = AttrMap(BASE=SolidShape, + cx = AttrMapValue(isNumber), + cy = AttrMapValue(isNumber), + r = AttrMapValue(isNumber), + ) + + def __init__(self, cx, cy, r, **kw): + SolidShape.__init__(self, kw) + self.cx = cx + self.cy = cy + self.r = r + + def copy(self): + new = self.__class__(self.cx, self.cy, self.r) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.cx - self.r, self.cy - self.r, self.cx + self.r, self.cy + self.r) + +class Ellipse(SolidShape): + _attrMap = AttrMap(BASE=SolidShape, + cx = AttrMapValue(isNumber), + cy = AttrMapValue(isNumber), + rx = AttrMapValue(isNumber), + ry = AttrMapValue(isNumber), + ) + + def __init__(self, cx, cy, rx, ry, **kw): + SolidShape.__init__(self, kw) + self.cx = cx + self.cy = cy + self.rx = rx + self.ry = ry + + def copy(self): + new = self.__class__(self.cx, self.cy, self.rx, self.ry) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return (self.cx - self.rx, self.cy - self.ry, self.cx + self.rx, self.cy + self.ry) + +class Wedge(SolidShape): + """A "slice of a pie" by default translates to a polygon moves anticlockwise + from start angle to end angle""" + + _attrMap = AttrMap(BASE=SolidShape, + centerx = AttrMapValue(isNumber), + centery = AttrMapValue(isNumber), + radius = AttrMapValue(isNumber), + startangledegrees = AttrMapValue(isNumber), + endangledegrees = AttrMapValue(isNumber), + yradius = AttrMapValue(isNumberOrNone), + radius1 = AttrMapValue(isNumberOrNone), + yradius1 = AttrMapValue(isNumberOrNone), + ) + + degreedelta = 1 # jump every 1 degrees + + def __init__(self, centerx, centery, radius, startangledegrees, endangledegrees, yradius=None, **kw): + SolidShape.__init__(self, kw) + while endangledegrees0.001: + degreedelta = min(self.degreedelta or 1.,angle) + radiansdelta = degreedelta*degreestoradians + n = max(1,int(angle/radiansdelta+0.5)) + radiansdelta = angle/n + n += 1 + else: + n = 1 + radiansdelta = 0 + CA = [] + CAA = CA.append + a = points.append + for angle in xrange(n): + angle = startangle+angle*radiansdelta + CAA((cos(angle),sin(angle))) + for c,s in CA: + a(centerx+radius*c) + a(centery+yradius*s) + if (radius1==0 or radius1 is None) and (yradius1==0 or yradius1 is None): + a(centerx); a(centery) + else: + CA.reverse() + for c,s in CA: + a(centerx+radius1*c) + a(centery+yradius1*s) + return Polygon(points) + + def copy(self): + new = self.__class__(self.centerx, + self.centery, + self.radius, + self.startangledegrees, + self.endangledegrees) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return self.asPolygon().getBounds() + +class Polygon(SolidShape): + """Defines a closed shape; Is implicitly + joined back to the start for you.""" + + _attrMap = AttrMap(BASE=SolidShape, + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self, points=[], **kw): + SolidShape.__init__(self, kw) + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + + def copy(self): + new = self.__class__(self.points) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return getPointsBounds(self.points) + +class PolyLine(LineShape): + """Series of line segments. Does not define a + closed shape; never filled even if apparently joined. + Put the numbers in the list, not two-tuples.""" + + _attrMap = AttrMap(BASE=LineShape, + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self, points=[], **kw): + LineShape.__init__(self, kw) + lenPoints = len(points) + if lenPoints: + if type(points[0]) in (ListType,TupleType): + L = [] + for (x,y) in points: + L.append(x) + L.append(y) + points = L + else: + assert len(points) % 2 == 0, 'Point list must have even number of elements!' + self.points = points + + def copy(self): + new = self.__class__(self.points) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + return getPointsBounds(self.points) + +class String(Shape): + """Not checked against the spec, just a way to make something work. + Can be anchored left, middle or end.""" + + # to do. + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + text = AttrMapValue(isString), + fontName = AttrMapValue(None), + fontSize = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + textAnchor = AttrMapValue(isTextAnchor), + encoding = AttrMapValue(isString), + ) + + def __init__(self, x, y, text, **kw): + self.x = x + self.y = y + self.text = text + self.textAnchor = 'start' + self.fontName = STATE_DEFAULTS['fontName'] + self.fontSize = STATE_DEFAULTS['fontSize'] + self.fillColor = STATE_DEFAULTS['fillColor'] + self.setProperties(kw) + self.encoding = 'cp1252' #matches only fonts we have! + + def getEast(self): + return self.x + stringWidth(self.text,self.fontName,self.fontSize, self.encoding) + + def copy(self): + new = self.__class__(self.x, self.y, self.text) + new.setProperties(self.getProperties()) + return new + + def getBounds(self): + # assumes constant drop of 0.2*size to baseline + w = stringWidth(self.text,self.fontName,self.fontSize, self.encoding) + if self.textAnchor == 'start': + x = self.x + elif self.textAnchor == 'middle': + x = self.x - 0.5*w + elif self.textAnchor == 'end': + x = self.x - w + return (x, self.y - 0.2 * self.fontSize, x+w, self.y + self.fontSize) + +class UserNode(_DrawTimeResizeable): + """A simple template for creating a new node. The user (Python + programmer) may subclasses this. provideNode() must be defined to + provide a Shape primitive when called by a renderer. It does + NOT inherit from Shape, as the renderer always replaces it, and + your own classes can safely inherit from it without getting + lots of unintended behaviour.""" + + def provideNode(self): + """Override this to create your own node. This lets widgets be + added to drawings; they must create a shape (typically a group) + so that the renderer can draw the custom node.""" + + raise NotImplementedError, "this method must be redefined by the user/programmer" + + +def test(): + r = Rect(10,10,200,50) + import pprint + pp = pprint.pprint + print 'a Rectangle:' + pp(r.getProperties()) + print + print 'verifying...', + r.verify() + print 'OK' + #print 'setting rect.z = "spam"' + #r.z = 'spam' + print 'deleting rect.width' + del r.width + print 'verifying...', + r.verify() + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/testdrawings.py b/bin/reportlab/graphics/testdrawings.py new file mode 100644 index 00000000000..d5c52fc57eb --- /dev/null +++ b/bin/reportlab/graphics/testdrawings.py @@ -0,0 +1,294 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/testdrawings.py +__version__=''' $Id $ ''' +"""This contains a number of routines to generate test drawings +for reportlab/graphics. For now they are contrived, but we will expand them +to try and trip up any parser. Feel free to add more. + +""" + +from reportlab.graphics.shapes import * +from reportlab.lib import colors + +def getDrawing1(): + """Hello World, on a rectangular background""" + + D = Drawing(400, 200) + D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) #round corners + D.add(String(180,100, 'Hello World', fillColor=colors.red)) + + + return D + + +def getDrawing2(): + """This demonstrates the basic shapes. There are + no groups or references. Each solid shape should have + a purple fill.""" + D = Drawing(400, 200) #, fillColor=colors.purple) + + D.add(Line(10,10,390,190)) + D.add(Circle(100,100,20, fillColor=colors.purple)) + D.add(Circle(200,100,20, fillColor=colors.purple)) + D.add(Circle(300,100,20, fillColor=colors.purple)) + + D.add(Wedge(330,100,40, -10,40, fillColor=colors.purple)) + + D.add(PolyLine([120,10,130,20,140,10,150,20,160,10, + 170,20,180,10,190,20,200,10])) + + D.add(Polygon([300,20,350,20,390,80,300,75, 330, 40])) + + D.add(Ellipse(50, 150, 40, 20)) + + D.add(Rect(120, 150, 60, 30, + strokeWidth=10, + strokeColor=colors.red, + fillColor=colors.yellow)) #square corners + + D.add(Rect(220, 150, 60, 30, 10, 10)) #round corners + + D.add(String(10,50, 'Basic Shapes', fillColor=colors.black)) + + return D + + +##def getDrawing2(): +## """This drawing uses groups. Each group has two circles and a comment. +## The line style is set at group level and should be red for the left, +## bvlue for the right.""" +## D = Drawing(400, 200) +## +## Group1 = Group() +## +## Group1.add(String(50, 50, 'Group 1', fillColor=colors.black)) +## Group1.add(Circle(75,100,25)) +## Group1.add(Circle(125,100,25)) +## D.add(Group1) +## +## Group2 = Group( +## String(250, 50, 'Group 2', fillColor=colors.black), +## Circle(275,100,25), +## Circle(325,100,25)#, + + +##def getDrawing2(): +## """This drawing uses groups. Each group has two circles and a comment. +## The line style is set at group level and should be red for the left, +## bvlue for the right.""" +## D = Drawing(400, 200) +## +## Group1 = Group() +## +## Group1.add(String(50, 50, 'Group 1', fillColor=colors.black)) +## Group1.add(Circle(75,100,25)) +## Group1.add(Circle(125,100,25)) +## D.add(Group1) +## +## Group2 = Group( +## String(250, 50, 'Group 2', fillColor=colors.black), +## Circle(275,100,25), +## Circle(325,100,25)#, +## +## #group attributes +## #strokeColor=colors.blue +## ) +## D.add(Group2) + +## return D +## +## +##def getDrawing3(): +## """This uses a named reference object. The house is a 'subroutine' +## the basic brick colored walls are defined, but the roof and window +## color are undefined and may be set by the container.""" +## +## D = Drawing(400, 200, fill=colors.bisque) +## +## +## House = Group( +## Rect(2,20,36,30, fill=colors.bisque), #walls +## Polygon([0,20,40,20,20,5]), #roof +## Rect(8, 38, 8, 12), #door +## Rect(25, 38, 8, 7), #window +## Rect(8, 25, 8, 7), #window +## Rect(25, 25, 8, 7) #window +## +## ) +## D.addDef('MyHouse', House) +## +## # one row all the same color +## D.add(String(20, 40, 'British Street...',fill=colors.black)) +## for i in range(6): +## x = i * 50 +## D.add(NamedReference('MyHouse', +## House, +## transform=translate(x, 40), +## fill = colors.brown +## ) +## ) +## +## # now do a row all different +## D.add(String(20, 120, 'Mediterranean Street...',fill=colors.black)) +## x = 0 +## for color in (colors.blue, colors.yellow, colors.orange, +## colors.red, colors.green, colors.chartreuse): +## D.add(NamedReference('MyHouse', +## House, +## transform=translate(x,120), +## fill = color, +## ) +## ) +## x = x + 50 +## #..by popular demand, the mayor gets a big one at the end +## D.add(NamedReference('MyHouse', +## House, +## transform=mmult(translate(x,110), scale(1.2,1.2)), +## fill = color, +## ) +## ) +## +## +## return D +## +##def getDrawing4(): +## """This tests that attributes are 'unset' correctly when +## one steps back out of a drawing node. All the circles are part of a +## group setting the line color to blue; the second circle explicitly +## sets it to red. Ideally, the third circle should go back to blue.""" +## D = Drawing(400, 200) +## +## +## G = Group( +## Circle(100,100,20), +## Circle(200,100,20, stroke=colors.blue), +## Circle(300,100,20), +## stroke=colors.red, +## stroke_width=3, +## fill=colors.aqua +## ) +## D.add(G) +## +## +## D.add(String(10,50, 'Stack Unwinding - should be red, blue, red')) +## +## return D +## +## +##def getDrawing5(): +## """This Rotates Coordinate Axes""" +## D = Drawing(400, 200) +## +## +## +## Axis = Group( +## Line(0,0,100,0), #x axis +## Line(0,0,0,50), # y axis +## Line(0,10,10,10), #ticks on y axis +## Line(0,20,10,20), +## Line(0,30,10,30), +## Line(0,40,10,40), +## Line(10,0,10,10), #ticks on x axis +## Line(20,0,20,10), +## Line(30,0,30,10), +## Line(40,0,40,10), +## Line(50,0,50,10), +## Line(60,0,60,10), +## Line(70,0,70,10), +## Line(80,0,80,10), +## Line(90,0,90,10), +## String(20, 35, 'Axes', fill=colors.black) +## ) +## +## D.addDef('Axes', Axis) +## +## D.add(NamedReference('Axis', Axis, +## transform=translate(10,10))) +## D.add(NamedReference('Axis', Axis, +## transform=mmult(translate(150,10),rotate(15))) +## ) +## return D +## +##def getDrawing6(): +## """This Rotates Text""" +## D = Drawing(400, 300, fill=colors.black) +## +## xform = translate(200,150) +## C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink, +## colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen) +## +## for i in range(12): +## D.add(String(0, 0, ' - - Rotated Text', fill=C[i%len(C)], transform=mmult(xform, rotate(30*i)))) +## +## return D +## +##def getDrawing7(): +## """This defines and tests a simple UserNode0 (the trailing zero denotes +## an experimental method which is not part of the supported API yet). +## Each of the four charts is a subclass of UserNode which generates a random +## series when rendered.""" +## +## class MyUserNode(UserNode0): +## import whrandom, math +## +## +## def provideNode(self, sender): +## """draw a simple chart that changes everytime it's drawn""" +## # print "here's a random number %s" % self.whrandom.random() +## #print "MyUserNode.provideNode being called by %s" % sender +## g = Group() +## #g._state = self._state # this is naughty +## PingoNode.__init__(g, self._state) # is this less naughty ? +## w = 80.0 +## h = 50.0 +## g.add(Rect(0,0, w, h, stroke=colors.black)) +## N = 10.0 +## x,y = (0,h) +## dx = w/N +## for ii in range(N): +## dy = (h/N) * self.whrandom.random() +## g.add(Line(x,y,x+dx, y-dy)) +## x = x + dx +## y = y - dy +## return g +## +## D = Drawing(400,200, fill=colors.white) # AR - same size as others +## +## D.add(MyUserNode()) +## +## graphcolor= [colors.green, colors.red, colors.brown, colors.purple] +## for ii in range(4): +## D.add(Group( MyUserNode(stroke=graphcolor[ii], stroke_width=2), +## transform=translate(ii*90,0) )) +## +## #un = MyUserNode() +## #print un.provideNode() +## return D +## +##def getDrawing8(): +## """Test Path operations--lineto, curveTo, etc.""" +## D = Drawing(400, 200, fill=None, stroke=colors.purple, stroke_width=2) +## +## xform = translate(200,100) +## C = (colors.black,colors.red,colors.green,colors.blue,colors.brown,colors.gray, colors.pink, +## colors.lavender,colors.lime, colors.mediumblue, colors.magenta, colors.limegreen) +## p = Path(50,50) +## p.lineTo(100,100) +## p.moveBy(-25,25) +## p.curveTo(150,125, 125,125, 200,50) +## p.curveTo(175, 75, 175, 98, 62, 87) +## +## +## D.add(p) +## D.add(String(10,30, 'Tests of path elements-lines and bezier curves-and text formating')) +## D.add(Line(220,150, 220,200, stroke=colors.red)) +## D.add(String(220,180, "Text should be centered", text_anchor="middle") ) +## +## +## return D + + +if __name__=='__main__': + print __doc__ \ No newline at end of file diff --git a/bin/reportlab/graphics/testshapes.py b/bin/reportlab/graphics/testshapes.py new file mode 100644 index 00000000000..261d7d73d34 --- /dev/null +++ b/bin/reportlab/graphics/testshapes.py @@ -0,0 +1,574 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/testshapes.py + +# testshapes.py - draws shapes onto a PDF canvas. + +""" +Execute the script to see some test drawings. + +This contains a number of routines to generate test drawings +for reportlab/graphics. For now many of them are contrived, +but we will expand them to try and trip up any parser. +Feel free to add more. +""" + +__version__ = ''' $Id $ ''' + + +import os, sys + +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import Flowable +from reportlab.graphics.shapes import * +from reportlab.graphics.renderPDF import _PDFRenderer +import unittest + +_FONTS = ['Times-Roman','Courier','Times-BoldItalic',] + +######################################################### +# +# Collections of shape drawings. +# +######################################################### + + +def getFailedDrawing(funcName): + """Generate a drawing in case something goes really wrong. + + This will create a drawing to be displayed whenever some + other drawing could not be executed, because the generating + function does something terribly wrong! The box contains + an attention triangle, plus some error message. + """ + + D = Drawing(400, 200) + + points = [200,170, 140,80, 260,80] + D.add(Polygon(points, + strokeWidth=0.5*cm, + strokeColor=colors.red, + fillColor=colors.yellow)) + + s = String(200, 40, + "Error in generating function '%s'!" % funcName, + textAnchor='middle') + D.add(s) + + return D + + +# These are the real drawings to be eye-balled. + +def getDrawing01(): + """Hello World, on a rectangular background. + + The rectangle's fillColor is yellow. + The string's fillColor is red. + """ + + D = Drawing(400, 200) + D.add(Rect(50, 50, 300, 100, fillColor=colors.yellow)) + D.add(String(180,100, 'Hello World', fillColor=colors.red)) + D.add(String(180,86, 'Some special characters \xc2\xa2\xc2\xa9\xc2\xae\xc2\xa3\xca\xa5\xd0\x96\xd6\x83\xd7\x90\xd9\x82\xe0\xa6\x95\xce\xb1\xce\xb2\xce\xb3', fillColor=colors.red)) + + return D + + +def getDrawing02(): + """Various Line shapes. + + The lines are blue and their strokeWidth is 5 mm. + One line has a strokeDashArray set to [5, 10, 15]. + """ + + D = Drawing(400, 200) + D.add(Line(50,50, 300,100, + strokeColor=colors.blue, + strokeWidth=0.5*cm, + )) + D.add(Line(50,100, 300,50, + strokeColor=colors.blue, + strokeWidth=0.5*cm, + strokeDashArray=[5, 10, 15], + )) + + #x = 1/0 # Comment this to see the actual drawing! + + return D + + +def getDrawing03(): + """Text strings in various sizes and different fonts. + + Font size increases from 12 to 36 and from bottom left + to upper right corner. The first ones should be in + Times-Roman. Finally, a solitary Courier string at + the top right corner. + """ + + D = Drawing(400, 200) + for size in range(12, 36, 4): + D.add(String(10+size*2, + 10+size*2, + 'Hello World', + fontName=_FONTS[0], + fontSize=size)) + + D.add(String(150, 150, + 'Hello World', + fontName=_FONTS[1], + fontSize=36)) + return D + + +def getDrawing04(): + """Text strings in various colours. + + Colours are blue, yellow and red from bottom left + to upper right. + """ + + D = Drawing(400, 200) + i = 0 + for color in (colors.blue, colors.yellow, colors.red): + D.add(String(50+i*30, 50+i*30, + 'Hello World', fillColor=color)) + i = i + 1 + + return D + + +def getDrawing05(): + """Text strings with various anchors (alignments). + + Text alignment conforms to the anchors in the left column. + """ + + D = Drawing(400, 200) + + lineX = 250 + D.add(Line(lineX,10, lineX,190, strokeColor=colors.gray)) + + y = 130 + for anchor in ('start', 'middle', 'end'): + D.add(String(lineX, y, 'Hello World', textAnchor=anchor)) + D.add(String(50, y, anchor + ':')) + y = y - 30 + + return D + + +def getDrawing06(): + """This demonstrates all the basic shapes at once. + + There are no groups or references. + Each solid shape should have a purple fill. + """ + + purple = colors.purple + purple = colors.green + + D = Drawing(400, 200) #, fillColor=purple) + + D.add(Line(10,10, 390,190)) + + D.add(Circle(100,100,20, fillColor=purple)) + D.add(Circle(200,100,40, fillColor=purple)) + D.add(Circle(300,100,30, fillColor=purple)) + + D.add(Wedge(330,100,40, -10,40, fillColor=purple)) + + D.add(PolyLine([120,10, 130,20, 140,10, 150,20, 160,10, + 170,20, 180,10, 190,20, 200,10], fillColor=purple)) + + D.add(Polygon([300,20, 350,20, 390,80, 300,75, 330,40], fillColor=purple)) + + D.add(Ellipse(50,150, 40, 20, fillColor=purple)) + + D.add(Rect(120,150, 60,30, + strokeWidth=10, + strokeColor=colors.yellow, + fillColor=purple)) #square corners + + D.add(Rect(220, 150, 60, 30, 10, 10, fillColor=purple)) #round corners + + from reportlab.lib.validators import inherit + D.add(String(10,50, 'Basic Shapes', fillColor=colors.black, fontName=inherit)) + + return D + +def getDrawing07(): + """This tests the ability to translate and rotate groups. The first set of axes should be + near the bottom left of the drawing. The second should be rotated counterclockwise + by 15 degrees. The third should be rotated by 30 degrees.""" + D = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), #x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), #ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), #ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + D.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.rotate(15) + + D.add(secondAxisGroup) + + + thirdAxisGroup = Group(Axis, transform=mmult(translate(300,10), rotate(30))) + D.add(thirdAxisGroup) + + return D + + +def getDrawing08(): + """This tests the ability to scale coordinates. The bottom left set of axes should be + near the bottom left of the drawing. The bottom right should be stretched vertically + by a factor of 2. The top left one should be stretched horizontally by a factor of 2. + The top right should have the vertical axiss leaning over to the right by 30 degrees.""" + D = Drawing(400, 200) + + Axis = Group( + Line(0,0,100,0), #x axis + Line(0,0,0,50), # y axis + Line(0,10,10,10), #ticks on y axis + Line(0,20,10,20), + Line(0,30,10,30), + Line(0,40,10,40), + Line(10,0,10,10), #ticks on x axis + Line(20,0,20,10), + Line(30,0,30,10), + Line(40,0,40,10), + Line(50,0,50,10), + Line(60,0,60,10), + Line(70,0,70,10), + Line(80,0,80,10), + Line(90,0,90,10), + String(20, 35, 'Axes', fill=colors.black) + ) + + firstAxisGroup = Group(Axis) + firstAxisGroup.translate(10,10) + D.add(firstAxisGroup) + + secondAxisGroup = Group(Axis) + secondAxisGroup.translate(150,10) + secondAxisGroup.scale(1,2) + D.add(secondAxisGroup) + + thirdAxisGroup = Group(Axis) + thirdAxisGroup.translate(10,125) + thirdAxisGroup.scale(2,1) + D.add(thirdAxisGroup) + + fourthAxisGroup = Group(Axis) + fourthAxisGroup.translate(250,125) + fourthAxisGroup.skew(30,0) + D.add(fourthAxisGroup) + + + return D + +def getDrawing09(): + """This tests rotated strings + + Some renderers will have a separate mechanism for font drawing. This test + just makes sure strings get transformed the same way as regular graphics.""" + D = Drawing(400, 200) + + fontName = _FONTS[0] + fontSize = 12 + text = "I should be totally horizontal and enclosed in a box" + textWidth = stringWidth(text, fontName, fontSize) + + + g1 = Group( + String(20, 20, text, fontName=fontName, fontSize = fontSize), + Rect(18, 18, textWidth + 4, fontSize + 4, fillColor=None) + ) + D.add(g1) + + text = "I should slope up by 15 degrees, so my right end is higher than my left" + textWidth = stringWidth(text, fontName, fontSize) + g2 = Group( + String(20, 20, text, fontName=fontName, fontSize = fontSize), + Rect(18, 18, textWidth + 4, fontSize + 4, fillColor=None) + ) + g2.translate(0, 50) + g2.rotate(15) + D.add(g2) + + return D + +def getDrawing10(): + """This tests nested groups with multiple levels of coordinate transformation. + Each box should be staggered up and to the right, moving by 25 points each time.""" + D = Drawing(400, 200) + + fontName = _FONTS[0] + fontSize = 12 + + g1 = Group( + Rect(0, 0, 100, 20, fillColor=colors.yellow), + String(5, 5, 'Text in the box', fontName=fontName, fontSize = fontSize) + ) + D.add(g1) + + g2 = Group(g1, transform = translate(25,25)) + D.add(g2) + + g3 = Group(g2, transform = translate(25,25)) + D.add(g3) + + g4 = Group(g3, transform = translate(25,25)) + D.add(g4) + + + return D + +from widgets.signsandsymbols import SmileyFace +def getDrawing11(): + '''test of anchoring''' + def makeSmiley(x, y, size, color): + "Make a smiley data item representation." + d = size + s = SmileyFace() + s.fillColor = color + s.x = x-d + s.y = y-d + s.size = d*2 + return s + + D = Drawing(400, 200) #, fillColor=colors.purple) + g = Group(transform=(1,0,0,1,0,0)) + g.add(makeSmiley(100,100,10,colors.red)) + g.add(Line(90,100,110,100,strokeColor=colors.green)) + g.add(Line(100,90,100,110,strokeColor=colors.green)) + D.add(g) + g = Group(transform=(2,0,0,2,100,-100)) + g.add(makeSmiley(100,100,10,colors.blue)) + g.add(Line(90,100,110,100,strokeColor=colors.green)) + g.add(Line(100,90,100,110,strokeColor=colors.green)) + D.add(g) + g = Group(transform=(2,0,0,2,0,0)) + return D + + +def getDrawing12(): + """Text strings in a non-standard font. + All that is required is to place the .afm and .pfb files + on the font patch given in rl_config.py, + for example in reportlab/lib/fonts/. + """ + faceName = "LettErrorRobot-Chrome" + D = Drawing(400, 200) + for size in range(12, 36, 4): + D.add(String(10+size*2, + 10+size*2, + 'Hello World', + fontName=faceName, + fontSize=size)) + return D + +def getDrawing13(): + 'Test Various TTF Fonts' + from reportlab.pdfbase import pdfmetrics, ttfonts + pdfmetrics.registerFont(ttfonts.TTFont("LuxiSerif", "luxiserif.ttf")) + pdfmetrics.registerFont(ttfonts.TTFont("Rina", "rina.ttf")) + _FONTS[1] = 'LuxiSerif' + _FONTS[2] = 'Rina' + F = ['Times-Roman','Courier','Helvetica','LuxiSerif', 'Rina'] + if sys.platform=='win32': + for name, ttf in [('Adventurer Light SF','Advlit.ttf'),('ArialMS','ARIAL.TTF'), + ('Arial Unicode MS', 'ARIALUNI.TTF'), + ('Book Antiqua','BKANT.TTF'), + ('Century Gothic','GOTHIC.TTF'), + ('Comic Sans MS', 'COMIC.TTF'), + ('Elementary Heavy SF Bold','Vwagh.ttf'), + ('Firenze SF','flot.ttf'), + ('Garamond','GARA.TTF'), + ('Jagger','Rols.ttf'), + ('Monotype Corsiva','MTCORSVA.TTF'), + ('Seabird SF','seag.ttf'), + ('Tahoma','TAHOMA.TTF'), + ('VerdanaMS','VERDANA.TTF'), + ]: + for D in ('c:\WINNT','c:\Windows'): + fn = os.path.join(D,'Fonts',ttf) + if os.path.isfile(fn): + try: + f = ttfonts.TTFont(name, fn) + pdfmetrics.registerFont(f) + F.append(name) + except: + pass + + def drawit(F,w=400,h=200,fontSize=12,slack=2,gap=5): + D = Drawing(w,h) + th = 2*gap + fontSize*1.2 + gh = gap + .2*fontSize + y = h + maxx = 0 + for fontName in F: + y -= th + text = fontName+": I should be totally horizontal and enclosed in a box and end in alphabetagamma \xc2\xa2\xc2\xa9\xc2\xae\xc2\xa3\xca\xa5\xd0\x96\xd6\x83\xd7\x90\xd9\x82\xe0\xa6\x95\xce\xb1\xce\xb2\xce\xb3" + textWidth = stringWidth(text, fontName, fontSize) + maxx = max(maxx,textWidth+20) + D.add( + Group(Rect(8, y-gh, textWidth + 4, th, strokeColor=colors.red, strokeWidth=.5, fillColor=colors.lightgrey), + String(10, y, text, fontName=fontName, fontSize = fontSize))) + y -= 5 + return maxx, h-y+gap, D + maxx, maxy, D = drawit(F) + if maxx>400 or maxy>200: _,_,D = drawit(F,maxx,maxy) + return D + +##def getDrawing14(): +## """This tests inherited properties. Each font should be as it says.""" +## D = Drawing(400, 200) +## +## fontSize = 12 +## D.fontName = 'Courier' +## +## g1 = Group( +## Rect(0, 0, 150, 20, fillColor=colors.yellow), +## String(5, 5, 'Inherited Courier', fontName=inherit, fontSize = fontSize) +## ) +## D.add(g1) +## +## g2 = Group(g1, transform = translate(25,25)) +## D.add(g2) +## +## g3 = Group(g2, transform = translate(25,25)) +## D.add(g3) +## +## g4 = Group(g3, transform = translate(25,25)) +## D.add(g4) +## +## +## return D +def getAllFunctionDrawingNames(doTTF=1): + "Get a list of drawing function names from somewhere." + + funcNames = [] + + # Here we get the names from the global name space. + symbols = globals().keys() + symbols.sort() + for funcName in symbols: + if funcName[0:10] == 'getDrawing': + if doTTF or funcName!='getDrawing13': + funcNames.append(funcName) + + return funcNames + +def _evalFuncDrawing(name, D, l=None, g=None): + try: + d = eval(name + '()', g or globals(), l or locals()) + except: + d = getFailedDrawing(name) + D.append((d, eval(name + '.__doc__'), name[3:])) + +def getAllTestDrawings(doTTF=1): + D = [] + for f in getAllFunctionDrawingNames(doTTF=doTTF): + _evalFuncDrawing(f,D) + return D + +def writePDF(drawings): + "Create and save a PDF file containing some drawings." + + pdfPath = os.path.splitext(sys.argv[0])[0] + '.pdf' + c = Canvas(pdfPath) + c.setFont(_FONTS[0], 32) + c.drawString(80, 750, 'ReportLab Graphics-Shapes Test') + + # Print drawings in a loop, with their doc strings. + c.setFont(_FONTS[0], 12) + y = 740 + i = 1 + for (drawing, docstring, funcname) in drawings: + if y < 300: # Allows 5-6 lines of text. + c.showPage() + y = 740 + # Draw a title. + y = y - 30 + c.setFont(_FONTS[2],12) + c.drawString(80, y, '%s (#%d)' % (funcname, i)) + c.setFont(_FONTS[0],12) + y = y - 14 + textObj = c.beginText(80, y) + textObj.textLines(docstring) + c.drawText(textObj) + y = textObj.getY() + y = y - drawing.height + drawing.drawOn(c, 80, y) + i = i + 1 + + c.save() + print 'wrote %s ' % pdfPath + + +class ShapesTestCase(unittest.TestCase): + "Test generating all kinds of shapes." + + def setUp(self): + "Prepare some things before the tests start." + + self.funcNames = getAllFunctionDrawingNames() + self.drawings = [] + + + def tearDown(self): + "Do what has to be done after the tests are over." + + writePDF(self.drawings) + + + # This should always succeed. If each drawing would be + # wrapped in a dedicated test method like this one, it + # would be possible to have a count for wrong tests + # as well... Something like this is left for later... + def testAllDrawings(self): + "Make a list of drawings." + + for f in self.funcNames: + if f[0:10] == 'getDrawing': + # Make an instance and get its doc string. + # If that fails, use a default error drawing. + _evalFuncDrawing(f,self.drawings) + + +def makeSuite(): + "Make a test suite for unit testing." + + suite = unittest.TestSuite() + suite.addTest(ShapesTestCase('testAllDrawings')) + return suite + + +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) diff --git a/bin/reportlab/graphics/widgetbase.py b/bin/reportlab/graphics/widgetbase.py new file mode 100644 index 00000000000..c529c0ded88 --- /dev/null +++ b/bin/reportlab/graphics/widgetbase.py @@ -0,0 +1,502 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgetbase.py +__version__=''' $Id: widgetbase.py 2668 2005-09-05 10:23:51Z rgbecker $ ''' +import string + +from reportlab.graphics import shapes +from reportlab import rl_config +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * + + +class PropHolder: + '''Base for property holders''' + + _attrMap = None + + def verify(self): + """If the _attrMap attribute is not None, this + checks all expected attributes are present; no + unwanted attributes are present; and (if a + checking function is found) checks each + attribute has a valid value. Either succeeds + or raises an informative exception. + """ + + if self._attrMap is not None: + for key in self.__dict__.keys(): + if key[0] <> '_': + msg = "Unexpected attribute %s found in %s" % (key, self) + assert self._attrMap.has_key(key), msg + for (attr, metavalue) in self._attrMap.items(): + msg = "Missing attribute %s from %s" % (attr, self) + assert hasattr(self, attr), msg + value = getattr(self, attr) + args = (value, attr, self.__class__.__name__) + assert metavalue.validate(value), "Invalid value %s for attribute %s in class %s" % args + + if rl_config.shapeChecking: + """This adds the ability to check every attribute assignment + as it is made. It slows down shapes but is a big help when + developing. It does not get defined if rl_config.shapeChecking = 0. + """ + + def __setattr__(self, name, value): + """By default we verify. This could be off + in some parallel base classes.""" + validateSetattr(self,name,value) + + + def getProperties(self,recur=1): + """Returns a list of all properties which can be edited and + which are not marked as private. This may include 'child + widgets' or 'primitive shapes'. You are free to override + this and provide alternative implementations; the default + one simply returns everything without a leading underscore. + """ + + from reportlab.lib.validators import isValidChild + + # TODO when we need it, but not before - + # expose sequence contents? + + props = {} + for name in self.__dict__.keys(): + if name[0:1] <> '_': + component = getattr(self, name) + + if recur and isValidChild(component): + # child object, get its properties too + childProps = component.getProperties(recur=recur) + for (childKey, childValue) in childProps.items(): + #key might be something indexed like '[2].fillColor' + #or simple like 'fillColor'; in the former case we + #don't need a '.' between me and my child. + if childKey[0] == '[': + props['%s%s' % (name, childKey)] = childValue + else: + props['%s.%s' % (name, childKey)] = childValue + else: + props[name] = component + + return props + + + def setProperties(self, propDict): + """Permits bulk setting of properties. These may include + child objects e.g. "chart.legend.width = 200". + + All assignments will be validated by the object as if they + were set individually in python code. + + All properties of a top-level object are guaranteed to be + set before any of the children, which may be helpful to + widget designers. + """ + + childPropDicts = {} + for (name, value) in propDict.items(): + parts = string.split(name, '.', 1) + if len(parts) == 1: + #simple attribute, set it now + setattr(self, name, value) + else: + (childName, remains) = parts + try: + childPropDicts[childName][remains] = value + except KeyError: + childPropDicts[childName] = {remains: value} + + # now assign to children + for (childName, childPropDict) in childPropDicts.items(): + child = getattr(self, childName) + child.setProperties(childPropDict) + + + def dumpProperties(self, prefix=""): + """Convenience. Lists them on standard output. You + may provide a prefix - mostly helps to generate code + samples for documentation. + """ + + propList = self.getProperties().items() + propList.sort() + if prefix: + prefix = prefix + '.' + for (name, value) in propList: + print '%s%s = %s' % (prefix, name, value) + + +class Widget(PropHolder, shapes.UserNode): + """Base for all user-defined widgets. Keep as simple as possible. Does + not inherit from Shape so that we can rewrite shapes without breaking + widgets and vice versa.""" + + def _setKeywords(self,**kw): + for k,v in kw.items(): + if not self.__dict__.has_key(k): + setattr(self,k,v) + + def draw(self): + msg = "draw() must be implemented for each Widget!" + raise shapes.NotImplementedError, msg + + def demo(self): + msg = "demo() must be implemented for each Widget!" + raise shapes.NotImplementedError, msg + + def provideNode(self): + return self.draw() + + def getBounds(self): + "Return outer boundary as x1,y1,x2,y2. Can be overridden for efficiency" + return self.draw().getBounds() + +_ItemWrapper={} + +class TypedPropertyCollection(PropHolder): + """A container with properties for objects of the same kind. + + This makes it easy to create lists of objects. You initialize + it with a class of what it is to contain, and that is all you + can add to it. You can assign properties to the collection + as a whole, or to a numeric index within it; if so it creates + a new child object to hold that data. + + So: + wedges = TypedPropertyCollection(WedgeProperties) + wedges.strokeWidth = 2 # applies to all + wedges.strokeColor = colors.red # applies to all + wedges[3].strokeColor = colors.blue # only to one + + The last line should be taken as a prescription of how to + create wedge no. 3 if one is needed; no error is raised if + there are only two data points. + + We try and make sensible use of tuple indeces. + line[(3,x)] is backed by line[(3,)], line[3] & line + """ + + def __init__(self, exampleClass): + #give it same validation rules as what it holds + self.__dict__['_value'] = exampleClass() + self.__dict__['_children'] = {} + + def wKlassFactory(self,Klass): + class WKlass(Klass): + def __getattr__(self,name): + try: + return self.__class__.__bases__[0].__getattr__(self,name) + except: + i = self._index + if i: + c = self._parent._children + if c.has_key(i) and c[i].__dict__.has_key(name): + return getattr(c[i],name) + elif len(i)==1: + i = i[0] + if c.has_key(i) and c[i].__dict__.has_key(name): + return getattr(c[i],name) + return getattr(self._parent,name) + return WKlass + + def __getitem__(self, index): + try: + return self._children[index] + except KeyError: + Klass = self._value.__class__ + if _ItemWrapper.has_key(Klass): + WKlass = _ItemWrapper[Klass] + else: + _ItemWrapper[Klass] = WKlass = self.wKlassFactory(Klass) + + child = WKlass() + child._parent = self + if type(index) in (type(()),type([])): + index = tuple(index) + if len(index)>1: + child._index = tuple(index[:-1]) + else: + child._index = None + else: + child._index = None + for i in filter(lambda x,K=child.__dict__.keys(): x in K,child._attrMap.keys()): + del child.__dict__[i] + + self._children[index] = child + return child + + def has_key(self,key): + if type(key) in (type(()),type([])): key = tuple(key) + return self._children.has_key(key) + + def __setitem__(self, key, value): + msg = "This collection can only hold objects of type %s" % self._value.__class__.__name__ + assert isinstance(value, self._value.__class__), msg + + def __len__(self): + return len(self._children.keys()) + + def getProperties(self,recur=1): + # return any children which are defined and whatever + # differs from the parent + props = {} + + for (key, value) in self._value.getProperties(recur=recur).items(): + props['%s' % key] = value + + for idx in self._children.keys(): + childProps = self._children[idx].getProperties(recur=recur) + for (key, value) in childProps.items(): + if not hasattr(self,key) or getattr(self, key)<>value: + newKey = '[%s].%s' % (idx, key) + props[newKey] = value + return props + + def setVector(self,**kw): + for name, value in kw.items(): + for i in xrange(len(value)): + setattr(self[i],name,value[i]) + + def __getattr__(self,name): + return getattr(self._value,name) + + def __setattr__(self,name,value): + return setattr(self._value,name,value) + +## No longer needed! +class StyleProperties(PropHolder): + """A container class for attributes used in charts and legends. + + Attributes contained can be those for any graphical element + (shape?) in the ReportLab graphics package. The idea for this + container class is to be useful in combination with legends + and/or the individual appearance of data series in charts. + + A legend could be as simple as a wrapper around a list of style + properties, where the 'desc' attribute contains a descriptive + string and the rest could be used by the legend e.g. to draw + something like a color swatch. The graphical presentation of + the legend would be its own business, though. + + A chart could be inspecting a legend or, more directly, a list + of style properties to pick individual attributes that it knows + about in order to render a particular row of the data. A bar + chart e.g. could simply use 'strokeColor' and 'fillColor' for + drawing the bars while a line chart could also use additional + ones like strokeWidth. + """ + + _attrMap = AttrMap( + strokeWidth = AttrMapValue(isNumber), + strokeLineCap = AttrMapValue(isNumber), + strokeLineJoin = AttrMapValue(isNumber), + strokeMiterLimit = AttrMapValue(None), + strokeDashArray = AttrMapValue(isListOfNumbersOrNone), + strokeOpacity = AttrMapValue(isNumber), + strokeColor = AttrMapValue(isColorOrNone), + fillColor = AttrMapValue(isColorOrNone), + desc = AttrMapValue(isString), + ) + + def __init__(self, **kwargs): + "Initialize with attributes if any." + + for k, v in kwargs.items(): + setattr(self, k, v) + + + def __setattr__(self, name, value): + "Verify attribute name and value, before setting it." + validateSetattr(self,name,value) + + +class TwoCircles(Widget): + def __init__(self): + self.leftCircle = shapes.Circle(100,100,20, fillColor=colors.red) + self.rightCircle = shapes.Circle(300,100,20, fillColor=colors.red) + + def draw(self): + return shapes.Group(self.leftCircle, self.rightCircle) + + +class Face(Widget): + """This draws a face with two eyes. + + It exposes a couple of properties + to configure itself and hides all other details. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + size = AttrMapValue(isNumber), + skinColor = AttrMapValue(isColorOrNone), + eyeColor = AttrMapValue(isColorOrNone), + mood = AttrMapValue(OneOf('happy','sad','ok')), + ) + + def __init__(self): + self.x = 10 + self.y = 10 + self.size = 80 + self.skinColor = None + self.eyeColor = colors.blue + self.mood = 'happy' + + def demo(self): + pass + + def draw(self): + s = self.size # abbreviate as we will use this a lot + g = shapes.Group() + g.transform = [1,0,0,1,self.x, self.y] + + # background + g.add(shapes.Circle(s * 0.5, s * 0.5, s * 0.5, fillColor=self.skinColor)) + + # left eye + g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.1, fillColor=colors.white)) + g.add(shapes.Circle(s * 0.35, s * 0.65, s * 0.05, fillColor=self.eyeColor)) + + # right eye + g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.1, fillColor=colors.white)) + g.add(shapes.Circle(s * 0.65, s * 0.65, s * 0.05, fillColor=self.eyeColor)) + + # nose + g.add(shapes.Polygon( + points=[s * 0.5, s * 0.6, s * 0.4, s * 0.3, s * 0.6, s * 0.3], + fillColor=None)) + + # mouth + if self.mood == 'happy': + offset = -0.05 + elif self.mood == 'sad': + offset = +0.05 + else: + offset = 0 + + g.add(shapes.Polygon( + points = [ + s * 0.3, s * 0.2, #left of mouth + s * 0.7, s * 0.2, #right of mouth + s * 0.6, s * (0.2 + offset), # the bit going up or down + s * 0.4, s * (0.2 + offset) # the bit going up or down + ], + fillColor = colors.pink, + strokeColor = colors.red, + strokeWidth = s * 0.03 + )) + + return g + + +class TwoFaces(Widget): + def __init__(self): + self.faceOne = Face() + self.faceOne.mood = "happy" + self.faceTwo = Face() + self.faceTwo.x = 100 + self.faceTwo.mood = "sad" + + def draw(self): + """Just return a group""" + return shapes.Group(self.faceOne, self.faceTwo) + + def demo(self): + """The default case already looks good enough, + no implementation needed here""" + pass + +class Sizer(Widget): + "Container to show size of all enclosed objects" + + _attrMap = AttrMap(BASE=shapes.SolidShape, + contents = AttrMapValue(isListOfShapes,desc="Contained drawable elements"), + ) + def __init__(self, *elements): + self.contents = [] + self.fillColor = colors.cyan + self.strokeColor = colors.magenta + + for elem in elements: + self.add(elem) + + def _addNamedNode(self,name,node): + 'if name is not None add an attribute pointing to node and add to the attrMap' + if name: + if name not in self._attrMap.keys(): + self._attrMap[name] = AttrMapValue(isValidChild) + setattr(self, name, node) + + def add(self, node, name=None): + """Appends non-None child node to the 'contents' attribute. In addition, + if a name is provided, it is subsequently accessible by name + """ + # propagates properties down + if node is not None: + assert isValidChild(node), "Can only add Shape or UserNode objects to a Group" + self.contents.append(node) + self._addNamedNode(name,node) + + def getBounds(self): + # get bounds of each object + if self.contents: + b = [] + for elem in self.contents: + b.append(elem.getBounds()) + return shapes.getRectsBounds(b) + else: + return (0,0,0,0) + + def draw(self): + g = shapes.Group() + (x1, y1, x2, y2) = self.getBounds() + r = shapes.Rect( + x = x1, + y = y1, + width = x2-x1, + height = y2-y1, + fillColor = self.fillColor, + strokeColor = self.strokeColor + ) + g.add(r) + for elem in self.contents: + g.add(elem) + return g + +def test(): + from reportlab.graphics.charts.piecharts import WedgeProperties + wedges = TypedPropertyCollection(WedgeProperties) + wedges.fillColor = colors.red + wedges.setVector(fillColor=(colors.blue,colors.green,colors.white)) + print len(_ItemWrapper) + + d = shapes.Drawing(400, 200) + tc = TwoCircles() + d.add(tc) + import renderPDF + renderPDF.drawToFile(d, 'sample_widget.pdf', 'A Sample Widget') + print 'saved sample_widget.pdf' + + d = shapes.Drawing(400, 200) + f = Face() + f.skinColor = colors.yellow + f.mood = "sad" + d.add(f, name='theFace') + print 'drawing 1 properties:' + d.dumpProperties() + renderPDF.drawToFile(d, 'face.pdf', 'A Sample Widget') + print 'saved face.pdf' + + d2 = d.expandUserNodes() + renderPDF.drawToFile(d2, 'face_copy.pdf', 'An expanded drawing') + print 'saved face_copy.pdf' + print 'drawing 2 properties:' + d2.dumpProperties() + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/__init__.py b/bin/reportlab/graphics/widgets/__init__.py new file mode 100644 index 00000000000..8558748139b --- /dev/null +++ b/bin/reportlab/graphics/widgets/__init__.py @@ -0,0 +1,4 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' \ No newline at end of file diff --git a/bin/reportlab/graphics/widgets/eventcal.py b/bin/reportlab/graphics/widgets/eventcal.py new file mode 100644 index 00000000000..20f0b817431 --- /dev/null +++ b/bin/reportlab/graphics/widgets/eventcal.py @@ -0,0 +1,303 @@ +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/eventcal.py +# Event Calendar widget +# author: Andy Robinson +"""This file is a +""" +__version__=''' $Id: eventcal.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Line, Rect, Polygon, Drawing, Group, String, Circle, Wedge +from reportlab.graphics.charts.textlabels import Label +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF + + + + +class EventCalendar(Widget): + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 300 + self.height = 150 + self.timeColWidth = None # if declared, use it; otherwise auto-size. + self.trackRowHeight = 20 + self.data = [] # list of Event objects + self.trackNames = None + + self.startTime = None #displays ALL data on day if not set + self.endTime = None # displays ALL data on day if not set + self.day = 0 + + + # we will keep any internal geometry variables + # here. These are computed by computeSize(), + # which is the first thing done when drawing. + self._talksVisible = [] # subset of data which will get plotted, cache + self._startTime = None + self._endTime = None + self._trackCount = 0 + self._colWidths = [] + self._colLeftEdges = [] # left edge of each column + + def computeSize(self): + "Called at start of draw. Sets various column widths" + self._talksVisible = self.getRelevantTalks(self.data) + self._trackCount = len(self.getAllTracks()) + self.computeStartAndEndTimes() + self._colLeftEdges = [self.x] + if self.timeColWidth is None: + w = self.width / (1 + self._trackCount) + self._colWidths = [w] * (1+ self._trackCount) + for i in range(self._trackCount): + self._colLeftEdges.append(self._colLeftEdges[-1] + w) + else: + self._colWidths = [self.timeColWidth] + w = (self.width - self.timeColWidth) / self._trackCount + for i in range(self._trackCount): + self._colWidths.append(w) + self._colLeftEdges.append(self._colLeftEdges[-1] + w) + + + + def computeStartAndEndTimes(self): + "Work out first and last times to display" + if self.startTime: + self._startTime = self.startTime + else: + for (title, speaker, trackId, day, start, duration) in self._talksVisible: + + if self._startTime is None: #first one + self._startTime = start + else: + if start < self._startTime: + self._startTime = start + + if self.endTime: + self._endTime = self.endTime + else: + for (title, speaker, trackId, day, start, duration) in self._talksVisible: + if self._endTime is None: #first one + self._endTime = start + duration + else: + if start + duration > self._endTime: + self._endTime = start + duration + + + + + def getAllTracks(self): + tracks = [] + for (title, speaker, trackId, day, hours, duration) in self.data: + if trackId is not None: + if trackId not in tracks: + tracks.append(trackId) + tracks.sort() + return tracks + + def getRelevantTalks(self, talkList): + "Scans for tracks actually used" + used = [] + for talk in talkList: + (title, speaker, trackId, day, hours, duration) = talk + assert trackId <> 0, "trackId must be None or 1,2,3... zero not allowed!" + if day == self.day: + if (((self.startTime is None) or ((hours + duration) >= self.startTime)) + and ((self.endTime is None) or (hours <= self.endTime))): + used.append(talk) + return used + + def scaleTime(self, theTime): + "Return y-value corresponding to times given" + axisHeight = self.height - self.trackRowHeight + # compute fraction between 0 and 1, 0 is at start of period + proportionUp = ((theTime - self._startTime) / (self._endTime - self._startTime)) + y = self.y + axisHeight - (axisHeight * proportionUp) + return y + + + def getTalkRect(self, startTime, duration, trackId, text): + "Return shapes for a specific talk" + g = Group() + y_bottom = self.scaleTime(startTime + duration) + y_top = self.scaleTime(startTime) + y_height = y_top - y_bottom + + if trackId is None: + #spans all columns + x = self._colLeftEdges[1] + width = self.width - self._colWidths[0] + else: + #trackId is 1-based and these arrays have the margin info in column + #zero, so no need to add 1 + x = self._colLeftEdges[trackId] + width = self._colWidths[trackId] + + lab = Label() + lab.setText(text) + lab.setOrigin(x + 0.5*width, y_bottom+0.5*y_height) + lab.boxAnchor = 'c' + lab.width = width + lab.height = y_height + lab.fontSize = 6 + + r = Rect(x, y_bottom, width, y_height, fillColor=colors.cyan) + g.add(r) + g.add(lab) + + #now for a label + # would expect to color-code and add text + return g + + def draw(self): + self.computeSize() + g = Group() + + # time column + g.add(Rect(self.x, self.y, self._colWidths[0], self.height - self.trackRowHeight, fillColor=colors.cornsilk)) + + # track headers + x = self.x + self._colWidths[0] + y = self.y + self.height - self.trackRowHeight + for trk in range(self._trackCount): + wid = self._colWidths[trk+1] + r = Rect(x, y, wid, self.trackRowHeight, fillColor=colors.yellow) + s = String(x + 0.5*wid, y, 'Track %d' % trk, align='middle') + g.add(r) + g.add(s) + x = x + wid + + for talk in self._talksVisible: + (title, speaker, trackId, day, start, duration) = talk + r = self.getTalkRect(start, duration, trackId, title + '\n' + speaker) + g.add(r) + + + return g + + + + +def test(): + "Make a conference event for day 1 of UP Python 2003" + + + d = Drawing(400,200) + + cal = EventCalendar() + cal.x = 50 + cal.y = 25 + cal.data = [ + # these might be better as objects instead of tuples, since I + # predict a large number of "optionsl" variables to affect + # formatting in future. + + #title, speaker, track id, day, start time (hrs), duration (hrs) + # track ID is 1-based not zero-based! + ('Keynote: Why design another programming language?', 'Guido van Rossum', None, 1, 9.0, 1.0), + + ('Siena Web Service Architecture', 'Marc-Andre Lemburg', 1, 1, 10.5, 1.5), + ('Extreme Programming in Python', 'Chris Withers', 2, 1, 10.5, 1.5), + ('Pattern Experiences in C++', 'Mark Radford', 3, 1, 10.5, 1.5), + ('What is the Type of std::toupper()', 'Gabriel Dos Reis', 4, 1, 10.5, 1.5), + ('Linguistic Variables: Clear Thinking with Fuzzy Logic ', 'Walter Banks', 5, 1, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 1, 12.0, 2.0), + + ("CORBA? Isn't that obsolete", 'Duncan Grisby', 1, 1, 14.0, 1.5), + ("Python Design Patterns", 'Duncan Booth', 2, 1, 14.0, 1.5), + ("Inside Security Checks and Safe Exceptions", 'Brandon Bray', 3, 1, 14.0, 1.5), + ("Studying at a Distance", 'Panel Discussion, Panel to include Alan Lenton & Francis Glassborow', 4, 1, 14.0, 1.5), + ("Coding Standards - Given the ANSI C Standard why do I still need a coding Standard", 'Randy Marques', 5, 1, 14.0, 1.5), + + ("RESTful Python", 'Hamish Lawson', 1, 1, 16.0, 1.5), + ("Parsing made easier - a radical old idea", 'Andrew Koenig', 2, 1, 16.0, 1.5), + ("C++ & Multimethods", 'Julian Smith', 3, 1, 16.0, 1.5), + ("C++ Threading", 'Kevlin Henney', 4, 1, 16.0, 1.5), + ("The Organisation Strikes Back", 'Alan Griffiths & Sarah Lees', 5, 1, 16.0, 1.5), + + ('Birds of a Feather meeting', '', None, 1, 17.5, 2.0), + + ('Keynote: In the Spirit of C', 'Greg Colvin', None, 2, 9.0, 1.0), + + ('The Infinite Filing Cabinet - object storage in Python', 'Jacob Hallen', 1, 2, 10.5, 1.5), + ('Introduction to Python and Jython for C++ and Java Programmers', 'Alex Martelli', 2, 2, 10.5, 1.5), + ('Template metaprogramming in Haskell', 'Simon Peyton Jones', 3, 2, 10.5, 1.5), + ('Plenty People Programming: C++ Programming in a Group, Workshop with a difference', 'Nico Josuttis', 4, 2, 10.5, 1.5), + ('Design and Implementation of the Boost Graph Library', 'Jeremy Siek', 5, 2, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 2, 12.0, 2.0), + + ("Building GUI Applications with PythonCard and PyCrust", 'Andy Todd', 1, 2, 14.0, 1.5), + ("Integrating Python, C and C++", 'Duncan Booth', 2, 2, 14.0, 1.5), + ("Secrets and Pitfalls of Templates", 'Nicolai Josuttis & David Vandevoorde', 3, 2, 14.0, 1.5), + ("Being a Mentor", 'Panel Discussion, Panel to include Alan Lenton & Francis Glassborow', 4, 2, 14.0, 1.5), + ("The Embedded C Extensions to C", 'Willem Wakker', 5, 2, 14.0, 1.5), + + ("Lightning Talks", 'Paul Brian', 1, 2, 16.0, 1.5), + ("Scripting Java Applications with Jython", 'Anthony Eden', 2, 2, 16.0, 1.5), + ("Metaprogramming and the Boost Metaprogramming Library", 'David Abrahams', 3, 2, 16.0, 1.5), + ("A Common Vendor ABI for C++ -- GCC's why, what and not", 'Nathan Sidwell & Gabriel Dos Reis', 4, 2, 16.0, 1.5), + ("The Timing and Cost of Choices", 'Hubert Matthews', 5, 2, 16.0, 1.5), + + ('Birds of a Feather meeting', '', None, 2, 17.5, 2.0), + + ('Keynote: The Cost of C & C++ Compatibility', 'Andy Koenig', None, 3, 9.0, 1.0), + + ('Prying Eyes: Generic Observer Implementations in C++', 'Andrei Alexandrescu', 1, 2, 10.5, 1.5), + ('The Roadmap to Generative Programming With C++', 'Ulrich Eisenecker', 2, 2, 10.5, 1.5), + ('Design Patterns in C++ and C# for the Common Language Runtime', 'Brandon Bray', 3, 2, 10.5, 1.5), + ('Extreme Hour (XH): (workshop) - Jutta Eckstein and Nico Josuttis', 'Jutta Ecstein', 4, 2, 10.5, 1.5), + ('The Lambda Library : Unnamed Functions for C++', 'Jaako Jarvi', 5, 2, 10.5, 1.5), + + ('lunch, short presentations, vendor presentations', '', None, 3, 12.0, 2.0), + + ('Reflective Metaprogramming', 'Daveed Vandevoorde', 1, 3, 14.0, 1.5), + ('Advanced Template Issues and Solutions (double session)', 'Herb Sutter',2, 3, 14.0, 3), + ('Concurrent Programming in Java (double session)', 'Angelika Langer', 3, 3, 14.0, 3), + ('What can MISRA-C (2nd Edition) do for us?', 'Chris Hills', 4, 3, 14.0, 1.5), + ('C++ Metaprogramming Concepts and Results', 'Walter E Brown', 5, 3, 14.0, 1.5), + + ('Binding C++ to Python with the Boost Python Library', 'David Abrahams', 1, 3, 16.0, 1.5), + ('Using Aspect Oriented Programming for Enterprise Application Integration', 'Arno Schmidmeier', 4, 3, 16.0, 1.5), + ('Defective C++', 'Marc Paterno', 5, 3, 16.0, 1.5), + + ("Speakers' Banquet & Birds of a Feather meeting", '', None, 3, 17.5, 2.0), + + ('Keynote: The Internet, Software and Computers - A Report Card', 'Alan Lenton', None, 4, 9.0, 1.0), + + ('Multi-Platform Software Development; Lessons from the Boost libraries', 'Beman Dawes', 1, 5, 10.5, 1.5), + ('The Stability of the C++ ABI', 'Steve Clamage', 2, 5, 10.5, 1.5), + ('Generic Build Support - A Pragmatic Approach to the Software Build Process', 'Randy Marques', 3, 5, 10.5, 1.5), + ('How to Handle Project Managers: a survival guide', 'Barb Byro', 4, 5, 10.5, 1.5), + + ('lunch, ACCU AGM', '', None, 5, 12.0, 2.0), + + ('Sauce: An OO recursive descent parser; its design and implementation.', 'Jon Jagger', 1, 5, 14.0, 1.5), + ('GNIRTS ESAC REWOL - Bringing the UNIX filters to the C++ iostream library.', 'JC van Winkel', 2, 5, 14.0, 1.5), + ('Pattern Writing: Live and Direct', 'Frank Buschmann & Kevlin Henney', 3, 5, 14.0, 3.0), + ('The Future of Programming Languages - A Goldfish Bowl', 'Francis Glassborow and friends', 3, 5, 14.0, 1.5), + + ('Honey, I Shrunk the Threads: Compile-time checked multithreaded transactions in C++', 'Andrei Alexandrescu', 1, 5, 16.0, 1.5), + ('Fun and Functionality with Functors', 'Lois Goldthwaite', 2, 5, 16.0, 1.5), + ('Agile Enough?', 'Alan Griffiths', 4, 5, 16.0, 1.5), + ("Conference Closure: A brief plenary session", '', None, 5, 17.5, 0.5), + + ] + + #return cal + cal.day = 1 + + d.add(cal) + + + for format in ['pdf']:#,'gif','png']: + out = d.asString(format) + open('eventcal.%s' % format, 'wb').write(out) + print 'saved eventcal.%s' % format + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/flags.py b/bin/reportlab/graphics/widgets/flags.py new file mode 100644 index 00000000000..d83541c0404 --- /dev/null +++ b/bin/reportlab/graphics/widgets/flags.py @@ -0,0 +1,879 @@ +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/flags.py +# Flag Widgets - a collection of flags as widgets +# author: John Precedo (johnp@reportlab.com) +"""This file is a collection of flag graphics as widgets. + +All flags are represented at the ratio of 1:2, even where the official ratio for the flag is something else +(such as 3:5 for the German national flag). The only exceptions are for where this would look _very_ wrong, +such as the Danish flag whose (ratio is 28:37), or the Swiss flag (which is square). + +Unless otherwise stated, these flags are all the 'national flags' of the countries, rather than their +state flags, naval flags, ensigns or any other variants. (National flags are the flag flown by civilians +of a country and the ones usually used to represent a country abroad. State flags are the variants used by +the government and by diplomatic missions overseas). + +To check on how close these are to the 'official' representations of flags, check the World Flag Database at +http://www.flags.ndirect.co.uk/ + +The flags this file contains are: + +EU Members: +United Kingdom, Austria, Belgium, Denmark, Finland, France, Germany, Greece, Ireland, Italy, Luxembourg, +Holland (The Netherlands), Spain, Sweden + +Others: +USA, Czech Republic, European Union, Switzerland, Turkey, Brazil + +(Brazilian flag contributed by Publio da Costa Melo [publio@planetarium.com.br]). +""" +__version__=''' $Id: flags.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import Line, Rect, Polygon, Drawing, Group, String, Circle, Wedge +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF +from signsandsymbols import _Symbol +import copy +from math import sin, cos, pi + +validFlag=OneOf(None, + 'UK', + 'USA', + 'Afghanistan', + 'Austria', + 'Belgium', + 'China', + 'Cuba', + 'Denmark', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Ireland', + 'Italy', + 'Japan', + 'Luxembourg', + 'Holland', + 'Palestine', + 'Portugal', + 'Russia', + 'Spain', + 'Sweden', + 'Norway', + 'CzechRepublic', + 'Turkey', + 'Switzerland', + 'EU', + 'Brazil' + ) + +_size = 100. + +class Star(_Symbol): + """This draws a 5-pointed star. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + _attrMap = AttrMap(BASE=_Symbol, + angle = AttrMapValue(isNumber, desc='angle in degrees'), + ) + _size = 100. + + def __init__(self): + _Symbol.__init__(self) + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = None + self.angle = 0 + + def demo(self): + D = Drawing(200, 100) + et = Star() + et.x=50 + et.y=0 + D.add(et) + labelFontSize = 10 + D.add(String(et.x+(et.size/2.0),(et.y-(1.2*labelFontSize)), + et.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + return D + + def draw(self): + s = float(self.size) #abbreviate as we will use this a lot + g = Group() + + # new algorithm from markers.StarFive + R = float(self.size)/2 + r = R*sin(18*(pi/180.0))/cos(36*(pi/180.0)) + P = [] + angle = 90 + for i in xrange(5): + for radius in R, r: + theta = angle*(pi/180.0) + P.append(radius*cos(theta)) + P.append(radius*sin(theta)) + angle = angle + 36 + # star specific bits + star = Polygon(P, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50) + g.rotate(self.angle) + g.shift(self.x+self.dx,self.y+self.dy) + g.add(star) + + return g + +class Flag(_Symbol): + """This is a generic flag class that all the flags in this file use as a basis. + + This class basically provides edges and a tidy-up routine to hide any bits of + line that overlap the 'outside' of the flag + + possible attributes: + 'x', 'y', 'size', 'fillColor' + """ + + _attrMap = AttrMap(BASE=_Symbol, + fillColor = AttrMapValue(isColor, desc='Background color'), + border = AttrMapValue(isBoolean, 'Whether a background is drawn'), + kind = AttrMapValue(validFlag, desc='Which flag'), + ) + + _cache = {} + + def __init__(self,**kw): + _Symbol.__init__(self) + self.kind = None + self.size = 100 + self.fillColor = colors.white + self.border=1 + self.setProperties(kw) + + def availableFlagNames(self): + '''return a list of the things we can display''' + return filter(lambda x: x is not None, self._attrMap['kind'].validate._enum) + + def _Flag_None(self): + s = _size # abbreviate as we will use this a lot + g = Group() + g.add(Rect(0, 0, s*2, s, fillColor = colors.purple, strokeColor = colors.black, strokeWidth=0)) + return g + + def _borderDraw(self,f): + s = self.size # abbreviate as we will use this a lot + g = Group() + g.add(f) + x, y, sW = self.x+self.dx, self.y+self.dy, self.strokeWidth/2. + g.insert(0,Rect(-sW, -sW, width=getattr(self,'_width',2*s)+3*sW, height=getattr(self,'_height',s)+2*sW, + fillColor = None, strokeColor = self.strokeColor, strokeWidth=sW*2)) + g.shift(x,y) + g.scale(s/_size, s/_size) + return g + + def draw(self): + kind = self.kind or 'None' + f = self._cache.get(kind) + if not f: + f = getattr(self,'_Flag_'+kind)() + self._cache[kind] = f._explode() + return self._borderDraw(f) + + def clone(self): + return copy.copy(self) + + def demo(self): + D = Drawing(200, 100) + name = self.availableFlagNames() + import time + name = name[int(time.time()) % len(name)] + fx = Flag() + fx.kind = name + fx.x = 0 + fx.y = 0 + D.add(fx) + labelFontSize = 10 + D.add(String(fx.x+(fx.size/2),(fx.y-(1.2*labelFontSize)), + name, fillColor=colors.black, textAnchor='middle', + fontSize=labelFontSize)) + labelFontSize = int(fx.size/4) + D.add(String(fx.x+(fx.size),(fx.y+((fx.size/2))), + "SAMPLE", fillColor=colors.gold, textAnchor='middle', + fontSize=labelFontSize, fontName="Helvetica-Bold")) + return D + + def _Flag_UK(self): + s = _size + g = Group() + w = s*2 + g.add(Rect(0, 0, w, s, fillColor = colors.navy, strokeColor = colors.black, strokeWidth=0)) + g.add(Polygon([0,0, s*.225,0, w,s*(1-.1125), w,s, w-s*.225,s, 0, s*.1125], fillColor = colors.mintcream, strokeColor=None, strokeWidth=0)) + g.add(Polygon([0,s*(1-.1125), 0, s, s*.225,s, w, s*.1125, w,0, w-s*.225,0], fillColor = colors.mintcream, strokeColor=None, strokeWidth=0)) + g.add(Polygon([0, s-(s/15), (s-((s/10)*4)), (s*0.65), (s-(s/10)*3), (s*0.65), 0, s], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([0, 0, (s-((s/10)*3)), (s*0.35), (s-((s/10)*2)), (s*0.35), (s/10), 0], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([w, s, (s+((s/10)*3)), (s*0.65), (s+((s/10)*2)), (s*0.65), w-(s/10), s], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Polygon([w, (s/15), (s+((s/10)*4)), (s*0.35), (s+((s/10)*3)), (s*0.35), w, 0], fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(((s*0.42)*2), 0, width=(0.16*s)*2, height=s, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s*0.35), width=w, height=s*0.3, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect(((s*0.45)*2), 0, width=(0.1*s)*2, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s*0.4), width=w, height=s*0.2, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_USA(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + for stripecounter in range (13,0, -1): + stripeheight = s/13.0 + if not (stripecounter%2 == 0): + stripecolor = colors.red + else: + stripecolor = colors.mintcream + redorwhiteline = Rect(0, (s-(stripeheight*stripecounter)), width=s*2, height=stripeheight, + fillColor = stripecolor, strokeColor = None, strokeWidth=20) + g.add(redorwhiteline) + + bluebox = Rect(0, (s-(stripeheight*7)), width=0.8*s, height=stripeheight*7, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + + lss = s*0.045 + lss2 = lss/2 + s9 = s/9 + s7 = s/7 + for starxcounter in range(5): + for starycounter in range(4): + ls = Star() + ls.size = lss + ls.x = 0-s/22+lss/2+s7+starxcounter*s7 + ls.fillColor = colors.mintcream + ls.y = s-(starycounter+1)*s9+lss2 + g.add(ls) + + for starxcounter in range(6): + for starycounter in range(5): + ls = Star() + ls.size = lss + ls.x = 0-(s/22)+lss/2+s/14+starxcounter*s7 + ls.fillColor = colors.mintcream + ls.y = s-(starycounter+1)*s9+(s/18)+lss2 + g.add(ls) + return g + + def _Flag_Afghanistan(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + greenbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.limegreen, strokeColor = None, strokeWidth=0) + g.add(greenbox) + + blackbox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(blackbox) + return g + + def _Flag_Austria(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.mintcream, + strokeColor = colors.black, strokeWidth=0) + g.add(box) + + + redbox1 = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox1) + + redbox2 = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox2) + return g + + def _Flag_Belgium(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.black, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + + box1 = Rect(0, 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(box1) + + box2 = Rect(((s/3.0)*2.0), 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.gold, strokeColor = None, strokeWidth=0) + g.add(box2) + + box3 = Rect(((s/3.0)*4.0), 0, width=(s/3.0)*2.0, height=s, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(box3) + return g + + def _Flag_China(self): + s = _size + g = Group() + self._width = w = s*1.5 + g.add(Rect(0, 0, w, s, fillColor=colors.red, strokeColor=None, strokeWidth=0)) + + def addStar(x,y,size,angle,g=g,w=s/20,x0=0,y0=s/2): + s = Star() + s.fillColor=colors.yellow + s.angle = angle + s.size = size*w*2 + s.x = x*w+x0 + s.y = y*w+y0 + g.add(s) + + addStar(5,5,3, 0) + addStar(10,1,1,36.86989765) + addStar(12,3,1,8.213210702) + addStar(12,6,1,16.60154960) + addStar(10,8,1,53.13010235) + return g + + def _Flag_Cuba(self): + s = _size + g = Group() + + for i in range(5): + stripe = Rect(0, i*s/5, width=s*2, height=s/5, + fillColor = [colors.darkblue, colors.mintcream][i%2], + strokeColor = None, + strokeWidth=0) + g.add(stripe) + + redwedge = Polygon(points = [ 0, 0, 4*s/5, (s/2), 0, s], + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redwedge) + + star = Star() + star.x = 2.5*s/10 + star.y = s/2 + star.size = 3*s/10 + star.fillColor = colors.white + g.add(star) + + box = Rect(0, 0, s*2, s, + fillColor = None, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + return g + + def _Flag_Denmark(self): + s = _size + g = Group() + self._width = w = s*1.4 + + box = Rect(0, 0, w, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whitebox1 = Rect(((s/5)*2), 0, width=s/6, height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox1) + + whitebox2 = Rect(0, ((s/2)-(s/12)), width=w, height=s/6, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox2) + return g + + def _Flag_Finland(self): + s = _size + g = Group() + + # crossbox specific bits + box = Rect(0, 0, s*2, s, + fillColor = colors.ghostwhite, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + blueline1 = Rect((s*0.6), 0, width=0.3*s, height=s, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline1) + + blueline2 = Rect(0, (s*0.4), width=s*2, height=s*0.3, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline2) + return g + + def _Flag_France(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.navy, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + bluebox = Rect(0, 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.blue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + + whitebox = Rect(((s/3.0)*2.0), 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox) + + redbox = Rect(((s/3.0)*4.0), 0, width=((s/3.0)*2.0), height=s, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redbox) + return g + + def _Flag_Germany(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.gold, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + blackbox1 = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.black, strokeColor = None, strokeWidth=0) + g.add(blackbox1) + + redbox1 = Rect(0, (s/3.0), width=s*2.0, height=s/3.0, + fillColor = colors.orangered, strokeColor = None, strokeWidth=0) + g.add(redbox1) + return g + + def _Flag_Greece(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, fillColor = colors.gold, + strokeColor = colors.black, strokeWidth=0) + g.add(box) + + for stripecounter in range (9,0, -1): + stripeheight = s/9.0 + if not (stripecounter%2 == 0): + stripecolor = colors.deepskyblue + else: + stripecolor = colors.mintcream + + blueorwhiteline = Rect(0, (s-(stripeheight*stripecounter)), width=s*2, height=stripeheight, + fillColor = stripecolor, strokeColor = None, strokeWidth=20) + g.add(blueorwhiteline) + + bluebox1 = Rect(0, ((s)-stripeheight*5), width=(stripeheight*5), height=stripeheight*5, + fillColor = colors.deepskyblue, strokeColor = None, strokeWidth=0) + g.add(bluebox1) + + whiteline1 = Rect(0, ((s)-stripeheight*3), width=stripeheight*5, height=stripeheight, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whiteline1) + + whiteline2 = Rect((stripeheight*2), ((s)-stripeheight*5), width=stripeheight, height=stripeheight*5, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whiteline2) + + return g + + def _Flag_Ireland(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.forestgreen, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whitebox = Rect(((s*2.0)/3.0), 0, width=(2.0*(s*2.0)/3.0), height=s, + fillColor = colors.mintcream, strokeColor = None, strokeWidth=0) + g.add(whitebox) + + orangebox = Rect(((2.0*(s*2.0)/3.0)), 0, width=(s*2.0)/3.0, height=s, + fillColor = colors.darkorange, strokeColor = None, strokeWidth=0) + g.add(orangebox) + return g + + def _Flag_Italy(self): + s = _size + g = Group() + g.add(Rect(0,0,s*2,s,fillColor=colors.forestgreen,strokeColor=None, strokeWidth=0)) + g.add(Rect((2*s)/3, 0, width=(s*4)/3, height=s, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + g.add(Rect((4*s)/3, 0, width=(s*2)/3, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Japan(self): + s = _size + g = Group() + w = self._width = s*1.5 + g.add(Rect(0,0,w,s,fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Circle(cx=w/2,cy=s/2,r=0.3*w,fillColor=colors.red,strokeColor=None, strokeWidth=0)) + return g + + def _Flag_Luxembourg(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + redbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox) + + bluebox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.dodgerblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + return g + + def _Flag_Holland(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + redbox = Rect(0, ((s/3.0)*2.0), width=s*2.0, height=s/3.0, + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redbox) + + bluebox = Rect(0, 0, width=s*2.0, height=s/3.0, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluebox) + return g + + def _Flag_Portugal(self): + return Group() + + def _Flag_Russia(self): + s = _size + g = Group() + w = self._width = s*1.5 + t = s/3 + g.add(Rect(0, 0, width=w, height=t, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, t, width=w, height=t, fillColor = colors.blue, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, 2*t, width=w, height=t, fillColor = colors.mintcream, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Spain(self): + s = _size + g = Group() + w = self._width = s*1.5 + g.add(Rect(0, 0, width=w, height=s, fillColor = colors.red, strokeColor = None, strokeWidth=0)) + g.add(Rect(0, (s/4), width=w, height=s/2, fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + return g + + def _Flag_Sweden(self): + s = _size + g = Group() + self._width = s*1.4 + box = Rect(0, 0, self._width, s, + fillColor = colors.dodgerblue, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + box1 = Rect(((s/5)*2), 0, width=s/6, height=s, + fillColor = colors.gold, strokeColor = None, strokeWidth=0) + g.add(box1) + + box2 = Rect(0, ((s/2)-(s/12)), width=self._width, height=s/6, + fillColor = colors.gold, + strokeColor = None, + strokeWidth=0) + g.add(box2) + return g + + def _Flag_Norway(self): + s = _size + g = Group() + self._width = s*1.4 + + box = Rect(0, 0, self._width, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + box = Rect(0, 0, self._width, s, + fillColor = colors.red, strokeColor = colors.black, strokeWidth=0) + g.add(box) + + whiteline1 = Rect(((s*0.2)*2), 0, width=s*0.2, height=s, + fillColor = colors.ghostwhite, strokeColor = None, strokeWidth=0) + g.add(whiteline1) + + whiteline2 = Rect(0, (s*0.4), width=self._width, height=s*0.2, + fillColor = colors.ghostwhite, strokeColor = None, strokeWidth=0) + g.add(whiteline2) + + blueline1 = Rect(((s*0.225)*2), 0, width=0.1*s, height=s, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline1) + + blueline2 = Rect(0, (s*0.45), width=self._width, height=s*0.1, + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(blueline2) + return g + + def _Flag_CzechRepublic(self): + s = _size + g = Group() + box = Rect(0, 0, s*2, s, + fillColor = colors.mintcream, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + redbox = Rect(0, 0, width=s*2, height=s/2, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redbox) + + bluewedge = Polygon(points = [ 0, 0, s, (s/2), 0, s], + fillColor = colors.darkblue, strokeColor = None, strokeWidth=0) + g.add(bluewedge) + return g + + def _Flag_Palestine(self): + s = _size + g = Group() + box = Rect(0, s/3, s*2, s/3, + fillColor = colors.mintcream, + strokeColor = None, + strokeWidth=0) + g.add(box) + + greenbox = Rect(0, 0, width=s*2, height=s/3, + fillColor = colors.limegreen, + strokeColor = None, + strokeWidth=0) + g.add(greenbox) + + blackbox = Rect(0, 2*s/3, width=s*2, height=s/3, + fillColor = colors.black, + strokeColor = None, + strokeWidth=0) + g.add(blackbox) + + redwedge = Polygon(points = [ 0, 0, 2*s/3, (s/2), 0, s], + fillColor = colors.red, strokeColor = None, strokeWidth=0) + g.add(redwedge) + return g + + def _Flag_Turkey(self): + s = _size + g = Group() + + box = Rect(0, 0, s*2, s, + fillColor = colors.red, + strokeColor = colors.black, + strokeWidth=0) + g.add(box) + + whitecircle = Circle(cx=((s*0.35)*2), cy=s/2, r=s*0.3, + fillColor = colors.mintcream, + strokeColor = None, + strokeWidth=0) + g.add(whitecircle) + + redcircle = Circle(cx=((s*0.39)*2), cy=s/2, r=s*0.24, + fillColor = colors.red, + strokeColor = None, + strokeWidth=0) + g.add(redcircle) + + ws = Star() + ws.angle = 15 + ws.size = s/5 + ws.x = (s*0.5)*2+ws.size/2 + ws.y = (s*0.5) + ws.fillColor = colors.mintcream + ws.strokeColor = None + g.add(ws) + return g + + def _Flag_Switzerland(self): + s = _size + g = Group() + self._width = s + + g.add(Rect(0, 0, s, s, fillColor = colors.red, strokeColor = colors.black, strokeWidth=0)) + g.add(Line((s/2), (s/5.5), (s/2), (s-(s/5.5)), + fillColor = colors.mintcream, strokeColor = colors.mintcream, strokeWidth=(s/5))) + g.add(Line((s/5.5), (s/2), (s-(s/5.5)), (s/2), + fillColor = colors.mintcream, strokeColor = colors.mintcream, strokeWidth=s/5)) + return g + + def _Flag_EU(self): + s = _size + g = Group() + w = self._width = 1.5*s + + g.add(Rect(0, 0, w, s, fillColor = colors.darkblue, strokeColor = None, strokeWidth=0)) + centerx=w/2 + centery=s/2 + radius=s/3 + yradius = radius + xradius = radius + nStars = 12 + delta = 2*pi/nStars + for i in range(nStars): + rad = i*delta + gs = Star() + gs.x=cos(rad)*radius+centerx + gs.y=sin(rad)*radius+centery + gs.size=s/10 + gs.fillColor=colors.gold + g.add(gs) + return g + + def _Flag_Brazil(self): + s = _size # abbreviate as we will use this a lot + g = Group() + + m = s/14 + self._width = w = (m * 20) + + def addStar(x,y,size, g=g, w=w, s=s, m=m): + st = Star() + st.fillColor=colors.mintcream + st.size = size*m + st.x = (w/2) + (x * (0.35 * m)) + st.y = (s/2) + (y * (0.35 * m)) + g.add(st) + + g.add(Rect(0, 0, w, s, fillColor = colors.green, strokeColor = None, strokeWidth=0)) + g.add(Polygon(points = [ 1.7*m, (s/2), (w/2), s-(1.7*m), w-(1.7*m),(s/2),(w/2), 1.7*m], + fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + g.add(Circle(cx=w/2, cy=s/2, r=3.5*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2)-(2*m), 0, 8.5*m, 50, 98.1, 8.5*m, + fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2), (s/2), 3.501*m, 156, 352, 3.501*m, + fillColor=colors.mintcream,strokeColor=None, strokeWidth=0)) + g.add(Wedge((w/2)-(2*m), 0, 8*m, 48.1, 100, 8*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + g.add(Rect(0, 0, w, (s/4) + 1.7*m, + fillColor = colors.green, strokeColor = None, strokeWidth=0)) + g.add(Polygon(points = [ 1.7*m,(s/2), (w/2),s/2 - 2*m, w-(1.7*m),(s/2) , (w/2),1.7*m], + fillColor = colors.yellow, strokeColor = None, strokeWidth=0)) + g.add(Wedge(w/2, s/2, 3.502*m, 166, 342.1, 3.502*m, + fillColor=colors.blue,strokeColor=None, strokeWidth=0)) + + addStar(3.2,3.5,0.3) + addStar(-8.5,1.5,0.3) + addStar(-7.5,-3,0.3) + addStar(-4,-5.5,0.3) + addStar(0,-4.5,0.3) + addStar(7,-3.5,0.3) + addStar(-3.5,-0.5,0.25) + addStar(0,-1.5,0.25) + addStar(1,-2.5,0.25) + addStar(3,-7,0.25) + addStar(5,-6.5,0.25) + addStar(6.5,-5,0.25) + addStar(7,-4.5,0.25) + addStar(-5.5,-3.2,0.25) + addStar(-6,-4.2,0.25) + addStar(-1,-2.75,0.2) + addStar(2,-5.5,0.2) + addStar(4,-5.5,0.2) + addStar(5,-7.5,0.2) + addStar(5,-5.5,0.2) + addStar(6,-5.5,0.2) + addStar(-8.8,-3.2,0.2) + addStar(2.5,0.5,0.2) + addStar(-0.2,-3.2,0.14) + addStar(-7.2,-2,0.14) + addStar(0,-8,0.1) + + sTmp = "ORDEM E PROGRESSO" + nTmp = len(sTmp) + delta = 0.850848010347/nTmp + radius = 7.9 *m + centerx = (w/2)-(2*m) + centery = 0 + for i in range(nTmp): + rad = 2*pi - i*delta -4.60766922527 + x=cos(rad)*radius+centerx + y=sin(rad)*radius+centery + if i == 6: + z = 0.35*m + else: + z= 0.45*m + g2 = Group(String(x, y, sTmp[i], fontName='Helvetica-Bold', + fontSize = z,strokeColor=None,fillColor=colors.green)) + g2.rotate(rad) + g.add(g2) + return g + +def makeFlag(name): + flag = Flag() + flag.kind = name + return flag + +def test(): + """This function produces three pdf files with examples of all the signs and symbols from this file. + """ +# page 1 + + labelFontSize = 10 + + X = (20,245) + + flags = [ + 'UK', + 'USA', + 'Afghanistan', + 'Austria', + 'Belgium', + 'Denmark', + 'Cuba', + 'Finland', + 'France', + 'Germany', + 'Greece', + 'Ireland', + 'Italy', + 'Luxembourg', + 'Holland', + 'Palestine', + 'Portugal', + 'Spain', + 'Sweden', + 'Norway', + 'CzechRepublic', + 'Turkey', + 'Switzerland', + 'EU', + 'Brazil', + ] + y = Y0 = 530 + f = 0 + D = None + for name in flags: + if not D: D = Drawing(450,650) + flag = makeFlag(name) + i = flags.index(name) + flag.x = X[i%2] + flag.y = y + D.add(flag) + D.add(String(flag.x+(flag.size/2),(flag.y-(1.2*labelFontSize)), + name, fillColor=colors.black, textAnchor='middle', fontSize=labelFontSize)) + if i%2: y = y - 125 + if (i%2 and y<0) or name==flags[-1]: + renderPDF.drawToFile(D, 'flags%02d.pdf'%f, 'flags.py - Page #%d'%(f+1)) + y = Y0 + f = f+1 + D = None + +if __name__=='__main__': + test() diff --git a/bin/reportlab/graphics/widgets/grids.py b/bin/reportlab/graphics/widgets/grids.py new file mode 100644 index 00000000000..d3072728a89 --- /dev/null +++ b/bin/reportlab/graphics/widgets/grids.py @@ -0,0 +1,504 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/grids.py +__version__=''' $Id: grids.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import isNumber, isColorOrNone, isBoolean, isListOfNumbers, OneOf, isListOfColors +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.graphics.shapes import Drawing, Group, Line, Rect, LineShape, definePath, EmptyClipPath +from reportlab.graphics.widgetbase import Widget + +def frange(start, end=None, inc=None): + "A range function, that does accept float increments..." + + if end == None: + end = start + 0.0 + start = 0.0 + + if inc == None: + inc = 1.0 + + L = [] + end = end - inc*0.0001 #to avoid numrical problems + while 1: + next = start + len(L) * inc + if inc > 0 and next >= end: + break + elif inc < 0 and next <= end: + break + L.append(next) + + return L + + +def makeDistancesList(list): + """Returns a list of distances between adjacent numbers in some input list. + + E.g. [1, 1, 2, 3, 5, 7] -> [0, 1, 1, 2, 2] + """ + + d = [] + for i in range(len(list[:-1])): + d.append(list[i+1] - list[i]) + + return d + + +class Grid(Widget): + """This makes a rectangular grid of equidistant stripes. + + The grid contains an outer border rectangle, and stripes + inside which can be drawn with lines and/or as solid tiles. + The drawing order is: outer rectangle, then lines and tiles. + + The stripes' width is indicated as 'delta'. The sequence of + stripes can have an offset named 'delta0'. Both values need + to be positive! + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), + desc='Determines if stripes are vertical or horizontal.'), + useLines = AttrMapValue(OneOf((0, 1)), + desc='Determines if stripes are drawn with lines.'), + useRects = AttrMapValue(OneOf((0, 1)), + desc='Determines if stripes are drawn with solid rectangles.'), + delta = AttrMapValue(isNumber, + desc='Determines the width/height of the stripes.'), + delta0 = AttrMapValue(isNumber, + desc='Determines the stripes initial width/height offset.'), + deltaSteps = AttrMapValue(isListOfNumbers, + desc='List of deltas to be used cyclically.'), + stripeColors = AttrMapValue(isListOfColors, + desc='Colors applied cyclically in the right or upper direction.'), + fillColor = AttrMapValue(isColorOrNone, + desc='Background color for entire rectangle.'), + strokeColor = AttrMapValue(isColorOrNone, + desc='Color used for lines.'), + strokeWidth = AttrMapValue(isNumber, + desc='Width used for lines.'), + rectStrokeColor = AttrMapValue(isColorOrNone, desc='Color for outer rect stroke.'), + rectStrokeWidth = AttrMapValue(isColorOrNone, desc='Width for outer rect stroke.'), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.orientation = 'vertical' + self.useLines = 0 + self.useRects = 1 + self.delta = 20 + self.delta0 = 0 + self.deltaSteps = [] + self.fillColor = colors.white + self.stripeColors = [colors.red, colors.green, colors.blue] + self.strokeColor = colors.black + self.strokeWidth = 2 + + + def demo(self): + D = Drawing(100, 100) + + g = Grid() + D.add(g) + + return D + + def makeOuterRect(self): + strokeColor = getattr(self,'rectStrokeColor',self.strokeColor) + strokeWidth = getattr(self,'rectStrokeWidth',self.strokeWidth) + if self.fillColor or (strokeColor and strokeWidth): + rect = Rect(self.x, self.y, self.width, self.height) + rect.fillColor = self.fillColor + rect.strokeColor = strokeColor + rect.strokeWidth = strokeWidth + return rect + else: + return None + + def makeLinePosList(self, start, isX=0): + "Returns a list of positions where to place lines." + + w, h = self.width, self.height + if isX: + length = w + else: + length = h + if self.deltaSteps: + r = [start + self.delta0] + i = 0 + while 1: + if r[-1] > start + length: + del r[-1] + break + r.append(r[-1] + self.deltaSteps[i % len(self.deltaSteps)]) + i = i + 1 + else: + r = frange(start + self.delta0, start + length, self.delta) + + r.append(start + length) + if self.delta0 != 0: + r.insert(0, start) + #print 'Grid.makeLinePosList() -> %s' % r + return r + + + def makeInnerLines(self): + # inner grid lines + group = Group() + + w, h = self.width, self.height + + if self.useLines == 1: + if self.orientation == 'vertical': + r = self.makeLinePosList(self.x, isX=1) + for x in r: + line = Line(x, self.y, x, self.y + h) + line.strokeColor = self.strokeColor + line.strokeWidth = self.strokeWidth + group.add(line) + elif self.orientation == 'horizontal': + r = self.makeLinePosList(self.y, isX=0) + for y in r: + line = Line(self.x, y, self.x + w, y) + line.strokeColor = self.strokeColor + line.strokeWidth = self.strokeWidth + group.add(line) + + return group + + + def makeInnerTiles(self): + # inner grid lines + group = Group() + + w, h = self.width, self.height + + # inner grid stripes (solid rectangles) + if self.useRects == 1: + cols = self.stripeColors + + if self.orientation == 'vertical': + r = self.makeLinePosList(self.x, isX=1) + elif self.orientation == 'horizontal': + r = self.makeLinePosList(self.y, isX=0) + + dist = makeDistancesList(r) + + i = 0 + for j in range(len(dist)): + if self.orientation == 'vertical': + x = r[j] + stripe = Rect(x, self.y, dist[j], h) + elif self.orientation == 'horizontal': + y = r[j] + stripe = Rect(self.x, y, w, dist[j]) + stripe.fillColor = cols[i % len(cols)] + stripe.strokeColor = None + group.add(stripe) + i = i + 1 + + return group + + + def draw(self): + # general widget bits + group = Group() + + group.add(self.makeOuterRect()) + group.add(self.makeInnerTiles()) + group.add(self.makeInnerLines(),name='_gridLines') + + return group + + +class DoubleGrid(Widget): + """This combines two ordinary Grid objects orthogonal to each other. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + grid0 = AttrMapValue(None, desc="The first grid component."), + grid1 = AttrMapValue(None, desc="The second grid component."), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + + g0 = Grid() + g0.x = self.x + g0.y = self.y + g0.width = self.width + g0.height = self.height + g0.orientation = 'vertical' + g0.useLines = 1 + g0.useRects = 0 + g0.delta = 20 + g0.delta0 = 0 + g0.deltaSteps = [] + g0.fillColor = colors.white + g0.stripeColors = [colors.red, colors.green, colors.blue] + g0.strokeColor = colors.black + g0.strokeWidth = 1 + + g1 = Grid() + g1.x = self.x + g1.y = self.y + g1.width = self.width + g1.height = self.height + g1.orientation = 'horizontal' + g1.useLines = 1 + g1.useRects = 0 + g1.delta = 20 + g1.delta0 = 0 + g1.deltaSteps = [] + g1.fillColor = colors.white + g1.stripeColors = [colors.red, colors.green, colors.blue] + g1.strokeColor = colors.black + g1.strokeWidth = 1 + + self.grid0 = g0 + self.grid1 = g1 + + +## # This gives an AttributeError: +## # DoubleGrid instance has no attribute 'grid0' +## def __setattr__(self, name, value): +## if name in ('x', 'y', 'width', 'height'): +## setattr(self.grid0, name, value) +## setattr(self.grid1, name, value) + + + def demo(self): + D = Drawing(100, 100) + g = DoubleGrid() + D.add(g) + return D + + + def draw(self): + group = Group() + g0, g1 = self.grid0, self.grid1 + # Order groups to make sure both v and h lines + # are visible (works only when there is only + # one kind of stripes, v or h). + G = g0.useRects == 1 and g1.useRects == 0 and (g0,g1) or (g1,g0) + for g in G: + group.add(g.makeOuterRect()) + for g in G: + group.add(g.makeInnerTiles()) + group.add(g.makeInnerLines(),name='_gridLines') + + return group + + +class ShadedRect(Widget): + """This makes a rectangle with shaded colors between two colors. + + Colors are interpolated linearly between 'fillColorStart' + and 'fillColorEnd', both of which appear at the margins. + If 'numShades' is set to one, though, only 'fillColorStart' + is used. + """ + + _attrMap = AttrMap( + x = AttrMapValue(isNumber, desc="The grid's lower-left x position."), + y = AttrMapValue(isNumber, desc="The grid's lower-left y position."), + width = AttrMapValue(isNumber, desc="The grid's width."), + height = AttrMapValue(isNumber, desc="The grid's height."), + orientation = AttrMapValue(OneOf(('vertical', 'horizontal')), desc='Determines if stripes are vertical or horizontal.'), + numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'), + fillColorStart = AttrMapValue(isColorOrNone, desc='Start value of the color shade.'), + fillColorEnd = AttrMapValue(isColorOrNone, desc='End value of the color shade.'), + strokeColor = AttrMapValue(isColorOrNone, desc='Color used for border line.'), + strokeWidth = AttrMapValue(isNumber, desc='Width used for lines.'), + cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), + ) + + def __init__(self,**kw): + self.x = 0 + self.y = 0 + self.width = 100 + self.height = 100 + self.orientation = 'vertical' + self.numShades = 20 + self.fillColorStart = colors.pink + self.fillColorEnd = colors.black + self.strokeColor = colors.black + self.strokeWidth = 2 + self.cylinderMode = 0 + self.setProperties(kw) + + def demo(self): + D = Drawing(100, 100) + g = ShadedRect() + D.add(g) + + return D + + def _flipRectCorners(self): + "Flip rectangle's corners if width or height is negative." + x, y, width, height, fillColorStart, fillColorEnd = self.x, self.y, self.width, self.height, self.fillColorStart, self.fillColorEnd + if width < 0 and height > 0: + x = x + width + width = -width + if self.orientation=='vertical': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart + elif height<0 and width>0: + y = y + height + height = -height + if self.orientation=='horizontal': fillColorStart, fillColorEnd = fillColorEnd, fillColorStart + elif height < 0 and height < 0: + x = x + width + width = -width + y = y + height + height = -height + return x, y, width, height, fillColorStart, fillColorEnd + + def draw(self): + # general widget bits + group = Group() + x, y, w, h, c0, c1 = self._flipRectCorners() + numShades = self.numShades + if self.cylinderMode: + if not numShades%2: numShades = numShades+1 + halfNumShades = (numShades-1)/2 + 1 + num = float(numShades) # must make it float! + vertical = self.orientation == 'vertical' + if vertical: + if numShades == 1: + V = [x] + else: + V = frange(x, x + w, w/num) + else: + if numShades == 1: + V = [y] + else: + V = frange(y, y + h, h/num) + + for v in V: + stripe = vertical and Rect(v, y, w/num, h) or Rect(x, v, w, h/num) + if self.cylinderMode: + if V.index(v)>=halfNumShades: + col = colors.linearlyInterpolatedColor(c1,c0,V[halfNumShades],V[-1], v) + else: + col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[halfNumShades], v) + else: + col = colors.linearlyInterpolatedColor(c0,c1,V[0],V[-1], v) + stripe.fillColor = col + stripe.strokeColor = col + stripe.strokeWidth = 1 + group.add(stripe) + if self.strokeColor and self.strokeWidth>=0: + rect = Rect(x, y, w, h) + rect.strokeColor = self.strokeColor + rect.strokeWidth = self.strokeWidth + rect.fillColor = None + group.add(rect) + return group + + +def colorRange(c0, c1, n): + "Return a range of intermediate colors between c0 and c1" + if n==1: return [c0] + + C = [] + if n>1: + lim = n-1 + for i in range(n): + C.append(colors.linearlyInterpolatedColor(c0,c1,0,lim, i)) + return C + + +def centroid(P): + '''compute average point of a set of points''' + return reduce(lambda x,y, fn=float(len(P)): (x[0]+y[0]/fn,x[1]+y[1]/fn),P,(0,0)) + +def rotatedEnclosingRect(P, angle, rect): + ''' + given P a sequence P of x,y coordinate pairs and an angle in degrees + find the centroid of P and the axis at angle theta through it + find the extreme points of P wrt axis parallel distance and axis + orthogonal distance. Then compute the least rectangle that will still + enclose P when rotated by angle. + + The class R + ''' + from math import pi, cos, sin, tan + x0, y0 = centroid(P) + theta = (angle/180.)*pi + s,c=sin(theta),cos(theta) + def parallelAxisDist((x,y),s=s,c=c,x0=x0,y0=y0): + return (s*(y-y0)+c*(x-x0)) + def orthogonalAxisDist((x,y),s=s,c=c,x0=x0,y0=y0): + return (c*(y-y0)+s*(x-x0)) + L = map(parallelAxisDist,P) + L.sort() + a0, a1 = L[0], L[-1] + L = map(orthogonalAxisDist,P) + L.sort() + b0, b1 = L[0], L[-1] + rect.x, rect.width = a0, a1-a0 + rect.y, rect.height = b0, b1-b0 + g = Group(transform=(c,s,-s,c,x0,y0)) + g.add(rect) + return g + +class ShadedPolygon(Widget,LineShape): + _attrMap = AttrMap(BASE=LineShape, + angle = AttrMapValue(isNumber,desc="Shading angle"), + fillColorStart = AttrMapValue(isColorOrNone), + fillColorEnd = AttrMapValue(isColorOrNone), + numShades = AttrMapValue(isNumber, desc='The number of interpolating colors.'), + cylinderMode = AttrMapValue(isBoolean, desc='True if shading reverses in middle.'), + points = AttrMapValue(isListOfNumbers), + ) + + def __init__(self,**kw): + self.angle = 90 + self.fillColorStart = colors.red + self.fillColorEnd = colors.green + self.cylinderMode = 0 + self.numShades = 50 + self.points = [-1,-1,2,2,3,-1] + LineShape.__init__(self,kw) + + def draw(self): + P = self.points + P = map(lambda i, P=P:(P[i],P[i+1]),xrange(0,len(P),2)) + path = definePath([('moveTo',)+P[0]]+map(lambda x: ('lineTo',)+x,P[1:])+['closePath'], + fillColor=None, strokeColor=None) + path.isClipPath = 1 + g = Group() + g.add(path) + rect = ShadedRect(strokeWidth=0,strokeColor=None) + for k in 'fillColorStart', 'fillColorEnd', 'numShades', 'cylinderMode': + setattr(rect,k,getattr(self,k)) + g.add(rotatedEnclosingRect(P, self.angle, rect)) + g.add(EmptyClipPath) + path = path.copy() + path.isClipPath = 0 + path.strokeColor = self.strokeColor + path.strokeWidth = self.strokeWidth + g.add(path) + return g + +if __name__=='__main__': #noruntests + from reportlab.lib.colors import blue + from reportlab.graphics.shapes import Drawing + angle=45 + D = Drawing(120,120) + D.add(ShadedPolygon(points=(10,10,60,60,110,10),strokeColor=None,strokeWidth=1,angle=90,numShades=50,cylinderMode=0)) + D.save(formats=['gif'],fnRoot='shobj',outDir='/tmp') diff --git a/bin/reportlab/graphics/widgets/markers.py b/bin/reportlab/graphics/widgets/markers.py new file mode 100644 index 00000000000..ed700773ab2 --- /dev/null +++ b/bin/reportlab/graphics/widgets/markers.py @@ -0,0 +1,228 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/markers.py +""" +This modules defines a collection of markers used in charts. +""" +__version__=''' $Id: markers.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +from types import FunctionType, ClassType +from reportlab.graphics.shapes import Rect, Line, Circle, Polygon, Drawing, Group +from reportlab.graphics.widgets.signsandsymbols import SmileyFace +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.validators import isNumber, isColorOrNone, OneOf, Validator +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.lib.colors import black +from reportlab.graphics.widgets.flags import Flag +from math import sin, cos, pi +import copy, new +_toradians = pi/180.0 + +class Marker(Widget): + '''A polymorphic class of markers''' + _attrMap = AttrMap(BASE=Widget, + kind = AttrMapValue( + OneOf(None, 'Square', 'Diamond', 'Circle', 'Cross', 'Triangle', 'StarSix', + 'Pentagon', 'Hexagon', 'Heptagon', 'Octagon', 'StarFive', + 'FilledSquare', 'FilledCircle', 'FilledDiamond', 'FilledCross', + 'FilledTriangle','FilledStarSix', 'FilledPentagon', 'FilledHexagon', + 'FilledHeptagon', 'FilledOctagon', 'FilledStarFive', + 'Smiley'), + desc='marker type name'), + size = AttrMapValue(isNumber,desc='marker size'), + x = AttrMapValue(isNumber,desc='marker x coordinate'), + y = AttrMapValue(isNumber,desc='marker y coordinate'), + dx = AttrMapValue(isNumber,desc='marker x coordinate adjustment'), + dy = AttrMapValue(isNumber,desc='marker y coordinate adjustment'), + angle = AttrMapValue(isNumber,desc='marker rotation'), + fillColor = AttrMapValue(isColorOrNone, desc='marker fill colour'), + strokeColor = AttrMapValue(isColorOrNone, desc='marker stroke colour'), + strokeWidth = AttrMapValue(isNumber, desc='marker stroke width'), + ) + + def __init__(self,*args,**kw): + self.kind = None + self.strokeColor = black + self.strokeWidth = 0.1 + self.fillColor = None + self.size = 5 + self.x = self.y = self.dx = self.dy = self.angle = 0 + self.setProperties(kw) + + def clone(self): + return new.instance(self.__class__,self.__dict__.copy()) + + def _Smiley(self): + x, y = self.x+self.dx, self.y+self.dy + d = self.size/2.0 + s = SmileyFace() + s.fillColor = self.fillColor + s.strokeWidth = self.strokeWidth + s.strokeColor = self.strokeColor + s.x = x-d + s.y = y-d + s.size = d*2 + return s + + def _Square(self): + x, y = self.x+self.dx, self.y+self.dy + d = self.size/2.0 + s = Rect(x-d,y-d,2*d,2*d,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) + return s + + def _Diamond(self): + d = self.size/2.0 + return self._doPolygon((-d,0,0,d,d,0,0,-d)) + + def _Circle(self): + x, y = self.x+self.dx, self.y+self.dy + s = Circle(x,y,self.size/2.0,fillColor=self.fillColor,strokeColor=self.strokeColor,strokeWidth=self.strokeWidth) + return s + + def _Cross(self): + x, y = self.x+self.dx, self.y+self.dy + s = float(self.size) + h, s = s/2, s/6 + return self._doPolygon((-s,-h,-s,-s,-h,-s,-h,s,-s,s,-s,h,s,h,s,s,h,s,h,-s,s,-s,s,-h)) + + def _Triangle(self): + x, y = self.x+self.dx, self.y+self.dy + r = float(self.size)/2 + c = 30*_toradians + s = sin(30*_toradians)*r + c = cos(c)*r + return self._doPolygon((0,r,-c,-s,c,-s)) + + def _StarSix(self): + r = float(self.size)/2 + c = 30*_toradians + s = sin(c)*r + c = cos(c)*r + z = s/2 + g = c/2 + return self._doPolygon((0,r,-z,s,-c,s,-s,0,-c,-s,-z,-s,0,-r,z,-s,c,-s,s,0,c,s,z,s)) + + def _StarFive(self): + R = float(self.size)/2 + r = R*sin(18*_toradians)/cos(36*_toradians) + P = [] + angle = 90 + for i in xrange(5): + for radius in R, r: + theta = angle*_toradians + P.append(radius*cos(theta)) + P.append(radius*sin(theta)) + angle = angle + 36 + return self._doPolygon(P) + + def _Pentagon(self): + return self._doNgon(5) + + def _Hexagon(self): + return self._doNgon(6) + + def _Heptagon(self): + return self._doNgon(7) + + def _Octagon(self): + return self._doNgon(8) + + def _doPolygon(self,P): + x, y = self.x+self.dx, self.y+self.dy + if x or y: P = map(lambda i,P=P,A=[x,y]: P[i] + A[i&1], range(len(P))) + return Polygon(P, strokeWidth =self.strokeWidth, strokeColor=self.strokeColor, fillColor=self.fillColor) + + def _doFill(self): + old = self.fillColor + if old is None: + self.fillColor = self.strokeColor + r = (self.kind and getattr(self,'_'+self.kind[6:]) or Group)() + self.fillColor = old + return r + + def _doNgon(self,n): + P = [] + size = float(self.size)/2 + for i in xrange(n): + r = (2.*i/n+0.5)*pi + P.append(size*cos(r)) + P.append(size*sin(r)) + return self._doPolygon(P) + + _FilledCircle = _doFill + _FilledSquare = _doFill + _FilledDiamond = _doFill + _FilledCross = _doFill + _FilledTriangle = _doFill + _FilledStarSix = _doFill + _FilledPentagon = _doFill + _FilledHexagon = _doFill + _FilledHeptagon = _doFill + _FilledOctagon = _doFill + _FilledStarFive = _doFill + + def draw(self): + if self.kind: + m = getattr(self,'_'+self.kind) + if self.angle: + _x, _dx, _y, _dy = self.x, self.dx, self.y, self.dy + self.x, self.dx, self.y, self.dy = 0,0,0,0 + try: + m = m() + finally: + self.x, self.dx, self.y, self.dy = _x, _dx, _y, _dy + if not isinstance(m,Group): + _m, m = m, Group() + m.add(_m) + if self.angle: m.rotate(self.angle) + x, y = _x+_dx, _y+_dy + if x or y: m.shift(x,y) + else: + m = m() + else: + m = Group() + return m + +def uSymbol2Symbol(uSymbol,x,y,color): + if type(uSymbol) == FunctionType: + symbol = uSymbol(x, y, 5, color) + elif type(uSymbol) == ClassType and issubclass(uSymbol,Widget): + size = 10. + symbol = uSymbol() + symbol.x = x - (size/2) + symbol.y = y - (size/2) + try: + symbol.size = size + symbol.color = color + except: + pass + elif isinstance(uSymbol,Marker) or isinstance(uSymbol,Flag): + symbol = uSymbol.clone() + if isinstance(uSymbol,Marker): symbol.fillColor = symbol.fillColor or color + symbol.x, symbol.y = x, y + else: + symbol = None + return symbol + +class _isSymbol(Validator): + def test(self,x): + return callable(x) or isinstance(x,Marker) or isinstance(x,Flag) \ + or (type(x)==ClassType and issubclass(x,Widget)) + +isSymbol = _isSymbol() + +def makeMarker(name,**kw): + if Marker._attrMap['kind'].validate(name): + m = apply(Marker,(),kw) + m.kind = name + elif name[-5:]=='_Flag' and Flag._attrMap['kind'].validate(name[:-5]): + m = apply(Flag,(),kw) + m.kind = name[:-5] + m.size = 10 + else: + raise ValueError, "Invalid marker name %s" % name + return m + +if __name__=='__main__': + D = Drawing() + D.add(Marker()) + D.save(fnRoot='Marker',formats=['pdf'], outDir='/tmp') diff --git a/bin/reportlab/graphics/widgets/signsandsymbols.py b/bin/reportlab/graphics/widgets/signsandsymbols.py new file mode 100644 index 00000000000..f7f193cf536 --- /dev/null +++ b/bin/reportlab/graphics/widgets/signsandsymbols.py @@ -0,0 +1,919 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/graphics/widgets/signsandsymbols.py +# signsandsymbols.py +# A collection of new widgets +# author: John Precedo (johnp@reportlab.com) +"""This file is a collection of widgets to produce some common signs and symbols. + +Widgets include: +- ETriangle (an equilateral triangle), +- RTriangle (a right angled triangle), +- Octagon, +- Crossbox, +- Tickbox, +- SmileyFace, +- StopSign, +- NoEntry, +- NotAllowed (the red roundel from 'no smoking' signs), +- NoSmoking, +- DangerSign (a black exclamation point in a yellow triangle), +- YesNo (returns a tickbox or a crossbox depending on a testvalue), +- FloppyDisk, +- ArrowOne, and +- ArrowTwo +""" +__version__=''' $Id: signsandsymbols.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib import colors +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics import shapes +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics import renderPDF + + +class _Symbol(Widget): + """Abstract base widget + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + """ + _nodoc = 1 + _attrMap = AttrMap( + x = AttrMapValue(isNumber,desc='symbol x coordinate'), + y = AttrMapValue(isNumber,desc='symbol y coordinate'), + dx = AttrMapValue(isNumber,desc='symbol x coordinate adjustment'), + dy = AttrMapValue(isNumber,desc='symbol x coordinate adjustment'), + size = AttrMapValue(isNumber), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + strokeWidth = AttrMapValue(isNumber), + ) + def __init__(self): + assert self.__class__.__name__!='_Symbol', 'Abstract class _Symbol instantiated' + self.x = self.y = self.dx = self.dy = 0 + self.size = 100 + self.fillColor = colors.red + self.strokeColor = None + self.strokeWidth = 0.1 + + def demo(self): + D = shapes.Drawing(200, 100) + s = float(self.size) + ob = self.__class__() + ob.x=50 + ob.y=0 + ob.draw() + D.add(ob) + D.add(shapes.String(ob.x+(s/2),(ob.y-12), + ob.__class__.__name__, fillColor=colors.black, textAnchor='middle', + fontSize=10)) + return D + +class ETriangle(_Symbol): + """This draws an equilateral triangle.""" + + def __init__(self): + pass #AbstractSymbol + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Triangle specific bits + ae = s*0.125 #(ae = 'an eighth') + triangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x+(s/2),self.y+s], + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50.) + g.add(triangle) + return g + +class RTriangle(_Symbol): + """This draws a right-angled triangle. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.green + self.strokeColor = None + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Triangle specific bits + ae = s*0.125 #(ae = 'an eighth') + triangle = shapes.Polygon(points = [ + self.x, self.y, + self.x+s, self.y, + self.x,self.y+s], + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=s/50.) + g.add(triangle) + return g + +class Octagon(_Symbol): + """This widget draws an Octagon. + + possible attributes: + 'x', 'y', 'size', 'fillColor', 'strokeColor' + + """ + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = None + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # Octagon specific bits + athird=s/3 + + octagon = shapes.Polygon(points=[self.x+athird, self.y, + self.x, self.y+athird, + self.x, self.y+(athird*2), + self.x+athird, self.y+s, + self.x+(athird*2), self.y+s, + self.x+s, self.y+(athird*2), + self.x+s, self.y+athird, + self.x+(athird*2), self.y], + strokeColor = self.strokeColor, + fillColor = self.fillColor, + strokeWidth=10) + g.add(octagon) + return g + +class Crossbox(_Symbol): + """This draws a black box with a red cross in it - a 'checkbox'. + + possible attributes: + 'x', 'y', 'size', 'crossColor', 'strokeColor', 'crosswidth' + + """ + + _attrMap = AttrMap(BASE=_Symbol, + crossColor = AttrMapValue(isColorOrNone), + crosswidth = AttrMapValue(isNumber), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.white + self.crossColor = colors.red + self.strokeColor = colors.black + self.crosswidth = 10 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # crossbox specific bits + box = shapes.Rect(self.x+1, self.y+1, s-2, s-2, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=2) + g.add(box) + + crossLine1 = shapes.Line(self.x+(s*0.15), self.y+(s*0.15), self.x+(s*0.85), self.y+(s*0.85), + fillColor = self.crossColor, + strokeColor = self.crossColor, + strokeWidth = self.crosswidth) + g.add(crossLine1) + + crossLine2 = shapes.Line(self.x+(s*0.15), self.y+(s*0.85), self.x+(s*0.85) ,self.y+(s*0.15), + fillColor = self.crossColor, + strokeColor = self.crossColor, + strokeWidth = self.crosswidth) + g.add(crossLine2) + + return g + + +class Tickbox(_Symbol): + """This draws a black box with a red tick in it - another 'checkbox'. + + possible attributes: + 'x', 'y', 'size', 'tickColor', 'strokeColor', 'tickwidth' + +""" + + _attrMap = AttrMap(BASE=_Symbol, + tickColor = AttrMapValue(isColorOrNone), + tickwidth = AttrMapValue(isNumber), + ) + + def __init__(self): + self.x = 0 + self.y = 0 + self.size = 100 + self.tickColor = colors.red + self.strokeColor = colors.black + self.fillColor = colors.white + self.tickwidth = 10 + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # tickbox specific bits + box = shapes.Rect(self.x+1, self.y+1, s-2, s-2, + fillColor = self.fillColor, + strokeColor = self.strokeColor, + strokeWidth=2) + g.add(box) + + tickLine = shapes.PolyLine(points = [self.x+(s*0.15), self.y+(s*0.35), self.x+(s*0.35), self.y+(s*0.15), + self.x+(s*0.35), self.y+(s*0.15), self.x+(s*0.85) ,self.y+(s*0.85)], + fillColor = self.tickColor, + strokeColor = self.tickColor, + strokeWidth = self.tickwidth) + g.add(tickLine) + + return g + +class SmileyFace(_Symbol): + """This draws a classic smiley face. + + possible attributes: + 'x', 'y', 'size', 'fillColor' + + """ + + def __init__(self): + _Symbol.__init__(self) + self.x = 0 + self.y = 0 + self.size = 100 + self.fillColor = colors.yellow + self.strokeColor = colors.black + + def draw(self): + # general widget bits + s = float(self.size) # abbreviate as we will use this a lot + g = shapes.Group() + + # SmileyFace specific bits + g.add(shapes.Circle(cx=self.x+(s/2), cy=self.y+(s/2), r=s/2, + fillColor=self.fillColor, strokeColor=self.strokeColor, + strokeWidth=max(s/38.,self.strokeWidth))) + + for i in (1,2): + g.add(shapes.Ellipse(self.x+(s/3)*i,self.y+(s/3)*2, s/30, s/10, + fillColor=self.strokeColor, strokeColor = self.strokeColor, + strokeWidth=max(s/38.,self.strokeWidth))) + + # calculate a pointslist for the mouth + # THIS IS A HACK! - don't use if there is a 'shapes.Arc' + centerx=self.x+(s/2) + centery=self.y+(s/2) + radius=s/3 + yradius = radius + xradius = radius + startangledegrees=200 + endangledegrees=340 + degreedelta = 1 + pointslist = [] + a = pointslist.append + from math import sin, cos, pi + degreestoradians = pi/180.0 + radiansdelta = degreedelta*degreestoradians + startangle = startangledegrees*degreestoradians + endangle = endangledegrees*degreestoradians + while endangle= searchto: + break # EXIT LOOP + match = m.group(0) + end = start + len(match) + c = match[0] + if c not in "#'\"": + # Must have matched a keyword. + if start <> searchfrom: + # there's still a redundant char before and after it, strip! + match = match[1:-1] + start = start + 1 + else: + # this is the first keyword in the text. + # Only a space at the end. + match = match[:-1] + end = end - 1 + tags_append((keywordTag, start, end, None)) + # If this was a defining keyword, look ahead to the + # following identifier. + if match in ["def", "class"]: + m = idSearch(pytext, end) + if m is not None: + start = m.start() + if start == end: + match = m.group(0) + end = start + len(match) + tags_append((identifierTag, start, end, None)) + elif c == "#": + tags_append((commentTag, start, end, None)) + else: + tags_append((stringTag, start, end, None)) + return tags + + +def test(path): + f = open(path) + text = f.read() + f.close() + tags = fontify(text) + for tag, start, end, sublist in tags: + print tag, `text[start:end]` \ No newline at end of file diff --git a/bin/reportlab/lib/__init__.py b/bin/reportlab/lib/__init__.py new file mode 100644 index 00000000000..22d3f38c1b7 --- /dev/null +++ b/bin/reportlab/lib/__init__.py @@ -0,0 +1,7 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +import os +RL_DEBUG = os.environ.has_key('RL_DEBUG') \ No newline at end of file diff --git a/bin/reportlab/lib/abag.py b/bin/reportlab/lib/abag.py new file mode 100644 index 00000000000..4c3c029a8d0 --- /dev/null +++ b/bin/reportlab/lib/abag.py @@ -0,0 +1,44 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/abag.py +__version__=''' $Id: abag.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +class ABag: + """ + 'Attribute Bag' - a trivial BAG class for holding attributes. + + You may initialize with keyword arguments. + a = ABag(k0=v0,....,kx=vx,....) ==> getattr(a,'kx')==vx + + c = a.clone(ak0=av0,.....) copy with optional additional attributes. + """ + def __init__(self,**attr): + for k,v in attr.items(): + setattr(self,k,v) + + def clone(self,**attr): + n = apply(ABag,(),self.__dict__) + if attr != {}: apply(ABag.__init__,(n,),attr) + return n + + def __repr__(self): + import string + n = self.__class__.__name__ + L = [n+"("] + keys = self.__dict__.keys() + for k in keys: + v = getattr(self, k) + rk = repr(k) + rv = repr(v) + rk = " "+string.replace(rk, "\n", "\n ") + rv = " "+string.replace(rv, "\n", "\n ") + L.append(rk) + L.append(rv) + L.append(") #"+n) + return string.join(L, "\n") + +if __name__=="__main__": + AB = ABag(a=1, c="hello") + CD = AB.clone() + print AB + print CD \ No newline at end of file diff --git a/bin/reportlab/lib/attrmap.py b/bin/reportlab/lib/attrmap.py new file mode 100644 index 00000000000..2f8e89c13f8 --- /dev/null +++ b/bin/reportlab/lib/attrmap.py @@ -0,0 +1,138 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/attrmap.py +__version__=''' $Id: attrmap.py 2741 2005-12-07 21:52:33Z andy $ ''' +from UserDict import UserDict +from reportlab.lib.validators import isAnything, _SequenceTypes, DerivedValue +from reportlab import rl_config + +class CallableValue: + '''a class to allow callable initial values''' + def __init__(self,func,*args,**kw): + #assert iscallable(func) + self.func = func + self.args = args + self.kw = kw + + def __call__(self): + return apply(self.func,self.args,self.kw) + +class AttrMapValue: + '''Simple multi-value holder for attribute maps''' + def __init__(self,validate=None,desc=None,initial=None, **kw): + self.validate = validate or isAnything + self.desc = desc + self._initial = initial + for k,v in kw.items(): + setattr(self,k,v) + + def __getattr__(self,name): + #hack to allow callable initial values + if name=='initial': + if isinstance(self._initial,CallableValue): return self._initial() + return self._initial + elif name=='hidden': + return 0 + raise AttributeError, name + +class AttrMap(UserDict): + def __init__(self,BASE=None,UNWANTED=[],**kw): + data = {} + if BASE: + if isinstance(BASE,AttrMap): + data = BASE.data #they used BASECLASS._attrMap + else: + if type(BASE) not in (type(()),type([])): BASE = (BASE,) + for B in BASE: + if hasattr(B,'_attrMap'): + data.update(getattr(B._attrMap,'data',{})) + else: + raise ValueError, 'BASE=%s has wrong kind of value' % str(B) + + UserDict.__init__(self,data) + self.remove(UNWANTED) + self.data.update(kw) + + def update(self,kw): + if isinstance(kw,AttrMap): kw = kw.data + self.data.update(kw) + + def remove(self,unwanted): + for k in unwanted: + try: + del self[k] + except KeyError: + pass + + def clone(self,UNWANTED=[],**kw): + c = AttrMap(BASE=self,UNWANTED=UNWANTED) + c.update(kw) + return c + +def validateSetattr(obj,name,value): + '''validate setattr(obj,name,value)''' + if rl_config.shapeChecking: + map = obj._attrMap + if map and name[0]!= '_': + #we always allow the inherited values; they cannot + #be checked until draw time. + if isinstance(value, DerivedValue): + #let it through + pass + else: + try: + validate = map[name].validate + if not validate(value): + raise AttributeError, "Illegal assignment of '%s' to '%s' in class %s" % (value, name, obj.__class__.__name__) + except KeyError: + raise AttributeError, "Illegal attribute '%s' in class %s" % (name, obj.__class__.__name__) + obj.__dict__[name] = value + +def _privateAttrMap(obj,ret=0): + '''clone obj._attrMap if required''' + A = obj._attrMap + oA = getattr(obj.__class__,'_attrMap',None) + if ret: + if oA is A: + return A.clone(), oA + else: + return A, None + else: + if oA is A: + obj._attrMap = A.clone() + +def _findObjectAndAttr(src, P): + '''Locate the object src.P for P a string, return parent and name of attribute + ''' + P = string.split(P, '.') + if len(P) == 0: + return None, None + else: + for p in P[0:-1]: + src = getattr(src, p) + return src, P[-1] + +def hook__setattr__(obj): + if not hasattr(obj,'__attrproxy__'): + C = obj.__class__ + import new + obj.__class__=new.classobj(C.__name__,(C,)+C.__bases__, + {'__attrproxy__':[], + '__setattr__':lambda self,k,v,osa=getattr(obj,'__setattr__',None),hook=hook: hook(self,k,v,osa)}) + +def addProxyAttribute(src,name,validate=None,desc=None,initial=None,dst=None): + ''' + Add a proxy attribute 'name' to src with targets dst + ''' + #sanity + assert hasattr(src,'_attrMap'), 'src object has no _attrMap' + A, oA = _privateAttrMap(src,1) + if type(dst) not in _SequenceTypes: dst = dst, + D = [] + DV = [] + for d in dst: + if type(d) in _SequenceTypes: + d, e = d[0], d[1:] + obj, attr = _findObjectAndAttr(src,d) + if obj: + dA = getattr(obj,'_attrMap',None) diff --git a/bin/reportlab/lib/codecharts.py b/bin/reportlab/lib/codecharts.py new file mode 100644 index 00000000000..3d8630364b8 --- /dev/null +++ b/bin/reportlab/lib/codecharts.py @@ -0,0 +1,365 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/codecharts.py +#$Header $ +__version__=''' $Id ''' +__doc__="""Routines to print code page (character set) drawings. + +To be sure we can accurately represent characters in various encodings +and fonts, we need some routines to display all those characters. +These are defined herein. The idea is to include flowable, drawable +and graphic objects for single and multi-byte fonts. """ +import string +import codecs + +from reportlab.pdfgen.canvas import Canvas +from reportlab.platypus import Flowable +from reportlab.pdfbase import pdfmetrics, cidfonts +from reportlab.graphics.shapes import Drawing, Group, String, Circle, Rect +from reportlab.graphics.widgetbase import Widget +from reportlab.lib import colors + +adobe2codec = { + 'WinAnsiEncoding':'winansi', + 'MacRomanEncoding':'macroman', + 'MacExpert':'macexpert', + 'PDFDoc':'pdfdoc', + + } + +class CodeChartBase(Flowable): + """Basic bits of drawing furniture used by + single and multi-byte versions: ability to put letters + into boxes.""" + + def calcLayout(self): + "Work out x and y positions for drawing" + + + rows = self.codePoints * 1.0 / self.charsPerRow + if rows == int(rows): + self.rows = int(rows) + else: + self.rows = int(rows) + 1 + # size allows for a gray column of labels + self.width = self.boxSize * (1+self.charsPerRow) + self.height = self.boxSize * (1+self.rows) + + #handy lists + self.ylist = [] + for row in range(self.rows + 2): + self.ylist.append(row * self.boxSize) + self.xlist = [] + for col in range(self.charsPerRow + 2): + self.xlist.append(col * self.boxSize) + + def formatByte(self, byt): + if self.hex: + return '%02X' % byt + else: + return '%d' % byt + + def drawChars(self, charList): + """Fills boxes in order. None means skip a box. + Empty boxes at end get filled with gray""" + extraNeeded = (self.rows * self.charsPerRow - len(charList)) + for i in range(extraNeeded): + charList.append(None) + #charList.extend([None] * extraNeeded) + row = 0 + col = 0 + self.canv.setFont(self.fontName, self.boxSize * 0.75) + for ch in charList: # may be 2 bytes or 1 + if ch is None: + self.canv.setFillGray(0.9) + self.canv.rect((1+col) * self.boxSize, (self.rows - row - 1) * self.boxSize, + self.boxSize, self.boxSize, stroke=0, fill=1) + self.canv.setFillGray(0.0) + else: + try: + self.canv.drawCentredString( + (col+1.5) * self.boxSize, + (self.rows - row - 0.875) * self.boxSize, + ch, + ) + except: + self.canv.setFillGray(0.9) + self.canv.rect((1+col) * self.boxSize, (self.rows - row - 1) * self.boxSize, + self.boxSize, self.boxSize, stroke=0, fill=1) + self.canv.drawCentredString( + (col+1.5) * self.boxSize, + (self.rows - row - 0.875) * self.boxSize, + '?', + ) + self.canv.setFillGray(0.0) + col = col + 1 + if col == self.charsPerRow: + row = row + 1 + col = 0 + + def drawLabels(self, topLeft = ''): + """Writes little labels in the top row and first column""" + self.canv.setFillGray(0.8) + self.canv.rect(0, self.ylist[-2], self.width, self.boxSize, fill=1, stroke=0) + self.canv.rect(0, 0, self.boxSize, self.ylist[-2], fill=1, stroke=0) + self.canv.setFillGray(0.0) + + #label each row and column + self.canv.setFont('Helvetica-Oblique',0.375 * self.boxSize) + byt = 0 + for row in range(self.rows): + if self.rowLabels: + label = self.rowLabels[row] + else: # format start bytes as hex or decimal + label = self.formatByte(row * self.charsPerRow) + self.canv.drawCentredString(0.5 * self.boxSize, + (self.rows - row - 0.75) * self.boxSize, + label + ) + for col in range(self.charsPerRow): + self.canv.drawCentredString((col + 1.5) * self.boxSize, + (self.rows + 0.25) * self.boxSize, + self.formatByte(col) + ) + + if topLeft: + self.canv.setFont('Helvetica-BoldOblique',0.5 * self.boxSize) + self.canv.drawCentredString(0.5 * self.boxSize, + (self.rows + 0.25) * self.boxSize, + topLeft + ) + +class SingleByteEncodingChart(CodeChartBase): + def __init__(self, faceName='Helvetica', encodingName='WinAnsiEncoding', + charsPerRow=16, boxSize=14, hex=1): + self.codePoints = 256 + self.faceName = faceName + self.encodingName = encodingName + self.fontName = self.faceName + '-' + self.encodingName + self.charsPerRow = charsPerRow + self.boxSize = boxSize + self.hex = hex + self.rowLabels = None + pdfmetrics.registerFont(pdfmetrics.Font(self.fontName, + self.faceName, + self.encodingName) + ) + + self.calcLayout() + + + def draw(self): + self.drawLabels() + charList = [None] * 32 + map(chr, range(32, 256)) + + #we need to convert these to Unicode, since ReportLab + #2.0 can only draw in Unicode. + + encName = self.encodingName + #apply some common translations + encName = adobe2codec.get(encName, encName) + decoder = codecs.lookup(encName)[1] + def decodeFunc(txt): + if txt is None: + return None + else: + return decoder(txt, errors='replace')[0] + + charList = [decodeFunc(ch) for ch in charList] + + + + self.drawChars(charList) + self.canv.grid(self.xlist, self.ylist) + + +class KutenRowCodeChart(CodeChartBase): + """Formats one 'row' of the 94x94 space used in many Asian encodings.aliases + + These deliberately resemble the code charts in Ken Lunde's "Understanding + CJKV Information Processing", to enable manual checking. Due to the large + numbers of characters, we don't try to make one graphic with 10,000 characters, + but rather output a sequence of these.""" + #would be cleaner if both shared one base class whose job + #was to draw the boxes, but never mind... + def __init__(self, row, faceName, encodingName): + self.row = row + self.codePoints = 94 + self.boxSize = 18 + self.charsPerRow = 20 + self.rows = 5 + self.rowLabels = ['00','20','40','60','80'] + self.hex = 0 + self.faceName = faceName + self.encodingName = encodingName + + try: + # the dependent files might not be available + font = cidfonts.CIDFont(self.faceName, self.encodingName) + pdfmetrics.registerFont(font) + except: + # fall back to English and at least shwo we can draw the boxes + self.faceName = 'Helvetica' + self.encodingName = 'WinAnsiEncoding' + self.fontName = self.faceName + '-' + self.encodingName + self.calcLayout() + + def makeRow(self, row): + """Works out the character values for this kuten row""" + cells = [] + if string.find(self.encodingName, 'EUC') > -1: + # it is an EUC family encoding. + for col in range(1, 95): + ch = chr(row + 160) + chr(col+160) + cells.append(ch) +## elif string.find(self.encodingName, 'GB') > -1: +## # it is an EUC family encoding. +## for col in range(1, 95): +## ch = chr(row + 160) + chr(col+160) + else: + cells.append([None] * 94) + return cells + + def draw(self): + self.drawLabels(topLeft= 'R%d' % self.row) + + # work out which characters we need for the row + #assert string.find(self.encodingName, 'EUC') > -1, 'Only handles EUC encoding today, you gave me %s!' % self.encodingName + + # pad out by 1 to match Ken Lunde's tables + charList = [None] + self.makeRow(self.row) + self.drawChars(charList) + self.canv.grid(self.xlist, self.ylist) + + +class Big5CodeChart(CodeChartBase): + """Formats one 'row' of the 94x160 space used in Big 5 + + These deliberately resemble the code charts in Ken Lunde's "Understanding + CJKV Information Processing", to enable manual checking.""" + def __init__(self, row, faceName, encodingName): + self.row = row + self.codePoints = 160 + self.boxSize = 18 + self.charsPerRow = 16 + self.rows = 10 + self.hex = 1 + self.faceName = faceName + self.encodingName = encodingName + self.rowLabels = ['4','5','6','7','A','B','C','D','E','F'] + try: + # the dependent files might not be available + font = cidfonts.CIDFont(self.faceName, self.encodingName) + pdfmetrics.registerFont(font) + except: + # fall back to English and at least shwo we can draw the boxes + self.faceName = 'Helvetica' + self.encodingName = 'WinAnsiEncoding' + self.fontName = self.faceName + '-' + self.encodingName + self.calcLayout() + + def makeRow(self, row): + """Works out the character values for this Big5 row. + Rows start at 0xA1""" + cells = [] + if string.find(self.encodingName, 'B5') > -1: + # big 5, different row size + for y in [4,5,6,7,10,11,12,13,14,15]: + for x in range(16): + col = y*16+x + ch = chr(row) + chr(col) + cells.append(ch) + + else: + cells.append([None] * 160) + return cells + + def draw(self): + self.drawLabels(topLeft='%02X' % self.row) + + charList = self.makeRow(self.row) + self.drawChars(charList) + self.canv.grid(self.xlist, self.ylist) + + +def hBoxText(msg, canvas, x, y, fontName): + """Helper for stringwidth tests on Asian fonts. + + Registers font if needed. Then draws the string, + and a box around it derived from the stringWidth function""" + canvas.saveState() + try: + font = pdfmetrics.getFont(fontName) + except KeyError: + font = cidfonts.UnicodeCIDFont(fontName) + pdfmetrics.registerFont(font) + + canvas.setFillGray(0.8) + canvas.rect(x,y,pdfmetrics.stringWidth(msg, fontName, 16),16,stroke=0,fill=1) + canvas.setFillGray(0) + canvas.setFont(fontName, 16,16) + canvas.drawString(x,y,msg) + canvas.restoreState() + + +class CodeWidget(Widget): + """Block showing all the characters""" + def __init__(self): + self.x = 0 + self.y = 0 + self.width = 160 + self.height = 160 + + def draw(self): + dx = self.width / 16.0 + dy = self.height / 16.0 + g = Group() + g.add(Rect(self.x, self.y, self.width, self.height, + fillColor=None, strokeColor=colors.black)) + for x in range(16): + for y in range(16): + charValue = y * 16 + x + if charValue > 32: + s = String(self.x + x * dx, + self.y + (self.height - y*dy), chr(charValue)) + g.add(s) + return g + + + + + + +def test(): + c = Canvas('codecharts.pdf') + c.setFont('Helvetica-Bold', 24) + c.drawString(72, 750, 'Testing code page charts') + cc1 = SingleByteEncodingChart() + cc1.drawOn(c, 72, 500) + + cc2 = SingleByteEncodingChart(charsPerRow=32) + cc2.drawOn(c, 72, 300) + + cc3 = SingleByteEncodingChart(charsPerRow=25, hex=0) + cc3.drawOn(c, 72, 100) + +## c.showPage() +## +## c.setFont('Helvetica-Bold', 24) +## c.drawString(72, 750, 'Multi-byte Kuten code chart examples') +## KutenRowCodeChart(1, 'HeiseiMin-W3','EUC-H').drawOn(c, 72, 600) +## KutenRowCodeChart(16, 'HeiseiMin-W3','EUC-H').drawOn(c, 72, 450) +## KutenRowCodeChart(84, 'HeiseiMin-W3','EUC-H').drawOn(c, 72, 300) +## +## c.showPage() +## c.setFont('Helvetica-Bold', 24) +## c.drawString(72, 750, 'Big5 Code Chart Examples') +## #Big5CodeChart(0xA1, 'MSungStd-Light-Acro','ETenms-B5-H').drawOn(c, 72, 500) + + c.save() + print 'saved codecharts.pdf' + +if __name__=='__main__': + test() + + diff --git a/bin/reportlab/lib/colors.py b/bin/reportlab/lib/colors.py new file mode 100644 index 00000000000..ed537259e06 --- /dev/null +++ b/bin/reportlab/lib/colors.py @@ -0,0 +1,565 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/colors.py +__version__=''' $Id: colors.py 2587 2005-05-03 11:41:54Z rgbecker $ ''' + +import string, math +from types import StringType, ListType, TupleType +from reportlab.lib.utils import fp_str +_SeqTypes = (ListType,TupleType) + +class Color: + """This class is used to represent color. Components red, green, blue + are in the range 0 (dark) to 1 (full intensity).""" + + def __init__(self, red=0, green=0, blue=0): + "Initialize with red, green, blue in range [0-1]." + self.red, self.green, self.blue = red,green,blue + + def __repr__(self): + return "Color(%s)" % string.replace(fp_str(self.red, self.green, self.blue),' ',',') + + def __hash__(self): + return hash( (self.red, self.green, self.blue) ) + + def __cmp__(self,other): + try: + dsum = 4*self.red-4*other.red + 2*self.green-2*other.green + self.blue-other.blue + except: + return -1 + if dsum > 0: return 1 + if dsum < 0: return -1 + return 0 + + def rgb(self): + "Returns a three-tuple of components" + return (self.red, self.green, self.blue) + + def bitmap_rgb(self): + return tuple(map(lambda x: int(x*255)&255, self.rgb())) + + def hexval(self): + return '0x%02x%02x%02x' % self.bitmap_rgb() + +class CMYKColor(Color): + """This represents colors using the CMYK (cyan, magenta, yellow, black) + model commonly used in professional printing. This is implemented + as a derived class so that renderers which only know about RGB "see it" + as an RGB color through its 'red','green' and 'blue' attributes, according + to an approximate function. + + The RGB approximation is worked out when the object in constructed, so + the color attributes should not be changed afterwards. + + Extra attributes may be attached to the class to support specific ink models, + and renderers may look for these.""" + + def __init__(self, cyan=0, magenta=0, yellow=0, black=0, + spotName=None, density=1, knockout=None): + """ + Initialize with four colors in range [0-1]. the optional + spotName, density & knockout may be of use to specific renderers. + spotName is intended for use as an identifier to the renderer not client programs. + density is used to modify the overall amount of ink. + knockout is a renderer dependent option that determines whether the applied colour + knocksout (removes) existing colour; None means use the global default. + """ + self.cyan = cyan + self.magenta = magenta + self.yellow = yellow + self.black = black + self.spotName = spotName + self.density = max(min(density,1),0) # force into right range + self.knockout = knockout + + # now work out the RGB approximation. override + self.red, self.green, self.blue = cmyk2rgb( (cyan, magenta, yellow, black) ) + + if density<1: + #density adjustment of rgb approximants, effectively mix with white + r, g, b = self.red, self.green, self.blue + r = density*(r-1)+1 + g = density*(g-1)+1 + b = density*(b-1)+1 + self.red, self.green, self.blue = (r,g,b) + + def __repr__(self): + return "CMYKColor(%s%s%s%s)" % ( + string.replace(fp_str(self.cyan, self.magenta, self.yellow, self.black),' ',','), + (self.spotName and (',spotName='+repr(self.spotName)) or ''), + (self.density!=1 and (',density='+fp_str(self.density)) or ''), + (self.knockout is not None and (',knockout=%d' % self.knockout) or ''), + ) + + def __hash__(self): + return hash( (self.cyan, self.magenta, self.yellow, self.black, self.density, self.spotName) ) + + def __cmp__(self,other): + """Partial ordering of colors according to a notion of distance. + + Comparing across the two color models is of limited use.""" + # why the try-except? What can go wrong? + if isinstance(other, CMYKColor): + dsum = (((( (self.cyan-other.cyan)*2 + + (self.magenta-other.magenta))*2+ + (self.yellow-other.yellow))*2+ + (self.black-other.black))*2+ + (self.density-other.density))*2 + cmp(self.spotName or '',other.spotName or '') + else: # do the RGB comparison + try: + dsum = ((self.red-other.red)*2+(self.green-other.green))*2+(self.blue-other.blue) + except: # or just return 'not equal' if not a color + return -1 + if dsum >= 0: + return dsum>0 + else: + return -1 + + def cmyk(self): + "Returns a tuple of four color components - syntactic sugar" + return (self.cyan, self.magenta, self.yellow, self.black) + + def _density_str(self): + return fp_str(self.density) + +class PCMYKColor(CMYKColor): + '''100 based CMYKColor with density and a spotName; just like Rimas uses''' + def __init__(self,cyan,magenta,yellow,black,density=100,spotName=None,knockout=None): + CMYKColor.__init__(self,cyan/100.,magenta/100.,yellow/100.,black/100.,spotName,density/100.,knockout=knockout) + + def __repr__(self): + return "PCMYKColor(%s%s%s%s)" % ( + string.replace(fp_str(self.cyan*100, self.magenta*100, self.yellow*100, self.black*100),' ',','), + (self.spotName and (',spotName='+repr(self.spotName)) or ''), + (self.density!=1 and (',density='+fp_str(self.density*100)) or ''), + (self.knockout is not None and (',knockout=%d' % self.knockout) or ''), + ) + +def cmyk2rgb((c,m,y,k),density=1): + "Convert from a CMYK color tuple to an RGB color tuple" + # From the Adobe Postscript Ref. Manual 2nd ed. + r = 1.0 - min(1.0, c + k) + g = 1.0 - min(1.0, m + k) + b = 1.0 - min(1.0, y + k) + return (r,g,b) + +def rgb2cmyk(r,g,b): + '''one way to get cmyk from rgb''' + c = 1 - r + m = 1 - g + y = 1 - b + k = min(c,m,y) + c = min(1,max(0,c-k)) + m = min(1,max(0,m-k)) + y = min(1,max(0,y-k)) + k = min(1,max(0,k)) + return (c,m,y,k) + +def color2bw(colorRGB): + "Transform an RGB color to a black and white equivalent." + + col = colorRGB + r, g, b = col.red, col.green, col.blue + n = (r + g + b) / 3.0 + bwColorRGB = Color(n, n, n) + return bwColorRGB + +def HexColor(val): + """This function converts a hex string, or an actual integer number, + into the corresponding color. E.g., in "AABBCC" or 0xAABBCC, + AA is the red, BB is the green, and CC is the blue (00-FF). + + HTML uses a hex string with a preceding hash; if this is present, + it is stripped off. (AR, 3-3-2000) + + For completeness I assume that #aabbcc or 0xaabbcc are hex numbers + otherwise a pure integer is converted as decimal rgb + """ + + if type(val) == StringType: + b = 10 + if val[:1] == '#': + val = val[1:] + b = 16 + elif string.lower(val[:2]) == '0x': + b = 16 + val = val[2:] + val = string.atoi(val,b) + return Color(((val>>16)&0xFF)/255.0,((val>>8)&0xFF)/255.0,(val&0xFF)/255.0) + +def linearlyInterpolatedColor(c0, c1, x0, x1, x): + """ + Linearly interpolates colors. Can handle RGB, CMYK and PCMYK + colors - give ValueError if colours aren't the same. + Doesn't currently handle 'Spot Color Interpolation'. + """ + + if c0.__class__ != c1.__class__: + raise ValueError, "Color classes must be the same for interpolation!" + if x1x0 + if xx1+1e-8: # fudge factor for numerical problems + raise ValueError, "Can't interpolate: x=%f is not between %f and %f!" % (x,x0,x1) + if x<=x0: + return c0 + elif x>=x1: + return c1 + + cname = c0.__class__.__name__ + dx = float(x1-x0) + x = x-x0 + + if cname == 'Color': # RGB + r = c0.red+x*(c1.red - c0.red)/dx + g = c0.green+x*(c1.green- c0.green)/dx + b = c0.blue+x*(c1.blue - c0.blue)/dx + return Color(r,g,b) + elif cname == 'CMYKColor': + c = c0.cyan+x*(c1.cyan - c0.cyan)/dx + m = c0.magenta+x*(c1.magenta - c0.magenta)/dx + y = c0.yellow+x*(c1.yellow - c0.yellow)/dx + k = c0.black+x*(c1.black - c0.black)/dx + d = c0.density+x*(c1.density - c0.density)/dx + return CMYKColor(c,m,y,k, density=d) + elif cname == 'PCMYKColor': + if cmykDistance(c0,c1)<1e-8: + #colors same do density and preserve spotName if any + assert c0.spotName == c1.spotName, "Identical cmyk, but different spotName" + c = c0.cyan + m = c0.magenta + y = c0.yellow + k = c0.black + d = c0.density+x*(c1.density - c0.density)/dx + return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100, spotName=c0.spotName) + elif cmykDistance(c0,_CMYK_white)<1e-8: + #special c0 is white + c = c1.cyan + m = c1.magenta + y = c1.yellow + k = c1.black + d = x*c1.density/dx + return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100, spotName=c1.spotName) + elif cmykDistance(c1,_CMYK_white)<1e-8: + #special c1 is white + c = c0.cyan + m = c0.magenta + y = c0.yellow + k = c0.black + d = x*c0.density/dx + d = c0.density*(1-x/dx) + return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100, spotName=c0.spotName) + else: + c = c0.cyan+x*(c1.cyan - c0.cyan)/dx + m = c0.magenta+x*(c1.magenta - c0.magenta)/dx + y = c0.yellow+x*(c1.yellow - c0.yellow)/dx + k = c0.black+x*(c1.black - c0.black)/dx + d = c0.density+x*(c1.density - c0.density)/dx + return PCMYKColor(c*100,m*100,y*100,k*100, density=d*100) + else: + raise ValueError, "Can't interpolate: Unknown color class %s!" % cname + +# special case -- indicates no drawing should be done +# this is a hangover from PIDDLE - suggest we ditch it since it is not used anywhere +#transparent = Color(-1, -1, -1) + +_CMYK_white=CMYKColor(0,0,0,0) +_PCMYK_white=PCMYKColor(0,0,0,0) +_CMYK_black=CMYKColor(0,0,0,1) +_PCMYK_black=PCMYKColor(0,0,0,100) + +# Special colors +ReportLabBlueOLD = HexColor(0x4e5688) +ReportLabBlue = HexColor(0x00337f) +ReportLabBluePCMYK = PCMYKColor(100,65,0,30,spotName='Pantone 288U') +ReportLabLightBlue = HexColor(0xb7b9d3) +ReportLabFidBlue=HexColor(0x3366cc) +ReportLabFidRed=HexColor(0xcc0033) +ReportLabGreen = HexColor(0x336600) +ReportLabLightGreen = HexColor(0x339933) + +# color constants -- mostly from HTML standard +aliceblue = HexColor(0xF0F8FF) +antiquewhite = HexColor(0xFAEBD7) +aqua = HexColor(0x00FFFF) +aquamarine = HexColor(0x7FFFD4) +azure = HexColor(0xF0FFFF) +beige = HexColor(0xF5F5DC) +bisque = HexColor(0xFFE4C4) +black = HexColor(0x000000) +blanchedalmond = HexColor(0xFFEBCD) +blue = HexColor(0x0000FF) +blueviolet = HexColor(0x8A2BE2) +brown = HexColor(0xA52A2A) +burlywood = HexColor(0xDEB887) +cadetblue = HexColor(0x5F9EA0) +chartreuse = HexColor(0x7FFF00) +chocolate = HexColor(0xD2691E) +coral = HexColor(0xFF7F50) +cornflowerblue = cornflower = HexColor(0x6495ED) +cornsilk = HexColor(0xFFF8DC) +crimson = HexColor(0xDC143C) +cyan = HexColor(0x00FFFF) +darkblue = HexColor(0x00008B) +darkcyan = HexColor(0x008B8B) +darkgoldenrod = HexColor(0xB8860B) +darkgray = HexColor(0xA9A9A9) +darkgreen = HexColor(0x006400) +darkkhaki = HexColor(0xBDB76B) +darkmagenta = HexColor(0x8B008B) +darkolivegreen = HexColor(0x556B2F) +darkorange = HexColor(0xFF8C00) +darkorchid = HexColor(0x9932CC) +darkred = HexColor(0x8B0000) +darksalmon = HexColor(0xE9967A) +darkseagreen = HexColor(0x8FBC8B) +darkslateblue = HexColor(0x483D8B) +darkslategray = HexColor(0x2F4F4F) +darkturquoise = HexColor(0x00CED1) +darkviolet = HexColor(0x9400D3) +deeppink = HexColor(0xFF1493) +deepskyblue = HexColor(0x00BFFF) +dimgray = HexColor(0x696969) +dodgerblue = HexColor(0x1E90FF) +firebrick = HexColor(0xB22222) +floralwhite = HexColor(0xFFFAF0) +forestgreen = HexColor(0x228B22) +fuchsia = HexColor(0xFF00FF) +gainsboro = HexColor(0xDCDCDC) +ghostwhite = HexColor(0xF8F8FF) +gold = HexColor(0xFFD700) +goldenrod = HexColor(0xDAA520) +gray = HexColor(0x808080) +grey = gray +green = HexColor(0x008000) +greenyellow = HexColor(0xADFF2F) +honeydew = HexColor(0xF0FFF0) +hotpink = HexColor(0xFF69B4) +indianred = HexColor(0xCD5C5C) +indigo = HexColor(0x4B0082) +ivory = HexColor(0xFFFFF0) +khaki = HexColor(0xF0E68C) +lavender = HexColor(0xE6E6FA) +lavenderblush = HexColor(0xFFF0F5) +lawngreen = HexColor(0x7CFC00) +lemonchiffon = HexColor(0xFFFACD) +lightblue = HexColor(0xADD8E6) +lightcoral = HexColor(0xF08080) +lightcyan = HexColor(0xE0FFFF) +lightgoldenrodyellow = HexColor(0xFAFAD2) +lightgreen = HexColor(0x90EE90) +lightgrey = HexColor(0xD3D3D3) +lightpink = HexColor(0xFFB6C1) +lightsalmon = HexColor(0xFFA07A) +lightseagreen = HexColor(0x20B2AA) +lightskyblue = HexColor(0x87CEFA) +lightslategray = HexColor(0x778899) +lightsteelblue = HexColor(0xB0C4DE) +lightyellow = HexColor(0xFFFFE0) +lime = HexColor(0x00FF00) +limegreen = HexColor(0x32CD32) +linen = HexColor(0xFAF0E6) +magenta = HexColor(0xFF00FF) +maroon = HexColor(0x800000) +mediumaquamarine = HexColor(0x66CDAA) +mediumblue = HexColor(0x0000CD) +mediumorchid = HexColor(0xBA55D3) +mediumpurple = HexColor(0x9370DB) +mediumseagreen = HexColor(0x3CB371) +mediumslateblue = HexColor(0x7B68EE) +mediumspringgreen = HexColor(0x00FA9A) +mediumturquoise = HexColor(0x48D1CC) +mediumvioletred = HexColor(0xC71585) +midnightblue = HexColor(0x191970) +mintcream = HexColor(0xF5FFFA) +mistyrose = HexColor(0xFFE4E1) +moccasin = HexColor(0xFFE4B5) +navajowhite = HexColor(0xFFDEAD) +navy = HexColor(0x000080) +oldlace = HexColor(0xFDF5E6) +olive = HexColor(0x808000) +olivedrab = HexColor(0x6B8E23) +orange = HexColor(0xFFA500) +orangered = HexColor(0xFF4500) +orchid = HexColor(0xDA70D6) +palegoldenrod = HexColor(0xEEE8AA) +palegreen = HexColor(0x98FB98) +paleturquoise = HexColor(0xAFEEEE) +palevioletred = HexColor(0xDB7093) +papayawhip = HexColor(0xFFEFD5) +peachpuff = HexColor(0xFFDAB9) +peru = HexColor(0xCD853F) +pink = HexColor(0xFFC0CB) +plum = HexColor(0xDDA0DD) +powderblue = HexColor(0xB0E0E6) +purple = HexColor(0x800080) +red = HexColor(0xFF0000) +rosybrown = HexColor(0xBC8F8F) +royalblue = HexColor(0x4169E1) +saddlebrown = HexColor(0x8B4513) +salmon = HexColor(0xFA8072) +sandybrown = HexColor(0xF4A460) +seagreen = HexColor(0x2E8B57) +seashell = HexColor(0xFFF5EE) +sienna = HexColor(0xA0522D) +silver = HexColor(0xC0C0C0) +skyblue = HexColor(0x87CEEB) +slateblue = HexColor(0x6A5ACD) +slategray = HexColor(0x708090) +snow = HexColor(0xFFFAFA) +springgreen = HexColor(0x00FF7F) +steelblue = HexColor(0x4682B4) +tan = HexColor(0xD2B48C) +teal = HexColor(0x008080) +thistle = HexColor(0xD8BFD8) +tomato = HexColor(0xFF6347) +turquoise = HexColor(0x40E0D0) +violet = HexColor(0xEE82EE) +wheat = HexColor(0xF5DEB3) +white = HexColor(0xFFFFFF) +whitesmoke = HexColor(0xF5F5F5) +yellow = HexColor(0xFFFF00) +yellowgreen = HexColor(0x9ACD32) +fidblue=HexColor(0x3366cc) +fidred=HexColor(0xcc0033) +fidlightblue=HexColor("#d6e0f5") + +ColorType=type(black) + + ################################################################ + # + # Helper functions for dealing with colors. These tell you + # which are predefined, so you can print color charts; + # and can give the nearest match to an arbitrary color object + # + ################################################################# + +def colorDistance(col1, col2): + """Returns a number between 0 and root(3) stating how similar + two colours are - distance in r,g,b, space. Only used to find + names for things.""" + return math.sqrt( + (col1.red - col2.red)**2 + + (col1.green - col2.green)**2 + + (col1.blue - col2.blue)**2 + ) + +def cmykDistance(col1, col2): + """Returns a number between 0 and root(4) stating how similar + two colours are - distance in r,g,b, space. Only used to find + names for things.""" + return math.sqrt( + (col1.cyan - col2.cyan)**2 + + (col1.magenta - col2.magenta)**2 + + (col1.yellow - col2.yellow)**2 + + (col1.black - col2.black)**2 + ) + +_namedColors = None + +def getAllNamedColors(): + #returns a dictionary of all the named ones in the module + # uses a singleton for efficiency + global _namedColors + if _namedColors is not None: return _namedColors + import colors + _namedColors = {} + for (name, value) in colors.__dict__.items(): + if isinstance(value, Color): + _namedColors[name] = value + + return _namedColors + +def describe(aColor,mode=0): + '''finds nearest colour match to aColor. + mode=0 print a string desription + mode=1 return a string description + mode=2 return (distance, colorName) + ''' + namedColors = getAllNamedColors() + closest = (10, None, None) #big number, name, color + for (name, color) in namedColors.items(): + distance = colorDistance(aColor, color) + if distance < closest[0]: + closest = (distance, name, color) + if mode<=1: + s = 'best match is %s, distance %0.4f' % (closest[1], closest[0]) + if mode==0: print s + else: return s + elif mode==2: + return (closest[1], closest[0]) + else: + raise ValueError, "Illegal value for mode "+str(mode) + +def toColor(arg,default=None): + '''try to map an arbitrary arg to a color instance''' + if isinstance(arg,Color): return arg + tArg = type(arg) + if tArg in _SeqTypes: + assert 3<=len(arg)<=4, 'Can only convert 3 and 4 sequences to color' + assert 0<=min(arg) and max(arg)<=1 + return len(arg)==3 and Color(arg[0],arg[1],arg[2]) or CMYKColor(arg[0],arg[1],arg[2],arg[3]) + elif tArg == StringType: + C = getAllNamedColors() + s = string.lower(arg) + if C.has_key(s): return C[s] + try: + return toColor(eval(arg)) + except: + pass + + try: + return HexColor(arg) + except: + if default is None: + raise 'Invalid color value', str(arg) + return default + +def toColorOrNone(arg,default=None): + '''as above but allows None as a legal value''' + if arg is None: + return None + else: + return toColor(arg, default) + +def setColors(**kw): + UNDEF = [] + progress = 1 + assigned = {} + while kw and progress: + progress = 0 + for k, v in kw.items(): + if type(v) in (type(()),type([])): + c = map(lambda x,UNDEF=UNDEF: toColor(x,UNDEF),v) + if type(v) is type(()): c = tuple(c) + ok = UNDEF not in c + else: + c = toColor(v,UNDEF) + ok = c is not UNDEF + if ok: + assigned[k] = c + del kw[k] + progress = 1 + + if kw: raise ValueError("Can't convert\n%s" % str(kw)) + getAllNamedColors() + for k, c in assigned.items(): + globals()[k] = c + if isinstance(c,Color): _namedColors[k] = c + +def Whiter(c,f): + '''given a color combine with white as c*f w*(1-f) 0<=f<=1''' + c = toColor(c) + if isinstance(c,PCMYKColor): + w = _PCMYK_white + elif isinstance(c,CMYKColor): w = _CMYK_white + else: w = white + return linearlyInterpolatedColor(w, c, 0, 1, f) + +def Blacker(c,f): + '''given a color combine with black as c*f+b*(1-f) 0<=f<=1''' + c = toColor(c) + if isinstance(c,PCMYKColor): + b = _PCMYK_black + elif isinstance(c,CMYKColor): b = _CMYK_black + else: b = black + return linearlyInterpolatedColor(b, c, 0, 1, f) diff --git a/bin/reportlab/lib/corp.py b/bin/reportlab/lib/corp.py new file mode 100644 index 00000000000..0904c4d3a9c --- /dev/null +++ b/bin/reportlab/lib/corp.py @@ -0,0 +1,452 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/corp.py +""" This module includes some reusable routines for ReportLab's + 'Corporate Image' - the logo, standard page backdrops and + so on - you are advised to do the same for your own company!""" +__version__=''' $Id: corp.py 2484 2004-12-10 07:27:50Z andy $ ''' + +from reportlab.lib.units import inch,cm +from reportlab.lib.validators import * +from reportlab.lib.attrmap import * +from reportlab.graphics.shapes import definePath, Group, Drawing, Rect, PolyLine, String +from reportlab.graphics.widgetbase import Widget +from reportlab.lib.colors import Color, black, white, ReportLabBlue +from reportlab.pdfbase.pdfmetrics import stringWidth +from math import sin, pi + +class RL_CorpLogo(Widget): + '''Dinu's fat letter logo as hacked into decent paths by Robin''' + _attrMap = AttrMap( + x = AttrMapValue(isNumber,'Logo x-coord'), + y = AttrMapValue(isNumber,'Logo y-coord'), + angle = AttrMapValue(isNumber,'Logo rotation'), + strokeColor = AttrMapValue(isColorOrNone, 'Logo lettering stroke color'), + fillColor = AttrMapValue(isColorOrNone, 'Logo lettering fill color'), + strokeWidth = AttrMapValue(isNumber,'Logo lettering stroke width'), + background = AttrMapValue(isColorOrNone,desc="Logo background color"), + border = AttrMapValue(isColorOrNone,desc="Logo border color"), + borderWidth = AttrMapValue(isNumber,desc="Logo border width (1)"), + shadow = AttrMapValue(isNumberOrNone,desc="None or fraction of background for shadowing" ), + width = AttrMapValue(isNumber, desc="width in points of the logo (default 129)"), + height = AttrMapValue(isNumber, desc="height in points of the logo (default 86)"), + skewX = AttrMapValue(isNumber, desc="x-skew of the logo (default 10)"), + skewY = AttrMapValue(isNumber, desc="y-skew of the logo (default 0)"), + showPage = AttrMapValue(isBoolean, desc="If true show the page lines"), + xFlip = AttrMapValue(isBoolean, desc="If true do x reversal"), + yFlip = AttrMapValue(isBoolean, desc="If true do y reversal"), + ) + + def __init__(self): + self.fillColor = white + self.strokeColor = None + self.strokeWidth = 0.1 + self.background = ReportLabBlue + self.border = None + self.borderWidth = 1 + self.shadow = 0.5 + self.height = 86 + self.width = 130 + self.x = self.y = self.angle = self.skewY = self._dx = 0 + self.skewX = 10 + self._dy = 35.5 + self.showPage = 1 + + def demo(self): + D = Drawing(self.width, self.height) + D.add(self) + return D + + def _paintLogo(self, g, dx=0, dy=0, strokeColor=None, strokeWidth=0.1, fillColor=white): + P = [ + ('moveTo' ,15.7246,0 ), ('lineTo' ,9.49521,0 ), ('lineTo' ,6.64988,6.83711 ), ('curveTo' ,6.62224,6.95315 ,6.57391,7.10646 ,6.50485,7.29708 ), ('curveTo' ,6.43578,7.48767 ,6.35059,7.71559 ,6.24931,7.98079 ), ('lineTo' ,6.29074,6.71282 ), ('lineTo' ,6.29074,0 ), ('lineTo' ,0.55862,0 ), ('lineTo' ,0.55862,19.19365 ), ('lineTo' ,6.45649,19.19365 ), ('curveTo' ,9.05324,19.19365 ,10.99617,18.73371 ,12.28532,17.8138 ), ('curveTo' ,13.92439,16.63697 ,14.7439,14.96293 ,14.7439,12.79161 ), ('curveTo' ,14.7439,10.47114 ,13.64354,8.86755 ,11.44276,7.98079 ), 'closePath', ('moveTo' ,6.31838,10.30542 ), ('lineTo' ,6.70513,10.30542 ), ('curveTo' ,7.36812,10.30542 ,7.92062,10.53331 ,8.36261,10.98912 ), ('curveTo' ,8.80461,11.44491 ,9.0256,12.02504 ,9.0256,12.72947 ), ('curveTo' ,9.0256,14.16321 ,8.19227,14.88004 ,6.52556,14.88004 ), ('lineTo' ,6.31838,14.88004 ), 'closePath', + ('moveTo' ,25.06173,4.54978 ), ('lineTo' ,30.47611,4.45033 ), ('curveTo' ,30.08951,2.88402 ,29.33668,1.70513 ,28.21787,0.91369 ), ('curveTo' ,27.09906,0.12223 ,25.63726,-0.27348 ,23.83245,-0.27348 ), ('curveTo' ,21.69611,-0.27348 ,20.02024,0.32322 ,18.80475,1.5166 ), ('curveTo' ,17.59846,2.72658 ,16.99531,4.37988 ,16.99531,6.47662 ), ('curveTo' ,16.99531,8.6065 ,17.64451,10.34269 ,18.94286,11.68527 ), ('curveTo' ,20.24124,13.03612 ,21.91711,13.71152 ,23.97056,13.71152 ), ('curveTo' ,26.01482,13.71152 ,27.64466,13.06096 ,28.86015,11.75985 ), ('curveTo' ,30.07566,10.45042 ,30.68326,8.71423 ,30.68326,6.5512 ), ('lineTo' ,30.65586,5.66859 ), ('lineTo' ,22.53407,5.66859 ), ('curveTo' ,22.59855,4.29287 ,23.03132,3.60503 ,23.83245,3.60503 ), ('curveTo' ,24.45861,3.60503 ,24.86837,3.91994 ,25.06173,4.54978 ), 'closePath', ('moveTo' ,25.18604,8.35371 ), ('curveTo' ,25.18604,8.60235 ,25.15384,8.83024 ,25.08937,9.03742 ), ('curveTo' ,25.02489,9.24463 ,24.93514,9.42278 ,24.82001,9.57197 ), ('curveTo' ,24.70492,9.72113 ,24.56911,9.83923 ,24.41255,9.92624 ), ('curveTo' ,24.25603,10.01326 ,24.08568,10.05678 ,23.90152,10.05678 ), ('curveTo' ,23.51474,10.05678 ,23.20169,9.89725 ,22.96225,9.57819 ), ('curveTo' ,22.72283,9.25913 ,22.60314,8.85096 ,22.60314,8.35371 ), 'closePath', + ('moveTo' ,38.36308,-5.99181 ), ('lineTo' ,32.82428,-5.99181 ), ('lineTo' ,32.82428,13.43804 ), ('lineTo' ,38.36308,13.43804 ), ('lineTo' ,38.23873,11.53608 ), ('curveTo' ,38.46886,11.93387 ,38.70371,12.27159 ,38.94327,12.54922 ), ('curveTo' ,39.18254,12.82685 ,39.44037,13.05268 ,39.71676,13.22671 ), ('curveTo' ,39.99286,13.40074 ,40.28988,13.52712 ,40.60753,13.60585 ), ('curveTo' ,40.92518,13.68459 ,41.27759,13.72396 ,41.66419,13.72396 ), ('curveTo' ,43.10068,13.72396 ,44.2702,13.07755 ,45.17246,11.78472 ), ('curveTo' ,46.06588,10.50844 ,46.51229,8.81368 ,46.51229,6.70038 ), ('curveTo' ,46.51229,4.55394 ,46.08415,2.85502 ,45.22785,1.60362 ), ('curveTo' ,44.38983,0.35221 ,43.23416,-0.27348 ,41.76084,-0.27348 ), ('curveTo' ,40.41659,-0.27348 ,39.24235,0.42679 ,38.23873,1.82739 ), ('curveTo' ,38.2847,1.40472 ,38.31239,1.04007 ,38.32153,0.73345 ), ('curveTo' ,38.34923,0.41851 ,38.36308,0.04146 ,38.36308,-0.3978 ), 'closePath', ('moveTo' ,40.7802,6.84954 ), ('curveTo' ,40.7802,7.72802 ,40.66734,8.40964 ,40.44193,8.89448 ), ('curveTo' ,40.21621,9.37929 ,39.89621,9.62168 ,39.48191,9.62168 ), ('curveTo' ,38.62533,9.62168 ,38.19718,8.68108 ,38.19718,6.79983 ), ('curveTo' ,38.19718,4.87712 ,38.61177,3.91581 ,39.44037,3.91581 ), ('curveTo' ,39.85466,3.91581 ,40.18174,4.1727 ,40.42101,4.68654 ), ('curveTo' ,40.66057,5.20037 ,40.7802,5.92135 ,40.7802,6.84954 ), 'closePath', + ('moveTo' ,62.10648,6.51392 ), ('curveTo' ,62.10648,4.44205 ,61.47118,2.79288 ,60.2003,1.56631 ), ('curveTo' ,58.92971,0.33978 ,57.22626,-0.27348 ,55.08965,-0.27348 ), ('curveTo' ,52.99018,-0.27348 ,51.31914,0.35221 ,50.07595,1.60362 ), ('curveTo' ,48.8419,2.8633 ,48.22517,4.55394 ,48.22517,6.67551 ), ('curveTo' ,48.22517,8.79709 ,48.85575,10.50016 ,50.1175,11.78472 ), ('curveTo' ,51.36982,13.07755 ,53.03172,13.72396 ,55.1035,13.72396 ), ('curveTo' ,57.28608,13.72396 ,58.99866,13.08168 ,60.24185,11.79712 ), ('curveTo' ,61.48503,10.51259 ,62.10648,8.75154 ,62.10648,6.51392 ), 'closePath', ('moveTo' ,56.73358,6.67551 ), ('curveTo' ,56.73358,7.17276 ,56.69675,7.62236 ,56.62308,8.02428 ), ('curveTo' ,56.54942,8.42623 ,56.44334,8.77016 ,56.30544,9.05607 ), ('curveTo' ,56.16724,9.34198 ,56.00134,9.56369 ,55.80804,9.72113 ), ('curveTo' ,55.61474,9.8786 ,55.39817,9.95733 ,55.1589,9.95733 ), ('curveTo' ,54.68921,9.95733 ,54.31174,9.65898 ,54.02621,9.06229 ), ('curveTo' ,53.74068,8.54018 ,53.59807,7.75702 ,53.59807,6.71282 ), ('curveTo' ,53.59807,5.68515 ,53.74068,4.90202 ,54.02621,4.36332 ), ('curveTo' ,54.31174,3.76663 ,54.69392,3.46828 ,55.17275,3.46828 ), ('curveTo' ,55.62388,3.46828 ,55.99692,3.7625 ,56.29159,4.35088 ), ('curveTo' ,56.58625,5.0056 ,56.73358,5.78047 ,56.73358,6.67551 ), 'closePath', + ('moveTo' ,69.78629,0 ), ('lineTo' ,64.2475,0 ), ('lineTo' ,64.2475,13.43804 ), ('lineTo' ,69.78629,13.43804 ), ('lineTo' ,69.49605,10.81507 ), ('curveTo' ,70.33407,12.77921 ,71.71988,13.76126 ,73.65346,13.76126 ), ('lineTo' ,73.65346,8.16725 ), ('curveTo' ,73.04586,8.4656 ,72.5302,8.61478 ,72.10647,8.61478 ), ('curveTo' ,71.36068,8.61478 ,70.78756,8.37236 ,70.38711,7.88755 ), ('curveTo' ,69.98637,7.40274 ,69.78629,6.69623 ,69.78629,5.76804 ), 'closePath', + ('moveTo' ,81.55427,0 ), ('lineTo' ,76.00163,0 ), ('lineTo' ,76.00163,9.42278 ), ('lineTo' ,74.42725,9.42278 ), ('lineTo' ,74.42725,13.43804 ), ('lineTo' ,76.00163,13.43804 ), ('lineTo' ,76.00163,17.39113 ), ('lineTo' ,81.55427,17.39113 ), ('lineTo' ,81.55427,13.43804 ), ('lineTo' ,83.39121,13.43804 ), ('lineTo' ,83.39121,9.42278 ), ('lineTo' ,81.55427,9.42278 ), 'closePath', + ('moveTo' ,95.17333,0 ), ('lineTo' ,85.09024,0 ), ('lineTo' ,85.09024,19.19365 ), ('lineTo' ,90.85002,19.19365 ), ('lineTo' ,90.85002,4.61196 ), ('lineTo' ,95.17333,4.61196 ), 'closePath', + ('moveTo' ,110.00787,0 ), ('lineTo' ,104.45523,0 ), ('curveTo' ,104.5012,0.44754 ,104.53803,0.87433 ,104.56573,1.2804 ), ('curveTo' ,104.59313,1.68651 ,104.62083,2.01385 ,104.64853,2.26246 ), ('curveTo' ,103.69087,0.57182 ,102.40644,-0.27348 ,100.79492,-0.27348 ), ('curveTo' ,99.39527,-0.27348 ,98.28557,0.35637 ,97.46611,1.61605 ), ('curveTo' ,96.65578,2.86746 ,96.25062,4.59952 ,96.25062,6.81227 ), ('curveTo' ,96.25062,8.95041 ,96.66963,10.63276 ,97.50765,11.8593 ), ('curveTo' ,98.34538,13.10242 ,99.4872,13.72396 ,100.93312,13.72396 ), ('curveTo' ,102.41557,13.72396 ,103.61249,12.92008 ,104.52418,11.31231 ), ('curveTo' ,104.50591,11.47806 ,104.49206,11.62309 ,104.48293,11.74741 ), ('curveTo' ,104.4735,11.87173 ,104.46437,11.9753 ,104.45523,12.05819 ), ('lineTo' ,104.39983,12.84135 ), ('lineTo' ,104.35858,13.43804 ), ('lineTo' ,110.00787,13.43804 ), 'closePath', ('moveTo' ,104.39983,6.88685 ), ('curveTo' ,104.39983,7.38409 ,104.37921,7.80676 ,104.33766,8.15481 ), ('curveTo' ,104.29641,8.5029 ,104.22952,8.78672 ,104.13758,9.00636 ), ('curveTo' ,104.04535,9.22598 ,103.92572,9.38341 ,103.77839,9.47874 ), ('curveTo' ,103.63106,9.57403 ,103.45161,9.62168 ,103.23974,9.62168 ), ('curveTo' ,102.30036,9.62168 ,101.83096,8.49875 ,101.83096,6.25285 ), ('curveTo' ,101.83096,4.64508 ,102.24967,3.8412 ,103.0877,3.8412 ), ('curveTo' ,103.96255,3.8412 ,104.39983,4.85641 ,104.39983,6.88685 ), 'closePath', + ('moveTo' ,118.22604,0 ), ('lineTo' ,112.5629,0 ), ('lineTo' ,112.5629,20.99616 ), ('lineTo' ,118.10169,20.99616 ), ('lineTo' ,118.10169,13.63694 ), ('curveTo' ,118.10169,13.01538 ,118.07399,12.30268 ,118.01889,11.49877 ), ('curveTo' ,118.52542,12.31096 ,119.03636,12.88693 ,119.55202,13.22671 ), ('curveTo' ,120.08625,13.55821 ,120.75838,13.72396 ,121.5687,13.72396 ), ('curveTo' ,123.07885,13.72396 ,124.24837,13.09827 ,125.07697,11.84686 ), ('curveTo' ,125.90586,10.60373 ,126.32015,8.85099 ,126.32015,6.5885 ), ('curveTo' ,126.32015,4.42546 ,125.89201,2.74314 ,125.03571,1.54147 ), ('curveTo' ,124.18826,0.3315 ,123.01432,-0.27348 ,121.51331,-0.27348 ), ('curveTo' ,120.78608,-0.27348 ,120.16905,-0.12432 ,119.66252,0.17403 ), ('curveTo' ,119.41383,0.3315 ,119.15835,0.54283 ,118.8961,0.80803 ), ('curveTo' ,118.63356,1.07322 ,118.36866,1.40472 ,118.10169,1.80252 ), ('curveTo' ,118.11112,1.64505 ,118.12025,1.51039 ,118.12939,1.3985 ), ('curveTo' ,118.13852,1.28662 ,118.14766,1.19339 ,118.15709,1.11881 ), 'closePath', ('moveTo' ,120.58806,6.70038 ), ('curveTo' ,120.58806,8.62306 ,120.11837,9.5844 ,119.17898,9.5844 ), ('curveTo' ,118.35039,9.5844 ,117.93609,8.67693 ,117.93609,6.86198 ), ('curveTo' ,117.93609,4.96417 ,118.36424,4.01526 ,119.22053,4.01526 ), ('curveTo' ,120.13222,4.01526 ,120.58806,4.91027 ,120.58806,6.70038 ), 'closePath', + ] + (self.showPage and [ + ('moveTo',38.30626,-7.28346),('lineTo',38.30626,-25.55261),('lineTo',85.15777,-25.55261),('lineTo',85.15777,-1.39019),('lineTo',90.46172,-1.39019),('lineTo',90.46172,-31.15121),('lineTo',32.70766,-31.15121),('lineTo',32.70766,-7.28346), 'closePath', + ('moveTo' ,32.70766,14.52164 ), ('lineTo' ,32.70766,47.81862 ), ('lineTo' ,80.14849,47.81862 ), ('lineTo' ,90.46172,37.21073 ), ('lineTo' ,90.46172,20.12025 ), ('lineTo' ,85.15777,20.12025 ), ('lineTo' ,85.15777,30.72814 ), ('lineTo' ,73.66589,30.72814 ), ('lineTo' ,73.66589,42.22002 ), ('lineTo' ,38.30626,42.22002 ), ('lineTo' ,38.30626,14.52164 ), 'closePath', ('moveTo' ,79.2645,36.32674 ), ('lineTo' ,85.15777,36.32674 ), ('lineTo' ,79.2645,42.22002 ), 'closePath', + ] or []) + g.add(definePath(P,strokeColor=strokeColor,strokeWidth=strokeWidth,fillColor=fillColor, dx=dx, dy=dy)) + + def draw(self): + fillColor = self.fillColor + strokeColor = self.strokeColor + g = Group() + bg = self.background + bd = self.border + bdw = self.borderWidth + shadow = self.shadow + x, y = self.x, self.y + if bg: + if shadow is not None and 0<=shadow<1: + shadow = Color(bg.red*shadow,bg.green*shadow,bg.blue*shadow) + self._paintLogo(g,dy=-2.5, dx=2,fillColor=shadow) + self._paintLogo(g,fillColor=fillColor,strokeColor=strokeColor) + g.skew(kx=self.skewX, ky=self.skewY) + g.shift(self._dx,self._dy) + G = Group() + G.add(g) + _w, _h = 130, 86 + w, h = self.width, self.height + if bg or (bd and bdw): + G.insert(0,Rect(0,0,_w,_h,fillColor=bg,strokeColor=bd,strokeWidth=bdw)) + if w!=_w or h!=_h: G.scale(w/float(_w),h/float(_h)) + + angle = self.angle + if self.angle: + w, h = w/2., h/2. + G.shift(-w,-h) + G.rotate(angle) + G.shift(w,h) + xFlip = getattr(self,'xFlip',0) and -1 or 0 + yFlip = getattr(self,'yFlip',0) and -1 or 0 + if xFlip or yFlip: + sx = xFlip or 1 + sy = yFlip or 1 + G.shift(sx*x+w*xFlip,sy*y+yFlip*h) + G = Group(G,transform=(sx,0,0,sy,0,0)) + else: + G.shift(x,y) + return G + +class RL_CorpLogoReversed(RL_CorpLogo): + def __init__(self): + RL_CorpLogo.__init__(self) + self.background = white + self.fillColor = ReportLabBlue + +class RL_CorpLogoThin(Widget): + """The ReportLab Logo. + + New version created by John Precedo on 7-8 August 2001. + Based on bitmapped imaged from E-Id. + Improved by Robin Becker.""" + + _attrMap = AttrMap( + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + height = AttrMapValue(isNumberOrNone), + width = AttrMapValue(isNumberOrNone), + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue( isColorOrNone) + ) + + _h = 90.5 + _w = 136.5 + _text='R e p o r t L a b' + _fontName = 'Helvetica-Bold' + _fontSize = 16 + + def __init__(self): + self.fillColor = ReportLabBlue + self.strokeColor = white + self.x = 0 + self.y = 0 + self.height = self._h + self.width = self._w + + def demo(self): + D = Drawing(self.width, self.height) + D.add(self) + return D + + def _getText(self, x=0, y=0, color=None): + return String(x,y, self._text, fontName=self._fontName, fontSize=self._fontSize, fillColor=color) + + def _sw(self,f=None,l=None): + text = self._text + if f is None: f = 0 + if l is None: l = len(text) + return stringWidth(text[f:l],self._fontName,self._fontSize) + + def _addPage(self, g, strokeWidth=3, color=None, dx=0, dy=0): + x1, x2 = 31.85+dx, 80.97+dx + fL = 10 # fold length + y1, y2 = dy-34, dy+50.5 + L = [[x1,dy-4,x1,y1, x2, y1, x2, dy-1], + [x1,dy+11,x1,y2,x2-fL,y2,x2,y2-fL,x2,dy+14], + [x2-10,y2,x2-10,y2-fL,x2,y2-fL]] + + for l in L: + g.add(PolyLine(l, strokeWidth=strokeWidth, strokeColor=color, strokeLineJoin=0)) + + def draw(self): + sx = 0.5 + fillColor = self.fillColor + strokeColor = self.strokeColor + shadow = Color(fillColor.red*sx,fillColor.green*sx,fillColor.blue*sx) + g = Group() + g2= Group() + g.add(Rect(fillColor=fillColor, strokeColor=fillColor, x=0, y=0, width=self._w, height=self._h)) + sx = (self._w-2)/self._sw() + g2.scale(sx,1) + self._addPage(g2,strokeWidth=3,dx=2,dy=-2.5,color=shadow) + self._addPage(g2,strokeWidth=3,color=strokeColor) + g2.scale(1/sx,1) + g2.add(self._getText(x=1,y=0,color=shadow)) + g2.add(self._getText(x=0,y=1,color=strokeColor)) + g2.scale(sx,1) + g2.skew(kx=10, ky=0) + g2.shift(0,38) + g.add(g2) + g.scale(self.width/self._w,self.height/self._h) + g.shift(self.x,self.y) + return g + +class ReportLabLogo: + """vector reportlab logo centered in a 250x by 150y rectangle""" + + def __init__(self, atx=0, aty=0, width=2.5*inch, height=1.5*inch, powered_by=0): + self.origin = (atx, aty) + self.dimensions = (width, height) + self.powered_by = powered_by + + def draw(self, canvas): + from reportlab.graphics import renderPDF + canvas.saveState() + (atx,aty) = self.origin + (width, height) = self.dimensions + logo = RL_CorpLogo() + logo.width, logo.height = width, height + renderPDF.draw(logo.demo(),canvas,atx,aty,0) + canvas.restoreState() + +class RL_BusinessCard(Widget): + """Widget that creates a single business card. + Uses RL_CorpLogo for the logo. + + For a black border around your card, set self.border to 1. + To change the details on the card, over-ride the following properties: + self.name, self.position, self.telephone, self.mobile, self.fax, self.email, self.web + The office locations are set in self.rh_blurb_top ("London office" etc), and + self.rh_blurb_bottom ("New York office" etc). + """ + # for items where it 'isString' the string can be an empty one... + _attrMap = AttrMap( + fillColor = AttrMapValue(isColorOrNone), + strokeColor = AttrMapValue(isColorOrNone), + altStrokeColor = AttrMapValue(isColorOrNone), + x = AttrMapValue(isNumber), + y = AttrMapValue(isNumber), + height = AttrMapValue(isNumber), + width = AttrMapValue(isNumber), + borderWidth = AttrMapValue(isNumber), + bleed=AttrMapValue(isNumberOrNone), + cropMarks=AttrMapValue(isBoolean), + border=AttrMapValue(isBoolean), + name=AttrMapValue(isString), + position=AttrMapValue(isString), + telephone=AttrMapValue(isString), + mobile=AttrMapValue(isString), + fax=AttrMapValue(isString), + email=AttrMapValue(isString), + web=AttrMapValue(isString), + rh_blurb_top=AttrMapValue(isListOfStringsOrNone), + rh_blurb_bottom=AttrMapValue(isListOfStringsOrNone) + ) + + _h = 5.35*cm + _w = 8.5*cm + _fontName = 'Helvetica-Bold' + _strapline = "strategic reporting solutions for e-business" + + + def __init__(self): + self.fillColor = ReportLabBlue + self.strokeColor = black + self.altStrokeColor = white + self.x = 0 + self.y = 0 + self.height = self._h + self.width = self._w + self.borderWidth = self.width/6.15 + self.bleed=0.2*cm + self.cropMarks=1 + self.border=0 + #Over-ride these with your own info + self.name="Joe Cool" + self.position="Freelance Demonstrator" + self.telephone="020 8545 7271" + self.mobile="-" + self.fax="020 8544 1311" + self.email="info@reportlab.com" + self.web="www.reportlab.com" + self.rh_blurb_top = ["London office:", + "ReportLab Europe Ltd", + "Lombard Business Park", + "8 Lombard Road", + "Wimbledon", + "London SW19 3TZ", + "United Kingdom"] + self.rh_blurb_bottom = ["New York office:", + "ReportLab Inc", + "219 Harper Street", + "Highland Park", + "New Jersey 08904", + "USA"] + + def demo(self): + D = Drawing(self.width, self.height) + D.add(self) + return D + + def draw(self): + fillColor = self.fillColor + strokeColor = self.strokeColor + + g = Group() + g.add(Rect(x = 0, y = 0, + fillColor = self.fillColor, + strokeColor = self.fillColor, + width = self.borderWidth, + height = self.height)) + g.add(Rect(x = 0, y = self.height-self.borderWidth, + fillColor = self.fillColor, + strokeColor = self.fillColor, + width = self.width, + height = self.borderWidth)) + + g2 = Group() + rl=RL_CorpLogo() + rl.height = 1.25*cm + rl.width = 1.9*cm + rl.draw() + g2.add(rl) + g.add(g2) + g2.shift(x=(self.width-(rl.width+(self.width/42))), + y=(self.height - (rl.height+(self.height/42)))) + + g.add(String(x = self.borderWidth/5.0, + y = ((self.height - (rl.height+(self.height/42)))+((38/90.5)*rl.height)), + fontSize = 6, + fillColor = self.altStrokeColor, + fontName = "Helvetica-BoldOblique", + textAnchor = 'start', + text = self._strapline)) + + leftText=["Tel:", "Mobile:", "Fax:", "Email:", "Web:"] + leftDetails=[self.telephone,self.mobile,self.fax,self.email,self.web] + leftText.reverse() + leftDetails.reverse() + for f in range(len(leftText),0,-1): + g.add(String(x = self.borderWidth+(self.borderWidth/5.0), + y = (self.borderWidth/5.0)+((f-1)*(5*1.2)), + fontSize = 5, + fillColor = self.strokeColor, + fontName = "Helvetica", + textAnchor = 'start', + text = leftText[f-1])) + g.add(String(x = self.borderWidth+(self.borderWidth/5.0)+self.borderWidth, + y = (self.borderWidth/5.0)+((f-1)*(5*1.2)), + fontSize = 5, + fillColor = self.strokeColor, + fontName = "Helvetica", + textAnchor = 'start', + text = leftDetails[f-1])) + + rightText=self.rh_blurb_bottom + rightText.reverse() + for f in range(len(rightText),0,-1): + g.add(String(x = self.width-((self.borderWidth/5.0)), + y = (self.borderWidth/5.0)+((f-1)*(5*1.2)), + fontSize = 5, + fillColor = self.strokeColor, + fontName = "Helvetica", + textAnchor = 'end', + text = rightText[f-1])) + + ty = (self.height-self.borderWidth-(self.borderWidth/5.0)+2) +# g.add(Line(self.borderWidth, ty, self.borderWidth+(self.borderWidth/5.0), ty)) +# g.add(Line(self.borderWidth+(self.borderWidth/5.0), ty, self.borderWidth+(self.borderWidth/5.0), +# ty+(self.borderWidth/5.0))) +# g.add(Line(self.borderWidth, ty-10, +# self.borderWidth+(self.borderWidth/5.0), ty-10)) + + rightText=self.rh_blurb_top + for f in range(1,(len(rightText)+1)): + g.add(String(x = self.width-(self.borderWidth/5.0), + y = ty-((f)*(5*1.2)), + fontSize = 5, + fillColor = self.strokeColor, + fontName = "Helvetica", + textAnchor = 'end', + text = rightText[f-1])) + + g.add(String(x = self.borderWidth+(self.borderWidth/5.0), + y = ty-10, + fontSize = 10, + fillColor = self.strokeColor, + fontName = "Helvetica", + textAnchor = 'start', + text = self.name)) + + ty1 = ty-10*1.2 + + g.add(String(x = self.borderWidth+(self.borderWidth/5.0), + y = ty1-8, + fontSize = 8, + fillColor = self.strokeColor, + fontName = "Helvetica", + textAnchor = 'start', + text = self.position)) + if self.border: + g.add(Rect(x = 0, y = 0, + fillColor=None, + strokeColor = black, + width = self.width, + height = self.height)) + g.shift(self.x,self.y) + return g + + +def test(): + """This function produces a pdf with examples. """ + + #white on blue + rl = RL_CorpLogo() + rl.width = 129 + rl.height = 86 + D = Drawing(rl.width,rl.height) + D.add(rl) + D.__dict__['verbose'] = 1 + D.save(fnRoot='corplogo_whiteonblue',formats=['pdf','eps','jpg','gif']) + + + #blue on white + rl = RL_CorpLogoReversed() + rl.width = 129 + rl.height = 86 + D = Drawing(rl.width,rl.height) + D.add(rl) + D.__dict__['verbose'] = 1 + D.save(fnRoot='corplogo_blueonwhite',formats=['pdf','eps','jpg','gif']) + + #gray on white + rl = RL_CorpLogoReversed() + rl.fillColor = Color(0.2, 0.2, 0.2) + rl.width = 129 + rl.height = 86 + D = Drawing(rl.width,rl.height) + D.add(rl) + D.__dict__['verbose'] = 1 + D.save(fnRoot='corplogo_grayonwhite',formats=['pdf','eps','jpg','gif']) + + + rl = RL_BusinessCard() + rl.x=25 + rl.y=25 + rl.border=1 + D = Drawing(rl.width+50,rl.height+50) + D.add(rl) + D.__dict__['verbose'] = 1 + D.save(fnRoot='RL_BusinessCard',formats=['pdf']) + +if __name__=='__main__': + test() diff --git a/bin/reportlab/lib/enums.py b/bin/reportlab/lib/enums.py new file mode 100644 index 00000000000..f28897a929c --- /dev/null +++ b/bin/reportlab/lib/enums.py @@ -0,0 +1,11 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/enums.py +__version__=''' $Id: enums.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__=""" +holder for all reportlab's enumerated types +""" +TA_LEFT = 0 +TA_CENTER = 1 +TA_RIGHT = 2 +TA_JUSTIFY = 4 \ No newline at end of file diff --git a/bin/reportlab/lib/extformat.py b/bin/reportlab/lib/extformat.py new file mode 100644 index 00000000000..f3450596081 --- /dev/null +++ b/bin/reportlab/lib/extformat.py @@ -0,0 +1,81 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/extformat.py +from tokenize import tokenprog +import sys + +def _matchorfail(text, pos): + match = tokenprog.match(text, pos) + if match is None: raise ValueError(text, pos) + return match, match.end() + +''' + Extended dictionary formatting + We allow expressions in the parentheses instead of + just a simple variable. +''' +def dictformat(_format, L={}, G={}): + format = _format + + S = {} + chunks = [] + pos = 0 + n = 0 + + while 1: + pc = format.find("%", pos) + if pc < 0: break + nextchar = format[pc+1] + + if nextchar == "(": + chunks.append(format[pos:pc]) + pos, level = pc+2, 1 + while level: + match, pos = _matchorfail(format, pos) + tstart, tend = match.regs[3] + token = format[tstart:tend] + if token == "(": level = level+1 + elif token == ")": level = level-1 + vname = '__superformat_%d' % n + n += 1 + S[vname] = eval(format[pc+2:pos-1],L,G) + chunks.append('%%(%s)' % vname) + else: + nc = pc+1+(nextchar=="%") + chunks.append(format[pos:nc]) + pos = nc + + if pos < len(format): chunks.append(format[pos:]) + return (''.join(chunks)) % S + +def magicformat(format): + """Evaluate and substitute the appropriate parts of the string.""" + try: 1/0 + except: frame = sys.exc_traceback.tb_frame + while frame.f_globals["__name__"] == __name__: frame = frame.f_back + return dictformat(format,frame.f_locals, frame.f_globals) + +if __name__=='__main__': + from reportlab.lib.formatters import DecimalFormatter + _DF={} + def df(n,dp=2,ds='.',ts=','): + try: + _df = _DF[dp,ds] + except KeyError: + _df = _DF[dp,ds] = DecimalFormatter(places=dp,decimalSep=ds,thousandSep=ts) + return _df(n) + + from reportlab.lib.extformat import magicformat + + Z={'abc': ('ab','c')} + x = 300000.23 + percent=79.2 + class dingo: + a=3 + print magicformat(''' +$%%(df(x,dp=3))s --> $%(df(x,dp=3))s +$%%(df(x,dp=2,ds=',',ts='.'))s --> $%(df(x,dp=2,ds=',',ts='.'))s +%%(percent).2f%%%% --> %(percent).2f%% +%%(dingo.a)s --> %(dingo.a)s +%%(Z['abc'][0])s --> %(Z['abc'][0])s +''') diff --git a/bin/reportlab/lib/fonts.py b/bin/reportlab/lib/fonts.py new file mode 100644 index 00000000000..7793530fbc8 --- /dev/null +++ b/bin/reportlab/lib/fonts.py @@ -0,0 +1,89 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/fonts.py +__version__=''' $Id: fonts.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +import string, sys, os +############################################################################### +# A place to put useful font stuff +############################################################################### +# +# Font Mappings +# The brute force approach to finding the correct postscript font name; +# much safer than the rule-based ones we tried. +# preprocessor to reduce font face names to the shortest list +# possible. Add any aliases you wish; it keeps looking up +# until it finds no more translations to do. Any input +# will be lowercased before checking. +_family_alias = { + 'serif':'times', + 'sansserif':'helvetica', + 'monospaced':'courier', + 'arial':'helvetica' + } +#maps a piddle font to a postscript one. +_tt2ps_map = { + #face, bold, italic -> ps name + ('times', 0, 0) :'Times-Roman', + ('times', 1, 0) :'Times-Bold', + ('times', 0, 1) :'Times-Italic', + ('times', 1, 1) :'Times-BoldItalic', + + ('courier', 0, 0) :'Courier', + ('courier', 1, 0) :'Courier-Bold', + ('courier', 0, 1) :'Courier-Oblique', + ('courier', 1, 1) :'Courier-BoldOblique', + + ('helvetica', 0, 0) :'Helvetica', + ('helvetica', 1, 0) :'Helvetica-Bold', + ('helvetica', 0, 1) :'Helvetica-Oblique', + ('helvetica', 1, 1) :'Helvetica-BoldOblique', + + + # there is only one Symbol font + ('symbol', 0, 0) :'Symbol', + ('symbol', 1, 0) :'Symbol', + ('symbol', 0, 1) :'Symbol', + ('symbol', 1, 1) :'Symbol', + + # ditto for dingbats + ('zapfdingbats', 0, 0) :'ZapfDingbats', + ('zapfdingbats', 1, 0) :'ZapfDingbats', + ('zapfdingbats', 0, 1) :'ZapfDingbats', + ('zapfdingbats', 1, 1) :'ZapfDingbats', + + + } + +_ps2tt_map={} +for k,v in _tt2ps_map.items(): + if not _ps2tt_map.has_key(k): + _ps2tt_map[string.lower(v)] = k + +def ps2tt(psfn): + 'ps fontname to family name, bold, italic' + psfn = string.lower(psfn) + if _ps2tt_map.has_key(psfn): + return _ps2tt_map[psfn] + raise ValueError, "Can't map determine family/bold/italic for %s" % psfn + +def tt2ps(fn,b,i): + 'family name + bold & italic to ps font name' + K = (string.lower(fn),b,i) + if _tt2ps_map.has_key(K): + return _tt2ps_map[K] + else: + fn, b1, i1 = ps2tt(K[0]) + K = fn, b1|b, i1|i + if _tt2ps_map.has_key(K): + return _tt2ps_map[K] + raise ValueError, "Can't find concrete font for family=%s, bold=%d, italic=%d" % (fn, b, i) + +def addMapping(face, bold, italic, psname): + 'allow a custom font to be put in the mapping' + k = (string.lower(face), bold, italic) + _tt2ps_map[k] = psname + # rebuild inverse - inefficient + for k,v in _tt2ps_map.items(): + if not _ps2tt_map.has_key(k): + _ps2tt_map[string.lower(v)] = k diff --git a/bin/reportlab/lib/formatters.py b/bin/reportlab/lib/formatters.py new file mode 100644 index 00000000000..19230f3c375 --- /dev/null +++ b/bin/reportlab/lib/formatters.py @@ -0,0 +1,100 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/formatters.py +__version__=''' $Id: formatters.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__=""" +These help format numbers and dates in a user friendly way. + +Used by the graphics framework. +""" +import string, sys, os + +class Formatter: + "Base formatter - simply applies python format strings" + def __init__(self, pattern): + self.pattern = pattern + def format(self, obj): + return self.pattern % obj + def __repr__(self): + return "%s('%s')" % (self.__class__.__name__, self.pattern) + def __call__(self, x): + return self.format(x) + + +class DecimalFormatter(Formatter): + """lets you specify how to build a decimal. + + A future NumberFormatter class will take Microsoft-style patterns + instead - "$#,##0.00" is WAY easier than this.""" + def __init__(self, places=2, decimalSep='.', thousandSep=None, prefix=None, suffix=None): + self.places = places + self.dot = decimalSep + self.comma = thousandSep + self.prefix = prefix + self.suffix = suffix + + def format(self, num): + # positivize the numbers + sign=num<0 + if sign: + num = -num + places, sep = self.places, self.dot + strip = places<=0 + if places and strip: places = -places + strInt = ('%.' + str(places) + 'f') % num + if places: + strInt, strFrac = strInt.split('.') + strFrac = sep + strFrac + if strip: + while strFrac and strFrac[-1] in ['0',sep]: strFrac = strFrac[:-1] + else: + strFrac = '' + + if self.comma is not None: + strNew = '' + while strInt: + left, right = strInt[0:-3], strInt[-3:] + if left == '': + #strNew = self.comma + right + strNew + strNew = right + strNew + else: + strNew = self.comma + right + strNew + strInt = left + strInt = strNew + + strBody = strInt + strFrac + if sign: strBody = '-' + strBody + if self.prefix: + strBody = self.prefix + strBody + if self.suffix: + strBody = strBody + self.suffix + return strBody + + def __repr__(self): + return "%s(places=%d, decimalSep=%s, thousandSep=%s, prefix=%s, suffix=%s)" % ( + self.__class__.__name__, + self.places, + repr(self.dot), + repr(self.comma), + repr(self.prefix), + repr(self.suffix) + ) + +if __name__=='__main__': + def t(n, s, places=2, decimalSep='.', thousandSep=None, prefix=None, suffix=None): + f=DecimalFormatter(places,decimalSep,thousandSep,prefix,suffix) + r = f(n) + print "places=%2d dot=%-4s comma=%-4s prefix=%-4s suffix=%-4s result=%10s %s" %(f.places, f.dot, f.comma, f.prefix, f.suffix,r, r==s and 'OK' or 'BAD') + t(1000.9,'1,000.9',1,thousandSep=',') + t(1000.95,'1,001.0',1,thousandSep=',') + t(1000.95,'1,001',-1,thousandSep=',') + t(1000.9,'1,001',0,thousandSep=',') + t(1000.9,'1000.9',1) + t(1000.95,'1001.0',1) + t(1000.95,'1001',-1) + t(1000.9,'1001',0) + t(1000.1,'1000.1',1) + t(1000.55,'1000.6',1) + t(1000.449,'1000.4',-1) + t(1000.45,'1000',0) \ No newline at end of file diff --git a/bin/reportlab/lib/logger.py b/bin/reportlab/lib/logger.py new file mode 100644 index 00000000000..35555873a3e --- /dev/null +++ b/bin/reportlab/lib/logger.py @@ -0,0 +1,61 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/logger.py +__version__=''' $Id: logger.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from sys import stderr +class Logger: + ''' + An extended file type thing initially equivalent to sys.stderr + You can add/remove file type things; it has a write method + ''' + def __init__(self): + self._fps = [stderr] + self._fns = {} + + def add(self,fp): + '''add the file/string fp to the destinations''' + if type(fp) is StringType: + if fp in self._fns: return + fp = open(fn,'wb') + self._fns[fn] = fp + self._fps.append(fp) + + def remove(self,fp): + '''remove the file/string fp from the destinations''' + if type(fp) is StringType: + if fp not in self._fns: return + fn = fp + fp = self._fns[fn] + del self.fns[fn] + if fp in self._fps: + del self._fps[self._fps.index(fp)] + + def write(self,text): + '''write text to all the destinations''' + if text[-1]!='\n': text=text+'\n' + map(lambda fp,t=text: fp.write(t),self._fps) + + def __call__(self,text): + self.write(text) + +logger=Logger() + +class WarnOnce: + + def __init__(self,kind='Warn'): + self.uttered = {} + self.pfx = '%s: '%kind + self.enabled = 1 + + def once(self,warning): + if not self.uttered.has_key(warning): + if self.enabled: logger.write(self.pfx + warning) + self.uttered[warning] = 1 + + def __call__(self,warning): + self.once(warning) + +warnOnce=WarnOnce() +infoOnce=WarnOnce('Info') \ No newline at end of file diff --git a/bin/reportlab/lib/normalDate.py b/bin/reportlab/lib/normalDate.py new file mode 100644 index 00000000000..efcea715013 --- /dev/null +++ b/bin/reportlab/lib/normalDate.py @@ -0,0 +1,605 @@ +#!/usr/bin/env python +# normalDate.py - version 1.0 - 20000717 +#hacked by Robin Becker 10/Apr/2001 +#major changes include +# using Types instead of type(0) etc +# BusinessDate class +# __radd__, __rsub__ methods +# formatMS stuff + +# derived from an original version created +# by Jeff Bauer of Rubicon Research and used +# with his kind permission +__version__=''' $Id: normalDate.py 2742 2005-12-12 11:58:08Z oualid $ ''' + + + +_bigBangScalar = -4345732 # based on (-9999, 1, 1) BC/BCE minimum +_bigCrunchScalar = 2958463 # based on (9999,12,31) AD/CE maximum +_daysInMonthNormal = [31,28,31,30,31,30,31,31,30,31,30,31] +_daysInMonthLeapYear = [31,29,31,30,31,30,31,31,30,31,30,31] +_dayOfWeekName = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', + 'Friday', 'Saturday', 'Sunday'] +_monthName = ['January', 'February', 'March', 'April', 'May', 'June', + 'July','August','September','October','November','December'] + +from types import IntType, StringType, ListType, TupleType +import string, re, time, datetime +if hasattr(time,'struct_time'): + _DateSeqTypes = (ListType,TupleType,time.struct_time) +else: + _DateSeqTypes = (ListType,TupleType) + +_fmtPat = re.compile('\\{(m{1,5}|yyyy|yy|d{1,4})\\}',re.MULTILINE|re.IGNORECASE) +_iso_re = re.compile(r'(\d\d\d\d|\d\d)-(\d\d)-(\d\d)') + +def getStdMonthNames(): + return map(string.lower,_monthName) + +def getStdShortMonthNames(): + return map(lambda x: x[:3],getStdMonthNames()) + +def getStdDayNames(): + return map(string.lower,_dayOfWeekName) + +def getStdShortDayNames(): + return map(lambda x: x[:3],getStdDayNames()) + +def isLeapYear(year): + """determine if specified year is leap year, returns Python boolean""" + if year < 1600: + if year % 4: + return 0 + else: + return 1 + elif year % 4 != 0: + return 0 + elif year % 100 != 0: + return 1 + elif year % 400 != 0: + return 0 + else: + return 1 + +class NormalDateException(Exception): + """Exception class for NormalDate""" + pass + +class NormalDate: + """ + NormalDate is a specialized class to handle dates without + all the excess baggage (time zones, daylight savings, leap + seconds, etc.) of other date structures. The minimalist + strategy greatly simplifies its implementation and use. + + Internally, NormalDate is stored as an integer with values + in a discontinuous range of -99990101 to 99991231. The + integer value is used principally for storage and to simplify + the user interface. Internal calculations are performed by + a scalar based on Jan 1, 1900. + + Valid NormalDate ranges include (-9999,1,1) B.C.E. through + (9999,12,31) C.E./A.D. + + 1.0 - No changes, except the version number. After 3 years of use + by various parties I think we can consider it stable. + 0.8 - added Prof. Stephen Walton's suggestion for a range method + - module author resisted the temptation to use lambda <0.5 wink> + 0.7 - added Dan Winkler's suggestions for __add__, __sub__ methods + 0.6 - modifications suggested by Kevin Digweed to fix: + - dayOfWeek, dayOfWeekAbbrev, clone methods + - permit NormalDate to be a better behaved superclass + 0.5 - minor tweaking + 0.4 - added methods __cmp__, __hash__ + - added Epoch variable, scoped to the module + - added setDay, setMonth, setYear methods + 0.3 - minor touch-ups + 0.2 - fixed bug for certain B.C.E leap years + - added Jim Fulton's suggestions for short alias class name =ND + and __getstate__, __setstate__ methods + + Special thanks: Roedy Green + """ + def __init__(self, normalDate=None): + """ + Accept 1 of 4 values to initialize a NormalDate: + 1. None - creates a NormalDate for the current day + 2. integer in yyyymmdd format + 3. string in yyyymmdd format + 4. tuple in (yyyy, mm, dd) - localtime/gmtime can also be used + """ + if normalDate is None: + self.setNormalDate(time.localtime(time.time())) + else: + self.setNormalDate(normalDate) + + def add(self, days): + """add days to date; use negative integers to subtract""" + if not type(days) is IntType: + raise NormalDateException( \ + 'add method parameter must be integer type') + self.normalize(self.scalar() + days) + + def __add__(self, days): + """add integer to normalDate and return a new, calculated value""" + if not type(days) is IntType: + raise NormalDateException( \ + '__add__ parameter must be integer type') + cloned = self.clone() + cloned.add(days) + return cloned + + def __radd__(self,days): + '''for completeness''' + return self.__add__(days) + + def clone(self): + """return a cloned instance of this normalDate""" + return self.__class__(self.normalDate) + + def __cmp__(self, target): + if target is None: + return 1 + elif not hasattr(target, 'normalDate'): + return 1 + else: + return cmp(self.normalDate, target.normalDate) + + def day(self): + """return the day as integer 1-31""" + return int(repr(self.normalDate)[-2:]) + + def dayOfWeek(self): + """return integer representing day of week, Mon=0, Tue=1, etc.""" + return apply(dayOfWeek, self.toTuple()) + + def dayOfWeekAbbrev(self): + """return day of week abbreviation for current date: Mon, Tue, etc.""" + return _dayOfWeekName[self.dayOfWeek()][:3] + + def dayOfWeekName(self): + """return day of week name for current date: Monday, Tuesday, etc.""" + return _dayOfWeekName[self.dayOfWeek()] + + def dayOfYear(self): + """day of year""" + if self.isLeapYear(): + daysByMonth = _daysInMonthLeapYear + else: + daysByMonth = _daysInMonthNormal + priorMonthDays = 0 + for m in xrange(self.month() - 1): + priorMonthDays = priorMonthDays + daysByMonth[m] + return self.day() + priorMonthDays + + def daysBetweenDates(self, normalDate): + """ + return value may be negative, since calculation is + self.scalar() - arg + """ + if type(normalDate) is _NDType: + return self.scalar() - normalDate.scalar() + else: + return self.scalar() - NormalDate(normalDate).scalar() + + def equals(self, target): + if type(target) is _NDType: + if target is None: + return self.normalDate is None + else: + return self.normalDate == target.normalDate + else: + return 0 + + def endOfMonth(self): + """returns (cloned) last day of month""" + return self.__class__(self.__repr__()[-8:-2]+str(self.lastDayOfMonth())) + + def firstDayOfMonth(self): + """returns (cloned) first day of month""" + return self.__class__(self.__repr__()[-8:-2]+"01") + + def formatUS(self): + """return date as string in common US format: MM/DD/YY""" + d = self.__repr__() + return "%s/%s/%s" % (d[-4:-2], d[-2:], d[-6:-4]) + + def formatUSCentury(self): + """return date as string in 4-digit year US format: MM/DD/YYYY""" + d = self.__repr__() + return "%s/%s/%s" % (d[-4:-2], d[-2:], d[-8:-4]) + + def _fmtM(self): + return str(self.month()) + + def _fmtMM(self): + return '%02d' % self.month() + + def _fmtMMM(self): + return self.monthAbbrev() + + def _fmtMMMM(self): + return self.monthName() + + def _fmtMMMMM(self): + return self.monthName()[0] + + def _fmtD(self): + return str(self.day()) + + def _fmtDD(self): + return '%02d' % self.day() + + def _fmtDDD(self): + return self.dayOfWeekAbbrev() + + def _fmtDDDD(self): + return self.dayOfWeekName() + + def _fmtYY(self): + return '%02d' % (self.year()%100) + + def _fmtYYYY(self): + return str(self.year()) + + def formatMS(self,fmt): + '''format like MS date using the notation + {YY} --> 2 digit year + {YYYY} --> 4 digit year + {M} --> month as digit + {MM} --> 2 digit month + {MMM} --> abbreviated month name + {MMMM} --> monthname + {MMMMM} --> first character of monthname + {D} --> day of month as digit + {DD} --> 2 digit day of month + {DDD} --> abrreviated weekday name + {DDDD} --> weekday name + ''' + r = fmt[:] + f = 0 + while 1: + m = _fmtPat.search(r,f) + if m: + y = getattr(self,'_fmt'+string.upper(m.group()[1:-1]))() + i, j = m.span() + r = (r[0:i] + y) + r[j:] + f = i + len(y) + else: + return r + + def __getstate__(self): + """minimize persistent storage requirements""" + return self.normalDate + + def __hash__(self): + return hash(self.normalDate) + + def __int__(self): + return self.normalDate + + def isLeapYear(self): + """ + determine if specified year is leap year, returning true (1) or + false (0) + """ + return isLeapYear(self.year()) + + def _isValidNormalDate(self, normalDate): + """checks for date validity in [-]yyyymmdd format""" + if type(normalDate) is not IntType: + return 0 + if len(repr(normalDate)) > 9: + return 0 + if normalDate < 0: + dateStr = "%09d" % normalDate + else: + dateStr = "%08d" % normalDate + if len(dateStr) < 8: + return 0 + elif len(dateStr) == 9: + if (dateStr[0] != '-' and dateStr[0] != '+'): + return 0 + year = int(dateStr[:-4]) + if year < -9999 or year > 9999 or year == 0: + return 0 # note: zero (0) is not a valid year + month = int(dateStr[-4:-2]) + if month < 1 or month > 12: + return 0 + if isLeapYear(year): + maxDay = _daysInMonthLeapYear[month - 1] + else: + maxDay = _daysInMonthNormal[month - 1] + day = int(dateStr[-2:]) + if day < 1 or day > maxDay: + return 0 + if year == 1582 and month == 10 and day > 4 and day < 15: + return 0 # special case of 10 days dropped: Oct 5-14, 1582 + return 1 + + def lastDayOfMonth(self): + """returns last day of the month as integer 28-31""" + if self.isLeapYear(): + return _daysInMonthLeapYear[self.month() - 1] + else: + return _daysInMonthNormal[self.month() - 1] + + def localeFormat(self): + """override this method to use your preferred locale format""" + return self.formatUS() + + def month(self): + """returns month as integer 1-12""" + return int(repr(self.normalDate)[-4:-2]) + + def monthAbbrev(self): + """returns month as a 3-character abbreviation, i.e. Jan, Feb, etc.""" + return _monthName[self.month() - 1][:3] + + def monthName(self): + """returns month name, i.e. January, February, etc.""" + return _monthName[self.month() - 1] + + def normalize(self, scalar): + """convert scalar to normalDate""" + if scalar < _bigBangScalar: + msg = "normalize(%d): scalar below minimum" % \ + _bigBangScalar + raise NormalDateException(msg) + if scalar > _bigCrunchScalar: + msg = "normalize(%d): scalar exceeds maximum" % \ + _bigCrunchScalar + raise NormalDateException(msg) + from math import floor + if scalar >= -115860: + year = 1600 + int(floor((scalar + 109573) / 365.2425)) + elif scalar >= -693597: + year = 4 + int(floor((scalar + 692502) / 365.2425)) + else: + year = -4 + int(floor((scalar + 695058) / 365.2425)) + days = scalar - firstDayOfYear(year) + 1 + if days <= 0: + year = year - 1 + days = scalar - firstDayOfYear(year) + 1 + daysInYear = 365 + if isLeapYear(year): + daysInYear = daysInYear + 1 + if days > daysInYear: + year = year + 1 + days = scalar - firstDayOfYear(year) + 1 + # add 10 days if between Oct 15, 1582 and Dec 31, 1582 + if (scalar >= -115860 and scalar <= -115783): + days = days + 10 + if isLeapYear(year): + daysByMonth = _daysInMonthLeapYear + else: + daysByMonth = _daysInMonthNormal + dc = 0; month = 12 + for m in xrange(len(daysByMonth)): + dc = dc + daysByMonth[m] + if dc >= days: + month = m + 1 + break + # add up the days in prior months + priorMonthDays = 0 + for m in xrange(month - 1): + priorMonthDays = priorMonthDays + daysByMonth[m] + day = days - priorMonthDays + self.setNormalDate((year, month, day)) + + def range(self, days): + """Return a range of normalDates as a list. Parameter + may be an int or normalDate.""" + if type(days) is not IntType: + days = days - self # if not int, assume arg is normalDate type + r = [] + for i in range(days): + r.append(self + i) + return r + + def __repr__(self): + """print format: [-]yyyymmdd""" + # Note: When disassembling a NormalDate string, be sure to + # count from the right, i.e. epochMonth = int(`Epoch`[-4:-2]), + # or the slice won't work for dates B.C. + if self.normalDate < 0: + return "%09d" % self.normalDate + else: + return "%08d" % self.normalDate + + def scalar(self): + """days since baseline date: Jan 1, 1900""" + (year, month, day) = self.toTuple() + days = firstDayOfYear(year) + day - 1 + if self.isLeapYear(): + for m in xrange(month - 1): + days = days + _daysInMonthLeapYear[m] + else: + for m in xrange(month - 1): + days = days + _daysInMonthNormal[m] + if year == 1582: + if month > 10 or (month == 10 and day > 4): + days = days - 10 + return days + + def setDay(self, day): + """set the day of the month""" + maxDay = self.lastDayOfMonth() + if day < 1 or day > maxDay: + msg = "day is outside of range 1 to %d" % maxDay + raise NormalDateException(msg) + (y, m, d) = self.toTuple() + self.setNormalDate((y, m, day)) + + def setMonth(self, month): + """set the month [1-12]""" + if month < 1 or month > 12: + raise NormalDateException('month is outside range 1 to 12') + (y, m, d) = self.toTuple() + self.setNormalDate((y, month, d)) + + def setNormalDate(self, normalDate): + """ + accepts date as scalar string/integer (yyyymmdd) or tuple + (year, month, day, ...)""" + tn=type(normalDate) + if tn is IntType: + self.normalDate = normalDate + elif tn is StringType: + try: + self.normalDate = int(normalDate) + except: + m = _iso_re.match(normalDate) + if m: + self.setNormalDate(m.group(1)+m.group(2)+m.group(3)) + else: + raise NormalDateException("unable to setNormalDate(%s)" % `normalDate`) + elif tn in _DateSeqTypes: + self.normalDate = int("%04d%02d%02d" % normalDate[:3]) + elif tn is _NDType: + self.normalDate = normalDate.normalDate + elif isinstance(normalDate,(datetime.datetime,datetime.date)): + self.normalDate = (normalDate.year*100+normalDate.month)*100+normalDate.day + if not self._isValidNormalDate(self.normalDate): + raise NormalDateException("unable to setNormalDate(%s)" % `normalDate`) + + def setYear(self, year): + if year == 0: + raise NormalDateException('cannot set year to zero') + elif year < -9999: + raise NormalDateException('year cannot be less than -9999') + elif year > 9999: + raise NormalDateException('year cannot be greater than 9999') + (y, m, d) = self.toTuple() + self.setNormalDate((year, m, d)) + + __setstate__ = setNormalDate + + def __sub__(self, v): + if type(v) is IntType: + return self.__add__(-v) + return self.scalar() - v.scalar() + + def __rsub__(self,v): + if type(v) is IntType: + return NormalDate(v) - self + else: + return v.scalar() - self.scalar() + + def toTuple(self): + """return date as (year, month, day) tuple""" + return (self.year(), self.month(), self.day()) + + def year(self): + """return year in yyyy format, negative values indicate B.C.""" + return int(repr(self.normalDate)[:-4]) + +################# Utility functions ################# + +def bigBang(): + """return lower boundary as a NormalDate""" + return NormalDate((-9999, 1, 1)) + +def bigCrunch(): + """return upper boundary as a NormalDate""" + return NormalDate((9999, 12, 31)) + +def dayOfWeek(y, m, d): + """return integer representing day of week, Mon=0, Tue=1, etc.""" + if m == 1 or m == 2: + m = m + 12 + y = y - 1 + return (d + 2*m + 3*(m+1)/5 + y + y/4 - y/100 + y/400) % 7 + +def firstDayOfYear(year): + """number of days to the first of the year, relative to Jan 1, 1900""" + if type(year) is not IntType: + msg = "firstDayOfYear() expected integer, got %s" % type(year) + raise NormalDateException(msg) + if year == 0: + raise NormalDateException('first day of year cannot be zero (0)') + elif year < 0: # BCE calculation + firstDay = (year * 365) + int((year - 1) / 4) - 693596 + else: # CE calculation + leapAdjust = int((year + 3) / 4) + if year > 1600: + leapAdjust = leapAdjust - int((year + 99 - 1600) / 100) + \ + int((year + 399 - 1600) / 400) + firstDay = year * 365 + leapAdjust - 693963 + if year > 1582: + firstDay = firstDay - 10 + return firstDay + +def FND(d): + '''convert to ND if required''' + return (type(d) is _NDType) and d or ND(d) + +Epoch=bigBang() +ND=NormalDate +_NDType = type(Epoch) +BDEpoch=ND(15821018) +BDEpochScalar = -115857 + +class BusinessDate(NormalDate): + """ + Specialised NormalDate + """ + def add(self, days): + """add days to date; use negative integers to subtract""" + if not type(days) is IntType: + raise NormalDateException('add method parameter must be integer type') + self.normalize(self.scalar() + days) + + def __add__(self, days): + """add integer to BusinessDate and return a new, calculated value""" + if not type(days) is IntType: + raise NormalDateException('__add__ parameter must be integer type') + cloned = self.clone() + cloned.add(days) + return cloned + + def __sub__(self, v): + return type(v) is IntType and self.__add__(-v) or self.scalar() - v.scalar() + + def asNormalDate(self): + return ND(self.normalDate) + + def daysBetweenDates(self, normalDate): + return self.asNormalDate.daysBetweenDates(normalDate) + + def _checkDOW(self): + if self.dayOfWeek()>4: raise NormalDateException("%s isn't a business day" % `self.normalDate`) + + def normalize(self, i): + i = int(i) + NormalDate.normalize(self,(i/5)*7+i%5+BDEpochScalar) + + def scalar(self): + d = self.asNormalDate() + i = d - BDEpoch #luckily BDEpoch is a Monday so we don't have a problem + #concerning the relative weekday + return 5*(i/7) + i%7 + + def setNormalDate(self, normalDate): + NormalDate.setNormalDate(self,normalDate) + self._checkDOW() + +if __name__ == '__main__': + today = NormalDate() + print "NormalDate test:" + print " Today (%s) is: %s %s" % (today, today.dayOfWeekAbbrev(), today.localeFormat()) + yesterday = today - 1 + print " Yesterday was: %s %s" % (yesterday.dayOfWeekAbbrev(), yesterday.localeFormat()) + tomorrow = today + 1 + print " Tomorrow will be: %s %s" % (tomorrow.dayOfWeekAbbrev(), tomorrow.localeFormat()) + print " Days between tomorrow and yesterday: %d" % (tomorrow - yesterday) + print today.formatMS('{d}/{m}/{yy}') + print today.formatMS('{dd}/{m}/{yy}') + print today.formatMS('{ddd} {d}/{m}/{yy}') + print today.formatMS('{dddd} {d}/{m}/{yy}') + print today.formatMS('{d}/{mm}/{yy}') + print today.formatMS('{d}/{mmm}/{yy}') + print today.formatMS('{d}/{mmmm}/{yy}') + print today.formatMS('{d}/{m}/{yyyy}') + b = BusinessDate('20010116') + print 'b=',b,'b.scalar()', b.scalar() diff --git a/bin/reportlab/lib/pagesizes.py b/bin/reportlab/lib/pagesizes.py new file mode 100644 index 00000000000..75fb8ddd588 --- /dev/null +++ b/bin/reportlab/lib/pagesizes.py @@ -0,0 +1,55 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/pagesizes.py + +"""This module defines a few common page sizes in points (1/72 inch). +To be expanded to include things like label sizes, envelope windows +etc.""" +__version__=''' $Id: pagesizes.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +from reportlab.lib.units import cm, inch + +_W, _H = (21*cm, 29.7*cm) + +A6 = (_W*.5, _H*.5) +A5 = (_H*.5, _W) +A4 = (_W, _H) +A3 = (_H, _W*2) +A2 = (_W*2, _H*2) +A1 = (_H*2, _W*4) +A0 = (_W*4, _H*4) + +LETTER = (8.5*inch, 11*inch) +LEGAL = (8.5*inch, 14*inch) +ELEVENSEVENTEEN = (11*inch, 17*inch) +# lower case is deprecated as of 12/2001, but here +# for compatability +letter=LETTER +legal=LEGAL +elevenSeventeen = ELEVENSEVENTEEN + +_BW, _BH = (25*cm, 35.3*cm) +B6 = (_BW*.5, _BH*.5) +B5 = (_BH*.5, _BW) +B4 = (_BW, _BH) +B3 = (_BH*2, _BW) +B2 = (_BW*2, _BH*2) +B1 = (_BH*4, _BW*2) +B0 = (_BW*4, _BH*4) + +def landscape(pagesize): + """Use this to get page orientation right""" + a, b = pagesize + if a < b: + return (b, a) + else: + return (a, b) + +def portrait(pagesize): + """Use this to get page orientation right""" + a, b = pagesize + if a >= b: + return (b, a) + else: + return (a, b) \ No newline at end of file diff --git a/bin/reportlab/lib/randomtext.py b/bin/reportlab/lib/randomtext.py new file mode 100644 index 00000000000..075b9d711c7 --- /dev/null +++ b/bin/reportlab/lib/randomtext.py @@ -0,0 +1,348 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/randomtext.py + +__version__=''' $Id: randomtext.py 2436 2004-09-11 14:18:33Z rgbecker $ ''' + +############################################################################### +# generates so-called 'Greek Text' for use in filling documents. +############################################################################### +""" +This module exposes a function randomText() which generates paragraphs. +These can be used when testing out document templates and stylesheets. +A number of 'themes' are provided - please contribute more! +We need some real Greek text too. + +There are currently six themes provided: + STARTUP (words suitable for a business plan - or not as the case may be), + COMPUTERS (names of programming languages and operating systems etc), + BLAH (variations on the word 'blah'), + BUZZWORD (buzzword bingo), + STARTREK (Star Trek), + PRINTING (print-related terms) + PYTHON (snippets and quotes from Monty Python) + CHOMSKY (random lingusitic nonsense) + +EXAMPLE USAGE: + from reportlab.lib import randomtext + print randomtext.randomText(randomtext.PYTHON, 10) + + This prints a random number of random sentences (up to a limit + of ten) using the theme 'PYTHON'. + +""" + +#theme one :-) +STARTUP = ['strategic', 'direction', 'proactive', 'venture capital', + 'reengineering', 'forecast', 'resources', 'SWOT analysis', + 'forward-thinking', 'profit', 'growth', 'doubletalk', 'B2B', 'B2C', + 'venture capital', 'IPO', "NASDAQ meltdown - we're all doomed!"] + +#theme two - computery things. +COMPUTERS = ['Python', 'Perl', 'Pascal', 'Java', 'Javascript', + 'VB', 'Basic', 'LISP', 'Fortran', 'ADA', 'APL', 'C', 'C++', + 'assembler', 'Larry Wall', 'Guido van Rossum', 'XML', 'HTML', + 'cgi', 'cgi-bin', 'Amiga', 'Macintosh', 'Dell', 'Microsoft', + 'firewall', 'server', 'Linux', 'Unix', 'MacOS', 'BeOS', 'AS/400', + 'sendmail', 'TCP/IP', 'SMTP', 'RFC822-compliant', 'dynamic', + 'Internet', 'A/UX', 'Amiga OS', 'BIOS', 'boot managers', 'CP/M', + 'DOS', 'file system', 'FreeBSD', 'Freeware', 'GEOS', 'GNU', + 'Hurd', 'Linux', 'Mach', 'Macintosh OS', 'mailing lists', 'Minix', + 'Multics', 'NetWare', 'NextStep', 'OS/2', 'Plan 9', 'Realtime', + 'UNIX', 'VMS', 'Windows', 'X Windows', 'Xinu', 'security', 'Intel', + 'encryption', 'PGP' , 'software', 'ActiveX', 'AppleScript', 'awk', + 'BETA', 'COBOL', 'Delphi', 'Dylan', 'Eiffel', 'extreme programming', + 'Forth', 'Fortran', 'functional languages', 'Guile', 'format your hard drive', + 'Icon', 'IDL', 'Infer', 'Intercal', 'J', 'Java', 'JavaScript', 'CD-ROM', + 'JCL', 'Lisp', '"literate programming"', 'Logo', 'MUMPS', 'C: drive', + 'Modula-2', 'Modula-3', 'Oberon', 'Occam', 'OpenGL', 'parallel languages', + 'Pascal', 'Perl', 'PL/I', 'PostScript', 'Prolog', 'hardware', 'Blue Screen of Death', + 'Rexx', 'RPG', 'Scheme', 'scripting languages', 'Smalltalk', 'crash!', 'disc crash', + 'Spanner', 'SQL', 'Tcl/Tk', 'TeX', 'TOM', 'Visual', 'Visual Basic', '4GL', + 'VRML', 'Virtual Reality Modeling Language', 'difference engine', '...went into "yo-yo mode"', + 'Sun', 'Sun Microsystems', 'Hewlett Packard', 'output device', + 'CPU', 'memory', 'registers', 'monitor', 'TFT display', 'plasma screen', + 'bug report', '"mis-feature"', '...millions of bugs!', 'pizza', + '"illiterate programming"','...lots of pizza!', 'pepperoni pizza', + 'coffee', 'Jolt Cola[TM]', 'beer', 'BEER!'] + +#theme three - 'blah' - for when you want to be subtle. :-) +BLAH = ['Blah', 'BLAH', 'blahblah', 'blahblahblah', 'blah-blah', + 'blah!', '"Blah Blah Blah"', 'blah-de-blah', 'blah?', 'blah!!!', + 'blah...', 'Blah.', 'blah;', 'blah, Blah, BLAH!', 'Blah!!!'] + +#theme four - 'buzzword bingo' time! +BUZZWORD = ['intellectual capital', 'market segment', 'flattening', + 'regroup', 'platform', 'client-based', 'long-term', 'proactive', + 'quality vector', 'out of the loop', 'implement', + 'streamline', 'cost-centered', 'phase', 'synergy', + 'synergize', 'interactive', 'facilitate', + 'appropriate', 'goal-setting', 'empowering', 'low-risk high-yield', + 'peel the onion', 'goal', 'downsize', 'result-driven', + 'conceptualize', 'multidisciplinary', 'gap analysis', 'dysfunctional', + 'networking', 'knowledge management', 'goal-setting', + 'mastery learning', 'communication', 'real-estate', 'quarterly', + 'scalable', 'Total Quality Management', 'best of breed', + 'nimble', 'monetize', 'benchmark', 'hardball', + 'client-centered', 'vision statement', 'empowerment', + 'lean & mean', 'credibility', 'synergistic', + 'backward-compatible', 'hardball', 'stretch the envelope', + 'bleeding edge', 'networking', 'motivation', 'best practice', + 'best of breed', 'implementation', 'Total Quality Management', + 'undefined', 'disintermediate', 'mindset', 'architect', + 'gap analysis', 'morale', 'objective', 'projection', + 'contribution', 'proactive', 'go the extra mile', 'dynamic', + 'world class', 'real estate', 'quality vector', 'credibility', + 'appropriate', 'platform', 'projection', 'mastery learning', + 'recognition', 'quality', 'scenario', 'performance based', + 'solutioning', 'go the extra mile', 'downsize', 'phase', + 'networking', 'experiencing slippage', 'knowledge management', + 'high priority', 'process', 'ethical', 'value-added', 'implement', + 're-factoring', 're-branding', 'embracing change'] + +#theme five - Star Trek +STARTREK = ['Starfleet', 'Klingon', 'Romulan', 'Cardassian', 'Vulcan', + 'Benzite', 'IKV Pagh', 'emergency transponder', 'United Federation of Planets', + 'Bolian', "K'Vort Class Bird-of-Prey", 'USS Enterprise', 'USS Intrepid', + 'USS Reliant', 'USS Voyager', 'Starfleet Academy', 'Captain Picard', + 'Captain Janeway', 'Tom Paris', 'Harry Kim', 'Counsellor Troi', + 'Lieutenant Worf', 'Lieutenant Commander Data', 'Dr. Beverly Crusher', + 'Admiral Nakamura', 'Irumodic Syndrome', 'Devron system', 'Admiral Pressman', + 'asteroid field', 'sensor readings', 'Binars', 'distress signal', 'shuttlecraft', + 'cloaking device', 'shuttle bay 2', 'Dr. Pulaski', 'Lwaxana Troi', 'Pacifica', + 'William Riker', "Chief O'Brian", 'Soyuz class science vessel', 'Wolf-359', + 'Galaxy class vessel', 'Utopia Planitia yards', 'photon torpedo', 'Archer IV', + 'quantum flux', 'spacedock', 'Risa', 'Deep Space Nine', 'blood wine', + 'quantum torpedoes', 'holodeck', 'Romulan Warbird', 'Betazoid', 'turbolift', 'battle bridge', + 'Memory Alpha', '...with a phaser!', 'Romulan ale', 'Ferrengi', 'Klingon opera', + 'Quark', 'wormhole', 'Bajoran', 'cruiser', 'warship', 'battlecruiser', '"Intruder alert!"', + 'scout ship', 'science vessel', '"Borg Invasion imminent!" ', '"Abandon ship!"', + 'Red Alert!', 'warp-core breech', '"All hands abandon ship! This is not a drill!"'] + +#theme six - print-related terms +PRINTING = ['points', 'picas', 'leading', 'kerning', 'CMYK', 'offset litho', + 'type', 'font family', 'typography', 'type designer', + 'baseline', 'white-out type', 'WOB', 'bicameral', 'bitmap', + 'blockletter', 'bleed', 'margin', 'body', 'widow', 'orphan', + 'cicero', 'cursive', 'letterform', 'sidehead', 'dingbat', 'leader', + 'DPI', 'drop-cap', 'paragraph', 'En', 'Em', 'flush left', 'left justified', + 'right justified', 'centered', 'italic', 'Latin letterform', 'ligature', + 'uppercase', 'lowercase', 'serif', 'sans-serif', 'weight', 'type foundry', + 'fleuron', 'folio', 'gutter', 'whitespace', 'humanist letterform', 'caption', + 'page', 'frame', 'ragged setting', 'flush-right', 'rule', 'drop shadows', + 'prepress', 'spot-colour', 'duotones', 'colour separations', 'four-colour printing', + 'Pantone[TM]', 'service bureau', 'imagesetter'] + +#it had to be done!... +#theme seven - the "full Monty"! +PYTHON = ['Good evening ladies and Bruces','I want to buy some cheese', 'You do have some cheese, do you?', + "Of course sir, it's a cheese shop sir, we've got...",'discipline?... naked? ... With a melon!?', + 'The Church Police!!' , "There's a dead bishop on the landing", 'Would you like a twist of lemming sir?', + '"Conquistador Coffee brings a new meaning to the word vomit"','Your lupins please', + 'Crelm Toothpaste, with the miracle ingredient Fraudulin', + "Well there's the first result and the Silly Party has held Leicester.", + 'Hello, I would like to buy a fish license please', "Look, it's people like you what cause unrest!", + "When we got home, our Dad would thrash us to sleep with his belt!", 'Luxury', "Gumby Brain Specialist", + "My brain hurts!!!", "My brain hurts too.", "How not to be seen", + "In this picture there are 47 people. None of them can be seen", + "Mrs Smegma, will you stand up please?", + "Mr. Nesbitt has learned the first lesson of 'Not Being Seen', not to stand up.", + "My hovercraft is full of eels", "Ah. You have beautiful thighs.", "My nipples explode with delight", + "Drop your panties Sir William, I cannot wait 'til lunchtime", + "I'm a completely self-taught idiot.", "I always wanted to be a lumberjack!!!", + "Told you so!! Oh, coitus!!", "", + "Nudge nudge?", "Know what I mean!", "Nudge nudge, nudge nudge?", "Say no more!!", + "Hello, well it's just after 8 o'clock, and time for the penguin on top of your television set to explode", + "Oh, intercourse the penguin!!", "Funny that penguin being there, isn't it?", + "I wish to register a complaint.", "Now that's what I call a dead parrot", "Pining for the fjords???", + "No, that's not dead, it's ,uhhhh, resting", "This is an ex-parrot!!", + "That parrot is definitely deceased.", "No, no, no - it's spelt Raymond Luxury Yach-t, but it's pronounced 'Throatwobbler Mangrove'.", + "You're a very silly man and I'm not going to interview you.", "No Mungo... never kill a customer." + "And I'd like to conclude by putting my finger up my nose", + "egg and Spam", "egg bacon and Spam", "egg bacon sausage and Spam", "Spam bacon sausage and Spam", + "Spam egg Spam Spam bacon and Spam", "Spam sausage Spam Spam Spam bacon Spam tomato and Spam", + "Spam Spam Spam egg and Spam", "Spam Spam Spam Spam Spam Spam baked beans Spam Spam Spam", + "Spam!!", "I don't like Spam!!!", "You can't have egg, bacon, Spam and sausage without the Spam!", + "I'll have your Spam. I Love it!", + "I'm having Spam Spam Spam Spam Spam Spam Spam baked beans Spam Spam Spam and Spam", + "Have you got anything without Spam?", "There's Spam egg sausage and Spam, that's not got much Spam in it.", + "No one expects the Spanish Inquisition!!", "Our weapon is surprise, surprise and fear!", + "Get the comfy chair!", "Amongst our weaponry are such diverse elements as: fear, surprise, ruthless efficiency, an almost fanatical devotion to the Pope, and nice red uniforms - Oh damn!", + "Nobody expects the... Oh bugger!", "What swims in the sea and gets caught in nets? Henri Bergson?", + "Goats. Underwater goats with snorkels and flippers?", "A buffalo with an aqualung?", + "Dinsdale was a looney, but he was a happy looney.", "Dinsdale!!", + "The 127th Upper-Class Twit of the Year Show", "What a great Twit!", + "thought by many to be this year's outstanding twit", + "...and there's a big crowd here today to see these prize idiots in action.", + "And now for something completely different.", "Stop that, it's silly", + "We interrupt this program to annoy you and make things generally irritating", + "This depraved and degrading spectacle is going to stop right now, do you hear me?", + "Stop right there!", "This is absolutely disgusting and I'm not going to stand for it", + "I object to all this sex on the television. I mean, I keep falling off", + "Right! Stop that, it's silly. Very silly indeed", "Very silly indeed", "Lemon curry?", + "And now for something completely different, a man with 3 buttocks", + "I've heard of unisex, but I've never had it", "That's the end, stop the program! Stop it!"] +leadins=[ + "To characterize a linguistic level L,", + "On the other hand,", + "This suggests that", + "It appears that", + "Furthermore,", + "We will bring evidence in favor of the following thesis: ", + "To provide a constituent structure for T(Z,K),", + "From C1, it follows that", + "For any transformation which is sufficiently diversified in application to be of any interest,", + "Analogously,", + "Clearly,", + "Note that", + "Of course,", + "Suppose, for instance, that", + "Thus", + "With this clarification,", + "Conversely,", + "We have already seen that", + "By combining adjunctions and certain deformations,", + "I suggested that these results would follow from the assumption that", + "If the position of the trace in (99c) were only relatively inaccessible to movement,", + "However, this assumption is not correct, since", + "Comparing these examples with their parasitic gap counterparts in (96) and (97), we see that", + "In the discussion of resumptive pronouns following (81),", + "So far,", + "Nevertheless,", + "For one thing,", + "Summarizing, then, we assume that", + "A consequence of the approach just outlined is that", + "Presumably,", + "On our assumptions,", + "It may be, then, that", + "It must be emphasized, once again, that", + "Let us continue to suppose that", + "Notice, incidentally, that", + "A majority of informed linguistic specialists agree that", + ] + +subjects = [ + "the notion of level of grammaticalness", + "a case of semigrammaticalness of a different sort", + "most of the methodological work in modern linguistics", + "a subset of English sentences interesting on quite independent grounds", + "the natural general principle that will subsume this case", + "an important property of these three types of EC", + "any associated supporting element", + "the appearance of parasitic gaps in domains relatively inaccessible to ordinary extraction", + "the speaker-hearer's linguistic intuition", + "the descriptive power of the base component", + "the earlier discussion of deviance", + "this analysis of a formative as a pair of sets of features", + "this selectionally introduced contextual feature", + "a descriptively adequate grammar", + "the fundamental error of regarding functional notions as categorial", + "relational information", + "the systematic use of complex symbols", + "the theory of syntactic features developed earlier", + ] + +verbs= [ + "can be defined in such a way as to impose", + "delimits", + "suffices to account for", + "cannot be arbitrary in", + "is not subject to", + "does not readily tolerate", + "raises serious doubts about", + "is not quite equivalent to", + "does not affect the structure of", + "may remedy and, at the same time, eliminate", + "is not to be considered in determining", + "is to be regarded as", + "is unspecified with respect to", + "is, apparently, determined by", + "is necessary to impose an interpretation on", + "appears to correlate rather closely with", + "is rather different from", + ] + +objects = [ + "problems of phonemic and morphological analysis.", + "a corpus of utterance tokens upon which conformity has been defined by the paired utterance test.", + "the traditional practice of grammarians.", + "the levels of acceptability from fairly high (e.g. (99a)) to virtual gibberish (e.g. (98d)).", + "a stipulation to place the constructions into these various categories.", + "a descriptive fact.", + "a parasitic gap construction.", + "the extended c-command discussed in connection with (34).", + "the ultimate standard that determines the accuracy of any proposed grammar.", + "the system of base rules exclusive of the lexicon.", + "irrelevant intervening contexts in selectional rules.", + "nondistinctness in the sense of distinctive feature theory.", + "a general convention regarding the forms of the grammar.", + "an abstract underlying order.", + "an important distinction in language use.", + "the requirement that branching is not tolerated within the dominance scope of a complex symbol.", + "the strong generative capacity of the theory.", + ] + +def format_wisdom(text,line_length=72): + try: + import textwrap + return textwrap.fill(text, line_length) + except: + return text + +def chomsky(times = 1): + if not isinstance(times, int): + return format_wisdom(__doc__) + import random + prevparts = [] + newparts = [] + output = [] + for i in xrange(times): + for partlist in (leadins, subjects, verbs, objects): + while 1: + part = random.choice(partlist) + if part not in prevparts: + break + newparts.append(part) + output.append(' '.join(newparts)) + prevparts = newparts + newparts = [] + return format_wisdom(' '.join(output)) + +from reportlab import rl_config +if rl_config.invariant: + if not getattr(rl_config,'_random',None): + rl_config._random = 1 + import random + random.seed(2342471922L) + del random +del rl_config + +def randomText(theme=STARTUP, sentences=5): + #this may or may not be appropriate in your company + if type(theme)==type(''): + if theme.lower()=='chomsky': return chomsky(sentences) + elif theme.upper() in ('STARTUP','COMPUTERS','BLAH','BUZZWORD','STARTREK','PRINTING','PYTHON'): + theme = globals()[theme] + else: + raise ValueError('Unknown theme "%s"' % theme) + + from random import randint, choice + + RANDOMWORDS = theme + + #sentences = 5 + output = "" + for sentenceno in range(randint(1,sentences)): + output = output + 'Blah' + for wordno in range(randint(10,25)): + if randint(0,4)==0: + word = choice(RANDOMWORDS) + else: + word = 'blah' + output = output + ' ' +word + output = output+'. ' + return output + +if __name__=='__main__': + print chomsky(5) diff --git a/bin/reportlab/lib/rltempfile.py b/bin/reportlab/lib/rltempfile.py new file mode 100644 index 00000000000..d4e58aab507 --- /dev/null +++ b/bin/reportlab/lib/rltempfile.py @@ -0,0 +1,29 @@ +#Copyright ReportLab Europe Ltd. 2000-2006 +#see license.txt for license details +# $URI:$ +__version__=''' $Id: rltempfile.py 2892 2006-05-19 14:16:02Z rgbecker $ ''' +_rl_tempdir=None +__all__ = ('get_rl_tempdir', 'get_rl_tempdir') +import os, tempfile +def _rl_getuid(): + if hasattr(os,'getuid'): + return os.getuid() + else: + return '' + +def get_rl_tempdir(*subdirs): + global _rl_tempdir + if _rl_tempdir is None: + _rl_tempdir = os.path.join(tempfile.gettempdir(),'ReportLab_tmp%s' % str(_rl_getuid())) + d = _rl_tempdir + if subdirs: d = os.path.join(*((d,)+subdirs)) + try: + os.makedirs(d) + except: + pass + return d + +def get_rl_tempfile(fn=None): + if not fn: + fn = tempfile.mktemp() + return os.path.join(get_rl_tempdir(),fn) diff --git a/bin/reportlab/lib/rparsexml.py b/bin/reportlab/lib/rparsexml.py new file mode 100644 index 00000000000..37982050ee2 --- /dev/null +++ b/bin/reportlab/lib/rparsexml.py @@ -0,0 +1,431 @@ +"""Radically simple xml parsing + +Example parse + +text in xml + +( "this", + {"type": "xml"}, + [ "text ", + ("b", None, ["in"], None), + " xml" + ] + None ) + +{ 0: "this" + "type": "xml" + 1: ["text ", + {0: "b", 1:["in"]}, + " xml"] +} + +Ie, xml tag translates to a tuple: + (name, dictofattributes, contentlist, miscellaneousinfo) + +where miscellaneousinfo can be anything, (but defaults to None) +(with the intention of adding, eg, line number information) + +special cases: name of "" means "top level, no containing tag". +Top level parse always looks like this + + ("", list, None, None) + + contained text of None means + +In order to support stuff like + + + +AT THE MOMENT & ETCETERA ARE IGNORED. THEY MUST BE PROCESSED +IN A POST-PROCESSING STEP. + +PROLOGUES ARE NOT UNDERSTOOD. OTHER STUFF IS PROBABLY MISSING. +""" + +RequirePyRXP = 0 # set this to 1 to disable the nonvalidating fallback parser. + +import string +try: + #raise ImportError, "dummy error" + simpleparse = 0 + import pyRXPU + def warnCB(s): + print s + pyRXP_parser = pyRXPU.Parser( + ErrorOnValidityErrors=1, + NoNoDTDWarning=1, + ExpandCharacterEntities=1, + ExpandGeneralEntities=1, + warnCB = warnCB, + srcName='string input', + ReturnUTF8 = 1, + ) + def parsexml(xmlText, oneOutermostTag=0,eoCB=None,entityReplacer=None): + pyRXP_parser.eoCB = eoCB + p = pyRXP_parser.parse(xmlText) + return oneOutermostTag and p or ('',None,[p],None) +except ImportError: + simpleparse = 1 + +NONAME = "" +NAMEKEY = 0 +CONTENTSKEY = 1 +CDATAMARKER = "" +replacelist = [("<", "<"), (">", ">"), ("&", "&")] # amp must be last +#replacelist = [] +def unEscapeContentList(contentList): + result = [] + from string import replace + for e in contentList: + if "&" in e: + for (old, new) in replacelist: + e = replace(e, old, new) + result.append(e) + return result + +def parsexmlSimple(xmltext, oneOutermostTag=0,eoCB=None,entityReplacer=unEscapeContentList): + """official interface: discard unused cursor info""" + if RequirePyRXP: + raise ImportError, "pyRXP not found, fallback parser disabled" + (result, cursor) = parsexml0(xmltext,entityReplacer=entityReplacer) + if oneOutermostTag: + return result[2][0] + else: + return result + +if simpleparse: + parsexml = parsexmlSimple + +def parseFile(filename): + raw = open(filename, 'r').read() + return parsexml(raw) + +verbose = 0 + +def skip_prologue(text, cursor): + """skip any prologue found after cursor, return index of rest of text""" + ### NOT AT ALL COMPLETE!!! definitely can be confused!!! + from string import find + prologue_elements = ("!DOCTYPE", "?xml", "!--") + done = None + while done is None: + #print "trying to skip:", repr(text[cursor:cursor+20]) + openbracket = find(text, "<", cursor) + if openbracket<0: break + past = openbracket+1 + found = None + for e in prologue_elements: + le = len(e) + if text[past:past+le]==e: + found = 1 + cursor = find(text, ">", past) + if cursor<0: + raise ValueError, "can't close prologue %s" % `e` + cursor = cursor+1 + if found is None: + done=1 + #print "done skipping" + return cursor + +def parsexml0(xmltext, startingat=0, toplevel=1, + # snarf in some globals + strip=string.strip, split=string.split, find=string.find, entityReplacer=unEscapeContentList, + #len=len, None=None + #LENCDATAMARKER=LENCDATAMARKER, CDATAMARKER=CDATAMARKER + ): + """simple recursive descent xml parser... + return (dictionary, endcharacter) + special case: comment returns (None, endcharacter)""" + #from string import strip, split, find + #print "parsexml0", `xmltext[startingat: startingat+10]` + # DEFAULTS + NameString = NONAME + ContentList = AttDict = ExtraStuff = None + if toplevel is not None: + #if verbose: print "at top level" + #if startingat!=0: + # raise ValueError, "have to start at 0 for top level!" + xmltext = strip(xmltext) + cursor = startingat + #look for interesting starting points + firstbracket = find(xmltext, "<", cursor) + afterbracket2char = xmltext[firstbracket+1:firstbracket+3] + #print "a", `afterbracket2char` + #firstampersand = find(xmltext, "&", cursor) + #if firstampersand>0 and firstampersand0: + #afterbracket2char = xmltext[firstbracket:firstbracket+2] + if toplevel is not None: + #print "toplevel with no outer tag" + NameString = name = NONAME + cursor = skip_prologue(xmltext, cursor) + #break + elif firstbracket<0: + raise ValueError, "non top level entry should be at start tag: %s" % repr(xmltext[:10]) + # special case: CDATA + elif afterbracket2char=="![" and xmltext[firstbracket:firstbracket+9]=="": + raise ValueError, "invalid comment: contains double dashes %s" % repr(xmltext[cursor:cursor+20]) + return (None, endcomment+1) # shortcut exit + else: + # get the rest of the tag + #if verbose: print "parsing start tag" + # make sure the tag isn't in doublequote pairs + closebracket = find(xmltext, ">", firstbracket) + noclose = closebracket<0 + startsearch = closebracket+1 + pastfirstbracket = firstbracket+1 + tagcontent = xmltext[pastfirstbracket:closebracket] + # shortcut, no equal means nothing but name in the tag content + if '=' not in tagcontent: + if tagcontent[-1]=="/": + # simple case + #print "simple case", tagcontent + tagcontent = tagcontent[:-1] + docontents = None + name = strip(tagcontent) + NameString = name + cursor = startsearch + else: + if '"' in tagcontent: + # check double quotes + stop = None + # not inside double quotes! (the split should have odd length) + if noclose or len(split(tagcontent+".", '"'))% 2: + stop=1 + while stop is None: + closebracket = find(xmltext, ">", startsearch) + startsearch = closebracket+1 + noclose = closebracket<0 + tagcontent = xmltext[pastfirstbracket:closebracket] + # not inside double quotes! (the split should have odd length) + if noclose or len(split(tagcontent+".", '"'))% 2: + stop=1 + if noclose: + raise ValueError, "unclosed start tag %s" % repr(xmltext[firstbracket:firstbracket+20]) + cursor = startsearch + #cursor = closebracket+1 + # handle simple tag /> syntax + if xmltext[closebracket-1]=="/": + #if verbose: print "it's a simple tag" + closebracket = closebracket-1 + tagcontent = tagcontent[:-1] + docontents = None + #tagcontent = xmltext[firstbracket+1:closebracket] + tagcontent = strip(tagcontent) + taglist = split(tagcontent, "=") + #if not taglist: + # raise ValueError, "tag with no name %s" % repr(xmltext[firstbracket:firstbracket+20]) + taglist0 = taglist[0] + taglist0list = split(taglist0) + #if len(taglist0list)>2: + # raise ValueError, "bad tag head %s" % repr(taglist0) + name = taglist0list[0] + #print "tag name is", name + NameString = name + # now parse the attributes + attributename = taglist0list[-1] + # put a fake att name at end of last taglist entry for consistent parsing + taglist[-1] = taglist[-1]+" f" + AttDict = D = {} + taglistindex = 1 + lasttaglistindex = len(taglist) + #for attentry in taglist[1:]: + while taglistindexlasttaglistindex: + raise ValueError, "unclosed value " + repr(attentry) + nextattentry = taglist[taglistindex] + taglistindex = taglistindex+1 + attentry = "%s=%s" % (attentry, nextattentry) + attentry = strip(attentry) # only needed for while loop... + attlist = split(attentry) + nextattname = attlist[-1] + attvalue = attentry[:-len(nextattname)] + attvalue = strip(attvalue) + try: + first = attvalue[0]; last=attvalue[-1] + except: + raise ValueError, "attvalue,attentry,attlist="+repr((attvalue, attentry,attlist)) + if first==last=='"' or first==last=="'": + attvalue = attvalue[1:-1] + #print attributename, "=", attvalue + D[attributename] = attvalue + attributename = nextattname + # pass over other tags and content looking for end tag + if docontents is not None: + #print "now looking for end tag" + ContentList = L + while docontents is not None: + nextopenbracket = find(xmltext, "<", cursor) + if nextopenbracket", nextopenbracket) + if nextclosebracket\n%s\n" % (name, attributes, textpprint, name) + # otherwise must be a simple tag + return "<%s %s/>" % (name, attributes) + +dump = 0 +def testparse(s): + from time import time + from pprint import pprint + now = time() + D = parsexmlSimple(s) + print "DONE", time()-now + if dump&4: + pprint(D) + #pprint(D) + if dump&1: + print "============== reformatting" + p = pprettyprint(D) + print p + +def test(): + testparse("""text <>in xml + + text in xml ]]> + just testing brackets feature + """) + +filenames = [ #"../../reportlab/demos/pythonpoint/pythonpoint.xml", + "samples/hamlet.xml"] + +#filenames = ["moa.xml"] + +dump=1 +if __name__=="__main__": + test() + from time import time + now = time() + for f in filenames: + t = open(f).read() + print "parsing", f + testparse(t) + print "elapsed", time()-now diff --git a/bin/reportlab/lib/sequencer.py b/bin/reportlab/lib/sequencer.py new file mode 100644 index 00000000000..c9d9274d2d7 --- /dev/null +++ b/bin/reportlab/lib/sequencer.py @@ -0,0 +1,284 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/sequencer.py +__version__=''' $Id: sequencer.py 2423 2004-08-24 11:36:22Z rgbecker $ ''' +"""This module defines a single public class, Sequencer, which aids in +numbering and formatting lists.""" +# +# roman numbers conversion thanks to +# +# fredrik lundh, november 1996 (based on a C hack from 1984) +# +# fredrik@pythonware.com +# http://www.pythonware.com + +_RN_TEMPLATES = [ 0, 01, 011, 0111, 012, 02, 021, 0211, 02111, 013 ] +_RN_LETTERS = "IVXLCDM" + +from string import lower + + +def _format_I(value): + if value < 0 or value > 3999: + raise ValueError, "illegal value" + str = "" + base = -1 + while value: + value, index = divmod(value, 10) + tmp = _RN_TEMPLATES[index] + while tmp: + tmp, index = divmod(tmp, 8) + str = _RN_LETTERS[index+base] + str + base = base + 2 + return str + +def _format_i(num): + return lower(_format_I(num)) + +def _format_123(num): + """The simplest formatter""" + return str(num) + +def _format_ABC(num): + """Uppercase. Wraps around at 26.""" + n = (num -1) % 26 + return chr(n+65) + +def _format_abc(num): + """Lowercase. Wraps around at 26.""" + n = (num -1) % 26 + return chr(n+97) + + +class _Counter: + """Private class used by Sequencer. Each counter + knows its format, and the IDs of anything it + resets, as well as its value. Starts at zero + and increments just before you get the new value, + so that it is still 'Chapter 5' and not 'Chapter 6' + when you print 'Figure 5.1'""" + + def __init__(self): + self._base = 0 + self._value = self._base + self._formatter = _format_123 + self._resets = [] + + def setFormatter(self, formatFunc): + self._formatter = formatFunc + + def reset(self, value=None): + if value: + self._value = value + else: + self._value = self._base + + def next(self): + self._value = self._value + 1 + v = self._value + for counter in self._resets: + counter.reset() + return v + + def _this(self): + return self._value + + def nextf(self): + """Returns next value formatted""" + return self._formatter(self.next()) + + def thisf(self): + return self._formatter(self._this()) + + def chain(self, otherCounter): + if not otherCounter in self._resets: + self._resets.append(otherCounter) + + +class Sequencer: + """Something to make it easy to number paragraphs, sections, + images and anything else. The features include registering + new string formats for sequences, and 'chains' whereby + some counters are reset when their parents. + It keeps track of a number of + 'counters', which are created on request: + Usage: + >>> seq = layout.Sequencer() + >>> seq.next('Bullets') + 1 + >>> seq.next('Bullets') + 2 + >>> seq.next('Bullets') + 3 + >>> seq.reset('Bullets') + >>> seq.next('Bullets') + 1 + >>> seq.next('Figures') + 1 + >>> + """ + + def __init__(self): + self._counters = {} #map key to current number + self._defaultCounter = None + + self._formatters = { + # the formats it knows initially + '1':_format_123, + 'A':_format_ABC, + 'a':_format_abc, + 'I':_format_I, + 'i':_format_i, + } + + def _getCounter(self, counter=None): + """Creates one if not present""" + try: + return self._counters[counter] + except KeyError: + cnt = _Counter() + self._counters[counter] = cnt + return cnt + + def _this(self, counter=None): + """Retrieves counter value but does not increment. For + new counters, sets base value to 1.""" + if not counter: + counter = self._defaultCounter + return self._getCounter(counter)._this() + + def next(self, counter=None): + """Retrieves the numeric value for the given counter, then + increments it by one. New counters start at one.""" + if not counter: + counter = self._defaultCounter + return self._getCounter(counter).next() + + def thisf(self, counter=None): + if not counter: + counter = self._defaultCounter + return self._getCounter(counter).thisf() + + def nextf(self, counter=None): + """Retrieves the numeric value for the given counter, then + increments it by one. New counters start at one.""" + if not counter: + counter = self._defaultCounter + return self._getCounter(counter).nextf() + + def setDefaultCounter(self, default=None): + """Changes the key used for the default""" + self._defaultCounter = default + + def registerFormat(self, format, func): + """Registers a new formatting function. The funtion + must take a number as argument and return a string; + fmt is a short menmonic string used to access it.""" + self._formatters[format] = func + + def setFormat(self, counter, format): + """Specifies that the given counter should use + the given format henceforth.""" + func = self._formatters[format] + self._getCounter(counter).setFormatter(func) + + def reset(self, counter=None, base=0): + if not counter: + counter = self._defaultCounter + self._getCounter(counter)._value = base + + def chain(self, parent, child): + p = self._getCounter(parent) + c = self._getCounter(child) + p.chain(c) + + def __getitem__(self, key): + """Allows compact notation to support the format function. + s['key'] gets current value, s['key+'] increments.""" + if key[-1:] == '+': + counter = key[:-1] + return self.nextf(counter) + else: + return self.thisf(key) + + def format(self, template): + """The crowning jewels - formats multi-level lists.""" + return template % self + + def dump(self): + """Write current state to stdout for diagnostics""" + counters = self._counters.items() + counters.sort() + print 'Sequencer dump:' + for (key, counter) in counters: + print ' %s: value = %d, base = %d, format example = %s' % ( + key, counter._this(), counter._base, counter.thisf()) + + +"""Your story builder needs to set this to""" +_sequencer = None + +def getSequencer(): + global _sequencer + if _sequencer is None: + _sequencer = Sequencer() + return _sequencer + +def setSequencer(seq): + global _sequencer + s = _sequencer + _sequencer = seq + return s + +def test(): + s = Sequencer() + print 'Counting using default sequence: %d %d %d' % (s.next(),s.next(), s.next()) + print 'Counting Figures: Figure %d, Figure %d, Figure %d' % ( + s.next('figure'), s.next('figure'), s.next('figure')) + print 'Back to default again: %d' % s.next() + s.setDefaultCounter('list1') + print 'Set default to list1: %d %d %d' % (s.next(),s.next(), s.next()) + s.setDefaultCounter() + print 'Set default to None again: %d %d %d' % (s.next(),s.next(), s.next()) + print + print 'Creating Appendix counter with format A, B, C...' + s.setFormat('Appendix', 'A') + print ' Appendix %s, Appendix %s, Appendix %s' % ( + s.nextf('Appendix'), s.nextf('Appendix'),s.nextf('Appendix')) + + def format_french(num): + return ('un','deux','trois','quatre','cinq')[(num-1)%5] + print + print 'Defining a custom format with french words:' + s.registerFormat('french', format_french) + s.setFormat('FrenchList', 'french') + print ' ', + for i in range(1,6): + print s.nextf('FrenchList'), + print + print 'Chaining H1 and H2 - H2 goes back to one when H1 increases' + s.chain('H1','H2') + print ' H1 = %d' % s.next('H1') + print ' H2 = %d' % s.next('H2') + print ' H2 = %d' % s.next('H2') + print ' H2 = %d' % s.next('H2') + print ' H1 = %d' % s.next('H1') + print ' H2 = %d' % s.next('H2') + print ' H2 = %d' % s.next('H2') + print ' H2 = %d' % s.next('H2') + print + print 'GetItem notation - append a plus to increment' + print ' seq["Appendix"] = %s' % s["Appendix"] + print ' seq["Appendix+"] = %s' % s["Appendix+"] + print ' seq["Appendix+"] = %s' % s["Appendix+"] + print ' seq["Appendix"] = %s' % s["Appendix"] + print + print 'Finally, string format notation for nested lists. Cool!' + print 'The expression ("Figure %(Chapter)s.%(Figure+)s" % seq) gives:' + print ' Figure %(Chapter)s.%(Figure+)s' % s + print ' Figure %(Chapter)s.%(Figure+)s' % s + print ' Figure %(Chapter)s.%(Figure+)s' % s + + +if __name__=='__main__': + test() diff --git a/bin/reportlab/lib/set_ops.py b/bin/reportlab/lib/set_ops.py new file mode 100644 index 00000000000..eb44fb2abbc --- /dev/null +++ b/bin/reportlab/lib/set_ops.py @@ -0,0 +1,38 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/set_ops.py +import types +import string + +def __set_coerce(t, S): + if t is types.ListType: + return list(S) + elif t is types.TupleType: + return tuple(S) + elif t is types.StringType: + return string.join(S, '') + return S + +def unique(seq): + result = [] + for i in seq: + if i not in result: + result.append(i) + return __set_coerce(type(seq), result) + +def intersect(seq1, seq2): + result = [] + if type(seq1) != type(seq2) and type(seq2) == types.StringType: seq2 = list(seq2) + for i in seq1: + if i in seq2 and i not in result: result.append(i) + return __set_coerce(type(seq1), result) + +def union(seq1, seq2): + if type(seq1) == type(seq2): + return unique(seq1 + seq2) + if type(seq1) == types.ListType or type(seq2) == types.ListType: + return unique(list(seq1) + list(seq2)) + if type(seq1) == types.TupleType or type(seq2) == types.TupleType: + return unique(tuple(seq1) + tuple(seq2)) + return unique(list(seq1) + list(seq2)) diff --git a/bin/reportlab/lib/styles.py b/bin/reportlab/lib/styles.py new file mode 100644 index 00000000000..4da775eaaf5 --- /dev/null +++ b/bin/reportlab/lib/styles.py @@ -0,0 +1,257 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/styles.py +__version__=''' $Id: styles.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' + +from reportlab.lib.colors import white, black +from reportlab.lib.enums import TA_LEFT, TA_CENTER + +########################################################### +# This class provides an 'instance inheritance' +# mechanism for its descendants, simpler than acquisition +# but not as far-reaching +########################################################### +class PropertySet: + defaults = {} + + def __init__(self, name, parent=None, **kw): + """When initialized, it copies the class defaults; + then takes a copy of the attributes of the parent + if any. All the work is done in init - styles + should cost little to use at runtime.""" + # step one - validate the hell out of it + assert not self.defaults.has_key('name'), "Class Defaults may not contain a 'name' attribute" + assert not self.defaults.has_key('parent'), "Class Defaults may not contain a 'parent' attribute" + if parent: + assert parent.__class__ == self.__class__, "Parent style must have same class as new style" + + #step two + self.name = name + self.parent = parent + self.__dict__.update(self.defaults) + + #step two - copy from parent if any. Try to be + # very strict that only keys in class defaults are + # allowed, so they cannot inherit + self.refresh() + + #step three - copy keywords if any + for (key, value) in kw.items(): + self.__dict__[key] = value + + + def __repr__(self): + return "<%s '%s'>" % (self.__class__.__name__, self.name) + + def refresh(self): + """re-fetches attributes from the parent on demand; + use if you have been hacking the styles. This is + used by __init__""" + if self.parent: + for (key, value) in self.parent.__dict__.items(): + if (key not in ['name','parent']): + self.__dict__[key] = value + + + def listAttrs(self, indent=''): + print indent + 'name =', self.name + print indent + 'parent =', self.parent + keylist = self.__dict__.keys() + keylist.sort() + keylist.remove('name') + keylist.remove('parent') + for key in keylist: + value = self.__dict__.get(key, None) + print indent + '%s = %s' % (key, value) + +class ParagraphStyle(PropertySet): + defaults = { + 'fontName':'Times-Roman', + 'fontSize':10, + 'leading':12, + 'leftIndent':0, + 'rightIndent':0, + 'firstLineIndent':0, + 'alignment':TA_LEFT, + 'spaceBefore':0, + 'spaceAfter':0, + 'bulletFontName':'Times-Roman', + 'bulletFontSize':10, + 'bulletIndent':0, + 'textColor': black, + 'backColor':None, + 'wordWrap':None, + } + +class LineStyle(PropertySet): + defaults = { + 'width':1, + 'color': black + } + def prepareCanvas(self, canvas): + """You can ask a LineStyle to set up the canvas for drawing + the lines.""" + canvas.setLineWidth(1) + #etc. etc. + +class StyleSheet1: + """This may or may not be used. The idea is to + 1. slightly simplify construction of stylesheets; + 2. enforce rules to validate styles when added + (e.g. we may choose to disallow having both + 'heading1' and 'Heading1' - actual rules are + open to discussion); + 3. allow aliases and alternate style lookup + mechanisms + 4. Have a place to hang style-manipulation + methods (save, load, maybe support a GUI + editor) + Access is via getitem, so they can be + compatible with plain old dictionaries. + """ + def __init__(self): + self.byName = {} + self.byAlias = {} + + + def __getitem__(self, key): + try: + return self.byAlias[key] + except KeyError: + try: + return self.byName[key] + except KeyError: + raise KeyError, "Style '%s' not found in stylesheet" % key + + def has_key(self, key): + if self.byAlias.has_key(key): + return 1 + elif self.byName.has_key(key): + return 1 + else: + return 0 + + def add(self, style, alias=None): + key = style.name + if self.byName.has_key(key): + raise KeyError, "Style '%s' already defined in stylesheet" % key + if self.byAlias.has_key(key): + raise KeyError, "Style name '%s' is already an alias in stylesheet" % key + + if alias: + if self.byName.has_key(alias): + raise KeyError, "Style '%s' already defined in stylesheet" % alias + if self.byAlias.has_key(alias): + raise KeyError, "Alias name '%s' is already an alias in stylesheet" % alias + #passed all tests? OK, add it + self.byName[key] = style + if alias: + self.byAlias[alias] = style + + def list(self): + styles = self.byName.items() + styles.sort() + alii = {} + for (alias, style) in self.byAlias.items(): + alii[style] = alias + for (name, style) in styles: + alias = alii.get(style, None) + print name, alias + style.listAttrs(' ') + print + + + + +def testStyles(): + pNormal = ParagraphStyle('Normal',None) + pNormal.fontName = 'Times-Roman' + pNormal.fontSize = 12 + pNormal.leading = 14.4 + + pNormal.listAttrs() + print + pPre = ParagraphStyle('Literal', pNormal) + pPre.fontName = 'Courier' + pPre.listAttrs() + return pNormal, pPre + +def getSampleStyleSheet(): + """Returns a stylesheet object""" + stylesheet = StyleSheet1() + + stylesheet.add(ParagraphStyle(name='Normal', + fontName='Times-Roman', + fontSize=10, + leading=12) + ) + + stylesheet.add(ParagraphStyle(name='BodyText', + parent=stylesheet['Normal'], + spaceBefore=6) + ) + stylesheet.add(ParagraphStyle(name='Italic', + parent=stylesheet['BodyText'], + fontName = 'Times-Italic') + ) + + stylesheet.add(ParagraphStyle(name='Heading1', + parent=stylesheet['Normal'], + fontName = 'Times-Bold', + fontSize=18, + leading=22, + spaceAfter=6), + alias='h1') + + stylesheet.add(ParagraphStyle(name='Title', + parent=stylesheet['Normal'], + fontName = 'Times-Bold', + fontSize=18, + leading=22, + alignment=TA_CENTER, + spaceAfter=6), + alias='title') + + stylesheet.add(ParagraphStyle(name='Heading2', + parent=stylesheet['Normal'], + fontName = 'Times-Bold', + fontSize=14, + leading=18, + spaceBefore=12, + spaceAfter=6), + alias='h2') + + stylesheet.add(ParagraphStyle(name='Heading3', + parent=stylesheet['Normal'], + fontName = 'Times-BoldItalic', + fontSize=12, + leading=14, + spaceBefore=12, + spaceAfter=6), + alias='h3') + + stylesheet.add(ParagraphStyle(name='Bullet', + parent=stylesheet['Normal'], + firstLineIndent=0, + spaceBefore=3), + alias='bu') + + stylesheet.add(ParagraphStyle(name='Definition', + parent=stylesheet['Normal'], + firstLineIndent=0, + leftIndent=36, + bulletIndent=0, + spaceBefore=6, + bulletFontName='Times-BoldItalic'), + alias='df') + + stylesheet.add(ParagraphStyle(name='Code', + parent=stylesheet['Normal'], + fontName='Courier', + fontSize=8, + leading=8.8, + firstLineIndent=0, + leftIndent=36)) + + + return stylesheet diff --git a/bin/reportlab/lib/textsplit.py b/bin/reportlab/lib/textsplit.py new file mode 100644 index 00000000000..5c6733c788a --- /dev/null +++ b/bin/reportlab/lib/textsplit.py @@ -0,0 +1,210 @@ +#Copyright ReportLab Europe Ltd. 2000-2006 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/textsplit.py + +"""Helpers for text wrapping, hyphenation, Asian text splitting and kinsoku shori. + +How to split a 'big word' depends on the language and the writing system. This module +works on a Unicode string. It ought to grow by allowing ore algoriths to be plugged +in based on possible knowledge of the language and desirable 'niceness' of the algorithm. + +""" + +__version__=''' $Id: textsplit.py 2833 2006-04-05 16:01:20Z rgbecker $ ''' + +from types import StringType, UnicodeType +import unicodedata +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.rl_config import _FUZZ + +CANNOT_START_LINE = [ + #strongly prohibited e.g. end brackets, stop, exclamation... + u'!\',.:;?!")]\u3001\u3002\u300d\u300f\u3011\u3015\uff3d\u3011\uff09', + #middle priority e.g. continuation small vowels - wrapped on two lines but one string... + u'\u3005\u2015\u3041\u3043\u3045\u3047\u3049\u3063\u3083\u3085\u3087\u308e\u30a1\u30a3' + u'\u30a5\u30a7\u30a9\u30c3\u30e3\u30e5\u30e7\u30ee\u30fc\u30f5\u30f6', + #weakly prohibited - continuations, celsius symbol etc. + u'\u309b\u309c\u30fb\u30fd\u30fe\u309d\u309e\u2015\u2010\xb0\u2032\u2033\u2103\uffe0\uff05\u2030' + ] + +ALL_CANNOT_START = u''.join(CANNOT_START_LINE) +CANNOT_END_LINE = [ + #strongly prohibited + u'\u2018\u201c\uff08[{\uff08\u3014\uff3b\uff5b\u3008\u300a\u300c\u300e\u3010', + #weaker - currency symbols, hash, postcode - prefixes + u'$\u00a3@#\uffe5\uff04\uffe1\uff20\u3012\u00a7' + ] +ALL_CANNOT_END = u''.join(CANNOT_END_LINE) +def getCharWidths(word, fontName, fontSize): + """Returns a list of glyph widths. Should be easy to optimize in _rl_accel + + >>> getCharWidths('Hello', 'Courier', 10) + [6.0, 6.0, 6.0, 6.0, 6.0] + >>> from reportlab.pdfbase.cidfonts import UnicodeCIDFont + >>> from reportlab.pdfbase.pdfmetrics import registerFont + >>> registerFont(UnicodeCIDFont('HeiseiMin-W3')) + >>> getCharWidths(u'\u6771\u4EAC', 'HeiseiMin-W3', 10) #most kanji are 100 ems + [10.0, 10.0] + """ + #character-level function call; the performance is going to SUCK + + return [stringWidth(uChar, fontName, fontSize) for uChar in word] + +def wordSplit(word, availWidth, fontName, fontSize, encoding='utf8'): + """Attempts to break a word which lacks spaces into two parts, the first of which + fits in the remaining space. It is allowed to add hyphens or whatever it wishes. + + This is intended as a wrapper for some language- and user-choice-specific splitting + algorithms. It should only be called after line breaking on spaces, which covers western + languages and is highly optimised already. It works on the 'last unsplit word'. + + Presumably with further study one could write a Unicode splitting algorithm for text + fragments whick was much faster. + + Courier characters should be 6 points wide. + >>> wordSplit('HelloWorld', 30, 'Courier', 10) + [[0.0, 'Hello'], [0.0, 'World']] + >>> wordSplit('HelloWorld', 31, 'Courier', 10) + [[1.0, 'Hello'], [1.0, 'World']] + """ + if type(word) is not UnicodeType: + uword = word.decode(encoding) + else: + uword = word + + charWidths = getCharWidths(uword, fontName, fontSize) + lines = dumbSplit(uword, charWidths, availWidth) + + if type(word) is not UnicodeType: + lines2 = [] + #convert back + for (extraSpace, text) in lines: + lines2.append([extraSpace, text.encode(encoding)]) + lines = lines2 + + return lines + +def dumbSplit(word, widths, availWidth): + """This function attempts to fit as many characters as possible into the available + space, cutting "like a knife" between characters. This would do for Chinese. + It returns a list of (text, extraSpace) items where text is a Unicode string, + and extraSpace is the points of unused space available on the line. This is a + structure which is fairly easy to display, and supports 'backtracking' approaches + after the fact. + + Test cases assume each character is ten points wide... + + >>> dumbSplit(u'Hello', [10]*5, 60) + [[10.0, u'Hello']] + >>> dumbSplit(u'Hello', [10]*5, 50) + [[0.0, u'Hello']] + >>> dumbSplit(u'Hello', [10]*5, 40) + [[0.0, u'Hell'], [30, u'o']] + """ + + _more = """ + #>>> dumbSplit(u'Hello', [10]*5, 4) # less than one character + #(u'', u'Hello') + # this says 'Nihongo wa muzukashii desu ne!' (Japanese is difficult isn't it?) in 12 characters + >>> jtext = u'\u65e5\u672c\u8a9e\u306f\u96e3\u3057\u3044\u3067\u3059\u306d\uff01' + >>> dumbSplit(jtext, [10]*11, 30) # + (u'\u65e5\u672c\u8a9e', u'\u306f\u96e3\u3057\u3044\u3067\u3059\u306d\uff01') + """ + assert type(word) is UnicodeType + lines = [] + widthUsed = 0.0 + lineStartPos = 0 + for (i, w) in enumerate(widths): + widthUsed += w + if widthUsed > availWidth + _FUZZ: + #used more than can fit... + #ping out with previous cut, then set up next line with one character + + extraSpace = availWidth - widthUsed + w + #print 'ending a line; used %d, available %d' % (widthUsed, availWidth) + selected = word[lineStartPos:i] + + + #This is the most important of the Japanese typography rules. + #if next character cannot start a line, wrap it up to this line so it hangs + #in the right margin. We won't do two or more though - that's unlikely and + #would result in growing ugliness. + nextChar = word[i] + if nextChar in ALL_CANNOT_START: + #it's punctuation or a closing bracket of some kind. 'wrap up' + #so it stays on the line above, slightly exceeding our target width. + #print 'wrapping up', repr(nextChar) + selected += nextChar + extraSpace -= w + i += 1 + + + + lines.append([extraSpace, selected]) + lineStartPos = i + widthUsed = w + i -= 1 + #any characters left? + if widthUsed > 0: + extraSpace = availWidth - widthUsed + lines.append([extraSpace, word[lineStartPos:]]) + + return lines + +def kinsokuShoriSplit(word, widths, availWidth): + #NOT USED OR FINISHED YET! + """Split according to Japanese rules according to CJKV (Lunde). + + Essentially look for "nice splits" so that we don't end a line + with an open bracket, or start one with a full stop, or stuff like + that. There is no attempt to try to split compound words into + constituent kanji. It currently uses wrap-down: packs as much + on a line as possible, then backtracks if needed + + + This returns a number of words each of which should just about fit + on a line. If you give it a whole paragraph at once, it will + do all the splits. + + It's possible we might slightly step over the width limit + if we do hanging punctuation marks in future (e.g. dangle a Japanese + full stop in the right margin rather than using a whole character + box. + + """ + lines = [] + assert len(word) == len(widths) + curWidth = 0.0 + curLine = [] + i = 0 #character index - we backtrack at times so cannot use for loop + while 1: + ch = word[i] + w = widths[i] + if curWidth + w < availWidth: + curLine.append(ch) + curWidth += w + else: + #end of line. check legality + if ch in CANNOT_END_LINE[0]: + pass + #to be completed + + +# This recipe refers: +# +# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/148061 +import re +rx=re.compile(u"([\u2e80-\uffff])", re.UNICODE) +def cjkwrap(text, width, encoding="utf8"): + return reduce(lambda line, word, width=width: '%s%s%s' % + (line, + [' ','\n', ''][(len(line)-line.rfind('\n')-1 + + len(word.split('\n',1)[0] ) >= width) or + line[-1:] == '\0' and 2], + word), + rx.sub(r'\1\0 ', unicode(text,encoding)).split(' ') + ).replace('\0', '').encode(encoding) + +if __name__=='__main__': + import doctest, textsplit + doctest.testmod(textsplit) diff --git a/bin/reportlab/lib/tocindex.py b/bin/reportlab/lib/tocindex.py new file mode 100644 index 00000000000..d94189fa630 --- /dev/null +++ b/bin/reportlab/lib/tocindex.py @@ -0,0 +1,294 @@ +# Tables of Contents and Indices +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/tocindex.py +__version__=''' $Id: tocindex.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__='' +""" +This module will contain standard Table of Contents and Index objects. +under development, and pending certain hooks adding in DocTemplate +As of today, it onyl handles the formatting aspect of TOCs +""" + +import string + +from reportlab.platypus import Flowable, BaseDocTemplate, Paragraph, \ + PageBreak, Frame, PageTemplate, NextPageTemplate +from reportlab.platypus.doctemplate import IndexingFlowable +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.platypus import tables +from reportlab.lib import enums +from reportlab.lib import colors +from reportlab.lib.units import inch, cm +from reportlab.rl_config import defaultPageSize + + ############################################################## + # + # we first define a paragraph style for each level of the + # table, and one for the table as whole; you can supply your + # own. + # + ############################################################## + + +levelZeroParaStyle = ParagraphStyle(name='LevelZero', + fontName='Times-Roman', + fontSize=10, + leading=12) +levelOneParaStyle = ParagraphStyle(name='LevelOne', + parent = levelZeroParaStyle, + firstLineIndent = 0, + leftIndent = 18) +levelTwoParaStyle = ParagraphStyle(name='LevelTwo', + parent = levelOneParaStyle, + firstLineIndent = 0, + leftIndent = 36) +levelThreeParaStyle = ParagraphStyle(name='LevelThree', + parent = levelTwoParaStyle, + firstLineIndent = 0, + leftIndent = 54) + +defaultTableStyle = tables.TableStyle([ + ('VALIGN',(0,0),(-1,-1),'TOP'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ]) + + + + + + +class TableOfContents0(IndexingFlowable): + """This creates a formatted table of contents. It presumes + a correct block of data is passed in. The data block contains + a list of (level, text, pageNumber) triplets. You can supply + a paragraph style for each level (starting at zero).""" + def __init__(self): + self.entries = [] + self.rightColumnWidth = 72 + self.levelStyles = [levelZeroParaStyle, + levelOneParaStyle, + levelTwoParaStyle, + levelThreeParaStyle] + self.tableStyle = defaultTableStyle + self._table = None + self._entries = [] + self._lastEntries = [] + + def beforeBuild(self): + # keep track of the last run + self._lastEntries = self._entries[:] + self.clearEntries() + + def isIndexing(self): + return 1 + + def isSatisfied(self): + return (self._entries == self._lastEntries) + + def notify(self, kind, stuff): + """DocTemplate framework can call this with all kinds + of events; we say we are interested in 'TOCEntry' + events.""" + if kind == 'TOCEntry': + (level, text, pageNum) = stuff + self.addEntry(level, text, pageNum) + #print 'TOC notified of ', stuff +## elif kind == 'debug': +## # hack to watch its state +## import pprint +## print 'Last Entries first 5:' +## for (level, text, pageNum) in self._lastEntries[0:5]: +## print (level, text[0:30], pageNum), +## print +## print 'Current Entries first 5:' +## for (level, text, pageNum) in self._lastEntries[0:5]: +## print (level, text[0:30], pageNum), + + + def clearEntries(self): + self._entries = [] + + def addEntry(self, level, text, pageNum): + """Adds one entry; allows incremental buildup by a doctemplate. + Requires that enough styles are defined.""" + assert type(level) == type(1), "Level must be an integer" + assert level < len(self.levelStyles), \ + "Table of contents must have a style defined " \ + "for paragraph level %d before you add an entry" % level + self._entries.append((level, text, pageNum)) + + def addEntries(self, listOfEntries): + """Bulk creation. If you knew the titles but + not the page numbers, you could supply them to + get sensible output on the first run.""" + for (level, text, pageNum) in listOfEntries: + self.addEntry(level, text, pageNum) + + def wrap(self, availWidth, availHeight): + """All table properties should be known by now.""" + widths = (availWidth - self.rightColumnWidth, + self.rightColumnWidth) + + # makes an internal table which does all the work. + # we draw the LAST RUN's entries! If there are + # none, we make some dummy data to keep the table + # from complaining + if len(self._lastEntries) == 0: + _tempEntries = [(0,'Placeholder for table of contents',0)] + else: + _tempEntries = self._lastEntries + tableData = [] + for (level, text, pageNum) in _tempEntries: + leftColStyle = self.levelStyles[level] + #right col style is right aligned + rightColStyle = ParagraphStyle(name='leftColLevel%d' % level, + parent=leftColStyle, + leftIndent=0, + alignment=enums.TA_RIGHT) + leftPara = Paragraph(text, leftColStyle) + rightPara = Paragraph(str(pageNum), rightColStyle) + tableData.append([leftPara, rightPara]) + self._table = tables.Table(tableData, colWidths=widths, + style=self.tableStyle) + self.width, self.height = self._table.wrap(availWidth, availHeight) + return (self.width, self.height) + + def split(self, availWidth, availHeight): + """At this stage we do not care about splitting the entries, + we wil just return a list of platypus tables. Presumably the + calling app has a pointer to the original TableOfContents object; + Platypus just sees tables.""" + return self._table.split(availWidth, availHeight) + + def drawOn(self, canvas, x, y, _sW=0): + """Don't do this at home! The standard calls for implementing + draw(); we are hooking this in order to delegate ALL the drawing + work to the embedded table object""" + self._table.drawOn(canvas, x, y, _sW) + + + + ################################################################################# + # + # everything from here down is concerned with creating a good example document + # i.e. test code as well as tutorial +PAGE_HEIGHT = defaultPageSize[1] +def getSampleTOCData(depth=3): + """Returns a longish block of page numbers and headings over 3 levels""" + from random import randint + pgNum = 2 + data = [] + for chapter in range(1,8): + data.append((0, """Chapter %d with a really long name which will hopefully + wrap onto a second line, fnding out if the right things happen with + full paragraphs n the table of contents""" % chapter, pgNum)) + pgNum = pgNum + randint(0,2) + if depth > 1: + for section in range(1,5): + data.append((1, 'Chapter %d Section %d' % (chapter, section), pgNum)) + pgNum = pgNum + randint(0,2) + if depth > 2: + for subSection in range(1,6): + data.append(2, 'Chapter %d Section %d Subsection %d' % + (chapter, section, subSection), + pgNum) + pgNum = pgNum + randint(0,1) + from pprint import pprint as pp + pp(data) + return data + + +def getSampleStory(depth=3): + """Makes a story with lots of paragraphs. Uses the random + TOC data and makes paragraphs to correspond to each.""" + from reportlab.platypus.doctemplate import randomText + from random import randint + + styles = getSampleStyleSheet() + TOCData = getSampleTOCData(depth) + + story = [Paragraph("This is a demo of the table of contents object", + styles['Heading1'])] + + toc = TableOfContents0() # empty on first pass + #toc.addEntries(TOCData) # init with random page numbers + story.append(toc) + + # the next full page should switch to internal page style + story.append(NextPageTemplate("Body")) + + # now make some paragraphs to correspond to it + for (level, text, pageNum) in TOCData: + if level == 0: + #page break before chapter + story.append(PageBreak()) + + headingStyle = (styles['Heading1'], styles['Heading2'], styles['Heading3'])[level] + headingPara = Paragraph(text, headingStyle) + story.append(headingPara) + # now make some body text + for i in range(randint(1,6)): + bodyPara = Paragraph(randomText(), + styles['Normal']) + story.append(bodyPara) + + return story + +class MyDocTemplate(BaseDocTemplate): + """Example of how to do the indexing. Need the onDraw hook + to find out which things are drawn on which pages""" + def afterInit(self): + """Set up the page templates""" + frameT = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='normal') + self.addPageTemplates([PageTemplate(id='Front',frames=frameT), + PageTemplate(id='Body',frames=frameT) + ]) + # just need a unique key generator for outline entries; + # easiest is to count all flowables in afterFlowable + # and set up a counter variable here + self._uniqueKey = 0 + + + def afterFlowable(self, flowable): + """Our rule for the table of contents is simply to take + the text of H1, H2 and H3 elements. We broadcast a + notification to the DocTemplate, which should inform + the TOC and let it pull them out. Also build an outline""" + self._uniqueKey = self._uniqueKey + 1 + + if hasattr(flowable, 'style'): + if flowable.style.name == 'Heading1': + self.notify('TOCEntry', (0, flowable.getPlainText(), self.page)) + self.canv.bookmarkPage(str(self._uniqueKey)) + self.canv.addOutlineEntry(flowable.getPlainText()[0:10], str(self._uniqueKey), 0) + + elif flowable.style.name == 'Heading2': + self.notify('TOCEntry', (1, flowable.getPlainText(), self.page)) + self.canv.bookmarkPage(str(self._uniqueKey)) + self.canv.addOutlineEntry(flowable.getPlainText(), str(self._uniqueKey), 1) + + elif flowable.style.name == 'Heading3': + self.notify('TOCEntry', (2, flowable.getPlainText(), self.page)) + self.canv.bookmarkPage(str(self._uniqueKey)) + self.canv.addOutlineEntry(flowable.getPlainText(), str(self._uniqueKey), 2) + + def beforePage(self): + """decorate the page""" + self.canv.saveState() + self.canv.setStrokeColor(colors.red) + self.canv.setLineWidth(5) + self.canv.line(66,72,66,PAGE_HEIGHT-72) + self.canv.setFont('Times-Roman',12) + self.canv.drawString(4 * inch, 0.75 * inch, "Page %d" % doc.page) + self.canv.restoreState() + +if __name__=='__main__': + from reportlab.platypus import SimpleDocTemplate + doc = MyDocTemplate('tocindex.pdf') + + #change this to depth=3 for a BIG document + story = getSampleStory(depth=2) + + doc.multiBuild(story, 'tocindex.pdf') \ No newline at end of file diff --git a/bin/reportlab/lib/units.py b/bin/reportlab/lib/units.py new file mode 100644 index 00000000000..79067d8462f --- /dev/null +++ b/bin/reportlab/lib/units.py @@ -0,0 +1,23 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/units.py +__version__=''' $Id: units.py 2524 2005-02-17 08:43:01Z rgbecker $ ''' + +inch = 72.0 +cm = inch / 2.54 +mm = cm * 0.1 +pica = 12.0 + +def toLength(s): + '''convert a string to a length''' + try: + if s[-2:]=='cm': return float(s[:-2])*cm + if s[-2:]=='in': return float(s[:-2])*inch + if s[-2:]=='pt': return float(s[:-2]) + if s[-1:]=='i': return float(s[:-1])*inch + if s[-2:]=='mm': return float(s[:-2])*mm + if s[-4:]=='pica': return float(s[:-4])*pica + return float(s) + except: + raise ValueError, "Can't convert '%s' to length" % s diff --git a/bin/reportlab/lib/utils.py b/bin/reportlab/lib/utils.py new file mode 100644 index 00000000000..c61dc8710a6 --- /dev/null +++ b/bin/reportlab/lib/utils.py @@ -0,0 +1,827 @@ +#Copyright ReportLab Europe Ltd. 2000-2006 +#see license.txt for license details +# $URI:$ +__version__=''' $Id: utils.py 2892 2006-05-19 14:16:02Z rgbecker $ ''' + +import string, os, sys, imp +from reportlab.lib.logger import warnOnce +from types import * +from rltempfile import get_rl_tempfile, get_rl_tempdir, _rl_getuid +SeqTypes = (ListType,TupleType) +if sys.hexversion<0x2020000: + def isSeqType(v): + return type(v) in SeqTypes +else: + def isSeqType(v): + return isinstance(v,(tuple,list)) + +if sys.hexversion<0x2030000: + True = 1 + False = 0 + +def _findFiles(dirList,ext='.ttf'): + from os.path import isfile, isdir, join as path_join + from os import listdir + ext = ext.lower() + R = [] + A = R.append + for D in dirList: + if not isdir(D): continue + for fn in listdir(D): + fn = path_join(D,fn) + if isfile(fn) and (not ext or fn.lower().endswith(ext)): A(fn) + return R + +try: + _UserDict = dict +except: + from UserDict import UserDict as _UserDict + +class CIDict(_UserDict): + def __init__(self,*a,**kw): + map(self.update, a) + self.update(kw) + + def update(self,D): + for k,v in D.items(): self[k] = v + + def __setitem__(self,k,v): + try: + k = k.lower() + except: + pass + _UserDict.__setitem__(self,k,v) + + def __getitem__(self,k): + try: + k = k.lower() + except: + pass + return _UserDict.__getitem__(self,k) + + def __delitem__(self,k): + try: + k = k.lower() + except: + pass + return _UserDict.__delitem__(self,k) + + def get(self,k,dv=None): + try: + return self[k] + except KeyError: + return dv + + def has_key(self,k): + try: + self[k] + return True + except: + return False + + def pop(self,k,*a): + try: + k = k.lower() + except: + pass + return _UserDict.pop(*((self,k)+a)) + + def setdefault(self,k,*a): + try: + k = k.lower() + except: + pass + return _UserDict.setdefault(*((self,k)+a)) + +if os.name == 'mac': + #with the Mac, we need to tag the file in a special + #way so the system knows it is a PDF file. + #This supplied by Joe Strout + import macfs, macostools + _KNOWN_MAC_EXT = { + 'BMP' : ('ogle','BMP '), + 'EPS' : ('ogle','EPSF'), + 'EPSF': ('ogle','EPSF'), + 'GIF' : ('ogle','GIFf'), + 'JPG' : ('ogle','JPEG'), + 'JPEG': ('ogle','JPEG'), + 'PCT' : ('ttxt','PICT'), + 'PICT': ('ttxt','PICT'), + 'PNG' : ('ogle','PNGf'), + 'PPM' : ('ogle','.PPM'), + 'TIF' : ('ogle','TIFF'), + 'TIFF': ('ogle','TIFF'), + 'PDF' : ('CARO','PDF '), + 'HTML': ('MSIE','TEXT'), + } + def markfilename(filename,creatorcode=None,filetype=None,ext='PDF'): + try: + if creatorcode is None or filetype is None and ext is not None: + try: + creatorcode, filetype = _KNOWN_MAC_EXT[string.upper(ext)] + except: + return + macfs.FSSpec(filename).SetCreatorType(creatorcode,filetype) + macostools.touched(filename) + except: + pass +else: + def markfilename(filename,creatorcode=None,filetype=None): + pass + +import reportlab +__RL_DIR=os.path.dirname(reportlab.__file__) #possibly relative +_RL_DIR=os.path.isabs(__RL_DIR) and __RL_DIR or os.path.abspath(__RL_DIR) +del reportlab + +#Attempt to detect if this copy of reportlab is running in a +#file system (as opposed to mostly running in a zip or McMillan +#archive or Jar file). This is used by test cases, so that +#we can write test cases that don't get activated in a compiled +try: + __file__ +except: + __file__ = sys.argv[0] +import glob, fnmatch +try: + _isFSD = not __loader__ + _archive = os.path.normcase(os.path.normpath(__loader__.archive)) + _archivepfx = _archive + os.sep + _archivedir = os.path.dirname(_archive) + _archivedirpfx = _archivedir + os.sep + _archivepfxlen = len(_archivepfx) + _archivedirpfxlen = len(_archivedirpfx) + def __startswith_rl(fn, + _archivepfx=_archivepfx, + _archivedirpfx=_archivedirpfx, + _archive=_archive, + _archivedir=_archivedir, + os_path_normpath=os.path.normpath, + os_path_normcase=os.path.normcase, + os_getcwd=os.getcwd, + os_sep=os.sep, + os_sep_len = len(os.sep)): + '''if the name starts with a known prefix strip it off''' + fn = os_path_normpath(fn.replace('/',os_sep)) + nfn = os_path_normcase(fn) + if nfn in (_archivedir,_archive): return 1,'' + if nfn.startswith(_archivepfx): return 1,fn[_archivepfxlen:] + if nfn.startswith(_archivedirpfx): return 1,fn[_archivedirpfxlen:] + cwd = os_path_normcase(os_getcwd()) + n = len(cwd) + if nfn.startswith(cwd): + if fn[n:].startswith(os_sep): return 1, fn[n+os_sep_len:] + if n==len(fn): return 1,'' + return not os.path.isabs(fn),fn + + def _startswith_rl(fn): + return __startswith_rl(fn)[1] + + def rl_glob(pattern,glob=glob.glob,fnmatch=fnmatch.fnmatch, _RL_DIR=_RL_DIR,pjoin=os.path.join): + c, pfn = __startswith_rl(pattern) + r = glob(pfn) + if c or r==[]: + r += map(lambda x,D=_archivepfx,pjoin=pjoin: pjoin(_archivepfx,x),filter(lambda x,pfn=pfn,fnmatch=fnmatch: fnmatch(x,pfn),__loader__._files.keys())) + return r +except: + _isFSD = os.path.isfile(__file__) #slight risk of wrong path + __loader__ = None + def _startswith_rl(fn): + return fn + def rl_glob(pattern,glob=glob.glob): + return glob(pattern) +del glob, fnmatch +_isFSSD = _isFSD and os.path.isfile(os.path.splitext(__file__)[0] +'.py') + +def isFileSystemDistro(): + '''return truth if a file system distribution''' + return _isFSD + +def isCompactDistro(): + '''return truth if not a file system distribution''' + return not _isFSD + +def isSourceDistro(): + '''return truth if a source file system distribution''' + return _isFSSD + +try: + #raise ImportError + ### NOTE! FP_STR SHOULD PROBABLY ALWAYS DO A PYTHON STR() CONVERSION ON ARGS + ### IN CASE THEY ARE "LAZY OBJECTS". ACCELLERATOR DOESN'T DO THIS (YET) + try: + from _rl_accel import fp_str # in case of builtin version + except ImportError: + from reportlab.lib._rl_accel import fp_str # specific +except ImportError: + from math import log + _log_10 = lambda x,log=log,_log_e_10=log(10.0): log(x)/_log_e_10 + _fp_fmts = "%.0f", "%.1f", "%.2f", "%.3f", "%.4f", "%.5f", "%.6f" + import re + _tz_re = re.compile('0+$') + del re + def fp_str(*a): + if len(a)==1 and isSeqType(a[0]): a = a[0] + s = [] + A = s.append + for i in a: + sa =abs(i) + if sa<=1e-7: A('0') + else: + l = sa<=1 and 6 or min(max(0,(6-int(_log_10(sa)))),6) + n = _fp_fmts[l]%i + if l: + n = _tz_re.sub('',n) + try: + if n[-1]=='.': n = n[:-1] + except: + print i, n + raise + A((n[0]!='0' or len(n)==1) and n or n[1:]) + return string.join(s) + +#hack test for comma users +if ',' in fp_str(0.25): + _FP_STR = fp_str + def fp_str(*a): + return string.replace(apply(_FP_STR,a),',','.') + +def recursiveImport(modulename, baseDir=None, noCWD=0, debug=0): + """Dynamically imports possible packagized module, or raises ImportError""" + normalize = lambda x: os.path.normcase(os.path.abspath(os.path.normpath(x))) + path = map(normalize,sys.path) + if baseDir: + if not isSeqType(baseDir): + tp = [baseDir] + else: + tp = filter(None,list(baseDir)) + for p in tp: + p = normalize(p) + if p not in path: path.insert(0,p) + + if noCWD: + for p in ('','.',normalize('.')): + while p in path: + if debug: print 'removed "%s" from path' % p + path.remove(p) + elif '.' not in path: + path.insert(0,'.') + + if debug: + import pprint + pp = pprint.pprint + print 'path=', + pp(path) + + #make import errors a bit more informative + opath = sys.path + try: + sys.path = path + exec 'import %s\nm = %s\n' % (modulename,modulename) in locals() + sys.path = opath + return m + except ImportError: + sys.path = opath + msg = "recursiveimport(%s,baseDir=%s) failed" % (modulename,baseDir) + if baseDir: + msg = msg + " under paths '%s'" % `path` + raise ImportError, msg + +def recursiveGetAttr(obj, name): + "Can call down into e.g. object1.object2[4].attr" + return eval(name, obj.__dict__) + +def recursiveSetAttr(obj, name, value): + "Can call down into e.g. object1.object2[4].attr = value" + #get the thing above last. + tokens = string.split(name, '.') + if len(tokens) == 1: + setattr(obj, name, value) + else: + most = string.join(tokens[:-1], '.') + last = tokens[-1] + parent = recursiveGetAttr(obj, most) + setattr(parent, last, value) + +def import_zlib(): + try: + import zlib + except ImportError: + zlib = None + from reportlab.rl_config import ZLIB_WARNINGS + if ZLIB_WARNINGS: warnOnce('zlib not available') + return zlib + + +# Image Capability Detection. Set a flag haveImages +# to tell us if either PIL or Java imaging libraries present. +# define PIL_Image as either None, or an alias for the PIL.Image +# module, as there are 2 ways to import it + +if sys.platform[0:4] == 'java': + try: + import javax.imageio + import java.awt.image + haveImages = 1 + except: + haveImages = 0 +else: + try: + from PIL import Image + except ImportError: + try: + import Image + except ImportError: + Image = None + haveImages = Image is not None + if haveImages: del Image + + +__StringIO=None +def getStringIO(buf=None): + '''unified StringIO instance interface''' + global __StringIO + if not __StringIO: + try: + from cStringIO import StringIO + except ImportError: + from StringIO import StringIO + __StringIO = StringIO + return buf is not None and __StringIO(buf) or __StringIO() + +class ArgvDictValue: + '''A type to allow clients of getArgvDict to specify a conversion function''' + def __init__(self,value,func): + self.value = value + self.func = func + +def getArgvDict(**kw): + ''' Builds a dictionary from its keyword arguments with overrides from sys.argv. + Attempts to be smart about conversions, but the value can be an instance + of ArgDictValue to allow specifying a conversion function. + ''' + def handleValue(v,av,func): + if func: + v = func(av) + else: + t = type(v) + if t is StringType: + v = av + elif t is FloatType: + v = float(av) + elif t is IntType: + v = int(av) + elif t is ListType: + v = list(eval(av)) + elif t is TupleType: + v = tuple(eval(av)) + else: + raise TypeError, "Can't convert string '%s' to %s" % (av,str(t)) + return v + + A = sys.argv[1:] + R = {} + for k, v in kw.items(): + if isinstance(v,ArgvDictValue): + v, func = v.value, v.func + else: + func = None + handled = 0 + ke = k+'=' + for a in A: + if string.find(a,ke)==0: + av = a[len(ke):] + A.remove(a) + R[k] = handleValue(v,av,func) + handled = 1 + break + + if not handled: R[k] = handleValue(v,v,func) + + return R + +def getHyphenater(hDict=None): + try: + from reportlab.lib.pyHnj import Hyphen + if hDict is None: hDict=os.path.join(os.path.dirname(__file__),'hyphen.mashed') + return Hyphen(hDict) + except ImportError, errMsg: + if str(errMsg)!='No module named pyHnj': raise + return None + +def _className(self): + '''Return a shortened class name''' + try: + name = self.__class__.__name__ + i=string.rfind(name,'.') + if i>=0: return name[i+1:] + return name + except AttributeError: + return str(self) + +def open_for_read_by_name(name,mode='b'): + if 'r' not in mode: mode = 'r'+mode + try: + return open(name,mode) + except IOError: + if _isFSD or __loader__ is None: raise + #we have a __loader__, perhaps the filename starts with + #the dirname(reportlab.__file__) or is relative + name = _startswith_rl(name) + s = __loader__.get_data(name) + if 'b' not in mode and os.linesep!='\n': s = s.replace(os.linesep,'\n') + return getStringIO(s) + +import urllib +def open_for_read(name,mode='b', urlopen=urllib.urlopen): + '''attempt to open a file or URL for reading''' + if hasattr(name,'read'): return name + try: + return open_for_read_by_name(name,mode) + except: + try: + return getStringIO(urlopen(name).read()) + except: + raise IOError('Cannot open resource "%s"' % name) +del urllib + +def open_and_read(name,mode='b'): + return open_for_read(name,mode).read() + +def open_and_readlines(name,mode='t'): + return open_and_read(name,mode).split('\n') + +def rl_isfile(fn,os_path_isfile=os.path.isfile): + if hasattr(fn,'read'): return True + if os_path_isfile(fn): return True + if _isFSD or __loader__ is None: return False + fn = _startswith_rl(fn) + return fn in __loader__._files.keys() + +def rl_isdir(pn,os_path_isdir=os.path.isdir,os_path_normpath=os.path.normpath): + if os_path_isdir(pn): return True + if _isFSD or __loader__ is None: return False + pn = _startswith_rl(os_path_normpath(pn)) + if not pn.endswith(os.sep): pn += os.sep + return len(filter(lambda x,pn=pn: x.startswith(pn),__loader__._files.keys()))>0 + +def rl_get_module(name,dir): + if sys.modules.has_key(name): + om = sys.modules[name] + del sys.modules[name] + else: + om = None + try: + f = None + try: + f, p, desc= imp.find_module(name,[dir]) + return imp.load_module(name,f,p,desc) + except: + if isCompactDistro(): + #attempt a load from inside the zip archive + import zipimport + dir = _startswith_rl(dir) + dir = (dir=='.' or not dir) and _archive or os.path.join(_archive,dir.replace('/',os.sep)) + zi = zipimport.zipimporter(dir) + return zi.load_module(name) + raise ImportError('%s[%s]' % (name,dir)) + finally: + if om: sys.modules[name] = om + del om + if f: f.close() + +def _isPILImage(im): + try: + from PIL.Image import Image + return isinstance(im,Image) + except ImportError: + return 0 + +class ImageReader: + "Wraps up either PIL or Java to get data from bitmaps" + def __init__(self, fileName): + if isinstance(fileName,ImageReader): + self.__dict__ = fileName.__dict__ #borgize + return + if not haveImages: + raise RuntimeError('Imaging Library not available, unable to import bitmaps') + #start wih lots of null private fields, to be populated by + #the relevant engine. + self.fileName = fileName + self._image = None + self._width = None + self._height = None + self._transparent = None + self._data = None + if _isPILImage(fileName): + self._image = fileName + self.fp = fileName.fp + try: + self.fileName = im.fileName + except AttributeError: + self.fileName = 'PILIMAGE_%d' % id(self) + else: + try: + self.fp = open_for_read(fileName,'b') + #detect which library we are using and open the image + if sys.platform[0:4] == 'java': + from javax.imageio import ImageIO + self._image = ImageIO.read(self.fp) + else: + import PIL.Image + self._image = PIL.Image.open(self.fp) + if self._image=='JPEG': self.jpeg_fh = self._jpeg_fp + except: + et,ev,tb = sys.exc_info() + if hasattr(ev,'args'): + a = str(ev.args[-1])+(' fileName='+fileName) + ev.args= ev.args[:-1]+(a,) + raise et,ev,tb + else: + raise + + + def _jpeg_fh(self): + fp = self.fp + fp.seek(0) + return fp + + def jpeg_fh(self): + return None + + def getSize(self): + if (self._width is None or self._height is None): + if sys.platform[0:4] == 'java': + self._width = self._image.getWidth() + self._height = self._image.getHeight() + else: + self._width, self._height = self._image.size + return (self._width, self._height) + + def getRGBData(self): + "Return byte array of RGB data as string" + if self._data is None: + if sys.platform[0:4] == 'java': + import jarray + from java.awt.image import PixelGrabber + width, height = self.getSize() + buffer = jarray.zeros(width*height, 'i') + pg = PixelGrabber(self._image, 0,0,width,height,buffer,0,width) + pg.grabPixels() + # there must be a way to do this with a cast not a byte-level loop, + # I just haven't found it yet... + pixels = [] + a = pixels.append + for i in range(len(buffer)): + rgb = buffer[i] + a(chr((rgb>>16)&0xff)) + a(chr((rgb>>8)&0xff)) + a(chr(rgb&0xff)) + self._data = ''.join(pixels) + self.mode = 'RGB' + else: + im = self._image + mode = self.mode = im.mode + if mode not in ('L','RGB','CMYK'): + im = im.convert('RGB') + self.mode = 'RGB' + self._data = im.tostring() + return self._data + + def getImageData(self): + width, height = self.getSize() + return width, height, self.getRGBData() + + def getTransparent(self): + if sys.platform[0:4] == 'java': + return None + else: + if self._image.info.has_key("transparency"): + transparency = self._image.info["transparency"] * 3 + palette = self._image.palette + try: + palette = palette.palette + except: + palette = palette.data + return map(ord, palette[transparency:transparency+3]) + else: + return None + +def getImageData(imageFileName): + "Get width, height and RGB pixels from image file. Wraps Java/PIL" + try: + return imageFileName.getImageData() + except AttributeError: + return ImageReader(imageFileName).getImageData() + +class DebugMemo: + '''Intended as a simple report back encapsulator + + Typical usages + 1) To record error data + dbg = DebugMemo(fn='dbgmemo.dbg',myVar=value) + dbg.add(anotherPayload='aaaa',andagain='bbb') + dbg.dump() + + 2) To show the recorded info + dbg = DebugMemo(fn='dbgmemo.dbg',mode='r') + dbg.load() + dbg.show() + + 3) To re-use recorded information + dbg = DebugMemo(fn='dbgmemo.dbg',mode='r') + dbg.load() + myTestFunc(dbg.payload('myVar'),dbg.payload('andagain')) + + in addition to the payload variables the dump records many useful bits + of information which are also printed in the show() method. + ''' + def __init__(self,fn='rl_dbgmemo.dbg',mode='w',getScript=1,modules=(),capture_traceback=1, stdout=None, **kw): + import time, socket + self.fn = fn + if mode!='w': return + if not stdout: + self.stdout = sys.stdout + else: + if hasattr(stdout,'write'): + self.stdout = stdout + else: + self.stdout = open(stdout,'w') + self.store = store = {} + if capture_traceback and sys.exc_info() != (None,None,None): + import traceback + s = getStringIO() + traceback.print_exc(None,s) + store['__traceback'] = s.getvalue() + cwd=os.getcwd() + lcwd = os.listdir(cwd) + exed = os.path.abspath(os.path.dirname(sys.argv[0])) + store.update({ 'gmt': time.asctime(time.gmtime(time.time())), + 'platform': sys.platform, + 'version': sys.version, + 'hexversion': hex(sys.hexversion), + 'executable': sys.executable, + 'exec_prefix': sys.exec_prefix, + 'prefix': sys.prefix, + 'path': sys.path, + 'argv': sys.argv, + 'cwd': cwd, + 'hostname': socket.gethostname(), + 'lcwd': lcwd, + 'byteorder': sys.byteorder, + 'maxint': sys.maxint, + 'maxint': getattr(sys,'maxunicode','????'), + 'api_version': getattr(sys,'api_version','????'), + 'version_info': getattr(sys,'version_info','????'), + 'winver': getattr(sys,'winver','????'), + 'environment': os.environ, + }) + for M,A in ( + (sys,('getwindowsversion','getfilesystemencoding')), + (os,('uname', 'ctermid', 'getgid', 'getuid', 'getegid', + 'geteuid', 'getlogin', 'getgroups', 'getpgrp', 'getpid', 'getppid', + )), + ): + for a in A: + if hasattr(M,a): + try: + store[a] = getattr(M,a)() + except: + pass + if exed!=cwd: + try: + store.update({'exed': exed, 'lexed': os.listdir(exed),}) + except: + pass + if getScript: + fn = os.path.abspath(sys.argv[0]) + if os.path.isfile(fn): + try: + store['__script'] = (fn,open(fn,'r').read()) + except: + pass + module_versions = {} + for n,m in sys.modules.items(): + if n=='reportlab' or n=='rlextra' or n[:10]=='reportlab.' or n[:8]=='rlextra.': + v = getattr(m,'__version__',None) + if v: module_versions[n] = v + store['__module_versions'] = module_versions + self.store['__payload'] = {} + self._add(kw) + + def _add(self,D): + payload = self.store['__payload'] + for k, v in D.items(): + payload[k] = v + + def add(self,**kw): + self._add(kw) + + def dump(self): + import pickle + pickle.dump(self.store,open(self.fn,'wb')) + + def load(self): + import pickle + self.store = pickle.load(open(self.fn,'rb')) + + def _show_module_versions(self,k,v): + self._writeln(k[2:]) + K = v.keys() + K.sort() + for k in K: + vk = v[k] + try: + m = recursiveImport(k,sys.path[:],1) + d = getattr(m,'__version__',None)==vk and 'SAME' or 'DIFFERENT' + except: + m = None + d = '??????unknown??????' + self._writeln(' %s = %s (%s)' % (k,vk,d)) + + def _banner(self,k,what): + self._writeln('###################%s %s##################' % (what,k[2:])) + + def _start(self,k): + self._banner(k,'Start ') + + def _finish(self,k): + self._banner(k,'Finish ') + + def _show_lines(self,k,v): + self._start(k) + self._writeln(v) + self._finish(k) + + def _show_file(self,k,v): + k = '%s %s' % (k,os.path.basename(v[0])) + self._show_lines(k,v[1]) + + def _show_payload(self,k,v): + if v: + import pprint + self._start(k) + pprint.pprint(v,self.stdout) + self._finish(k) + + specials = {'__module_versions': _show_module_versions, + '__payload': _show_payload, + '__traceback': _show_lines, + '__script': _show_file, + } + def show(self): + K = self.store.keys() + K.sort() + for k in K: + if k not in self.specials.keys(): self._writeln('%-15s = %s' % (k,self.store[k])) + for k in K: + if k in self.specials.keys(): apply(self.specials[k],(self,k,self.store[k])) + + def payload(self,name): + return self.store['__payload'][name] + + def __setitem__(self,name,value): + self.store['__payload'][name] = value + + def __getitem__(self,name): + return self.store['__payload'][name] + + def _writeln(self,msg): + self.stdout.write(msg+'\n') + +def _flatten(L,a): + for x in L: + if isSeqType(x): _flatten(x,a) + else: a(x) + +def flatten(L): + '''recursively flatten the list or tuple L''' + R = [] + _flatten(L,R.append) + return R + +def find_locals(func,depth=0): + '''apply func to the locals at each stack frame till func returns a non false value''' + while 1: + _ = func(sys._getframe(depth).f_locals) + if _: return _ + depth += 1 + +class _FmtSelfDict: + def __init__(self,obj,overrideArgs): + self.obj = obj + self._overrideArgs = overrideArgs + def __getitem__(self,k): + try: + return self._overrideArgs[k] + except KeyError: + try: + return self.obj.__dict__[k] + except KeyError: + return getattr(self.obj,k) + +class FmtSelfDict: + '''mixin to provide the _fmt method''' + def _fmt(self,fmt,**overrideArgs): + D = _FmtSelfDict(self, overrideArgs) + return fmt % D diff --git a/bin/reportlab/lib/validators.py b/bin/reportlab/lib/validators.py new file mode 100644 index 00000000000..3f7f06edb79 --- /dev/null +++ b/bin/reportlab/lib/validators.py @@ -0,0 +1,324 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/lib/validators.py +__version__=''' $Id: validators.py 2875 2006-05-18 07:00:16Z andy $ ''' +""" +This module contains some standard verifying functions which can be +used in an attribute map. +""" + +import string, sys, codecs +from types import * +_SequenceTypes = (ListType,TupleType) +_NumberTypes = (FloatType,IntType) +from reportlab.lib import colors +if sys.hexversion<0x2030000: + True = 1 + False = 0 + +class Validator: + "base validator class" + def __call__(self,x): + return self.test(x) + + def __str__(self): + return getattr(self,'_str',self.__class__.__name__) + + def normalize(self,x): + return x + + def normalizeTest(self,x): + try: + self.normalize(x) + return True + except: + return False + +class _isAnything(Validator): + def test(self,x): + return True + +class _isNothing(Validator): + def test(self,x): + return False + +class _isBoolean(Validator): + if sys.hexversion>=0x2030000: + def test(self,x): + if type(x) in (IntType,BooleanType): return x in (0,1) + return self.normalizeTest(x) + else: + def test(self,x): + if type(x) is IntType: return x in (0,1) + return self.normalizeTest(x) + + def normalize(self,x): + if x in (0,1): return x + try: + S = string.upper(x) + except: + raise ValueError, 'Must be boolean' + if S in ('YES','TRUE'): return True + if S in ('NO','FALSE',None): return False + raise ValueError, 'Must be boolean' + +class _isString(Validator): + def test(self,x): + return type(x) in (StringType, UnicodeType) + +class _isCodec(Validator): + def test(self,x): + if type(x) not in (StringType, UnicodeType): + return False + try: + a,b,c,d = codecs.lookup(x) + return True + except LookupError: + return False + +class _isNumber(Validator): + def test(self,x): + if type(x) in _NumberTypes: return True + return self.normalizeTest(x) + + def normalize(self,x): + try: + return float(x) + except: + return int(x) + +class _isInt(Validator): + def test(self,x): + if type(x) not in (IntType,StringType): return False + return self.normalizeTest(x) + + def normalize(self,x): + return int(x) + +class _isNumberOrNone(_isNumber): + def test(self,x): + return x is None or isNumber(x) + + def normalize(self,x): + if x is None: return x + return _isNumber.normalize(x) + +class _isListOfNumbersOrNone(Validator): + "ListOfNumbersOrNone validator class." + def test(self, x): + if x is None: return True + return isListOfNumbers(x) + +class _isListOfShapes(Validator): + "ListOfShapes validator class." + def test(self, x): + from reportlab.graphics.shapes import Shape + if type(x) in _SequenceTypes: + answer = 1 + for element in x: + if not isinstance(x, Shape): + answer = 0 + return answer + else: + return False + +class _isListOfStringsOrNone(Validator): + "ListOfStringsOrNone validator class." + + def test(self, x): + if x is None: return True + return isListOfStrings(x) + +class _isTransform(Validator): + "Transform validator class." + def test(self, x): + if type(x) in _SequenceTypes: + if len(x) == 6: + for element in x: + if not isNumber(element): + return False + return True + else: + return False + else: + return False + +class _isColor(Validator): + "Color validator class." + def test(self, x): + return isinstance(x, colors.Color) + +class _isColorOrNone(Validator): + "ColorOrNone validator class." + def test(self, x): + if x is None: return True + return isColor(x) + +class _isValidChild(Validator): + "ValidChild validator class." + def test(self, x): + """Is this child allowed in a drawing or group? + I.e. does it descend from Shape or UserNode? + """ + + from reportlab.graphics.shapes import UserNode, Shape + return isinstance(x, UserNode) or isinstance(x, Shape) + +class _isValidChildOrNone(_isValidChild): + def test(self,x): + return _isValidChild.test(self,x) or x is None + +class _isCallable(Validator): + def test(self, x): + return callable(x) + +class OneOf(Validator): + """Make validator functions for list of choices. + + Usage: + f = reportlab.lib.validators.OneOf('happy','sad') + or + f = reportlab.lib.validators.OneOf(('happy','sad')) + f('sad'),f('happy'), f('grumpy') + (1,1,0) + """ + def __init__(self, enum,*args): + if type(enum) in [ListType,TupleType]: + if args!=(): + raise ValueError, "Either all singleton args or a single sequence argument" + self._enum = tuple(enum)+args + else: + self._enum = (enum,)+args + + def test(self, x): + return x in self._enum + +class SequenceOf(Validator): + def __init__(self,elemTest,name=None,emptyOK=1, NoneOK=0, lo=0,hi=0x7fffffff): + self._elemTest = elemTest + self._emptyOK = emptyOK + self._NoneOK = NoneOK + self._lo, self._hi = lo, hi + if name: self._str = name + + def test(self, x): + if type(x) not in _SequenceTypes: + if x is None: return self._NoneOK + return False + if x==[] or x==(): + return self._emptyOK + elif not self._lo<=len(x)<=self._hi: return False + for e in x: + if not self._elemTest(e): return False + return True + +class EitherOr(Validator): + def __init__(self,tests,name=None): + if type(tests) not in _SequenceTypes: tests = (tests,) + self._tests = tests + if name: self._str = name + + def test(self, x): + for t in self._tests: + if t(x): return True + return False + +class NoneOr(Validator): + def __init__(self,elemTest,name=None): + self._elemTest = elemTest + if name: self._str = name + + def test(self, x): + if x is None: return True + return self._elemTest(x) + +class Auto(Validator): + def __init__(self,**kw): + self.__dict__.update(kw) + + def test(self,x): + return x is self.__class__ or isinstance(x,self.__class__) + +class AutoOr(NoneOr): + def test(self,x): + return isAuto(x) or self._elemTest(x) + +class isInstanceOf(Validator): + def __init__(self,klass=None): + self._klass = klass + def test(self,x): + return isinstance(x,self._klass) + +class matchesPattern(Validator): + """Matches value, or its string representation, against regex""" + def __init__(self, pattern): + self._pattern = re.compile(pattern) + + def test(self,x): + print 'testing %s against %s' % (x, self._pattern) + if type(x) is StringType: + text = x + else: + text = str(x) + return (self._pattern.match(text) <> None) + +class DerivedValue: + """This is used for magic values which work themselves out. + An example would be an "inherit" property, so that one can have + + drawing.chart.categoryAxis.labels.fontName = inherit + + and pick up the value from the top of the drawing. + Validators will permit this provided that a value can be pulled + in which satisfies it. And the renderer will have special + knowledge of these so they can evaluate themselves. + """ + def getValue(self, renderer, attr): + """Override this. The renderers will pass the renderer, + and the attribute name. Algorithms can then backtrack up + through all the stuff the renderer provides, including + a correct stack of parent nodes.""" + return None + +class Inherit(DerivedValue): + def __repr__(self): + return "inherit" + + def getValue(self, renderer, attr): + return renderer.getStateValue(attr) + +inherit = Inherit() + + +isAuto = Auto() +isBoolean = _isBoolean() +isString = _isString() +isCodec = _isCodec() +isNumber = _isNumber() +isInt = _isInt() +isNoneOrInt = NoneOr(isInt,'isNoneOrInt') +isNumberOrNone = _isNumberOrNone() +isTextAnchor = OneOf('start','middle','end','boxauto') +isListOfNumbers = SequenceOf(isNumber,'isListOfNumbers') +isListOfNumbersOrNone = _isListOfNumbersOrNone() +isListOfShapes = _isListOfShapes() +isListOfStrings = SequenceOf(isString,'isListOfStrings') +isListOfStringsOrNone = _isListOfStringsOrNone() +isTransform = _isTransform() +isColor = _isColor() +isListOfColors = SequenceOf(isColor,'isListOfColors') +isColorOrNone = _isColorOrNone() +isShape = isValidChild = _isValidChild() +isNoneOrShape = isValidChildOrNone = _isValidChildOrNone() +isAnything = _isAnything() +isNothing = _isNothing() +isXYCoord = SequenceOf(isNumber,lo=2,hi=2,emptyOK=0) +isBoxAnchor = OneOf('nw','n','ne','w','c','e','sw','s','se', 'autox', 'autoy') +isNoneOrString = NoneOr(isString,'NoneOrString') +isNoneOrListOfNoneOrStrings=SequenceOf(isNoneOrString,'isNoneOrListOfNoneOrStrings',NoneOK=1) +isListOfNoneOrString=SequenceOf(isNoneOrString,'isListOfNoneOrString',NoneOK=0) +isNoneOrListOfNoneOrNumbers=SequenceOf(isNumberOrNone,'isNoneOrListOfNoneOrNumbers',NoneOK=1) +isCallable = _isCallable() +isStringOrCallable=EitherOr((isString,isCallable),'isStringOrCallable') +isStringOrCallableOrNone=NoneOr(isStringOrCallable,'isStringOrCallableNone') +isStringOrNone=NoneOr(isString,'isStringOrNone') diff --git a/bin/reportlab/lib/xmllib.py b/bin/reportlab/lib/xmllib.py new file mode 100644 index 00000000000..041646fbb42 --- /dev/null +++ b/bin/reportlab/lib/xmllib.py @@ -0,0 +1,769 @@ +# A parser for XML, using the derived class as static DTD. +# Author: Sjoerd Mullender. + +# sgmlop support added by fredrik@pythonware.com (May 19, 1998) + +import re +import string + +try: + import sgmlop # this works for both builtin on the path or relative +except ImportError: + sgmlop = None + +# standard entity defs + +ENTITYDEFS = { + 'lt': '<', + 'gt': '>', + 'amp': '&', + 'quot': '"', + 'apos': '\'' + } + +# XML parser base class -- find tags and call handler functions. +# Usage: p = XMLParser(); p.feed(data); ...; p.close(). +# The dtd is defined by deriving a class which defines methods with +# special names to handle tags: start_foo and end_foo to handle +# and , respectively. The data between tags is passed to the +# parser by calling self.handle_data() with some data as argument (the +# data may be split up in arbutrary chunks). Entity references are +# passed by calling self.handle_entityref() with the entity reference +# as argument. + +# -------------------------------------------------------------------- +# original re-based XML parser + +_S = '[ \t\r\n]+' +_opS = '[ \t\r\n]*' +_Name = '[a-zA-Z_:][-a-zA-Z0-9._:]*' +interesting = re.compile('[&<]') +incomplete = re.compile('&(' + _Name + '|#[0-9]*|#x[0-9a-fA-F]*)?|' + '<([a-zA-Z_:][^<>]*|' + '/([a-zA-Z_:][^<>]*)?|' + '![^<>]*|' + '\?[^<>]*)?') + +ref = re.compile('&(' + _Name + '|#[0-9]+|#x[0-9a-fA-F]+);?') +entityref = re.compile('&(?P' + _Name + ')[^-a-zA-Z0-9._:]') +charref = re.compile('&#(?P[0-9]+[^0-9]|x[0-9a-fA-F]+[^0-9a-fA-F])') +space = re.compile(_S) +newline = re.compile('\n') + +starttagopen = re.compile('<' + _Name) +endtagopen = re.compile('/?)>') +endbracket = re.compile('>') +tagfind = re.compile(_Name) +cdataopen = re.compile('') +special = re.compile('[^<>]*)>') +procopen = re.compile('<\?(?P' + _Name + ')' + _S) +procclose = re.compile('\?>') +commentopen = re.compile('') +doubledash = re.compile('--') +attrfind = re.compile( + _opS + '(?P' + _Name + ')' + '(' + _opS + '=' + _opS + + '(?P\'[^\']*\'|"[^"]*"|[-a-zA-Z0-9.:+*%?!()_#=~]+))') + +class SlowXMLParser: + + # Interface -- initialize and reset this instance + def __init__(self, verbose=0): + self.verbose = verbose + self.reset() + + # Interface -- reset this instance. Loses all unprocessed data + def reset(self): + self.rawdata = '' + self.stack = [] + self.lasttag = '???' + self.nomoretags = 0 + self.literal = 0 + self.lineno = 1 + + # For derived classes only -- enter literal mode (CDATA) till EOF + def setnomoretags(self): + self.nomoretags = self.literal = 1 + + # For derived classes only -- enter literal mode (CDATA) + def setliteral(self, *args): + self.literal = 1 + + # Interface -- feed some data to the parser. Call this as + # often as you want, with as little or as much text as you + # want (may include '\n'). (This just saves the text, all the + # processing is done by goahead().) + def feed(self, data): + self.rawdata = self.rawdata + data + self.goahead(0) + + # Interface -- handle the remaining data + def close(self): + self.goahead(1) + + # Interface -- translate references + def translate_references(self, data): + newdata = [] + i = 0 + while 1: + res = ref.search(data, i) + if res is None: + newdata.append(data[i:]) + return string.join(newdata, '') + if data[res.end(0) - 1] != ';': + self.syntax_error(self.lineno, + '; missing after entity/char reference') + newdata.append(data[i:res.start(0)]) + str = res.group(1) + if str[0] == '#': + if str[1] == 'x': + newdata.append(chr(string.atoi(str[2:], 16))) + else: + newdata.append(chr(string.atoi(str[1:]))) + else: + try: + newdata.append(self.entitydefs[str]) + except KeyError: + # can't do it, so keep the entity ref in + newdata.append('&' + str + ';') + i = res.end(0) + + # Internal -- handle data as far as reasonable. May leave state + # and data to be processed by a subsequent call. If 'end' is + # true, force handling all data as if followed by EOF marker. + def goahead(self, end): + rawdata = self.rawdata + i = 0 + n = len(rawdata) + while i < n: + if self.nomoretags: + data = rawdata[i:n] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = n + break + res = interesting.search(rawdata, i) + if res: + j = res.start(0) + else: + j = n + if i < j: + data = rawdata[i:j] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = j + if i == n: break + if rawdata[i] == '<': + if starttagopen.match(rawdata, i): + if self.literal: + data = rawdata[i] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = i+1 + continue + k = self.parse_starttag(i) + if k < 0: break + self.lineno = self.lineno + string.count(rawdata[i:k], '\n') + i = k + continue + if endtagopen.match(rawdata, i): + k = self.parse_endtag(i) + if k < 0: break + self.lineno = self.lineno + string.count(rawdata[i:k], '\n') + i = k + self.literal = 0 + continue + if commentopen.match(rawdata, i): + if self.literal: + data = rawdata[i] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = i+1 + continue + k = self.parse_comment(i) + if k < 0: break + self.lineno = self.lineno + string.count(rawdata[i:k], '\n') + i = k + continue + if cdataopen.match(rawdata, i): + k = self.parse_cdata(i) + if k < 0: break + self.lineno = self.lineno + string.count(rawdata[i:i], '\n') + i = k + continue + res = procopen.match(rawdata, i) + if res: + k = self.parse_proc(i, res) + if k < 0: break + self.lineno = self.lineno + string.count(rawdata[i:k], '\n') + i = k + continue + res = special.match(rawdata, i) + if res: + if self.literal: + data = rawdata[i] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = i+1 + continue + self.handle_special(res.group('special')) + self.lineno = self.lineno + string.count(res.group(0), '\n') + i = res.end(0) + continue + elif rawdata[i] == '&': + res = charref.match(rawdata, i) + if res is not None: + i = res.end(0) + if rawdata[i-1] != ';': + self.syntax_error(self.lineno, '; missing in charref') + i = i-1 + self.handle_charref(res.group('char')[:-1]) + self.lineno = self.lineno + string.count(res.group(0), '\n') + continue + res = entityref.match(rawdata, i) + if res is not None: + i = res.end(0) + if rawdata[i-1] != ';': + self.syntax_error(self.lineno, '; missing in entityref') + i = i-1 + self.handle_entityref(res.group('name')) + self.lineno = self.lineno + string.count(res.group(0), '\n') + continue + else: + raise RuntimeError, 'neither < nor & ??' + # We get here only if incomplete matches but + # nothing else + res = incomplete.match(rawdata, i) + if not res: + data = rawdata[i] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = i+1 + continue + j = res.end(0) + if j == n: + break # Really incomplete + self.syntax_error(self.lineno, 'bogus < or &') + data = res.group(0) + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = j + # end while + if end and i < n: + data = rawdata[i:n] + self.handle_data(data) + self.lineno = self.lineno + string.count(data, '\n') + i = n + self.rawdata = rawdata[i:] + # XXX if end: check for empty stack + + # Internal -- parse comment, return length or -1 if not terminated + def parse_comment(self, i): + rawdata = self.rawdata + if rawdata[i:i+4] <> ' width of glyph + widthVectorsByFont fontName -> vector of widths +""" +import string, UserDict, os, sys + +# mapping of name to width vector, starts empty until fonts are added +# e.g. widths['Courier'] = [...600,600,600,...] +widthVectorsByFont = {} +fontsByName = {} +fontsByBaseEnc = {} +# this is a list of the standard 14 font names in Acrobat Reader +standardFonts = ( + 'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique', + 'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique', + 'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic', + 'Symbol','ZapfDingbats') + +standardFontAttributes = { + #family, bold, italic defined for basic ones + 'Courier':('Courier',0,0), + 'Courier-Bold':('Courier',1,0), + 'Courier-Oblique':('Courier',0,1), + 'Courier-BoldOblique':('Courier',1,1), + + 'Helvetica':('Helvetica',0,0), + 'Helvetica-Bold':('Helvetica',1,0), + 'Helvetica-Oblique':('Helvetica',0,1), + 'Helvetica-BoldOblique':('Helvetica',1,1), + + 'Times-Roman':('Times-Roman',0,0), + 'Times-Bold':('Times-Roman',1,0), + 'Times-Italic':('Times-Roman',0,1), + 'Times-BoldItalic':('Times-Roman',1,1), + + 'Symbol':('Symbol',0,0), + 'ZapfDingbats':('ZapfDingbats',0,0) + + } + +#this maps fontnames to the equivalent filename root. +_font2fnrMapWin32 = { + 'symbol': 'Sy______', + 'zapfdingbats': 'Zd______', + 'helvetica': '_a______', + 'helvetica-bold': '_ab_____', + 'helvetica-boldoblique': '_abi____', + 'helvetica-oblique': '_ai_____', + 'times-bold': '_eb_____', + 'times-bolditalic': '_ebi____', + 'times-italic': '_ei_____', + 'times-roman': '_er_____', + 'courier-bold': 'cob_____', + 'courier-boldoblique': 'cobo____', + 'courier': 'com_____', + 'courier-oblique': 'coo_____', + } +if sys.platform in ('linux2',): + _font2fnrMapLinux2 ={ + 'symbol': 'Symbol', + 'zapfdingbats': 'ZapfDingbats', + 'helvetica': 'Arial', + 'helvetica-bold': 'Arial-Bold', + 'helvetica-boldoblique': 'Arial-BoldItalic', + 'helvetica-oblique': 'Arial-Italic', + 'times-bold': 'TimesNewRoman-Bold', + 'times-bolditalic':'TimesNewRoman-BoldItalic', + 'times-italic': 'TimesNewRoman-Italic', + 'times-roman': 'TimesNewRoman', + 'courier-bold': 'Courier-Bold', + 'courier-boldoblique': 'Courier-BoldOblique', + 'courier': 'Courier', + 'courier-oblique': 'Courier-Oblique', + } + _font2fnrMap = _font2fnrMapLinux2 + _revmap = None +else: + _font2fnrMap = _font2fnrMapWin32 + +def _findFNR(fontName): + return _font2fnrMap[string.lower(fontName)] + +from reportlab.rl_config import T1SearchPath +from reportlab.lib.utils import rl_isfile +def _searchT1Dirs(n,rl_isfile=rl_isfile,T1SearchPath=T1SearchPath): + assert T1SearchPath!=[], "No Type-1 font search path" + for d in T1SearchPath: + f = os.path.join(d,n) + if rl_isfile(f): return f + return None +del T1SearchPath, rl_isfile + +def findT1File(fontName,ext='.pfb'): + if sys.platform in ('linux2',) and ext=='.pfb': + try: + f = _searchT1Dirs(_findFNR(fontName)) + if f: return f + except: + pass + global _revmap + if not _revmap: + for k, v in _font2fnrMap.items(): + if k in _font2fnrMapWin32.keys(): + _font2fnrMapWin32[string.lower(v)] = _font2fnrMapWin32[k] + revmap = 1 + try: + f = _searchT1Dirs(_font2fnrMapWin32[string.lower(fontName)]+ext) + if f: return f + except: + pass + + return _searchT1Dirs(_findFNR(fontName)+ext) + +# this lists the predefined font encodings - WinAnsi and MacRoman. We have +# not added MacExpert - it's possible, but would complicate life and nobody +# is asking. StandardEncoding means something special. +standardEncodings = ('WinAnsiEncoding','MacRomanEncoding','StandardEncoding','SymbolEncoding','ZapfDingbatsEncoding','PDFDocEncoding', 'MacExpertEncoding') + +#this is the global mapping of standard encodings to name vectors +class _Name2StandardEncodingMap(UserDict.UserDict): + '''Trivial fake dictionary with some [] magic''' + _XMap = {'winansi':'WinAnsiEncoding','macroman': 'MacRomanEncoding','standard':'StandardEncoding','symbol':'SymbolEncoding', 'zapfdingbats':'ZapfDingbatsEncoding','pdfdoc':'PDFDocEncoding', 'macexpert':'MacExpertEncoding'} + def __setitem__(self,x,v): + y = string.lower(x) + if y[-8:]=='encoding': y = y[:-8] + y = self._XMap[y] + if y in self.keys(): raise IndexError, 'Encoding %s is already set' % y + self.data[y] = v + + def __getitem__(self,x): + y = string.lower(x) + if y[-8:]=='encoding': y = y[:-8] + y = self._XMap[y] + return self.data[y] + +encodings = _Name2StandardEncodingMap() +encodings['WinAnsiEncoding'] = ( + None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, 'space', 'exclam', + 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', + 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', + 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', + 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', + 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', + 'asciitilde', 'bullet', 'Euro', 'bullet', 'quotesinglbase', 'florin', + 'quotedblbase', 'ellipsis', 'dagger', 'daggerdbl', 'circumflex', + 'perthousand', 'Scaron', 'guilsinglleft', 'OE', 'bullet', 'Zcaron', + 'bullet', 'bullet', 'quoteleft', 'quoteright', 'quotedblleft', + 'quotedblright', 'bullet', 'endash', 'emdash', 'tilde', 'trademark', + 'scaron', 'guilsinglright', 'oe', 'bullet', 'zcaron', 'Ydieresis', + 'space', 'exclamdown', 'cent', 'sterling', 'currency', 'yen', 'brokenbar', + 'section', 'dieresis', 'copyright', 'ordfeminine', 'guillemotleft', + 'logicalnot', 'hyphen', 'registered', 'macron', 'degree', 'plusminus', + 'twosuperior', 'threesuperior', 'acute', 'mu', 'paragraph', 'periodcentered', + 'cedilla', 'onesuperior', 'ordmasculine', 'guillemotright', 'onequarter', + 'onehalf', 'threequarters', 'questiondown', 'Agrave', 'Aacute', + 'Acircumflex', 'Atilde', 'Adieresis', 'Aring', 'AE', 'Ccedilla', + 'Egrave', 'Eacute', 'Ecircumflex', 'Edieresis', 'Igrave', 'Iacute', + 'Icircumflex', 'Idieresis', 'Eth', 'Ntilde', 'Ograve', 'Oacute', + 'Ocircumflex', 'Otilde', 'Odieresis', 'multiply', 'Oslash', 'Ugrave', + 'Uacute', 'Ucircumflex', 'Udieresis', 'Yacute', 'Thorn', 'germandbls', + 'agrave', 'aacute', 'acircumflex', 'atilde', 'adieresis', 'aring', 'ae', + 'ccedilla', 'egrave', 'eacute', 'ecircumflex', 'edieresis', 'igrave', + 'iacute', 'icircumflex', 'idieresis', 'eth', 'ntilde', 'ograve', 'oacute', + 'ocircumflex', 'otilde', 'odieresis', 'divide', 'oslash', 'ugrave', 'uacute', + 'ucircumflex', 'udieresis', 'yacute', 'thorn', 'ydieresis') + +encodings['MacRomanEncoding'] = ( + None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, 'space', 'exclam', + 'quotedbl', 'numbersign', 'dollar', 'percent', 'ampersand', + 'quotesingle', 'parenleft', 'parenright', 'asterisk', 'plus', 'comma', + 'hyphen', 'period', 'slash', 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', 'less', + 'equal', 'greater', 'question', 'at', 'A', 'B', 'C', 'D', 'E', 'F', + 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z', 'bracketleft', 'backslash', 'bracketright', + 'asciicircum', 'underscore', 'grave', 'a', 'b', 'c', 'd', 'e', 'f', + 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', 'braceleft', 'bar', 'braceright', + 'asciitilde', None, 'Adieresis', 'Aring', 'Ccedilla', 'Eacute', + 'Ntilde', 'Odieresis', 'Udieresis', 'aacute', 'agrave', 'acircumflex', + 'adieresis', 'atilde', 'aring', 'ccedilla', 'eacute', 'egrave', + 'ecircumflex', 'edieresis', 'iacute', 'igrave', 'icircumflex', + 'idieresis', 'ntilde', 'oacute', 'ograve', 'ocircumflex', 'odieresis', + 'otilde', 'uacute', 'ugrave', 'ucircumflex', 'udieresis', 'dagger', + 'degree', 'cent', 'sterling', 'section', 'bullet', 'paragraph', + 'germandbls', 'registered', 'copyright', 'trademark', 'acute', + 'dieresis', None, 'AE', 'Oslash', None, 'plusminus', None, None, 'yen', + 'mu', None, None, None, None, None, 'ordfeminine', 'ordmasculine', None, + 'ae', 'oslash', 'questiondown', 'exclamdown', 'logicalnot', None, 'florin', + None, None, 'guillemotleft', 'guillemotright', 'ellipsis', 'space', 'Agrave', + 'Atilde', 'Otilde', 'OE', 'oe', 'endash', 'emdash', 'quotedblleft', + 'quotedblright', 'quoteleft', 'quoteright', 'divide', None, 'ydieresis', + 'Ydieresis', 'fraction', 'currency', 'guilsinglleft', 'guilsinglright', + 'fi', 'fl', 'daggerdbl', 'periodcentered', 'quotesinglbase', + 'quotedblbase', 'perthousand', 'Acircumflex', 'Ecircumflex', 'Aacute', + 'Edieresis', 'Egrave', 'Iacute', 'Icircumflex', 'Idieresis', 'Igrave', + 'Oacute', 'Ocircumflex', None, 'Ograve', 'Uacute', 'Ucircumflex', + 'Ugrave', 'dotlessi', 'circumflex', 'tilde', 'macron', 'breve', + 'dotaccent', 'ring', 'cedilla', 'hungarumlaut', 'ogonek', 'caron') +encodings['SymbolEncoding']=(None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, 'space', + 'exclam', 'universal', 'numbersign', 'existential', 'percent', 'ampersand', 'suchthat', + 'parenleft', 'parenright', 'asteriskmath', 'plus', 'comma', 'minus', 'period', 'slash', 'zero', + 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'colon', 'semicolon', + 'less', 'equal', 'greater', 'question', 'congruent', 'Alpha', 'Beta', 'Chi', 'Delta', 'Epsilon', + 'Phi', 'Gamma', 'Eta', 'Iota', 'theta1', 'Kappa', 'Lambda', 'Mu', 'Nu', 'Omicron', 'Pi', 'Theta', + 'Rho', 'Sigma', 'Tau', 'Upsilon', 'sigma1', 'Omega', 'Xi', 'Psi', 'Zeta', 'bracketleft', + 'therefore', 'bracketright', 'perpendicular', 'underscore', 'radicalex', 'alpha', 'beta', 'chi', + 'delta', 'epsilon', 'phi', 'gamma', 'eta', 'iota', 'phi1', 'kappa', 'lambda', 'mu', 'nu', + 'omicron', 'pi', 'theta', 'rho', 'sigma', 'tau', 'upsilon', 'omega1', 'omega', 'xi', 'psi', 'zeta', + 'braceleft', 'bar', 'braceright', 'similar', None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, 'Euro', 'Upsilon1', 'minute', 'lessequal', + 'fraction', 'infinity', 'florin', 'club', 'diamond', 'heart', 'spade', 'arrowboth', 'arrowleft', + 'arrowup', 'arrowright', 'arrowdown', 'degree', 'plusminus', 'second', 'greaterequal', 'multiply', + 'proportional', 'partialdiff', 'bullet', 'divide', 'notequal', 'equivalence', 'approxequal', + 'ellipsis', 'arrowvertex', 'arrowhorizex', 'carriagereturn', 'aleph', 'Ifraktur', 'Rfraktur', + 'weierstrass', 'circlemultiply', 'circleplus', 'emptyset', 'intersection', 'union', + 'propersuperset', 'reflexsuperset', 'notsubset', 'propersubset', 'reflexsubset', 'element', + 'notelement', 'angle', 'gradient', 'registerserif', 'copyrightserif', 'trademarkserif', 'product', + 'radical', 'dotmath', 'logicalnot', 'logicaland', 'logicalor', 'arrowdblboth', 'arrowdblleft', + 'arrowdblup', 'arrowdblright', 'arrowdbldown', 'lozenge', 'angleleft', 'registersans', + 'copyrightsans', 'trademarksans', 'summation', 'parenlefttp', 'parenleftex', 'parenleftbt', + 'bracketlefttp', 'bracketleftex', 'bracketleftbt', 'bracelefttp', 'braceleftmid', 'braceleftbt', + 'braceex', None, 'angleright', 'integral', 'integraltp', 'integralex', 'integralbt', + 'parenrighttp', 'parenrightex', 'parenrightbt', 'bracketrighttp', 'bracketrightex', + 'bracketrightbt', 'bracerighttp', 'bracerightmid', 'bracerightbt', None) +encodings['ZapfDingbatsEncoding'] = ( None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, + 'space', 'a1', 'a2', 'a202', 'a3', 'a4', 'a5', 'a119', 'a118', 'a117', 'a11', 'a12', 'a13', 'a14', + 'a15', 'a16', 'a105', 'a17', 'a18', 'a19', 'a20', 'a21', 'a22', 'a23', 'a24', 'a25', 'a26', 'a27', + 'a28', 'a6', 'a7', 'a8', 'a9', 'a10', 'a29', 'a30', 'a31', 'a32', 'a33', 'a34', 'a35', 'a36', + 'a37', 'a38', 'a39', 'a40', 'a41', 'a42', 'a43', 'a44', 'a45', 'a46', 'a47', 'a48', 'a49', 'a50', + 'a51', 'a52', 'a53', 'a54', 'a55', 'a56', 'a57', 'a58', 'a59', 'a60', 'a61', 'a62', 'a63', 'a64', + 'a65', 'a66', 'a67', 'a68', 'a69', 'a70', 'a71', 'a72', 'a73', 'a74', 'a203', 'a75', 'a204', 'a76', + 'a77', 'a78', 'a79', 'a81', 'a82', 'a83', 'a84', 'a97', 'a98', 'a99', 'a100', None, 'a89', 'a90', + 'a93', 'a94', 'a91', 'a92', 'a205', 'a85', 'a206', 'a86', 'a87', 'a88', 'a95', 'a96', None, None, + None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, + None, 'a101', 'a102', 'a103', 'a104', 'a106', 'a107', 'a108', 'a112', 'a111', 'a110', 'a109', + 'a120', 'a121', 'a122', 'a123', 'a124', 'a125', 'a126', 'a127', 'a128', 'a129', 'a130', 'a131', + 'a132', 'a133', 'a134', 'a135', 'a136', 'a137', 'a138', 'a139', 'a140', 'a141', 'a142', 'a143', + 'a144', 'a145', 'a146', 'a147', 'a148', 'a149', 'a150', 'a151', 'a152', 'a153', 'a154', 'a155', + 'a156', 'a157', 'a158', 'a159', 'a160', 'a161', 'a163', 'a164', 'a196', 'a165', 'a192', 'a166', + 'a167', 'a168', 'a169', 'a170', 'a171', 'a172', 'a173', 'a162', 'a174', 'a175', 'a176', 'a177', + 'a178', 'a179', 'a193', 'a180', 'a199', 'a181', 'a200', 'a182', None, 'a201', 'a183', 'a184', + 'a197', 'a185', 'a194', 'a198', 'a186', 'a195', 'a187', 'a188', 'a189', 'a190', 'a191', None) +encodings['StandardEncoding']=(None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,"space","exclam", + "quotedbl","numbersign","dollar","percent","ampersand","quoteright","parenleft","parenright","asterisk","plus", + "comma","hyphen","period","slash","zero","one","two","three","four","five","six","seven","eight","nine","colon", + "semicolon","less","equal","greater","question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O", + "P","Q","R","S","T","U","V","W","X","Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore", + "quoteleft","a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y", + "z","braceleft","bar","braceright","asciitilde",None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None, + None,None,None,"exclamdown","cent","sterling","fraction","yen","florin","section","currency","quotesingle","quotedblleft", + "guillemotleft","guilsinglleft","guilsinglright","fi","fl",None,"endash","dagger","daggerdbl","periodcentered",None, + "paragraph","bullet","quotesinglbase","quotedblbase","quotedblright","guillemotright","ellipsis","perthousand", + None,"questiondown",None,"grave","acute","circumflex","tilde","macron","breve","dotaccent","dieresis",None,"ring", + "cedilla",None,"hungarumlaut","ogonek","caron","emdash",None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,"AE",None,"ordfeminine", + None,None,None,None,"Lslash","Oslash","OE","ordmasculine",None,None,None,None,None,"ae",None,None,None,"dotlessi",None,None,"lslash","oslash", + "oe","germandbls",None,None,None,None) +encodings['PDFDocEncoding']=(None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None, + None,None,None,None,None,"breve","caron","circumflex", + "dotaccent","hungarumlaut","ogonek","ring","tilde","space","exclam","quotedbl","numbersign","dollar","percent", + "ampersand","quotesingle","parenleft","parenright","asterisk","plus","comma","hyphen","period","slash","zero", + "one","two","three","four","five","six","seven","eight","nine","colon","semicolon","less","equal","greater", + "question","at","A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X", + "Y","Z","bracketleft","backslash","bracketright","asciicircum","underscore","grave","a","b","c","d","e","f","g", + "h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","braceleft","bar","braceright", + "asciitilde",None,"bullet","dagger","daggerdbl","ellipsis","emdash","endash","florin","fraction","guilsinglleft", + "guilsinglright","minus","perthousand","quotedblbase","quotedblleft","quotedblright","quoteleft","quoteright", + "quotesinglbase","trademark","fi","fl","Lslash","OE","Scaron","Ydieresis","Zcaron","dotlessi","lslash","oe", + "scaron","zcaron",None,"Euro","exclamdown","cent","sterling","currency","yen","brokenbar","section","dieresis", + "copyright","ordfeminine","guillemotleft","logicalnot",None,"registered","macron","degree","plusminus","twosuperior", + "threesuperior","acute","mu","paragraph","periodcentered","cedilla","onesuperior","ordmasculine","guillemotright", + "onequarter","onehalf","threequarters","questiondown","Agrave","Aacute","Acircumflex","Atilde","Adieresis","Aring", + "AE","Ccedilla","Egrave","Eacute","Ecircumflex","Edieresis","Igrave","Iacute","Icircumflex","Idieresis","Eth", + "Ntilde","Ograve","Oacute","Ocircumflex","Otilde","Odieresis","multiply","Oslash","Ugrave","Uacute","Ucircumflex", + "Udieresis","Yacute","Thorn","germandbls","agrave","aacute","acircumflex","atilde","adieresis","aring","ae", + "ccedilla","egrave","eacute","ecircumflex","edieresis","igrave","iacute","icircumflex","idieresis","eth","ntilde", + "ograve","oacute","ocircumflex","otilde","odieresis","divide","oslash","ugrave","uacute","ucircumflex","udieresis", + "yacute","thorn","ydieresis") +encodings['MacExpertEncoding'] = (None, None, None, None, None, None, None, None, None, None, None, None, None, None, + None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, + 'space', 'exclamsmall', 'Hungarumlautsmall', 'centoldstyle', 'dollaroldstyle', 'dollarsuperior', 'ampersandsmall', + 'Acutesmall', 'parenleftsuperior', 'parenrightsuperior', 'twodotenleader', 'onedotenleader', 'comma', 'hyphen', + 'period', 'fraction', 'zerooldstyle', 'oneoldstyle', 'twooldstyle', 'threeoldstyle', 'fouroldstyle', + 'fiveoldstyle', 'sixoldstyle', 'sevenoldstyle', 'eightoldstyle', 'nineoldstyle', 'colon', 'semicolon', None, + 'threequartersemdash', None, 'questionsmall', None, None, None, None, 'Ethsmall', None, None, 'onequarter', + 'onehalf', 'threequarters', 'oneeighth', 'threeeighths', 'fiveeighths', 'seveneighths', 'onethird', 'twothirds', + None, None, None, None, None, None, 'ff', 'fi', 'fl', 'ffi', 'ffl', 'parenleftinferior', None, + 'parenrightinferior', 'Circumflexsmall', 'hypheninferior', 'Gravesmall', 'Asmall', 'Bsmall', 'Csmall', 'Dsmall', + 'Esmall', 'Fsmall', 'Gsmall', 'Hsmall', 'Ismall', 'Jsmall', 'Ksmall', 'Lsmall', 'Msmall', 'Nsmall', 'Osmall', + 'Psmall', 'Qsmall', 'Rsmall', 'Ssmall', 'Tsmall', 'Usmall', 'Vsmall', 'Wsmall', 'Xsmall', 'Ysmall', 'Zsmall', + 'colonmonetary', 'onefitted', 'rupiah', 'Tildesmall', None, None, 'asuperior', 'centsuperior', None, None, None, + None, 'Aacutesmall', 'Agravesmall', 'Acircumflexsmall', 'Adieresissmall', 'Atildesmall', 'Aringsmall', + 'Ccedillasmall', 'Eacutesmall', 'Egravesmall', 'Ecircumflexsmall', 'Edieresissmall', 'Iacutesmall', 'Igravesmall', + 'Icircumflexsmall', 'Idieresissmall', 'Ntildesmall', 'Oacutesmall', 'Ogravesmall', 'Ocircumflexsmall', + 'Odieresissmall', 'Otildesmall', 'Uacutesmall', 'Ugravesmall', 'Ucircumflexsmall', 'Udieresissmall', None, + 'eightsuperior', 'fourinferior', 'threeinferior', 'sixinferior', 'eightinferior', 'seveninferior', 'Scaronsmall', + None, 'centinferior', 'twoinferior', None, 'Dieresissmall', None, 'Caronsmall', 'osuperior', 'fiveinferior', None, + 'commainferior', 'periodinferior', 'Yacutesmall', None, 'dollarinferior', None, None, 'Thornsmall', None, + 'nineinferior', 'zeroinferior', 'Zcaronsmall', 'AEsmall', 'Oslashsmall', 'questiondownsmall', 'oneinferior', + 'Lslashsmall', None, None, None, None, None, None, 'Cedillasmall', None, None, None, None, None, 'OEsmall', + 'figuredash', 'hyphensuperior', None, None, None, None, 'exclamdownsmall', None, 'Ydieresissmall', None, + 'onesuperior', 'twosuperior', 'threesuperior', 'foursuperior', 'fivesuperior', 'sixsuperior', 'sevensuperior', + 'ninesuperior', 'zerosuperior', None, 'esuperior', 'rsuperior', 'tsuperior', None, None, 'isuperior', 'ssuperior', + 'dsuperior', None, None, None, None, None, 'lsuperior', 'Ogoneksmall', 'Brevesmall', 'Macronsmall', 'bsuperior', + 'nsuperior', 'msuperior', 'commasuperior', 'periodsuperior', 'Dotaccentsmall', 'Ringsmall', None, None, None, None) +ascent_descent = { + 'Courier': (629, -157), + 'Courier-Bold': (626, -142), + 'Courier-BoldOblique': (626, -142), + 'Courier-Oblique': (629, -157), + 'Helvetica': (718, -207), + 'Helvetica-Bold': (718, -207), + 'Helvetica-BoldOblique': (718, -207), + 'Helvetica-Oblique': (718, -207), + 'Times-Roman': (683, -217), + 'Times-Bold': (676, -205), + 'Times-BoldItalic': (699, -205), + 'Times-Italic': (683, -205), + 'Symbol': (0, 0), + 'ZapfDingbats': (0, 0) + } + +# nuild this up one entry at a time to stay under JPython's 64k limit. +widthsByFontGlyph = {} +widthsByFontGlyph['Helvetica'] = {'A': 667, + 'AE': 1000, + 'Aacute': 667, + 'Acircumflex': 667, + 'Adieresis': 667, + 'Agrave': 667, + 'Aring': 667, + 'Atilde': 667, + 'B': 667, + 'C': 722, + 'Ccedilla': 722, + 'D': 722, + 'E': 667, + 'Eacute': 667, + 'Ecircumflex': 667, + 'Edieresis': 667, + 'Egrave': 667, + 'Eth': 722, + 'Euro': 556, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'Iacute': 278, + 'Icircumflex': 278, + 'Idieresis': 278, + 'Igrave': 278, + 'J': 500, + 'K': 667, + 'L': 556, + 'Lslash': 556, + 'M': 833, + 'N': 722, + 'Ntilde': 722, + 'O': 778, + 'OE': 1000, + 'Oacute': 778, + 'Ocircumflex': 778, + 'Odieresis': 778, + 'Ograve': 778, + 'Oslash': 778, + 'Otilde': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'Scaron': 667, + 'T': 611, + 'Thorn': 667, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Yacute': 667, + 'Ydieresis': 667, + 'Z': 611, + 'Zcaron': 611, + 'a': 556, + 'aacute': 556, + 'acircumflex': 556, + 'acute': 333, + 'adieresis': 556, + 'ae': 889, + 'agrave': 556, + 'ampersand': 667, + 'aring': 556, + 'asciicircum': 469, + 'asciitilde': 584, + 'asterisk': 389, + 'at': 1015, + 'atilde': 556, + 'b': 556, + 'backslash': 278, + 'bar': 260, + 'braceleft': 334, + 'braceright': 334, + 'bracketleft': 278, + 'bracketright': 278, + 'breve': 333, + 'brokenbar': 260, + 'bullet': 350, + 'c': 500, + 'caron': 333, + 'ccedilla': 500, + 'cedilla': 333, + 'cent': 556, + 'circumflex': 333, + 'colon': 278, + 'comma': 278, + 'copyright': 737, + 'currency': 556, + 'd': 556, + 'dagger': 556, + 'daggerdbl': 556, + 'degree': 400, + 'dieresis': 333, + 'divide': 584, + 'dollar': 556, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 556, + 'eacute': 556, + 'ecircumflex': 556, + 'edieresis': 556, + 'egrave': 556, + 'eight': 556, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 556, + 'equal': 584, + 'eth': 556, + 'exclam': 278, + 'exclamdown': 333, + 'f': 278, + 'fi': 500, + 'five': 556, + 'fl': 500, + 'florin': 556, + 'four': 556, + 'fraction': 167, + 'g': 556, + 'germandbls': 611, + 'grave': 333, + 'greater': 584, + 'guillemotleft': 556, + 'guillemotright': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 556, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 222, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 222, + 'k': 500, + 'l': 222, + 'less': 584, + 'logicalnot': 584, + 'lslash': 222, + 'm': 833, + 'macron': 333, + 'minus': 584, + 'mu': 556, + 'multiply': 584, + 'n': 556, + 'nine': 556, + 'ntilde': 556, + 'numbersign': 556, + 'o': 556, + 'oacute': 556, + 'ocircumflex': 556, + 'odieresis': 556, + 'oe': 944, + 'ogonek': 333, + 'ograve': 556, + 'one': 556, + 'onehalf': 834, + 'onequarter': 834, + 'onesuperior': 333, + 'ordfeminine': 370, + 'ordmasculine': 365, + 'oslash': 611, + 'otilde': 556, + 'p': 556, + 'paragraph': 537, + 'parenleft': 333, + 'parenright': 333, + 'percent': 889, + 'period': 278, + 'periodcentered': 278, + 'perthousand': 1000, + 'plus': 584, + 'plusminus': 584, + 'q': 556, + 'question': 556, + 'questiondown': 611, + 'quotedbl': 355, + 'quotedblbase': 333, + 'quotedblleft': 333, + 'quotedblright': 333, + 'quoteleft': 222, + 'quoteright': 222, + 'quotesinglbase': 222, + 'quotesingle': 191, + 'r': 333, + 'registered': 737, + 'ring': 333, + 's': 500, + 'scaron': 500, + 'section': 556, + 'semicolon': 278, + 'seven': 556, + 'six': 556, + 'slash': 278, + 'space': 278, + 'sterling': 556, + 't': 278, + 'thorn': 556, + 'three': 556, + 'threequarters': 834, + 'threesuperior': 333, + 'tilde': 333, + 'trademark': 1000, + 'two': 556, + 'twosuperior': 333, + 'u': 556, + 'uacute': 556, + 'ucircumflex': 556, + 'udieresis': 556, + 'ugrave': 556, + 'underscore': 556, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'yacute': 500, + 'ydieresis': 500, + 'yen': 556, + 'z': 500, + 'zcaron': 500, + 'zero': 556} + +widthsByFontGlyph['Helvetica-Bold'] = {'A': 722, + 'AE': 1000, + 'Aacute': 722, + 'Acircumflex': 722, + 'Adieresis': 722, + 'Agrave': 722, + 'Aring': 722, + 'Atilde': 722, + 'B': 722, + 'C': 722, + 'Ccedilla': 722, + 'D': 722, + 'E': 667, + 'Eacute': 667, + 'Ecircumflex': 667, + 'Edieresis': 667, + 'Egrave': 667, + 'Eth': 722, + 'Euro': 556, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'Iacute': 278, + 'Icircumflex': 278, + 'Idieresis': 278, + 'Igrave': 278, + 'J': 556, + 'K': 722, + 'L': 611, + 'Lslash': 611, + 'M': 833, + 'N': 722, + 'Ntilde': 722, + 'O': 778, + 'OE': 1000, + 'Oacute': 778, + 'Ocircumflex': 778, + 'Odieresis': 778, + 'Ograve': 778, + 'Oslash': 778, + 'Otilde': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'Scaron': 667, + 'T': 611, + 'Thorn': 667, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Yacute': 667, + 'Ydieresis': 667, + 'Z': 611, + 'Zcaron': 611, + 'a': 556, + 'aacute': 556, + 'acircumflex': 556, + 'acute': 333, + 'adieresis': 556, + 'ae': 889, + 'agrave': 556, + 'ampersand': 722, + 'aring': 556, + 'asciicircum': 584, + 'asciitilde': 584, + 'asterisk': 389, + 'at': 975, + 'atilde': 556, + 'b': 611, + 'backslash': 278, + 'bar': 280, + 'braceleft': 389, + 'braceright': 389, + 'bracketleft': 333, + 'bracketright': 333, + 'breve': 333, + 'brokenbar': 280, + 'bullet': 350, + 'c': 556, + 'caron': 333, + 'ccedilla': 556, + 'cedilla': 333, + 'cent': 556, + 'circumflex': 333, + 'colon': 333, + 'comma': 278, + 'copyright': 737, + 'currency': 556, + 'd': 611, + 'dagger': 556, + 'daggerdbl': 556, + 'degree': 400, + 'dieresis': 333, + 'divide': 584, + 'dollar': 556, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 556, + 'eacute': 556, + 'ecircumflex': 556, + 'edieresis': 556, + 'egrave': 556, + 'eight': 556, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 556, + 'equal': 584, + 'eth': 611, + 'exclam': 333, + 'exclamdown': 333, + 'f': 333, + 'fi': 611, + 'five': 556, + 'fl': 611, + 'florin': 556, + 'four': 556, + 'fraction': 167, + 'g': 611, + 'germandbls': 611, + 'grave': 333, + 'greater': 584, + 'guillemotleft': 556, + 'guillemotright': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 611, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 278, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 278, + 'k': 556, + 'l': 278, + 'less': 584, + 'logicalnot': 584, + 'lslash': 278, + 'm': 889, + 'macron': 333, + 'minus': 584, + 'mu': 611, + 'multiply': 584, + 'n': 611, + 'nine': 556, + 'ntilde': 611, + 'numbersign': 556, + 'o': 611, + 'oacute': 611, + 'ocircumflex': 611, + 'odieresis': 611, + 'oe': 944, + 'ogonek': 333, + 'ograve': 611, + 'one': 556, + 'onehalf': 834, + 'onequarter': 834, + 'onesuperior': 333, + 'ordfeminine': 370, + 'ordmasculine': 365, + 'oslash': 611, + 'otilde': 611, + 'p': 611, + 'paragraph': 556, + 'parenleft': 333, + 'parenright': 333, + 'percent': 889, + 'period': 278, + 'periodcentered': 278, + 'perthousand': 1000, + 'plus': 584, + 'plusminus': 584, + 'q': 611, + 'question': 611, + 'questiondown': 611, + 'quotedbl': 474, + 'quotedblbase': 500, + 'quotedblleft': 500, + 'quotedblright': 500, + 'quoteleft': 278, + 'quoteright': 278, + 'quotesinglbase': 278, + 'quotesingle': 238, + 'r': 389, + 'registered': 737, + 'ring': 333, + 's': 556, + 'scaron': 556, + 'section': 556, + 'semicolon': 333, + 'seven': 556, + 'six': 556, + 'slash': 278, + 'space': 278, + 'sterling': 556, + 't': 333, + 'thorn': 611, + 'three': 556, + 'threequarters': 834, + 'threesuperior': 333, + 'tilde': 333, + 'trademark': 1000, + 'two': 556, + 'twosuperior': 333, + 'u': 611, + 'uacute': 611, + 'ucircumflex': 611, + 'udieresis': 611, + 'ugrave': 611, + 'underscore': 556, + 'v': 556, + 'w': 778, + 'x': 556, + 'y': 556, + 'yacute': 556, + 'ydieresis': 556, + 'yen': 556, + 'z': 500, + 'zcaron': 500, + 'zero': 556} + +widthsByFontGlyph['Helvetica-Oblique'] = {'A': 667, + 'AE': 1000, + 'Aacute': 667, + 'Acircumflex': 667, + 'Adieresis': 667, + 'Agrave': 667, + 'Aring': 667, + 'Atilde': 667, + 'B': 667, + 'C': 722, + 'Ccedilla': 722, + 'D': 722, + 'E': 667, + 'Eacute': 667, + 'Ecircumflex': 667, + 'Edieresis': 667, + 'Egrave': 667, + 'Eth': 722, + 'Euro': 556, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'Iacute': 278, + 'Icircumflex': 278, + 'Idieresis': 278, + 'Igrave': 278, + 'J': 500, + 'K': 667, + 'L': 556, + 'Lslash': 556, + 'M': 833, + 'N': 722, + 'Ntilde': 722, + 'O': 778, + 'OE': 1000, + 'Oacute': 778, + 'Ocircumflex': 778, + 'Odieresis': 778, + 'Ograve': 778, + 'Oslash': 778, + 'Otilde': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'Scaron': 667, + 'T': 611, + 'Thorn': 667, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Yacute': 667, + 'Ydieresis': 667, + 'Z': 611, + 'Zcaron': 611, + 'a': 556, + 'aacute': 556, + 'acircumflex': 556, + 'acute': 333, + 'adieresis': 556, + 'ae': 889, + 'agrave': 556, + 'ampersand': 667, + 'aring': 556, + 'asciicircum': 469, + 'asciitilde': 584, + 'asterisk': 389, + 'at': 1015, + 'atilde': 556, + 'b': 556, + 'backslash': 278, + 'bar': 260, + 'braceleft': 334, + 'braceright': 334, + 'bracketleft': 278, + 'bracketright': 278, + 'breve': 333, + 'brokenbar': 260, + 'bullet': 350, + 'c': 500, + 'caron': 333, + 'ccedilla': 500, + 'cedilla': 333, + 'cent': 556, + 'circumflex': 333, + 'colon': 278, + 'comma': 278, + 'copyright': 737, + 'currency': 556, + 'd': 556, + 'dagger': 556, + 'daggerdbl': 556, + 'degree': 400, + 'dieresis': 333, + 'divide': 584, + 'dollar': 556, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 556, + 'eacute': 556, + 'ecircumflex': 556, + 'edieresis': 556, + 'egrave': 556, + 'eight': 556, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 556, + 'equal': 584, + 'eth': 556, + 'exclam': 278, + 'exclamdown': 333, + 'f': 278, + 'fi': 500, + 'five': 556, + 'fl': 500, + 'florin': 556, + 'four': 556, + 'fraction': 167, + 'g': 556, + 'germandbls': 611, + 'grave': 333, + 'greater': 584, + 'guillemotleft': 556, + 'guillemotright': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 556, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 222, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 222, + 'k': 500, + 'l': 222, + 'less': 584, + 'logicalnot': 584, + 'lslash': 222, + 'm': 833, + 'macron': 333, + 'minus': 584, + 'mu': 556, + 'multiply': 584, + 'n': 556, + 'nine': 556, + 'ntilde': 556, + 'numbersign': 556, + 'o': 556, + 'oacute': 556, + 'ocircumflex': 556, + 'odieresis': 556, + 'oe': 944, + 'ogonek': 333, + 'ograve': 556, + 'one': 556, + 'onehalf': 834, + 'onequarter': 834, + 'onesuperior': 333, + 'ordfeminine': 370, + 'ordmasculine': 365, + 'oslash': 611, + 'otilde': 556, + 'p': 556, + 'paragraph': 537, + 'parenleft': 333, + 'parenright': 333, + 'percent': 889, + 'period': 278, + 'periodcentered': 278, + 'perthousand': 1000, + 'plus': 584, + 'plusminus': 584, + 'q': 556, + 'question': 556, + 'questiondown': 611, + 'quotedbl': 355, + 'quotedblbase': 333, + 'quotedblleft': 333, + 'quotedblright': 333, + 'quoteleft': 222, + 'quoteright': 222, + 'quotesinglbase': 222, + 'quotesingle': 191, + 'r': 333, + 'registered': 737, + 'ring': 333, + 's': 500, + 'scaron': 500, + 'section': 556, + 'semicolon': 278, + 'seven': 556, + 'six': 556, + 'slash': 278, + 'space': 278, + 'sterling': 556, + 't': 278, + 'thorn': 556, + 'three': 556, + 'threequarters': 834, + 'threesuperior': 333, + 'tilde': 333, + 'trademark': 1000, + 'two': 556, + 'twosuperior': 333, + 'u': 556, + 'uacute': 556, + 'ucircumflex': 556, + 'udieresis': 556, + 'ugrave': 556, + 'underscore': 556, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'yacute': 500, + 'ydieresis': 500, + 'yen': 556, + 'z': 500, + 'zcaron': 500, + 'zero': 556} + + + +widthsByFontGlyph['Helvetica-BoldOblique'] = {'A': 722, + 'AE': 1000, + 'Aacute': 722, + 'Acircumflex': 722, + 'Adieresis': 722, + 'Agrave': 722, + 'Aring': 722, + 'Atilde': 722, + 'B': 722, + 'C': 722, + 'Ccedilla': 722, + 'D': 722, + 'E': 667, + 'Eacute': 667, + 'Ecircumflex': 667, + 'Edieresis': 667, + 'Egrave': 667, + 'Eth': 722, + 'Euro': 556, + 'F': 611, + 'G': 778, + 'H': 722, + 'I': 278, + 'Iacute': 278, + 'Icircumflex': 278, + 'Idieresis': 278, + 'Igrave': 278, + 'J': 556, + 'K': 722, + 'L': 611, + 'Lslash': 611, + 'M': 833, + 'N': 722, + 'Ntilde': 722, + 'O': 778, + 'OE': 1000, + 'Oacute': 778, + 'Ocircumflex': 778, + 'Odieresis': 778, + 'Ograve': 778, + 'Oslash': 778, + 'Otilde': 778, + 'P': 667, + 'Q': 778, + 'R': 722, + 'S': 667, + 'Scaron': 667, + 'T': 611, + 'Thorn': 667, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 667, + 'W': 944, + 'X': 667, + 'Y': 667, + 'Yacute': 667, + 'Ydieresis': 667, + 'Z': 611, + 'Zcaron': 611, + 'a': 556, + 'aacute': 556, + 'acircumflex': 556, + 'acute': 333, + 'adieresis': 556, + 'ae': 889, + 'agrave': 556, + 'ampersand': 722, + 'aring': 556, + 'asciicircum': 584, + 'asciitilde': 584, + 'asterisk': 389, + 'at': 975, + 'atilde': 556, + 'b': 611, + 'backslash': 278, + 'bar': 280, + 'braceleft': 389, + 'braceright': 389, + 'bracketleft': 333, + 'bracketright': 333, + 'breve': 333, + 'brokenbar': 280, + 'bullet': 350, + 'c': 556, + 'caron': 333, + 'ccedilla': 556, + 'cedilla': 333, + 'cent': 556, + 'circumflex': 333, + 'colon': 333, + 'comma': 278, + 'copyright': 737, + 'currency': 556, + 'd': 611, + 'dagger': 556, + 'daggerdbl': 556, + 'degree': 400, + 'dieresis': 333, + 'divide': 584, + 'dollar': 556, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 556, + 'eacute': 556, + 'ecircumflex': 556, + 'edieresis': 556, + 'egrave': 556, + 'eight': 556, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 556, + 'equal': 584, + 'eth': 611, + 'exclam': 333, + 'exclamdown': 333, + 'f': 333, + 'fi': 611, + 'five': 556, + 'fl': 611, + 'florin': 556, + 'four': 556, + 'fraction': 167, + 'g': 611, + 'germandbls': 611, + 'grave': 333, + 'greater': 584, + 'guillemotleft': 556, + 'guillemotright': 556, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 611, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 278, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 278, + 'k': 556, + 'l': 278, + 'less': 584, + 'logicalnot': 584, + 'lslash': 278, + 'm': 889, + 'macron': 333, + 'minus': 584, + 'mu': 611, + 'multiply': 584, + 'n': 611, + 'nine': 556, + 'ntilde': 611, + 'numbersign': 556, + 'o': 611, + 'oacute': 611, + 'ocircumflex': 611, + 'odieresis': 611, + 'oe': 944, + 'ogonek': 333, + 'ograve': 611, + 'one': 556, + 'onehalf': 834, + 'onequarter': 834, + 'onesuperior': 333, + 'ordfeminine': 370, + 'ordmasculine': 365, + 'oslash': 611, + 'otilde': 611, + 'p': 611, + 'paragraph': 556, + 'parenleft': 333, + 'parenright': 333, + 'percent': 889, + 'period': 278, + 'periodcentered': 278, + 'perthousand': 1000, + 'plus': 584, + 'plusminus': 584, + 'q': 611, + 'question': 611, + 'questiondown': 611, + 'quotedbl': 474, + 'quotedblbase': 500, + 'quotedblleft': 500, + 'quotedblright': 500, + 'quoteleft': 278, + 'quoteright': 278, + 'quotesinglbase': 278, + 'quotesingle': 238, + 'r': 389, + 'registered': 737, + 'ring': 333, + 's': 556, + 'scaron': 556, + 'section': 556, + 'semicolon': 333, + 'seven': 556, + 'six': 556, + 'slash': 278, + 'space': 278, + 'sterling': 556, + 't': 333, + 'thorn': 611, + 'three': 556, + 'threequarters': 834, + 'threesuperior': 333, + 'tilde': 333, + 'trademark': 1000, + 'two': 556, + 'twosuperior': 333, + 'u': 611, + 'uacute': 611, + 'ucircumflex': 611, + 'udieresis': 611, + 'ugrave': 611, + 'underscore': 556, + 'v': 556, + 'w': 778, + 'x': 556, + 'y': 556, + 'yacute': 556, + 'ydieresis': 556, + 'yen': 556, + 'z': 500, + 'zcaron': 500, + 'zero': 556} + +# Courier can be expressed more compactly! +_w = {} +for charname in widthsByFontGlyph['Helvetica'].keys(): + _w[charname] = 600 +widthsByFontGlyph['Courier'] = _w +widthsByFontGlyph['Courier-Bold'] = _w +widthsByFontGlyph['Courier-Oblique'] = _w +widthsByFontGlyph['Courier-BoldOblique'] = _w + +widthsByFontGlyph['Times-Roman'] = {'A': 722, + 'AE': 889, + 'Aacute': 722, + 'Acircumflex': 722, + 'Adieresis': 722, + 'Agrave': 722, + 'Aring': 722, + 'Atilde': 722, + 'B': 667, + 'C': 667, + 'Ccedilla': 667, + 'D': 722, + 'E': 611, + 'Eacute': 611, + 'Ecircumflex': 611, + 'Edieresis': 611, + 'Egrave': 611, + 'Eth': 722, + 'Euro': 500, + 'F': 556, + 'G': 722, + 'H': 722, + 'I': 333, + 'Iacute': 333, + 'Icircumflex': 333, + 'Idieresis': 333, + 'Igrave': 333, + 'J': 389, + 'K': 722, + 'L': 611, + 'Lslash': 611, + 'M': 889, + 'N': 722, + 'Ntilde': 722, + 'O': 722, + 'OE': 889, + 'Oacute': 722, + 'Ocircumflex': 722, + 'Odieresis': 722, + 'Ograve': 722, + 'Oslash': 722, + 'Otilde': 722, + 'P': 556, + 'Q': 722, + 'R': 667, + 'S': 556, + 'Scaron': 556, + 'T': 611, + 'Thorn': 556, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 722, + 'W': 944, + 'X': 722, + 'Y': 722, + 'Yacute': 722, + 'Ydieresis': 722, + 'Z': 611, + 'Zcaron': 611, + 'a': 444, + 'aacute': 444, + 'acircumflex': 444, + 'acute': 333, + 'adieresis': 444, + 'ae': 667, + 'agrave': 444, + 'ampersand': 778, + 'aring': 444, + 'asciicircum': 469, + 'asciitilde': 541, + 'asterisk': 500, + 'at': 921, + 'atilde': 444, + 'b': 500, + 'backslash': 278, + 'bar': 200, + 'braceleft': 480, + 'braceright': 480, + 'bracketleft': 333, + 'bracketright': 333, + 'breve': 333, + 'brokenbar': 200, + 'bullet': 350, + 'c': 444, + 'caron': 333, + 'ccedilla': 444, + 'cedilla': 333, + 'cent': 500, + 'circumflex': 333, + 'colon': 278, + 'comma': 250, + 'copyright': 760, + 'currency': 500, + 'd': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'degree': 400, + 'dieresis': 333, + 'divide': 564, + 'dollar': 500, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 444, + 'eacute': 444, + 'ecircumflex': 444, + 'edieresis': 444, + 'egrave': 444, + 'eight': 500, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 500, + 'equal': 564, + 'eth': 500, + 'exclam': 333, + 'exclamdown': 333, + 'f': 333, + 'fi': 556, + 'five': 500, + 'fl': 556, + 'florin': 500, + 'four': 500, + 'fraction': 167, + 'g': 500, + 'germandbls': 500, + 'grave': 333, + 'greater': 564, + 'guillemotleft': 500, + 'guillemotright': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 500, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 278, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 278, + 'k': 500, + 'l': 278, + 'less': 564, + 'logicalnot': 564, + 'lslash': 278, + 'm': 778, + 'macron': 333, + 'minus': 564, + 'mu': 500, + 'multiply': 564, + 'n': 500, + 'nine': 500, + 'ntilde': 500, + 'numbersign': 500, + 'o': 500, + 'oacute': 500, + 'ocircumflex': 500, + 'odieresis': 500, + 'oe': 722, + 'ogonek': 333, + 'ograve': 500, + 'one': 500, + 'onehalf': 750, + 'onequarter': 750, + 'onesuperior': 300, + 'ordfeminine': 276, + 'ordmasculine': 310, + 'oslash': 500, + 'otilde': 500, + 'p': 500, + 'paragraph': 453, + 'parenleft': 333, + 'parenright': 333, + 'percent': 833, + 'period': 250, + 'periodcentered': 250, + 'perthousand': 1000, + 'plus': 564, + 'plusminus': 564, + 'q': 500, + 'question': 444, + 'questiondown': 444, + 'quotedbl': 408, + 'quotedblbase': 444, + 'quotedblleft': 444, + 'quotedblright': 444, + 'quoteleft': 333, + 'quoteright': 333, + 'quotesinglbase': 333, + 'quotesingle': 180, + 'r': 333, + 'registered': 760, + 'ring': 333, + 's': 389, + 'scaron': 389, + 'section': 500, + 'semicolon': 278, + 'seven': 500, + 'six': 500, + 'slash': 278, + 'space': 250, + 'sterling': 500, + 't': 278, + 'thorn': 500, + 'three': 500, + 'threequarters': 750, + 'threesuperior': 300, + 'tilde': 333, + 'trademark': 980, + 'two': 500, + 'twosuperior': 300, + 'u': 500, + 'uacute': 500, + 'ucircumflex': 500, + 'udieresis': 500, + 'ugrave': 500, + 'underscore': 500, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'yacute': 500, + 'ydieresis': 500, + 'yen': 500, + 'z': 444, + 'zcaron': 444, + 'zero': 500} + +widthsByFontGlyph['Times-Bold'] = {'A': 722, + 'AE': 1000, + 'Aacute': 722, + 'Acircumflex': 722, + 'Adieresis': 722, + 'Agrave': 722, + 'Aring': 722, + 'Atilde': 722, + 'B': 667, + 'C': 722, + 'Ccedilla': 722, + 'D': 722, + 'E': 667, + 'Eacute': 667, + 'Ecircumflex': 667, + 'Edieresis': 667, + 'Egrave': 667, + 'Eth': 722, + 'Euro': 500, + 'F': 611, + 'G': 778, + 'H': 778, + 'I': 389, + 'Iacute': 389, + 'Icircumflex': 389, + 'Idieresis': 389, + 'Igrave': 389, + 'J': 500, + 'K': 778, + 'L': 667, + 'Lslash': 667, + 'M': 944, + 'N': 722, + 'Ntilde': 722, + 'O': 778, + 'OE': 1000, + 'Oacute': 778, + 'Ocircumflex': 778, + 'Odieresis': 778, + 'Ograve': 778, + 'Oslash': 778, + 'Otilde': 778, + 'P': 611, + 'Q': 778, + 'R': 722, + 'S': 556, + 'Scaron': 556, + 'T': 667, + 'Thorn': 611, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 722, + 'W': 1000, + 'X': 722, + 'Y': 722, + 'Yacute': 722, + 'Ydieresis': 722, + 'Z': 667, + 'Zcaron': 667, + 'a': 500, + 'aacute': 500, + 'acircumflex': 500, + 'acute': 333, + 'adieresis': 500, + 'ae': 722, + 'agrave': 500, + 'ampersand': 833, + 'aring': 500, + 'asciicircum': 581, + 'asciitilde': 520, + 'asterisk': 500, + 'at': 930, + 'atilde': 500, + 'b': 556, + 'backslash': 278, + 'bar': 220, + 'braceleft': 394, + 'braceright': 394, + 'bracketleft': 333, + 'bracketright': 333, + 'breve': 333, + 'brokenbar': 220, + 'bullet': 350, + 'c': 444, + 'caron': 333, + 'ccedilla': 444, + 'cedilla': 333, + 'cent': 500, + 'circumflex': 333, + 'colon': 333, + 'comma': 250, + 'copyright': 747, + 'currency': 500, + 'd': 556, + 'dagger': 500, + 'daggerdbl': 500, + 'degree': 400, + 'dieresis': 333, + 'divide': 570, + 'dollar': 500, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 444, + 'eacute': 444, + 'ecircumflex': 444, + 'edieresis': 444, + 'egrave': 444, + 'eight': 500, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 500, + 'equal': 570, + 'eth': 500, + 'exclam': 333, + 'exclamdown': 333, + 'f': 333, + 'fi': 556, + 'five': 500, + 'fl': 556, + 'florin': 500, + 'four': 500, + 'fraction': 167, + 'g': 500, + 'germandbls': 556, + 'grave': 333, + 'greater': 570, + 'guillemotleft': 500, + 'guillemotright': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 556, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 278, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 333, + 'k': 556, + 'l': 278, + 'less': 570, + 'logicalnot': 570, + 'lslash': 278, + 'm': 833, + 'macron': 333, + 'minus': 570, + 'mu': 556, + 'multiply': 570, + 'n': 556, + 'nine': 500, + 'ntilde': 556, + 'numbersign': 500, + 'o': 500, + 'oacute': 500, + 'ocircumflex': 500, + 'odieresis': 500, + 'oe': 722, + 'ogonek': 333, + 'ograve': 500, + 'one': 500, + 'onehalf': 750, + 'onequarter': 750, + 'onesuperior': 300, + 'ordfeminine': 300, + 'ordmasculine': 330, + 'oslash': 500, + 'otilde': 500, + 'p': 556, + 'paragraph': 540, + 'parenleft': 333, + 'parenright': 333, + 'percent': 1000, + 'period': 250, + 'periodcentered': 250, + 'perthousand': 1000, + 'plus': 570, + 'plusminus': 570, + 'q': 556, + 'question': 500, + 'questiondown': 500, + 'quotedbl': 555, + 'quotedblbase': 500, + 'quotedblleft': 500, + 'quotedblright': 500, + 'quoteleft': 333, + 'quoteright': 333, + 'quotesinglbase': 333, + 'quotesingle': 278, + 'r': 444, + 'registered': 747, + 'ring': 333, + 's': 389, + 'scaron': 389, + 'section': 500, + 'semicolon': 333, + 'seven': 500, + 'six': 500, + 'slash': 278, + 'space': 250, + 'sterling': 500, + 't': 333, + 'thorn': 556, + 'three': 500, + 'threequarters': 750, + 'threesuperior': 300, + 'tilde': 333, + 'trademark': 1000, + 'two': 500, + 'twosuperior': 300, + 'u': 556, + 'uacute': 556, + 'ucircumflex': 556, + 'udieresis': 556, + 'ugrave': 556, + 'underscore': 500, + 'v': 500, + 'w': 722, + 'x': 500, + 'y': 500, + 'yacute': 500, + 'ydieresis': 500, + 'yen': 500, + 'z': 444, + 'zcaron': 444, + 'zero': 500} + +widthsByFontGlyph['Times-Italic'] = {'A': 611, + 'AE': 889, + 'Aacute': 611, + 'Acircumflex': 611, + 'Adieresis': 611, + 'Agrave': 611, + 'Aring': 611, + 'Atilde': 611, + 'B': 611, + 'C': 667, + 'Ccedilla': 667, + 'D': 722, + 'E': 611, + 'Eacute': 611, + 'Ecircumflex': 611, + 'Edieresis': 611, + 'Egrave': 611, + 'Eth': 722, + 'Euro': 500, + 'F': 611, + 'G': 722, + 'H': 722, + 'I': 333, + 'Iacute': 333, + 'Icircumflex': 333, + 'Idieresis': 333, + 'Igrave': 333, + 'J': 444, + 'K': 667, + 'L': 556, + 'Lslash': 556, + 'M': 833, + 'N': 667, + 'Ntilde': 667, + 'O': 722, + 'OE': 944, + 'Oacute': 722, + 'Ocircumflex': 722, + 'Odieresis': 722, + 'Ograve': 722, + 'Oslash': 722, + 'Otilde': 722, + 'P': 611, + 'Q': 722, + 'R': 611, + 'S': 500, + 'Scaron': 500, + 'T': 556, + 'Thorn': 611, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 611, + 'W': 833, + 'X': 611, + 'Y': 556, + 'Yacute': 556, + 'Ydieresis': 556, + 'Z': 556, + 'Zcaron': 556, + 'a': 500, + 'aacute': 500, + 'acircumflex': 500, + 'acute': 333, + 'adieresis': 500, + 'ae': 667, + 'agrave': 500, + 'ampersand': 778, + 'aring': 500, + 'asciicircum': 422, + 'asciitilde': 541, + 'asterisk': 500, + 'at': 920, + 'atilde': 500, + 'b': 500, + 'backslash': 278, + 'bar': 275, + 'braceleft': 400, + 'braceright': 400, + 'bracketleft': 389, + 'bracketright': 389, + 'breve': 333, + 'brokenbar': 275, + 'bullet': 350, + 'c': 444, + 'caron': 333, + 'ccedilla': 444, + 'cedilla': 333, + 'cent': 500, + 'circumflex': 333, + 'colon': 333, + 'comma': 250, + 'copyright': 760, + 'currency': 500, + 'd': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'degree': 400, + 'dieresis': 333, + 'divide': 675, + 'dollar': 500, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 444, + 'eacute': 444, + 'ecircumflex': 444, + 'edieresis': 444, + 'egrave': 444, + 'eight': 500, + 'ellipsis': 889, + 'emdash': 889, + 'endash': 500, + 'equal': 675, + 'eth': 500, + 'exclam': 333, + 'exclamdown': 389, + 'f': 278, + 'fi': 500, + 'five': 500, + 'fl': 500, + 'florin': 500, + 'four': 500, + 'fraction': 167, + 'g': 500, + 'germandbls': 500, + 'grave': 333, + 'greater': 675, + 'guillemotleft': 500, + 'guillemotright': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 500, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 278, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 278, + 'k': 444, + 'l': 278, + 'less': 675, + 'logicalnot': 675, + 'lslash': 278, + 'm': 722, + 'macron': 333, + 'minus': 675, + 'mu': 500, + 'multiply': 675, + 'n': 500, + 'nine': 500, + 'ntilde': 500, + 'numbersign': 500, + 'o': 500, + 'oacute': 500, + 'ocircumflex': 500, + 'odieresis': 500, + 'oe': 667, + 'ogonek': 333, + 'ograve': 500, + 'one': 500, + 'onehalf': 750, + 'onequarter': 750, + 'onesuperior': 300, + 'ordfeminine': 276, + 'ordmasculine': 310, + 'oslash': 500, + 'otilde': 500, + 'p': 500, + 'paragraph': 523, + 'parenleft': 333, + 'parenright': 333, + 'percent': 833, + 'period': 250, + 'periodcentered': 250, + 'perthousand': 1000, + 'plus': 675, + 'plusminus': 675, + 'q': 500, + 'question': 500, + 'questiondown': 500, + 'quotedbl': 420, + 'quotedblbase': 556, + 'quotedblleft': 556, + 'quotedblright': 556, + 'quoteleft': 333, + 'quoteright': 333, + 'quotesinglbase': 333, + 'quotesingle': 214, + 'r': 389, + 'registered': 760, + 'ring': 333, + 's': 389, + 'scaron': 389, + 'section': 500, + 'semicolon': 333, + 'seven': 500, + 'six': 500, + 'slash': 278, + 'space': 250, + 'sterling': 500, + 't': 278, + 'thorn': 500, + 'three': 500, + 'threequarters': 750, + 'threesuperior': 300, + 'tilde': 333, + 'trademark': 980, + 'two': 500, + 'twosuperior': 300, + 'u': 500, + 'uacute': 500, + 'ucircumflex': 500, + 'udieresis': 500, + 'ugrave': 500, + 'underscore': 500, + 'v': 444, + 'w': 667, + 'x': 444, + 'y': 444, + 'yacute': 444, + 'ydieresis': 444, + 'yen': 500, + 'z': 389, + 'zcaron': 389, + 'zero': 500} + +widthsByFontGlyph['Times-BoldItalic'] = {'A': 667, + 'AE': 944, + 'Aacute': 667, + 'Acircumflex': 667, + 'Adieresis': 667, + 'Agrave': 667, + 'Aring': 667, + 'Atilde': 667, + 'B': 667, + 'C': 667, + 'Ccedilla': 667, + 'D': 722, + 'E': 667, + 'Eacute': 667, + 'Ecircumflex': 667, + 'Edieresis': 667, + 'Egrave': 667, + 'Eth': 722, + 'Euro': 500, + 'F': 667, + 'G': 722, + 'H': 778, + 'I': 389, + 'Iacute': 389, + 'Icircumflex': 389, + 'Idieresis': 389, + 'Igrave': 389, + 'J': 500, + 'K': 667, + 'L': 611, + 'Lslash': 611, + 'M': 889, + 'N': 722, + 'Ntilde': 722, + 'O': 722, + 'OE': 944, + 'Oacute': 722, + 'Ocircumflex': 722, + 'Odieresis': 722, + 'Ograve': 722, + 'Oslash': 722, + 'Otilde': 722, + 'P': 611, + 'Q': 722, + 'R': 667, + 'S': 556, + 'Scaron': 556, + 'T': 611, + 'Thorn': 611, + 'U': 722, + 'Uacute': 722, + 'Ucircumflex': 722, + 'Udieresis': 722, + 'Ugrave': 722, + 'V': 667, + 'W': 889, + 'X': 667, + 'Y': 611, + 'Yacute': 611, + 'Ydieresis': 611, + 'Z': 611, + 'Zcaron': 611, + 'a': 500, + 'aacute': 500, + 'acircumflex': 500, + 'acute': 333, + 'adieresis': 500, + 'ae': 722, + 'agrave': 500, + 'ampersand': 778, + 'aring': 500, + 'asciicircum': 570, + 'asciitilde': 570, + 'asterisk': 500, + 'at': 832, + 'atilde': 500, + 'b': 500, + 'backslash': 278, + 'bar': 220, + 'braceleft': 348, + 'braceright': 348, + 'bracketleft': 333, + 'bracketright': 333, + 'breve': 333, + 'brokenbar': 220, + 'bullet': 350, + 'c': 444, + 'caron': 333, + 'ccedilla': 444, + 'cedilla': 333, + 'cent': 500, + 'circumflex': 333, + 'colon': 333, + 'comma': 250, + 'copyright': 747, + 'currency': 500, + 'd': 500, + 'dagger': 500, + 'daggerdbl': 500, + 'degree': 400, + 'dieresis': 333, + 'divide': 570, + 'dollar': 500, + 'dotaccent': 333, + 'dotlessi': 278, + 'e': 444, + 'eacute': 444, + 'ecircumflex': 444, + 'edieresis': 444, + 'egrave': 444, + 'eight': 500, + 'ellipsis': 1000, + 'emdash': 1000, + 'endash': 500, + 'equal': 570, + 'eth': 500, + 'exclam': 389, + 'exclamdown': 389, + 'f': 333, + 'fi': 556, + 'five': 500, + 'fl': 556, + 'florin': 500, + 'four': 500, + 'fraction': 167, + 'g': 500, + 'germandbls': 500, + 'grave': 333, + 'greater': 570, + 'guillemotleft': 500, + 'guillemotright': 500, + 'guilsinglleft': 333, + 'guilsinglright': 333, + 'h': 556, + 'hungarumlaut': 333, + 'hyphen': 333, + 'i': 278, + 'iacute': 278, + 'icircumflex': 278, + 'idieresis': 278, + 'igrave': 278, + 'j': 278, + 'k': 500, + 'l': 278, + 'less': 570, + 'logicalnot': 606, + 'lslash': 278, + 'm': 778, + 'macron': 333, + 'minus': 606, + 'mu': 576, + 'multiply': 570, + 'n': 556, + 'nine': 500, + 'ntilde': 556, + 'numbersign': 500, + 'o': 500, + 'oacute': 500, + 'ocircumflex': 500, + 'odieresis': 500, + 'oe': 722, + 'ogonek': 333, + 'ograve': 500, + 'one': 500, + 'onehalf': 750, + 'onequarter': 750, + 'onesuperior': 300, + 'ordfeminine': 266, + 'ordmasculine': 300, + 'oslash': 500, + 'otilde': 500, + 'p': 500, + 'paragraph': 500, + 'parenleft': 333, + 'parenright': 333, + 'percent': 833, + 'period': 250, + 'periodcentered': 250, + 'perthousand': 1000, + 'plus': 570, + 'plusminus': 570, + 'q': 500, + 'question': 500, + 'questiondown': 500, + 'quotedbl': 555, + 'quotedblbase': 500, + 'quotedblleft': 500, + 'quotedblright': 500, + 'quoteleft': 333, + 'quoteright': 333, + 'quotesinglbase': 333, + 'quotesingle': 278, + 'r': 389, + 'registered': 747, + 'ring': 333, + 's': 389, + 'scaron': 389, + 'section': 500, + 'semicolon': 333, + 'seven': 500, + 'six': 500, + 'slash': 278, + 'space': 250, + 'sterling': 500, + 't': 278, + 'thorn': 500, + 'three': 500, + 'threequarters': 750, + 'threesuperior': 300, + 'tilde': 333, + 'trademark': 1000, + 'two': 500, + 'twosuperior': 300, + 'u': 556, + 'uacute': 556, + 'ucircumflex': 556, + 'udieresis': 556, + 'ugrave': 556, + 'underscore': 500, + 'v': 444, + 'w': 667, + 'x': 500, + 'y': 444, + 'yacute': 444, + 'ydieresis': 444, + 'yen': 500, + 'z': 389, + 'zcaron': 389, + 'zero': 500} + +widthsByFontGlyph['Symbol'] = {'Alpha': 722, + 'Beta': 667, + 'Chi': 722, + 'Delta': 612, + 'Epsilon': 611, + 'Eta': 722, + 'Euro': 750, + 'Gamma': 603, + 'Ifraktur': 686, + 'Iota': 333, + 'Kappa': 722, + 'Lambda': 686, + 'Mu': 889, + 'Nu': 722, + 'Omega': 768, + 'Omicron': 722, + 'Phi': 763, + 'Pi': 768, + 'Psi': 795, + 'Rfraktur': 795, + 'Rho': 556, + 'Sigma': 592, + 'Tau': 611, + 'Theta': 741, + 'Upsilon': 690, + 'Upsilon1': 620, + 'Xi': 645, + 'Zeta': 611, + 'aleph': 823, + 'alpha': 631, + 'ampersand': 778, + 'angle': 768, + 'angleleft': 329, + 'angleright': 329, + 'apple': 790, + 'approxequal': 549, + 'arrowboth': 1042, + 'arrowdblboth': 1042, + 'arrowdbldown': 603, + 'arrowdblleft': 987, + 'arrowdblright': 987, + 'arrowdblup': 603, + 'arrowdown': 603, + 'arrowhorizex': 1000, + 'arrowleft': 987, + 'arrowright': 987, + 'arrowup': 603, + 'arrowvertex': 603, + 'asteriskmath': 500, + 'bar': 200, + 'beta': 549, + 'braceex': 494, + 'braceleft': 480, + 'braceleftbt': 494, + 'braceleftmid': 494, + 'bracelefttp': 494, + 'braceright': 480, + 'bracerightbt': 494, + 'bracerightmid': 494, + 'bracerighttp': 494, + 'bracketleft': 333, + 'bracketleftbt': 384, + 'bracketleftex': 384, + 'bracketlefttp': 384, + 'bracketright': 333, + 'bracketrightbt': 384, + 'bracketrightex': 384, + 'bracketrighttp': 384, + 'bullet': 460, + 'carriagereturn': 658, + 'chi': 549, + 'circlemultiply': 768, + 'circleplus': 768, + 'club': 753, + 'colon': 278, + 'comma': 250, + 'congruent': 549, + 'copyrightsans': 790, + 'copyrightserif': 790, + 'degree': 400, + 'delta': 494, + 'diamond': 753, + 'divide': 549, + 'dotmath': 250, + 'eight': 500, + 'element': 713, + 'ellipsis': 1000, + 'emptyset': 823, + 'epsilon': 439, + 'equal': 549, + 'equivalence': 549, + 'eta': 603, + 'exclam': 333, + 'existential': 549, + 'five': 500, + 'florin': 500, + 'four': 500, + 'fraction': 167, + 'gamma': 411, + 'gradient': 713, + 'greater': 549, + 'greaterequal': 549, + 'heart': 753, + 'infinity': 713, + 'integral': 274, + 'integralbt': 686, + 'integralex': 686, + 'integraltp': 686, + 'intersection': 768, + 'iota': 329, + 'kappa': 549, + 'lambda': 549, + 'less': 549, + 'lessequal': 549, + 'logicaland': 603, + 'logicalnot': 713, + 'logicalor': 603, + 'lozenge': 494, + 'minus': 549, + 'minute': 247, + 'mu': 576, + 'multiply': 549, + 'nine': 500, + 'notelement': 713, + 'notequal': 549, + 'notsubset': 713, + 'nu': 521, + 'numbersign': 500, + 'omega': 686, + 'omega1': 713, + 'omicron': 549, + 'one': 500, + 'parenleft': 333, + 'parenleftbt': 384, + 'parenleftex': 384, + 'parenlefttp': 384, + 'parenright': 333, + 'parenrightbt': 384, + 'parenrightex': 384, + 'parenrighttp': 384, + 'partialdiff': 494, + 'percent': 833, + 'period': 250, + 'perpendicular': 658, + 'phi': 521, + 'phi1': 603, + 'pi': 549, + 'plus': 549, + 'plusminus': 549, + 'product': 823, + 'propersubset': 713, + 'propersuperset': 713, + 'proportional': 713, + 'psi': 686, + 'question': 444, + 'radical': 549, + 'radicalex': 500, + 'reflexsubset': 713, + 'reflexsuperset': 713, + 'registersans': 790, + 'registerserif': 790, + 'rho': 549, + 'second': 411, + 'semicolon': 278, + 'seven': 500, + 'sigma': 603, + 'sigma1': 439, + 'similar': 549, + 'six': 500, + 'slash': 278, + 'space': 250, + 'spade': 753, + 'suchthat': 439, + 'summation': 713, + 'tau': 439, + 'therefore': 863, + 'theta': 521, + 'theta1': 631, + 'three': 500, + 'trademarksans': 786, + 'trademarkserif': 890, + 'two': 500, + 'underscore': 500, + 'union': 768, + 'universal': 713, + 'upsilon': 576, + 'weierstrass': 987, + 'xi': 493, + 'zero': 500, + 'zeta': 494} + +widthsByFontGlyph['ZapfDingbats'] = {'a1': 974, + 'a10': 692, + 'a100': 668, + 'a101': 732, + 'a102': 544, + 'a103': 544, + 'a104': 910, + 'a105': 911, + 'a106': 667, + 'a107': 760, + 'a108': 760, + 'a109': 626, + 'a11': 960, + 'a110': 694, + 'a111': 595, + 'a112': 776, + 'a117': 690, + 'a118': 791, + 'a119': 790, + 'a12': 939, + 'a120': 788, + 'a121': 788, + 'a122': 788, + 'a123': 788, + 'a124': 788, + 'a125': 788, + 'a126': 788, + 'a127': 788, + 'a128': 788, + 'a129': 788, + 'a13': 549, + 'a130': 788, + 'a131': 788, + 'a132': 788, + 'a133': 788, + 'a134': 788, + 'a135': 788, + 'a136': 788, + 'a137': 788, + 'a138': 788, + 'a139': 788, + 'a14': 855, + 'a140': 788, + 'a141': 788, + 'a142': 788, + 'a143': 788, + 'a144': 788, + 'a145': 788, + 'a146': 788, + 'a147': 788, + 'a148': 788, + 'a149': 788, + 'a15': 911, + 'a150': 788, + 'a151': 788, + 'a152': 788, + 'a153': 788, + 'a154': 788, + 'a155': 788, + 'a156': 788, + 'a157': 788, + 'a158': 788, + 'a159': 788, + 'a16': 933, + 'a160': 894, + 'a161': 838, + 'a162': 924, + 'a163': 1016, + 'a164': 458, + 'a165': 924, + 'a166': 918, + 'a167': 927, + 'a168': 928, + 'a169': 928, + 'a17': 945, + 'a170': 834, + 'a171': 873, + 'a172': 828, + 'a173': 924, + 'a174': 917, + 'a175': 930, + 'a176': 931, + 'a177': 463, + 'a178': 883, + 'a179': 836, + 'a18': 974, + 'a180': 867, + 'a181': 696, + 'a182': 874, + 'a183': 760, + 'a184': 946, + 'a185': 865, + 'a186': 967, + 'a187': 831, + 'a188': 873, + 'a189': 927, + 'a19': 755, + 'a190': 970, + 'a191': 918, + 'a192': 748, + 'a193': 836, + 'a194': 771, + 'a195': 888, + 'a196': 748, + 'a197': 771, + 'a198': 888, + 'a199': 867, + 'a2': 961, + 'a20': 846, + 'a200': 696, + 'a201': 874, + 'a202': 974, + 'a203': 762, + 'a204': 759, + 'a205': 509, + 'a206': 410, + 'a21': 762, + 'a22': 761, + 'a23': 571, + 'a24': 677, + 'a25': 763, + 'a26': 760, + 'a27': 759, + 'a28': 754, + 'a29': 786, + 'a3': 980, + 'a30': 788, + 'a31': 788, + 'a32': 790, + 'a33': 793, + 'a34': 794, + 'a35': 816, + 'a36': 823, + 'a37': 789, + 'a38': 841, + 'a39': 823, + 'a4': 719, + 'a40': 833, + 'a41': 816, + 'a42': 831, + 'a43': 923, + 'a44': 744, + 'a45': 723, + 'a46': 749, + 'a47': 790, + 'a48': 792, + 'a49': 695, + 'a5': 789, + 'a50': 776, + 'a51': 768, + 'a52': 792, + 'a53': 759, + 'a54': 707, + 'a55': 708, + 'a56': 682, + 'a57': 701, + 'a58': 826, + 'a59': 815, + 'a6': 494, + 'a60': 789, + 'a61': 789, + 'a62': 707, + 'a63': 687, + 'a64': 696, + 'a65': 689, + 'a66': 786, + 'a67': 787, + 'a68': 713, + 'a69': 791, + 'a7': 552, + 'a70': 785, + 'a71': 791, + 'a72': 873, + 'a73': 761, + 'a74': 762, + 'a75': 759, + 'a76': 892, + 'a77': 892, + 'a78': 788, + 'a79': 784, + 'a8': 537, + 'a81': 438, + 'a82': 138, + 'a83': 277, + 'a84': 415, + 'a85': 509, + 'a86': 410, + 'a87': 234, + 'a88': 234, + 'a89': 390, + 'a9': 577, + 'a90': 390, + 'a91': 276, + 'a92': 276, + 'a93': 317, + 'a94': 317, + 'a95': 334, + 'a96': 334, + 'a97': 392, + 'a98': 392, + 'a99': 668, + 'space': 278} diff --git a/bin/reportlab/pdfbase/cidfonts.py b/bin/reportlab/pdfbase/cidfonts.py new file mode 100644 index 00000000000..ba438c40f53 --- /dev/null +++ b/bin/reportlab/pdfbase/cidfonts.py @@ -0,0 +1,516 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/cidfonts.py +#$Header $ +__version__=''' $Id: cidfonts.py 2905 2006-05-23 14:49:28Z andy $ ''' +__doc__="""CID (Asian multi-byte) font support. + +This defines classes to represent CID fonts. They know how to calculate +their own width and how to write themselves into PDF files.""" + +import os +from types import ListType, TupleType, DictType +from string import find, split, strip +import marshal +import md5 +import time + +import reportlab +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase._cidfontdata import allowedTypeFaces, allowedEncodings, CIDFontInfo, \ + defaultUnicodeEncodings, widthsByUnichar +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase import pdfdoc +from reportlab.pdfbase.pdfutils import _escape +from reportlab.rl_config import CMapSearchPath + + +#quick hackery for 2.0 release. Now we always do unicode, and have built in +#the CMAP data, any code to load CMap files is not needed. +DISABLE_CMAP = True + + +def findCMapFile(name): + "Returns full filename, or raises error" + for dirname in CMapSearchPath: + cmapfile = dirname + os.sep + name + if os.path.isfile(cmapfile): + #print "found", cmapfile + return cmapfile + raise IOError, 'CMAP file for encodings "%s" not found!' % name + +def structToPDF(structure): + "Converts deeply nested structure to PDFdoc dictionary/array objects" + if type(structure) is DictType: + newDict = {} + for k, v in structure.items(): + newDict[k] = structToPDF(v) + return pdfdoc.PDFDictionary(newDict) + elif type(structure) in (ListType, TupleType): + newList = [] + for elem in structure: + newList.append(structToPDF(elem)) + return pdfdoc.PDFArray(newList) + else: + return structure + +class CIDEncoding(pdfmetrics.Encoding): + """Multi-byte encoding. These are loaded from CMAP files. + + A CMAP file is like a mini-codec. It defines the correspondence + between code points in the (multi-byte) input data and Character + IDs. """ + # aims to do similar things to Brian Hooper's CMap class, + # but I could not get it working and had to rewrite. + # also, we should really rearrange our current encoding + # into a SingleByteEncoding since many of its methods + # should not apply here. + + def __init__(self, name, useCache=1): + self.name = name + self._mapFileHash = None + self._codeSpaceRanges = [] + self._notDefRanges = [] + self._cmap = {} + self.source = None + if not DISABLE_CMAP: + if useCache: + from reportlab.lib.utils import get_rl_tempdir + fontmapdir = get_rl_tempdir('FastCMAPS') + if os.path.isfile(fontmapdir + os.sep + name + '.fastmap'): + self.fastLoad(fontmapdir) + self.source = fontmapdir + os.sep + name + '.fastmap' + else: + self.parseCMAPFile(name) + self.source = 'CMAP: ' + name + self.fastSave(fontmapdir) + else: + self.parseCMAPFile(name) + + def _hash(self, text): + hasher = md5.new() + hasher.update(text) + return hasher.digest() + + def parseCMAPFile(self, name): + """This is a tricky one as CMAP files are Postscript + ones. Some refer to others with a 'usecmap' + command""" + #started = time.clock() + cmapfile = findCMapFile(name) + # this will CRAWL with the unicode encodings... + rawdata = open(cmapfile, 'r').read() + + self._mapFileHash = self._hash(rawdata) + #if it contains the token 'usecmap', parse the other + #cmap file first.... + usecmap_pos = find(rawdata, 'usecmap') + if usecmap_pos > -1: + #they tell us to look in another file + #for the code space ranges. The one + # to use will be the previous word. + chunk = rawdata[0:usecmap_pos] + words = split(chunk) + otherCMAPName = words[-1] + #print 'referred to another CMAP %s' % otherCMAPName + self.parseCMAPFile(otherCMAPName) + # now continue parsing this, as it may + # override some settings + + + words = split(rawdata) + while words <> []: + if words[0] == 'begincodespacerange': + words = words[1:] + while words[0] <> 'endcodespacerange': + strStart, strEnd, words = words[0], words[1], words[2:] + start = int(strStart[1:-1], 16) + end = int(strEnd[1:-1], 16) + self._codeSpaceRanges.append((start, end),) + elif words[0] == 'beginnotdefrange': + words = words[1:] + while words[0] <> 'endnotdefrange': + strStart, strEnd, strValue = words[0:3] + start = int(strStart[1:-1], 16) + end = int(strEnd[1:-1], 16) + value = int(strValue) + self._notDefRanges.append((start, end, value),) + words = words[3:] + elif words[0] == 'begincidrange': + words = words[1:] + while words[0] <> 'endcidrange': + strStart, strEnd, strValue = words[0:3] + start = int(strStart[1:-1], 16) + end = int(strEnd[1:-1], 16) + value = int(strValue) + # this means that 'start' corresponds to 'value', + # start+1 corresponds to value+1 and so on up + # to end + offset = 0 + while start + offset <= end: + self._cmap[start + offset] = value + offset + offset = offset + 1 + words = words[3:] + + else: + words = words[1:] + #finished = time.clock() + #print 'parsed CMAP %s in %0.4f seconds' % (self.name, finished - started) + + def translate(self, text): + "Convert a string into a list of CIDs" + output = [] + cmap = self._cmap + lastChar = '' + for char in text: + if lastChar <> '': + #print 'convert character pair "%s"' % (lastChar + char) + num = ord(lastChar) * 256 + ord(char) + else: + #print 'convert character "%s"' % char + num = ord(char) + lastChar = char + found = 0 + for low, high in self._codeSpaceRanges: + if low < num < high: + try: + cid = cmap[num] + #print '%d -> %d' % (num, cid) + except KeyError: + #not defined. Try to find the appropriate + # notdef character, or failing that return + # zero + cid = 0 + for low2, high2, notdef in self._notDefRanges: + if low2 < num < high2: + cid = notdef + break + output.append(cid) + found = 1 + break + if found: + lastChar = '' + else: + lastChar = char + return output + + def fastSave(self, directory): + f = open(os.path.join(directory, self.name + '.fastmap'), 'wb') + marshal.dump(self._mapFileHash, f) + marshal.dump(self._codeSpaceRanges, f) + marshal.dump(self._notDefRanges, f) + marshal.dump(self._cmap, f) + f.close() + + def fastLoad(self, directory): + started = time.clock() + f = open(os.path.join(directory, self.name + '.fastmap'), 'rb') + self._mapFileHash = marshal.load(f) + self._codeSpaceRanges = marshal.load(f) + self._notDefRanges = marshal.load(f) + self._cmap = marshal.load(f) + f.close() + finished = time.clock() + #print 'loaded %s in %0.4f seconds' % (self.name, finished - started) + + def getData(self): + """Simple persistence helper. Return a dict with all that matters.""" + return { + 'mapFileHash': self._mapFileHash, + 'codeSpaceRanges': self._codeSpaceRanges, + 'notDefRanges': self._notDefRanges, + 'cmap': self._cmap, + + } + + +class CIDTypeFace(pdfmetrics.TypeFace): + """Multi-byte type face. + + Conceptually similar to a single byte typeface, + but the glyphs are identified by a numeric Character + ID (CID) and not a glyph name. """ + def __init__(self, name): + """Initialised from one of the canned dictionaries in allowedEncodings + + Or rather, it will be shortly...""" + pdfmetrics.TypeFace.__init__(self, name) + self._extractDictInfo(name) + def _extractDictInfo(self, name): + try: + fontDict = CIDFontInfo[name] + except KeyError: + raise KeyError, ("Unable to find information on CID typeface '%s'" % name + + "Only the following font names work:" + repr(allowedTypeFaces) + ) + descFont = fontDict['DescendantFonts'][0] + self.ascent = descFont['FontDescriptor']['Ascent'] + self.descent = descFont['FontDescriptor']['Descent'] + self._defaultWidth = descFont['DW'] + self._explicitWidths = self._expandWidths(descFont['W']) + + # should really support self.glyphWidths, self.glyphNames + # but not done yet. + + + def _expandWidths(self, compactWidthArray): + """Expands Adobe nested list structure to get a dictionary of widths. + + Here is an example of such a structure. + ( + # starting at character ID 1, next n characters have the widths given. + 1, (277,305,500,668,668,906,727,305,445,445,508,668,305,379,305,539), + # all Characters from ID 17 to 26 are 668 em units wide + 17, 26, 668, + 27, (305, 305, 668, 668, 668, 566, 871, 727, 637, 652, 699, 574, 555, + 676, 687, 242, 492, 664, 582, 789, 707, 734, 582, 734, 605, 605, + 641, 668, 727, 945, 609, 609, 574, 445, 668, 445, 668, 668, 590, + 555, 609, 547, 602, 574, 391, 609, 582, 234, 277, 539, 234, 895, + 582, 605, 602, 602, 387, 508, 441, 582, 562, 781, 531, 570, 555, + 449, 246, 449, 668), + # these must be half width katakana and the like. + 231, 632, 500 + ) + """ + data = compactWidthArray[:] + widths = {} + while data: + start, data = data[0], data[1:] + if type(data[0]) in (ListType, TupleType): + items, data = data[0], data[1:] + for offset in range(len(items)): + widths[start + offset] = items[offset] + else: + end, width, data = data[0], data[1], data[2:] + for idx in range(start, end+1): + widths[idx] = width + return widths + + def getCharWidth(self, characterId): + return self._explicitWidths.get(characterId, self._defaultWidth) + +class CIDFont(pdfmetrics.Font): + "Represents a built-in multi-byte font" + def __init__(self, face, encoding): + + self._multiByte = 1 + assert face in allowedTypeFaces, "TypeFace '%s' not supported! Use any of these instead: %s" % (face, allowedTypeFaces) + self.faceName = face + #should cache in registry... + self.face = CIDTypeFace(face) + + assert encoding in allowedEncodings, "Encoding '%s' not supported! Use any of these instead: %s" % (encoding, allowedEncodings) + self.encodingName = encoding + self.encoding = CIDEncoding(encoding) + + #legacy hack doing quick cut and paste. + self.fontName = self.faceName + '-' + self.encodingName + self.name = self.fontName + + # need to know if it is vertical or horizontal + self.isVertical = (self.encodingName[-1] == 'V') + + + #no substitutes initially + self.substitutionFonts = [] + + def formatForPdf(self, text): + encoded = _escape(text) + #print 'encoded CIDFont:', encoded + return encoded + + def stringWidth(self, text, size, encoding=None): + """This presumes non-Unicode input. UnicodeCIDFont wraps it for that context""" + cidlist = self.encoding.translate(text) + if self.isVertical: + #this part is "not checked!" but seems to work. + #assume each is 1000 ems high + return len(cidlist) * size + else: + w = 0 + for cid in cidlist: + w = w + self.face.getCharWidth(cid) + return 0.001 * w * size + + + def addObjects(self, doc): + """The explicit code in addMinchoObjects and addGothicObjects + will be replaced by something that pulls the data from + _cidfontdata.py in the next few days.""" + internalName = 'F' + repr(len(doc.fontMapping)+1) + + bigDict = CIDFontInfo[self.face.name] + bigDict['Name'] = '/' + internalName + bigDict['Encoding'] = '/' + self.encodingName + + #convert to PDF dictionary/array objects + cidObj = structToPDF(bigDict) + + # link into document, and add to font map + r = doc.Reference(cidObj, internalName) + fontDict = doc.idToObject['BasicFonts'].dict + fontDict[internalName] = r + doc.fontMapping[self.name] = '/' + internalName + + +class UnicodeCIDFont(CIDFont): + """Wraps up CIDFont to hide explicit encoding choice; + encodes text for output as UTF16. + + lang should be one of 'jpn',chs','cht','kor' for now. + if vertical is set, it will select a different widths array + and possibly glyphs for some punctuation marks. + + halfWidth is only for Japanese. + + + >>> dodgy = UnicodeCIDFont('nonexistent') + Traceback (most recent call last): + ... + KeyError: "don't know anything about CID font nonexistent" + >>> heisei = UnicodeCIDFont('HeiseiMin-W3') + >>> heisei.name + 'HeiseiMin-W3' + >>> heisei.language + 'jpn' + >>> heisei.encoding.name + 'UniJIS-UCS2-H' + >>> #This is how PDF data gets encoded. + >>> print heisei.formatForPdf('hello') + \\377\\376h\\000e\\000l\\000l\\000o\\000 + >>> tokyo = u'\u6771\u4AEC' + >>> print heisei.formatForPdf(tokyo) + \\377\\376qg\\354J + + """ + + def __init__(self, face, isVertical=False, isHalfWidth=False): + #pass + try: + lang, defaultEncoding = defaultUnicodeEncodings[face] + except KeyError: + raise KeyError("don't know anything about CID font %s" % face) + + #we know the languages now. + self.language = lang + + #rebuilt encoding string. They follow rules which work + #for the 7 fonts provided. + enc = defaultEncoding[:-1] + if isHalfWidth: + enc = enc + 'HW-' + if isVertical: + enc = enc + 'V' + else: + enc = enc + 'H' + + #now we can do the more general case + CIDFont.__init__(self, face, enc) + #self.encName = 'utf_16_le' + #it's simpler for unicode, just use the face name + self.name = self.fontName = face + self.vertical = isVertical + self.isHalfWidth = isHalfWidth + + self.unicodeWidths = widthsByUnichar[self.name] + + + def formatForPdf(self, text): + #these ones should be encoded asUTF16 minus the BOM + from codecs import utf_16_be_encode + #print 'formatting %s: %s' % (type(text), repr(text)) + if type(text) is not unicode: + text = text.decode('utf8') + utfText = utf_16_be_encode(text)[0] + encoded = _escape(utfText) + #print ' encoded:',encoded + return encoded + # + #result = _escape(encoded) + #print ' -> %s' % repr(result) + #return result + + + def stringWidth(self, text, size, encoding=None): + "Just ensure we do width test on characters, not bytes..." + if type(text) is type(''): + text = text.decode('utf8') + + widths = self.unicodeWidths + return size * 0.001 * sum([widths.get(uch, 1000) for uch in text]) + #return CIDFont.stringWidth(self, text, size, encoding) + + +def precalculate(cmapdir): + # crunches through all, making 'fastmap' files + import os + files = os.listdir(cmapdir) + for file in files: + if os.path.isfile(cmapdir + os.sep + self.name + '.fastmap'): + continue + try: + enc = CIDEncoding(file) + except: + print 'cannot parse %s, skipping' % enc + continue + enc.fastSave(cmapdir) + print 'saved %s.fastmap' % file + +def test(): + # only works if you have cirrect encodings on your box! + c = Canvas('test_japanese.pdf') + c.setFont('Helvetica', 30) + c.drawString(100,700, 'Japanese Font Support') + + pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','90ms-RKSJ-H')) + pdfmetrics.registerFont(CIDFont('HeiseiKakuGo-W5','90ms-RKSJ-H')) + + + # the two typefaces + c.setFont('HeiseiMin-W3-90ms-RKSJ-H', 16) + # this says "This is HeiseiMincho" in shift-JIS. Not all our readers + # have a Japanese PC, so I escaped it. On a Japanese-capable + # system, print the string to see Kanji + message1 = '\202\261\202\352\202\315\225\275\220\254\226\276\222\251\202\305\202\267\201B' + c.drawString(100, 675, message1) + c.save() + print 'saved test_japanese.pdf' + + +## print 'CMAP_DIR = ', CMAP_DIR +## tf1 = CIDTypeFace('HeiseiMin-W3') +## print 'ascent = ',tf1.ascent +## print 'descent = ',tf1.descent +## for cid in [1,2,3,4,5,18,19,28,231,1742]: +## print 'width of cid %d = %d' % (cid, tf1.getCharWidth(cid)) + + encName = '90ms-RKSJ-H' + enc = CIDEncoding(encName) + print message1, '->', enc.translate(message1) + + f = CIDFont('HeiseiMin-W3','90ms-RKSJ-H') + print 'width = %0.2f' % f.stringWidth(message1, 10) + + + #testing all encodings +## import time +## started = time.time() +## import glob +## for encName in _cidfontdata.allowedEncodings: +## #encName = '90ms-RKSJ-H' +## enc = CIDEncoding(encName) +## print 'encoding %s:' % encName +## print ' codeSpaceRanges = %s' % enc._codeSpaceRanges +## print ' notDefRanges = %s' % enc._notDefRanges +## print ' mapping size = %d' % len(enc._cmap) +## finished = time.time() +## print 'constructed all encodings in %0.2f seconds' % (finished - started) + +if __name__=='__main__': + import doctest, cidfonts + doctest.testmod(cidfonts) + #test() + + + + diff --git a/bin/reportlab/pdfbase/pdfdoc.py b/bin/reportlab/pdfbase/pdfdoc.py new file mode 100755 index 00000000000..e3a12ff4d1e --- /dev/null +++ b/bin/reportlab/pdfbase/pdfdoc.py @@ -0,0 +1,1892 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/pdfdoc.py +__version__=''' $Id: pdfdoc.py 2854 2006-05-10 12:57:21Z rgbecker $ ''' +__doc__=""" +The module pdfdoc.py handles the 'outer structure' of PDF documents, ensuring that +all objects are properly cross-referenced and indexed to the nearest byte. The +'inner structure' - the page descriptions - are presumed to be generated before +each page is saved. +pdfgen.py calls this and provides a 'canvas' object to handle page marking operators. +piddlePDF calls pdfgen and offers a high-level interface. + +The classes within this generally mirror structures in the PDF file +and are not part of any public interface. Instead, canvas and font +classes are made available elsewhere for users to manipulate. +""" + +import string, types, binascii, codecs +from reportlab.pdfbase import pdfutils +from reportlab.pdfbase.pdfutils import LINEEND # this constant needed in both +from reportlab import rl_config +from reportlab.lib.utils import import_zlib, open_for_read, fp_str +from reportlab.pdfbase import pdfmetrics + +from sys import platform +try: + from sys import version_info +except: # pre-2.0 + # may be inaccurate but will at least + #work in anything which seeks to format + # version_info into a string + version_info = (1,5,2,'unknown',0) + +if platform[:4] == 'java' and version_info[:2] == (2, 1): + # workaround for list()-bug in Jython 2.1 (should be fixed in 2.2) + def list(sequence): + def f(x): + return x + return map(f, sequence) + +class PDFError(Exception): + pass + + +# set this flag to get more vertical whitespace (and larger files) +LongFormat = 1 +##if LongFormat: (doesn't work) +## pass +##else: +## LINEEND = "\n" # no wasteful carriage returns! + +# __InternalName__ is a special attribute that can only be set by the Document arbitrator +__InternalName__ = "__InternalName__" + +# __RefOnly__ marks reference only elements that must be formatted on top level +__RefOnly__ = "__RefOnly__" + +# __Comment__ provides a (one line) comment to inline with an object ref, if present +# if it is more than one line then percentize it... +__Comment__ = "__Comment__" + +# If DoComments is set then add helpful (space wasting) comment lines to PDF files +DoComments = 1 +if not LongFormat: + DoComments = 0 + +# name for standard font dictionary +BasicFonts = "BasicFonts" + +# name for the pages object +Pages = "Pages" + +### generic utilities + + +# for % substitutions +LINEENDDICT = {"LINEEND": LINEEND, "PERCENT": "%"} + +from types import InstanceType +def format(element, document, toplevel=0, InstanceType=InstanceType): + """Indirection step for formatting. + Ensures that document parameters alter behaviour + of formatting for all elements. + """ + t=type(element) + if t is InstanceType: + if not toplevel and hasattr(element, __RefOnly__): + # the object cannot be a component at non top level. + # make a reference to it and return it's format + return document.Reference(element).format(document) + else: + f = element.format(document) + if not rl_config.invariant and DoComments and hasattr(element, __Comment__): + f = "%s%s%s%s" % ("% ", element.__Comment__, LINEEND, f) + return f + elif t in (float, int): + #use a controlled number formatting routine + #instead of str, so Jython/Python etc do not differ + return fp_str(element) + else: + return str(element) + +def xObjectName(externalname): + return "FormXob.%s" % externalname + +# backwards compatibility +formName = xObjectName + + +# no encryption +class NoEncryption: + def encode(self, t): + "encode a string, stream, text" + return t + def prepare(self, document): + # get ready to do encryption + pass + def register(self, objnum, version): + # enter a new direct object + pass + def info(self): + # the representation of self in file if any (should be None or PDFDict) + return None + +class DummyDoc: + "used to bypass encryption when required" + encrypt = NoEncryption() + +### the global document structure manager + +class PDFDocument: + _ID = None + objectcounter = 0 + inObject = None + # set this to define filters + defaultStreamFilters = None + encrypt = NoEncryption() # default no encryption + pageCounter = 1 + def __init__(self, + dummyoutline=0, + compression=rl_config.pageCompression, + invariant=rl_config.invariant, + filename=None): + + # allow None value to be passed in to mean 'give system defaults' + if invariant is None: + self.invariant = rl_config.invariant + else: + self.invariant = invariant + self.setCompression(compression) + # signature for creating PDF ID + import md5 + sig = self.signature = md5.new() + sig.update("a reportlab document") + if not self.invariant: + cat = _getTimeStamp() + else: + cat = 946684800.0 + sig.update(repr(cat)) # initialize with timestamp digest + # mapping of internal identifier ("Page001") to PDF objectnumber and generation number (34, 0) + self.idToObjectNumberAndVersion = {} + # mapping of internal identifier ("Page001") to PDF object (PDFPage instance) + self.idToObject = {} + # internal id to file location + self.idToOffset = {} + # number to id + self.numberToId = {} + cat = self.Catalog = self._catalog = PDFCatalog() + pages = self.Pages = PDFPages() + cat.Pages = pages + if dummyoutline: + outlines = PDFOutlines0() + else: + outlines = PDFOutlines() + self.Outlines = self.outline = outlines + cat.Outlines = outlines + self.info = PDFInfo() + self.info.invariant = self.invariant + #self.Reference(self.Catalog) + #self.Reference(self.Info) + self.fontMapping = {} + #make an empty font dictionary + DD = PDFDictionary({}) + DD.__Comment__ = "The standard fonts dictionary" + DDR = self.Reference(DD, BasicFonts) + self.delayedFonts = [] + + def setCompression(self, onoff): + # XXX: maybe this should also set self.defaultStreamFilters? + self.compression = onoff + + def updateSignature(self, thing): + "add information to the signature" + if self._ID: return # but not if its used already! + self.signature.update(str(thing)) + + def ID(self): + "A unique fingerprint for the file (unless in invariant mode)" + if self._ID: + return self._ID + digest = self.signature.digest() + doc = DummyDoc() + ID = PDFString(digest,enc='raw') + IDs = ID.format(doc) + self._ID = "%s %% ReportLab generated PDF document -- digest (http://www.reportlab.com) %s [%s %s] %s" % ( + LINEEND, LINEEND, IDs, IDs, LINEEND) + return self._ID + + def SaveToFile(self, filename, canvas): + if callable(getattr(filename, "write",None)): + myfile = 0 + f = filename + filename = str(getattr(filename,'name','')) + else : + myfile = 1 + filename = str(filename) + f = open(filename, "wb") + f.write(self.GetPDFData(canvas)) + if myfile: + f.close() + import os + if os.name=='mac': + from reportlab.lib.utils import markfilename + markfilename(filename) # do platform specific file junk + if getattr(canvas,'_verbosity',None): print 'saved', filename + + def GetPDFData(self, canvas): + # realize delayed fonts + for fnt in self.delayedFonts: + fnt.addObjects(self) + # add info stuff to signature + self.info.invariant = self.invariant + self.info.digest(self.signature) + ### later: maybe add more info to sig? + # prepare outline + self.Reference(self.Catalog) + self.Reference(self.info) + outline = self.outline + outline.prepare(self, canvas) + return self.format() + + def inPage(self): + """specify the current object as a page (enables reference binding and other page features)""" + if self.inObject is not None: + if self.inObject=="page": return + raise ValueError, "can't go in page already in object %s" % self.inObject + self.inObject = "page" + + def inForm(self): + """specify that we are in a form xobject (disable page features, etc)""" + # don't need this check anymore since going in a form pushes old context at canvas level. + #if self.inObject not in ["form", None]: + # raise ValueError, "can't go in form already in object %s" % self.inObject + self.inObject = "form" + # don't need to do anything else, I think... + + def getInternalFontName(self, psfontname): + fm = self.fontMapping + if fm.has_key(psfontname): + return fm[psfontname] + else: + try: + # does pdfmetrics know about it? if so, add + fontObj = pdfmetrics.getFont(psfontname) + if getattr(fontObj, '_dynamicFont', 0): + raise PDFError, "getInternalFontName(%s) called for a dynamic font" % repr(psfontname) + fontObj.addObjects(self) + #self.addFont(fontObj) + return fm[psfontname] + except KeyError: + raise PDFError, "Font %s not known!" % repr(psfontname) + + def thisPageName(self): + return "Page"+repr(self.pageCounter) + + def thisPageRef(self): + return PDFObjectReference(self.thisPageName()) + + def addPage(self, page): + name = self.thisPageName() + self.Reference(page, name) + self.Pages.addPage(page) + self.pageCounter = self.pageCounter+1 + self.inObject = None + + + def addForm(self, name, form): + """add a Form XObject.""" + # XXX should check that name is a legal PDF name + if self.inObject != "form": + self.inForm() + self.Reference(form, xObjectName(name)) + self.inObject = None + + def annotationName(self, externalname): + return "Annot.%s"%externalname + + def addAnnotation(self, name, annotation): + self.Reference(annotation, self.annotationName(name)) + + def refAnnotation(self, name): + internalname = self.annotationName(name) + return PDFObjectReference(internalname) + + def setTitle(self, title): + "embeds in PDF file" + self.info.title = title + + def setAuthor(self, author): + "embedded in PDF file" + self.info.author = author + + def setSubject(self, subject): + "embeds in PDF file" + self.info.subject = subject + + def setDateFormatter(self, dateFormatter): + self.info._dateFormatter = dateFormatter + + def getAvailableFonts(self): + fontnames = self.fontMapping.keys() + # the standard 14 are also always available! (even if not initialized yet) + import _fontdata + for name in _fontdata.standardFonts: + if name not in fontnames: + fontnames.append(name) + fontnames.sort() + return fontnames + + def format(self): + # register the Catalog/INfo and then format the objects one by one until exhausted + # (possible infinite loop if there is a bug that continually makes new objects/refs...) + # Prepare encryption + self.encrypt.prepare(self) + cat = self.Catalog + info = self.info + self.Reference(self.Catalog) + self.Reference(self.info) + # register the encryption dictionary if present + encryptref = None + encryptinfo = self.encrypt.info() + if encryptinfo: + encryptref = self.Reference(encryptinfo) + # make std fonts (this could be made optional + counter = 0 # start at first object (object 1 after preincrement) + ids = [] # the collection of object ids in object number order + numbertoid = self.numberToId + idToNV = self.idToObjectNumberAndVersion + idToOb = self.idToObject + idToOf = self.idToOffset + ### note that new entries may be "appended" DURING FORMATTING + done = None + File = PDFFile() # output collector + while done is None: + counter += 1 # do next object... + if numbertoid.has_key(counter): + id = numbertoid[counter] + #printidToOb + obj = idToOb[id] + IO = PDFIndirectObject(id, obj) + # register object number and version + #encrypt.register(id, + IOf = IO.format(self) + # add a comment to the PDF output + if not rl_config.invariant and DoComments: + try: + classname = obj.__class__.__name__ + except: + classname = repr(obj) + File.add("%% %s: class %s %s" % (repr(id), classname[:50], LINEEND)) + offset = File.add(IOf) + idToOf[id] = offset + ids.append(id) + else: + done = 1 + # sanity checks (must happen AFTER formatting) + lno = len(numbertoid) + if counter-1!=lno: + raise ValueError, "counter %s doesn't match number to id dictionary %s" %(counter, lno) + # now add the xref + xref = PDFCrossReferenceTable() + xref.addsection(0, ids) + xreff = xref.format(self) + xrefoffset = File.add(xreff) + # now add the trailer + trailer = PDFTrailer( + startxref = xrefoffset, + Size = lno+1, + Root = self.Reference(cat), + Info = self.Reference(info), + Encrypt = encryptref, + ID = self.ID(), + ) + trailerf = trailer.format(self) + File.add(trailerf) + # return string format for pdf file + return File.format(self) + + def hasForm(self, name): + """test for existence of named form""" + internalname = xObjectName(name) + return self.idToObject.has_key(internalname) + + def getFormBBox(self, name): + "get the declared bounding box of the form as a list" + internalname = xObjectName(name) + if self.idToObject.has_key(internalname): + theform = self.idToObject[internalname] + if isinstance(theform, PDFFormXObject): + # internally defined form + return theform.BBoxList() + elif isinstance(theform, PDFStream): + # externally defined form + return list(theform.dictionary.dict["BBox"].sequence) + else: + raise ValueError, "I don't understand the form instance %s" % repr(name) + + def getXObjectName(self, name): + """Lets canvas find out what form is called internally. + Never mind whether it is defined yet or not.""" + return xObjectName(name) + + def xobjDict(self, formnames): + """construct an xobject dict (for inclusion in a resource dict, usually) + from a list of form names (images not yet supported)""" + D = {} + for name in formnames: + internalname = xObjectName(name) + reference = PDFObjectReference(internalname) + D[internalname] = reference + #print "xobjDict D", D + return PDFDictionary(D) + + def Reference(self, object, name=None, InstanceType=InstanceType): + ### note references may "grow" during the final formatting pass: don't use d.keys()! + # don't make references to other references, or non instances, unless they are named! + #print"object type is ", type(object) + tob = type(object) + idToObject = self.idToObject + if name is None and ( + (tob is not InstanceType) or (tob is InstanceType and object.__class__ is PDFObjectReference)): + return object + if hasattr(object, __InternalName__): + # already registered + intname = object.__InternalName__ + if name is not None and name!=intname: + raise ValueError, "attempt to reregister object %s with new name %s" % ( + repr(intname), repr(name)) + if not idToObject.has_key(intname): + raise ValueError, "object named but not registered" + return PDFObjectReference(intname) + # otherwise register the new object + objectcounter = self.objectcounter = self.objectcounter+1 + if name is None: + name = "R"+repr(objectcounter) + if idToObject.has_key(name): + other = idToObject[name] + if other!=object: + raise ValueError, "redefining named object: "+repr(name) + return PDFObjectReference(name) + if tob is InstanceType: + object.__InternalName__ = name + #print "name", name, "counter", objectcounter + self.idToObjectNumberAndVersion[name] = (objectcounter, 0) + self.numberToId[objectcounter] = name + idToObject[name] = object + return PDFObjectReference(name) + +### chapter 4 Objects + +PDFtrue = "true" +PDFfalse = "false" +PDFnull = "null" + +class PDFText: + def __init__(self, t): + self.t = t + def format(self, document): + result = binascii.hexlify(document.encrypt.encode(self.t)) + return "<%s>" % result + def __str__(self): + dummydoc = DummyDoc() + return self.format(dummydoc) + +def PDFnumber(n): + return n + +import re +_re_cleanparens=re.compile('[^()]') +del re +def _isbalanced(s): + '''test whether a string is balanced in parens''' + s = _re_cleanparens.sub('',s) + n = 0 + for c in s: + if c=='(': n+=1 + else: + n -= 1 + if n<0: return 0 + return not n and 1 or 0 + +def _checkPdfdoc(utext): + '''return true if no Pdfdoc encoding errors''' + try: + utext.encode('pdfdoc') + return 1 + except UnicodeEncodeError, e: + return 0 + +class PDFString: + def __init__(self, s, escape=1, enc='auto'): + '''s can be unicode/utf8 or a PDFString + if escape is true then the output will be passed through escape + if enc is raw then the string will be left alone + if enc is auto we'll try and automatically adapt to utf_16_be if the + effective string is not entirely in pdfdoc + ''' + if isinstance(s,PDFString): + self.s = s.s + self.escape = s.escape + self.enc = s.enc + else: + self.s = s + self.escape = escape + self.enc = enc + def format(self, document): + s = self.s + enc = getattr(self,'enc','auto') + if type(s) is str: + if enc is 'auto': + try: + u = s.decode('utf8') + except: + print s + raise + if _checkPdfdoc(u): + s = u.encode('pdfdoc') + else: + s = codecs.BOM_UTF16_BE+u.encode('utf_16_be') + elif type(s) is unicode: + if enc is 'auto': + if _checkPdfdoc(s): + s = s.encode('pdfdoc') + else: + s = codecs.BOM_UTF16_BE+s.encode('utf_16_be') + else: + s = codecs.BOM_UTF16_BE+s.encode('utf_16_be') + else: + raise ValueError('PDFString argument must be str/unicode not %s' % type(s)) + + escape = getattr(self,'escape',1) + if not isinstance(document.encrypt,NoEncryption): + s = document.encrypt.encode(s) + escape = 1 + if escape: + try: + es = "(%s)" % pdfutils._escape(s) + except: + raise ValueError("cannot escape %s %s" % (s, repr(s))) + if escape&2: + es = es.replace('\\012','\n') + if escape&4 and _isbalanced(s): + es = es.replace('\\(','(').replace('\\)',')') + return es + else: + return '(%s)' % s + def __str__(self): + return "(%s)" % pdfutils._escape(self.s) + +def PDFName(data,lo=chr(0x21),hi=chr(0x7e)): + # might need to change this to class for encryption + # NOTE: RESULT MUST ALWAYS SUPPORT MEANINGFUL COMPARISONS (EQUALITY) AND HASH + # first convert the name + L = list(data) + for i,c in enumerate(L): + if chi or c in "%()<>{}[]#": + L[i] = "#"+hex(ord(c))[2:] # forget the 0x thing... + return "/"+(''.join(L)) + +class PDFDictionary: + + multiline = LongFormat + def __init__(self, dict=None): + """dict should be namestring to value eg "a": 122 NOT pdfname to value NOT "/a":122""" + if dict is None: + self.dict = {} + else: + self.dict = dict.copy() + def __setitem__(self, name, value): + self.dict[name] = value + def Reference(name, document): + self.dict[name] = document.Reference(self.dict[name]) + def format(self, document,IND=LINEEND+' '): + dict = self.dict + keys = dict.keys() + keys.sort() + L = [(format(PDFName(k),document)+" "+format(dict[k],document)) for k in keys] + if self.multiline: + L = IND.join(L) + else: + # break up every 6 elements anyway + t=L.insert + for i in xrange(6, len(L), 6): + t(i,LINEEND) + L = " ".join(L) + return "<< %s >>" % L + +# stream filters are objects to support round trip and +# possibly in the future also support parameters +class PDFStreamFilterZCompress: + pdfname = "FlateDecode" + def encode(self, text): + from reportlab.lib.utils import import_zlib + zlib = import_zlib() + if not zlib: raise ImportError, "cannot z-compress zlib unavailable" + return zlib.compress(text) + def decode(self, encoded): + from reportlab.lib.utils import import_zlib + zlib = import_zlib() + if not zlib: raise ImportError, "cannot z-decompress zlib unavailable" + return zlib.decompress(encoded) + +# need only one of these, unless we implement parameters later +PDFZCompress = PDFStreamFilterZCompress() + +class PDFStreamFilterBase85Encode: + pdfname = "ASCII85Decode" + def encode(self, text): + from pdfutils import _AsciiBase85Encode, _wrap + return _wrap(_AsciiBase85Encode(text)) + def decode(self, text): + from pdfutils import _AsciiBase85Decode + return _AsciiBase85Decode(text) + +# need only one of these too +PDFBase85Encode = PDFStreamFilterBase85Encode() + +STREAMFMT = ("%(dictionary)s%(LINEEND)s" # dictionary + "stream" # stream keyword + "%(LINEEND)s" # a line end (could be just a \n) + "%(content)s" # the content, with no lineend + "endstream%(LINEEND)s" # the endstream keyword + ) +class PDFStream: + '''set dictionary elements explicitly stream.dictionary[name]=value''' + ### compression stuff not implemented yet + __RefOnly__ = 1 # must be at top level + def __init__(self, dictionary=None, content=None): + if dictionary is None: + dictionary = PDFDictionary() + self.dictionary = dictionary + self.content = content + self.filters = None + def format(self, document): + dictionary = self.dictionary + # copy it for modification + dictionary = PDFDictionary(dictionary.dict.copy()) + content = self.content + filters = self.filters + if self.content is None: + raise ValueError, "stream content not set" + if filters is None: + filters = document.defaultStreamFilters + # only apply filters if they haven't been applied elsewhere + if filters is not None and not dictionary.dict.has_key("Filter"): + # apply filters in reverse order listed + rf = list(filters) + rf.reverse() + fnames = [] + for f in rf: + #print "*****************content:"; print repr(content[:200]) + #print "*****************filter", f.pdfname + content = f.encode(content) + fnames.insert(0, PDFName(f.pdfname)) + #print "*****************finally:"; print content[:200] + #print "****** FILTERS", fnames + #stop + dictionary["Filter"] = PDFArray(fnames) + # "stream encoding is done after all filters have been applied" + content = document.encrypt.encode(content) + fc = format(content, document) + #print "type(content)", type(content), len(content), type(self.dictionary) + lc = len(content) + #if fc!=content: burp + # set dictionary length parameter + dictionary["Length"] = lc + fd = format(dictionary, document) + sdict = LINEENDDICT.copy() + sdict["dictionary"] = fd + sdict["content"] = fc + return STREAMFMT % sdict + +def teststream(content=None): + #content = "" # test + if content is None: + content = teststreamcontent + content = string.strip(content) + content = string.replace(content, "\n", LINEEND) + LINEEND + S = PDFStream() + S.content = content + S.filters = [PDFBase85Encode, PDFZCompress] + # nothing else needed... + S.__Comment__ = "test stream" + return S + +teststreamcontent = """ +1 0 0 1 0 0 cm BT /F9 12 Tf 14.4 TL ET +1.00 0.00 1.00 rg +n 72.00 72.00 432.00 648.00 re B* +""" +class PDFArray: + multiline = LongFormat + def __init__(self, sequence): + self.sequence = list(sequence) + def References(self, document): + """make all objects in sequence references""" + self.sequence = map(document.Reference, self.sequence) + def format(self, document, IND=LINEEND+' '): + L = [format(e, document) for e in self.sequence] + if self.multiline: + L = IND.join(L) + else: + # break up every 10 elements anyway + breakline = LINEEND+" " + for i in xrange(10, len(L), 10): + L.insert(i,breakline) + L = ' '.join(L) + return "[ %s ]" % L + +INDIRECTOBFMT = ("%(n)s %(v)s obj%(LINEEND)s" + "%(content)s" "%(LINEEND)s" + "endobj" "%(LINEEND)s") + +class PDFIndirectObject: + __RefOnly__ = 1 + def __init__(self, name, content): + self.name = name + self.content = content + def format(self, document): + name = self.name + (n, v) = document.idToObjectNumberAndVersion[name] + # set encryption parameters + document.encrypt.register(n, v) + content = self.content + fcontent = format(content, document, toplevel=1) # yes this is at top level + sdict = LINEENDDICT.copy() + sdict["n"] = n + sdict["v"] = v + sdict["content"] = fcontent + return INDIRECTOBFMT % sdict + +class PDFObjectReference: + def __init__(self, name): + self.name = name + def format(self, document): + try: + return "%s %s R" % document.idToObjectNumberAndVersion[self.name] + except: + raise KeyError, "forward reference to %s not resolved upon final formatting" % repr(self.name) + +### chapter 5 +# Following Ken Lunde's advice and the PDF spec, this includes +# some high-order bytes. I chose the characters for Tokyo +# in Shift-JIS encoding, as these cannot be mistaken for +# any other encoding, and we'll be able to tell if something +# has run our PDF files through a dodgy Unicode conversion. +PDFHeader = ( +"%PDF-1.3"+LINEEND+ +"%\223\214\213\236 ReportLab Generated PDF document http://www.reportlab.com"+LINEEND) + +class PDFFile: + ### just accumulates strings: keeps track of current offset + def __init__(self): + self.strings = [] + self.write = self.strings.append + self.offset = 0 + self.add(PDFHeader) + + def closeOrReset(self): + pass + + def add(self, s): + """should be constructed as late as possible, return position where placed""" + result = self.offset + self.offset = result+len(s) + self.write(s) + return result + def format(self, document): + strings = map(str, self.strings) # final conversion, in case of lazy objects + return string.join(strings, "") + +XREFFMT = '%0.10d %0.5d n' + +class PDFCrossReferenceSubsection: + def __init__(self, firstentrynumber, idsequence): + self.firstentrynumber = firstentrynumber + self.idsequence = idsequence + def format(self, document): + """id sequence should represent contiguous object nums else error. free numbers not supported (yet)""" + firstentrynumber = self.firstentrynumber + idsequence = self.idsequence + entries = list(idsequence) + nentries = len(idsequence) + # special case: object number 0 is always free + taken = {} + if firstentrynumber==0: + taken[0] = "standard free entry" + nentries = nentries+1 + entries.insert(0, "0000000000 65535 f") + idToNV = document.idToObjectNumberAndVersion + idToOffset = document.idToOffset + lastentrynumber = firstentrynumber+nentries-1 + for id in idsequence: + (num, version) = idToNV[id] + if taken.has_key(num): + raise ValueError, "object number collision %s %s %s" % (num, repr(id), repr(taken[id])) + if num>lastentrynumber or num>""" + +class PDFOutlines0: + __Comment__ = "TEST OUTLINE!" + text = string.replace(DUMMYOUTLINE, "\n", LINEEND) + __RefOnly__ = 1 + def format(self, document): + return self.text + + +class OutlineEntryObject: + "an entry in an outline" + Title = Dest = Parent = Prev = Next = First = Last = Count = None + def format(self, document): + D = {} + D["Title"] = PDFString(self.Title) + D["Parent"] = self.Parent + D["Dest"] = self.Dest + for n in ("Prev", "Next", "First", "Last", "Count"): + v = getattr(self, n) + if v is not None: + D[n] = v + PD = PDFDictionary(D) + return PD.format(document) + + +class PDFOutlines: + """takes a recursive list of outline destinations + like + out = PDFOutline1() + out.setNames(canvas, # requires canvas for name resolution + "chapter1dest", + ("chapter2dest", + ["chapter2section1dest", + "chapter2section2dest", + "chapter2conclusiondest"] + ), # end of chapter2 description + "chapter3dest", + ("chapter4dest", ["c4s1", "c4s2"]) + ) + Higher layers may build this structure incrementally. KISS at base level. + """ + # first attempt, many possible features missing. + #no init for now + mydestinations = ready = None + counter = 0 + currentlevel = -1 # ie, no levels yet + + def __init__(self): + self.destinationnamestotitles = {} + self.destinationstotitles = {} + self.levelstack = [] + self.buildtree = [] + self.closedict = {} # dictionary of "closed" destinations in the outline + + def addOutlineEntry(self, destinationname, level=0, title=None, closed=None): + """destinationname of None means "close the tree" """ + from types import IntType, TupleType + if destinationname is None and level!=0: + raise ValueError, "close tree must have level of 0" + if type(level) is not IntType: raise ValueError, "level must be integer, got %s" % type(level) + if level<0: raise ValueError, "negative levels not allowed" + if title is None: title = destinationname + currentlevel = self.currentlevel + stack = self.levelstack + tree = self.buildtree + # adjust currentlevel and stack to match level + if level>currentlevel: + if level>currentlevel+1: + raise ValueError, "can't jump from outline level %s to level %s, need intermediates" %(currentlevel, level) + level = currentlevel = currentlevel+1 + stack.append([]) + while levelref + if Ot is ListType or Ot is TupleType: + L = [] + for o in object: + L.append(self.translateNames(canvas, o)) + if Ot is TupleType: + return tuple(L) + return L + raise "in outline, destination name must be string: got a %s" % Ot + + def prepare(self, document, canvas): + """prepare all data structures required for save operation (create related objects)""" + if self.mydestinations is None: + if self.levelstack: + self.addOutlineEntry(None) # close the tree + destnames = self.levelstack[0] + #from pprint import pprint; pprint(destnames); stop + self.mydestinations = self.translateNames(canvas, destnames) + else: + self.first = self.last = None + self.count = 0 + self.ready = 1 + return + #self.first = document.objectReference("Outline.First") + #self.last = document.objectReference("Outline.Last") + # XXXX this needs to be generalized for closed entries! + self.count = count(self.mydestinations, self.closedict) + (self.first, self.last) = self.maketree(document, self.mydestinations, toplevel=1) + self.ready = 1 + + def maketree(self, document, destinationtree, Parent=None, toplevel=0): + from types import ListType, TupleType, DictType + tdestinationtree = type(destinationtree) + if toplevel: + levelname = "Outline" + Parent = document.Reference(document.Outlines) + else: + self.count = self.count+1 + levelname = "Outline.%s" % self.count + if Parent is None: + raise ValueError, "non-top level outline elt parent must be specified" + if tdestinationtree is not ListType and tdestinationtree is not TupleType: + raise ValueError, "destinationtree must be list or tuple, got %s" + nelts = len(destinationtree) + lastindex = nelts-1 + lastelt = firstref = lastref = None + destinationnamestotitles = self.destinationnamestotitles + closedict = self.closedict + for index in range(nelts): + eltobj = OutlineEntryObject() + eltobj.Parent = Parent + eltname = "%s.%s" % (levelname, index) + eltref = document.Reference(eltobj, eltname) + #document.add(eltname, eltobj) + if lastelt is not None: + lastelt.Next = eltref + eltobj.Prev = lastref + if firstref is None: + firstref = eltref + lastref = eltref + lastelt = eltobj # advance eltobj + lastref = eltref + elt = destinationtree[index] + te = type(elt) + if te is DictType: + # simple leaf {name: dest} + leafdict = elt + elif te is TupleType: + # leaf with subsections: ({name: ref}, subsections) XXXX should clean up (see count(...)) + try: + (leafdict, subsections) = elt + except: + raise ValueError, "destination tree elt tuple should have two elts, got %s" % len(elt) + eltobj.Count = count(subsections, closedict) + (eltobj.First, eltobj.Last) = self.maketree(document, subsections, eltref) + else: + raise ValueError, "destination tree elt should be dict or tuple, got %s" % te + try: + [(Title, Dest)] = leafdict.items() + except: + raise ValueError, "bad outline leaf dictionary, should have one entry "+str(elt) + eltobj.Title = destinationnamestotitles[Title] + eltobj.Dest = Dest + if te is TupleType and closedict.has_key(Dest): + # closed subsection, count should be negative + eltobj.Count = -eltobj.Count + return (firstref, lastref) + +def count(tree, closedict=None): + """utility for outline: recursively count leaves in a tuple/list tree""" + from operator import add + from types import TupleType, ListType + tt = type(tree) + if tt is TupleType: + # leaf with subsections XXXX should clean up this structural usage + (leafdict, subsections) = tree + [(Title, Dest)] = leafdict.items() + if closedict and closedict.has_key(Dest): + return 1 # closed tree element + if tt is TupleType or tt is ListType: + #return reduce(add, map(count, tree)) + counts = [] + for e in tree: + counts.append(count(e, closedict)) + return reduce(add, counts) + return 1 + +class PDFInfo: + """PDF documents can have basic information embedded, viewable from + File | Document Info in Acrobat Reader. If this is wrong, you get + Postscript errors while printing, even though it does not print.""" + producer = "ReportLab http://www.reportlab.com" + title = "untitled" + author = "anonymous" + subject = "unspecified" + _dateFormatter = None + + def __init__(self): + self.invariant = rl_config.invariant + + def digest(self, md5object): + # add self information to signature + for x in (self.title, self.author, self.subject): + md5object.update(str(x)) + + def format(self, document): + D = {} + D["Title"] = PDFString(self.title) + D["Author"] = PDFString(self.author) + D["CreationDate"] = PDFDate(invariant=self.invariant,dateFormatter=self._dateFormatter) + D["Producer"] = PDFString(self.producer) + D["Subject"] = PDFString(self.subject) + PD = PDFDictionary(D) + return PD.format(document) + +# skipping thumbnails, etc + + +class Annotation: + """superclass for all annotations.""" + defaults = [("Type", PDFName("Annot"),)] + required = ("Type", "Rect", "Contents", "Subtype") + permitted = required+( + "Border", "C", "T", "M", "F", "H", "BS", "AA", "AS", "Popup", "P", "AP") + def cvtdict(self, d, escape=1): + """transform dict args from python form to pdf string rep as needed""" + Rect = d["Rect"] + if type(Rect) is not types.StringType: + d["Rect"] = PDFArray(Rect) + d["Contents"] = PDFString(d["Contents"],escape) + return d + def AnnotationDict(self, **kw): + if 'escape' in kw: + escape = kw['escape'] + del kw['escape'] + else: + escape = 1 + d = {} + for (name,val) in self.defaults: + d[name] = val + d.update(kw) + for name in self.required: + if not d.has_key(name): + raise ValueError, "keyword argument %s missing" % name + d = self.cvtdict(d,escape=escape) + permitted = self.permitted + for name in d.keys(): + if name not in permitted: + raise ValueError, "bad annotation dictionary name %s" % name + return PDFDictionary(d) + def Dict(self): + raise ValueError, "DictString undefined for virtual superclass Annotation, must overload" + # but usually + #return self.AnnotationDict(self, Rect=(a,b,c,d)) or whatever + def format(self, document): + D = self.Dict() + return D.format(document) + +class TextAnnotation(Annotation): + permitted = Annotation.permitted + ( + "Open", "Name") + def __init__(self, Rect, Contents, **kw): + self.Rect = Rect + self.Contents = Contents + self.otherkw = kw + def Dict(self): + d = {} + d.update(self.otherkw) + d["Rect"] = self.Rect + d["Contents"] = self.Contents + d["Subtype"] = "/Text" + return self.AnnotationDict(**d) + +class FreeTextAnnotation(Annotation): + permitted = Annotation.permitted + ("DA",) + def __init__(self, Rect, Contents, DA, **kw): + self.Rect = Rect + self.Contents = Contents + self.DA = DA + self.otherkw = kw + def Dict(self): + d = {} + d.update(self.otherkw) + d["Rect"] = self.Rect + d["Contents"] = self.Contents + d["DA"] = self.DA + d["Subtype"] = "/FreeText" + return self.AnnotationDict(**d) + +class LinkAnnotation(Annotation): + + permitted = Annotation.permitted + ( + "Dest", "A", "PA") + def __init__(self, Rect, Contents, Destination, Border="[0 0 1]", **kw): + self.Border = Border + self.Rect = Rect + self.Contents = Contents + self.Destination = Destination + self.otherkw = kw + + def dummyDictString(self): # old, testing + return """ + << /Type /Annot /Subtype /Link /Rect [71 717 190 734] /Border [16 16 1] + /Dest [23 0 R /Fit] >> + """ + + def Dict(self): + d = {} + d.update(self.otherkw) + d["Border"] = self.Border + d["Rect"] = self.Rect + d["Contents"] = self.Contents + d["Subtype"] = "/Link" + d["Dest"] = self.Destination + return apply(self.AnnotationDict, (), d) + + +# skipping names tree + +# skipping actions + +# skipping names trees + +# skipping to chapter 7 + +class PDFRectangle: + def __init__(self, llx, lly, urx, ury): + self.llx, self.lly, self.ulx, self.ury = llx, lly, urx, ury + def format(self, document): + A = PDFArray([self.llx, self.lly, self.ulx, self.ury]) + return format(A, document) + +_NOWT=None +def _getTimeStamp(): + global _NOWT + if not _NOWT: + import time + _NOWT = time.time() + return _NOWT + +class PDFDate: + # gmt offset not yet suppported + def __init__(self, yyyy=None, mm=None, dd=None, hh=None, m=None, s=None, + invariant=rl_config.invariant,dateFormatter=None): + if None in (yyyy, mm, dd, hh, m, s): + if invariant: + now = (2000,01,01,00,00,00,0) + else: + import time + now = tuple(time.localtime(_getTimeStamp())[:6]) + if yyyy is None: yyyy=now[0] + if mm is None: mm=now[1] + if dd is None: dd=now[2] + if hh is None: hh=now[3] + if m is None: m=now[4] + if s is None: s=now[5] + self.date=(yyyy,mm,dd,hh,m,s) + self.dateFormatter = dateFormatter + + def format(self, doc): + dfmt = self.dateFormatter or ( + lambda yyyy,mm,dd,hh,m,s: '%04d%02d%02d%02d%02d%02d' % (yyyy,mm,dd,hh,m,s)) + return format(PDFString(dfmt(*self.date)), doc) + +class Destination: + """not a pdfobject! This is a placeholder that can delegates + to a pdf object only after it has been defined by the methods + below. EG a Destination can refer to Appendix A before it has been + defined, but only if Appendix A is explicitly noted as a destination + and resolved before the document is generated... + For example the following sequence causes resolution before doc generation. + d = Destination() + d.fit() # or other format defining method call + d.setPage(p) + (at present setPageRef is called on generation of the page). + """ + representation = format = page = None + def __init__(self,name): + self.name = name + self.fmt = self.page = None + def format(self, document): + f = self.fmt + if f is None: raise ValueError, "format not resolved %s" % self.name + p = self.page + if p is None: raise ValueError, "Page reference unbound %s" % self.name + f.page = p + return f.format(document) + def xyz(self, left, top, zoom): # see pdfspec mar 11 99 pp184+ + self.fmt = PDFDestinationXYZ(None, left, top, zoom) + def fit(self): + self.fmt = PDFDestinationFit(None) + def fitb(self): + self.fmt = PDFDestinationFitB(None) + def fith(self, top): + self.fmt = PDFDestinationFitH(None,top) + def fitv(self, left): + self.fmt = PDFDestinationFitV(None, left) + def fitbh(self, top): + self.fmt = PDFDestinationFitBH(None, top) + def fitbv(self, left): + self.fmt = PDFDestinationFitBV(None, left) + def fitr(self, left, bottom, right, top): + self.fmt = PDFDestinationFitR(None, left, bottom, right, top) + def setPage(self, page): + self.page = page + #self.fmt.page = page # may not yet be defined! + +class PDFDestinationXYZ: + typename = "XYZ" + def __init__(self, page, left, top, zoom): + self.page = page + self.top = top + self.zoom = zoom + self.left = left + def format(self, document): + pageref = document.Reference(self.page) + A = PDFArray( [ pageref, PDFName(self.typename), self.left, self.top, self.zoom ] ) + return format(A, document) + +class PDFDestinationFit: + typename = "Fit" + def __init__(self, page): + self.page = page + def format(self, document): + pageref = document.Reference(self.page) + A = PDFArray( [ pageref, PDFName(self.typename) ] ) + return format(A, document) + +class PDFDestinationFitB(PDFDestinationFit): + typename = "FitB" + +class PDFDestinationFitH: + typename = "FitH" + def __init__(self, page, top): + self.page = page; self.top=top + def format(self, document): + pageref = document.Reference(self.page) + A = PDFArray( [ pageref, PDFName(self.typename), self.top ] ) + return format(A, document) + +class PDFDestinationFitBH(PDFDestinationFitH): + typename = "FitBH" + +class PDFDestinationFitV: + typename = "FitV" + def __init__(self, page, left): + self.page = page; self.left=left + def format(self, document): + pageref = document.Reference(self.page) + A = PDFArray( [ pageref, PDFName(self.typename), self.left ] ) + return format(A, document) + +class PDFDestinationBV(PDFDestinationFitV): + typename = "FitBV" + +class PDFDestinationFitR: + typename = "FitR" + def __init__(self, page, left, bottom, right, top): + self.page = page; self.left=left; self.bottom=bottom; self.right=right; self.top=top + def format(self, document): + pageref = document.Reference(self.page) + A = PDFArray( [ pageref, PDFName(self.typename), self.left, self.bottom, self.right, self.top] ) + return format(A, document) + +# named destinations need nothing + +# skipping filespecs + +class PDFResourceDictionary: + """each element *could* be reset to a reference if desired""" + def __init__(self): + self.ColorSpace = {} + self.XObject = {} + self.ExtGState = {} + self.Font = {} + self.Pattern = {} + self.ProcSet = [] + self.Properties = {} + self.Shading = {} + # ?by default define the basicprocs + self.basicProcs() + stdprocs = map(PDFName, string.split("PDF Text ImageB ImageC ImageI")) + dict_attributes = ("ColorSpace", "XObject", "ExtGState", "Font", "Pattern", "Properties", "Shading") + def allProcs(self): + # define all standard procsets + self.ProcSet = self.stdprocs + def basicProcs(self): + self.ProcSet = self.stdprocs[:2] # just PDF and Text + def basicFonts(self): + self.Font = PDFObjectReference(BasicFonts) + def format(self, document): + D = {} + from types import ListType, DictType + for dname in self.dict_attributes: + v = getattr(self, dname) + if type(v) is DictType: + if v: + dv = PDFDictionary(v) + D[dname] = dv + else: + D[dname] = v + v = self.ProcSet + dname = "ProcSet" + if type(v) is ListType: + if v: + dv = PDFArray(v) + D[dname] = dv + else: + D[dname] = v + DD = PDFDictionary(D) + return format(DD, document) + + ############################################################################## + # + # Font objects - the PDFDocument.addFont() method knows which of these + # to construct when given a user-facing Font object + # + ############################################################################## + + +class PDFType1Font: + """no init: set attributes explicitly""" + __RefOnly__ = 1 + # note! /Name appears to be an undocumented attribute.... + name_attributes = string.split("Type Subtype BaseFont Name") + Type = "Font" + Subtype = "Type1" + # these attributes are assumed to already be of the right type + local_attributes = string.split("FirstChar LastChar Widths Encoding ToUnicode FontDescriptor") + def format(self, document): + D = {} + for name in self.name_attributes: + if hasattr(self, name): + value = getattr(self, name) + D[name] = PDFName(value) + for name in self.local_attributes: + if hasattr(self, name): + value = getattr(self, name) + D[name] = value + #print D + PD = PDFDictionary(D) + return PD.format(document) + +## These attribute listings will be useful in future, even if we +## put them elsewhere + +class PDFTrueTypeFont(PDFType1Font): + Subtype = "TrueType" + #local_attributes = string.split("FirstChar LastChar Widths Encoding ToUnicode FontDescriptor") #same + +##class PDFMMType1Font(PDFType1Font): +## Subtype = "MMType1" +## +##class PDFType3Font(PDFType1Font): +## Subtype = "Type3" +## local_attributes = string.split( +## "FirstChar LastChar Widths CharProcs FontBBox FontMatrix Resources Encoding") +## +##class PDFType0Font(PDFType1Font): +## Subtype = "Type0" +## local_attributes = string.split( +## "DescendantFonts Encoding") +## +##class PDFCIDFontType0(PDFType1Font): +## Subtype = "CIDFontType0" +## local_attributes = string.split( +## "CIDSystemInfo FontDescriptor DW W DW2 W2 Registry Ordering Supplement") +## +##class PDFCIDFontType0(PDFType1Font): +## Subtype = "CIDFontType2" +## local_attributes = string.split( +## "BaseFont CIDToGIDMap CIDSystemInfo FontDescriptor DW W DW2 W2") +## +##class PDFEncoding(PDFType1Font): +## Type = "Encoding" +## name_attributes = string.split("Type BaseEncoding") +## # these attributes are assumed to already be of the right type +## local_attributes = ["Differences"] +## + +# UGLY ALERT - this needs turning into something O-O, it was hacked +# across from the pdfmetrics.Encoding class to avoid circularity + +# skipping CMaps + +class PDFFormXObject: + # like page requires .info set by some higher level (doc) + # XXXX any resource used in a form must be propagated up to the page that (recursively) uses + # the form!! (not implemented yet). + XObjects = Annots = BBox = Matrix = Contents = stream = Resources = None + hasImages = 1 # probably should change + compression = 0 + def __init__(self, lowerx, lowery, upperx, uppery): + #not done + self.lowerx = lowerx; self.lowery=lowery; self.upperx=upperx; self.uppery=uppery + + def setStreamList(self, data): + if type(data) is types.ListType: + data = string.join(data, LINEEND) + self.stream = data + + def BBoxList(self): + "get the declared bounding box for the form as a list" + if self.BBox: + return list(self.BBox.sequence) + else: + return [self.lowerx, self.lowery, self.upperx, self.uppery] + + def format(self, document): + self.BBox = self.BBox or PDFArray([self.lowerx, self.lowery, self.upperx, self.uppery]) + self.Matrix = self.Matrix or PDFArray([1, 0, 0, 1, 0, 0]) + if not self.Annots: + self.Annots = None + else: + #these must be transferred to the page when the form is used + raise ValueError, "annotations not reimplemented yet" + if not self.Contents: + stream = self.stream + if not stream: + self.Contents = teststream() + else: + S = PDFStream() + S.content = stream + # need to add filter stuff (?) + S.__Comment__ = "xobject form stream" + self.Contents = S + if not self.Resources: + resources = PDFResourceDictionary() + # fonts! + resources.basicFonts() + if self.hasImages: + resources.allProcs() + else: + resources.basicProcs() + if self.XObjects: + #print "XObjects", self.XObjects.dict + resources.XObject = self.XObjects + if self.compression: + self.Contents.filters = [PDFBase85Encode, PDFZCompress] + sdict = self.Contents.dictionary + sdict["Type"] = PDFName("XObject") + sdict["Subtype"] = PDFName("Form") + sdict["FormType"] = 1 + sdict["BBox"] = self.BBox + sdict["Matrix"] = self.Matrix + sdict["Resources"] = resources + return self.Contents.format(document) + +class PDFPostScriptXObject: + "For embedding PD (e.g. tray commands) in PDF" + def __init__(self, content=None): + self.content = content + + def format(self, document): + S = PDFStream() + S.content = self.content + S.__Comment__ = "xobject postscript stream" + sdict = S.dictionary + sdict["Type"] = PDFName("XObject") + sdict["Subtype"] = PDFName("PS") + return S.format(document) + +_mode2CS={'RGB':'DeviceRGB', 'L':'DeviceGray', 'CMYK':'DeviceCMYK'} +class PDFImageXObject: + # first attempts at a hard-coded one + # in the file, Image XObjects are stream objects. We already + # have a PDFStream object with 3 attributes: dictionary, content + # and filters. So the job of this thing is to construct the + # right PDFStream instance and ask it to format itself. + def __init__(self, name, source=None, mask=None): + self.name = name + self.width = 24 + self.height = 23 + self.bitsPerComponent = 1 + self.colorSpace = 'DeviceGray' + self._filters = 'ASCII85Decode', + self.streamContent = """ + 003B00 002700 002480 0E4940 114920 14B220 3CB650 + 75FE88 17FF8C 175F14 1C07E2 3803C4 703182 F8EDFC + B2BBC2 BB6F84 31BFC2 18EA3C 0E3E00 07FC00 03F800 + 1E1800 1FF800> + """ + self.mask = mask + + if source is None: + pass # use the canned one. + elif type(source)==type(''): + # it is a filename + import os + ext = string.lower(os.path.splitext(source)[1]) + src = open_for_read(source) + if not(ext in ('.jpg', '.jpeg') and self.loadImageFromJPEG(src)): + self.loadImageFromA85(src) + else: # it is already a PIL Image + self.loadImageFromSRC(source) + + def loadImageFromA85(self,source): + IMG=[] + imagedata = map(string.strip,pdfutils.makeA85Image(source,IMG=IMG)) + words = string.split(imagedata[1]) + self.width, self.height = map(string.atoi,(words[1],words[3])) + self.colorSpace = {'/RGB':'DeviceRGB', '/G':'DeviceGray', '/CMYK':'DeviceCMYK'}[words[7]] + self.bitsPerComponent = 8 + self._filters = 'ASCII85Decode','FlateDecode' #'A85','Fl' + if IMG: self._checkTransparency(IMG[0]) + elif self.mask=='auto': self.mask = None + self.streamContent = string.join(imagedata[3:-1],'') + + def loadImageFromJPEG(self,imageFile): + try: + try: + info = pdfutils.readJPEGInfo(imageFile) + finally: + imageFile.seek(0) #reset file pointer + except: + return False + self.width, self.height = info[0], info[1] + self.bitsPerComponent = 8 + if info[2] == 1: + self.colorSpace = 'DeviceGray' + elif info[2] == 3: + self.colorSpace = 'DeviceRGB' + else: #maybe should generate an error, is this right for CMYK? + self.colorSpace = 'DeviceCMYK' + self._dotrans = 1 + self.streamContent = pdfutils._AsciiBase85Encode(imageFile.read()) + self._filters = 'ASCII85Decode','DCTDecode' #'A85','DCT' + self.mask = None + return True + + def _checkTransparency(self,im): + if self.mask=='auto': + tc = im.getTransparent() + if tc: + self.mask = (tc[0], tc[0], tc[1], tc[1], tc[2], tc[2]) + else: + self.mask = None + elif hasattr(self.mask,'rgb'): + _ = self.mask.rgb() + self.mask = _[0],_[0],_[1],_[1],_[2],_[2] + + def loadImageFromSRC(self, im): + "Extracts the stream, width and height" + fp = im.jpeg_fh() + if fp: + self.loadImageFromJPEG(fp) + else: + zlib = import_zlib() + if not zlib: return + self.width, self.height = im.getSize() + raw = im.getRGBData() + assert(len(raw) == self.width*self.height, "Wrong amount of data for image") + self.streamContent = pdfutils._AsciiBase85Encode(zlib.compress(raw)) + self.colorSpace= _mode2CS[im.mode] + self.bitsPerComponent = 8 + self._filters = 'ASCII85Decode','FlateDecode' #'A85','Fl' + self._checkTransparency(im) + + def format(self, document): + S = PDFStream() + S.content = self.streamContent + dict = S.dictionary + dict["Type"] = PDFName("XObject") + dict["Subtype"] = PDFName("Image") + dict["Width"] = self.width + dict["Height"] = self.height + dict["BitsPerComponent"] = self.bitsPerComponent + dict["ColorSpace"] = PDFName(self.colorSpace) + if self.colorSpace=='DeviceCMYK' and getattr(self,'_dotrans',0): + dict["Decode"] = PDFArray([1,0,1,0,1,0,1,0]) + dict["Filter"] = PDFArray(map(PDFName,self._filters)) + dict["Length"] = len(self.streamContent) + if self.mask: dict["Mask"] = PDFArray(self.mask) + return S.format(document) + +if __name__=="__main__": + print "There is no script interpretation for pdfdoc." diff --git a/bin/reportlab/pdfbase/pdfform.py b/bin/reportlab/pdfbase/pdfform.py new file mode 100644 index 00000000000..9fca917021e --- /dev/null +++ b/bin/reportlab/pdfbase/pdfform.py @@ -0,0 +1,632 @@ + +"""Support for Acrobat Forms in ReportLab documents + +This module is somewhat experimental at this time. + +Includes basic support for + textfields, + select fields (drop down lists), and + check buttons. + +The public interface consists of functions at the moment. +At some later date these operations may be made into canvas +methods. (comments?) + +The ...Absolute(...) functions position the fields with respect +to the absolute canvas coordinate space -- that is, they do not +respect any coordinate transforms in effect for the canvas. + +The ...Relative(...) functions position the ONLY THE LOWER LEFT +CORNER of the field using the coordinate transform in effect for +the canvas. THIS WILL ONLY WORK CORRECTLY FOR TRANSLATED COORDINATES +-- THE SHAPE, SIZE, FONTSIZE, AND ORIENTATION OF THE FIELD WILL NOT BE EFFECTED +BY SCALING, ROTATION, SKEWING OR OTHER NON-TRANSLATION COORDINATE +TRANSFORMS. + +Please note that all field names (titles) in a given document must be unique. +Textfields and select fields only support the "base 14" canvas fonts +at this time. + +See individual function docstrings below for more information. + +The function test1(...) generates a simple test file. + +THIS CONTRIBUTION WAS COMMISSIONED BY REPORTLAB USERS +WHO WISH TO REMAIN ANONYMOUS. +""" + +### NOTE: MAKE THE STRING FORMATS DYNAMIC IN PATTERNS TO SUPPORT ENCRYPTION XXXX + +import string +from reportlab.pdfbase.pdfdoc import LINEEND, PDFString, PDFStream, PDFDictionary, PDFName + +#==========================public interfaces + +def textFieldAbsolute(canvas, title, x, y, width, height, value="", maxlen=1000000, multiline=0): + """Place a text field on the current page + with name title at ABSOLUTE position (x,y) with + dimensions (width, height), using value as the default value and + maxlen as the maximum permissible length. If multiline is set make + it a multiline field. + """ + theform = getForm(canvas) + return theform.textField(canvas, title, x, y, x+width, y+height, value, maxlen, multiline) + +def textFieldRelative(canvas, title, xR, yR, width, height, value="", maxlen=1000000, multiline=0): + "same as textFieldAbsolute except the x and y are relative to the canvas coordinate transform" + (xA, yA) = canvas.absolutePosition(xR,yR) + return textFieldAbsolute(canvas, title, xA, yA, width, height, value, maxlen, multiline) + +def buttonFieldAbsolute(canvas, title, value, x, y): + """Place a check button field on the current page + with name title and default value value (one of "Yes" or "Off") + at ABSOLUTE position (x,y). + """ + theform = getForm(canvas) + return theform.buttonField(canvas, title, value, x, y) + +def buttonFieldRelative(canvas, title, value, xR, yR): + "same as buttonFieldAbsolute except the x and y are relative to the canvas coordinate transform" + (xA, yA) = canvas.absolutePosition(xR,yR) + return buttonFieldAbsolute(canvas, title, value, xA, yA) + +def selectFieldAbsolute(canvas, title, value, options, x, y, width, height): + """Place a select field (drop down list) on the current page + with name title and + with options listed in the sequence options + default value value (must be one of options) + at ABSOLUTE position (x,y) with dimensions (width, height).""" + theform = getForm(canvas) + theform.selectField(canvas, title, value, options, x, y, x+width, y+height) + +def selectFieldRelative(canvas, title, value, options, xR, yR, width, height): + "same as textFieldAbsolute except the x and y are relative to the canvas coordinate transform" + (xA, yA) = canvas.absolutePosition(xR,yR) + return selectFieldAbsolute(canvas, title, value, options, xA, yA, width, height) + +def test1(): + from reportlab.pdfgen import canvas + fn = "formtest1.pdf" + c = canvas.Canvas(fn) + # first page + c.setFont("Courier", 10) + c.drawString(100, 500, "hello world") + textFieldAbsolute(c, "fieldA", 100, 600, 100, 20, "default value") + textFieldAbsolute(c, "fieldB", 100, 300, 100, 50, "another default value", multiline=1) + selectFieldAbsolute(c, "fieldC", "France", ["Canada", "France", "China"], 100, 200, 100, 20) + c.rect(100, 600, 100, 20) + buttonFieldAbsolute(c, "field2", "Yes", 100, 700) + c.rect(100, 700, 20, 20) + buttonFieldAbsolute(c, "field3", "Off", 100, 800) + c.rect(100, 800, 20, 20) + # second page + c.showPage() + c.setFont("Helvetica", 7) + c.translate(50, 20) + c.drawString(100, 500, "hello world") + textFieldRelative(c, "fieldA_1", 100, 600, 100, 20, "default value 2") + c.setStrokeColorRGB(1,0,0) + c.setFillColorRGB(0,1,0.5) + textFieldRelative(c, "fieldB_1", 100, 300, 100, 50, "another default value 2", multiline=1) + selectFieldRelative(c, "fieldC_1", "France 1", ["Canada 0", "France 1", "China 2"], 100, 200, 100, 20) + c.rect(100, 600, 100, 20) + buttonFieldRelative(c, "field2_1", "Yes", 100, 700) + c.rect(100, 700, 20, 20) + buttonFieldRelative(c, "field3_1", "Off", 100, 800) + c.rect(100, 800, 20, 20) + c.save() + print "wrote", fn + +#==========================end of public interfaces + +from pdfpattern import PDFPattern + +def getForm(canvas): + "get form from canvas, create the form if needed" + try: + return canvas.AcroForm + except AttributeError: + theform = canvas.AcroForm = AcroForm() + # install the form in the document + d = canvas._doc + cat = d._catalog + cat.AcroForm = theform + return theform + +class AcroForm: + def __init__(self): + self.fields = [] + def textField(self, canvas, title, xmin, ymin, xmax, ymax, value="", maxlen=1000000, multiline=0): + # determine the page ref + doc = canvas._doc + page = doc.thisPageRef() + # determine text info + (R,G,B) = canvas._fillColorRGB + #print "rgb", (R,G,B) + font = canvas. _fontname + fontsize = canvas. _fontsize + field = TextField(title, value, xmin, ymin, xmax, ymax, page, maxlen, + font, fontsize, R, G, B, multiline) + self.fields.append(field) + canvas._addAnnotation(field) + def selectField(self, canvas, title, value, options, xmin, ymin, xmax, ymax): + # determine the page ref + doc = canvas._doc + page = doc.thisPageRef() + # determine text info + (R,G,B) = canvas._fillColorRGB + #print "rgb", (R,G,B) + font = canvas. _fontname + fontsize = canvas. _fontsize + field = SelectField(title, value, options, xmin, ymin, xmax, ymax, page, + font=font, fontsize=fontsize, R=R, G=G, B=B) + self.fields.append(field) + canvas._addAnnotation(field) + def buttonField(self, canvas, title, value, xmin, ymin): + # determine the page ref + doc = canvas._doc + page = doc.thisPageRef() + field = ButtonField(title, value, xmin, ymin, page) + self.fields.append(field) + canvas._addAnnotation(field) + def format(self, document): + from reportlab.pdfbase.pdfdoc import PDFArray + proxy = PDFPattern(FormPattern, Resources=GLOBALRESOURCES, fields=PDFArray(self.fields)) + return proxy.format(document) + +FormPattern = [ +'<<', LINEEND, +' /NeedAppearances true ', LINEEND, +' /DA ', PDFString('/Helv 0 Tf 0 g '), LINEEND, +' /DR ', LINEEND, +["Resources"], +' /Fields ', LINEEND, +["fields"], +'>>' +] + +def FormFontsDictionary(): + from reportlab.pdfbase.pdfdoc import PDFDictionary + fontsdictionary = PDFDictionary() + fontsdictionary.__RefOnly__ = 1 + for (fullname, shortname) in FORMFONTNAMES.items(): + fontsdictionary[shortname] = FormFont(fullname, shortname) + fontsdictionary["ZaDb"] = ZADB + return fontsdictionary + +def FormResources(): + return PDFPattern(FormResourcesDictionaryPattern, + Encoding=ENCODING, Font=GLOBALFONTSDICTIONARY) + +ZaDbPattern = [ +' <<' +' /BaseFont' +' /ZapfDingbats' +' /Name' +' /ZaDb' +' /Subtype' +' /Type1' +' /Type' +' /Font' +'>>'] + +ZADB = PDFPattern(ZaDbPattern) + +FormResourcesDictionaryPattern = [ +'<<', +' /Encoding ', +["Encoding"], LINEEND, +' /Font ', +["Font"], LINEEND, +'>>' +] + +FORMFONTNAMES = { + "Helvetica": "Helv", + "Helvetica-Bold": "HeBo", + 'Courier': "Cour", + 'Courier-Bold': "CoBo", + 'Courier-Oblique': "CoOb", + 'Courier-BoldOblique': "CoBO", + 'Helvetica-Oblique': "HeOb", + 'Helvetica-BoldOblique': "HeBO", + 'Times-Roman': "Time", + 'Times-Bold': "TiBo", + 'Times-Italic': "TiIt", + 'Times-BoldItalic': "TiBI", + } + +EncodingPattern = [ +'<<', +' /PDFDocEncoding ', +["PDFDocEncoding"], LINEEND, +'>>', +] + +PDFDocEncodingPattern = [ +'<<' +' /Differences' +' [' +' 24' +' /breve' +' /caron' +' /circumflex' +' /dotaccent' +' /hungarumlaut' +' /ogonek' +' /ring' +' /tilde' +' 39' +' /quotesingle' +' 96' +' /grave' +' 128' +' /bullet' +' /dagger' +' /daggerdbl' +' /ellipsis' +' /emdash' +' /endash' +' /florin' +' /fraction' +' /guilsinglleft' +' /guilsinglright' +' /minus' +' /perthousand' +' /quotedblbase' +' /quotedblleft' +' /quotedblright' +' /quoteleft' +' /quoteright' +' /quotesinglbase' +' /trademark' +' /fi' +' /fl' +' /Lslash' +' /OE' +' /Scaron' +' /Ydieresis' +' /Zcaron' +' /dotlessi' +' /lslash' +' /oe' +' /scaron' +' /zcaron' +' 160' +' /Euro' +' 164' +' /currency' +' 166' +' /brokenbar' +' 168' +' /dieresis' +' /copyright' +' /ordfeminine' +' 172' +' /logicalnot' +' /.notdef' +' /registered' +' /macron' +' /degree' +' /plusminus' +' /twosuperior' +' /threesuperior' +' /acute' +' /mu' +' 183' +' /periodcentered' +' /cedilla' +' /onesuperior' +' /ordmasculine' +' 188' +' /onequarter' +' /onehalf' +' /threequarters' +' 192' +' /Agrave' +' /Aacute' +' /Acircumflex' +' /Atilde' +' /Adieresis' +' /Aring' +' /AE' +' /Ccedilla' +' /Egrave' +' /Eacute' +' /Ecircumflex' +' /Edieresis' +' /Igrave' +' /Iacute' +' /Icircumflex' +' /Idieresis' +' /Eth' +' /Ntilde' +' /Ograve' +' /Oacute' +' /Ocircumflex' +' /Otilde' +' /Odieresis' +' /multiply' +' /Oslash' +' /Ugrave' +' /Uacute' +' /Ucircumflex' +' /Udieresis' +' /Yacute' +' /Thorn' +' /germandbls' +' /agrave' +' /aacute' +' /acircumflex' +' /atilde' +' /adieresis' +' /aring' +' /ae' +' /ccedilla' +' /egrave' +' /eacute' +' /ecircumflex' +' /edieresis' +' /igrave' +' /iacute' +' /icircumflex' +' /idieresis' +' /eth' +' /ntilde' +' /ograve' +' /oacute' +' /ocircumflex' +' /otilde' +' /odieresis' +' /divide' +' /oslash' +' /ugrave' +' /uacute' +' /ucircumflex' +' /udieresis' +' /yacute' +' /thorn' +' /ydieresis' +' ]' +' /Type' +' /Encoding' +'>>'] + +# global constant +PDFDOCENC = PDFPattern(PDFDocEncodingPattern) +# global constant +ENCODING = PDFPattern(EncodingPattern, PDFDocEncoding=PDFDOCENC) + + +def FormFont(BaseFont, Name): + from reportlab.pdfbase.pdfdoc import PDFName + return PDFPattern(FormFontPattern, BaseFont=PDFName(BaseFont), Name=PDFName(Name), Encoding=PDFDOCENC) + +FormFontPattern = [ +'<<', +' /BaseFont ', +["BaseFont"], LINEEND, +' /Encoding ', +["Encoding"], LINEEND, +' /Name ', +["Name"], LINEEND, +' /Subtype ' +' /Type1 ' +' /Type ' +' /Font ' +'>>' ] + +# global constants +GLOBALFONTSDICTIONARY = FormFontsDictionary() +GLOBALRESOURCES = FormResources() + + +def TextField(title, value, xmin, ymin, xmax, ymax, page, + maxlen=1000000, font="Helvetica-Bold", fontsize=9, R=0, G=0, B=0.627, multiline=0): + from reportlab.pdfbase.pdfdoc import PDFString, PDFName + Flags = 0 + if multiline: + Flags = Flags | (1<<12) # bit 13 is at position 12 :) + fontname = FORMFONTNAMES[font] + return PDFPattern(TextFieldPattern, + value=PDFString(value), maxlen=maxlen, page=page, + title=PDFString(title), + xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, + fontname=PDFName(fontname), fontsize=fontsize, R=R, G=G, B=B, Flags=Flags) + + +TextFieldPattern = [ +'<<' +' /DA' +' (', ["fontname"],' ',["fontsize"],' Tf ',["R"],' ',["G"],' ',["B"],' rg)' +' /DV ', +["value"], LINEEND, +' /F 4 /FT /Tx' +'/MK << /BC [ 0 0 0 ] >>' +' /MaxLen ', +["maxlen"], LINEEND, +' /P ', +["page"], LINEEND, +' /Rect ' +' [', ["xmin"], " ", ["ymin"], " ", ["xmax"], " ", ["ymax"], ' ]' +'/Subtype /Widget' +' /T ', +["title"], LINEEND, +' /Type' +' /Annot' +' /V ', +["value"], LINEEND, +' /Ff ', +["Flags"],LINEEND, +'>>'] + +def SelectField(title, value, options, xmin, ymin, xmax, ymax, page, + font="Helvetica-Bold", fontsize=9, R=0, G=0, B=0.627): + #print "ARGS", (title, value, options, xmin, ymin, xmax, ymax, page, font, fontsize, R, G, B) + from reportlab.pdfbase.pdfdoc import PDFString, PDFName, PDFArray + if value not in options: + raise ValueError, "value %s must be one of options %s" % (repr(value), repr(options)) + fontname = FORMFONTNAMES[font] + optionstrings = map(PDFString, options) + optionarray = PDFArray(optionstrings) + return PDFPattern(SelectFieldPattern, + Options=optionarray, + Selected=PDFString(value), Page=page, + Name=PDFString(title), + xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax, + fontname=PDFName(fontname), fontsize=fontsize, R=R, G=G, B=B) + +SelectFieldPattern = [ +'<< % a select list',LINEEND, +' /DA ', +' (', ["fontname"],' ',["fontsize"],' Tf ',["R"],' ',["G"],' ',["B"],' rg)',LINEEND, +#' (/Helv 12 Tf 0 g)',LINEEND, +' /DV ', +["Selected"],LINEEND, +' /F ', +' 4',LINEEND, +' /FT ', +' /Ch',LINEEND, +' /MK ', +' <<', +' /BC', +' [', +' 0', +' 0', +' 0', +' ]', +' /BG', +' [', +' 1', +' 1', +' 1', +' ]', +' >>',LINEEND, +' /Opt ', +["Options"],LINEEND, +' /P ', +["Page"],LINEEND, +'/Rect', +' [',["xmin"], " ", ["ymin"], " ", ["xmax"], " ", ["ymax"], +' ] ',LINEEND, +'/Subtype', +' /Widget',LINEEND, +' /T ', +["Name"],LINEEND, +' /Type ', +' /Annot', +' /V ', +["Selected"],LINEEND, +'>>'] + +def ButtonField(title, value, xmin, ymin, page): + if value not in ("Yes", "Off"): + raise ValueError, "button value must be 'Yes' or 'Off': "+repr(value) + (dx, dy) = (16.77036, 14.90698) + return PDFPattern(ButtonFieldPattern, + Name=PDFString(title), + xmin=xmin, ymin=ymin, xmax=xmin+dx, ymax=ymin+dy, + Hide=HIDE, + APDOff=APDOFF, + APDYes=APDYES, + APNYes=APNYES, + Value=PDFName(value), + Page=page) + +ButtonFieldPattern = ['<< ', +'/AA', +' <<', +' /D ', +["Hide"], LINEEND, +#' %(imported.18.0)s', +' >> ', +'/AP ', +' <<', +' /D', +' <<', +' /Off ', +#' %(imported.40.0)s', +["APDOff"], LINEEND, +' /Yes ', +#' %(imported.39.0)s', +["APDYes"], LINEEND, +' >>', LINEEND, +' /N', +' << ', +' /Yes ', +#' %(imported.38.0)s', +["APNYes"], LINEEND, +' >>', +' >>', LINEEND, +' /AS ', +["Value"], LINEEND, +' /DA ', +PDFString('/ZaDb 0 Tf 0 g'), LINEEND, +'/DV ', +["Value"], LINEEND, +'/F ', +' 4 ', +'/FT ', +' /Btn ', +'/H ', +' /T ', +'/MK ', +' <<', +' /AC (\\376\\377)', +#PDFString('\376\377'), +' /CA ', +PDFString('4'), +' /RC ', +PDFString('\376\377'), +' >> ',LINEEND, +'/P ', +["Page"], LINEEND, +'/Rect', +' [',["xmin"], " ", ["ymin"], " ", ["xmax"], " ", ["ymax"], +' ] ',LINEEND, +'/Subtype', +' /Widget ', +'/T ', +["Name"], LINEEND, +'/Type', +' /Annot ', +'/V ', +["Value"], LINEEND, +' >>'] + +HIDE = PDFPattern([ +'<< ' +'/S ' +' /Hide ' +'>>']) + +def buttonStreamDictionary(): + "everything except the length for the button appearance streams" + result = PDFDictionary() + result["SubType"] = "/Form" + result["BBox"] = "[0 0 16.77036 14.90698]" + font = PDFDictionary() + font["ZaDb"] = ZADB + resources = PDFDictionary() + resources["ProcSet"] = "[ /PDF /Text ]" + resources["Font"] = font + result["Resources"] = resources + return result + +def ButtonStream(content): + dict = buttonStreamDictionary() + result = PDFStream(dict, content) + result.filters = [] + return result + +APDOFF = ButtonStream('0.749 g 0 0 16.7704 14.907 re f'+LINEEND) +APDYES = ButtonStream( +'0.749 g 0 0 16.7704 14.907 re f q 1 1 14.7704 12.907 re W '+ +'n BT /ZaDb 11.3086 Tf 0 g 1 0 0 1 3.6017 3.3881 Tm (4) Tj ET'+LINEEND) +APNYES = ButtonStream( +'q 1 1 14.7704 12.907 re W n BT /ZaDb 11.3086 Tf 0 g 1 0 0 1 3.6017 3.3881 Tm (4) Tj ET Q'+LINEEND) + +#==== script interpretation + +if __name__=="__main__": + test1() \ No newline at end of file diff --git a/bin/reportlab/pdfbase/pdfmetrics.py b/bin/reportlab/pdfbase/pdfmetrics.py new file mode 100755 index 00000000000..58cf3f051b6 --- /dev/null +++ b/bin/reportlab/pdfbase/pdfmetrics.py @@ -0,0 +1,796 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/pdfmetrics.py +#$Header $ +__version__=''' $Id: pdfmetrics.py 2873 2006-05-17 10:59:59Z rgbecker $ ''' +__doc__=""" +This provides a database of font metric information and +efines Font, Encoding and TypeFace classes aimed at end users. + +There are counterparts to some of these in pdfbase/pdfdoc.py, but +the latter focus on constructing the right PDF objects. These +classes are declarative and focus on letting the user construct +and query font objects. + +The module maintains a registry of font objects at run time. + +It is independent of the canvas or any particular context. It keeps +a registry of Font, TypeFace and Encoding objects. Ideally these +would be pre-loaded, but due to a nasty circularity problem we +trap attempts to access them and do it on first access. +""" +import string, os +from types import StringType, ListType, TupleType +from reportlab.pdfbase import _fontdata +from reportlab.lib.logger import warnOnce +from reportlab.lib.utils import rl_isfile, rl_glob, rl_isdir, open_and_read, open_and_readlines +from reportlab.rl_config import defaultEncoding +import rl_codecs + +rl_codecs.RL_Codecs.register() +standardFonts = _fontdata.standardFonts +standardEncodings = _fontdata.standardEncodings + +_typefaces = {} +_encodings = {} +_fonts = {} + +def _py_unicode2T1(utext,fonts): + '''return a list of (font,string) pairs representing the unicode text''' + #print 'unicode2t1(%s, %s): %s' % (utext, fonts, type(utext)) + #if type(utext) + R = [] + font, fonts = fonts[0], fonts[1:] + enc = font.encName + if 'UCS-2' in enc: + enc = 'UTF16' + while utext: + try: + R.append((font,utext.encode(enc))) + break + except UnicodeEncodeError, e: + i0, il = e.args[2:4] + if i0: + R.append((font,utext[:i0].encode(enc))) + if fonts: + R.extend(_py_unicode2T1(utext[i0:il],fonts)) + else: + R.append((_notdefFont,_notdefChar*(il-i0))) + utext = utext[il:] + return R + +try: + from _rl_accel import unicode2T1 +except ImportError: + unicode2T1 = _py_unicode2T1 + +class FontError(Exception): + pass +class FontNotFoundError(Exception): + pass + +def parseAFMFile(afmFileName): + """Quick and dirty - gives back a top-level dictionary + with top-level items, and a 'widths' key containing + a dictionary of glyph names and widths. Just enough + needed for embedding. A better parser would accept + options for what data you wwanted, and preserve the + order.""" + + lines = open_and_readlines(afmFileName, 'r') + if len(lines)<=1: + #likely to be a MAC file + if lines: lines = string.split(lines[0],'\r') + if len(lines)<=1: + raise ValueError, 'AFM file %s hasn\'t enough data' % afmFileName + topLevel = {} + glyphLevel = [] + + lines = map(string.strip, lines) + #pass 1 - get the widths + inMetrics = 0 # os 'TOP', or 'CHARMETRICS' + for line in lines: + if line[0:16] == 'StartCharMetrics': + inMetrics = 1 + elif line[0:14] == 'EndCharMetrics': + inMetrics = 0 + elif inMetrics: + chunks = string.split(line, ';') + chunks = map(string.strip, chunks) + cidChunk, widthChunk, nameChunk = chunks[0:3] + + # character ID + l, r = string.split(cidChunk) + assert l == 'C', 'bad line in font file %s' % line + cid = string.atoi(r) + + # width + l, r = string.split(widthChunk) + assert l == 'WX', 'bad line in font file %s' % line + width = string.atoi(r) + + # name + l, r = string.split(nameChunk) + assert l == 'N', 'bad line in font file %s' % line + name = r + + glyphLevel.append((cid, width, name)) + + # pass 2 font info + inHeader = 0 + for line in lines: + if line[0:16] == 'StartFontMetrics': + inHeader = 1 + if line[0:16] == 'StartCharMetrics': + inHeader = 0 + elif inHeader: + if line[0:7] == 'Comment': pass + try: + left, right = string.split(line,' ',1) + except: + raise ValueError, "Header information error in afm %s: line='%s'" % (afmFileName, line) + try: + right = string.atoi(right) + except: + pass + topLevel[left] = right + + + return (topLevel, glyphLevel) + +class TypeFace: + def __init__(self, name): + self.name = name + self.glyphNames = [] + self.glyphWidths = {} + self.ascent = 0 + self.descent = 0 + + + # all typefaces of whatever class should have these 3 attributes. + # these are the basis for family detection. + self.familyName = None # should set on load/construction if possible + self.bold = 0 # bold faces should set this + self.italic = 0 #italic faces should set this + + + if name == 'ZapfDingbats': + self.requiredEncoding = 'ZapfDingbatsEncoding' + elif name == 'Symbol': + self.requiredEncoding = 'SymbolEncoding' + else: + self.requiredEncoding = None + if name in standardFonts: + self.builtIn = 1 + self._loadBuiltInData(name) + else: + self.builtIn = 0 + + def _loadBuiltInData(self, name): + """Called for the built in 14 fonts. Gets their glyph data. + We presume they never change so this can be a shared reference.""" + name = str(name) #needed for pycanvas&jython/2.1 compatibility + self.glyphWidths = _fontdata.widthsByFontGlyph[name] + self.glyphNames = self.glyphWidths.keys() + self.ascent,self.descent = _fontdata.ascent_descent[name] + + def getFontFiles(self): + "Info function, return list of the font files this depends on." + return [] + + def findT1File(self, ext='.pfb'): + possible_exts = (string.lower(ext), string.upper(ext)) + if hasattr(self,'pfbFileName'): + r_basename = os.path.splitext(self.pfbFileName)[0] + for e in possible_exts: + if rl_isfile(r_basename + e): + return r_basename + e + try: + r = _fontdata.findT1File(self.name) + except: + afm = bruteForceSearchForAFM(self.name) + if afm: + if string.lower(ext) == '.pfb': + for e in possible_exts: + pfb = os.path.splitext(afm)[0] + e + if rl_isfile(pfb): + r = pfb + else: + r = None + elif string.lower(ext) == '.afm': + r = afm + else: + r = None + if r is None: + warnOnce("Can't find %s for face '%s'" % (ext, self.name)) + return r + +def bruteForceSearchForFile(fn,searchPath=None): + if searchPath is None: from reportlab.rl_config import T1SearchPath as searchPath + if rl_isfile(fn): return fn + bfn = os.path.basename(fn) + for dirname in searchPath: + if not rl_isdir(dirname): continue + tfn = os.path.join(dirname,bfn) + if rl_isfile(tfn): return tfn + return fn + +def bruteForceSearchForAFM(faceName): + """Looks in all AFM files on path for face with given name. + + Returns AFM file name or None. Ouch!""" + from reportlab.rl_config import T1SearchPath + + for dirname in T1SearchPath: + if not rl_isdir(dirname): continue + possibles = rl_glob(dirname + os.sep + '*.[aA][fF][mM]') + for possible in possibles: + (topDict, glyphDict) = parseAFMFile(possible) + if topDict['FontName'] == faceName: + return possible + return None + +#for faceName in standardFonts: +# registerTypeFace(TypeFace(faceName)) + + +class Encoding: + """Object to help you create and refer to encodings.""" + def __init__(self, name, base=None): + self.name = name + self.frozen = 0 + if name in standardEncodings: + assert base is None, "Can't have a base encoding for a standard encoding" + self.baseEncodingName = name + self.vector = _fontdata.encodings[name] + elif base == None: + # assume based on the usual one + self.baseEncodingName = defaultEncoding + self.vector = _fontdata.encodings[defaultEncoding] + elif type(base) is StringType: + baseEnc = getEncoding(base) + self.baseEncodingName = baseEnc.name + self.vector = baseEnc.vector[:] + elif type(base) in (ListType, TupleType): + self.baseEncodingName = defaultEncoding + self.vector = base[:] + elif isinstance(base, Encoding): + # accept a vector + self.baseEncodingName = base.name + self.vector = base.vector[:] + + def __getitem__(self, index): + "Return glyph name for that code point, or None" + # THIS SHOULD BE INLINED FOR SPEED + return self.vector[index] + + def __setitem__(self, index, value): + # should fail if they are frozen + assert self.frozen == 0, 'Cannot modify a frozen encoding' + if self.vector[index]!=value: + L = list(self.vector) + L[index] = value + self.vector = tuple(L) + + def freeze(self): + self.vector = tuple(self.vector) + self.frozen = 1 + + def isEqual(self, other): + return ((self.name == other.name) and (self.vector == other.vector)) + + def modifyRange(self, base, newNames): + """Set a group of character names starting at the code point 'base'.""" + assert self.frozen == 0, 'Cannot modify a frozen encoding' + idx = base + for name in newNames: + self.vector[idx] = name + idx = idx + 1 + + def getDifferences(self, otherEnc): + """Return a compact list of the code points differing between two encodings + + This is in the Adobe format: list of + [[b1, name1, name2, name3], + [b2, name4]] + where b1...bn is the starting code point, and the glyph names following + are assigned consecutive code points.""" + + ranges = [] + curRange = None + for i in xrange(len(self.vector)): + glyph = self.vector[i] + if glyph==otherEnc.vector[i]: + if curRange: + ranges.append(curRange) + curRange = [] + else: + if curRange: + curRange.append(glyph) + elif glyph: + curRange = [i, glyph] + if curRange: + ranges.append(curRange) + return ranges + + def makePDFObject(self): + "Returns a PDF Object representing self" + # avoid circular imports - this cannot go at module level + from reportlab.pdfbase import pdfdoc + + D = {} + baseEnc = getEncoding(self.baseEncodingName) + differences = self.getDifferences(baseEnc) #[None] * 256) + + # if no differences, we just need the base name + if differences == []: + return pdfdoc.PDFName(self.baseEncodingName) + else: + #make up a dictionary describing the new encoding + diffArray = [] + for range in differences: + diffArray.append(range[0]) # numbers go 'as is' + for glyphName in range[1:]: + if glyphName is not None: + # there is no way to 'unset' a character in the base font. + diffArray.append('/' + glyphName) + + #print 'diffArray = %s' % diffArray + D["Differences"] = pdfdoc.PDFArray(diffArray) + D["BaseEncoding"] = pdfdoc.PDFName(self.baseEncodingName) + D["Type"] = pdfdoc.PDFName("Encoding") + PD = pdfdoc.PDFDictionary(D) + return PD + +#for encName in standardEncodings: +# registerEncoding(Encoding(encName)) + +standardT1SubstitutionFonts = [] +class Font: + """Represents a font (i.e combination of face and encoding). + + Defines suitable machinery for single byte fonts. This is + a concrete class which can handle the basic built-in fonts; + not clear yet if embedded ones need a new font class or + just a new typeface class (which would do the job through + composition)""" + def __init__(self, name, faceName, encName): + self.fontName = name + face = self.face = getTypeFace(faceName) + self.encoding= getEncoding(encName) + self.encName = encName + if face.builtIn and face.requiredEncoding is None: + _ = standardT1SubstitutionFonts + else: + _ = [] + self.substitutionFonts = _ + self._calcWidths() + + # multi byte fonts do their own stringwidth calculations. + # signal this here. + self._multiByte = 0 + + + def __repr__(self): + return "<%s %s>" % (self.__class__.__name__, self.face.name) + + def _calcWidths(self): + """Vector of widths for stringWidth function""" + #synthesize on first request + w = [0] * 256 + gw = self.face.glyphWidths + vec = self.encoding.vector + for i in range(256): + glyphName = vec[i] + if glyphName is not None: + try: + width = gw[glyphName] + w[i] = width + except KeyError: + import reportlab.rl_config + if reportlab.rl_config.warnOnMissingFontGlyphs: + print 'typeface "%s" does not have a glyph "%s", bad font!' % (self.face.name, glyphName) + else: + pass + self.widths = w + + def _py_stringWidth(self, text, size, encoding='utf8'): + """This is the "purist" approach to width. The practical approach + is to use the stringWidth function, which may be swapped in for one + written in C.""" + if not isinstance(text,unicode): text = text.decode(encoding) + return sum([sum(map(f.widths.__getitem__,map(ord,t))) for f, t in unicode2T1(text,[self]+self.substitutionFonts)])*0.001*size + stringWidth = _py_stringWidth + + def _formatWidths(self): + "returns a pretty block in PDF Array format to aid inspection" + text = '[' + for i in range(256): + text = text + ' ' + str(self.widths[i]) + if i == 255: + text = text + ' ]' + if i % 16 == 15: + text = text + '\n' + return text + + def addObjects(self, doc): + """Makes and returns one or more PDF objects to be added + to the document. The caller supplies the internal name + to be used (typically F1, F2... in sequence) """ + # avoid circular imports - this cannot go at module level + from reportlab.pdfbase import pdfdoc + + # construct a Type 1 Font internal object + internalName = 'F' + repr(len(doc.fontMapping)+1) + pdfFont = pdfdoc.PDFType1Font() + pdfFont.Name = internalName + pdfFont.BaseFont = self.face.name + pdfFont.__Comment__ = 'Font %s' % self.fontName + pdfFont.Encoding = self.encoding.makePDFObject() + + # is it a built-in one? if not, need more stuff. + if not self.face.name in standardFonts: + pdfFont.FirstChar = 0 + pdfFont.LastChar = 255 + pdfFont.Widths = pdfdoc.PDFArray(self.widths) + pdfFont.FontDescriptor = self.face.addObjects(doc) + # now link it in + ref = doc.Reference(pdfFont, internalName) + + # also refer to it in the BasicFonts dictionary + fontDict = doc.idToObject['BasicFonts'].dict + fontDict[internalName] = pdfFont + + # and in the font mappings + doc.fontMapping[self.fontName] = '/' + internalName + +PFB_MARKER=chr(0x80) +PFB_ASCII=chr(1) +PFB_BINARY=chr(2) +PFB_EOF=chr(3) +def _pfbSegLen(p,d): + '''compute a pfb style length from the first 4 bytes of string d''' + return ((((ord(d[p+3])<<8)|ord(d[p+2])<<8)|ord(d[p+1]))<<8)|ord(d[p]) + +def _pfbCheck(p,d,m,fn): + if d[p]!=PFB_MARKER or d[p+1]!=m: + raise ValueError, 'Bad pfb file\'%s\' expected chr(%d)chr(%d) at char %d, got chr(%d)chr(%d)' % (fn,ord(PFB_MARKER),ord(m),p,ord(d[p]),ord(d[p+1])) + if m==PFB_EOF: return + p = p + 2 + l = _pfbSegLen(p,d) + p = p + 4 + if p+l>len(d): + raise ValueError, 'Bad pfb file\'%s\' needed %d+%d bytes have only %d!' % (fn,p,l,len(d)) + return p, p+l + + +class EmbeddedType1Face(TypeFace): + """A Type 1 font other than one of the basic 14. + + Its glyph data will be embedded in the PDF file.""" + def __init__(self, afmFileName, pfbFileName): + # ignore afm file for now + TypeFace.__init__(self, None) + #None is a hack, name will be supplied by AFM parse lower done + #in this __init__ method. + self.afmFileName = os.path.abspath(afmFileName) + self.pfbFileName = os.path.abspath(pfbFileName) + self.requiredEncoding = None + self._loadGlyphs(pfbFileName) + self._loadMetrics(afmFileName) + + def getFontFiles(self): + return [self.afmFileName, self.pfbFileName] + + def _loadGlyphs(self, pfbFileName): + """Loads in binary glyph data, and finds the four length + measurements needed for the font descriptor""" + pfbFileName = bruteForceSearchForFile(pfbFileName) + assert rl_isfile(pfbFileName), 'file %s not found' % pfbFileName + d = open_and_read(pfbFileName, 'b') + s1, l1 = _pfbCheck(0,d,PFB_ASCII,pfbFileName) + s2, l2 = _pfbCheck(l1,d,PFB_BINARY,pfbFileName) + s3, l3 = _pfbCheck(l2,d,PFB_ASCII,pfbFileName) + _pfbCheck(l3,d,PFB_EOF,pfbFileName) + self._binaryData = d[s1:l1]+d[s2:l2]+d[s3:l3] + + self._length = len(self._binaryData) + self._length1 = l1-s1 + self._length2 = l2-s2 + self._length3 = l3-s3 + + + def _loadMetrics(self, afmFileName): + """Loads in and parses font metrics""" + #assert os.path.isfile(afmFileName), "AFM file %s not found" % afmFileName + afmFileName = bruteForceSearchForFile(afmFileName) + (topLevel, glyphData) = parseAFMFile(afmFileName) + + self.name = topLevel['FontName'] + self.familyName = topLevel['FamilyName'] + self.ascent = topLevel.get('Ascender', 1000) + self.descent = topLevel.get('Descender', 0) + self.capHeight = topLevel.get('CapHeight', 1000) + self.italicAngle = topLevel.get('ItalicAngle', 0) + self.stemV = topLevel.get('stemV', 0) + self.xHeight = topLevel.get('XHeight', 1000) + + strBbox = topLevel.get('FontBBox', [0,0,1000,1000]) + tokens = string.split(strBbox) + self.bbox = [] + for tok in tokens: + self.bbox.append(string.atoi(tok)) + + glyphWidths = {} + for (cid, width, name) in glyphData: + glyphWidths[name] = width + self.glyphWidths = glyphWidths + self.glyphNames = glyphWidths.keys() + self.glyphNames.sort() + + # for font-specific encodings like Symbol, Dingbats, Carta we + # need to make a new encoding as well.... + if topLevel.get('EncodingScheme', None) == 'FontSpecific': + names = [None] * 256 + for (code, width, name) in glyphData: + if code >=0 and code <=255: + names[code] = name + encName = self.name + 'Encoding' + self.requiredEncoding = encName + enc = Encoding(encName, names) + registerEncoding(enc) + + def addObjects(self, doc): + """Add whatever needed to PDF file, and return a FontDescriptor reference""" + from reportlab.pdfbase import pdfdoc + + fontFile = pdfdoc.PDFStream() + fontFile.content = self._binaryData + #fontFile.dictionary['Length'] = self._length + fontFile.dictionary['Length1'] = self._length1 + fontFile.dictionary['Length2'] = self._length2 + fontFile.dictionary['Length3'] = self._length3 + #fontFile.filters = [pdfdoc.PDFZCompress] + + fontFileRef = doc.Reference(fontFile, 'fontFile:' + self.pfbFileName) + + fontDescriptor = pdfdoc.PDFDictionary({ + 'Type': '/FontDescriptor', + 'Ascent':self.ascent, + 'CapHeight':self.capHeight, + 'Descent':self.descent, + 'Flags': 34, + 'FontBBox':pdfdoc.PDFArray(self.bbox), + 'FontName':pdfdoc.PDFName(self.name), + 'ItalicAngle':self.italicAngle, + 'StemV':self.stemV, + 'XHeight':self.xHeight, + 'FontFile': fontFileRef, + }) + fontDescriptorRef = doc.Reference(fontDescriptor, 'fontDescriptor:' + self.name) + return fontDescriptorRef + +def registerTypeFace(face): + assert isinstance(face, TypeFace), 'Not a TypeFace: %s' % face + _typefaces[face.name] = face + # HACK - bold/italic do not apply for type 1, so egister + # all combinations of mappings. + from reportlab.lib import fonts + ttname = string.lower(face.name) + if not face.name in standardFonts: + fonts.addMapping(ttname, 0, 0, face.name) + fonts.addMapping(ttname, 1, 0, face.name) + fonts.addMapping(ttname, 0, 1, face.name) + fonts.addMapping(ttname, 1, 1, face.name) + +def registerEncoding(enc): + assert isinstance(enc, Encoding), 'Not an Encoding: %s' % enc + if _encodings.has_key(enc.name): + # already got one, complain if they are not the same + if enc.isEqual(_encodings[enc.name]): + enc.freeze() + else: + raise FontError('Encoding "%s" already registered with a different name vector!' % enc.Name) + else: + _encodings[enc.name] = enc + enc.freeze() + # have not yet dealt with immutability! + +def registerFont(font): + "Registers a font, including setting up info for accelerated stringWidth" + #assert isinstance(font, Font), 'Not a Font: %s' % font + fontName = font.fontName + _fonts[fontName] = font + if font._multiByte: + # CID fonts don't need to have typeface registered. + #need to set mappings so it can go in a paragraph even if within + # bold tags + from reportlab.lib import fonts + ttname = string.lower(font.fontName) + fonts.addMapping(ttname, 0, 0, font.fontName) + fonts.addMapping(ttname, 1, 0, font.fontName) + fonts.addMapping(ttname, 0, 1, font.fontName) + fonts.addMapping(ttname, 1, 1, font.fontName) + + +def getTypeFace(faceName): + """Lazily construct known typefaces if not found""" + try: + return _typefaces[faceName] + except KeyError: + # not found, construct it if known + if faceName in standardFonts: + face = TypeFace(faceName) + (face.familyName, face.bold, face.italic) = _fontdata.standardFontAttributes[faceName] + registerTypeFace(face) +## print 'auto-constructing type face %s with family=%s, bold=%d, italic=%d' % ( +## face.name, face.familyName, face.bold, face.italic) + return face + else: + #try a brute force search + afm = bruteForceSearchForAFM(faceName) + if afm: + for e in ('.pfb', '.PFB'): + pfb = os.path.splitext(afm)[0] + e + if rl_isfile(pfb): break + assert rl_isfile(pfb), 'file %s not found!' % pfb + face = EmbeddedType1Face(afm, pfb) + registerTypeFace(face) + return face + else: + raise + +def getEncoding(encName): + """Lazily construct known encodings if not found""" + try: + return _encodings[encName] + except KeyError: + if encName in standardEncodings: + enc = Encoding(encName) + registerEncoding(enc) + #print 'auto-constructing encoding %s' % encName + return enc + else: + raise + +def findFontAndRegister(fontName): + '''search for and register a font given it's name''' + #it might have a font-specific encoding e.g. Symbol + # or Dingbats. If not, take the default. + face = getTypeFace(fontName) + if face.requiredEncoding: + font = Font(fontName, fontName, face.requiredEncoding) + else: + font = Font(fontName, fontName, defaultEncoding) + registerFont(font) + return font + +def _py_getFont(fontName): + """Lazily constructs known fonts if not found. + + Names of form 'face-encoding' will be built if + face and encoding are known. Also if the name is + just one of the standard 14, it will make up a font + in the default encoding.""" + try: + return _fonts[fontName] + except KeyError: + return findFontAndRegister(fontName) + +try: + from _rl_accel import getFontU as getFont +except ImportError: + getFont = _py_getFont + +_notdefFont,_notdefChar = getFont('ZapfDingbats'),chr(110) +standardT1SubstitutionFonts.extend([getFont('Symbol'),getFont('ZapfDingbats')]) + +def getAscentDescent(fontName): + font = getFont(fontName) + try: + return font.ascent,font.descent + except: + return font.face.ascent,font.face.descent + +def getAscent(fontName): + return getAscentDescent(fontName)[0] + +def getDescent(fontName): + return getAscentDescent(fontName)[1] + +def getRegisteredFontNames(): + "Returns what's in there" + reg = _fonts.keys() + reg.sort() + return reg + +def _py_stringWidth(text, fontName, fontSize, encoding='utf8'): + """Define this anyway so it can be tested, but whether it is used or not depends on _rl_accel""" + return getFont(fontName).stringWidth(text, fontSize, encoding=encoding) + +try: + from _rl_accel import stringWidthU as stringWidth +except ImportError: + stringWidth = _py_stringWidth + +try: + from _rl_accel import _instanceStringWidthU + import new + Font.stringWidth = new.instancemethod(_instanceStringWidthU,None,Font) +except ImportError: + pass + +def dumpFontData(): + print 'Registered Encodings:' + keys = _encodings.keys() + keys.sort() + for encName in keys: + print ' ',encName + + print + print 'Registered Typefaces:' + faces = _typefaces.keys() + faces.sort() + for faceName in faces: + print ' ',faceName + + + print + print 'Registered Fonts:' + k = _fonts.keys() + k.sort() + for key in k: + font = _fonts[key] + print ' %s (%s/%s)' % (font.fontName, font.face.name, font.encoding.name) + +def test3widths(texts): + # checks all 3 algorithms give same answer, note speed + import time + for fontName in standardFonts[0:1]: +## t0 = time.time() +## for text in texts: +## l1 = stringWidth(text, fontName, 10) +## t1 = time.time() +## print 'fast stringWidth took %0.4f' % (t1 - t0) + + t0 = time.time() + w = getFont(fontName).widths + for text in texts: + l2 = 0 + for ch in text: + l2 = l2 + w[ord(ch)] + t1 = time.time() + print 'slow stringWidth took %0.4f' % (t1 - t0) + + t0 = time.time() + for text in texts: + l3 = getFont(fontName).stringWidth(text, 10) + t1 = time.time() + print 'class lookup and stringWidth took %0.4f' % (t1 - t0) + print + +def testStringWidthAlgorithms(): + rawdata = open('../../rlextra/rml2pdf/doc/rml_user_guide.prep').read() + print 'rawdata length %d' % len(rawdata) + print 'test one huge string...' + test3widths([rawdata]) + print + words = string.split(rawdata) + print 'test %d shorter strings (average length %0.2f chars)...' % (len(words), 1.0*len(rawdata)/len(words)) + test3widths(words) + + +def test(): + helv = TypeFace('Helvetica') + registerTypeFace(helv) + print helv.glyphNames[0:30] + + wombat = TypeFace('Wombat') + print wombat.glyphNames + registerTypeFace(wombat) + + dumpFontData() + +if __name__=='__main__': + test() + testStringWidthAlgorithms() diff --git a/bin/reportlab/pdfbase/pdfpattern.py b/bin/reportlab/pdfbase/pdfpattern.py new file mode 100644 index 00000000000..0ba62c9bed6 --- /dev/null +++ b/bin/reportlab/pdfbase/pdfpattern.py @@ -0,0 +1,59 @@ +""" +helper for importing pdf structures into a ReportLab generated document +""" +from reportlab.pdfbase.pdfdoc import format + +import string + +class PDFPattern: + __RefOnly__ = 1 + def __init__(self, pattern_sequence, **keywordargs): + """ + Description of a kind of PDF object using a pattern. + + Pattern sequence should contain strings or singletons of form [string]. + Strings are literal strings to be used in the object. + Singletons are names of keyword arguments to include. + Keyword arguments can be non-instances which are substituted directly in string conversion, + or they can be object instances in which case they should be pdfdoc.* style + objects with a x.format(doc) method. + Keyword arguments may be set on initialization or subsequently using __setitem__, before format. + "constant object" instances can also be inserted in the patterns. + """ + self.pattern = pattern_sequence + self.arguments = keywordargs + from types import StringType, InstanceType + toptypes = (StringType, InstanceType) + for x in pattern_sequence: + if type(x) not in toptypes: + if len(x)!=1: + raise ValueError, "sequence elts must be strings or singletons containing strings: "+repr(x) + if type(x[0]) is not StringType: + raise ValueError, "Singletons must contain strings or instances only: "+repr(x[0]) + def __setitem__(self, item, value): + self.arguments[item] = value + def __getitem__(self, item): + return self.arguments[item] + def format(self, document): + L = [] + arguments = self.arguments + from types import StringType, InstanceType + for x in self.pattern: + tx = type(x) + if tx is StringType: + L.append(x) + elif tx is InstanceType: + L.append( x.format(document) ) + else: + name = x[0] + value = arguments.get(name, None) + if value is None: + raise ValueError, "%s value not defined" % repr(name) + if type(value) is InstanceType: + #L.append( value.format(document) ) + L.append(format(value, document)) + else: + L.append( str(value) ) + return string.join(L, "") + + diff --git a/bin/reportlab/pdfbase/pdfutils.py b/bin/reportlab/pdfbase/pdfutils.py new file mode 100755 index 00000000000..8a13c894d76 --- /dev/null +++ b/bin/reportlab/pdfbase/pdfutils.py @@ -0,0 +1,460 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/pdfutils.py +__version__=''' $Id: pdfutils.py 2765 2006-02-02 18:48:12Z rgbecker $ ''' +__doc__='' +# pdfutils.py - everything to do with images, streams, +# compression, and some constants + +import os +from reportlab import rl_config +from string import join, replace, strip, split +from reportlab.lib.utils import getStringIO, ImageReader + +LINEEND = '\015\012' + +def _chunker(src,dst=[],chunkSize=60): + for i in xrange(0,len(src),chunkSize): + dst.append(src[i:i+chunkSize]) + return dst + +########################################################## +# +# Image compression helpers. Preprocessing a directory +# of images will offer a vast speedup. +# +########################################################## + +_mode2cs = {'RGB':'RGB', 'CMYK': 'CMYK', 'L': 'G'} + +def makeA85Image(filename,IMG=None): + import zlib + img = ImageReader(filename) + if IMG is not None: IMG.append(img) + + imgwidth, imgheight = img.getSize() + raw = img.getRGBData() + + code = [] + append = code.append + # this describes what is in the image itself + append('BI') + append('/W %s /H %s /BPC 8 /CS /%s /F [/A85 /Fl]' % (imgwidth, imgheight,_mode2cs[img.mode])) + append('ID') + #use a flate filter and Ascii Base 85 + assert(len(raw) == imgwidth * imgheight, "Wrong amount of data for image") + compressed = zlib.compress(raw) #this bit is very fast... + encoded = _AsciiBase85Encode(compressed) #...sadly this may not be + + #append in blocks of 60 characters + _chunker(encoded,code) + + append('EI') + return code + +def cacheImageFile(filename, returnInMemory=0, IMG=None): + "Processes image as if for encoding, saves to a file with .a85 extension." + + cachedname = os.path.splitext(filename)[0] + '.a85' + if filename==cachedname: + if cachedImageExists(filename): + from reportlab.lib.utils import open_for_read + if returnInMemory: return split(open_for_read(cachedname).read(),LINEEND)[:-1] + else: + raise IOError, 'No such cached image %s' % filename + else: + code = makeA85Image(filename,IMG) + if returnInMemory: return code + + #save it to a file + f = open(cachedname,'wb') + f.write(join(code, LINEEND)+LINEEND) + f.close() + if rl_config.verbose: + print 'cached image as %s' % cachedname + + +def preProcessImages(spec): + """Preprocesses one or more image files. + + Accepts either a filespec ('C:\mydir\*.jpg') or a list + of image filenames, crunches them all to save time. Run this + to save huge amounts of time when repeatedly building image + documents.""" + + import types, glob + + if type(spec) is types.StringType: + filelist = glob.glob(spec) + else: #list or tuple OK + filelist = spec + + for filename in filelist: + if cachedImageExists(filename): + if rl_config.verbose: + print 'cached version of %s already exists' % filename + else: + cacheImageFile(filename) + + +def cachedImageExists(filename): + """Determines if a cached image already exists for a given file. + + Determines if a cached image exists which has the same name + and equal or newer date to the given file.""" + cachedname = os.path.splitext(filename)[0] + '.a85' + if os.path.isfile(cachedname): + #see if it is newer + original_date = os.stat(filename)[8] + cached_date = os.stat(cachedname)[8] + if original_date > cached_date: + return 0 + else: + return 1 + else: + return 0 + + +############################################################## +# +# PDF Helper functions +# +############################################################## + +try: + from _rl_accel import escapePDF, _instanceEscapePDF + _escape = escapePDF +except ImportError: + try: + from reportlab.lib._rl_accel import escapePDF, _instanceEscapePDF + _escape = escapePDF + except ImportError: + _instanceEscapePDF=None + if rl_config.sys_version>='2.1': + _ESCAPEDICT={} + for c in range(0,256): + if c<32 or c>=127: + _ESCAPEDICT[chr(c)]= '\\%03o' % c + elif c in (ord('\\'),ord('('),ord(')')): + _ESCAPEDICT[chr(c)] = '\\'+chr(c) + else: + _ESCAPEDICT[chr(c)] = chr(c) + del c + #Michael Hudson donated this + def _escape(s): + return join(map(lambda c, d=_ESCAPEDICT: d[c],s),'') + else: + def _escape(s): + """Escapes some PDF symbols (in fact, parenthesis). + PDF escapes are almost like Python ones, but brackets + need slashes before them too. Uses Python's repr function + and chops off the quotes first.""" + s = repr(s)[1:-1] + s = replace(s, '(','\(') + s = replace(s, ')','\)') + return s + +def _normalizeLineEnds(text,desired=LINEEND): + """Normalizes different line end character(s). + + Ensures all instances of CR, LF and CRLF end up as + the specified one.""" + unlikely = '\000\001\002\003' + text = replace(text, '\015\012', unlikely) + text = replace(text, '\015', unlikely) + text = replace(text, '\012', unlikely) + text = replace(text, unlikely, desired) + return text + + +def _AsciiHexEncode(input): + """Encodes input using ASCII-Hex coding. + + This is a verbose encoding used for binary data within + a PDF file. One byte binary becomes two bytes of ASCII. + Helper function used by images.""" + output = getStringIO() + for char in input: + output.write('%02x' % ord(char)) + output.write('>') + return output.getvalue() + + +def _AsciiHexDecode(input): + """Decodes input using ASCII-Hex coding. + + Not used except to provide a test of the inverse function.""" + + #strip out all whitespace + stripped = join(split(input),'') + assert stripped[-1] == '>', 'Invalid terminator for Ascii Hex Stream' + stripped = stripped[:-1] #chop off terminator + assert len(stripped) % 2 == 0, 'Ascii Hex stream has odd number of bytes' + + i = 0 + output = getStringIO() + while i < len(stripped): + twobytes = stripped[i:i+2] + output.write(chr(eval('0x'+twobytes))) + i = i + 2 + return output.getvalue() + + +if 1: # for testing always define this + def _AsciiBase85EncodePYTHON(input): + """Encodes input using ASCII-Base85 coding. + + This is a compact encoding used for binary data within + a PDF file. Four bytes of binary data become five bytes of + ASCII. This is the default method used for encoding images.""" + outstream = getStringIO() + # special rules apply if not a multiple of four bytes. + whole_word_count, remainder_size = divmod(len(input), 4) + cut = 4 * whole_word_count + body, lastbit = input[0:cut], input[cut:] + + for i in range(whole_word_count): + offset = i*4 + b1 = ord(body[offset]) + b2 = ord(body[offset+1]) + b3 = ord(body[offset+2]) + b4 = ord(body[offset+3]) + + if b1<128: + num = (((((b1<<8)|b2)<<8)|b3)<<8)|b4 + else: + num = 16777216L * b1 + 65536 * b2 + 256 * b3 + b4 + + if num == 0: + #special case + outstream.write('z') + else: + #solve for five base-85 numbers + temp, c5 = divmod(num, 85) + temp, c4 = divmod(temp, 85) + temp, c3 = divmod(temp, 85) + c1, c2 = divmod(temp, 85) + assert ((85**4) * c1) + ((85**3) * c2) + ((85**2) * c3) + (85*c4) + c5 == num, 'dodgy code!' + outstream.write(chr(c1+33)) + outstream.write(chr(c2+33)) + outstream.write(chr(c3+33)) + outstream.write(chr(c4+33)) + outstream.write(chr(c5+33)) + + # now we do the final bit at the end. I repeated this separately as + # the loop above is the time-critical part of a script, whereas this + # happens only once at the end. + + #encode however many bytes we have as usual + if remainder_size > 0: + while len(lastbit) < 4: + lastbit = lastbit + '\000' + b1 = ord(lastbit[0]) + b2 = ord(lastbit[1]) + b3 = ord(lastbit[2]) + b4 = ord(lastbit[3]) + + num = 16777216L * b1 + 65536 * b2 + 256 * b3 + b4 + + #solve for c1..c5 + temp, c5 = divmod(num, 85) + temp, c4 = divmod(temp, 85) + temp, c3 = divmod(temp, 85) + c1, c2 = divmod(temp, 85) + + #print 'encoding: %d %d %d %d -> %d -> %d %d %d %d %d' % ( + # b1,b2,b3,b4,num,c1,c2,c3,c4,c5) + lastword = chr(c1+33) + chr(c2+33) + chr(c3+33) + chr(c4+33) + chr(c5+33) + #write out most of the bytes. + outstream.write(lastword[0:remainder_size + 1]) + + #terminator code for ascii 85 + outstream.write('~>') + return outstream.getvalue() + + def _AsciiBase85DecodePYTHON(input): + """Decodes input using ASCII-Base85 coding. + + This is not used - Acrobat Reader decodes for you + - but a round trip is essential for testing.""" + outstream = getStringIO() + #strip all whitespace + stripped = join(split(input),'') + #check end + assert stripped[-2:] == '~>', 'Invalid terminator for Ascii Base 85 Stream' + stripped = stripped[:-2] #chop off terminator + + #may have 'z' in it which complicates matters - expand them + stripped = replace(stripped,'z','!!!!!') + # special rules apply if not a multiple of five bytes. + whole_word_count, remainder_size = divmod(len(stripped), 5) + #print '%d words, %d leftover' % (whole_word_count, remainder_size) + #assert remainder_size <> 1, 'invalid Ascii 85 stream!' + cut = 5 * whole_word_count + body, lastbit = stripped[0:cut], stripped[cut:] + + for i in range(whole_word_count): + offset = i*5 + c1 = ord(body[offset]) - 33 + c2 = ord(body[offset+1]) - 33 + c3 = ord(body[offset+2]) - 33 + c4 = ord(body[offset+3]) - 33 + c5 = ord(body[offset+4]) - 33 + + num = ((85L**4) * c1) + ((85**3) * c2) + ((85**2) * c3) + (85*c4) + c5 + + temp, b4 = divmod(num,256) + temp, b3 = divmod(temp,256) + b1, b2 = divmod(temp, 256) + + assert num == 16777216 * b1 + 65536 * b2 + 256 * b3 + b4, 'dodgy code!' + outstream.write(chr(b1)) + outstream.write(chr(b2)) + outstream.write(chr(b3)) + outstream.write(chr(b4)) + + #decode however many bytes we have as usual + if remainder_size > 0: + while len(lastbit) < 5: + lastbit = lastbit + '!' + c1 = ord(lastbit[0]) - 33 + c2 = ord(lastbit[1]) - 33 + c3 = ord(lastbit[2]) - 33 + c4 = ord(lastbit[3]) - 33 + c5 = ord(lastbit[4]) - 33 + num = (((85*c1+c2)*85+c3)*85+c4)*85L + (c5 + +(0,0,0xFFFFFF,0xFFFF,0xFF)[remainder_size]) + temp, b4 = divmod(num,256) + temp, b3 = divmod(temp,256) + b1, b2 = divmod(temp, 256) + assert num == 16777216 * b1 + 65536 * b2 + 256 * b3 + b4, 'dodgy code!' + #print 'decoding: %d %d %d %d %d -> %d -> %d %d %d %d' % ( + # c1,c2,c3,c4,c5,num,b1,b2,b3,b4) + + #the last character needs 1 adding; the encoding loses + #data by rounding the number to x bytes, and when + #divided repeatedly we get one less + if remainder_size == 2: + lastword = chr(b1) + elif remainder_size == 3: + lastword = chr(b1) + chr(b2) + elif remainder_size == 4: + lastword = chr(b1) + chr(b2) + chr(b3) + else: + lastword = '' + outstream.write(lastword) + + #terminator code for ascii 85 + return outstream.getvalue() + +try: + from _rl_accel import _AsciiBase85Encode # builtin or on the path +except ImportError: + try: + from reportlab.lib._rl_accel import _AsciiBase85Encode # where we think it should be + except ImportError: + _AsciiBase85Encode = _AsciiBase85EncodePYTHON + +try: + from _rl_accel import _AsciiBase85Decode # builtin or on the path +except ImportError: + try: + from reportlab.lib._rl_accel import _AsciiBase85Decode # where we think it should be + except ImportError: + _AsciiBase85Decode = _AsciiBase85DecodePYTHON + +def _wrap(input, columns=60): + "Wraps input at a given column size by inserting LINEEND characters." + + output = [] + length = len(input) + i = 0 + pos = columns * i + while pos < length: + output.append(input[pos:pos+columns]) + i = i + 1 + pos = columns * i + + return join(output, LINEEND) + + +######################################################################### +# +# JPEG processing code - contributed by Eric Johnson +# +######################################################################### + +# Read data from the JPEG file. We should probably be using PIL to +# get this information for us -- but this way is more fun! +# Returns (width, height, color components) as a triple +# This is based on Thomas Merz's code from GhostScript (viewjpeg.ps) +def readJPEGInfo(image): + "Read width, height and number of components from open JPEG file." + + import struct + from pdfdoc import PDFError + + #Acceptable JPEG Markers: + # SROF0=baseline, SOF1=extended sequential or SOF2=progressive + validMarkers = [0xC0, 0xC1, 0xC2] + + #JPEG markers without additional parameters + noParamMarkers = \ + [ 0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0x01 ] + + #Unsupported JPEG Markers + unsupportedMarkers = \ + [ 0xC3, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF ] + + #read JPEG marker segments until we find SOFn marker or EOF + done = 0 + while not done: + x = struct.unpack('B', image.read(1)) + if x[0] == 0xFF: #found marker + x = struct.unpack('B', image.read(1)) + #print "Marker: ", '%0.2x' % x[0] + #check marker type is acceptable and process it + if x[0] in validMarkers: + image.seek(2, 1) #skip segment length + x = struct.unpack('B', image.read(1)) #data precision + if x[0] != 8: + raise PDFError('JPEG must have 8 bits per component') + y = struct.unpack('BB', image.read(2)) + height = (y[0] << 8) + y[1] + y = struct.unpack('BB', image.read(2)) + width = (y[0] << 8) + y[1] + y = struct.unpack('B', image.read(1)) + color = y[0] + return width, height, color + done = 1 + elif x[0] in unsupportedMarkers: + raise PDFError('JPEG Unsupported JPEG marker: %0.2x' % x[0]) + elif x[0] not in noParamMarkers: + #skip segments with parameters + #read length and skip the data + x = struct.unpack('BB', image.read(2)) + image.seek( (x[0] << 8) + x[1] - 2, 1) + +class _fusc: + def __init__(self,k, n): + assert k, 'Argument k should be a non empty string' + self._k = k + self._klen = len(k) + self._n = int(n) or 7 + + def encrypt(self,s): + return self.__rotate(_AsciiBase85Encode(''.join(map(chr,self.__fusc(map(ord,s))))),self._n) + + def decrypt(self,s): + return ''.join(map(chr,self.__fusc(map(ord,_AsciiBase85Decode(self.__rotate(s,-self._n)))))) + + def __rotate(self,s,n): + l = len(s) + if n<0: n = l+n + n %= l + if not n: return s + return s[-n:]+s[:l-n] + + def __fusc(self,s): + slen = len(s) + return map(lambda x,y: x ^ y,s,map(ord,((int(slen/self._klen)+1)*self._k)[:slen])) diff --git a/bin/reportlab/pdfbase/rl_codecs.py b/bin/reportlab/pdfbase/rl_codecs.py new file mode 100644 index 00000000000..326ed09fbd2 --- /dev/null +++ b/bin/reportlab/pdfbase/rl_codecs.py @@ -0,0 +1,1027 @@ +#codecs support +__all__=['RL_Codecs'] +class RL_Codecs: + __rl_codecs_data = { + 'winansi':({ + 0x007f: 0x2022, # BULLET + 0x0080: 0x20ac, # EURO SIGN + 0x0081: 0x2022, # BULLET + 0x0082: 0x201a, # SINGLE LOW-9 QUOTATION MARK + 0x0083: 0x0192, # LATIN SMALL LETTER F WITH HOOK + 0x0084: 0x201e, # DOUBLE LOW-9 QUOTATION MARK + 0x0085: 0x2026, # HORIZONTAL ELLIPSIS + 0x0086: 0x2020, # DAGGER + 0x0087: 0x2021, # DOUBLE DAGGER + 0x0088: 0x02c6, # MODIFIER LETTER CIRCUMFLEX ACCENT + 0x0089: 0x2030, # PER MILLE SIGN + 0x008a: 0x0160, # LATIN CAPITAL LETTER S WITH CARON + 0x008b: 0x2039, # SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 0x008c: 0x0152, # LATIN CAPITAL LIGATURE OE + 0x008d: 0x2022, # BULLET + 0x008e: 0x017d, # LATIN CAPITAL LETTER Z WITH CARON + 0x008f: 0x2022, # BULLET + 0x0090: 0x2022, # BULLET + 0x0091: 0x2018, # LEFT SINGLE QUOTATION MARK + 0x0092: 0x2019, # RIGHT SINGLE QUOTATION MARK + 0x0093: 0x201c, # LEFT DOUBLE QUOTATION MARK + 0x0094: 0x201d, # RIGHT DOUBLE QUOTATION MARK + 0x0095: 0x2022, # BULLET + 0x0096: 0x2013, # EN DASH + 0x0097: 0x2014, # EM DASH + 0x0098: 0x02dc, # SMALL TILDE + 0x0099: 0x2122, # TRADE MARK SIGN + 0x009a: 0x0161, # LATIN SMALL LETTER S WITH CARON + 0x009b: 0x203a, # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 0x009c: 0x0153, # LATIN SMALL LIGATURE OE + 0x009d: 0x2022, # BULLET + 0x009e: 0x017e, # LATIN SMALL LETTER Z WITH CARON + 0x009f: 0x0178, # LATIN CAPITAL LETTER Y WITH DIAERESIS + 0x00a0: 0x0020, # SPACE + 0x00ad: 0x002d, # HYPHEN-MINUS + }, {0x2022:0x7f,0x20:0x20,0x2d:0x2d,0xa0:0x20}), + 'macroman':({ + 0x007f: None, # UNDEFINED + 0x0080: 0x00c4, # LATIN CAPITAL LETTER A WITH DIAERESIS + 0x0081: 0x00c5, # LATIN CAPITAL LETTER A WITH RING ABOVE + 0x0082: 0x00c7, # LATIN CAPITAL LETTER C WITH CEDILLA + 0x0083: 0x00c9, # LATIN CAPITAL LETTER E WITH ACUTE + 0x0084: 0x00d1, # LATIN CAPITAL LETTER N WITH TILDE + 0x0085: 0x00d6, # LATIN CAPITAL LETTER O WITH DIAERESIS + 0x0086: 0x00dc, # LATIN CAPITAL LETTER U WITH DIAERESIS + 0x0087: 0x00e1, # LATIN SMALL LETTER A WITH ACUTE + 0x0088: 0x00e0, # LATIN SMALL LETTER A WITH GRAVE + 0x0089: 0x00e2, # LATIN SMALL LETTER A WITH CIRCUMFLEX + 0x008a: 0x00e4, # LATIN SMALL LETTER A WITH DIAERESIS + 0x008b: 0x00e3, # LATIN SMALL LETTER A WITH TILDE + 0x008c: 0x00e5, # LATIN SMALL LETTER A WITH RING ABOVE + 0x008d: 0x00e7, # LATIN SMALL LETTER C WITH CEDILLA + 0x008e: 0x00e9, # LATIN SMALL LETTER E WITH ACUTE + 0x008f: 0x00e8, # LATIN SMALL LETTER E WITH GRAVE + 0x0090: 0x00ea, # LATIN SMALL LETTER E WITH CIRCUMFLEX + 0x0091: 0x00eb, # LATIN SMALL LETTER E WITH DIAERESIS + 0x0092: 0x00ed, # LATIN SMALL LETTER I WITH ACUTE + 0x0093: 0x00ec, # LATIN SMALL LETTER I WITH GRAVE + 0x0094: 0x00ee, # LATIN SMALL LETTER I WITH CIRCUMFLEX + 0x0095: 0x00ef, # LATIN SMALL LETTER I WITH DIAERESIS + 0x0096: 0x00f1, # LATIN SMALL LETTER N WITH TILDE + 0x0097: 0x00f3, # LATIN SMALL LETTER O WITH ACUTE + 0x0098: 0x00f2, # LATIN SMALL LETTER O WITH GRAVE + 0x0099: 0x00f4, # LATIN SMALL LETTER O WITH CIRCUMFLEX + 0x009a: 0x00f6, # LATIN SMALL LETTER O WITH DIAERESIS + 0x009b: 0x00f5, # LATIN SMALL LETTER O WITH TILDE + 0x009c: 0x00fa, # LATIN SMALL LETTER U WITH ACUTE + 0x009d: 0x00f9, # LATIN SMALL LETTER U WITH GRAVE + 0x009e: 0x00fb, # LATIN SMALL LETTER U WITH CIRCUMFLEX + 0x009f: 0x00fc, # LATIN SMALL LETTER U WITH DIAERESIS + 0x00a0: 0x2020, # DAGGER + 0x00a1: 0x00b0, # DEGREE SIGN + 0x00a4: 0x00a7, # SECTION SIGN + 0x00a5: 0x2022, # BULLET + 0x00a6: 0x00b6, # PILCROW SIGN + 0x00a7: 0x00df, # LATIN SMALL LETTER SHARP S + 0x00a8: 0x00ae, # REGISTERED SIGN + 0x00aa: 0x2122, # TRADE MARK SIGN + 0x00ab: 0x00b4, # ACUTE ACCENT + 0x00ac: 0x00a8, # DIAERESIS + 0x00ad: None, # UNDEFINED + 0x00ae: 0x00c6, # LATIN CAPITAL LETTER AE + 0x00af: 0x00d8, # LATIN CAPITAL LETTER O WITH STROKE + 0x00b0: None, # UNDEFINED + 0x00b2: None, # UNDEFINED + 0x00b3: None, # UNDEFINED + 0x00b4: 0x00a5, # YEN SIGN + 0x00b6: None, # UNDEFINED + 0x00b7: None, # UNDEFINED + 0x00b8: None, # UNDEFINED + 0x00b9: None, # UNDEFINED + 0x00ba: None, # UNDEFINED + 0x00bb: 0x00aa, # FEMININE ORDINAL INDICATOR + 0x00bc: 0x00ba, # MASCULINE ORDINAL INDICATOR + 0x00bd: None, # UNDEFINED + 0x00be: 0x00e6, # LATIN SMALL LETTER AE + 0x00bf: 0x00f8, # LATIN SMALL LETTER O WITH STROKE + 0x00c0: 0x00bf, # INVERTED QUESTION MARK + 0x00c1: 0x00a1, # INVERTED EXCLAMATION MARK + 0x00c2: 0x00ac, # NOT SIGN + 0x00c3: None, # UNDEFINED + 0x00c4: 0x0192, # LATIN SMALL LETTER F WITH HOOK + 0x00c5: None, # UNDEFINED + 0x00c6: None, # UNDEFINED + 0x00c7: 0x00ab, # LEFT-POINTING DOUBLE ANGLE QUOTATION MARK + 0x00c8: 0x00bb, # RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK + 0x00c9: 0x2026, # HORIZONTAL ELLIPSIS + 0x00ca: 0x0020, # SPACE + 0x00cb: 0x00c0, # LATIN CAPITAL LETTER A WITH GRAVE + 0x00cc: 0x00c3, # LATIN CAPITAL LETTER A WITH TILDE + 0x00cd: 0x00d5, # LATIN CAPITAL LETTER O WITH TILDE + 0x00ce: 0x0152, # LATIN CAPITAL LIGATURE OE + 0x00cf: 0x0153, # LATIN SMALL LIGATURE OE + 0x00d0: 0x2013, # EN DASH + 0x00d1: 0x2014, # EM DASH + 0x00d2: 0x201c, # LEFT DOUBLE QUOTATION MARK + 0x00d3: 0x201d, # RIGHT DOUBLE QUOTATION MARK + 0x00d4: 0x2018, # LEFT SINGLE QUOTATION MARK + 0x00d5: 0x2019, # RIGHT SINGLE QUOTATION MARK + 0x00d6: 0x00f7, # DIVISION SIGN + 0x00d7: None, # UNDEFINED + 0x00d8: 0x00ff, # LATIN SMALL LETTER Y WITH DIAERESIS + 0x00d9: 0x0178, # LATIN CAPITAL LETTER Y WITH DIAERESIS + 0x00da: 0x2044, # FRACTION SLASH + 0x00db: 0x00a4, # CURRENCY SIGN + 0x00dc: 0x2039, # SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 0x00dd: 0x203a, # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 0x00de: 0xfb01, # LATIN SMALL LIGATURE FI + 0x00df: 0xfb02, # LATIN SMALL LIGATURE FL + 0x00e0: 0x2021, # DOUBLE DAGGER + 0x00e1: 0x00b7, # MIDDLE DOT + 0x00e2: 0x201a, # SINGLE LOW-9 QUOTATION MARK + 0x00e3: 0x201e, # DOUBLE LOW-9 QUOTATION MARK + 0x00e4: 0x2030, # PER MILLE SIGN + 0x00e5: 0x00c2, # LATIN CAPITAL LETTER A WITH CIRCUMFLEX + 0x00e6: 0x00ca, # LATIN CAPITAL LETTER E WITH CIRCUMFLEX + 0x00e7: 0x00c1, # LATIN CAPITAL LETTER A WITH ACUTE + 0x00e8: 0x00cb, # LATIN CAPITAL LETTER E WITH DIAERESIS + 0x00e9: 0x00c8, # LATIN CAPITAL LETTER E WITH GRAVE + 0x00ea: 0x00cd, # LATIN CAPITAL LETTER I WITH ACUTE + 0x00eb: 0x00ce, # LATIN CAPITAL LETTER I WITH CIRCUMFLEX + 0x00ec: 0x00cf, # LATIN CAPITAL LETTER I WITH DIAERESIS + 0x00ed: 0x00cc, # LATIN CAPITAL LETTER I WITH GRAVE + 0x00ee: 0x00d3, # LATIN CAPITAL LETTER O WITH ACUTE + 0x00ef: 0x00d4, # LATIN CAPITAL LETTER O WITH CIRCUMFLEX + 0x00f0: None, # UNDEFINED + 0x00f1: 0x00d2, # LATIN CAPITAL LETTER O WITH GRAVE + 0x00f2: 0x00da, # LATIN CAPITAL LETTER U WITH ACUTE + 0x00f3: 0x00db, # LATIN CAPITAL LETTER U WITH CIRCUMFLEX + 0x00f4: 0x00d9, # LATIN CAPITAL LETTER U WITH GRAVE + 0x00f5: 0x0131, # LATIN SMALL LETTER DOTLESS I + 0x00f6: 0x02c6, # MODIFIER LETTER CIRCUMFLEX ACCENT + 0x00f7: 0x02dc, # SMALL TILDE + 0x00f8: 0x00af, # MACRON + 0x00f9: 0x02d8, # BREVE + 0x00fa: 0x02d9, # DOT ABOVE + 0x00fb: 0x02da, # RING ABOVE + 0x00fc: 0x00b8, # CEDILLA + 0x00fd: 0x02dd, # DOUBLE ACUTE ACCENT + 0x00fe: 0x02db, # OGONEK + 0x00ff: 0x02c7, # CARON + },None), + 'standard':({ + 0x0027: 0x2019, # RIGHT SINGLE QUOTATION MARK + 0x0060: 0x2018, # LEFT SINGLE QUOTATION MARK + 0x007f: None, # UNDEFINED + 0x0080: None, # UNDEFINED + 0x0081: None, # UNDEFINED + 0x0082: None, # UNDEFINED + 0x0083: None, # UNDEFINED + 0x0084: None, # UNDEFINED + 0x0085: None, # UNDEFINED + 0x0086: None, # UNDEFINED + 0x0087: None, # UNDEFINED + 0x0088: None, # UNDEFINED + 0x0089: None, # UNDEFINED + 0x008a: None, # UNDEFINED + 0x008b: None, # UNDEFINED + 0x008c: None, # UNDEFINED + 0x008d: None, # UNDEFINED + 0x008e: None, # UNDEFINED + 0x008f: None, # UNDEFINED + 0x0090: None, # UNDEFINED + 0x0091: None, # UNDEFINED + 0x0092: None, # UNDEFINED + 0x0093: None, # UNDEFINED + 0x0094: None, # UNDEFINED + 0x0095: None, # UNDEFINED + 0x0096: None, # UNDEFINED + 0x0097: None, # UNDEFINED + 0x0098: None, # UNDEFINED + 0x0099: None, # UNDEFINED + 0x009a: None, # UNDEFINED + 0x009b: None, # UNDEFINED + 0x009c: None, # UNDEFINED + 0x009d: None, # UNDEFINED + 0x009e: None, # UNDEFINED + 0x009f: None, # UNDEFINED + 0x00a0: None, # UNDEFINED + 0x00a4: 0x2044, # FRACTION SLASH + 0x00a6: 0x0192, # LATIN SMALL LETTER F WITH HOOK + 0x00a8: 0x00a4, # CURRENCY SIGN + 0x00a9: 0x0027, # APOSTROPHE + 0x00aa: 0x201c, # LEFT DOUBLE QUOTATION MARK + 0x00ac: 0x2039, # SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 0x00ad: 0x203a, # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 0x00ae: 0xfb01, # LATIN SMALL LIGATURE FI + 0x00af: 0xfb02, # LATIN SMALL LIGATURE FL + 0x00b0: None, # UNDEFINED + 0x00b1: 0x2013, # EN DASH + 0x00b2: 0x2020, # DAGGER + 0x00b3: 0x2021, # DOUBLE DAGGER + 0x00b4: 0x00b7, # MIDDLE DOT + 0x00b5: None, # UNDEFINED + 0x00b7: 0x2022, # BULLET + 0x00b8: 0x201a, # SINGLE LOW-9 QUOTATION MARK + 0x00b9: 0x201e, # DOUBLE LOW-9 QUOTATION MARK + 0x00ba: 0x201d, # RIGHT DOUBLE QUOTATION MARK + 0x00bc: 0x2026, # HORIZONTAL ELLIPSIS + 0x00bd: 0x2030, # PER MILLE SIGN + 0x00be: None, # UNDEFINED + 0x00c0: None, # UNDEFINED + 0x00c1: 0x0060, # GRAVE ACCENT + 0x00c2: 0x00b4, # ACUTE ACCENT + 0x00c3: 0x02c6, # MODIFIER LETTER CIRCUMFLEX ACCENT + 0x00c4: 0x02dc, # SMALL TILDE + 0x00c5: 0x00af, # MACRON + 0x00c6: 0x02d8, # BREVE + 0x00c7: 0x02d9, # DOT ABOVE + 0x00c8: 0x00a8, # DIAERESIS + 0x00c9: None, # UNDEFINED + 0x00ca: 0x02da, # RING ABOVE + 0x00cb: 0x00b8, # CEDILLA + 0x00cc: None, # UNDEFINED + 0x00cd: 0x02dd, # DOUBLE ACUTE ACCENT + 0x00ce: 0x02db, # OGONEK + 0x00cf: 0x02c7, # CARON + 0x00d0: 0x2014, # EM DASH + 0x00d1: None, # UNDEFINED + 0x00d2: None, # UNDEFINED + 0x00d3: None, # UNDEFINED + 0x00d4: None, # UNDEFINED + 0x00d5: None, # UNDEFINED + 0x00d6: None, # UNDEFINED + 0x00d7: None, # UNDEFINED + 0x00d8: None, # UNDEFINED + 0x00d9: None, # UNDEFINED + 0x00da: None, # UNDEFINED + 0x00db: None, # UNDEFINED + 0x00dc: None, # UNDEFINED + 0x00dd: None, # UNDEFINED + 0x00de: None, # UNDEFINED + 0x00df: None, # UNDEFINED + 0x00e0: None, # UNDEFINED + 0x00e1: 0x00c6, # LATIN CAPITAL LETTER AE + 0x00e2: None, # UNDEFINED + 0x00e3: 0x00aa, # FEMININE ORDINAL INDICATOR + 0x00e4: None, # UNDEFINED + 0x00e5: None, # UNDEFINED + 0x00e6: None, # UNDEFINED + 0x00e7: None, # UNDEFINED + 0x00e8: 0x0141, # LATIN CAPITAL LETTER L WITH STROKE + 0x00e9: 0x00d8, # LATIN CAPITAL LETTER O WITH STROKE + 0x00ea: 0x0152, # LATIN CAPITAL LIGATURE OE + 0x00eb: 0x00ba, # MASCULINE ORDINAL INDICATOR + 0x00ec: None, # UNDEFINED + 0x00ed: None, # UNDEFINED + 0x00ee: None, # UNDEFINED + 0x00ef: None, # UNDEFINED + 0x00f0: None, # UNDEFINED + 0x00f1: 0x00e6, # LATIN SMALL LETTER AE + 0x00f2: None, # UNDEFINED + 0x00f3: None, # UNDEFINED + 0x00f4: None, # UNDEFINED + 0x00f5: 0x0131, # LATIN SMALL LETTER DOTLESS I + 0x00f6: None, # UNDEFINED + 0x00f7: None, # UNDEFINED + 0x00f8: 0x0142, # LATIN SMALL LETTER L WITH STROKE + 0x00f9: 0x00f8, # LATIN SMALL LETTER O WITH STROKE + 0x00fa: 0x0153, # LATIN SMALL LIGATURE OE + 0x00fb: 0x00df, # LATIN SMALL LETTER SHARP S + 0x00fc: None, # UNDEFINED + 0x00fd: None, # UNDEFINED + 0x00fe: None, # UNDEFINED + 0x00ff: None, # UNDEFINED + },None), + 'symbol':({ + 0x0022: 0x2200, # FOR ALL + 0x0024: 0x2203, # THERE EXISTS + 0x0027: 0x220b, # CONTAINS AS MEMBER + 0x002a: 0x2217, # ASTERISK OPERATOR + 0x002d: 0x2212, # MINUS SIGN + 0x0040: 0x2245, # APPROXIMATELY EQUAL TO + 0x0041: 0x0391, # GREEK CAPITAL LETTER ALPHA + 0x0042: 0x0392, # GREEK CAPITAL LETTER BETA + 0x0043: 0x03a7, # GREEK CAPITAL LETTER CHI + 0x0044: 0x2206, # INCREMENT + 0x0045: 0x0395, # GREEK CAPITAL LETTER EPSILON + 0x0046: 0x03a6, # GREEK CAPITAL LETTER PHI + 0x0047: 0x0393, # GREEK CAPITAL LETTER GAMMA + 0x0048: 0x0397, # GREEK CAPITAL LETTER ETA + 0x0049: 0x0399, # GREEK CAPITAL LETTER IOTA + 0x004a: 0x03d1, # GREEK THETA SYMBOL + 0x004b: 0x039a, # GREEK CAPITAL LETTER KAPPA + 0x004c: 0x039b, # GREEK CAPITAL LETTER LAMDA + 0x004d: 0x039c, # GREEK CAPITAL LETTER MU + 0x004e: 0x039d, # GREEK CAPITAL LETTER NU + 0x004f: 0x039f, # GREEK CAPITAL LETTER OMICRON + 0x0050: 0x03a0, # GREEK CAPITAL LETTER PI + 0x0051: 0x0398, # GREEK CAPITAL LETTER THETA + 0x0052: 0x03a1, # GREEK CAPITAL LETTER RHO + 0x0053: 0x03a3, # GREEK CAPITAL LETTER SIGMA + 0x0054: 0x03a4, # GREEK CAPITAL LETTER TAU + 0x0055: 0x03a5, # GREEK CAPITAL LETTER UPSILON + 0x0056: 0x03c2, # GREEK SMALL LETTER FINAL SIGMA + 0x0057: 0x2126, # OHM SIGN + 0x0058: 0x039e, # GREEK CAPITAL LETTER XI + 0x0059: 0x03a8, # GREEK CAPITAL LETTER PSI + 0x005a: 0x0396, # GREEK CAPITAL LETTER ZETA + 0x005c: 0x2234, # THEREFORE + 0x005e: 0x22a5, # UP TACK + 0x0060: 0xf8e5, # [unknown unicode name for radicalex] + 0x0061: 0x03b1, # GREEK SMALL LETTER ALPHA + 0x0062: 0x03b2, # GREEK SMALL LETTER BETA + 0x0063: 0x03c7, # GREEK SMALL LETTER CHI + 0x0064: 0x03b4, # GREEK SMALL LETTER DELTA + 0x0065: 0x03b5, # GREEK SMALL LETTER EPSILON + 0x0066: 0x03c6, # GREEK SMALL LETTER PHI + 0x0067: 0x03b3, # GREEK SMALL LETTER GAMMA + 0x0068: 0x03b7, # GREEK SMALL LETTER ETA + 0x0069: 0x03b9, # GREEK SMALL LETTER IOTA + 0x006a: 0x03d5, # GREEK PHI SYMBOL + 0x006b: 0x03ba, # GREEK SMALL LETTER KAPPA + 0x006c: 0x03bb, # GREEK SMALL LETTER LAMDA + 0x006d: 0x00b5, # MICRO SIGN + 0x006e: 0x03bd, # GREEK SMALL LETTER NU + 0x006f: 0x03bf, # GREEK SMALL LETTER OMICRON + 0x0070: 0x03c0, # GREEK SMALL LETTER PI + 0x0071: 0x03b8, # GREEK SMALL LETTER THETA + 0x0072: 0x03c1, # GREEK SMALL LETTER RHO + 0x0073: 0x03c3, # GREEK SMALL LETTER SIGMA + 0x0074: 0x03c4, # GREEK SMALL LETTER TAU + 0x0075: 0x03c5, # GREEK SMALL LETTER UPSILON + 0x0076: 0x03d6, # GREEK PI SYMBOL + 0x0077: 0x03c9, # GREEK SMALL LETTER OMEGA + 0x0078: 0x03be, # GREEK SMALL LETTER XI + 0x0079: 0x03c8, # GREEK SMALL LETTER PSI + 0x007a: 0x03b6, # GREEK SMALL LETTER ZETA + 0x007e: 0x223c, # TILDE OPERATOR + 0x007f: None, # UNDEFINED + 0x0080: None, # UNDEFINED + 0x0081: None, # UNDEFINED + 0x0082: None, # UNDEFINED + 0x0083: None, # UNDEFINED + 0x0084: None, # UNDEFINED + 0x0085: None, # UNDEFINED + 0x0086: None, # UNDEFINED + 0x0087: None, # UNDEFINED + 0x0088: None, # UNDEFINED + 0x0089: None, # UNDEFINED + 0x008a: None, # UNDEFINED + 0x008b: None, # UNDEFINED + 0x008c: None, # UNDEFINED + 0x008d: None, # UNDEFINED + 0x008e: None, # UNDEFINED + 0x008f: None, # UNDEFINED + 0x0090: None, # UNDEFINED + 0x0091: None, # UNDEFINED + 0x0092: None, # UNDEFINED + 0x0093: None, # UNDEFINED + 0x0094: None, # UNDEFINED + 0x0095: None, # UNDEFINED + 0x0096: None, # UNDEFINED + 0x0097: None, # UNDEFINED + 0x0098: None, # UNDEFINED + 0x0099: None, # UNDEFINED + 0x009a: None, # UNDEFINED + 0x009b: None, # UNDEFINED + 0x009c: None, # UNDEFINED + 0x009d: None, # UNDEFINED + 0x009e: None, # UNDEFINED + 0x009f: None, # UNDEFINED + 0x00a0: 0x20ac, # EURO SIGN + 0x00a1: 0x03d2, # GREEK UPSILON WITH HOOK SYMBOL + 0x00a2: 0x2032, # PRIME + 0x00a3: 0x2264, # LESS-THAN OR EQUAL TO + 0x00a4: 0x2044, # FRACTION SLASH + 0x00a5: 0x221e, # INFINITY + 0x00a6: 0x0192, # LATIN SMALL LETTER F WITH HOOK + 0x00a7: 0x2663, # BLACK CLUB SUIT + 0x00a8: 0x2666, # BLACK DIAMOND SUIT + 0x00a9: 0x2665, # BLACK HEART SUIT + 0x00aa: 0x2660, # BLACK SPADE SUIT + 0x00ab: 0x2194, # LEFT RIGHT ARROW + 0x00ac: 0x2190, # LEFTWARDS ARROW + 0x00ad: 0x2191, # UPWARDS ARROW + 0x00ae: 0x2192, # RIGHTWARDS ARROW + 0x00af: 0x2193, # DOWNWARDS ARROW + 0x00b2: 0x2033, # DOUBLE PRIME + 0x00b3: 0x2265, # GREATER-THAN OR EQUAL TO + 0x00b4: 0x00d7, # MULTIPLICATION SIGN + 0x00b5: 0x221d, # PROPORTIONAL TO + 0x00b6: 0x2202, # PARTIAL DIFFERENTIAL + 0x00b7: 0x2022, # BULLET + 0x00b8: 0x00f7, # DIVISION SIGN + 0x00b9: 0x2260, # NOT EQUAL TO + 0x00ba: 0x2261, # IDENTICAL TO + 0x00bb: 0x2248, # ALMOST EQUAL TO + 0x00bc: 0x2026, # HORIZONTAL ELLIPSIS + 0x00bd: 0xf8e6, # [unknown unicode name for arrowvertex] + 0x00be: 0xf8e7, # [unknown unicode name for arrowhorizex] + 0x00bf: 0x21b5, # DOWNWARDS ARROW WITH CORNER LEFTWARDS + 0x00c0: 0x2135, # ALEF SYMBOL + 0x00c1: 0x2111, # BLACK-LETTER CAPITAL I + 0x00c2: 0x211c, # BLACK-LETTER CAPITAL R + 0x00c3: 0x2118, # SCRIPT CAPITAL P + 0x00c4: 0x2297, # CIRCLED TIMES + 0x00c5: 0x2295, # CIRCLED PLUS + 0x00c6: 0x2205, # EMPTY SET + 0x00c7: 0x2229, # INTERSECTION + 0x00c8: 0x222a, # UNION + 0x00c9: 0x2283, # SUPERSET OF + 0x00ca: 0x2287, # SUPERSET OF OR EQUAL TO + 0x00cb: 0x2284, # NOT A SUBSET OF + 0x00cc: 0x2282, # SUBSET OF + 0x00cd: 0x2286, # SUBSET OF OR EQUAL TO + 0x00ce: 0x2208, # ELEMENT OF + 0x00cf: 0x2209, # NOT AN ELEMENT OF + 0x00d0: 0x2220, # ANGLE + 0x00d1: 0x2207, # NABLA + 0x00d2: 0xf6da, # [unknown unicode name for registerserif] + 0x00d3: 0xf6d9, # [unknown unicode name for copyrightserif] + 0x00d4: 0xf6db, # [unknown unicode name for trademarkserif] + 0x00d5: 0x220f, # N-ARY PRODUCT + 0x00d6: 0x221a, # SQUARE ROOT + 0x00d7: 0x22c5, # DOT OPERATOR + 0x00d8: 0x00ac, # NOT SIGN + 0x00d9: 0x2227, # LOGICAL AND + 0x00da: 0x2228, # LOGICAL OR + 0x00db: 0x21d4, # LEFT RIGHT DOUBLE ARROW + 0x00dc: 0x21d0, # LEFTWARDS DOUBLE ARROW + 0x00dd: 0x21d1, # UPWARDS DOUBLE ARROW + 0x00de: 0x21d2, # RIGHTWARDS DOUBLE ARROW + 0x00df: 0x21d3, # DOWNWARDS DOUBLE ARROW + 0x00e0: 0x25ca, # LOZENGE + 0x00e1: 0x2329, # LEFT-POINTING ANGLE BRACKET + 0x00e2: 0xf8e8, # [unknown unicode name for registersans] + 0x00e3: 0xf8e9, # [unknown unicode name for copyrightsans] + 0x00e4: 0xf8ea, # [unknown unicode name for trademarksans] + 0x00e5: 0x2211, # N-ARY SUMMATION + 0x00e6: 0xf8eb, # [unknown unicode name for parenlefttp] + 0x00e7: 0xf8ec, # [unknown unicode name for parenleftex] + 0x00e8: 0xf8ed, # [unknown unicode name for parenleftbt] + 0x00e9: 0xf8ee, # [unknown unicode name for bracketlefttp] + 0x00ea: 0xf8ef, # [unknown unicode name for bracketleftex] + 0x00eb: 0xf8f0, # [unknown unicode name for bracketleftbt] + 0x00ec: 0xf8f1, # [unknown unicode name for bracelefttp] + 0x00ed: 0xf8f2, # [unknown unicode name for braceleftmid] + 0x00ee: 0xf8f3, # [unknown unicode name for braceleftbt] + 0x00ef: 0xf8f4, # [unknown unicode name for braceex] + 0x00f0: None, # UNDEFINED + 0x00f1: 0x232a, # RIGHT-POINTING ANGLE BRACKET + 0x00f2: 0x222b, # INTEGRAL + 0x00f3: 0x2320, # TOP HALF INTEGRAL + 0x00f4: 0xf8f5, # [unknown unicode name for integralex] + 0x00f5: 0x2321, # BOTTOM HALF INTEGRAL + 0x00f6: 0xf8f6, # [unknown unicode name for parenrighttp] + 0x00f7: 0xf8f7, # [unknown unicode name for parenrightex] + 0x00f8: 0xf8f8, # [unknown unicode name for parenrightbt] + 0x00f9: 0xf8f9, # [unknown unicode name for bracketrighttp] + 0x00fa: 0xf8fa, # [unknown unicode name for bracketrightex] + 0x00fb: 0xf8fb, # [unknown unicode name for bracketrightbt] + 0x00fc: 0xf8fc, # [unknown unicode name for bracerighttp] + 0x00fd: 0xf8fd, # [unknown unicode name for bracerightmid] + 0x00fe: 0xf8fe, # [unknown unicode name for bracerightbt] + 0x00ff: None, # UNDEFINED + },None), + 'zapfdingbats':({ + 0x0021: 0x2701, # UPPER BLADE SCISSORS + 0x0022: 0x2702, # BLACK SCISSORS + 0x0023: 0x2703, # LOWER BLADE SCISSORS + 0x0024: 0x2704, # WHITE SCISSORS + 0x0025: 0x260e, # BLACK TELEPHONE + 0x0026: 0x2706, # TELEPHONE LOCATION SIGN + 0x0027: 0x2707, # TAPE DRIVE + 0x0028: 0x2708, # AIRPLANE + 0x0029: 0x2709, # ENVELOPE + 0x002a: 0x261b, # BLACK RIGHT POINTING INDEX + 0x002b: 0x261e, # WHITE RIGHT POINTING INDEX + 0x002c: 0x270c, # VICTORY HAND + 0x002d: 0x270d, # WRITING HAND + 0x002e: 0x270e, # LOWER RIGHT PENCIL + 0x002f: 0x270f, # PENCIL + 0x0030: 0x2710, # UPPER RIGHT PENCIL + 0x0031: 0x2711, # WHITE NIB + 0x0032: 0x2712, # BLACK NIB + 0x0033: 0x2713, # CHECK MARK + 0x0034: 0x2714, # HEAVY CHECK MARK + 0x0035: 0x2715, # MULTIPLICATION X + 0x0036: 0x2716, # HEAVY MULTIPLICATION X + 0x0037: 0x2717, # BALLOT X + 0x0038: 0x2718, # HEAVY BALLOT X + 0x0039: 0x2719, # OUTLINED GREEK CROSS + 0x003a: 0x271a, # HEAVY GREEK CROSS + 0x003b: 0x271b, # OPEN CENTRE CROSS + 0x003c: 0x271c, # HEAVY OPEN CENTRE CROSS + 0x003d: 0x271d, # LATIN CROSS + 0x003e: 0x271e, # SHADOWED WHITE LATIN CROSS + 0x003f: 0x271f, # OUTLINED LATIN CROSS + 0x0040: 0x2720, # MALTESE CROSS + 0x0041: 0x2721, # STAR OF DAVID + 0x0042: 0x2722, # FOUR TEARDROP-SPOKED ASTERISK + 0x0043: 0x2723, # FOUR BALLOON-SPOKED ASTERISK + 0x0044: 0x2724, # HEAVY FOUR BALLOON-SPOKED ASTERISK + 0x0045: 0x2725, # FOUR CLUB-SPOKED ASTERISK + 0x0046: 0x2726, # BLACK FOUR POINTED STAR + 0x0047: 0x2727, # WHITE FOUR POINTED STAR + 0x0048: 0x2605, # BLACK STAR + 0x0049: 0x2729, # STRESS OUTLINED WHITE STAR + 0x004a: 0x272a, # CIRCLED WHITE STAR + 0x004b: 0x272b, # OPEN CENTRE BLACK STAR + 0x004c: 0x272c, # BLACK CENTRE WHITE STAR + 0x004d: 0x272d, # OUTLINED BLACK STAR + 0x004e: 0x272e, # HEAVY OUTLINED BLACK STAR + 0x004f: 0x272f, # PINWHEEL STAR + 0x0050: 0x2730, # SHADOWED WHITE STAR + 0x0051: 0x2731, # HEAVY ASTERISK + 0x0052: 0x2732, # OPEN CENTRE ASTERISK + 0x0053: 0x2733, # EIGHT SPOKED ASTERISK + 0x0054: 0x2734, # EIGHT POINTED BLACK STAR + 0x0055: 0x2735, # EIGHT POINTED PINWHEEL STAR + 0x0056: 0x2736, # SIX POINTED BLACK STAR + 0x0057: 0x2737, # EIGHT POINTED RECTILINEAR BLACK STAR + 0x0058: 0x2738, # HEAVY EIGHT POINTED RECTILINEAR BLACK STAR + 0x0059: 0x2739, # TWELVE POINTED BLACK STAR + 0x005a: 0x273a, # SIXTEEN POINTED ASTERISK + 0x005b: 0x273b, # TEARDROP-SPOKED ASTERISK + 0x005c: 0x273c, # OPEN CENTRE TEARDROP-SPOKED ASTERISK + 0x005d: 0x273d, # HEAVY TEARDROP-SPOKED ASTERISK + 0x005e: 0x273e, # SIX PETALLED BLACK AND WHITE FLORETTE + 0x005f: 0x273f, # BLACK FLORETTE + 0x0060: 0x2740, # WHITE FLORETTE + 0x0061: 0x2741, # EIGHT PETALLED OUTLINED BLACK FLORETTE + 0x0062: 0x2742, # CIRCLED OPEN CENTRE EIGHT POINTED STAR + 0x0063: 0x2743, # HEAVY TEARDROP-SPOKED PINWHEEL ASTERISK + 0x0064: 0x2744, # SNOWFLAKE + 0x0065: 0x2745, # TIGHT TRIFOLIATE SNOWFLAKE + 0x0066: 0x2746, # HEAVY CHEVRON SNOWFLAKE + 0x0067: 0x2747, # SPARKLE + 0x0068: 0x2748, # HEAVY SPARKLE + 0x0069: 0x2749, # BALLOON-SPOKED ASTERISK + 0x006a: 0x274a, # EIGHT TEARDROP-SPOKED PROPELLER ASTERISK + 0x006b: 0x274b, # HEAVY EIGHT TEARDROP-SPOKED PROPELLER ASTERISK + 0x006c: 0x25cf, # BLACK CIRCLE + 0x006d: 0x274d, # SHADOWED WHITE CIRCLE + 0x006e: 0x25a0, # BLACK SQUARE + 0x006f: 0x274f, # LOWER RIGHT DROP-SHADOWED WHITE SQUARE + 0x0070: 0x2750, # UPPER RIGHT DROP-SHADOWED WHITE SQUARE + 0x0071: 0x2751, # LOWER RIGHT SHADOWED WHITE SQUARE + 0x0072: 0x2752, # UPPER RIGHT SHADOWED WHITE SQUARE + 0x0073: 0x25b2, # BLACK UP-POINTING TRIANGLE + 0x0074: 0x25bc, # BLACK DOWN-POINTING TRIANGLE + 0x0075: 0x25c6, # BLACK DIAMOND + 0x0076: 0x2756, # BLACK DIAMOND MINUS WHITE X + 0x0077: 0x25d7, # RIGHT HALF BLACK CIRCLE + 0x0078: 0x2758, # LIGHT VERTICAL BAR + 0x0079: 0x2759, # MEDIUM VERTICAL BAR + 0x007a: 0x275a, # HEAVY VERTICAL BAR + 0x007b: 0x275b, # HEAVY SINGLE TURNED COMMA QUOTATION MARK ORNAMENT + 0x007c: 0x275c, # HEAVY SINGLE COMMA QUOTATION MARK ORNAMENT + 0x007d: 0x275d, # HEAVY DOUBLE TURNED COMMA QUOTATION MARK ORNAMENT + 0x007e: 0x275e, # HEAVY DOUBLE COMMA QUOTATION MARK ORNAMENT + 0x007f: None, # UNDEFINED + 0x0080: 0x2768, # MEDIUM LEFT PARENTHESIS ORNAMENT + 0x0081: 0x2769, # MEDIUM RIGHT PARENTHESIS ORNAMENT + 0x0082: 0x276a, # MEDIUM FLATTENED LEFT PARENTHESIS ORNAMENT + 0x0083: 0x276b, # MEDIUM FLATTENED RIGHT PARENTHESIS ORNAMENT + 0x0084: 0x276c, # MEDIUM LEFT-POINTING ANGLE BRACKET ORNAMENT + 0x0085: 0x276d, # MEDIUM RIGHT-POINTING ANGLE BRACKET ORNAMENT + 0x0086: 0x276e, # HEAVY LEFT-POINTING ANGLE QUOTATION MARK ORNAMENT + 0x0087: 0x276f, # HEAVY RIGHT-POINTING ANGLE QUOTATION MARK ORNAMENT + 0x0088: 0x2770, # HEAVY LEFT-POINTING ANGLE BRACKET ORNAMENT + 0x0089: 0x2771, # HEAVY RIGHT-POINTING ANGLE BRACKET ORNAMENT + 0x008a: 0x2772, # LIGHT LEFT TORTOISE SHELL BRACKET ORNAMENT + 0x008b: 0x2773, # LIGHT RIGHT TORTOISE SHELL BRACKET ORNAMENT + 0x008c: 0x2774, # MEDIUM LEFT CURLY BRACKET ORNAMENT + 0x008d: 0x2775, # MEDIUM RIGHT CURLY BRACKET ORNAMENT + 0x008e: None, # UNDEFINED + 0x008f: None, # UNDEFINED + 0x0090: None, # UNDEFINED + 0x0091: None, # UNDEFINED + 0x0092: None, # UNDEFINED + 0x0093: None, # UNDEFINED + 0x0094: None, # UNDEFINED + 0x0095: None, # UNDEFINED + 0x0096: None, # UNDEFINED + 0x0097: None, # UNDEFINED + 0x0098: None, # UNDEFINED + 0x0099: None, # UNDEFINED + 0x009a: None, # UNDEFINED + 0x009b: None, # UNDEFINED + 0x009c: None, # UNDEFINED + 0x009d: None, # UNDEFINED + 0x009e: None, # UNDEFINED + 0x009f: None, # UNDEFINED + 0x00a0: None, # UNDEFINED + 0x00a1: 0x2761, # CURVED STEM PARAGRAPH SIGN ORNAMENT + 0x00a2: 0x2762, # HEAVY EXCLAMATION MARK ORNAMENT + 0x00a3: 0x2763, # HEAVY HEART EXCLAMATION MARK ORNAMENT + 0x00a4: 0x2764, # HEAVY BLACK HEART + 0x00a5: 0x2765, # ROTATED HEAVY BLACK HEART BULLET + 0x00a6: 0x2766, # FLORAL HEART + 0x00a7: 0x2767, # ROTATED FLORAL HEART BULLET + 0x00a8: 0x2663, # BLACK CLUB SUIT + 0x00a9: 0x2666, # BLACK DIAMOND SUIT + 0x00aa: 0x2665, # BLACK HEART SUIT + 0x00ab: 0x2660, # BLACK SPADE SUIT + 0x00ac: 0x2460, # CIRCLED DIGIT ONE + 0x00ad: 0x2461, # CIRCLED DIGIT TWO + 0x00ae: 0x2462, # CIRCLED DIGIT THREE + 0x00af: 0x2463, # CIRCLED DIGIT FOUR + 0x00b0: 0x2464, # CIRCLED DIGIT FIVE + 0x00b1: 0x2465, # CIRCLED DIGIT SIX + 0x00b2: 0x2466, # CIRCLED DIGIT SEVEN + 0x00b3: 0x2467, # CIRCLED DIGIT EIGHT + 0x00b4: 0x2468, # CIRCLED DIGIT NINE + 0x00b5: 0x2469, # CIRCLED NUMBER TEN + 0x00b6: 0x2776, # DINGBAT NEGATIVE CIRCLED DIGIT ONE + 0x00b7: 0x2777, # DINGBAT NEGATIVE CIRCLED DIGIT TWO + 0x00b8: 0x2778, # DINGBAT NEGATIVE CIRCLED DIGIT THREE + 0x00b9: 0x2779, # DINGBAT NEGATIVE CIRCLED DIGIT FOUR + 0x00ba: 0x277a, # DINGBAT NEGATIVE CIRCLED DIGIT FIVE + 0x00bb: 0x277b, # DINGBAT NEGATIVE CIRCLED DIGIT SIX + 0x00bc: 0x277c, # DINGBAT NEGATIVE CIRCLED DIGIT SEVEN + 0x00bd: 0x277d, # DINGBAT NEGATIVE CIRCLED DIGIT EIGHT + 0x00be: 0x277e, # DINGBAT NEGATIVE CIRCLED DIGIT NINE + 0x00bf: 0x277f, # DINGBAT NEGATIVE CIRCLED NUMBER TEN + 0x00c0: 0x2780, # DINGBAT CIRCLED SANS-SERIF DIGIT ONE + 0x00c1: 0x2781, # DINGBAT CIRCLED SANS-SERIF DIGIT TWO + 0x00c2: 0x2782, # DINGBAT CIRCLED SANS-SERIF DIGIT THREE + 0x00c3: 0x2783, # DINGBAT CIRCLED SANS-SERIF DIGIT FOUR + 0x00c4: 0x2784, # DINGBAT CIRCLED SANS-SERIF DIGIT FIVE + 0x00c5: 0x2785, # DINGBAT CIRCLED SANS-SERIF DIGIT SIX + 0x00c6: 0x2786, # DINGBAT CIRCLED SANS-SERIF DIGIT SEVEN + 0x00c7: 0x2787, # DINGBAT CIRCLED SANS-SERIF DIGIT EIGHT + 0x00c8: 0x2788, # DINGBAT CIRCLED SANS-SERIF DIGIT NINE + 0x00c9: 0x2789, # DINGBAT CIRCLED SANS-SERIF NUMBER TEN + 0x00ca: 0x278a, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT ONE + 0x00cb: 0x278b, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT TWO + 0x00cc: 0x278c, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT THREE + 0x00cd: 0x278d, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FOUR + 0x00ce: 0x278e, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT FIVE + 0x00cf: 0x278f, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SIX + 0x00d0: 0x2790, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT SEVEN + 0x00d1: 0x2791, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT EIGHT + 0x00d2: 0x2792, # DINGBAT NEGATIVE CIRCLED SANS-SERIF DIGIT NINE + 0x00d3: 0x2793, # DINGBAT NEGATIVE CIRCLED SANS-SERIF NUMBER TEN + 0x00d4: 0x2794, # HEAVY WIDE-HEADED RIGHTWARDS ARROW + 0x00d5: 0x2192, # RIGHTWARDS ARROW + 0x00d6: 0x2194, # LEFT RIGHT ARROW + 0x00d7: 0x2195, # UP DOWN ARROW + 0x00d8: 0x2798, # HEAVY SOUTH EAST ARROW + 0x00d9: 0x2799, # HEAVY RIGHTWARDS ARROW + 0x00da: 0x279a, # HEAVY NORTH EAST ARROW + 0x00db: 0x279b, # DRAFTING POINT RIGHTWARDS ARROW + 0x00dc: 0x279c, # HEAVY ROUND-TIPPED RIGHTWARDS ARROW + 0x00dd: 0x279d, # TRIANGLE-HEADED RIGHTWARDS ARROW + 0x00de: 0x279e, # HEAVY TRIANGLE-HEADED RIGHTWARDS ARROW + 0x00df: 0x279f, # DASHED TRIANGLE-HEADED RIGHTWARDS ARROW + 0x00e0: 0x27a0, # HEAVY DASHED TRIANGLE-HEADED RIGHTWARDS ARROW + 0x00e1: 0x27a1, # BLACK RIGHTWARDS ARROW + 0x00e2: 0x27a2, # THREE-D TOP-LIGHTED RIGHTWARDS ARROWHEAD + 0x00e3: 0x27a3, # THREE-D BOTTOM-LIGHTED RIGHTWARDS ARROWHEAD + 0x00e4: 0x27a4, # BLACK RIGHTWARDS ARROWHEAD + 0x00e5: 0x27a5, # HEAVY BLACK CURVED DOWNWARDS AND RIGHTWARDS ARROW + 0x00e6: 0x27a6, # HEAVY BLACK CURVED UPWARDS AND RIGHTWARDS ARROW + 0x00e7: 0x27a7, # SQUAT BLACK RIGHTWARDS ARROW + 0x00e8: 0x27a8, # HEAVY CONCAVE-POINTED BLACK RIGHTWARDS ARROW + 0x00e9: 0x27a9, # RIGHT-SHADED WHITE RIGHTWARDS ARROW + 0x00ea: 0x27aa, # LEFT-SHADED WHITE RIGHTWARDS ARROW + 0x00eb: 0x27ab, # BACK-TILTED SHADOWED WHITE RIGHTWARDS ARROW + 0x00ec: 0x27ac, # FRONT-TILTED SHADOWED WHITE RIGHTWARDS ARROW + 0x00ed: 0x27ad, # HEAVY LOWER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW + 0x00ee: 0x27ae, # HEAVY UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW + 0x00ef: 0x27af, # NOTCHED LOWER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW + 0x00f0: None, # UNDEFINED + 0x00f1: 0x27b1, # NOTCHED UPPER RIGHT-SHADOWED WHITE RIGHTWARDS ARROW + 0x00f2: 0x27b2, # CIRCLED HEAVY WHITE RIGHTWARDS ARROW + 0x00f3: 0x27b3, # WHITE-FEATHERED RIGHTWARDS ARROW + 0x00f4: 0x27b4, # BLACK-FEATHERED SOUTH EAST ARROW + 0x00f5: 0x27b5, # BLACK-FEATHERED RIGHTWARDS ARROW + 0x00f6: 0x27b6, # BLACK-FEATHERED NORTH EAST ARROW + 0x00f7: 0x27b7, # HEAVY BLACK-FEATHERED SOUTH EAST ARROW + 0x00f8: 0x27b8, # HEAVY BLACK-FEATHERED RIGHTWARDS ARROW + 0x00f9: 0x27b9, # HEAVY BLACK-FEATHERED NORTH EAST ARROW + 0x00fa: 0x27ba, # TEARDROP-BARBED RIGHTWARDS ARROW + 0x00fb: 0x27bb, # HEAVY TEARDROP-SHANKED RIGHTWARDS ARROW + 0x00fc: 0x27bc, # WEDGE-TAILED RIGHTWARDS ARROW + 0x00fd: 0x27bd, # HEAVY WEDGE-TAILED RIGHTWARDS ARROW + 0x00fe: 0x27be, # OPEN-OUTLINED RIGHTWARDS ARROW + 0x00ff: None, # UNDEFINED + },None), + 'pdfdoc':({ + 0x007f: None, # UNDEFINED + 0x0080: 0x2022, # BULLET + 0x0081: 0x2020, # DAGGER + 0x0082: 0x2021, # DOUBLE DAGGER + 0x0083: 0x2026, # HORIZONTAL ELLIPSIS + 0x0084: 0x2014, # EM DASH + 0x0085: 0x2013, # EN DASH + 0x0086: 0x0192, # LATIN SMALL LETTER F WITH HOOK + 0x0087: 0x2044, # FRACTION SLASH + 0x0088: 0x2039, # SINGLE LEFT-POINTING ANGLE QUOTATION MARK + 0x0089: 0x203a, # SINGLE RIGHT-POINTING ANGLE QUOTATION MARK + 0x008a: 0x2212, # MINUS SIGN + 0x008b: 0x2030, # PER MILLE SIGN + 0x008c: 0x201e, # DOUBLE LOW-9 QUOTATION MARK + 0x008d: 0x201c, # LEFT DOUBLE QUOTATION MARK + 0x008e: 0x201d, # RIGHT DOUBLE QUOTATION MARK + 0x008f: 0x2018, # LEFT SINGLE QUOTATION MARK + 0x0090: 0x2019, # RIGHT SINGLE QUOTATION MARK + 0x0091: 0x201a, # SINGLE LOW-9 QUOTATION MARK + 0x0092: 0x2122, # TRADE MARK SIGN + 0x0093: 0xfb01, # LATIN SMALL LIGATURE FI + 0x0094: 0xfb02, # LATIN SMALL LIGATURE FL + 0x0095: 0x0141, # LATIN CAPITAL LETTER L WITH STROKE + 0x0096: 0x0152, # LATIN CAPITAL LIGATURE OE + 0x0097: 0x0160, # LATIN CAPITAL LETTER S WITH CARON + 0x0098: 0x0178, # LATIN CAPITAL LETTER Y WITH DIAERESIS + 0x0099: 0x017d, # LATIN CAPITAL LETTER Z WITH CARON + 0x009a: 0x0131, # LATIN SMALL LETTER DOTLESS I + 0x009b: 0x0142, # LATIN SMALL LETTER L WITH STROKE + 0x009c: 0x0153, # LATIN SMALL LIGATURE OE + 0x009d: 0x0161, # LATIN SMALL LETTER S WITH CARON + 0x009e: 0x017e, # LATIN SMALL LETTER Z WITH CARON + 0x009f: None, # UNDEFINED + 0x00a0: 0x20ac, # EURO SIGN + 0x00ad: None, # UNDEFINED + 24: 0x02d8, #breve + 25: 0x02c7, #caron + 26: 0x02c6, #circumflex + 27: 0x02d9, #dotaccent + 28: 0x02dd, #hungarumlaut + 29: 0x02db, #ogonek + 30: 0x02da, #ring + 31: 0x02dc, #tilde + },None), + 'macexpert':({ + 0x0021: 0xf721, # [unknown unicode name for exclamsmall] + 0x0022: 0xf6f8, # [unknown unicode name for Hungarumlautsmall] + 0x0023: 0xf7a2, # [unknown unicode name for centoldstyle] + 0x0024: 0xf724, # [unknown unicode name for dollaroldstyle] + 0x0025: 0xf6e4, # [unknown unicode name for dollarsuperior] + 0x0026: 0xf726, # [unknown unicode name for ampersandsmall] + 0x0027: 0xf7b4, # [unknown unicode name for Acutesmall] + 0x0028: 0x207d, # SUPERSCRIPT LEFT PARENTHESIS + 0x0029: 0x207e, # SUPERSCRIPT RIGHT PARENTHESIS + 0x002a: 0x2025, # TWO DOT LEADER + 0x002b: 0x2024, # ONE DOT LEADER + 0x002f: 0x2044, # FRACTION SLASH + 0x0030: 0xf730, # [unknown unicode name for zerooldstyle] + 0x0031: 0xf731, # [unknown unicode name for oneoldstyle] + 0x0032: 0xf732, # [unknown unicode name for twooldstyle] + 0x0033: 0xf733, # [unknown unicode name for threeoldstyle] + 0x0034: 0xf734, # [unknown unicode name for fouroldstyle] + 0x0035: 0xf735, # [unknown unicode name for fiveoldstyle] + 0x0036: 0xf736, # [unknown unicode name for sixoldstyle] + 0x0037: 0xf737, # [unknown unicode name for sevenoldstyle] + 0x0038: 0xf738, # [unknown unicode name for eightoldstyle] + 0x0039: 0xf739, # [unknown unicode name for nineoldstyle] + 0x003c: None, # UNDEFINED + 0x003d: 0xf6de, # [unknown unicode name for threequartersemdash] + 0x003e: None, # UNDEFINED + 0x003f: 0xf73f, # [unknown unicode name for questionsmall] + 0x0040: None, # UNDEFINED + 0x0041: None, # UNDEFINED + 0x0042: None, # UNDEFINED + 0x0043: None, # UNDEFINED + 0x0044: 0xf7f0, # [unknown unicode name for Ethsmall] + 0x0045: None, # UNDEFINED + 0x0046: None, # UNDEFINED + 0x0047: 0x00bc, # VULGAR FRACTION ONE QUARTER + 0x0048: 0x00bd, # VULGAR FRACTION ONE HALF + 0x0049: 0x00be, # VULGAR FRACTION THREE QUARTERS + 0x004a: 0x215b, # VULGAR FRACTION ONE EIGHTH + 0x004b: 0x215c, # VULGAR FRACTION THREE EIGHTHS + 0x004c: 0x215d, # VULGAR FRACTION FIVE EIGHTHS + 0x004d: 0x215e, # VULGAR FRACTION SEVEN EIGHTHS + 0x004e: 0x2153, # VULGAR FRACTION ONE THIRD + 0x004f: 0x2154, # VULGAR FRACTION TWO THIRDS + 0x0050: None, # UNDEFINED + 0x0051: None, # UNDEFINED + 0x0052: None, # UNDEFINED + 0x0053: None, # UNDEFINED + 0x0054: None, # UNDEFINED + 0x0055: None, # UNDEFINED + 0x0056: 0xfb00, # LATIN SMALL LIGATURE FF + 0x0057: 0xfb01, # LATIN SMALL LIGATURE FI + 0x0058: 0xfb02, # LATIN SMALL LIGATURE FL + 0x0059: 0xfb03, # LATIN SMALL LIGATURE FFI + 0x005a: 0xfb04, # LATIN SMALL LIGATURE FFL + 0x005b: 0x208d, # SUBSCRIPT LEFT PARENTHESIS + 0x005c: None, # UNDEFINED + 0x005d: 0x208e, # SUBSCRIPT RIGHT PARENTHESIS + 0x005e: 0xf6f6, # [unknown unicode name for Circumflexsmall] + 0x005f: 0xf6e5, # [unknown unicode name for hypheninferior] + 0x0060: 0xf760, # [unknown unicode name for Gravesmall] + 0x0061: 0xf761, # [unknown unicode name for Asmall] + 0x0062: 0xf762, # [unknown unicode name for Bsmall] + 0x0063: 0xf763, # [unknown unicode name for Csmall] + 0x0064: 0xf764, # [unknown unicode name for Dsmall] + 0x0065: 0xf765, # [unknown unicode name for Esmall] + 0x0066: 0xf766, # [unknown unicode name for Fsmall] + 0x0067: 0xf767, # [unknown unicode name for Gsmall] + 0x0068: 0xf768, # [unknown unicode name for Hsmall] + 0x0069: 0xf769, # [unknown unicode name for Ismall] + 0x006a: 0xf76a, # [unknown unicode name for Jsmall] + 0x006b: 0xf76b, # [unknown unicode name for Ksmall] + 0x006c: 0xf76c, # [unknown unicode name for Lsmall] + 0x006d: 0xf76d, # [unknown unicode name for Msmall] + 0x006e: 0xf76e, # [unknown unicode name for Nsmall] + 0x006f: 0xf76f, # [unknown unicode name for Osmall] + 0x0070: 0xf770, # [unknown unicode name for Psmall] + 0x0071: 0xf771, # [unknown unicode name for Qsmall] + 0x0072: 0xf772, # [unknown unicode name for Rsmall] + 0x0073: 0xf773, # [unknown unicode name for Ssmall] + 0x0074: 0xf774, # [unknown unicode name for Tsmall] + 0x0075: 0xf775, # [unknown unicode name for Usmall] + 0x0076: 0xf776, # [unknown unicode name for Vsmall] + 0x0077: 0xf777, # [unknown unicode name for Wsmall] + 0x0078: 0xf778, # [unknown unicode name for Xsmall] + 0x0079: 0xf779, # [unknown unicode name for Ysmall] + 0x007a: 0xf77a, # [unknown unicode name for Zsmall] + 0x007b: 0x20a1, # COLON SIGN + 0x007c: 0xf6dc, # [unknown unicode name for onefitted] + 0x007d: 0xf6dd, # [unknown unicode name for rupiah] + 0x007e: 0xf6fe, # [unknown unicode name for Tildesmall] + 0x007f: None, # UNDEFINED + 0x0080: None, # UNDEFINED + 0x0081: 0xf6e9, # [unknown unicode name for asuperior] + 0x0082: 0xf6e0, # [unknown unicode name for centsuperior] + 0x0083: None, # UNDEFINED + 0x0084: None, # UNDEFINED + 0x0085: None, # UNDEFINED + 0x0086: None, # UNDEFINED + 0x0087: 0xf7e1, # [unknown unicode name for Aacutesmall] + 0x0088: 0xf7e0, # [unknown unicode name for Agravesmall] + 0x0089: 0xf7e2, # [unknown unicode name for Acircumflexsmall] + 0x008a: 0xf7e4, # [unknown unicode name for Adieresissmall] + 0x008b: 0xf7e3, # [unknown unicode name for Atildesmall] + 0x008c: 0xf7e5, # [unknown unicode name for Aringsmall] + 0x008d: 0xf7e7, # [unknown unicode name for Ccedillasmall] + 0x008e: 0xf7e9, # [unknown unicode name for Eacutesmall] + 0x008f: 0xf7e8, # [unknown unicode name for Egravesmall] + 0x0090: 0xf7ea, # [unknown unicode name for Ecircumflexsmall] + 0x0091: 0xf7eb, # [unknown unicode name for Edieresissmall] + 0x0092: 0xf7ed, # [unknown unicode name for Iacutesmall] + 0x0093: 0xf7ec, # [unknown unicode name for Igravesmall] + 0x0094: 0xf7ee, # [unknown unicode name for Icircumflexsmall] + 0x0095: 0xf7ef, # [unknown unicode name for Idieresissmall] + 0x0096: 0xf7f1, # [unknown unicode name for Ntildesmall] + 0x0097: 0xf7f3, # [unknown unicode name for Oacutesmall] + 0x0098: 0xf7f2, # [unknown unicode name for Ogravesmall] + 0x0099: 0xf7f4, # [unknown unicode name for Ocircumflexsmall] + 0x009a: 0xf7f6, # [unknown unicode name for Odieresissmall] + 0x009b: 0xf7f5, # [unknown unicode name for Otildesmall] + 0x009c: 0xf7fa, # [unknown unicode name for Uacutesmall] + 0x009d: 0xf7f9, # [unknown unicode name for Ugravesmall] + 0x009e: 0xf7fb, # [unknown unicode name for Ucircumflexsmall] + 0x009f: 0xf7fc, # [unknown unicode name for Udieresissmall] + 0x00a0: None, # UNDEFINED + 0x00a1: 0x2078, # SUPERSCRIPT EIGHT + 0x00a2: 0x2084, # SUBSCRIPT FOUR + 0x00a3: 0x2083, # SUBSCRIPT THREE + 0x00a4: 0x2086, # SUBSCRIPT SIX + 0x00a5: 0x2088, # SUBSCRIPT EIGHT + 0x00a6: 0x2087, # SUBSCRIPT SEVEN + 0x00a7: 0xf6fd, # [unknown unicode name for Scaronsmall] + 0x00a8: None, # UNDEFINED + 0x00a9: 0xf6df, # [unknown unicode name for centinferior] + 0x00aa: 0x2082, # SUBSCRIPT TWO + 0x00ab: None, # UNDEFINED + 0x00ac: 0xf7a8, # [unknown unicode name for Dieresissmall] + 0x00ad: None, # UNDEFINED + 0x00ae: 0xf6f5, # [unknown unicode name for Caronsmall] + 0x00af: 0xf6f0, # [unknown unicode name for osuperior] + 0x00b0: 0x2085, # SUBSCRIPT FIVE + 0x00b1: None, # UNDEFINED + 0x00b2: 0xf6e1, # [unknown unicode name for commainferior] + 0x00b3: 0xf6e7, # [unknown unicode name for periodinferior] + 0x00b4: 0xf7fd, # [unknown unicode name for Yacutesmall] + 0x00b5: None, # UNDEFINED + 0x00b6: 0xf6e3, # [unknown unicode name for dollarinferior] + 0x00b7: None, # UNDEFINED + 0x00b8: None, # UNDEFINED + 0x00b9: 0xf7fe, # [unknown unicode name for Thornsmall] + 0x00ba: None, # UNDEFINED + 0x00bb: 0x2089, # SUBSCRIPT NINE + 0x00bc: 0x2080, # SUBSCRIPT ZERO + 0x00bd: 0xf6ff, # [unknown unicode name for Zcaronsmall] + 0x00be: 0xf7e6, # [unknown unicode name for AEsmall] + 0x00bf: 0xf7f8, # [unknown unicode name for Oslashsmall] + 0x00c0: 0xf7bf, # [unknown unicode name for questiondownsmall] + 0x00c1: 0x2081, # SUBSCRIPT ONE + 0x00c2: 0xf6f9, # [unknown unicode name for Lslashsmall] + 0x00c3: None, # UNDEFINED + 0x00c4: None, # UNDEFINED + 0x00c5: None, # UNDEFINED + 0x00c6: None, # UNDEFINED + 0x00c7: None, # UNDEFINED + 0x00c8: None, # UNDEFINED + 0x00c9: 0xf7b8, # [unknown unicode name for Cedillasmall] + 0x00ca: None, # UNDEFINED + 0x00cb: None, # UNDEFINED + 0x00cc: None, # UNDEFINED + 0x00cd: None, # UNDEFINED + 0x00ce: None, # UNDEFINED + 0x00cf: 0xf6fa, # [unknown unicode name for OEsmall] + 0x00d0: 0x2012, # FIGURE DASH + 0x00d1: 0xf6e6, # [unknown unicode name for hyphensuperior] + 0x00d2: None, # UNDEFINED + 0x00d3: None, # UNDEFINED + 0x00d4: None, # UNDEFINED + 0x00d5: None, # UNDEFINED + 0x00d6: 0xf7a1, # [unknown unicode name for exclamdownsmall] + 0x00d7: None, # UNDEFINED + 0x00d8: 0xf7ff, # [unknown unicode name for Ydieresissmall] + 0x00d9: None, # UNDEFINED + 0x00da: 0x00b9, # SUPERSCRIPT ONE + 0x00db: 0x00b2, # SUPERSCRIPT TWO + 0x00dc: 0x00b3, # SUPERSCRIPT THREE + 0x00dd: 0x2074, # SUPERSCRIPT FOUR + 0x00de: 0x2075, # SUPERSCRIPT FIVE + 0x00df: 0x2076, # SUPERSCRIPT SIX + 0x00e0: 0x2077, # SUPERSCRIPT SEVEN + 0x00e1: 0x2079, # SUPERSCRIPT NINE + 0x00e2: 0x2070, # SUPERSCRIPT ZERO + 0x00e3: None, # UNDEFINED + 0x00e4: 0xf6ec, # [unknown unicode name for esuperior] + 0x00e5: 0xf6f1, # [unknown unicode name for rsuperior] + 0x00e6: 0xf6f3, # [unknown unicode name for tsuperior] + 0x00e7: None, # UNDEFINED + 0x00e8: None, # UNDEFINED + 0x00e9: 0xf6ed, # [unknown unicode name for isuperior] + 0x00ea: 0xf6f2, # [unknown unicode name for ssuperior] + 0x00eb: 0xf6eb, # [unknown unicode name for dsuperior] + 0x00ec: None, # UNDEFINED + 0x00ed: None, # UNDEFINED + 0x00ee: None, # UNDEFINED + 0x00ef: None, # UNDEFINED + 0x00f0: None, # UNDEFINED + 0x00f1: 0xf6ee, # [unknown unicode name for lsuperior] + 0x00f2: 0xf6fb, # [unknown unicode name for Ogoneksmall] + 0x00f3: 0xf6f4, # [unknown unicode name for Brevesmall] + 0x00f4: 0xf7af, # [unknown unicode name for Macronsmall] + 0x00f5: 0xf6ea, # [unknown unicode name for bsuperior] + 0x00f6: 0x207f, # SUPERSCRIPT LATIN SMALL LETTER N + 0x00f7: 0xf6ef, # [unknown unicode name for msuperior] + 0x00f8: 0xf6e2, # [unknown unicode name for commasuperior] + 0x00f9: 0xf6e8, # [unknown unicode name for periodsuperior] + 0x00fa: 0xf6f7, # [unknown unicode name for Dotaccentsmall] + 0x00fb: 0xf6fc, # [unknown unicode name for Ringsmall] + 0x00fc: None, # UNDEFINED + 0x00fd: None, # UNDEFINED + 0x00fe: None, # UNDEFINED + 0x00ff: None, # UNDEFINED + },None), + } + #for k,v in __rl_codecs_data.items(): + # __rl_codecs_data[k+'enc'] = __rl_codecs_data[k+'encoding'] = v + #del k,v + + def __init__(self): + raise NotImplementedError + + def _256_exception_codec((exceptions,rexceptions)): + import codecs + decoding_map = codecs.make_identity_dict(xrange(32,256)) + decoding_map.update(exceptions) + encoding_map = codecs.make_encoding_map(decoding_map) + if rexceptions: encoding_map.update(rexceptions) + ### Codec APIs + class Codec(codecs.Codec): + def encode(self,input,errors='strict',charmap_encode=codecs.charmap_encode,encoding_map=encoding_map): + return charmap_encode(input,errors,encoding_map) + + def decode(self,input,errors='strict',charmap_decode=codecs.charmap_decode,decoding_map=decoding_map): + return charmap_decode(input,errors,decoding_map) + + class StreamWriter(Codec,codecs.StreamWriter): + pass + + class StreamReader(Codec,codecs.StreamReader): + pass + C = Codec() + return (C.encode,C.decode,StreamReader,StreamWriter) + _256_exception_codec=staticmethod(_256_exception_codec) + + __rl_codecs_cache = {} + + def __rl_codecs(name,cache=__rl_codecs_cache,data=__rl_codecs_data): + try: + return cache[name] + except KeyError: + cache[name] = c = RL_Codecs._256_exception_codec( + data[name]) + return c + __rl_codecs=staticmethod(__rl_codecs) + + def _rl_codecs(name): + name = name.lower() + from pdfmetrics import standardEncodings + for e in standardEncodings: + e = e[:-8].lower() + if name.startswith(e): return RL_Codecs.__rl_codecs(e) + return None + _rl_codecs=staticmethod(_rl_codecs) + + def register(): + import codecs + codecs.register(RL_Codecs._rl_codecs) + register=staticmethod(register) diff --git a/bin/reportlab/pdfbase/ttfonts.py b/bin/reportlab/pdfbase/ttfonts.py new file mode 100644 index 00000000000..c6f9c922dd4 --- /dev/null +++ b/bin/reportlab/pdfbase/ttfonts.py @@ -0,0 +1,1113 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfbase/ttfonts.py +"""TrueType font support + +This defines classes to represent TrueType fonts. They know how to calculate +their own width and how to write themselves into PDF files. They support +subsetting and embedding and can represent all 16-bit Unicode characters. + +Note on dynamic fonts +--------------------- + +Usually a Font in ReportLab corresponds to a fixed set of PDF objects (Font, +FontDescriptor, Encoding). But with dynamic font subsetting a single TTFont +will result in a number of Font/FontDescriptor/Encoding object sets, and the +contents of those will depend on the actual characters used for printing. + +To support dynamic font subsetting a concept of "dynamic font" was introduced. +Dynamic Fonts have a _dynamicFont attribute set to 1. Since other Font object +may lack a this attribute, you should use constructs like + + if getattr(font, '_dynamicFont', 0): + # dynamic font + else: + # traditional static font + +Dynamic fonts have the following additional functions: + + def splitString(self, text, doc): + '''Splits text into a number of chunks, each of which belongs to a + single subset. Returns a list of tuples (subset, string). Use + subset numbers with getSubsetInternalName. Doc is used to identify + a document so that different documents may have different dynamically + constructed subsets.''' + + def getSubsetInternalName(self, subset, doc): + '''Returns the name of a PDF Font object corresponding to a given + subset of this dynamic font. Use this function instead of + PDFDocument.getInternalFontName.''' + +You must never call PDFDocument.getInternalFontName for dynamic fonts. + +If you have a traditional static font, mapping to PDF text output operators +is simple: + + '%s 14 Tf (%s) Tj' % (getInternalFontName(psfontname), text) + +If you have a dynamic font, use this instead: + + for subset, chunk in font.splitString(text, doc): + '%s 14 Tf (%s) Tj' % (font.getSubsetInternalName(subset, doc), chunk) + +(Tf is a font setting operator and Tj is a text ouput operator. You should +also escape invalid characters in Tj argument, see TextObject._formatText. +Oh, and that 14 up there is font size.) + +Canvas and TextObject have special support for dynamic fonts. +""" + +__version__ = '$Id: ttfonts.py 2865 2006-05-15 16:37:44Z rgbecker $' + +import string +from types import StringType, UnicodeType +from struct import pack, unpack +from cStringIO import StringIO +from reportlab.pdfbase import pdfmetrics, pdfdoc + +def _L2U32(L): + return unpack('l',pack('L',L))[0] + +class TTFError(pdfdoc.PDFError): + "TrueType font exception" + pass + + +def SUBSETN(n,table=string.maketrans('0123456789','ABCDEFGHIJ')): + return ('%6.6d'%n).translate(table) +# +# Helpers +# + +from codecs import utf_8_encode, utf_8_decode, latin_1_decode +parse_utf8=lambda x, decode=utf_8_decode: map(ord,decode(x)[0]) +parse_latin1 = lambda x, decode=latin_1_decode: map(ord,decode(x)[0]) +def latin1_to_utf8(text): + "helper to convert when needed from latin input" + return utf_8_encode(latin_1_decode(text)[0])[0] + +def makeToUnicodeCMap(fontname, subset): + """Creates a ToUnicode CMap for a given subset. See Adobe + _PDF_Reference (ISBN 0-201-75839-3) for more information.""" + cmap = [ + "/CIDInit /ProcSet findresource begin", + "12 dict begin", + "begincmap", + "/CIDSystemInfo", + "<< /Registry (%s)" % fontname, + "/Ordering (%s)" % fontname, + "/Supplement 0", + ">> def", + "/CMapName /%s def" % fontname, + "/CMapType 2 def", + "1 begincodespacerange", + "<00> <%02X>" % (len(subset) - 1), + "endcodespacerange", + "%d beginbfchar" % len(subset) + ] + map(lambda n, subset=subset: "<%02X> <%04X>" % (n, subset[n]), + xrange(len(subset))) + [ + "endbfchar", + "endcmap", + "CMapName currentdict /CMap defineresource pop", + "end", + "end" + ] + return string.join(cmap, "\n") + +def splice(stream, offset, value): + """Splices the given value into stream at the given offset and + returns the resulting stream (the original is unchanged)""" + return stream[:offset] + value + stream[offset + len(value):] + +def _set_ushort(stream, offset, value): + """Writes the given unsigned short value into stream at the given + offset and returns the resulting stream (the original is unchanged)""" + return splice(stream, offset, pack(">H", value)) + +import sys +try: + import _rl_accel +except ImportError: + try: + from reportlab.lib import _rl_accel + except ImportError: + _rl_accel = None + +try: + hex32 = _rl_accel.hex32 +except: + def hex32(i): + return '0X%8.8X' % (long(i)&0xFFFFFFFFL) +try: + add32 = _rl_accel.add32 +except: + if sys.hexversion>=0x02030000: + def add32(x, y): + "Calculate (x + y) modulo 2**32" + return _L2U32((long(x)+y) & 0xffffffffL) + else: + def add32(x, y): + "Calculate (x + y) modulo 2**32" + lo = (x & 0xFFFF) + (y & 0xFFFF) + hi = (x >> 16) + (y >> 16) + (lo >> 16) + return (hi << 16) | (lo & 0xFFFF) + +try: + calcChecksum = _rl_accel.calcChecksum +except: + def calcChecksum(data): + """Calculates PDF-style checksums""" + if len(data)&3: data = data + (4-(len(data)&3))*"\0" + sum = 0 + for n in unpack(">%dl" % (len(data)>>2), data): + sum = add32(sum,n) + return sum +del _rl_accel, sys +# +# TrueType font handling +# + +GF_ARG_1_AND_2_ARE_WORDS = 1 << 0 +GF_ARGS_ARE_XY_VALUES = 1 << 1 +GF_ROUND_XY_TO_GRID = 1 << 2 +GF_WE_HAVE_A_SCALE = 1 << 3 +GF_RESERVED = 1 << 4 +GF_MORE_COMPONENTS = 1 << 5 +GF_WE_HAVE_AN_X_AND_Y_SCALE = 1 << 6 +GF_WE_HAVE_A_TWO_BY_TWO = 1 << 7 +GF_WE_HAVE_INSTRUCTIONS = 1 << 8 +GF_USE_MY_METRICS = 1 << 9 +GF_OVERLAP_COMPOUND = 1 << 10 +GF_SCALED_COMPONENT_OFFSET = 1 << 11 +GF_UNSCALED_COMPONENT_OFFSET = 1 << 12 + +def TTFOpenFile(fn): + '''Opens a TTF file possibly after searching TTFSearchPath + returns (filename,file) + ''' + from reportlab.lib.utils import rl_isfile, open_for_read + try: + f = open_for_read(fn,'rb') + return fn, f + except IOError: + import os + if not os.path.isabs(fn): + from reportlab import rl_config + for D in rl_config.TTFSearchPath: + tfn = os.path.join(D,fn) + if rl_isfile(tfn): + f = open_for_read(tfn,'rb') + return tfn, f + raise TTFError('Can\'t open file "%s"' % fn) + +class TTFontParser: + "Basic TTF file parser" + ttfVersions = (0x00010000,0x74727565,0x74746366) + ttcVersions = (0x00010000,0x00020000) + fileKind='TTF' + + def __init__(self, file, validate=0,subfontIndex=0): + """Loads and parses a TrueType font file. file can be a filename or a + file object. If validate is set to a false values, skips checksum + validation. This can save time, especially if the font is large. + """ + self.validate = validate + self.readFile(file) + isCollection = self.readHeader() + if isCollection: + self.readTTCHeader() + self.getSubfont(subfontIndex) + else: + if self.validate: self.checksumFile() + self.readTableDirectory() + self.subfontNameX = '' + + def readTTCHeader(self): + self.ttcVersion = self.read_ulong() + self.fileKind = 'TTC' + self.ttfVersions = self.ttfVersions[:-1] + if self.ttcVersion not in self.ttcVersions: + raise TTFError('"%s" is not a %s file: can\'t read version 0x%8.8x' %(self.filename,self.fileKind,self.ttcVersion)) + self.numSubfonts = self.read_ulong() + self.subfontOffsets = [] + a = self.subfontOffsets.append + for i in xrange(self.numSubfonts): + a(self.read_ulong()) + + def getSubfont(self,subfontIndex): + if self.fileKind!='TTC': + raise TTFError('"%s" is not a TTC file: use this method' % (self.filename,self.fileKind)) + try: + pos = self.subfontOffsets[subfontIndex] + except IndexError: + raise TTFError('TTC file "%s": bad subfontIndex %s not in [0,%d]' % (self.filename,subfontIndex,self.numSubfonts-1)) + self.seek(pos) + self.readHeader() + self.readTableDirectory() + self.subfontNameX = '-'+str(subfontIndex) + + def readTableDirectory(self): + try: + self.numTables = self.read_ushort() + self.searchRange = self.read_ushort() + self.entrySelector = self.read_ushort() + self.rangeShift = self.read_ushort() + + # Read table directory + self.table = {} + self.tables = [] + for n in xrange(self.numTables): + record = {} + record['tag'] = self.read_tag() + record['checksum'] = self.read_ulong() + record['offset'] = self.read_ulong() + record['length'] = self.read_ulong() + self.tables.append(record) + self.table[record['tag']] = record + except: + raise TTFError('Corrupt %s file "%s" cannot read Table Directory' % (self.fileKind, self.filename)) + if self.validate: self.checksumTables() + + def readHeader(self): + '''read the sfnt header at the current position''' + try: + self.version = version = self.read_ulong() + except: + raise TTFError('"%s" is not a %s file: can\'t read version' %(self.filename,self.fileKind)) + + if version==0x4F54544F: + raise TTFError('%s file "%s": postscript outlines are not supported'%(self.fileKind,self.filename)) + + if version not in self.ttfVersions: + raise TTFError('Not a TrueType font: version=0x%8.8X' % version) + return version==self.ttfVersions[-1] + + def readFile(self,file): + if type(file) is StringType: + self.filename, file = TTFOpenFile(file) + else: + self.filename = '(ttf)' + + self._ttf_data = file.read() + self._pos = 0 + + def checksumTables(self): + # Check the checksums for all tables + for t in self.tables: + table = self.get_chunk(t['offset'], t['length']) + checkSum = calcChecksum(table) + if t['tag'] == 'head': + adjustment = unpack('>l', table[8:8+4])[0] + checkSum = add32(checkSum, -adjustment) + if t['checksum'] != checkSum: + raise TTFError('TTF file "%s": invalid checksum %s table: %s' % (self.filename,hex32(checkSum),t['tag'])) + + def checksumFile(self): + # Check the checksums for the whole file + checkSum = calcChecksum(self._ttf_data) + if add32(_L2U32(0xB1B0AFBAL), -checkSum) != 0: + raise TTFError('TTF file "%s": invalid checksum %s len: %d &3: %d' % (self.filename,hex32(checkSum),len(self._ttf_data),(len(self._ttf_data)&3))) + + def get_table_pos(self, tag): + "Returns the offset and size of a given TTF table." + offset = self.table[tag]['offset'] + length = self.table[tag]['length'] + return (offset, length) + + def seek(self, pos): + "Moves read pointer to a given offset in file." + self._pos = pos + + def skip(self, delta): + "Skip the given number of bytes." + self._pos = self._pos + delta + + def seek_table(self, tag, offset_in_table = 0): + """Moves read pointer to the given offset within a given table and + returns absolute offset of that position in the file.""" + self._pos = self.get_table_pos(tag)[0] + offset_in_table + return self._pos + + def read_tag(self): + "Read a 4-character tag" + self._pos += 4 + return self._ttf_data[self._pos - 4:self._pos] + + def read_ushort(self): + "Reads an unsigned short" + self._pos += 2 + return unpack('>H',self._ttf_data[self._pos-2:self._pos])[0] + + def read_ulong(self): + "Reads an unsigned long" + self._pos += 4 + return unpack('>l',self._ttf_data[self._pos - 4:self._pos])[0] + + def read_short(self): + "Reads a signed short" + self._pos += 2 + return unpack('>h',self._ttf_data[self._pos-2:self._pos])[0] + + def get_ushort(self, pos): + "Return an unsigned short at given position" + return unpack('>H',self._ttf_data[pos:pos+2])[0] + + def get_ulong(self, pos): + "Return an unsigned long at given position" + return unpack('>l',self._ttf_data[pos:pos+4])[0] + + def get_chunk(self, pos, length): + "Return a chunk of raw data at given position" + return self._ttf_data[pos:pos+length] + + def get_table(self, tag): + "Return the given TTF table" + pos, length = self.get_table_pos(tag) + return self._ttf_data[pos:pos+length] + +class TTFontMaker: + "Basic TTF file generator" + + def __init__(self): + "Initializes the generator." + self.tables = {} + + def add(self, tag, data): + "Adds a table to the TTF file." + if tag == 'head': + data = splice(data, 8, '\0\0\0\0') + self.tables[tag] = data + + def makeStream(self): + "Finishes the generation and returns the TTF file as a string" + stm = StringIO() + + numTables = len(self.tables) + searchRange = 1 + entrySelector = 0 + while searchRange * 2 <= numTables: + searchRange = searchRange * 2 + entrySelector = entrySelector + 1 + searchRange = searchRange * 16 + rangeShift = numTables * 16 - searchRange + + # Header + stm.write(pack(">lHHHH", 0x00010000, numTables, searchRange, + entrySelector, rangeShift)) + + # Table directory + tables = self.tables.items() + tables.sort() # XXX is this the correct order? + offset = 12 + numTables * 16 + for tag, data in tables: + if tag == 'head': + head_start = offset + checksum = calcChecksum(data) + stm.write(tag) + stm.write(pack(">LLL", checksum, offset, len(data))) + paddedLength = (len(data)+3)&~3 + offset = offset + paddedLength + + # Table data + for tag, data in tables: + data = data + "\0\0\0" + stm.write(data[:len(data)&~3]) + + checksum = calcChecksum(stm.getvalue()) + checksum = add32(_L2U32(0xB1B0AFBAL), -checksum) + stm.seek(head_start + 8) + stm.write(pack('>L', checksum)) + + return stm.getvalue() + +class TTFontFile(TTFontParser): + "TTF file parser and generator" + + def __init__(self, file, charInfo=1, validate=0,subfontIndex=0): + """Loads and parses a TrueType font file. + + file can be a filename or a file object. If validate is set to a false + values, skips checksum validation. This can save time, especially if + the font is large. See TTFontFile.extractInfo for more information. + """ + TTFontParser.__init__(self, file, validate=validate,subfontIndex=subfontIndex) + self.extractInfo(charInfo) + + def extractInfo(self, charInfo=1): + """Extract typographic information from the loaded font file. + + The following attributes will be set: + name - PostScript font name + flags - Font flags + ascent - Typographic ascender in 1/1000ths of a point + descent - Typographic descender in 1/1000ths of a point + capHeight - Cap height in 1/1000ths of a point (0 if not available) + bbox - Glyph bounding box [l,t,r,b] in 1/1000ths of a point + italicAngle - Italic angle in degrees ccw + stemV - stem weight in 1/1000ths of a point (approximate) + If charInfo is true, the following will also be set: + defaultWidth - default glyph width in 1/1000ths of a point + charWidths - dictionary of character widths for every supported + UCS character code + + This will only work if the font has a Unicode cmap (platform 3, + encoding 1, format 4 or platform 0 any encoding format 4). Setting + charInfo to false avoids this requirement. + """ + # name - Naming table + name_offset = self.seek_table("name") + format = self.read_ushort() + if format != 0: + raise TTFError, "Unknown name table format (%d)" % format + numRecords = self.read_ushort() + string_data_offset = name_offset + self.read_ushort() + names = {1:None,2:None,3:None,4:None,6:None} + K = names.keys() + nameCount = len(names) + for i in xrange(numRecords): + platformId = self.read_ushort() + encodingId = self.read_ushort() + languageId = self.read_ushort() + nameId = self.read_ushort() + length = self.read_ushort() + offset = self.read_ushort() + if nameId not in K: continue + N = None + if platformId == 3 and encodingId == 1 and languageId == 0x409: # Microsoft, Unicode, US English, PS Name + opos = self._pos + try: + self.seek(string_data_offset + offset) + if length % 2 != 0: + raise TTFError, "PostScript name is UTF-16BE string of odd length" + length /= 2 + N = [] + A = N.append + while length > 0: + char = self.read_ushort() + A(chr(char)) + length -= 1 + N = ''.join(N) + finally: + self._pos = opos + elif platformId == 1 and encodingId == 0 and languageId == 0: # Macintosh, Roman, English, PS Name + # According to OpenType spec, if PS name exists, it must exist + # both in MS Unicode and Macintosh Roman formats. Apparently, + # you can find live TTF fonts which only have Macintosh format. + N = self.get_chunk(string_data_offset + offset, length) + if N and names[nameId]==None: + names[nameId] = N + nameCount -= 1 + if nameCount==0: break + psName = names[6] + if not psName: + raise TTFError, "Could not find PostScript font name" + for c in psName: + oc = ord(c) + if oc<33 or oc>126 or c in ('[', ']', '(', ')', '{', '}', '<', '>', '/', '%'): + raise TTFError, "psName contains invalid character '%s' ie U+%04X" % (c,ord(c)) + self.name = psName + self.familyName = names[1] or psName + self.styleName = names[2] or 'Regular' + self.fullName = names[4] or psName + self.uniqueFontID = names[3] or psName + + # head - Font header table + self.seek_table("head") + ver_maj, ver_min = self.read_ushort(), self.read_ushort() + if ver_maj != 1: + raise TTFError, 'Unknown head table version %d.%04x' % (ver_maj, ver_min) + self.skip(8) + magic = self.read_ulong() + if magic != 0x5F0F3CF5: + raise TTFError, 'Invalid head table magic %04x' % magic + self.skip(2) + unitsPerEm = self.read_ushort() + scale = lambda x, unitsPerEm=unitsPerEm: x * 1000 / unitsPerEm + self.skip(16) + xMin = self.read_short() + yMin = self.read_short() + xMax = self.read_short() + yMax = self.read_short() + self.bbox = map(scale, [xMin, yMin, xMax, yMax]) + self.skip(3*2) + indexToLocFormat = self.read_ushort() + glyphDataFormat = self.read_ushort() + + # OS/2 - OS/2 and Windows metrics table + # (needs data from head table) + if self.table.has_key("OS/2"): + self.seek_table("OS/2") + version = self.read_ushort() + self.skip(2) + usWeightClass = self.read_ushort() + self.skip(2) + fsType = self.read_ushort() + if fsType == 0x0002 or (fsType & 0x0300) != 0: + raise TTFError, 'Font does not allow subsetting/embedding (%04X)' % fsType + self.skip(58) #11*2 + 10 + 4*4 + 4 + 3*2 + sTypoAscender = self.read_short() + sTypoDescender = self.read_short() + self.ascent = scale(sTypoAscender) # XXX: for some reason it needs to be multiplied by 1.24--1.28 + self.descent = scale(sTypoDescender) + + if version > 1: + self.skip(16) #3*2 + 2*4 + 2 + sCapHeight = self.read_short() + self.capHeight = scale(sCapHeight) + else: + self.capHeight = self.ascent + else: + # Microsoft TTFs require an OS/2 table; Apple ones do not. Try to + # cope. The data is not very important anyway. + usWeightClass = 500 + self.ascent = scale(yMax) + self.descent = scale(yMin) + self.capHeight = self.ascent + + # There's no way to get stemV from a TTF file short of analyzing actual outline data + # This fuzzy formula is taken from pdflib sources, but we could just use 0 here + self.stemV = 50 + int((usWeightClass / 65.0) ** 2) + + # post - PostScript table + # (needs data from OS/2 table) + self.seek_table("post") + ver_maj, ver_min = self.read_ushort(), self.read_ushort() + if ver_maj not in (1, 2, 3, 4): + # Adobe/MS documents 1, 2, 2.5, 3; Apple also has 4. + # From Apple docs it seems that we do not need to care + # about the exact version, so if you get this error, you can + # try to remove this check altogether. + raise TTFError, 'Unknown post table version %d.%04x' % (ver_maj, ver_min) + self.italicAngle = self.read_short() + self.read_ushort() / 65536.0 + self.skip(4) #2*2 + isFixedPitch = self.read_ulong() + + self.flags = FF_SYMBOLIC # All fonts that contain characters + # outside the original Adobe character + # set are considered "symbolic". + if self.italicAngle != 0: + self.flags = self.flags | FF_ITALIC + if usWeightClass >= 600: # FW_REGULAR == 500, FW_SEMIBOLD == 600 + self.flags = self.flags | FF_FORCEBOLD + if isFixedPitch: + self.flags = self.flags | FF_FIXED + # XXX: FF_SERIF? FF_SCRIPT? FF_ALLCAP? FF_SMALLCAP? + + # hhea - Horizontal header table + self.seek_table("hhea") + ver_maj, ver_min = self.read_ushort(), self.read_ushort() + if ver_maj != 1: + raise TTFError, 'Unknown hhea table version %d.%04x' % (ver_maj, ver_min) + self.skip(28) + metricDataFormat = self.read_ushort() + if metricDataFormat != 0: + raise TTFError, 'Unknown horizontal metric data format (%d)' % metricDataFormat + numberOfHMetrics = self.read_ushort() + if numberOfHMetrics == 0: + raise TTFError, 'Number of horizontal metrics is 0' + + # maxp - Maximum profile table + self.seek_table("maxp") + ver_maj, ver_min = self.read_ushort(), self.read_ushort() + if ver_maj != 1: + raise TTFError, 'Unknown maxp table version %d.%04x' % (ver_maj, ver_min) + numGlyphs = self.read_ushort() + + if not charInfo: + self.charToGlyph = None + self.defaultWidth = None + self.charWidths = None + return + + if glyphDataFormat != 0: + raise TTFError, 'Unknown glyph data format (%d)' % glyphDataFormat + + # cmap - Character to glyph index mapping table + cmap_offset = self.seek_table("cmap") + self.skip(2) + cmapTableCount = self.read_ushort() + unicode_cmap_offset = None + for n in xrange(cmapTableCount): + platformID = self.read_ushort() + encodingID = self.read_ushort() + offset = self.read_ulong() + if platformID == 3 and encodingID == 1: # Microsoft, Unicode + format = self.get_ushort(cmap_offset + offset) + if format == 4: + unicode_cmap_offset = cmap_offset + offset + break + elif platformID == 0: # Unicode -- assume all encodings are compatible + format = self.get_ushort(cmap_offset + offset) + if format == 4: + unicode_cmap_offset = cmap_offset + offset + break + if unicode_cmap_offset is None: + raise TTFError, 'Font does not have cmap for Unicode (platform 3, encoding 1, format 4 or platform 0 any encoding format 4)' + self.seek(unicode_cmap_offset + 2) + length = self.read_ushort() + limit = unicode_cmap_offset + length + self.skip(2) + segCount = self.read_ushort() / 2 + self.skip(6) + endCount = map(lambda x, self=self: self.read_ushort(), xrange(segCount)) + self.skip(2) + startCount = map(lambda x, self=self: self.read_ushort(), xrange(segCount)) + idDelta = map(lambda x, self=self: self.read_short(), xrange(segCount)) + idRangeOffset_start = self._pos + idRangeOffset = map(lambda x, self=self: self.read_ushort(), xrange(segCount)) + + # Now it gets tricky. + glyphToChar = {} + charToGlyph = {} + for n in xrange(segCount): + for unichar in xrange(startCount[n], endCount[n] + 1): + if idRangeOffset[n] == 0: + glyph = (unichar + idDelta[n]) & 0xFFFF + else: + offset = (unichar - startCount[n]) * 2 + idRangeOffset[n] + offset = idRangeOffset_start + 2 * n + offset + if offset >= limit: + # workaround for broken fonts (like Thryomanes) + glyph = 0 + else: + glyph = self.get_ushort(offset) + if glyph != 0: + glyph = (glyph + idDelta[n]) & 0xFFFF + charToGlyph[unichar] = glyph + if glyphToChar.has_key(glyph): + glyphToChar[glyph].append(unichar) + else: + glyphToChar[glyph] = [unichar] + self.charToGlyph = charToGlyph + + # hmtx - Horizontal metrics table + # (needs data from hhea, maxp, and cmap tables) + self.seek_table("hmtx") + aw = None + self.charWidths = {} + self.hmetrics = [] + for glyph in xrange(numberOfHMetrics): + # advance width and left side bearing. lsb is actually signed + # short, but we don't need it anyway (except for subsetting) + aw, lsb = self.read_ushort(), self.read_ushort() + self.hmetrics.append((aw, lsb)) + aw = scale(aw) + if glyph == 0: + self.defaultWidth = aw + if glyphToChar.has_key(glyph): + for char in glyphToChar[glyph]: + self.charWidths[char] = aw + for glyph in xrange(numberOfHMetrics, numGlyphs): + # the rest of the table only lists advance left side bearings. + # so we reuse aw set by the last iteration of the previous loop + lsb = self.read_ushort() + self.hmetrics.append((aw, lsb)) + if glyphToChar.has_key(glyph): + for char in glyphToChar[glyph]: + self.charWidths[char] = aw + + # loca - Index to location + self.seek_table('loca') + self.glyphPos = [] + if indexToLocFormat == 0: + for n in xrange(numGlyphs + 1): + self.glyphPos.append(self.read_ushort() << 1) + elif indexToLocFormat == 1: + for n in xrange(numGlyphs + 1): + self.glyphPos.append(self.read_ulong()) + else: + raise TTFError, 'Unknown location table format (%d)' % indexToLocFormat + + # Subsetting + + def makeSubset(self, subset): + """Create a subset of a TrueType font""" + output = TTFontMaker() + + # Build a mapping of glyphs in the subset to glyph numbers in + # the original font. Also build a mapping of UCS codes to + # glyph values in the new font. + + # Start with 0 -> 0: "missing character" + glyphMap = [0] # new glyph index -> old glyph index + glyphSet = {0:0} # old glyph index -> new glyph index + codeToGlyph = {} # unicode -> new glyph index + for code in subset: + if self.charToGlyph.has_key(code): + originalGlyphIdx = self.charToGlyph[code] + else: + originalGlyphIdx = 0 + if not glyphSet.has_key(originalGlyphIdx): + glyphSet[originalGlyphIdx] = len(glyphMap) + glyphMap.append(originalGlyphIdx) + codeToGlyph[code] = glyphSet[originalGlyphIdx] + + # Also include glyphs that are parts of composite glyphs + start = self.get_table_pos('glyf')[0] + n = 0 + while n < len(glyphMap): + originalGlyphIdx = glyphMap[n] + glyphPos = self.glyphPos[originalGlyphIdx] + glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos + self.seek(start + glyphPos) + numberOfContours = self.read_short() + if numberOfContours < 0: + # composite glyph + self.skip(8) + flags = GF_MORE_COMPONENTS + while flags & GF_MORE_COMPONENTS: + flags = self.read_ushort() + glyphIdx = self.read_ushort() + if not glyphSet.has_key(glyphIdx): + glyphSet[glyphIdx] = len(glyphMap) + glyphMap.append(glyphIdx) + if flags & GF_ARG_1_AND_2_ARE_WORDS: + self.skip(4) + else: + self.skip(2) + if flags & GF_WE_HAVE_A_SCALE: + self.skip(2) + elif flags & GF_WE_HAVE_AN_X_AND_Y_SCALE: + self.skip(4) + elif flags & GF_WE_HAVE_A_TWO_BY_TWO: + self.skip(8) + n = n + 1 + + numGlyphs = n = len(glyphMap) + while n > 1 and self.hmetrics[n][0] == self.hmetrics[n - 1][0]: + n = n - 1 + numberOfHMetrics = n + + # The following tables are simply copied from the original + for tag in ('name', 'OS/2', 'cvt ', 'fpgm', 'prep'): + try: + output.add(tag, self.get_table(tag)) + except KeyError: + # Apparently some of the tables are optional (cvt, fpgm, prep). + # The lack of the required ones (name, OS/2) would have already + # been caught before. + pass + + # post - PostScript + post = "\x00\x03\x00\x00" + self.get_table('post')[4:16] + "\x00" * 16 + output.add('post', post) + + # hhea - Horizontal Header + hhea = self.get_table('hhea') + hhea = _set_ushort(hhea, 34, numberOfHMetrics) + output.add('hhea', hhea) + + # maxp - Maximum Profile + maxp = self.get_table('maxp') + maxp = _set_ushort(maxp, 4, numGlyphs) + output.add('maxp', maxp) + + # cmap - Character to glyph mapping + # XXX maybe use format 0 if possible, not 6? + entryCount = len(subset) + length = 10 + entryCount * 2 + cmap = [0, 1, # version, number of tables + 1, 0, 0,12, # platform, encoding, offset (hi,lo) + 6, length, 0, # format, length, language + 0, + entryCount] + \ + map(codeToGlyph.get, subset) + cmap = apply(pack, [">%dH" % len(cmap)] + cmap) + output.add('cmap', cmap) + + # hmtx - Horizontal Metrics + hmtx = [] + for n in xrange(numGlyphs): + originalGlyphIdx = glyphMap[n] + aw, lsb = self.hmetrics[originalGlyphIdx] + if n < numberOfHMetrics: + hmtx.append(aw) + hmtx.append(lsb) + hmtx = apply(pack, [">%dH" % len(hmtx)] + hmtx) + output.add('hmtx', hmtx) + + # glyf - Glyph data + glyphData = self.get_table('glyf') + offsets = [] + glyf = [] + pos = 0 + for n in xrange(numGlyphs): + offsets.append(pos) + originalGlyphIdx = glyphMap[n] + glyphPos = self.glyphPos[originalGlyphIdx] + glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos + data = glyphData[glyphPos:glyphPos+glyphLen] + # Fix references in composite glyphs + if glyphLen > 2 and unpack(">h", data[:2])[0] < 0: + # composite glyph + pos_in_glyph = 10 + flags = GF_MORE_COMPONENTS + while flags & GF_MORE_COMPONENTS: + flags = unpack(">H", data[pos_in_glyph:pos_in_glyph+2])[0] + glyphIdx = unpack(">H", data[pos_in_glyph+2:pos_in_glyph+4])[0] + data = _set_ushort(data, pos_in_glyph + 2, glyphSet[glyphIdx]) + pos_in_glyph = pos_in_glyph + 4 + if flags & GF_ARG_1_AND_2_ARE_WORDS: + pos_in_glyph = pos_in_glyph + 4 + else: + pos_in_glyph = pos_in_glyph + 2 + if flags & GF_WE_HAVE_A_SCALE: + pos_in_glyph = pos_in_glyph + 2 + elif flags & GF_WE_HAVE_AN_X_AND_Y_SCALE: + pos_in_glyph = pos_in_glyph + 4 + elif flags & GF_WE_HAVE_A_TWO_BY_TWO: + pos_in_glyph = pos_in_glyph + 8 + glyf.append(data) + pos = pos + glyphLen + if pos % 4 != 0: + padding = 4 - pos % 4 + glyf.append('\0' * padding) + pos = pos + padding + offsets.append(pos) + output.add('glyf', string.join(glyf, "")) + + # loca - Index to location + loca = [] + if (pos + 1) >> 1 > 0xFFFF: + indexToLocFormat = 1 # long format + for offset in offsets: + loca.append(offset) + loca = apply(pack, [">%dL" % len(loca)] + loca) + else: + indexToLocFormat = 0 # short format + for offset in offsets: + loca.append(offset >> 1) + loca = apply(pack, [">%dH" % len(loca)] + loca) + output.add('loca', loca) + + # head - Font header + head = self.get_table('head') + head = _set_ushort(head, 50, indexToLocFormat) + output.add('head', head) + + return output.makeStream() + + +# +# TrueType font embedding +# + +# PDF font flags (see PDF Reference Guide table 5.19) +FF_FIXED = 1 << 1-1 +FF_SERIF = 1 << 2-1 +FF_SYMBOLIC = 1 << 3-1 +FF_SCRIPT = 1 << 4-1 +FF_NONSYMBOLIC = 1 << 6-1 +FF_ITALIC = 1 << 7-1 +FF_ALLCAP = 1 << 17-1 +FF_SMALLCAP = 1 << 18-1 +FF_FORCEBOLD = 1 << 19-1 + +class TTFontFace(TTFontFile, pdfmetrics.TypeFace): + """TrueType typeface. + + Conceptually similar to a single byte typeface, but the glyphs are + identified by UCS character codes instead of glyph names.""" + + def __init__(self, filename, validate=0, subfontIndex=0): + "Loads a TrueType font from filename." + pdfmetrics.TypeFace.__init__(self, None) + TTFontFile.__init__(self, filename, validate=validate, subfontIndex=subfontIndex) + + def getCharWidth(self, code): + "Returns the width of character U+" + return self.charWidths.get(code, self.defaultWidth) + + def addSubsetObjects(self, doc, fontname, subset): + """Generate a TrueType font subset and add it to the PDF document. + Returns a PDFReference to the new FontDescriptor object.""" + + fontFile = pdfdoc.PDFStream() + fontFile.content = self.makeSubset(subset) + fontFile.dictionary['Length1'] = len(fontFile.content) + if doc.compression: + fontFile.filters = [pdfdoc.PDFZCompress] + fontFileRef = doc.Reference(fontFile, 'fontFile:%s(%s)' % (self.filename, fontname)) + + flags = self.flags & ~ FF_NONSYMBOLIC + flags = flags | FF_SYMBOLIC + + fontDescriptor = pdfdoc.PDFDictionary({ + 'Type': '/FontDescriptor', + 'Ascent': self.ascent, + 'CapHeight': self.capHeight, + 'Descent': self.descent, + 'Flags': flags, + 'FontBBox': pdfdoc.PDFArray(self.bbox), + 'FontName': pdfdoc.PDFName(fontname), + 'ItalicAngle': self.italicAngle, + 'StemV': self.stemV, + 'FontFile2': fontFileRef, + }) + return doc.Reference(fontDescriptor, 'fontDescriptor:' + fontname) + +class TTEncoding: + """Encoding for TrueType fonts (always UTF-8). + + TTEncoding does not directly participate in PDF object creation, since + we need a number of different 8-bit encodings for every generated font + subset. TTFont itself cares about that.""" + + def __init__(self): + self.name = "UTF-8" + + +class TTFont: + """Represents a TrueType font. + + Its encoding is always UTF-8. + + Note: you cannot use the same TTFont object for different documents + at the same time. + + Example of usage: + + font = ttfonts.TTFont('PostScriptFontName', '/path/to/font.ttf') + pdfmetrics.registerFont(font) + + canvas.setFont('PostScriptFontName', size) + canvas.drawString(x, y, "Some text encoded in UTF-8") + """ + + class State: + def __init__(self): + self.assignments = {} + self.nextCode = 0 + self.internalName = None + self.frozen = 0 + + # Let's add the first 128 unicodes to the 0th subset, so ' ' + # always has code 32 (for word spacing to work) and the ASCII + # output is readable + subset0 = range(128) + self.subsets = [subset0] + for n in subset0: + self.assignments[n] = n + self.nextCode = 128 + + def __init__(self, name, filename, validate=0, subfontIndex=0): + """Loads a TrueType font from filename. + + If validate is set to a false values, skips checksum validation. This + can save time, especially if the font is large. + """ + self.fontName = name + self.face = TTFontFace(filename, validate=validate, subfontIndex=subfontIndex) + self.encoding = TTEncoding() + self._multiByte = 1 # We want our own stringwidth + self._dynamicFont = 1 # We want dynamic subsetting + self.state = {} + + def _py_stringWidth(self, text, size, encoding='utf-8'): + "Calculate text width" + if type(text) is not UnicodeType: + text = unicode(text, encoding or 'utf-8') # encoding defaults to utf-8 + g = self.face.charWidths.get + dw = self.face.defaultWidth + return 0.001*size*sum([g(ord(u),dw) for u in text]) + stringWidth = _py_stringWidth + + def splitString(self, text, doc, encoding='utf-8'): + """Splits text into a number of chunks, each of which belongs to a + single subset. Returns a list of tuples (subset, string). Use subset + numbers with getSubsetInternalName. Doc is needed for distinguishing + subsets when building different documents at the same time.""" + try: state = self.state[doc] + except KeyError: state = self.state[doc] = TTFont.State() + curSet = -1 + cur = [] + results = [] + if type(text) is not UnicodeType: + text = unicode(text, encoding or 'utf-8') # encoding defaults to utf-8 + for code in map(ord,text): + if state.assignments.has_key(code): + n = state.assignments[code] + else: + if state.frozen: + raise pdfdoc.PDFError, "Font %s is already frozen, cannot add new character U+%04X" % (self.fontName, code) + n = state.nextCode + if n & 0xFF == 32: + # make code 32 always be a space character + state.subsets[n >> 8].append(32) + state.nextCode += 1 + n = state.nextCode + state.nextCode += 1 + state.assignments[code] = n + if (n & 0xFF) == 0: + state.subsets.append([]) + state.subsets[n >> 8].append(code) + if (n >> 8) != curSet: + if cur: + results.append((curSet, string.join(map(chr, cur), ""))) + curSet = (n >> 8) + cur = [] + cur.append(n & 0xFF) + if cur: + results.append((curSet, string.join(map(chr, cur), ""))) + return results + + def getSubsetInternalName(self, subset, doc): + """Returns the name of a PDF Font object corresponding to a given + subset of this dynamic font. Use this function instead of + PDFDocument.getInternalFontName.""" + try: state = self.state[doc] + except KeyError: state = self.state[doc] = TTFont.State() + if subset < 0 or subset >= len(state.subsets): + raise IndexError, 'Subset %d does not exist in font %s' % (subset, self.fontName) + if state.internalName is None: + state.internalName = 'F%d' % (len(doc.fontMapping) + 1) + doc.fontMapping[self.fontName] = '/' + state.internalName + doc.delayedFonts.append(self) + return '/%s+%d' % (state.internalName, subset) + + def addObjects(self, doc): + """Makes one or more PDF objects to be added to the document. The + caller supplies the internal name to be used (typically F1, F2, ... in + sequence). + + This method creates a number of Font and FontDescriptor objects. Every + FontDescriptor is a (no more than) 256 character subset of the original + TrueType font.""" + try: state = self.state[doc] + except KeyError: state = self.state[doc] = TTFont.State() + state.frozen = 1 + for n in xrange(len(state.subsets)): + subset = state.subsets[n] + internalName = self.getSubsetInternalName(n, doc)[1:] + baseFontName = "%s+%s%s" % (SUBSETN(n),self.face.name,self.face.subfontNameX) + + pdfFont = pdfdoc.PDFTrueTypeFont() + pdfFont.__Comment__ = 'Font %s subset %d' % (self.fontName, n) + pdfFont.Name = internalName + pdfFont.BaseFont = baseFontName + + pdfFont.FirstChar = 0 + pdfFont.LastChar = len(subset) - 1 + + widths = map(self.face.getCharWidth, subset) + pdfFont.Widths = pdfdoc.PDFArray(widths) + + cmapStream = pdfdoc.PDFStream() + cmapStream.content = makeToUnicodeCMap(baseFontName, subset) + if doc.compression: + cmapStream.filters = [pdfdoc.PDFZCompress] + pdfFont.ToUnicode = doc.Reference(cmapStream, 'toUnicodeCMap:' + baseFontName) + + pdfFont.FontDescriptor = self.face.addSubsetObjects(doc, baseFontName, subset) + + # link it in + ref = doc.Reference(pdfFont, internalName) + fontDict = doc.idToObject['BasicFonts'].dict + fontDict[internalName] = pdfFont + del self.state[doc] +try: + from _rl_accel import _instanceStringWidthTTF + import new + TTFont.stringWidth = new.instancemethod(_instanceStringWidthTTF,None,TTFont) +except ImportError: + pass diff --git a/bin/reportlab/pdfgen/__init__.py b/bin/reportlab/pdfgen/__init__.py new file mode 100755 index 00000000000..9dc4437c817 --- /dev/null +++ b/bin/reportlab/pdfgen/__init__.py @@ -0,0 +1,5 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfgen/__init__.py +__version__=''' $Id: __init__.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__='' \ No newline at end of file diff --git a/bin/reportlab/pdfgen/canvas.py b/bin/reportlab/pdfgen/canvas.py new file mode 100755 index 00000000000..d06e9670997 --- /dev/null +++ b/bin/reportlab/pdfgen/canvas.py @@ -0,0 +1,1462 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfgen/canvas.py +__version__=''' $Id: canvas.py 2854 2006-05-10 12:57:21Z rgbecker $ ''' +__doc__=""" +The Canvas object is the primary interface for creating PDF files. See +doc/userguide.pdf for copious examples. +""" +ENABLE_TRACKING = 1 # turn this off to do profile testing w/o tracking + +import os +import sys +import re +from string import join, split, strip, atoi, replace, upper, digits +import tempfile +from types import * +from math import sin, cos, tan, pi, ceil +import md5 + +from reportlab import rl_config +from reportlab.pdfbase import pdfutils +from reportlab.pdfbase import pdfdoc +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen import pdfgeom, pathobject, textobject +from reportlab.lib.utils import import_zlib +from reportlab.lib.utils import fp_str + + +digitPat = re.compile('\d') #used in decimal alignment + +zlib = import_zlib() +_SeqTypes=(TupleType,ListType) + +# Robert Kern +# Constants for closing paths. +# May be useful if one changes 'arc' and 'rect' to take a +# default argument that tells how to close the path. +# That way we can draw filled shapes. + +FILL_EVEN_ODD = 0 +FILL_NON_ZERO = 1 + #this is used by path-closing routines. + #map stroke, fill, fillmode -> operator + # fillmode: 1 = non-Zero (obviously), 0 = evenOdd +PATH_OPS = {(0, 0, FILL_EVEN_ODD) : 'n', #no op + (0, 0, FILL_NON_ZERO) : 'n', #no op + (1, 0, FILL_EVEN_ODD) : 'S', #stroke only + (1, 0, FILL_NON_ZERO) : 'S', #stroke only + (0, 1, FILL_EVEN_ODD) : 'f*', #Fill only + (0, 1, FILL_NON_ZERO) : 'f', #Fill only + (1, 1, FILL_EVEN_ODD) : 'B*', #Stroke and Fill + (1, 1, FILL_NON_ZERO) : 'B', #Stroke and Fill + } + +_escapePDF = pdfutils._escape +_instanceEscapePDF = pdfutils._instanceEscapePDF + +if sys.hexversion >= 0x02000000: + def _digester(s): + return md5.md5(s).hexdigest() +else: + # hexdigest not available in 1.5 + def _digester(s): + return join(map(lambda x : "%02x" % ord(x), md5.md5(s).digest()), '') + +def _annFormat(D,color,thickness,dashArray): + from reportlab.pdfbase.pdfdoc import PDFArray + if color: + D["C"] = PDFArray([color.red, color.green, color.blue]) + border = [0,0,0] + if thickness: + border[2] = thickness + if dashArray: + border.append(PDFArray(dashArray)) + D["Border"] = PDFArray(border) + +class Canvas(textobject._PDFColorSetter): + """This class is the programmer's interface to the PDF file format. Methods + are (or will be) provided here to do just about everything PDF can do. + + The underlying model to the canvas concept is that of a graphics state machine + that at any given point in time has a current font, fill color (for figure + interiors), stroke color (for figure borders), line width and geometric transform, among + many other characteristics. + + Canvas methods generally either draw something (like canvas.line) using the + current state of the canvas or change some component of the canvas + state (like canvas.setFont). The current state can be saved and restored + using the saveState/restoreState methods. + + Objects are "painted" in the order they are drawn so if, for example + two rectangles overlap the last draw will appear "on top". PDF form + objects (supported here) are used to draw complex drawings only once, + for possible repeated use. + + There are other features of canvas which are not visible when printed, + such as outlines and bookmarks which are used for navigating a document + in a viewer. + + Here is a very silly example usage which generates a Hello World pdf document. + + from reportlab.pdfgen import canvas + c = canvas.Canvas("hello.pdf") + from reportlab.lib.units import inch + # move the origin up and to the left + c.translate(inch,inch) + # define a large font + c.setFont("Helvetica", 80) + # choose some colors + c.setStrokeColorRGB(0.2,0.5,0.3) + c.setFillColorRGB(1,0,1) + # draw a rectangle + c.rect(inch,inch,6*inch,9*inch, fill=1) + # make text go straight up + c.rotate(90) + # change color + c.setFillColorRGB(0,0,0.77) + # say hello (note after rotate the y coord needs to be negative!) + c.drawString(3*inch, -3*inch, "Hello World") + c.showPage() + c.save() + """ + + def __init__(self,filename, + pagesize=None, + bottomup = 1, + pageCompression=None, + invariant = None, + verbosity=0): + """Create a canvas of a given size. etc. + + You may pass a file-like object to filename as an alternative to + a string. + + Most of the attributes are private - we will use set/get methods + as the preferred interface. Default page size is A4.""" + if pagesize is None: pagesize = rl_config.defaultPageSize + if invariant is None: invariant = rl_config.invariant + self._filename = filename + + self._doc = pdfdoc.PDFDocument(compression=pageCompression, + invariant=invariant, filename=filename) + + + #this only controls whether it prints 'saved ...' - 0 disables + self._verbosity = verbosity + + #this is called each time a page is output if non-null + self._onPage = None + + self._pagesize = pagesize + self._pageRotation = 0 + #self._currentPageHasImages = 0 + self._pageTransition = None + self._pageDuration = None + self._destinations = {} # dictionary of destinations for cross indexing. + + self.setPageCompression(pageCompression) + self._pageNumber = 1 # keep a count + #self3 = [] #where the current page's marking operators accumulate + # when we create a form we need to save operations not in the form + self._codeStack = [] + self._restartAccumulators() # restart all accumulation state (generalized, arw) + self._annotationCount = 0 + + self._outlines = [] # list for a name tree + self._psCommandsBeforePage = [] #for postscript tray/font commands + self._psCommandsAfterPage = [] #for postscript tray/font commands + + #PostScript has the origin at bottom left. It is easy to achieve a top- + #down coord system by translating to the top of the page and setting y + #scale to -1, but then text is inverted. So self.bottomup is used + #to also set the text matrix accordingly. You can now choose your + #drawing coordinates. + self.bottomup = bottomup + self.imageCaching = rl_config.defaultImageCaching + self._make_preamble() + self.init_graphics_state() + self.state_stack = [] + + def init_graphics_state(self): + #initial graphics state, never modify any of these in place + self._x = 0 + self._y = 0 + self._fontname = 'Times-Roman' + self._fontsize = 12 + + self._dynamicFont = 0 + self._textMode = 0 #track if between BT/ET + self._leading = 14.4 + self._currentMatrix = (1., 0., 0., 1., 0., 0.) + self._fillMode = 0 #even-odd + + #text state + self._charSpace = 0 + self._wordSpace = 0 + self._horizScale = 100 + self._textRenderMode = 0 + self._rise = 0 + self._textLineMatrix = (1., 0., 0., 1., 0., 0.) + self._textMatrix = (1., 0., 0., 1., 0., 0.) + + # line drawing + self._lineCap = 0 + self._lineJoin = 0 + self._lineDash = None #not done + self._lineWidth = 0 + self._mitreLimit = 0 + + self._fillColorRGB = (0,0,0) + self._strokeColorRGB = (0,0,0) + + def push_state_stack(self): + state = {} + d = self.__dict__ + for name in self.STATE_ATTRIBUTES: + state[name] = d[name] #getattr(self, name) + self.state_stack.append(state) + + def pop_state_stack(self): + state = self.state_stack[-1] + del self.state_stack[-1] + d = self.__dict__ + d.update(state) + + STATE_ATTRIBUTES = split(""" + _x _y _fontname _fontsize _dynamicFont _textMode _leading _currentMatrix _fillMode + _fillMode _charSpace _wordSpace _horizScale _textRenderMode _rise _textLineMatrix + _textMatrix _lineCap _lineJoin _lineDash _lineWidth _mitreLimit _fillColorRGB + _strokeColorRGB""") + STATE_RANGE = range(len(STATE_ATTRIBUTES)) + + #self._addStandardFonts() + + def _make_preamble(self): + # yuk + iName = self._doc.getInternalFontName('Helvetica') + if self.bottomup: + #must set an initial font + self._preamble = '1 0 0 1 0 0 cm BT %s 12 Tf 14.4 TL ET' % iName + else: + #switch coordinates, flip text and set font + self._preamble = '1 0 0 -1 0 %s cm BT %s 12 Tf 14.4 TL ET' % (fp_str(self._pagesize[1]), iName) + + if not _instanceEscapePDF: + def _escape(self, s): + return _escapePDF(s) + + #info functions - non-standard + def setAuthor(self, author): + """identify the author for invisible embedding inside the PDF document. + the author annotation will appear in the the text of the file but will + not automatically be seen when the document is viewed, but is visible + in document properties etc etc.""" + self._doc.setAuthor(author) + + def setDateFormatter(self, dateFormatter): + """accepts a func(yyyy,mm,dd,hh,m,s) used to create embedded formatted date""" + self._doc.setDateFormatter(dateFormatter) + + def addOutlineEntry(self, title, key, level=0, closed=None): + """Adds a new entry to the outline at given level. If LEVEL not specified, + entry goes at the top level. If level specified, it must be + no more than 1 greater than the outline level in the last call. + + The key must be the (unique) name of a bookmark. + the title is the (non-unique) name to be displayed for the entry. + + If closed is set then the entry should show no subsections by default + when displayed. + + Example + c.addOutlineEntry("first section", "section1") + c.addOutlineEntry("introduction", "s1s1", 1, closed=1) + c.addOutlineEntry("body", "s1s2", 1) + c.addOutlineEntry("detail1", "s1s2s1", 2) + c.addOutlineEntry("detail2", "s1s2s2", 2) + c.addOutlineEntry("conclusion", "s1s3", 1) + c.addOutlineEntry("further reading", "s1s3s1", 2) + c.addOutlineEntry("second section", "section1") + c.addOutlineEntry("introduction", "s2s1", 1) + c.addOutlineEntry("body", "s2s2", 1, closed=1) + c.addOutlineEntry("detail1", "s2s2s1", 2) + c.addOutlineEntry("detail2", "s2s2s2", 2) + c.addOutlineEntry("conclusion", "s2s3", 1) + c.addOutlineEntry("further reading", "s2s3s1", 2) + + generated outline looks like + - first section + |- introduction + |- body + | |- detail1 + | |- detail2 + |- conclusion + | |- further reading + - second section + |- introduction + |+ body + |- conclusion + | |- further reading + + Note that the second "body" is closed. + + Note that you can jump from level 5 to level 3 but not + from 3 to 5: instead you need to provide all intervening + levels going down (4 in this case). Note that titles can + collide but keys cannot. + """ + #to be completed + #self._outlines.append(title) + self._doc.outline.addOutlineEntry(key, level, title, closed=closed) + + def setOutlineNames0(self, *nametree): # keep this for now (?) + """nametree should can be a recursive tree like so + c.setOutlineNames( + "chapter1dest", + ("chapter2dest", + ["chapter2section1dest", + "chapter2section2dest", + "chapter2conclusiondest"] + ), # end of chapter2 description + "chapter3dest", + ("chapter4dest", ["c4s1", "c4s2"]) + ) + each of the string names inside must be bound to a bookmark + before the document is generated. + """ + self._doc.outline.setNames(*((self,)+nametree)) + + def setTitle(self, title): + """write a title into the PDF file that won't automatically display + in the document itself.""" + self._doc.setTitle(title) + + def setSubject(self, subject): + """write a subject into the PDF file that won't automatically display + in the document itself.""" + self._doc.setSubject(subject) + + def pageHasData(self): + "Info function - app can call it after showPage to see if it needs a save" + return len(self._code) == 0 + + def showOutline(self): + """Specify that Acrobat Reader should start with the outline tree visible. + showFullScreen() and showOutline() conflict; the one called last + wins.""" + self._doc._catalog.showOutline() + + def showFullScreen0(self): + """Specify that Acrobat Reader should start in full screen mode. + showFullScreen() and showOutline() conflict; the one called last + wins.""" + self._doc._catalog.showFullScreen() + + def showPage(self): + """Close the current page and possibly start on a new page.""" + + # ensure a space at the end of the stream - Acrobat does + # not mind, but Ghostscript dislikes 'Qendstream' even if + # the length marker finishes after 'Q' + self._code.append(' ') + page = pdfdoc.PDFPage() + page.pagewidth = self._pagesize[0] + page.pageheight = self._pagesize[1] + page.Rotate = self._pageRotation + page.hasImages = self._currentPageHasImages + page.setPageTransition(self._pageTransition) + page.setCompression(self._pageCompression) + if self._pageDuration is not None: + page.Dur = self._pageDuration + + strm = self._psCommandsBeforePage + [self._preamble] + self._code + self._psCommandsAfterPage + page.setStream(strm) + self._setXObjects(page) + self._setAnnotations(page) + self._doc.addPage(page) + + if self._onPage: self._onPage(self._pageNumber) + self._startPage() + + def _startPage(self): + #now get ready for the next one + self._pageNumber = self._pageNumber+1 + self._restartAccumulators() + self.init_graphics_state() + self.state_stack = [] + + def setPageCallBack(self, func): + """func(pageNum) will be called on each page end. + + This is mainly a hook for progress monitoring. + Call setPageCallback(None) to clear a callback.""" + self._onPage = func + + def _setAnnotations(self,page): + page.Annots = self._annotationrefs + + def _setXObjects(self, thing): + """for pages and forms, define the XObject dictionary for resources, if needed""" + forms = self._formsinuse + if forms: + xobjectsdict = self._doc.xobjDict(forms) + thing.XObjects = xobjectsdict + else: + thing.XObjects = None + + def _bookmarkReference(self, name): + """get a reference to a (possibly undefined, possibly unbound) bookmark""" + d = self._destinations + try: + return d[name] + except: + result = d[name] = pdfdoc.Destination(name) # newly defined, unbound + return result + + def bookmarkPage(self, key, + fit="Fit", + left=None, + top=None, + bottom=None, + right=None, + zoom=None + ): + """ + This creates a bookmark to the current page which can + be referred to with the given key elsewhere. + + PDF offers very fine grained control over how Acrobat + reader is zoomed when people link to this. The default + is to keep the user's current zoom settings. the last + arguments may or may not be needed depending on the + choice of 'fitType'. + + Fit types and the other arguments they use are: + /XYZ left top zoom - fine grained control. null + or zero for any of the parameters means 'leave + as is', so "0,0,0" will keep the reader's settings. + NB. Adobe Reader appears to prefer "null" to 0's. + + /Fit - entire page fits in window + + /FitH top - top coord at top of window, width scaled + to fit. + + /FitV left - left coord at left of window, height + scaled to fit + + /FitR left bottom right top - scale window to fit + the specified rectangle + + (question: do we support /FitB, FitBH and /FitBV + which are hangovers from version 1.1 / Acrobat 3.0?)""" + dest = self._bookmarkReference(key) + self._doc.inPage() # try to enable page-only features + pageref = self._doc.thisPageRef() + + #None = "null" for PDF + if left is None: + left = "null" + if top is None: + top = "null" + if bottom is None: + bottom = "null" + if right is None: + right = "null" + if zoom is None: + zoom = "null" + + if fit == "XYZ": + dest.xyz(left,top,zoom) + elif fit == "Fit": + dest.fit() + elif fit == "FitH": + dest.fith(top) + elif fit == "FitV": + dest.fitv(left) + elif fit == "FitR": + dest.fitr(left,bottom,right,top) + #Do we need these (version 1.1 / Acrobat 3 versions)? + elif fit == "FitB": + dest.fitb() + elif fit == "FitBH": + dest.fitbh(top) + elif fit == "FitBV": + dest.fitbv(left) + else: + raise "Unknown Fit type %s" % (fit,) + + dest.setPage(pageref) + return dest + + def bookmarkHorizontalAbsolute(self, key, top, left=0, fit='XYZ', **kw): + """Bind a bookmark (destination) to the current page at a horizontal position. + Note that the yhorizontal of the book mark is with respect to the default + user space (where the origin is at the lower left corner of the page) + and completely ignores any transform (translation, scale, skew, rotation, + etcetera) in effect for the current graphics state. The programmer is + responsible for making sure the bookmark matches an appropriate item on + the page.""" + #This method should probably be deprecated since it is just a sub-set of bookmarkPage + return self.bookmarkPage(key, fit=fit, top=top, left=left, zoom=0) + + def bookmarkHorizontal(self, key, relativeX, relativeY, **kw): + """w.r.t. the current transformation, bookmark this horizontal.""" + (left, top) = self.absolutePosition(relativeX,relativeY) + self.bookmarkHorizontalAbsolute(key, top, left=left, **kw) + + #def _inPage0(self): disallowed! + # """declare a page, enable page features""" + # self._doc.inPage() + + #def _inForm0(self): + # "deprecated in favore of beginForm...endForm" + # self._doc.inForm() + + def doForm(self, name): + """use a form XObj in current operation stream. + + The form should either have been defined previously using + beginForm ... endForm, or may be defined later. If it is not + defined at save time, an exception will be raised. The form + will be drawn within the context of the current graphics + state.""" + self._code.append("/%s Do" % self._doc.getXObjectName(name)) + self._formsinuse.append(name) + + def hasForm(self, name): + """Query whether form XObj really exists yet.""" + return self._doc.hasForm(name) + + + ###################################################### + # + # Image routines + # + ###################################################### + + def drawInlineImage(self, image, x,y, width=None,height=None): + """Draw an Image into the specified rectangle. If width and + height are omitted, they are calculated from the image size. + Also allow file names as well as images. The size in pixels + of the image is returned.""" + + self._currentPageHasImages = 1 + from pdfimages import PDFImage + img_obj = PDFImage(image, x,y, width, height) + img_obj.drawInlineImage(self) + return (img_obj.width, img_obj.height) + + def drawImage(self, image, x, y, width=None, height=None, mask=None): + """Draws the image (ImageReader object or filename) as specified. + + "image" may be an image filename or a ImageReader object. If width + and height are not given, the "natural" width and height in pixels + is used at a scale of 1 point to 1 pixel. + + The mask parameter takes 6 numbers and defines the range of + RGB values which will be masked out or treated as transparent. + For example with [0,2,40,42,136,139], it will mask out any + pixels with a Red value from 0-2, Green from 40-42 and + Blue from 136-139 (on a scale of 0-255) + + The method returns the width and height of the underlying image since + this is often useful for layout algorithms. + + Unlike drawInlineImage, this creates 'external images' which + are only stored once in the PDF file but can be drawn many times. + If you give it the same filename twice, even at different locations + and sizes, it will reuse the first occurrence. If you use ImageReader + objects, it tests whether the image content has changed before deciding + whether to reuse it. + + In general you should use drawImage in preference to drawInlineImage + unless you have read the PDF Spec and understand the tradeoffs.""" + self._currentPageHasImages = 1 + + # first, generate a unique name/signature for the image. If ANYTHING + # is different, even the mask, this should be different. + if type(image) == type(''): + #filename, use it + name = _digester('%s%s' % (image, mask)) + else: + rawdata = image.getRGBData() + name = _digester(rawdata+str(mask)) + + # in the pdf document, this will be prefixed with something to + # say it is an XObject. Does it exist yet? + regName = self._doc.getXObjectName(name) + imgObj = self._doc.idToObject.get(regName, None) + if not imgObj: + #first time seen, create and register the PDFImageXobject + imgObj = pdfdoc.PDFImageXObject(name, image, mask=mask) + imgObj.name = name + self._setXObjects(imgObj) + self._doc.Reference(imgObj, regName) + self._doc.addForm(name, imgObj) + + # ensure we have a size, as PDF will make it 1x1 pixel otherwise! + if width is None: + width = imgObj.width + if height is None: + height = imgObj.height + + # scale and draw + self.saveState() + self.translate(x, y) + self.scale(width, height) + self._code.append("/%s Do" % regName) + self.restoreState() + + # track what's been used on this page + self._formsinuse.append(name) + + return (imgObj.width, imgObj.height) + + def _restartAccumulators(self): + if self._codeStack: + # restore the saved code + saved = self._codeStack[-1] + del self._codeStack[-1] + (self._code, self._formsinuse, self._annotationrefs, self._formData) = saved + else: + self._code = [] # ready for more... + self._psCommandsAfterPage = [] + self._currentPageHasImages = 1 # for safety... + self._formsinuse = [] + self._annotationrefs = [] + self._formData = None + + def _pushAccumulators(self): + "when you enter a form, save accumulator info not related to the form for page (if any)" + saved = (self._code, self._formsinuse, self._annotationrefs, self._formData) + self._codeStack.append(saved) + self._code = [] # ready for more... + self._currentPageHasImages = 1 # for safety... + self._formsinuse = [] + self._annotationrefs = [] + self._formData = None + + def beginForm(self, name, lowerx=0, lowery=0, upperx=None, uppery=None): + """declare the current graphics stream to be a named form. + A graphics stream can either be a page or a form, not both. + Some operations (like bookmarking) are permitted for pages + but not forms. The form will not automatically be shown in the + document but must be explicitly referenced using doForm in pages + that require the form.""" + self.push_state_stack() + self.init_graphics_state() + if self._code: + # save the code that is not in the formf + self._pushAccumulators() + #self._codeStack.append(self._code) + #self._code = [] + self._formData = (name, lowerx, lowery, upperx, uppery) + self._doc.inForm() + #self._inForm0() + + def endForm(self): + """emit the current collection of graphics operations as a Form + as declared previously in beginForm.""" + (name, lowerx, lowery, upperx, uppery) = self._formData + #self.makeForm0(name, lowerx, lowery, upperx, uppery) + # fall through! makeForm0 disallowed + #def makeForm0(self, name, lowerx=0, lowery=0, upperx=None, uppery=None): + """Like showpage, but make a form using accumulated operations instead""" + # deprecated in favor or beginForm(...)... endForm() + (w,h) = self._pagesize + if upperx is None: upperx=w + if uppery is None: uppery=h + form = pdfdoc.PDFFormXObject(lowerx=lowerx, lowery=lowery, upperx=upperx, uppery=uppery) + form.compression = self._pageCompression + form.setStreamList([self._preamble] + self._code) # ??? minus preamble (seems to be needed!) + self._setXObjects(form) + self._setAnnotations(form) + self._doc.addForm(name, form) + self._restartAccumulators() + self.pop_state_stack() + + + def addPostScriptCommand(self, command, position=1): + """Embed literal Postscript in the document. + + With position=0, it goes at very beginning of page stream; + with position=1, at current point; and + with position=2, at very end of page stream. What that does + to the resulting Postscript depends on Adobe's header :-) + + Use with extreme caution, but sometimes needed for printer tray commands. + Acrobat 4.0 will export Postscript to a printer or file containing + the given commands. Adobe Reader 6.0 no longer does as this feature is + deprecated. 5.0, I don't know about (please let us know!). This was + funded by Bob Marshall of Vector.co.uk and tested on a Lexmark 750. + See test_pdfbase_postscript.py for 2 test cases - one will work on + any Postscript device, the other uses a 'setpapertray' command which + will error in Distiller but work on printers supporting it. + """ + #check if we've done this one already... + rawName = 'PS' + md5.md5(command).hexdigest() + regName = self._doc.getXObjectName(rawName) + psObj = self._doc.idToObject.get(regName, None) + if not psObj: + #first use of this chunk of Postscript, make an object + psObj = pdfdoc.PDFPostScriptXObject(command + '\r\n') + self._setXObjects(psObj) + self._doc.Reference(psObj, regName) + self._doc.addForm(rawName, psObj) + if position == 0: + self._psCommandsBeforePage.append("/%s Do" % regName) + elif position==1: + self._code.append("/%s Do" % regName) + else: + self._psCommandsAfterPage.append("/%s Do" % regName) + + self._formsinuse.append(rawName) + + def _absRect(self,rect,relative=0): + if not rect: + w,h = self._pagesize + rect = (0,0,w,h) + elif relative: + lx, ly, ux, uy = rect + xll,yll = self.absolutePosition(lx,ly) + xur,yur = self.absolutePosition(ux, uy) + xul,yul = self.absolutePosition(lx, uy) + xlr,ylr = self.absolutePosition(ux, ly) + xs = xll, xur, xul, xlr + ys = yll, yur, yul, ylr + xmin, ymin = min(xs), min(ys) + xmax, ymax = max(xs), max(ys) + rect = xmin, ymin, xmax, ymax + return rect + + def freeTextAnnotation(self, contents, DA, Rect=None, addtopage=1, name=None, relative=0, **kw): + """DA is the default appearance string???""" + Rect = self._absRect(Rect,relative) + self._addAnnotation(pdfdoc.FreeTextAnnotation(Rect, contents, DA, **kw), name, addtopage) + + def textAnnotation(self, contents, Rect=None, addtopage=1, name=None, relative=0, **kw): + """Experimental, but works. + """ + Rect = self._absRect(Rect,relative) + self._addAnnotation(pdfdoc.TextAnnotation(Rect, contents, **kw), name, addtopage) + textAnnotation0 = textAnnotation #deprecated + + def inkAnnotation(self, contents, InkList=None, Rect=None, addtopage=1, name=None, relative=0, **kw): + raise NotImplementedError + "Experimental" + Rect = self._absRect(Rect,relative) + if not InkList: + InkList = ((100,100,100,h-100,w-100,h-100,w-100,100),) + self._addAnnotation(pdfdoc.InkAnnotation(Rect, contents, InkList, **kw), name, addtopage) + inkAnnotation0 = inkAnnotation #deprecated + + def linkAbsolute(self, contents, destinationname, Rect=None, addtopage=1, name=None, + thickness=0, color=None, dashArray=None, **kw): + """rectangular link annotation positioned wrt the default user space. + The identified rectangle on the page becomes a "hot link" which + when clicked will send the viewer to the page and position identified + by the destination. + + Rect identifies (lowerx, lowery, upperx, uppery) for lower left + and upperright points of the rectangle. Translations and other transforms + are IGNORED (the rectangular position is given with respect + to the default user space. + destinationname should be the name of a bookmark (which may be defined later + but must be defined before the document is generated). + + You may want to use the keyword argument Border='[0 0 0]' to + suppress the visible rectangle around the during viewing link.""" + return self.linkRect(contents, destinationname, Rect, addtopage, name, relative=0, + thickness=thickness, color=color, dashArray=dashArray, **kw) + + def linkRect(self, contents, destinationname, Rect=None, addtopage=1, name=None, relative=0, + thickness=0, color=None, dashArray=None, **kw): + """rectangular link annotation w.r.t the current user transform. + if the transform is skewed/rotated the absolute rectangle will use the max/min x/y + """ + destination = self._bookmarkReference(destinationname) # permitted to be undefined... must bind later... + Rect = self._absRect(Rect,relative) + kw["Rect"] = Rect + kw["Contents"] = contents + kw["Destination"] = destination + _annFormat(kw,color,thickness,dashArray) + return self._addAnnotation(pdfdoc.LinkAnnotation(**kw), name, addtopage) + + def linkURL(self, url, rect, relative=0, thickness=0, color=None, dashArray=None, kind="URI", **kw): + """Create a rectangular URL 'hotspot' in the given rectangle. + + if relative=1, this is in the current coord system, otherwise + in absolute page space. + The remaining options affect the border appearance; the border is + drawn by Acrobat, not us. Set thickness to zero to hide it. + Any border drawn this way is NOT part of the page stream and + will not show when printed to a Postscript printer or distilled; + it is safest to draw your own.""" + from reportlab.pdfbase.pdfdoc import PDFDictionary, PDFName, PDFArray, PDFString + #tried the documented BS element in the pdf spec but it + #does not work, and Acrobat itself does not appear to use it! + + ann = PDFDictionary(dict=kw) + ann["Type"] = PDFName("Annot") + ann["Subtype"] = PDFName("Link") + ann["Rect"] = PDFArray(self._absRect(rect,relative)) # the whole page for testing + + # the action is a separate dictionary + A = PDFDictionary() + A["Type"] = PDFName("Action") # not needed? + uri = PDFString(url) + A['S'] = PDFName(kind) + if kind=="URI": + A["URI"] = uri + elif kind=='GoToR': + A["F"] = uri + A["D"] = "[ 0 /XYZ null null null ]" + else: + raise ValueError("Unknown linkURI kind '%s'" % kind) + + ann["A"] = A + _annFormat(ann,color,thickness,dashArray) + self._addAnnotation(ann) + + def _addAnnotation(self, annotation, name=None, addtopage=1): + count = self._annotationCount = self._annotationCount+1 + if not name: name="NUMBER"+repr(count) + self._doc.addAnnotation(name, annotation) + if addtopage: + self._annotatePage(name) + + def _annotatePage(self, name): + ref = self._doc.refAnnotation(name) + self._annotationrefs.append(ref) + + def getPageNumber(self): + "get the page number for the current page being generated." + return self._pageNumber + + def save(self): + """Saves and close the PDF document in the file. + If there is current data a ShowPage is executed automatically. + After this operation the canvas must not be used further.""" + if len(self._code): self.showPage() + self._doc.SaveToFile(self._filename, self) + + def getpdfdata(self): + """Returns the PDF data that would normally be written to a file. + If there is current data a ShowPage is executed automatically. + After this operation the canvas must not be used further.""" + if len(self._code): self.showPage() + return self._doc.GetPDFData(self) + + def setPageSize(self, size): + """accepts a 2-tuple in points for paper size for this + and subsequent pages""" + self._pagesize = size + self._make_preamble() + + def setPageRotation(self, rot): + """Instruct display device that this page is to be rotated""" + assert rot % 90.0 == 0.0, "Rotation must be a multiple of 90 degrees" + self._pageRotation = rot + + def addLiteral(self, s, escaped=1): + """introduce the literal text of PDF operations s into the current stream. + Only use this if you are an expert in the PDF file format.""" + s = str(s) # make sure its a string + if escaped==0: + s = self._escape(s) # convert to string for safety + self._code.append(s) + + ###################################################################### + # + # coordinate transformations + # + ###################################################################### + def resetTransforms(self): + """I want to draw something (eg, string underlines) w.r.t. the default user space. + Reset the matrix! This should be used usually as follows: + canv.saveState() + canv.resetTransforms() + ...draw some stuff in default space coords... + canv.restoreState() # go back! + """ + # we have to adjoin the inverse, since reset is not a basic operation (without save/restore) + (selfa, selfb, selfc, selfd, selfe, selff) = self._currentMatrix + det = selfa*selfd - selfc*selfb + resulta = selfd/det + resultc = -selfc/det + resulte = (selfc*selff - selfd*selfe)/det + resultd = selfa/det + resultb = -selfb/det + resultf = (selfe*selfb - selff*selfa)/det + self.transform(resulta, resultb, resultc, resultd, resulte, resultf) + + def transform(self, a,b,c,d,e,f): + """adjoin a mathematical transform to the current graphics state matrix. + Not recommended for beginners.""" + #"""How can Python track this?""" + if ENABLE_TRACKING: + a0,b0,c0,d0,e0,f0 = self._currentMatrix + self._currentMatrix = (a0*a+c0*b, b0*a+d0*b, + a0*c+c0*d, b0*c+d0*d, + a0*e+c0*f+e0, b0*e+d0*f+f0) + if self._code and self._code[-1][-3:]==' cm': + L = split(self._code[-1]) + a0, b0, c0, d0, e0, f0 = map(float,L[-7:-1]) + s = len(L)>7 and join(L)+ ' %s cm' or '%s cm' + self._code[-1] = s % fp_str(a0*a+c0*b,b0*a+d0*b,a0*c+c0*d,b0*c+d0*d,a0*e+c0*f+e0,b0*e+d0*f+f0) + else: + self._code.append('%s cm' % fp_str(a,b,c,d,e,f)) + ### debug +## (a,b,c,d,e,f) = self.Kolor +## self.Kolor = (f,a,b,c,d,e) +## self.setStrokeColorRGB(f,a,b) +## self.setFillColorRGB(f,a,b) +## self.line(-90,-1000,1,1); self.line(1000,-90,-1,1) +## self.drawString(0,0,"here") +## Kolor = (0, 0.5, 1, 0.25, 0.7, 0.3) + + def absolutePosition(self, x, y): + """return the absolute position of x,y in user space w.r.t. default user space""" + if not ENABLE_TRACKING: + raise ValueError, "tracking not enabled! (canvas.ENABLE_TRACKING=0)" + (a,b,c,d,e,f) = self._currentMatrix + xp = a*x + c*y + e + yp = b*x + d*y + f + return (xp, yp) + + def translate(self, dx, dy): + """move the origin from the current (0,0) point to the (dx,dy) point + (with respect to the current graphics state).""" + self.transform(1,0,0,1,dx,dy) + + def scale(self, x, y): + """Scale the horizontal dimension by x and the vertical by y + (with respect to the current graphics state). + For example canvas.scale(2.0, 0.5) will make everything short and fat.""" + self.transform(x,0,0,y,0,0) + + def rotate(self, theta): + """Canvas.rotate(theta) + + Rotate the canvas by the angle theta (in degrees).""" + c = cos(theta * pi / 180) + s = sin(theta * pi / 180) + self.transform(c, s, -s, c, 0, 0) + + def skew(self, alpha, beta): + tanAlpha = tan(alpha * pi / 180) + tanBeta = tan(beta * pi / 180) + self.transform(1, tanAlpha, tanBeta, 1, 0, 0) + + ###################################################################### + # + # graphics state management + # + ###################################################################### + + def saveState(self): + """Save the current graphics state to be restored later by restoreState. + + For example: + canvas.setFont("Helvetica", 20) + canvas.saveState() + ... + canvas.setFont("Courier", 9) + ... + canvas.restoreState() + # if the save/restore pairs match then font is Helvetica 20 again. + """ + self.push_state_stack() + self._code.append('q') + + def restoreState(self): + """restore the graphics state to the matching saved state (see saveState).""" + self._code.append('Q') + self.pop_state_stack() + + ############################################################### + # + # Drawing methods. These draw things directly without + # fiddling around with Path objects. We can add any geometry + # methods we wish as long as their meaning is precise and + # they are of general use. + # + # In general there are two patterns. Closed shapes + # have the pattern shape(self, args, stroke=1, fill=0); + # by default they draw an outline only. Line segments come + # in three flavours: line, bezier, arc (which is a segment + # of an elliptical arc, approximated by up to four bezier + # curves, one for each quadrant. + # + # In the case of lines, we provide a 'plural' to unroll + # the inner loop; it is useful for drawing big grids + ################################################################ + + #--------first the line drawing methods----------------------- + + def line(self, x1,y1, x2,y2): + """draw a line segment from (x1,y1) to (x2,y2) (with color, thickness and + other attributes determined by the current graphics state).""" + self._code.append('n %s m %s l S' % (fp_str(x1, y1), fp_str(x2, y2))) + + def lines(self, linelist): + """Like line(), permits many lines to be drawn in one call. + for example for the figure + | + -- -- + | + + crosshairs = [(20,0,20,10), (20,30,20,40), (0,20,10,20), (30,20,40,20)] + canvas.lines(crosshairs) + """ + self._code.append('n') + for (x1,y1,x2,y2) in linelist: + self._code.append('%s m %s l' % (fp_str(x1, y1), fp_str(x2, y2))) + self._code.append('S') + + def grid(self, xlist, ylist): + """Lays out a grid in current line style. Supply list of + x an y positions.""" + assert len(xlist) > 1, "x coordinate list must have 2+ items" + assert len(ylist) > 1, "y coordinate list must have 2+ items" + lines = [] + y0, y1 = ylist[0], ylist[-1] + x0, x1 = xlist[0], xlist[-1] + for x in xlist: + lines.append((x,y0,x,y1)) + for y in ylist: + lines.append((x0,y,x1,y)) + self.lines(lines) + + def bezier(self, x1, y1, x2, y2, x3, y3, x4, y4): + "Bezier curve with the four given control points" + self._code.append('n %s m %s c S' % + (fp_str(x1, y1), fp_str(x2, y2, x3, y3, x4, y4)) + ) + def arc(self, x1,y1, x2,y2, startAng=0, extent=90): + """Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2, + starting at startAng degrees and covering extent degrees. Angles + start with 0 to the right (+x) and increase counter-clockwise. + These should have x1.""" + + pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent) + #move to first point + self._code.append('n %s m' % fp_str(pointList[0][:2])) + for curve in pointList: + self._code.append('%s c' % fp_str(curve[2:])) + # stroke + self._code.append('S') + + #--------now the shape drawing methods----------------------- + + def rect(self, x, y, width, height, stroke=1, fill=0): + "draws a rectangle with lower left corner at (x,y) and width and height as given." + self._code.append('n %s re ' % fp_str(x, y, width, height) + + PATH_OPS[stroke, fill, self._fillMode]) + + def ellipse(self, x1, y1, x2, y2, stroke=1, fill=0): + """Draw an ellipse defined by an enclosing rectangle. + + Note that (x1,y1) and (x2,y2) are the corner points of + the enclosing rectangle. + + Uses bezierArc, which conveniently handles 360 degrees. + Special thanks to Robert Kern.""" + + pointList = pdfgeom.bezierArc(x1,y1, x2,y2, 0, 360) + #move to first point + self._code.append('n %s m' % fp_str(pointList[0][:2])) + for curve in pointList: + self._code.append('%s c' % fp_str(curve[2:])) + #finish + self._code.append(PATH_OPS[stroke, fill, self._fillMode]) + + def wedge(self, x1,y1, x2,y2, startAng, extent, stroke=1, fill=0): + """Like arc, but connects to the centre of the ellipse. + Most useful for pie charts and PacMan!""" + + x_cen = (x1+x2)/2. + y_cen = (y1+y2)/2. + pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent) + + self._code.append('n %s m' % fp_str(x_cen, y_cen)) + # Move the pen to the center of the rectangle + self._code.append('%s l' % fp_str(pointList[0][:2])) + for curve in pointList: + self._code.append('%s c' % fp_str(curve[2:])) + # finish the wedge + self._code.append('%s l ' % fp_str(x_cen, y_cen)) + # final operator + self._code.append(PATH_OPS[stroke, fill, self._fillMode]) + + def circle(self, x_cen, y_cen, r, stroke=1, fill=0): + """draw a cirle centered at (x_cen,y_cen) with radius r (special case of ellipse)""" + + x1 = x_cen - r + x2 = x_cen + r + y1 = y_cen - r + y2 = y_cen + r + self.ellipse(x1, y1, x2, y2, stroke, fill) + + def roundRect(self, x, y, width, height, radius, stroke=1, fill=0): + """Draws a rectangle with rounded corners. The corners are + approximately quadrants of a circle, with the given radius.""" + #use a precomputed set of factors for the bezier approximation + #to a circle. There are six relevant points on the x axis and y axis. + #sketch them and it should all make sense! + t = 0.4472 * radius + + x0 = x + x1 = x0 + t + x2 = x0 + radius + x3 = x0 + width - radius + x4 = x0 + width - t + x5 = x0 + width + + y0 = y + y1 = y0 + t + y2 = y0 + radius + y3 = y0 + height - radius + y4 = y0 + height - t + y5 = y0 + height + + self._code.append('n %s m' % fp_str(x2, y0)) + self._code.append('%s l' % fp_str(x3, y0)) # bottom row + self._code.append('%s c' + % fp_str(x4, y0, x5, y1, x5, y2)) # bottom right + + self._code.append('%s l' % fp_str(x5, y3)) # right edge + self._code.append('%s c' + % fp_str(x5, y4, x4, y5, x3, y5)) # top right + + self._code.append('%s l' % fp_str(x2, y5)) # top row + self._code.append('%s c' + % fp_str(x1, y5, x0, y4, x0, y3)) # top left + + self._code.append('%s l' % fp_str(x0, y2)) # left edge + self._code.append('%s c' + % fp_str(x0, y1, x1, y0, x2, y0)) # bottom left + + self._code.append('h') #close off, although it should be where it started anyway + + self._code.append(PATH_OPS[stroke, fill, self._fillMode]) + + ################################################## + # + # Text methods + # + # As with graphics, a separate object ensures that + # everything is bracketed between text operators. + # The methods below are a high-level convenience. + # use PDFTextObject for multi-line text. + ################################################## + + def drawString(self, x, y, text): + """Draws a string in the current text styles.""" + #we could inline this for speed if needed + t = self.beginText(x, y) + t.textLine(text) + self.drawText(t) + + def drawRightString(self, x, y, text): + """Draws a string right-aligned with the x coordinate""" + width = self.stringWidth(text, self._fontname, self._fontsize) + t = self.beginText(x - width, y) + t.textLine(text) + self.drawText(t) + + def drawCentredString(self, x, y, text): + """Draws a string centred on the x coordinate.""" + width = self.stringWidth(text, self._fontname, self._fontsize) + t = self.beginText(x - 0.5*width, y) + t.textLine(text) + self.drawText(t) + + def drawAlignedString(self, x, y, text, pivotChar="."): + """Draws a string aligned on the first '.' (or other pivot character). + + The centre position of the pivot character will be used as x. + So, you could draw a straight line down through all the decimals in a + column of numbers, and anything without a decimal should be + optically aligned with those that have. + + There is one special rule to help with accounting formatting. Here's + how normal numbers should be aligned on the 'dot'. Look at the + LAST two: + 12,345,67 + 987.15 + 42 + -1,234.56 + (456.78) + (456) + 27 inches + 13cm + Since the last three do not contain a dot, a crude dot-finding + rule would place them wrong. So we test for the special case + where no pivot is found, digits are present, but the last character + is not a digit. We then work back from the end of the string + This case is a tad slower but hopefully rare. + + """ + parts = text.split(pivotChar,1) + pivW = self.stringWidth(pivotChar, self._fontname, self._fontsize) + + if len(parts) == 1 and digitPat.search(text) is not None and text[-1] not in digits: + #we have no decimal but it ends in a bracket, or 'in' or something. + #the cut should be after the last digit. + leftText = parts[0][0:-1] + rightText = parts[0][-1] + #any more? + while leftText[-1] not in digits: + rightText = leftText[-1] + rightText + leftText = leftText[0:-1] + + self.drawRightString(x-0.5*pivW, y, leftText) + self.drawString(x-0.5*pivW, y, rightText) + + else: + #normal case + leftText = parts[0] + self.drawRightString(x-0.5*pivW, y, leftText) + if len(parts) > 1: + rightText = pivotChar + parts[1] + self.drawString(x-0.5*pivW, y, rightText) + + def getAvailableFonts(self): + """Returns the list of PostScript font names available. + + Standard set now, but may grow in future with font embedding.""" + fontnames = self._doc.getAvailableFonts() + fontnames.sort() + return fontnames + + def addFont(self, fontObj): + "add a new font for subsequent use." + self._doc.addFont(fontObj) + + def _addStandardFonts(self): + """Ensures the standard 14 fonts are available in the system encoding. + Called by canvas on initialization""" + for fontName in pdfmetrics.standardFonts: + self.addFont(pdfmetrics.fontsByName[fontName]) + + def listLoadedFonts0(self): + "Convenience function to list all loaded fonts" + names = pdfmetrics.widths.keys() + names.sort() + return names + + def setFont(self, psfontname, size, leading = None): + """Sets the font. If leading not specified, defaults to 1.2 x + font size. Raises a readable exception if an illegal font + is supplied. Font names are case-sensitive! Keeps track + of font name and size for metrics.""" + self._fontname = psfontname + self._fontsize = size + if leading is None: + leading = size * 1.2 + self._leading = leading + font = pdfmetrics.getFont(self._fontname) + + self._dynamicFont = getattr(font, '_dynamicFont', 0) + if not self._dynamicFont: + pdffontname = self._doc.getInternalFontName(psfontname) + self._code.append('BT %s %s Tf %s TL ET' % (pdffontname, fp_str(size), fp_str(leading))) + + def setFontSize(self, size=None, leading=None): + '''Sets font size or leading without knowing the font face''' + if size is None: size = self._fontsize + if leading is None: leading = self._leading + self.setFont(self._fontname, size, leading) + + def stringWidth(self, text, fontName, fontSize): + "gets width of a string in the given font and size" + return pdfmetrics.stringWidth(text, fontName, fontSize) + + # basic graphics modes + + def setLineWidth(self, width): + self._lineWidth = width + self._code.append('%s w' % fp_str(width)) + + def setLineCap(self, mode): + """0=butt,1=round,2=square""" + assert mode in (0,1,2), "Line caps allowed: 0=butt,1=round,2=square" + self._lineCap = mode + self._code.append('%d J' % mode) + + def setLineJoin(self, mode): + """0=mitre, 1=round, 2=bevel""" + assert mode in (0,1,2), "Line Joins allowed: 0=mitre, 1=round, 2=bevel" + self._lineJoin = mode + self._code.append('%d j' % mode) + + def setMiterLimit(self, limit): + self._miterLimit = limit + self._code.append('%s M' % fp_str(limit)) + + def setDash(self, array=[], phase=0): + """Two notations. pass two numbers, or an array and phase""" + if type(array) == IntType or type(array) == FloatType: + self._code.append('[%s %s] 0 d' % (array, phase)) + elif type(array) == ListType or type(array) == TupleType: + assert phase >= 0, "phase is a length in user space" + textarray = ' '.join(map(str, array)) + self._code.append('[%s] %s d' % (textarray, phase)) + + # path stuff - the separate path object builds it + + def beginPath(self): + """Returns a fresh path object. Paths are used to draw + complex figures. The object returned follows the protocol + for a pathobject.PDFPathObject instance""" + return pathobject.PDFPathObject() + + def drawPath(self, aPath, stroke=1, fill=0): + "Draw the path object in the mode indicated" + gc = aPath.getCode(); pathops = PATH_OPS[stroke, fill, self._fillMode] + item = "%s %s" % (gc, pathops) # ENSURE STRING CONVERSION + self._code.append(item) + #self._code.append(aPath.getCode() + ' ' + PATH_OPS[stroke, fill, self._fillMode]) + + def clipPath(self, aPath, stroke=1, fill=0): + "clip as well as drawing" + gc = aPath.getCode(); pathops = PATH_OPS[stroke, fill, self._fillMode] + clip = (self._fillMode == FILL_EVEN_ODD and ' W* ' or ' W ') + item = "%s%s%s" % (gc, clip, pathops) # ensure string conversion + self._code.append(item) + #self._code.append( aPath.getCode() + # + (self._fillMode == FILL_EVEN_ODD and ' W* ' or ' W ') + # + PATH_OPS[stroke,fill,self._fillMode]) + + def beginText(self, x=0, y=0): + """Returns a fresh text object. Text objects are used + to add large amounts of text. See textobject.PDFTextObject""" + return textobject.PDFTextObject(self, x, y) + + def drawText(self, aTextObject): + """Draws a text object""" + self._code.append(str(aTextObject.getCode())) + + def setPageCompression(self, pageCompression=1): + """Possible values None, 1 or 0 + If None the value from rl_config will be used. + If on, the page data will be compressed, leading to much + smaller files, but takes a little longer to create the files. + This applies to all subsequent pages, or until setPageCompression() + is next called.""" + if pageCompression is None: pageCompression = rl_config.pageCompression + if pageCompression and not zlib: + self._pageCompression = 0 + else: + self._pageCompression = pageCompression + self._doc.setCompression(self._pageCompression) + + def setPageDuration(self, duration=None): + """Allows hands-off animation of presentations :-) + + If this is set to a number, in full screen mode, Acrobat Reader + will advance to the next page after this many seconds. The + duration of the transition itself (fade/flicker etc.) is controlled + by the 'duration' argument to setPageTransition; this controls + the time spent looking at the page. This is effective for all + subsequent pages.""" + + self._pageDuration = duration + + def setPageTransition(self, effectname=None, duration=1, + direction=0,dimension='H',motion='I'): + """PDF allows page transition effects for use when giving + presentations. There are six possible effects. You can + just guive the effect name, or supply more advanced options + to refine the way it works. There are three types of extra + argument permitted, and here are the allowed values: + direction_arg = [0,90,180,270] + dimension_arg = ['H', 'V'] + motion_arg = ['I','O'] (start at inside or outside) + + This table says which ones take which arguments: + + PageTransitionEffects = { + 'Split': [direction_arg, motion_arg], + 'Blinds': [dimension_arg], + 'Box': [motion_arg], + 'Wipe' : [direction_arg], + 'Dissolve' : [], + 'Glitter':[direction_arg] + } + Have fun! + """ + # This builds a Python dictionary with the right arguments + # for the Trans dictionary in the PDFPage object, + # and stores it in the variable _pageTransition. + # showPage later passes this to the setPageTransition method + # of the PDFPage object, which turns it to a PDFDictionary. + self._pageTransition = {} + if not effectname: + return + + #first check each optional argument has an allowed value + if direction in [0,90,180,270]: + direction_arg = ('Di', '/%d' % direction) + else: + raise 'PDFError', ' directions allowed are 0,90,180,270' + + if dimension in ['H', 'V']: + dimension_arg = ('Dm', '/' + dimension) + else: + raise'PDFError','dimension values allowed are H and V' + + if motion in ['I','O']: + motion_arg = ('M', '/' + motion) + else: + raise'PDFError','motion values allowed are I and O' + + # this says which effects require which argument types from above + PageTransitionEffects = { + 'Split': [direction_arg, motion_arg], + 'Blinds': [dimension_arg], + 'Box': [motion_arg], + 'Wipe' : [direction_arg], + 'Dissolve' : [], + 'Glitter':[direction_arg] + } + + try: + args = PageTransitionEffects[effectname] + except KeyError: + raise 'PDFError', 'Unknown Effect Name "%s"' % effectname + + # now build the dictionary + transDict = {} + transDict['Type'] = '/Trans' + transDict['D'] = '%d' % duration + transDict['S'] = '/' + effectname + for (key, value) in args: + transDict[key] = value + self._pageTransition = transDict + + def getCurrentPageContent(self): + """Return uncompressed contents of current page buffer. + + This is useful in creating test cases and assertions of what + got drawn, without necessarily saving pages to disk""" + return '\n'.join(self._code) + + + +if _instanceEscapePDF: + import new + Canvas._escape = new.instancemethod(_instanceEscapePDF,None,Canvas) + +if __name__ == '__main__': + print 'For test scripts, look in reportlab/test' diff --git a/bin/reportlab/pdfgen/pathobject.py b/bin/reportlab/pdfgen/pathobject.py new file mode 100755 index 00000000000..2eaacc8d8e2 --- /dev/null +++ b/bin/reportlab/pdfgen/pathobject.py @@ -0,0 +1,101 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfgen/pathobject.py +__version__=''' $Id: pathobject.py 2537 2005-03-15 14:19:29Z rgbecker $ ''' +__doc__=""" +PDFPathObject is an efficient way to draw paths on a Canvas. Do not +instantiate directly, obtain one from the Canvas instead. + +Progress Reports: +8.83, 2000-01-13, gmcm: + created from pdfgen.py +""" + +import string +from reportlab.pdfgen import pdfgeom +from reportlab.lib.utils import fp_str + + +class PDFPathObject: + """Represents a graphic path. There are certain 'modes' to PDF + drawing, and making a separate object to expose Path operations + ensures they are completed with no run-time overhead. Ask + the Canvas for a PDFPath with getNewPathObject(); moveto/lineto/ + curveto wherever you want; add whole shapes; and then add it back + into the canvas with one of the relevant operators. + + Path objects are probably not long, so we pack onto one line""" + + def __init__(self): + self._code = [] + #self._code.append('n') #newpath + self._code_append = self._init_code_append + + def _init_code_append(self,c): + assert c.endswith(' m') or c.endswith(' re'), 'path must start with a moveto or rect' + code_append = self._code.append + code_append('n') + code_append(c) + self._code_append = code_append + + def getCode(self): + "pack onto one line; used internally" + return string.join(self._code, ' ') + + def moveTo(self, x, y): + self._code_append('%s m' % fp_str(x,y)) + + def lineTo(self, x, y): + self._code_append('%s l' % fp_str(x,y)) + + def curveTo(self, x1, y1, x2, y2, x3, y3): + self._code_append('%s c' % fp_str(x1, y1, x2, y2, x3, y3)) + + def arc(self, x1,y1, x2,y2, startAng=0, extent=90): + """Contributed to piddlePDF by Robert Kern, 28/7/99. + Draw a partial ellipse inscribed within the rectangle x1,y1,x2,y2, + starting at startAng degrees and covering extent degrees. Angles + start with 0 to the right (+x) and increase counter-clockwise. + These should have x1.""" + + pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent) + #move to first point + self._code_append('%s m' % fp_str(pointList[0][:2])) + for curve in pointList: + self._code_append('%s c' % fp_str(curve[2:])) + + def arcTo(self, x1,y1, x2,y2, startAng=0, extent=90): + """Like arc, but draws a line from the current point to + the start if the start is not the current point.""" + pointList = pdfgeom.bezierArc(x1,y1, x2,y2, startAng, extent) + self._code_append('%s l' % fp_str(pointList[0][:2])) + for curve in pointList: + self._code_append('%s c' % fp_str(curve[2:])) + + def rect(self, x, y, width, height): + """Adds a rectangle to the path""" + self._code_append('%s re' % fp_str((x, y, width, height))) + + def ellipse(self, x, y, width, height): + """adds an ellipse to the path""" + pointList = pdfgeom.bezierArc(x, y, x + width,y + height, 0, 360) + self._code_append('%s m' % fp_str(pointList[0][:2])) + for curve in pointList: + self._code_append('%s c' % fp_str(curve[2:])) + + def circle(self, x_cen, y_cen, r): + """adds a circle to the path""" + x1 = x_cen - r + #x2 = x_cen + r + y1 = y_cen - r + #y2 = y_cen + r + width = height = 2*r + #self.ellipse(x_cen - r, y_cen - r, x_cen + r, y_cen + r) + self.ellipse(x1, y1, width, height) + + def close(self): + "draws a line back to where it started" + self._code_append('h') diff --git a/bin/reportlab/pdfgen/pdfgeom.py b/bin/reportlab/pdfgen/pdfgeom.py new file mode 100755 index 00000000000..34ef826b0b8 --- /dev/null +++ b/bin/reportlab/pdfgen/pdfgeom.py @@ -0,0 +1,77 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfgen/pdfgeom.py +__version__=''' $Id: pdfgeom.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +__doc__=""" +This module includes any mathematical methods needed for PIDDLE. +It should have no dependencies beyond the Python library. + +So far, just Robert Kern's bezierArc. +""" + +from math import sin, cos, pi, ceil + + +def bezierArc(x1,y1, x2,y2, startAng=0, extent=90): + """bezierArc(x1,y1, x2,y2, startAng=0, extent=90) --> List of Bezier +curve control points. + +(x1, y1) and (x2, y2) are the corners of the enclosing rectangle. The +coordinate system has coordinates that increase to the right and down. +Angles, measured in degress, start with 0 to the right (the positive X +axis) and increase counter-clockwise. The arc extends from startAng +to startAng+extent. I.e. startAng=0 and extent=180 yields an openside-down +semi-circle. + +The resulting coordinates are of the form (x1,y1, x2,y2, x3,y3, x4,y4) +such that the curve goes from (x1, y1) to (x4, y4) with (x2, y2) and +(x3, y3) as their respective Bezier control points.""" + + x1,y1, x2,y2 = min(x1,x2), max(y1,y2), max(x1,x2), min(y1,y2) + + if abs(extent) <= 90: + arcList = [startAng] + fragAngle = float(extent) + Nfrag = 1 + else: + arcList = [] + Nfrag = int(ceil(abs(extent)/90.)) + fragAngle = float(extent) / Nfrag + + x_cen = (x1+x2)/2. + y_cen = (y1+y2)/2. + rx = (x2-x1)/2. + ry = (y2-y1)/2. + halfAng = fragAngle * pi / 360. + kappa = abs(4. / 3. * (1. - cos(halfAng)) / sin(halfAng)) + + if fragAngle < 0: + sign = -1 + else: + sign = 1 + + pointList = [] + + for i in range(Nfrag): + theta0 = (startAng + i*fragAngle) * pi / 180. + theta1 = (startAng + (i+1)*fragAngle) *pi / 180. + if fragAngle > 0: + pointList.append((x_cen + rx * cos(theta0), + y_cen - ry * sin(theta0), + x_cen + rx * (cos(theta0) - kappa * sin(theta0)), + y_cen - ry * (sin(theta0) + kappa * cos(theta0)), + x_cen + rx * (cos(theta1) + kappa * sin(theta1)), + y_cen - ry * (sin(theta1) - kappa * cos(theta1)), + x_cen + rx * cos(theta1), + y_cen - ry * sin(theta1))) + else: + pointList.append((x_cen + rx * cos(theta0), + y_cen - ry * sin(theta0), + x_cen + rx * (cos(theta0) + kappa * sin(theta0)), + y_cen - ry * (sin(theta0) - kappa * cos(theta0)), + x_cen + rx * (cos(theta1) - kappa * sin(theta1)), + y_cen - ry * (sin(theta1) + kappa * cos(theta1)), + x_cen + rx * cos(theta1), + y_cen - ry * sin(theta1))) + + return pointList \ No newline at end of file diff --git a/bin/reportlab/pdfgen/pdfimages.py b/bin/reportlab/pdfgen/pdfimages.py new file mode 100644 index 00000000000..f9e010314aa --- /dev/null +++ b/bin/reportlab/pdfgen/pdfimages.py @@ -0,0 +1,188 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfgen/pdfimages.py +__version__=''' $Id: pdfimages.py 2744 2005-12-15 12:28:18Z rgbecker $ ''' +__doc__=""" +Image functionality sliced out of canvas.py for generalization +""" + +import os +import string +from types import StringType +import reportlab +from reportlab.pdfbase import pdfutils +from reportlab.pdfbase import pdfdoc +from reportlab.lib.utils import fp_str, getStringIO +from reportlab.lib.utils import import_zlib, haveImages + + +class PDFImage: + """Wrapper around different "image sources". You can make images + from a PIL Image object, a filename (in which case it uses PIL), + an image we previously cached (optimisation, hardly used these + days) or a JPEG (which PDF supports natively).""" + + def __init__(self, image, x,y, width=None, height=None, caching=0): + self.image = image + self.point = (x,y) + self.dimensions = (width, height) + self.filename = None + self.imageCaching = caching + # the following facts need to be determined, + # whatever the source. Declare what they are + # here for clarity. + self.colorSpace = 'DeviceRGB' + self.bitsPerComponent = 8 + self.filters = [] + self.source = None # JPEG or PIL, set later + self.getImageData() + + def jpg_imagedata(self): + #directly process JPEG files + #open file, needs some error handling!! + fp = open(self.image, 'rb') + result = self._jpg_imagedata(fp) + fp.close() + return result + + def _jpg_imagedata(self,imageFile): + self.source = 'JPEG' + info = pdfutils.readJPEGInfo(imageFile) + imgwidth, imgheight = info[0], info[1] + if info[2] == 1: + colorSpace = 'DeviceGray' + elif info[2] == 3: + colorSpace = 'DeviceRGB' + else: #maybe should generate an error, is this right for CMYK? + colorSpace = 'DeviceCMYK' + imageFile.seek(0) #reset file pointer + imagedata = [] + #imagedata.append('BI /Width %d /Height /BitsPerComponent 8 /ColorSpace /%s /Filter [/Filter [ /ASCII85Decode /DCTDecode] ID' % (info[0], info[1], colorSpace)) + imagedata.append('BI /W %d /H %d /BPC 8 /CS /%s /F [/A85 /DCT] ID' % (imgwidth, imgheight, colorSpace)) + #write in blocks of (??) 60 characters per line to a list + compressed = imageFile.read() + encoded = pdfutils._AsciiBase85Encode(compressed) + pdfutils._chunker(encoded,imagedata) + imagedata.append('EI') + return (imagedata, imgwidth, imgheight) + + def cache_imagedata(self): + image = self.image + if not pdfutils.cachedImageExists(image): + zlib = import_zlib() + if not zlib: return + if not haveImages: return + pdfutils.cacheImageFile(image) + + #now we have one cached, slurp it in + cachedname = os.path.splitext(image)[0] + '.a85' + imagedata = open(cachedname,'rb').readlines() + #trim off newlines... + imagedata = map(string.strip, imagedata) + return imagedata + + def PIL_imagedata(self): + image = self.image + if image.format=='JPEG': + fp=image.fp + fp.seek(0) + return self._jpg_imagedata(fp) + self.source = 'PIL' + zlib = import_zlib() + if not zlib: return + myimage = image.convert('RGB') + imgwidth, imgheight = myimage.size + + # this describes what is in the image itself + # *NB* according to the spec you can only use the short form in inline images + #imagedata=['BI /Width %d /Height /BitsPerComponent 8 /ColorSpace /%s /Filter [/Filter [ /ASCII85Decode /FlateDecode] ID]' % (imgwidth, imgheight,'RGB')] + imagedata=['BI /W %d /H %d /BPC 8 /CS /RGB /F [/A85 /Fl] ID' % (imgwidth, imgheight)] + + #use a flate filter and Ascii Base 85 to compress + raw = myimage.tostring() + assert(len(raw) == imgwidth * imgheight, "Wrong amount of data for image") + compressed = zlib.compress(raw) #this bit is very fast... + encoded = pdfutils._AsciiBase85Encode(compressed) #...sadly this may not be + #append in blocks of 60 characters + pdfutils._chunker(encoded,imagedata) + imagedata.append('EI') + return (imagedata, imgwidth, imgheight) + + def getImageData(self): + "Gets data, height, width - whatever type of image" + image = self.image + (width, height) = self.dimensions + + if type(image) == StringType: + self.filename = image + if os.path.splitext(image)[1] in ['.jpg', '.JPG', '.jpeg', '.JPEG']: + (imagedata, imgwidth, imgheight) = self.jpg_imagedata() + else: + if not self.imageCaching: + imagedata = pdfutils.cacheImageFile(image,returnInMemory=1) + else: + imagedata = self.cache_imagedata() + #parse line two for width, height + words = string.split(imagedata[1]) + imgwidth = string.atoi(words[1]) + imgheight = string.atoi(words[3]) + else: + import sys + if sys.platform[0:4] == 'java': + #jython, PIL not available + (imagedata, imgwidth, imgheight) = self.JAVA_imagedata() + else: + (imagedata, imgwidth, imgheight) = self.PIL_imagedata() + #now build the PDF for the image. + if not width: + width = imgwidth + if not height: + height = imgheight + self.width = width + self.height = height + self.imageData = imagedata + + def drawInlineImage(self, canvas): #, image, x,y, width=None,height=None): + """Draw an Image into the specified rectangle. If width and + height are omitted, they are calculated from the image size. + Also allow file names as well as images. This allows a + caching mechanism""" + if self.width<1e-6 or self.height<1e-6: return False + (x,y) = self.point + # this says where and how big to draw it + if not canvas.bottomup: y = y+self.height + canvas._code.append('q %s 0 0 %s cm' % (fp_str(self.width), fp_str(self.height, x, y))) + # self._code.extend(imagedata) if >=python-1.5.2 + for line in self.imageData: + canvas._code.append(line) + canvas._code.append('Q') + return True + + def format(self, document): + """Allow it to be used within pdfdoc framework. This only + defines how it is stored, not how it is drawn later.""" + + dict = pdfdoc.PDFDictionary() + dict['Type'] = '/XObject' + dict['Subtype'] = '/Image' + dict['Width'] = self.width + dict['Height'] = self.height + dict['BitsPerComponent'] = 8 + dict['ColorSpace'] = pdfdoc.PDFName(self.colorSpace) + content = string.join(self.imageData[3:-1], '\n') + '\n' + strm = pdfdoc.PDFStream(dictionary=dict, content=content) + return strm.format(document) + +if __name__=='__main__': + srcfile = os.path.join( + os.path.dirname(reportlab.__file__), + 'test', + 'pythonpowered.gif' + ) + assert os.path.isfile(srcfile), 'image not found' + pdfdoc.LongFormat = 1 + img = PDFImage(srcfile, 100, 100) + import pprint + doc = pdfdoc.PDFDocument() + print 'source=',img.source + print img.format(doc) diff --git a/bin/reportlab/pdfgen/pycanvas.py b/bin/reportlab/pdfgen/pycanvas.py new file mode 100644 index 00000000000..849690eb1f2 --- /dev/null +++ b/bin/reportlab/pdfgen/pycanvas.py @@ -0,0 +1,309 @@ +# a Pythonesque Canvas v0.8 +# Author : Jerome Alet - +# License : ReportLab's license +# +# $Id: pycanvas.py 1821 2002-11-06 17:11:31Z rgbecker $ +# +__doc__ = """pycanvas.Canvas : a Canvas class which can also output Python source code. + +pycanvas.Canvas class works exactly like canvas.Canvas, but you can +call str() on pycanvas.Canvas instances. Doing so will return the +Python source code equivalent to your own program, which would, when +run, produce the same PDF document as your original program. + +Generated Python source code defines a doIt() function which accepts +a filename or file-like object as its first parameter, and an +optional boolean parameter named "regenerate". + +The doIt() function will generate a PDF document and save it in the +file you specified in this argument. If the regenerate parameter is +set then it will also return an automatically generated equivalent +Python source code as a string of text, which you can run again to +produce the very same PDF document and the Python source code, which +you can run again... ad nauseam ! If the regenerate parameter is +unset or not used at all (it then defaults to being unset) then None +is returned and the doIt() function is much much faster, it is also +much faster than the original non-serialized program. + +the reportlab/test/test_pdfgen_pycanvas.py program is the test suite +for pycanvas, you can do the following to run it : + + First set verbose=1 in reportlab/rl_config.py + + then from the command interpreter : + + $ cd reportlab/test + $ python test_pdfgen_pycanvas.py >n1.py + + this will produce both n1.py and test_pdfgen_pycanvas.pdf + + then : + + $ python n1.py n1.pdf >n2.py + $ python n2.py n2.pdf >n3.py + $ ... + + n1.py, n2.py, n3.py and so on will be identical files. + they eventually may end being a bit different because of + rounding problems, mostly in the comments, but this + doesn't matter since the values really are the same + (e.g. 0 instead of 0.0, or .53 instead of 0.53) + + n1.pdf, n2.pdf, n3.pdf and so on will be PDF files + similar to test_pdfgen_pycanvas.pdf. + +Alternatively you can import n1.py (or n3.py, or n16384.py if you prefer) +in your own program, and then call its doIt function : + + import n1 + pythonsource = n1.doIt("myfile.pdf", regenerate=1) + +Or if you don't need the python source code and want a faster result : + + import n1 + n1.doIt("myfile.pdf") + +When the generated source code is run directly as an independant program, +then the equivalent python source code is printed to stdout, e.g. : + + python n1.py + + will print the python source code equivalent to n1.py + +Why would you want to use such a beast ? + + - To linearize (serialize?) a program : optimizing some complex + parts for example. + + - To debug : reading the generated Python source code may help you or + the ReportLab team to diagnose problems. The generated code is now + clearly commented and shows nesting levels, page numbers, and so + on. You can use the generated script when asking for support : we + can see the results you obtain without needing your datas or complete + application. + + - To create standalone scripts : say your program uses a high level + environment to generate its output (databases, RML, etc...), using + this class would give you an equivalent program but with complete + independance from the high level environment (e.g. if you don't + have Oracle). + + - To contribute some nice looking PDF documents to the ReportLab website + without having to send a complete application you don't want to + distribute. + + - ... Insert your own ideas here ... + + - For fun because you can do it ! +""" + +import cStringIO +from reportlab.pdfgen import canvas +from reportlab.pdfgen import pathobject +from reportlab.pdfgen import textobject + +PyHeader = '''#! /usr/bin/env python + +# +# This code was entirely generated by ReportLab (http://www.reportlab.com) +# + +import sys +from reportlab.pdfgen import pathobject +from reportlab.pdfgen import textobject +from reportlab.lib.colors import Color + +def doIt(file, regenerate=0) : + """Generates a PDF document, save it into file. + + file : either a filename or a file-like object. + + regenerate : if set then this function returns the Python source + code which when run will produce the same result. + if unset then this function returns None, and is + much faster. + """ + if regenerate : + from reportlab.pdfgen.pycanvas import Canvas + else : + from reportlab.pdfgen.canvas import Canvas +''' + +PyFooter = ''' + # if we want the equivalent Python source code, then send it back + if regenerate : + return str(c) + +if __name__ == "__main__" : + if len(sys.argv) != 2 : + # second argument must be the name of the PDF file to create + sys.stderr.write("%s needs one and only one argument\\n" % sys.argv[0]) + sys.exit(-1) + else : + # we've got a filename, we can proceed. + print doIt(sys.argv[1], regenerate=1) + sys.exit(0)''' + +def buildargs(*args, **kwargs) : + """Constructs a printable list of arguments suitable for use in source function calls.""" + arguments = "" + for arg in args : + arguments = arguments + ("%s, " % repr(arg)) + for (kw, val) in kwargs.items() : + arguments = arguments+ ("%s=%s, " % (kw, repr(val))) + if arguments[-2:] == ", " : + arguments = arguments[:-2] + return arguments + +class PDFAction : + """Base class to fake method calls or attributes on PDF objects (Canvas, PDFPathObject, PDFTextObject).""" + def __init__(self, parent, action) : + """Saves a pointer to the parent object, and the method name.""" + self._parent = parent + self._action = action + + def __getattr__(self, name) : + """Probably a method call on an attribute, returns the real one.""" + return getattr(getattr(self._parent._object, self._action), name) + + def __call__(self, *args, **kwargs) : + """The fake method is called, print it then call the real one.""" + if not self._parent._parent._in : + self._precomment() + self._parent._parent._PyWrite(" %s.%s(%s)" % (self._parent._name, self._action, apply(buildargs, args, kwargs))) + self._postcomment() + self._parent._parent._in = self._parent._parent._in + 1 + retcode = apply(getattr(self._parent._object, self._action), args, kwargs) + self._parent._parent._in = self._parent._parent._in - 1 + return retcode + + def __hash__(self) : + return hash(getattr(self._parent._object, self._action)) + + def __coerce__(self, other) : + """Needed.""" + return coerce(getattr(self._parent._object, self._action), other) + + def _precomment(self) : + """To be overriden.""" + pass + + def _postcomment(self) : + """To be overriden.""" + pass + +class PDFObject : + """Base class for PDF objects like PDFPathObject and PDFTextObject.""" + _number = 0 + def __init__(self, parent) : + """Saves a pointer to the parent Canvas.""" + self._parent = parent + self._initdone = 0 + + def __getattr__(self, name) : + """The user's programs wants to call one of our methods or get an attribute, fake it.""" + return PDFAction(self, name) + + def __repr__(self) : + """Returns the name used in the generated source code (e.g. 'p' or 't').""" + return self._name + + def __call__(self, *args, **kwargs) : + """Real object initialisation is made here, because now we've got the arguments.""" + if not self._initdone : + self.__class__._number = self.__class__._number + 1 + methodname = apply(self._postinit, args, kwargs) + self._parent._PyWrite("\n # create PDF%sObject number %i\n %s = %s.%s(%s)" % (methodname[5:], self.__class__._number, self._name, self._parent._name, methodname, apply(buildargs, args, kwargs))) + self._initdone = 1 + return self + +class Canvas : + """Our fake Canvas class, which will intercept each and every method or attribute access.""" + class TextObject(PDFObject) : + _name = "t" + def _postinit(self, *args, **kwargs) : + self._object = apply(textobject.PDFTextObject, (self._parent, ) + args, kwargs) + return "beginText" + + class PathObject(PDFObject) : + _name = "p" + def _postinit(self, *args, **kwargs) : + self._object = apply(pathobject.PDFPathObject, args, kwargs) + return "beginPath" + + class Action(PDFAction) : + """Class called for every Canvas method call.""" + def _precomment(self) : + """Outputs comments before the method call.""" + if self._action == "showPage" : + self._parent._PyWrite("\n # Ends page %i" % self._parent._pagenumber) + elif self._action == "saveState" : + state = {} + d = self._parent._object.__dict__ + for name in self._parent._object.STATE_ATTRIBUTES: + state[name] = d[name] + self._parent._PyWrite("\n # Saves context level %i %s" % (self._parent._contextlevel, state)) + self._parent._contextlevel = self._parent._contextlevel + 1 + elif self._action == "restoreState" : + self._parent._contextlevel = self._parent._contextlevel - 1 + self._parent._PyWrite("\n # Restores context level %i %s" % (self._parent._contextlevel, self._parent._object.state_stack[-1])) + elif self._action == "beginForm" : + self._parent._formnumber = self._parent._formnumber + 1 + self._parent._PyWrite("\n # Begins form %i" % self._parent._formnumber) + elif self._action == "endForm" : + self._parent._PyWrite("\n # Ends form %i" % self._parent._formnumber) + elif self._action == "save" : + self._parent._PyWrite("\n # Saves the PDF document to disk") + + def _postcomment(self) : + """Outputs comments after the method call.""" + if self._action == "showPage" : + self._parent._pagenumber = self._parent._pagenumber + 1 + self._parent._PyWrite("\n # Begins page %i" % self._parent._pagenumber) + elif self._action in [ "endForm", "drawPath", "clipPath" ] : + self._parent._PyWrite("") + + _name = "c" + def __init__(self, *args, **kwargs) : + """Initialize and begins source code.""" + self._parent = self # nice trick, isn't it ? + self._in = 0 + self._contextlevel = 0 + self._pagenumber = 1 + self._formnumber = 0 + self._footerpresent = 0 + self._object = apply(canvas.Canvas, args, kwargs) + self._pyfile = cStringIO.StringIO() + self._PyWrite(PyHeader) + try : + del kwargs["filename"] + except KeyError : + pass + self._PyWrite(" # create the PDF document\n %s = Canvas(file, %s)\n\n # Begins page 1" % (self._name, apply(buildargs, args[1:], kwargs))) + + def __nonzero__(self) : + """This is needed by platypus' tables.""" + return 1 + + def __str__(self) : + """Returns the equivalent Python source code.""" + if not self._footerpresent : + self._PyWrite(PyFooter) + self._footerpresent = 1 + return self._pyfile.getvalue() + + def __getattr__(self, name) : + """Method or attribute access.""" + if name == "beginPath" : + return self.PathObject(self) + elif name == "beginText" : + return self.TextObject(self) + else : + return self.Action(self, name) + + def _PyWrite(self, pycode) : + """Outputs the source code with a trailing newline.""" + self._pyfile.write("%s\n" % pycode) + +if __name__ == '__main__': + print 'For test scripts, look in reportlab/test' diff --git a/bin/reportlab/pdfgen/textobject.py b/bin/reportlab/pdfgen/textobject.py new file mode 100755 index 00000000000..32affe605d3 --- /dev/null +++ b/bin/reportlab/pdfgen/textobject.py @@ -0,0 +1,398 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/pdfgen/textobject.py +__version__=''' $Id: textobject.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' +__doc__=""" +PDFTextObject is an efficient way to add text to a Canvas. Do not +instantiate directly, obtain one from the Canvas instead. + +Progress Reports: +8.83, 2000-01-13, gmcm: + created from pdfgen.py +""" + +import string +from types import * +from reportlab.lib.colors import Color, CMYKColor, toColor +from reportlab.lib.utils import fp_str +from reportlab.pdfbase import pdfmetrics + +_SeqTypes=(TupleType,ListType) + +class _PDFColorSetter: + '''Abstracts the color setting operations; used in Canvas and Textobject + asseumes we have a _code object''' + def setFillColorCMYK(self, c, m, y, k): + """set the fill color useing negative color values + (cyan, magenta, yellow and darkness value). + Takes 4 arguments between 0.0 and 1.0""" + self._fillColorCMYK = (c, m, y, k) + self._code.append('%s k' % fp_str(c, m, y, k)) + + def setStrokeColorCMYK(self, c, m, y, k): + """set the stroke color useing negative color values + (cyan, magenta, yellow and darkness value). + Takes 4 arguments between 0.0 and 1.0""" + self._strokeColorCMYK = (c, m, y, k) + self._code.append('%s K' % fp_str(c, m, y, k)) + + def setFillColorRGB(self, r, g, b): + """Set the fill color using positive color description + (Red,Green,Blue). Takes 3 arguments between 0.0 and 1.0""" + self._fillColorRGB = (r, g, b) + self._code.append('%s rg' % fp_str(r,g,b)) + + def setStrokeColorRGB(self, r, g, b): + """Set the stroke color using positive color description + (Red,Green,Blue). Takes 3 arguments between 0.0 and 1.0""" + self._strokeColorRGB = (r, g, b) + self._code.append('%s RG' % fp_str(r,g,b)) + + def setFillColor(self, aColor): + """Takes a color object, allowing colors to be referred to by name""" + if isinstance(aColor, CMYKColor): + d = aColor.density + c,m,y,k = (d*aColor.cyan, d*aColor.magenta, d*aColor.yellow, d*aColor.black) + self._fillColorCMYK = (c, m, y, k) + self._code.append('%s k' % fp_str(c, m, y, k)) + elif isinstance(aColor, Color): + rgb = (aColor.red, aColor.green, aColor.blue) + self._fillColorRGB = rgb + self._code.append('%s rg' % fp_str(rgb) ) + elif type(aColor) in _SeqTypes: + l = len(aColor) + if l==3: + self._fillColorRGB = aColor + self._code.append('%s rg' % fp_str(aColor) ) + elif l==4: + self.setFillColorCMYK(aColor[0], aColor[1], aColor[2], aColor[3]) + else: + raise 'Unknown color', str(aColor) + elif type(aColor) is StringType: + self.setFillColor(toColor(aColor)) + else: + raise 'Unknown color', str(aColor) + + def setStrokeColor(self, aColor): + """Takes a color object, allowing colors to be referred to by name""" + if isinstance(aColor, CMYKColor): + d = aColor.density + c,m,y,k = (d*aColor.cyan, d*aColor.magenta, d*aColor.yellow, d*aColor.black) + self._strokeColorCMYK = (c, m, y, k) + self._code.append('%s K' % fp_str(c, m, y, k)) + elif isinstance(aColor, Color): + rgb = (aColor.red, aColor.green, aColor.blue) + self._strokeColorRGB = rgb + self._code.append('%s RG' % fp_str(rgb) ) + elif type(aColor) in _SeqTypes: + l = len(aColor) + if l==3: + self._strokeColorRGB = aColor + self._code.append('%s RG' % fp_str(aColor) ) + elif l==4: + self.setStrokeColorCMYK(aColor[0], aColor[1], aColor[2], aColor[3]) + else: + raise 'Unknown color', str(aColor) + elif type(aColor) is StringType: + self.setStrokeColor(toColor(aColor)) + else: + raise 'Unknown color', str(aColor) + + def setFillGray(self, gray): + """Sets the gray level; 0.0=black, 1.0=white""" + self._fillColorRGB = (gray, gray, gray) + self._code.append('%s g' % fp_str(gray)) + + def setStrokeGray(self, gray): + """Sets the gray level; 0.0=black, 1.0=white""" + self._strokeColorRGB = (gray, gray, gray) + self._code.append('%s G' % fp_str(gray)) + +class PDFTextObject(_PDFColorSetter): + """PDF logically separates text and graphics drawing; text + operations need to be bracketed between BT (Begin text) and + ET operators. This class ensures text operations are + properly encapusalted. Ask the canvas for a text object + with beginText(x, y). Do not construct one directly. + Do not use multiple text objects in parallel; PDF is + not multi-threaded! + + It keeps track of x and y coordinates relative to its origin.""" + + def __init__(self, canvas, x=0,y=0): + self._code = ['BT'] #no point in [] then append RGB + self._canvas = canvas #canvas sets this so it has access to size info + self._fontname = self._canvas._fontname + self._fontsize = self._canvas._fontsize + self._leading = self._canvas._leading + font = pdfmetrics.getFont(self._fontname) + self._dynamicFont = getattr(font, '_dynamicFont', 0) + self._curSubset = -1 + + self.setTextOrigin(x, y) + + def getCode(self): + "pack onto one line; used internally" + self._code.append('ET') + return string.join(self._code, ' ') + + def setTextOrigin(self, x, y): + if self._canvas.bottomup: + self._code.append('1 0 0 1 %s Tm' % fp_str(x, y)) #bottom up + else: + self._code.append('1 0 0 -1 %s Tm' % fp_str(x, y)) #top down + + # The current cursor position is at the text origin + self._x0 = self._x = x + self._y0 = self._y = y + + def setTextTransform(self, a, b, c, d, e, f): + "Like setTextOrigin, but does rotation, scaling etc." + if not self._canvas.bottomup: + c = -c #reverse bottom row of the 2D Transform + d = -d + self._code.append('%s Tm' % fp_str(a, b, c, d, e, f)) + + # The current cursor position is at the text origin Note that + # we aren't keeping track of all the transform on these + # coordinates: they are relative to the rotations/sheers + # defined in the matrix. + self._x0 = self._x = e + self._y0 = self._y = f + + def moveCursor(self, dx, dy): + + """Starts a new line at an offset dx,dy from the start of the + current line. This does not move the cursor relative to the + current position, and it changes the current offset of every + future line drawn (i.e. if you next do a textLine() call, it + will move the cursor to a position one line lower than the + position specificied in this call. """ + + # Check if we have a previous move cursor call, and combine + # them if possible. + if self._code and self._code[-1][-3:]==' Td': + L = string.split(self._code[-1]) + if len(L)==3: + del self._code[-1] + else: + self._code[-1] = string.join(L[:-4]) + + # Work out the last movement + lastDx = float(L[-3]) + lastDy = float(L[-2]) + + # Combine the two movement + dx += lastDx + dy -= lastDy + + # We will soon add the movement to the line origin, so if + # we've already done this for lastDx, lastDy, remove it + # first (so it will be right when added back again). + self._x0 -= lastDx + self._y0 -= lastDy + + # Output the move text cursor call. + self._code.append('%s Td' % fp_str(dx, -dy)) + + # Keep track of the new line offsets and the cursor position + self._x0 += dx + self._y0 += dy + self._x = self._x0 + self._y = self._y0 + + def setXPos(self, dx): + """Starts a new line dx away from the start of the + current line - NOT from the current point! So if + you call it in mid-sentence, watch out.""" + self.moveCursor(dx,0) + + def getCursor(self): + """Returns current text position relative to the last origin.""" + return (self._x, self._y) + + def getStartOfLine(self): + """Returns a tuple giving the text position of the start of the + current line.""" + return (self._x0, self._y0) + + def getX(self): + """Returns current x position relative to the last origin.""" + return self._x + + def getY(self): + """Returns current y position relative to the last origin.""" + return self._y + + def _setFont(self, psfontname, size): + """Sets the font and fontSize + Raises a readable exception if an illegal font + is supplied. Font names are case-sensitive! Keeps track + of font anme and size for metrics.""" + self._fontname = psfontname + self._fontsize = size + font = pdfmetrics.getFont(self._fontname) + + self._dynamicFont = getattr(font, '_dynamicFont', 0) + if self._dynamicFont: + self._curSubset = -1 + else: + pdffontname = self._canvas._doc.getInternalFontName(psfontname) + self._code.append('%s %s Tf' % (pdffontname, fp_str(size))) + + def setFont(self, psfontname, size, leading = None): + """Sets the font. If leading not specified, defaults to 1.2 x + font size. Raises a readable exception if an illegal font + is supplied. Font names are case-sensitive! Keeps track + of font anme and size for metrics.""" + self._fontname = psfontname + self._fontsize = size + if leading is None: + leading = size * 1.2 + self._leading = leading + font = pdfmetrics.getFont(self._fontname) + + self._dynamicFont = getattr(font, '_dynamicFont', 0) + if self._dynamicFont: + self._curSubset = -1 + else: + pdffontname = self._canvas._doc.getInternalFontName(psfontname) + self._code.append('%s %s Tf %s TL' % (pdffontname, fp_str(size), fp_str(leading))) + + def setCharSpace(self, charSpace): + """Adjusts inter-character spacing""" + self._charSpace = charSpace + self._code.append('%s Tc' % fp_str(charSpace)) + + def setWordSpace(self, wordSpace): + """Adjust inter-word spacing. This can be used + to flush-justify text - you get the width of the + words, and add some space between them.""" + self._wordSpace = wordSpace + self._code.append('%s Tw' % fp_str(wordSpace)) + + def setHorizScale(self, horizScale): + "Stretches text out horizontally" + self._horizScale = 100 + horizScale + self._code.append('%s Tz' % fp_str(horizScale)) + + def setLeading(self, leading): + "How far to move down at the end of a line." + self._leading = leading + self._code.append('%s TL' % fp_str(leading)) + + def setTextRenderMode(self, mode): + """Set the text rendering mode. + + 0 = Fill text + 1 = Stroke text + 2 = Fill then stroke + 3 = Invisible + 4 = Fill text and add to clipping path + 5 = Stroke text and add to clipping path + 6 = Fill then stroke and add to clipping path + 7 = Add to clipping path""" + + assert mode in (0,1,2,3,4,5,6,7), "mode must be in (0,1,2,3,4,5,6,7)" + self._textRenderMode = mode + self._code.append('%d Tr' % mode) + + def setRise(self, rise): + "Move text baseline up or down to allow superscrip/subscripts" + self._rise = rise + self._y = self._y - rise # + ? _textLineMatrix? + self._code.append('%s Ts' % fp_str(rise)) + + def _formatText(self, text): + "Generates PDF text output operator(s)" + canv = self._canvas + font = pdfmetrics.getFont(self._fontname) + R = [] + if self._dynamicFont: + #it's a truetype font and should be utf8. If an error is raised, + for subset, t in font.splitString(text, canv._doc): + if subset != self._curSubset: + pdffontname = font.getSubsetInternalName(subset, canv._doc) + R.append("%s %s Tf %s TL" % (pdffontname, fp_str(self._fontsize), fp_str(self._leading))) + self._curSubset = subset + R.append("(%s) Tj" % canv._escape(t)) + elif font._multiByte: + #all the fonts should really work like this - let them know more about PDF... + R.append("%s %s Tf %s TL" % ( + canv._doc.getInternalFontName(font.fontName), + fp_str(self._fontsize), + fp_str(self._leading) + )) + R.append("(%s) Tj" % font.formatForPdf(text)) + + else: + #convert to T1 coding + fc = font + if not isinstance(text,unicode): + try: + text = text.decode('utf8') + except UnicodeDecodeError,e: + i,j = e.args[2:4] + raise UnicodeDecodeError(*(e.args[:4]+('%s\n%s-->%s<--%s' % (e.args[4],text[i-10:i],text[i:j],text[j:j+10]),))) + + for f, t in pdfmetrics.unicode2T1(text,[font]+font.substitutionFonts): + if f!=fc: + R.append("%s %s Tf %s TL" % (canv._doc.getInternalFontName(f.fontName), fp_str(self._fontsize), fp_str(self._leading))) + fc = f + R.append("(%s) Tj" % canv._escape(t)) + if font!=fc: + R.append("%s %s Tf %s TL" % (canv._doc.getInternalFontName(self._fontname), fp_str(self._fontsize), fp_str(self._leading))) + return ' '.join(R) + + def _textOut(self, text, TStar=0): + "prints string at current point, ignores text cursor" + self._code.append('%s%s' % (self._formatText(text), (TStar and ' T*' or ''))) + + def textOut(self, text): + """prints string at current point, text cursor moves across.""" + self._x = self._x + self._canvas.stringWidth(text, self._fontname, self._fontsize) + self._code.append(self._formatText(text)) + + def textLine(self, text=''): + """prints string at current point, text cursor moves down. + Can work with no argument to simply move the cursor down.""" + + # Update the coordinates of the cursor + self._x = self._x0 + if self._canvas.bottomup: + self._y = self._y - self._leading + else: + self._y = self._y + self._leading + + # Update the location of the start of the line + # self._x0 is unchanged + self._y0 = self._y + + # Output the text followed by a PDF newline command + self._code.append('%s T*' % self._formatText(text)) + + def textLines(self, stuff, trim=1): + """prints multi-line or newlined strings, moving down. One + comon use is to quote a multi-line block in your Python code; + since this may be indented, by default it trims whitespace + off each line and from the beginning; set trim=0 to preserve + whitespace.""" + if type(stuff) == StringType: + lines = string.split(string.strip(stuff), '\n') + if trim==1: + lines = map(string.strip,lines) + elif type(stuff) == ListType: + lines = stuff + elif type(stuff) == TupleType: + lines = stuff + else: + assert 1==0, "argument to textlines must be string,, list or tuple" + + # Output each line one at a time. This used to be a long-hand + # copy of the textLine code, now called as a method. + for line in lines: + self.textLine(line) + + def __nonzero__(self): + 'PDFTextObject is true if it has something done after the init' + return self._code != ['BT'] diff --git a/bin/reportlab/platypus/__init__.py b/bin/reportlab/platypus/__init__.py new file mode 100755 index 00000000000..7a410633350 --- /dev/null +++ b/bin/reportlab/platypus/__init__.py @@ -0,0 +1,15 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/__init__.py +__version__=''' $Id: __init__.py 2775 2006-02-15 12:17:24Z rgbecker $ ''' +__doc__='' +from reportlab.platypus.flowables import Flowable, Image, Macro, PageBreak, Preformatted, Spacer, XBox, \ + CondPageBreak, KeepTogether, TraceInfo, FailOnWrap, FailOnDraw, PTOContainer, \ + KeepInFrame, ParagraphAndImage, ImageAndFlowables +from reportlab.platypus.paragraph import Paragraph, cleanBlockQuotedText, ParaLines +from reportlab.platypus.paraparser import ParaFrag +from reportlab.platypus.tables import Table, TableStyle, CellStyle, LongTable +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate import BaseDocTemplate, NextPageTemplate, PageTemplate, ActionFlowable, \ + SimpleDocTemplate, FrameBreak, PageBegin, Indenter +from xpreformatted import XPreformatted diff --git a/bin/reportlab/platypus/doctemplate.py b/bin/reportlab/platypus/doctemplate.py new file mode 100644 index 00000000000..f38d6506b1f --- /dev/null +++ b/bin/reportlab/platypus/doctemplate.py @@ -0,0 +1,953 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/doctemplate.py + +__version__=''' $Id: doctemplate.py 2852 2006-05-08 15:04:15Z rgbecker $ ''' + +__doc__=""" +This module contains the core structure of platypus. + +Platypus constructs documents. Document styles are determined by DocumentTemplates. + +Each DocumentTemplate contains one or more PageTemplates which defines the look of the +pages of the document. + +Each PageTemplate has a procedure for drawing the "non-flowing" part of the page +(for example the header, footer, page number, fixed logo graphic, watermark, etcetera) and +a set of Frames which enclose the flowing part of the page (for example the paragraphs, +tables, or non-fixed diagrams of the text). + +A document is built when a DocumentTemplate is fed a sequence of Flowables. +The action of the build consumes the flowables in order and places them onto +frames on pages as space allows. When a frame runs out of space the next frame +of the page is used. If no frame remains a new page is created. A new page +can also be created if a page break is forced. + +The special invisible flowable NextPageTemplate can be used to specify +the page template for the next page (which by default is the one being used +for the current frame). +""" + +from reportlab.platypus.flowables import * +from reportlab.lib.units import inch +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.rl_config import defaultPageSize, verbose +import reportlab.lib.sequencer +from reportlab.pdfgen import canvas + +from types import * +import sys +import logging +logger = logging.getLogger("reportlab.platypus") + +class LayoutError(Exception): + pass + +def _doNothing(canvas, doc): + "Dummy callback for onPage" + pass + + +class IndexingFlowable(Flowable): + """Abstract interface definition for flowables which might + hold references to other pages or themselves be targets + of cross-references. XRefStart, XRefDest, Table of Contents, + Indexes etc.""" + def isIndexing(self): + return 1 + + def isSatisfied(self): + return 1 + + def notify(self, kind, stuff): + """This will be called by the framework wherever 'stuff' happens. + 'kind' will be a value that can be used to decide whether to + pay attention or not.""" + pass + + def beforeBuild(self): + """Called by multiBuild before it starts; use this to clear + old contents""" + pass + + def afterBuild(self): + """Called after build ends but before isSatisfied""" + pass + +class ActionFlowable(Flowable): + '''This Flowable is never drawn, it can be used for data driven controls + For example to change a page template (from one column to two, for example) + use NextPageTemplate which creates an ActionFlowable. + ''' + def __init__(self,action=()): + #must call super init to ensure it has a width and height (of zero), + #as in some cases the packer might get called on it... + Flowable.__init__(self) + if type(action) not in (ListType, TupleType): + action = (action,) + self.action = tuple(action) + + def apply(self,doc): + ''' + This is called by the doc.build processing to allow the instance to + implement its behaviour + ''' + action = self.action[0] + args = tuple(self.action[1:]) + arn = 'handle_'+action + try: + apply(getattr(doc,arn), args) + except AttributeError, aerr: + if aerr.args[0]==arn: + raise NotImplementedError, "Can't handle ActionFlowable(%s)" % action + else: + raise + except "bogus": + t, v, unused = sys.exc_info() + raise t, "%s\n handle_%s args=%s"%(v,action,args) + + def __call__(self): + return self + + def identity(self, maxLen=None): + return "ActionFlowable: %s%s" % (str(self.action),self._frameName()) + +class LCActionFlowable(ActionFlowable): + locChanger = 1 #we cause a frame or page change + + def wrap(self, availWidth, availHeight): + '''Should never be called.''' + raise NotImplementedError + + def draw(self): + '''Should never be called.''' + raise NotImplementedError + +class NextFrameFlowable(ActionFlowable): + def __init__(self,ix,resume=0): + ActionFlowable.__init__(self,('nextFrame',ix,resume)) + +class CurrentFrameFlowable(LCActionFlowable): + def __init__(self,ix,resume=0): + ActionFlowable.__init__(self,('currentFrame',ix,resume)) + +class NullActionFlowable(ActionFlowable): + def apply(self): + pass + +class _FrameBreak(LCActionFlowable): + ''' + A special ActionFlowable that allows setting doc._nextFrameIndex + + eg story.append(FrameBreak('mySpecialFrame')) + ''' + def __call__(self,ix=None,resume=0): + r = self.__class__(self.action+(resume,)) + r._ix = ix + return r + + def apply(self,doc): + if getattr(self,'_ix',None): + doc.handle_nextFrame(self._ix) + ActionFlowable.apply(self,doc) + +FrameBreak = _FrameBreak('frameEnd') +PageBegin = LCActionFlowable('pageBegin') + +def _evalMeasurement(n): + if type(n) is type(''): + from paraparser import _num + n = _num(n) + if type(n) is type(()): n = n[1] + return n + +class FrameActionFlowable(Flowable): + def __init__(self,*arg,**kw): + raise NotImplementedError('Abstract Class') + + def frameAction(self,frame): + raise NotImplementedError('Abstract Class') + +class Indenter(FrameActionFlowable): + """Increases or decreases left and right margins of frame. + + This allows one to have a 'context-sensitive' indentation + and makes nested lists way easier. + """ + def __init__(self, left=0, right=0): + self.left = _evalMeasurement(left) + self.right = _evalMeasurement(right) + + def frameAction(self, frame): + frame._leftExtraIndent += self.left + frame._rightExtraIndent += self.right + +class NextPageTemplate(ActionFlowable): + """When you get to the next page, use the template specified (change to two column, for example) """ + def __init__(self,pt): + ActionFlowable.__init__(self,('nextPageTemplate',pt)) + + +class PageTemplate: + """ + essentially a list of Frames and an onPage routine to call at the start + of a page when this is selected. onPageEnd gets called at the end. + derived classes can also implement beforeDrawPage and afterDrawPage if they want + """ + def __init__(self,id=None,frames=[],onPage=_doNothing, onPageEnd=_doNothing, + pagesize=None): + if type(frames) not in (ListType,TupleType): frames = [frames] + assert filter(lambda x: not isinstance(x,Frame), frames)==[], "frames argument error" + self.id = id + self.frames = frames + self.onPage = onPage + self.onPageEnd = onPageEnd + self.pagesize = pagesize + + def beforeDrawPage(self,canv,doc): + """Override this if you want additional functionality or prefer + a class based page routine. Called before any flowables for + this page are processed.""" + pass + + def checkPageSize(self,canv,doc): + '''This gets called by the template framework + If canv size != template size then the canv size is set to + the template size or if that's not available to the + doc size. + ''' + #### NEVER EVER EVER COMPARE FLOATS FOR EQUALITY + #RGB converting pagesizes to ints means we are accurate to one point + #RGB I suggest we should be aiming a little better + cp = None + dp = None + sp = None + if canv._pagesize: cp = map(int, canv._pagesize) + if self.pagesize: sp = map(int, self.pagesize) + if doc.pagesize: dp = map(int, doc.pagesize) + if cp!=sp: + if sp: + canv.setPageSize(self.pagesize) + elif cp!=dp: + canv.setPageSize(doc.pagesize) + + def afterDrawPage(self, canv, doc): + """This is called after the last flowable for the page has + been processed. You might use this if the page header or + footer needed knowledge of what flowables were drawn on + this page.""" + pass + + +class BaseDocTemplate: + """ + First attempt at defining a document template class. + + The basic idea is simple. + 0) The document has a list of data associated with it + this data should derive from flowables. We'll have + special classes like PageBreak, FrameBreak to do things + like forcing a page end etc. + + 1) The document has one or more page templates. + + 2) Each page template has one or more frames. + + 3) The document class provides base methods for handling the + story events and some reasonable methods for getting the + story flowables into the frames. + + 4) The document instances can override the base handler routines. + + Most of the methods for this class are not called directly by the user, + but in some advanced usages they may need to be overridden via subclassing. + + EXCEPTION: doctemplate.build(...) must be called for most reasonable uses + since it builds a document using the page template. + + Each document template builds exactly one document into a file specified + by the filename argument on initialization. + + Possible keyword arguments for the initialization: + + pageTemplates: A list of templates. Must be nonempty. Names + assigned to the templates are used for referring to them so no two used + templates should have the same name. For example you might want one template + for a title page, one for a section first page, one for a first page of + a chapter and two more for the interior of a chapter on odd and even pages. + If this argument is omitted then at least one pageTemplate should be provided + using the addPageTemplates method before the document is built. + pageSize: a 2-tuple or a size constant from reportlab/lib/pagesizes.pu. + Used by the SimpleDocTemplate subclass which does NOT accept a list of + pageTemplates but makes one for you; ignored when using pageTemplates. + + showBoundary: if set draw a box around the frame boundaries. + leftMargin: + rightMargin: + topMargin: + bottomMargin: Margin sizes in points (default 1 inch) + These margins may be overridden by the pageTemplates. They are primarily of interest + for the SimpleDocumentTemplate subclass. + allowSplitting: If set flowables (eg, paragraphs) may be split across frames or pages + (default: 1) + title: Internal title for document (does not automatically display on any page) + author: Internal author for document (does not automatically display on any page) + """ + _initArgs = { 'pagesize':defaultPageSize, + 'pageTemplates':[], + 'showBoundary':0, + 'leftMargin':inch, + 'rightMargin':inch, + 'topMargin':inch, + 'bottomMargin':inch, + 'allowSplitting':1, + 'title':None, + 'author':None, + 'invariant':None, + 'pageCompression':None, + '_pageBreakQuick':1, + 'rotation':0, + '_debug':0} + _invalidInitArgs = () + _firstPageTemplateIndex = 0 + + def __init__(self, filename, **kw): + """create a document template bound to a filename (see class documentation for keyword arguments)""" + self.filename = filename + + for k in self._initArgs.keys(): + if not kw.has_key(k): + v = self._initArgs[k] + else: + if k in self._invalidInitArgs: + raise ValueError, "Invalid argument %s" % k + v = kw[k] + setattr(self,k,v) + + p = self.pageTemplates + self.pageTemplates = [] + self.addPageTemplates(p) + + # facility to assist multi-build and cross-referencing. + # various hooks can put things into here - key is what + # you want, value is a page number. This can then be + # passed to indexing flowables. + self._pageRefs = {} + self._indexingFlowables = [] + + + #callback facility for progress monitoring + self._onPage = None + self._onProgress = None + self._flowableCount = 0 # so we know how far to go + + + #infinite loop detection if we start doing lots of empty pages + self._curPageFlowableCount = 0 + self._emptyPages = 0 + self._emptyPagesAllowed = 10 + + #context sensitive margins - set by story, not from outside + self._leftExtraIndent = 0.0 + self._rightExtraIndent = 0.0 + + self._calc() + self.afterInit() + + def _calc(self): + self._rightMargin = self.pagesize[0] - self.rightMargin + self._topMargin = self.pagesize[1] - self.topMargin + self.width = self._rightMargin - self.leftMargin + self.height = self._topMargin - self.bottomMargin + + def setPageCallBack(self, func): + 'Simple progress monitor - func(pageNo) called on each new page' + self._onPage = func + + def setProgressCallBack(self, func): + '''Cleverer progress monitor - func(typ, value) called regularly''' + self._onProgress = func + + def clean_hanging(self): + 'handle internal postponed actions' + while len(self._hanging): + self.handle_flowable(self._hanging) + + def addPageTemplates(self,pageTemplates): + 'add one or a sequence of pageTemplates' + if type(pageTemplates) not in (ListType,TupleType): + pageTemplates = [pageTemplates] + #this test below fails due to inconsistent imports! + #assert filter(lambda x: not isinstance(x,PageTemplate), pageTemplates)==[], "pageTemplates argument error" + for t in pageTemplates: + self.pageTemplates.append(t) + + def handle_documentBegin(self): + '''implement actions at beginning of document''' + self._hanging = [PageBegin] + self.pageTemplate = self.pageTemplates[self._firstPageTemplateIndex] + self.page = 0 + self.beforeDocument() + + def handle_pageBegin(self): + '''Perform actions required at beginning of page. + shouldn't normally be called directly''' + self.page += 1 + if self._debug: logger.debug("beginning page %d" % self.page) + self.pageTemplate.beforeDrawPage(self.canv,self) + self.pageTemplate.checkPageSize(self.canv,self) + self.pageTemplate.onPage(self.canv,self) + for f in self.pageTemplate.frames: f._reset() + self.beforePage() + #keep a count of flowables added to this page. zero indicates bad stuff + self._curPageFlowableCount = 0 + if hasattr(self,'_nextFrameIndex'): + del self._nextFrameIndex + self.frame = self.pageTemplate.frames[0] + self.frame._debug = self._debug + self.handle_frameBegin() + + def handle_pageEnd(self): + ''' show the current page + check the next page template + hang a page begin + ''' + #detect infinite loops... + if self._curPageFlowableCount == 0: + self._emptyPages += 1 + else: + self._emptyPages = 0 + if self._emptyPages >= self._emptyPagesAllowed: + if 1: + ident = "More than %d pages generated without content - halting layout. Likely that a flowable is too large for any frame." % self._emptyPagesAllowed + #leave to keep apart from the raise + raise LayoutError(ident) + else: + pass #attempt to restore to good state + else: + if self._onProgress: + self._onProgress('PAGE', self.canv.getPageNumber()) + self.pageTemplate.afterDrawPage(self.canv, self) + self.pageTemplate.onPageEnd(self.canv, self) + self.afterPage() + if self._debug: logger.debug("ending page %d" % self.page) + self.canv.setPageRotation(getattr(self.pageTemplate,'rotation',self.rotation)) + self.canv.showPage() + + if hasattr(self,'_nextPageTemplateCycle'): + #they are cycling through pages'; we keep the index + cyc = self._nextPageTemplateCycle + idx = self._nextPageTemplateIndex + self.pageTemplate = cyc[idx] #which is one of the ones in the list anyway + #bump up by 1 + self._nextPageTemplateIndex = (idx + 1) % len(cyc) + elif hasattr(self,'_nextPageTemplateIndex'): + self.pageTemplate = self.pageTemplates[self._nextPageTemplateIndex] + del self._nextPageTemplateIndex + if self._emptyPages==0: + pass #store good state here + self._hanging.append(PageBegin) + + def handle_pageBreak(self,slow=None): + '''some might choose not to end all the frames''' + if self._pageBreakQuick and not slow: + self.handle_pageEnd() + else: + n = len(self._hanging) + while len(self._hanging)==n: + self.handle_frameEnd() + + def handle_frameBegin(self,resume=0): + '''What to do at the beginning of a frame''' + f = self.frame + if f._atTop: + if self.showBoundary or self.frame.showBoundary: + self.frame.drawBoundary(self.canv) + f._leftExtraIndent = self._leftExtraIndent + f._rightExtraIndent = self._rightExtraIndent + + def handle_frameEnd(self,resume=0): + ''' Handles the semantics of the end of a frame. This includes the selection of + the next frame or if this is the last frame then invoke pageEnd. + ''' + + self._leftExtraIndent = self.frame._leftExtraIndent + self._rightExtraIndent = self.frame._rightExtraIndent + + if hasattr(self,'_nextFrameIndex'): + self.frame = self.pageTemplate.frames[self._nextFrameIndex] + self.frame._debug = self._debug + del self._nextFrameIndex + self.handle_frameBegin(resume) + elif hasattr(self.frame,'lastFrame') or self.frame is self.pageTemplate.frames[-1]: + self.handle_pageEnd() + self.frame = None + else: + f = self.frame + self.frame = self.pageTemplate.frames[self.pageTemplate.frames.index(f) + 1] + self.frame._debug = self._debug + self.handle_frameBegin() + + def handle_nextPageTemplate(self,pt): + '''On endPage change to the page template with name or index pt''' + if type(pt) is StringType: + if hasattr(self, '_nextPageTemplateCycle'): del self._nextPageTemplateCycle + for t in self.pageTemplates: + if t.id == pt: + self._nextPageTemplateIndex = self.pageTemplates.index(t) + return + raise ValueError, "can't find template('%s')"%pt + elif type(pt) is IntType: + if hasattr(self, '_nextPageTemplateCycle'): del self._nextPageTemplateCycle + self._nextPageTemplateIndex = pt + elif type(pt) in (ListType, TupleType): + #used for alternating left/right pages + #collect the refs to the template objects, complain if any are bad + cycle = [] + for templateName in pt: + found = 0 + for t in self.pageTemplates: + if t.id == templateName: + cycle.append(t) + found = 1 + if not found: + raise ValueError("Cannot find page template called %s" % templateName) + #double-check all of them are there... + + first = cycle[0] + #ensure we start on the first one + self._nextPageTemplateCycle = cycle + self._nextPageTemplateIndex = 0 #indexes into the cycle + else: + raise TypeError, "argument pt should be string or integer or list" + + def handle_nextFrame(self,fx,resume=0): + '''On endFrame change to the frame with name or index fx''' + if type(fx) is StringType: + for f in self.pageTemplate.frames: + if f.id == fx: + self._nextFrameIndex = self.pageTemplate.frames.index(f) + return + raise ValueError, "can't find frame('%s')"%fx + elif type(fx) is IntType: + self._nextFrameIndex = fx + else: + raise TypeError, "argument fx should be string or integer" + + def handle_currentFrame(self,fx,resume=0): + '''change to the frame with name or index fx''' + self.handle_nextFrame(fx,resume) + self.handle_frameEnd(resume) + + def handle_breakBefore(self, flowables): + '''preprocessing step to allow pageBreakBefore and frameBreakBefore attributes''' + first = flowables[0] + # if we insert a page break before, we'll process that, see it again, + # and go in an infinite loop. So we need to set a flag on the object + # saying 'skip me'. This should be unset on the next pass + if hasattr(first, '_skipMeNextTime'): + delattr(first, '_skipMeNextTime') + return + # this could all be made much quicker by putting the attributes + # in to the flowables with a defult value of 0 + if hasattr(first,'pageBreakBefore') and first.pageBreakBefore == 1: + first._skipMeNextTime = 1 + first.insert(0, PageBreak()) + return + if hasattr(first,'style') and hasattr(first.style, 'pageBreakBefore') and first.style.pageBreakBefore == 1: + first._skipMeNextTime = 1 + flowables.insert(0, PageBreak()) + return + if hasattr(first,'frameBreakBefore') and first.frameBreakBefore == 1: + first._skipMeNextTime = 1 + flowables.insert(0, FrameBreak()) + return + if hasattr(first,'style') and hasattr(first.style, 'frameBreakBefore') and first.style.frameBreakBefore == 1: + first._skipMeNextTime = 1 + flowables.insert(0, FrameBreak()) + return + + def handle_keepWithNext(self, flowables): + "implements keepWithNext" + i = 0 + n = len(flowables) + while i maxPasses: + raise IndexError, "Index entries not resolved after %d passes" % maxPasses + + if verbose: print 'saved' + + #these are pure virtuals override in derived classes + #NB these get called at suitable places by the base class + #so if you derive and override the handle_xxx methods + #it's up to you to ensure that they maintain the needed consistency + def afterInit(self): + """This is called after initialisation of the base class.""" + pass + + def beforeDocument(self): + """This is called before any processing is + done on the document.""" + pass + + def beforePage(self): + """This is called at the beginning of page + processing, and immediately before the + beforeDrawPage method of the current page + template.""" + pass + + def afterPage(self): + """This is called after page processing, and + immediately after the afterDrawPage method + of the current page template.""" + pass + + def filterFlowables(self,flowables): + '''called to filter flowables at the start of the main handle_flowable method. + Upon return if flowables[0] has been set to None it is discarded and the main + method returns. + ''' + pass + + def afterFlowable(self, flowable): + '''called after a flowable has been rendered''' + pass + + +class SimpleDocTemplate(BaseDocTemplate): + """A special case document template that will handle many simple documents. + See documentation for BaseDocTemplate. No pageTemplates are required + for this special case. A page templates are inferred from the + margin information and the onFirstPage, onLaterPages arguments to the build method. + + A document which has all pages with the same look except for the first + page may can be built using this special approach. + """ + _invalidInitArgs = ('pageTemplates',) + + def handle_pageBegin(self): + '''override base method to add a change of page template after the firstpage. + ''' + self._handle_pageBegin() + self._handle_nextPageTemplate('Later') + + def build(self,flowables,onFirstPage=_doNothing, onLaterPages=_doNothing, canvasmaker=canvas.Canvas): + """build the document using the flowables. Annotate the first page using the onFirstPage + function and later pages using the onLaterPages function. The onXXX pages should follow + the signature + + def myOnFirstPage(canvas, document): + # do annotations and modify the document + ... + + The functions can do things like draw logos, page numbers, + footers, etcetera. They can use external variables to vary + the look (for example providing page numbering or section names). + """ + self._calc() #in case we changed margins sizes etc + frameT = Frame(self.leftMargin, self.bottomMargin, self.width, self.height, id='normal') + self.addPageTemplates([PageTemplate(id='First',frames=frameT, onPage=onFirstPage,pagesize=self.pagesize), + PageTemplate(id='Later',frames=frameT, onPage=onLaterPages,pagesize=self.pagesize)]) + if onFirstPage is _doNothing and hasattr(self,'onFirstPage'): + self.pageTemplates[0].beforeDrawPage = self.onFirstPage + if onLaterPages is _doNothing and hasattr(self,'onLaterPages'): + self.pageTemplates[1].beforeDrawPage = self.onLaterPages + BaseDocTemplate.build(self,flowables, canvasmaker=canvasmaker) + + +def progressCB(typ, value): + """Example prototype for progress monitoring. + + This aims to provide info about what is going on + during a big job. It should enable, for example, a reasonably + smooth progress bar to be drawn. We design the argument + signature to be predictable and conducive to programming in + other (type safe) languages. If set, this will be called + repeatedly with pairs of values. The first is a string + indicating the type of call; the second is a numeric value. + + typ 'STARTING', value = 0 + typ 'SIZE_EST', value = numeric estimate of job size + typ 'PASS', value = number of this rendering pass + typ 'PROGRESS', value = number between 0 and SIZE_EST + typ 'PAGE', value = page number of page + type 'FINISHED', value = 0 + + The sequence is + STARTING - always called once + SIZE_EST - always called once + PROGRESS - called often + PAGE - called often when page is emitted + FINISHED - called when really, really finished + + some juggling is needed to accurately estimate numbers of + pages in pageDrawing mode. + + NOTE: the SIZE_EST is a guess. It is possible that the + PROGRESS value may slightly exceed it, or may even step + back a little on rare occasions. The only way to be + really accurate would be to do two passes, and I don't + want to take that performance hit. + """ + print 'PROGRESS MONITOR: %-10s %d' % (typ, value) + +if __name__ == '__main__': + + def myFirstPage(canvas, doc): + canvas.saveState() + canvas.setStrokeColor(red) + canvas.setLineWidth(5) + canvas.line(66,72,66,PAGE_HEIGHT-72) + canvas.setFont('Times-Bold',24) + canvas.drawString(108, PAGE_HEIGHT-108, "TABLE OF CONTENTS DEMO") + canvas.setFont('Times-Roman',12) + canvas.drawString(4 * inch, 0.75 * inch, "First Page") + canvas.restoreState() + + def myLaterPages(canvas, doc): + canvas.saveState() + canvas.setStrokeColor(red) + canvas.setLineWidth(5) + canvas.line(66,72,66,PAGE_HEIGHT-72) + canvas.setFont('Times-Roman',12) + canvas.drawString(4 * inch, 0.75 * inch, "Page %d" % doc.page) + canvas.restoreState() + + def run(): + objects_to_draw = [] + from reportlab.lib.styles import ParagraphStyle + #from paragraph import Paragraph + from doctemplate import SimpleDocTemplate + + #need a style + normal = ParagraphStyle('normal') + normal.firstLineIndent = 18 + normal.spaceBefore = 6 + from reportlab.lib.randomtext import randomText + import random + for i in range(15): + height = 0.5 + (2*random.random()) + box = XBox(6 * inch, height * inch, 'Box Number %d' % i) + objects_to_draw.append(box) + para = Paragraph(randomText(), normal) + objects_to_draw.append(para) + + SimpleDocTemplate('doctemplate.pdf').build(objects_to_draw, + onFirstPage=myFirstPage,onLaterPages=myLaterPages) + + run() diff --git a/bin/reportlab/platypus/figures.py b/bin/reportlab/platypus/figures.py new file mode 100644 index 00000000000..00aeef7c898 --- /dev/null +++ b/bin/reportlab/platypus/figures.py @@ -0,0 +1,372 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/figures.py +"""This includes some demos of platypus for use in the API proposal""" +__version__=''' $Id: figures.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +import os + +from reportlab.lib import colors +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.utils import recursiveImport +from reportlab.platypus import Frame +from reportlab.platypus import Flowable +from reportlab.platypus import Paragraph +from reportlab.lib.units import inch +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER +from reportlab.lib.validators import isColor +from reportlab.lib.colors import toColor + +captionStyle = ParagraphStyle('Caption', fontName='Times-Italic', fontSize=10, alignment=TA_CENTER) + +class Figure(Flowable): + def __init__(self, width, height, caption="", + captionFont="Times-Italic", captionSize=12, + background=None, + captionTextColor=toColor('black'), + captionBackColor=None, + border=1): + Flowable.__init__(self) + self.width = width + self.figureHeight = height + self.caption = caption + self.captionFont = captionFont + self.captionSize = captionSize + self.captionTextColor = captionTextColor + self.captionBackColor = captionBackColor + self._captionData = None + self.captionHeight = 0 # work out later + self.background = background + self.border = border + self.spaceBefore = 12 + self.spaceAfter = 12 + + def _getCaptionPara(self): + caption = self.caption + captionFont = self.captionFont + captionSize = self.captionSize + captionTextColor = self.captionTextColor + captionBackColor = self.captionBackColor + if self._captionData!=(caption,captionFont,captionSize,captionTextColor,captionBackColor): + self._captionData = (caption,captionFont,captionSize,captionTextColor,captionBackColor) + self.captionStyle = ParagraphStyle( + 'Caption', + fontName=captionFont, + fontSize=captionSize, + leading=1.2*captionSize, + textColor = captionTextColor, + backColor = captionBackColor, + spaceBefore=captionSize * 0.5, + alignment=TA_CENTER) + #must build paragraph now to get sequencing in synch with rest of story + self.captionPara = Paragraph(self.caption, self.captionStyle) + + def wrap(self, availWidth, availHeight): + # try to get the caption aligned + if self.caption: + self._getCaptionPara() + (w, h) = self.captionPara.wrap(self.width, availHeight - self.figureHeight) + self.captionHeight = h + self.height = self.captionHeight + self.figureHeight + if w>self.width: self.width = w + else: + self.height = self.figureHeight + self.dx = 0.5 * (availWidth - self.width) + return (self.width, self.height) + + def draw(self): + self.canv.translate(self.dx, 0) + if self.caption: + self._getCaptionPara() + self.drawCaption() + self.canv.translate(0, self.captionHeight) + if self.background: + self.drawBackground() + if self.border: + self.drawBorder() + self.drawFigure() + + def drawBorder(self): + self.canv.rect(0, 0, self.width, self.figureHeight) + + def _doBackground(self, color): + self.canv.saveState() + self.canv.setFillColor(self.background) + self.canv.rect(0, 0, self.width, self.figureHeight, fill=1) + self.canv.restoreState() + + def drawBackground(self): + """For use when using a figure on a differently coloured background. + Allows you to specify a colour to be used as a background for the figure.""" + if isColor(self.background): + self._doBackground(self.background) + else: + try: + c = toColor(self.background) + self._doBackground(c) + except: + pass + + def drawCaption(self): + self.captionPara.drawOn(self.canv, 0, 0) + + def drawFigure(self): + pass + +def drawPage(canvas,x, y, width, height): + #draws something which looks like a page + pth = canvas.beginPath() + corner = 0.05*width + + # shaded backdrop offset a little + canvas.setFillColorRGB(0.5,0.5,0.5) + canvas.rect(x + corner, y - corner, width, height, stroke=0, fill=1) + + #'sheet of paper' in light yellow + canvas.setFillColorRGB(1,1,0.9) + canvas.setLineWidth(0) + canvas.rect(x, y, width, height, stroke=1, fill=1) + + #reset + canvas.setFillColorRGB(0,0,0) + canvas.setStrokeColorRGB(0,0,0) + +class PageFigure(Figure): + """Shows a blank page in a frame, and draws on that. Used in + illustrations of how PLATYPUS works.""" + def __init__(self, background=None): + Figure.__init__(self, 3*inch, 3*inch) + self.caption = 'Figure 1 - a blank page' + self.captionStyle = captionStyle + self.background = background + + def drawVirtualPage(self): + pass + + def drawFigure(self): + drawPage(self.canv, 0.625*inch, 0.25*inch, 1.75*inch, 2.5*inch) + self.canv.translate(0.625*inch, 0.25*inch) + self.canv.scale(1.75/8.27, 2.5/11.69) + self.drawVirtualPage() + +class PlatPropFigure1(PageFigure): + """This shows a page with a frame on it""" + def __init__(self): + PageFigure.__init__(self) + self.caption = "Figure 1 - a page with a simple frame" + def drawVirtualPage(self): + demo1(self.canv) + +class FlexFigure(Figure): + """Base for a figure class with a caption. Can grow or shrink in proportion""" + def __init__(self, width, height, caption, background=None): + Figure.__init__(self, width, height, caption, + captionFont="Helvetica-Oblique", captionSize=8, + background=None) + self.shrinkToFit = 1 #if set and wrap is too tight, shrinks + self.growToFit = 1 #if set and wrap is too tight, shrinks + self.scaleFactor = None + self.captionStyle = ParagraphStyle( + 'Caption', + fontName='Times', #'Helvetica-Oblique', + fontSize=4, #8, + spaceBefore=9, #3, + alignment=TA_CENTER + ) + self._scaleFactor = None + self.background = background + + def _scale(self,availWidth,availHeight): + "Rescale to fit according to the rules, but only once" + if self._scaleFactor is None or self.width>availWidth or self.height>availHeight: + w, h = Figure.wrap(self, availWidth, availHeight) + captionHeight = h - self.figureHeight + if self.scaleFactor is None: + #scale factor None means auto + self._scaleFactor = min(availWidth/self.width,(availHeight-captionHeight)/self.figureHeight) + else: #they provided a factor + self._scaleFactor = self.scaleFactor + if self._scaleFactor<1 and self.shrinkToFit: + self.width = self.width * self._scaleFactor - 0.0001 + self.figureHeight = self.figureHeight * self._scaleFactor + elif self._scaleFactor>1 and self.growToFit: + self.width = self.width*self._scaleFactor - 0.0001 + self.figureHeight = self.figureHeight * self._scaleFactor + + def wrap(self, availWidth, availHeight): + self._scale(availWidth,availHeight) + return Figure.wrap(self, availWidth, availHeight) + + def split(self, availWidth, availHeight): + self._scale(availWidth,availHeight) + return Figure.split(self, availWidth, availHeight) + +class ImageFigure(FlexFigure): + """Image with a caption below it""" + def __init__(self, filename, caption, background=None): + assert os.path.isfile(filename), 'image file %s not found' % filename + from reportlab.lib.utils import ImageReader + w, h = ImageReader(filename).getSize() + self.filename = filename + FlexFigure.__init__(self, w, h, caption, background) + + def drawFigure(self): + self.canv.drawInlineImage(self.filename, + 0, 0,self.width, self.figureHeight) + +class DrawingFigure(FlexFigure): + """Drawing with a caption below it. Clunky, scaling fails.""" + def __init__(self, modulename, classname, caption, baseDir=None, background=None): + module = recursiveImport(modulename, baseDir) + klass = getattr(module, classname) + self.drawing = klass() + FlexFigure.__init__(self, + self.drawing.width, + self.drawing.height, + caption, + background) + self.growToFit = 1 + + def drawFigure(self): + self.canv.scale(self._scaleFactor, self._scaleFactor) + self.drawing.drawOn(self.canv, 0, 0) + + +try: + from rlextra.pageCatcher.pageCatcher import restoreForms, storeForms, storeFormsInMemory, restoreFormsInMemory + _hasPageCatcher = 1 +except ImportError: + _hasPageCatcher = 0 +if _hasPageCatcher: + #################################################################### + # + # PageCatcher plugins + # These let you use our PageCatcher product to add figures + # to other documents easily. + #################################################################### + class PageCatcherCachingMixIn: + "Helper functions to cache pages for figures" + + def getFormName(self, pdfFileName, pageNo): + #naming scheme works within a directory only + dirname, filename = os.path.split(pdfFileName) + root, ext = os.path.splitext(filename) + return '%s_page%d' % (root, pageNo) + + + def needsProcessing(self, pdfFileName, pageNo): + "returns 1 if no forms or form is older" + formName = self.getFormName(pdfFileName, pageNo) + if os.path.exists(formName + '.frm'): + formModTime = os.stat(formName + '.frm')[8] + pdfModTime = os.stat(pdfFileName)[8] + return (pdfModTime > formModTime) + else: + return 1 + + def processPDF(self, pdfFileName, pageNo): + formName = self.getFormName(pdfFileName, pageNo) + storeForms(pdfFileName, formName + '.frm', + prefix= formName + '_', + pagenumbers=[pageNo]) + #print 'stored %s.frm' % formName + return formName + '.frm' + + class cachePageCatcherFigureNonA4(FlexFigure, PageCatcherCachingMixIn): + """PageCatcher page with a caption below it. Size to be supplied.""" + # This should merge with PageFigure into one class that reuses + # form information to determine the page orientation... + def __init__(self, filename, pageNo, caption, width, height, background=None): + self.dirname, self.filename = os.path.split(filename) + if self.dirname == '': + self.dirname = os.curdir + self.pageNo = pageNo + self.formName = self.getFormName(self.filename, self.pageNo) + '_' + str(pageNo) + FlexFigure.__init__(self, width, height, caption, background) + + def drawFigure(self): + self.canv.saveState() + if not self.canv.hasForm(self.formName): + restorePath = self.dirname + os.sep + self.filename + #does the form file exist? if not, generate it. + formFileName = self.getFormName(restorePath, self.pageNo) + '.frm' + if self.needsProcessing(restorePath, self.pageNo): + #print 'preprocessing PDF %s page %s' % (restorePath, self.pageNo) + self.processPDF(restorePath, self.pageNo) + names = restoreForms(formFileName, self.canv) + self.canv.scale(self._scaleFactor, self._scaleFactor) + self.canv.doForm(self.formName) + self.canv.restoreState() + + class cachePageCatcherFigure(cachePageCatcherFigureNonA4): + """PageCatcher page with a caption below it. Presumes A4, Portrait. + This needs our commercial PageCatcher product, or you'll get a blank.""" + def __init__(self, filename, pageNo, caption, width=595, height=842, background=None): + cachePageCatcherFigureNonA4.__init__(self, filename, pageNo, caption, width, height, background=background) + + class PageCatcherFigureNonA4(FlexFigure): + """PageCatcher page with a caption below it. Size to be supplied.""" + # This should merge with PageFigure into one class that reuses + # form information to determine the page orientation... + _cache = {} + def __init__(self, filename, pageNo, caption, width, height, background=None, caching=None): + fn = self.filename = filename + self.pageNo = pageNo + fn = fn.replace(os.sep,'_').replace('/','_').replace('\\','_').replace('-','_').replace(':','_') + self.prefix = fn.replace('.','_')+'_'+str(pageNo)+'_' + self.formName = self.prefix + str(pageNo) + self.caching = caching + FlexFigure.__init__(self, width, height, caption, background) + + def drawFigure(self): + if not self.canv.hasForm(self.formName): + if self.filename in self._cache: + f,data = self._cache[self.filename] + else: + f = open(self.filename,'rb') + pdf = f.read() + f.close() + f, data = storeFormsInMemory(pdf, pagenumbers=[self.pageNo], prefix=self.prefix) + if self.caching=='memory': + self._cache[self.filename] = f, data + f = restoreFormsInMemory(data, self.canv) + self.canv.saveState() + self.canv.scale(self._scaleFactor, self._scaleFactor) + self.canv.doForm(self.formName) + self.canv.restoreState() + + class PageCatcherFigure(PageCatcherFigureNonA4): + """PageCatcher page with a caption below it. Presumes A4, Portrait. + This needs our commercial PageCatcher product, or you'll get a blank.""" + def __init__(self, filename, pageNo, caption, width=595, height=842, background=None, caching=None): + PageCatcherFigureNonA4.__init__(self, filename, pageNo, caption, width, height, background=background, caching=caching) + +def demo1(canvas): + frame = Frame( + 2*inch, # x + 4*inch, # y at bottom + 4*inch, # width + 5*inch, # height + showBoundary = 1 # helps us see what's going on + ) + bodyStyle = ParagraphStyle('Body', fontName='Times-Roman', fontSize=24, leading=28, spaceBefore=6) + para1 = Paragraph('Spam spam spam spam. ' * 5, bodyStyle) + para2 = Paragraph('Eggs eggs eggs. ' * 5, bodyStyle) + mydata = [para1, para2] + + #this does the packing and drawing. The frame will consume + #items from the front of the list as it prints them + frame.addFromList(mydata,canvas) + +def test1(): + c = Canvas('figures.pdf') + f = Frame(inch, inch, 6*inch, 9*inch, showBoundary=1) + v = PlatPropFigure1() + v.captionTextColor = toColor('blue') + v.captionBackColor = toColor('lightyellow') + f.addFromList([v],c) + c.save() + +if __name__ == '__main__': + test1() diff --git a/bin/reportlab/platypus/flowables.py b/bin/reportlab/platypus/flowables.py new file mode 100644 index 00000000000..f22dd9ee696 --- /dev/null +++ b/bin/reportlab/platypus/flowables.py @@ -0,0 +1,1051 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/flowables.py +__version__=''' $Id: flowables.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' +__doc__=""" +A flowable is a "floating element" in a document whose exact position is determined by the +other elements that precede it, such as a paragraph, a diagram interspersed between paragraphs, +a section header, etcetera. Examples of non-flowables include page numbering annotations, +headers, footers, fixed diagrams or logos, among others. + +Flowables are defined here as objects which know how to determine their size and which +can draw themselves onto a page with respect to a relative "origin" position determined +at a higher level. The object's draw() method should assume that (0,0) corresponds to the +bottom left corner of the enclosing rectangle that will contain the object. The attributes +vAlign and hAlign may be used by 'packers' as hints as to how the object should be placed. + +Some Flowables also know how to "split themselves". For example a +long paragraph might split itself between one page and the next. + +Packers should set the canv attribute during wrap, split & draw operations to allow +the flowable to work out sizes etc in the proper context. + +The "text" of a document usually consists mainly of a sequence of flowables which +flow into a document from top to bottom (with column and page breaks controlled by +higher level components). +""" +import os +import string +from copy import deepcopy +from types import ListType, TupleType, StringType + +from reportlab.lib.colors import red, gray, lightgrey +from reportlab.lib.utils import fp_str +from reportlab.pdfbase import pdfutils + +from reportlab.rl_config import _FUZZ, overlapAttachedSpace +__all__=('TraceInfo','Flowable','XBox','Preformatted','Image','Spacer','PageBreak','SlowPageBreak', + 'CondPageBreak','KeepTogether','Macro','CallerMacro','ParagraphAndImage', + 'FailOnWrap','HRFlowable','PTOContainer','KeepInFrame','UseUpSpace') + + +class TraceInfo: + "Holder for info about where an object originated" + def __init__(self): + self.srcFile = '(unknown)' + self.startLineNo = -1 + self.startLinePos = -1 + self.endLineNo = -1 + self.endLinePos = -1 + +############################################################# +# Flowable Objects - a base class and a few examples. +# One is just a box to get some metrics. We also have +# a paragraph, an image and a special 'page break' +# object which fills the space. +############################################################# +class Flowable: + """Abstract base class for things to be drawn. Key concepts: + 1. It knows its size + 2. It draws in its own coordinate system (this requires the + base API to provide a translate() function. + """ + _fixedWidth = 0 #assume wrap results depend on arguments? + _fixedHeight = 0 + + def __init__(self): + self.width = 0 + self.height = 0 + self.wrapped = 0 + + #these are hints to packers/frames as to how the floable should be positioned + self.hAlign = 'LEFT' #CENTER/CENTRE or RIGHT + self.vAlign = 'BOTTOM' #MIDDLE or TOP + + #optional holder for trace info + self._traceInfo = None + self._showBoundary = None + + #many flowables handle text and must be processed in the + #absence of a canvas. tagging them with their encoding + #helps us to get conversions right. Use Python codec names. + self.encoding = None + + + def _drawOn(self,canv): + '''ensure canv is set on and then draw''' + self.canv = canv + self.draw()#this is the bit you overload + del self.canv + + def drawOn(self, canvas, x, y, _sW=0): + "Tell it to draw itself on the canvas. Do not override" + if _sW and hasattr(self,'hAlign'): + a = self.hAlign + if a in ['CENTER','CENTRE']: + x = x + 0.5*_sW + elif a == 'RIGHT': + x = x + _sW + elif a != 'LEFT': + raise ValueError, "Bad hAlign value "+str(a) + canvas.saveState() + canvas.translate(x, y) + self._drawOn(canvas) + if hasattr(self, '_showBoundary') and self._showBoundary: + #diagnostic tool support + canvas.setStrokeColor(gray) + canvas.rect(0,0,self.width, self.height) + canvas.restoreState() + + def wrapOn(self, canv, aW, aH): + '''intended for use by packers allows setting the canvas on + during the actual wrap''' + self.canv = canv + w, h = self.wrap(aW,aH) + del self.canv + return w, h + + def wrap(self, availWidth, availHeight): + """This will be called by the enclosing frame before objects + are asked their size, drawn or whatever. It returns the + size actually used.""" + return (self.width, self.height) + + def minWidth(self): + """This should return the minimum required width""" + return getattr(self,'_minWidth',self.width) + + def splitOn(self, canv, aW, aH): + '''intended for use by packers allows setting the canvas on + during the actual split''' + self.canv = canv + S = self.split(aW,aH) + del self.canv + return S + + def split(self, availWidth, availheight): + """This will be called by more sophisticated frames when + wrap fails. Stupid flowables should return []. Clever flowables + should split themselves and return a list of flowables""" + return [] + + def getKeepWithNext(self): + """returns boolean determining whether the next flowable should stay with this one""" + if hasattr(self,'keepWithNext'): return self.keepWithNext + elif hasattr(self,'style') and hasattr(self.style,'keepWithNext'): return self.style.keepWithNext + else: return 0 + + def getSpaceAfter(self): + """returns how much space should follow this item if another item follows on the same page.""" + if hasattr(self,'spaceAfter'): return self.spaceAfter + elif hasattr(self,'style') and hasattr(self.style,'spaceAfter'): return self.style.spaceAfter + else: return 0 + + def getSpaceBefore(self): + """returns how much space should precede this item if another item precedess on the same page.""" + if hasattr(self,'spaceBefore'): return self.spaceBefore + elif hasattr(self,'style') and hasattr(self.style,'spaceBefore'): return self.style.spaceBefore + else: return 0 + + def isIndexing(self): + """Hook for IndexingFlowables - things which have cross references""" + return 0 + + def identity(self, maxLen=None): + ''' + This method should attempt to return a string that can be used to identify + a particular flowable uniquely. The result can then be used for debugging + and or error printouts + ''' + if hasattr(self, 'getPlainText'): + r = self.getPlainText(identify=1) + elif hasattr(self, 'text'): + r = str(self.text) + else: + r = '...' + if r and maxLen: + r = r[:maxLen] + return "<%s at %s%s>%s" % (self.__class__.__name__, hex(id(self)), self._frameName(), r) + + def _frameName(self): + f = getattr(self,'_frame',None) + if f and f.id: return ' frame=%s' % f.id + return '' + +class XBox(Flowable): + """Example flowable - a box with an x through it and a caption. + This has a known size, so does not need to respond to wrap().""" + def __init__(self, width, height, text = 'A Box'): + Flowable.__init__(self) + self.width = width + self.height = height + self.text = text + + def __repr__(self): + return "XBox(w=%s, h=%s, t=%s)" % (self.width, self.height, self.text) + + def draw(self): + self.canv.rect(0, 0, self.width, self.height) + self.canv.line(0, 0, self.width, self.height) + self.canv.line(0, self.height, self.width, 0) + + #centre the text + self.canv.setFont('Times-Roman',12) + self.canv.drawCentredString(0.5*self.width, 0.5*self.height, self.text) + +def _trimEmptyLines(lines): + #don't want the first or last to be empty + while len(lines) and string.strip(lines[0]) == '': + lines = lines[1:] + while len(lines) and string.strip(lines[-1]) == '': + lines = lines[:-1] + return lines + +def _dedenter(text,dedent=0): + ''' + tidy up text - carefully, it is probably code. If people want to + indent code within a source script, you can supply an arg to dedent + and it will chop off that many character, otherwise it leaves + left edge intact. + ''' + lines = string.split(text, '\n') + if dedent>0: + templines = _trimEmptyLines(lines) + lines = [] + for line in templines: + line = string.rstrip(line[dedent:]) + lines.append(line) + else: + lines = _trimEmptyLines(lines) + + return lines + +class Preformatted(Flowable): + """This is like the HTML
 tag.
+    It attempts to display text exactly as you typed it in a fixed width "typewriter" font.
+    The line breaks are exactly where you put
+    them, and it will not be wrapped."""
+    def __init__(self, text, style, bulletText = None, dedent=0):
+        """text is the text to display. If dedent is set then common leading space
+        will be chopped off the front (for example if the entire text is indented
+        6 spaces or more then each line will have 6 spaces removed from the front).
+        """
+        self.style = style
+        self.bulletText = bulletText
+        self.lines = _dedenter(text,dedent)
+
+    def __repr__(self):
+        bT = self.bulletText
+        H = "Preformatted("
+        if bT is not None:
+            H = "Preformatted(bulletText=%s," % repr(bT)
+        return "%s'''\\ \n%s''')" % (H, string.join(self.lines,'\n'))
+
+    def wrap(self, availWidth, availHeight):
+        self.width = availWidth
+        self.height = self.style.leading*len(self.lines)
+        return (self.width, self.height)
+
+    def split(self, availWidth, availHeight):
+        #returns two Preformatted objects
+
+        #not sure why they can be called with a negative height
+        if availHeight < self.style.leading:
+            return []
+
+        linesThatFit = int(availHeight * 1.0 / self.style.leading)
+
+        text1 = string.join(self.lines[0:linesThatFit], '\n')
+        text2 = string.join(self.lines[linesThatFit:], '\n')
+        style = self.style
+        if style.firstLineIndent != 0:
+            style = deepcopy(style)
+            style.firstLineIndent = 0
+        return [Preformatted(text1, self.style), Preformatted(text2, style)]
+
+
+    def draw(self):
+        #call another method for historical reasons.  Besides, I
+        #suspect I will be playing with alternate drawing routines
+        #so not doing it here makes it easier to switch.
+
+        cur_x = self.style.leftIndent
+        cur_y = self.height - self.style.fontSize
+        self.canv.addLiteral('%PreformattedPara')
+        if self.style.textColor:
+            self.canv.setFillColor(self.style.textColor)
+        tx = self.canv.beginText(cur_x, cur_y)
+        #set up the font etc.
+        tx.setFont( self.style.fontName,
+                    self.style.fontSize,
+                    self.style.leading)
+
+        for text in self.lines:
+            tx.textLine(text)
+        self.canv.drawText(tx)
+
+class Image(Flowable):
+    """an image (digital picture).  Formats supported by PIL/Java 1.4 (the Python/Java Imaging Library
+       are supported.  At the present time images as flowables are always centered horozontally
+       in the frame. We allow for two kinds of lazyness to allow for many images in a document
+       which could lead to file handle starvation.
+       lazy=1 don't open image until required.
+       lazy=2 open image when required then shut it.
+    """
+    _fixedWidth = 1
+    _fixedHeight = 1
+    def __init__(self, filename, width=None, height=None, kind='direct', mask="auto", lazy=1):
+        """If size to draw at not specified, get it from the image."""
+        self.hAlign = 'CENTER'
+        self._mask = mask
+        # if it is a JPEG, will be inlined within the file -
+        # but we still need to know its size now
+        fp = hasattr(filename,'read')
+        if fp:
+            self._file = filename
+            self.filename = `filename`
+        else:
+            self._file = self.filename = filename
+        if not fp and os.path.splitext(filename)[1] in ['.jpg', '.JPG', '.jpeg', '.JPEG']:
+            from reportlab.lib.utils import open_for_read
+            f = open_for_read(filename, 'b')
+            info = pdfutils.readJPEGInfo(f)
+            f.close()
+            self.imageWidth = info[0]
+            self.imageHeight = info[1]
+            self._img = None
+            self._setup(width,height,kind,0)
+        elif fp:
+            self._setup(width,height,kind,0)
+        else:
+            self._setup(width,height,kind,lazy)
+
+    def _setup(self,width,height,kind,lazy):
+        self._lazy = lazy
+        self._width = width
+        self._height = height
+        self._kind = kind
+        if lazy<=0: self._setup_inner()
+
+    def _setup_inner(self):
+        width = self._width
+        height = self._height
+        kind = self._kind
+        img = self._img
+        if img: self.imageWidth, self.imageHeight = img.getSize()
+        if self._lazy>=2: del self._img
+        if kind in ['direct','absolute']:
+            self.drawWidth = width or self.imageWidth
+            self.drawHeight = height or self.imageHeight
+        elif kind in ['percentage','%']:
+            self.drawWidth = self.imageWidth*width*0.01
+            self.drawHeight = self.imageHeight*height*0.01
+        elif kind in ['bound','proportional']:
+            factor = min(float(width)/self.imageWidth,float(height)/self.imageHeight)
+            self.drawWidth = self.imageWidth*factor
+            self.drawHeight = self.imageHeight*factor
+
+    def __getattr__(self,a):
+        if a=='_img':
+            from reportlab.lib.utils import ImageReader  #this may raise an error
+            self._img = ImageReader(self._file)
+            del self._file
+            return self._img
+        elif a in ('drawWidth','drawHeight','imageWidth','imageHeight'):
+            self._setup_inner()
+            return self.__dict__[a]
+        raise AttributeError(a)
+
+    def wrap(self, availWidth, availHeight):
+        #the caller may decide it does not fit.
+        return (self.drawWidth, self.drawHeight)
+
+    def draw(self):
+        lazy = self._lazy
+        if lazy>=2: self._lazy = 1
+        self.canv.drawImage(    self._img or self.filename,
+                                0,
+                                0,
+                                self.drawWidth,
+                                self.drawHeight,
+                                mask=self._mask,
+                                )
+        if lazy>=2:
+            self._img = None
+            self._lazy = lazy
+
+    def identity(self,maxLen=None):
+        r = Flowable.identity(self,maxLen)
+        if r[-4:]=='>...' and type(self.filename) is StringType:
+            r = "%s filename=%s>" % (r[:-4],self.filename)
+        return r
+
+class Spacer(Flowable):
+    """A spacer just takes up space and doesn't draw anything - it guarantees
+       a gap between objects."""
+    _fixedWidth = 1
+    _fixedHeight = 1
+    def __init__(self, width, height):
+        self.width = width
+        self.height = height
+
+    def __repr__(self):
+        return "%s(%s, %s)" % (self.__class__.__name__,self.width, self.height)
+
+    def draw(self):
+        pass
+
+class UseUpSpace(Flowable):
+    def __init__(self):
+        pass
+
+    def __repr__(self):
+        return "%s()" % self.__class__.__name__
+
+    def wrap(self, availWidth, availHeight):
+        self.width = availWidth
+        self.height = availHeight
+        return (availWidth,availHeight-1e-8)  #step back a point
+
+    def draw(self):
+        pass
+
+class PageBreak(UseUpSpace):
+    """Move on to the next page in the document.
+       This works by consuming all remaining space in the frame!"""
+
+class SlowPageBreak(PageBreak):
+    pass
+
+class CondPageBreak(Spacer):
+    """Throw a page if not enough vertical space"""
+    def __init__(self, height):
+        self.height = height
+
+    def __repr__(self):
+        return "CondPageBreak(%s)" %(self.height,)
+
+    def wrap(self, availWidth, availHeight):
+        if availHeightaH and (not self._maxHeight or aH>self._maxHeight)
+        C1 = self._H0>aH
+        if C0 or C1:
+            if C0:
+                from doctemplate import FrameBreak
+                A = FrameBreak
+            else:
+                from doctemplate import NullActionFlowable
+                A = NullActionFlowable
+            S.insert(0,A())
+        return S
+
+    def identity(self, maxLen=None):
+        msg = " containing :%s" % (hex(id(self)),self._frameName(),"\n".join([f.identity() for f in self._content]))
+        if maxLen:
+            return msg[0:maxLen]
+        else:
+            return msg
+
+class Macro(Flowable):
+    """This is not actually drawn (i.e. it has zero height)
+    but is executed when it would fit in the frame.  Allows direct
+    access to the canvas through the object 'canvas'"""
+    def __init__(self, command):
+        self.command = command
+    def __repr__(self):
+        return "Macro(%s)" % repr(self.command)
+    def wrap(self, availWidth, availHeight):
+        return (0,0)
+    def draw(self):
+        exec self.command in globals(), {'canvas':self.canv}
+
+class CallerMacro(Flowable):
+    '''
+    like Macro, but with callable command(s)
+    drawCallable(self)
+    wrapCallable(self,aW,aH)
+    '''
+    def __init__(self, drawCallable=None, wrapCallable=None):
+        _ = lambda *args: None
+        self._drawCallable = drawCallable or _
+        self._wrapCallable = wrapCallable or _
+    def __repr__(self):
+        return "CallerMacro(%s)" % repr(self.command)
+    def wrap(self, aW, aH):
+        self._wrapCallable(self,aW,aH)
+        return (0,0)
+    def draw(self):
+        self._drawCallable(self)
+
+class ParagraphAndImage(Flowable):
+    '''combine a Paragraph and an Image'''
+    def __init__(self,P,I,xpad=3,ypad=3,side='right'):
+        self.P = P
+        self.I = I
+        self.xpad = xpad
+        self.ypad = ypad
+        self._side = side
+
+    def getSpaceBefore(self):
+        return max(self.P.getSpaceBefore(),self.I.getSpaceBefore())
+
+    def getSpaceAfter(self):
+        return max(self.P.getSpaceAfter(),self.I.getSpaceAfter())
+
+    def wrap(self,availWidth,availHeight):
+        wI, hI = self.I.wrap(availWidth,availHeight)
+        self.wI = wI
+        self.hI = hI
+        # work out widths array for breaking
+        self.width = availWidth
+        P = self.P
+        style = P.style
+        xpad = self.xpad
+        ypad = self.ypad
+        leading = style.leading
+        leftIndent = style.leftIndent
+        later_widths = availWidth - leftIndent - style.rightIndent
+        intermediate_widths = later_widths - xpad - wI
+        first_line_width = intermediate_widths - style.firstLineIndent
+        P.width = 0
+        nIW = int((hI+ypad)/leading)
+        P.blPara = P.breakLines([first_line_width] + nIW*[intermediate_widths]+[later_widths])
+        if self._side=='left':
+            self._offsets = [wI+xpad]*(1+nIW)+[0]
+        P.height = len(P.blPara.lines)*leading
+        self.height = max(hI,P.height)
+        return (self.width, self.height)
+
+    def split(self,availWidth, availHeight):
+        P, wI, hI, ypad = self.P, self.wI, self.hI, self.ypad
+        if hI+ypad>availHeight or len(P.frags)<=0: return []
+        S = P.split(availWidth,availHeight)
+        if not S: return S
+        P = self.P = S[0]
+        del S[0]
+        style = P.style
+        P.height = len(self.P.blPara.lines)*style.leading
+        self.height = max(hI,P.height)
+        return [self]+S
+
+    def draw(self):
+        canv = self.canv
+        if self._side=='left':
+            self.I.drawOn(canv,0,self.height-self.hI)
+            self.P._offsets = self._offsets
+            try:
+                self.P.drawOn(canv,0,0)
+            finally:
+                del self.P._offsets
+        else:
+            self.I.drawOn(canv,self.width-self.wI-self.xpad,self.height-self.hI)
+            self.P.drawOn(canv,0,0)
+
+class FailOnWrap(Flowable):
+    def wrap(self, availWidth, availHeight):
+        raise ValueError("FailOnWrap flowable wrapped and failing as ordered!")
+
+    def draw(self):
+        pass
+
+class FailOnDraw(Flowable):
+    def wrap(self, availWidth, availHeight):
+        return (0,0)
+
+    def draw(self):
+        raise ValueError("FailOnDraw flowable drawn, and failing as ordered!")
+
+class HRFlowable(Flowable):
+    '''Like the hr tag'''
+    def __init__(self,
+            width="80%",
+            thickness=1,
+            lineCap='round',
+            color=lightgrey,
+            spaceBefore=1, spaceAfter=1,
+            hAlign='CENTER', vAlign='BOTTOM',
+            dash=None):
+        Flowable.__init__(self)
+        self.width = width
+        self.lineWidth = thickness
+        self.lineCap=lineCap
+        self.spaceBefore = spaceBefore
+        self.spaceAfter = spaceAfter
+        self.color = color
+        self.hAlign = hAlign
+        self.vAlign = vAlign
+        self.dash = dash
+
+    def __repr__(self):
+        return "HRFlowable(width=%s, height=%s)" % (self.width, self.height)
+
+    def wrap(self, availWidth, availHeight):
+        w = self.width
+        if type(w) is type(''):
+            w = w.strip()
+            if w.endswith('%'): w = availWidth*float(w[:-1])*0.01
+            else: w = float(w)
+        w = min(w,availWidth)
+        self._width = w
+        return w, self.lineWidth
+
+    def draw(self):
+        canv = self.canv
+        canv.saveState()
+        canv.setLineWidth(self.lineWidth)
+        canv.setLineCap({'butt':0,'round':1, 'square': 2}[self.lineCap.lower()])
+        canv.setStrokeColor(self.color)
+        if self.dash: canv.setDash(self.dash)
+        canv.line(0, 0, self._width, self.height)
+        canv.restoreState()
+
+class _PTOInfo:
+    def __init__(self,trailer,header):
+        self.trailer = _flowableSublist(trailer)
+        self.header = _flowableSublist(header)
+
+class _Container(_ContainerSpace):  #Abstract some common container like behaviour
+    def drawOn(self, canv, x, y, _sW=0, scale=1.0, content=None, aW=None):
+        '''we simulate being added to a frame'''
+        pS = 0
+        if aW is None: aW = self.width
+        aW = scale*(aW+_sW)
+        if content is None:
+            content = self._content
+        y += self.height*scale
+        for c in content:
+            w, h = c.wrapOn(canv,aW,0xfffffff)
+            if w<_FUZZ or h<_FUZZ: continue
+            if c is not content[0]: h += max(c.getSpaceBefore()-pS,0)
+            y -= h
+            c.drawOn(canv,x,y,_sW=aW-w)
+            if c is not content[-1]:
+                pS = c.getSpaceAfter()
+                y -= pS
+
+class PTOContainer(_Container,Flowable):
+    '''PTOContainer(contentList,trailerList,headerList)
+    
+    A container for flowables decorated with trailer & header lists.
+    If the split operation would be called then the trailer and header
+    lists are injected before and after the split. This allows specialist
+    "please turn over" and "continued from previous" like behaviours.''' 
+    def __init__(self,content,trailer=None,header=None):
+        I = _PTOInfo(trailer,header)
+        self._content = C = []
+        for _ in _flowableSublist(content):
+            if isinstance(_,PTOContainer):
+                C.extend(_._content)
+            else:
+                C.append(_)
+                if not hasattr(_,'_ptoinfo'): _._ptoinfo = I
+
+    def wrap(self,availWidth,availHeight):
+        self.width, self.height = _listWrapOn(self._content,availWidth,self.canv)
+        return self.width,self.height
+
+    def split(self, availWidth, availHeight):
+        if availHeight<0: return []
+        canv = self.canv
+        C = self._content
+        x = i = H = pS = hx = 0
+        n = len(C)
+        I2W = {}
+        for x in xrange(n):
+            c = C[x]
+            I = c._ptoinfo
+            if I not in I2W.keys():
+                T = I.trailer
+                Hdr = I.header
+                tW, tH = _listWrapOn(T, availWidth, self.canv)
+                tSB = T[0].getSpaceBefore()
+                I2W[I] = T,tW,tH,tSB
+            else:
+                T,tW,tH,tSB = I2W[I]
+            _, h = c.wrapOn(canv,availWidth,0xfffffff)
+            if x:
+                hx = max(c.getSpaceBefore()-pS,0)
+                h += hx
+            pS = c.getSpaceAfter()
+            H += h+pS
+            tHS = tH+max(tSB,pS)
+            if H+tHS>=availHeight-_FUZZ: break
+            i += 1
+
+        #first retract last thing we tried
+        H -= (h+pS)
+
+        #attempt a sub split on the last one we have
+        aH = (availHeight-H-tHS-hx)*0.99999
+        if aH>=0.05*availHeight:
+            SS = c.splitOn(canv,availWidth,aH)
+        else:
+            SS = []
+        F = [UseUpSpace()]
+
+        if len(SS)>1:
+            R1 = C[:i] + SS[:1] + T + F
+            R2 = Hdr + SS[1:]+C[i+1:]
+        elif not i:
+            return []
+        else:
+            R1 = C[:i]+T+F
+            R2 = Hdr + C[i:]
+        T =  R1 + [PTOContainer(R2,deepcopy(I.trailer),deepcopy(I.header))]
+        return T
+
+#utility functions used by KeepInFrame
+def _hmodel(s0,s1,h0,h1):
+    # calculate the parameters in the model
+    # h = a/s**2 + b/s
+    a11 = 1./s0**2
+    a12 = 1./s0
+    a21 = 1./s1**2
+    a22 = 1./s1
+    det = a11*a22-a12*a21
+    b11 = a22/det
+    b12 = -a12/det
+    b21 = -a21/det
+    b22 = a11/det
+    a = b11*h0+b12*h1
+    b = b21*h0+b22*h1
+    return a,b
+
+def _qsolve(h,(a,b)):
+    '''solve the model v = a/s**2 + b/s for an s which gives us v==h'''
+    if abs(a)<=_FUZZ:
+        return b/h
+    t = 0.5*b/a
+    from math import sqrt
+    f = -h/a
+    r = t*t-f
+    if r<0: return None
+    r = sqrt(r)
+    if t>=0:
+        s1 = -t - r 
+    else:
+        s1 = -t + r
+    s2 = f/s1
+    return max(1./s1, 1./s2)
+
+class KeepInFrame(_Container,Flowable):
+    def __init__(self, maxWidth, maxHeight, content=[], mergeSpace=1, mode='shrink', name=''):
+        '''mode describes the action to take when overflowing
+            error       raise an error in the normal way
+            continue    ignore ie just draw it and report maxWidth, maxHeight
+            shrink      shrinkToFit
+            truncate    fit as much as possible
+        '''
+        self.name = name
+        self.maxWidth = maxWidth
+        self.maxHeight = maxHeight
+        self.mode = mode
+        assert mode in ('error','overflow','shrink','truncate'), '%s invalid mode value %s' % (self.identity(),mode)
+        assert maxHeight>=0,  '%s invalid maxHeight value %s' % (self.identity(),maxHeight)
+        if mergeSpace is None: mergeSpace = overlapAttachedSpace
+        self.mergespace = mergeSpace
+        self._content = content
+
+    def _getAvailableWidth(self):
+        return self.maxWidth - self._leftExtraIndent - self._rightExtraIndent
+
+    def identity(self, maxLen=None):
+        return "<%s at %s%s%s> size=%sx%s" % (self.__class__.__name__, hex(id(self)), self._frameName(),
+                getattr(self,'name','') and (' name="%s"'% getattr(self,'name','')) or '',
+                getattr(self,'maxWidth','') and (' maxWidth=%s'%fp_str(getattr(self,'maxWidth',0))) or '',
+                getattr(self,'maxHeight','')and (' maxHeight=%s' % fp_str(getattr(self,'maxHeight')))or '')
+
+    def wrap(self,availWidth,availHeight):
+        from doctemplate import LayoutError
+        mode = self.mode
+        maxWidth = float(self.maxWidth or availWidth)
+        maxHeight = float(self.maxHeight or availHeight)
+        W, H = _listWrapOn(self._content,availWidth,self.canv)
+        if (mode=='error' and (W>availWidth+_FUZZ or H>availHeight+_FUZZ)):
+            ident = 'content %sx%s too large for %s' % (W,H,self.identity(30))
+            #leave to keep apart from the raise
+            raise LayoutError(ident)
+        elif W<=availWidth+_FUZZ and H<=availHeight+_FUZZ:
+            self.width = W-_FUZZ      #we take what we get
+            self.height = H-_FUZZ
+        elif (maxWidth>=availWidth+_FUZZ or maxHeight>=availHeight+_FUZZ):
+            ident = 'Specified size too large for available space %sx%s in %s' % (availWidth,availHeight,self.identity(30))
+            #leave to keep apart from the raise
+            raise LayoutError(ident)
+        elif mode in ('overflow','truncate'):   #we lie
+            self.width = min(maxWidth,W)-_FUZZ
+            self.height = min(maxHeight,H)-_FUZZ
+        else:
+            def func(x):
+                W, H = _listWrapOn(self._content,x*availWidth,self.canv)
+                W /= x
+                H /= x
+                return W, H
+            W0 = W
+            H0 = H
+            s0 = 1
+            if W>maxWidth+_FUZZ:
+                #squeeze out the excess width and or Height
+                s1 = W/maxWidth
+                W, H = func(s1)
+                if H<=maxHeight+_FUZZ:
+                    self.width = W-_FUZZ
+                    self.height = H-_FUZZ
+                    self._scale = s1
+                    return W,H
+                s0 = s1
+                H0 = H
+                W0 = W
+            s1 = H/maxHeight
+            W, H = func(s1)
+            self.width = W-_FUZZ
+            self.height = H-_FUZZ
+            self._scale = s1
+            if H=maxHeight+_FUZZ:
+                #the standard case W should be OK, H is short we want
+                #to find the smallest s with H<=maxHeight
+                H1 = H
+                for f in 0, 0.01, 0.05, 0.10, 0.15:
+                    #apply the quadratic model
+                    s = _qsolve(maxHeight*(1-f),_hmodel(s0,s1,H0,H1))
+                    W, H = func(s)
+                    if H<=maxHeight+_FUZZ and W<=maxWidth+_FUZZ:
+                        self.width = W-_FUZZ
+                        self.height = H-_FUZZ
+                        self._scale = s
+                        break
+
+        return self.width, self.height
+
+    def drawOn(self, canv, x, y, _sW=0):
+        scale = getattr(self,'_scale',1.0)
+        truncate = self.mode=='truncate'
+        ss = scale!=1.0 or truncate
+        if ss:
+            canv.saveState()
+            if truncate:
+                p = canv.beginPath()
+                p.rect(x, y, self.width,self.height)
+                canv.clipPath(p,stroke=0)
+            else:
+                canv.translate(x,y)
+                x=y=0
+                canv.scale(1.0/scale, 1.0/scale)
+        _Container.drawOn(self, canv, x, y, _sW=_sW, scale=scale)
+        if ss: canv.restoreState()
+
+class ImageAndFlowables(_Container,Flowable):
+    '''combine a list of flowables and an Image'''
+    def __init__(self,I,F,imageLeftPadding=0,imageRightPadding=3,imageTopPadding=0,imageBottomPadding=3,
+                    imageSide='right'):
+        self._content = _flowableSublist(F)
+        self._I = I
+        self._irpad = imageRightPadding
+        self._ilpad = imageLeftPadding
+        self._ibpad = imageBottomPadding
+        self._itpad = imageTopPadding
+        self._side = imageSide
+
+    def getSpaceAfter(self):
+        if hasattr(self,'_C1'):
+            C = self._C1
+        elif hasattr(self,'_C0'):
+            C = self._C0
+        else:
+            C = self._content
+        return _Container.getSpaceAfter(self,C)
+
+    def getSpaceBefore(self):
+        return max(self._I.getSpaceBefore(),_Container.getSpaceBefore(self))
+
+    def _reset(self):
+        for a in ('_wrapArgs','_C0','_C1'):
+            try:
+                delattr(self,a)
+            except:
+                pass
+
+    def wrap(self,availWidth,availHeight):
+        canv = self.canv
+        if hasattr(self,'_wrapArgs'):
+            if self._wrapArgs==(availWidth,availHeight):
+                return self.width,self.height
+            self._reset()
+        self._wrapArgs = availWidth, availHeight
+        wI, hI = self._I.wrap(availWidth,availHeight)
+        self._wI = wI
+        self._hI = hI
+        ilpad = self._ilpad
+        irpad = self._irpad
+        ibpad = self._ibpad
+        itpad = self._itpad
+        self._iW = availWidth - irpad - wI - ilpad
+        aH = itpad + hI + ibpad
+        W,H0,self._C0,self._C1 = self._findSplit(canv,self._iW,aH)
+        self.width = availWidth
+        aH = self._aH = max(aH,H0)
+        if not self._C1:
+            self.height = aH
+        else:
+            W1,H1 = _listWrapOn(self._C1,availWidth,canv)
+            self.height = aH+H1
+        return self.width, self.height
+
+    def split(self,availWidth, availHeight):
+        if hasattr(self,'_wrapArgs'):
+            if self._wrapArgs!=(availWidth,availHeight):
+                self._reset()
+        W,H=self.wrap(availWidth,availHeight)
+        if self._aH>availHeight: return []
+        C1 = self._C1
+        if C1:
+            c0 = C1[0]
+            S = c0.split(availWidth,availHeight-self._aH)
+            if not S:
+                self._C1 = []
+                self.height = self._aH
+            else:
+                self._C1 = [S[0]]
+                self.height = self._aH + S[0].height
+                C1 = S[1:]+C1[1:]
+        else:
+            self._C1 = []
+            self.height = self._aH
+        return [self]+C1
+
+    def drawOn(self, canv, x, y, _sW=0):
+        if self._side=='left':
+            Ix = x + self._ilpad
+            Fx = Ix+ self._irpad + self._wI
+        else:
+            Ix = x + self.width-self._wI-self._irpad - self._ilpad
+            Fx = x
+        self._I.drawOn(canv,Ix,y+self.height-self._itpad-self._hI)
+        _Container.drawOn(self, canv, Fx, y, content=self._C0, aW=self._iW)
+        if self._C1:
+            _Container.drawOn(self, canv, x, y-self._aH,content=self._C1)
+
+    def _findSplit(self,canv,availWidth,availHeight,mergeSpace=1,obj=None):
+        '''return max width, required height for a list of flowables F'''
+        W = 0
+        H = 0
+        pS = sB = 0
+        atTop = 1
+        F = self._content
+        for i,f in enumerate(F):
+            w,h = f.wrapOn(canv,availWidth,0xfffffff)
+            if w<=_FUZZ or h<=_FUZZ: continue
+            W = max(W,w)
+            if not atTop:
+                s = f.getSpaceBefore()
+                if mergeSpace: s = max(s-pS,0)
+                H += s
+            else:
+                if obj is not None: obj._spaceBefore = f.getSpaceBefore()
+                atTop = 0
+            if H>=availHeight:
+                return W, availHeight, F[:i],F[i:]
+            H += h
+            if H>availHeight:
+                from paragraph import Paragraph
+                aH = availHeight-(H-h)
+                if isinstance(f,(Paragraph,Preformatted)):
+                    leading = f.style.leading
+                    nH = leading*int(aH/float(leading))+_FUZZ
+                    if nH0:
+            flowable._frame = self
+            flowable.canv = canv #so they can use stringWidth etc
+            w, h = flowable.wrap(aW, h)
+            del flowable.canv, flowable._frame
+        else:
+            return 0
+
+        h += s
+        y -= h
+
+        if y < p-_FUZZ:
+            if not rl_config.allowTableBoundsErrors and ((h>self._aH or w>aW) and not trySplit):
+                raise "LayoutError", "Flowable %s (%sx%s points) too large for frame (%sx%s points)." % (
+                    flowable.__class__, w,h, aW,self._aH)
+            return 0
+        else:
+            #now we can draw it, and update the current point.
+            flowable._frame = self
+            flowable.drawOn(canv, self._x + self._leftExtraIndent, y, _sW=aW-w)
+            if self._debug: logger.debug('drew %s' % flowable.identity())
+            del flowable._frame
+            s = flowable.getSpaceAfter()
+            y -= s
+            if self._oASpace: self._prevASpace = s
+            if y!=self._y: self._atTop = 0
+            self._y = y
+            return 1
+
+    add = _add
+
+    def split(self,flowable,canv):
+        '''Ask the flowable to split using up the available space.'''
+        y = self._y
+        p = self._y1p
+        s = 0
+        if not self._atTop: s = flowable.getSpaceBefore()
+        flowable.canv = canv    #some flowables might need this
+        r = flowable.split(self._aW, y-p-s)
+        del flowable.canv
+        return r
+
+    def drawBoundary(self,canv):
+        "draw the frame boundary as a rectangle (primarily for debugging)."
+        from reportlab.lib.colors import Color, CMYKColor, toColor
+        sb = self.showBoundary
+        isColor = type(sb) in (type(''),type(()),type([])) or isinstance(sb,Color)
+        if isColor:
+            sb = toColor(sb,self)
+            if sb is self: isColor = 0
+            else:
+                canv.saveState()
+                canv.setStrokeColor(sb)
+        canv.rect(
+                self._x1,
+                self._y1,
+                self._x2 - self._x1,
+                self._y2 - self._y1
+                )
+        if isColor: canv.restoreState()
+
+    def addFromList(self, drawlist, canv):
+        """Consumes objects from the front of the list until the
+        frame is full.  If it cannot fit one object, raises
+        an exception."""
+
+        if self._debug: logger.debug("enter Frame.addFromlist() for frame %s" % self.id)
+        if self.showBoundary:
+            self.drawBoundary(canv)
+
+        while len(drawlist) > 0:
+            head = drawlist[0]
+            if self.add(head,canv,trySplit=0):
+                del drawlist[0]
+            else:
+                #leave it in the list for later
+                break
diff --git a/bin/reportlab/platypus/para.py b/bin/reportlab/platypus/para.py
new file mode 100644
index 00000000000..554c11003b0
--- /dev/null
+++ b/bin/reportlab/platypus/para.py
@@ -0,0 +1,2394 @@
+"""new experimental paragraph implementation
+
+Intended to allow support for paragraphs in paragraphs, hotlinks,
+embedded flowables, and underlining.  The main entry point is the
+function
+
+def Paragraph(text, style, bulletText=None, frags=None)
+
+Which is intended to be plug compatible with the "usual" platypus
+paragraph except that it supports more functionality.
+
+In this implementation you may embed paragraphs inside paragraphs
+to create hierarchically organized documents.
+
+This implementation adds the following paragraph-like tags (which
+support the same attributes as paragraphs, for font specification, etc).
+
+- Unnumberred lists (ala html):
+
+    
    +
  • first one
  • +
  • second one
  • +
+ +Also
    (default) or
      ,
        . + +- Numberred lists (ala html): + +
          +
        1. first one
        2. +
        3. second one
        4. +
        + +Also
          (default) or
            ,
              . + +- Display lists (ala HTML): + +For example + +
              +
              frogs
              Little green slimy things. Delicious with garlic
              +
              kittens
              cute, furry, not edible
              +
              bunnies
              cute, furry,. Delicious with garlic
              +
              + +ALSO the following additional internal paragraph markup tags are supported + +underlined text + +hyperlinked text +hyperlinked text + +Go to the end (go to document internal destination) +Go to the beginning + +This is the document start + (define document destination inside paragraph, color is optional) + +""" + +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.lib.utils import fp_str +from reportlab.platypus.flowables import Flowable +from reportlab.lib import colors + +from types import StringType, UnicodeType, InstanceType, TupleType, ListType, FloatType + +# SET THIS TO CAUSE A VIEWING BUG WITH ACROREAD 3 (for at least one input) +# CAUSEERROR = 0 + +debug = 0 + +DUMPPROGRAM = 0 + +TOOSMALLSPACE = 1e-5 + +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY + +# indent changes effect the next line +# align changes effect the current line + +# need to fix spacing questions... if ends with space then space may be inserted + +# NEGATIVE SPACE SHOULD NEVER BE EXPANDED (IN JUSTIFICATION, EG) + +class paragraphEngine: + # text origin of 0,0 is upper left corner + def __init__(self, program = None): + from reportlab.lib.colors import black + if program is None: + program = [] + self.lineOpHandlers = [] # for handling underlining and hyperlinking, etc + self.program = program + self.indent = self.rightIndent = 0.0 + self.baseindent = 0.0 # adjust this to add more indentation for bullets, eg + self.fontName = "Helvetica" + self.fontSize = 10 + self.leading = 12 + self.fontColor = black + self.x = self.y = self.rise = 0.0 + from reportlab.lib.enums import TA_LEFT + self.alignment = TA_LEFT + self.textStateStack = [] + + TEXT_STATE_VARIABLES = ("indent", "rightIndent", "fontName", "fontSize", + "leading", "fontColor", "lineOpHandlers", "rise", + "alignment") + #"textStateStack") + + def pushTextState(self): + state = [] + for var in self.TEXT_STATE_VARIABLES: + val = getattr(self, var) + state.append(val) + #self.textStateStack.append(state) + self.textStateStack = self.textStateStack+[state] # fresh copy + #print "push", self.textStateStack + #print "push", len(self.textStateStack), state + return state + + def popTextState(self): + state = self.textStateStack[-1] + self.textStateStack = self.textStateStack[:-1] + #print "pop", self.textStateStack + state = state[:] # copy for destruction + #print "pop", len(self.textStateStack), state + #print "handlers before", self.lineOpHandlers + for var in self.TEXT_STATE_VARIABLES: + val = state[0] + del state[0] + setattr(self, var, val) + + def format(self, maxwidth, maxheight, program, leading=0): + "return program with line operations added if at least one line fits" + # note: a generated formatted segment should not be formatted again + startstate = self.__dict__.copy() + #remainder = self.cleanProgram(program) + remainder = program[:] + #program1 = remainder[:] # debug only + lineprogram = [] + #if maxheight=self.leading and remainder: + #print "getting line with statestack", len(self.textStateStack) + #heightremaining = heightremaining - self.leading + indent = self.indent + rightIndent = self.rightIndent + linewidth = maxwidth - indent - rightIndent + beforelinestate = self.__dict__.copy() + if linewidthleading: + heightremaining = heightremaining-leading + else: + room = 0 + #self.resetState(beforelinestate) + self.__dict__.update(beforelinestate) + break # no room for this line +## if debug: +## print "line", line +## if lineIsFull: print "is full" +## else: print "is partially full" +## print "consumes", cursor, "elements" +## print "covers", currentLength, "of", maxwidth + alignment = self.alignment # last declared alignment for this line used + # recompute linewidth using the used indent + #linewidth = maxwidth - usedIndent - rightIndent + remainder = remainder[cursor:] + if not remainder: + # trim off the extra end of line + del line[-1] + # do justification if any + #line = self.shrinkWrap(line + if alignment==TA_LEFT: + #if debug: + # print "ALIGN LEFT" + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + pass + elif alignment==TA_CENTER: + #if debug: + # print "ALIGN CENTER" + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + line = self.centerAlign(line, currentLength, maxLength) + elif alignment==TA_RIGHT: + #if debug: + # print "ALIGN RIGHT" + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + line = self.rightAlign(line, currentLength, maxLength) + elif alignment==TA_JUSTIFY: + #if debug: + # print "JUSTIFY" + if remainder and lineIsFull: + if justStrings: + line = simpleJustifyAlign(line, currentLength, maxLength) + else: + line = self.justifyAlign(line, currentLength, maxLength) + else: + if justStrings: + line = stringLine(line, currentLength) + else: + line = self.shrinkWrap(line) + if debug: + print "no justify because line is not full or end of para" + else: + raise ValueError, "bad alignment "+repr(alignment) + if not justStrings: + line = self.cleanProgram(line) + lineprogram.extend(line) + laststate = self.__dict__.copy() + #self.resetState(startstate) + self.__dict__.update(startstate) + heightused = maxheight - heightremaining + return (lineprogram, remainder, laststate, heightused) + + def getState(self): + # inlined + return self.__dict__.copy() + + def resetState(self, state): + # primarily inlined + self.__dict__.update(state) + +## def sizeOfWord(self, word): +## inlineThisFunctionForEfficiency +## return float(stringWidth(word, self.fontName, self.fontSize)) + + def fitLine(self, program, totalLength): + "fit words (and other things) onto a line" + # assuming word lengths and spaces have not been yet added + # fit words onto a line up to maxlength, adding spaces and respecting extra space + from reportlab.pdfbase.pdfmetrics import stringWidth + usedIndent = self.indent + maxLength = totalLength - usedIndent - self.rightIndent + done = 0 + line = [] + cursor = 0 + lineIsFull = 0 + currentLength = 0 + maxcursor = len(program) + needspace = 0 + first = 1 + terminated = None + fontName = self.fontName + fontSize = self.fontSize + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + justStrings = 1 + while not done and cursormaxLength and not first: # always do at least one thing + # this word won't fit + #if debug: + # print "WORD", opcode, "wont fit, width", width, "fullwidth", fullwidth + # print " currentLength", currentLength, "newlength", newlength, "maxLength", maxLength + done = 1 + lineIsFull = 1 + else: + # fit the word: add a space then the word + if lastneedspace: + line.append( spacewidth ) # expandable space: positive + if opcode: + line.append( opcode ) + if abs(width)>TOOSMALLSPACE: + line.append( -width ) # non expanding space: negative + currentLength = newlength + #print line + #stop + first = 0 + elif topcode is FloatType: + justStrings = 0 + aopcode = abs(opcode) # negative means non expanding + if aopcode>TOOSMALLSPACE: + nextLength = currentLength+aopcode + if nextLength>maxLength and not first: # always do at least one thing + #if debug: print "EXPLICIT spacer won't fit", maxLength, nextLength, opcode + done = 1 + else: + if aopcode>TOOSMALLSPACE: + currentLength = nextLength + line.append(opcode) + first = 0 + elif topcode is TupleType: + justStrings = 0 + indicator = opcode[0] + #line.append(opcode) + if indicator=="nextLine": + # advance to nextLine + #(i, endallmarks) = opcode + line.append(opcode) + cursor = cursor+1 # consume this element + terminated = done = 1 + #if debug: + # print "nextLine encountered" + elif indicator=="color": + # change fill color + oldcolor = self.fontColor + (i, colorname) = opcode + #print "opcode", opcode + if type(colorname) in (StringType, UnicodeType): + color = self.fontColor = getattr(colors, colorname) + else: + color = self.fontColor = colorname # assume its something sensible :) + line.append(opcode) + elif indicator=="face": + # change font face + (i, fontname) = opcode + fontName = self.fontName = fontname + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + line.append(opcode) + elif indicator=="size": + # change font size + (i, fontsize) = opcode + size = abs(float(fontsize)) + if type(fontsize) in (StringType, UnicodeType): + if fontsize[:1]=="+": + fontSize = self.fontSize = self.fontSize + size + elif fontsize[:1]=="-": + fontSize = self.fontSize = self.fontSize - size + else: + fontSize = self.fontSize = size + else: + fontSize = self.fontSize = size + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + line.append(opcode) + elif indicator=="leading": + # change font leading + (i, leading) = opcode + self.leading = leading + line.append(opcode) + elif indicator=="indent": + # increase the indent + (i, increment) = opcode + indent = self.indent = self.indent + increment + if first: + usedIndent = max(indent, usedIndent) + maxLength = totalLength - usedIndent - self.rightIndent + line.append(opcode) + elif indicator=="push": + self.pushTextState() + line.append(opcode) + elif indicator=="pop": + try: + self.popTextState() + except: +## print "stack fault near", cursor +## for i in program[max(0, cursor-10):cursor+10]: +## if i==cursor: +## print "***>>>", +## print i + raise + fontName = self.fontName + fontSize = self.fontSize + spacewidth = stringWidth(" ", fontName, fontSize) #self.sizeOfWord(" ") + line.append(opcode) + elif indicator=="bullet": + (i, bullet, indent, font, size) = opcode + # adjust for base indent (only at format time -- only execute once) + indent = indent + self.baseindent + opcode = (i, bullet, indent, font, size) + if not first: + raise ValueError, "bullet not at beginning of line" + bulletwidth = float(stringWidth(bullet, font, size)) + spacewidth = float(stringWidth(" ", font, size)) + bulletmin = indent+spacewidth+bulletwidth + # decrease the line size to allow bullet + usedIndent = max(bulletmin, usedIndent) + if first: + maxLength = totalLength - usedIndent - self.rightIndent + line.append(opcode) + elif indicator=="rightIndent": + # increase the right indent + (i, increment) = opcode + self.rightIndent = self.rightIndent+increment + if first: + maxLength = totalLength - usedIndent - self.rightIndent + line.append(opcode) + elif indicator=="rise": + (i, rise) = opcode + newrise = self.rise = self.rise+rise + line.append(opcode) + elif indicator=="align": + (i, alignment) = opcode + #if debug: + # print "SETTING ALIGNMENT", alignment + self.alignment = alignment + line.append(opcode) + elif indicator=="lineOperation": + (i, handler) = opcode + line.append(opcode) + self.lineOpHandlers = self.lineOpHandlers + [handler] # fresh copy + elif indicator=="endLineOperation": + (i, handler) = opcode + h = self.lineOpHandlers[:] # fresh copy + h.remove(handler) + self.lineOpHandlers = h + line.append(opcode) + + else: + raise ValueError, "at format time don't understand indicator "+repr(indicator) + else: + raise ValueError, "op must be string, float, instance, or tuple "+repr(opcode) + if not done: + cursor = cursor+1 + #first = 0 +## if debug: +## if done: +## print "DONE FLAG IS SET" +## if cursor>=maxcursor: +## print "AT END OF PROGRAM" + if not terminated: + line.append( ("nextLine", 0) ) + #print "fitline", line + return (lineIsFull, line, cursor, currentLength, usedIndent, maxLength, justStrings) + + def centerAlign(self, line, lineLength, maxLength): + diff = maxLength-lineLength + shift = diff/2.0 + if shift>TOOSMALLSPACE: + return self.insertShift(line, shift) + return line + + def rightAlign(self, line, lineLength, maxLength): + shift = maxLength-lineLength + #die + if shift>TOOSMALLSPACE: + return self.insertShift(line, shift) + return line + + def insertShift(self, line, shift): + # insert shift just before first visible element in line + result = [] + first = 1 + for e in line: + te = type(e) + if first and (te in (StringType, UnicodeType, InstanceType)): + result.append(shift) + first = 0 + result.append(e) + return result + + def justifyAlign(self, line, lineLength, maxLength): + diff = maxLength-lineLength + # count EXPANDABLE SPACES AFTER THE FIRST VISIBLE + spacecount = 0 + visible = 0 + for e in line: + te = type(e) + if te is FloatType and e>TOOSMALLSPACE and visible: + spacecount = spacecount+1 + elif te in (StringType, UnicodeType, InstanceType): + visible = 1 + #if debug: print "diff is", diff, "wordcount", wordcount #; die + if spacecount<1: + return line + shift = diff/float(spacecount) + if shift<=TOOSMALLSPACE: + #if debug: print "shift too small", shift + return line + first = 1 + visible = 0 + result = [] + cursor = 0 + nline = len(line) + while cursorTOOSMALLSPACE and visible: + expanded = e+shift + result[-1] = expanded + cursor = cursor+1 + return result + +## if not first: +## #if debug: print "shifting", shift, e +## #result.append(shift) +## # add the shift in result before any start markers before e +## insertplace = len(result)-1 +## done = 0 +## myshift = shift +## while insertplace>0 and not done: +## beforeplace = insertplace-1 +## beforething = result[beforeplace] +## thingtype = type(beforething) +## if thingtype is TupleType: +## indicator = beforething[0] +## if indicator=="endLineOperation": +## done = 1 +## elif debug: +## print "adding shift before", beforething +## elif thingtype is FloatType: +## myshift = myshift + beforething +## del result[beforeplace] +## else: +## done = 1 +## if not done: +## insertplace = beforeplace +## result.insert(insertplace, myshift) +## first = 0 +## cursor = cursor+1 +## return result + + def shrinkWrap(self, line): + # for non justified text, collapse adjacent text/shift's into single operations + result = [] + index = 0 + maxindex = len(line) + while index0: + thefloats = -thefloats + if nexte<0 and thefloats>0: + nexte = -nexte + thefloats = thefloats + nexte + elif tnexte in (StringType, UnicodeType): + thestrings.append(nexte) + index = index+1 + if index0: + last = -last + if e<0 and last>0: + e = -e + last = float(last)+e + else: + if abs(last)>TOOSMALLSPACE: + result.append(last) + result.append(e) + last = 0 + if last: + result.append(last) + # now go backwards and delete all floats occurring after all visible elements +## count = len(result)-1 +## done = 0 +## while count>0 and not done: +## e = result[count] +## te = type(e) +## if te is StringType or te is InstanceType or te is TupleType: +## done = 1 +## elif te is FloatType: +## del result[count] +## count = count-1 + # move end operations left and start operations left up to visibles + change = 1 + rline = range(len(result)-1) + while change: + #print line + change = 0 + for index in rline: + nextindex = index+1 + this = result[index] + next = result[nextindex] + doswap = 0 + tthis = type(this) + tnext = type(next) + # don't swap visibles + if tthis in (StringType, UnicodeType) or \ + tnext in (StringType, UnicodeType) or \ + this is InstanceType or tnext is InstanceType: + doswap = 0 + # only swap two tuples if the second one is an end operation and the first is something else + elif tthis is TupleType: + thisindicator = this[0] + if tnext is TupleType: + nextindicator = next[0] + doswap = 0 + if (nextindicator=="endLineOperation" and thisindicator!="endLineOperation" + and thisindicator!="lineOperation"): + doswap = 1 # swap nonend<>end + elif tnext==FloatType: + if thisindicator=="lineOperation": + doswap = 1 # begin <> space + if doswap: + #print "swap", line[index],line[nextindex] + result[index] = next + result[nextindex] = this + change = 1 + return result + + def runOpCodes(self, program, canvas, textobject): + "render the line(s)" + + escape = canvas._escape + code = textobject._code + startstate = self.__dict__.copy() + font = None + size = None + # be sure to set them before using them (done lazily below) + #textobject.setFont(self.fontName, self.fontSize) + textobject.setFillColor(self.fontColor) + xstart = self.x + thislineindent = self.indent + thislinerightIndent = self.rightIndent + indented = 0 + for opcode in program: + topcode = type(opcode) + if topcode in (StringType, UnicodeType, InstanceType): + if not indented: + if abs(thislineindent)>TOOSMALLSPACE: + #if debug: print "INDENTING", thislineindent + #textobject.moveCursor(thislineindent, 0) + code.append('%s Td' % fp_str(thislineindent, 0)) + self.x = self.x + thislineindent + for handler in self.lineOpHandlers: + #handler.end_at(x, y, self, canvas, textobject) # finish, eg, underlining this line + handler.start_at(self.x, self.y, self, canvas, textobject) # start underlining the next + indented = 1 + # lazily set font (don't do it again if not needed) + if font!=self.fontName or size!=self.fontSize: + font = self.fontName + size = self.fontSize + textobject.setFont(font, size) + if topcode in (StringType, UnicodeType): + #textobject.textOut(opcode) + text = escape(opcode) + code.append('(%s) Tj' % text) + else: + # drawable thing + opcode.execute(self, textobject, canvas) + elif topcode is FloatType: + # use abs value (ignore expandable marking) + opcode = abs(opcode) + if opcode>TOOSMALLSPACE: + #textobject.moveCursor(opcode, 0) + code.append('%s Td' % fp_str(opcode, 0)) + self.x = self.x + opcode + elif topcode is TupleType: + indicator = opcode[0] + if indicator=="nextLine": + # advance to nextLine + (i, endallmarks) = opcode + x = self.x + y = self.y + newy = self.y = self.y-self.leading + newx = self.x = xstart + thislineindent = self.indent + thislinerightIndent = self.rightIndent + indented = 0 + for handler in self.lineOpHandlers: + handler.end_at(x, y, self, canvas, textobject) # finish, eg, underlining this line + #handler.start_at(newx, newy, self, canvas, textobject)) # start underlining the next + textobject.setTextOrigin(newx, newy) + elif indicator=="color": + # change fill color + oldcolor = self.fontColor + (i, colorname) = opcode + #print "opcode", opcode + if type(colorname) in (StringType, UnicodeType): + color = self.fontColor = getattr(colors, colorname) + else: + color = self.fontColor = colorname # assume its something sensible :) + #if debug: + # print color.red, color.green, color.blue + # print dir(color) + #print "color is", color + #from reportlab.lib.colors import green + #if color is green: print "color is green" + if color!=oldcolor: + textobject.setFillColor(color) + elif indicator=="face": + # change font face + (i, fontname) = opcode + self.fontName = fontname + #textobject.setFont(self.fontName, self.fontSize) + elif indicator=="size": + # change font size + (i, fontsize) = opcode + size = abs(float(fontsize)) + if type(fontsize) in (StringType, UnicodeType): + if fontsize[:1]=="+": + fontSize = self.fontSize = self.fontSize + size + elif fontsize[:1]=="-": + fontSize = self.fontSize = self.fontSize - size + else: + fontSize = self.fontSize = size + else: + fontSize = self.fontSize = size + #(i, fontsize) = opcode + self.fontSize = fontSize + textobject.setFont(self.fontName, self.fontSize) + elif indicator=="leading": + # change font leading + (i, leading) = opcode + self.leading = leading + elif indicator=="indent": + # increase the indent + (i, increment) = opcode + indent = self.indent = self.indent + increment + thislineindent = max(thislineindent, indent) + elif indicator=="push": + self.pushTextState() + elif indicator=="pop": + oldcolor = self.fontColor + oldfont = self.fontName + oldsize = self.fontSize + self.popTextState() + #if CAUSEERROR or oldfont!=self.fontName or oldsize!=self.fontSize: + # textobject.setFont(self.fontName, self.fontSize) + if oldcolor!=self.fontColor: + textobject.setFillColor(self.fontColor) + elif indicator=="wordSpacing": + (i, ws) = opcode + textobject.setWordSpace(ws) + elif indicator=="bullet": + (i, bullet, indent, font, size) = opcode + if abs(self.x-xstart)>TOOSMALLSPACE: + raise ValueError, "bullet not at beginning of line" + bulletwidth = float(stringWidth(bullet, font, size)) + spacewidth = float(stringWidth(" ", font, size)) + bulletmin = indent+spacewidth+bulletwidth + # decrease the line size to allow bullet as needed + if bulletmin > thislineindent: + #if debug: print "BULLET IS BIG", bullet, bulletmin, thislineindent + thislineindent = bulletmin + textobject.moveCursor(indent, 0) + textobject.setFont(font, size) + textobject.textOut(bullet) + textobject.moveCursor(-indent, 0) + #textobject.textOut("M") + textobject.setFont(self.fontName, self.fontSize) + elif indicator=="rightIndent": + # increase the right indent + (i, increment) = opcode + self.rightIndent = self.rightIndent+increment + elif indicator=="rise": + (i, rise) = opcode + newrise = self.rise = self.rise+rise + textobject.setRise(newrise) + elif indicator=="align": + (i, alignment) = opcode + self.alignment = alignment + elif indicator=="lineOperation": + (i, handler) = opcode + handler.start_at(self.x, self.y, self, canvas, textobject) + #self.lineOpHandlers.append(handler) + #if debug: print "adding", handler, self.lineOpHandlers + self.lineOpHandlers = self.lineOpHandlers + [handler] # fresh copy! + elif indicator=="endLineOperation": + (i, handler) = opcode + handler.end_at(self.x, self.y, self, canvas, textobject) + newh = self.lineOpHandlers = self.lineOpHandlers[:] # fresh copy + #if debug: print "removing", handler, self.lineOpHandlers + if handler in newh: + self.lineOpHandlers.remove(handler) + else: + pass + #print "WARNING: HANDLER", handler, "NOT IN", newh + else: + raise ValueError, "don't understand indicator "+repr(indicator) + else: + raise ValueError, "op must be string float or tuple "+repr(opcode) + laststate = self.__dict__.copy() + #self.resetState(startstate) + self.__dict__.update(startstate) + return laststate + +def stringLine(line, length): + "simple case: line with just strings and spacings which can be ignored" + + strings = [] + for x in line: + if type(x) in (StringType, UnicodeType): + strings.append(x) + text = ' '.join(strings) + result = [text, float(length)] + nextlinemark = ("nextLine", 0) + if line and line[-1]==nextlinemark: + result.append( nextlinemark ) + return result + +def simpleJustifyAlign(line, currentLength, maxLength): + "simple justification with only strings" + + strings = [] + for x in line[:-1]: + if type(x) in (StringType, UnicodeType): + strings.append(x) + nspaces = len(strings)-1 + slack = maxLength-currentLength + text = ' '.join(strings) + if nspaces>0 and slack>0: + wordspacing = slack/float(nspaces) + result = [("wordSpacing", wordspacing), text, maxLength, ("wordSpacing", 0)] + else: + result = [text, currentLength, ("nextLine", 0)] + nextlinemark = ("nextLine", 0) + if line and line[-1]==nextlinemark: + result.append( nextlinemark ) + return result + +from reportlab.lib.colors import black + +def readBool(text): + if text.upper() in ("Y", "YES", "TRUE", "1"): + return 1 + elif text.upper() in ("N", "NO", "FALSE", "0"): + return 0 + else: + raise RMLError, "true/false attribute has illegal value '%s'" % text + +def readAlignment(text): + up = text.upper() + if up == 'LEFT': + return TA_LEFT + elif up == 'RIGHT': + return TA_RIGHT + elif up in ['CENTER', 'CENTRE']: + return TA_CENTER + elif up == 'JUSTIFY': + return TA_JUSTIFY + +def readLength(text): + """Read a dimension measurement: accept "3in", "5cm", + "72 pt" and so on.""" + text = text.strip() + try: + return float(text) + except ValueError: + text = text.lower() + numberText, units = text[:-2],text[-2:] + numberText = numberText.strip() + try: + number = float(numberText) + except ValueError: + raise ValueError, "invalid length attribute '%s'" % text + try: + multiplier = { + 'in':72, + 'cm':28.3464566929, #72/2.54; is this accurate? + 'mm':2.83464566929, + 'pt':1 + }[units] + except KeyError: + raise RMLError, "invalid length attribute '%s'" % text + + return number * multiplier + +def lengthSequence(s, converter=readLength): + """from "(2, 1)" or "2,1" return [2,1], for example""" + s = s.strip() + if s[:1]=="(" and s[-1:]==")": + s = s[1:-1] + sl = s.split(',') + sl = [s.strip() for s in sl] + sl = [converter(s) for s in sl] + return sl + + +def readColor(text): + """Read color names or tuples, RGB or CMYK, and return a Color object.""" + if not text: + return None + from reportlab.lib import colors + from string import letters + if text[0] in letters: + return colors.__dict__[text] + tup = lengthSequence(text) + + msg = "Color tuple must have 3 (or 4) elements for RGB (or CMYC)." + assert 3 <= len(tup) <= 4, msg + msg = "Color tuple must have all elements <= 1.0." + for i in range(len(tup)): + assert tup[i] <= 1.0, msg + + if len(tup) == 3: + colClass = colors.Color + elif len(tup) == 4: + colClass = colors.CMYKColor + return apply(colClass, tup) + +class StyleAttributeConverters: + fontSize=[readLength] + leading=[readLength] + leftIndent=[readLength] + rightIndent=[readLength] + firstLineIndent=[readLength] + alignment=[readAlignment] + spaceBefore=[readLength] + spaceAfter=[readLength] + bulletFontSize=[readLength] + bulletIndent=[readLength] + textColor=[readColor] + backColor=[readColor] + +class SimpleStyle: + "simplified paragraph style without all the fancy stuff" + name = "basic" + fontName='Times-Roman' + fontSize=10 + leading=12 + leftIndent=0 + rightIndent=0 + firstLineIndent=0 + alignment=TA_LEFT + spaceBefore=0 + spaceAfter=0 + bulletFontName='Times-Roman' + bulletFontSize=10 + bulletIndent=0 + textColor=black + backColor=None + + def __init__(self, name, parent=None, **kw): + mydict = self.__dict__ + if parent: + for (a,b) in parent.__dict__.items(): + mydict[a]=b + for (a,b) in kw.items(): + mydict[a] = b + + def addAttributes(self, dictionary): + for key in dictionary.keys(): + value = dictionary[key] + if value is not None: + if hasattr(StyleAttributeConverters, key): + converter = getattr(StyleAttributeConverters, key)[0] + value = converter(value) + setattr(self, key, value) + + +DEFAULT_ALIASES = { + "h1.defaultStyle": "Heading1", + "h2.defaultStyle": "Heading2", + "h3.defaultStyle": "Heading3", + "h4.defaultStyle": "Heading4", + "h5.defaultStyle": "Heading5", + "h6.defaultStyle": "Heading6", + "title.defaultStyle": "Title", + "subtitle.defaultStyle": "SubTitle", + "para.defaultStyle": "Normal", + "pre.defaultStyle": "Code", + "ul.defaultStyle": "UnorderedList", + "ol.defaultStyle": "OrderedList", + "li.defaultStyle": "Definition", + } + +class FastPara(Flowable): + "paragraph with no special features (not even a single ampersand!)" + + def __init__(self, style, simpletext): + #if debug: + # print "FAST", id(self) + if "&" in simpletext: + raise ValueError, "no ampersands please!" + self.style = style + self.simpletext = simpletext + self.lines = None + + def wrap(self, availableWidth, availableHeight): + simpletext = self.simpletext + self.availableWidth = availableWidth + style = self.style + text = self.simpletext + rightIndent = style.rightIndent + leftIndent = style.leftIndent + leading = style.leading + font = style.fontName + size = style.fontSize + firstindent = style.firstLineIndent + #textcolor = style.textColor + words = simpletext.split() + lines = [] + from reportlab.pdfbase.pdfmetrics import stringWidth + spacewidth = stringWidth(" ", font, size) + currentline = [] + currentlength = 0 + firstmaxlength = availableWidth - rightIndent - firstindent + maxlength = availableWidth - rightIndent - leftIndent + if maxlengthavailableHeight: + done = 1 + if currentlength and not done: + lines.append( (' '.join(currentline), currentlength, len(currentline) )) + heightused = heightused+leading + self.lines = lines + self.height = heightused + remainder = self.remainder = ' '.join(words[cursor:]) + #print "lines", lines + #print "remainder is", remainder + else: + remainder = None + heightused = self.height + lines = self.lines + if remainder: + result = (availableWidth, availableHeight+leading) # need to split + else: + result = (availableWidth, heightused) + #if debug: print "wrap is", (availableWidth, availableHeight), result, len(lines) + return result + + def split(self, availableWidth, availableHeight): + style = self.style + leading = style.leading + if availableHeight1: + # patch from doug@pennatus.com, 9 Nov 2002, no extraspace on last line + textobject.setWordSpace((basicWidth-length)/(nwords-1.0)) + else: + textobject.setWordSpace(0.0) + textobject.setTextOrigin(x,y) + text = escape(text) + code.append('(%s) Tj' % text) + #textobject.textOut(text) + y = y-leading + c.drawText(textobject) + + def getSpaceBefore(self): + #if debug: + # print "got space before", self.spaceBefore + return self.style.spaceBefore + + def getSpaceAfter(self): + #print "got space after", self.spaceAfter + return self.style.spaceAfter + +def defaultContext(): + result = {} + from reportlab.lib.styles import getSampleStyleSheet + styles = getSampleStyleSheet() + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + result[stylenamekey] = styles[stylenamevalue] + return result + +def buildContext(stylesheet=None): + result = {} + from reportlab.lib.styles import getSampleStyleSheet + if stylesheet is not None: + # Copy styles with the same name as aliases + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + if stylesheet.has_key(stylenamekey): + result[stylenamekey] = stylesheet[stylenamekey] + # Then make aliases + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + if stylesheet.has_key(stylenamevalue): + result[stylenamekey] = stylesheet[stylenamevalue] + + styles = getSampleStyleSheet() + # Then, fill in defaults if they were not filled yet. + for (stylenamekey, stylenamevalue) in DEFAULT_ALIASES.items(): + if not result.has_key(stylenamekey) and styles.has_key(stylenamevalue): + result[stylenamekey] = styles[stylenamevalue] + return result + +class Para(Flowable): + + spaceBefore = 0 + spaceAfter = 0 + + def __init__(self, style, parsedText=None, bulletText=None, state=None, context=None, baseindent=0): + #print id(self), "para", parsedText + self.baseindent = baseindent + self.context = buildContext(context) + self.parsedText = parsedText + self.bulletText = bulletText + self.style1 = style # make sure Flowable doesn't use this unless wanted! call it style1 NOT style + #self.spaceBefore = self.spaceAfter = 0 + self.program = [] # program before layout + self.formattedProgram = [] # after layout + self.remainder = None # follow on paragraph if any + self.state = state # initial formatting state (for completions) + if not state: + self.spaceBefore = style.spaceBefore + self.spaceAfter = style.spaceAfter + #self.spaceBefore = "invalid value" + #if hasattr(self, "spaceBefore") and debug: + # print "spaceBefore is", self.spaceBefore, self.parsedText + self.bold = 0 + self.italic = 0 + self.face = style.fontName + self.size = style.fontSize + + def getSpaceBefore(self): + #if debug: + # print "got space before", self.spaceBefore + return self.spaceBefore + + def getSpaceAfter(self): + #print "got space after", self.spaceAfter + return self.spaceAfter + + def wrap(self, availableWidth, availableHeight): + if debug: + print "WRAPPING", id(self), availableWidth, availableHeight + print " ", self.formattedProgram + print " ", self.program + self.availableHeight = availableHeight + self.myengine = p = paragraphEngine() + p.baseindent = self.baseindent # for shifting bullets as needed + parsedText = self.parsedText + formattedProgram = self.formattedProgram + state = self.state + if state: + leading = state["leading"] + else: + leading = self.style1.leading + program = self.program + self.cansplit = 1 # until proven otherwise + if state: + p.resetState(state) + p.x = 0 + p.y = 0 + needatleast = state["leading"] + else: + needatleast = self.style1.leading + if availableHeight<=needatleast: + self.cansplit = 0 + #if debug: + # print "CANNOT COMPILE, NEED AT LEAST", needatleast, 'AVAILABLE', availableHeight + return (availableHeight+1, availableWidth) # cannot split + if parsedText is None and program is None: + raise ValueError, "need parsedText for formatting" + if not program: + self.program = program = self.compileProgram(parsedText) + if not self.formattedProgram: + (formattedProgram, remainder, \ + laststate, heightused) = p.format(availableWidth, availableHeight, program, leading) + self.formattedProgram = formattedProgram + self.height = heightused + self.laststate = laststate + self.remainderProgram = remainder + else: + heightused = self.height + remainder = None + # too big if there is a remainder + if remainder: + # lie about the height: it must be split anyway + #if debug: + # print "I need to split", self.formattedProgram + # print "heightused", heightused, "available", availableHeight, "remainder", len(remainder) + height = availableHeight + 1 + #print "laststate is", laststate + #print "saving remainder", remainder + self.remainder = Para(self.style1, parsedText=None, bulletText=None, \ + state=laststate, context=self.context) + self.remainder.program = remainder + self.remainder.spaceAfter = self.spaceAfter + self.spaceAfter = 0 + else: + self.remainder = None # no extra + height = heightused + if height>availableHeight: + height = availableHeight-0.1 + #if debug: + # print "giving height", height, "of", availableHeight, self.parsedText + result = (availableWidth, height) + if debug: + (w, h) = result + if abs(availableHeight-h)<0.2: + print "exact match???" + repr(availableHeight, h) + print "wrap is", (availableWidth, availableHeight), result + return result + + def split(self, availableWidth, availableHeight): + #if debug: + # print "SPLITTING", id(self), availableWidth, availableHeight + if availableHeight<=0 or not self.cansplit: + #if debug: + # print "cannot split", availableWidth, "too small" + return [] # wrap failed to find a split + self.availableHeight = availableHeight + formattedProgram = self.formattedProgram + #print "formattedProgram is", formattedProgram + if formattedProgram is None: + raise ValueError, "must call wrap before split" + elif not formattedProgram: + # no first line in self: fail to split + return [] + remainder = self.remainder + if remainder: + #print "SPLITTING" + result= [self, remainder] + else: + result= [self] + #if debug: print "split is", result + return result + + def draw(self): + p = self.myengine #paragraphEngine() + formattedProgram = self.formattedProgram + if formattedProgram is None: + raise ValueError, "must call wrap before draw" + state = self.state + laststate = self.laststate + if state: + p.resetState(state) + p.x = 0 + p.y = 0 + c = self.canv + #if debug: + # print id(self), "page number", c.getPageNumber() + height = self.height + if state: + leading = state["leading"] + else: + leading = self.style1.leading + #if debug: + # c.rect(0,0,-1, height-self.size, fill=1, stroke=1) + c.translate(0, height-self.size) + t = c.beginText() + #t.setTextOrigin(0,0) + if DUMPPROGRAM or debug: + print "="*44, "now running program" + for x in formattedProgram: + print x + print "-"*44 + laststate = p.runOpCodes(formattedProgram, c, t) + #print laststate["x"], laststate["y"] + c.drawText(t) + + def compileProgram(self, parsedText, program=None): + style = self.style1 + # standard parameters + #program = self.program + if program is None: + program = [] + a = program.append + fn = style.fontName + # add style information if there was no initial state + a( ("face", fn ) ) + from reportlab.lib.fonts import ps2tt + (self.face, self.bold, self.italic) = ps2tt(fn) + a( ("size", style.fontSize ) ) + self.size = style.fontSize + a( ("align", style.alignment ) ) + a( ("indent", style.leftIndent ) ) + if style.firstLineIndent: + a( ("indent", style.firstLineIndent ) ) # must be undone later + a( ("rightIndent", style.rightIndent ) ) + a( ("leading", style.leading) ) + if style.textColor: + a( ("color", style.textColor) ) + #a( ("nextLine", 0) ) # clear for next line + if self.bulletText: + self.do_bullet(self.bulletText, program) + self.compileComponent(parsedText, program) + # now look for a place where to insert the unindent after the first line + if style.firstLineIndent: + count = 0 + for x in program: + count = count+1 + tx = type(x) + if tx in (StringType, UnicodeType, InstanceType): + break + program.insert( count, ("indent", -style.firstLineIndent ) ) # defaults to end if no visibles + #print "="*8, id(self), "program is" + #for x in program: + # print x +## print "="*11 +## # check pushes and pops +## stackcount = 0 +## dump = 0 +## for x in program: +## if dump: +## print "dump:", x +## if type(x) is TupleType: +## i = x[0] +## if i=="push": +## stackcount = stackcount+1 +## print " "*stackcount, "push", stackcount +## if i=="pop": +## stackcount = stackcount-1 +## print " "*stackcount, "pop", stackcount +## if stackcount<0: +## dump=1 +## print "STACK UNDERFLOW!" +## if dump: stop + return program + + def linearize(self, program = None, parsedText=None): + #print "LINEARIZING", self + #program = self.program = [] + if parsedText is None: + parsedText = self.parsedText + style = self.style1 + if program is None: + program = [] + program.append( ("push",) ) + if style.spaceBefore: + program.append( ("leading", style.spaceBefore+style.leading) ) + else: + program.append( ("leading", style.leading) ) + program.append( ("nextLine", 0) ) + program = self.compileProgram(parsedText, program=program) + program.append( ("pop",) ) + # go to old margin + program.append( ("push",) ) + if style.spaceAfter: + program.append( ("leading", style.spaceAfter) ) + else: + program.append( ("leading", 0) ) + program.append( ("nextLine", 0) ) + program.append( ("pop",) ) + + def compileComponent(self, parsedText, program): + import types + ttext = type(parsedText) + #program = self.program + if ttext in (StringType, UnicodeType): + # handle special characters here... + # short cut + if parsedText: + stext = parsedText.strip() + if not stext: + program.append(" ") # contract whitespace to single space + else: + handleSpecialCharacters(self, parsedText, program) + elif ttext is ListType: + for e in parsedText: + self.compileComponent(e, program) + elif ttext is TupleType: + (tagname, attdict, content, extra) = parsedText + if not attdict: + attdict = {} + compilername = "compile_"+tagname + compiler = getattr(self, compilername, None) + if compiler is not None: + compiler(attdict, content, extra, program) + else: + # just pass the tag through + if debug: + L = [ "<" + tagname ] + a = L.append + if not attdict: attdict = {} + for (k, v) in attdict.items(): + a(" %s=%s" % (k,v)) + if content: + a(">") + a(str(content)) + a("" % tagname) + else: + a("/>") + t = ''.join(L) + handleSpecialCharacters(self, t, program) + else: + raise ValueError, "don't know how to handle tag " + repr(tagname) + + def shiftfont(self, program, face=None, bold=None, italic=None): + oldface = self.face + oldbold = self.bold + olditalic = self.italic + oldfontinfo = (oldface, oldbold, olditalic) + if face is None: face = oldface + if bold is None: bold = oldbold + if italic is None: italic = olditalic + self.face = face + self.bold = bold + self.italic = italic + from reportlab.lib.fonts import tt2ps + font = tt2ps(face,bold,italic) + oldfont = tt2ps(oldface,oldbold,olditalic) + if font!=oldfont: + program.append( ("face", font ) ) + return oldfontinfo + + def compile_(self, attdict, content, extra, program): + # "anonymous" tag: just do the content + for e in content: + self.compileComponent(e, program) + #compile_para = compile_ # at least for now... + + def compile_pageNumber(self, attdict, content, extra, program): + program.append(PageNumberObject()) + + def compile_b(self, attdict, content, extra, program): + (f,b,i) = self.shiftfont(program, bold=1) + for e in content: + self.compileComponent(e, program) + self.shiftfont(program, bold=b) + + def compile_i(self, attdict, content, extra, program): + (f,b,i) = self.shiftfont(program, italic=1) + for e in content: + self.compileComponent(e, program) + self.shiftfont(program, italic=i) + + def compile_u(self, attdict, content, extra, program): + # XXXX must eventually add things like alternative colors + #program = self.program + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + program.append( ('endLineOperation', UNDERLINE) ) + + def compile_sub(self, attdict, content, extra, program): + size = self.size + self.size = newsize = size * 0.7 + rise = size*0.5 + #program = self.program + program.append( ('size', newsize) ) + self.size = size + program.append( ('rise', -rise) ) + for e in content: + self.compileComponent(e, program) + program.append( ('size', size) ) + program.append( ('rise', rise) ) + + def compile_ul(self, attdict, content, extra, program, tagname="ul"): + # by transformation + #print "compile", tagname, attdict + atts = attdict.copy() + bulletmaker = bulletMaker(tagname, atts, self.context) + # now do each element as a separate paragraph + for e in content: + te = type(e) + if te in (StringType, UnicodeType): + if e.strip(): + raise ValueError, "don't expect CDATA between list elements" + elif te is TupleType: + (tagname, attdict1, content1, extra) = e + if tagname!="li": + raise ValueError, "don't expect %s inside list" % repr(tagname) + newatts = atts.copy() + if attdict1: + newatts.update(attdict1) + bulletmaker.makeBullet(newatts) + self.compile_para(newatts, content1, extra, program) + + def compile_ol(self, attdict, content, extra, program): + return self.compile_ul(attdict, content, extra, program, tagname="ol") + + def compile_dl(self, attdict, content, extra, program): + # by transformation + #print "compile", tagname, attdict + atts = attdict.copy() + # by transformation + #print "compile", tagname, attdict + atts = attdict.copy() + bulletmaker = bulletMaker("dl", atts, self.context) + # now do each element as a separate paragraph + contentcopy = list(content) # copy for destruction + bullet = "" + while contentcopy: + e = contentcopy[0] + del contentcopy[0] + te = type(e) + if te in (StringType, UnicodeType): + if e.strip(): + raise ValueError, "don't expect CDATA between list elements" + elif not contentcopy: + break # done at ending whitespace + else: + continue # ignore intermediate whitespace + elif te is TupleType: + (tagname, attdict1, content1, extra) = e + if tagname!="dd" and tagname!="dt": + raise ValueError, "don't expect %s here inside list, expect 'dd' or 'dt'" % \ + repr(tagname) + if tagname=="dt": + if bullet: + raise ValueError, "dt will not be displayed unless followed by a dd: "+repr(bullet) + if content1: + self.compile_para(attdict1, content1, extra, program) + # raise ValueError, \ + # "only simple strings supported in dd content currently: "+repr(content1) + elif tagname=="dd": + newatts = atts.copy() + if attdict1: + newatts.update(attdict1) + bulletmaker.makeBullet(newatts, bl=bullet) + self.compile_para(newatts, content1, extra, program) + bullet = "" # don't use this bullet again + if bullet: + raise ValueError, "dt will not be displayed unless followed by a dd"+repr(bullet) + + def compile_super(self, attdict, content, extra, program): + size = self.size + self.size = newsize = size * 0.7 + rise = size*0.5 + #program = self.program + program.append( ('size', newsize) ) + program.append( ('rise', rise) ) + for e in content: + self.compileComponent(e, program) + program.append( ('size', size) ) + self.size = size + program.append( ('rise', -rise) ) + + def compile_font(self, attdict, content, extra, program): + #program = self.program + program.append( ("push",) ) # store current data + if attdict.has_key("face"): + face = attdict["face"] + from reportlab.lib.fonts import tt2ps + try: + font = tt2ps(face,self.bold,self.italic) + except: + font = face # better work! + program.append( ("face", font ) ) + if attdict.has_key("color"): + colorname = attdict["color"] + program.append( ("color", colorname) ) + if attdict.has_key("size"): + #size = float(attdict["size"]) # really should convert int, cm etc here! + size = attdict["size"] + program.append( ("size", size) ) + for e in content: + self.compileComponent(e, program) + program.append( ("pop",) ) # restore as before + + def compile_a(self, attdict, content, extra, program): + url = attdict["href"] + colorname = attdict.get("color", "blue") + #program = self.program + Link = HotLink(url) + program.append( ("push",) ) # store current data + program.append( ("color", colorname) ) + program.append( ('lineOperation', Link) ) + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + program.append( ('endLineOperation', UNDERLINE) ) + program.append( ('endLineOperation', Link) ) + program.append( ("pop",) ) # restore as before + + def compile_link(self, attdict, content, extra, program): + dest = attdict["destination"] + colorname = attdict.get("color", None) + #program = self.program + Link = InternalLink(dest) + program.append( ("push",) ) # store current data + if colorname: + program.append( ("color", colorname) ) + program.append( ('lineOperation', Link) ) + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + program.append( ('endLineOperation', UNDERLINE) ) + program.append( ('endLineOperation', Link) ) + program.append( ("pop",) ) # restore as before + + def compile_setLink(self, attdict, content, extra, program): + dest = attdict["destination"] + colorname = attdict.get("color", "blue") + #program = self.program + Link = DefDestination(dest) + program.append( ("push",) ) # store current data + if colorname: + program.append( ("color", colorname) ) + program.append( ('lineOperation', Link) ) + if colorname: + program.append( ('lineOperation', UNDERLINE) ) + for e in content: + self.compileComponent(e, program) + if colorname: + program.append( ('endLineOperation', UNDERLINE) ) + program.append( ('endLineOperation', Link) ) + program.append( ("pop",) ) # restore as before + + #def compile_p(self, attdict, content, extra, program): + # # have to be careful about base indent here! + # not finished + + def compile_bullet(self, attdict, content, extra, program): + ### eventually should allow things like images and graphics in bullets too XXXX + if len(content)!=1 or type(content[0]) not in (StringType, UnicodeType): + raise ValueError, "content for bullet must be a single string" + text = content[0] + self.do_bullet(text, program) + + def do_bullet(self, text, program): + style = self.style1 + #program = self.program + indent = style.bulletIndent + self.baseindent + font = style.bulletFontName + size = style.bulletFontSize + program.append( ("bullet", text, indent, font, size) ) + + def compile_tt(self, attdict, content, extra, program): + (f,b,i) = self.shiftfont(program, face="Courier") + for e in content: + self.compileComponent(e, program) + self.shiftfont(program, face=f) + + def compile_greek(self, attdict, content, extra, program): + self.compile_font({"face": "symbol"}, content, extra, program) + + def compile_evalString(self, attdict, content, extra, program): + program.append( EvalStringObject(attdict, content, extra, self.context) ) + + def compile_name(self, attdict, content, extra, program): + program.append( NameObject(attdict, content, extra, self.context) ) + + def compile_getName(self, attdict, content, extra, program): + program.append( GetNameObject(attdict, content, extra, self.context) ) + + def compile_seq(self, attdict, content, extra, program): + program.append( SeqObject(attdict, content, extra, self.context) ) + + def compile_seqReset(self, attdict, content, extra, program): + program.append( SeqResetObject(attdict, content, extra, self.context) ) + + def compile_seqDefault(self, attdict, content, extra, program): + program.append( SeqDefaultObject(attdict, content, extra, self.context) ) + + def compile_para(self, attdict, content, extra, program, stylename = "para.defaultStyle"): + if attdict is None: + attdict = {} + context = self.context + stylename = attdict.get("style", stylename) + style = context[stylename] + newstyle = SimpleStyle(name="rml2pdf internal embedded style", parent=style) + newstyle.addAttributes(attdict) + bulletText = attdict.get("bulletText", None) + mystyle = self.style1 + thepara = Para(newstyle, content, context=context, bulletText=bulletText) + # possible ref loop on context, break later + # now compile it and add it to the program + mybaseindent = self.baseindent + self.baseindent = thepara.baseindent = mystyle.leftIndent + self.baseindent + thepara.linearize(program=program) + program.append( ("nextLine", 0) ) + self.baseindent = mybaseindent + +class bulletMaker: + def __init__(self, tagname, atts, context): + self.tagname = tagname + #print "context is", context + style = "li.defaultStyle" + self.style = style = atts.get("style", style) + typ = {"ul": "disc", "ol": "1", "dl": None}[tagname] + #print tagname, "bulletmaker type is", typ + self.typ =typ = atts.get("type", typ) + #print tagname, "bulletmaker type is", typ + if not atts.has_key("leftIndent"): + # get the style so you can choose an indent length + thestyle = context[style] + from reportlab.pdfbase.pdfmetrics import stringWidth + size = thestyle.fontSize + indent = stringWidth("XXX", "Courier", size) + atts["leftIndent"] = str(indent) + self.count = 0 + + def makeBullet(self, atts, bl=None): + count = self.count = self.count+1 + typ = self.typ + tagname = self.tagname + #print "makeBullet", tagname, typ, count + # forget space before for non-first elements + if count>1: + atts["spaceBefore"] = "0" + if bl is None: + if tagname=="ul": + if typ=="disc": bl = chr(109) + elif typ=="circle": bl = chr(108) + elif typ=="square": bl = chr(110) + else: + raise ValueError, "unordered list type %s not implemented" % repr(typ) + if not atts.has_key("bulletFontName"): + atts["bulletFontName"] = "ZapfDingbats" + elif tagname=="ol": + if typ=="1": bl = repr(count) + elif typ=="a": + theord = ord("a")+count-1 + bl = chr(theord) + elif typ=="A": + theord = ord("A")+count-1 + bl = chr(theord) + else: + raise ValueError, "ordered bullet type %s not implemented" % repr(typ) + else: + raise ValueError, "bad tagname "+repr(tagname) + if not atts.has_key("bulletText"): + atts["bulletText"] = bl + if not atts.has_key("style"): + atts["style"] = self.style + +class EvalStringObject: + "this will only work if rml2pdf is present" + + tagname = "evalString" + + def __init__(self, attdict, content, extra, context): + if not attdict: + attdict = {} + self.attdict = attdict + self.content = content + self.context = context + self.extra = extra + + def getOp(self, tuple, engine): + from rlextra.rml2pdf.rml2pdf import Controller + #print "tuple", tuple + op = self.op = Controller.processTuple(tuple, self.context, {}) + return op + + def width(self, engine): + from reportlab.pdfbase.pdfmetrics import stringWidth + content = self.content + if not content: + content = [] + tuple = (self.tagname, self.attdict, content, self.extra) + op = self.op = self.getOp(tuple, engine) + #print op.__class__ + #print op.pcontent + #print self + s = str(op) + return stringWidth(s, engine.fontName, engine.fontSize) + + def execute(self, engine, textobject, canvas): + textobject.textOut(str(self.op)) + +class SeqObject(EvalStringObject): + + def getOp(self, tuple, engine): + from reportlab.lib.sequencer import getSequencer + globalsequencer = getSequencer() + attr = self.attdict + #if it has a template, use that; otherwise try for id; + #otherwise take default sequence + if attr.has_key('template'): + templ = attr['template'] + op = self.op = templ % globalsequencer + return op + elif attr.has_key('id'): + id = attr['id'] + else: + id = None + op = self.op = globalsequencer.nextf(id) + return op + +class NameObject(EvalStringObject): + tagname = "name" + def execute(self, engine, textobject, canvas): + pass # name doesn't produce any output + +class SeqDefaultObject(NameObject): + + def getOp(self, tuple, engine): + from reportlab.lib.sequencer import getSequencer + globalsequencer = getSequencer() + attr = self.attdict + try: + default = attr['id'] + except KeyError: + default = None + globalsequencer.setDefaultCounter(default) + self.op = "" + return "" + +class SeqResetObject(NameObject): + + def getOp(self, tuple, engine): + from reportlab.lib.sequencer import getSequencer + import math + globalsequencer = getSequencer() + attr = self.attdict + try: + id = attr['id'] + except KeyError: + id = None + try: + base = math.atoi(attr['base']) + except: + base=0 + globalsequencer.reset(id, base) + self.op = "" + return "" + +class GetNameObject(EvalStringObject): + tagname = "getName" + +class PageNumberObject: + + def __init__(self, example="XXX"): + self.example = example # XXX SHOULD ADD THE ABILITY TO PASS IN EXAMPLES + + def width(self, engine): + from reportlab.pdfbase.pdfmetrics import stringWidth + return stringWidth(self.example, engine.fontName, engine.fontSize) + + def execute(self, engine, textobject, canvas): + n = canvas.getPageNumber() + textobject.textOut(str(n)) + +### this should be moved into rml2pdf +def EmbedInRml2pdf(): + "make the para the default para implementation in rml2pdf" + from rlextra.rml2pdf.rml2pdf import MapNode, Controller # may not need to use superclass? + global paraMapper, theParaMapper, ulMapper + + class paraMapper(MapNode): + #stylename = "para.defaultStyle" + def translate(self, nodetuple, controller, context, overrides): + (tagname, attdict, content, extra) = nodetuple + stylename = tagname+".defaultStyle" + stylename = attdict.get("style", stylename) + style = context[stylename] + mystyle = SimpleStyle(name="rml2pdf internal style", parent=style) + mystyle.addAttributes(attdict) + bulletText = attdict.get("bulletText", None) + # can we use the fast implementation? + import types + result = None + if not bulletText and len(content)==1: + text = content[0] + if type(text) in (StringType, UnicodeType) and "&" not in text: + result = FastPara(mystyle, text) + if result is None: + result = Para(mystyle, content, context=context, bulletText=bulletText) # possible ref loop on context, break later + return result + + theParaMapper = paraMapper() + + class ulMapper(MapNode): + # wrap in a default para and let the para do it + def translate(self, nodetuple, controller, context, overrides): + thepara = ("para", {}, [nodetuple], None) + return theParaMapper.translate(thepara, controller, context, overrides) + + # override rml2pdf interpreters (should be moved to rml2pdf) + theListMapper = ulMapper() + Controller["ul"] = theListMapper + Controller["ol"] = theListMapper + Controller["dl"] = theListMapper + Controller["para"] = theParaMapper + Controller["h1"] = theParaMapper + Controller["h2"] = theParaMapper + Controller["h3"] = theParaMapper + Controller["title"] = theParaMapper + +def handleSpecialCharacters(engine, text, program=None): + from paraparser import greeks, symenc + from string import whitespace, atoi, atoi_error + standard={'lt':'<', 'gt':'>', 'amp':'&'} + # add space prefix if space here + if text[0:1] in whitespace: + program.append(" ") + #print "handling", repr(text) + # shortcut + if 0 and "&" not in text: + result = [] + for x in text.split(): + result.append(x+" ") + if result: + last = result[-1] + if text[-1:] not in whitespace: + result[-1] = last.strip() + program.extend(result) + return program + if program is None: + program = [] + amptext = text.split("&") + first = 1 + lastfrag = amptext[-1] + for fragment in amptext: + if not first: + # check for special chars + semi = fragment.find(";") + if semi>0: + name = fragment[:semi] + if name[0]=='#': + try: + if name[1] == 'x': + n = atoi(name[2:], 16) + else: + n = atoi(name[1:]) + except atoi_error: + n = -1 + if 0<=n<=255: fragment = chr(n)+fragment[semi+1:] + elif symenc.has_key(n): + fragment = fragment[semi+1:] + (f,b,i) = engine.shiftfont(program, face="symbol") + program.append(symenc[n]) + engine.shiftfont(program, face=f) + if fragment and fragment[0] in whitespace: + program.append(" ") # follow with a space + else: + fragment = "&"+fragment + elif standard.has_key(name): + fragment = standard[name]+fragment[semi+1:] + elif greeks.has_key(name): + fragment = fragment[semi+1:] + greeksub = greeks[name] + (f,b,i) = engine.shiftfont(program, face="symbol") + program.append(greeksub) + engine.shiftfont(program, face=f) + if fragment and fragment[0] in whitespace: + program.append(" ") # follow with a space + else: + # add back the & + fragment = "&"+fragment + else: + # add back the & + fragment = "&"+fragment + # add white separated components of fragment followed by space + sfragment = fragment.split() + for w in sfragment[:-1]: + program.append(w+" ") + # does the last one need a space? + if sfragment and fragment: + # reader 3 used to go nuts if you don't special case the last frag, but it's fixed? + if fragment[-1] in whitespace: # or fragment==lastfrag: + program.append( sfragment[-1]+" " ) + else: + last = sfragment[-1].strip() + if last: + #print "last is", repr(last) + program.append( last ) + first = 0 + #print "HANDLED", program + return program + +def Paragraph(text, style, bulletText=None, frags=None, context=None): + """ Paragraph(text, style, bulletText=None) + intended to be like a platypus Paragraph but better. + """ + # if there is no & or < in text then use the fast paragraph + if "&" not in text and "<" not in text: + return FastPara(style, simpletext=text) + else: + # use the fully featured one. + from reportlab.lib import rparsexml + parsedpara = rparsexml.parsexmlSimple(text,entityReplacer=None) + return Para(style, parsedText=parsedpara, bulletText=bulletText, state=None, context=context) + +class UnderLineHandler: + def __init__(self, color=None): + self.color = color + def start_at(self, x,y, para, canvas, textobject): + self.xStart = x + self.yStart = y + def end_at(self, x, y, para, canvas, textobject): + offset = para.fontSize/8.0 + canvas.saveState() + color = self.color + if self.color is None: + color = para.fontColor + canvas.setStrokeColor(color) + canvas.line(self.xStart, self.yStart-offset, x,y-offset) + canvas.restoreState() + +UNDERLINE = UnderLineHandler() + +class HotLink(UnderLineHandler): + + def __init__(self, url): + self.url = url + + def end_at(self, x, y, para, canvas, textobject): + fontsize = para.fontSize + rect = [self.xStart, self.yStart, x,y+fontsize] + if debug: + print "LINKING RECTANGLE", rect + #canvas.rect(self.xStart, self.yStart, x-self.xStart,y+fontsize-self.yStart, stroke=1) + self.link(rect, canvas) + + def link(self, rect, canvas): + canvas.linkURL(self.url, rect, relative=1) + +class InternalLink(HotLink): + + def link(self, rect, canvas): + destinationname = self.url + contents = "" + canvas.linkRect(contents, destinationname, rect, Border="[0 0 0]") + +class DefDestination(HotLink): + + defined = 0 + + def link(self, rect, canvas): + destinationname = self.url + if not self.defined: + [x, y, x1, y1] = rect + canvas.bookmarkHorizontal(destinationname, x, y1) # use the upper y + self.defined = 1 + +def splitspace(text): + # split on spacing but include spaces at element ends + stext = text.split() + result = [] + for e in stext: + result.append(e+" ") + return result + + +testparagraph = """ +This is Text. +This is bold text. +This is Text. +This is italic text. + +
                +
              • this is an element at 1 +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth +more text monospaced and back to normal + +
                  +
                • this is an element at 2 + +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                    +
                  • this is an element at 3 + +more text and even more text and on and on and so forth + + +
                    +
                    frogs
                    Little green slimy things. Delicious with garlic
                    +
                    kittens
                    cute, furry, not edible
                    +
                    bunnies
                    cute, furry,. Delicious with garlic
                    +
                    + +more text and even more text and on and on and so forth + +
                      +
                    • this is an element at 4 + +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                    • +
                    + +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                  • +
                  +more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                • + +
                +UNDERLINED more text and even more text and on and on and so forth +more text and even more text and on and on and so forth + +
                  +
                1. first element of the alpha list + +
                    +
                  • first element of the square unnumberred list
                  • + +
                  • second element of the unnumberred list
                  • + +
                  • third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list +
                  • + +
                  • fourth element of the unnumberred list
                  • + +
                  + +
                2. + +
                3. second element of the alpha list
                4. + +
                5. third element of the alpha list + third element of the unnumberred list &#33; --> ! + third element of the unnumberred list &#8704; --> ∀ + third element of the unnumberred list &exist; --> ∃ + third element of the unnumberred list + third element of the unnumberred list + third element of the unnumberred list +
                6. + +
                7. fourth element of the alpha list
                8. + +
                + + +
              • +
              +""" + +testparagraph1 = """ +goto www.reportlab.com. + + + +Red letter. thisisareallylongword andsoisthis andthisislonger +justified text paragraph example +justified text paragraph example +justified text paragraph example + + + +Green letter. +centered text paragraph example +centered text paragraph example +centered text paragraph example + + +Blue letter. +right justified text paragraph example +right justified text paragraph example +right justified text paragraph example + + +Yellow letter. +left justified text paragraph example +left justified text paragraph example +left justified text paragraph example + + +""" + +def test2(canv,testpara): + #print test_program; return + from reportlab.lib.units import inch + from reportlab.lib.styles import ParagraphStyle + from reportlab.lib import rparsexml + parsedpara = rparsexml.parsexmlSimple(testpara,entityReplacer=None) + S = ParagraphStyle("Normal", None) + P = Para(S, parsedpara) + (w, h) = P.wrap(5*inch, 10*inch) + print "wrapped as", (h,w) + canv.saveState() + canv.translate(1*inch, 1*inch) + canv.rect(0,0,5*inch,10*inch, fill=0, stroke=1) + P.canv = canv + canv.saveState() + P.draw() + canv.restoreState() + canv.setStrokeColorRGB(1, 0, 0) + #canv.translate(0, 3*inch) + canv.rect(0,0,w,h, fill=0, stroke=1) + canv.restoreState() + canv.showPage() + +testlink = HotLink("http://www.reportlab.com") + +test_program = [ + ('push',), + ('indent', 100), + ('rightIndent', 200), + ('bullet', 'very long bullet', 50, 'Courier', 14), + ('align', TA_CENTER), + ('face', "Times-Roman"), + ('size', 12), + ('leading', 14), + ] + splitspace("This is the first segment of the first paragraph.") + [ + ('lineOperation', testlink), + ]+splitspace("HOTLINK This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [ + ('endLineOperation', testlink), + ('nextLine', 0), + ('align', TA_LEFT), + ('bullet', 'Bullet', 10, 'Courier', 8), + ('face', "Times-Roman"), + ('size', 12), + ('leading', 14), + ] + splitspace("This is the SECOND!!! segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [ + ('nextLine', 0), + ('align', TA_JUSTIFY), + ('bullet', 'Bullet not quite as long this time', 50, 'Courier', 8), + ('face', "Helvetica-Oblique"), + ('size', 12), + ('leading', 14), + ('push',), + ('color', 'red'), + ] + splitspace("This is the THIRD!!! segment of the first paragraph." + ) + [ + ('lineOperation', UNDERLINE), + ] + splitspace("This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. This is the first segment of the first paragraph. ") + [ + ('endLineOperation', UNDERLINE), + ('rise', 5), + "raised ", "text ", + ('rise', -10), + "lowered ", "text ", + ('rise', 5), + "normal ", "text ", + ('pop',), + ('indent', 100), + ('rightIndent', 50), + ('nextLine', 0), + ('align', TA_RIGHT), + ('bullet', 'O', 50, 'Courier', 14), + ('face', "Helvetica"), + ('size', 12), + ('leading', 14), + ] + splitspace("And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a And this is the remainder of the paragraph indented further. a a a a a a a a ") + [ + ('pop',), + ('nextLine', 0),] + +def test(): + from pprint import pprint + #print test_program; return + from reportlab.pdfgen import canvas + from reportlab.lib.units import inch + fn = "paratest0.pdf" + c = canvas.Canvas(fn) + test2(c,testparagraph) + test2(c,testparagraph1) + if 1: + remainder = test_program + test_program + test_program + laststate = {} + while remainder: + print "NEW PAGE" + c.translate(inch, 8*inch) + t = c.beginText() + t.setTextOrigin(0,0) + p = paragraphEngine() + p.resetState(laststate) + p.x = 0 + p.y = 0 + maxwidth = 7*inch + maxheight = 500 + (formattedprogram, remainder, laststate, height) = p.format(maxwidth, maxheight, remainder) + if debug: + pprint( formattedprogram )#; return + laststate = p.runOpCodes(formattedprogram, c, t) + c.drawText(t) + c.showPage() + print "="*30, "x=", laststate["x"], "y=", laststate["y"] + c.save() + print fn + +if __name__=="__main__": + test() diff --git a/bin/reportlab/platypus/paragraph.py b/bin/reportlab/platypus/paragraph.py new file mode 100644 index 00000000000..ca84f4ddc05 --- /dev/null +++ b/bin/reportlab/platypus/paragraph.py @@ -0,0 +1,1082 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/paragraph.py +__version__=''' $Id: paragraph.py 2857 2006-05-11 13:06:52Z rgbecker $ ''' +from string import join, whitespace, find +from operator import truth +from types import StringType, ListType +from reportlab.pdfbase.pdfmetrics import stringWidth, getFont +from reportlab.platypus.paraparser import ParaParser +from reportlab.platypus.flowables import Flowable +from reportlab.lib.colors import Color +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.utils import _className +from reportlab.lib.textsplit import wordSplit +from copy import deepcopy +from reportlab.lib.abag import ABag +import re + +#on UTF8 branch, split and strip must be unicode-safe! +def split(text, delim=' '): + if type(text) is str: text = text.decode('utf8') + if type(delim) is str: delim = delim.decode('utf8') + return [uword.encode('utf8') for uword in text.split(delim)] + +def strip(text): + if type(text) is str: text = text.decode('utf8') + return text.strip().encode('utf8') + +class ParaLines(ABag): + """ + class ParaLines contains the broken into lines representation of Paragraphs + kind=0 Simple + fontName, fontSize, textColor apply to whole Paragraph + lines [(extraSpace1,words1),....,(extraspaceN,wordsN)] + + + kind==1 Complex + lines [FragLine1,...,FragLineN] + """ + +class FragLine(ABag): + """class FragLine contains a styled line (ie a line with more than one style) + + extraSpace unused space for justification only + wordCount 1+spaces in line for justification purposes + words [ParaFrags] style text lumps to be concatenated together + fontSize maximum fontSize seen on the line; not used at present, + but could be used for line spacing. + """ + +#our one and only parser +# XXXXX if the parser has any internal state using only one is probably a BAD idea! +_parser=ParaParser() + +def _lineClean(L): + return join(filter(truth,split(strip(L)))) + +def cleanBlockQuotedText(text,joiner=' '): + """This is an internal utility which takes triple- + quoted text form within the document and returns + (hopefully) the paragraph the user intended originally.""" + L=filter(truth,map(_lineClean, split(text, '\n'))) + return join(L, joiner) + +def setXPos(tx,dx): + if dx>1e-6 or dx<-1e-6: + tx.setXPos(dx) + +def _leftDrawParaLine( tx, offset, extraspace, words, last=0): + setXPos(tx,offset) + tx._textOut(join(words),1) + setXPos(tx,-offset) + return offset + +def _centerDrawParaLine( tx, offset, extraspace, words, last=0): + m = offset + 0.5 * extraspace + setXPos(tx,m) + tx._textOut(join(words),1) + setXPos(tx,-m) + return m + +def _rightDrawParaLine( tx, offset, extraspace, words, last=0): + m = offset + extraspace + setXPos(tx,m) + tx._textOut(join(words),1) + setXPos(tx,-m) + return m + +def _justifyDrawParaLine( tx, offset, extraspace, words, last=0): + setXPos(tx,offset) + text = join(words) + if last: + #last one, left align + tx._textOut(text,1) + else: + nSpaces = len(words)-1 + if nSpaces: + tx.setWordSpace(extraspace / float(nSpaces)) + tx._textOut(text,1) + tx.setWordSpace(0) + else: + tx._textOut(text,1) + setXPos(tx,-offset) + return offset + +def _putFragLine(tx,words): + cur_x = 0 + xtraState = tx.XtraState + for f in words: + if hasattr(f,'cbDefn'): + func = getattr(tx._canvas,f.cbDefn.name,None) + if not func: + raise AttributeError, "Missing %s callback attribute '%s'" % (f.cbDefn.kind,f.cbDefn.name) + func(tx._canvas,f.cbDefn.kind,f.cbDefn.label) + if f is words[-1]: tx._textOut('',1) + else: + if (tx._fontname,tx._fontsize)!=(f.fontName,f.fontSize): + tx._setFont(f.fontName, f.fontSize) + if xtraState.textColor!=f.textColor: + xtraState.textColor = f.textColor + tx.setFillColor(f.textColor) + if xtraState.rise!=f.rise: + xtraState.rise=f.rise + tx.setRise(f.rise) + tx._textOut(f.text,f is words[-1]) # cheap textOut + txtlen = tx._canvas.stringWidth(f.text, tx._fontname, tx._fontsize) + if not xtraState.underline and f.underline: + xtraState.underline = 1 + xtraState.underline_x = cur_x + xtraState.underlineColor = f.textColor + elif xtraState.underline: + if not f.underline: + xtraState.underline = 0 + spacelen = tx._canvas.stringWidth(' ', tx._fontname, tx._fontsize) + xtraState.underlines.append( (xtraState.underline_x, cur_x-spacelen, xtraState.underlineColor) ) + xtraState.underlineColor = None + elif xtraState.textColor!=xtraState.underlineColor: + xtraState.underlines.append( (xtraState.underline_x, cur_x, xtraState.underlineColor) ) + xtraState.underlineColor = xtraState.textColor + xtraState.underline_x = cur_x + if not xtraState.link and f.link: + xtraState.link = f.link + xtraState.link_x = cur_x + elif xtraState.link and f.link is not xtraState.link: + spacelen = tx._canvas.stringWidth(' ', tx._fontname, tx._fontsize) + xtraState.links.append( (xtraState.link_x, cur_x-spacelen, xtraState.link) ) + xtraState.link = None + cur_x += txtlen + if xtraState.underline: + xtraState.underlines.append( (xtraState.underline_x, cur_x, xtraState.underlineColor) ) + if xtraState.link: + xtraState.links.append( (xtraState.link_x, cur_x, xtraState.link) ) + +def _leftDrawParaLineX( tx, offset, line, last=0): + setXPos(tx,offset) + _putFragLine(tx, line.words) + setXPos(tx,-offset) + return offset + +def _centerDrawParaLineX( tx, offset, line, last=0): + m = offset+0.5*line.extraSpace + setXPos(tx,m) + _putFragLine(tx, line.words) + setXPos(tx,-m) + return m + +def _rightDrawParaLineX( tx, offset, line, last=0): + m = offset+line.extraSpace + setXPos(tx,m) + _putFragLine(tx, line.words) + setXPos(tx,-m) + return m + +def _justifyDrawParaLineX( tx, offset, line, last=0): + setXPos(tx,offset) + if last: + #last one, left align + _putFragLine(tx, line.words) + else: + nSpaces = line.wordCount - 1 + if nSpaces: + tx.setWordSpace(line.extraSpace / float(nSpaces)) + _putFragLine(tx, line.words) + tx.setWordSpace(0) + else: + _putFragLine(tx, line.words) + setXPos(tx,-offset) + return offset + +try: + from _rl_accel import _sameFrag +except ImportError: + try: + from reportlab.lib._rl_accel import _sameFrag + except ImportError: + def _sameFrag(f,g): + 'returns 1 if two ParaFrags map out the same' + if hasattr(f,'cbDefn') or hasattr(g,'cbDefn'): return 0 + for a in ('fontName', 'fontSize', 'textColor', 'rise', 'underline', 'link'): + if getattr(f,a)!=getattr(g,a): return 0 + return 1 + +def _getFragWords(frags): + ''' given a Parafrag list return a list of fragwords + [[size, (f00,w00), ..., (f0n,w0n)],....,[size, (fm0,wm0), ..., (f0n,wmn)]] + each pair f,w represents a style and some string + each sublist represents a word + ''' + R = [] + W = [] + n = 0 + for f in frags: + text = f.text + #del f.text # we can't do this until we sort out splitting + # of paragraphs + if text!='': + S = split(text) + if S==[]: S = [''] + if W!=[] and text[0] in whitespace: + W.insert(0,n) + R.append(W) + W = [] + n = 0 + + for w in S[:-1]: + W.append((f,w)) + n += stringWidth(w, f.fontName, f.fontSize) + W.insert(0,n) + R.append(W) + W = [] + n = 0 + + w = S[-1] + W.append((f,w)) + n += stringWidth(w, f.fontName, f.fontSize) + if text[-1] in whitespace: + W.insert(0,n) + R.append(W) + W = [] + n = 0 + elif hasattr(f,'cbDefn'): + W.append((f,'')) + + if W!=[]: + W.insert(0,n) + R.append(W) + + return R + +def _split_blParaSimple(blPara,start,stop): + f = blPara.clone() + for a in ('lines', 'kind', 'text'): + if hasattr(f,a): delattr(f,a) + + f.words = [] + for l in blPara.lines[start:stop]: + for w in l[1]: + f.words.append(w) + return [f] + +def _split_blParaHard(blPara,start,stop): + f = [] + lines = blPara.lines[start:stop] + for l in lines: + for w in l.words: + f.append(w) + if l is not lines[-1]: + i = len(f)-1 + while hasattr(f[i],'cbDefn'): i = i-1 + g = f[i] + if g.text and g.text[-1]!=' ': g.text += ' ' + return f + +def _drawBullet(canvas, offset, cur_y, bulletText, style): + '''draw a bullet text could be a simple string or a frag list''' + tx2 = canvas.beginText(style.bulletIndent, cur_y) + tx2.setFont(style.bulletFontName, style.bulletFontSize) + tx2.setFillColor(hasattr(style,'bulletColor') and style.bulletColor or style.textColor) + if type(bulletText) is StringType: + tx2.textOut(bulletText) + else: + for f in bulletText: + tx2.setFont(f.fontName, f.fontSize) + tx2.setFillColor(f.textColor) + tx2.textOut(f.text) + + canvas.drawText(tx2) + #AR making definition lists a bit less ugly + #bulletEnd = tx2.getX() + bulletEnd = tx2.getX() + style.bulletFontSize * 0.6 + offset = max(offset,bulletEnd - style.leftIndent) + return offset + +def _handleBulletWidth(bulletText,style,maxWidths): + '''work out bullet width and adjust maxWidths[0] if neccessary + ''' + if bulletText <> None: + if type(bulletText) is StringType: + bulletWidth = stringWidth( bulletText, style.bulletFontName, style.bulletFontSize) + else: + #it's a list of fragments + bulletWidth = 0 + for f in bulletText: + bulletWidth = bulletWidth + stringWidth(f.text, f.fontName, f.fontSize) + bulletRight = style.bulletIndent + bulletWidth + 0.6 * style.bulletFontSize + indent = style.leftIndent+style.firstLineIndent + if bulletRight > indent: + #..then it overruns, and we have less space available on line 1 + maxWidths[0] = maxWidths[0] - (bulletRight - indent) + +def splitLines0(frags,widths): + ''' + given a list of ParaFrags we return a list of ParaLines + + each ParaLine has + 1) ExtraSpace + 2) blankCount + 3) [textDefns....] + each text definition is a (ParaFrag, start, limit) triplet + ''' + #initialise the algorithm + lines = [] + lineNum = 0 + maxW = widths[lineNum] + i = -1 + l = len(frags) + lim = start = 0 + while 1: + #find a non whitespace character + while imaxW and line!=[]: + cLen = cLen-w + #this is the end of the line + while g.text[lim]==' ': + lim = lim - 1 + nSpaces = nSpaces-1 + break + if j<0: j = lim + if g[0] is f: g[2] = j #extend + else: + g = (f,start,j) + line.append(g) + if j==lim: + i += 1 + +def _do_under_line(i, t_off, tx): + y = tx.XtraState.cur_y - i*tx.XtraState.style.leading - tx.XtraState.f.fontSize/8.0 # 8.0 factor copied from para.py + text = join(tx.XtraState.lines[i][1]) + textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize) + tx._canvas.line(t_off, y, t_off+textlen, y) + +def _do_under(i, t_off, tx): + xtraState = tx.XtraState + y = xtraState.cur_y - i*xtraState.style.leading - xtraState.f.fontSize/8.0 # 8.0 factor copied from para.py + ulc = None + for x1,x2,c in xtraState.underlines: + if c!=ulc: + tx._canvas.setStrokeColor(c) + ulc = c + tx._canvas.line(t_off+x1, y, t_off+x2, y) + xtraState.underlines = [] + xtraState.underline=0 + xtraState.underlineColor=None + +_scheme_re = re.compile('^[a-zA-Z][-+a-zA-Z0-9]+$') +def _doLink(tx,link,rect): + if type(link) is unicode: + link = unicode.encode('utf8') + parts = link.split(':',1) + scheme = len(parts)==2 and parts[0].lower() or '' + if _scheme_re.match(scheme) and scheme!='document': + kind=scheme.lower()=='pdf' and 'GoToR' or 'URI' + if kind=='GoToR': link = parts[1] + tx._canvas.linkURL(link, rect, relative=1, kind=kind) + else: + tx._canvas.linkRect("", scheme!='document' and link or parts[1], rect, relative=1) + +def _do_link_line(i, t_off, tx): + xs = tx.XtraState + leading = xs.style.leading + y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py + text = join(xs.lines[i][1]) + textlen = tx._canvas.stringWidth(text, tx._fontname, tx._fontsize) + _doLink(tx, xs.link, (t_off, y, t_off+textlen, y+leading)) + +def _do_link(i, t_off, tx): + xs = tx.XtraState + leading = xs.style.leading + y = xs.cur_y - i*leading - xs.f.fontSize/8.0 # 8.0 factor copied from para.py + for x1,x2,link in xs.links: + tx._canvas.line(t_off+x1, y, t_off+x2, y) + _doLink(tx, link, (t_off+x1, y, t_off+x2, y+leading)) + xs.links = [] + xs.link=None + +class Paragraph(Flowable): + """ Paragraph(text, style, bulletText=None, caseSensitive=1) + text a string of stuff to go into the paragraph. + style is a style definition as in reportlab.lib.styles. + bulletText is an optional bullet defintion. + caseSensitive set this to 0 if you want the markup tags and their attributes to be case-insensitive. + + This class is a flowable that can format a block of text + into a paragraph with a given style. + + The paragraph Text can contain XML-like markup including the tags: + ... - bold + ... - italics + ... - underline + ... - superscript + ... - subscript + + + + The whole may be surrounded by tags + + It will also be able to handle any MathML specified Greek characters. + """ + def __init__(self, text, style, bulletText = None, frags=None, caseSensitive=1, encoding='utf8'): + self.caseSensitive = caseSensitive + self.encoding = encoding + self._setup(text, style, bulletText, frags, cleanBlockQuotedText) + + def __repr__(self): + import string + n = self.__class__.__name__ + L = [n+"("] + keys = self.__dict__.keys() + for k in keys: + v = getattr(self, k) + rk = repr(k) + rv = repr(v) + rk = " "+string.replace(rk, "\n", "\n ") + rv = " "+string.replace(rv, "\n", "\n ") + L.append(rk) + L.append(rv) + L.append(") #"+n) + return string.join(L, "\n") + + def _setup(self, text, style, bulletText, frags, cleaner): + if frags is None: + text = cleaner(text) + _parser.caseSensitive = self.caseSensitive + style, frags, bulletTextFrags = _parser.parse(text,style) + if frags is None: + raise "xml parser error (%s) in paragraph beginning\n'%s'"\ + % (_parser.errors[0],text[:min(30,len(text))]) + if bulletTextFrags: bulletText = bulletTextFrags + + #AR hack + self.text = text + self.frags = frags + self.style = style + self.bulletText = bulletText + self.debug = 0 #turn this on to see a pretty one with all the margins etc. + + def wrap(self, availWidth, availHeight): + # work out widths array for breaking + self.width = availWidth + leftIndent = self.style.leftIndent + first_line_width = availWidth - (leftIndent+self.style.firstLineIndent) - self.style.rightIndent + later_widths = availWidth - leftIndent - self.style.rightIndent + + if self.style.wordWrap == 'CJK': + #use Asian text wrap algorithm to break characters + self.blPara = self.breakLinesCJK([first_line_width, later_widths]) + else: + self.blPara = self.breakLines([first_line_width, later_widths]) + self.height = len(self.blPara.lines) * self.style.leading + return (self.width, self.height) + + def minWidth(self): + 'Attempt to determine a minimum sensible width' + frags = self.frags + nFrags= len(frags) + if not nFrags: return 0 + if nFrags==1: + f = frags[0] + fS = f.fontSize + fN = f.fontName + words = hasattr(f,'text') and split(f.text, ' ') or f.words + func = lambda w, fS=fS, fN=fN: stringWidth(w,fN,fS) + else: + words = _getFragWords(frags) + func = lambda x: x[0] + return max(map(func,words)) + + def _get_split_blParaFunc(self): + return self.blPara.kind==0 and _split_blParaSimple or _split_blParaHard + + def split(self,availWidth, availHeight): + if len(self.frags)<=0: return [] + + #the split information is all inside self.blPara + if not hasattr(self,'blPara'): + self.wrap(availWidth,availHeight) + blPara = self.blPara + style = self.style + leading = style.leading + lines = blPara.lines + n = len(lines) + s = int(availHeight/leading) + if s<=1: + del self.blPara + return [] + if n<=s: return [self] + func = self._get_split_blParaFunc() + + P1=self.__class__(None,style,bulletText=self.bulletText,frags=func(blPara,0,s)) + #this is a major hack + P1.blPara = ParaLines(kind=1,lines=blPara.lines[0:s],aH=availHeight,aW=availWidth) + P1._JustifyLast = 1 + P1._splitpara = 1 + P1.height = s*leading + P1.width = availWidth + if style.firstLineIndent != 0: + style = deepcopy(style) + style.firstLineIndent = 0 + P2=self.__class__(None,style,bulletText=None,frags=func(blPara,s,n)) + return [P1,P2] + + def draw(self): + #call another method for historical reasons. Besides, I + #suspect I will be playing with alternate drawing routines + #so not doing it here makes it easier to switch. + self.drawPara(self.debug) + + def breakLines(self, width): + """ + Returns a broken line structure. There are two cases + + A) For the simple case of a single formatting input fragment the output is + A fragment specifier with + kind = 0 + fontName, fontSize, leading, textColor + lines= A list of lines + Each line has two items. + 1) unused width in points + 2) word list + + B) When there is more than one input formatting fragment the output is + A fragment specifier with + kind = 1 + lines= A list of fragments each having fields + extraspace (needed for justified) + fontSize + words=word list + each word is itself a fragment with + various settings + + This structure can be used to easily draw paragraphs with the various alignments. + You can supply either a single width or a list of widths; the latter will have its + last item repeated until necessary. A 2-element list is useful when there is a + different first line indent; a longer list could be created to facilitate custom wraps + around irregular objects.""" + + if type(width) <> ListType: maxWidths = [width] + else: maxWidths = width + lines = [] + lineno = 0 + style = self.style + fFontSize = float(style.fontSize) + + #for bullets, work out width and ensure we wrap the right amount onto line one + _handleBulletWidth(self.bulletText,style,maxWidths) + + maxWidth = maxWidths[0] + + self.height = 0 + frags = self.frags + nFrags= len(frags) + if nFrags==1: + f = frags[0] + fontSize = f.fontSize + fontName = f.fontName + words = hasattr(f,'text') and split(f.text, ' ') or f.words + spaceWidth = stringWidth(' ', fontName, fontSize, self.encoding) + cLine = [] + currentWidth = - spaceWidth # hack to get around extra space for word 1 + for word in words: + #this underscores my feeling that Unicode throughout would be easier! + wordWidth = stringWidth(word, fontName, fontSize, self.encoding) + newWidth = currentWidth + spaceWidth + wordWidth + if newWidth <= maxWidth or len(cLine) == 0: + # fit one more on this line + cLine.append(word) + currentWidth = newWidth + else: + if currentWidth > self.width: self.width = currentWidth + #end of line + lines.append((maxWidth - currentWidth, cLine)) + cLine = [word] + currentWidth = wordWidth + lineno += 1 + try: + maxWidth = maxWidths[lineno] + except IndexError: + maxWidth = maxWidths[-1] # use the last one + + #deal with any leftovers on the final line + if cLine!=[]: + if currentWidth>self.width: self.width = currentWidth + lines.append((maxWidth - currentWidth, cLine)) + + return f.clone(kind=0, lines=lines) + elif nFrags<=0: + return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName, + textColor=style.textColor, lines=[]) + else: + if hasattr(self,'blPara') and getattr(self,'_splitpara',0): + #NB this is an utter hack that awaits the proper information + #preserving splitting algorithm + return self.blPara + n = 0 + nSp = 0 + for w in _getFragWords(frags): + spaceWidth = stringWidth(' ',w[-1][0].fontName, w[-1][0].fontSize) + + if n==0: + currentWidth = -spaceWidth # hack to get around extra space for word 1 + words = [] + maxSize = 0 + + wordWidth = w[0] + f = w[1][0] + if wordWidth>0: + newWidth = currentWidth + spaceWidth + wordWidth + else: + newWidth = currentWidth + if newWidth<=maxWidth or n==0: + # fit one more on this line + n += 1 + maxSize = max(maxSize,f.fontSize) + nText = w[1][1] + if words==[]: + g = f.clone() + words = [g] + g.text = nText + elif not _sameFrag(g,f): + if currentWidth>0 and ((nText!='' and nText[0]!=' ') or hasattr(f,'cbDefn')): + if hasattr(g,'cbDefn'): + i = len(words)-1 + while hasattr(words[i],'cbDefn'): i = i-1 + words[i].text += ' ' + else: + g.text += ' ' + nSp += 1 + g = f.clone() + words.append(g) + g.text = nText + else: + if nText!='' and nText[0]!=' ': + g.text += ' ' + nText + + for i in w[2:]: + g = i[0].clone() + g.text=i[1] + words.append(g) + maxSize = max(maxSize,g.fontSize) + + currentWidth = newWidth + else: + if currentWidth>self.width: self.width = currentWidth + #end of line + lines.append(FragLine(extraSpace=(maxWidth - currentWidth),wordCount=n, + words=words, fontSize=maxSize)) + + #start new line + lineno += 1 + try: + maxWidth = maxWidths[lineno] + except IndexError: + maxWidth = maxWidths[-1] # use the last one + currentWidth = wordWidth + n = 1 + maxSize = f.fontSize + g = f.clone() + words = [g] + g.text = w[1][1] + + for i in w[2:]: + g = i[0].clone() + g.text=i[1] + words.append(g) + maxSize = max(maxSize,g.fontSize) + + #deal with any leftovers on the final line + if words<>[]: + if currentWidth>self.width: self.width = currentWidth + lines.append(ParaLines(extraSpace=(maxWidth - currentWidth),wordCount=n, + words=words, fontSize=maxSize)) + return ParaLines(kind=1, lines=lines) + + return lines + + + def breakLinesCJK(self, width): + """Initially, the dumbest possible wrapping algorithm. + Cannot handle font variations.""" + + style = self.style + #for now we only handle one fragment. Need to generalize this quickly. + if len(self.frags) > 1: + raise ValueError('CJK Wordwrap can only handle one fragment per paragraph for now') + elif len(self.frags) == 0: + return ParaLines(kind=0, fontSize=style.fontSize, fontName=style.fontName, + textColor=style.textColor, lines=[]) + f = self.frags[0] + if 1 and hasattr(self,'blPara') and getattr(self,'_splitpara',0): + #NB this is an utter hack that awaits the proper information + #preserving splitting algorithm + return f.clone(kind=0, lines=self.blPara.lines) + if type(width)!=ListType: maxWidths = [width] + else: maxWidths = width + lines = [] + lineno = 0 + fFontSize = float(style.fontSize) + + #for bullets, work out width and ensure we wrap the right amount onto line one + _handleBulletWidth(self.bulletText, style, maxWidths) + + maxWidth = maxWidths[0] + + self.height = 0 + + + f = self.frags[0] + + if hasattr(f,'text'): + text = f.text + else: + text = ''.join(getattr(f,'words',[])) + + from reportlab.lib.textsplit import wordSplit + lines = wordSplit(text, maxWidths[0], f.fontName, f.fontSize) + #the paragraph drawing routine assumes multiple frags per line, so we need an + #extra list like this + # [space, [text]] + # + wrappedLines = [(sp, [line]) for (sp, line) in lines] + return f.clone(kind=0, lines=wrappedLines) + + + def beginText(self, x, y): + return self.canv.beginText(x, y) + + def drawPara(self,debug=0): + """Draws a paragraph according to the given style. + Returns the final y position at the bottom. Not safe for + paragraphs without spaces e.g. Japanese; wrapping + algorithm will go infinite.""" + + #stash the key facts locally for speed + canvas = self.canv + style = self.style + blPara = self.blPara + lines = blPara.lines + + #work out the origin for line 1 + leftIndent = style.leftIndent + cur_x = leftIndent + + #if has a background, draw it + if style.backColor: + canvas.saveState() + canvas.setFillColor(style.backColor) + canvas.rect(leftIndent, + 0, + self.width - (leftIndent+style.rightIndent), + self.height, + fill=1, + stroke=0) + canvas.restoreState() + + if debug: + # This boxes and shades stuff to show how the paragraph + # uses its space. Useful for self-documentation so + # the debug code stays! + # box the lot + canvas.rect(0, 0, self.width, self.height) + #left and right margins + canvas.saveState() + canvas.setFillColor(Color(0.9,0.9,0.9)) + canvas.rect(0, 0, leftIndent, self.height) + canvas.rect(self.width - style.rightIndent, 0, style.rightIndent, self.height) + # shade above and below + canvas.setFillColor(Color(1.0,1.0,0.0)) + canvas.restoreState() + #self.drawLine(x + leftIndent, y, x + leftIndent, cur_y) + + + nLines = len(lines) + bulletText = self.bulletText + if nLines > 0: + _offsets = getattr(self,'_offsets',[0]) + _offsets += (nLines-len(_offsets))*[_offsets[-1]] + canvas.saveState() + #canvas.addLiteral('%% %s.drawPara' % _className(self)) + alignment = style.alignment + offset = style.firstLineIndent+_offsets[0] + lim = nLines-1 + noJustifyLast = not (hasattr(self,'_JustifyLast') and self._JustifyLast) + + if blPara.kind==0: + if alignment == TA_LEFT: + dpl = _leftDrawParaLine + elif alignment == TA_CENTER: + dpl = _centerDrawParaLine + elif self.style.alignment == TA_RIGHT: + dpl = _rightDrawParaLine + elif self.style.alignment == TA_JUSTIFY: + dpl = _justifyDrawParaLine + f = blPara + cur_y = self.height - f.fontSize + if bulletText <> None: + offset = _drawBullet(canvas,offset,cur_y,bulletText,style) + + #set up the font etc. + canvas.setFillColor(f.textColor) + + tx = self.beginText(cur_x, cur_y) + + #now the font for the rest of the paragraph + tx.setFont(f.fontName, f.fontSize, style.leading) + t_off = dpl( tx, offset, lines[0][0], lines[0][1], noJustifyLast and nLines==1) + if f.underline or f.link: + xs = tx.XtraState=ABag() + xs.cur_y = cur_y + xs.f = f + xs.style = style + xs.lines = lines + xs.underlines=[] + xs.underlineColor=None + xs.links=[] + xs.link=f.link + canvas.setStrokeColor(f.textColor) + if f.underline: _do_under_line(0, t_off+leftIndent, tx) + if f.link: _do_link_line(0, t_off+leftIndent, tx) + + #now the middle of the paragraph, aligned with the left margin which is our origin. + for i in xrange(1, nLines): + t_off = dpl( tx, _offsets[i], lines[i][0], lines[i][1], noJustifyLast and i==lim) + if f.underline: _do_under_line(i, t_off+leftIndent, tx) + if f.link: _do_link_line(i, t_off+leftIndent, tx) + else: + for i in xrange(1, nLines): + dpl( tx, _offsets[i], lines[i][0], lines[i][1], noJustifyLast and i==lim) + else: + f = lines[0] + cur_y = self.height - f.fontSize + # default? + dpl = _leftDrawParaLineX + if bulletText <> None: + offset = _drawBullet(canvas,offset,cur_y,bulletText,style) + if alignment == TA_LEFT: + dpl = _leftDrawParaLineX + elif alignment == TA_CENTER: + dpl = _centerDrawParaLineX + elif self.style.alignment == TA_RIGHT: + dpl = _rightDrawParaLineX + elif self.style.alignment == TA_JUSTIFY: + dpl = _justifyDrawParaLineX + else: + raise ValueError, "bad align %s" % repr(alignment) + + #set up the font etc. + tx = self.beginText(cur_x, cur_y) + xs = tx.XtraState=ABag() + xs.textColor=None + xs.rise=0 + xs.underline=0 + xs.underlines=[] + xs.underlineColor=None + xs.links=[] + xs.link=None + tx.setLeading(style.leading) + xs.cur_y = cur_y + xs.f = f + xs.style = style + #f = lines[0].words[0] + #tx._setFont(f.fontName, f.fontSize) + + + tx._fontname,tx._fontsize = None, None + t_off = dpl( tx, offset, lines[0], noJustifyLast and nLines==1) + _do_under(0, t_off+leftIndent, tx) + _do_link(0, t_off+leftIndent, tx) + + #now the middle of the paragraph, aligned with the left margin which is our origin. + for i in range(1, nLines): + f = lines[i] + t_off = dpl( tx, _offsets[i], f, noJustifyLast and i==lim) + _do_under(i, t_off+leftIndent, tx) + _do_link(i, t_off+leftIndent, tx) + + canvas.drawText(tx) + canvas.restoreState() + + def getPlainText(self,identify=None): + """Convenience function for templates which want access + to the raw text, without XML tags. """ + frags = getattr(self,'frags',None) + if frags: + plains = [] + for frag in frags: + if hasattr(frag, 'text'): + plains.append(frag.text) + return join(plains, '') + elif identify: + text = getattr(self,'text',None) + if text is None: text = repr(self) + return text + else: + return '' + + def getActualLineWidths0(self): + """Convenience function; tells you how wide each line + actually is. For justified styles, this will be + the same as the wrap width; for others it might be + useful for seeing if paragraphs will fit in spaces.""" + assert hasattr(self, 'width'), "Cannot call this method before wrap()" + if self.blPara.kind: + func = lambda frag, w=self.width: w - frag.extraSpace + else: + func = lambda frag, w=self.width: w - frag[0] + return map(func,self.blPara.lines) + +if __name__=='__main__': #NORUNTESTS + def dumpParagraphLines(P): + print 'dumpParagraphLines()' % id(P) + lines = P.blPara.lines + n =len(lines) + for l in range(n): + line = lines[l] + if hasattr(line,'words'): + words = line.words + else: + words = line[1] + nwords = len(words) + print 'line%d: %d(%s)\n ' % (l,nwords,str(getattr(line,'wordCount','Unknown'))), + for w in range(nwords): + print "%d:'%s'"%(w,getattr(words[w],'text',words[w])), + print + + def dumpParagraphFrags(P): + print 'dumpParagraphFrags() minWidth() = %.2f' % (id(P), P.minWidth()) + frags = P.frags + n =len(frags) + for l in range(n): + print "frag%d: '%s'" % (l, frags[l].text) + + l = 0 + cum = 0 + for W in _getFragWords(frags): + cum += W[0] + print "fragword%d: cum=%3d size=%d" % (l, cum, W[0]), + for w in W[1:]: + print "'%s'" % w[1], + print + l += 1 + + + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + import sys + TESTS = sys.argv[1:] + if TESTS==[]: TESTS=['4'] + def flagged(i,TESTS=TESTS): + return 'all' in TESTS or '*' in TESTS or str(i) in TESTS + + styleSheet = getSampleStyleSheet() + B = styleSheet['BodyText'] + style = ParagraphStyle("discussiontext", parent=B) + style.fontName= 'Helvetica' + if flagged(1): + text='''The CMYK or subtractive method follows the way a printer +mixes three pigments (cyan, magenta, and yellow) to form colors. +Because mixing chemicals is more difficult than combining light there +is a fourth parameter for darkness. For example a chemical +combination of the CMY pigments generally never makes a perfect +black -- instead producing a muddy color -- so, to get black printers +don't use the CMY pigments but use a direct black ink. Because +CMYK maps more directly to the way printer hardware works it may +be the case that &| & | colors specified in CMYK will provide better fidelity +and better control when printed. +''' + P=Paragraph(text,style) + dumpParagraphFrags(P) + aW, aH = 456.0, 42.8 + w,h = P.wrap(aW, aH) + dumpParagraphLines(P) + S = P.split(aW,aH) + for s in S: + s.wrap(aW,aH) + dumpParagraphLines(s) + aH = 500 + + if flagged(2): + P=Paragraph("""Price*""", styleSheet['Normal']) + dumpParagraphFrags(P) + w,h = P.wrap(24, 200) + dumpParagraphLines(P) + + if flagged(3): + text = """Dieses Kapitel bietet eine schnelle Programme :: starten + +Eingabeaufforderung :: (>>>) + +>>> (Eingabeaufforderung) + +Einführung in Python Python :: Einführung +. +Das Ziel ist, die grundlegenden Eigenschaften von Python darzustellen, ohne +sich zu sehr in speziellen Regeln oder Details zu verstricken. Dazu behandelt +dieses Kapitel kurz die wesentlichen Konzepte wie Variablen, Ausdrücke, +Kontrollfluss, Funktionen sowie Ein- und Ausgabe. Es erhebt nicht den Anspruch, +umfassend zu sein.""" + P=Paragraph(text, styleSheet['Code']) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) + + if flagged(4): + text='''Die eingebaute Funktion range(i, j [, stride]) erzeugt eine Liste von Ganzzahlen und füllt sie mit Werten k, für die gilt: i <= k < j. Man kann auch eine optionale Schrittweite angeben. Die eingebaute Funktion xrange() erfüllt einen ähnlichen Zweck, gibt aber eine unveränderliche Sequenz vom Typ XRangeType zurück. Anstatt alle Werte in der Liste abzuspeichern, berechnet diese Liste ihre Werte, wann immer sie angefordert werden. Das ist sehr viel speicherschonender, wenn mit sehr langen Listen von Ganzzahlen gearbeitet wird. XRangeType kennt eine einzige Methode, s.tolist(), die seine Werte in eine Liste umwandelt.''' + aW = 420 + aH = 64.4 + P=Paragraph(text, B) + dumpParagraphFrags(P) + w,h = P.wrap(aW,aH) + print 'After initial wrap',w,h + dumpParagraphLines(P) + S = P.split(aW,aH) + dumpParagraphFrags(S[0]) + w0,h0 = S[0].wrap(aW,aH) + print 'After split wrap',w0,h0 + dumpParagraphLines(S[0]) + + if flagged(5): + text = ' %s & %s < >]]>' % (chr(163),chr(163)) + P=Paragraph(text, styleSheet['Code']) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) + + if flagged(6): + for text in ['''Here comes Helvetica 14 with strong emphasis.''', + '''Here comes Helvetica 14 with strong emphasis.''', + '''Here comes Courier 3cm and normal again.''', + ]: + P=Paragraph(text, styleSheet['Normal'], caseSensitive=0) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) + + if flagged(7): + text = """Generated by:Dilbert""" + P=Paragraph(text, styleSheet['Code']) + dumpParagraphFrags(P) + w,h = P.wrap(6*72, 9.7*72) + dumpParagraphLines(P) diff --git a/bin/reportlab/platypus/paraparser.py b/bin/reportlab/platypus/paraparser.py new file mode 100644 index 00000000000..bb6936683b2 --- /dev/null +++ b/bin/reportlab/platypus/paraparser.py @@ -0,0 +1,898 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/paraparser.py +__version__=''' $Id: paraparser.py 2853 2006-05-10 12:56:39Z rgbecker $ ''' +import string +import re +from types import TupleType, UnicodeType, StringType +import sys +import os +import copy + +import reportlab.lib.sequencer +from reportlab.lib.abag import ABag + +from reportlab.lib import xmllib + +from reportlab.lib.colors import toColor, white, black, red, Color +from reportlab.lib.fonts import tt2ps, ps2tt +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.units import inch,mm,cm,pica +_re_para = re.compile(r'^\s*<\s*para(?:\s+|>|/>)') + +sizeDelta = 2 # amount to reduce font size by for super and sub script +subFraction = 0.5 # fraction of font size that a sub script should be lowered +superFraction = 0.5 # fraction of font size that a super script should be raised + +def _num(s, unit=1): + """Convert a string like '10cm' to an int or float (in points). + The default unit is point, but optionally you can use other + default units like mm. + """ + if s[-2:]=='cm': + unit=cm + s = s[:-2] + if s[-2:]=='in': + unit=inch + s = s[:-2] + if s[-2:]=='pt': + unit=1 + s = s[:-2] + if s[-1:]=='i': + unit=inch + s = s[:-1] + if s[-2:]=='mm': + unit=mm + s = s[:-2] + if s[-4:]=='pica': + unit=pica + s = s[:-4] + if s[0] in ['+','-']: + try: + return ('relative',int(s)*unit) + except ValueError: + return ('relative',float(s)*unit) + else: + try: + return int(s)*unit + except ValueError: + return float(s)*unit + +def _align(s): + s = string.lower(s) + if s=='left': return TA_LEFT + elif s=='right': return TA_RIGHT + elif s=='justify': return TA_JUSTIFY + elif s in ('centre','center'): return TA_CENTER + else: raise ValueError + +_paraAttrMap = {'font': ('fontName', None), + 'face': ('fontName', None), + 'fontsize': ('fontSize', _num), + 'size': ('fontSize', _num), + 'leading': ('leading', _num), + 'lindent': ('leftIndent', _num), + 'rindent': ('rightIndent', _num), + 'findent': ('firstLineIndent', _num), + 'align': ('alignment', _align), + 'spaceb': ('spaceBefore', _num), + 'spacea': ('spaceAfter', _num), + 'bfont': ('bulletFontName', None), + 'bfontsize': ('bulletFontSize',_num), + 'bindent': ('bulletIndent',_num), + 'bcolor': ('bulletColor',toColor), + 'color':('textColor',toColor), + 'backcolor':('backColor',toColor), + 'bgcolor':('backColor',toColor), + 'bg':('backColor',toColor), + 'fg': ('textColor',toColor), + } + +_bulletAttrMap = { + 'font': ('bulletFontName', None), + 'face': ('bulletFontName', None), + 'size': ('bulletFontSize',_num), + 'fontsize': ('bulletFontSize',_num), + 'indent': ('bulletIndent',_num), + 'color': ('bulletColor',toColor), + 'fg': ('bulletColor',toColor), + } + +#things which are valid font attributes +_fontAttrMap = {'size': ('fontSize', _num), + 'face': ('fontName', None), + 'name': ('fontName', None), + 'fg': ('textColor', toColor), + 'color':('textColor', toColor), + 'backcolor':('backColor',toColor), + 'bgcolor':('backColor',toColor), + } +#things which are valid font attributes +_linkAttrMap = {'size': ('fontSize', _num), + 'face': ('fontName', None), + 'name': ('fontName', None), + 'fg': ('textColor', toColor), + 'color':('textColor', toColor), + 'backcolor':('backColor',toColor), + 'bgcolor':('backColor',toColor), + 'dest': ('link', None), + 'destination': ('link', None), + 'target': ('link', None), + 'href': ('link', None), + } + +def _addAttributeNames(m): + K = m.keys() + for k in K: + n = m[k][0] + if not m.has_key(n): m[n] = m[k] + n = string.lower(n) + if not m.has_key(n): m[n] = m[k] + +_addAttributeNames(_paraAttrMap) +_addAttributeNames(_fontAttrMap) +_addAttributeNames(_bulletAttrMap) + +def _applyAttributes(obj, attr): + for k, v in attr.items(): + if type(v) is TupleType and v[0]=='relative': + #AR 20/5/2000 - remove 1.5.2-ism + #v = v[1]+getattr(obj,k,0) + if hasattr(obj, k): + v = v[1]+getattr(obj,k) + else: + v = v[1] + setattr(obj,k,v) + +#Named character entities intended to be supported from the special font +#with additions suggested by Christoph Zwerschke who also suggested the +#numeric entity names that follow. +greeks = { + 'pound': '\xc2\xa3', + 'nbsp': '\xc2\xa0', + 'alefsym': '\xe2\x84\xb5', + 'Alpha': '\xce\x91', + 'alpha': '\xce\xb1', + 'and': '\xe2\x88\xa7', + 'ang': '\xe2\x88\xa0', + 'asymp': '\xe2\x89\x88', + 'Beta': '\xce\x92', + 'beta': '\xce\xb2', + 'bull': '\xe2\x80\xa2', + 'cap': '\xe2\x88\xa9', + 'Chi': '\xce\xa7', + 'chi': '\xcf\x87', + 'clubs': '\xe2\x99\xa3', + 'cong': '\xe2\x89\x85', + 'cup': '\xe2\x88\xaa', + 'darr': '\xe2\x86\x93', + 'dArr': '\xe2\x87\x93', + 'delta': '\xce\xb4', + 'Delta': '\xe2\x88\x86', + 'diams': '\xe2\x99\xa6', + 'empty': '\xe2\x88\x85', + 'Epsilon': '\xce\x95', + 'epsilon': '\xce\xb5', + 'epsiv': '\xce\xb5', + 'equiv': '\xe2\x89\xa1', + 'Eta': '\xce\x97', + 'eta': '\xce\xb7', + 'euro': '\xe2\x82\xac', + 'exist': '\xe2\x88\x83', + 'forall': '\xe2\x88\x80', + 'frasl': '\xe2\x81\x84', + 'Gamma': '\xce\x93', + 'gamma': '\xce\xb3', + 'ge': '\xe2\x89\xa5', + 'harr': '\xe2\x86\x94', + 'hArr': '\xe2\x87\x94', + 'hearts': '\xe2\x99\xa5', + 'hellip': '\xe2\x80\xa6', + 'image': '\xe2\x84\x91', + 'infin': '\xe2\x88\x9e', + 'int': '\xe2\x88\xab', + 'Iota': '\xce\x99', + 'iota': '\xce\xb9', + 'isin': '\xe2\x88\x88', + 'Kappa': '\xce\x9a', + 'kappa': '\xce\xba', + 'Lambda': '\xce\x9b', + 'lambda': '\xce\xbb', + 'lang': '\xe2\x8c\xa9', + 'larr': '\xe2\x86\x90', + 'lArr': '\xe2\x87\x90', + 'lceil': '\xef\xa3\xae', + 'le': '\xe2\x89\xa4', + 'lfloor': '\xef\xa3\xb0', + 'lowast': '\xe2\x88\x97', + 'loz': '\xe2\x97\x8a', + 'minus': '\xe2\x88\x92', + 'mu': '\xc2\xb5', + 'Mu': '\xce\x9c', + 'nabla': '\xe2\x88\x87', + 'ne': '\xe2\x89\xa0', + 'ni': '\xe2\x88\x8b', + 'notin': '\xe2\x88\x89', + 'nsub': '\xe2\x8a\x84', + 'Nu': '\xce\x9d', + 'nu': '\xce\xbd', + 'oline': '\xef\xa3\xa5', + 'omega': '\xcf\x89', + 'Omega': '\xe2\x84\xa6', + 'Omicron': '\xce\x9f', + 'omicron': '\xce\xbf', + 'oplus': '\xe2\x8a\x95', + 'or': '\xe2\x88\xa8', + 'otimes': '\xe2\x8a\x97', + 'part': '\xe2\x88\x82', + 'perp': '\xe2\x8a\xa5', + 'Phi': '\xce\xa6', + 'phi': '\xcf\x95', + 'phis': '\xcf\x86', + 'Pi': '\xce\xa0', + 'pi': '\xcf\x80', + 'piv': '\xcf\x96', + 'prime': '\xe2\x80\xb2', + 'prod': '\xe2\x88\x8f', + 'prop': '\xe2\x88\x9d', + 'Psi': '\xce\xa8', + 'psi': '\xcf\x88', + 'radic': '\xe2\x88\x9a', + 'rang': '\xe2\x8c\xaa', + 'rarr': '\xe2\x86\x92', + 'rArr': '\xe2\x87\x92', + 'rceil': '\xef\xa3\xb9', + 'real': '\xe2\x84\x9c', + 'rfloor': '\xef\xa3\xbb', + 'Rho': '\xce\xa1', + 'rho': '\xcf\x81', + 'sdot': '\xe2\x8b\x85', + 'Sigma': '\xce\xa3', + 'sigma': '\xcf\x83', + 'sigmaf': '\xcf\x82', + 'sigmav': '\xcf\x82', + 'sim': '\xe2\x88\xbc', + 'spades': '\xe2\x99\xa0', + 'sub': '\xe2\x8a\x82', + 'sube': '\xe2\x8a\x86', + 'sum': '\xe2\x88\x91', + 'sup': '\xe2\x8a\x83', + 'supe': '\xe2\x8a\x87', + 'Tau': '\xce\xa4', + 'tau': '\xcf\x84', + 'there4': '\xe2\x88\xb4', + 'Theta': '\xce\x98', + 'theta': '\xce\xb8', + 'thetasym': '\xcf\x91', + 'thetav': '\xcf\x91', + 'trade': '\xef\xa3\xaa', + 'uarr': '\xe2\x86\x91', + 'uArr': '\xe2\x87\x91', + 'upsih': '\xcf\x92', + 'Upsilon': '\xce\xa5', + 'upsilon': '\xcf\x85', + 'weierp': '\xe2\x84\x98', + 'Xi': '\xce\x9e', + 'xi': '\xce\xbe', + 'Zeta': '\xce\x96', + 'zeta': '\xce\xb6', + } + +#------------------------------------------------------------------------ +class ParaFrag(ABag): + """class ParaFrag contains the intermediate representation of string + segments as they are being parsed by the XMLParser. + fontname, fontSize, rise, textColor, cbDefn + """ + + +_greek2Utf8=None +def _greekConvert(data): + global _greek2Utf8 + if not _greek2Utf8: + from reportlab.pdfbase.rl_codecs import RL_Codecs + import codecs + dm = decoding_map = codecs.make_identity_dict(xrange(32,256)) + for k in xrange(0,32): + dm[k] = None + dm.update(RL_Codecs._RL_Codecs__rl_codecs_data['symbol'][0]) + _greek2Utf8 = {} + for k,v in dm.iteritems(): + if not v: + u = '\0' + else: + u = unichr(v).encode('utf8') + _greek2Utf8[chr(k)] = u + return ''.join(map(_greek2Utf8.__getitem__,data)) + +#------------------------------------------------------------------ +# !!! NOTE !!! THIS TEXT IS NOW REPLICATED IN PARAGRAPH.PY !!! +# The ParaFormatter will be able to format the following +# tags: +# < /b > - bold +# < /i > - italics +# < u > < /u > - underline +# < super > < /super > - superscript +# < sup > < /sup > - superscript +# < sub > < /sub > - subscript +# +# < bullet > - bullet text (at head of para only) +# +# +# +# - +# +# The whole may be surrounded by tags +# +# It will also be able to handle any MathML specified Greek characters. +#------------------------------------------------------------------ +class ParaParser(xmllib.XMLParser): + + #---------------------------------------------------------- + # First we will define all of the xml tag handler functions. + # + # start_(attributes) + # end_() + # + # While parsing the xml ParaFormatter will call these + # functions to handle the string formatting tags. + # At the start of each tag the corresponding field will + # be set to 1 and at the end tag the corresponding field will + # be set to 0. Then when handle_data is called the options + # for that data will be aparent by the current settings. + #---------------------------------------------------------- + + def __getattr__( self, attrName ): + """This way we can handle the same way as (ignoring case).""" + if attrName!=attrName.lower() and attrName!="caseSensitive" and not self.caseSensitive and \ + (attrName.startswith("start_") or attrName.startswith("end_")): + return getattr(self,attrName.lower()) + raise AttributeError, attrName + + #### bold + def start_b( self, attributes ): + self._push(bold=1) + + def end_b( self ): + self._pop(bold=1) + + def start_strong( self, attributes ): + self._push(bold=1) + + def end_strong( self ): + self._pop(bold=1) + + #### italics + def start_i( self, attributes ): + self._push(italic=1) + + def end_i( self ): + self._pop(italic=1) + + def start_em( self, attributes ): + self._push(italic=1) + + def end_em( self ): + self._pop(italic=1) + + #### underline + def start_u( self, attributes ): + self._push(underline=1) + + def end_u( self ): + self._pop(underline=1) + + #### link + def start_link(self, attributes): + self._push(**self.getAttributes(attributes,_linkAttrMap)) + + def end_link(self): + frag = self._stack[-1] + del self._stack[-1] + assert frag.link!=None + + #### super script + def start_super( self, attributes ): + self._push(super=1) + + def end_super( self ): + self._pop(super=1) + + start_sup = start_super + end_sup = end_super + + #### sub script + def start_sub( self, attributes ): + self._push(sub=1) + + def end_sub( self ): + self._pop(sub=1) + + #### greek script + #### add symbol encoding + def handle_charref(self, name): + try: + if name[0]=='x': + n = int(name[1:],16) + else: + n = int(name) + except ValueError: + self.unknown_charref(name) + return + self.handle_data(unichr(n).encode('utf8')) + + def handle_entityref(self,name): + if greeks.has_key(name): + self.handle_data(greeks[name]) + else: + xmllib.XMLParser.handle_entityref(self,name) + + def syntax_error(self,lineno,message): + self._syntax_error(message) + + def _syntax_error(self,message): + if message[:10]=="attribute " and message[-17:]==" value not quoted": return + self.errors.append(message) + + def start_greek(self, attr): + self._push(greek=1) + + def end_greek(self): + self._pop(greek=1) + + def start_unichar(self, attr): + if attr.has_key('name'): + if attr.has_key('code'): + self._syntax_error(' invalid with both name and code attributes') + try: + v = unicodedata.lookup(attr['name']).encode('utf8') + except KeyError: + self._syntax_error(' invalid name attribute\n"%s"' % name) + v = '\0' + elif attr.has_key('code'): + try: + v = unichr(int(eval(attr['code']))).encode('utf8') + except: + self._syntax_error(' invalid code attribute %s' % attr['code']) + v = '\0' + else: + v = None + if attr: + self._syntax_error(' invalid attribute %s' % attr.keys()[0]) + + if v is not None: + self.handle_data(v) + self._push(_selfClosingTag='unichar') + + def end_unichar(self): + self._pop() + + def start_font(self,attr): + self._push(**self.getAttributes(attr,_fontAttrMap)) + + def end_font(self): + self._pop() + + def _initial_frag(self,attr,attrMap,bullet=0): + style = self._style + if attr!={}: + style = copy.deepcopy(style) + _applyAttributes(style,self.getAttributes(attr,attrMap)) + self._style = style + + # initialize semantic values + frag = ParaFrag() + frag.sub = 0 + frag.super = 0 + frag.rise = 0 + frag.underline = 0 + frag.greek = 0 + frag.link = None + if bullet: + frag.fontName, frag.bold, frag.italic = ps2tt(style.bulletFontName) + frag.fontSize = style.bulletFontSize + frag.textColor = hasattr(style,'bulletColor') and style.bulletColor or style.textColor + else: + frag.fontName, frag.bold, frag.italic = ps2tt(style.fontName) + frag.fontSize = style.fontSize + frag.textColor = style.textColor + return frag + + def start_para(self,attr): + self._stack = [self._initial_frag(attr,_paraAttrMap)] + + def end_para(self): + self._pop() + + def start_bullet(self,attr): + if hasattr(self,'bFragList'): + self._syntax_error('only one tag allowed') + self.bFragList = [] + frag = self._initial_frag(attr,_bulletAttrMap,1) + frag.isBullet = 1 + self._stack.append(frag) + + def end_bullet(self): + self._pop() + + #--------------------------------------------------------------- + def start_seqdefault(self, attr): + try: + default = attr['id'] + except KeyError: + default = None + self._seq.setDefaultCounter(default) + + def end_seqdefault(self): + pass + + def start_seqreset(self, attr): + try: + id = attr['id'] + except KeyError: + id = None + try: + base = int(attr['base']) + except: + base=0 + self._seq.reset(id, base) + + def end_seqreset(self): + pass + + def start_seqchain(self, attr): + try: + order = attr['order'] + except KeyError: + order = '' + order = order.split() + seq = self._seq + for p,c in zip(order[:-1],order[1:]): + seq.chain(p, c) + end_seqchain = end_seqreset + + def start_seqformat(self, attr): + try: + id = attr['id'] + except KeyError: + id = None + try: + value = attr['value'] + except KeyError: + value = '1' + self._seq.setFormat(id,value) + end_seqformat = end_seqreset + + # AR hacking in aliases to allow the proper casing for RML. + # the above ones should be deprecated over time. 2001-03-22 + start_seqDefault = start_seqdefault + end_seqDefault = end_seqdefault + start_seqReset = start_seqreset + end_seqReset = end_seqreset + start_seqChain = start_seqchain + end_seqChain = end_seqchain + start_seqFormat = start_seqformat + end_seqFormat = end_seqformat + + def start_seq(self, attr): + #if it has a template, use that; otherwise try for id; + #otherwise take default sequence + if attr.has_key('template'): + templ = attr['template'] + self.handle_data(templ % self._seq) + return + elif attr.has_key('id'): + id = attr['id'] + else: + id = None + output = self._seq.nextf(id) + self.handle_data(output) + + def end_seq(self): + pass + + def start_onDraw(self,attr): + defn = ABag() + if attr.has_key('name'): defn.name = attr['name'] + else: self._syntax_error(' needs at least a name attribute') + + if attr.has_key('label'): defn.label = attr['label'] + defn.kind='onDraw' + self._push(cbDefn=defn) + self.handle_data('') + self._pop() + + #--------------------------------------------------------------- + def _push(self,**attr): + frag = copy.copy(self._stack[-1]) + _applyAttributes(frag,attr) + self._stack.append(frag) + + def _pop(self,**kw): + frag = self._stack[-1] + del self._stack[-1] + for k, v in kw.items(): + assert getattr(frag,k)==v + return frag + + def getAttributes(self,attr,attrMap): + A = {} + for k, v in attr.items(): + if not self.caseSensitive: + k = string.lower(k) + if k in attrMap.keys(): + j = attrMap[k] + func = j[1] + try: + A[j[0]] = (func is None) and v or func(v) + except: + self._syntax_error('%s: invalid value %s'%(k,v)) + else: + self._syntax_error('invalid attribute name %s'%k) + return A + + #---------------------------------------------------------------- + + def __init__(self,verbose=0): + self.caseSensitive = 0 + xmllib.XMLParser.__init__(self,verbose=verbose) + + def _iReset(self): + self.fragList = [] + if hasattr(self, 'bFragList'): delattr(self,'bFragList') + + def _reset(self, style): + '''reset the parser''' + xmllib.XMLParser.reset(self) + + # initialize list of string segments to empty + self.errors = [] + self._style = style + self._iReset() + + #---------------------------------------------------------------- + def handle_data(self,data): + "Creates an intermediate representation of string segments." + + frag = copy.copy(self._stack[-1]) + if hasattr(frag,'cbDefn'): + if data!='': syntax_error('Only tag allowed') + elif hasattr(frag,'_selfClosingTag'): + if data!='': syntax_error('No content allowed in %s tag' % frag._selfClosingTag) + return + else: + # if sub and super are both on they will cancel each other out + if frag.sub == 1 and frag.super == 1: + frag.sub = 0 + frag.super = 0 + + if frag.sub: + frag.rise = -frag.fontSize*subFraction + frag.fontSize = max(frag.fontSize-sizeDelta,3) + elif frag.super: + frag.rise = frag.fontSize*superFraction + frag.fontSize = max(frag.fontSize-sizeDelta,3) + + if frag.greek: + frag.fontName = 'symbol' + data = _greekConvert(data) + + # bold, italic, and underline + x = frag.fontName = tt2ps(frag.fontName,frag.bold,frag.italic) + + #save our data + frag.text = data + + if hasattr(frag,'isBullet'): + delattr(frag,'isBullet') + self.bFragList.append(frag) + else: + self.fragList.append(frag) + + def handle_cdata(self,data): + self.handle_data(data) + + def _setup_for_parse(self,style): + self._seq = reportlab.lib.sequencer.getSequencer() + self._reset(style) # reinitialise the parser + + def parse(self, text, style): + """Given a formatted string will return a list of + ParaFrag objects with their calculated widths. + If errors occur None will be returned and the + self.errors holds a list of the error messages. + """ + # AR 20040612 - when we feed Unicode strings in, sgmlop + # tries to coerce to ASCII. Must intercept, coerce to + # any 8-bit encoding which defines most of 256 points, + # and revert at end. Yuk. Preliminary step prior to + # removal of parser altogether. + enc = self._enc = 'cp1252' #our legacy default + self._UNI = type(text) is UnicodeType + if self._UNI: + text = text.encode(enc) + + self._setup_for_parse(style) + # the xmlparser requires that all text be surrounded by xml + # tags, therefore we must throw some unused flags around the + # given string + if not(len(text)>=6 and text[0]=='<' and _re_para.match(text)): + text = ""+text+"" + self.feed(text) + self.close() # force parsing to complete + return self._complete_parse() + + def _complete_parse(self): + del self._seq + style = self._style + del self._style + if len(self.errors)==0: + fragList = self.fragList + bFragList = hasattr(self,'bFragList') and self.bFragList or None + self._iReset() + else: + fragList = bFragList = None + + if self._UNI: + #reconvert to unicode + if fragList: + for frag in fragList: + frag.text = unicode(frag.text, self._enc) + if bFragList: + for frag in bFragList: + frag.text = unicode(frag.text, self._enc) + + return style, fragList, bFragList + + def _tt_parse(self,tt): + tag = tt[0] + try: + start = getattr(self,'start_'+tag) + end = getattr(self,'end_'+tag) + except AttributeError: + raise ValueError('Invalid tag "%s"' % tag) + start(tt[1] or {}) + C = tt[2] + if C: + M = self._tt_handlers + for c in C: + M[type(c) is TupleType](c) + end() + + def tt_parse(self,tt,style): + '''parse from tupletree form''' + self._setup_for_parse(style) + self._tt_handlers = self.handle_data,self._tt_parse + self._tt_parse(tt) + return self._complete_parse() + +if __name__=='__main__': + from reportlab.platypus import cleanBlockQuotedText + _parser=ParaParser() + def check_text(text,p=_parser): + print '##########' + text = cleanBlockQuotedText(text) + l,rv,bv = p.parse(text,style) + if rv is None: + for l in _parser.errors: + print l + else: + print 'ParaStyle', l.fontName,l.fontSize,l.textColor + for l in rv: + print l.fontName,l.fontSize,l.textColor,l.bold, l.rise, '|%s|'%l.text[:25], + if hasattr(l,'cbDefn'): + print 'cbDefn',l.cbDefn.name,l.cbDefn.label,l.cbDefn.kind + else: print + + style=ParaFrag() + style.fontName='Times-Roman' + style.fontSize = 12 + style.textColor = black + style.bulletFontName = black + style.bulletFontName='Times-Roman' + style.bulletFontSize=12 + + text=''' + aDβ + + Tell me, O muse, of that ingenious hero who travelled far and wide + after he had sacked the famous town of Troy. Many cities did he visit, + and many were the nations with whose manners and customs he was acquainted; + moreover he suffered much by sea while trying to save his own life + and bring his men safely home; but do what he might he could not save + his men, for they perished through their own sheer folly in eating + the cattle of the Sun-god Hyperion; so the god prevented them from + ever reaching home. Tell me, too, about all these things, O daughter + of Jove, from whatsoever source you1 may know them. + ''' + check_text(text) + check_text(' ') + check_text('ReportLab -- Reporting for the Internet Age') + check_text(''' + τTell me, O muse, of that ingenious hero who travelled far and wide + after he had sacked the famous town of Troy. Many cities did he visit, + and many were the nations with whose manners and customs he was acquainted; + moreover he suffered much by sea while trying to save his own life + and bring his men safely home; but do what he might he could not save + his men, for they perished through their own sheer folly in eating + the cattle of the Sun-god Hyperion; so the god prevented them from + ever reaching home. Tell me, too, about all these things, O daughter + of Jove, from whatsoever source you may know them.''') + check_text(''' + Telemachus took this speech as of good omen and rose at once, for + he was bursting with what he had to say. He stood in the middle of + the assembly and the good herald Pisenor brought him his staff. Then, + turning to Aegyptius, "Sir," said he, "it is I, as you will shortly + learn, who have convened you, for it is I who am the most aggrieved. + I have not got wind of any host approaching about which I would warn + you, nor is there any matter of public moment on which I would speak. + My grieveance is purely personal, and turns on two great misfortunes + which have fallen upon my house. The first of these is the loss of + my excellent father, who was chief among all you here present, and + was like a father to every one of you; the second is much more serious, + and ere long will be the utter ruin of my estate. The sons of all + the chief men among you are pestering my mother to marry them against + her will. They are afraid to go to her father Icarius, asking him + to choose the one he likes best, and to provide marriage gifts for + his daughter, but day by day they keep hanging about my father's house, + sacrificing our oxen, sheep, and fat goats for their banquets, and + never giving so much as a thought to the quantity of wine they drink. + No estate can stand such recklessness; we have now no Ulysses to ward + off harm from our doors, and I cannot hold my own against them. I + shall never all my days be as good a man as he was, still I would + indeed defend myself if I had power to do so, for I cannot stand such + treatment any longer; my house is being disgraced and ruined. Have + respect, therefore, to your own consciences and to public opinion. + Fear, too, the wrath of heaven, lest the gods should be displeased + and turn upon you. I pray you by Jove and Themis, who is the beginning + and the end of councils, [do not] hold back, my friends, and leave + me singlehanded- unless it be that my brave father Ulysses did some + wrong to the Achaeans which you would now avenge on me, by aiding + and abetting these suitors. Moreover, if I am to be eaten out of house + and home at all, I had rather you did the eating yourselves, for I + could then take action against you to some purpose, and serve you + with notices from house to house till I got paid in full, whereas + now I have no remedy."''') + + check_text(''' +But as the sun was rising from the fair sea into the firmament of +heaven to shed light on mortals and immortals, they reached Pylos +the city of Neleus. Now the people of Pylos were gathered on the sea +shore to offer sacrifice of black bulls to Neptune lord of the Earthquake. +There were nine guilds with five hundred men in each, and there were +nine bulls to each guild. As they were eating the inward meats and +burning the thigh bones [on the embers] in the name of Neptune, Telemachus +and his crew arrived, furled their sails, brought their ship to anchor, +and went ashore. ''') + check_text(''' +So the neighbours and kinsmen of Menelaus were feasting and making +merry in his house. There was a bard also to sing to them and play +his lyre, while two tumblers went about performing in the midst of +them when the man struck up with his tune.]''') + check_text(''' +"When we had passed the [Wandering] rocks, with Scylla and terrible +Charybdis, we reached the noble island of the sun-god, where were +the goodly cattle and sheep belonging to the sun Hyperion. While still +at sea in my ship I could bear the cattle lowing as they came home +to the yards, and the sheep bleating. Then I remembered what the blind +Theban prophet Teiresias had told me, and how carefully Aeaean Circe +had warned me to shun the island of the blessed sun-god. So being +much troubled I said to the men, 'My men, I know you are hard pressed, +but listen while I tell you the prophecy that Teiresias made me, and +how carefully Aeaean Circe warned me to shun the island of the blessed +sun-god, for it was here, she said, that our worst danger would lie. +Head the ship, therefore, away from the island.''') + check_text('''A<B>C&D"E'F''') + check_text('''A< B> C& D" E' F''') + check_text('''&'"]]>''') + check_text('''+ +There was a bard also to sing to them and play +his lyre, while two tumblers went about performing in the midst of +them when the man struck up with his tune.]''') + check_text('''A paragraph''') + check_text('''B paragraph''') + # HVB, 30.05.2003: Test for new features + _parser.caseSensitive=0 + check_text('''Here comes Helvetica 14 with strong emphasis.''') + check_text('''Here comes Helvetica 14 with strong emphasis.''') + check_text('''Here comes Courier 3cm and normal again.''') diff --git a/bin/reportlab/platypus/tableofcontents.py b/bin/reportlab/platypus/tableofcontents.py new file mode 100644 index 00000000000..c1ad3d525db --- /dev/null +++ b/bin/reportlab/platypus/tableofcontents.py @@ -0,0 +1,329 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/tableofcontents.py +""" +This module defines a single TableOfContents() class that can be used to +create automatically a table of tontents for Platypus documents like +this: + + story = [] + toc = TableOfContents() + story.append(toc) + # some heading paragraphs here... + doc = MyTemplate(path) + doc.multiBuild(story) + +The data needed to create the table is a list of (level, text, pageNum) +triplets, plus some paragraph styles for each level of the table itself. +The triplets will usually be created in a document template's method +like afterFlowable(), making notification calls using the notify() +method with appropriate data like this: + + (level, text, pageNum) = ... + self.notify('TOCEntry', (level, text, pageNum)) + +As the table of contents need at least two passes over the Platypus +story which is why the moultiBuild0() method must be called. + +The levelParaStyle variables are the paragraph styles used +to format the entries in the table of contents. Their indentation +is calculated like this: each entry starts at a multiple of some +constant named delta. If one entry spans more than one line, all +lines after the first are indented by the same constant named +epsilon. +""" +__version__=''' $Id: tableofcontents.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +import string + +from reportlab.lib import enums +from reportlab.lib.units import cm +from reportlab.lib.styles import ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.doctemplate import IndexingFlowable +from reportlab.platypus.tables import TableStyle, Table + + +# Default paragraph styles for tables of contents. +# (This could also be generated automatically or even +# on-demand if it is not known how many levels the +# TOC will finally need to display...) + +delta = 1*cm +epsilon = 0.5*cm + +levelZeroParaStyle = \ + ParagraphStyle(name='LevelZero', + fontName='Times-Roman', + fontSize=10, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 0*delta + epsilon) + +levelOneParaStyle = \ + ParagraphStyle(name='LevelOne', + parent = levelZeroParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 1*delta + epsilon) + +levelTwoParaStyle = \ + ParagraphStyle(name='LevelTwo', + parent = levelOneParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 2*delta + epsilon) + +levelThreeParaStyle = \ + ParagraphStyle(name='LevelThree', + parent = levelTwoParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 3*delta + epsilon) + +levelFourParaStyle = \ + ParagraphStyle(name='LevelFour', + parent = levelTwoParaStyle, + leading=11, + firstLineIndent = -epsilon, + leftIndent = 4*delta + epsilon) + +defaultTableStyle = \ + TableStyle([('VALIGN', (0,0), (-1,-1), 'TOP')]) + + +class TableOfContents(IndexingFlowable): + """This creates a formatted table of contents. + + It presumes a correct block of data is passed in. + The data block contains a list of (level, text, pageNumber) + triplets. You can supply a paragraph style for each level + (starting at zero). + """ + + def __init__(self): + self.entries = [] + self.rightColumnWidth = 72 + self.levelStyles = [levelZeroParaStyle, + levelOneParaStyle, + levelTwoParaStyle, + levelThreeParaStyle, + levelFourParaStyle] + self.tableStyle = defaultTableStyle + self._table = None + self._entries = [] + self._lastEntries = [] + + + def beforeBuild(self): + # keep track of the last run + self._lastEntries = self._entries[:] + self.clearEntries() + + + def isIndexing(self): + return 1 + + + def isSatisfied(self): + return (self._entries == self._lastEntries) + + def notify(self, kind, stuff): + """The notification hook called to register all kinds of events. + + Here we are interested in 'TOCEntry' events only. + """ + if kind == 'TOCEntry': + (level, text, pageNum) = stuff + self.addEntry(level, text, pageNum) + + + def clearEntries(self): + self._entries = [] + + + def addEntry(self, level, text, pageNum): + """Adds one entry to the table of contents. + + This allows incremental buildup by a doctemplate. + Requires that enough styles are defined.""" + + assert type(level) == type(1), "Level must be an integer" + assert level < len(self.levelStyles), \ + "Table of contents must have a style defined " \ + "for paragraph level %d before you add an entry" % level + + self._entries.append((level, text, pageNum)) + + + def addEntries(self, listOfEntries): + """Bulk creation of entries in the table of contents. + + If you knew the titles but not the page numbers, you could + supply them to get sensible output on the first run.""" + + for (level, text, pageNum) in listOfEntries: + self.addEntry(level, text, pageNum) + + + def wrap(self, availWidth, availHeight): + "All table properties should be known by now." + + widths = (availWidth - self.rightColumnWidth, + self.rightColumnWidth) + + # makes an internal table which does all the work. + # we draw the LAST RUN's entries! If there are + # none, we make some dummy data to keep the table + # from complaining + if len(self._lastEntries) == 0: + _tempEntries = [(0,'Placeholder for table of contents',0)] + else: + _tempEntries = self._lastEntries + + tableData = [] + for (level, text, pageNum) in _tempEntries: + leftColStyle = self.levelStyles[level] + #right col style is right aligned + rightColStyle = ParagraphStyle(name='leftColLevel%d' % level, + parent=leftColStyle, + leftIndent=0, + alignment=enums.TA_RIGHT) + leftPara = Paragraph(text, leftColStyle) + rightPara = Paragraph(str(pageNum), rightColStyle) + tableData.append([leftPara, rightPara]) + + self._table = Table(tableData, colWidths=widths, + style=self.tableStyle) + + self.width, self.height = self._table.wrapOn(self.canv,availWidth, availHeight) + return (self.width, self.height) + + + def split(self, availWidth, availHeight): + """At this stage we do not care about splitting the entries, + we will just return a list of platypus tables. Presumably the + calling app has a pointer to the original TableOfContents object; + Platypus just sees tables. + """ + return self._table.splitOn(self.canv,availWidth, availHeight) + + + def drawOn(self, canvas, x, y, _sW=0): + """Don't do this at home! The standard calls for implementing + draw(); we are hooking this in order to delegate ALL the drawing + work to the embedded table object. + """ + self._table.drawOn(canvas, x, y, _sW) + + +class SimpleIndex(IndexingFlowable): + """This creates a very simple index. + + Entries have a string key, and appear with a page number on + the right. Prototype for more sophisticated multi-level index.""" + def __init__(self): + #keep stuff in a dictionary while building + self._entries = {} + self._lastEntries = {} + self._table = None + self.textStyle = ParagraphStyle(name='index', + fontName='Times-Roman', + fontSize=12) + def isIndexing(self): + return 1 + + def isSatisfied(self): + return (self._entries == self._lastEntries) + + def beforeBuild(self): + # keep track of the last run + self._lastEntries = self._entries.copy() + self.clearEntries() + + def clearEntries(self): + self._entries = {} + + def notify(self, kind, stuff): + """The notification hook called to register all kinds of events. + + Here we are interested in 'IndexEntry' events only. + """ + if kind == 'IndexEntry': + (text, pageNum) = stuff + self.addEntry(text, pageNum) + + def addEntry(self, text, pageNum): + """Allows incremental buildup""" + if self._entries.has_key(text): + self._entries[text].append(str(pageNum)) + else: + self._entries[text] = [pageNum] + + def split(self, availWidth, availHeight): + """At this stage we do not care about splitting the entries, + we will just return a list of platypus tables. Presumably the + calling app has a pointer to the original TableOfContents object; + Platypus just sees tables. + """ + return self._table.splitOn(self.canv,availWidth, availHeight) + + def wrap(self, availWidth, availHeight): + "All table properties should be known by now." + # makes an internal table which does all the work. + # we draw the LAST RUN's entries! If there are + # none, we make some dummy data to keep the table + # from complaining + if len(self._lastEntries) == 0: + _tempEntries = [('Placeholder for index',[0,1,2])] + else: + _tempEntries = self._lastEntries.items() + _tempEntries.sort() + + tableData = [] + for (text, pageNumbers) in _tempEntries: + #right col style is right aligned + allText = text + ': ' + string.join(map(str, pageNumbers), ', ') + para = Paragraph(allText, self.textStyle) + tableData.append([para]) + + self._table = Table(tableData, colWidths=[availWidth]) + + self.width, self.height = self._table.wrapOn(self.canv,availWidth, availHeight) + return (self.width, self.height) + + def drawOn(self, canvas, x, y, _sW=0): + """Don't do this at home! The standard calls for implementing + draw(); we are hooking this in order to delegate ALL the drawing + work to the embedded table object. + """ + self._table.drawOn(canvas, x, y, _sW) + +class ReferenceText(IndexingFlowable): + """Fakery to illustrate how a reference would work if we could + put it in a paragraph.""" + def __init__(self, textPattern, targetKey): + self.textPattern = textPattern + self.target = targetKey + self.paraStyle = ParagraphStyle('tmp') + self._lastPageNum = None + self._pageNum = -999 + self._para = None + + def beforeBuild(self): + self._lastPageNum = self._pageNum + + def notify(self, kind, stuff): + if kind == 'Target': + (key, pageNum) = stuff + if key == self.target: + self._pageNum = pageNum + + def wrap(self, availWidth, availHeight): + text = self.textPattern % self._lastPageNum + self._para = Paragraph(text, self.paraStyle) + return self._para.wrap(availWidth, availHeight) + + def drawOn(self, canvas, x, y, _sW=0): + self._para.drawOn(canvas, x, y, _sW) + + diff --git a/bin/reportlab/platypus/tables.py b/bin/reportlab/platypus/tables.py new file mode 100755 index 00000000000..62fefbd8d77 --- /dev/null +++ b/bin/reportlab/platypus/tables.py @@ -0,0 +1,1384 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/tables.py +__version__=''' $Id: tables.py 2849 2006-05-06 08:25:23Z andy $ ''' + +__doc__=""" +Tables are created by passing the constructor a tuple of column widths, a tuple of row heights and the data in +row order. Drawing of the table can be controlled by using a TableStyle instance. This allows control of the +color and weight of the lines (if any), and the font, alignment and padding of the text. + +None values in the sequence of row heights or column widths, mean that the corresponding rows +or columns should be automatically sized. + +All the cell values should be convertible to strings; embedded newline '\\n' characters +cause the value to wrap (ie are like a traditional linefeed). + +See the test output from running this module as a script for a discussion of the method for constructing +tables and table styles. +""" +from reportlab.platypus.flowables import Flowable, Preformatted +from reportlab import rl_config +from reportlab.lib.styles import PropertySet, ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.utils import fp_str +from reportlab.pdfbase import pdfmetrics +import operator, string +from types import TupleType, ListType, StringType, FloatType, IntType + +class CellStyle(PropertySet): + defaults = { + 'fontname':'Times-Roman', + 'fontsize':10, + 'leading':12, + 'leftPadding':6, + 'rightPadding':6, + 'topPadding':3, + 'bottomPadding':3, + 'firstLineIndent':0, + 'color':colors.black, + 'alignment': 'LEFT', + 'background': (1,1,1), + 'valign': 'BOTTOM', + 'href': None, + 'destination':None, + } + +LINECAPS={None: None, 'butt':0,'round':1,'projecting':2,'squared':2} +LINEJOINS={None: None, 'miter':0, 'mitre':0, 'round':1,'bevel':2} + +# experimental replacement +class CellStyle1(PropertySet): + fontname = "Times-Roman" + fontsize = 10 + leading = 12 + leftPadding = 6 + rightPadding = 6 + topPadding = 3 + bottomPadding = 3 + firstLineIndent = 0 + color = colors.black + alignment = 'LEFT' + background = (1,1,1) + valign = "BOTTOM" + href = None + destination = None + def __init__(self, name, parent=None): + self.name = name + if parent is not None: + parent.copy(self) + def copy(self, result=None): + if result is None: + result = CellStyle1() + for name in dir(self): + setattr(result, name, gettattr(self, name)) + return result +CellStyle = CellStyle1 + +class TableStyle: + def __init__(self, cmds=None, parent=None, **kw): + #handle inheritance from parent first. + commands = [] + if parent: + # copy the parents list at construction time + commands = commands + parent.getCommands() + self._opts = parent._opts + for a in ('spaceBefore','spaceAfter'): + if hasattr(parent,a): + setattr(self,a,getattr(parent,a)) + if cmds: + commands = commands + list(cmds) + self._cmds = commands + self._opts={} + self._opts.update(kw) + + def add(self, *cmd): + self._cmds.append(cmd) + def __repr__(self): + L = map(repr, self._cmds) + import string + L = string.join(L, " \n") + return "TableStyle(\n%s\n) # end TableStyle" % L + def getCommands(self): + return self._cmds + +TableStyleType = type(TableStyle()) +_SeqTypes = (TupleType, ListType) + +def _rowLen(x): + return type(x) not in _SeqTypes and 1 or len(x) + +def _calc_pc(V,avail): + '''check list V for percentage or * values + 1) absolute values go through unchanged + 2) percentages are used as weights for unconsumed space + 3) if no None values were seen '*' weights are + set equally with unclaimed space + otherwise * weights are assigned as None''' + R = [] + r = R.append + I = [] + i = I.append + J = [] + j = J.append + s = avail + w = n = 0. + for v in V: + if type(v) is type(""): + v = v.strip() + if not v: + v = None + n += 1 + elif v.endswith('%'): + v = float(v[:-1]) + w += v + i(len(R)) + elif v=='*': + j(len(R)) + else: + v = float(v) + s -= v + elif v is None: + n += 1 + else: + s -= v + r(v) + s = max(0.,s) + f = s/max(100.,w) + for i in I: + R[i] *= f + s -= R[i] + s = max(0.,s) + m = len(J) + if m: + v = n==0 and s/m or None + for j in J: + R[j] = v + return R + +def _hLine(canvLine, scp, ecp, y, hBlocks, FUZZ=rl_config._FUZZ): + ''' + Draw horizontal lines; do not draw through regions specified in hBlocks + This also serves for vertical lines with a suitable canvLine + ''' + if hBlocks: hBlocks = hBlocks.get(y,None) + if not hBlocks or scp>=hBlocks[-1][1]-FUZZ or ecp<=hBlocks[0][0]+FUZZ: + canvLine(scp,y,ecp,y) + else: + i = 0 + n = len(hBlocks) + while scp=ecp-FUZZ: + i += 1 + continue + i0 = max(scp,x0) + i1 = min(ecp,x1) + if i0>scp: canvLine(scp,y,i0,y) + scp = i1 + if scp%s" % (self.__class__.__name__, id(self), nr, nc, vx) + + def _listCellGeom(self, V,w,s,W=None,H=None,aH=72000): + if not V: return 0,0 + aW = w-s.leftPadding-s.rightPadding + aH = aH - s.topPadding - s.bottomPadding + t = 0 + w = 0 + canv = getattr(self,'canv',None) + for v in V: + vw, vh = v.wrapOn(canv,aW, aH) + if W is not None: W.append(vw) + if H is not None: H.append(vh) + w = max(w,vw) + t = t + vh + v.getSpaceBefore()+v.getSpaceAfter() + return w, t - V[0].getSpaceBefore()-V[-1].getSpaceAfter() + + def _calc_width(self,availWidth,W=None): + if getattr(self,'_width_calculated_once',None): return + #comments added by Andy to Robin's slightly terse variable names + if not W: W = _calc_pc(self._argW,availWidth) #widths array + if None in W: #some column widths are not given + canv = getattr(self,'canv',None) + saved = None + colSpanCells = self._spanCmds and self._colSpanCells or () + if W is self._argW: W = W[:] + while None in W: + j = W.index(None) #find first unspecified column + f = lambda x,j=j: operator.getitem(x,j) + V = map(f,self._cellvalues) #values for this column + S = map(f,self._cellStyles) #styles for this column + w = 0 + i = 0 + + for v, s in map(None, V, S): + #if the current cell is part of a spanned region, + #assume a zero size. + if (j, i) in colSpanCells: + t = 0.0 + else:#work out size + t = self._elementWidth(v,s) + if t is None: + raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width\n%s" % (v.identity(30),i,j,self.identity(30)) + t = t + s.leftPadding+s.rightPadding + if t>w: w = t #record a new maximum + i = i + 1 + + W[j] = w + + self._colWidths = W + width = 0 + self._colpositions = [0] #index -1 is right side boundary; we skip when processing cells + for w in W: + width = width + w + self._colpositions.append(width) + + self._width = width + self._width_calculated_once = 1 + + def _elementWidth(self,v,s): + t = type(v) + if t in _SeqTypes: + w = 0 + for e in v: + ew = self._elementWidth(e,s) + if ew is None: return None + w = max(w,ew) + return w + elif isinstance(v,Flowable) and v._fixedWidth: + if hasattr(v, 'width') and type(v.width) in (IntType,FloatType): return v.width + if hasattr(v, 'drawWidth') and type(v.drawWidth) in (IntType,FloatType): return v.drawWidth + # Even if something is fixedWidth, the attribute to check is not + # necessarily consistent (cf. Image.drawWidth). Therefore, we'll + # be extra-careful and fall through to this code if necessary. + if hasattr(v, 'minWidth'): + try: + w = v.minWidth() # should be all flowables + if type(w) in (FloatType,IntType): return w + except AttributeError: + pass + v = string.split(v is not None and str(v) or '', "\n") + return max(map(lambda a, b=s.fontname, c=s.fontsize,d=pdfmetrics.stringWidth: d(a,b,c), v)) + + def _calc_height(self, availHeight, availWidth, H=None, W=None): + + H = self._argH + if not W: W = _calc_pc(self._argW,availWidth) #widths array + + hmax = lim = len(H) + longTable = getattr(self,'_longTableOptimize',rl_config.longTableOptimize) + + if None in H: + canv = getattr(self,'canv',None) + saved = None + #get a handy list of any cells which span rows. should be ignored for sizing + if self._spanCmds: + rowSpanCells = self._rowSpanCells + colSpanCells = self._colSpanCells + spanRanges = self._spanRanges + colpositions = self._colpositions + else: + rowSpanCells = colSpanCells = () + if canv: saved = canv._fontname, canv._fontsize, canv._leading + H = H[:] #make a copy as we'll change it + self._rowHeights = H + while None in H: + i = H.index(None) + if longTable: + hmax = i + height = reduce(operator.add, H[:i], 0) + # we can stop if we have filled up all available room + if height > availHeight: break + V = self._cellvalues[i] # values for row i + S = self._cellStyles[i] # styles for row i + h = 0 + j = 0 + for v, s, w in map(None, V, S, W): # value, style, width (lengths must match) + ji = j,i + if ji in rowSpanCells: + t = 0.0 # don't count it, it's either occluded or unreliable + else: + t = type(v) + if t in _SeqTypes or isinstance(v,Flowable): + if not t in _SeqTypes: v = (v,) + if w is None: + raise ValueError, "Flowable %s in cell(%d,%d) can't have auto width in\n%s" % (v[0].identity(30),i,j,self.identity(30)) + if canv: canv._fontname, canv._fontsize, canv._leading = s.fontname, s.fontsize, s.leading or 1.2*s.fontsize + if ji in colSpanCells: + t = spanRanges[ji] + w = max(colpositions[t[2]+1]-colpositions[t[0]],w) + dW,t = self._listCellGeom(v,w,s) + if canv: canv._fontname, canv._fontsize, canv._leading = saved + dW = dW + s.leftPadding + s.rightPadding + if not rl_config.allowTableBoundsErrors and dW>w: + raise "LayoutError", "Flowable %s (%sx%s points) too wide for cell(%d,%d) (%sx* points) in\n%s" % (v[0].identity(30),fp_str(dW),fp_str(t),i,j, fp_str(w), self.identity(30)) + else: + v = string.split(v is not None and str(v) or '', "\n") + t = s.leading*len(v) + t = t+s.bottomPadding+s.topPadding + if t>h: h = t #record a new maximum + j = j + 1 + H[i] = h + if None not in H: hmax = lim + + height = self._height = reduce(operator.add, H[:hmax], 0) + self._rowpositions = [height] # index 0 is actually topline; we skip when processing cells + for h in H[:hmax]: + height = height - h + self._rowpositions.append(height) + assert abs(height)<1e-8, 'Internal height error' + self._hmax = hmax + + def _calc(self, availWidth, availHeight): + #if hasattr(self,'_width'): return + + #in some cases there are unsizable things in + #cells. If so, apply a different algorithm + #and assign some withs in a less (thanks to Gary Poster) dumb way. + #this CHANGES the widths array. + if (None in self._colWidths or '*' in self._colWidths) and self._hasVariWidthElements(): + W = self._calcPreliminaryWidths(availWidth) #widths + else: + W = None + + # need to know which cells are part of spanned + # ranges, so _calc_height and _calc_width can ignore them + # in sizing + if self._spanCmds: + self._calcSpanRanges() + if None in self._argH: + self._calc_width(availWidth,W=W) + + # calculate the full table height + self._calc_height(availHeight,availWidth,W=W) + + # calculate the full table width + self._calc_width(availWidth,W=W) + + if self._spanCmds: + #now work out the actual rect for each spanned cell from the underlying grid + self._calcSpanRects() + + def _hasVariWidthElements(self, upToRow=None): + """Check for flowables in table cells and warn up front. + + Allow a couple which we know are fixed size such as + images and graphics.""" + bad = 0 + if upToRow is None: upToRow = self._nrows + for row in range(min(self._nrows, upToRow)): + for col in range(self._ncols): + value = self._cellvalues[row][col] + if not self._canGetWidth(value): + bad = 1 + #raise Exception('Unsizable elements found at row %d column %d in table with content:\n %s' % (row, col, value)) + return bad + + def _canGetWidth(self, thing): + "Can we work out the width quickly?" + if type(thing) in (ListType, TupleType): + for elem in thing: + if not self._canGetWidth(elem): + return 0 + return 1 + elif isinstance(thing, Flowable): + return thing._fixedWidth # must loosen this up + else: #string, number, None etc. + #anything else gets passed to str(...) + # so should be sizable + return 1 + + def _calcPreliminaryWidths(self, availWidth): + """Fallback algorithm for when main one fails. + + Where exact width info not given but things like + paragraphs might be present, do a preliminary scan + and assign some best-guess values.""" + + W = list(self._argW) # _calc_pc(self._argW,availWidth) + verbose = 0 + totalDefined = 0.0 + percentDefined = 0 + percentTotal = 0 + numberUndefined = 0 + numberGreedyUndefined = 0 + for w in W: + if w is None: + numberUndefined += 1 + elif w == '*': + numberUndefined += 1 + numberGreedyUndefined += 1 + elif _endswith(w,'%'): + percentDefined += 1 + percentTotal += float(w[:-1]) + else: + assert type(w) in (IntType, FloatType) + totalDefined = totalDefined + w + if verbose: print 'prelim width calculation. %d columns, %d undefined width, %0.2f units remain' % ( + self._ncols, numberUndefined, availWidth - totalDefined) + + #check columnwise in each None column to see if they are sizable. + given = [] + sizeable = [] + unsizeable = [] + minimums = {} + totalMinimum = 0 + elementWidth = self._elementWidth + for colNo in range(self._ncols): + w = W[colNo] + if w is None or w=='*' or _endswith(w,'%'): + siz = 1 + current = final = None + for rowNo in range(self._nrows): + value = self._cellvalues[rowNo][colNo] + style = self._cellStyles[rowNo][colNo] + new = elementWidth(value,style)+style.leftPadding+style.rightPadding + final = max(current, new) + current = new + siz = siz and self._canGetWidth(value) # irrelevant now? + if siz: + sizeable.append(colNo) + else: + unsizeable.append(colNo) + minimums[colNo] = final + totalMinimum += final + else: + given.append(colNo) + if len(given) == self._ncols: + return + if verbose: print 'predefined width: ',given + if verbose: print 'uncomputable width: ',unsizeable + if verbose: print 'computable width: ',sizeable + + # how much width is left: + remaining = availWidth - (totalMinimum + totalDefined) + if remaining > 0: + # we have some room left; fill it. + definedPercentage = (totalDefined/availWidth)*100 + percentTotal += definedPercentage + if numberUndefined and percentTotal < 100: + undefined = numberGreedyUndefined or numberUndefined + defaultWeight = (100-percentTotal)/undefined + percentTotal = 100 + defaultDesired = (defaultWeight/percentTotal)*availWidth + else: + defaultWeight = defaultDesired = 1 + # we now calculate how wide each column wanted to be, and then + # proportionately shrink that down to fit the remaining available + # space. A column may not shrink less than its minimum width, + # however, which makes this a bit more complicated. + desiredWidths = [] + totalDesired = 0 + effectiveRemaining = remaining + for colNo, minimum in minimums.items(): + w = W[colNo] + if _endswith(w,'%'): + desired = (float(w[:-1])/percentTotal)*availWidth + elif w == '*': + desired = defaultDesired + else: + desired = not numberGreedyUndefined and defaultDesired or 1 + if desired <= minimum: + W[colNo] = minimum + else: + desiredWidths.append( + (desired-minimum, minimum, desired, colNo)) + totalDesired += desired + effectiveRemaining += minimum + if desiredWidths: # else we're done + # let's say we have two variable columns. One wanted + # 88 points, and one wanted 264 points. The first has a + # minWidth of 66, and the second of 55. We have 71 points + # to divide up in addition to the totalMinimum (i.e., + # remaining==71). Our algorithm tries to keep the proportion + # of these variable columns. + # + # To do this, we add up the minimum widths of the variable + # columns and the remaining width. That's 192. We add up the + # totalDesired width. That's 352. That means we'll try to + # shrink the widths by a proportion of 192/352--.545454. + # That would make the first column 48 points, and the second + # 144 points--adding up to the desired 192. + # + # Unfortunately, that's too small for the first column. It + # must be 66 points. Therefore, we go ahead and save that + # column width as 88 points. That leaves (192-88==) 104 + # points remaining. The proportion to shrink the remaining + # column is (104/264), which, multiplied by the desired + # width of 264, is 104: the amount assigned to the remaining + # column. + proportion = effectiveRemaining/totalDesired + # we sort the desired widths by difference between desired and + # and minimum values, a value called "disappointment" in the + # code. This means that the columns with a bigger + # disappointment will have a better chance of getting more of + # the available space. + desiredWidths.sort() + finalSet = [] + for disappointment, minimum, desired, colNo in desiredWidths: + adjusted = proportion * desired + if adjusted < minimum: + W[colNo] = minimum + totalDesired -= desired + effectiveRemaining -= minimum + if totalDesired: + proportion = effectiveRemaining/totalDesired + else: + finalSet.append((minimum, desired, colNo)) + for minimum, desired, colNo in finalSet: + adjusted = proportion * desired + assert adjusted >= minimum + W[colNo] = adjusted + else: + for colNo, minimum in minimums.items(): + W[colNo] = minimum + if verbose: print 'new widths are:', W + self._argW = self._colWidths = W + return W + + def minWidth(self): + W = list(self._argW) + width = 0 + elementWidth = self._elementWidth + rowNos = xrange(self._nrows) + values = self._cellvalues + styles = self._cellStyles + for colNo in xrange(len(W)): + w = W[colNo] + if w is None or w=='*' or _endswith(w,'%'): + final = 0 + for rowNo in rowNos: + value = values[rowNo][colNo] + style = styles[rowNo][colNo] + new = (elementWidth(value,style)+ + style.leftPadding+style.rightPadding) + final = max(final, new) + width += final + else: + width += float(w) + return width # XXX + 1/2*(left and right border widths) + + def _calcSpanRanges(self): + """Work out rects for tables which do row and column spanning. + + This creates some mappings to let the later code determine + if a cell is part of a "spanned" range. + self._spanRanges shows the 'coords' in integers of each + 'cell range', or None if it was clobbered: + (col, row) -> (col0, row0, col1, row1) + + Any cell not in the key is not part of a spanned region + """ + self._spanRanges = spanRanges = {} + for x in xrange(self._ncols): + for y in xrange(self._nrows): + spanRanges[x,y] = (x, y, x, y) + self._colSpanCells = [] + self._rowSpanCells = [] + csa = self._colSpanCells.append + rsa = self._rowSpanCells.append + for (cmd, start, stop) in self._spanCmds: + x0, y0 = start + x1, y1 = stop + + #normalize + if x0 < 0: x0 = x0 + self._ncols + if x1 < 0: x1 = x1 + self._ncols + if y0 < 0: y0 = y0 + self._nrows + if y1 < 0: y1 = y1 + self._nrows + if x0 > x1: x0, x1 = x1, x0 + if y0 > y1: y0, y1 = y1, y0 + + if x0!=x1 or y0!=y1: + #column span + if x0!=x1: + for y in xrange(y0, y1+1): + for x in xrange(x0,x1+1): + csa((x,y)) + #row span + if y0!=y1: + for y in xrange(y0, y1+1): + for x in xrange(x0,x1+1): + rsa((x,y)) + + for y in xrange(y0, y1+1): + for x in xrange(x0,x1+1): + spanRanges[x,y] = None + # set the main entry + spanRanges[x0,y0] = (x0, y0, x1, y1) + + def _calcSpanRects(self): + """Work out rects for tables which do row and column spanning. + + Based on self._spanRanges, which is already known, + and the widths which were given or previously calculated, + self._spanRects shows the real coords for drawing: + (col, row) -> (x, y, width, height) + + for each cell. Any cell which 'does not exist' as another + has spanned over it will get a None entry on the right + """ + if getattr(self,'_spanRects',None): return + colpositions = self._colpositions + rowpositions = self._rowpositions + self._spanRects = spanRects = {} + self._vBlocks = vBlocks = {} + self._hBlocks = hBlocks = {} + for (coord, value) in self._spanRanges.items(): + if value is None: + spanRects[coord] = None + else: + col,row = coord + col0, row0, col1, row1 = value + if col1-col0>0: + for _ in xrange(col0+1,col1+1): + vBlocks.setdefault(colpositions[_],[]).append((rowpositions[row1+1],rowpositions[row0])) + if row1-row0>0: + for _ in xrange(row0+1,row1+1): + hBlocks.setdefault(rowpositions[_],[]).append((colpositions[col0],colpositions[col1+1])) + x = colpositions[col0] + y = rowpositions[row1+1] + width = colpositions[col1+1] - x + height = rowpositions[row0] - y + spanRects[coord] = (x, y, width, height) + + for _ in hBlocks, vBlocks: + for value in _.values(): + value.sort() + + def setStyle(self, tblstyle): + if type(tblstyle) is not TableStyleType: + tblstyle = TableStyle(tblstyle) + for cmd in tblstyle.getCommands(): + self._addCommand(cmd) + for k,v in tblstyle._opts.items(): + setattr(self,k,v) + for a in ('spaceBefore','spaceAfter'): + if not hasattr(self,a) and hasattr(tblstyle,a): + setattr(self,a,getattr(tblstyle,a)) + + def _addCommand(self,cmd): + if cmd[0] in ('BACKGROUND','ROWBACKGROUNDS','COLBACKGROUNDS'): + self._bkgrndcmds.append(cmd) + elif cmd[0] == 'SPAN': + self._spanCmds.append(cmd) + elif _isLineCommand(cmd): + # we expect op, start, stop, weight, colour, cap, dashes, join + cmd = list(cmd) + if len(cmd)<5: raise ValueError('bad line command '+str(cmd)) + + #determine line cap value at position 5. This can be string or numeric. + if len(cmd)<6: + cmd.append(1) + else: + cap = _convert2int(cmd[5], LINECAPS, 0, 2, 'cap', cmd) + cmd[5] = cap + + #dashes at index 6 - this is a dash array: + if len(cmd)<7: cmd.append(None) + + #join mode at index 7 - can be string or numeric, look up as for caps + if len(cmd)<8: cmd.append(1) + else: + join = _convert2int(cmd[7], LINEJOINS, 0, 2, 'join', cmd) + cmd[7] = join + + #linecount at index 8. Default is 1, set to 2 for double line. + if len(cmd)<9: cmd.append(1) + else: + lineCount = cmd[8] + if lineCount is None: + lineCount = 1 + cmd[8] = lineCount + assert lineCount >= 1 + #linespacing at index 9. Not applicable unless 2+ lines, defaults to line + #width so you get a visible gap between centres + if len(cmd)<10: cmd.append(cmd[3]) + else: + space = cmd[9] + if space is None: + space = cmd[3] + cmd[9] = space + assert len(cmd) == 10 + + self._linecmds.append(tuple(cmd)) + else: + (op, (sc, sr), (ec, er)), values = cmd[:3] , cmd[3:] + if sc < 0: sc = sc + self._ncols + if ec < 0: ec = ec + self._ncols + if sr < 0: sr = sr + self._nrows + if er < 0: er = er + self._nrows + for i in range(sr, er+1): + for j in range(sc, ec+1): + _setCellStyle(self._cellStyles, i, j, op, values) + + def _drawLines(self): + ccap, cdash, cjoin = None, None, None + self.canv.saveState() + for op, (sc,sr), (ec,er), weight, color, cap, dash, join, count, space in self._linecmds: + if type(sr) is type('') and sr.startswith('split'): continue + if sc < 0: sc = sc + self._ncols + if ec < 0: ec = ec + self._ncols + if sr < 0: sr = sr + self._nrows + if er < 0: er = er + self._nrows + if cap!=None and ccap!=cap: + self.canv.setLineCap(cap) + ccap = cap + if dash is None or dash == []: + if cdash is not None: + self.canv.setDash() + cdash = None + elif dash != cdash: + self.canv.setDash(dash) + cdash = dash + if join is not None and cjoin!=join: + self.canv.setLineJoin(join) + cjoin = join + getattr(self,_LineOpMap.get(op, '_drawUnknown' ))( (sc, sr), (ec, er), weight, color, count, space) + self.canv.restoreState() + self._curcolor = None + + def _drawUnknown(self, (sc, sr), (ec, er), weight, color, count, space): + raise ValueError, "Unknown line command '%s'" % op + + def _drawGrid(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawBox( (sc, sr), (ec, er), weight, color, count, space) + self._drawInnerGrid( (sc, sr), (ec, er), weight, color, count, space) + + def _drawBox(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawHLines((sc, sr), (ec, sr), weight, color, count, space) + self._drawHLines((sc, er+1), (ec, er+1), weight, color, count, space) + self._drawVLines((sc, sr), (sc, er), weight, color, count, space) + self._drawVLines((ec+1, sr), (ec+1, er), weight, color, count, space) + + def _drawInnerGrid(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawHLines((sc, sr+1), (ec, er), weight, color, count, space) + self._drawVLines((sc+1, sr), (ec, er), weight, color, count, space) + + def _prepLine(self, weight, color): + if color != self._curcolor: + self.canv.setStrokeColor(color) + self._curcolor = color + if weight != self._curweight: + self.canv.setLineWidth(weight) + self._curweight = weight + + def _drawHLines(self, (sc, sr), (ec, er), weight, color, count, space): + ecp = self._colpositions[sc:ec+2] + rp = self._rowpositions[sr:er+1] + if len(ecp)<=1 or len(rp)<1: return + self._prepLine(weight, color) + scp = ecp[0] + ecp = ecp[-1] + hBlocks = getattr(self,'_hBlocks',{}) + canvLine = self.canv.line + if count == 1: + for y in rp: + _hLine(canvLine, scp, ecp, y, hBlocks) + else: + lf = lambda x0,y0,x1,y1,canvLine=canvLine, ws=weight+space, count=count: _multiLine(x0,x1,y0,canvLine,ws,count) + for y in rp: + _hLine(lf, scp, ecp, y, hBlocks) + + def _drawHLinesB(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawHLines((sc, sr+1), (ec, er+1), weight, color, count, space) + + def _drawVLines(self, (sc, sr), (ec, er), weight, color, count, space): + erp = self._rowpositions[sr:er+2] + cp = self._colpositions[sc:ec+1] + if len(erp)<=1 or len(cp)<1: return + self._prepLine(weight, color) + srp = erp[0] + erp = erp[-1] + vBlocks = getattr(self,'_vBlocks',{}) + canvLine = lambda y0, x0, y1, x1, _line=self.canv.line: _line(x0,y0,x1,y1) + if count == 1: + for x in cp: + _hLine(canvLine, erp, srp, x, vBlocks) + else: + lf = lambda x0,y0,x1,y1,canvLine=canvLine, ws=weight+space, count=count: _multiLine(x0,x1,y0,canvLine,ws,count) + for x in cp: + _hLine(lf, erp, srp, x, vBlocks) + + def _drawVLinesA(self, (sc, sr), (ec, er), weight, color, count, space): + self._drawVLines((sc+1, sr), (ec+1, er), weight, color, count, space) + + def wrap(self, availWidth, availHeight): + self._calc(availWidth, availHeight) + #nice and easy, since they are predetermined size + self.availWidth = availWidth + return (self._width, self._height) + + def onSplit(self,T,byRow=1): + ''' + This method will be called when the Table is split. + Special purpose tables can override to do special stuff. + ''' + pass + + def _cr_0(self,n,cmds): + for c in cmds: + c = tuple(c) + (sc,sr), (ec,er) = c[1:3] + if sr>=n: continue + if er>=n: er = n-1 + self._addCommand((c[0],)+((sc, sr), (ec, er))+c[3:]) + + def _cr_1_1(self,n,repeatRows, cmds): + for c in cmds: + c = tuple(c) + (sc,sr), (ec,er) = c[1:3] + if sr in ('splitfirst','splitlast'): self._addCommand(c) + else: + if sr>=0 and sr>=repeatRows and sr=0 and er=repeatRows and sr=repeatRows and sr>=n: sr=sr+repeatRows-n + if er>=repeatRows and er=repeatRows and er>=n: er=er+repeatRows-n + self._addCommand((c[0],)+((sc, sr), (ec, er))+c[3:]) + + def _cr_1_0(self,n,cmds): + for c in cmds: + c = tuple(c) + (sc,sr), (ec,er) = c[1:3] + if sr in ('splitfirst','splitlast'): self._addCommand(c) + else: + if er>=0 and er=0 and sr=n: sr = sr-n + if er>=n: er = er-n + self._addCommand((c[0],)+((sc, sr), (ec, er))+c[3:]) + + def _splitRows(self,availHeight): + n=self._getFirstPossibleSplitRowPosition(availHeight) + if n<=self.repeatRows: return [] + lim = len(self._rowHeights) + if n==lim: return [self] + + repeatRows = self.repeatRows + repeatCols = self.repeatCols + splitByRow = self.splitByRow + data = self._cellvalues + + #we're going to split into two superRows + #R0 = slelf.__class__( data[:n], self._argW, self._argH[:n], + R0 = self.__class__( data[:n], self._colWidths, self._argH[:n], + repeatRows=repeatRows, repeatCols=repeatCols, + splitByRow=splitByRow) + + #copy the styles and commands + R0._cellStyles = self._cellStyles[:n] + + A = [] + # hack up the line commands + for op, (sc,sr), (ec,er), weight, color, cap, dash, join, count, space in self._linecmds: + if type(sr)is type('') and sr.startswith('split'): + A.append((op,(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space)) + if sr=='splitlast': + sr = er = n-1 + elif sr=='splitfirst': + sr = n + er = n + + if sc < 0: sc = sc + self._ncols + if ec < 0: ec = ec + self._ncols + if sr < 0: sr = sr + self._nrows + if er < 0: er = er + self._nrows + + if op in ('BOX','OUTLINE','GRID'): + if sr=n: + # we have to split the BOX + A.append(('LINEABOVE',(sc,sr), (ec,sr), weight, color, cap, dash, join, count, space)) + A.append(('LINEBEFORE',(sc,sr), (sc,er), weight, color, cap, dash, join, count, space)) + A.append(('LINEAFTER',(ec,sr), (ec,er), weight, color, cap, dash, join, count, space)) + A.append(('LINEBELOW',(sc,er), (ec,er), weight, color, cap, dash, join, count, space)) + if op=='GRID': + A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) + A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) + A.append(('INNERGRID',(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + else: + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + elif op in ('INNERGRID','LINEABOVE'): + if sr=n: + A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) + A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + elif op == 'LINEBELOW': + if sr=(n-1): + A.append(('LINEABOVE',(sc,n), (ec,n), weight, color, cap, dash, join, count, space)) + A.append((op,(sc,sr), (ec,er), weight, color)) + elif op == 'LINEABOVE': + if sr<=n and er>=n: + A.append(('LINEBELOW',(sc,n-1), (ec,n-1), weight, color, cap, dash, join, count, space)) + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + else: + A.append((op,(sc,sr), (ec,er), weight, color, cap, dash, join, count, space)) + + R0._cr_0(n,A) + R0._cr_0(n,self._bkgrndcmds) + R0._cr_0(n,self._spanCmds) + + if repeatRows: + #R1 = slelf.__class__(data[:repeatRows]+data[n:],self._argW, + R1 = self.__class__(data[:repeatRows]+data[n:],self._colWidths, + self._argH[:repeatRows]+self._argH[n:], + repeatRows=repeatRows, repeatCols=repeatCols, + splitByRow=splitByRow) + R1._cellStyles = self._cellStyles[:repeatRows]+self._cellStyles[n:] + R1._cr_1_1(n,repeatRows,A) + R1._cr_1_1(n,repeatRows,self._bkgrndcmds) + R1._cr_1_1(n,repeatRows,self._spanCmds) + else: + #R1 = slelf.__class__(data[n:], self._argW, self._argH[n:], + R1 = self.__class__(data[n:], self._colWidths, self._argH[n:], + repeatRows=repeatRows, repeatCols=repeatCols, + splitByRow=splitByRow) + R1._cellStyles = self._cellStyles[n:] + R1._cr_1_0(n,A) + R1._cr_1_0(n,self._bkgrndcmds) + R1._cr_1_0(n,self._spanCmds) + + + R0.hAlign = R1.hAlign = self.hAlign + R0.vAlign = R1.vAlign = self.vAlign + self.onSplit(R0) + self.onSplit(R1) + return [R0,R1] + + def _getFirstPossibleSplitRowPosition(self,availHeight): + if self._spanCmds: + impossible={} + for xy in self._rowSpanCells: + r=self._spanRanges[xy] + if r!=None: + y1,y2=r[1],r[3] + if y1!=y2: + ymin=min(y1,y2) #normalize + ymax=max(y1,y2) #normalize + y=ymin+1 + while 1: + if y>ymax: break + impossible[y]=None #split at position y is impossible because of overlapping rowspan + y=y+1 + else: + impossible={} # any split possible because table does *not* have rowspans + h = 0 + n = 1 + split_at = 0 # from this point of view 0 is the first position where the table may *always* be splitted + for rh in self._rowHeights: + if h+rh>availHeight: + break + if not impossible.has_key(n): + split_at=n + h=h+rh + n=n+1 + return split_at + + def split(self, availWidth, availHeight): + self._calc(availWidth, availHeight) + if self.splitByRow: + if not rl_config.allowTableBoundsErrors and self._width>availWidth: return [] + return self._splitRows(availHeight) + else: + raise NotImplementedError + + def draw(self): + self._curweight = self._curcolor = self._curcellstyle = None + self._drawBkgrnd() + if self._spanCmds == []: + # old fashioned case, no spanning, steam on and do each cell + for row, rowstyle, rowpos, rowheight in map(None, self._cellvalues, self._cellStyles, self._rowpositions[1:], self._rowHeights): + for cellval, cellstyle, colpos, colwidth in map(None, row, rowstyle, self._colpositions[:-1], self._colWidths): + self._drawCell(cellval, cellstyle, (colpos, rowpos), (colwidth, rowheight)) + else: + # we have some row or col spans, need a more complex algorithm + # to find the rect for each + for rowNo in range(self._nrows): + for colNo in range(self._ncols): + cellRect = self._spanRects[colNo, rowNo] + if cellRect is not None: + (x, y, width, height) = cellRect + cellval = self._cellvalues[rowNo][colNo] + cellstyle = self._cellStyles[rowNo][colNo] + self._drawCell(cellval, cellstyle, (x, y), (width, height)) + self._drawLines() + + + def _drawBkgrnd(self): + nrows = self._nrows + ncols = self._ncols + for cmd, (sc, sr), (ec, er), arg in self._bkgrndcmds: + if sc < 0: sc = sc + ncols + if ec < 0: ec = ec + ncols + if sr < 0: sr = sr + nrows + if er < 0: er = er + nrows + x0 = self._colpositions[sc] + y0 = self._rowpositions[sr] + x1 = self._colpositions[min(ec+1,ncols)] + y1 = self._rowpositions[min(er+1,nrows)] + w, h = x1-x0, y1-y0 + canv = self.canv + if callable(arg): + apply(arg,(self,canv, x0, y0, w, h)) + elif cmd == 'ROWBACKGROUNDS': + #Need a list of colors to cycle through. The arguments + #might be already colours, or convertible to colors, or + # None, or the string 'None'. + #It's very common to alternate a pale shade with None. + colorCycle = map(colors.toColorOrNone, arg) + count = len(colorCycle) + rowCount = er - sr + 1 + for i in range(rowCount): + color = colorCycle[i%count] + h = self._rowHeights[sr + i] + if color: + canv.setFillColor(color) + canv.rect(x0, y0, w, -h, stroke=0,fill=1) + y0 = y0 - h + + elif cmd == 'COLBACKGROUNDS': + #cycle through colours columnwise + colorCycle = map(colors.toColorOrNone, arg) + count = len(colorCycle) + colCount = ec - sc + 1 + for i in range(colCount): + color = colorCycle[i%count] + w = self._colWidths[sc + i] + if color: + canv.setFillColor(color) + canv.rect(x0, y0, w, h, stroke=0,fill=1) + x0 = x0 +w + else: #cmd=='BACKGROUND' + canv.setFillColor(colors.toColor(arg)) + canv.rect(x0, y0, w, h, stroke=0,fill=1) + + def _drawCell(self, cellval, cellstyle, (colpos, rowpos), (colwidth, rowheight)): + if self._curcellstyle is not cellstyle: + cur = self._curcellstyle + if cur is None or cellstyle.color != cur.color: + self.canv.setFillColor(cellstyle.color) + if cur is None or cellstyle.leading != cur.leading or cellstyle.fontname != cur.fontname or cellstyle.fontsize != cur.fontsize: + self.canv.setFont(cellstyle.fontname, cellstyle.fontsize, cellstyle.leading) + self._curcellstyle = cellstyle + + just = cellstyle.alignment + valign = cellstyle.valign + n = type(cellval) + if n in _SeqTypes or isinstance(cellval,Flowable): + if not n in _SeqTypes: cellval = (cellval,) + # we assume it's a list of Flowables + W = [] + H = [] + w, h = self._listCellGeom(cellval,colwidth,cellstyle,W=W, H=H,aH=rowheight) + if valign=='TOP': + y = rowpos + rowheight - cellstyle.topPadding + elif valign=='BOTTOM': + y = rowpos+cellstyle.bottomPadding + h + else: + y = rowpos+(rowheight+cellstyle.bottomPadding-cellstyle.topPadding+h)/2.0 + if cellval: y += cellval[0].getSpaceBefore() + for v, w, h in map(None,cellval,W,H): + if just=='LEFT': x = colpos+cellstyle.leftPadding + elif just=='RIGHT': x = colpos+colwidth-cellstyle.rightPadding - w + elif just in ('CENTRE', 'CENTER'): + x = colpos+(colwidth+cellstyle.leftPadding-cellstyle.rightPadding-w)/2.0 + else: + raise ValueError, 'Invalid justification %s' % just + y -= v.getSpaceBefore() + y -= h + v.drawOn(self.canv,x,y) + y -= v.getSpaceAfter() + else: + if just == 'LEFT': + draw = self.canv.drawString + x = colpos + cellstyle.leftPadding + elif just in ('CENTRE', 'CENTER'): + draw = self.canv.drawCentredString + x = colpos + colwidth * 0.5 + elif just == 'RIGHT': + draw = self.canv.drawRightString + x = colpos + colwidth - cellstyle.rightPadding + elif just == 'DECIMAL': + draw = self.canv.drawAlignedString + x = colpos + colwidth - cellstyle.rightPadding + else: + raise ValueError, 'Invalid justification %s' % just + vals = string.split(str(cellval), "\n") + n = len(vals) + leading = cellstyle.leading + fontsize = cellstyle.fontsize + if valign=='BOTTOM': + y = rowpos + cellstyle.bottomPadding+n*leading-fontsize + elif valign=='TOP': + y = rowpos + rowheight - cellstyle.topPadding - fontsize + elif valign=='MIDDLE': + #tim roberts pointed out missing fontsize correction 2004-10-04 + y = rowpos + (cellstyle.bottomPadding + rowheight-cellstyle.topPadding+n*leading)/2.0 - fontsize + else: + raise ValueError, "Bad valign: '%s'" % str(valign) + + for v in vals: + draw(x, y, v) + y -= leading + + if cellstyle.href: + #external hyperlink + self.canv.linkURL(cellstyle.href, (colpos, rowpos, colpos + colwidth, rowpos + rowheight), relative=1) + if cellstyle.destination: + #external hyperlink + self.canv.linkRect("", cellstyle.destination, Rect=(colpos, rowpos, colpos + colwidth, rowpos + rowheight), relative=1) + +_LineOpMap = { 'GRID':'_drawGrid', + 'BOX':'_drawBox', + 'OUTLINE':'_drawBox', + 'INNERGRID':'_drawInnerGrid', + 'LINEBELOW':'_drawHLinesB', + 'LINEABOVE':'_drawHLines', + 'LINEBEFORE':'_drawVLines', + 'LINEAFTER':'_drawVLinesA', } + +class LongTable(Table): + '''Henning von Bargen's changes will be active''' + _longTableOptimize = 1 + +LINECOMMANDS = _LineOpMap.keys() + +def _isLineCommand(cmd): + return cmd[0] in LINECOMMANDS + +def _setCellStyle(cellStyles, i, j, op, values): + #new = CellStyle('<%d, %d>' % (i,j), cellStyles[i][j]) + #cellStyles[i][j] = new + ## modify in place!!! + new = cellStyles[i][j] + if op == 'FONT': + n = len(values) + new.fontname = values[0] + if n>1: + new.fontsize = values[1] + if n>2: + new.leading = values[2] + else: + new.leading = new.fontsize*1.2 + elif op in ('FONTNAME', 'FACE'): + new.fontname = values[0] + elif op in ('SIZE', 'FONTSIZE'): + new.fontsize = values[0] + elif op == 'LEADING': + new.leading = values[0] + elif op == 'TEXTCOLOR': + new.color = colors.toColor(values[0], colors.Color(0,0,0)) + elif op in ('ALIGN', 'ALIGNMENT'): + new.alignment = values[0] + elif op == 'VALIGN': + new.valign = values[0] + elif op == 'LEFTPADDING': + new.leftPadding = values[0] + elif op == 'RIGHTPADDING': + new.rightPadding = values[0] + elif op == 'TOPPADDING': + new.topPadding = values[0] + elif op == 'BOTTOMPADDING': + new.bottomPadding = values[0] + elif op == 'HREF': + new.href = values[0] + elif op == 'DESTINATION': + new.destination = values[0] + +GRID_STYLE = TableStyle( + [('GRID', (0,0), (-1,-1), 0.25, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +BOX_STYLE = TableStyle( + [('BOX', (0,0), (-1,-1), 0.50, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +LABELED_GRID_STYLE = TableStyle( + [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 2, colors.black), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEAFTER', (0,0), (0,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +COLORED_GRID_STYLE = TableStyle( + [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 2, colors.red), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEAFTER', (0,0), (0,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) +LIST_STYLE = TableStyle( + [('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + + +# experimental iterator which can apply a sequence +# of colors e.g. Blue, None, Blue, None as you move +# down. + + +if __name__ == '__main__': + from reportlab.test.test_platypus_tables import old_tables_test + old_tables_test() diff --git a/bin/reportlab/platypus/xpreformatted.py b/bin/reportlab/platypus/xpreformatted.py new file mode 100644 index 00000000000..73ebe37687c --- /dev/null +++ b/bin/reportlab/platypus/xpreformatted.py @@ -0,0 +1,316 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/platypus/xpreformatted.py +__version__=''' $Id: xpreformatted.py 2426 2004-09-02 11:52:56Z rgbecker $ ''' + +import string +from types import StringType, ListType + +from reportlab.lib import PyFontify +from paragraph import Paragraph, cleanBlockQuotedText, _handleBulletWidth, \ + ParaLines, _getFragWords, stringWidth, _sameFrag +from flowables import _dedenter + + +def _getFragLines(frags): + lines = [] + cline = [] + W = frags[:] + while W != []: + w = W[0] + t = w.text + del W[0] + i = string.find(t,'\n') + if i>=0: + tleft = t[i+1:] + cline.append(w.clone(text=t[:i])) + lines.append(cline) + cline = [] + if tleft!='': + W.insert(0,w.clone(text=tleft)) + else: + cline.append(w) + if cline!=[]: + lines.append(cline) + return lines + +def _split_blPara(blPara,start,stop): + f = blPara.clone() + for a in ('lines', 'text'): + if hasattr(f,a): delattr(f,a) + f.lines = blPara.lines[start:stop] + return [f] + +# Will be removed shortly. +def _countSpaces(text): + return string.count(text, ' ') +## i = 0 +## s = 0 +## while 1: +## j = string.find(text,' ',i) +## if j<0: return s +## s = s + 1 +## i = j + 1 + +def _getFragWord(frags): + ''' given a fragment list return a list of lists + [size, spaces, (f00,w00), ..., (f0n,w0n)] + each pair f,w represents a style and some string + ''' + W = [] + n = 0 + s = 0 + for f in frags: + text = f.text[:] + W.append((f,text)) + n = n + stringWidth(text, f.fontName, f.fontSize) + + #s = s + _countSpaces(text) + s = s + string.count(text, ' ') # much faster for many blanks + + #del f.text # we can't do this until we sort out splitting + # of paragraphs + return n, s, W + + +class XPreformatted(Paragraph): + def __init__(self, text, style, bulletText = None, frags=None, caseSensitive=1, dedent=0): + self.caseSensitive = caseSensitive + cleaner = lambda text, dedent=dedent: string.join(_dedenter(text or '',dedent),'\n') + self._setup(text, style, bulletText, frags, cleaner) + + def breakLines(self, width): + """ + Returns a broken line structure. There are two cases + + A) For the simple case of a single formatting input fragment the output is + A fragment specifier with + kind = 0 + fontName, fontSize, leading, textColor + lines= A list of lines + Each line has two items. + 1) unused width in points + 2) a list of words + + B) When there is more than one input formatting fragment the out put is + A fragment specifier with + kind = 1 + lines= A list of fragments each having fields + extraspace (needed for justified) + fontSize + words=word list + each word is itself a fragment with + various settings + + This structure can be used to easily draw paragraphs with the various alignments. + You can supply either a single width or a list of widths; the latter will have its + last item repeated until necessary. A 2-element list is useful when there is a + different first line indent; a longer list could be created to facilitate custom wraps + around irregular objects.""" + + if type(width) <> ListType: maxWidths = [width] + else: maxWidths = width + lines = [] + lineno = 0 + maxWidth = maxWidths[lineno] + style = self.style + fFontSize = float(style.fontSize) + requiredWidth = 0 + + #for bullets, work out width and ensure we wrap the right amount onto line one + _handleBulletWidth(self.bulletText,style,maxWidths) + + self.height = 0 + frags = self.frags + nFrags= len(frags) + if nFrags==1: + f = frags[0] + if hasattr(f,'text'): + fontSize = f.fontSize + fontName = f.fontName + kind = 0 + L=string.split(f.text, '\n') + for l in L: + currentWidth = stringWidth(l,fontName,fontSize) + requiredWidth = max(currentWidth,requiredWidth) + extraSpace = maxWidth-currentWidth + lines.append((extraSpace,string.split(l,' '),currentWidth)) + lineno = lineno+1 + maxWidth = lineno', ''), + 'keyword' : ('', ''), + 'parameter' : ('', ''), + 'identifier' : ('', ''), + 'string' : ('', '') } + + def __init__(self, text, style, bulletText = None, dedent=0, frags=None): + if text: + text = self.fontify(self.escapeHtml(text)) + apply(XPreformatted.__init__, + (self, text, style), + {'bulletText':bulletText, 'dedent':dedent, 'frags':frags}) + + def escapeHtml(self, text): + s = string.replace(text, '&', '&') + s = string.replace(s, '<', '<') + s = string.replace(s, '>', '>') + return s + + def fontify(self, code): + "Return a fontified version of some Python code." + + if code[0] == '\n': + code = code[1:] + + tags = PyFontify.fontify(code) + fontifiedCode = '' + pos = 0 + for k, i, j, dummy in tags: + fontifiedCode = fontifiedCode + code[pos:i] + s, e = self.formats[k] + fontifiedCode = fontifiedCode + s + code[i:j] + e + pos = j + + fontifiedCode = fontifiedCode + code[pos:] + + return fontifiedCode + + +if __name__=='__main__': #NORUNTESTS + def dumpXPreformattedLines(P): + print '\n############dumpXPreforemattedLines(%s)' % str(P) + lines = P.blPara.lines + n =len(lines) + for l in range(n): + line = lines[l] + words = line.words + nwords = len(words) + print 'line%d: %d(%d)\n ' % (l,nwords,line.wordCount), + for w in range(nwords): + print "%d:'%s'"%(w,words[w].text), + print + + def dumpXPreformattedFrags(P): + print '\n############dumpXPreforemattedFrags(%s)' % str(P) + frags = P.frags + n =len(frags) + for l in range(n): + print "frag%d: '%s'" % (l, frags[l].text) + + l = 0 + for L in _getFragLines(frags): + n=0 + for W in _getFragWords(L): + print "frag%d.%d: size=%d" % (l, n, W[0]), + n = n + 1 + for w in W[1:]: + print "'%s'" % w[1], + print + l = l + 1 + + def try_it(text,style,dedent,aW,aH): + P=XPreformatted(text,style,dedent=dedent) + dumpXPreformattedFrags(P) + w,h = P.wrap(aW, aH) + dumpXPreformattedLines(P) + S = P.split(aW,aH) + dumpXPreformattedLines(P) + for s in S: + s.wrap(aW,aH) + dumpXPreformattedLines(s) + aH = 500 + + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + styleSheet = getSampleStyleSheet() + B = styleSheet['BodyText'] + DTstyle = ParagraphStyle("discussiontext", parent=B) + DTstyle.fontName= 'Helvetica' + for (text,dedent,style, aW, aH, active) in [(''' + + +The CMYK or subtractive + +method follows the way a printer +mixes three pigments (cyan, magenta, and yellow) to form colors. +Because mixing chemicals is more difficult than combining light there +is a fourth parameter for darkness. For example a chemical +combination of the CMY pigments generally never makes a perfect + +black -- instead producing a muddy color -- so, to get black printers +don't use the CMY pigments but use a direct black ink. Because +CMYK maps more directly to the way printer hardware works it may +be the case that &| & | colors specified in CMYK will provide better fidelity +and better control when printed. + + +''',0,DTstyle, 456.0, 42.8, 0), +(''' + + This is a non rearranging form of the Paragraph class; + XML tags are allowed in text and have the same + + meanings as for the Paragraph class. + As for Preformatted, if dedent is non zero dedent + common leading spaces will be removed from the + front of each line. + +''',3, DTstyle, 456.0, 42.8, 0), +("""\ + class FastXMLParser: + # Nonsense method + def nonsense(self): + self.foo = 'bar' +""",0, styleSheet['Code'], 456.0, 4.8, 1), +]: + if active: try_it(text,style,dedent,aW,aH) diff --git a/bin/reportlab/rl_config.py b/bin/reportlab/rl_config.py new file mode 100644 index 00000000000..fed48d5d027 --- /dev/null +++ b/bin/reportlab/rl_config.py @@ -0,0 +1,155 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/rl_config.py +__version__=''' $Id: rl_config.py 2899 2006-05-22 12:42:07Z rgbecker $ ''' + +allowTableBoundsErrors = 1 # set to 0 to die on too large elements in tables in debug (recommend 1 for production use) +shapeChecking = 1 +defaultEncoding = 'WinAnsiEncoding' # 'WinAnsi' or 'MacRoman' +defaultGraphicsFontName= 'Times-Roman' #initializer for STATE_DEFAULTS in shapes.py +pageCompression = 1 # default page compression mode +defaultPageSize = 'A4' #default page size +defaultImageCaching = 0 #set to zero to remove those annoying cached images +ZLIB_WARNINGS = 1 +warnOnMissingFontGlyphs = 0 #if 1, warns of each missing glyph +verbose = 0 +showBoundary = 0 # turns on and off boundary behaviour in Drawing +emptyTableAction= 'error' # one of 'error', 'indicate', 'ignore' +invariant= 0 #produces repeatable,identical PDFs with same timestamp info (for regression testing) +eps_preview_transparent= None #set to white etc +overlapAttachedSpace= 1 #if set non false then adajacent flowable space after + #and space before are merged (max space is used). +longTableOptimize = 0 #default don't use Henning von Bargen's long table optimizations +autoConvertEncoding = 0 #convert internally as needed (experimental) +_FUZZ= 1e-6 #fuzz for layout arithmetic + +# places to look for T1Font information +T1SearchPath = ( + 'c:/Program Files/Adobe/Acrobat 9.0/Resource/Font', + 'c:/Program Files/Adobe/Acrobat 8.0/Resource/Font', + 'c:/Program Files/Adobe/Acrobat 7.0/Resource/Font', + 'c:/Program Files/Adobe/Acrobat 6.0/Resource/Font', #Win32, Acrobat 6 + 'c:/Program Files/Adobe/Acrobat 5.0/Resource/Font', #Win32, Acrobat 5 + 'c:/Program Files/Adobe/Acrobat 4.0/Resource/Font', #Win32, Acrobat 4 + '%(disk)s/Applications/Python %(sys_version)s/reportlab/fonts', #Mac? + '/usr/lib/Acrobat9/Resource/Font', #Linux, Acrobat 5? + '/usr/lib/Acrobat8/Resource/Font', #Linux, Acrobat 5? + '/usr/lib/Acrobat7/Resource/Font', #Linux, Acrobat 5? + '/usr/lib/Acrobat6/Resource/Font', #Linux, Acrobat 5? + '/usr/lib/Acrobat5/Resource/Font', #Linux, Acrobat 5? + '/usr/lib/Acrobat4/Resource/Font', #Linux, Acrobat 4 + '/usr/local/Acrobat9/Resource/Font', #Linux, Acrobat 5? + '/usr/local/Acrobat8/Resource/Font', #Linux, Acrobat 5? + '/usr/local/Acrobat7/Resource/Font', #Linux, Acrobat 5? + '/usr/local/Acrobat6/Resource/Font', #Linux, Acrobat 5? + '/usr/local/Acrobat5/Resource/Font', #Linux, Acrobat 5? + '/usr/local/Acrobat4/Resource/Font', #Linux, Acrobat 4 + '%(REPORTLAB_DIR)s/fonts', #special + '%(REPORTLAB_DIR)s/../fonts', #special + '%(REPORTLAB_DIR)s/../../fonts', #special + '%(HOME)s/fonts', #special + ) + +# places to look for TT Font information +TTFSearchPath = ( + 'c:/winnt/fonts', + 'c:/windows/fonts', + '/usr/lib/X11/fonts/TrueType/', + '%(REPORTLAB_DIR)s/fonts', #special + '%(REPORTLAB_DIR)s/../fonts', #special + '%(REPORTLAB_DIR)s/../../fonts',#special + '%(HOME)s/fonts', #special + ) + +# places to look for CMap files - should ideally merge with above +CMapSearchPath = ( + '/usr/lib/Acrobat9/Resource/CMap', + '/usr/lib/Acrobat8/Resource/CMap', + '/usr/lib/Acrobat7/Resource/CMap', + '/usr/lib/Acrobat6/Resource/CMap', + '/usr/lib/Acrobat5/Resource/CMap', + '/usr/lib/Acrobat4/Resource/CMap', + '/usr/local/Acrobat9/Resource/CMap', + '/usr/local/Acrobat8/Resource/CMap', + '/usr/local/Acrobat7/Resource/CMap', + '/usr/local/Acrobat6/Resource/CMap', + '/usr/local/Acrobat5/Resource/CMap', + '/usr/local/Acrobat4/Resource/CMap', + 'C:\\Program Files\\Adobe\\Acrobat\\Resource\\CMap', + 'C:\\Program Files\\Adobe\\Acrobat 9.0\\Resource\\CMap', + 'C:\\Program Files\\Adobe\\Acrobat 8.0\\Resource\\CMap', + 'C:\\Program Files\\Adobe\\Acrobat 7.0\\Resource\\CMap', + 'C:\\Program Files\\Adobe\\Acrobat 6.0\\Resource\\CMap', + 'C:\\Program Files\\Adobe\\Acrobat 5.0\\Resource\\CMap', + 'C:\\Program Files\\Adobe\\Acrobat 4.0\\Resource\\CMap', + '%(REPORTLAB_DIR)s/fonts/CMap', #special + '%(REPORTLAB_DIR)s/../fonts/CMap', #special + '%(REPORTLAB_DIR)s/../../fonts/CMap', #special + '%(HOME)s/fonts/CMap', #special + ) + +#### Normally don't need to edit below here #### +try: + from local_rl_config import * +except: + pass + +_SAVED = {} +sys_version=None + +def _setOpt(name, value, conv=None): + '''set a module level value from environ/default''' + from os import environ + ename = 'RL_'+name + if environ.has_key(ename): + value = environ[ename] + if conv: value = conv(value) + globals()[name] = value + +def _startUp(): + '''This function allows easy resetting to the global defaults + If the environment contains 'RL_xxx' then we use the value + else we use the given default''' + V = ('T1SearchPath','CMapSearchPath', 'TTFSearchPath', + 'shapeChecking', 'defaultEncoding', + 'pageCompression', 'defaultPageSize', 'defaultImageCaching', + 'ZLIB_WARNINGS', 'warnOnMissingFontGlyphs', 'verbose', 'emptyTableAction', + 'invariant','eps_preview_transparent', + ) + import os, sys, string + global sys_version, _unset_ + sys_version = string.split(sys.version)[0] #strip off the other garbage + from reportlab.lib import pagesizes + from reportlab.lib.utils import rl_isdir + + if _SAVED=={}: + _unset_ = getattr(sys,'_rl_config__unset_',None) + if _unset_ is None: + class _unset_: pass + sys._rl_config__unset_ = _unset_ = _unset_() + for k in V: + _SAVED[k] = globals()[k] + + #places to search for Type 1 Font files + import reportlab + D = {'REPORTLAB_DIR': os.path.abspath(os.path.dirname(reportlab.__file__)), + 'HOME': os.environ.get('HOME',os.getcwd()), + 'disk': string.split(os.getcwd(), ':')[0], + 'sys_version': sys_version, + } + + for name in ('T1SearchPath','TTFSearchPath','CMapSearchPath'): + P=[] + for p in _SAVED[name]: + d = string.replace(p % D,'/',os.sep) + if rl_isdir(d): P.append(d) + _setOpt(name,P) + + for k in V[3:]: + v = _SAVED[k] + if type(v)==type(1): conv = int + elif k=='defaultPageSize': conv = lambda v,M=pagesizes: getattr(M,v) + else: conv = None + _setOpt(k,v,conv) + +_startUp() diff --git a/bin/reportlab/setup.py b/bin/reportlab/setup.py new file mode 100644 index 00000000000..96f7d1ee69e --- /dev/null +++ b/bin/reportlab/setup.py @@ -0,0 +1,258 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/setup.py +__version__=''' $Id: setup.py 2511 2005-01-14 15:42:12Z rgbecker $ ''' + +import os, sys, distutils, glob +from distutils.core import setup, Extension + +# from Zope - App.Common.package_home + +def package_home(globals_dict): + __name__=globals_dict['__name__'] + m=sys.modules[__name__] + r=os.path.split(m.__path__[0])[0] + return r + +pjoin = os.path.join +abspath = os.path.abspath +isfile = os.path.isfile +isdir = os.path.isfile +dirname = os.path.dirname +package_path = pjoin(package_home(distutils.__dict__), 'site-packages', 'reportlab') + +def get_version(): + #determine Version + if __name__=='__main__': + HERE=os.path.dirname(sys.argv[0]) + else: + HERE=os.path.dirname(__file__) + + #first try source + FN = pjoin(HERE,'__init__') + try: + for l in open(pjoin(FN+'.py'),'r').readlines(): + if l.startswith('Version'): + exec l.strip() + return Version + except: + pass + + #don't have source, try import + import imp + for desc in ('.pyc', 'rb', 2), ('.pyo', 'rb', 2): + try: + fn = FN+desc[0] + f = open(fn,desc[1]) + m = imp.load_module('reportlab',f,fn,desc) + return m.Version + except: + pass + raise ValueError('Cannot determine ReportLab Version') + +def getVersionFromCCode(fn): + import re + tag = re.search(r'^#define\s+VERSION\s+"([^"]*)"',open(fn,'r').read(),re.M) + return tag and tag.group(1) or '' + +def _rl_accel_dir_info(dir): + import stat + fn = pjoin(dir,'_rl_accel') + return getVersionFromCCode(fn),os.stat(fn)[stat.ST_MTIME] + +def _cmp_rl_accel_dirs(a,b): + return cmp(_rl_accel_dir_info(b),__rl_accel_dir_info(a)) + +def _find_rl_accel(): + '''locate where the accelerator code lives''' + _ = [] + for x in [ + './rl_addons/rl_accel', + '../rl_addons/rl_accel', + '../../rl_addons/rl_accel', + './rl_accel', + '../rl_accel', + '../../rl_accel', + './lib'] \ + + glob.glob('./rl_accel-*/rl_accel')\ + + glob.glob('../rl_accel-*/rl_accel') \ + + glob.glob('../../rl_accel-*/rl_accel') \ + : + fn = pjoin(x,'_rl_accel.c') + if isfile(pjoin(x,'_rl_accel.c')): + _.append(x) + if _: + if len(_)>1: _.sort(_cmp_rl_accel_dirs) + return abspath(_[0]) + return None + +_FILES= [ + 'README', + 'changes', + 'license.txt', + + 'demos/colors/colortest.py', + 'demos/gadflypaper/00readme.txt', + 'demos/gadflypaper/gfe.py', + 'demos/odyssey/00readme.txt', + 'demos/odyssey/dodyssey.py', + 'demos/odyssey/fodyssey.py', + 'demos/odyssey/odyssey.py', + 'demos/odyssey/odyssey.txt', + 'demos/rlzope/readme.txt', + 'demos/rlzope/rlzope.py', + 'demos/stdfonts/00readme.txt', + 'demos/stdfonts/stdfonts.py', + 'demos/tests/testdemos.py', + + 'docs/00readme.txt', + 'docs/genAll.py', + 'docs/graphguide/ch1_intro.py', + 'docs/graphguide/ch2_concepts.py', + 'docs/graphguide/ch3_shapes.py', + 'docs/graphguide/ch4_widgets.py', + 'docs/graphguide/ch5_charts.py', + 'docs/graphguide/gengraphguide.py', + 'docs/images/Edit_Prefs.gif', + 'docs/images/Python_21.gif', + 'docs/images/Python_21_HINT.gif', + 'docs/images/fileExchange.gif', + 'docs/images/jpn.gif', + 'docs/images/jpnchars.jpg', + 'docs/images/lj8100.jpg', + 'docs/images/replogo.a85', + 'docs/images/replogo.gif', + 'docs/reference/build.bat', + 'docs/reference/genreference.py', + 'docs/reference/reference.yml', + 'docs/userguide/app_demos.py', + 'docs/userguide/ch1_intro.py', + 'docs/userguide/ch2_graphics.py', + 'docs/userguide/ch2a_fonts.py', + 'docs/userguide/ch3_pdffeatures.py', + 'docs/userguide/ch4_platypus_concepts.py', + 'docs/userguide/ch5_paragraphs.py', + 'docs/userguide/ch6_tables.py', + 'docs/userguide/ch7_custom.py', + 'docs/userguide/ch9_future.py', + 'docs/userguide/genuserguide.py', + 'docs/userguide/testfile.txt', + + 'extensions/README', + + 'fonts/00readme.txt', + 'fonts/LeERC___.AFM', + 'fonts/LeERC___.PFB', + 'fonts/luxiserif.ttf', + 'fonts/luxiserif_license.txt', + 'fonts/rina.ttf', + 'fonts/rina_license.txt', + + 'test/pythonpowered.gif', + + 'tools/README', + 'tools/docco/README', + 'tools/py2pdf/README', + 'tools/py2pdf/demo-config.txt', + 'tools/py2pdf/vertpython.jpg', + 'tools/pythonpoint/README', + 'tools/pythonpoint/pythonpoint.dtd', + 'tools/pythonpoint/demos/htu.xml', + 'tools/pythonpoint/demos/LeERC___.AFM', + 'tools/pythonpoint/demos/LeERC___.PFB', + 'tools/pythonpoint/demos/leftlogo.a85', + 'tools/pythonpoint/demos/leftlogo.gif', + 'tools/pythonpoint/demos/lj8100.jpg', + 'tools/pythonpoint/demos/monterey.xml', + 'tools/pythonpoint/demos/outline.gif', + 'tools/pythonpoint/demos/pplogo.gif', + 'tools/pythonpoint/demos/python.gif', + 'tools/pythonpoint/demos/pythonpoint.xml', + 'tools/pythonpoint/demos/spectrum.png', + 'tools/pythonpoint/demos/vertpython.gif', + ] +# why oh why don't most setup scripts have a script handler? +# if you don't have one, you can't edit in Pythonwin +def run(): + RL_ACCEL = _find_rl_accel() + LIBS = [] + DATA_FILES = {} + if not RL_ACCEL: + EXT_MODULES = [] + print '***************************************************' + print '*No rl_accel code found, you can obtain it at *' + print '*http://www.reportlab.org/downloads.html#_rl_accel*' + print '***************************************************' + else: + fn = pjoin(RL_ACCEL,'lib','hyphen.mashed') + if isfile(fn): DATA_FILES[pjoin(package_path, 'lib')] = [fn] + EXT_MODULES = [ + Extension( '_rl_accel', + [pjoin(RL_ACCEL,'_rl_accel.c')], + include_dirs=[], + define_macros=[], + library_dirs=[], + libraries=LIBS, # libraries to link against + ), + Extension( 'sgmlop', + [pjoin(RL_ACCEL,'sgmlop.c')], + include_dirs=[], + define_macros=[], + library_dirs=[], + libraries=LIBS, # libraries to link against + ), + Extension( 'pyHnj', + [pjoin(RL_ACCEL,'pyHnjmodule.c'), + pjoin(RL_ACCEL,'hyphen.c'), + pjoin(RL_ACCEL,'hnjalloc.c')], + include_dirs=[], + define_macros=[], + library_dirs=[], + libraries=LIBS, # libraries to link against + ), + ] + for fn in _FILES: + fn = os.sep.join(fn.split('/')) + if isfile(fn): + tn = dirname(fn) + tn = tn and pjoin(package_path,tn) or package_path + DATA_FILES.setdefault(tn,[]).append(fn) + setup( + name="Reportlab", + version=get_version(), + license="BSD license (see license.txt for details), Copyright (c) 2000-2003, ReportLab Inc.", + description="The Reportlab Toolkit", + long_description="""The ReportLab Toolkit. +An Open Source Python library for generating PDFs and graphics. +""", + + author="Robinson, Watters, Becker, Precedo and many more...", + author_email="info@reportlab.com", + url="http://www.reportlab.com/", + + package_dir = {'reportlab': '.'}, + + packages=[ # include anything with an __init__ + 'reportlab', + 'reportlab.extensions', + 'reportlab.graphics.charts', + 'reportlab.graphics.samples', + 'reportlab.graphics.widgets', + 'reportlab.graphics', + 'reportlab.lib', + 'reportlab.pdfbase', + 'reportlab.pdfgen', + 'reportlab.platypus', + 'reportlab.test', + 'reportlab.tools.docco', + 'reportlab.tools.py2pdf', + 'reportlab.tools.pythonpoint.styles', + 'reportlab.tools.pythonpoint', + 'reportlab.tools', + ], + data_files = DATA_FILES.items(), + ext_modules = EXT_MODULES, + ) + +if __name__=='__main__': + run() diff --git a/bin/reportlab/test/00readme.txt b/bin/reportlab/test/00readme.txt new file mode 100644 index 00000000000..1b1a0b3c6e2 --- /dev/null +++ b/bin/reportlab/test/00readme.txt @@ -0,0 +1,20 @@ +This directory should contain all test scripts. + +All test scripts should be named like test_*.py, where * describes which +parts of the ReportLab toolkit are being tested (see sample test scripts). + +The test scripts are expected to make use of the PyUnit test environment +named unittest (see pyunit.sourceforge.net). For convenience this comes +bundled with the ReportLab toolkit in the reportlab.test subpackage. + +As of now, this folder has a flat structure, but it might be restructured +in the future as the amount of tests will grow dramatically. + +The file runAll.py begins by deleting any files with extension ".pdf" or +".log" in the test directory, so you can't confuse old and current +test output. It then loops over all test scripts following the +aforementioned pattern and executes them. + +Any PDF or log files written should be examined, at least before +major releases. If you are writing tests, ensure that you only +leave behind files which you intend a human to check. diff --git a/bin/reportlab/test/__init__.py b/bin/reportlab/test/__init__.py new file mode 100644 index 00000000000..64ff636921c --- /dev/null +++ b/bin/reportlab/test/__init__.py @@ -0,0 +1,3 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/__init__.py diff --git a/bin/reportlab/test/pythonpowered.gif b/bin/reportlab/test/pythonpowered.gif new file mode 100644 index 00000000000..34e774c937e Binary files /dev/null and b/bin/reportlab/test/pythonpowered.gif differ diff --git a/bin/reportlab/test/runAll.py b/bin/reportlab/test/runAll.py new file mode 100644 index 00000000000..390fb444d6b --- /dev/null +++ b/bin/reportlab/test/runAll.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/runAll.py +"""Runs all test files in all subfolders. +""" +import os, glob, sys, string, traceback +from reportlab.test import unittest +from reportlab.test.utils import GlobDirectoryWalker, outputfile, printLocation + +def makeSuite(folder, exclude=[],nonImportable=[],pattern='test_*.py'): + "Build a test suite of all available test files." + + allTests = unittest.TestSuite() + + if os.path.isdir(folder): sys.path.insert(0, folder) + for filename in GlobDirectoryWalker(folder, pattern): + modname = os.path.splitext(os.path.basename(filename))[0] + if modname not in exclude: + try: + exec 'import %s as module' % modname + allTests.addTest(module.makeSuite()) + except: + tt, tv, tb = sys.exc_info()[:] + nonImportable.append((filename,traceback.format_exception(tt,tv,tb))) + del tt,tv,tb + del sys.path[0] + + return allTests + + +def main(pattern='test_*.py'): + try: + folder = os.path.dirname(__file__) + assert folder + except: + folder = os.path.dirname(sys.argv[0]) or os.getcwd() + #allow for Benn's "screwball cygwin distro": + if folder == '': + folder = '.' + from reportlab.lib.utils import isSourceDistro + haveSRC = isSourceDistro() + + def cleanup(folder,patterns=('*.pdf', '*.log','*.svg','runAll.txt', 'test_*.txt','_i_am_actually_a_*.*')): + if not folder: return + for pat in patterns: + for filename in GlobDirectoryWalker(folder, pattern=pat): + try: + os.remove(filename) + except: + pass + + # special case for reportlab/test directory - clean up + # all PDF & log files before starting run. You don't + # want this if reusing runAll anywhere else. + if string.find(folder, 'reportlab' + os.sep + 'test') > -1: cleanup(folder) + cleanup(outputfile('')) + NI = [] + cleanOnly = '--clean' in sys.argv + if not cleanOnly: + testSuite = makeSuite(folder,nonImportable=NI,pattern=pattern+(not haveSRC and 'c' or '')) + unittest.TextTestRunner().run(testSuite) + if haveSRC: cleanup(folder,patterns=('*.pyc','*.pyo')) + if not cleanOnly: + if NI: + sys.stderr.write('\n###################### the following tests could not be imported\n') + for f,tb in NI: + print 'file: "%s"\n%s\n' % (f,string.join(tb,'')) + printLocation() + +def mainEx(): + '''for use in subprocesses''' + try: + main() + finally: + sys.stdout.flush() + sys.stderr.flush() + sys.stdout.close() + os.close(sys.stderr.fileno()) + +def runExternally(): + cmd = sys.executable+' -c"from reportlab.test import runAll;runAll.mainEx()"' + i,o,e=os.popen3(cmd) + i.close() + out = o.read() + err=e.read() + return '\n'.join((out,err)) + +def checkForFailure(outerr): + return '\nFAILED' in outerr + +if __name__ == '__main__': #noruntests + main() diff --git a/bin/reportlab/test/test_charts_textlabels.py b/bin/reportlab/test/test_charts_textlabels.py new file mode 100644 index 00000000000..51407220147 --- /dev/null +++ b/bin/reportlab/test/test_charts_textlabels.py @@ -0,0 +1,279 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_charts_textlabels.py +""" +Tests for the text Label class. +""" + +import os, sys, copy +from os.path import join, basename, splitext + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import * +from reportlab.graphics.charts.textlabels import Label +from reportlab.platypus.flowables import Spacer, PageBreak +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.xpreformatted import XPreformatted +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + #canvas.rect(2.5*cm, 2.5*cm, 15*cm, 25*cm) + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + "The document template used for all PDF documents." + + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template = PageTemplate('normal', [frame1], myMainPageFrame) + self.addPageTemplates(template) + + +class LabelTestCase(unittest.TestCase): + "Test Label class." + + def _test0(self): + "Perform original test function." + + pdfPath = outputfile('test_charts_textlabels.pdf') + c = Canvas(pdfPath) + + label = Label() + demoLabel = label.demo() + demoLabel.drawOn(c, 0, 0) + + c.save() + + + def _makeProtoLabel(self): + "Return a label prototype for further modification." + + protoLabel = Label() + protoLabel.dx = 0 + protoLabel.dy = 0 + protoLabel.boxStrokeWidth = 0.1 + protoLabel.boxStrokeColor = colors.black + protoLabel.boxFillColor = colors.yellow + # protoLabel.text = 'Hello World!' # Does not work as expected. + + return protoLabel + + + def _makeDrawings(self, protoLabel, text=None): + # Set drawing dimensions. + w, h = drawWidth, drawHeight = 400, 100 + + drawings = [] + + for boxAnchors in ('sw se nw ne', 'w e n s', 'c'): + boxAnchors = string.split(boxAnchors, ' ') + + # Create drawing. + d = Drawing(w, h) + d.add(Line(0,h/2, w, h/2, strokeColor=colors.gray, strokeWidth=0.5)) + d.add(Line(w/2,0, w/2, h, strokeColor=colors.gray, strokeWidth=0.5)) + + labels = [] + for boxAnchor in boxAnchors: + # Modify label, put it on a drawing. + label = copy.deepcopy(protoLabel) + label.boxAnchor = boxAnchor + args = {'ba':boxAnchor, 'text':text or 'Hello World!'} + label.setText('(%(ba)s) %(text)s (%(ba)s)' % args) + labels.append(label) + + for label in labels: + d.add(label) + + drawings.append(d) + + return drawings + + + def test1(self): + "Test all different box anchors." + + # Build story. + story = [] + styleSheet = getSampleStyleSheet() + bt = styleSheet['BodyText'] + h1 = styleSheet['Heading1'] + h2 = styleSheet['Heading2'] + h3 = styleSheet['Heading3'] + + story.append(Paragraph('Tests for class Label', h1)) + story.append(Paragraph('Testing box anchors', h2)) + story.append(Paragraph("""This should display "Hello World" labels +written as black text on a yellow box relative to the origin of the crosshair +axes. The labels indicate their relative position being one of the nine +canonical points of a box: sw, se, nw, ne, w, e, n, s or c (standing for +southwest, southeast... and center).""", bt)) + story.append(Spacer(0, 0.5*cm)) + + # Round 1a + story.append(Paragraph('Helvetica 10pt', h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.textAnchor = 'start' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 10 + drawings = self._makeDrawings(protoLabel) + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + # Round 1b + story.append(Paragraph('Helvetica 18pt', h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.textAnchor = 'start' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 18 + drawings = self._makeDrawings(protoLabel) + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + # Round 1c + story.append(Paragraph('Helvetica 18pt, multi-line', h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.textAnchor = 'start' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 18 + drawings = self._makeDrawings(protoLabel, text='Hello\nWorld!') + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + story.append(Paragraph('Testing text (and box) anchors', h2)) + story.append(Paragraph("""This should display labels as before, +but now with a fixes size and showing some effect of setting the +textAnchor attribute.""", bt)) + story.append(Spacer(0, 0.5*cm)) + + # Round 2a + story.append(Paragraph("Helvetica 10pt, textAnchor='start'", h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.width = 4*cm + protoLabel.height = 1.5*cm + protoLabel.textAnchor = 'start' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 10 + drawings = self._makeDrawings(protoLabel) + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + # Round 2b + story.append(Paragraph("Helvetica 10pt, textAnchor='middle'", h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.width = 4*cm + protoLabel.height = 1.5*cm + protoLabel.textAnchor = 'middle' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 10 + drawings = self._makeDrawings(protoLabel) + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + # Round 2c + story.append(Paragraph("Helvetica 10pt, textAnchor='end'", h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.width = 4*cm + protoLabel.height = 1.5*cm + protoLabel.textAnchor = 'end' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 10 + drawings = self._makeDrawings(protoLabel) + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + # Round 2d + story.append(Paragraph("Helvetica 10pt, multi-line, textAnchor='start'", h3)) + story.append(Spacer(0, 0.5*cm)) + + w, h = drawWidth, drawHeight = 400, 100 + protoLabel = self._makeProtoLabel() + protoLabel.setOrigin(drawWidth/2, drawHeight/2) + protoLabel.width = 4*cm + protoLabel.height = 1.5*cm + protoLabel.textAnchor = 'start' + protoLabel.fontName = 'Helvetica' + protoLabel.fontSize = 10 + drawings = self._makeDrawings(protoLabel, text='Hello\nWorld!') + for d in drawings: + story.append(d) + story.append(Spacer(0, 1*cm)) + + story.append(PageBreak()) + + path = outputfile('test_charts_textlabels.pdf') + doc = MyDocTemplate(path) + doc.multiBuild(story) + + +def makeSuite(): + return makeSuiteForClasses(LabelTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_docs_build.py b/bin/reportlab/test/test_docs_build.py new file mode 100644 index 00000000000..ef14a6e4d3b --- /dev/null +++ b/bin/reportlab/test/test_docs_build.py @@ -0,0 +1,51 @@ +"""Tests that all manuals can be built. +""" + + +import os, sys + +from reportlab.test import unittest +from reportlab.test.utils import SecureTestCase, printLocation + + +class ManualTestCase(SecureTestCase): + "Runs all 3 manual-builders from the top." + + def test0(self): + "Test if all manuals buildable from source." + + import reportlab + rlFolder = os.path.dirname(reportlab.__file__) + docsFolder = os.path.join(rlFolder, 'docs') + os.chdir(docsFolder) + + if os.path.isfile('userguide.pdf'): + os.remove('userguide.pdf') + if os.path.isfile('graphguide.pdf'): + os.remove('graphguide.pdf') + if os.path.isfile('reference.pdf'): + os.remove('reference.pdf') + if os.path.isfile('graphics_reference.pdf'): + os.remove('graphics_reference.pdf') + + os.system("%s genAll.py -s" % sys.executable) + + assert os.path.isfile('userguide.pdf'), 'genAll.py failed to generate userguide.pdf!' + assert os.path.isfile('graphguide.pdf'), 'genAll.py failed to generate graphguide.pdf!' + assert os.path.isfile('reference.pdf'), 'genAll.py failed to generate reference.pdf!' + assert os.path.isfile('graphics_reference.pdf'), 'genAll.py failed to generate graphics_reference.pdf!' + + +def makeSuite(): + suite = unittest.TestSuite() + loader = unittest.TestLoader() + if sys.platform[:4] != 'java': + suite.addTest(loader.loadTestsFromTestCase(ManualTestCase)) + + return suite + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_docstrings.py b/bin/reportlab/test/test_docstrings.py new file mode 100644 index 00000000000..db1f2d56e6f --- /dev/null +++ b/bin/reportlab/test/test_docstrings.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_docstrings.py + +"""This is a test on a package level that find all modules, +classes, methods and functions that do not have a doc string +and lists them in individual log files. + +Currently, methods with leading and trailing double underscores +are skipped. +""" + +import os, sys, glob, string, re +from types import ModuleType, ClassType, MethodType, FunctionType + +import reportlab +from reportlab.test import unittest +from reportlab.test.utils import SecureTestCase, GlobDirectoryWalker, outputfile, printLocation + + +RL_HOME = os.path.dirname(reportlab.__file__) + +def getModuleObjects(folder, rootName, typ, pattern='*.py'): + "Get a list of all objects defined *somewhere* in a package." + + # Define some abbreviations. + find = string.find + split = string.split + replace = string.replace + + objects = [] + lookup = {} + for file in GlobDirectoryWalker(folder, pattern): + folder = os.path.dirname(file) + + if os.path.basename(file) == '__init__.py': + continue + +## if os.path.exists(os.path.join(folder, '__init__.py')): +#### print 'skipping', os.path.join(folder, '__init__.py') +## continue + + sys.path.insert(0, folder) + cwd = os.getcwd() + os.chdir(folder) + + modName = os.path.splitext(os.path.basename(file))[0] + prefix = folder[find(folder, rootName):] + prefix = replace(prefix, os.sep, '.') + mName = prefix + '.' + modName + + try: + module = __import__(mName) + except ImportError: + # Restore sys.path and working directory. + os.chdir(cwd) + del sys.path[0] + continue + + # Get the 'real' (leaf) module + # (__import__ loads only the top-level one). + if find(mName, '.') != -1: + for part in split(mName, '.')[1:]: + module = getattr(module, part) + + # Find the objects in the module's content. + modContentNames = dir(module) + + # Handle modules. + if typ == ModuleType: + if find(module.__name__, 'reportlab') > -1: + objects.append((mName, module)) + continue + + for n in modContentNames: + obj = eval(mName + '.' + n) + # Handle functions and classes. + if typ in (FunctionType, ClassType): + if type(obj) == typ and not lookup.has_key(obj): + if typ == ClassType: + if find(obj.__module__, rootName) != 0: + continue + objects.append((mName, obj)) + lookup[obj] = 1 + # Handle methods. + elif typ == MethodType: + if type(obj) == ClassType: + for m in dir(obj): + a = getattr(obj, m) + if type(a) == typ and not lookup.has_key(a): + if find(a.im_class.__module__, rootName) != 0: + continue + cName = obj.__name__ + objects.append((mName, a)) + lookup[a] = 1 + + # Restore sys.path and working directory. + os.chdir(cwd) + del sys.path[0] + return objects + +class DocstringTestCase(SecureTestCase): + "Testing if objects in the ReportLab package have docstrings." + + def _writeLogFile(self, objType): + "Write log file for different kind of documentable objects." + + cwd = os.getcwd() + objects = getModuleObjects(RL_HOME, 'reportlab', objType) + objects.sort() + os.chdir(cwd) + + expl = {FunctionType:'functions', + ClassType:'classes', + MethodType:'methods', + ModuleType:'modules'}[objType] + + path = outputfile("test_docstrings-%s.log" % expl) + file = open(path, 'w') + file.write('No doc strings found for the following %s below.\n\n' % expl) + p = re.compile('__.+__') + + lines = [] + for name, obj in objects: + if objType == MethodType: + n = obj.__name__ + # Skip names with leading and trailing double underscores. + if p.match(n): + continue + + if objType == FunctionType: + if not obj.__doc__ or len(obj.__doc__) == 0: + lines.append("%s.%s\n" % (name, obj.__name__)) + else: + if not obj.__doc__ or len(obj.__doc__) == 0: + if objType == ClassType: + lines.append("%s.%s\n" % (obj.__module__, obj.__name__)) + elif objType == MethodType: + lines.append("%s.%s\n" % (obj.im_class, obj.__name__)) + else: + lines.append("%s\n" % (obj.__name__)) + + lines.sort() + for line in lines: + file.write(line) + + file.close() + + def test0(self): + "Test if functions have a doc string." + self._writeLogFile(FunctionType) + + def test1(self): + "Test if classes have a doc string." + self._writeLogFile(ClassType) + + def test2(self): + "Test if methods have a doc string." + self._writeLogFile(MethodType) + + def test3(self): + "Test if modules have a doc string." + self._writeLogFile(ModuleType) + +def makeSuite(): + suite = unittest.TestSuite() + loader = unittest.TestLoader() + if sys.platform[:4] != 'java': suite.addTest(loader.loadTestsFromTestCase(DocstringTestCase)) + return suite + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_extra.py b/bin/reportlab/test/test_extra.py new file mode 100644 index 00000000000..e8e7828710a --- /dev/null +++ b/bin/reportlab/test/test_extra.py @@ -0,0 +1,102 @@ +"""This executes tests defined outside the normal test suite. + +See docstring for class ExternalTestCase for more information. +""" + + +import os, string, fnmatch, re, sys + +import reportlab +from reportlab.test import unittest +from reportlab.test.utils import SecureTestCase, printLocation + + +RL_HOME = os.path.dirname(reportlab.__file__) +EXTRA_FILE = 'extra.txt' + + +class ExternalTestCase(SecureTestCase): + """Test case starting cases external to the normal RL suite. + + This test case runs additional test cases defined in external + modules which must also be using the unittest framework. + These additional modules must be defined in a file named + 'extra.txt' which needs to be located in the reportlab/test + folder and contains one path per line. + + The paths of these modules can contain stuff from the Unix + world like '~', '.', '..' and '$HOME' and can have absolute + or relative paths. If they are relative they start from the + reportlab/test folder. + + This is a sample 'extra.txt' file: + + foo.py + ./foo.py + bar/foo.py + ../foo.py + /bar/foo.py + ~/foo.py + ~/bar/foo.py + ~/../foo.py + $HOME/bar/foo.py + """ + + def test0(self): + "Execute external test cases." + + cwd = os.getcwd() + + # look for a file named 'extra.txt' in test directory, + # exit if not found + extraFilename = os.path.join(RL_HOME, 'test', EXTRA_FILE) + if not os.path.exists(extraFilename): + return + + # read module paths from file + extraModulenames = open(extraFilename).readlines() + extraModulenames = map(string.strip, extraModulenames) + + # expand pathnames as much as possible + for f in extraModulenames: + if f == '': + continue + if f[0] == '#': + continue + f = os.path.expanduser(f) + f = os.path.expandvars(f) + f = os.path.abspath(f) + + if os.path.exists(f): + # look for a makeSuite function and execute it if present + folder = os.path.abspath(os.path.dirname(f)) + modname = os.path.splitext(os.path.basename(f))[0] + os.chdir(folder) + sys.path.insert(0, folder) + + module = __import__(modname) # seems to fail sometimes... + if 'makeSuite' in dir(module): + print "running", f + testSuite = module.makeSuite() + unittest.TextTestRunner().run(testSuite) + os.chdir(cwd) + sys.path = sys.path[1:] + + +def makeSuite(): + suite = unittest.TestSuite() + loader = unittest.TestLoader() + if sys.platform[:4] != 'java': + suite.addTest(loader.loadTestsFromTestCase(ExternalTestCase)) + + return suite + + +#noruntests +if __name__ == "__main__": + if len(sys.argv) > 1: + EXTRA_FILE = sys.argv[1] + assert os.path.isfile(EXTRA_FILE), 'file %s not found!' % EXTRA_FILE + # otherwise, extra.txt will be used + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_graphics_charts.py b/bin/reportlab/test/test_graphics_charts.py new file mode 100644 index 00000000000..b4190e91c6b --- /dev/null +++ b/bin/reportlab/test/test_graphics_charts.py @@ -0,0 +1,423 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_graphics_charts.py +""" +Tests for chart class. +""" + +import os, sys, copy +from os.path import join, basename, splitext + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.validators import Auto +from reportlab.pdfgen.canvas import Canvas +from reportlab.graphics.shapes import * +from reportlab.graphics.charts.textlabels import Label +from reportlab.platypus.flowables import Spacer, PageBreak +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.xpreformatted import XPreformatted +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate \ + import PageTemplate, BaseDocTemplate + +from reportlab.graphics.charts.barcharts import VerticalBarChart +from reportlab.graphics.charts.linecharts import HorizontalLineChart +from reportlab.graphics.charts.lineplots import LinePlot +from reportlab.graphics.charts.piecharts import Pie +from reportlab.graphics.charts.legends import Legend +from reportlab.graphics.charts.spider import SpiderChart +from reportlab.graphics.widgets.markers import makeMarker + + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + #canvas.rect(2.5*cm, 2.5*cm, 15*cm, 25*cm) + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + "The document template used for all PDF documents." + + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template = PageTemplate('normal', [frame1], myMainPageFrame) + self.addPageTemplates(template) + + +def sample1bar(data=[(13, 5, 20, 22, 37, 45, 19, 4)]): + drawing = Drawing(400, 200) + + bc = VerticalBarChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc) + + return drawing + + +def sample2bar(data=[(13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5)]): + return sample1bar(data) + + +def sample1line(data=[(13, 5, 20, 22, 37, 45, 19, 4)]): + drawing = Drawing(400, 200) + + bc = HorizontalLineChart() + bc.x = 50 + bc.y = 50 + bc.height = 125 + bc.width = 300 + bc.data = data + + bc.strokeColor = colors.black + + bc.valueAxis.valueMin = 0 + bc.valueAxis.valueMax = 60 + bc.valueAxis.valueStep = 15 + + bc.categoryAxis.labels.boxAnchor = 'ne' + bc.categoryAxis.labels.dx = 8 + bc.categoryAxis.labels.dy = -2 + bc.categoryAxis.labels.angle = 30 + + catNames = string.split('Jan Feb Mar Apr May Jun Jul Aug', ' ') + catNames = map(lambda n:n+'-99', catNames) + bc.categoryAxis.categoryNames = catNames + drawing.add(bc) + + return drawing + + +def sample2line(data=[(13, 5, 20, 22, 37, 45, 19, 4), + (14, 6, 21, 23, 38, 46, 20, 5)]): + return sample1line(data) + + +def sample3(drawing=None): + "Add sample swatches to a diagram." + + d = drawing or Drawing(400, 200) + + swatches = Legend() + swatches.alignment = 'right' + swatches.x = 80 + swatches.y = 160 + swatches.deltax = 60 + swatches.dxTextSpace = 10 + swatches.columnMaximum = 4 + items = [(colors.red, 'before'), (colors.green, 'after')] + swatches.colorNamePairs = items + + d.add(swatches, 'legend') + + return d + + +def sample4pie(): + width = 300 + height = 150 + d = Drawing(width, height) + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [1, 50, 100, 100, 100, 100, 100, 100, 100, 50] + pc.labels = ['0','a','b','c','d','e','f','g','h','i'] + pc.slices.strokeWidth=0.5 + pc.slices[3].popout = 20 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + d.add(pc) + legend = Legend() + legend.x = width-5 + legend.y = height-5 + legend.dx = 20 + legend.dy = 5 + legend.deltax = 0 + legend.boxAnchor = 'nw' + legend.colorNamePairs=Auto(chart=pc) + d.add(legend) + return d + +def autoLegender(i,chart,styleObj,sym='symbol'): + if sym: + setattr(styleObj[0],sym, makeMarker('Diamond',size=6)) + setattr(styleObj[1],sym,makeMarker('Square')) + width = 300 + height = 150 + legend = Legend() + legend.x = width-5 + legend.y = 5 + legend.dx = 20 + legend.dy = 5 + legend.deltay = 0 + legend.boxAnchor = 'se' + if i=='col auto': + legend.colorNamePairs[0]=(Auto(chart=chart),'auto chart=self.chart') + legend.colorNamePairs[1]=(Auto(obj=chart,index=1),'auto chart=self.chart index=1') + elif i=='full auto': + legend.colorNamePairs=Auto(chart=chart) + elif i=='swatch set': + legend.swatchMarker=makeMarker('Circle') + legend.swatchMarker.size = 10 + elif i=='swatch auto': + legend.swatchMarker=Auto(chart=chart) + d = Drawing(width,height) + d.background = Rect(0,0,width,height,strokeWidth=1,strokeColor=colors.red,fillColor=None) + m = makeMarker('Cross') + m.x = width-5 + m.y = 5 + m.fillColor = colors.red + m.strokeColor = colors.yellow + d.add(chart) + d.add(legend) + d.add(m) + return d + +def lpleg(i=None): + chart = LinePlot() + return autoLegender(i,chart,chart.lines) + +def hlcleg(i=None): + chart = HorizontalLineChart() + return autoLegender(i,chart,chart.lines) + +def bcleg(i=None): + chart = VerticalBarChart() + return autoLegender(i,chart,chart.bars,None) + +def pcleg(i=None): + chart = Pie() + return autoLegender(i,chart,chart.slices,None) + +def scleg(i=None): + chart = SpiderChart() + return autoLegender(i,chart,chart.strands,None) + +def plpleg(i=None): + from reportlab.lib.colors import pink, red, green + pie = Pie() + pie.x = 0 + pie.y = 0 + pie.pointerLabelMode='LeftAndRight' + pie.slices.label_boxStrokeColor = red + pie.simpleLabels = 0 + pie.sameRadii = 1 + pie.data = [1, 0.1, 1.7, 4.2,0,0] + pie.labels = ['abcdef', 'b', 'c', 'd','e','fedcba'] + pie.strokeWidth=1 + pie.strokeColor=green + pie.slices.label_pointer_piePad = 6 + pie.width = 160 + pie.direction = 'clockwise' + pie.pointerLabelMode = 'LeftRight' + return autoLegender(i,pie,pie.slices,None) + +def notFail(d): + try: + return d.getContents() + except: + import traceback + traceback.print_exc() + return None + +STORY = [] +styleSheet = getSampleStyleSheet() +bt = styleSheet['BodyText'] +h1 = styleSheet['Heading1'] +h2 = styleSheet['Heading2'] +h3 = styleSheet['Heading3'] +FINISHED = 0 + + +class ChartTestCase(unittest.TestCase): + "Test chart classes." + + def setUp(self): + "Hook method for setting up the test fixture before exercising it." + + global STORY + self.story = STORY + + if self.story == []: + self.story.append(Paragraph('Tests for chart classes', h1)) + + def tearDown(self): + "Hook method for deconstructing the test fixture after testing it." + + if FINISHED: + path=outputfile('test_graphics_charts.pdf') + doc = MyDocTemplate(path) + doc.build(self.story) + + def test0(self): + "Test bar charts." + + story = self.story + story.append(Paragraph('Single data row', h2)) + + story.append(Spacer(0, 0.5*cm)) + drawing = sample1bar() + story.append(drawing) + story.append(Spacer(0, 1*cm)) + + + def test1(self): + "Test bar charts." + + story = self.story + story.append(Paragraph('Double data row', h2)) + + story.append(Spacer(0, 0.5*cm)) + drawing = sample2bar() + story.append(drawing) + story.append(Spacer(0, 1*cm)) + + + def test2(self): + "Test bar charts." + + story = self.story + story.append(Paragraph('Double data row with legend', h2)) + + story.append(Spacer(0, 0.5*cm)) + drawing = sample2bar() + drawing = sample3(drawing) + story.append(drawing) + story.append(Spacer(0, 1*cm)) + + + def test3(self): + "Test line charts." + + story = self.story + story.append(Paragraph('Single data row', h2)) + + story.append(Spacer(0, 0.5*cm)) + drawing = sample1line() + story.append(drawing) + story.append(Spacer(0, 1*cm)) + + + def test4(self): + "Test line charts." + + story = self.story + story.append(Paragraph('Single data row', h2)) + + story.append(Spacer(0, 0.5*cm)) + drawing = sample2line() + story.append(drawing) + story.append(Spacer(0, 1*cm)) + + def test4b(self): + story = self.story + for code in (lpleg, hlcleg, bcleg, pcleg, scleg, plpleg): + code_name = code.func_code.co_name + for i in ('standard', 'col auto', 'full auto', 'swatch set', 'swatch auto'): + d = code(i) + assert notFail(d),'getContents failed for %s %s' % (code_name,i) + story.append(Paragraph('%s %s' % (i,code_name), h2)) + story.append(Spacer(0, 0.5*cm)) + story.append(code(i)) + story.append(Spacer(0, 1*cm)) + + def test5(self): + "Test pie charts." + + story = self.story + story.append(Paragraph('Pie', h2)) + + story.append(Spacer(0, 0.5*cm)) + drawing = sample4pie() + story.append(drawing) + story.append(Spacer(0, 1*cm)) + + def test6(self): + from reportlab.graphics.charts.piecharts import Pie, _makeSideArcDefs, intervalIntersection + L = [] + + def intSum(arcs,A): + s = 0 + for a in A: + for arc in arcs: + i = intervalIntersection(arc[1:],a) + if i: s += i[1] - i[0] + return s + + def subtest(sa,d): + pc = Pie() + pc.direction=d + pc.startAngle=sa + arcs = _makeSideArcDefs(sa,d) + A = [x[1] for x in pc.makeAngles()] + arcsum = sum([a[2]-a[1] for a in arcs]) + isum = intSum(arcs,A) + mi = max([a[2]-a[1] for a in arcs]) + ni = min([a[2]-a[1] for a in arcs]) + l = [] + s = (arcsum-360) + if s>1e-8: l.append('Arc length=%s != 360' % s) + s = abs(isum-360) + if s>1e-8: l.append('interval intersection length=%s != 360' % s) + if mi>360: l.append('max interval intersection length=%s >360' % mi) + if ni<0: l.append('min interval intersection length=%s <0' % ni) + if l: + l.append('sa: %s d: %s' % (sa,d)) + l.append('sidearcs: %s' % str(arcs)) + l.append('Angles: %s' % A) + raise ValueError('piecharts._makeSideArcDefs failure\n%s' % '\n'.join(l)) + + for d in ('anticlockwise','clockwise'): + for sa in (225, 180, 135, 90, 45, 0, -45, -90): + subtest(sa,d) + + # This triggers the document build operation (hackish). + global FINISHED + FINISHED = 1 + +def makeSuite(): + return makeSuiteForClasses(ChartTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_graphics_images.py b/bin/reportlab/test/test_graphics_images.py new file mode 100644 index 00000000000..58ca119a196 --- /dev/null +++ b/bin/reportlab/test/test_graphics_images.py @@ -0,0 +1,91 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +""" +Tests for RLG Image shapes. +""" + +import os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.graphics.shapes import Image, Drawing +from reportlab.graphics import renderPDF +from reportlab.lib.pagesizes import A4 + + +IMAGES = [] +IMAGENAME = 'cs_logo.gif' +IMAGENAME = 'pythonpowered.gif' + + +class ImageTestCase(unittest.TestCase): + "Test RLG Image shape." + + def __del__(self): + if IMAGES[-1] != None: + return + else: + del IMAGES[-1] + + d = Drawing(A4[0], A4[1]) + for img in IMAGES: + d.add(img) + outPath = outputfile("test_graphics_images.pdf") + renderPDF.drawToFile(d, outPath) #, '') + assert os.path.exists(outPath) == 1 + + + def test0(self): + "Test convert a bitmap file as Image shape into a tmp. PDF file." + + d = Drawing(110, 44) + inPath = IMAGENAME + img = Image(0, 0, 110, 44, inPath) + d.add(img) + IMAGES.append(img) + + + def test1(self): + "Test Image shape, adding it to a PDF page." + + inPath = IMAGENAME + img = Image(0, 0, 110, 44, inPath) + IMAGES.append(img) + + + def test2(self): + "Test scaled Image shape adding it to a PDF page." + + inPath = IMAGENAME + img = Image(0, 0, 110, 44, inPath) + d = Drawing(110, 44) + d.add(img) + d.translate(120, 0) + d.scale(2, 2) + IMAGES.append(d) + + + def test3(self): + "Test rotated Image shape adding it to a PDF page." + + inPath = IMAGENAME + img = Image(0, 0, 110, 44, inPath) + d = Drawing(110, 44) + d.add(img) + d.translate(420, 0) + d.scale(2, 2) + d.rotate(45) + IMAGES.append(d) + + IMAGES.append(None) # used to indicate last test + + +def makeSuite(): + return makeSuiteForClasses(ImageTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_graphics_layout.py b/bin/reportlab/test/test_graphics_layout.py new file mode 100644 index 00000000000..a4f13360c24 --- /dev/null +++ b/bin/reportlab/test/test_graphics_layout.py @@ -0,0 +1,82 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_graphics_layout.py +""" +Tests for getBounds methods of various graphical widgets +""" + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation + +from reportlab.graphics import shapes +##from reportlab.graphics.charts.barcharts import VerticalBarChart +##from reportlab.graphics.charts.linecharts import HorizontalLineChart +##from reportlab.graphics.charts.piecharts import Pie +##from reportlab.graphics.charts.legends import Legend + +class BoundsTestCase(unittest.TestCase): + def testLine(self): + s = shapes.Line(10,20,30,40) + assert s.getBounds() == (10,20,30,40) + + def testRect(self): + s = shapes.Rect(10,20,30,40) #width, height + assert s.getBounds() == (10,20,40,60) + + def testCircle(self): + s = shapes.Circle(100, 50, 10) + assert s.getBounds() == (90,40,110,60) + + def testEllipse(self): + s = shapes.Ellipse(100, 50, 10, 5) + assert s.getBounds() == (90,45,110,55) + + def testWedge(self): + s = shapes.Wedge(0,0,10,0,90) + assert s.getBounds() == (0,0,10,10), 'expected (0,0,10,10) got %s' % repr(s.getBounds()) + + def testPolygon(self): + points = [0,0,10,30,25,15] + s = shapes.Polygon(points) + assert s.getBounds() == (0,0,25,30) + + s = shapes.PolyLine(points) + assert s.getBounds() == (0,0,25,30) + + def testString(self): + s = shapes.String(0,0,'Hello World', fontName='Courier',fontSize=10) + assert s.getBounds() == (0, -2.0, 66.0, 10) + + def testGroup(self): + g = shapes.Group() + g.add(shapes.Rect(0,0,10,10)) + g.add(shapes.Rect(50,50,10,10)) + assert g.getBounds() == (0,0,60,60) + + g.translate(40,40) + assert g.getBounds() == (40,40,100,100) + + g.translate(-40,-40) + g.rotate(90) + #approx bounds needed, trig functions create an error of 3e-15 + assert map(int, g.getBounds()) == [-60,0,0,60] + + def testWidget(self): + from reportlab.graphics.charts.barcharts import VerticalBarChart + vbc = VerticalBarChart() + vbc.x = 50 + vbc.y = 50 + from reportlab.graphics.widgetbase import Sizer + siz = Sizer() + siz.add(vbc, 'vbc') + assert siz.getBounds()[0:2] <> (0,0) + + +def makeSuite(): + return makeSuiteForClasses(BoundsTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_graphics_speed.py b/bin/reportlab/test/test_graphics_speed.py new file mode 100644 index 00000000000..21fc6f9f47e --- /dev/null +++ b/bin/reportlab/test/test_graphics_speed.py @@ -0,0 +1,86 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_graphics_speed.py +""" +This does a test drawing with lots of things in it, running +with and without attribute checking. +""" + +__version__ = ''' $Id $ ''' + + +import os, sys, time, profile + +import reportlab.rl_config +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus import Flowable +from reportlab.graphics.shapes import * +from reportlab.graphics.charts.piecharts import Pie + + +class GraphicsSpeedTestCase(unittest.TestCase): + "Test speed of the graphics rendering process." + + def test0(self, isFast=0): + """Hello World, on a rectangular background. + + The rectangle's fillColor is yellow. + The string's fillColor is red. + """ + reportlab.rl_config.shapeChecking = not isFast + + pdfPath = outputfile('test_graphics_speed_fast.pdf') + c = Canvas(pdfPath) + t0 = time.time() + + d = Drawing(400, 200) + num = 100 + for i in range(num): + pc = Pie() + pc.x = 150 + pc.y = 50 + pc.data = [10,20,30,40,50,60] + pc.labels = ['a','b','c','d','e','f'] + pc.slices.strokeWidth=0.5 + pc.slices[3].popout = 20 + pc.slices[3].strokeWidth = 2 + pc.slices[3].strokeDashArray = [2,2] + pc.slices[3].labelRadius = 1.75 + pc.slices[3].fontColor = colors.red + d.add(pc) + d.drawOn(c, 80, 500) + + t1 = time.time() + + result = 'drew %d pie charts in %0.4f' % (num, t1 - t0) + open(outputfile('test_graphics_speed_test%s.log' % (isFast+1)), 'w').write(result) + + + def test1(self, isFast=1): + "Same as test1(), but with shape checking turned on." + + self.test0(isFast) + + + def test2(self): + "This is a profiled version of test1()." + + fileName = outputfile('test_graphics_speed_profile.log') + # This runs ok, when only this test script is executed, + # but fails, when imported from runAll.py... +## profile.run("t = GraphicsSpeedTestCase('test2')", fileName) + + +def makeSuite(): + return makeSuiteForClasses(GraphicsSpeedTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_hello.py b/bin/reportlab/test/test_hello.py new file mode 100644 index 00000000000..2197f405d06 --- /dev/null +++ b/bin/reportlab/test/test_hello.py @@ -0,0 +1,34 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_hello.py +__version__=''' $Id''' +__doc__="""most basic test possible that makes a PDF. + +Useful if you want to test that a really minimal PDF is healthy, +since the output is about the smallest thing we can make.""" + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.pdfgen.canvas import Canvas + + +class HelloTestCase(unittest.TestCase): + "Simplest test that makes PDF" + + def test(self): + c = Canvas(outputfile('test_hello.pdf')) + c.setAuthor('\xe3\x83\x9b\xe3\x83\x86\xe3\x83\xab\xe3\x83\xbbe\xe3\x83\x91\xe3\x83\xb3\xe3\x83\x95\xe3\x83\xac\xe3\x83\x83\xe3\x83\x88') + c.setFont('Helvetica-Bold', 36) + c.drawString(100,700, 'Hello World') + c.save() + + +def makeSuite(): + return makeSuiteForClasses(HelloTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_images.py b/bin/reportlab/test/test_images.py new file mode 100644 index 00000000000..591b2295480 --- /dev/null +++ b/bin/reportlab/test/test_images.py @@ -0,0 +1,55 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_images.py +__version__=''' $Id''' +__doc__="""Tests to do with image handling. + +Most of them make use of test\pythonpowereed.gif.""" +import os,md5 + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation +from reportlab.lib.utils import ImageReader + + +"""To avoid depending on external stuff, I made a small 5x5 image and +attach its 'file contents' here in several formats. + +The image looks like this, with K=black, R=red, G=green, B=blue, W=white. + + K R G B W + K R G B W + K R G B W + K R G B W + K R G B W + +""" + +sampleRAW = '\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\xff\x00\x00\x00\xff\x00\x00\x00\xff\x00\x00\x00\xff\xff\xff\xff' +samplePNG = '\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x05\x00\x00\x00\x05\x08\x02\x00\x00\x00\x02\r\xb1\xb2\x00\x00\x00:IDATx\x9cb```\xf8\x0f\xc3\xff\xff\xff\x07\x00\x00\x00\xff\xffbb@\x05\x00\x00\x00\x00\xff\xffB\xe7\x03\x00\x00\x00\xff\xffB\xe7\x03\x00\x00\x00\xff\xffB\xe7\x03\x00\x00\x00\xff\xff\x03\x00\x9e\x01\x06\x03\x03\xc4A\xb4\x00\x00\x00\x00IEND\xaeB`\x82' + + +class ReaderTestCase(unittest.TestCase): + "Simplest tests to import images, work under Jython or PIL" + + def test(self): + import reportlab.test + from reportlab.lib.utils import rl_isfile + imageFileName = os.path.dirname(reportlab.test.__file__) + os.sep + 'pythonpowered.gif' + assert rl_isfile(imageFileName), "%s not found!" % imageFileName + + ir = ImageReader(imageFileName) + assert ir.getSize() == (110,44) + pixels = ir.getRGBData() + assert md5.md5(pixels).hexdigest() == '02e000bf3ffcefe9fc9660c95d7e27cf' + + +def makeSuite(): + return makeSuiteForClasses(ReaderTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_invariant.py b/bin/reportlab/test/test_invariant.py new file mode 100644 index 00000000000..d75c7c37e10 --- /dev/null +++ b/bin/reportlab/test/test_invariant.py @@ -0,0 +1,56 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_invariant.py +__version__=''' $Id''' +__doc__="""Verfy that if in invariant mode, repeated runs +make identical file. This does NOT test across platforms +or python versions, only a user can do that :-)""" + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.pdfgen.canvas import Canvas +filename = outputfile('test_invariant.pdf') + +class InvarTestCase(unittest.TestCase): + "Simplest test that makes PDF" + def test(self): + + import os + c = Canvas(filename, invariant=1, pageCompression=0) + c.setFont('Helvetica-Bold', 36) + c.drawString(100,700, 'Hello World') + gif = os.path.join(os.path.dirname(unittest.__file__),'pythonpowered.gif') + c.drawImage(gif,100,600) + c.save() + + raw1 = open(filename, 'rb').read() + + c = Canvas(filename, invariant=1, pageCompression=0) + c.setFont('Helvetica-Bold', 36) + c.drawString(100,700, 'Hello World') + c.drawImage(gif,100,600) + c.save() + + raw2 = open(filename, 'rb').read() + assert raw1 == raw2, 'repeated runs differ!' + +def makeSuite(): + return makeSuiteForClasses(InvarTestCase) + + +#noruntests +if __name__ == "__main__": + # add some diagnostics, useful in invariant tests + import sys, md5, os + verbose = ('-v' in sys.argv) + unittest.TextTestRunner().run(makeSuite()) + if verbose: + #tell us about the file we produced + fileSize = os.stat(filename)[6] + raw = open(filename,'rb').read() + digest = md5.md5(raw).hexdigest() + major, minor = sys.version_info[0:2] + print '%s on %s (Python %d.%d):\n %d bytes, digest %s' % ( + filename,sys.platform, major, minor, fileSize, digest) + printLocation() diff --git a/bin/reportlab/test/test_lib_colors.py b/bin/reportlab/test/test_lib_colors.py new file mode 100644 index 00000000000..37ced998f26 --- /dev/null +++ b/bin/reportlab/test/test_lib_colors.py @@ -0,0 +1,161 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_lib_colors.py +"""Tests for the reportlab.lib.colors module. +""" + + +import os, math + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen.canvas import Canvas +import reportlab.pdfgen.canvas +from reportlab.lib import colors +from reportlab.lib.units import inch, cm + + +def framePage(canvas, title): + canvas.setFont('Times-BoldItalic',20) + canvas.drawString(inch, 10.5 * inch, title) + + canvas.setFont('Times-Roman',10) + canvas.drawCentredString(4.135 * inch, 0.75 * inch, + 'Page %d' % canvas.getPageNumber()) + + #draw a border + canvas.setStrokeColorRGB(1,0,0) + canvas.setLineWidth(5) + canvas.line(0.8 * inch, inch, 0.8 * inch, 10.75 * inch) + #reset carefully afterwards + canvas.setLineWidth(1) + canvas.setStrokeColorRGB(0,0,0) + + +class ColorTestCase(unittest.TestCase): + "" + + def test0(self): + "Test color2bw function on all named colors." + + cols = colors.getAllNamedColors().values() + for col in cols: + gray = colors.color2bw(col) + r, g, b = gray.red, gray.green, gray.blue + assert r == g == b + + + def test1(self): + "Test colorDistance function." + + cols = colors.getAllNamedColors().values() + for col in cols: + d = colors.colorDistance(col, col) + assert d == 0 + + + def test2(self): + "Test toColor function on half a dozen ways to say 'red'." + + allRed = [colors.red, [1, 0, 0], (1, 0, 0), + 'red', 'RED', '0xFF0000', '0xff0000'] + + for thing in allRed: + assert colors.toColor(thing) == colors.red + + + def test3(self): + "Test roundtrip RGB to CMYK conversion." + + # Take all colors as test subjects, except 'transparent'. +## rgbCols = colors.getAllNamedColors() +## del rgbCols['transparent'] + rgbCols = colors.getAllNamedColors().items() + + # Make a roundtrip test (RGB > CMYK > RGB). + for name, rgbCol in rgbCols: + r1, g1, b1 = rgbCol.red, rgbCol.green, rgbCol.blue + c, m, y, k = colors.rgb2cmyk(r1, g1, b1) + r2, g2, b2 = colors.cmyk2rgb((c, m, y, k)) + rgbCol2 = colors.Color(r2, g2, b2) + + # Make sure the differences for each RGB component + # isreally small (< power(10, -N)! + N = 16 # max. value found to work on Python2.0 and Win2K. + deltas = map(abs, (r1-r2, g1-g2, b1-b2)) + assert deltas < [math.pow(10, -N)] * 3 + + def test4(self): + "Construct CMYK instances and test round trip conversion" + + rgbCols = colors.getAllNamedColors().items() + + # Make a roundtrip test (RGB > CMYK > RGB). + for name, rgbCol in rgbCols: + r1, g1, b1 = rgbCol.red, rgbCol.green, rgbCol.blue + c, m, y, k = colors.rgb2cmyk(r1, g1, b1) + cmykCol = colors.CMYKColor(c,m,y,k) + r2, g2, b2 = cmykCol.red, cmykCol.green, cmykCol.blue #colors.cmyk2rgb((c, m, y, k)) + rgbCol2 = colors.Color(r2, g2, b2) + + # Make sure the differences for each RGB component + # isreally small (< power(10, -N)! + N = 16 # max. value found to work on Python2.0 and Win2K. + deltas = map(abs, (r1-r2, g1-g2, b1-b2)) + assert deltas < [math.pow(10, -N)] * 3 + + + def test5(self): + "List and display all named colors and their gray equivalents." + + canvas = reportlab.pdfgen.canvas.Canvas(outputfile('test_lib_colors.pdf')) + + #do all named colors + framePage(canvas, 'Color Demo - page %d' % canvas.getPageNumber()) + + all_colors = reportlab.lib.colors.getAllNamedColors().items() + all_colors.sort() # alpha order by name + canvas.setFont('Times-Roman', 10) + text = 'This shows all the named colors in the HTML standard (plus their gray and CMYK equivalents).' + canvas.drawString(72,740, text) + + canvas.drawString(200,725,'Pure RGB') + canvas.drawString(300,725,'B&W Approx') + canvas.drawString(400,725,'CMYK Approx') + + y = 700 + for (name, color) in all_colors: + canvas.setFillColor(colors.black) + canvas.drawString(100, y, name) + canvas.setFillColor(color) + canvas.rect(200, y-10, 80, 30, fill=1) + canvas.setFillColor(colors.color2bw(color)) + canvas.rect(300, y-10, 80, 30, fill=1) + + c, m, yel, k = colors.rgb2cmyk(color.red, color.green, color.blue) + CMYK = colors.CMYKColor(c,m,yel,k) + canvas.setFillColor(CMYK) + canvas.rect(400, y-10, 80, 30, fill=1) + + y = y - 40 + if y < 100: + canvas.showPage() + framePage(canvas, 'Color Demo - page %d' % canvas.getPageNumber()) + canvas.setFont('Times-Roman', 10) + y = 700 + canvas.drawString(200,725,'Pure RGB') + canvas.drawString(300,725,'B&W Approx') + canvas.drawString(400,725,'CMYK Approx') + + canvas.save() + + +def makeSuite(): + return makeSuiteForClasses(ColorTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_lib_sequencer.py b/bin/reportlab/test/test_lib_sequencer.py new file mode 100644 index 00000000000..f44686ce271 --- /dev/null +++ b/bin/reportlab/test/test_lib_sequencer.py @@ -0,0 +1,123 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_lib_sequencer.py +"""Tests for the reportlab.lib.sequencer module. +""" + + +import sys, random + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation +from reportlab.lib.sequencer import Sequencer + + +class SequencerTestCase(unittest.TestCase): + "Test Sequencer usage." + + def test0(self): + "Test sequencer default counter." + + seq = Sequencer() + msg = 'Initial value is not zero!' + assert seq._this() == 0, msg + + + def test1(self): + "Test incrementing default counter." + + seq = Sequencer() + + for i in range(1, 101): + n = seq.next() + msg = 'Sequence value is not correct!' + assert seq._this() == n, msg + + + def test2(self): + "Test resetting default counter." + + seq = Sequencer() + start = seq._this() + + for i in range(1, 101): + n = seq.next() + + seq.reset() + + msg = 'Sequence value not correctly reset!' + assert seq._this() == start, msg + + + def test3(self): + "Test incrementing dedicated counter." + + seq = Sequencer() + + for i in range(1, 101): + n = seq.next('myCounter1') + msg = 'Sequence value is not correct!' + assert seq._this('myCounter1') == n, msg + + + def test4(self): + "Test resetting dedicated counter." + + seq = Sequencer() + start = seq._this('myCounter1') + + for i in range(1, 101): + n = seq.next('myCounter1') + + seq.reset('myCounter1') + + msg = 'Sequence value not correctly reset!' + assert seq._this('myCounter1') == start, msg + + + def test5(self): + "Test incrementing multiple dedicated counters." + + seq = Sequencer() + startMyCounter0 = seq._this('myCounter0') + startMyCounter1 = seq._this('myCounter1') + + for i in range(1, 101): + n = seq.next('myCounter0') + msg = 'Sequence value is not correct!' + assert seq._this('myCounter0') == n, msg + m = seq.next('myCounter1') + msg = 'Sequence value is not correct!' + assert seq._this('myCounter1') == m, msg + + +## def testRandom(self): +## "Test randomly manipulating multiple dedicated counters." +## +## seq = Sequencer() +## counterNames = ['c0', 'c1', 'c2', 'c3'] +## +## # Init. +## for cn in counterNames: +## setattr(self, cn, seq._this(cn)) +## msg = 'Counter start value is not correct!' +## assert seq._this(cn) == 0, msg +## +## # Increment/decrement. +## for i in range(1, 101): +## n = seq.next('myCounter0') +## msg = 'Sequence value is not correct!' +## assert seq._this('myCounter0') == n, msg +## m = seq.next('myCounter1') +## msg = 'Sequence value is not correct!' +## assert seq._this('myCounter1') == m, msg + + +def makeSuite(): + return makeSuiteForClasses(SequencerTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_lib_utils.py b/bin/reportlab/test/test_lib_utils.py new file mode 100644 index 00000000000..d3e6031356e --- /dev/null +++ b/bin/reportlab/test/test_lib_utils.py @@ -0,0 +1,115 @@ +"""Tests for reportlab.lib.utils +""" +import os +import reportlab +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation +from reportlab.lib import colors +from reportlab.lib.utils import recursiveImport, recursiveGetAttr, recursiveSetAttr, rl_isfile, \ + isCompactDistro + +class ImporterTestCase(unittest.TestCase): + "Test import utilities" + count = 0 + + def setUp(self): + from time import time + from reportlab.lib.utils import get_rl_tempdir + s = `int(time())` + `self.count` + self.__class__.count += 1 + self._tempdir = get_rl_tempdir('reportlab_test','tmp_%s' % s) + _testmodulename = os.path.join(self._tempdir,'test_module_%s.py' % s) + f = open(_testmodulename,'w') + f.write('__all__=[]\n') + f.close() + self._testmodulename = os.path.splitext(os.path.basename(_testmodulename))[0] + + def tearDown(self): + from shutil import rmtree + rmtree(self._tempdir,1) + + def test1(self): + "try stuff known to be in the path" + m1 = recursiveImport('reportlab.pdfgen.canvas') + import reportlab.pdfgen.canvas + assert m1 == reportlab.pdfgen.canvas + + def test2(self): + "try under a well known directory NOT on the path" + D = os.path.join(os.path.dirname(reportlab.__file__), 'tools','pythonpoint') + fn = os.path.join(D,'stdparser.py') + if rl_isfile(fn) or rl_isfile(fn+'c') or rl_isfile(fn+'o'): + m1 = recursiveImport('stdparser', baseDir=D) + + def test3(self): + "ensure CWD is on the path" + try: + cwd = os.getcwd() + os.chdir(self._tempdir) + m1 = recursiveImport(self._testmodulename) + finally: + os.chdir(cwd) + + def test4(self): + "ensure noCWD removes current dir from path" + try: + cwd = os.getcwd() + os.chdir(self._tempdir) + import sys + try: + del sys.modules[self._testmodulename] + except KeyError: + pass + self.assertRaises(ImportError, + recursiveImport, + self._testmodulename, + noCWD=1) + finally: + os.chdir(cwd) + + def test5(self): + "recursive attribute setting/getting on modules" + import reportlab.lib.units + inch = recursiveGetAttr(reportlab, 'lib.units.inch') + assert inch == 72 + + recursiveSetAttr(reportlab, 'lib.units.cubit', 18*inch) + cubit = recursiveGetAttr(reportlab, 'lib.units.cubit') + assert cubit == 18*inch + + def test6(self): + "recursive attribute setting/getting on drawings" + from reportlab.graphics.charts.barcharts import sampleH1 + drawing = sampleH1() + recursiveSetAttr(drawing, 'barchart.valueAxis.valueMax', 72) + theMax = recursiveGetAttr(drawing, 'barchart.valueAxis.valueMax') + assert theMax == 72 + + def test7(self): + "test open and read of a simple relative file" + from reportlab.lib.utils import open_and_read + b = open_and_read('../docs/images/Edit_Prefs.gif') + + def test8(self): + "test open and read of a relative file: URL" + from reportlab.lib.utils import open_and_read + b = open_and_read('file:../docs/images/Edit_Prefs.gif') + + def test9(self): + "test open and read of an http: URL" + from reportlab.lib.utils import open_and_read + b = open_and_read('http://www.reportlab.com/rsrc/encryption.gif') + + def test10(self): + "test open and read of a simple relative file" + from reportlab.lib.utils import open_and_read, getStringIO + b = getStringIO(open_and_read('../docs/images/Edit_Prefs.gif')) + b = open_and_read(b) + +def makeSuite(): + return makeSuiteForClasses(ImporterTestCase) + + +if __name__ == "__main__": #noruntests + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_lib_validators.py b/bin/reportlab/test/test_lib_validators.py new file mode 100644 index 00000000000..70f74f2ed86 --- /dev/null +++ b/bin/reportlab/test/test_lib_validators.py @@ -0,0 +1,165 @@ +"""Tests (incomplete) for the reportlab.lib.validators module. +""" + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation +from reportlab.lib import colors +from reportlab.lib import validators + + +class ValidatorTestCase(unittest.TestCase): + "Test validating functions." + + def test0(self): + "Test isBoolean validator." + + msg = "Validation failed for 'boolean' %s!" + + booleans = [0, 1, 'yes','no','true','false'] + badbooleans = ['a',3,-1,()] + isBoolean = validators.isBoolean + for b in booleans: + assert isBoolean(b) == 1, msg % str(b) + for b in badbooleans: + assert isBoolean(b) == 0, msg % str(b) + + + def test1(self): + "Test isNumber validator." + + msg = 'Validation failed for number %s!' + + numbers = [0, 1, 2, -1, -2, 0.0, 0.1, -0.1] + badNumbers = ['aaa',(1,1),(1+1j),colors] + isNumber = validators.isNumber + isListOfNumbers = validators.isListOfNumbers + for n in numbers: + assert isNumber(n) == 1, msg % str(n) + for n in badNumbers: + assert isNumber(n) == 0, msg % str(n) + + msg = 'Validation failed for numbers %s!' + + assert isListOfNumbers(numbers) == 1, msg % str(numbers) + assert isListOfNumbers(badNumbers) == 0, msg % str(badNumbers) + assert isListOfNumbers(numbers+[colors]) == 0, msg % str(numbers+[colors]) + + + def test2(self): + "Test isNumberOrNone validator." + + msg = 'Validation failed for number %s!' + + numbers = [None, 0, 1, 2, -1, -2, 0.0, 0.1, -0.1] #, 2L, -2L] + isNumberOrNone = validators.isNumberOrNone + for n in numbers: + assert isNumberOrNone(n) == 1, msg % str(n) + + + def test4(self): + "Test isString validator." + + msg = 'Validation failed for string %s!' + + strings = ['', '\n', ' ', 'foo', '""'] + badStrings = [1,2.0,None,('a','b')] + isString = validators.isString + isListOfStrings = validators.isListOfStrings + for s in strings: + assert isString(s) == 1, msg % str(s) + for s in badStrings: + assert isString(s) == 0, msg % str(s) + + msg = 'Validation failed for strings %s!' + + assert isListOfStrings(strings) == 1, msg % str(strings) + assert isListOfStrings(badStrings) == 0, msg % str(badStrings) + assert isListOfStrings(strings+[1]) == 0, msg % str(strings+[1]) + + + def test5(self): + "Test isTextAnchor validator." + + msg = 'Validation failed for text anchor %s!' + + strings = ['start', 'middle', 'end'] + isTextAnchor = validators.isTextAnchor + for s in strings: + assert isTextAnchor(s) == 1, msg % s + + """ + def isListOfNumbersOrNone(x): + def isListOfShapes(x): + def isListOfStrings(x): + def isListOfStringsOrNone(x): + def isTransform(x): + def isColor(x): + def isColorOrNone(x): + def isValidChild(x): + class OneOf: + class SequenceOf: + """ + + + def test6(self): + "Test OneOf validator." + + msg = 'Validation failed for OneOf %s!' + + choices = ('clockwise', 'anticlockwise') + OneOf = validators.OneOf(choices) + for c in choices: + assert OneOf(c) == 1, msg % c + for c in ('a', 'b', 'c'): + assert OneOf(c) == 0, msg % c + + OneOf = validators.OneOf('clockwise', 'anticlockwise') + for c in choices: + assert OneOf(c) == 1, msg % c + for c in ('a', 'b', 'c'): + assert OneOf(c) == 0, msg % c + + try: + validators.OneOf(choices,'bongo') + raise AssertionError, "OneOf failed to detect bad arguments" + except ValueError: + pass + + + def test7(self): + "Test isInt validator" + + msg = 'Validation failed for isInt %s!' + + isInt = validators.isInt + for c in (1,2,-3,0,'-4','4'): + assert isInt(c), msg % str(c) + + for c in (1.2,0.0,-3.0,'-4.0','4.4','AAAA'): + assert not isInt(c), msg % str(c) + + + def test8(self): + "test Sequence of validator" + + msg = 'Validation failed for SequenceOf %s!' + + v=validators.SequenceOf(validators.OneOf(('eps','pdf','png','gif','jpg','tif')),lo=1,hi=3,emptyOK=0) + for c in (['png'],('eps',),('eps','pdf')): + assert v(c), msg % str(c) + v._lo = 2 + for c in ([],(),('eps'),('eps','pdf','a'),['eps','pdf','png','gif']): + assert not v(c), msg % str(c) + v._emptyOK=1 + for c in ([],(),('eps','pdf')): + assert v(c), msg % str(c) + + +def makeSuite(): + return makeSuiteForClasses(ValidatorTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_multibyte_chs.py b/bin/reportlab/test/test_multibyte_chs.py new file mode 100644 index 00000000000..59dde43d459 --- /dev/null +++ b/bin/reportlab/test/test_multibyte_chs.py @@ -0,0 +1,84 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/rlj/jpsupport.py +# Temporary japanese support for ReportLab. +""" +The code in this module will disappear any day now and be replaced +by classes in reportlab.pdfbase.cidfonts +""" + + +import string, os +import codecs +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib import colors +from reportlab.lib.codecharts import KutenRowCodeChart, hBoxText + +global VERBOSE +VERBOSE = 0 + + +class CHSFontTests(unittest.TestCase): + + def test0(self): + "A basic document drawing some strings" + + # if they do not have the Japanese font files, go away quietly + from reportlab.pdfbase.cidfonts import UnicodeCIDFont, findCMapFile + + + pdfmetrics.registerFont(UnicodeCIDFont('STSong-Light')) + + c = Canvas(outputfile('test_multibyte_chs.pdf')) + c.setFont('Helvetica', 30) + c.drawString(100,700, 'Simplified Chinese Font Support') + + + c.setFont('Helvetica', 10) + c.drawString(100,680, 'Short sample: "China - Zhang Ziyi" (famous actress)') + # the two typefaces + + hBoxText(u'\u4e2d\u56fd - \u7ae0\u5b50\u6021', + c, + 100, + 660, + 'STSong-Light', + ) + + + c.setFont('Helvetica',10) + c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) + c.showPage() + +## # full kuten chart in EUC +## c.setFont('Helvetica', 18) +## c.drawString(72,750, 'Characters available in GB 2312-80, EUC encoding') +## y = 600 +## enc = 'GB_EUC_H' +## for row in range(1, 95): +## KutenRowCodeChart(row, 'STSong-Light',enc).drawOn(c, 72, y) +## y = y - 125 +## if y < 50: +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## c.showPage() +## y = 700 +## + c.save() + if VERBOSE: + print 'saved '+outputfile('test_multibyte_chs.pdf') + + +def makeSuite(): + return makeSuiteForClasses(CHSFontTests) + + +#noruntests +if __name__ == "__main__": + VERBOSE = 1 + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_multibyte_cht.py b/bin/reportlab/test/test_multibyte_cht.py new file mode 100644 index 00000000000..2191d6e06ec --- /dev/null +++ b/bin/reportlab/test/test_multibyte_cht.py @@ -0,0 +1,131 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/rlj/jpsupport.py +# Temporary japanese support for ReportLab. +""" +Test of traditional Chinese (as written in Taiwan) +""" + + +import string, os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib import colors +from reportlab.lib.codecharts import Big5CodeChart, hBoxText + +global VERBOSE +VERBOSE = 0 + + +class CHTFontTests(unittest.TestCase): + + def hDraw(self, c, msg, fnt, x, y): + "Helper - draws it with a box around" + c.setFont(fnt, 16, 16) + c.drawString(x, y, msg) + c.rect(x,y,pdfmetrics.stringWidth(msg, fnt, 16),16,stroke=1,fill=0) + + + def test0(self): + "A basic document drawing some strings" + + # if they do not have the Japanese font files, go away quietly + from reportlab.pdfbase.cidfonts import UnicodeCIDFont, findCMapFile + + +## enc = 'ETenms-B5-H' +## try: +## findCMapFile(enc) +## except: +## #they don't have the font pack, return silently +## print 'CMap not found' +## return + pdfmetrics.registerFont(UnicodeCIDFont('MSung-Light')) + + c = Canvas(outputfile('test_multibyte_cht.pdf')) + c.setFont('Helvetica', 24) + c.drawString(100,700, 'Traditional Chinese Font Support') + c.setFont('Helvetica', 10) + c.drawString(100,680, 'Short sample: "Taiwan - Ang Lee" (movie director)') + + hBoxText(u'\u81fa\u7063 - \u674e\u5b89' , c, 100, 600, 'MSung-Light') + + +## #hBoxText(message3 + ' MHei-Medium', c, 100, 580, 'MHei-Medium', enc) +## +## +## +## c.setFont('Helvetica', 10) +## tx = c.beginText(100, 500) +## tx.textLines(""" +## This test document shows Traditional Chinese output from Reportlab PDF Library. +## You may use one Chinese font, MSung-Light, and a number of different +## encodings. +## +## The available encoding names (with comments from the PDF specification) are: +## encodings_cht = [ +## 'B5pc-H', # Macintosh, Big Five character set, Big Five encoding, +## # Script Manager code 2 +## 'B5pc-V', # Vertical version of B5pc-H +## 'ETen-B5-H', # Microsoft Code Page 950 (lfCharSet 0x88), Big Five +## # character set with ETen extensions +## 'ETen-B5-V', # Vertical version of ETen-B5-H +## 'ETenms-B5-H', # Microsoft Code Page 950 (lfCharSet 0x88), Big Five +## # character set with ETen extensions; this uses proportional +## # forms for half-width Latin characters. +## 'ETenms-B5-V', # Vertical version of ETenms-B5-H +## 'CNS-EUC-H', # CNS 11643-1992 character set, EUC-TW encoding +## 'CNS-EUC-V', # Vertical version of CNS-EUC-H +## 'UniCNS-UCS2-H', # Unicode (UCS-2) encoding for the Adobe-CNS1 +## # character collection +## 'UniCNS-UCS2-V' # Vertical version of UniCNS-UCS2-H. +## ] +## +## The next 32 pages show the complete character set available in the encoding +## "ETen-B5-H". This is Big5 with the ETen extensions. ETen extensions are the +## most common extension to Big5 and include circled and roman numbers, Japanese +## hiragana and katakana, Cyrillic and fractions in rows C6-C8; and 7 extra characters +## and some line drawing characters in row F9. +## """) +## c.drawText(tx) +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## +## c.showPage() +## +## # full Big5 code page +## c.setFont('Helvetica', 18) +## c.drawString(72,750, 'Characters available in Big 5') +## y = 500 +## for row in range(0xA1,0xFF): +## cc = Big5CodeChart(row, 'MSung-Light',enc) +## cc.charsPerRow = 16 +## cc.rows = 10 +## cc.codePoints = 160 +## cc.drawOn(c, 72, y) +## y = y - cc.height - 25 +## if y < 50: +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## c.showPage() +## y = 600 +## +## + c.save() + if VERBOSE: + print 'saved '+outputfile('test_multibyte_cht.pdf') + + +def makeSuite(): + return makeSuiteForClasses(CHTFontTests) + + +#noruntests +if __name__ == "__main__": + VERBOSE = 1 + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_multibyte_jpn.py b/bin/reportlab/test/test_multibyte_jpn.py new file mode 100644 index 00000000000..c973bc441c0 --- /dev/null +++ b/bin/reportlab/test/test_multibyte_jpn.py @@ -0,0 +1,460 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history www.reportlab.co.uk/rl-cgi/viewcvs.cgi/rlextra/rlj/jpsupport.py +# Temporary japanese support for ReportLab. +""" +The code in this module will disappear any day now and be replaced +by classes in reportlab.pdfbase.cidfonts +""" + + +import string, os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib import colors +from reportlab.lib.codecharts import KutenRowCodeChart +from reportlab.pdfbase.cidfonts import CIDFont, findCMapFile, UnicodeCIDFont +global VERBOSE +VERBOSE = 0 + + +class JapaneseFontTests(unittest.TestCase): + + def hDraw(self, c, msg, fnt, x, y): + "Helper - draws it with a box around" + c.setFont(fnt, 16, 16) + font = pdfmetrics.getFont(fnt) + c.drawString(x, y, msg) + width = font.stringWidth(msg, 16) + c.rect(x,y,width,16,stroke=1,fill=0) + + def test0(self): + "A basic document drawing some strings" + +## # if they do not have the Japanese font files, go away quietly +## try: +## from reportlab.pdfbase.cidfonts import CIDFont, findCMapFile, UnicodeCIDFont +## findCMapFile('90ms-RKSJ-H') +## findCMapFile('90msp-RKSJ-H') +## findCMapFile('UniJIS-UCS2-H') +## findCMapFile('EUC-H') +## except: +## #don't have the font pack. return silently +## return +## +## pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','90ms-RKSJ-H')) +## pdfmetrics.registerFont(CIDFont('HeiseiKakuGo-W5','90ms-RKSJ-H')) + + c = Canvas(outputfile('test_multibyte_jpn.pdf')) + c.setFont('Helvetica', 30) + c.drawString(100,700, 'Japanese Font Support') + + c.setStrokeColor(colors.red) + +## # the two typefaces +## c.setFont('HeiseiMin-W3-90ms-RKSJ-H', 16) +## # this says "This is HeiseiMincho" in shift-JIS. Not all our readers +## # have a Japanese PC, so I escaped it. On a Japanese-capable +## # system, print the string to see Kanji +## message1 = '\202\261\202\352\202\315\225\275\220\254\226\276\222\251\202\305\202\267\201B' +## c.drawString(100, 675, message1) +## wid = pdfmetrics.stringWidth(message1, 'HeiseiMin-W3-90ms-RKSJ-H', 16) +## c.rect(100,675,wid,16,stroke=1,fill=0) +## +## c.setFont('HeiseiKakuGo-W5-90ms-RKSJ-H', 16) +## # this says "This is HeiseiKakugo" in shift-JIS +## message2 = '\202\261\202\352\202\315\225\275\220\254\212p\203S\203V\203b\203N\202\305\202\267\201B' +## c.drawString(100, 650, message2) +## wid = pdfmetrics.stringWidth(message2, 'HeiseiKakuGo-W5-90ms-RKSJ-H', 16) +## c.rect(100,650,wid,16,stroke=1,fill=0) +## +## +## +## self.hDraw(c, '\223\214\213\236 says Tokyo in Shift-JIS', 'HeiseiMin-W3-90ms-RKSJ-H', 100, 600) +## +## +## pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','90msp-RKSJ-H')) +## self.hDraw(c, '\223\214\213\236, but in proportional Shift-JIS.', 'HeiseiMin-W3-90msp-RKSJ-H', 100, 575) +## +## pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','EUC-H')) +## self.hDraw(c, '\xC5\xEC\xB5\xFE says Tokyo in EUC', 'HeiseiMin-W3-EUC-H', 100, 550) +## +## #this is super-slow until we do encoding caching. +## pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','UniJIS-UCS2-H')) +## +## def asciiToUCS2(text): +## s = '' +## for ch in text: +## s = s + chr(0) + ch +## return s +## msg = '\x67\x71\x4E\xAC' + asciiToUCS2(' says Tokyo in UTF16') +## self.hDraw(c, msg,'HeiseiMin-W3-UniJIS-UCS2-H', 100, 525) + + #unicode font automatically supplies the encoding + pdfmetrics.registerFont(UnicodeCIDFont('HeiseiMin-W3')) + + + msg = u'\u6771\u4EAC : Unicode font, unicode input' + self.hDraw(c, msg, 'HeiseiMin-W3', 100, 500) + + msg = u'\u6771\u4EAC : Unicode font, utf8 input'.encode('utf8') + self.hDraw(c, msg, 'HeiseiMin-W3', 100, 475) + + +## # now try verticals +## pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','90ms-RKSJ-V')) +## c.setFont('HeiseiMin-W3-90ms-RKSJ-V', 16) +## c.drawString(400, 650, '\223\214\213\236 vertical Shift-JIS') +## height = c.stringWidth('\223\214\213\236 vertical Shift-JIS', 'HeiseiMin-W3-90ms-RKSJ-V', 16) +## c.rect(400-8,650,16,-height) +## +## pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','EUC-V')) +## c.setFont('HeiseiMin-W3-EUC-V', 16) +## c.drawString(425, 650, '\xC5\xEC\xB5\xFE vertical EUC') +## height = c.stringWidth('\xC5\xEC\xB5\xFE vertical EUC', 'HeiseiMin-W3-EUC-V', 16) +## c.rect(425-8,650,16,-height) +## + + + from reportlab.platypus.paragraph import Paragraph + from reportlab.lib.styles import ParagraphStyle + jStyle = ParagraphStyle('jtext', + fontName='HeiseiMin-W3', + fontSize=12, + wordWrap="CJK" + ) + + gatwickText = '\xe3\x82\xac\xe3\x83\x88\xe3\x82\xa6\xe3\x82\xa3\xe3\x83\x83\xe3\x82\xaf\xe7\xa9\xba\xe6\xb8\xaf\xe3\x81\xa8\xe9\x80\xa3\xe7\xb5\xa1\xe9\x80\x9a\xe8\xb7\xaf\xe3\x81\xa7\xe7\x9b\xb4\xe7\xb5\x90\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3\x82\x8b\xe5\x94\xaf\xe4\xb8\x80\xe3\x81\xae\xe3\x83\x9b\xe3\x83\x86\xe3\x83\xab\xe3\x81\xa7\xe3\x81\x82\xe3\x82\x8b\xe5\xbd\x93\xe3\x83\x9b\xe3\x83\x86\xe3\x83\xab\xe3\x81\xaf\xe3\x80\x81\xe8\xa1\x97\xe3\x81\xae\xe4\xb8\xad\xe5\xbf\x83\xe9\x83\xa8\xe3\x81\x8b\xe3\x82\x8930\xe5\x88\x86\xe3\x81\xae\xe5\xa0\xb4\xe6\x89\x80\xe3\x81\xab\xe3\x81\x94\xe3\x81\x96\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82\xe5\x85\xa8\xe5\xae\xa2\xe5\xae\xa4\xe3\x81\xab\xe9\xab\x98\xe9\x80\x9f\xe3\x82\xa4\xe3\x83\xb3\xe3\x82\xbf\xe3\x83\xbc\xe3\x83\x8d\xe3\x83\x83\xe3\x83\x88\xe7\x92\xb0\xe5\xa2\x83\xe3\x82\x92\xe5\xae\x8c\xe5\x82\x99\xe3\x81\x97\xe3\x81\xa6\xe3\x81\x8a\xe3\x82\x8a\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82\xe3\x83\x95\xe3\x82\xa1\xe3\x83\x9f\xe3\x83\xaa\xe3\x83\xbc\xe3\x83\xab\xe3\x83\xbc\xe3\x83\xa0\xe3\x81\xaf5\xe5\x90\x8d\xe6\xa7\x98\xe3\x81\xbe\xe3\x81\xa7\xe3\x81\x8a\xe6\xb3\x8a\xe3\x82\x8a\xe3\x81\x84\xe3\x81\x9f\xe3\x81\xa0\xe3\x81\x91\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82\xe3\x81\xbe\xe3\x81\x9f\xe3\x80\x81\xe3\x82\xa8\xe3\x82\xb0\xe3\x82\xbc\xe3\x82\xaf\xe3\x83\x86\xe3\x82\xa3\xe3\x83\x96\xe3\x83\xab\xe3\x83\xbc\xe3\x83\xa0\xe3\x81\xae\xe3\x81\x8a\xe5\xae\xa2\xe6\xa7\x98\xe3\x81\xaf\xe3\x80\x81\xe3\x82\xa8\xe3\x82\xb0\xe3\x82\xbc\xe3\x82\xaf\xe3\x83\x86\xe3\x82\xa3\xe3\x83\x96\xe3\x83\xa9\xe3\x82\xa6\xe3\x83\xb3\xe3\x82\xb8\xe3\x82\x92\xe3\x81\x94\xe5\x88\xa9\xe7\x94\xa8\xe3\x81\x84\xe3\x81\x9f\xe3\x81\xa0\xe3\x81\x91\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82\xe4\xba\x8b\xe5\x89\x8d\xe3\x81\xab\xe3\x81\x94\xe4\xba\x88\xe7\xb4\x84\xe3\x81\x84\xe3\x81\x9f\xe3\x81\xa0\xe3\x81\x91\xe3\x82\x8b\xe3\x82\xbf\xe3\x82\xa4\xe3\x83\xa0\xe3\x83\x88\xe3\x82\xa5\xe3\x83\x95\xe3\x83\xa9\xe3\x82\xa4\xe3\x83\xbb\xe3\x83\x91\xe3\x83\x83\xe3\x82\xb1\xe3\x83\xbc\xe3\x82\xb8\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x81\xe7\xa9\xba\xe6\xb8\xaf\xe3\x81\xae\xe9\xa7\x90\xe8\xbb\x8a\xe6\x96\x99\xe9\x87\x91\xe3\x81\x8c\xe5\x90\xab\xe3\x81\xbe\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x8a\xe3\x82\x8a\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82' + + c.setFont('HeiseiMin-W3', 12) +## from reportlab.lib.textsplit import wordSplit +## y = 400 +## splat = wordSplit(gatwickText, 250, 'HeiseiMin-W3', 12, encoding='utf8') +## for (line, extraSpace) in splat: +## c.drawString(100,y,line) +## y -= 14 + jPara = Paragraph(gatwickText, jStyle) + jPara.wrap(250, 200) + #from pprint import pprint as pp + #pp(jPara.blPara) + jPara.drawOn(c, 100, 250) + + c.setFillColor(colors.purple) + tx = c.beginText(100, 200) + tx.setFont('Helvetica', 12) + tx.textLines("""This document shows sample output in Japanese + from the Reportlab PDF library. This page shows the two fonts + available and tests our ability to measure the width of glyphs + in both horizontal and vertical writing, with proportional and + fixed-width characters. The red boxes should be the same width + (or height) as the character strings they surround. + The next pages show more samples and information. + """) + c.drawText(tx) + c.setFont('Helvetica',10) + c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) + + + + c.showPage() + + c.setFont('Helvetica', 30) + c.drawString(100,700, 'Japanese TrueType Font Support') + msg = u'\u6771\u4EAC : Unicode font, utf8 input'.encode('utf8') + from reportlab.pdfbase.ttfonts import TTFont + try: + msmincho = TTFont('MS Mincho','msmincho.ttc',subfontIndex=0) + fn = ' file=msmincho.ttc subfont 0' + except: + try: + msmincho = TTFont('MS Mincho','msmincho.ttf') + fn = 'file=msmincho.ttf' + except: + msmincho = None + if msmincho is None: + c.drawString(100,600, 'Cannot find msmincho.ttf or msmincho.ttc') + else: + pdfmetrics.registerFont(msmincho) + c.setFont('MS Mincho', 30) + c.drawString(100,600, msg+fn) + if fn.endswith('0'): + try: + msmincho1 = TTFont('MS Mincho 1','msmincho.ttc',subfontIndex=1) + pdfmetrics.registerFont(msmincho1) + fn = ' file=msmincho.ttc subfont 1' + c.setFont('MS Mincho 1',30) + c.drawString(100,500,msg+fn) + except: + c.setFont('Helvetica',30) + c.drawString(100,500,msg+fn) + + c.showPage() + + # realistic text sample +## sample = """Adobe Acrobat +##\x83h\x83L\x83\x85\x83\x81\x83\x93\x83g\x82\xaa\x8aJ\x82\xa9\x82\xc8\x82\xad\x82\xc4\x8d\xa2\x82\xc1\x82\xbd\x82\xb1\x82\xc6\x82\xcd +##\x82\xa0\x82\xe8\x82\xdc\x82\xb9\x82\xf1\x82\xa9\x81B\x8e\x96\x8b\xc6\x8cv\x89\xe6\x8f\x91\x81A\x89c\x8b\xc6\x83\x8c\x83|\x81[\x83g +##\x81A\x83J\x83^\x83\x8d\x83O\x82\xe2\x83p\x83\x93\x83t\x83\x8c\x83b\x83g\x82\xc8\x82\xc7\x90\xa7\x8d\xec\x95\xa8\x82\xcc\x8e\xed +##\x97\xde\x82\xc9\x82\xa9\x82\xa9\x82\xed\x82\xe7\x82\xb8\x81A +##\x83h\x83L\x83\x85\x83\x81\x83\x93\x83g\x82\xcdAdobe® Acrobat® 5.0\x82\xf0\x8eg\x82\xc1\x82\xc4Adobe PDF\x81iPortable Document +##Format\x81j\x83t\x83@\x83C\x83\x8b\x82\xc9\x95\xcf\x8a\xb7\x82\xb5\x82\xdc\x82\xb5\x82\xe5\x82\xa4\x81B\x96\xb3\x8f\x9e\x94z\x95z\x82\xcc +##Adobe Acrobat Reader\x82\xf0\x8eg\x82\xa6\x82\xce\x81A\x83n\x81[\x83h\x83E\x83F\x83A\x81A\x83\\\x83t\x83g\x83E\x83F\x83A\x82\xc9\x82\xa9 +##\x82\xa9\x82\xed\x82\xe7\x82\xb8\x81A\x92N\x82\xc5\x82\xe0\x82\xa0\x82\xc8\x82\xbd\x82\xcc\x83h\x83L\x83\x85\x83\x81\x83\x93\x83g\x82\xf0 +##\x83I\x83\x8a\x83W\x83i\x83\x8b\x82\xcc\x91\xcc\x8d\xd9\x82\xc5\x8aJ\x82\xad\x82\xb1\x82\xc6\x82\xaa\x82\xc5\x82\xab\x82\xdc\x82\xb7\x81B +##\x82\xa0\x82\xc8\x82\xbd\x82\xcc\x88\xd3\x90}\x82\xb5\x82\xbd\x82\xc6\x82\xa8\x82\xe8\x82\xc9\x8f\xee\x95\xf1\x82\xf0\x93`\x82\xa6\x82\xe9 +##\x82\xb1\x82\xc6\x82\xaa\x82\xc5\x82\xab\x82\xdc\x82\xb7\x81B +##\x82\xb3\x82\xe7\x82\xc9\x81AAdobe Acrobat 5.0\x82\xc5\x82\xcd\x81AWeb\x83u\x83\x89\x83E\x83U\x82\xa9\x82\xe7\x83R\x83\x81\x83\x93\x83g\x82\xe2 +##\x83}\x81[\x83N\x83A\x83b\x83v\x82\xf0\x8f\x91\x82\xab\x8d\x9e\x82\xf1\x82\xbe\x82\xe8\x81A\x93d\x8eq\x8f\x90\x96\xbc\x82\xf0\x8f\x91\x82\xab +##\x8d\x9e\x82\xdd\x81A\x8c\xb4\x96{\x82\xc6\x82\xb5\x82\xc4\x83\x8d\x81[\x83J\x83\x8b\x82\xc9\x95\xdb\x91\xb6\x82\xb7\x82\xe9\x82\xb1\x82\xc6\x82\xe0\x89\xc2\x94\\\x82\xc5\x82\xb7\x81B +##\x8a\xe9\x8b\xc6\x93\xe0\x82\xa0\x82\xe9\x82\xa2\x82\xcd\x8a\xe9\x8b\xc6\x82\xcc\x98g\x82\xf0\x92\xb4\x82\xa6\x82\xc4\x83`\x81[\x83\x80\x82\xc5 +##\x82\xcc\x83h\x83L\x83\x85\x83\x81\x83\x93\x83g\x83\x8f\x81[\x83N\x82\xcc\x90\xb6\x8eY\x90\xab\x82\xf0\x8c\xfc\x8f\xe3\x82\xb3\x82\xb9\x82\xe9\x82\xb1\x82\xc6\x82\xaa\x82\xc5\x82\xab\x82\xdc\x82\xb7\x81B +## +##Adobe Acrobat 5.0\x82\xc5\x8d\xec\x90\xac\x82\xb5\x82\xbdAdobe PDF\x82\xcd\x81A(Acrobat 5.0\x82\xc5\x82\xcc\x82\xdd\x83T\x83|\x81[\x83g +##\x82\xb5\x82\xc4\x82\xa2\x82\xe9\x88\xc3\x8d\x86\x89\xbb\x90\xdd\x92\xe8\x82\xf0\x8f\x9c\x82\xa2\x82\xc4\x82\xcd)\x8f]\x97\x88\x82\xdc +##\x82\xc5\x82\xcc\x83o\x81[\x83W\x83\x87\x83\x93(3\x82\xa8\x82\xe6\x82\xd1\x82S)\x82\xccAcrobat Reader\x82\xc5\x82\xe0\x8aJ\x82\xad +##\x82\xb1\x82\xc6\x82\xaa\x82\xc5\x82\xab\x82\xdc\x82\xb7\x81B\x8f\xee\x95\xf1\x8b\xa4\x97L\x82\xcc\x83c\x81[\x83\x8b\x82\xc6\x82\xb5 +##\x82\xc4\x81A\x82\xb3\x82\xe7\x82\xc9\x90i\x95\xe0\x82\xb5\x82\xbdAdobe Acrobat 5.0\x82\xf0\x81A\x8f]\x97\x88\x82\xcc\x8a\xc2\x8b\xab +##\x82\xc5\x82\xe0\x88\xc0\x90S\x82\xb5\x82\xc4\x82\xb2\x97\x98\x97p\x82\xa2\x82\xbd\x82\xbe\x82\xaf\x82\xdc\x82\xb7\x81B +## +##\x96{\x90\xbb\x95i\x82\xf0\x83l\x83b\x83g\x83\x8f\x81[\x83N\x82\xc8\x82\xc7\x82\xf0\x89\xee\x82\xb5\x82\xc4\x92\xbc\x90\xda\x82\xa0\x82\xe9 +##\x82\xa2\x82\xcd\x8a\xd4\x90\xda\x82\xc9\x95\xa1\x90\x94\x82\xcc\x92[\x96\x96\x82\xa9\x82\xe7\x8eg\x97p\x82\xb7\x82\xe9\x8f\xea\x8d\x87\x81A +##\x82\xbb\x82\xcc\x92[\x96\x96\x82\xc6\x93\xaf\x90\x94\x82\xcc\x83\x89\x83C\x83Z\x83\x93\x83X\x82\xf0\x82\xb2\x8dw\x93\xfc\x82\xad\x82\xbe +##\x82\xb3\x82\xa2\x81B\x96{\x90\xbb\x95i\x82\xcd\x83N\x83\x89\x83C\x83A\x83\x93\x83g\x97p\x83\\\x83t\x83g\x83E\x83F\x83A\x82\xc5\x82\xa0\x82\xe8 +##\x81A\x83T\x81[\x83o\x97p\x83\\\x83t\x83g\x83E\x83F\x83A\x82\xc6\x82\xb5\x82\xc4\x82\xa8\x8eg\x82\xa2\x82\xa2\x82\xbd\x82\xbe\x82\xad\x82\xb1\x82\xc6 +##\x82\xcd\x81A\x8f\xe3\x8bL\x95\xfb\x96@\x82\xc9\x82\xe6\x82\xe9\x88\xc8\x8aO\x81A\x8b\x96\x91\xf8\x82\xb3\x82\xea\x82\xc4\x82\xa2\x82\xdc\x82\xb9 +##\x82\xf1\x81B\x95\xa1\x90\x94\x82\xcc\x83\x89\x83C\x83Z\x83\x93\x83X\x82\xf0\x82\xb2\x8dw\x93\xfc\x82\xb3\x82\xea\x82\xe9\x8f\xea\x8d\x87\x82\xc9 +##\x82\xcd\x83\x89\x83C\x83Z\x83\x93\x83X\x83v\x83\x8d\x83O\x83\x89\x83\x80\x82\xf0\x82\xb2\x97\x98\x97p\x82\xc9\x82\xc8\x82\xe9\x82\xc6\x82\xa8\x93\xbe\x82\xc5\x82\xb7\x81B +## +## +##\x81y\x82\xa8\x92m\x82\xe7\x82\xb9\x81zMicrosoft Office XP\x82\xa9\x82\xe7PDF\x82\xf0\x8d\xec\x90\xac\x82\xb7\x82\xe9\x82\xc9\x82\xcd +##""" +## c.setFont('Helvetica', 24) +## c.drawString(100,750, "Sample text from Adobe's web site") +## tx = c.beginText(100,700) +## tx.setFont('Helvetica', 10) +## tx.textLine('Note: line wrapping has not been preserved and some lines may be wrapped in mid-word.') +## tx.textLine('We are just testing that we see Japanese and not random characters!') +## tx.setFont('HeiseiMin-W3-90ms-RKSJ-H',6) +## tx.textLines(sample) +## tx.setFont('Helvetica', 8) +## tx.textLine() +## tx.textLine() +## tx.textLines(""" +## This test document shows Japanese output from the Reportlab PDF Library. +## You may use two fonts, HeiseiMin-W3 and HeiseiKakuGo-W5, and a number of +## different encodings. +## +## The available encoding names (with comments from the PDF specification) are: +## encodings_jpn = [ +## # official encoding names, comments taken verbatim from PDF Spec +## '83pv-RKSJ-H', #Macintosh, JIS X 0208 character set with KanjiTalk6 +## #extensions, Shift-JIS encoding, Script Manager code 1 +## '90ms-RKSJ-H', #Microsoft Code Page 932 (lfCharSet 0x80), JIS X 0208 +## #character set with NEC and IBM extensions +## '90ms-RKSJ-V', #Vertical version of 90ms-RKSJ-H +## '90msp-RKSJ-H', #Same as 90ms-RKSJ-H, but replaces half-width Latin +## #characters with proportional forms +## '90msp-RKSJ-V', #Vertical version of 90msp-RKSJ-H +## '90pv-RKSJ-H', #Macintosh, JIS X 0208 character set with KanjiTalk7 +## #extensions, Shift-JIS encoding, Script Manager code 1 +## 'Add-RKSJ-H', #JIS X 0208 character set with Fujitsu FMR extensions, +## #Shift-JIS encoding +## 'Add-RKSJ-V', #Vertical version of Add-RKSJ-H +## 'EUC-H', #JIS X 0208 character set, EUC-JP encoding +## 'EUC-V', #Vertical version of EUC-H +## 'Ext-RKSJ-H', #JIS C 6226 (JIS78) character set with NEC extensions, +## #Shift-JIS encoding +## 'Ext-RKSJ-V', #Vertical version of Ext-RKSJ-H +## 'H', #JIS X 0208 character set, ISO-2022-JP encoding, +## 'V', #Vertical version of H +## 'UniJIS-UCS2-H', #Unicode (UCS-2) encoding for the Adobe-Japan1 character +## #collection +## 'UniJIS-UCS2-V', #Vertical version of UniJIS-UCS2-H +## 'UniJIS-UCS2-HW-H', #Same as UniJIS-UCS2-H, but replaces proportional Latin +## #characters with half-width forms +## 'UniJIS-UCS2-HW-V' #Vertical version of UniJIS-UCS2-HW-H +## ] +## +## The next few pages show the complete character set available in the encoding +## "90ms-RKSJ-H" - Shift-JIS with the standard Microsoft extensions. +## """) +## c.drawText(tx) +## +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## +## +## +## c.showPage() + + from reportlab.lib import textsplit + + c.setFont('HeiseiMin-W3', 14) + y = 700 + c.drawString(70, y, 'cannot end line') + y -= 20 + for group in textsplit.CANNOT_START_LINE: + c.drawString(70, y, group) + y -= 20 + c.setFont('Helvetica',10) + c.drawString(70, y, ' '.join(map(lambda x: repr(x)[4:-1], group))) + c.setFont('HeiseiMin-W3', 14) + y -= 20 + + + + y -= 20 + c.drawString(70, y, 'cannot end line') + y -= 20 + for group in textsplit.CANNOT_END_LINE: + c.drawString(70, y, group) + y -= 20 + c.setFont('Helvetica',10) + c.drawString(70, y, ' '.join(map(lambda x: repr(x)[2:], group))) + c.setFont('HeiseiMin-W3', 14) + y -= 20 + + c.showPage() + + #utf8 encoded paragraph + sample2_uni = u'''\u30ac\u30c8\u30a6\u30a3\u30c3\u30af\u7a7a\u6e2f\u3068\u9023\u7d61\u901a + \u8def\u3067\u76f4\u7d50\u3055\u308c\u3066\u3044\u308b\u552f\u4e00\u306e\u30db\u30c6\u30eb + \u3067\u3042\u308b\u5f53\u30db\u30c6\u30eb\u306f\u3001\u8857\u306e\u4e2d\u5fc3\u90e8\u304b + \u308930\u5206\u306e\u5834\u6240\u306b\u3054\u3056\u3044\u307e\u3059\u3002\u5168\u5ba2\u5ba4 + \u306b\u9ad8\u901f\u30a4\u30f3\u30bf\u30fc\u30cd\u30c3\u30c8\u74b0\u5883\u3092\u5b8c\u5099 + \u3057\u3066\u304a\u308a\u307e\u3059\u3002\u30d5\u30a1\u30df\u30ea\u30fc\u30eb\u30fc\u30e0 + \u306f5\u540d\u69d8\u307e\u3067\u304a\u6cca\u308a\u3044\u305f\u3060\u3051\u307e\u3059\u3002 + \u307e\u305f\u3001\u30a8\u30b0\u30bc\u30af\u30c6\u30a3\u30d6\u30eb\u30fc\u30e0\u306e\u304a + \u5ba2\u69d8\u306f\u3001\u30a8\u30b0\u30bc\u30af\u30c6\u30a3\u30d6\u30e9\u30a6\u30f3\u30b8 + \u3092\u3054\u5229\u7528\u3044\u305f\u3060\u3051\u307e\u3059\u3002\u4e8b\u524d\u306b\u3054 + \u4e88\u7d04\u3044\u305f\u3060\u3051\u308b\u30bf\u30a4\u30e0\u30c8\u30a5\u30d5\u30e9\u30a4 + \u30fb\u30d1\u30c3\u30b1\u30fc\u30b8\u306b\u306f\u3001\u7a7a\u6e2f\u306e\u99d0\u8eca\u6599 + \u91d1\u304c\u542b\u307e\u308c\u3066\u304a\u308a\u307e\u3059\u3002''' + + oneline_uni = u''.join(sample2_uni.split()) + sample2_utf8 = oneline_uni.encode('utf8') + + from reportlab.platypus import Paragraph + from reportlab.lib.styles import ParagraphStyle + jsty = ParagraphStyle('japanese',fontName='HeiseiMin-W3', wordWrap='CJK') + jpara = Paragraph(oneline_uni, style=jsty) + + c.drawString(100, 710, 'Try to wrap a paragraph using a style with wordWrap="CJK"') + w, h = jpara.wrap(400,400) + jpara.drawOn(c, 100, 700 - h) + + #now try to split it... + c.drawString(100, 510, 'Now try to split a paragraph as if over a page break') + + topPara, bottomPara = jpara.split(400, 30) + w1, h1 = topPara.wrap(400, 30) + topPara.drawOn(c, 100, 450) + + w2, h2 = bottomPara.wrap(400, 30) + bottomPara.drawOn(c, 100, 400) + #print 'split into heights %0.2f, %0.2f' % (topPara.height, bottomPara.height) + + + + +## c.showPage() +## +## +## # full kuten chart in EUC +## c.setFont('Helvetica', 24) +## c.drawString(72,750, 'Characters available in JIS 0208-1997') +## y = 600 +## for row in range(1, 95): +## KutenRowCodeChart(row, 'HeiseiMin-W3','EUC-H').drawOn(c, 72, y) +## y = y - 125 +## if y < 50: +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## c.showPage() +## y = 700 +## +## c.showPage() + + + #try with Unicode truetype - Mincho for starters +## import time +## started = time.clock() +## c.showPage() +## c.setFont('Helvetica',16) +## c.drawString(100,750, 'About to say Tokyo in MS Gothic...') +## +## from reportlab.pdfbase.ttfonts import TTFont, TTFontFile +## f = TTFontFile("msgothic.ttf") +## print f.name +## +## pdfmetrics.registerFont(TTFont(f.name, f)) +## +## utfText = u'Andr\202'.encode('utf8') +## c.setFont(f.name,16) +## c.drawString(100,700, utfText) +## +## +## #tokyoUCS2 = '\x67\x71\x4E\xAC' +## finished = time.clock() + + + + + + c.save() + + + if VERBOSE: + print 'saved test_multibyte_jpn.pdf' + + + def ___test2_all(self): + """Dumps out ALl GLYPHS in a CID font. + + Reach for your microscope :-)""" + try: + from reportlab.pdfbase.cidfonts import CIDFont, findCMapFile + findCMapFile('90ms-RKSJ-H') + findCMapFile('Identity-H') + except: + #don't have the font pack. return silently + return + + pdfmetrics.registerFont(CIDFont('HeiseiMin-W3','Identity-H')) + + c = Canvas('test_japanese_2.pdf') + c.setFont('Helvetica', 30) + c.drawString(100,800, 'All Glyphs in Adobe-Japan-1-2 collection!') + + # the two typefaces + c.setFont('HeiseiMin-W3-Identity-H', 2) + + x0 = 50 + y0 = 700 + dx = 2 + dy = 2 + for row in range(256): + for cell in range(256): + s = chr(row) + chr(cell) + x = x0 + cell*dx + y = y0 - row*dy + c.drawString(x,y,s) + + c.save() + if VERBOSE: + print 'saved '+outputfile('test_multibyte_jpn.pdf') + + +def makeSuite(): + return makeSuiteForClasses(JapaneseFontTests) + + +#noruntests +if __name__ == "__main__": + VERBOSE = 1 + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_multibyte_kor.py b/bin/reportlab/test/test_multibyte_kor.py new file mode 100644 index 00000000000..8f100b6627b --- /dev/null +++ b/bin/reportlab/test/test_multibyte_kor.py @@ -0,0 +1,125 @@ + +import string, os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib import colors +from reportlab.lib.codecharts import KutenRowCodeChart, hBoxText +from reportlab.pdfbase.cidfonts import UnicodeCIDFont, findCMapFile +global VERBOSE +VERBOSE = 0 + + + +class KoreanFontTests(unittest.TestCase): + + def test0(self): + + # if they do not have the font files or encoding, go away quietly +## try: +## from reportlab.pdfbase.cidfonts import CIDFont, findCMapFile +## findCMapFile('KSCms-UHC-H') +## except: +## #don't have the font pack. return silently +## print 'CMap not found' +## return + + localFontName = 'HYSMyeongJo-Medium' + c = Canvas(outputfile('test_multibyte_kor.pdf')) + c.setFont('Helvetica', 30) + c.drawString(100,700, 'Korean Font Support') + c.setFont('Helvetica', 10) + c.drawString(100,680, 'Short sample in Unicode; grey area should outline the text with correct width.') + + + hBoxText(u'\ub300\ud55c\ubbfc\uad6d = Korea', + c, 100, 660, 'HYSMyeongJo-Medium') + hBoxText(u'\uc548\uc131\uae30 = AHN Sung-Gi (Actor)', + c, 100, 640, 'HYGothic-Medium') + +## pdfmetrics.registerFont(UnicodeCIDFont('HYSMyeongJo-Medium')) +## c.setFont('Helvetica', 10) +## c.drawString(100,610, "Longer sample From Adobe's Acrobat web page in EUC:") +## +## sample = """\xbf\xad \xbc\xf6 \xbe\xf8\xb4\xc2 \xb9\xae\xbc\xad\xb4\xc2 \xbe\xc6\xb9\xab\xb7\xb1 \xbc\xd2\xbf\xeb\xc0\xcc \xbe\xf8\xbd\xc0\xb4\xcf\xb4\xd9. \xbb\xe7\xbe\xf7 \xb0\xe8\xc8\xb9\xbc\xad, \xbd\xba\xc7\xc1\xb7\xb9\xb5\xe5\xbd\xc3\xc6\xae, \xb1\xd7\xb7\xa1\xc7\xc8\xc0\xcc \xb8\xb9\xc0\xcc \xc6\xf7\xc7\xd4\xb5\xc8 \xbc\xd2\xc3\xa5\xc0\xda \xb6\xc7\xb4\xc2 \xc0\xa5 +##\xbb\xe7\xc0\xcc\xc6\xae\xb8\xa6 \xc0\xdb\xbc\xba\xc7\xcf\xb4\xc2 \xb0\xe6\xbf\xec Adobe\xa2\xe7 Acrobat\xa2\xe7 5.0 \xbc\xd2\xc7\xc1\xc6\xae\xbf\xfe\xbe\xee\xb8\xa6 \xbb\xe7\xbf\xeb\xc7\xd8\xbc\xad \xc7\xd8\xb4\xe7 \xb9\xae\xbc\xad\xb8\xa6 Adobe +##Portable Document Format (PDF) \xc6\xc4\xc0\xcf\xb7\xce \xba\xaf\xc8\xaf\xc7\xd2 \xbc\xf6 \xc0\xd6\xbd\xc0\xb4\xcf\xb4\xd9. \xb4\xa9\xb1\xb8\xb3\xaa \xb1\xa4\xb9\xfc\xc0\xa7\xc7\xd1 \xc1\xbe\xb7\xf9\xc0\xc7 +##\xc7\xcf\xb5\xe5\xbf\xfe\xbe\xee\xbf\xcd \xbc\xd2\xc7\xc1\xc6\xae\xbf\xfe\xbe\xee\xbf\xa1\xbc\xad \xb9\xae\xbc\xad\xb8\xa6 \xbf\xad \xbc\xf6 \xc0\xd6\xc0\xb8\xb8\xe7 \xb7\xb9\xc0\xcc\xbe\xc6\xbf\xf4, \xc6\xf9\xc6\xae, \xb8\xb5\xc5\xa9, \xc0\xcc\xb9\xcc\xc1\xf6 \xb5\xee\xc0\xbb \xbf\xf8\xba\xbb \xb1\xd7\xb4\xeb\xb7\xce \xc0\xc7\xb5\xb5\xc7\xd1 \xb9\xd9 \xb4\xeb\xb7\xce +##\xc7\xa5\xbd\xc3\xc7\xd2 \xbc\xf6 \xc0\xd6\xbd\xc0\xb4\xcf\xb4\xd9. Acrobat 5.0\xc0\xbb \xbb\xe7\xbf\xeb\xc7\xcf\xbf\xa9 \xc0\xa5 \xba\xea\xb6\xf3\xbf\xec\xc0\xfa\xbf\xa1\xbc\xad \xb9\xae\xbc\xad\xb8\xa6 \xbd\xc2\xc0\xce\xc7\xcf\xb0\xed \xc1\xd6\xbc\xae\xc0\xbb \xc3\xdf\xb0\xa1\xc7\xcf\xb4\xc2 \xb9\xe6\xbd\xc4\xc0\xb8\xb7\xce +##\xb1\xe2\xbe\xf7\xc0\xc7 \xbb\xfd\xbb\xea\xbc\xba\xc0\xbb \xc7\xe2\xbb\xf3\xbd\xc3\xc5\xb3 \xbc\xf6 \xc0\xd6\xbd\xc0\xb4\xcf\xb4\xd9. +## +##\xc0\xfa\xc0\xdb\xb1\xc7 © 2001 Adobe Systems Incorporated. \xb8\xf0\xb5\xe7 \xb1\xc7\xb8\xae\xb0\xa1 \xba\xb8\xc8\xa3\xb5\xcb\xb4\xcf\xb4\xd9. +##\xbb\xe7\xbf\xeb\xc0\xda \xbe\xe0\xb0\xfc +##\xbf\xc2\xb6\xf3\xc0\xce \xbb\xe7\xbf\xeb\xc0\xda \xba\xb8\xc8\xa3 \xb1\xd4\xc1\xa4 +##Adobe\xc0\xc7 \xc0\xe5\xbe\xd6\xc0\xda \xc1\xf6\xbf\xf8 +##\xbc\xd2\xc7\xc1\xc6\xae\xbf\xfe\xbe\xee \xba\xd2\xb9\xfd \xc0\xcc\xbf\xeb \xb9\xe6\xc1\xf6 +##""" +## tx = c.beginText(100,600) +## tx.setFont('HYSMyeongJo-Medium-KSC-EUC-H', 7, 8) +## tx.textLines(sample) +## tx.setFont('Helvetica', 10, 12) +## tx.textLine() +## tx.textLines("""This test document shows Korean output from the Reportlab PDF Library. +## You may use one Korean font, HYSMyeongJo-Medium, and a number of different +## encodings. +## +## The available encoding names (with comments from the PDF specification) are: +## encodings_kor = [ +## 'KSC-EUC-H', # KS X 1001:1992 character set, EUC-KR encoding +## 'KSC-EUC-V', # Vertical version of KSC-EUC-H +## 'KSCms-UHC-H', # Microsoft Code Page 949 (lfCharSet 0x81), KS X 1001:1992 +## #character set plus 8,822 additional hangul, Unified Hangul +## #Code (UHC) encoding +## 'KSCms-UHC-V', #Vertical version of KSCms-UHC-H +## 'KSCms-UHC-HW-H', #Same as KSCms-UHC-H, but replaces proportional Latin +## # characters with halfwidth forms +## 'KSCms-UHC-HW-V', #Vertical version of KSCms-UHC-HW-H +## 'KSCpc-EUC-H', #Macintosh, KS X 1001:1992 character set with MacOS-KH +## #extensions, Script Manager Code 3 +## 'UniKS-UCS2-H', #Unicode (UCS-2) encoding for the Adobe-Korea1 character collection +## 'UniKS-UCS2-V' #Vertical version of UniKS-UCS2-H +## ] +## +## The following pages show all characters in the KS X 1001:1992 standard, using the +## encoding 'KSC-EUC-H' above. More characters (a LOT more) are available if you +## use UHC encoding or the Korean Unicode subset, for which the correct encoding +## names are also listed above. +## """) +## +## c.drawText(tx) +## +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## c.showPage() +## +## # full kuten chart in EUC +## c.setFont('Helvetica', 18) +## c.drawString(72,750, 'Characters available in KS X 1001:1992, EUC encoding') +## y = 600 +## for row in range(1, 95): +## KutenRowCodeChart(row, 'HYSMyeongJo-Medium','KSC-EUC-H').drawOn(c, 72, y) +## y = y - 125 +## if y < 50: +## c.setFont('Helvetica',10) +## c.drawCentredString(297, 36, 'Page %d' % c.getPageNumber()) +## c.showPage() +## y = 700 + + c.save() + + if VERBOSE: + print 'saved '+outputfile('test_multibyte_kor.pdf') + + +def makeSuite(): + return makeSuiteForClasses(KoreanFontTests) + + +#noruntests +if __name__ == "__main__": + VERBOSE = 1 + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_paragraphs.py b/bin/reportlab/test/test_paragraphs.py new file mode 100644 index 00000000000..bd11843e6db --- /dev/null +++ b/bin/reportlab/test/test_paragraphs.py @@ -0,0 +1,174 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_paragraphs.py +# tests some paragraph styles + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.platypus import Paragraph, SimpleDocTemplate, XBox, Indenter, XPreformatted +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.units import inch +from reportlab.lib.colors import red, black, navy, white, green +from reportlab.lib.randomtext import randomText +from reportlab.rl_config import defaultPageSize + +(PAGE_WIDTH, PAGE_HEIGHT) = defaultPageSize + + +def myFirstPage(canvas, doc): + canvas.saveState() + canvas.setStrokeColor(red) + canvas.setLineWidth(5) + canvas.line(66,72,66,PAGE_HEIGHT-72) + canvas.setFont('Times-Bold',24) + canvas.drawString(108, PAGE_HEIGHT-54, "TESTING PARAGRAPH STYLES") + canvas.setFont('Times-Roman',12) + canvas.drawString(4 * inch, 0.75 * inch, "First Page") + canvas.restoreState() + + +def myLaterPages(canvas, doc): + canvas.saveState() + canvas.setStrokeColor(red) + canvas.setLineWidth(5) + canvas.line(66,72,66,PAGE_HEIGHT-72) + canvas.setFont('Times-Roman',12) + canvas.drawString(4 * inch, 0.75 * inch, "Page %d" % doc.page) + canvas.restoreState() + + +class ParagraphTestCase(unittest.TestCase): + "Test Paragraph class (eyeball-test)." + + def test0(self): + """Test... + + The story should contain... + + Features to be visually confirmed by a human being are: + + 1. ... + 2. ... + 3. ... + """ + + story = [] + + #need a style + styNormal = ParagraphStyle('normal') + styGreen = ParagraphStyle('green',parent=styNormal,textColor=green) + + # some to test + stySpaced = ParagraphStyle('spaced', + parent=styNormal, + spaceBefore=12, + spaceAfter=12) + + + story.append( + Paragraph("This is a normal paragraph. " + + randomText(), styNormal)) + story.append( + Paragraph("This has 12 points space before and after, set in the style. " + + randomText(), stySpaced)) + story.append( + Paragraph("This is normal. " + + randomText(), styNormal)) + + story.append( + Paragraph(""" + This has 12 points space before and after, set inline with + XML tag. It works too.""" + randomText() + "This got a background from the para tag""", styNormal)) + + + story.append( + Paragraph("""\n\tThis has newlines and tabs on the front but inside the para tag""", styNormal)) + story.append( + Paragraph(""" This has spaces on the front but inside the para tag""", styNormal)) + + story.append( + Paragraph("""\n\tThis has newlines and tabs on the front but no para tag""", styNormal)) + story.append( + Paragraph(""" This has spaces on the front but no para tag""", styNormal)) + + story.append(Paragraph("""This has blue text here.""", styNormal)) + story.append(Paragraph("""This has italic text here.""", styNormal)) + story.append(Paragraph("""This has bold text here.""", styNormal)) + story.append(Paragraph("""This has underlined text here.""", styNormal)) + story.append(Paragraph("""This has blue and red underlined text here.""", styNormal)) + story.append(Paragraph("""green underlining""", styGreen)) + story.append(Paragraph("""green underlining""", styGreen)) + story.append(Paragraph("""This has m2 a superscript.""", styNormal)) + story.append(Paragraph("""This has m2 a subscript. Like H2O!""", styNormal)) + story.append(Paragraph("""This has a font change to Helvetica.""", styNormal)) + #This one fails: + #story.append(Paragraph("""This has a font change to Helvetica-Oblique.""", styNormal)) + story.append(Paragraph("""This has a font change to Helvetica in italics.""", styNormal)) + + story.append(Paragraph('''This one uses upper case tags and has set caseSensitive=0: Here comes Helvetica 14 with strong emphasis.''', styNormal, caseSensitive=0)) + story.append(Paragraph('''The same as before, but has set not set caseSensitive, thus the tags are ignored: Here comes Helvetica 14 with strong emphasis.''', styNormal)) + story.append(Paragraph('''This one uses fonts with size "14pt" and also uses the em and strong tags: Here comes Helvetica 14 with strong emphasis.''', styNormal, caseSensitive=0)) + story.append(Paragraph('''This uses a font size of 3cm: Here comes Courier 3cm and normal again.''', styNormal, caseSensitive=0)) + story.append(Paragraph('''This is just a very long silly text to see if the caseSensitive flag also works if the paragraph is very long. '''*20, styNormal, caseSensitive=0)) + story.append(Indenter("1cm")) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Indenter("1cm")) + story.append(XPreformatted(")Indented list.", styNormal)) + story.append(XPreformatted(")Indented list.", styNormal)) + story.append(Indenter("-1cm")) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Indenter("-1cm")) + story.append(Paragraph("Indented list using seqChain/Format", stySpaced)) + story.append(Indenter("1cm")) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Indenter("1cm")) + story.append(XPreformatted(")Indented list.", styNormal)) + story.append(XPreformatted(")Indented list.", styNormal)) + story.append(Indenter("-1cm")) + story.append(Paragraph(")Indented list. %s" % randomText(), styNormal)) + story.append(Indenter("1cm")) + story.append(XPreformatted(")Indented list.", styNormal)) + story.append(Indenter("1cm")) + story.append(XPreformatted(")Indented list. line1", styNormal)) + story.append(XPreformatted(")Indented list. line2", styNormal)) + story.append(Indenter("-1cm")) + story.append(XPreformatted(")Indented list.", styNormal)) + story.append(Indenter("-1cm")) + story.append(Indenter("-1cm")) + + template = SimpleDocTemplate(outputfile('test_paragraphs.pdf'), + showBoundary=1) + template.build(story, + onFirstPage=myFirstPage, onLaterPages=myLaterPages) + + +def makeSuite(): + return makeSuiteForClasses(ParagraphTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfbase_encodings.py b/bin/reportlab/test/test_pdfbase_encodings.py new file mode 100644 index 00000000000..2c92a984a8c --- /dev/null +++ b/bin/reportlab/test/test_pdfbase_encodings.py @@ -0,0 +1,255 @@ +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfbase import pdfutils + +from reportlab.platypus.paragraph import Paragraph +from reportlab.lib.styles import ParagraphStyle +from reportlab.graphics.shapes import Drawing, String, Ellipse +import re +import codecs +textPat = re.compile(r'\([^(]*\)') + +#test sentences +testCp1252 = 'copyright %s trademark %s registered %s ReportLab! Ol%s!' % (chr(169), chr(153),chr(174), chr(0xe9)) +testUni = unicode(testCp1252, 'cp1252') +testUTF8 = testUni.encode('utf-8') +# expected result is octal-escaped text in the PDF +expectedCp1252 = pdfutils._escape(testCp1252) + +def extractText(pdfOps): + """Utility to rip out the PDF text within a block of PDF operators. + + PDF will show a string draw as something like "(Hello World) Tj" + i.e. text is in curved brackets. Crude and dirty, probably fails + on escaped brackets. + """ + found = textPat.findall(pdfOps) + #chop off '(' and ')' + return map(lambda x:x[1:-1], found) + +def subsetToUnicode(ttf, subsetCodeStr): + """Return unicode string represented by given subsetCode string + as found when TrueType font rendered to PDF, ttf must be the font + object that was used.""" + # This relies on TTFont internals and uses the first document + # and subset it finds + subset = ttf.state.values()[0].subsets[0] + chrs = [] + for codeStr in subsetCodeStr.split('\\'): + if codeStr: + chrs.append(unichr(subset[int(codeStr[1:], 8)])) + return u''.join(chrs) + +class TextEncodingTestCase(unittest.TestCase): + """Tests of expected Unicode and encoding behaviour + """ + def setUp(self): + self.luxi = TTFont("Luxi", "luxiserif.ttf") + pdfmetrics.registerFont(self.luxi) + self.styNormal = ParagraphStyle(name='Helvetica', fontName='Helvetica-Oblique') + self.styTrueType = ParagraphStyle(name='TrueType', fontName='luxi') + + def testStringWidth(self): + msg = 'Hello World' + assert abs(pdfmetrics.stringWidth(msg, 'Courier', 10) - 66.0) < 0.01 + assert abs(pdfmetrics.stringWidth(msg, 'Helvetica', 10) - 51.67) < 0.01 + assert abs(pdfmetrics.stringWidth(msg, 'Times-Roman', 10) - 50.27) < 0.01 + assert abs(pdfmetrics.stringWidth(msg, 'Luxi', 10) - 50.22) < 0.01 + + uniMsg1 = u"Hello World" + assert abs(pdfmetrics.stringWidth(uniMsg1, 'Courier', 10) - 66.0) < 0.01 + assert abs(pdfmetrics.stringWidth(uniMsg1, 'Helvetica', 10) - 51.67) < 0.01 + assert abs(pdfmetrics.stringWidth(uniMsg1, 'Times-Roman', 10) - 50.27) < 0.01 + assert abs(pdfmetrics.stringWidth(uniMsg1, 'Luxi', 10) - 50.22) < 0.01 + + + # Courier are all 600 ems wide. So if one 'measures as utf8' one will + # get a wrong width as extra characters are seen + assert len(testCp1252) == 52 + assert abs(pdfmetrics.stringWidth(testCp1252, 'Courier', 10, 'cp1252') - 312.0) < 0.01 + # the test string has 5 more bytes and so "measures too long" if passed to + # a single-byte font which treats it as a single-byte string. + assert len(testUTF8)==57 + assert abs(pdfmetrics.stringWidth(testUTF8, 'Courier', 10) - 312.0) < 0.01 + + assert len(testUni)==52 + assert abs(pdfmetrics.stringWidth(testUni, 'Courier', 10) - 312.0) < 0.01 + + + # now try a TrueType font. Should be able to accept Unicode or UTF8 + assert abs(pdfmetrics.stringWidth(testUTF8, 'Luxi', 10) - 224.44) < 0.01 + assert abs(pdfmetrics.stringWidth(testUni, 'Luxi', 10) - 224.44) < 0.01 + + def testUtf8Canvas(self): + """Verify canvas declared as utf8 autoconverts. + + This assumes utf8 input. It converts to the encoding of the + underlying font, so both text lines APPEAR the same.""" + + + c = Canvas(outputfile('test_pdfbase_encodings_utf8.pdf')) + + c.drawString(100,700, testUTF8) + + # Set a font with UTF8 encoding + c.setFont('Luxi', 12) + + # This should pass the UTF8 through unchanged + c.drawString(100,600, testUTF8) + # and this should convert from Unicode to UTF8 + c.drawString(100,500, testUni) + + + # now add a paragraph in Latin-1 in the latin-1 style + p = Paragraph(testUTF8, style=self.styNormal, encoding="utf-8") + w, h = p.wrap(150, 100) + p.drawOn(c, 100, 400) #3 + c.rect(100,300,w,h) + + # now add a paragraph in UTF-8 in the UTF-8 style + p2 = Paragraph(testUTF8, style=self.styTrueType, encoding="utf-8") + w, h = p2.wrap(150, 100) + p2.drawOn(c, 300, 400) #4 + c.rect(100,300,w,h) + + # now add a paragraph in Unicode in the latin-1 style + p3 = Paragraph(testUni, style=self.styNormal) + w, h = p3.wrap(150, 100) + p3.drawOn(c, 100, 300) + c.rect(100,300,w,h) + + # now add a paragraph in Unicode in the UTF-8 style + p4 = Paragraph(testUni, style=self.styTrueType) + p4.wrap(150, 100) + p4.drawOn(c, 300, 300) + c.rect(300,300,w,h) + + # now a graphic + d1 = Drawing(400,50) + d1.add(Ellipse(200,25,200,12.5, fillColor=None)) + d1.add(String(200,25,testUTF8, textAnchor='middle', encoding='utf-8')) + d1.drawOn(c, 100, 150) + + # now a graphic in utf8 + d2 = Drawing(400,50) + d2.add(Ellipse(200,25,200,12.5, fillColor=None)) + d2.add(String(200,25,testUTF8, fontName='Luxi', textAnchor='middle', encoding='utf-8')) + d2.drawOn(c, 100, 100) + + # now a graphic in Unicode with T1 font + d3 = Drawing(400,50) + d3.add(Ellipse(200,25,200,12.5, fillColor=None)) + d3.add(String(200,25,testUni, textAnchor='middle')) + d3.drawOn(c, 100, 50) + + # now a graphic in Unicode with TT font + d4 = Drawing(400,50) + d4.add(Ellipse(200,25,200,12.5, fillColor=None)) + d4.add(String(200,25,testUni, fontName='Luxi', textAnchor='middle')) + d4.drawOn(c, 100, 0) + + extracted = extractText(c.getCurrentPageContent()) + self.assertEquals(extracted[0], expectedCp1252) + self.assertEquals(extracted[1], extracted[2]) + #self.assertEquals(subsetToUnicode(self.luxi, extracted[1]), testUni) + c.save() + +class FontEncodingTestCase(unittest.TestCase): + """Make documents with custom encodings of Type 1 built-in fonts. + + Nothing really to do with character encodings; this is about hacking the font itself""" + + def test0(self): + "Make custom encodings of standard fonts" + + # make a custom encoded font. + c = Canvas(outputfile('test_pdfbase_encodings.pdf')) + c.setPageCompression(0) + c.setFont('Helvetica', 12) + c.drawString(100, 700, 'The text below should be in a custom encoding in which all vowels become "z"') + + # invent a new language where vowels are replaced with letter 'z' + zenc = pdfmetrics.Encoding('EncodingWithoutVowels', 'WinAnsiEncoding') + for ch in 'aeiou': + zenc[ord(ch)] = 'z' + for ch in 'AEIOU': + zenc[ord(ch)] = 'Z' + pdfmetrics.registerEncoding(zenc) + + # now we can make a font based on this encoding + # AR hack/workaround: the name of the encoding must be a Python codec! + f = pdfmetrics.Font('FontWithoutVowels', 'Helvetica-Oblique', 'EncodingWithoutVowels') + pdfmetrics.registerFont(f) + + c.setFont('FontWithoutVowels', 12) + c.drawString(125, 675, "The magic word is squamish ossifrage") + + # now demonstrate adding a Euro to MacRoman, which lacks one + c.setFont('Helvetica', 12) + c.drawString(100, 650, "MacRoman encoding lacks a Euro. We'll make a Mac font with the Euro at #219:") + + # WinAnsi Helvetica + pdfmetrics.registerFont(pdfmetrics.Font('Helvetica-WinAnsi', 'Helvetica-Oblique', 'WinAnsiEncoding')) + c.setFont('Helvetica-WinAnsi', 12) + c.drawString(125, 625, 'WinAnsi with Euro: character 128 = "\200"') + + pdfmetrics.registerFont(pdfmetrics.Font('MacHelvNoEuro', 'Helvetica-Oblique', 'MacRomanEncoding')) + c.setFont('MacHelvNoEuro', 12) + c.drawString(125, 600, 'Standard MacRoman, no Euro: Character 219 = "\333"') # oct(219)=0333 + + # now make our hacked encoding + euroMac = pdfmetrics.Encoding('MacWithEuro', 'MacRomanEncoding') + euroMac[219] = 'Euro' + pdfmetrics.registerEncoding(euroMac) + + pdfmetrics.registerFont(pdfmetrics.Font('MacHelvWithEuro', 'Helvetica-Oblique', 'MacWithEuro')) + + c.setFont('MacHelvWithEuro', 12) + c.drawString(125, 575, 'Hacked MacRoman with Euro: Character 219 = "\333"') # oct(219)=0333 + + # now test width setting with and without _rl_accel - harder + # make an encoding where 'm' becomes 'i' + c.setFont('Helvetica', 12) + c.drawString(100, 500, "Recode 'm' to 'i' and check we can measure widths. Boxes should surround letters.") + sample = 'Mmmmm. ' * 6 + 'Mmmm' + + c.setFont('Helvetica-Oblique',12) + c.drawString(125, 475, sample) + w = c.stringWidth(sample, 'Helvetica-Oblique', 12) + c.rect(125, 475, w, 12) + + narrowEnc = pdfmetrics.Encoding('m-to-i') + narrowEnc[ord('m')] = 'i' + narrowEnc[ord('M')] = 'I' + pdfmetrics.registerEncoding(narrowEnc) + + pdfmetrics.registerFont(pdfmetrics.Font('narrow', 'Helvetica-Oblique', 'm-to-i')) + c.setFont('narrow', 12) + c.drawString(125, 450, sample) + w = c.stringWidth(sample, 'narrow', 12) + c.rect(125, 450, w, 12) + + c.setFont('Helvetica', 12) + c.drawString(100, 400, "Symbol & Dingbats fonts - check we still get valid PDF in StandardEncoding") + c.setFont('Symbol', 12) + c.drawString(100, 375, 'abcdefghijklmn') + c.setFont('ZapfDingbats', 12) + c.drawString(300, 375, 'abcdefghijklmn') + + c.save() + +def makeSuite(): + return makeSuiteForClasses( + TextEncodingTestCase, + + #FontEncodingTestCase - nobbled for now due to old stuff which needs removing. + ) + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfbase_fontembed.py b/bin/reportlab/test/test_pdfbase_fontembed.py new file mode 100644 index 00000000000..e8b37165e1f --- /dev/null +++ b/bin/reportlab/test/test_pdfbase_fontembed.py @@ -0,0 +1,90 @@ +import os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase import pdfmetrics +from reportlab.test.test_pdfbase_pdfmetrics import makeWidthTestForAllGlyphs + + +class EmbeddingTestCase(unittest.TestCase): + "Make documents with embedded fonts" + + def test0(self): + """Make documents with embedded fonts. + + Just vam Rossum has kindly donated a font which we may use + for testing purposes. You need to contact him at just@letterror.com + if you want to use it for real.""" + + #LettError fonts should always be there. The others are voluntary. + + ok = 1 + + c = Canvas(outputfile('test_pdfbase_fontembed.pdf')) + c.setPageCompression(0) + c.setFont('Helvetica', 12) + c.drawString(100, 700, 'This is Helvetica. The text below should be different fonts...') + + if os.path.isfile('GDB_____.AFM') and os.path.isfile('GDB_____.PFB'): + # a normal text font + garaFace = pdfmetrics.EmbeddedType1Face('GDB_____.AFM','GDB_____.PFB') + faceName = 'AGaramond-Bold' # pulled from AFM file + pdfmetrics.registerTypeFace(garaFace) + + garaFont = pdfmetrics.Font('MyGaramondBold', faceName, 'WinAnsiEncoding') + pdfmetrics.registerFont(garaFont) + + c.setFont('AGaramond-Bold', 12) + c.drawString(100, 650, 'This should be in AGaramond-Bold') + + if os.path.isfile('CR______.AFM') and os.path.isfile('CR______.PFB'): + + # one with a custom encoding + cartaFace = pdfmetrics.EmbeddedType1Face('CR______.AFM','CR______.PFB') + faceName = 'Carta' # pulled from AFM file + pdfmetrics.registerTypeFace(cartaFace) + + cartaFont = pdfmetrics.Font('Carta', 'Carta', 'CartaEncoding') + pdfmetrics.registerFont(cartaFont) + + text = 'This should be in Carta, a map symbol font:' + c.setFont('Helvetica', 12) + c.drawString(100, 600, text) + w = c.stringWidth(text, 'Helvetica', 12) + + c.setFont('Carta', 12) + c.drawString(100+w, 600, ' Hello World') + + # LettError sample - creates on demand, we hope + y = 550 +## justFace = pdfmetrics.EmbeddedType1Face('LeERC___.AFM','LeERC___.PFB') +## +## faceName = 'LettErrorRobot-Chrome' # pulled from AFM file +## pdfmetrics.registerTypeFace(justFace) +## +## justFont = pdfmetrics.Font('LettErrorRobot-Chrome', faceName, 'WinAnsiEncoding') +## pdfmetrics.registerFont(justFont) + + c.setFont('LettErrorRobot-Chrome', 12) + c.drawString(100, y, 'This should be in LettErrorRobot-Chrome') + + def testNamedFont(canv, fontName): + canv.showPage() + makeWidthTestForAllGlyphs(canv, fontName, outlining=0) + + testNamedFont(c, 'LettErrorRobot-Chrome') + + c.save() + + + +def makeSuite(): + return makeSuiteForClasses(EmbeddingTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfbase_pdfmetrics.py b/bin/reportlab/test/test_pdfbase_pdfmetrics.py new file mode 100644 index 00000000000..abfb9c06261 --- /dev/null +++ b/bin/reportlab/test/test_pdfbase_pdfmetrics.py @@ -0,0 +1,121 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfbase_pdfmetrics.py +#test_pdfbase_pdfmetrics_widths +""" +Various tests for PDF metrics. + +The main test prints out a PDF documents enabling checking of widths of every +glyph in every standard font. Long! +""" +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase import _fontdata +from reportlab.pdfgen.canvas import Canvas +from reportlab.lib import colors + +verbose = 0 +fontNamesToTest = _fontdata.standardFonts #[0:12] #leaves out Symbol and Dingbats for now + + +def decoratePage(c, header): + c.setFont('Helvetica-Oblique',10) + c.drawString(72, 800, header) + c.drawCentredString(297, 54, 'Page %d' % c.getPageNumber()) + + +def makeWidthTestForAllGlyphs(canv, fontName, outlining=1): + """New page, then runs down doing all the glyphs in one encoding""" + thisFont = pdfmetrics.getFont(fontName) + encName = thisFont.encName + canv.setFont('Helvetica-Bold', 12) + title = 'Glyph Metrics Test for font %s, ascent=%s, descent=%s, encoding=%s' % (fontName, str(thisFont.face.ascent), str(thisFont.face.descent), encName) + canv.drawString(80, 750, title) + canv.setFont('Helvetica-Oblique',10) + canv.drawCentredString(297, 54, 'Page %d' % canv.getPageNumber()) + + if outlining: + # put it in the outline + canv.bookmarkPage('GlyphWidths:' + fontName) + canv.addOutlineEntry(fontName,'GlyphWidths:' + fontName, level=1) + + y = 720 + widths = thisFont.widths + glyphNames = thisFont.encoding.vector + # need to get the right list of names for the font in question + for i in range(256): + if y < 72: + canv.showPage() + decoratePage(canv, title) + y = 750 + glyphName = glyphNames[i] + if glyphName is not None: + canv.setFont('Helvetica', 10) + text = unicode(chr(i),encName).encode('utf8')*30 + try: + w = canv.stringWidth(text, fontName, 10) + canv.drawString(80, y, '%03d %s w=%3d' % (i, glyphName, int((w/3.)*10))) + canv.setFont(fontName, 10) + canv.drawString(200, y, text) + + # now work out width and put a red marker next to the end. + canv.setFillColor(colors.red) + canv.rect(200 + w, y-1, 5, 10, stroke=0, fill=1) + canv.setFillColor(colors.black) + except KeyError: + canv.drawString(200, y, 'Could not find glyph named "%s"' % glyphName) + y = y - 12 + + +def makeTestDoc(fontNames): + filename = outputfile('test_pdfbase_pdfmetrics.pdf') + c = Canvas(filename) + c.bookmarkPage('Glyph Width Tests') + c.showOutline() + c.addOutlineEntry('Glyph Width Tests', 'Glyph Width Tests', level=0) + if verbose: + print # get it on a different line to the unittest log output. + for fontName in fontNames: + if verbose: + print 'width test for', fontName + + makeWidthTestForAllGlyphs(c, fontName) + c.showPage() + c.save() + if verbose: + if verbose: + print 'saved',filename + + +class PDFMetricsTestCase(unittest.TestCase): + "Test various encodings used in PDF files." + + def test0(self): + "Visual test for correct glyph widths" + makeTestDoc(fontNamesToTest) + + +def makeSuite(): + return makeSuiteForClasses(PDFMetricsTestCase) + + +#noruntests +if __name__=='__main__': + usage = """Usage: + (1) test_pdfbase_pdfmetrics.py - makes doc for all standard fonts + (2) test_pdfbase_pdfmetrics.py fontname - " " for just one font.""" + import sys + verbose = 1 + # accept font names as arguments; otherwise it does the lot + if len(sys.argv) > 1: + for arg in sys.argv[1:]: + if not arg in fontNamesToTest: + print 'unknown font %s' % arg + print usage + sys.exit(0) + + fontNamesToTest = sys.argv[1:] + + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfbase_pdfutils.py b/bin/reportlab/test/test_pdfbase_pdfutils.py new file mode 100644 index 00000000000..8c042e6977e --- /dev/null +++ b/bin/reportlab/test/test_pdfbase_pdfutils.py @@ -0,0 +1,52 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfbase_pdfutils.py +"""Tests for utility functions in reportlab.pdfbase.pdfutils. +""" + + +import os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation + +from reportlab.pdfbase.pdfutils import _AsciiHexEncode, _AsciiHexDecode +from reportlab.pdfbase.pdfutils import _AsciiBase85Encode, _AsciiBase85Decode + + +class PdfEncodingTestCase(unittest.TestCase): + "Test various encodings used in PDF files." + + def testAsciiHex(self): + "Test if the obvious test for whether ASCII-Hex encoding works." + + plainText = 'What is the average velocity of a sparrow?' + encoded = _AsciiHexEncode(plainText) + decoded = _AsciiHexDecode(encoded) + + msg = "Round-trip AsciiHex encoding failed." + assert decoded == plainText, msg + + + def testAsciiBase85(self): + "Test if the obvious test for whether ASCII-Base85 encoding works." + + msg = "Round-trip AsciiBase85 encoding failed." + plain = 'What is the average velocity of a sparrow?' + + #the remainder block can be absent or from 1 to 4 bytes + for i in xrange(55): + encoded = _AsciiBase85Encode(plain) + decoded = _AsciiBase85Decode(encoded) + assert decoded == plain, msg + plain = plain + chr(i) + + +def makeSuite(): + return makeSuiteForClasses(PdfEncodingTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfbase_postscript.py b/bin/reportlab/test/test_pdfbase_postscript.py new file mode 100644 index 00000000000..e5642502406 --- /dev/null +++ b/bin/reportlab/test/test_pdfbase_postscript.py @@ -0,0 +1,77 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfbase_postscript.py +__version__=''' $Id''' +__doc__="""Tests Postscript XObjects. + +Nothing visiblke in Acrobat, but the resulting files +contain graphics and tray commands if exported to +a Postscript device in Acrobat 4.0""" + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.pdfgen.canvas import Canvas + + +class PostScriptTestCase(unittest.TestCase): + "Simplest test that makes PDF" + + def testVisible(self): + "Makes a document with extra text - should export and distill" + c = Canvas(outputfile('test_pdfbase_postscript_visible.pdf')) + c.setPageCompression(0) + + c.setFont('Helvetica-Bold', 18) + c.drawString(100,700, 'Hello World. This is page 1 of a 2 page document.') + c.showPage() + + c.setFont('Helvetica-Bold', 16) + c.drawString(100,700, 'Page 2. This has some postscript drawing code.') + c.drawString(100,680, 'If you print it using a PS device and Acrobat 4/5,') + c.drawString(100,660, 'or export to Postscript, you should see the word') + c.drawString(100,640, '"Hello PostScript" below. In ordinary Acrobat Reader') + c.drawString(100,620, 'we expect to see nothing.') + c.addPostScriptCommand('/Helvetica findfont 48 scalefont setfont 100 400 moveto (Hello PostScript) show') + + + c.drawString(100,500, 'This document also inserts two postscript') + c.drawString(100,480, ' comments at beginning and endof the stream;') + c.drawString(100,460, 'search files for "%PS_BEFORE" and "%PS_AFTER".') + c.addPostScriptCommand('%PS_BEFORE', position=0) + c.addPostScriptCommand('%PS_AFTER', position=2) + + c.save() + + def testTray(self): + "Makes a document with tray command - only works on printers supporting it" + c = Canvas(outputfile('test_pdfbase_postscript_tray.pdf')) + c.setPageCompression(0) + + c.setFont('Helvetica-Bold', 18) + c.drawString(100,700, 'Hello World. This is page 1 of a 2 page document.') + c.drawString(100,680, 'This also has a tray command ("5 setpapertray").') + c.addPostScriptCommand('5 setpapertray') + c.showPage() + + c.setFont('Helvetica-Bold', 16) + c.drawString(100,700, 'Page 2. This should come from a different tray.') + c.drawString(100,680, 'Also, if you print it using a PS device and Acrobat 4/5,') + c.drawString(100,660, 'or export to Postscript, you should see the word') + c.drawString(100,640, '"Hello PostScript" below. In ordinary Acrobat Reader') + c.drawString(100,620, 'we expect to see nothing.') + c.addPostScriptCommand('/Helvetica findfont 48 scalefont setfont 100 400 moveto (Hello PostScript) show') + + + c.save() + +def makeSuite(): + return makeSuiteForClasses(PostScriptTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + print 'saved '+outputfile('test_pdfgen_postscript_visible.pdf') + print 'saved '+outputfile('test_pdfgen_postscript_tray.pdf') + printLocation() diff --git a/bin/reportlab/test/test_pdfbase_ttfonts.py b/bin/reportlab/test/test_pdfbase_ttfonts.py new file mode 100644 index 00000000000..9c228dc8cf1 --- /dev/null +++ b/bin/reportlab/test/test_pdfbase_ttfonts.py @@ -0,0 +1,391 @@ + +"""Test TrueType font subsetting & embedding code. + +This test uses a sample font (luxiserif.ttf) taken from XFree86 which is called Luxi +Serif Regular and is covered under the license in ../fonts/luxiserif_licence.txt. +""" + +import string +from cStringIO import StringIO + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen.canvas import Canvas +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.pdfdoc import PDFDocument, PDFError +from reportlab.pdfbase.ttfonts import TTFont, TTFontFace, TTFontFile, TTFOpenFile, \ + TTFontParser, TTFontMaker, TTFError, \ + parse_utf8, makeToUnicodeCMap, \ + FF_SYMBOLIC, FF_NONSYMBOLIC, \ + calcChecksum, add32, _L2U32 + + +def utf8(code): + "Convert a given UCS character index into UTF-8" + if code < 0 or code > 0x7FFFFFFF: + raise ValueError, 'Invalid UCS character 0x%x' % code + elif code < 0x00000080: + return chr(code) + elif code < 0x00000800: + return '%c%c' % \ + (0xC0 + (code >> 6), + 0x80 + (code & 0x3F)) + elif code < 0x00010000: + return '%c%c%c' % \ + (0xE0 + (code >> 12), + 0x80 + ((code >> 6) & 0x3F), + 0x80 + (code & 0x3F)) + elif code < 0x00200000: + return '%c%c%c%c' % \ + (0xF0 + (code >> 18), + 0x80 + ((code >> 12) & 0x3F), + 0x80 + ((code >> 6) & 0x3F), + 0x80 + (code & 0x3F)) + elif code < 0x04000000: + return '%c%c%c%c%c' % \ + (0xF8 + (code >> 24), + 0x80 + ((code >> 18) & 0x3F), + 0x80 + ((code >> 12) & 0x3F), + 0x80 + ((code >> 6) & 0x3F), + 0x80 + (code & 0x3F)) + else: + return '%c%c%c%c%c%c' % \ + (0xFC + (code >> 30), + 0x80 + ((code >> 24) & 0x3F), + 0x80 + ((code >> 18) & 0x3F), + 0x80 + ((code >> 12) & 0x3F), + 0x80 + ((code >> 6) & 0x3F), + 0x80 + (code & 0x3F)) + +def _simple_subset_generation(fn,npages,alter=0): + c = Canvas(outputfile(fn)) + c.setFont('Helvetica', 30) + c.drawString(100,700, 'Unicode TrueType Font Test %d pages' % npages) + # Draw a table of Unicode characters + for p in xrange(npages): + for fontName in ('TestFont','RinaFont'): + c.setFont(fontName, 10) + for i in xrange(32): + for j in xrange(32): + ch = utf8(i * 32 + j+p*alter) + c.drawString(80 + j * 13 + int(j / 16) * 4, 600 - i * 13 - int(i / 8) * 8, ch) + c.showPage() + c.save() + +class TTFontsTestCase(unittest.TestCase): + "Make documents with TrueType fonts" + + def testTTF(self): + "Test PDF generation with TrueType fonts" + pdfmetrics.registerFont(TTFont("TestFont", "luxiserif.ttf")) + pdfmetrics.registerFont(TTFont("RinaFont", "rina.ttf")) + _simple_subset_generation('test_pdfbase_ttfonts1.pdf',1) + _simple_subset_generation('test_pdfbase_ttfonts3.pdf',3) + _simple_subset_generation('test_pdfbase_ttfonts35.pdf',3,5) + + # Do it twice with the same font object + c = Canvas(outputfile('test_pdfbase_ttfontsadditional.pdf')) + # Draw a table of Unicode characters + c.setFont('TestFont', 10) + c.drawString(100, 700, 'Hello, ' + utf8(0xffee)) + c.save() + + +class TTFontFileTestCase(unittest.TestCase): + "Tests TTFontFile, TTFontParser and TTFontMaker classes" + + def testFontFileFailures(self): + "Tests TTFontFile constructor error checks" + self.assertRaises(TTFError, TTFontFile, "nonexistent file") + self.assertRaises(TTFError, TTFontFile, StringIO("")) + self.assertRaises(TTFError, TTFontFile, StringIO("invalid signature")) + self.assertRaises(TTFError, TTFontFile, StringIO("OTTO - OpenType not supported yet")) + self.assertRaises(TTFError, TTFontFile, StringIO("\0\1\0\0")) + + def testFontFileReads(self): + "Tests TTFontParset.read_xxx" + + class FakeTTFontFile(TTFontParser): + def __init__(self, data): + self._ttf_data = data + self._pos = 0 + + ttf = FakeTTFontFile("\x81\x02\x03\x04" "\x85\x06" "ABCD" "\x7F\xFF" "\x80\x00" "\xFF\xFF") + self.assertEquals(ttf.read_ulong(), _L2U32(0x81020304L)) # big-endian + self.assertEquals(ttf._pos, 4) + self.assertEquals(ttf.read_ushort(), 0x8506) + self.assertEquals(ttf._pos, 6) + self.assertEquals(ttf.read_tag(), 'ABCD') + self.assertEquals(ttf._pos, 10) + self.assertEquals(ttf.read_short(), 0x7FFF) + self.assertEquals(ttf.read_short(), -0x8000) + self.assertEquals(ttf.read_short(), -1) + + def testFontFile(self): + "Tests TTFontFile and TTF parsing code" + ttf = TTFontFile("luxiserif.ttf") + self.assertEquals(ttf.name, "LuxiSerif") + self.assertEquals(ttf.flags, FF_SYMBOLIC) + self.assertEquals(ttf.italicAngle, 0.0) + self.assertEquals(ttf.ascent, 783) # FIXME: or 992? + self.assertEquals(ttf.descent, -206) # FIXME: or -210? + self.assertEquals(ttf.capHeight, 0) + self.assertEquals(ttf.bbox, [-204, -211, 983, 992]) + self.assertEquals(ttf.stemV, 87) + self.assertEquals(ttf.defaultWidth, 250) + + def testAdd32(self): + "Test add32" + self.assertEquals(add32(10, -6), 4) + self.assertEquals(add32(6, -10), -4) + self.assertEquals(add32(_L2U32(0x80000000L), -1), 0x7FFFFFFF) + self.assertEquals(add32(0x7FFFFFFF, 1), _L2U32(0x80000000L)) + + def testChecksum(self): + "Test calcChecksum function" + self.assertEquals(calcChecksum(""), 0) + self.assertEquals(calcChecksum("\1"), 0x01000000) + self.assertEquals(calcChecksum("\x01\x02\x03\x04\x10\x20\x30\x40"), 0x11223344) + self.assertEquals(calcChecksum("\x81"), _L2U32(0x81000000L)) + self.assertEquals(calcChecksum("\x81\x02"), _L2U32(0x81020000L)) + self.assertEquals(calcChecksum("\x81\x02\x03"), _L2U32(0x81020300L)) + self.assertEquals(calcChecksum("\x81\x02\x03\x04"), _L2U32(0x81020304L)) + self.assertEquals(calcChecksum("\x81\x02\x03\x04\x05"), _L2U32(0x86020304L)) + self.assertEquals(calcChecksum("\x41\x02\x03\x04\xD0\x20\x30\x40"), 0x11223344) + self.assertEquals(calcChecksum("\xD1\x02\x03\x04\x40\x20\x30\x40"), 0x11223344) + self.assertEquals(calcChecksum("\x81\x02\x03\x04\x90\x20\x30\x40"), 0x11223344) + self.assertEquals(calcChecksum("\x7F\xFF\xFF\xFF\x00\x00\x00\x01"), _L2U32(0x80000000L)) + + def testFontFileChecksum(self): + "Tests TTFontFile and TTF parsing code" + file = TTFOpenFile("luxiserif.ttf")[1].read() + TTFontFile(StringIO(file), validate=1) # should not fail + file1 = file[:12345] + "\xFF" + file[12346:] # change one byte + self.assertRaises(TTFError, TTFontFile, StringIO(file1), validate=1) + file1 = file[:8] + "\xFF" + file[9:] # change one byte + self.assertRaises(TTFError, TTFontFile, StringIO(file1), validate=1) + + def testSubsetting(self): + "Tests TTFontFile and TTF parsing code" + ttf = TTFontFile("luxiserif.ttf") + subset = ttf.makeSubset([0x41, 0x42]) + subset = TTFontFile(StringIO(subset), 0) + for tag in ('cmap', 'head', 'hhea', 'hmtx', 'maxp', 'name', 'OS/2', + 'post', 'cvt ', 'fpgm', 'glyf', 'loca', 'prep'): + self.assert_(subset.get_table(tag)) + + subset.seek_table('loca') + for n in range(4): + pos = subset.read_ushort() # this is actually offset / 2 + self.failIf(pos % 2 != 0, "glyph %d at +%d should be long aligned" % (n, pos * 2)) + + self.assertEquals(subset.name, "LuxiSerif") + self.assertEquals(subset.flags, FF_SYMBOLIC) + self.assertEquals(subset.italicAngle, 0.0) + self.assertEquals(subset.ascent, 783) # FIXME: or 992? + self.assertEquals(subset.descent, -206) # FIXME: or -210? + self.assertEquals(subset.capHeight, 0) + self.assertEquals(subset.bbox, [-204, -211, 983, 992]) + self.assertEquals(subset.stemV, 87) + + def testFontMaker(self): + "Tests TTFontMaker class" + ttf = TTFontMaker() + ttf.add("ABCD", "xyzzy") + ttf.add("QUUX", "123") + ttf.add("head", "12345678xxxx") + stm = ttf.makeStream() + ttf = TTFontParser(StringIO(stm), 0) + self.assertEquals(ttf.get_table("ABCD"), "xyzzy") + self.assertEquals(ttf.get_table("QUUX"), "123") + + +class TTFontFaceTestCase(unittest.TestCase): + "Tests TTFontFace class" + + def testAddSubsetObjects(self): + "Tests TTFontFace.addSubsetObjects" + face = TTFontFace("luxiserif.ttf") + doc = PDFDocument() + fontDescriptor = face.addSubsetObjects(doc, "TestFont", [ 0x78, 0x2017 ]) + fontDescriptor = doc.idToObject[fontDescriptor.name].dict + self.assertEquals(fontDescriptor['Type'], '/FontDescriptor') + self.assertEquals(fontDescriptor['Ascent'], face.ascent) + self.assertEquals(fontDescriptor['CapHeight'], face.capHeight) + self.assertEquals(fontDescriptor['Descent'], face.descent) + self.assertEquals(fontDescriptor['Flags'], (face.flags & ~FF_NONSYMBOLIC) | FF_SYMBOLIC) + self.assertEquals(fontDescriptor['FontName'], "/TestFont") + self.assertEquals(fontDescriptor['FontBBox'].sequence, face.bbox) + self.assertEquals(fontDescriptor['ItalicAngle'], face.italicAngle) + self.assertEquals(fontDescriptor['StemV'], face.stemV) + fontFile = fontDescriptor['FontFile2'] + fontFile = doc.idToObject[fontFile.name] + self.assert_(fontFile.content != "") + + +class TTFontTestCase(unittest.TestCase): + "Tests TTFont class" + + def testParseUTF8(self): + "Tests parse_utf8" + self.assertEquals(parse_utf8(""), []) + for i in range(0, 0x80): + self.assertEquals(parse_utf8(chr(i)), [i]) + for i in range(0x80, 0xA0): + self.assertRaises(ValueError, parse_utf8, chr(i)) + self.assertEquals(parse_utf8("abc"), [0x61, 0x62, 0x63]) + self.assertEquals(parse_utf8("\xC2\xA9x"), [0xA9, 0x78]) + self.assertEquals(parse_utf8("\xE2\x89\xA0x"), [0x2260, 0x78]) + self.assertRaises(ValueError, parse_utf8, "\xE2\x89x") + # for i in range(0, 0xFFFF): - overkill + for i in range(0x80, 0x200) + range(0x300, 0x400) + [0xFFFE, 0xFFFF]: + self.assertEquals(parse_utf8(utf8(i)), [i]) + + def testStringWidth(self): + "Test TTFont.stringWidth" + font = TTFont("TestFont", "luxiserif.ttf") + self.assert_(font.stringWidth("test", 10) > 0) + width = font.stringWidth(utf8(0x2260) * 2, 1000) + expected = font.face.getCharWidth(0x2260) * 2 + self.assert_(abs(width - expected) < 0.01, "%g != %g" % (width, expected)) + + def testSplitString(self): + "Tests TTFont.splitString" + doc = PDFDocument() + font = TTFont("TestFont", "luxiserif.ttf") + text = string.join(map(utf8, xrange(0, 511)), "") + allchars = string.join(map(chr, xrange(0, 256)), "") + nospace = allchars[:32] + allchars[33:] + chunks = [(0, allchars), (1, nospace)] + self.assertEquals(font.splitString(text, doc), chunks) + # Do it twice + self.assertEquals(font.splitString(text, doc), chunks) + + text = string.join(map(utf8, range(510, -1, -1)), "") + allchars = string.join(map(chr, range(255, -1, -1)), "") + nospace = allchars[:223] + allchars[224:] + chunks = [(1, nospace), (0, allchars)] + self.assertEquals(font.splitString(text, doc), chunks) + + def testSplitStringSpaces(self): + # In order for justification (word spacing) to work, the space + # glyph must have a code 32, and no other character should have + # that code in any subset, or word spacing will be applied to it. + + doc = PDFDocument() + font = TTFont("TestFont", "luxiserif.ttf") + text = string.join(map(utf8, range(512, -1, -1)), "") + chunks = font.splitString(text, doc) + state = font.state[doc] + self.assertEquals(state.assignments[32], 32) + self.assertEquals(state.subsets[0][32], 32) + self.assertEquals(state.subsets[1][32], 32) + + def testSubsetInternalName(self): + "Tests TTFont.getSubsetInternalName" + doc = PDFDocument() + font = TTFont("TestFont", "luxiserif.ttf") + # Actually generate some subsets + text = string.join(map(utf8, range(0, 513)), "") + font.splitString(text, doc) + self.assertRaises(IndexError, font.getSubsetInternalName, -1, doc) + self.assertRaises(IndexError, font.getSubsetInternalName, 3, doc) + self.assertEquals(font.getSubsetInternalName(0, doc), "/F1+0") + self.assertEquals(font.getSubsetInternalName(1, doc), "/F1+1") + self.assertEquals(font.getSubsetInternalName(2, doc), "/F1+2") + self.assertEquals(doc.delayedFonts, [font]) + + def testAddObjectsEmpty(self): + "TTFont.addObjects should not fail when no characters were used" + font = TTFont("TestFont", "luxiserif.ttf") + doc = PDFDocument() + font.addObjects(doc) + + def no_longer_testAddObjectsResets(self): + "Test that TTFont.addObjects resets the font" + # Actually generate some subsets + doc = PDFDocument() + font = TTFont("TestFont", "luxiserif.ttf") + font.splitString('a', doc) # create some subset + doc = PDFDocument() + font.addObjects(doc) + self.assertEquals(font.frozen, 0) + self.assertEquals(font.nextCode, 0) + self.assertEquals(font.subsets, []) + self.assertEquals(font.assignments, {}) + font.splitString('ba', doc) # should work + + def testParallelConstruction(self): + "Test that TTFont can be used for different documents at the same time" + doc1 = PDFDocument() + doc2 = PDFDocument() + font = TTFont("TestFont", "luxiserif.ttf") + self.assertEquals(font.splitString(u'hello ', doc1), [(0, 'hello ')]) + self.assertEquals(font.splitString(u'hello ', doc2), [(0, 'hello ')]) + self.assertEquals(font.splitString(u'\u0410\u0411'.encode('UTF-8'), doc1), [(0, '\x80\x81')]) + self.assertEquals(font.splitString(u'\u0412'.encode('UTF-8'), doc2), [(0, '\x80')]) + font.addObjects(doc1) + self.assertEquals(font.splitString(u'\u0413'.encode('UTF-8'), doc2), [(0, '\x81')]) + font.addObjects(doc2) + + def testAddObjects(self): + "Test TTFont.addObjects" + # Actually generate some subsets + doc = PDFDocument() + font = TTFont("TestFont", "luxiserif.ttf") + font.splitString('a', doc) # create some subset + internalName = font.getSubsetInternalName(0, doc)[1:] + font.addObjects(doc) + pdfFont = doc.idToObject[internalName] + self.assertEquals(doc.idToObject['BasicFonts'].dict[internalName], pdfFont) + self.assertEquals(pdfFont.Name, internalName) + self.assertEquals(pdfFont.BaseFont, "AAAAAA+LuxiSerif") + self.assertEquals(pdfFont.FirstChar, 0) + self.assertEquals(pdfFont.LastChar, 127) + self.assertEquals(len(pdfFont.Widths.sequence), 128) + toUnicode = doc.idToObject[pdfFont.ToUnicode.name] + self.assert_(toUnicode.content != "") + fontDescriptor = doc.idToObject[pdfFont.FontDescriptor.name] + self.assertEquals(fontDescriptor.dict['Type'], '/FontDescriptor') + + def testMakeToUnicodeCMap(self): + "Test makeToUnicodeCMap" + self.assertEquals(makeToUnicodeCMap("TestFont", [ 0x1234, 0x4321, 0x4242 ]), +"""/CIDInit /ProcSet findresource begin +12 dict begin +begincmap +/CIDSystemInfo +<< /Registry (TestFont) +/Ordering (TestFont) +/Supplement 0 +>> def +/CMapName /TestFont def +/CMapType 2 def +1 begincodespacerange +<00> <02> +endcodespacerange +3 beginbfchar +<00> <1234> +<01> <4321> +<02> <4242> +endbfchar +endcmap +CMapName currentdict /CMap defineresource pop +end +end""") + + +def makeSuite(): + suite = makeSuiteForClasses( + TTFontsTestCase, + TTFontFileTestCase, + TTFontFaceTestCase, + TTFontTestCase) + return suite + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfgen_callback.py b/bin/reportlab/test/test_pdfgen_callback.py new file mode 100644 index 00000000000..a4e3e280c74 --- /dev/null +++ b/bin/reportlab/test/test_pdfgen_callback.py @@ -0,0 +1,39 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfgen_callback.py +__version__=''' $Id: test_pdfgen_callback.py 2619 2005-06-24 14:49:15Z rgbecker $ ''' +__doc__='checks callbacks work' + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen.canvas import Canvas +from reportlab.test.test_pdfgen_general import makeDocument + +_PAGE_COUNT = 0 + + +class CallBackTestCase(unittest.TestCase): + "checks it gets called" + + def callMe(self, pageNo): + self.pageCount = pageNo + + def test0(self): + "Make a PDFgen document with most graphics features" + + self.pageCount = 0 + makeDocument(outputfile('test_pdfgen_callback.pdf'), pageCallBack=self.callMe) + #no point saving it! + assert self.pageCount >= 7, 'page count not called!' + + +def makeSuite(): + return makeSuiteForClasses(CallBackTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfgen_general.py b/bin/reportlab/test/test_pdfgen_general.py new file mode 100644 index 00000000000..d20f0ee2f8f --- /dev/null +++ b/bin/reportlab/test/test_pdfgen_general.py @@ -0,0 +1,840 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfgen_general.py +__version__=''' $Id: test_pdfgen_general.py 2852 2006-05-08 15:04:15Z rgbecker $ ''' +__doc__='testscript for reportlab.pdfgen' +#tests and documents new low-level canvas + +import os, string + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen import canvas # gmcm 2000/10/13, pdfgen now a package +from reportlab.lib.units import inch, cm +from reportlab.lib import colors +from reportlab.lib.utils import haveImages + +################################################################# +# +# first some drawing utilities +# +# +################################################################ + +BASEFONT = ('Times-Roman', 10) +def framePageForm(c): + c.beginForm("frame") + c.saveState() + # forms can't do non-constant operations + #canvas.setFont('Times-BoldItalic',20) + #canvas.drawString(inch, 10.5 * inch, title) + + #c.setFont('Times-Roman',10) + #c.drawCentredString(4.135 * inch, 0.75 * inch, + # 'Page %d' % c.getPageNumber()) + + #draw a border + c.setFillColor(colors.ReportLabBlue) + c.rect(0.3*inch, inch, 0.5*inch, 10*inch, fill=1) + from reportlab.lib import corp + c.translate(0.8*inch, 9.6*inch) + c.rotate(90) + logo = corp.ReportLabLogo(width=1.3*inch, height=0.5*inch, powered_by=1) + c.setFillColorRGB(1,1,1) + c.setStrokeColorRGB(1,1,1) + logo.draw(c) + #c.setStrokeColorRGB(1,0,0) + #c.setLineWidth(5) + #c.line(0.8 * inch, inch, 0.8 * inch, 10.75 * inch) + #reset carefully afterwards + #canvas.setLineWidth(1) + #canvas.setStrokeColorRGB(0,0,0)\ + c.restoreState() + c.endForm() + +def framePage(canvas, title): + global closeit + titlelist.append(title) + #canvas._inPage0() # do we need this at all? would be good to eliminate it + canvas.saveState() + canvas.setFont('Times-BoldItalic',20) + + canvas.drawString(inch, 10.5 * inch, title) + canvas.bookmarkHorizontalAbsolute(title, 10.8*inch) + #newsection(title) + canvas.addOutlineEntry(title+" section", title, level=0, closed=closeit) + closeit = not closeit # close every other one + canvas.setFont('Times-Roman',10) + canvas.drawCentredString(4.135 * inch, 0.75 * inch, + 'Page %d' % canvas.getPageNumber()) + canvas.restoreState() + canvas.doForm("frame") + + +def makesubsection(canvas, title, horizontal): + canvas.bookmarkHorizontalAbsolute(title, horizontal) + #newsubsection(title) + canvas.addOutlineEntry(title+" subsection", title, level=1) + + +# outline helpers +#outlinenametree = [] +#def newsection(name): +# outlinenametree.append(name) + + +#def newsubsection(name): +# from types import TupleType +# thissection = outlinenametree[-1] +# if type(thissection) is not TupleType: +# subsectionlist = [] +# thissection = outlinenametree[-1] = (thissection, subsectionlist) +# else: +# (sectionname, subsectionlist) = thissection +# subsectionlist.append(name) + + +class DocBlock: + """A DocBlock has a chunk of commentary and a chunk of code. + It prints the code and commentary, then executes the code, + which is presumed to draw in a region reserved for it. + """ + def __init__(self): + self.comment1 = "A doc block" + self.code = "canvas.setTextOrigin(cm, cm)\ncanvas.textOut('Hello World')" + self.comment2 = "That was a doc block" + self.drawHeight = 0 + + def _getHeight(self): + "splits into lines" + self.comment1lines = string.split(self.comment1, '\n') + self.codelines = string.split(self.code, '\n') + self.comment2lines = string.split(self.comment2, '\n') + textheight = (len(self.comment1lines) + + len(self.code) + + len(self.comment2lines) + + 18) + return max(textheight, self.drawHeight) + + def draw(self, canvas, x, y): + #specifies top left corner + canvas.saveState() + height = self._getHeight() + canvas.rect(x, y-height, 6*inch, height) + #first draw the text + canvas.setTextOrigin(x + 3 * inch, y - 12) + canvas.setFont('Times-Roman',10) + canvas.textLines(self.comment1) + drawCode(canvas, self.code) + canvas.textLines(self.comment2) + + #now a box for the drawing, slightly within rect + canvas.rect(x + 9, y - height + 9, 198, height - 18) + #boundary: + self.namespace = {'canvas':canvas,'cm': cm,'inch':inch} + canvas.translate(x+9, y - height + 9) + codeObj = compile(self.code, '','exec') + exec codeObj in self.namespace + + canvas.restoreState() + + +def drawAxes(canvas, label): + """draws a couple of little rulers showing the coords - + uses points as units so you get an imperial ruler + one inch on each side""" + #y axis + canvas.line(0,0,0,72) + for y in range(9): + tenths = (y+1) * 7.2 + canvas.line(-6,tenths,0,tenths) + canvas.line(-6, 66, 0, 72) #arrow... + canvas.line(6, 66, 0, 72) #arrow... + + canvas.line(0,0,72,0) + for x in range(9): + tenths = (x+1) * 7.2 + canvas.line(tenths,-6,tenths, 0) + canvas.line(66, -6, 72, 0) #arrow... + canvas.line(66, +6, 72, 0) #arrow... + + canvas.drawString(18, 30, label) + + +def drawCrossHairs(canvas, x, y): + """just a marker for checking text metrics - blue for fun""" + + canvas.saveState() + canvas.setStrokeColorRGB(0,1,0) + canvas.line(x-6,y,x+6,y) + canvas.line(x,y-6,x,y+6) + canvas.restoreState() + + +def drawCode(canvas, code): + """Draws a block of text at current point, indented and in Courier""" + canvas.addLiteral('36 0 Td') + canvas.setFillColor(colors.blue) + canvas.setFont('Courier',10) + + t = canvas.beginText() + t.textLines(code) + c.drawText(t) + + canvas.setFillColor(colors.black) + canvas.addLiteral('-36 0 Td') + canvas.setFont('Times-Roman',10) + + +def makeDocument(filename, pageCallBack=None): + #the extra arg is a hack added later, so other + #tests can get hold of the canvas just before it is + #saved + global titlelist, closeit + titlelist = [] + closeit = 0 + + c = canvas.Canvas(filename) + c.setPageCompression(0) + c.setPageCallBack(pageCallBack) + framePageForm(c) # define the frame form + c.showOutline() + + framePage(c, 'PDFgen graphics API test script') + makesubsection(c, "PDFgen", 10*inch) + + #quickie encoding test: when canvas encoding not set, + #the following should do (tm), (r) and (c) + msg_uni = u'copyright\u00A9 trademark\u2122 registered\u00AE ReportLab in unicode!' + msg_utf8 = msg_uni.replace('unicode','utf8').encode('utf8') + c.drawString(100, 100, msg_uni) + c.drawString(100, 80, msg_utf8) + + + + + t = c.beginText(inch, 10*inch) + t.setFont('Times-Roman', 10) + drawCrossHairs(c, t.getX(),t.getY()) + t.textLines(""" +The ReportLab library permits you to create PDF documents directly from +your Python code. The "pdfgen" subpackage is the lowest level exposed +to the user and lets you directly position test and graphics on the +page, with access to almost the full range of PDF features. + The API is intended to closely mirror the PDF / Postscript imaging +model. There is an almost one to one correspondence between commands +and PDF operators. However, where PDF provides several ways to do a job, +we have generally only picked one. + The test script attempts to use all of the methods exposed by the Canvas +class, defined in reportlab/pdfgen/canvas.py + First, let's look at text output. There are some basic commands +to draw strings: +- canvas.setFont(fontname, fontsize [, leading]) +- canvas.drawString(x, y, text) +- canvas.drawRightString(x, y, text) +- canvas.drawCentredString(x, y, text) + +The coordinates are in points starting at the bottom left corner of the +page. When setting a font, the leading (i.e. inter-line spacing) +defaults to 1.2 * fontsize if the fontsize is not provided. + +For more sophisticated operations, you can create a Text Object, defined +in reportlab/pdfgen/testobject.py. Text objects produce tighter PDF, run +faster and have many methods for precise control of spacing and position. +Basic usage goes as follows: +- tx = canvas.beginText(x, y) +- tx.textOut('Hello') # this moves the cursor to the right +- tx.textLine('Hello again') # prints a line and moves down +- y = tx.getY() # getX, getY and getCursor track position +- canvas.drawText(tx) # all gets drawn at the end + +The green crosshairs below test whether the text cursor is working +properly. They should appear at the bottom left of each relevant +substring. +""") + + t.setFillColorRGB(1,0,0) + t.setTextOrigin(inch, 4*inch) + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('textOut moves across:') + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('textOut moves across:') + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('textOut moves across:') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('textLine moves down') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('textLine moves down') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('textLine moves down') + drawCrossHairs(c, t.getX(),t.getY()) + + t.setTextOrigin(4*inch,3.25*inch) + drawCrossHairs(c, t.getX(),t.getY()) + t.textLines('This is a multi-line\nstring with embedded newlines\ndrawn with textLines().\n') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLines(['This is a list of strings', + 'drawn with textLines().']) + c.drawText(t) + + t = c.beginText(2*inch,2*inch) + t.setFont('Times-Roman',10) + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('Small text.') + drawCrossHairs(c, t.getX(),t.getY()) + t.setFont('Courier',14) + t.textOut('Bigger fixed width text.') + drawCrossHairs(c, t.getX(),t.getY()) + t.setFont('Times-Roman',10) + t.textOut('Small text again.') + drawCrossHairs(c, t.getX(),t.getY()) + c.drawText(t) + + #try out the decimal tabs high on the right. + c.setStrokeColor(colors.silver) + c.line(7*inch, 6*inch, 7*inch, 4.5*inch) + + c.setFillColor(colors.black) + c.setFont('Times-Roman',10) + c.drawString(6*inch, 6.2*inch, "Testing decimal alignment") + c.drawString(6*inch, 6.05*inch, "- aim for silver line") + c.line(7*inch, 6*inch, 7*inch, 4.5*inch) + + c.drawAlignedString(7*inch, 5.8*inch, "1,234,567.89") + c.drawAlignedString(7*inch, 5.6*inch, "3,456.789") + c.drawAlignedString(7*inch, 5.4*inch, "123") + c.setFillColor(colors.red) + c.drawAlignedString(7*inch, 5.2*inch, "(7,192,302.30)") + + #mark the cursor where it stopped + c.showPage() + + + ############################################################## + # + # page 2 - line styles + # + ############################################################### + + #page 2 - lines and styles + framePage(c, 'Line Drawing Styles') + + + + # three line ends, lines drawn the hard way + #firt make some vertical end markers + c.setDash(4,4) + c.setLineWidth(0) + c.line(inch,9.2*inch,inch, 7.8*inch) + c.line(3*inch,9.2*inch,3*inch, 7.8*inch) + c.setDash() #clears it + + c.setLineWidth(5) + c.setLineCap(0) + p = c.beginPath() + p.moveTo(inch, 9*inch) + p.lineTo(3*inch, 9*inch) + c.drawPath(p) + c.drawString(4*inch, 9*inch, 'the default - butt caps project half a width') + makesubsection(c, "caps and joins", 8.5*inch) + + c.setLineCap(1) + p = c.beginPath() + p.moveTo(inch, 8.5*inch) + p.lineTo(3*inch, 8.5*inch) + c.drawPath(p) + c.drawString(4*inch, 8.5*inch, 'round caps') + + c.setLineCap(2) + p = c.beginPath() + p.moveTo(inch, 8*inch) + p.lineTo(3*inch, 8*inch) + c.drawPath(p) + c.drawString(4*inch, 8*inch, 'square caps') + + c.setLineCap(0) + + # three line joins + c.setLineJoin(0) + p = c.beginPath() + p.moveTo(inch, 7*inch) + p.lineTo(2*inch, 7*inch) + p.lineTo(inch, 6.7*inch) + c.drawPath(p) + c.drawString(4*inch, 6.8*inch, 'Default - mitered join') + + c.setLineJoin(1) + p = c.beginPath() + p.moveTo(inch, 6.5*inch) + p.lineTo(2*inch, 6.5*inch) + p.lineTo(inch, 6.2*inch) + c.drawPath(p) + c.drawString(4*inch, 6.3*inch, 'round join') + + c.setLineJoin(2) + p = c.beginPath() + p.moveTo(inch, 6*inch) + p.lineTo(2*inch, 6*inch) + p.lineTo(inch, 5.7*inch) + c.drawPath(p) + c.drawString(4*inch, 5.8*inch, 'bevel join') + + c.setDash(6,6) + p = c.beginPath() + p.moveTo(inch, 5*inch) + p.lineTo(3*inch, 5*inch) + c.drawPath(p) + c.drawString(4*inch, 5*inch, 'dash 6 points on, 6 off- setDash(6,6) setLineCap(0)') + makesubsection(c, "dash patterns", 5*inch) + + c.setLineCap(1) + p = c.beginPath() + p.moveTo(inch, 4.5*inch) + p.lineTo(3*inch, 4.5*inch) + c.drawPath(p) + c.drawString(4*inch, 4.5*inch, 'dash 6 points on, 6 off- setDash(6,6) setLineCap(1)') + + c.setLineCap(0) + c.setDash([1,2,3,4,5,6],0) + p = c.beginPath() + p.moveTo(inch, 4.0*inch) + p.lineTo(3*inch, 4.0*inch) + c.drawPath(p) + c.drawString(4*inch, 4*inch, 'dash growing - setDash([1,2,3,4,5,6],0) setLineCap(0)') + + c.setLineCap(1) + c.setLineJoin(1) + c.setDash(32,12) + p = c.beginPath() + p.moveTo(inch, 3.0*inch) + p.lineTo(2.5*inch, 3.0*inch) + p.lineTo(inch, 2*inch) + c.drawPath(p) + c.drawString(4*inch, 3*inch, 'dash pattern, join and cap style interacting - ') + c.drawString(4*inch, 3*inch - 12, 'round join & miter results in sausages') + c.textAnnotation('Annotation',Rect=(4*inch, 3*inch-72, inch,inch-12)) + + c.showPage() + + +############################################################## +# +# higher level shapes +# +############################################################### + framePage(c, 'Shape Drawing Routines') + + t = c.beginText(inch, 10*inch) + t.textLines(""" +Rather than making your own paths, you have access to a range of shape routines. +These are built in pdfgen out of lines and bezier curves, but use the most compact +set of operators possible. We can add any new ones that are of general use at no +cost to performance.""") + t.textLine() + + #line demo + makesubsection(c, "lines", 10*inch) + c.line(inch, 8*inch, 3*inch, 8*inch) + t.setTextOrigin(4*inch, 8*inch) + t.textLine('canvas.line(x1, y1, x2, y2)') + + #bezier demo - show control points + makesubsection(c, "bezier curves", 7.5*inch) + (x1, y1, x2, y2, x3, y3, x4, y4) = ( + inch, 6.5*inch, + 1.2*inch, 7.5 * inch, + 3*inch, 7.5 * inch, + 3.5*inch, 6.75 * inch + ) + c.bezier(x1, y1, x2, y2, x3, y3, x4, y4) + c.setDash(3,3) + c.line(x1,y1,x2,y2) + c.line(x3,y3,x4,y4) + c.setDash() + t.setTextOrigin(4*inch, 7 * inch) + t.textLine('canvas.bezier(x1, y1, x2, y2, x3, y3, x4, y4)') + + #rectangle + makesubsection(c, "rectangles", 7*inch) + c.rect(inch, 5.25 * inch, 2 * inch, 0.75 * inch) + t.setTextOrigin(4*inch, 5.5 * inch) + t.textLine('canvas.rect(x, y, width, height) - x,y is lower left') + + #wedge + makesubsection(c, "wedges", 5*inch) + c.wedge(inch, 5*inch, 3*inch, 4*inch, 0, 315) + t.setTextOrigin(4*inch, 4.5 * inch) + t.textLine('canvas.wedge(x1, y1, x2, y2, startDeg, extentDeg)') + t.textLine('Note that this is an elliptical arc, not just circular!') + + #wedge the other way + c.wedge(inch, 4*inch, 3*inch, 3*inch, 0, -45) + t.setTextOrigin(4*inch, 3.5 * inch) + t.textLine('Use a negative extent to go clockwise') + + #circle + makesubsection(c, "circles", 3.5*inch) + c.circle(1.5*inch, 2*inch, 0.5 * inch) + c.circle(3*inch, 2*inch, 0.5 * inch) + t.setTextOrigin(4*inch, 2 * inch) + t.textLine('canvas.circle(x, y, radius)') + c.drawText(t) + + c.showPage() + +############################################################## +# +# Page 4 - fonts +# +############################################################### + framePage(c, "Font Control") + + c.drawString(inch, 10*inch, 'Listing available fonts...') + + y = 9.5*inch + for fontname in c.getAvailableFonts(): + c.setFont(fontname,24) + c.drawString(inch, y, 'This should be %s' % fontname) + y = y - 28 + makesubsection(c, "fonts and colors", 4*inch) + + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 4*inch) + t.textLines("""Now we'll look at the color functions and how they interact + with the text. In theory, a word is just a shape; so setFillColorRGB() + determines most of what you see. If you specify other text rendering + modes, an outline color could be defined by setStrokeColorRGB() too""") + c.drawText(t) + + t = c.beginText(inch, 2.75 * inch) + t.setFont('Times-Bold',36) + t.setFillColor(colors.green) #green + t.textLine('Green fill, no stroke') + + #t.setStrokeColorRGB(1,0,0) #ou can do this in a text object, or the canvas. + t.setStrokeColor(colors.red) #ou can do this in a text object, or the canvas. + t.setTextRenderMode(2) # fill and stroke + t.textLine('Green fill, red stroke - yuk!') + + t.setTextRenderMode(0) # back to default - fill only + t.setFillColorRGB(0,0,0) #back to default + t.setStrokeColorRGB(0,0,0) #ditto + c.drawText(t) + c.showPage() + +######################################################################### +# +# Page 5 - coord transforms +# +######################################################################### + framePage(c, "Coordinate Transforms") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + t.textLines("""This shows coordinate transformations. We draw a set of axes, + moving down the page and transforming space before each one. + You can use saveState() and restoreState() to unroll transformations. + Note that functions which track the text cursor give the cursor position + in the current coordinate system; so if you set up a 6 inch high frame + 2 inches down the page to draw text in, and move the origin to its top + left, you should stop writing text after six inches and not eight.""") + c.drawText(t) + + drawAxes(c, "0. at origin") + c.addLiteral('%about to translate space') + c.translate(2*inch, 7 * inch) + drawAxes(c, '1. translate near top of page') + + c.saveState() + c.translate(1*inch, -2 * inch) + drawAxes(c, '2. down 2 inches, across 1') + c.restoreState() + + c.saveState() + c.translate(0, -3 * inch) + c.scale(2, -1) + drawAxes(c, '3. down 3 from top, scale (2, -1)') + c.restoreState() + + c.saveState() + c.translate(0, -5 * inch) + c.rotate(-30) + drawAxes(c, "4. down 5, rotate 30' anticlockwise") + c.restoreState() + + c.saveState() + c.translate(3 * inch, -5 * inch) + c.skew(0,30) + drawAxes(c, "5. down 5, 3 across, skew beta 30") + c.restoreState() + + c.showPage() + +######################################################################### +# +# Page 6 - clipping +# +######################################################################### + framePage(c, "Clipping") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + t.textLines("""This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text.""") + c.drawText(t) + + c.saveState() + #c.setFillColorRGB(0,0,1) + p = c.beginPath() + #make a chesboard effect, 1 cm squares + for i in range(14): + x0 = (3 + i) * cm + for j in range(7): + y0 = (16 + j) * cm + p.rect(x0, y0, 0.85*cm, 0.85*cm) + c.addLiteral('%Begin clip path') + c.clipPath(p) + c.addLiteral('%End clip path') + t = c.beginText(3 * cm, 22.5 * cm) + t.textLines("""This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text. + This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text. + This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text.""") + c.drawText(t) + + c.restoreState() + + t = c.beginText(inch, 5 * inch) + t.textLines("""You can also use text as an outline for clipping with the text render mode. + The API is not particularly clean on this and one has to follow the right sequence; + this can be optimized shortly.""") + c.drawText(t) + + #first the outline + c.saveState() + t = c.beginText(inch, 3.0 * inch) + t.setFont('Helvetica-BoldOblique',108) + t.setTextRenderMode(5) #stroke and add to path + t.textLine('Python!') + t.setTextRenderMode(0) + c.drawText(t) #this will make a clipping mask + + #now some small stuff which wil be drawn into the current clip mask + t = c.beginText(inch, 4 * inch) + t.setFont('Times-Roman',6) + t.textLines((('spam ' * 40) + '\n') * 15) + c.drawText(t) + + #now reset canvas to get rid of the clipping mask + c.restoreState() + + c.showPage() + + +######################################################################### +# +# Page 7 - images +# +######################################################################### + framePage(c, "Images") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + if not haveImages: + c.drawString(inch, 11*inch, + "Python or Java Imaging Library not found! Below you see rectangles instead of images.") + + t.textLines("""PDFgen uses the Python Imaging Library (or, under Jython, java.awt.image and javax.imageio) + to process a very wide variety of image formats. + This page shows image capabilities. If I've done things right, the bitmap should have + its bottom left corner aligned with the crosshairs. + There are two methods for drawing images. The recommended use is to call drawImage. + This produces the smallest PDFs and the fastest generation times as each image's binary data is + only embedded once in the file. Also you can use advanced features like transparency masks. + You can also use drawInlineImage, which puts images in the page stream directly. + This is slightly faster for Acrobat to render or for very small images, but wastes + space if you use images more than once.""") + + c.drawText(t) + + if haveImages: + gif = os.path.join(os.path.dirname(unittest.__file__),'pythonpowered.gif') + c.drawInlineImage(gif,2*inch, 7*inch) + else: + c.rect(2*inch, 7*inch, 110, 44) + + c.line(1.5*inch, 7*inch, 4*inch, 7*inch) + c.line(2*inch, 6.5*inch, 2*inch, 8*inch) + c.drawString(4.5 * inch, 7.25*inch, 'inline image drawn at natural size') + + if haveImages: + c.drawInlineImage(gif,2*inch, 5*inch, inch, inch) + else: + c.rect(2*inch, 5*inch, inch, inch) + + c.line(1.5*inch, 5*inch, 4*inch, 5*inch) + c.line(2*inch, 4.5*inch, 2*inch, 6*inch) + c.drawString(4.5 * inch, 5.25*inch, 'inline image distorted to fit box') + + c.drawString(1.5 * inch, 4*inch, 'Image XObjects can be defined once in the file and drawn many times.') + c.drawString(1.5 * inch, 3.75*inch, 'This results in faster generation and much smaller files.') + + for i in range(5): + if haveImages: + (w, h) = c.drawImage(gif, (1.5 + i)*inch, 3*inch) + else: + c.rect((1.5 + i)*inch, 3*inch, 110, 44) + + myMask = [254,255,222,223,0,1] + c.drawString(1.5 * inch, 2.5*inch, "The optional 'mask' parameter lets you define transparent colors. We used a color picker") + c.drawString(1.5 * inch, 2.3*inch, "to determine that the yellow in the image above is RGB=(225,223,0). We then define a mask") + c.drawString(1.5 * inch, 2.1*inch, "spanning these RGB values: %s. The background vanishes!!" % myMask) + c.drawString(2.5*inch, 1.2*inch, 'This would normally be obscured') + if haveImages: + c.drawImage(gif, 1*inch, 1.2*inch, w, h, mask=myMask) + c.drawImage(gif, 3*inch, 1.2*inch, w, h, mask='auto') + else: + c.rect(1*inch, 1.2*inch, w, h) + c.rect(3*inch, 1.2*inch, w, h) + + c.showPage() + + if haveImages: + import shutil + c.drawString(1*inch, 10.25*inch, 'This jpeg is actually a gif') + jpg = outputfile('_i_am_actually_a_gif.jpg') + shutil.copyfile(gif,jpg) + c.drawImage(jpg, 1*inch, 9.25*inch, w, h, mask='auto') + tjpg = os.path.join(os.path.dirname(os.path.dirname(gif)),'docs','images','lj8100.jpg') + if os.path.isfile(tjpg): + c.drawString(4*inch, 10.25*inch, 'This gif is actually a jpeg') + tgif = outputfile(os.path.basename('_i_am_actually_a_jpeg.gif')) + shutil.copyfile(tjpg,tgif) + c.drawImage(tgif, 4*inch, 9.25*inch, w, h, mask='auto') + c.showPage() + + +######################################################################### +# +# Page 8 - Forms and simple links +# +######################################################################### + framePage(c, "Forms and Links") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + t.textLines("""Forms are sequences of text or graphics operations + which are stored only once in a PDF file and used as many times + as desired. The blue logo bar to the left is an example of a form + in this document. See the function framePageForm in this demo script + for an example of how to use canvas.beginForm(name, ...) ... canvas.endForm(). + + Documents can also contain cross references where (for example) a rectangle + on a page may be bound to a position on another page. If the user clicks + on the rectangle the PDF viewer moves to the bound position on the other + page. There are many other types of annotations and links supported by PDF. + + For example, there is a bookmark to each page in this document and below + is a browsable index that jumps to those pages. In addition we show two + URL hyperlinks; for these, you specify a rectangle but must draw the contents + or any surrounding rectangle yourself. + """) + c.drawText(t) + + nentries = len(titlelist) + xmargin = 3*inch + xmax = 7*inch + ystart = 6.54*inch + ydelta = 0.4*inch + for i in range(nentries): + yposition = ystart - i*ydelta + title = titlelist[i] + c.drawString(xmargin, yposition, title) + c.linkAbsolute(title, title, (xmargin-ydelta/4, yposition-ydelta/4, xmax, yposition+ydelta/2)) + + # test URLs + r1 = (inch, 3*inch, 5*inch, 3.25*inch) # this is x1,y1,x2,y2 + c.linkURL('http://www.reportlab.com/', r1, thickness=1, color=colors.green) + c.drawString(inch+3, 3*inch+6, 'Hyperlink to www.reportlab.com, with green border') + + r1 = (inch, 2.5*inch, 5*inch, 2.75*inch) # this is x1,y1,x2,y2 + c.linkURL('mailto:reportlab-users@egroups.com', r1) #, border=0) + c.drawString(inch+3, 2.5*inch+6, 'mailto: hyperlink, without border') + + r1 = (inch, 2*inch, 5*inch, 2.25*inch) # this is x1,y1,x2,y2 + c.linkURL('http://www.reportlab.com/', r1, + thickness=2, + dashArray=[2,4], + color=colors.magenta) + c.drawString(inch+3, 2*inch+6, 'Hyperlink with custom border style') + + xpdf = outputfile('test_hello.pdf').replace('\\','/') + link = 'Hard link to %s, with red border' % xpdf + r1 = (inch, 1.5*inch, inch+2*3+c.stringWidth(link,c._fontname, c._fontsize), 1.75*inch) # this is x1,y1,x2,y2 + c.linkURL(xpdf, r1, thickness=1, color=colors.red, kind='GoToR') + c.drawString(inch+3, 1.5*inch+6, link ) + + ### now do stuff for the outline + #for x in outlinenametree: print x + #stop + #apply(c.setOutlineNames0, tuple(outlinenametree)) + return c + + +def run(filename): + c = makeDocument(filename) + c.save() + c = makeDocument(filename) + import os + f = os.path.splitext(filename) + f = open('%sm%s' % (f[0],f[1]),'wb') + f.write(c.getpdfdata()) + f.close() + + + +def pageShapes(c): + """Demonstrates the basic lines and shapes""" + + c.showPage() + framePage(c, "Basic line and shape routines""") + c.setTextOrigin(inch, 10 * inch) + c.setFont('Times-Roman', 12) + c.textLines("""pdfgen provides some basic routines for drawing straight and curved lines, + and also for solid shapes.""") + + y = 9 * inch + d = DocBlock() + d.comment1 = 'Lesson one' + d.code = "canvas.textOut('hello, world')" + print d.code + + d.comment2 = 'Lesson two' + + d.draw(c, inch, 9 * inch) + + +class PdfgenTestCase(unittest.TestCase): + "Make documents with lots of Pdfgen features" + + def test0(self): + "Make a PDFgen document with most graphics features" + run(outputfile('test_pdfgen_general.pdf')) + +def makeSuite(): + return makeSuiteForClasses(PdfgenTestCase) + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfgen_links.py b/bin/reportlab/test/test_pdfgen_links.py new file mode 100644 index 00000000000..7f3b87acd26 --- /dev/null +++ b/bin/reportlab/test/test_pdfgen_links.py @@ -0,0 +1,187 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#this test and associates functionality kinds donated by Ian Sparks. +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfgen_links.py +""" +Tests for internal links and destinations +""" + +# +# Fit tests +# +# Modification History +# ==================== +# +# 11-Mar-2003 Ian Sparks +# * Initial version. +# +# +from reportlab.pdfgen import canvas +from reportlab.lib.units import inch +from reportlab.lib.pagesizes import letter +from reportlab.lib import colors + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +def markPage(c,height=letter[1],width=letter[0]): + height = height / inch + width = width / inch + for y in range(int(height)): + for x in range(int(width)): + c.drawString(x*inch,y*inch,"x=%d y=%d" % (x,y) ) + c.line(x*inch,0,x*inch,height*inch) + c.line(0,y*inch,width*inch,y*inch) + +fn = outputfile("test_pdfgen_links.pdf") + + +class LinkTestCase(unittest.TestCase): + "Test classes." + def test1(self): + + c = canvas.Canvas(fn,pagesize=letter) + #Page 1 + c.setFont("Courier", 10) + markPage(c) + + c.bookmarkPage("P1") + c.addOutlineEntry("Page 1","P1") + + #Note : XYZ Left is ignored because at this zoom the whole page fits the screen + c.bookmarkPage("P1_XYZ",fit="XYZ",top=7*inch,left=3*inch,zoom=0.5) + c.addOutlineEntry("Page 1 XYZ #1 (top=7,left=3,zoom=0.5)","P1_XYZ",level=1) + + c.bookmarkPage("P1_XYZ2",fit="XYZ",top=7*inch,left=3*inch,zoom=5) + c.addOutlineEntry("Page 1 XYZ #2 (top=7,left=3,zoom=5)","P1_XYZ2",level=1) + + c.bookmarkPage("P1_FIT",fit="Fit") + c.addOutlineEntry("Page 1 Fit","P1_FIT",level=1) + + c.bookmarkPage("P1_FITH",fit="FitH",top=2*inch) + c.addOutlineEntry("Page 1 FitH (top = 2 inch)","P1_FITH",level=1) + + c.bookmarkPage("P1_FITV",fit="FitV",left=3*inch) + c.addOutlineEntry("Page 1 FitV (left = 3 inch)","P1_FITV",level=1) + + c.bookmarkPage("P1_FITR",fit="FitR",left=1*inch,bottom=2*inch,right=5*inch,top=6*inch) + c.addOutlineEntry("Page 1 FitR (left=1,bottom=2,right=5,top=6)","P1_FITR",level=1) + + c.bookmarkPage("P1_FORWARD") + c.addOutlineEntry("Forward References","P1_FORWARD",level=2) + c.addOutlineEntry("Page 3 XYZ (top=7,left=3,zoom=0)","P3_XYZ",level=3) + + + #Create link to FitR on page 3 + c.saveState() + c.setFont("Courier", 14) + c.setFillColor(colors.blue) + c.drawString(inch+20,inch+20,"Click to jump to the meaning of life") + c.linkAbsolute("","MOL",(inch+10,inch+10,6*inch,2*inch)) + c.restoreState() + + #Create linkAbsolute to page 2 + c.saveState() + c.setFont("Courier", 14) + c.setFillColor(colors.green) + c.drawString(4*inch,4*inch,"Jump to 2.5 inch position on page 2") + c.linkAbsolute("","HYPER_1",(3.75*inch,3.75*inch,8.25*inch,4.25*inch)) + c.restoreState() + + + c.showPage() + + #Page 2 + c.setFont("Helvetica", 10) + markPage(c) + + c.bookmarkPage("P2") + c.addOutlineEntry("Page 2","P2") + + #Note : This time left will be at 3*inch because the zoom makes the page to big to fit + c.bookmarkPage("P2_XYZ",fit="XYZ",top=7*inch,left=3*inch,zoom=2) + c.addOutlineEntry("Page 2 XYZ (top=7,left=3,zoom=2.0)","P2_XYZ",level=1) + + c.bookmarkPage("P2_FIT",fit="Fit") + c.addOutlineEntry("Page 2 Fit","P2_FIT",level=1) + + c.bookmarkPage("P2_FITH",fit="FitH",top=2*inch) + c.addOutlineEntry("Page 2 FitH (top = 2 inch)","P2_FITH",level=1) + + c.bookmarkPage("P2_FITV",fit="FitV",left=10*inch) + c.addOutlineEntry("Page 2 FitV (left = 10 inch)","P2_FITV",level=1) + + c.bookmarkPage("P2_FITR",fit="FitR",left=1*inch,bottom=2*inch,right=5*inch,top=6*inch) + c.addOutlineEntry("Page 2 FitR (left=1,bottom=2,right=5,top=6)","P2_FITR",level=1) + + c.bookmarkPage("P2_FORWARD") + c.addOutlineEntry("Forward References","P2_FORWARD",level=2) + c.addOutlineEntry("Page 3 XYZ (top=7,left=3,zoom=0)","P3_XYZ",level=3) + c.bookmarkPage("P2_BACKWARD") + c.addOutlineEntry("Backward References","P2_BACKWARD",level=2) + c.addOutlineEntry("Page 1 Fit","P1_FIT",level=3) + c.addOutlineEntry("Page 1 FitR (left=1,bottom=2,right=5,top=6)","P1_FITR",level=3) + + #Horizontal absolute test from page 1. Note that because of the page size used on page 3 all this will do + #is put the view centered on the bookmark. If you want to see it "up close and personal" change page3 to be + #the same page size as the other pages. + c.saveState() + c.setFont("Courier", 14) + c.setFillColor(colors.green) + c.drawString(2.5*inch,2.5*inch,"This line is hyperlinked from page 1") + # c.bookmarkHorizontalAbsolute("HYPER_1",3*inch) #slightly higher than the text otherwise text is of screen above. + c.bookmarkPage("HYPER_1",fit="XYZ",top=2.5*inch,bottom=2*inch) + c.restoreState() + + # + + c.showPage() + + #Page 3 + c.setFont("Times-Roman", 10) + #Turn the page on its size and make it 2* the normal "width" in order to have something to test FitV against. + c.setPageSize((2*letter[1],letter[0])) + markPage(c,height=letter[0],width=2*letter[1]) + + c.bookmarkPage("P3") + c.addOutlineEntry("Page 3 (Double-wide landscape page)","P3") + + #Note : XYZ with no zoom (set it to something first + c.bookmarkPage("P3_XYZ",fit="XYZ",top=7*inch,left=3*inch,zoom=0) + c.addOutlineEntry("Page 3 XYZ (top=7,left=3,zoom=0)","P3_XYZ",level=1) + + #FitV works here because the page is so wide it can"t all fit on the page + c.bookmarkPage("P3_FITV",fit="FitV",left=10*inch) + c.addOutlineEntry("Page 3 FitV (left = 10 inch)","P3_FITV",level=1) + + + c.bookmarkPage("P3_BACKWARD") + c.addOutlineEntry("Backward References","P3_BACKWARD",level=2) + c.addOutlineEntry("Page 1 XYZ #1 (top=7,left=3,zoom=0.5)","P1_XYZ",level=3) + c.addOutlineEntry("Page 1 XYZ #2 (top=7,left=3,zoom=5)","P1_XYZ2",level=3) + c.addOutlineEntry("Page 2 FitV (left = 10 inch)","P2_FITV",level=3) + + #Add link from page 1 + c.saveState() + c.setFont("Courier", 40) + c.setFillColor(colors.green) + c.drawString(5*inch,6*inch,"42") + c.bookmarkPage("MOL",fit="FitR",left=4*inch,top=7*inch,bottom=4*inch,right=6*inch) + + + + + c.showOutline() + c.save() + + + +def makeSuite(): + return makeSuiteForClasses(LinkTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + print "wrote", fn + printLocation() diff --git a/bin/reportlab/test/test_pdfgen_pagemodes.py b/bin/reportlab/test/test_pdfgen_pagemodes.py new file mode 100644 index 00000000000..fdf6ab465b0 --- /dev/null +++ b/bin/reportlab/test/test_pdfgen_pagemodes.py @@ -0,0 +1,70 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pdfgen_pagemodes.py +# full screen test + +"""Tests for PDF page modes support in reportlab.pdfgen. +""" + + +import os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen.canvas import Canvas + + +def fileDoesExist(path): + "Check if a file does exist." + return os.path.exists(path) + + +class PdfPageModeTestCase(unittest.TestCase): + "Testing different page modes for opening a file in Acrobat Reader." + + baseFileName = 'test_pagemodes_' + + def _doTest(self, filename, mode, desc): + "A generic method called by all test real methods." + + filename = outputfile(self.baseFileName + filename) + c = Canvas(filename) + + # Handle different modes. + if mode == 'FullScreen': + c.showFullScreen0() + elif mode == 'Outline': + c.bookmarkPage('page1') + c.addOutlineEntry('Token Outline Entry', 'page1') + c.showOutline() + elif mode == 'UseNone': + pass + + c.setFont('Helvetica', 20) + c.drawString(100, 700, desc) + c.save() + + assert fileDoesExist(filename) + + + def test0(self): + "This should open in full screen mode." + self._doTest('FullScreen.pdf', 'FullScreen', self.test0.__doc__) + + def test1(self): + "This should open with outline visible." + self._doTest('Outline.pdf', 'Outline', self.test1.__doc__) + + def test2(self): + "This should open in the user's default mode." + self._doTest('UseNone.pdf', 'UseNone', self.test2.__doc__) + +def makeSuite(): + return makeSuiteForClasses(PdfPageModeTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pdfgen_pycanvas.py b/bin/reportlab/test/test_pdfgen_pycanvas.py new file mode 100644 index 00000000000..d7a5c6eee5c --- /dev/null +++ b/bin/reportlab/test/test_pdfgen_pycanvas.py @@ -0,0 +1,790 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +__version__=''' $Id: test_pdfgen_pycanvas.py 2619 2005-06-24 14:49:15Z rgbecker $ ''' +__doc__='testscript for reportlab.pdfgen' +#tests and documents new low-level canvas and the pycanvas module to output Python source code. + +import string, os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen import pycanvas # gmcm 2000/10/13, pdfgen now a package +from reportlab.lib.units import inch, cm +from reportlab.lib import colors +from reportlab.lib.utils import haveImages, _RL_DIR, rl_isfile + +################################################################# +# +# first some drawing utilities +# +# +################################################################ + +BASEFONT = ('Times-Roman', 10) +def framePageForm(c): + c.beginForm("frame") + c.saveState() + # forms can't do non-constant operations + #canvas.setFont('Times-BoldItalic',20) + #canvas.drawString(inch, 10.5 * inch, title) + + #c.setFont('Times-Roman',10) + #c.drawCentredString(4.135 * inch, 0.75 * inch, + # 'Page %d' % c.getPageNumber()) + + #draw a border + c.setFillColor(colors.ReportLabBlue) + c.rect(0.3*inch, inch, 0.5*inch, 10*inch, fill=1) + from reportlab.lib import corp + c.translate(0.8*inch, 9.6*inch) + c.rotate(90) + logo = corp.ReportLabLogo(width=1.3*inch, height=0.5*inch, powered_by=1) + c.setFillColorRGB(1,1,1) + c.setStrokeColorRGB(1,1,1) + logo.draw(c) + #c.setStrokeColorRGB(1,0,0) + #c.setLineWidth(5) + #c.line(0.8 * inch, inch, 0.8 * inch, 10.75 * inch) + #reset carefully afterwards + #canvas.setLineWidth(1) + #canvas.setStrokeColorRGB(0,0,0)\ + c.restoreState() + c.endForm() + +titlelist = [] +closeit = 0 + + +def framePage(canvas, title): + global closeit + titlelist.append(title) + #canvas._inPage0() # do we need this at all? would be good to eliminate it + canvas.saveState() + canvas.setFont('Times-BoldItalic',20) + + canvas.drawString(inch, 10.5 * inch, title) + canvas.bookmarkHorizontalAbsolute(title, 10.8*inch) + #newsection(title) + canvas.addOutlineEntry(title+" section", title, level=0, closed=closeit) + closeit = not closeit # close every other one + canvas.setFont('Times-Roman',10) + canvas.drawCentredString(4.135 * inch, 0.75 * inch, + 'Page %d' % canvas.getPageNumber()) + canvas.restoreState() + canvas.doForm("frame") + + +def makesubsection(canvas, title, horizontal): + canvas.bookmarkHorizontalAbsolute(title, horizontal) + #newsubsection(title) + canvas.addOutlineEntry(title+" subsection", title, level=1) + + +# outline helpers +#outlinenametree = [] +#def newsection(name): +# outlinenametree.append(name) + + +#def newsubsection(name): +# from types import TupleType +# thissection = outlinenametree[-1] +# if type(thissection) is not TupleType: +# subsectionlist = [] +# thissection = outlinenametree[-1] = (thissection, subsectionlist) +# else: +# (sectionname, subsectionlist) = thissection +# subsectionlist.append(name) + + +class DocBlock: + """A DocBlock has a chunk of commentary and a chunk of code. + It prints the code and commentary, then executes the code, + which is presumed to draw in a region reserved for it. + """ + def __init__(self): + self.comment1 = "A doc block" + self.code = "canvas.setTextOrigin(cm, cm)\ncanvas.textOut('Hello World')" + self.comment2 = "That was a doc block" + self.drawHeight = 0 + + def _getHeight(self): + "splits into lines" + self.comment1lines = string.split(self.comment1, '\n') + self.codelines = string.split(self.code, '\n') + self.comment2lines = string.split(self.comment2, '\n') + textheight = (len(self.comment1lines) + + len(self.code) + + len(self.comment2lines) + + 18) + return max(textheight, self.drawHeight) + + def draw(self, canvas, x, y): + #specifies top left corner + canvas.saveState() + height = self._getHeight() + canvas.rect(x, y-height, 6*inch, height) + #first draw the text + canvas.setTextOrigin(x + 3 * inch, y - 12) + canvas.setFont('Times-Roman',10) + canvas.textLines(self.comment1) + drawCode(canvas, self.code) + canvas.textLines(self.comment2) + + #now a box for the drawing, slightly within rect + canvas.rect(x + 9, y - height + 9, 198, height - 18) + #boundary: + self.namespace = {'canvas':canvas,'cm': cm,'inch':inch} + canvas.translate(x+9, y - height + 9) + codeObj = compile(self.code, '','exec') + exec codeObj in self.namespace + + canvas.restoreState() + + +def drawAxes(canvas, label): + """draws a couple of little rulers showing the coords - + uses points as units so you get an imperial ruler + one inch on each side""" + #y axis + canvas.line(0,0,0,72) + for y in range(9): + tenths = (y+1) * 7.2 + canvas.line(-6,tenths,0,tenths) + canvas.line(-6, 66, 0, 72) #arrow... + canvas.line(6, 66, 0, 72) #arrow... + + canvas.line(0,0,72,0) + for x in range(9): + tenths = (x+1) * 7.2 + canvas.line(tenths,-6,tenths, 0) + canvas.line(66, -6, 72, 0) #arrow... + canvas.line(66, +6, 72, 0) #arrow... + + canvas.drawString(18, 30, label) + + +def drawCrossHairs(canvas, x, y): + """just a marker for checking text metrics - blue for fun""" + + canvas.saveState() + canvas.setStrokeColorRGB(0,1,0) + canvas.line(x-6,y,x+6,y) + canvas.line(x,y-6,x,y+6) + canvas.restoreState() + + +def drawCode(canvas, code): + """Draws a block of text at current point, indented and in Courier""" + canvas.addLiteral('36 0 Td') + canvas.setFillColor(colors.blue) + canvas.setFont('Courier',10) + + t = canvas.beginText() + t.textLines(code) + c.drawText(t) + + canvas.setFillColor(colors.black) + canvas.addLiteral('-36 0 Td') + canvas.setFont('Times-Roman',10) + + +def makeDocument(filename, pageCallBack=None): + #the extra arg is a hack added later, so other + #tests can get hold of the canvas just before it is + #saved + + c = pycanvas.Canvas(filename) + c.setPageCompression(0) + c.setPageCallBack(pageCallBack) + framePageForm(c) # define the frame form + c.showOutline() + + framePage(c, 'PDFgen graphics API test script') + makesubsection(c, "PDFgen and PIDDLE", 10*inch) + + t = c.beginText(inch, 10*inch) + t.setFont('Times-Roman', 10) + drawCrossHairs(c, t.getX(),t.getY()) + t.textLines(""" +The ReportLab library permits you to create PDF documents directly from +your Python code. The "pdfgen" subpackage is the lowest level exposed +to the user and lets you directly position test and graphics on the +page, with access to almost the full range of PDF features. + The API is intended to closely mirror the PDF / Postscript imaging +model. There is an almost one to one correspondence between commands +and PDF operators. However, where PDF provides several ways to do a job, +we have generally only picked one. + The test script attempts to use all of the methods exposed by the Canvas +class, defined in reportlab/pdfgen/canvas.py + First, let's look at text output. There are some basic commands +to draw strings: +- canvas.setFont(fontname, fontsize [, leading]) +- canvas.drawString(x, y, text) +- canvas.drawRightString(x, y, text) +- canvas.drawCentredString(x, y, text) + +The coordinates are in points starting at the bottom left corner of the +page. When setting a font, the leading (i.e. inter-line spacing) +defaults to 1.2 * fontsize if the fontsize is not provided. + +For more sophisticated operations, you can create a Text Object, defined +in reportlab/pdfgen/testobject.py. Text objects produce tighter PDF, run +faster and have many methods for precise control of spacing and position. +Basic usage goes as follows: +- tx = canvas.beginText(x, y) +- tx.textOut('Hello') # this moves the cursor to the right +- tx.textLine('Hello again') # prints a line and moves down +- y = tx.getY() # getX, getY and getCursor track position +- canvas.drawText(tx) # all gets drawn at the end + +The green crosshairs below test whether the text cursor is working +properly. They should appear at the bottom left of each relevant +substring. +""") + + t.setFillColorRGB(1,0,0) + t.setTextOrigin(inch, 4*inch) + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('textOut moves across:') + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('textOut moves across:') + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('textOut moves across:') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('textLine moves down') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('textLine moves down') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLine('textLine moves down') + drawCrossHairs(c, t.getX(),t.getY()) + + t.setTextOrigin(4*inch,3.25*inch) + drawCrossHairs(c, t.getX(),t.getY()) + t.textLines('This is a multi-line\nstring with embedded newlines\ndrawn with textLines().\n') + drawCrossHairs(c, t.getX(),t.getY()) + t.textLines(['This is a list of strings', + 'drawn with textLines().']) + c.drawText(t) + + t = c.beginText(2*inch,2*inch) + t.setFont('Times-Roman',10) + drawCrossHairs(c, t.getX(),t.getY()) + t.textOut('Small text.') + drawCrossHairs(c, t.getX(),t.getY()) + t.setFont('Courier',14) + t.textOut('Bigger fixed width text.') + drawCrossHairs(c, t.getX(),t.getY()) + t.setFont('Times-Roman',10) + t.textOut('Small text again.') + drawCrossHairs(c, t.getX(),t.getY()) + c.drawText(t) + + #mark the cursor where it stopped + c.showPage() + + + ############################################################## + # + # page 2 - line styles + # + ############################################################### + + #page 2 - lines and styles + framePage(c, 'Line Drawing Styles') + + + + # three line ends, lines drawn the hard way + #firt make some vertical end markers + c.setDash(4,4) + c.setLineWidth(0) + c.line(inch,9.2*inch,inch, 7.8*inch) + c.line(3*inch,9.2*inch,3*inch, 7.8*inch) + c.setDash() #clears it + + c.setLineWidth(5) + c.setLineCap(0) + p = c.beginPath() + p.moveTo(inch, 9*inch) + p.lineTo(3*inch, 9*inch) + c.drawPath(p) + c.drawString(4*inch, 9*inch, 'the default - butt caps project half a width') + makesubsection(c, "caps and joins", 8.5*inch) + + c.setLineCap(1) + p = c.beginPath() + p.moveTo(inch, 8.5*inch) + p.lineTo(3*inch, 8.5*inch) + c.drawPath(p) + c.drawString(4*inch, 8.5*inch, 'round caps') + + c.setLineCap(2) + p = c.beginPath() + p.moveTo(inch, 8*inch) + p.lineTo(3*inch, 8*inch) + c.drawPath(p) + c.drawString(4*inch, 8*inch, 'square caps') + + c.setLineCap(0) + + # three line joins + c.setLineJoin(0) + p = c.beginPath() + p.moveTo(inch, 7*inch) + p.lineTo(2*inch, 7*inch) + p.lineTo(inch, 6.7*inch) + c.drawPath(p) + c.drawString(4*inch, 6.8*inch, 'Default - mitered join') + + c.setLineJoin(1) + p = c.beginPath() + p.moveTo(inch, 6.5*inch) + p.lineTo(2*inch, 6.5*inch) + p.lineTo(inch, 6.2*inch) + c.drawPath(p) + c.drawString(4*inch, 6.3*inch, 'round join') + + c.setLineJoin(2) + p = c.beginPath() + p.moveTo(inch, 6*inch) + p.lineTo(2*inch, 6*inch) + p.lineTo(inch, 5.7*inch) + c.drawPath(p) + c.drawString(4*inch, 5.8*inch, 'bevel join') + + c.setDash(6,6) + p = c.beginPath() + p.moveTo(inch, 5*inch) + p.lineTo(3*inch, 5*inch) + c.drawPath(p) + c.drawString(4*inch, 5*inch, 'dash 6 points on, 6 off- setDash(6,6) setLineCap(0)') + makesubsection(c, "dash patterns", 5*inch) + + c.setLineCap(1) + p = c.beginPath() + p.moveTo(inch, 4.5*inch) + p.lineTo(3*inch, 4.5*inch) + c.drawPath(p) + c.drawString(4*inch, 4.5*inch, 'dash 6 points on, 6 off- setDash(6,6) setLineCap(1)') + + c.setLineCap(0) + c.setDash([1,2,3,4,5,6],0) + p = c.beginPath() + p.moveTo(inch, 4.0*inch) + p.lineTo(3*inch, 4.0*inch) + c.drawPath(p) + c.drawString(4*inch, 4*inch, 'dash growing - setDash([1,2,3,4,5,6],0) setLineCap(0)') + + c.setLineCap(1) + c.setLineJoin(1) + c.setDash(32,12) + p = c.beginPath() + p.moveTo(inch, 3.0*inch) + p.lineTo(2.5*inch, 3.0*inch) + p.lineTo(inch, 2*inch) + c.drawPath(p) + c.drawString(4*inch, 3*inch, 'dash pattern, join and cap style interacting - ') + c.drawString(4*inch, 3*inch - 12, 'round join & miter results in sausages') + + c.showPage() + + +############################################################## +# +# higher level shapes +# +############################################################### + framePage(c, 'Shape Drawing Routines') + + t = c.beginText(inch, 10*inch) + t.textLines(""" +Rather than making your own paths, you have access to a range of shape routines. +These are built in pdfgen out of lines and bezier curves, but use the most compact +set of operators possible. We can add any new ones that are of general use at no +cost to performance.""") + t.textLine() + + #line demo + makesubsection(c, "lines", 10*inch) + c.line(inch, 8*inch, 3*inch, 8*inch) + t.setTextOrigin(4*inch, 8*inch) + t.textLine('canvas.line(x1, y1, x2, y2)') + + #bezier demo - show control points + makesubsection(c, "bezier curves", 7.5*inch) + (x1, y1, x2, y2, x3, y3, x4, y4) = ( + inch, 6.5*inch, + 1.2*inch, 7.5 * inch, + 3*inch, 7.5 * inch, + 3.5*inch, 6.75 * inch + ) + c.bezier(x1, y1, x2, y2, x3, y3, x4, y4) + c.setDash(3,3) + c.line(x1,y1,x2,y2) + c.line(x3,y3,x4,y4) + c.setDash() + t.setTextOrigin(4*inch, 7 * inch) + t.textLine('canvas.bezier(x1, y1, x2, y2, x3, y3, x4, y4)') + + #rectangle + makesubsection(c, "rectangles", 7*inch) + c.rect(inch, 5.25 * inch, 2 * inch, 0.75 * inch) + t.setTextOrigin(4*inch, 5.5 * inch) + t.textLine('canvas.rect(x, y, width, height) - x,y is lower left') + + #wedge + makesubsection(c, "wedges", 5*inch) + c.wedge(inch, 5*inch, 3*inch, 4*inch, 0, 315) + t.setTextOrigin(4*inch, 4.5 * inch) + t.textLine('canvas.wedge(x1, y1, x2, y2, startDeg, extentDeg)') + t.textLine('Note that this is an elliptical arc, not just circular!') + + #wedge the other way + c.wedge(inch, 4*inch, 3*inch, 3*inch, 0, -45) + t.setTextOrigin(4*inch, 3.5 * inch) + t.textLine('Use a negative extent to go clockwise') + + #circle + makesubsection(c, "circles", 3.5*inch) + c.circle(1.5*inch, 2*inch, 0.5 * inch) + c.circle(3*inch, 2*inch, 0.5 * inch) + t.setTextOrigin(4*inch, 2 * inch) + t.textLine('canvas.circle(x, y, radius)') + c.drawText(t) + + c.showPage() + +############################################################## +# +# Page 4 - fonts +# +############################################################### + framePage(c, "Font Control") + + c.drawString(inch, 10*inch, 'Listing available fonts...') + + y = 9.5*inch + for fontname in c.getAvailableFonts(): + c.setFont(fontname,24) + c.drawString(inch, y, 'This should be %s' % fontname) + y = y - 28 + makesubsection(c, "fonts and colors", 4*inch) + + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 4*inch) + t.textLines("""Now we'll look at the color functions and how they interact + with the text. In theory, a word is just a shape; so setFillColorRGB() + determines most of what you see. If you specify other text rendering + modes, an outline color could be defined by setStrokeColorRGB() too""") + c.drawText(t) + + t = c.beginText(inch, 2.75 * inch) + t.setFont('Times-Bold',36) + t.setFillColor(colors.green) #green + t.textLine('Green fill, no stroke') + + #t.setStrokeColorRGB(1,0,0) #ou can do this in a text object, or the canvas. + t.setStrokeColor(colors.red) #ou can do this in a text object, or the canvas. + t.setTextRenderMode(2) # fill and stroke + t.textLine('Green fill, red stroke - yuk!') + + t.setTextRenderMode(0) # back to default - fill only + t.setFillColorRGB(0,0,0) #back to default + t.setStrokeColorRGB(0,0,0) #ditto + c.drawText(t) + c.showPage() + +######################################################################### +# +# Page 5 - coord transforms +# +######################################################################### + framePage(c, "Coordinate Transforms") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + t.textLines("""This shows coordinate transformations. We draw a set of axes, + moving down the page and transforming space before each one. + You can use saveState() and restoreState() to unroll transformations. + Note that functions which track the text cursor give the cursor position + in the current coordinate system; so if you set up a 6 inch high frame + 2 inches down the page to draw text in, and move the origin to its top + left, you should stop writing text after six inches and not eight.""") + c.drawText(t) + + drawAxes(c, "0. at origin") + c.addLiteral('%about to translate space') + c.translate(2*inch, 7 * inch) + drawAxes(c, '1. translate near top of page') + + c.saveState() + c.translate(1*inch, -2 * inch) + drawAxes(c, '2. down 2 inches, across 1') + c.restoreState() + + c.saveState() + c.translate(0, -3 * inch) + c.scale(2, -1) + drawAxes(c, '3. down 3 from top, scale (2, -1)') + c.restoreState() + + c.saveState() + c.translate(0, -5 * inch) + c.rotate(-30) + drawAxes(c, "4. down 5, rotate 30' anticlockwise") + c.restoreState() + + c.saveState() + c.translate(3 * inch, -5 * inch) + c.skew(0,30) + drawAxes(c, "5. down 5, 3 across, skew beta 30") + c.restoreState() + + c.showPage() + +######################################################################### +# +# Page 6 - clipping +# +######################################################################### + framePage(c, "Clipping") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + t.textLines("""This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text.""") + c.drawText(t) + + c.saveState() + #c.setFillColorRGB(0,0,1) + p = c.beginPath() + #make a chesboard effect, 1 cm squares + for i in range(14): + x0 = (3 + i) * cm + for j in range(7): + y0 = (16 + j) * cm + p.rect(x0, y0, 0.85*cm, 0.85*cm) + c.addLiteral('%Begin clip path') + c.clipPath(p) + c.addLiteral('%End clip path') + t = c.beginText(3 * cm, 22.5 * cm) + t.textLines("""This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text. + This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text. + This shows clipping at work. We draw a chequerboard of rectangles + into a path object, and clip it. This then forms a mask which limits the region of + the page on which one can draw. This paragraph was drawn after setting the clipping + path, and so you should only see part of the text.""") + c.drawText(t) + + c.restoreState() + + t = c.beginText(inch, 5 * inch) + t.textLines("""You can also use text as an outline for clipping with the text render mode. + The API is not particularly clean on this and one has to follow the right sequence; + this can be optimized shortly.""") + c.drawText(t) + + #first the outline + c.saveState() + t = c.beginText(inch, 3.0 * inch) + t.setFont('Helvetica-BoldOblique',108) + t.setTextRenderMode(5) #stroke and add to path + t.textLine('Python!') + t.setTextRenderMode(0) + c.drawText(t) #this will make a clipping mask + + #now some small stuff which wil be drawn into the current clip mask + t = c.beginText(inch, 4 * inch) + t.setFont('Times-Roman',6) + t.textLines((('spam ' * 40) + '\n') * 15) + c.drawText(t) + + #now reset canvas to get rid of the clipping mask + c.restoreState() + + c.showPage() + + +######################################################################### +# +# Page 7 - images +# +######################################################################### + framePage(c, "Images") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + if not haveImages: + c.drawString(inch, 11*inch, + "Python Imaging Library not found! Below you see rectangles instead of images.") + + t.textLines("""PDFgen uses the Python Imaging Library to process a very wide variety of image formats. + This page shows image capabilities. If I've done things right, the bitmap should have + its bottom left corner aligned with the crosshairs. + There are two methods for drawing images. The recommended use is to call drawImage. + This produces the smallest PDFs and the fastest generation times as each image's binary data is + only embedded once in the file. Also you can use advanced features like transparency masks. + You can also use drawInlineImage, which puts images in the page stream directly. + This is slightly faster for Acrobat to render or for very small images, but wastes + space if you use images more than once.""") + + c.drawText(t) + + gif = os.path.join(_RL_DIR,'test','pythonpowered.gif') + if haveImages and rl_isfile(gif): + c.drawInlineImage(gif,2*inch, 7*inch) + else: + c.rect(2*inch, 7*inch, 110, 44) + + c.line(1.5*inch, 7*inch, 4*inch, 7*inch) + c.line(2*inch, 6.5*inch, 2*inch, 8*inch) + c.drawString(4.5 * inch, 7.25*inch, 'inline image drawn at natural size') + + if haveImages and rl_isfile(gif): + c.drawInlineImage(gif,2*inch, 5*inch, inch, inch) + else: + c.rect(2*inch, 5*inch, inch, inch) + + c.line(1.5*inch, 5*inch, 4*inch, 5*inch) + c.line(2*inch, 4.5*inch, 2*inch, 6*inch) + c.drawString(4.5 * inch, 5.25*inch, 'inline image distorted to fit box') + + c.drawString(1.5 * inch, 4*inch, 'Image XObjects can be defined once in the file and drawn many times.') + c.drawString(1.5 * inch, 3.75*inch, 'This results in faster generation and much smaller files.') + + for i in range(5): + if haveImages: + (w, h) = c.drawImage(gif, (1.5 + i)*inch, 3*inch) + else: + c.rect((1.5 + i)*inch, 3*inch, 110, 44) + + myMask = [254,255,222,223,0,1] + c.drawString(1.5 * inch, 2.5*inch, "The optional 'mask' parameter lets you define transparent colors. We used a color picker") + c.drawString(1.5 * inch, 2.3*inch, "to determine that the yellow in the image above is RGB=(225,223,0). We then define a mask") + c.drawString(1.5 * inch, 2.1*inch, "spanning these RGB values: %s. The background vanishes!!" % myMask) + c.drawString(2.5*inch, 1.2*inch, 'This would normally be obscured') + if haveImages: + c.drawImage(gif, 3*inch, 1.2*inch, w, h, mask=myMask) + else: + c.rect(3*inch, 1.2*inch, 110, 44) + + c.showPage() + + +######################################################################### +# +# Page 8 - Forms and simple links +# +######################################################################### + framePage(c, "Forms and Links") + c.setFont('Times-Roman', 12) + t = c.beginText(inch, 10 * inch) + t.textLines("""Forms are sequences of text or graphics operations + which are stored only once in a PDF file and used as many times + as desired. The blue logo bar to the left is an example of a form + in this document. See the function framePageForm in this demo script + for an example of how to use canvas.beginForm(name, ...) ... canvas.endForm(). + + Documents can also contain cross references where (for example) a rectangle + on a page may be bound to a position on another page. If the user clicks + on the rectangle the PDF viewer moves to the bound position on the other + page. There are many other types of annotations and links supported by PDF. + + For example, there is a bookmark to each page in this document and below + is a browsable index that jumps to those pages. In addition we show two + URL hyperlinks; for these, you specify a rectangle but must draw the contents + or any surrounding rectangle yourself. + """) + c.drawText(t) + + nentries = len(titlelist) + xmargin = 3*inch + xmax = 7*inch + ystart = 6.54*inch + ydelta = 0.4*inch + for i in range(nentries): + yposition = ystart - i*ydelta + title = titlelist[i] + c.drawString(xmargin, yposition, title) + c.linkAbsolute(title, title, (xmargin-ydelta/4, yposition-ydelta/4, xmax, yposition+ydelta/2)) + + + # test URLs + r1 = (inch, 3*inch, 5*inch, 3.25*inch) # this is x1,y1,x2,y2 + c.linkURL('http://www.reportlab.com/', r1, thickness=1, color=colors.green) + c.drawString(inch+3, 3*inch+6, 'Hyperlink to www.reportlab.com, with green border') + + r1 = (inch, 2.5*inch, 5*inch, 2.75*inch) # this is x1,y1,x2,y2 + c.linkURL('mailto:reportlab-users@egroups.com', r1) #, border=0) + c.drawString(inch+3, 2.5*inch+6, 'mailto: hyperlink, without border') + + r1 = (inch, 2*inch, 5*inch, 2.25*inch) # this is x1,y1,x2,y2 + c.linkURL('http://www.reportlab.com/', r1, + thickness=2, + dashArray=[2,4], + color=colors.magenta) + c.drawString(inch+3, 2*inch+6, 'Hyperlink with custom border style') + + ### now do stuff for the outline + #for x in outlinenametree: print x + #stop + #apply(c.setOutlineNames0, tuple(outlinenametree)) + return c + + +def run(filename): + c = makeDocument(filename) + c.save() + source = str(c) + open(outputfile("test_pdfgen_pycanvas_out.txt"),"w").write(source) + import reportlab.rl_config + if reportlab.rl_config.verbose: + print source + + +def pageShapes(c): + """Demonstrates the basic lines and shapes""" + + c.showPage() + framePage(c, "Basic line and shape routines""") + c.setTextOrigin(inch, 10 * inch) + c.setFont('Times-Roman', 12) + c.textLines("""pdfgen provides some basic routines for drawing straight and curved lines, + and also for solid shapes.""") + + y = 9 * inch + d = DocBlock() + d.comment1 = 'Lesson one' + d.code = "canvas.textOut('hello, world')" + print d.code + + d.comment2 = 'Lesson two' + + d.draw(c, inch, 9 * inch) + + +class PdfgenTestCase(unittest.TestCase): + "Make documents with lots of Pdfgen features" + + def test0(self): + "Make a PDFgen document with most graphics features" + run(outputfile('test_pdfgen_pycanvas.pdf')) + +def makeSuite(): + return makeSuiteForClasses(PdfgenTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_breaking.py b/bin/reportlab/test/test_platypus_breaking.py new file mode 100644 index 00000000000..7db78203a1e --- /dev/null +++ b/bin/reportlab/test/test_platypus_breaking.py @@ -0,0 +1,114 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_breaking.py +"""Tests pageBreakBefore, frameBreakBefore, keepWithNext... +""" + +import sys, os, time +from string import split, strip, join, whitespace +from operator import truth +from types import StringType, ListType + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.platypus.flowables import Flowable +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.lib.randomtext import randomText, PYTHON +from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate +from reportlab.platypus.paragraph import * + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 15.5*cm, 6*cm, 10*cm, id='F1') + frame2 = Frame(11.5*cm, 15.5*cm, 6*cm, 10*cm, id='F2') + frame3 = Frame(2.5*cm, 2.5*cm, 6*cm, 10*cm, id='F3') + frame4 = Frame(11.5*cm, 2.5*cm, 6*cm, 10*cm, id='F4') + self.allowSplitting = 0 + self.showBoundary = 1 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template = PageTemplate('normal', [frame1, frame2, frame3, frame4], myMainPageFrame) + self.addPageTemplates(template) + + +def _test0(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + + styleSheet = getSampleStyleSheet() + h1 = styleSheet['Heading1'] + h1.pageBreakBefore = 1 + h1.keepWithNext = 1 + + h2 = styleSheet['Heading2'] + h2.frameBreakBefore = 1 + h2.keepWithNext = 1 + + h3 = styleSheet['Heading3'] + h3.backColor = colors.cyan + h3.keepWithNext = 1 + + bt = styleSheet['BodyText'] + + story.append(Paragraph(""" + Subsequent pages test pageBreakBefore, frameBreakBefore and + keepTogether attributes. Generated at %s. The number in brackets + at the end of each paragraph is its position in the story. (%d)""" % ( + time.ctime(time.time()), len(story)), bt)) + + for i in range(10): + story.append(Paragraph('Heading 1 always starts a new page (%d)' % len(story), h1)) + for j in range(3): + story.append(Paragraph('Heading1 paragraphs should always' + 'have a page break before. Heading 2 on the other hand' + 'should always have a FRAME break before (%d)' % len(story), bt)) + story.append(Paragraph('Heading 2 always starts a new frame (%d)' % len(story), h2)) + story.append(Paragraph('Heading1 paragraphs should always' + 'have a page break before. Heading 2 on the other hand' + 'should always have a FRAME break before (%d)' % len(story), bt)) + for j in range(3): + story.append(Paragraph(randomText(theme=PYTHON, sentences=2)+' (%d)' % len(story), bt)) + story.append(Paragraph('I should never be at the bottom of a frame (%d)' % len(story), h3)) + story.append(Paragraph(randomText(theme=PYTHON, sentences=1)+' (%d)' % len(story), bt)) + + doc = MyDocTemplate(outputfile('test_platypus_breaking.pdf')) + doc.multiBuild(story) + + +class BreakingTestCase(unittest.TestCase): + "Test multi-page splitting of paragraphs (eyeball-test)." + def test0(self): + _test0(self) + + +def makeSuite(): + return makeSuiteForClasses(BreakingTestCase) + + +#noruntests +if __name__ == "__main__": #NORUNTESTS + if 'debug' in sys.argv: + _test0(None) + else: + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_general.py b/bin/reportlab/test/test_platypus_general.py new file mode 100644 index 00000000000..1bf87c94579 --- /dev/null +++ b/bin/reportlab/test/test_platypus_general.py @@ -0,0 +1,581 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_general.py +__version__=''' $Id: test_platypus_general.py 2619 2005-06-24 14:49:15Z rgbecker $ ''' + +#tests and documents Page Layout API +__doc__="""This is not obvious so here's a brief explanation. This module is both +the test script and user guide for layout. Each page has two frames on it: +one for commentary, and one for demonstration objects which may be drawn in +various esoteric ways. The two functions getCommentary() and getExamples() +return the 'story' for each. The run() function gets the stories, then +builds a special "document model" in which the frames are added to each page +and drawn into. +""" + +import string, copy, sys, os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfgen import canvas +from reportlab import platypus +from reportlab.platypus import BaseDocTemplate, PageTemplate, Flowable, FrameBreak +from reportlab.platypus import Paragraph, Preformatted +from reportlab.lib.units import inch, cm +from reportlab.lib.styles import PropertySet, getSampleStyleSheet, ParagraphStyle +from reportlab.lib import colors +from reportlab.rl_config import defaultPageSize +from reportlab.lib.utils import haveImages, _RL_DIR, rl_isfile, open_for_read +if haveImages: + _GIF = os.path.join(_RL_DIR,'test','pythonpowered.gif') + if not rl_isfile(_GIF): _GIF = None +else: + _GIF = None +_JPG = os.path.join(_RL_DIR,'docs','images','lj8100.jpg') +if not rl_isfile(_JPG): _JPG = None + +def getFurl(fn): + furl = fn.replace(os.sep,'/') + if sys.platform=='win32' and furl[1]==':': furl = furl[0]+'|'+furl[2:] + if furl[0]!='/': furl = '/'+furl + return 'file://'+furl + +PAGE_HEIGHT = defaultPageSize[1] + +################################################################# +# +# first some drawing utilities +# +# +################################################################ + +BASEFONT = ('Times-Roman', 10) + +def framePage(canvas,doc): + #canvas.drawImage("snkanim.gif", 36, 36) + canvas.saveState() + canvas.setStrokeColorRGB(1,0,0) + canvas.setLineWidth(5) + canvas.line(66,72,66,PAGE_HEIGHT-72) + + canvas.setFont('Times-Italic',12) + canvas.drawRightString(523, PAGE_HEIGHT - 56, "Platypus User Guide and Test Script") + + canvas.setFont('Times-Roman',12) + canvas.drawString(4 * inch, 0.75 * inch, + "Page %d" % canvas.getPageNumber()) + canvas.restoreState() + +def getParagraphs(textBlock): + """Within the script, it is useful to whack out a page in triple + quotes containing separate paragraphs. This breaks one into its + constituent paragraphs, using blank lines as the delimiter.""" + lines = string.split(textBlock, '\n') + paras = [] + currentPara = [] + for line in lines: + if len(string.strip(line)) == 0: + #blank, add it + if currentPara <> []: + paras.append(string.join(currentPara, '\n')) + currentPara = [] + else: + currentPara.append(line) + #...and the last one + if currentPara <> []: + paras.append(string.join(currentPara, '\n')) + + return paras + +def getCommentary(): + """Returns the story for the commentary - all the paragraphs.""" + + styleSheet = getSampleStyleSheet() + + story = [] + story.append(Paragraph(""" + PLATYPUS User Guide and Test Script + """, styleSheet['Heading1'])) + + + spam = """ + Welcome to PLATYPUS! + + Platypus stands for "Page Layout and Typography Using Scripts". It is a high + level page layout library which lets you programmatically create complex + documents with a minimum of effort. + + This document is both the user guide & the output of the test script. + In other words, a script used platypus to create the document you are now + reading, and the fact that you are reading it proves that it works. Or + rather, that it worked for this script anyway. It is a first release! + + Platypus is built 'on top of' PDFgen, the Python library for creating PDF + documents. To learn about PDFgen, read the document testpdfgen.pdf. + + """ + + for text in getParagraphs(spam): + story.append(Paragraph(text, styleSheet['BodyText'])) + + story.append(Paragraph(""" + What concepts does PLATYPUS deal with? + """, styleSheet['Heading2'])) + story.append(Paragraph(""" + The central concepts in PLATYPUS are Flowable Objects, Frames, Flow + Management, Styles and Style Sheets, Paragraphs and Tables. This is + best explained in contrast to PDFgen, the layer underneath PLATYPUS. + PDFgen is a graphics library, and has primitive commans to draw lines + and strings. There is nothing in it to manage the flow of text down + the page. PLATYPUS works at the conceptual level fo a desktop publishing + package; you can write programs which deal intelligently with graphic + objects and fit them onto the page. + """, styleSheet['BodyText'])) + + story.append(Paragraph(""" + How is this document organized? + """, styleSheet['Heading2'])) + + story.append(Paragraph(""" + Since this is a test script, we'll just note how it is organized. + the top of each page contains commentary. The bottom half contains + example drawings and graphic elements to whicht he commentary will + relate. Down below, you can see the outline of a text frame, and + various bits and pieces within it. We'll explain how they work + on the next page. + """, styleSheet['BodyText'])) + + story.append(FrameBreak()) + ####################################################################### + # Commentary Page 2 + ####################################################################### + + story.append(Paragraph(""" + Flowable Objects + """, styleSheet['Heading2'])) + spam = """ + The first and most fundamental concept is that of a 'Flowable Object'. + In PDFgen, you draw stuff by calling methods of the canvas to set up + the colors, fonts and line styles, and draw the graphics primitives. + If you set the pen color to blue, everything you draw after will be + blue until you change it again. And you have to handle all of the X-Y + coordinates yourself. + + A 'Flowable object' is exactly what it says. It knows how to draw itself + on the canvas, and the way it does so is totally independent of what + you drew before or after. Furthermore, it draws itself at the location + on the page you specify. + + The most fundamental Flowable Objects in most documents are likely to be + paragraphs, tables, diagrams/charts and images - but there is no + restriction. You can write your own easily, and I hope that people + will start to contribute them. PINGO users - we provide a "PINGO flowable" object to let + you insert platform-independent graphics into the flow of a document. + + When you write a flowable object, you inherit from Flowable and + must implement two methods. object.wrap(availWidth, availHeight) will be called by other parts of + the system, and tells you how much space you have. You should return + how much space you are going to use. For a fixed-size object, this + is trivial, but it is critical - PLATYPUS needs to figure out if things + will fit on the page before drawing them. For other objects such as paragraphs, + the height is obviously determined by the available width. + + + The second method is object.draw(). Here, you do whatever you want. + The Flowable base class sets things up so that you have an origin of + (0,0) for your drawing, and everything will fit nicely if you got the + height and width right. It also saves and restores the graphics state + around your calls, so you don;t have to reset all the properties you + changed. + + Programs which actually draw a Flowable don't + call draw() this directly - they call object.drawOn(canvas, x, y). + So you can write code in your own coordinate system, and things + can be drawn anywhere on the page (possibly even scaled or rotated). + """ + for text in getParagraphs(spam): + story.append(Paragraph(text, styleSheet['BodyText'])) + + story.append(FrameBreak()) + ####################################################################### + # Commentary Page 3 + ####################################################################### + + story.append(Paragraph(""" + Available Flowable Objects + """, styleSheet['Heading2'])) + + story.append(Paragraph(""" + Platypus comes with a basic set of flowable objects. Here we list their + class names and tell you what they do: + """, styleSheet['BodyText'])) + #we can use the bullet feature to do a definition list + story.append(Paragraph(""" + This is a contrived object to give an example of a Flowable - + just a fixed-size box with an X through it and a centred string.""", + styleSheet['Definition'], + bulletText='XBox ' #hack - spot the extra space after + )) + + story.append(Paragraph(""" + This is the basic unit of a document. Paragraphs can be finely + tuned and offer a host of properties through their associated + ParagraphStyle.""", + styleSheet['Definition'], + bulletText='Paragraph ' #hack - spot the extra space after + )) + + story.append(Paragraph(""" + This is used for printing code and other preformatted text. + There is no wrapping, and line breaks are taken where they occur. + Many paragraph style properties do not apply. You may supply + an optional 'dedent' parameter to trim a number of characters + off the front of each line.""", + styleSheet['Definition'], + bulletText='Preformatted ' #hack - spot the extra space after + )) + story.append(Paragraph(""" + This is a straight wrapper around an external image file. By default + the image will be drawn at a scale of one pixel equals one point, and + centred in the frame. You may supply an optional width and height.""", + styleSheet['Definition'], + bulletText='Image ' #hack - spot the extra space after + )) + + story.append(Paragraph(""" + This is a table drawing class; it is intended to be simpler + than a full HTML table model yet be able to draw attractive output, + and behave intelligently when the numbers of rows and columns vary. + Still need to add the cell properties (shading, alignment, font etc.)""", + styleSheet['Definition'], + bulletText='Table ' #hack - spot the extra space after + )) + + story.append(Paragraph(""" + This is a 'null object' which merely takes up space on the page. + Use it when you want some extra padding betweene elements.""", + styleSheet['Definition'], + bulletText='Spacer ' #hack - spot the extra space after + )) + + story.append(Paragraph(""" + A FrameBreak causes the document to call its handle_frameEnd method.""", + styleSheet['Definition'], + bulletText='FrameBreak ' #hack - spot the extra space after + )) + + story.append(Paragraph(""" + This is in progress, but a macro is basically a chunk of Python code to + be evaluated when it is drawn. It could do lots of neat things.""", + styleSheet['Definition'], + bulletText='Macro ' #hack - spot the extra space after + )) + + story.append(FrameBreak()) + + story.append(Paragraph( + "The next example uses a custom font", + styleSheet['Italic'])) + def code(txt,story=story,styleSheet=styleSheet): + story.append(Preformatted(txt,styleSheet['Code'])) + code('''import reportlab.rl_config + reportlab.rl_config.warnOnMissingFontGlyphs = 0 + + from reportlab.pdfbase import pdfmetrics + fontDir = os.path.join(os.path.dirname(reportlab.__file__),'fonts') + face = pdfmetrics.EmbeddedType1Face(os.path.join(fontDir,'LeERC___.AFM'), + os.path.join(fontDir,'LeERC___.PFB')) + faceName = face.name # should be 'LettErrorRobot-Chrome' + pdfmetrics.registerTypeFace(face) + font = pdfmetrics.Font(faceName, faceName, 'WinAnsiEncoding') + pdfmetrics.registerFont(font) + + + # put it inside a paragraph. + story.append(Paragraph( + """This is an ordinary paragraph, which happens to contain + text in an embedded font: + LettErrorRobot-Chrome. + Now for the real challenge...""", styleSheet['Normal'])) + + + styRobot = ParagraphStyle('Robot', styleSheet['Normal']) + styRobot.fontSize = 16 + styRobot.leading = 20 + styRobot.fontName = 'LettErrorRobot-Chrome' + + story.append(Paragraph( + "This whole paragraph is 16-point Letterror-Robot-Chrome.", + styRobot))''') + + story.append(FrameBreak()) + if _GIF: + story.append(Paragraph("""We can use images via the file name""", styleSheet['BodyText'])) + code(''' story.append(platypus.Image('%s'))'''%_GIF) + code(''' story.append(platypus.Image('%s'.encode('utf8')))''' % _GIF) + story.append(Paragraph("""They can also be used with a file URI or from an open python file!""", styleSheet['BodyText'])) + code(''' story.append(platypus.Image('%s'))'''% getFurl(_GIF)) + code(''' story.append(platypus.Image(open_for_read('%s','b')))''' % _GIF) + story.append(FrameBreak()) + story.append(Paragraph("""Images can even be obtained from the internet.""", styleSheet['BodyText'])) + code(''' img = platypus.Image('http://www.reportlab.com/rsrc/encryption.gif') + story.append(img)''') + story.append(FrameBreak()) + + if _JPG: + story.append(Paragraph("""JPEGs are a native PDF image format. They should be available even if PIL cannot be used.""", styleSheet['BodyText'])) + story.append(FrameBreak()) + return story + +def getExamples(): + """Returns all the example flowable objects""" + styleSheet = getSampleStyleSheet() + + story = [] + + #make a style with indents and spacing + sty = ParagraphStyle('obvious', None) + sty.leftIndent = 18 + sty.rightIndent = 18 + sty.firstLineIndent = 18 + sty.spaceBefore = 6 + sty.spaceAfter = 6 + story.append(Paragraph("""Now for some demo stuff - we need some on this page, + even before we explain the concepts fully""", styleSheet['BodyText'])) + p = Paragraph(""" + Platypus is all about fitting objects into frames on the page. You + are looking at a fairly simple Platypus paragraph in Debug mode. + It has some gridlines drawn around it to show the left and right indents, + and the space before and after, all of which are attributes set in + the style sheet. To be specific, this paragraph has left and + right indents of 18 points, a first line indent of 36 points, + and 6 points of space before and after itself. A paragraph + object fills the width of the enclosing frame, as you would expect.""", sty) + + p.debug = 1 #show me the borders + story.append(p) + + story.append(Paragraph("""Same but with justification 1.5 extra leading and green text.""", styleSheet['BodyText'])) + p = Paragraph(""" + Platypus is all about fitting objects into frames on the page. You + are looking at a fairly simple Platypus paragraph in Debug mode. + It has some gridlines drawn around it to show the left and right indents, + and the space before and after, all of which are attributes set in + the style sheet. To be specific, this paragraph has left and + right indents of 18 points, a first line indent of 36 points, + and 6 points of space before and after itself. A paragraph + object fills the width of the enclosing frame, as you would expect.""", sty) + + p.debug = 1 #show me the borders + story.append(p) + + story.append(platypus.XBox(4*inch, 0.75*inch, + 'This is a box with a fixed size')) + + story.append(Paragraph(""" + All of this is being drawn within a text frame which was defined + on the page. This frame is in 'debug' mode so you can see the border, + and also see the margins which it reserves. A frame does not have + to have margins, but they have been set to 6 points each to create + a little space around the contents. + """, styleSheet['BodyText'])) + + story.append(FrameBreak()) + + ####################################################################### + # Examples Page 2 + ####################################################################### + + story.append(Paragraph(""" + Here's the base class for Flowable... + """, styleSheet['Italic'])) + + code = '''class Flowable: + """Abstract base class for things to be drawn. Key concepts: + 1. It knows its size + 2. It draws in its own coordinate system (this requires the + base API to provide a translate() function. + """ + def __init__(self): + self.width = 0 + self.height = 0 + self.wrapped = 0 + + def drawOn(self, canvas, x, y): + "Tell it to draw itself on the canvas. Do not override" + self.canv = canvas + self.canv.saveState() + self.canv.translate(x, y) + + self.draw() #this is the bit you overload + + self.canv.restoreState() + del self.canv + + def wrap(self, availWidth, availHeight): + """This will be called by the enclosing frame before objects + are asked their size, drawn or whatever. It returns the + size actually used.""" + return (self.width, self.height) + ''' + + story.append(Preformatted(code, styleSheet['Code'], dedent=4)) + story.append(FrameBreak()) + ####################################################################### + # Examples Page 3 + ####################################################################### + + story.append(Paragraph( + "Here are some examples of the remaining objects above.", + styleSheet['Italic'])) + + story.append(Paragraph("This is a bullet point", styleSheet['Bullet'], bulletText='O')) + story.append(Paragraph("Another bullet point", styleSheet['Bullet'], bulletText='O')) + + + story.append(Paragraph("""Here is a Table, which takes all kinds of formatting options...""", + styleSheet['Italic'])) + story.append(platypus.Spacer(0, 12)) + + g = platypus.Table( + (('','North','South','East','West'), + ('Quarter 1',100,200,300,400), + ('Quarter 2',100,200,300,400), + ('Total',200,400,600,800)), + (72,36,36,36,36), + (24, 16,16,18) + ) + style = platypus.TableStyle([('ALIGN', (1,1), (-1,-1), 'RIGHT'), + ('ALIGN', (0,0), (-1,0), 'CENTRE'), + ('GRID', (0,0), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEBELOW',(1,-1), (-1, -1), 2, (0.5, 0.5, 0.5)), + ('TEXTCOLOR', (0,1), (0,-1), colors.black), + ('BACKGROUND', (0,0), (-1,0), (0,0.7,0.7)) + ]) + g.setStyle(style) + story.append(g) + story.append(FrameBreak()) + + ####################################################################### + # Examples Page 4 - custom fonts + ####################################################################### + # custom font with LettError-Robot font + import reportlab.rl_config + reportlab.rl_config.warnOnMissingFontGlyphs = 0 + + from reportlab.pdfbase import pdfmetrics + fontDir = os.path.join(os.path.dirname(reportlab.__file__),'fonts') + face = pdfmetrics.EmbeddedType1Face(os.path.join(fontDir,'LeERC___.AFM'),os.path.join(fontDir,'LeERC___.PFB')) + faceName = face.name # should be 'LettErrorRobot-Chrome' + pdfmetrics.registerTypeFace(face) + font = pdfmetrics.Font(faceName, faceName, 'WinAnsiEncoding') + pdfmetrics.registerFont(font) + + + # put it inside a paragraph. + story.append(Paragraph( + """This is an ordinary paragraph, which happens to contain + text in an embedded font: + LettErrorRobot-Chrome. + Now for the real challenge...""", styleSheet['Normal'])) + + + styRobot = ParagraphStyle('Robot', styleSheet['Normal']) + styRobot.fontSize = 16 + styRobot.leading = 20 + styRobot.fontName = 'LettErrorRobot-Chrome' + + story.append(Paragraph( + "This whole paragraph is 16-point Letterror-Robot-Chrome.", + styRobot)) + story.append(FrameBreak()) + + if _GIF: + story.append(Paragraph("Here is an Image flowable obtained from a string filename.",styleSheet['Italic'])) + story.append(platypus.Image(_GIF)) + story.append(Paragraph( "Here is an Image flowable obtained from a utf8 filename.", styleSheet['Italic'])) + story.append(platypus.Image(_GIF.encode('utf8'))) + story.append(Paragraph("Here is an Image flowable obtained from a string file url.",styleSheet['Italic'])) + story.append(platypus.Image(getFurl(_GIF))) + story.append(Paragraph("Here is an Image flowable obtained from an open file.",styleSheet['Italic'])) + story.append(platypus.Image(open_for_read(_GIF,'b'))) + story.append(FrameBreak()) + try: + img = platypus.Image('http://www.reportlab.com/rsrc/encryption.gif') + story.append(Paragraph("Here is an Image flowable obtained from a string http url.",styleSheet['Italic'])) + story.append(img) + except: + story.append(Paragraph("The image could not be obtained from a string http url.",styleSheet['Italic'])) + story.append(FrameBreak()) + + if _JPG: + img = platypus.Image(_JPG) + story.append(Paragraph("Here is an JPEG Image flowable obtained from a filename.",styleSheet['Italic'])) + story.append(img) + story.append(Paragraph("Here is an JPEG Image flowable obtained from an open file.",styleSheet['Italic'])) + img = platypus.Image(open_for_read(_JPG,'b')) + story.append(img) + story.append(FrameBreak()) + + + return story + +class AndyTemplate(BaseDocTemplate): + _invalidInitArgs = ('pageTemplates',) + def __init__(self, filename, **kw): + frame1 = platypus.Frame(inch, 5.6*inch, 6*inch, 5.2*inch,id='F1') + frame2 = platypus.Frame(inch, inch, 6*inch, 4.5*inch, showBoundary=1,id='F2') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__,(self,filename),kw) + self.addPageTemplates(PageTemplate('normal',[frame1,frame2],framePage)) + + def fillFrame(self,flowables): + f = self.frame + while len(flowables)>0 and f is self.frame: + self.handle_flowable(flowables) + + def build(self, flowables1, flowables2): + assert filter(lambda x: not isinstance(x,Flowable), flowables1)==[], "flowables1 argument error" + assert filter(lambda x: not isinstance(x,Flowable), flowables2)==[], "flowables2 argument error" + self._startBuild() + while (len(flowables1) > 0 or len(flowables1) > 0): + self.clean_hanging() + self.fillFrame(flowables1) + self.fillFrame(flowables2) + + self._endBuild() + + +def showProgress(pageNo): + print 'CALLBACK SAYS: page %d' % pageNo + + +def run(): + doc = AndyTemplate(outputfile('test_platypus_general.pdf')) + #doc.setPageCallBack(showProgress) + commentary = getCommentary() + examples = getExamples() + doc.build(commentary,examples) + + +class PlatypusTestCase(unittest.TestCase): + "Make documents with lots of Platypus features" + + def test0(self): + "Make a platypus document" + run() + + +def makeSuite(): + return makeSuiteForClasses(PlatypusTestCase) + + +#noruntests +if __name__ == "__main__": + if '-debug' in sys.argv: + run() + else: + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_indents.py b/bin/reportlab/test/test_platypus_indents.py new file mode 100644 index 00000000000..ae7d9f44c87 --- /dev/null +++ b/bin/reportlab/test/test_platypus_indents.py @@ -0,0 +1,140 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_indents.py +"""Tests for context-dependent indentation +""" + +import sys, os, random +from string import split, strip, join, whitespace +from operator import truth +from types import StringType, ListType + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus.paraparser import ParaParser +from reportlab.platypus.flowables import Flowable +from reportlab.lib.colors import Color +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.utils import _className +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate \ + import PageTemplate, BaseDocTemplate, Indenter, FrameBreak, NextPageTemplate +from reportlab.platypus import tableofcontents +from reportlab.platypus.tableofcontents import TableOfContents +from reportlab.platypus.tables import TableStyle, Table +from reportlab.platypus.paragraph import * +from reportlab.platypus.paragraph import _getFragWords +from reportlab.platypus.flowables import Spacer + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + canvas.rect(2.5*cm, 2.5*cm, 15*cm, 25*cm) + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template1 = PageTemplate('normal', [frame1], myMainPageFrame) + + frame2 = Frame(2.5*cm, 16*cm, 15*cm, 10*cm, id='F2', showBoundary=1) + frame3 = Frame(2.5*cm, 2.5*cm, 15*cm, 10*cm, id='F3', showBoundary=1) + + template2 = PageTemplate('updown', [frame2, frame3]) + self.addPageTemplates([template1, template2]) + + +class IndentTestCase(unittest.TestCase): + "Test multi-page splitting of paragraphs (eyeball-test)." + + def test0(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + + styleSheet = getSampleStyleSheet() + h1 = styleSheet['Heading1'] + h1.spaceBefore = 18 + bt = styleSheet['BodyText'] + bt.spaceBefore = 6 + + story.append(Paragraph('Test of context-relative indentation',h1)) + + story.append(Spacer(18,18)) + + story.append(Indenter(0,0)) + story.append(Paragraph("This should be indented 0 points at each edge. " + ("spam " * 25),bt)) + story.append(Indenter(0,0)) + + story.append(Indenter(36,0)) + story.append(Paragraph("This should be indented 36 points at the left. " + ("spam " * 25),bt)) + story.append(Indenter(-36,0)) + + story.append(Indenter(0,36)) + story.append(Paragraph("This should be indented 36 points at the right. " + ("spam " * 25),bt)) + story.append(Indenter(0,-36)) + + story.append(Indenter(36,36)) + story.append(Paragraph("This should be indented 36 points at each edge. " + ("spam " * 25),bt)) + story.append(Indenter(36,36)) + story.append(Paragraph("This should be indented a FURTHER 36 points at each edge. " + ("spam " * 25),bt)) + story.append(Indenter(-72,-72)) + + story.append(Paragraph("This should be back to normal at each edge. " + ("spam " * 25),bt)) + + + story.append(Indenter(36,36)) + story.append(Paragraph(("""This should be indented 36 points at the left + and right. It should run over more than one page and the indent should + continue on the next page. """ + (random.randint(0,10) * 'x') + ' ') * 20 ,bt)) + story.append(Indenter(-36,-36)) + + story.append(NextPageTemplate('updown')) + story.append(FrameBreak()) + story.append(Paragraph('Another test of context-relative indentation',h1)) + story.append(NextPageTemplate('normal')) # so NEXT page is different template... + story.append(Paragraph("""This time we see if the indent level is continued across + frames...this page has 2 frames, let's see if it carries top to bottom. Then + onto a totally different template.""",bt)) + + story.append(Indenter(0,0)) + story.append(Paragraph("This should be indented 0 points at each edge. " + ("spam " * 25),bt)) + story.append(Indenter(0,0)) + story.append(Indenter(36,72)) + story.append(Paragraph(("""This should be indented 36 points at the left + and 72 at the right. It should run over more than one frame and one page, and the indent should + continue on the next page. """ + (random.randint(0,10) * 'x') + ' ') * 35 ,bt)) + + story.append(Indenter(-36,-72)) + story.append(Paragraph("This should be back to normal at each edge. " + ("spam " * 25),bt)) + doc = MyDocTemplate(outputfile('test_platypus_indents.pdf')) + doc.multiBuild(story) + + +#noruntests +def makeSuite(): + return makeSuiteForClasses(IndentTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_leftright.py b/bin/reportlab/test/test_platypus_leftright.py new file mode 100644 index 00000000000..6f9369ec0e6 --- /dev/null +++ b/bin/reportlab/test/test_platypus_leftright.py @@ -0,0 +1,158 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_breaking.py +"""Tests ability to cycle through multiple page templates +""" + +import sys, os, time +from string import split, strip, join, whitespace +from operator import truth +from types import StringType, ListType + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.platypus.flowables import Flowable +from reportlab.lib import colors +from reportlab.lib.pagesizes import A4 +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.lib.randomtext import randomText, PYTHON +from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate, NextPageTemplate +from reportlab.platypus.paragraph import * + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + canvas.restoreState() + + + +class LeftPageTemplate(PageTemplate): + def __init__(self): + #allow a bigger margin on the right for the staples + frame = Frame(1.5*cm, 2.5*cm, 16*cm, 25*cm, id='F1') + + PageTemplate.__init__(self, + id='left', + frames=[frame], + pagesize=A4) + def beforeDrawPage(self, canv, doc): + "Decorate the page with an asymetric design" + canv.setFillColor(colors.cyan) + + canv.rect(0.5*cm, 2.5*cm, 1*cm, 25*cm, stroke=1, fill=1) + canv.circle(19*cm, 10*cm, 0.5*cm, stroke=1, fill=1) + canv.circle(19*cm, 20*cm, 0.5*cm, stroke=1, fill=1) + canv.setFillColor(colors.black) + + +class RightPageTemplate(PageTemplate): + def __init__(self): + #allow a bigger margin on the right for the staples + frame = Frame(3.5*cm, 2.5*cm, 16*cm, 25*cm, id='F1') + + PageTemplate.__init__(self, + id='right', + frames=[frame], + pagesize=A4) + def beforeDrawPage(self, canv, doc): + "Decorate the page with an asymetric design" + canv.setFillColor(colors.cyan) + canv.rect(19.5*cm, 2.5*cm, 1*cm, 25*cm, stroke=1, fill=1) + canv.circle(2*cm, 10*cm, 0.5*cm, stroke=1, fill=1) + canv.circle(2*cm, 20*cm, 0.5*cm, stroke=1, fill=1) + canv.setFillColor(colors.black) + + +class MyDocTemplate(BaseDocTemplate): + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + apply(BaseDocTemplate.__init__, (self, filename), kw) + self.addPageTemplates( + [ + PageTemplate(id='plain', + frames=[Frame(2.5*cm, 2.5*cm, 16*cm, 25*cm, id='F1')] + ), + LeftPageTemplate(), + RightPageTemplate() + ] + ) + + +class LeftRightTestCase(unittest.TestCase): + "Test multi-page splitting of paragraphs (eyeball-test)." + def testIt(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + + styleSheet = getSampleStyleSheet() + h1 = styleSheet['Heading1'] + h1.pageBreakBefore = 1 + h1.keepWithNext = 1 + + h2 = styleSheet['Heading2'] + h2.frameBreakBefore = 1 + h2.keepWithNext = 1 + + h3 = styleSheet['Heading3'] + h3.backColor = colors.cyan + h3.keepWithNext = 1 + + bt = styleSheet['BodyText'] + + story.append(Paragraph(""" + This tests ability to alternate left and right templates. We start on + a plain one. The next page should display a left-side template, + with a big inner margin and staple-like holes on the right.""",style=bt)) + + story.append(NextPageTemplate(['left','right'])) + + + story.append(Paragraph(""" + One can specify a list of templates instead of a single one in + order to sequence through them.""",style=bt)) + for i in range(10): + story.append(Paragraph('Heading 1 always starts a new page (%d)' % len(story), h1)) + for j in range(3): + story.append(Paragraph('Heading1 paragraphs should always' + 'have a page break before. Heading 2 on the other hand' + 'should always have a FRAME break before (%d)' % len(story), bt)) + story.append(Paragraph('Heading 2 always starts a new frame (%d)' % len(story), h2)) + story.append(Paragraph('Heading1 paragraphs should always' + 'have a page break before. Heading 2 on the other hand' + 'should always have a FRAME break before (%d)' % len(story), bt)) + for j in range(3): + story.append(Paragraph(randomText(theme=PYTHON, sentences=2)+' (%d)' % len(story), bt)) + story.append(Paragraph('I should never be at the bottom of a frame (%d)' % len(story), h3)) + story.append(Paragraph(randomText(theme=PYTHON, sentences=1)+' (%d)' % len(story), bt)) + + story.append(NextPageTemplate('plain')) + story.append(Paragraph('Back to plain old page template',h1)) + story.append(Paragraph('Back to plain old formatting', bt)) + + + #doc = MyDocTemplate(outputfile('test_platypus_leftright.pdf')) + doc = MyDocTemplate(outputfile('test_platypus_leftright.pdf')) + doc.multiBuild(story) + + +def makeSuite(): + return makeSuiteForClasses(LeftRightTestCase) + + +#noruntests +if __name__ == "__main__": #NORUNTESTS + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_paragraphs.py b/bin/reportlab/test/test_platypus_paragraphs.py new file mode 100644 index 00000000000..0720577fc85 --- /dev/null +++ b/bin/reportlab/test/test_platypus_paragraphs.py @@ -0,0 +1,184 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_paragraphs.py +"""Tests for the reportlab.platypus.paragraphs module. +""" + +import sys, os +from string import split, strip, join, whitespace +from operator import truth +from types import StringType, ListType + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.pdfbase.pdfmetrics import stringWidth +from reportlab.platypus.paraparser import ParaParser +from reportlab.platypus.flowables import Flowable +from reportlab.lib.colors import Color +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.utils import _className +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate \ + import PageTemplate, BaseDocTemplate +from reportlab.platypus import tableofcontents +from reportlab.platypus.tableofcontents import TableOfContents +from reportlab.platypus.tables import TableStyle, Table +from reportlab.platypus.paragraph import * +from reportlab.platypus.paragraph import _getFragWords + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + canvas.rect(2.5*cm, 2.5*cm, 15*cm, 25*cm) + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template = PageTemplate('normal', [frame1], myMainPageFrame) + self.addPageTemplates(template) + + +class ParagraphCorners(unittest.TestCase): + "some corner cases which should parse" + def check(text,bt = getSampleStyleSheet()['BodyText']): + try: + P = Paragraph(text,st) + except: + raise AssertionError("'%s' should parse"%text) + + def test0(self): + self.check('') + self.check('') + self.check('\t\t\t\n\n\n') + self.check('\t\t\t\n\n\n') + self.check('') + self.check('') + self.check(' ') + self.check('\t\t\n\t\t\t ') + + + +class ParagraphSplitTestCase(unittest.TestCase): + "Test multi-page splitting of paragraphs (eyeball-test)." + + def test0(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + styleSheet = getSampleStyleSheet() + bt = styleSheet['BodyText'] + text = '''If you imagine that the box of X's tothe left is +an image, what I want to be able to do is flow a +series of paragraphs around the image +so that once the bottom of the image is reached, then text will flow back to the +left margin. I know that it would be possible to something like this +using tables, but I can't see how to have a generic solution. +There are two examples of this in the demonstration section of the reportlab +site. +If you look at the "minimal" euro python conference brochure, at the end of the +timetable section (page 8), there are adverts for "AdSu" and "O'Reilly". I can +see how the AdSu one might be done generically, but the O'Reilly, unsure... +I guess I'm hoping that I've missed something, and that +it's actually easy to do using platypus. +''' + from reportlab.platypus.flowables import ParagraphAndImage, Image + from reportlab.lib.utils import _RL_DIR + gif = os.path.join(_RL_DIR,'test','pythonpowered.gif') + story.append(ParagraphAndImage(Paragraph(text,bt),Image(gif))) + phrase = 'This should be a paragraph spanning at least three pages. ' + description = ''.join([('%d: '%i)+phrase for i in xrange(250)]) + story.append(ParagraphAndImage(Paragraph(description, bt),Image(gif),side='left')) + + doc = MyDocTemplate(outputfile('test_platypus_paragraphandimage.pdf')) + doc.multiBuild(story) + + def test1(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + styleSheet = getSampleStyleSheet() + h3 = styleSheet['Heading3'] + bt = styleSheet['BodyText'] + text = '''If you imagine that the box of X's tothe left is +an image, what I want to be able to do is flow a +series of paragraphs around the image +so that once the bottom of the image is reached, then text will flow back to the +left margin. I know that it would be possible to something like this +using tables, but I can't see how to have a generic solution. +There are two examples of this in the demonstration section of the reportlab +site. +If you look at the "minimal" euro python conference brochure, at the end of the +timetable section (page 8), there are adverts for "AdSu" and "O'Reilly". I can +see how the AdSu one might be done generically, but the O'Reilly, unsure... +I guess I'm hoping that I've missed something, and that +it's actually easy to do using platypus.We can do greek letters mDngG. This should be a +u with a dieresis on top <unichar code=0xfc/>="" and this &#xfc;="ü" and this \\xc3\\xbc="\xc3\xbc". On the other hand this +should be a pound sign &pound;="£" and this an alpha &alpha;="α". You can have links in the page ReportLab. +Use scheme "pdf:" to indicate an external PDF link, "http:", "https:" to indicate an external link eg something to open in +your browser. If an internal link begins with something that looks like a scheme, precede with "document:". +''' + from reportlab.platypus.flowables import ImageAndFlowables, Image + from reportlab.lib.utils import _RL_DIR + gif = os.path.join(_RL_DIR,'test','pythonpowered.gif') + heading = Paragraph('This is a heading',h3) + story.append(ImageAndFlowables(Image(gif),[heading,Paragraph(text,bt)])) + phrase = 'This should be a paragraph spanning at least three pages. ' + description = ''.join([('%d: '%i)+phrase for i in xrange(250)]) + story.append(ImageAndFlowables(Image(gif),[heading,Paragraph(description, bt)],imageSide='left')) + + doc = MyDocTemplate(outputfile('test_platypus_imageandflowables.pdf')) + doc.multiBuild(story) + +class FragmentTestCase(unittest.TestCase): + "Test fragmentation of paragraphs." + + def test0(self): + "Test empty paragraph." + + styleSheet = getSampleStyleSheet() + B = styleSheet['BodyText'] + text = '' + P = Paragraph(text, B) + frags = map(lambda f:f.text, P.frags) + assert frags == [] + + + def test1(self): + "Test simple paragraph." + + styleSheet = getSampleStyleSheet() + B = styleSheet['BodyText'] + text = "XYZ" + P = Paragraph(text, B) + frags = map(lambda f:f.text, P.frags) + assert frags == ['X', 'Y', 'Z'] + + +#noruntests +def makeSuite(): + return makeSuiteForClasses(FragmentTestCase, ParagraphSplitTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_paraparser.py b/bin/reportlab/test/test_platypus_paraparser.py new file mode 100644 index 00000000000..99cd6f0e4d1 --- /dev/null +++ b/bin/reportlab/test/test_platypus_paraparser.py @@ -0,0 +1,91 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history TBC +#$Header$ +__version__=''' $Id''' +__doc__="""Tests of intra-paragraph parsing behaviour in Platypus.""" + +from types import TupleType, ListType, StringType, UnicodeType +from pprint import pprint as pp + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile +from reportlab.platypus import cleanBlockQuotedText +from reportlab.platypus.paraparser import ParaParser, ParaFrag +from reportlab.lib.colors import black + +class ParaParserTestCase(unittest.TestCase): + """Tests of data structures created by paragraph parser. Esp. ability + to accept unicode and preserve it""" + + def setUp(self): + style=ParaFrag() + style.fontName='Times-Roman' + style.fontSize = 12 + style.textColor = black + style.bulletFontName = black + style.bulletFontName='Times-Roman' + style.bulletFontSize=12 + self.style = style + + def testPlain(self): + txt = "Hello World" + stuff = ParaParser().parse(txt, self.style) + assert type(stuff) is TupleType + assert len(stuff) == 3 + assert stuff[1][0].text == 'Hello World' + + def testBold(self): + txt = "Hello Bold World" + fragList = ParaParser().parse(txt, self.style)[1] + self.assertEquals(map(lambda x:x.text, fragList), ['Hello ','Bold',' World']) + self.assertEquals(fragList[1].fontName, 'Times-Bold') + + def testEntity(self): + "Numeric entities should be unescaped by parser" + txt = "Hello © copyright" + fragList = ParaParser().parse(txt, self.style)[1] + self.assertEquals(map(lambda x:x.text, fragList), ['Hello ','\xc2\xa9',' copyright']) + + def testEscaped(self): + "Escaped high-bit stuff should go straight through" + txt = "Hello \xc2\xa9 copyright" + fragList = ParaParser().parse(txt, self.style)[1] + assert fragList[0].text == txt + + def testPlainUnicode(self): + "See if simple unicode goes through" + txt = u"Hello World" + stuff = ParaParser().parse(txt, self.style) + assert type(stuff) is TupleType + assert len(stuff) == 3 + assert stuff[1][0].text == u'Hello World' + + def testBoldUnicode(self): + txt = u"Hello Bold World" + fragList = ParaParser().parse(txt, self.style)[1] + self.assertEquals(map(lambda x:x.text, fragList), [u'Hello ',u'Bold',u' World']) + self.assertEquals(fragList[1].fontName, 'Times-Bold') + + def testEntityUnicode(self): + "Numeric entities should be unescaped by parser" + txt = u"Hello © copyright" + fragList = ParaParser().parse(txt, self.style)[1] + self.assertEquals(map(lambda x:x.text, fragList), [u'Hello ',u'\xc2\xa9',u' copyright']) + + def testEscapedUnicode(self): + "Escaped high-bit stuff should go straight through" + txt = u"Hello \xa9 copyright" + fragList = ParaParser().parse(txt, self.style)[1] + assert fragList[0].text == txt + + + +def makeSuite(): + return makeSuiteForClasses(ParaParserTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) diff --git a/bin/reportlab/test/test_platypus_pto.py b/bin/reportlab/test/test_platypus_pto.py new file mode 100644 index 00000000000..2d119b2f6a5 --- /dev/null +++ b/bin/reportlab/test/test_platypus_pto.py @@ -0,0 +1,195 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_breaking.py +"""Tests pageBreakBefore, frameBreakBefore, keepWithNext... +""" + +import sys + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.platypus.flowables import Flowable, PTOContainer, KeepInFrame +from reportlab.lib.units import cm +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.lib.colors import toColor, black +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.tables import Table +from reportlab.platypus.frames import Frame +from reportlab.lib.randomtext import randomText +from reportlab.platypus.doctemplate import PageTemplate, BaseDocTemplate, FrameBreak + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + canvas.restoreState() + +def _showDoc(fn,story): + pageTemplate = PageTemplate('normal', [Frame(72, 440, 170, 284, id='F1'), + Frame(326, 440, 170, 284, id='F2'), + Frame(72, 72, 170, 284, id='F3'), + Frame(326, 72, 170, 284, id='F4'), + ], myMainPageFrame) + doc = BaseDocTemplate(outputfile(fn), + pageTemplates = pageTemplate, + showBoundary = 1, + ) + doc.multiBuild(story) + +text2 ='''We have already seen that the natural general principle that will +subsume this case cannot be arbitrary in the requirement that branching +is not tolerated within the dominance scope of a complex symbol. +Notice, incidentally, that the speaker-hearer's linguistic intuition is +to be regarded as the strong generative capacity of the theory. A +consequence of the approach just outlined is that the descriptive power +of the base component does not affect the structure of the levels of +acceptability from fairly high (e.g. (99a)) to virtual gibberish (e.g. +(98d)). By combining adjunctions and certain deformations, a +descriptively adequate grammar cannot be arbitrary in the strong +generative capacity of the theory.''' + +text1=''' +On our assumptions, a descriptively adequate grammar delimits the strong +generative capacity of the theory. For one thing, the fundamental error +of regarding functional notions as categorial is to be regarded as a +corpus of utterance tokens upon which conformity has been defined by the +paired utterance test. A majority of informed linguistic specialists +agree that the appearance of parasitic gaps in domains relatively +inaccessible to ordinary extraction is necessary to impose an +interpretation on the requirement that branching is not tolerated within +the dominance scope of a complex symbol. It may be, then, that the +speaker-hearer's linguistic intuition appears to correlate rather +closely with the ultimate standard that determines the accuracy of any +proposed grammar. Analogously, the notion of level of grammaticalness +may remedy and, at the same time, eliminate a general convention +regarding the forms of the grammar.''' + +text0 = '''To characterize a linguistic level L, +this selectionally introduced contextual +feature delimits the requirement that +branching is not tolerated within the +dominance scope of a complex +symbol. Notice, incidentally, that the +notion of level of grammaticalness +does not affect the structure of the +levels of acceptability from fairly high +(e.g. (99a)) to virtual gibberish (e.g. +(98d)). Suppose, for instance, that a +subset of English sentences interesting +on quite independent grounds appears +to correlate rather closely with an +important distinction in language use. +Presumably, this analysis of a +formative as a pair of sets of features is +not quite equivalent to the system of +base rules exclusive of the lexicon. We +have already seen that the appearance +of parasitic gaps in domains relatively +inaccessible to ordinary extraction +does not readily tolerate the strong +generative capacity of the theory.''' + +def _ptoTestCase(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + def fbreak(story=story): + story.append(FrameBreak()) + + styleSheet = getSampleStyleSheet() + H1 = styleSheet['Heading1'] + H1.pageBreakBefore = 0 + H1.keepWithNext = 0 + + bt = styleSheet['BodyText'] + pto = ParagraphStyle('pto',parent=bt) + pto.alignment = TA_RIGHT + pto.fontSize -= 1 + def ColorParagraph(c,text,style): + return Paragraph('%s' % (c,text),style) + + def ptoblob(blurb,content,trailer=None,header=None, story=story, H1=H1): + if type(content) not in (type([]),type(())): content = [content] + story.append(PTOContainer([Paragraph(blurb,H1)]+list(content),trailer,header)) + + t0 = [ColorParagraph('blue','Please turn over', pto )] + h0 = [ColorParagraph('blue','continued from previous page', pto )] + t1 = [ColorParagraph('red','Please turn over(inner)', pto )] + h1 = [ColorParagraph('red','continued from previous page(inner)', pto )] + ptoblob('First Try at a PTO',[Paragraph(text0,bt)],t0,h0) + fbreak() + c1 = Table([('alignment', 'align\012alignment'), + ('bulletColor', 'bulletcolor\012bcolor'), + ('bulletFontName', 'bfont\012bulletfontname'), + ('bulletFontSize', 'bfontsize\012bulletfontsize'), + ('bulletIndent', 'bindent\012bulletindent'), + ('firstLineIndent', 'findent\012firstlineindent'), + ('fontName', 'face\012fontname\012font'), + ('fontSize', 'size\012fontsize'), + ('leading', 'leading'), + ('leftIndent', 'leftindent\012lindent'), + ('rightIndent', 'rightindent\012rindent'), + ('spaceAfter', 'spaceafter\012spacea'), + ('spaceBefore', 'spacebefore\012spaceb'), + ('textColor', 'fg\012textcolor\012color')], + style = [ + ('VALIGN',(0,0),(-1,-1),'TOP'), + ('INNERGRID', (0,0), (-1,-1), 0.25, black), + ('BOX', (0,0), (-1,-1), 0.25, black), + ], + ) + ptoblob('PTO with a table inside',c1,t0,h0) + fbreak() + ptoblob('A long PTO',[Paragraph(text0+' '+text1,bt)],t0,h0) + fbreak() + ptoblob('2 PTO (inner split)',[ColorParagraph('pink',text0,bt),PTOContainer([ColorParagraph(black,'Inner Starts',H1),ColorParagraph('yellow',text2,bt),ColorParagraph('black','Inner Ends',H1)],t1,h1),ColorParagraph('magenta',text1,bt)],t0,h0) + _showDoc('test_platypus_pto.pdf',story) + +def _KeepInFrameTestCase(self,mode,offset=12): + story = [] + def fbreak(story=story): + story.append(FrameBreak()) + styleSheet = getSampleStyleSheet() + H1 = styleSheet['Heading1'] + H1.pageBreakBefore = 0 + H1.keepWithNext = 0 + bt = styleSheet['BodyText'] + story.append(KeepInFrame(170-offset,284-offset,[Paragraph(text0,bt)],mode=mode)) + fbreak() + story.append(KeepInFrame(170-offset,284-offset,[Paragraph(text0,bt),Paragraph(text1,bt)],mode=mode)) + fbreak() + story.append(KeepInFrame(170-offset,284-offset,[Paragraph(text0,bt),Paragraph(text1,bt),Paragraph(text2,bt)],mode=mode)) + _showDoc('test_platypus_KeepInFrame%s.pdf'%mode,story) + +class TestCases(unittest.TestCase): + "Test multi-page splitting of paragraphs (eyeball-test)." + def test0(self): + _ptoTestCase(self) + def test1(self): + _KeepInFrameTestCase(self,mode="shrink") + def test2(self): + _KeepInFrameTestCase(self,mode="overflow") + def test3(self): + _KeepInFrameTestCase(self,mode="truncate") + def test4(self): + from reportlab.platypus.doctemplate import LayoutError + self.assertRaises(LayoutError, _KeepInFrameTestCase,*(self,"error")) + def test5(self): + from reportlab.platypus.doctemplate import LayoutError + self.assertRaises(LayoutError, _KeepInFrameTestCase,*(self,"shrink",0)) + +def makeSuite(): + return makeSuiteForClasses(TestCases) + +#noruntests +if __name__ == "__main__": #NORUNTESTS + if 'debug' in sys.argv: + _KeepInFrameTestCase(None) + else: + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_tables.py b/bin/reportlab/test/test_platypus_tables.py new file mode 100644 index 00000000000..705d1f9a396 --- /dev/null +++ b/bin/reportlab/test/test_platypus_tables.py @@ -0,0 +1,717 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_tables.py +__version__=''' $Id: test_platypus_tables.py 2848 2006-05-04 23:45:29Z andy $ ''' +__doc__='Test script for reportlab.tables' + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +from reportlab.platypus import Spacer, SimpleDocTemplate, Table, TableStyle +from reportlab.lib.units import inch, cm +from reportlab.lib import colors + + +def getTable(): + t = Table((('','North','South','East','West'), + ('Quarter 1',100,200,300,400), + ('Quarter 2',100,400,600,800), + ('Total',300,600,900,'1,200')), + (72,36,36,36,36), + (24, 16,16,18) + ) + return t + + +def makeStyles(): + styles = [] + for i in range(5): + styles.append(TableStyle([('ALIGN', (1,1), (-1,-1), 'RIGHT'), + ('ALIGN', (0,0), (-1,0), 'CENTRE'), + ('HREF', (0,0), (0,0), 'www.google.com'), + ])) + for style in styles[1:]: + style.add('GRID', (0,0), (-1,-1), 0.25, colors.black) + for style in styles[2:]: + style.add('LINEBELOW', (0,0), (-1,0), 2, colors.black) + for style in styles[3:]: + style.add('LINEABOVE', (0, -1), (-1,-1), 2, colors.black) + styles[-1].add('LINEBELOW',(1,-1), (-1, -1), 2, (0.5, 0.5, 0.5)) + return styles + + +def run(): + doc = SimpleDocTemplate(outputfile('test_platypus_tables.pdf'), pagesize=(8.5*inch, 11*inch), showBoundary=1) + styles = makeStyles() + lst = [] + for style in styles: + t = getTable() + t.setStyle(style) +## print '--------------' +## for rowstyle in t._cellstyles: +## for s in rowstyle: +## print s.alignment + lst.append(t) + lst.append(Spacer(0,12)) + doc.build(lst) + +def old_tables_test(): + from reportlab.lib.units import inch, cm + from reportlab.platypus.flowables import Image, PageBreak, Spacer, XBox + from reportlab.platypus.paragraph import Paragraph + from reportlab.platypus.xpreformatted import XPreformatted + from reportlab.platypus.flowables import Preformatted + from reportlab.platypus.doctemplate import SimpleDocTemplate + from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle + from reportlab.platypus.tables import GRID_STYLE, BOX_STYLE, LABELED_GRID_STYLE, COLORED_GRID_STYLE, LIST_STYLE, LongTable + rowheights = (24, 16, 16, 16, 16) + rowheights2 = (24, 16, 16, 16, 30) + colwidths = (50, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32) + data = ( + ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13), + ('Hats', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843') + ) + data2 = ( + ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13), + ('Hats\nLarge', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843') + ) + styleSheet = getSampleStyleSheet() + lst = [] + lst.append(Paragraph("Tables", styleSheet['Heading1'])) + lst.append(Paragraph(__doc__, styleSheet['BodyText'])) + lst.append(Paragraph("The Tables (shown in different styles below) were created using the following code:", styleSheet['BodyText'])) + lst.append(Preformatted(""" + colwidths = (50, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32) + rowheights = (24, 16, 16, 16, 16) + data = ( + ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13), + ('Hats', 893, 912, '1,212', 643, 789, 159, + 888, '1,298', 832, 453, '1,344','2,843') + ) + t = Table(data, colwidths, rowheights) + """, styleSheet['Code'], dedent=4)) + lst.append(Paragraph(""" + You can then give the Table a TableStyle object to control its format. The first TableStyle used was + created as follows: + """, styleSheet['BodyText'])) + lst.append(Preformatted(""" +GRID_STYLE = TableStyle( + [('GRID', (0,0), (-1,-1), 0.25, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + """, styleSheet['Code'])) + lst.append(Paragraph(""" + TableStyles are created by passing in a list of commands. There are two types of commands - line commands + and cell formatting commands. In all cases, the first three elements of a command are the command name, + the starting cell and the ending cell. + """, styleSheet['BodyText'])) + lst.append(Paragraph(""" + Line commands always follow this with the weight and color of the desired lines. Colors can be names, + or they can be specified as a (R,G,B) tuple, where R, G and B are floats and (0,0,0) is black. The line + command names are: GRID, BOX, OUTLINE, INNERGRID, LINEBELOW, LINEABOVE, LINEBEFORE + and LINEAFTER. BOX and OUTLINE are equivalent, and GRID is the equivalent of applying both BOX and + INNERGRID. + """, styleSheet['BodyText'])) + lst.append(Paragraph(""" + Cell formatting commands are: + """, styleSheet['BodyText'])) + lst.append(Paragraph(""" + FONT - takes fontname, fontsize and (optional) leading. + """, styleSheet['Definition'])) + lst.append(Paragraph(""" + TEXTCOLOR - takes a color name or (R,G,B) tuple. + """, styleSheet['Definition'])) + lst.append(Paragraph(""" + ALIGNMENT (or ALIGN) - takes one of LEFT, RIGHT, CENTRE (or CENTER) or DECIMAL. + """, styleSheet['Definition'])) + lst.append(Paragraph(""" + LEFTPADDING - defaults to 6. + """, styleSheet['Definition'])) + lst.append(Paragraph(""" + RIGHTPADDING - defaults to 6. + """, styleSheet['Definition'])) + lst.append(Paragraph(""" + BOTTOMPADDING - defaults to 3. + """, styleSheet['Definition'])) + lst.append(Paragraph(""" + A tablestyle is applied to a table by calling Table.setStyle(tablestyle). + """, styleSheet['BodyText'])) + t = Table(data, colwidths, rowheights) + t.setStyle(GRID_STYLE) + lst.append(PageBreak()) + lst.append(Paragraph("This is GRID_STYLE\n", styleSheet['BodyText'])) + lst.append(t) + + t = Table(data, colwidths, rowheights) + t.setStyle(BOX_STYLE) + lst.append(Paragraph("This is BOX_STYLE\n", styleSheet['BodyText'])) + lst.append(t) + lst.append(Paragraph(""" + It was created as follows: + """, styleSheet['BodyText'])) + lst.append(Preformatted(""" +BOX_STYLE = TableStyle( + [('BOX', (0,0), (-1,-1), 0.50, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + """, styleSheet['Code'])) + + t = Table(data, colwidths, rowheights) + t.setStyle(LABELED_GRID_STYLE) + lst.append(Paragraph("This is LABELED_GRID_STYLE\n", styleSheet['BodyText'])) + lst.append(t) + t = Table(data2, colwidths, rowheights2) + t.setStyle(LABELED_GRID_STYLE) + lst.append(Paragraph("This is LABELED_GRID_STYLE ILLUSTRATES EXPLICIT LINE SPLITTING WITH NEWLINE (different heights and data)\n", styleSheet['BodyText'])) + lst.append(t) + lst.append(Paragraph(""" + It was created as follows: + """, styleSheet['BodyText'])) + lst.append(Preformatted(""" +LABELED_GRID_STYLE = TableStyle( + [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 2, colors.black), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEAFTER', (0,0), (0,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + """, styleSheet['Code'])) + lst.append(PageBreak()) + + t = Table(data, colwidths, rowheights) + t.setStyle(COLORED_GRID_STYLE) + lst.append(Paragraph("This is COLORED_GRID_STYLE\n", styleSheet['BodyText'])) + lst.append(t) + lst.append(Paragraph(""" + It was created as follows: + """, styleSheet['BodyText'])) + lst.append(Preformatted(""" +COLORED_GRID_STYLE = TableStyle( + [('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 2, colors.red), + ('LINEBELOW', (0,0), (-1,0), 2, colors.black), + ('LINEAFTER', (0,0), (0,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + """, styleSheet['Code'])) + + t = Table(data, colwidths, rowheights) + t.setStyle(LIST_STYLE) + lst.append(Paragraph("This is LIST_STYLE\n", styleSheet['BodyText'])) + lst.append(t) + lst.append(Paragraph(""" + It was created as follows: + """, styleSheet['BodyText'])) + lst.append(Preformatted(""" +LIST_STYLE = TableStyle( + [('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + """, styleSheet['Code'])) + + t = Table(data, colwidths, rowheights) + ts = TableStyle( + [('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 3, colors.green,'butt'), + ('LINEBELOW', (0,-1), (-1,-1), 1, colors.white,'butt'), + ('ALIGN', (1,1), (-1,-1), 'RIGHT'), + ('TEXTCOLOR', (0,1), (0,-1), colors.red), + ('BACKGROUND', (0,0), (-1,0), colors.Color(0,0.7,0.7))] + ) + t.setStyle(ts) + lst.append(Paragraph("This is a custom style\n", styleSheet['BodyText'])) + lst.append(t) + lst.append(Paragraph(""" + It was created as follows: + """, styleSheet['BodyText'])) + lst.append(Preformatted(""" + ts = TableStyle( + [('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 3, colors.green,'butt'), + ('LINEBELOW', (0,-1), (-1,-1), 1, colors.white,'butt'), + ('ALIGN', (1,1), (-1,-1), 'RIGHT'), + ('TEXTCOLOR', (0,1), (0,-1), colors.red), + ('BACKGROUND', (0,0), (-1,0), colors.Color(0,0.7,0.7))] + ) + """, styleSheet['Code'])) + data = ( + ('', 'Jan\nCold', 'Feb\n', 'Mar\n','Apr\n','May\n', 'Jun\nHot', 'Jul\n', 'Aug\nThunder', 'Sep\n', 'Oct\n', 'Nov\n', 'Dec\n'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13), + ('Hats', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843') + ) + c = list(colwidths) + c[0] = None + c[8] = None + t = Table(data, c, [None]+list(rowheights[1:])) + t.setStyle(LIST_STYLE) + lst.append(Paragraph(""" + This is a LIST_STYLE table with the first rowheight set to None ie automatic. + The top row cells are split at a newline '\\n' character. The first and August + column widths were also set to None. + """, styleSheet['BodyText'])) + lst.append(t) + + lst.append(Paragraph(""" + This demonstrates a number of features useful in financial statements. The first is decimal alignment; + with ALIGN=DECIMAL the numbers align on the points; and the points are aligned based on + the RIGHTPADDING, which is usually 3 points so you should set it higher. The second is multiple lines; + one can specify double or triple lines and control the separation if desired. Finally, the coloured + negative numbers were (we regret to say) done in the style; we don't have a way to conditionally + format numbers based on value yet. + """, styleSheet['BodyText'])) + + + t = Table([[u'Corporate Assets','Amount'], + ['Fixed Assets','1,234,567.89'], + ['Company Vehicle','1,234.8901'], + ['Petty Cash','42'], + [u'Intellectual Property\u00ae','(42,078,231.56)'], + ['Overdraft','(12,345)'], + ['Boardroom Flat Screen','60 inches'], + ['Net Position','Deep Sh*t.Really'] + ], + [144,72]) + + ts = TableStyle( + [#first the top row + ('ALIGN', (1,1), (-1,-1), 'CENTER'), + ('LINEABOVE', (0,0), (-1,0), 1, colors.purple), + ('LINEBELOW', (0,0), (-1,0), 1, colors.purple), + ('FONT', (0,0), (-1,0), 'Times-Bold'), + + #bottom row has a line above, and two lines below + ('LINEABOVE', (0,-1), (-1,-1), 1, colors.purple), #last 2 are count, sep + ('LINEBELOW', (0,-1), (-1,-1), 0.5, colors.purple, 1, None, None, 4,1), + ('LINEBELOW', (0,-1), (-1,-1), 1, colors.red), + ('FONT', (0,-1), (-1,-1), 'Times-Bold'), + + #numbers column + ('ALIGN', (1,1), (-1,-1), 'DECIMAL'), + ('RIGHTPADDING', (1,1), (-1,-1), 36), + ('TEXTCOLOR', (1,4), (1,4), colors.red), + + #red cell + ] + ) + + t.setStyle(ts) + lst.append(t) + lst.append(Spacer(36,36)) + lst.append(Paragraph(""" + The red numbers should be aligned LEFT & BOTTOM, the blue RIGHT & TOP + and the green CENTER & MIDDLE. + """, styleSheet['BodyText'])) + XY = [['X00y', 'X01y', 'X02y', 'X03y', 'X04y'], + ['X10y', 'X11y', 'X12y', 'X13y', 'X14y'], + ['X20y', 'X21y', 'X22y', 'X23y', 'X24y'], + ['X30y', 'X31y', 'X32y', 'X33y', 'X34y']] + t=Table(XY, 5*[0.6*inch], 4*[0.6*inch]) + t.setStyle([('ALIGN',(1,1),(-2,-2),'LEFT'), + ('TEXTCOLOR',(1,1),(-2,-2),colors.red), + + ('VALIGN',(0,0),(1,-1),'TOP'), + ('ALIGN',(0,0),(1,-1),'RIGHT'), + ('TEXTCOLOR',(0,0),(1,-1),colors.blue), + + ('ALIGN',(0,-1),(-1,-1),'CENTER'), + ('VALIGN',(0,-1),(-1,-1),'MIDDLE'), + ('TEXTCOLOR',(0,-1),(-1,-1),colors.green), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ]) + lst.append(t) + data = [('alignment', 'align\012alignment'), + ('bulletColor', 'bulletcolor\012bcolor'), + ('bulletFontName', 'bfont\012bulletfontname'), + ('bulletFontSize', 'bfontsize\012bulletfontsize'), + ('bulletIndent', 'bindent\012bulletindent'), + ('firstLineIndent', 'findent\012firstlineindent'), + ('fontName', 'face\012fontname\012font'), + ('fontSize', 'size\012fontsize'), + ('leading', 'leading'), + ('leftIndent', 'leftindent\012lindent'), + ('rightIndent', 'rightindent\012rindent'), + ('spaceAfter', 'spaceafter\012spacea'), + ('spaceBefore', 'spacebefore\012spaceb'), + ('textColor', 'fg\012textcolor\012color')] + t = Table(data) + t.setStyle([ + ('VALIGN',(0,0),(-1,-1),'TOP'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ]) + lst.append(t) + t = Table([ ('Attribute', 'Synonyms'), + ('alignment', 'align, alignment'), + ('bulletColor', 'bulletcolor, bcolor'), + ('bulletFontName', 'bfont, bulletfontname'), + ('bulletFontSize', 'bfontsize, bulletfontsize'), + ('bulletIndent', 'bindent, bulletindent'), + ('firstLineIndent', 'findent, firstlineindent'), + ('fontName', 'face, fontname, font'), + ('fontSize', 'size, fontsize'), + ('leading', 'leading'), + ('leftIndent', 'leftindent, lindent'), + ('rightIndent', 'rightindent, rindent'), + ('spaceAfter', 'spaceafter, spacea'), + ('spaceBefore', 'spacebefore, spaceb'), + ('textColor', 'fg, textcolor, color')]) + t.repeatRows = 1 + t.setStyle([ + ('FONT',(0,0),(-1,1),'Times-Bold',10,12), + ('FONT',(0,1),(-1,-1),'Courier',8,8), + ('VALIGN',(0,0),(-1,-1),'MIDDLE'), + ('INNERGRID', (0,0), (-1,-1), 0.25, colors.black), + ('BOX', (0,0), (-1,-1), 0.25, colors.black), + ('BACKGROUND', (0, 0), (-1, 0), colors.green), + ('BACKGROUND', (0, 1), (-1, -1), colors.pink), + ('ALIGN', (0, 0), (-1, 0), 'CENTER'), + ('ALIGN', (0, 1), (0, -1), 'LEFT'), + ('ALIGN', (-1, 1), (-1, -1), 'RIGHT'), + ('FONT', (0, 0), (-1, 0), 'Times-Bold', 12), + ('ALIGN', (1, 1), (1, -1), 'CENTER'), + ]) + lst.append(t) + lst.append(Table(XY, + style=[ ('FONT',(0,0),(-1,-1),'Times-Roman', 5,6), + ('GRID', (0,0), (-1,-1), 0.25, colors.blue),])) + lst.append(Table(XY, + style=[ ('FONT',(0,0),(-1,-1),'Times-Roman', 10,12), + ('GRID', (0,0), (-1,-1), 0.25, colors.black),])) + lst.append(Table(XY, + style=[ ('FONT',(0,0),(-1,-1),'Times-Roman', 20,24), + ('GRID', (0,0), (-1,-1), 0.25, colors.red),])) + lst.append(PageBreak()) + data= [['00', '01', '02', '03', '04'], + ['10', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] + t=Table(data,style=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('BOX',(0,0),(-1,-1),2,colors.black), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ('BACKGROUND', (0, 0), (0, 1), colors.pink), + ('BACKGROUND', (1, 1), (1, 2), colors.lavender), + ('BACKGROUND', (2, 2), (2, 3), colors.orange), + ]) + lst.append(Paragraph("Illustrating splits: nosplit", styleSheet['BodyText'])) + lst.append(t) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits: split(4in,30)", styleSheet['BodyText'])) + for s in t.split(4*inch,30): + lst.append(s) + lst.append(Spacer(0,6)) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits: split(4in,36)", styleSheet['BodyText'])) + for s in t.split(4*inch,36): + lst.append(s) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits: split(4in,56)", styleSheet['BodyText'])) + lst.append(Spacer(0,6)) + for s in t.split(4*inch,56): + lst.append(s) + lst.append(Spacer(0,6)) + + lst.append(PageBreak()) + data= [['00', '01', '02', '03', '04'], + ['', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '', '33', '34']] + sty=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('BOX',(0,0),(-1,-1),2,colors.black), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ('BACKGROUND', (0, 0), (0, 1), colors.pink), + ('SPAN',(0,0),(0,1)), + ('BACKGROUND', (2, 2), (2, 3), colors.orange), + ('SPAN',(2,2),(2,3)), + ] + t=Table(data,style=sty) + lst.append(Paragraph("Illustrating splits with spans: nosplit", styleSheet['BodyText'])) + lst.append(t) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits with spans: split(4in,30)", styleSheet['BodyText'])) + for s in t.split(4*inch,30): + lst.append(s) + lst.append(Spacer(0,6)) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits with spans: split(4in,36)", styleSheet['BodyText'])) + for s in t.split(4*inch,36): + lst.append(s) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits with spans: split(4in,56)", styleSheet['BodyText'])) + lst.append(Spacer(0,6)) + for s in t.split(4*inch,56): + lst.append(s) + lst.append(Spacer(0,6)) + + data= [['00', '01', '02', '03', '04'], + ['', '11', '12', '13', ''], + ['20', '21', '22', '23', '24'], + ['30', '31', '', '33', ''], + ['40', '41', '', '43', '44']] + sty=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('BOX',(0,0),(-1,-1),2,colors.black), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ('BACKGROUND', (0, 0), (0, 1), colors.pink), + ('SPAN',(0,0),(0,1)), + ('BACKGROUND',(-2,1),(-1,1),colors.palegreen), + ('SPAN',(-2,1),(-1,1)), + ('BACKGROUND',(-2,3),(-1,3),colors.yellow), + ('SPAN',(-2,3),(-1,3)), + ('BACKGROUND', (2, 3), (2, 4), colors.orange), + ('SPAN',(2,3),(2,4)), + ] + + t=Table(data,style=sty,repeatRows=2) + lst.append(Paragraph("Illustrating splits with spans and repeatRows: nosplit", styleSheet['BodyText'])) + lst.append(t) + lst.append(Spacer(0,6)) + if 0: + lst.append(Paragraph("Illustrating splits with spans and repeatRows: split(4in,30)", styleSheet['BodyText'])) + for s in t.split(4*inch,30): + lst.append(s) + lst.append(Spacer(0,6)) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits with spans and repeatRows: split(4in,36)", styleSheet['BodyText'])) + for s in t.split(4*inch,36): + lst.append(s) + lst.append(Spacer(0,6)) + lst.append(Paragraph("Illustrating splits with spans and repeatRows: split(4in,56)", styleSheet['BodyText'])) + lst.append(Spacer(0,6)) + t=Table(data,style=sty,repeatRows=2) + for s in t.split(4*inch,56): + lst.append(s) + lst.append(Spacer(0,6)) + + lst.append(PageBreak()) + import os, reportlab.platypus + I = Image(os.path.join(os.path.dirname(reportlab.platypus.__file__),'..','tools','pythonpoint','demos','leftlogo.gif')) + I.drawHeight = 1.25*inch*I.drawHeight / I.drawWidth + I.drawWidth = 1.25*inch + #I.drawWidth = 9.25*inch #uncomment to see better messaging + P = Paragraph("The ReportLab Left Logo Image", styleSheet["BodyText"]) + data= [['A', 'B', 'C', Paragraph("A paragraph1",styleSheet["BodyText"]), 'D'], + ['00', '01', '02', [I,P], '04'], + ['10', '11', '12', [I,P], '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] + + t=Table(data,style=[('GRID',(1,1),(-2,-2),1,colors.green), + ('BOX',(0,0),(1,-1),2,colors.red), + ('LINEABOVE',(1,2),(-2,2),1,colors.blue), + ('LINEBEFORE',(2,1),(2,-2),1,colors.pink), + ('BACKGROUND', (0, 0), (0, 1), colors.pink), + ('BACKGROUND', (1, 1), (1, 2), colors.lavender), + ('BACKGROUND', (2, 2), (2, 3), colors.orange), + ('BOX',(0,0),(-1,-1),2,colors.black), + ('GRID',(0,0),(-1,-1),0.5,colors.black), + ('VALIGN',(3,0),(3,0),'BOTTOM'), + ('BACKGROUND',(3,0),(3,0),colors.limegreen), + ('BACKGROUND',(3,1),(3,1),colors.khaki), + ('ALIGN',(3,1),(3,1),'CENTER'), + ('BACKGROUND',(3,2),(3,2),colors.beige), + ('ALIGN',(3,2),(3,2),'LEFT'), + ]) + + t._argW[3]=1.5*inch + lst.append(t) + + # now for an attempt at column spanning. + lst.append(PageBreak()) + data= [['A', 'BBBBB', 'C', 'D', 'E'], + ['00', '01', '02', '03', '04'], + ['10', '11', '12', '13', '14'], + ['20', '21', '22', '23', '24'], + ['30', '31', '32', '33', '34']] + sty = [ + ('ALIGN',(0,0),(-1,-1),'CENTER'), + ('VALIGN',(0,0),(-1,-1),'TOP'), + ('GRID',(0,0),(-1,-1),1,colors.green), + ('BOX',(0,0),(-1,-1),2,colors.red), + + #span 'BBBB' across middle 3 cells in top row + ('SPAN',(1,0),(3,0)), + #now color the first cell in this range only, + #i.e. the one we want to have spanned. Hopefuly + #the range of 3 will come out khaki. + ('BACKGROUND',(1,0),(1,0),colors.khaki), + + ('SPAN',(0,2),(-1,2)), + + + #span 'AAA'down entire left column + ('SPAN',(0,0), (0, 1)), + ('BACKGROUND',(0,0),(0,0),colors.cyan), + ('LINEBELOW', (0,'splitlast'), (-1,'splitlast'), 1, colors.white,'butt'), + ] + t=Table(data,style=sty, colWidths = [20] * 5, rowHeights = [20]*5) + lst.append(t) + + # now for an attempt at percentage widths + lst.append(Spacer(18,18)) + lst.append(Paragraph("This table has colWidths=5*['14%']!", styleSheet['BodyText'])) + t=Table(data,style=sty, colWidths = ['14%'] * 5, rowHeights = [20]*5) + lst.append(t) + + lst.append(Spacer(18,18)) + lst.append(Paragraph("This table has colWidths=['14%','10%','19%','22%','*']!", styleSheet['BodyText'])) + t=Table(data,style=sty, colWidths = ['14%','10%','19%','22%','*'], rowHeights = [20]*5) + lst.append(t) + + # Mike's test example + lst.append(Spacer(18,18)) + lst.append(Paragraph('Mike\'s Spanning Example', styleSheet['Heading1'])) + data= [[Paragraph('World Domination: The First Five Years', styleSheet['BodyText']), ''], + [Paragraph('World Domination: The First Five Years', styleSheet['BodyText']),''], + [Paragraph('World Domination: The First Five Years', styleSheet['BodyText']), ''], + ] + t=Table(data, style=[('SPAN',(0,0),(1,0)),('SPAN',(0,1),(1,1)),('SPAN',(0,2),(1,2)),], colWidths = [3*cm,8*cm], rowHeights = [None]*3) + lst.append(t) + + lst.append(Spacer(18,18)) + lst.append(Paragraph('Mike\'s Non-spanning Example', styleSheet['Heading1'])) + data= [[Paragraph('World Domination: The First Five Years', styleSheet['BodyText'])], + [Paragraph('World Domination: The First Five Years', styleSheet['BodyText'])], + [Paragraph('World Domination: The First Five Years', styleSheet['BodyText'])], + ] + t=Table(data, style=[], colWidths = [11*cm], rowHeights = [None]*3) + lst.append(t) + + lst.append(Spacer(18,18)) + lst.append(Paragraph('xpre example', styleSheet['Heading1'])) + data= [ [ + XPreformatted('Account Details', styleSheet['Heading3']), + '', XPreformatted('Client Details', styleSheet['Heading3']), + ], #end of row 0 + ] + t=Table(data, style=[], colWidths = [80,230.0,80], rowHeights = [None]*1) + lst.append(t) + + lst.append(PageBreak()) + + lst.append(Paragraph('Trying colour cycling in background', styleSheet['Heading1'])) + lst.append(Paragraph("This should alternate pale blue and uncolored by row", styleSheet['BodyText'])) + data= [['001', '01', '02', '03', '04', '05'], + ['002', '01', '02', '03', '04', '05'], + ['003', '01', '02', '03', '04', '05'], + ['004', '01', '02', '03', '04', '05'], + ['005', '01', '02', '03', '04', '05'], + ['006', '01', '02', '03', '04', '05'], + ['007', '01', '02', '03', '04', '05'], + ['008', '01', '02', '03', '04', '05'], + ['009', '01', '02', '03', '04', '05'], + ['010', '01', '02', '03', '04', '05'], + + ] + t=Table(data,style=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('ROWBACKGROUNDS', (0, 0), (-1, -1), (0xD0D0FF, None)), + ]) + lst.append(t) + lst.append(Spacer(0,6)) + lst.append(Paragraph("And this should pale blue, pale pink and None by column", styleSheet['BodyText'])) + data= [['001', '01', '02', '03', '04', '05'], + ['002', '01', '02', '03', '04', '05'], + ['003', '01', '02', '03', '04', '05'], + ['004', '01', '02', '03', '04', '05'], + ['005', '01', '02', '03', '04', '05'], + ['006', '01', '02', '03', '04', '05'], + ['007', '01', '02', '03', '04', '05'], + ['008', '01', '02', '03', '04', '05'], + ['009', '01', '02', '03', '04', '05'], + ['010', '01', '02', '03', '04', '05'], + + ] + t=Table(data,style=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('COLBACKGROUNDS', (0, 0), (-1, -1), (0xD0D0FF, 0xFFD0D0, None)), + ]) + lst.append(t) + + lst.append(PageBreak()) + lst.append(Paragraph("This spanning example illustrates automatic removal of grids and lines in spanned cells!", styleSheet['BodyText'])) + lst.append(Spacer(0,6)) + data= [['Top\nLeft', '', '02', '03', '04', '05', '06', '07'], + ['', '', '12', 'Span (3,1) (6,2)', '','','','17'], + ['20', '21', '22', '', '','','','27'], + ['30', '31', '32', '33', '34','35','36','37'], + ['40', 'In The\nMiddle', '', '', '44','45','46','47'], + ['50', '', '', '', '54','55','56','57'], + ['60', '', '', '','64', '65', 'Bottom\nRight', ''], + ['70', '71', '72', '73','74', '75', '', '']] + t=Table(data,style=[ + ('GRID',(0,0),(-1,-1),0.5,colors.grey), + ('BACKGROUND',(0,0),(1,1),colors.palegreen), + ('SPAN',(0,0),(1,1)), + ('BACKGROUND',(-2,-2),(-1,-1), colors.pink), + ('SPAN',(-2,-2),(-1,-1)), + ('SPAN',(1,4),(3,6)), + ('BACKGROUND',(1,4),(3,6), colors.lightblue), + ('SPAN',(3,1),(6,2)), + ('BACKGROUND',(3,1),(6,2), colors.peachpuff), + ('VALIGN',(3,1),(6,2),'TOP'), + ('LINEABOVE', (0,2),(-1,2), 1, colors.black, 0, None, None, 2, 2), + ('LINEBEFORE', (3,0),(3,-1), 1, colors.black, 0, None, None, 2, 2), + ]) + lst.append(t) + + lst.append(PageBreak()) + + lst.append(Paragraph("und jetzt noch eine Tabelle mit 5000 Zeilen:", styleSheet['BodyText'])) + sty = [ ('GRID',(0,0),(-1,-1),1,colors.green), + ('BOX',(0,0),(-1,-1),2,colors.red), + ] + data = [[str(i), Paragraph("xx "* (i%10), styleSheet["BodyText"]), Paragraph("blah "*(i%40), styleSheet["BodyText"])] for i in xrange(500)] + t=LongTable(data, style=sty, colWidths = [50,100,200]) + lst.append(t) + + SimpleDocTemplate(outputfile('tables.pdf'), showBoundary=1).build(lst) + + +class TablesTestCase(unittest.TestCase): + "Make documents with tables" + + def test0(self): + "Make a document full of tables" + run() + + def test1(self): + "Make a document full of tables" + old_tables_test() + + +def makeSuite(): + return makeSuiteForClasses(TablesTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_toc.py b/bin/reportlab/test/test_platypus_toc.py new file mode 100644 index 00000000000..7edb25a2b16 --- /dev/null +++ b/bin/reportlab/test/test_platypus_toc.py @@ -0,0 +1,183 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_toc.py +"""Tests for the Platypus TableOfContents class. + +Currently there is only one such test. Most such tests, like this +one, will be generating a PDF document that needs to be eye-balled +in order to find out if it is 'correct'. +""" + + +import sys, os +from os.path import join, basename, splitext +from math import sqrt + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.lib.units import inch, cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.xpreformatted import XPreformatted +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate \ + import PageTemplate, BaseDocTemplate +from reportlab.platypus import tableofcontents +from reportlab.platypus.tableofcontents import TableOfContents +from reportlab.platypus.tables import TableStyle, Table +from reportlab.lib import randomtext + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + canvas.rect(2.5*cm, 2.5*cm, 15*cm, 25*cm) + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + "The document template used for all PDF documents." + + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template = PageTemplate('normal', [frame1], myMainPageFrame) + self.addPageTemplates(template) + + + def afterFlowable(self, flowable): + "Registers TOC entries and makes outline entries." + + if flowable.__class__.__name__ == 'Paragraph': + styleName = flowable.style.name + if styleName[:7] == 'Heading': + # Register TOC entries. + level = int(styleName[7:]) + text = flowable.getPlainText() + pageNum = self.page + self.notify('TOCEntry', (level, text, pageNum)) + + # Add PDF outline entries (not really needed/tested here). + key = str(hash(flowable)) + c = self.canv + c.bookmarkPage(key) + c.addOutlineEntry(text, key, level=level, closed=0) + + +def makeHeaderStyle(level, fontName='Times-Roman'): + "Make a header style for different levels." + + assert level >= 0, "Level must be >= 0." + + PS = ParagraphStyle + size = 24.0 / sqrt(1+level) + style = PS(name = 'Heading' + str(level), + fontName = fontName, + fontSize = size, + leading = size*1.2, + spaceBefore = size/4.0, + spaceAfter = size/8.0) + + return style + + +def makeBodyStyle(): + "Body text style - the default will do" + return ParagraphStyle('body') + + +def makeTocHeaderStyle(level, delta, epsilon, fontName='Times-Roman'): + "Make a header style for different levels." + + assert level >= 0, "Level must be >= 0." + + PS = ParagraphStyle + size = 12 + style = PS(name = 'Heading' + str(level), + fontName = fontName, + fontSize = size, + leading = size*1.2, + spaceBefore = size/4.0, + spaceAfter = size/8.0, + firstLineIndent = -epsilon, + leftIndent = level*delta + epsilon) + + return style + + +class TocTestCase(unittest.TestCase): + "Test TableOfContents class (eyeball-test)." + + def test0(self): + """Test story with TOC and a cascaded header hierarchy. + + The story should contain exactly one table of contents that is + immediatly followed by a list of of cascaded levels of header + lines, each nested one level deeper than the previous one. + + Features to be visually confirmed by a human being are: + + 1. TOC lines are indented in multiples of 1 cm. + 2. Wrapped TOC lines continue with additional 0.5 cm indentation. + 3. ... + """ + + maxLevels = 12 + + # Create styles to be used for document headers + # on differnet levels. + headerLevelStyles = [] + for i in range(maxLevels): + headerLevelStyles.append(makeHeaderStyle(i)) + + # Create styles to be used for TOC entry lines + # for headers on differnet levels. + tocLevelStyles = [] + d, e = tableofcontents.delta, tableofcontents.epsilon + for i in range(maxLevels): + tocLevelStyles.append(makeTocHeaderStyle(i, d, e)) + + # Build story. + story = [] + styleSheet = getSampleStyleSheet() + bt = styleSheet['BodyText'] + + description = '%s' % self.test0.__doc__ + story.append(XPreformatted(description, bt)) + + toc = TableOfContents() + toc.levelStyles = tocLevelStyles + story.append(toc) + + for i in range(maxLevels): + story.append(Paragraph('HEADER, LEVEL %d' % i, + headerLevelStyles[i])) + #now put some body stuff in. + txt = randomtext.randomText(randomtext.PYTHON, 5) + para = Paragraph(txt, makeBodyStyle()) + story.append(para) + + path = outputfile('test_platypus_toc.pdf') + doc = MyDocTemplate(path) + doc.multiBuild(story) + + +def makeSuite(): + return makeSuiteForClasses(TocTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_platypus_xref.py b/bin/reportlab/test/test_platypus_xref.py new file mode 100644 index 00000000000..0b61139c20e --- /dev/null +++ b/bin/reportlab/test/test_platypus_xref.py @@ -0,0 +1,140 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_platypus_xref.py +"""Test long documents with indexes, tables and cross-references +""" + +import sys, os, time +from string import split, strip, join, whitespace, find +from operator import truth +from types import StringType, ListType + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus import Paragraph, Flowable, Frame, PageTemplate, BaseDocTemplate +from reportlab.platypus.frames import Frame +from reportlab.lib.randomtext import randomText, PYTHON +from reportlab.platypus.tableofcontents import TableOfContents, SimpleIndex + + +def myMainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + canvas.setFont('Times-Roman', 12) + pageNumber = canvas.getPageNumber() + canvas.drawString(10*cm, cm, str(pageNumber)) + canvas.restoreState() + + +class MyDocTemplate(BaseDocTemplate): + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 16*cm, 25*cm, id='Frame1') + self.allowSplitting = 0 + self.showBoundary = 1 + apply(BaseDocTemplate.__init__, (self, filename), kw) + template = PageTemplate('normal', [frame1], myMainPageFrame) + self.addPageTemplates(template) + + def afterFlowable(self, flowable): + "Registers TOC and Index entries and makes outline entries." + if flowable.__class__.__name__ == 'Paragraph': + styleName = flowable.style.name + if styleName == 'Heading1': + level = 0 + text = flowable.getPlainText() + pageNum = self.page + self.notify('TOCEntry', (level, text, pageNum)) + + # Add PDF outline entries (not really needed/tested here). + key = str(hash(flowable)) + c = self.canv + c.bookmarkPage(key) + c.addOutlineEntry(text, key, level=level, closed=0) + + # index a bunch of pythonic buzzwords. In real life this + # would be driven by markup. + try: + text = flowable.getPlainText() + except: + return + for phrase in ['uniform','depraved','finger', 'Fraudulin']: + if find(text, phrase) > -1: + self.notify('IndexEntry', (phrase, self.page)) + #print 'IndexEntry:',phrase, self.page + + +def _test0(self): + "This makes one long multi-page paragraph." + + # Build story. + story = [] + + styleSheet = getSampleStyleSheet() + h1 = styleSheet['Heading1'] + h1.pageBreakBefore = 1 + h1.keepWithNext = 1 + h1.outlineLevel = 0 + + h2 = styleSheet['Heading2'] + h2.backColor = colors.cyan + h2.keepWithNext = 1 + h2.outlineLevel = 1 + + bt = styleSheet['BodyText'] + + story.append(Paragraph("""Cross-Referencing Test""", styleSheet["Title"])) + story.append(Paragraph(""" + Subsequent pages test cross-references: indexes, tables and individual + cross references. The number in brackets at the end of each paragraph + is its position in the story. (%d)""" % len(story), bt)) + + story.append(Paragraph("""Table of Contents:""", styleSheet["Title"])) + toc = TableOfContents() + story.append(toc) + + chapterNum = 1 + for i in range(10): + story.append(Paragraph('Chapter %d: Chapters always starts a new page' % chapterNum, h1)) + chapterNum = chapterNum + 1 + for j in range(3): + story.append(Paragraph('Heading1 paragraphs should always' + 'have a page break before. Heading 2 on the other hand' + 'should always have a FRAME break before (%d)' % len(story), bt)) + story.append(Paragraph('Heading 2 should always be kept with the next thing (%d)' % len(story), h2)) + for j in range(3): + story.append(Paragraph(randomText(theme=PYTHON, sentences=2)+' (%d)' % len(story), bt)) + story.append(Paragraph('I should never be at the bottom of a frame (%d)' % len(story), h2)) + story.append(Paragraph(randomText(theme=PYTHON, sentences=1)+' (%d)' % len(story), bt)) + + story.append(Paragraph('The Index which goes at the back', h1)) + story.append(SimpleIndex()) + + doc = MyDocTemplate(outputfile('test_platypus_xref.pdf')) + doc.multiBuild(story) + + +class BreakingTestCase(unittest.TestCase): + "Test multi-page splitting of paragraphs (eyeball-test)." + def test0(self): + _test0(self) + + +def makeSuite(): + return makeSuiteForClasses(BreakingTestCase) + + +#noruntests +if __name__ == "__main__": + if 'debug' in sys.argv: + _test1(None) + else: + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_pyfiles.py b/bin/reportlab/test/test_pyfiles.py new file mode 100644 index 00000000000..8a7393ee7d0 --- /dev/null +++ b/bin/reportlab/test/test_pyfiles.py @@ -0,0 +1,158 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_pyfiles.py +"""Tests performed on all Python source files of the ReportLab distribution. +""" + + +import os, sys, string, fnmatch, re + +import reportlab +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, SecureTestCase, GlobDirectoryWalker, outputfile, printLocation +from reportlab.lib.utils import open_and_read, open_and_readlines + +RL_HOME = os.path.dirname(reportlab.__file__) + + +# Helper function and class. + +def unique(seq): + "Remove elements from a list that occur more than once." + + # Return input if it has less than 2 elements. + if len(seq) < 2: + return seq + + # Make a sorted copy of the input sequence. + seq2 = seq[:] + if type(seq2) == type(''): + seq2 = map(None, seq2) + seq2.sort() + + # Remove adjacent elements if they are identical. + i = 0 + while i < len(seq2)-1: + elem = seq2[i] + try: + while elem == seq2[i+1]: + del seq2[i+1] + except IndexError: + pass + i = i + 1 + + # Try to return something of the same type as the input. + if type(seq) == type(''): + return string.join(seq2, '') + else: + return seq2 + +class SelfTestCase(unittest.TestCase): + "Test unique() function." + + def testUnique(self): + "Test unique() function." + + cases = [([], []), + ([0], [0]), + ([0, 1, 2], [0, 1, 2]), + ([2, 1, 0], [0, 1, 2]), + ([0, 0, 1, 1, 2, 2, 3, 3], [0, 1, 2, 3]), + ('abcabcabc', 'abc') + ] + + msg = "Failed: unique(%s) returns %s instead of %s." + for sequence, expectedOutput in cases: + output = unique(sequence) + args = (sequence, output, expectedOutput) + assert output == expectedOutput, msg % args + + +class AsciiFileTestCase(unittest.TestCase): + "Test if Python files are pure ASCII ones." + + def testAscii(self): + "Test if Python files are pure ASCII ones." + + RL_HOME = os.path.dirname(reportlab.__file__) + allPyFiles = GlobDirectoryWalker(RL_HOME, '*.py') + + for path in allPyFiles: + fileContent = open_and_read(path,'r') + nonAscii = filter(lambda c: ord(c)>127, fileContent) + nonAscii = unique(nonAscii) + + truncPath = path[string.find(path, 'reportlab'):] + args = (truncPath, repr(map(ord, nonAscii))) + msg = "File %s contains characters: %s." % args +## if nonAscii: +## print msg + assert nonAscii == '', msg + + +class FilenameTestCase(unittest.TestCase): + "Test if Python files contain trailing digits." + + def testTrailingDigits(self): + "Test if Python files contain trailing digits." + + allPyFiles = GlobDirectoryWalker(RL_HOME, '*.py') + + for path in allPyFiles: + #hack - exclude barcode extensions from this test + if string.find(path, 'barcode'): + pass + else: + basename = os.path.splitext(path)[0] + truncPath = path[string.find(path, 'reportlab'):] + msg = "Filename %s contains trailing digits." % truncPath + assert basename[-1] not in string.digits, msg + + ## if basename[-1] in string.digits: + ## print truncPath + + +class FirstLineTestCase(SecureTestCase): + "Testing if objects in the ReportLab package have docstrings." + + def findSuspiciousModules(self, folder, rootName): + "Get all modul paths with non-Unix-like first line." + + firstLinePat = re.compile('^#!.*python.*') + + paths = [] + for file in GlobDirectoryWalker(folder, '*.py'): + if os.path.basename(file) == '__init__.py': + continue + firstLine = open_and_readlines(file)[0] + if not firstLinePat.match(firstLine): + paths.append(file) + + return paths + + def test1(self): + "Test if all Python files have a Unix-like first line." + + path = outputfile("test_firstline.log") + file = open(path, 'w') + file.write('No Unix-like first line found in the files below.\n\n') + + paths = self.findSuspiciousModules(RL_HOME, 'reportlab') + paths.sort() + + for p in paths: + file.write("%s\n" % p) + + file.close() + +def makeSuite(): + suite = makeSuiteForClasses(SelfTestCase, AsciiFileTestCase, FilenameTestCase) + if sys.platform[:4] != 'java': + loader = unittest.TestLoader() + suite.addTest(loader.loadTestsFromTestCase(FirstLineTestCase)) + return suite + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_renderSVG.py b/bin/reportlab/test/test_renderSVG.py new file mode 100755 index 00000000000..a2d5c1cd77e --- /dev/null +++ b/bin/reportlab/test/test_renderSVG.py @@ -0,0 +1,165 @@ +#!/usr/bin/env python + +import sys, string +from xml.dom import minidom +from xml.sax._exceptions import SAXReaderNotAvailable + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.graphics.shapes import * +from reportlab.graphics import renderSVG + + + + +def warnIgnoredRestofTest(): + "Raise a warning (if possible) about a not fully completed test." + + version = sys.version_info[:2] + msg = "XML parser not found - consider installing expat! Rest of test(s) ignored!" + if version >= (2, 1): + import warnings + warnings.warn(msg) + else: + # should better also be printed only once... + print msg + + + + +# Check if we have a default XML parser available or not. + +try: + import xml + from xml.sax import make_parser + p = xml.sax.make_parser() + HAVE_XML_PARSER = 1 +except SAXReaderNotAvailable: + HAVE_XML_PARSER = 0 + + + + +def load(path): + "Helper function to read the generated SVG again." + + doc = minidom.parse(path) + doc.normalize() + return doc.documentElement + + + + +class RenderSvgSimpleTestCase(unittest.TestCase): + "Testing renderSVG module." + + def test0(self): + "Test two strings in drawing." + + path = outputfile("test_renderSVG_simple_test0.svg") + + d = Drawing(200, 100) + d.add(String(0, 0, "foo")) + d.add(String(100, 0, "bar")) + renderSVG.drawToFile(d, path) + + if not HAVE_XML_PARSER: + warnIgnoredRestofTest() + return + + svg = load(path) + fg = svg.getElementsByTagName('g')[0] # flipping group + dg = fg.getElementsByTagName('g')[0] # diagram group + textChildren = dg.getElementsByTagName('text') # text nodes + t0 = string.strip(textChildren[0].childNodes[0].nodeValue) + t1 = string.strip(textChildren[1].childNodes[0].nodeValue) + assert t0 == 'foo' + assert t1 == 'bar' + + + def test1(self): + "Test two strings in group in drawing." + + path = outputfile("test_renderSVG_simple_test1.svg") + + d = Drawing(200, 100) + g = Group() + g.add(String(0, 0, "foo")) + g.add(String(100, 0, "bar")) + d.add(g) + renderSVG.drawToFile(d, path) + + if not HAVE_XML_PARSER: + warnIgnoredRestofTest() + return + + svg = load(path) + fg = svg.getElementsByTagName('g')[0] # flipping group + dg = fg.getElementsByTagName('g')[0] # diagram group + g = dg.getElementsByTagName('g')[0] # custom group + textChildren = g.getElementsByTagName('text') # text nodes + t0 = string.strip(textChildren[0].childNodes[0].nodeValue) + t1 = string.strip(textChildren[1].childNodes[0].nodeValue) + + assert t0 == 'foo' + assert t1 == 'bar' + + + def test2(self): + "Test two strings in transformed group in drawing." + + path = outputfile("test_renderSVG_simple_test2.svg") + + d = Drawing(200, 100) + g = Group() + g.add(String(0, 0, "foo")) + g.add(String(100, 0, "bar")) + g.scale(1.5, 1.2) + g.translate(50, 0) + d.add(g) + renderSVG.drawToFile(d, path) + + if not HAVE_XML_PARSER: + warnIgnoredRestofTest() + return + + svg = load(path) + fg = svg.getElementsByTagName('g')[0] # flipping group + dg = fg.getElementsByTagName('g')[0] # diagram group + g = dg.getElementsByTagName('g')[0] # custom group + textChildren = g.getElementsByTagName('text') # text nodes + t0 = string.strip(textChildren[0].childNodes[0].nodeValue) + t1 = string.strip(textChildren[1].childNodes[0].nodeValue) + + assert t0 == 'foo' + assert t1 == 'bar' + + + + +class RenderSvgAxesTestCase(unittest.TestCase): + "Testing renderSVG module on Axes widgets." + + def test0(self): + "Test two strings in drawing." + + path = outputfile("axestest0.svg") + from reportlab.graphics.charts.axes import XCategoryAxis + + d = XCategoryAxis().demo() + renderSVG.drawToFile(d, path) + + + + +def makeSuite(): + return makeSuiteForClasses(RenderSvgSimpleTestCase, RenderSvgAxesTestCase) + + + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_rl_accel.py b/bin/reportlab/test/test_rl_accel.py new file mode 100755 index 00000000000..7e7bd5c2467 --- /dev/null +++ b/bin/reportlab/test/test_rl_accel.py @@ -0,0 +1,168 @@ +__version__=''' $Id''' +__doc__='''basic tests.''' + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation + +def getrc(defns,depth=1): + from sys import getrefcount, _getframe + f = _getframe(depth) + G0 = f.f_globals + L = f.f_locals + if L is not G0: + LL = [L] + while 1: + f = f.f_back + G = f.f_globals + L = f.f_locals + if G is not G0 or G is L: break + LL.append(L) + L = {} + for l in reversed(LL): + L.update(l) + else: + L = L.copy() + G0 = G0.copy() + return [getrefcount(eval(x,L,G0))-1 for x in defns.split()] + +def checkrc(defns,rcv0): + rcv1 = getrc(defns,2) + return ' '.join(["%s %d-->%d" % (x,v,w) for x,v,w in zip(defns.split(),rcv0,rcv1) if v!=w]) + +class RlAccelTestCase(unittest.TestCase): + + def testFpStr(self): + # should give siz decimal places if less than 1. + # if more, give up to seven sig figs + from _rl_accel import fp_str + assert fp_str(1,2,3)=='1 2 3' + assert fp_str(1) == '1' + + assert fp_str(595.275574) == '595.2756' + assert fp_str(59.5275574) == '59.52756' + assert fp_str(5.95275574) == '5.952756' + + def test_AsciiBase85Encode(self): + from _rl_accel import _AsciiBase85Encode + assert _AsciiBase85Encode('Dragan Andric')=='6ul^K@;[2RDIdd%@f~>' + + def test_AsciiBase85Decode(self): + from _rl_accel import _AsciiBase85Decode + assert _AsciiBase85Decode('6ul^K@;[2RDIdd%@f~>')=='Dragan Andric' + + def testEscapePDF(self): + from _rl_accel import escapePDF + assert escapePDF('(test)')=='\\(test\\)' + + def test_instanceEscapePDF(self): + from _rl_accel import _instanceEscapePDF + assert _instanceEscapePDF('', '(test)')=='\\(test\\)' + + def testCalcChecksum(self): + from _rl_accel import calcChecksum + assert calcChecksum('test')==1952805748 + + def testStringWidth(self): + from _rl_accel import stringWidthU + from reportlab.pdfbase.pdfmetrics import _py_stringWidth, getFont, registerFont, _fonts + from reportlab.pdfbase.ttfonts import TTFont + ttfn = 'Luxi-Serif' + t1fn = 'Times-Roman' + registerFont(TTFont(ttfn, "luxiserif.ttf")) + ttf = getFont(ttfn) + t1f = getFont(t1fn) + testCp1252 = 'copyright %s trademark %s registered %s ReportLab! Ol%s!' % (chr(169), chr(153),chr(174), chr(0xe9)) + enc='cp1252' + senc = 'utf8' + intern(senc) + ts = 'ABCDEF\xce\x91\xce\xb2G' + utext = 'ABCDEF\xce\x91\xce\xb2G'.decode('utf8') + fontSize = 12 + defns="ttfn t1fn ttf t1f testCp1252 enc senc ts utext fontSize ttf.face ttf.face.charWidths ttf.face.defaultWidth t1f.widths t1f.encName t1f.substitutionFonts _fonts" + rcv = getrc(defns) + def tfunc(ts,fn,fontSize,enc): + w1 = stringWidthU(ts,fn,fontSize,enc) + w2 = _py_stringWidth(ts,fn,fontSize,enc) + assert abs(w1-w2)<1e-10,"stringWidthU(%r,%r,%s,%r)-->%r != _py_stringWidth(...)-->%r" % (ts,fn,fontSize,enc,w1,w2) + tfunc(testCp1252,t1fn,fontSize,enc) + tfunc(ts,t1fn,fontSize,senc) + tfunc(utext,t1fn,fontSize,senc) + tfunc(ts,ttfn,fontSize,senc) + tfunc(testCp1252,ttfn,fontSize,enc) + tfunc(utext,ttfn,fontSize,senc) + rcc = checkrc(defns,rcv) + assert not rcc, "rc diffs (%s)" % rcc + + def test_instanceStringWidth(self): + from reportlab.pdfbase.pdfmetrics import registerFont, getFont, _fonts, unicode2T1 + from reportlab.pdfbase.ttfonts import TTFont + ttfn = 'Luxi-Serif' + t1fn = 'Times-Roman' + registerFont(TTFont(ttfn, "luxiserif.ttf")) + ttf = getFont(ttfn) + t1f = getFont(t1fn) + testCp1252 = 'copyright %s trademark %s registered %s ReportLab! Ol%s!' % (chr(169), chr(153),chr(174), chr(0xe9)) + enc='cp1252' + senc = 'utf8' + ts = 'ABCDEF\xce\x91\xce\xb2G' + utext = 'ABCDEF\xce\x91\xce\xb2G'.decode(senc) + fontSize = 12 + defns="ttfn t1fn ttf t1f testCp1252 enc senc ts utext fontSize ttf.face ttf.face.charWidths ttf.face.defaultWidth t1f.widths t1f.encName t1f.substitutionFonts _fonts" + rcv = getrc(defns) + def tfunc(f,ts,fontSize,enc): + w1 = f.stringWidth(ts,fontSize,enc) + w2 = f._py_stringWidth(ts,fontSize,enc) + assert abs(w1-w2)<1e-10,"f(%r).stringWidthU(%r,%s,%r)-->%r != f._py_stringWidth(...)-->%r" % (f,ts,fontSize,enc,w1,w2) + tfunc(t1f,testCp1252,fontSize,enc) + tfunc(t1f,ts,fontSize,senc) + tfunc(t1f,utext,fontSize,senc) + tfunc(ttf,ts,fontSize,senc) + tfunc(ttf,testCp1252,fontSize,enc) + tfunc(ttf,utext,fontSize,senc) + rcc = checkrc(defns,rcv) + assert not rcc, "rc diffs (%s)" % rcc + + def test_unicode2T1(self): + from reportlab.pdfbase.pdfmetrics import _py_unicode2T1, getFont, _fonts + from _rl_accel import unicode2T1 + t1fn = 'Times-Roman' + t1f = getFont(t1fn) + enc = 'cp1252' + senc = 'utf8' + testCp1252 = ('copyright %s trademark %s registered %s ReportLab! Ol%s!' % (chr(169), chr(153),chr(174), chr(0xe9))).decode(enc) + utext = 'This is the end of the \xce\x91\xce\xb2 world. This is the end of the \xce\x91\xce\xb2 world jap=\xe3\x83\x9b\xe3\x83\x86. This is the end of the \xce\x91\xce\xb2 world. This is the end of the \xce\x91\xce\xb2 world jap=\xe3\x83\x9b\xe3\x83\x86'.decode('utf8') + def tfunc(f,ts): + w1 = unicode2T1(ts,[f]+f.substitutionFonts) + w2 = _py_unicode2T1(ts,[f]+f.substitutionFonts) + assert w1==w2,"%r != %r" % (w1,w2) + defns="t1fn t1f testCp1252 enc senc utext t1f.widths t1f.encName t1f.substitutionFonts _fonts" + rcv = getrc(defns) + tfunc(t1f,testCp1252) + tfunc(t1f,utext) + rcc = checkrc(defns,rcv) + assert not rcc, "rc diffs (%s)" % rcc + + def test_getFont(self): + from reportlab.pdfbase.pdfmetrics import _py_getFont, getFont + from _rl_accel import getFontU + assert getFontU is getFont + t1fn = 'Times-Roman' + assert _py_getFont(t1fn) is getFontU(t1fn) + + def test_sameFrag(self): + pass + +def makeSuite(): + # only run the tests if _rl_accel is present + try: + import _rl_accel + Klass = RlAccelTestCase + except: + class Klass(unittest.TestCase): + pass + return makeSuiteForClasses(Klass) + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_source_chars.py b/bin/reportlab/test/test_source_chars.py new file mode 100644 index 00000000000..1b4204b7ba1 --- /dev/null +++ b/bin/reportlab/test/test_source_chars.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_source_chars.py + +"""This tests for things in source files. Initially, absence of tabs :-) +""" + +import os, sys, glob, string, re +from types import ModuleType, ClassType, MethodType, FunctionType + +import reportlab +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, SecureTestCase, GlobDirectoryWalker, printLocation +from reportlab.lib.utils import open_and_read + + +class SourceTester(SecureTestCase): + def setUp(self): + SecureTestCase.setUp(self) + try: + fn = __file__ + except: + fn = sys.argv[0] + + self.output = open(outputfile(os.path.splitext(os.path.basename(fn))[0]+'.txt'),'w') + + def checkFileForTabs(self, filename): + txt = open_and_read(filename, 'r') + chunks = string.split(txt, '\t') + tabCount = len(chunks) - 1 + if tabCount: + #raise Exception, "File %s contains %d tab characters!" % (filename, tabCount) + self.output.write("file %s contains %d tab characters!\n" % (filename, tabCount)) + + def checkFileForTrailingSpaces(self, filename): + txt = open_and_read(filename, 'r') + initSize = len(txt) + badLines = 0 + badChars = 0 + for line in string.split(txt, '\n'): + stripped = string.rstrip(line) + spaces = len(line) - len(stripped) # OK, so they might be trailing tabs, who cares? + if spaces: + badLines = badLines + 1 + badChars = badChars + spaces + + if badChars <> 0: + self.output.write("file %s contains %d trailing spaces, or %0.2f%% wastage\n" % (filename, badChars, 100.0*badChars/initSize)) + + def testFiles(self): + topDir = os.path.dirname(reportlab.__file__) + w = GlobDirectoryWalker(topDir, '*.py') + for filename in w: + self.checkFileForTabs(filename) + self.checkFileForTrailingSpaces(filename) + +def zapTrailingWhitespace(dirname): + """Eliminates trailing spaces IN PLACE. Use with extreme care + and only after a backup or with version-controlled code.""" + assert os.path.isdir(dirname), "Directory not found!" + print "This will eliminate all trailing spaces in py files under %s." % dirname + ok = raw_input("Shall I proceed? type YES > ") + if ok <> 'YES': + print 'aborted by user' + return + w = GlobDirectoryWalker(dirname, '*.py') + for filename in w: + # trim off final newline and detect real changes + txt = open(filename, 'r').read() + badChars = 0 + cleaned = [] + for line in string.split(txt, '\n'): + stripped = string.rstrip(line) + cleaned.append(stripped) + spaces = len(line) - len(stripped) # OK, so they might be trailing tabs, who cares? + if spaces: + badChars = badChars + spaces + + if badChars <> 0: + open(filename, 'w').write(string.join(cleaned, '\n')) + print "file %s contained %d trailing spaces, FIXED" % (filename, badChars) + print 'done' + +def makeSuite(): + return makeSuiteForClasses(SourceTester) + + +#noruntests +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == 'zap' and os.path.isdir(sys.argv[2]): + zapTrailingWhitespace(sys.argv[2]) + else: + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_table_layout.py b/bin/reportlab/test/test_table_layout.py new file mode 100644 index 00000000000..d33be6d01d2 --- /dev/null +++ b/bin/reportlab/test/test_table_layout.py @@ -0,0 +1,425 @@ +import operator, string + +from reportlab.platypus import * +#from reportlab import rl_config +from reportlab.lib.styles import PropertySet, getSampleStyleSheet, ParagraphStyle +from reportlab.lib import colors +from reportlab.platypus.paragraph import Paragraph +#from reportlab.lib.utils import fp_str +#from reportlab.pdfbase import pdfmetrics +from reportlab.platypus.flowables import PageBreak + + +import os + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + + +from types import TupleType, ListType, StringType + + +class TableTestCase(unittest.TestCase): + + + def getDataBlock(self): + "Helper - data for our spanned table" + return [ + # two rows are for headers + ['Region','Product','Period',None,None,None,'Total'], + [None,None,'Q1','Q2','Q3','Q4',None], + + # now for data + ['North','Spam',100,110,120,130,460], + ['North','Eggs',101,111,121,131,464], + ['North','Guinness',102,112,122,132,468], + + ['South','Spam',100,110,120,130,460], + ['South','Eggs',101,111,121,131,464], + ['South','Guinness',102,112,122,132,468], + ] + + def test_document(self): + + rowheights = (24, 16, 16, 16, 16) + rowheights2 = (24, 16, 16, 16, 30) + colwidths = (50, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32) + GRID_STYLE = TableStyle( + [('GRID', (0,0), (-1,-1), 0.25, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT')] + ) + + styleSheet = getSampleStyleSheet() + styNormal = styleSheet['Normal'] + styNormal.spaceBefore = 6 + styNormal.spaceAfter = 6 + + data = ( + ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Miscellaneous accessories', 0,0,0,0,0,0,1,0,0,0,2,13), + ('Hats', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843') + ) + data2 = ( + ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13), + ('Hats\nLarge', 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843') + ) + + + data3 = ( + ('', 'Jan', 'Feb', 'Mar','Apr','May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'), + ('Mugs', 0, 4, 17, 3, 21, 47, 12, 33, 2, -2, 44, 89), + ('T-Shirts', 0, 42, 9, -3, 16, 4, 72, 89, 3, 19, 32, 119), + ('Key Ring', 0,0,0,0,0,0,1,0,0,0,2,13), + (Paragraph("Let's really mess things up with a paragraph",styNormal), + 893, 912, '1,212', 643, 789, 159, 888, '1,298', 832, 453, '1,344','2,843') + ) + + lst = [] + + + lst.append(Paragraph("""Basics about column sizing and cell contents""", styleSheet['Heading1'])) + + t1 = Table(data, colwidths, rowheights) + t1.setStyle(GRID_STYLE) + lst.append(Paragraph("This is GRID_STYLE with explicit column widths. Each cell contains a string or number\n", styleSheet['BodyText'])) + lst.append(t1) + lst.append(Spacer(18,18)) + + t2 = Table(data, None, None) + t2.setStyle(GRID_STYLE) + lst.append(Paragraph("""This is GRID_STYLE with no size info. It + does the sizes itself, measuring each text string + and computing the space it needs. If the text is + too wide for the frame, the table will overflow + as seen here.""", + styNormal)) + lst.append(t2) + lst.append(Spacer(18,18)) + + t3 = Table(data2, None, None) + t3.setStyle(GRID_STYLE) + lst.append(Paragraph("""This demonstrates the effect of adding text strings with + newlines to a cell. It breaks where you specify, and if rowHeights is None (i.e + automatic) then you'll see the effect. See bottom left cell.""", + styNormal)) + lst.append(t3) + lst.append(Spacer(18,18)) + + + colWidths = (None, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32) + t3 = Table(data3, colWidths, None) + t3.setStyle(GRID_STYLE) + lst.append(Paragraph("""This table does not specify the size of the first column, + so should work out a sane one. In this case the element + at bottom left is a paragraph, which has no intrinsic size + (the height and width are a function of each other). So, + it tots up the extra space in the frame and divides it + between any such unsizeable columns. As a result the + table fills the width of the frame (except for the + 6 point padding on either size).""", + styNormal)) + lst.append(t3) + lst.append(PageBreak()) + + lst.append(Paragraph("""Row and Column spanning""", styleSheet['Heading1'])) + + lst.append(Paragraph("""This shows a very basic table. We do a faint pink grid + to show what's behind it - imagine this is not printed, as we'll overlay it later + with some black lines. We're going to "span" some cells, and have put a + value of None in the data to signify the cells we don't care about. + (In real life if you want an empty cell, put '' in it rather than None). """, styNormal)) + + sty = TableStyle([ + #very faint grid to show what's where + ('GRID', (0,0), (-1,-1), 0.25, colors.pink), + ]) + + t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty) + lst.append(t) + + + + lst.append(Paragraph("""We now center the text for the "period" + across the four cells for each quarter. To do this we add a 'span' + command to the style to make the cell at row 1 column 3 cover 4 cells, + and a 'center' command for all cells in the top row. The spanning + is not immediately evident but trust us, it's happening - the word + 'Period' is centered across the 4 columns. Note also that the + underlying grid shows through. All line drawing commands apply + to the underlying grid, so you have to take care what you put + grids through.""", styNormal)) + sty = TableStyle([ + # + ('GRID', (0,0), (-1,-1), 0.25, colors.pink), + ('ALIGN', (0,0), (-1,0), 'CENTER'), + ('SPAN', (2,0), (5,0)), + ]) + + t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty) + lst.append(t) + + lst.append(Paragraph("""We repeat this for the words 'Region', Product' + and 'Total', which each span the top 2 rows; and for 'Nprth' and 'South' + which span 3 rows. At the moment each cell's alignment is the default + (bottom), so these words appear to have "dropped down"; in fact they + are sitting on the bottom of their allocated ranges. You will just see that + all the 'None' values vanished, as those cells are not drawn any more.""", styNormal)) + sty = TableStyle([ + # + ('GRID', (0,0), (-1,-1), 0.25, colors.pink), + ('ALIGN', (0,0), (-1,0), 'CENTER'), + ('SPAN', (2,0), (5,0)), + #span the other column heads down 2 rows + ('SPAN', (0,0), (0,1)), + ('SPAN', (1,0), (1,1)), + ('SPAN', (6,0), (6,1)), + #span the 'north' and 'south' down 3 rows each + ('SPAN', (0,2), (0,4)), + ('SPAN', (0,5), (0,7)), + ]) + + t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty) + lst.append(t) + + + lst.append(PageBreak()) + + + lst.append(Paragraph("""Now we'll tart things up a bit. First, + we set the vertical alignment of each spanned cell to 'middle'. + Next we add in some line drawing commands which do not slash across + the spanned cells (this needs a bit of work). + Finally we'll add some thicker lines to divide it up, and hide the pink. Voila! + """, styNormal)) + sty = TableStyle([ + # +# ('GRID', (0,0), (-1,-1), 0.25, colors.pink), + ('TOPPADDING', (0,0), (-1,-1), 3), + + #span the 'period' + ('SPAN', (2,0), (5,0)), + #span the other column heads down 2 rows + ('SPAN', (0,0), (0,1)), + ('SPAN', (1,0), (1,1)), + ('SPAN', (6,0), (6,1)), + #span the 'north' and 'south' down 3 rows each + ('SPAN', (0,2), (0,4)), + ('SPAN', (0,5), (0,7)), + + #top row headings are centred + ('ALIGN', (0,0), (-1,0), 'CENTER'), + #everything we span is vertically centred + #span the other column heads down 2 rows + ('VALIGN', (0,0), (0,1), 'MIDDLE'), + ('VALIGN', (1,0), (1,1), 'MIDDLE'), + ('VALIGN', (6,0), (6,1), 'MIDDLE'), + #span the 'north' and 'south' down 3 rows each + ('VALIGN', (0,2), (0,4), 'MIDDLE'), + ('VALIGN', (0,5), (0,7), 'MIDDLE'), + + #numeric stuff right aligned + ('ALIGN', (2,1), (-1,-1), 'RIGHT'), + + #draw lines carefully so as not to swipe through + #any of the 'spanned' cells + ('GRID', (1,2), (-1,-1), 1.0, colors.black), + ('BOX', (0,2), (0,4), 1.0, colors.black), + ('BOX', (0,5), (0,7), 1.0, colors.black), + ('BOX', (0,0), (0,1), 1.0, colors.black), + ('BOX', (1,0), (1,1), 1.0, colors.black), + + ('BOX', (2,0), (5,0), 1.0, colors.black), + ('GRID', (2,1), (5,1), 1.0, colors.black), + + ('BOX', (6,0), (6,1), 1.0, colors.black), + + # do fatter boxes around some cells + ('BOX', (0,0), (-1,1), 2.0, colors.black), + ('BOX', (0,2), (-1,4), 2.0, colors.black), + ('BOX', (0,5), (-1,7), 2.0, colors.black), + ('BOX', (-1,0), (-1,-1), 2.0, colors.black), + + ]) + + t = Table(self.getDataBlock(), colWidths=None, rowHeights=None, style=sty) + lst.append(t) + + lst.append(Paragraph("""How cells get sized""", styleSheet['Heading1'])) + + lst.append(Paragraph("""So far the table has been auto-sized. This can be + computationally expensive, and can lead to yucky effects. Imagine a lot of + numbers, one of which goes to 4 figures - tha numeric column will be wider. + The best approach is to specify the column + widths where you know them, and let the system do the heights. Here we set some + widths - an inch for the text columns and half an inch for the numeric ones. + """, styNormal)) + + t = Table(self.getDataBlock(), + colWidths=(72,72,36,36,36,36,56), + rowHeights=None, + style=sty) + lst.append(t) + + lst.append(Paragraph("""The auto-sized example 2 steps back demonstrates + one advanced feature of the sizing algorithm. In the table below, + the columns for Q1-Q4 should all be the same width. We've made + the text above it a bit longer than "Period". Note that this text + is technically in the 3rd column; on our first implementation this + was sized and column 3 was therefore quite wide. To get it right, + we ensure that any cells which span columns, or which are 'overwritten' + by cells which span columns, are assigned zero width in the cell + sizing. Thus, only the string 'Q1' and the numbers below it are + calculated in estimating the width of column 3, and the phrase + "What time of year?" is not used. However, row-spanned cells are + taken into account. ALL the cells in the leftmost column + have a vertical span (or are occluded by others which do) + but it can still work out a sane width for them. + + """, styNormal)) + + data = self.getDataBlock() + data[0][2] = "Which time of year?" + #data[7][0] = Paragraph("Let's really mess things up with a paragraph",styNormal) + t = Table(data, + #colWidths=(72,72,36,36,36,36,56), + rowHeights=None, + style=sty) + lst.append(t) + + lst.append(Paragraph("""Paragraphs and unsizeable objects in table cells.""", styleSheet['Heading1'])) + + lst.append(Paragraph("""Paragraphs and other flowable objects make table + sizing much harder. In general the height of a paragraph is a function + of its width so you can't ask it how wide it wants to be - and the + REALLY wide all-on-one-line solution is rarely what is wanted. We + refer to Paragraphs and their kin as "unsizeable objects". In this example + we have set the widths of all but the first column. As you can see + it uses all the available space across the page for the first column. + Note also that this fairly large cell does NOT contribute to the + height calculation for its 'row'. Under the hood it is in the + same row as the second Spam, but this row gets a height based on + its own contents and not the cell with the paragraph. + + """, styNormal)) + + + data = self.getDataBlock() + data[5][0] = Paragraph("Let's really mess things up with a paragraph, whose height is a function of the width you give it.",styNormal) + t = Table(data, + colWidths=(None,72,36,36,36,36,56), + rowHeights=None, + style=sty) + lst.append(t) + + + lst.append(Paragraph("""This one demonstrates that our current algorithm + does not cover all cases :-( The height of row 0 is being driven by + the width of the para, which thinks it should fit in 1 column and not 4. + To really get this right would involve multiple passes through all the cells + applying rules until everything which can be sized is sized (possibly + backtracking), applying increasingly dumb and brutal + rules on each pass. + """, styNormal)) + data = self.getDataBlock() + data[0][2] = Paragraph("Let's really mess things up with a paragraph.",styNormal) + data[5][0] = Paragraph("Let's really mess things up with a paragraph, whose height is a function of the width you give it.",styNormal) + t = Table(data, + colWidths=(None,72,36,36,36,36,56), + rowHeights=None, + style=sty) + lst.append(t) + + lst.append(Paragraph("""To avoid these problems remember the golden rule + of ReportLab tables: (1) fix the widths if you can, (2) don't use + a paragraph when a string will do. + """, styNormal)) + + lst.append(Paragraph("""Unsized columns that contain flowables without + precise widths, such as paragraphs and nested tables, + still need to try and keep their content within borders and ideally + even honor percentage requests. This can be tricky--and expensive. + But sometimes you can't follow the golden rules. + """, styNormal)) + + lst.append(Paragraph("""The code first calculates the minimum width + for each unsized column by iterating over every flowable in each column + and remembering the largest minimum width. It then allocates + available space to accomodate the minimum widths. Any remaining space + is divided up, treating a width of '*' as greedy, a width of None as + non-greedy, and a percentage as a weight. If a column is already + wider than its percentage warrants, it is not further expanded, and + the other widths accomodate it. + """, styNormal)) + + lst.append(Paragraph("""For instance, consider this tortured table. + It contains four columns, with widths of None, None, 60%, and 20%, + respectively, and a single row. The first cell contains a paragraph. + The second cell contains a table with fixed column widths that total + about 50% of the total available table width. The third cell contains + a string. The last cell contains a table with no set widths but a + single cell containing a paragraph. + """, styNormal)) + ministy = TableStyle([ + ('GRID', (0,0), (-1,-1), 1.0, colors.black), + ]) + nested1 = [Paragraph( + 'This is a paragraph. The column has a width of None.', + styNormal)] + nested2 = [Table( + [[Paragraph( + 'This table is set to take up two and a half inches. The ' + 'column that holds it has a width of None.', styNormal)]], + colWidths=(180,), + rowHeights=None, + style=ministy)] + nested3 = '60% width' + nested4 = [Table( + [[[Paragraph( + "This is a table with a paragraph in it but no width set. " + "The column width in the containing table is 20%.", + styNormal)]]], + colWidths=(None,), + rowHeights=None, + style=ministy)] + t = Table([[nested1, nested2, nested3, nested4]], + colWidths=(None, None, '60%', '20%'), + rowHeights=None, + style=ministy) + lst.append(t) + + lst.append(Paragraph("""Notice that the second column does expand to + account for the minimum size of its contents; and that the remaining + space goes to the third column, in an attempt to honor the '60%' + request as much as possible. This is reminiscent of the typical HTML + browser approach to tables.""", styNormal)) + + lst.append(Paragraph("""To get an idea of how potentially expensive + this is, consider the case of the last column: the table gets the + minimum width of every flowable of every cell in the column. In this + case one of the flowables is a table with a column without a set + width, so the nested table must itself iterate over its flowables. + The contained paragraph then calculates the width of every word in it + to see what the biggest word is, given the set font face and size. It + is easy to imagine creating a structure of this sort that took an + unacceptably large amount of time to calculate. Remember the golden + rule, if you can. """, styNormal)) + + lst.append(Paragraph("""This code does not yet handle spans well.""", + styNormal)) + + SimpleDocTemplate(outputfile('test_table_layout.pdf'), showBoundary=1).build(lst) + +def makeSuite(): + return makeSuiteForClasses(TableTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + print 'saved '+outputfile('test_table_layout.pdf') + printLocation() diff --git a/bin/reportlab/test/test_tools_pythonpoint.py b/bin/reportlab/test/test_tools_pythonpoint.py new file mode 100644 index 00000000000..bddd0138096 --- /dev/null +++ b/bin/reportlab/test/test_tools_pythonpoint.py @@ -0,0 +1,39 @@ +"""Tests for the PythonPoint tool. +""" +import os, sys, string +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation +import reportlab + +class PythonPointTestCase(unittest.TestCase): + "Some very crude tests on PythonPoint." + def test0(self): + "Test if pythonpoint.pdf can be created from pythonpoint.xml." + + join, dirname, isfile, abspath = os.path.join, os.path.dirname, os.path.isfile, os.path.abspath + rlDir = abspath(dirname(reportlab.__file__)) + from reportlab.tools.pythonpoint import pythonpoint + from reportlab.lib.utils import isCompactDistro, open_for_read + ppDir = dirname(pythonpoint.__file__) + xml = join(ppDir, 'demos', 'pythonpoint.xml') + datafilename = 'pythonpoint.pdf' + outDir = outputfile('') + if isCompactDistro(): + cwd = None + xml = open_for_read(xml) + else: + cwd = os.getcwd() + os.chdir(join(ppDir, 'demos')) + pdf = join(outDir, datafilename) + if isfile(pdf): os.remove(pdf) + pythonpoint.process(xml, outDir=outDir, verbose=0, datafilename=datafilename) + if cwd: os.chdir(cwd) + assert os.path.exists(pdf) + +def makeSuite(): + return makeSuiteForClasses(PythonPointTestCase) + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_utils.py b/bin/reportlab/test/test_utils.py new file mode 100644 index 00000000000..18f8790f175 --- /dev/null +++ b/bin/reportlab/test/test_utils.py @@ -0,0 +1,37 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +__version__='''$Id: test_utils.py 2619 2005-06-24 14:49:15Z rgbecker $''' +__doc__="""Test reportlab.lib.util module""" + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + + +class FmtTestCase(unittest.TestCase): + + def testFmt(self): + from reportlab.lib.utils import FmtSelfDict + class MixedIn(FmtSelfDict): + def __init__(self): + self.a = 'AA' + self._b = '_BB' + self.d = '(overridden)' + obj = MixedIn() + self.assertEqual('blah', obj._fmt('blah')) + self.assertEqual('blah %', obj._fmt('blah %%')) + self.assertRaises(ValueError, obj._fmt, 'blah %') + self.assertEqual( + 'moon AA june_BB spoon %(a)sCC ni', + obj._fmt('moon %(a)s june%(_b)s spoon %%(a)s%(c)s %(d)s', c='CC', C='boon', d='ni')) + self.assertRaises(AttributeError, obj._fmt, '%(c)s') # XXX bit weird, can this be changed? + + +def makeSuite(): + return makeSuiteForClasses(FmtTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_widgetbase_tpc.py b/bin/reportlab/test/test_widgetbase_tpc.py new file mode 100644 index 00000000000..4e8c0b1e33a --- /dev/null +++ b/bin/reportlab/test/test_widgetbase_tpc.py @@ -0,0 +1,138 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/test/test_widgetbase_tpc.py +""" +Tests for TypedPropertyCollection class. +""" + +import os, sys, copy +from os.path import join, basename, splitext + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, printLocation + +from reportlab.graphics.widgetbase import PropHolder, TypedPropertyCollection +from reportlab.lib.attrmap import AttrMap, AttrMapValue +from reportlab.lib.validators import isNumber + + +TPC = TypedPropertyCollection + + +class PH(PropHolder): + _attrMap = AttrMap( + a = AttrMapValue(isNumber), + b = AttrMapValue(isNumber) + ) + + +class APH(PH): + def __init__(self): + self.a = 1 + + +class BPH(APH): + def __init__(self): + APH.__init__(self) + + def __getattr__(self,name): + if name=='b': return -1 + raise AttributeError + + +class TPCTestCase(unittest.TestCase): + "Test TypedPropertyCollection class." + + def test0(self): + "Test setting an invalid collective attribute." + + t = TPC(PH) + try: + t.c = 42 + except AttributeError: + pass + + + def test1(self): + "Test setting a valid collective attribute." + + t = TPC(PH) + t.a = 42 + assert t.a == 42 + + + def test2(self): + "Test setting a valid collective attribute with an invalid value." + + t = TPC(PH) + try: + t.a = 'fourty-two' + except AttributeError: + pass + + + def test3(self): + "Test setting a valid collective attribute with a convertible invalid value." + + t = TPC(PH) + t.a = '42' + assert t.a == '42' # Or should it rather be an integer? + + + def test4(self): + "Test accessing an unset collective attribute." + + t = TPC(PH) + try: + t.a + except AttributeError: + pass + + + def test5(self): + "Test overwriting a collective attribute in one slot." + + t = TPC(PH) + t.a = 42 + t[0].a = 4242 + assert t[0].a == 4242 + + + def test6(self): + "Test overwriting a one slot attribute with a collective one." + + t = TPC(PH) + t[0].a = 4242 + t.a = 42 + assert t[0].a == 4242 + + + def test7(self): + "Test to ensure we can handle classes with __getattr__ methods" + + a=TypedPropertyCollection(APH) + b=TypedPropertyCollection(BPH) + + a.a=3 + b.a=4 + try: + a.b + assert 1, "Shouldn't be able to see a.b" + except AttributeError: + pass + a.b=0 + assert a.b==0, "Wrong value for "+str(a.b) + assert b.b==-1, "This should call __getattr__ special" + b.b=0 + assert a[0].b==0 + assert b[0].b==-1, "Class __getattr__ should return -1" + + +def makeSuite(): + return makeSuiteForClasses(TPCTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/test_widgets_grids.py b/bin/reportlab/test/test_widgets_grids.py new file mode 100644 index 00000000000..a1d4a07eb6a --- /dev/null +++ b/bin/reportlab/test/test_widgets_grids.py @@ -0,0 +1,454 @@ + +from reportlab.test import unittest +from reportlab.test.utils import makeSuiteForClasses, outputfile, printLocation + +from reportlab.lib import colors +from reportlab.graphics.shapes import Drawing, Group, Line, Rect +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics.widgets.grids import * +from reportlab.graphics import renderPDF +from reportlab.graphics import renderSVG + + +class GridTestCase(unittest.TestCase): + "Testing diagrams containing grid widgets." + + def _test0(self): + "Create color ranges." + + c0, c1 = colors.Color(0, 0, 0), colors.Color(1, 1, 1) + for c in colorRange(c0, c1, 4): + print c + print + + c0, c1 = colors.CMYKColor(0, 0, 0, 0), colors.CMYKColor(0, 0, 0, 1) + for c in colorRange(c0, c1, 4): + print c + print + + c0, c1 = colors.PCMYKColor(0, 0, 0, 0), colors.PCMYKColor(0, 0, 0, 100) + for c in colorRange(c0, c1, 4): + print c + print + + + def makeDrawing0(self): + "Generate a RLG drawing with some uncommented grid samples." + + D = Drawing(450, 650) + + d = 80 + s = 50 + + for row in range(10): + y = 530 - row*d + if row == 0: + for col in range(4): + x = 20 + col*d + g = Grid() + g.x = x + g.y = y + g.width = s + g.height = s + g.useRects = 0 + g.useLines = 1 + if col == 0: + pass + elif col == 1: + g.delta0 = 10 + elif col == 2: + g.orientation = 'horizontal' + elif col == 3: + g.deltaSteps = [5, 10, 20, 30] + g.demo() + D.add(g) + elif row == 1: + for col in range(4): + x = 20 + col*d + g = Grid() + g.y = y + g.x = x + g.width = s + g.height = s + if col == 0: + pass + elif col == 1: + g.delta0 = 10 + elif col == 2: + g.orientation = 'horizontal' + elif col == 3: + g.deltaSteps = [5, 10, 20, 30] + g.useRects = 1 + g.useLines = 0 + g.demo() + D.add(g) + elif row == 2: + for col in range(3): + x = 20 + col*d + g = Grid() + g.x = x + g.y = y + g.width = s + g.height = s + g.useLines = 1 + g.useRects = 1 + if col == 0: + pass + elif col == 1: + g.delta0 = 10 + elif col == 2: + g.orientation = 'horizontal' + g.demo() + D.add(g) + elif row == 3: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y + sr.width = s + sr.height = s + sr.fillColorStart = colors.Color(0, 0, 0) + sr.fillColorEnd = colors.Color(1, 1, 1) + if col == 0: + sr.numShades = 5 + elif col == 1: + sr.numShades = 2 + elif col == 2: + sr.numShades = 1 + sr.demo() + D.add(sr) + elif row == 4: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y + sr.width = s + sr.height = s + sr.fillColorStart = colors.red + sr.fillColorEnd = colors.blue + sr.orientation = 'horizontal' + if col == 0: + sr.numShades = 10 + elif col == 1: + sr.numShades = 20 + elif col == 2: + sr.numShades = 50 + sr.demo() + D.add(sr) + elif row == 5: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y + sr.width = s + sr.height = s + sr.fillColorStart = colors.white + sr.fillColorEnd = colors.green + sr.orientation = 'horizontal' + if col == 0: + sr.numShades = 10 + elif col == 1: + sr.numShades = 20 + sr.orientation = 'vertical' + elif col == 2: + sr.numShades = 50 + sr.demo() + D.add(sr) + elif row == 6: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y+s + sr.width = s + sr.height = -s + sr.fillColorStart = colors.white + sr.fillColorEnd = colors.green + sr.orientation = 'horizontal' + if col == 0: + sr.numShades = 10 + elif col == 1: + sr.numShades = 20 + sr.orientation = 'vertical' + elif col == 2: + sr.numShades = 50 + sr.demo() + D.add(sr) + + return D + + + def makeDrawing1(self): + "Generate a RLG drawing with some uncommented grid samples." + + D = Drawing(450, 650) + + d = 80 + s = 50 + + for row in range(2): + y = 530 - row*d + if row == 0: + for col in range(4): + x = 20 + col*d + g = DoubleGrid() + g.x = x + g.y = y + g.width = s + g.height = s + + # This should be done implicitely... + g.grid0.x = x + g.grid0.y = y + g.grid1.x = x + g.grid1.y = y + g.grid0.width = s + g.grid0.height = s + g.grid1.width = s + g.grid1.height = s + + if col == 0: + pass + elif col == 1: + g.grid0.delta0 = 10 + elif col == 2: + g.grid0.delta0 = 5 + elif col == 3: + g.grid0.deltaSteps = [5, 10, 20, 30] + g.demo() + D.add(g) + elif row == 1: + for col in range(4): + x = 20 + col*d + g = DoubleGrid() + g.x = x + g.y = y + g.width = s + g.height = s + + # This should be done implicitely... + g.grid0.x = x + g.grid0.y = y + g.grid1.x = x + g.grid1.y = y + g.grid0.width = s + g.grid0.height = s + g.grid1.width = s + g.grid1.height = s + + if col == 0: + g.grid0.useRects = 0 + g.grid0.useLines = 1 + g.grid1.useRects = 0 + g.grid1.useLines = 1 + elif col == 1: + g.grid0.useRects = 1 + g.grid0.useLines = 1 + g.grid1.useRects = 0 + g.grid1.useLines = 1 + elif col == 2: + g.grid0.useRects = 1 + g.grid0.useLines = 0 + g.grid1.useRects = 0 + g.grid1.useLines = 1 + elif col == 3: + g.grid0.useRects = 1 + g.grid0.useLines = 0 + g.grid1.useRects = 1 + g.grid1.useLines = 0 + g.demo() + D.add(g) + + return D + + + def makeDrawing2(self): + "Generate a RLG drawing with some uncommented grid samples." + + D = Drawing(450, 650) + + d = 80 + s = 50 + + for row in range(10): + y = 530 - row*d + if row == 0: + for col in range(4): + x = 20 + col*d + g = Grid() + g.x = x + g.y = y + g.width = s + g.height = s + g.useRects = 0 + g.useLines = 1 + if col == 0: + pass + elif col == 1: + g.delta0 = 10 + elif col == 2: + g.orientation = 'horizontal' + elif col == 3: + g.deltaSteps = [5, 10, 20, 30] + g.demo() + D.add(g) + elif row == 1: + for col in range(4): + x = 20 + col*d + g = Grid() + g.y = y + g.x = x + g.width = s + g.height = s + if col == 0: + pass + elif col == 1: + g.delta0 = 10 + elif col == 2: + g.orientation = 'horizontal' + elif col == 3: + g.deltaSteps = [5, 10, 20, 30] + g.useRects = 1 + g.useLines = 0 + g.demo() + D.add(g) + elif row == 2: + for col in range(3): + x = 20 + col*d + g = Grid() + g.x = x + g.y = y + g.width = s + g.height = s + g.useLines = 1 + g.useRects = 1 + if col == 0: + pass + elif col == 1: + g.delta0 = 10 + elif col == 2: + g.orientation = 'horizontal' + g.demo() + D.add(g) + elif row == 3: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y + sr.width = s + sr.height = s + ## sr.fillColorStart = colors.Color(0, 0, 0) + ## sr.fillColorEnd = colors.Color(1, 1, 1) + sr.fillColorStart = colors.CMYKColor(0, 0, 0, 0) + sr.fillColorEnd = colors.CMYKColor(1, 1, 1, 1) + if col == 0: + sr.numShades = 5 + elif col == 1: + sr.numShades = 2 + elif col == 2: + sr.numShades = 1 + sr.demo() + D.add(sr) + elif row == 4: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y + sr.width = s + sr.height = s + ## sr.fillColorStart = colors.red + ## sr.fillColorEnd = colors.blue + sr.fillColorStart = colors.CMYKColor(1, 0, 0, 0) + sr.fillColorEnd = colors.CMYKColor(0, 0, 1, 0) + sr.orientation = 'horizontal' + if col == 0: + sr.numShades = 10 + elif col == 1: + sr.numShades = 20 + elif col == 2: + sr.numShades = 50 + sr.demo() + D.add(sr) + elif row == 5: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y + sr.width = s + sr.height = s + ## sr.fillColorStart = colors.white + ## sr.fillColorEnd = colors.green + sr.fillColorStart = colors.PCMYKColor(11.0,11.0,72.0,0.0, spotName='PANTONE 458 CV',density=1.00) + sr.fillColorEnd = colors.PCMYKColor(100.0,65.0,0.0,30.0, spotName='PANTONE 288 CV',density=1.00) + sr.orientation = 'horizontal' + if col == 0: + sr.numShades = 10 + elif col == 1: + sr.numShades = 20 + sr.orientation = 'vertical' + elif col == 2: + sr.numShades = 50 + sr.demo() + D.add(sr) + elif row == 6: + for col in range(3): + x = 20 + col*d + sr = ShadedRect() + sr.x = x + sr.y = y+s + sr.width = s + sr.height = -s + sr.fillColorStart = colors.white + sr.fillColorEnd = colors.green + sr.orientation = 'horizontal' + if col == 0: + sr.numShades = 10 + elif col == 1: + sr.numShades = 20 + sr.orientation = 'vertical' + elif col == 2: + sr.numShades = 50 + sr.demo() + D.add(sr) + + return D + + + def test0(self): + "Generate PDF and SVG documents of first sample drawing." + + d = self.makeDrawing0() + renderPDF.drawToFile(d, outputfile('test_widgets_grids0.pdf')) + renderSVG.drawToFile(d, outputfile('test_widgets_grids0.svg')) + + + def test1(self): + "Generate PDF and SVG documents of second sample drawing." + + d = self.makeDrawing1() + renderPDF.drawToFile(d, outputfile('test_widgets_grids1.pdf')) + renderSVG.drawToFile(d, outputfile('test_widgets_grids1.svg')) + + + def test2(self): + "Generate PDF and SVG documents of third sample drawing." + + d = self.makeDrawing2() + renderPDF.drawToFile(d, outputfile('test_widgets_grids2.pdf')) + renderSVG.drawToFile(d, outputfile('test_widgets_grids2.svg')) + + +def makeSuite(): + return makeSuiteForClasses(GridTestCase) + + +#noruntests +if __name__ == "__main__": + unittest.TextTestRunner().run(makeSuite()) + printLocation() diff --git a/bin/reportlab/test/unittest.py b/bin/reportlab/test/unittest.py new file mode 100644 index 00000000000..2523f431c40 --- /dev/null +++ b/bin/reportlab/test/unittest.py @@ -0,0 +1,723 @@ +#!/usr/bin/env python +''' +Python unit testing framework, based on Erich Gamma's JUnit and Kent Beck's +Smalltalk testing framework. + +This module contains the core framework classes that form the basis of +specific test cases and suites (TestCase, TestSuite etc.), and also a +text-based utility class for running the tests and reporting the results + (TextTestRunner). + +Simple usage: + + import unittest + + class IntegerArithmenticTestCase(unittest.TestCase): + def testAdd(self): ## test method names begin 'test*' + self.assertEquals((1 + 2), 3) + self.assertEquals(0 + 1, 1) + def testMultiply(self): + self.assertEquals((0 * 10), 0) + self.assertEquals((5 * 8), 40) + + if __name__ == '__main__': + unittest.main() + +Further information is available in the bundled documentation, and from + + http://pyunit.sourceforge.net/ + +Copyright (c) 1999, 2000, 2001 Steve Purcell +This module is free software, and you may redistribute it and/or modify +it under the same terms as Python itself, so long as this copyright message +and disclaimer are retained in their original form. + +IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, +SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF +THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, +AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE, +SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. +''' + +__author__ = "Steve Purcell" +__email__ = "stephen_purcell at yahoo dot com" +__version__ = "#Revision: 1.43 $"[11:-2] + +import time +import sys +import traceback +import string +import os +import types + +############################################################################## +# Test framework core +############################################################################## + +class TestResult: + """Holder for test result information. + + Test results are automatically managed by the TestCase and TestSuite + classes, and do not need to be explicitly manipulated by writers of tests. + + Each instance holds the total number of tests run, and collections of + failures and errors that occurred among those test runs. The collections + contain tuples of (testcase, exceptioninfo), where exceptioninfo is the + formatted traceback of the error that occurred. + """ + def __init__(self): + self.failures = [] + self.errors = [] + self.testsRun = 0 + self.shouldStop = 0 + + def startTest(self, test): + "Called when the given test is about to be run" + self.testsRun = self.testsRun + 1 + + def stopTest(self, test): + "Called when the given test has been run" + pass + + def addError(self, test, err): + """Called when an error has occurred. 'err' is a tuple of values as + returned by sys.exc_info(). + """ + self.errors.append((test, self._exc_info_to_string(err))) + + def addFailure(self, test, err): + """Called when an error has occurred. 'err' is a tuple of values as + returned by sys.exc_info().""" + self.failures.append((test, self._exc_info_to_string(err))) + + def addSuccess(self, test): + "Called when a test has completed successfully" + pass + + def wasSuccessful(self): + "Tells whether or not this result was a success" + return len(self.failures) == len(self.errors) == 0 + + def stop(self): + "Indicates that the tests should be aborted" + self.shouldStop = 1 + + def _exc_info_to_string(self, err): + """Converts a sys.exc_info()-style tuple of values into a string.""" + return string.join(apply(traceback.format_exception, err), '') + + def __repr__(self): + return "<%s run=%i errors=%i failures=%i>" % \ + (self.__class__, self.testsRun, len(self.errors), + len(self.failures)) + + +class TestCase: + """A class whose instances are single test cases. + + By default, the test code itself should be placed in a method named + 'runTest'. + + If the fixture may be used for many test cases, create as + many test methods as are needed. When instantiating such a TestCase + subclass, specify in the constructor arguments the name of the test method + that the instance is to execute. + + Test authors should subclass TestCase for their own tests. Construction + and deconstruction of the test's environment ('fixture') can be + implemented by overriding the 'setUp' and 'tearDown' methods respectively. + + If it is necessary to override the __init__ method, the base class + __init__ method must always be called. It is important that subclasses + should not change the signature of their __init__ method, since instances + of the classes are instantiated automatically by parts of the framework + in order to be run. + """ + + # This attribute determines which exception will be raised when + # the instance's assertion methods fail; test methods raising this + # exception will be deemed to have 'failed' rather than 'errored' + + failureException = AssertionError + + def __init__(self, methodName='runTest'): + """Create an instance of the class that will use the named test + method when executed. Raises a ValueError if the instance does + not have a method with the specified name. + """ + try: + self.__testMethodName = methodName + testMethod = getattr(self, methodName) + self.__testMethodDoc = testMethod.__doc__ + except AttributeError: + raise ValueError, "no such test method in %s: %s" % \ + (self.__class__, methodName) + + def setUp(self): + "Hook method for setting up the test fixture before exercising it." + pass + + def tearDown(self): + "Hook method for deconstructing the test fixture after testing it." + pass + + def countTestCases(self): + return 1 + + def defaultTestResult(self): + return TestResult() + + def shortDescription(self): + """Returns a one-line description of the test, or None if no + description has been provided. + + The default implementation of this method returns the first line of + the specified test method's docstring. + """ + doc = self.__testMethodDoc + return doc and string.strip(string.split(doc, "\n")[0]) or None + + def id(self): + return "%s.%s" % (self.__class__, self.__testMethodName) + + def __str__(self): + return "%s (%s)" % (self.__testMethodName, self.__class__) + + def __repr__(self): + return "<%s testMethod=%s>" % \ + (self.__class__, self.__testMethodName) + + def run(self, result=None): + return self(result) + + def __call__(self, result=None): + if result is None: result = self.defaultTestResult() + result.startTest(self) + testMethod = getattr(self, self.__testMethodName) + try: + try: + self.setUp() + except KeyboardInterrupt: + raise + except: + result.addError(self, self.__exc_info()) + return + + ok = 0 + try: + testMethod() + ok = 1 + except self.failureException, e: + result.addFailure(self, self.__exc_info()) + except KeyboardInterrupt: + raise + except: + result.addError(self, self.__exc_info()) + + try: + self.tearDown() + except KeyboardInterrupt: + raise + except: + result.addError(self, self.__exc_info()) + ok = 0 + if ok: result.addSuccess(self) + finally: + result.stopTest(self) + + def debug(self): + """Run the test without collecting errors in a TestResult""" + self.setUp() + getattr(self, self.__testMethodName)() + self.tearDown() + + def __exc_info(self): + """Return a version of sys.exc_info() with the traceback frame + minimised; usually the top level of the traceback frame is not + needed. + """ + exctype, excvalue, tb = sys.exc_info() + if sys.platform[:4] == 'java': ## tracebacks look different in Jython + return (exctype, excvalue, tb) + newtb = tb.tb_next + if newtb is None: + return (exctype, excvalue, tb) + return (exctype, excvalue, newtb) + + def fail(self, msg=None): + """Fail immediately, with the given message.""" + raise self.failureException, msg + + def failIf(self, expr, msg=None): + "Fail the test if the expression is true." + if expr: raise self.failureException, msg + + def failUnless(self, expr, msg=None): + """Fail the test unless the expression is true.""" + if not expr: raise self.failureException, msg + + def failUnlessRaises(self, excClass, callableObj, *args, **kwargs): + """Fail unless an exception of class excClass is thrown + by callableObj when invoked with arguments args and keyword + arguments kwargs. If a different type of exception is + thrown, it will not be caught, and the test case will be + deemed to have suffered an error, exactly as for an + unexpected exception. + """ + try: + apply(callableObj, args, kwargs) + except excClass: + return + else: + if hasattr(excClass,'__name__'): excName = excClass.__name__ + else: excName = str(excClass) + raise self.failureException, excName + + def failUnlessEqual(self, first, second, msg=None): + """Fail if the two objects are unequal as determined by the '!=' + operator. + """ + if first != second: + raise self.failureException, \ + (msg or '%s != %s' % (`first`, `second`)) + + def failIfEqual(self, first, second, msg=None): + """Fail if the two objects are equal as determined by the '==' + operator. + """ + if first == second: + raise self.failureException, \ + (msg or '%s == %s' % (`first`, `second`)) + + assertEqual = assertEquals = failUnlessEqual + + assertNotEqual = assertNotEquals = failIfEqual + + assertRaises = failUnlessRaises + + assert_ = failUnless + + + +class TestSuite: + """A test suite is a composite test consisting of a number of TestCases. + + For use, create an instance of TestSuite, then add test case instances. + When all tests have been added, the suite can be passed to a test + runner, such as TextTestRunner. It will run the individual test cases + in the order in which they were added, aggregating the results. When + subclassing, do not forget to call the base class constructor. + """ + def __init__(self, tests=()): + self._tests = [] + self.addTests(tests) + + def __repr__(self): + return "<%s tests=%s>" % (self.__class__, self._tests) + + __str__ = __repr__ + + def countTestCases(self): + cases = 0 + for test in self._tests: + cases = cases + test.countTestCases() + return cases + + def addTest(self, test): + self._tests.append(test) + + def addTests(self, tests): + for test in tests: + self.addTest(test) + + def run(self, result): + return self(result) + + def __call__(self, result): + for test in self._tests: + if result.shouldStop: + break + test(result) + return result + + def debug(self): + """Run the tests without collecting errors in a TestResult""" + for test in self._tests: test.debug() + + +class FunctionTestCase(TestCase): + """A test case that wraps a test function. + + This is useful for slipping pre-existing test functions into the + PyUnit framework. Optionally, set-up and tidy-up functions can be + supplied. As with TestCase, the tidy-up ('tearDown') function will + always be called if the set-up ('setUp') function ran successfully. + """ + + def __init__(self, testFunc, setUp=None, tearDown=None, + description=None): + TestCase.__init__(self) + self.__setUpFunc = setUp + self.__tearDownFunc = tearDown + self.__testFunc = testFunc + self.__description = description + + def setUp(self): + if self.__setUpFunc is not None: + self.__setUpFunc() + + def tearDown(self): + if self.__tearDownFunc is not None: + self.__tearDownFunc() + + def runTest(self): + self.__testFunc() + + def id(self): + return self.__testFunc.__name__ + + def __str__(self): + return "%s (%s)" % (self.__class__, self.__testFunc.__name__) + + def __repr__(self): + return "<%s testFunc=%s>" % (self.__class__, self.__testFunc) + + def shortDescription(self): + if self.__description is not None: return self.__description + doc = self.__testFunc.__doc__ + return doc and string.strip(string.split(doc, "\n")[0]) or None + + + +############################################################################## +# Locating and loading tests +############################################################################## + +class TestLoader: + """This class is responsible for loading tests according to various + criteria and returning them wrapped in a Test + """ + testMethodPrefix = 'test' + sortTestMethodsUsing = cmp + suiteClass = TestSuite + + def loadTestsFromTestCase(self, testCaseClass): + """Return a suite of all tests cases contained in testCaseClass""" + return self.suiteClass(map(testCaseClass, + self.getTestCaseNames(testCaseClass))) + + def loadTestsFromModule(self, module): + """Return a suite of all tests cases contained in the given module""" + tests = [] + for name in dir(module): + obj = getattr(module, name) + if type(obj) == types.ClassType and issubclass(obj, TestCase): + tests.append(self.loadTestsFromTestCase(obj)) + return self.suiteClass(tests) + + def loadTestsFromName(self, name, module=None): + """Return a suite of all tests cases given a string specifier. + + The name may resolve either to a module, a test case class, a + test method within a test case class, or a callable object which + returns a TestCase or TestSuite instance. + + The method optionally resolves the names relative to a given module. + """ + parts = string.split(name, '.') + if module is None: + if not parts: + raise ValueError, "incomplete test name: %s" % name + else: + parts_copy = parts[:] + while parts_copy: + try: + module = __import__(string.join(parts_copy,'.')) + break + except ImportError: + del parts_copy[-1] + if not parts_copy: raise + parts = parts[1:] + obj = module + for part in parts: + obj = getattr(obj, part) + + import unittest + if type(obj) == types.ModuleType: + return self.loadTestsFromModule(obj) + elif type(obj) == types.ClassType and issubclass(obj, unittest.TestCase): + return self.loadTestsFromTestCase(obj) + elif type(obj) == types.UnboundMethodType: + return obj.im_class(obj.__name__) + elif callable(obj): + test = obj() + if not isinstance(test, unittest.TestCase) and \ + not isinstance(test, unittest.TestSuite): + raise ValueError, \ + "calling %s returned %s, not a test" % (obj,test) + return test + else: + raise ValueError, "don't know how to make test from: %s" % obj + + def loadTestsFromNames(self, names, module=None): + """Return a suite of all tests cases found using the given sequence + of string specifiers. See 'loadTestsFromName()'. + """ + suites = [] + for name in names: + suites.append(self.loadTestsFromName(name, module)) + return self.suiteClass(suites) + + def getTestCaseNames(self, testCaseClass): + """Return a sorted sequence of method names found within testCaseClass + """ + testFnNames = filter(lambda n,p=self.testMethodPrefix: n[:len(p)] == p, + dir(testCaseClass)) + for baseclass in testCaseClass.__bases__: + for testFnName in self.getTestCaseNames(baseclass): + if testFnName not in testFnNames: # handle overridden methods + testFnNames.append(testFnName) + if self.sortTestMethodsUsing: + testFnNames.sort(self.sortTestMethodsUsing) + return testFnNames + + + +defaultTestLoader = TestLoader() + + +############################################################################## +# Patches for old functions: these functions should be considered obsolete +############################################################################## + +def _makeLoader(prefix, sortUsing, suiteClass=None): + loader = TestLoader() + loader.sortTestMethodsUsing = sortUsing + loader.testMethodPrefix = prefix + if suiteClass: loader.suiteClass = suiteClass + return loader + +def getTestCaseNames(testCaseClass, prefix, sortUsing=cmp): + return _makeLoader(prefix, sortUsing).getTestCaseNames(testCaseClass) + +def makeSuite(testCaseClass, prefix='test', sortUsing=cmp, suiteClass=TestSuite): + return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromTestCase(testCaseClass) + +def findTestCases(module, prefix='test', sortUsing=cmp, suiteClass=TestSuite): + return _makeLoader(prefix, sortUsing, suiteClass).loadTestsFromModule(module) + + +############################################################################## +# Text UI +############################################################################## + +class _WritelnDecorator: + """Used to decorate file-like objects with a handy 'writeln' method""" + def __init__(self,stream): + self.stream = stream + + def __getattr__(self, attr): + return getattr(self.stream,attr) + + def writeln(self, *args): + if args: apply(self.write, args) + self.write('\n') # text-mode streams translate to \r\n if needed + + +class _TextTestResult(TestResult): + """A test result class that can print formatted text results to a stream. + + Used by TextTestRunner. + """ + separator1 = '=' * 70 + separator2 = '-' * 70 + + def __init__(self, stream, descriptions, verbosity): + TestResult.__init__(self) + self.stream = stream + self.showAll = verbosity > 1 + self.dots = verbosity == 1 + self.descriptions = descriptions + + def getDescription(self, test): + if self.descriptions: + return test.shortDescription() or str(test) + else: + return str(test) + + def startTest(self, test): + TestResult.startTest(self, test) + if self.showAll: + self.stream.write(self.getDescription(test)) + self.stream.write(" ... ") + + def addSuccess(self, test): + TestResult.addSuccess(self, test) + if self.showAll: + self.stream.writeln("ok") + elif self.dots: + self.stream.write('.') + + def addError(self, test, err): + TestResult.addError(self, test, err) + if self.showAll: + self.stream.writeln("ERROR") + elif self.dots: + self.stream.write('E') + + def addFailure(self, test, err): + TestResult.addFailure(self, test, err) + if self.showAll: + self.stream.writeln("FAIL") + elif self.dots: + self.stream.write('F') + + def printErrors(self): + if self.dots or self.showAll: + self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + def printErrorList(self, flavour, errors): + for test, err in errors: + self.stream.writeln(self.separator1) + self.stream.writeln("%s: %s" % (flavour,self.getDescription(test))) + self.stream.writeln(self.separator2) + self.stream.writeln("%s" % err) + + +class TextTestRunner: + """A test runner class that displays results in textual form. + + It prints out the names of tests as they are run, errors as they + occur, and a summary of the results at the end of the test run. + """ + def __init__(self, stream=sys.stderr, descriptions=1, verbosity=1): + self.stream = _WritelnDecorator(stream) + self.descriptions = descriptions + self.verbosity = verbosity + + def _makeResult(self): + return _TextTestResult(self.stream, self.descriptions, self.verbosity) + + def run(self, test): + "Run the given test case or test suite." + result = self._makeResult() + startTime = time.time() + test(result) + stopTime = time.time() + timeTaken = float(stopTime - startTime) + result.printErrors() + self.stream.writeln(result.separator2) + run = result.testsRun + self.stream.writeln("Ran %d test%s in %.3fs" % + (run, run != 1 and "s" or "", timeTaken)) + self.stream.writeln() + if not result.wasSuccessful(): + self.stream.write("FAILED (") + failed, errored = map(len, (result.failures, result.errors)) + if failed: + self.stream.write("failures=%d" % failed) + if errored: + if failed: self.stream.write(", ") + self.stream.write("errors=%d" % errored) + self.stream.writeln(")") + else: + self.stream.writeln("OK") + return result + + + +############################################################################## +# Facilities for running tests from the command line +############################################################################## + +class TestProgram: + """A command-line program that runs a set of tests; this is primarily + for making test modules conveniently executable. + """ + USAGE = """\ +Usage: %(progName)s [options] [test] [...] + +Options: + -h, --help Show this message + -v, --verbose Verbose output + -q, --quiet Minimal output + +Examples: + %(progName)s - run default set of tests + %(progName)s MyTestSuite - run suite 'MyTestSuite' + %(progName)s MyTestCase.testSomething - run MyTestCase.testSomething + %(progName)s MyTestCase - run all 'test*' test methods + in MyTestCase +""" + def __init__(self, module='__main__', defaultTest=None, + argv=None, testRunner=None, testLoader=defaultTestLoader): + if type(module) == type(''): + self.module = __import__(module) + for part in string.split(module,'.')[1:]: + self.module = getattr(self.module, part) + else: + self.module = module + if argv is None: + argv = sys.argv + self.verbosity = 1 + self.defaultTest = defaultTest + self.testRunner = testRunner + self.testLoader = testLoader + self.progName = os.path.basename(argv[0]) + self.parseArgs(argv) + self.runTests() + + def usageExit(self, msg=None): + if msg: print msg + print self.USAGE % self.__dict__ + sys.exit(2) + + def parseArgs(self, argv): + import getopt + try: + options, args = getopt.getopt(argv[1:], 'hHvq', + ['help','verbose','quiet']) + for opt, value in options: + if opt in ('-h','-H','--help'): + self.usageExit() + if opt in ('-q','--quiet'): + self.verbosity = 0 + if opt in ('-v','--verbose'): + self.verbosity = 2 + if len(args) == 0 and self.defaultTest is None: + self.test = self.testLoader.loadTestsFromModule(self.module) + return + if len(args) > 0: + self.testNames = args + else: + self.testNames = (self.defaultTest,) + self.createTests() + except getopt.error, msg: + self.usageExit(msg) + + def createTests(self): + self.test = self.testLoader.loadTestsFromNames(self.testNames, + self.module) + + def runTests(self): + if self.testRunner is None: + self.testRunner = TextTestRunner(verbosity=self.verbosity) + result = self.testRunner.run(self.test) + sys.exit(not result.wasSuccessful()) + +main = TestProgram + + +############################################################################## +# Executing this module from the command line +############################################################################## + +if __name__ == "__main__": + main(module=None) diff --git a/bin/reportlab/test/utils.py b/bin/reportlab/test/utils.py new file mode 100644 index 00000000000..7eb2f3c6c35 --- /dev/null +++ b/bin/reportlab/test/utils.py @@ -0,0 +1,308 @@ +"""Utilities for testing Python packages. +""" +import sys, os, string, fnmatch, copy, re +from ConfigParser import ConfigParser +from reportlab.test import unittest + +# Helper functions. +def isWritable(D): + try: + fn = '00DELETE.ME' + f = open(fn, 'w') + f.write('test of writability - can be deleted') + f.close() + if os.path.isfile(fn): + os.remove(fn) + return 1 + except: + return 0 + +_TEST_DIR_IS_WRITABLE = None #not known yet +_OUTDIR = None +def canWriteTestOutputHere(): + """Is it a writable file system distro being invoked within + test directory? If so, can write test output here. If not, + it had better go in a temp directory. Only do this once per + process""" + + global _TEST_DIR_IS_WRITABLE, _OUTDIR + if _TEST_DIR_IS_WRITABLE is not None: + return _TEST_DIR_IS_WRITABLE + + D = [d[9:] for d in sys.argv if d.startswith('--outdir=')] + if D: + _OUTDIR = D[-1] + try: + os.makedirs(_OUTDIR) + except: + pass + map(sys.argv.remove,D) + _TEST_DIR_IS_WRITABLE = isWritable(_OUTDIR) + else: + from reportlab.lib.utils import isSourceDistro + if isSourceDistro(): + curDir = os.getcwd() + parentDir = os.path.dirname(curDir) + if curDir.endswith('test') and parentDir.endswith('reportlab'): + #we're probably being run within the test directory. + #now check it's writeable. + _TEST_DIR_IS_WRITABLE = isWritable(curDir) + _OUTDIR = curDir + return _TEST_DIR_IS_WRITABLE + +def outputfile(fn): + """This works out where to write test output. If running + code in a zip file or a locked down file system, this will be a + temp directory; otherwise, the output of 'test_foo.py' will + normally be a file called 'test_foo.pdf', next door. + """ + if canWriteTestOutputHere(): + D = _OUTDIR + else: + from reportlab.lib.utils import isSourceDistro, get_rl_tempdir + D = get_rl_tempdir('reportlab_test') + if fn: D = os.path.join(D,fn) + return D + +def printLocation(depth=1): + if sys._getframe(depth).f_locals.get('__name__')=='__main__': + outDir = outputfile('') + if outDir!=_OUTDIR: + print 'Logs and output files written to folder "%s"' % outDir + +def makeSuiteForClasses(*classes): + "Return a test suite with tests loaded from provided classes." + + suite = unittest.TestSuite() + loader = unittest.TestLoader() + for C in classes: + suite.addTest(loader.loadTestsFromTestCase(C)) + return suite + +def getCVSEntries(folder, files=1, folders=0): + """Returns a list of filenames as listed in the CVS/Entries file. + + 'folder' is the folder that should contain the CVS subfolder. + If there is no such subfolder an empty list is returned. + 'files' is a boolean; 1 and 0 means to return files or not. + 'folders' is a boolean; 1 and 0 means to return folders or not. + """ + + join = os.path.join + split = string.split + + # If CVS subfolder doesn't exist return empty list. + try: + f = open(join(folder, 'CVS', 'Entries')) + except IOError: + return [] + + # Return names of files and/or folders in CVS/Entries files. + allEntries = [] + for line in f.readlines(): + if folders and line[0] == 'D' \ + or files and line[0] != 'D': + entry = split(line, '/')[1] + if entry: + allEntries.append(join(folder, entry)) + + return allEntries + + +# Still experimental class extending ConfigParser's behaviour. +class ExtConfigParser(ConfigParser): + "A slightly extended version to return lists of strings." + + pat = re.compile('\s*\[.*\]\s*') + + def getstringlist(self, section, option): + "Coerce option to a list of strings or return unchanged if that fails." + + value = apply(ConfigParser.get, (self, section, option)) + + # This seems to allow for newlines inside values + # of the config file, but be careful!! + val = string.replace(value, '\n', '') + + if self.pat.match(val): + return eval(val) + else: + return value + + +# This class as suggested by /F with an additional hook +# to be able to filter filenames. + +class GlobDirectoryWalker: + "A forward iterator that traverses files in a directory tree." + + def __init__(self, directory, pattern='*'): + self.index = 0 + self.pattern = pattern + directory.replace('/',os.sep) + if os.path.isdir(directory): + self.stack = [directory] + self.files = [] + else: + from reportlab.lib.utils import isCompactDistro, __loader__, rl_isdir + if not isCompactDistro() or not __loader__ or not rl_isdir(directory): + raise ValueError('"%s" is not a directory' % directory) + self.directory = directory[len(__loader__.archive)+len(os.sep):] + pfx = self.directory+os.sep + n = len(pfx) + self.files = map(lambda x, n=n: x[n:],filter(lambda x,pfx=pfx: x.startswith(pfx),__loader__._files.keys())) + self.stack = [] + + def __getitem__(self, index): + while 1: + try: + file = self.files[self.index] + self.index = self.index + 1 + except IndexError: + # pop next directory from stack + self.directory = self.stack.pop() + self.files = os.listdir(self.directory) + # now call the hook + self.files = self.filterFiles(self.directory, self.files) + self.index = 0 + else: + # got a filename + fullname = os.path.join(self.directory, file) + if os.path.isdir(fullname) and not os.path.islink(fullname): + self.stack.append(fullname) + if fnmatch.fnmatch(file, self.pattern): + return fullname + + def filterFiles(self, folder, files): + "Filter hook, overwrite in subclasses as needed." + + return files + + +class RestrictedGlobDirectoryWalker(GlobDirectoryWalker): + "An restricted directory tree iterator." + + def __init__(self, directory, pattern='*', ignore=None): + apply(GlobDirectoryWalker.__init__, (self, directory, pattern)) + + if ignore == None: + ignore = [] + self.ignoredPatterns = [] + if type(ignore) == type([]): + for p in ignore: + self.ignoredPatterns.append(p) + elif type(ignore) == type(''): + self.ignoredPatterns.append(ignore) + + + def filterFiles(self, folder, files): + "Filters all items from files matching patterns to ignore." + + indicesToDelete = [] + for i in xrange(len(files)): + f = files[i] + for p in self.ignoredPatterns: + if fnmatch.fnmatch(f, p): + indicesToDelete.append(i) + indicesToDelete.reverse() + for i in indicesToDelete: + del files[i] + + return files + + +class CVSGlobDirectoryWalker(GlobDirectoryWalker): + "An directory tree iterator that checks for CVS data." + + def filterFiles(self, folder, files): + """Filters files not listed in CVS subfolder. + + This will look in the CVS subfolder of 'folder' for + a file named 'Entries' and filter all elements from + the 'files' list that are not listed in 'Entries'. + """ + + join = os.path.join + cvsFiles = getCVSEntries(folder) + if cvsFiles: + indicesToDelete = [] + for i in xrange(len(files)): + f = files[i] + if join(folder, f) not in cvsFiles: + indicesToDelete.append(i) + indicesToDelete.reverse() + for i in indicesToDelete: + del files[i] + + return files + + +# An experimental untested base class with additional 'security'. + +class SecureTestCase(unittest.TestCase): + """Secure testing base class with additional pre- and postconditions. + + We try to ensure that each test leaves the environment it has + found unchanged after the test is performed, successful or not. + + Currently we restore sys.path and the working directory, but more + of this could be added easily, like removing temporary files or + similar things. + + Use this as a base class replacing unittest.TestCase and call + these methods in subclassed versions before doing your own + business! + """ + + def setUp(self): + "Remember sys.path and current working directory." + + self._initialPath = copy.copy(sys.path) + self._initialWorkDir = os.getcwd() + + + def tearDown(self): + "Restore previous sys.path and working directory." + + sys.path = self._initialPath + os.chdir(self._initialWorkDir) + + +class ScriptThatMakesFileTest(unittest.TestCase): + """Runs a Python script at OS level, expecting it to produce a file. + + It CDs to the working directory to run the script.""" + def __init__(self, scriptDir, scriptName, outFileName, verbose=0): + self.scriptDir = scriptDir + self.scriptName = scriptName + self.outFileName = outFileName + self.verbose = verbose + # normally, each instance is told which method to run) + unittest.TestCase.__init__(self) + + def setUp(self): + + self.cwd = os.getcwd() + #change to reportlab directory first, so that + #relative paths may be given to scriptdir + import reportlab + self.rl_dir = os.path.dirname(reportlab.__file__) + self.fn = __file__ + os.chdir(self.rl_dir) + + os.chdir(self.scriptDir) + assert os.path.isfile(self.scriptName), "Script %s not found!" % self.scriptName + if os.path.isfile(self.outFileName): + os.remove(self.outFileName) + + def tearDown(self): + os.chdir(self.cwd) + + def runTest(self): + fmt = sys.platform=='win32' and '"%s" %s' or '%s %s' + p = os.popen(fmt % (sys.executable,self.scriptName),'r') + out = p.read() + if self.verbose: + print out + status = p.close() + assert os.path.isfile(self.outFileName), "File %s not created!" % self.outFileName diff --git a/bin/reportlab/tools/README b/bin/reportlab/tools/README new file mode 100644 index 00000000000..f5f75d08de3 --- /dev/null +++ b/bin/reportlab/tools/README @@ -0,0 +1,10 @@ +This directory is the home of various ReportLab tools. +They are packaged such that they can be used more easily. + +Tool candidates are: + + - PythonPoint + - docpy.py + - graphdocpy.py + - ... + diff --git a/bin/reportlab/tools/__init__.py b/bin/reportlab/tools/__init__.py new file mode 100644 index 00000000000..da52b52d322 --- /dev/null +++ b/bin/reportlab/tools/__init__.py @@ -0,0 +1,3 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/__init__.py diff --git a/bin/reportlab/tools/docco/README b/bin/reportlab/tools/docco/README new file mode 100644 index 00000000000..b23b5661348 --- /dev/null +++ b/bin/reportlab/tools/docco/README @@ -0,0 +1,8 @@ +This directory contains a number of tools to do with documentation and +documentation building. + +Some of these are our own internal tools which we use for building the +ReportLab documentation. Some tools are obsolete and will be removed +in due course. + +In the mean time, use at your own risk. diff --git a/bin/reportlab/tools/docco/__init__.py b/bin/reportlab/tools/docco/__init__.py new file mode 100644 index 00000000000..3a0b73da24d --- /dev/null +++ b/bin/reportlab/tools/docco/__init__.py @@ -0,0 +1,3 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/__init__.py diff --git a/bin/reportlab/tools/docco/codegrab.py b/bin/reportlab/tools/docco/codegrab.py new file mode 100644 index 00000000000..87a580a2a3e --- /dev/null +++ b/bin/reportlab/tools/docco/codegrab.py @@ -0,0 +1,228 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/codegrab.py +#codegrab.py +""" +This grabs various Python class, method and function +headers and their doc strings to include in documents +""" + +import imp +import types +import string +import os +import sys + +class Struct: + pass + +def getObjectsDefinedIn(modulename, directory=None): + """Returns two tuple of (functions, classes) defined + in the given module. 'directory' must be the directory + containing the script; modulename should not include + the .py suffix""" + + if directory: + searchpath = [directory] + else: + searchpath = sys.path # searches usual Python path + + #might be a package. If so, check the top level + #package is there, then recalculate the path needed + words = string.split(modulename, '.') + if len(words) > 1: + packagename = words[0] + packagefound = imp.find_module(packagename, searchpath) + assert packagefound, "Package %s not found" % packagename + (file, packagepath, description) = packagefound + #now the full path should be known, if it is in the + #package + + directory = apply(os.path.join, tuple([packagepath] + words[1:-1])) + modulename = words[-1] + searchpath = [directory] + + + + #find and import the module. + found = imp.find_module(modulename, searchpath) + assert found, "Module %s not found" % modulename + (file, pathname, description) = found + mod = imp.load_module(modulename, file, pathname, description) + + #grab the code too, minus trailing newlines + lines = open(pathname, 'r').readlines() + lines = map(string.rstrip, lines) + + result = Struct() + result.functions = [] + result.classes = [] + result.doc = mod.__doc__ + for name in dir(mod): + value = getattr(mod, name) + if type(value) is types.FunctionType: + path, file = os.path.split(value.func_code.co_filename) + root, ext = os.path.splitext(file) + #we're possibly interested in it + if root == modulename: + #it was defined here + funcObj = value + fn = Struct() + fn.name = name + fn.proto = getFunctionPrototype(funcObj, lines) + if funcObj.__doc__: + fn.doc = dedent(funcObj.__doc__) + else: + fn.doc = '(no documentation string)' + #is it official? + if name[0:1] == '_': + fn.status = 'private' + elif name[-1] in '0123456789': + fn.status = 'experimental' + else: + fn.status = 'official' + + result.functions.append(fn) + elif type(value) == types.ClassType: + if value.__module__ == modulename: + cl = Struct() + cl.name = name + if value.__doc__: + cl.doc = dedent(value.__doc__) + else: + cl.doc = "(no documentation string)" + + cl.bases = [] + for base in value.__bases__: + cl.bases.append(base.__name__) + if name[0:1] == '_': + cl.status = 'private' + elif name[-1] in '0123456789': + cl.status = 'experimental' + else: + cl.status = 'official' + + cl.methods = [] + #loop over dict finding methods defined here + # Q - should we show all methods? + # loop over dict finding methods defined here + items = value.__dict__.items() + items.sort() + for (key2, value2) in items: + if type(value2) <> types.FunctionType: + continue # not a method + elif os.path.splitext(value2.func_code.co_filename)[0] == modulename: + continue # defined in base class + else: + #we want it + meth = Struct() + meth.name = key2 + name2 = value2.func_code.co_name + meth.proto = getFunctionPrototype(value2, lines) + if name2!=key2: + meth.doc = 'pointer to '+name2 + meth.proto = string.replace(meth.proto,name2,key2) + else: + if value2.__doc__: + meth.doc = dedent(value2.__doc__) + else: + meth.doc = "(no documentation string)" + #is it official? + if key2[0:1] == '_': + meth.status = 'private' + elif key2[-1] in '0123456789': + meth.status = 'experimental' + else: + meth.status = 'official' + cl.methods.append(meth) + result.classes.append(cl) + return result + +def getFunctionPrototype(f, lines): + """Pass in the function object and list of lines; + it extracts the header as a multiline text block.""" + firstLineNo = f.func_code.co_firstlineno - 1 + lineNo = firstLineNo + brackets = 0 + while 1: + line = lines[lineNo] + for char in line: + if char == '(': + brackets = brackets + 1 + elif char == ')': + brackets = brackets - 1 + if brackets == 0: + break + else: + lineNo = lineNo + 1 + + usefulLines = lines[firstLineNo:lineNo+1] + return string.join(usefulLines, '\n') + + +def dedent(comment): + """Attempts to dedent the lines to the edge. Looks at no. + of leading spaces in line 2, and removes up to that number + of blanks from other lines.""" + commentLines = string.split(comment, '\n') + if len(commentLines) < 2: + cleaned = map(string.lstrip, commentLines) + else: + spc = 0 + for char in commentLines[1]: + if char in string.whitespace: + spc = spc + 1 + else: + break + #now check other lines + cleaned = [] + for line in commentLines: + for i in range(min(len(line),spc)): + if line[0] in string.whitespace: + line = line[1:] + cleaned.append(line) + return string.join(cleaned, '\n') + + + +def dumpDoc(modulename, directory=None): + """Test support. Just prints docco on the module + to standard output.""" + docco = getObjectsDefinedIn(modulename, directory) + print 'codegrab.py - ReportLab Documentation Utility' + print 'documenting', modulename + '.py' + print '-------------------------------------------------------' + print + if docco.functions == []: + print 'No functions found' + else: + print 'Functions:' + for f in docco.functions: + print f.proto + print ' ' + f.doc + + if docco.classes == []: + print 'No classes found' + else: + print 'Classes:' + for c in docco.classes: + print c.name + print ' ' + c.doc + for m in c.methods: + print m.proto # it is already indented in the file! + print ' ' + m.doc + print + +def test(m='reportlab.platypus.paragraph'): + dumpDoc(m) + +if __name__=='__main__': + import sys + print 'Path to search:' + for line in sys.path: + print ' ',line + M = sys.argv[1:] + if M==[]: + M.append('reportlab.platypus.paragraph') + for m in M: + test(m) \ No newline at end of file diff --git a/bin/reportlab/tools/docco/docpy.py b/bin/reportlab/tools/docco/docpy.py new file mode 100644 index 00000000000..75a77efe773 --- /dev/null +++ b/bin/reportlab/tools/docco/docpy.py @@ -0,0 +1,1247 @@ +#!/usr/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/docpy.py + +"""Generate documentation from live Python objects. + +This is an evolving module that allows to generate documentation +for python modules in an automated fashion. The idea is to take +live Python objects and inspect them in order to use as much mean- +ingful information as possible to write in some formatted way into +different types of documents. + +In principle a skeleton captures the gathered information and +makes it available via a certain API to formatters that use it +in whatever way they like to produce something of interest. The +API allows for adding behaviour in subclasses of these formatters, +such that, e.g. for certain classes it is possible to trigger +special actions like displaying a sample image of a class that +represents some graphical widget, say. + +Type the following for usage info: + + python docpy.py -h +""" + +# Much inspired by Ka-Ping Yee's htmldoc.py. +# Needs the inspect module. + +# Dinu Gherman + + +__version__ = '0.8' + + +import sys, os, re, types, string, getopt, copy, time +from string import find, join, split, replace, expandtabs, rstrip + +from reportlab.pdfgen import canvas +from reportlab.lib import colors +from reportlab.lib.units import inch, cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib import enums +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.platypus.flowables import Flowable, Spacer +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.flowables \ + import Flowable, Preformatted,Spacer, Image, KeepTogether, PageBreak +from reportlab.platypus.tableofcontents import TableOfContents +from reportlab.platypus.xpreformatted import XPreformatted +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate \ + import PageTemplate, BaseDocTemplate +from reportlab.platypus.tables import TableStyle, Table +import inspect + +#################################################################### +# +# Stuff needed for building PDF docs. +# +#################################################################### + + +def mainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + pageNumber = canvas.getPageNumber() + canvas.line(2*cm, A4[1]-2*cm, A4[0]-2*cm, A4[1]-2*cm) + canvas.line(2*cm, 2*cm, A4[0]-2*cm, 2*cm) + if pageNumber > 1: + canvas.setFont('Times-Roman', 12) + canvas.drawString(4 * inch, cm, "%d" % pageNumber) + if hasattr(canvas, 'headerLine'): # hackish + headerline = string.join(canvas.headerLine, ' \215 ') # bullet + canvas.drawString(2*cm, A4[1]-1.75*cm, headerline) + + canvas.setFont('Times-Roman', 8) + msg = "Generated with reportlab.lib.docpy. See http://www.reportlab.com!" + canvas.drawString(2*cm, 1.65*cm, msg) + + canvas.restoreState() + + +class MyTemplate(BaseDocTemplate): + "The document template used for all PDF documents." + + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + self.addPageTemplates(PageTemplate('normal', [frame1], mainPageFrame)) + + + def afterFlowable(self, flowable): + "Takes care of header line, TOC and outline entries." + + if flowable.__class__.__name__ == 'Paragraph': + f = flowable + name7 = f.style.name[:7] + name8 = f.style.name[:8] + + # Build a list of heading parts. + # So far, this is the *last* item on the *previous* page... + if name7 == 'Heading' and not hasattr(self.canv, 'headerLine'): + self.canv.headerLine = [] + + if name8 == 'Heading0': + self.canv.headerLine = [f.text] # hackish + elif name8 == 'Heading1': + if len(self.canv.headerLine) == 2: + del self.canv.headerLine[-1] + elif len(self.canv.headerLine) == 3: + del self.canv.headerLine[-1] + del self.canv.headerLine[-1] + self.canv.headerLine.append(f.text) + elif name8 == 'Heading2': + if len(self.canv.headerLine) == 3: + del self.canv.headerLine[-1] + self.canv.headerLine.append(f.text) + + if name7 == 'Heading': + # Register TOC entries. + headLevel = int(f.style.name[7:]) + self.notify('TOCEntry', (headLevel, flowable.getPlainText(), self.page)) + + # Add PDF outline entries. + c = self.canv + title = f.text + key = str(hash(f)) + + try: + if headLevel == 0: + isClosed = 0 + else: + isClosed = 1 + + c.bookmarkPage(key) + c.addOutlineEntry(title, key, level=headLevel, + closed=isClosed) + except ValueError: + pass + + +#################################################################### +# +# Utility functions (Ka-Ping Yee). +# +#################################################################### + +def htmlescape(text): + "Escape special HTML characters, namely &, <, >." + return replace(replace(replace(text, '&', '&'), + '<', '<'), + '>', '>') + +def htmlrepr(object): + return htmlescape(repr(object)) + + +def defaultformat(object): + return '=' + htmlrepr(object) + + +def getdoc(object): + result = inspect.getdoc(object) + if not result: + try: + result = inspect.getcomments(object) + except: + pass + return result and rstrip(result) + '\n' or '' + + +def reduceDocStringLength(docStr): + "Return first line of a multiline string." + + return split(docStr, '\n')[0] + + +#################################################################### +# +# More utility functions +# +#################################################################### + +def makeHtmlSection(text, bgcolor='#FFA0FF'): + """Create HTML code for a section. + + This is usually a header for all classes or functions. +u """ + text = htmlescape(expandtabs(text)) + result = [] + result.append("""""") + result.append("""
              """ % bgcolor) + result.append("""

              %s

              """ % text) + result.append("""
              """) + result.append('') + + return join(result, '\n') + + +def makeHtmlSubSection(text, bgcolor='#AAA0FF'): + """Create HTML code for a subsection. + + This is usually a class or function name. + """ + text = htmlescape(expandtabs(text)) + result = [] + result.append("""""") + result.append("""
              """ % bgcolor) + result.append("""

              %s

              """ % text) + result.append("""
              """) + result.append('') + + return join(result, '\n') + + +def makeHtmlInlineImage(text): + """Create HTML code for an inline image. + """ + + return """%s""" % (text, text) + + +#################################################################### +# +# Core "standard" docpy classes +# +#################################################################### + +class PackageSkeleton0: + """A class collecting 'interesting' information about a package.""" + pass # Not yet! + + +class ModuleSkeleton0: + """A class collecting 'interesting' information about a module.""" + + def __init__(self): + # This is an ad-hoc, somewhat questionable 'data structure', + # but for the time being it serves its purpose and is fairly + # self-contained. + self.module = {} + self.functions = {} + self.classes = {} + + + # Might need more like this, later. + def getModuleName(self): + """Return the name of the module being treated.""" + + return self.module['name'] + + + # These inspect methods all rely on the inspect module. + def inspect(self, object): + """Collect information about a given object.""" + + self.moduleSpace = object + + # Very non-OO, left for later... + if inspect.ismodule(object): + self._inspectModule(object) + elif inspect.isclass(object): + self._inspectClass(object) + elif inspect.ismethod(object): + self._inspectMethod(object) + elif inspect.isfunction(object): + self._inspectFunction(object) + elif inspect.isbuiltin(object): + self._inspectBuiltin(object) + else: + msg = "Don't know how to document this kind of object." + raise TypeError, msg + + + def _inspectModule(self, object): + """Collect information about a given module object.""" + name = object.__name__ + + self.module['name'] = name + if hasattr(object, '__version__'): + self.module['version'] = object.__version__ + + cadr = lambda list: list[1] + modules = map(cadr, inspect.getmembers(object, inspect.ismodule)) + + classes, cdict = [], {} + for key, value in inspect.getmembers(object, inspect.isclass): + if (inspect.getmodule(value) or object) is object: + classes.append(value) + cdict[key] = cdict[value] = '#' + key + + functions, fdict = [], {} + for key, value in inspect.getmembers(object, inspect.isroutine): + #if inspect.isbuiltin(value) or inspect.getmodule(value) is object: + functions.append(value) + fdict[key] = '#-' + key + if inspect.isfunction(value): fdict[value] = fdict[key] + + for c in classes: + for base in c.__bases__: + key, modname = base.__name__, base.__module__ + if modname != name and sys.modules.has_key(modname): + module = sys.modules[modname] + if hasattr(module, key) and getattr(module, key) is base: + if not cdict.has_key(key): + cdict[key] = cdict[base] = modname + '.txt#' + key + +## doc = getdoc(object) or 'No doc string.' + doc = getdoc(object) + self.module['doc'] = doc + + if modules: + self.module['importedModules'] = map(lambda m:m.__name__, modules) + + if classes: + for item in classes: + self._inspectClass(item, fdict, cdict) + + if functions: + for item in functions: + self._inspectFunction(item, fdict, cdict) + + + def _inspectClass(self, object, functions={}, classes={}): + """Collect information about a given class object.""" + + name = object.__name__ + bases = object.__bases__ + results = [] + + if bases: + parents = [] + for base in bases: + parents.append(base) + + self.classes[name] = {} + if bases: + self.classes[name]['bases'] = parents + + methods, mdict = [], {} + for key, value in inspect.getmembers(object, inspect.ismethod): + methods.append(value) + mdict[key] = mdict[value] = '#' + name + '-' + key + + if methods: + if not self.classes[name].has_key('methods'): + self.classes[name]['methods'] = {} + for item in methods: + self._inspectMethod(item, functions, classes, mdict, name) + +## doc = getdoc(object) or 'No doc string.' + doc = getdoc(object) + self.classes[name]['doc'] = doc + + + def _inspectMethod(self, object, functions={}, classes={}, methods={}, clname=''): + """Collect information about a given method object.""" + + self._inspectFunction(object.im_func, functions, classes, methods, clname) + + + def _inspectFunction(self, object, functions={}, classes={}, methods={}, clname=''): + """Collect information about a given function object.""" + try: + args, varargs, varkw, defaults = inspect.getargspec(object) + argspec = inspect.formatargspec( + args, varargs, varkw, defaults, + defaultformat=defaultformat) + except TypeError: + argspec = '( ... )' + +## doc = getdoc(object) or 'No doc string.' + doc = getdoc(object) + + if object.__name__ == '': + decl = [' lambda ', argspec[1:-1]] + # print ' %s' % decl + # Do something with lambda functions as well... + # ... + else: + decl = object.__name__ + if not clname: + self.functions[object.__name__] = {'signature':argspec, 'doc':doc} + else: + theMethods = self.classes[clname]['methods'] + if not theMethods.has_key(object.__name__): + theMethods[object.__name__] = {} + + theMethod = theMethods[object.__name__] + theMethod['signature'] = argspec + theMethod['doc'] = doc + + + def _inspectBuiltin(self, object): + """Collect information about a given built-in.""" + + print object.__name__ + '( ... )' + + + def walk(self, formatter): + """Call event methods in a visiting formatter.""" + + s = self + f = formatter + + # The order is fixed, but could be made flexible + # with one more template method... + + # Module + modName = s.module['name'] + modDoc = s.module['doc'] + imported = s.module.get('importedModules', []) + imported.sort() + # f.indentLevel = f.indentLevel + 1 + f.beginModule(modName, modDoc, imported) + + # Classes + f.indentLevel = f.indentLevel + 1 + f.beginClasses(s.classes.keys()) + items = s.classes.items() + items.sort() + for k, v in items: + cDoc = s.classes[k]['doc'] + bases = s.classes[k].get('bases', []) + f.indentLevel = f.indentLevel + 1 + f.beginClass(k, cDoc, bases) + + # This if should move out of this method. + if not s.classes[k].has_key('methods'): + s.classes[k]['methods'] = {} + + # Methods + #f.indentLevel = f.indentLevel + 1 + f.beginMethods(s.classes[k]['methods'].keys()) + items = s.classes[k]['methods'].items() + items.sort() + for m, v in items: + mDoc = v['doc'] + sig = v['signature'] + f.indentLevel = f.indentLevel + 1 + f.beginMethod(m, mDoc, sig) + f.indentLevel = f.indentLevel - 1 + f.endMethod(m, mDoc, sig) + + #f.indentLevel = f.indentLevel - 1 + f.endMethods(s.classes[k]['methods'].keys()) + + f.indentLevel = f.indentLevel - 1 + f.endClass(k, cDoc, bases) + + # And what about attributes?! + + f.indentLevel = f.indentLevel - 1 + f.endClasses(s.classes.keys()) + + # Functions + f.indentLevel = f.indentLevel + 1 + f.beginFunctions(s.functions.keys()) + items = s.functions.items() + items.sort() + for k, v in items: + doc = v['doc'] + sig = v['signature'] + f.indentLevel = f.indentLevel + 1 + f.beginFunction(k, doc, sig) + f.indentLevel = f.indentLevel - 1 + f.endFunction(k, doc, sig) + f.indentLevel = f.indentLevel - 1 + f.endFunctions(s.functions.keys()) + + #f.indentLevel = f.indentLevel - 1 + f.endModule(modName, modDoc, imported) + + # Constants?! + + +#################################################################### +# +# Core "standard" docpy document builders +# +#################################################################### + +class DocBuilder0: + """An abstract class to document the skeleton of a Python module. + + Instances take a skeleton instance s and call their s.walk() + method. The skeleton, in turn, will walk over its tree structure + while generating events and calling equivalent methods from a + specific interface (begin/end methods). + """ + + fileSuffix = None + + def __init__(self, skeleton=None): + self.skeleton = skeleton + self.packageName = None + self.indentLevel = 0 + + + def write(self, skeleton=None): + if skeleton: + self.skeleton = skeleton + self.skeleton.walk(self) + + + # Event-method API, called by associated skeleton instances. + # In fact, these should raise a NotImplementedError, but for now we + # just don't do anything here. + + # The following four methods are *not* called by skeletons! + def begin(self, name='', typ=''): pass + def end(self): pass + + # Methods for packaging should move into a future PackageSkeleton... + def beginPackage(self, name): + self.packageName = name + + def endPackage(self, name): + pass + + # Only this subset is really called by associated skeleton instances. + + def beginModule(self, name, doc, imported): pass + def endModule(self, name, doc, imported): pass + + def beginClasses(self, names): pass + def endClasses(self, names): pass + + def beginClass(self, name, doc, bases): pass + def endClass(self, name, doc, bases): pass + + def beginMethods(self, names): pass + def endMethods(self, names): pass + + def beginMethod(self, name, doc, sig): pass + def endMethod(self, name, doc, sig): pass + + def beginFunctions(self, names): pass + def endFunctions(self, names): pass + + def beginFunction(self, name, doc, sig): pass + def endFunction(self, name, doc, sig): pass + + +class AsciiDocBuilder0(DocBuilder0): + """Document the skeleton of a Python module in ASCII format. + + The output will be an ASCII file with nested lines representing + the hiearchical module structure. + + Currently, no doc strings are listed. + """ + + fileSuffix = '.txt' + outLines = [] + indentLabel = ' ' + + def end(self): + # This if should move into DocBuilder0... + if self.packageName: + self.outPath = self.packageName + self.fileSuffix + elif self.skeleton: + self.outPath = self.skeleton.getModuleName() + self.fileSuffix + else: + self.outPath = '' + + if self.outPath: + file = open(self.outPath, 'w') + for line in self.outLines: + file.write(line + '\n') + file.close() + + + def beginPackage(self, name): + DocBuilder0.beginPackage(self, name) + lev, label = self.indentLevel, self.indentLabel + self.outLines.append('%sPackage: %s' % (lev*label, name)) + self.outLines.append('') + + + def beginModule(self, name, doc, imported): + append = self.outLines.append + lev, label = self.indentLevel, self.indentLabel + self.outLines.append('%sModule: %s' % (lev*label, name)) +## self.outLines.append('%s%s' % ((lev+1)*label, reduceDocStringLength(doc))) + append('') + + if imported: + self.outLines.append('%sImported' % ((lev+1)*label)) + append('') + for m in imported: + self.outLines.append('%s%s' % ((lev+2)*label, m)) + append('') + + + def beginClasses(self, names): + if names: + lev, label = self.indentLevel, self.indentLabel + self.outLines.append('%sClasses' % (lev*label)) + self.outLines.append('') + + + def beginClass(self, name, doc, bases): + append = self.outLines.append + lev, label = self.indentLevel, self.indentLabel + + if bases: + bases = map(lambda b:b.__name__, bases) # hack + append('%s%s(%s)' % (lev*label, name, join(bases, ', '))) + else: + append('%s%s' % (lev*label, name)) + return + +## append('%s%s' % ((lev+1)*label, reduceDocStringLength(doc))) + self.outLines.append('') + + + def endClass(self, name, doc, bases): + self.outLines.append('') + + + def beginMethod(self, name, doc, sig): + append = self.outLines.append + lev, label = self.indentLevel, self.indentLabel + append('%s%s%s' % (lev*label, name, sig)) +## append('%s%s' % ((lev+1)*label, reduceDocStringLength(doc))) +## append('') + + + def beginFunctions(self, names): + if names: + lev, label = self.indentLevel, self.indentLabel + self.outLines.append('%sFunctions' % (lev*label)) + self.outLines.append('') + + + def endFunctions(self, names): + self.outLines.append('') + + + def beginFunction(self, name, doc, sig): + append = self.outLines.append + lev, label = self.indentLevel, self.indentLabel + self.outLines.append('%s%s%s' % (lev*label, name, sig)) +## append('%s%s' % ((lev+1)*label, reduceDocStringLength(doc))) +## append('') + + +class HtmlDocBuilder0(DocBuilder0): + "A class to write the skeleton of a Python source in HTML format." + + fileSuffix = '.html' + outLines = [] + + def begin(self, name='', typ=''): + self.outLines.append("""""") + self.outLines.append("""""") + + + def end(self): + if self.packageName: + self.outPath = self.packageName + self.fileSuffix + elif self.skeleton: + self.outPath = self.skeleton.getModuleName() + self.fileSuffix + else: + self.outPath = '' + + if self.outPath: + file = open(self.outPath, 'w') + self.outLines.append('') + for line in self.outLines: + file.write(line + '\n') + file.close() + + + def beginPackage(self, name): + DocBuilder0.beginPackage(self, name) + + self.outLines.append("""%s""" % name) + self.outLines.append("""""") + self.outLines.append("""

              %s

              """ % name) + self.outLines.append('') + + + def beginModule(self, name, doc, imported): + if not self.packageName: + self.outLines.append("""%s""" % name) + self.outLines.append("""""") + + self.outLines.append("""

              %s

              """ % name) + self.outLines.append('') + for line in split(doc, '\n'): + self.outLines.append("""%s""" % htmlescape(line)) + self.outLines.append('
              ') + self.outLines.append('') + + if imported: + self.outLines.append(makeHtmlSection('Imported Modules')) + self.outLines.append("""
                """) + for m in imported: + self.outLines.append("""
              • %s
              • """ % m) + self.outLines.append("""
              """) + + + def beginClasses(self, names): + self.outLines.append(makeHtmlSection('Classes')) + + + def beginClass(self, name, doc, bases): + DocBuilder0.beginClass(self, name, doc, bases) + +## # Keep an eye on the base classes. +## self.currentBaseClasses = bases + + if bases: + bases = map(lambda b:b.__name__, bases) # hack + self.outLines.append(makeHtmlSubSection('%s(%s)' % (name, join(bases, ', ')))) + else: + self.outLines.append(makeHtmlSubSection('%s' % name)) + for line in split(doc, '\n'): + self.outLines.append("""%s""" % htmlescape(line)) + self.outLines.append('
              ') + + self.outLines.append('') + + + def beginMethods(self, names): + pass +## if names: +## self.outLines.append('

              Method Interface

              ') +## self.outLines.append('') + + + def beginMethod(self, name, doc, sig): + self.beginFunction(name, doc, sig) + + + def beginFunctions(self, names): + self.outLines.append(makeHtmlSection('Functions')) + + + def beginFunction(self, name, doc, sig): + append = self.outLines.append + append("""
              %s%s
              """ % (name, sig)) + append('') + for line in split(doc, '\n'): + append("""
              %s
              """ % htmlescape(line)) + append('
              ') + append('
              ') + append('') + + +class PdfDocBuilder0(DocBuilder0): + "Document the skeleton of a Python module in PDF format." + + fileSuffix = '.pdf' + + def makeHeadingStyle(self, level, typ=None, doc=''): + "Make a heading style for different types of module content." + + if typ in ('package', 'module', 'class'): + style = ParagraphStyle(name='Heading'+str(level), + fontName = 'Courier-Bold', + fontSize=14, + leading=18, + spaceBefore=12, + spaceAfter=6) + elif typ in ('method', 'function'): + if doc: + style = ParagraphStyle(name='Heading'+str(level), + fontName = 'Courier-Bold', + fontSize=12, + leading=18, + firstLineIndent=-18, + leftIndent=36, + spaceBefore=0, + spaceAfter=-3) + else: + style = ParagraphStyle(name='Heading'+str(level), + fontName = 'Courier-Bold', + fontSize=12, + leading=18, + firstLineIndent=-18, + leftIndent=36, + spaceBefore=0, + spaceAfter=0) + + else: + style = ParagraphStyle(name='Heading'+str(level), + fontName = 'Times-Bold', + fontSize=14, + leading=18, + spaceBefore=12, + spaceAfter=6) + + return style + + + def begin(self, name='', typ=''): + styleSheet = getSampleStyleSheet() + self.code = styleSheet['Code'] + self.bt = styleSheet['BodyText'] + self.story = [] + + # Cover page + t = time.gmtime(time.time()) + timeString = time.strftime("%Y-%m-%d %H:%M", t) + self.story.append(Paragraph('Documentation for %s "%s"' % (typ, name), self.bt)) + self.story.append(Paragraph('Generated by: docpy.py version %s' % __version__, self.bt)) + self.story.append(Paragraph('Date generated: %s' % timeString, self.bt)) + self.story.append(Paragraph('Format: PDF', self.bt)) + self.story.append(PageBreak()) + + # Table of contents + toc = TableOfContents() + self.story.append(toc) + self.story.append(PageBreak()) + + + def end(self): + if self.outPath is not None: + pass + elif self.packageName: + self.outPath = self.packageName + self.fileSuffix + elif self.skeleton: + self.outPath = self.skeleton.getModuleName() + self.fileSuffix + else: + self.outPath = '' + print 'output path is %s' % self.outPath + if self.outPath: + doc = MyTemplate(self.outPath) + doc.multiBuild(self.story) + + + def beginPackage(self, name): + DocBuilder0.beginPackage(self, name) + story = self.story + story.append(Paragraph(name, self.makeHeadingStyle(self.indentLevel, 'package'))) + + + def beginModule(self, name, doc, imported): + story = self.story + bt = self.bt + story.append(Paragraph(name, self.makeHeadingStyle(self.indentLevel, 'module'))) + if doc: + story.append(XPreformatted(htmlescape(doc), bt)) + story.append(XPreformatted('', bt)) + + if imported: + story.append(Paragraph('Imported modules', self.makeHeadingStyle(self.indentLevel + 1))) + for m in imported: + p = Paragraph('\201 %s' % m, bt) + p.style.bulletIndent = 10 + p.style.leftIndent = 18 + story.append(p) + + + def endModule(self, name, doc, imported): + DocBuilder0.endModule(self, name, doc, imported) + self.story.append(PageBreak()) + + + def beginClasses(self, names): + self.story.append(Paragraph('Classes', self.makeHeadingStyle(self.indentLevel))) + + + def beginClass(self, name, doc, bases): + bt = self.bt + story = self.story + if bases: + bases = map(lambda b:b.__name__, bases) # hack + story.append(Paragraph('%s(%s)' % (name, join(bases, ', ')), self.makeHeadingStyle(self.indentLevel, 'class'))) + else: + story.append(Paragraph(name, self.makeHeadingStyle(self.indentLevel, 'class'))) + + if doc: + story.append(XPreformatted(htmlescape(doc), bt)) + story.append(XPreformatted('', bt)) + + + def beginMethod(self, name, doc, sig): + bt = self.bt + story = self.story + story.append(Paragraph(name+sig, self.makeHeadingStyle(self.indentLevel, 'method', doc))) + if doc: + story.append(XPreformatted(htmlescape(doc), bt)) + story.append(XPreformatted('', bt)) + + + def beginFunctions(self, names): + if names: + self.story.append(Paragraph('Functions', self.makeHeadingStyle(self.indentLevel))) + + + def beginFunction(self, name, doc, sig): + bt = self.bt + story = self.story + story.append(Paragraph(name+sig, self.makeHeadingStyle(self.indentLevel, 'function'))) + if doc: + story.append(XPreformatted(htmlescape(doc), bt)) + story.append(XPreformatted('', bt)) + + +class UmlPdfDocBuilder0(PdfDocBuilder0): + "Document the skeleton of a Python module with UML class diagrams." + + fileSuffix = '.pdf' + + def begin(self, name='', typ=''): + styleSheet = getSampleStyleSheet() + self.h1 = styleSheet['Heading1'] + self.h2 = styleSheet['Heading2'] + self.h3 = styleSheet['Heading3'] + self.code = styleSheet['Code'] + self.bt = styleSheet['BodyText'] + self.story = [] + self.classCompartment = '' + self.methodCompartment = [] + + + def beginModule(self, name, doc, imported): + story = self.story + h1, h2, h3, bt = self.h1, self.h2, self.h3, self.bt + styleSheet = getSampleStyleSheet() + bt1 = styleSheet['BodyText'] + + story.append(Paragraph(name, h1)) + story.append(XPreformatted(doc, bt1)) + + if imported: + story.append(Paragraph('Imported modules', self.makeHeadingStyle(self.indentLevel + 1))) + for m in imported: + p = Paragraph('\201 %s' % m, bt1) + p.style.bulletIndent = 10 + p.style.leftIndent = 18 + story.append(p) + + + def endModule(self, name, doc, imported): + self.story.append(PageBreak()) + PdfDocBuilder0.endModule(self, name, doc, imported) + + + def beginClasses(self, names): + h1, h2, h3, bt = self.h1, self.h2, self.h3, self.bt + if names: + self.story.append(Paragraph('Classes', h2)) + + + def beginClass(self, name, doc, bases): + self.classCompartment = '' + self.methodCompartment = [] + + if bases: + bases = map(lambda b:b.__name__, bases) # hack + self.classCompartment = '%s(%s)' % (name, join(bases, ', ')) + else: + self.classCompartment = name + + + def endClass(self, name, doc, bases): + h1, h2, h3, bt, code = self.h1, self.h2, self.h3, self.bt, self.code + styleSheet = getSampleStyleSheet() + bt1 = styleSheet['BodyText'] + story = self.story + + # Use only the first line of the class' doc string -- + # no matter how long! (Do the same later for methods) + classDoc = reduceDocStringLength(doc) + + tsa = tableStyleAttributes = [] + + # Make table with class and method rows + # and add it to the story. + p = Paragraph('%s' % self.classCompartment, bt) + p.style.alignment = TA_CENTER + rows = [(p,)] + # No doc strings, now... + # rows = rows + [(Paragraph('%s' % classDoc, bt1),)] + lenRows = len(rows) + tsa.append(('BOX', (0,0), (-1,lenRows-1), 0.25, colors.black)) + for name, doc, sig in self.methodCompartment: + nameAndSig = Paragraph('%s%s' % (name, sig), bt1) + rows.append((nameAndSig,)) + # No doc strings, now... + # docStr = Paragraph('%s' % reduceDocStringLength(doc), bt1) + # rows.append((docStr,)) + tsa.append(('BOX', (0,lenRows), (-1,-1), 0.25, colors.black)) + t = Table(rows, (12*cm,)) + tableStyle = TableStyle(tableStyleAttributes) + t.setStyle(tableStyle) + self.story.append(t) + self.story.append(Spacer(1*cm, 1*cm)) + + + def beginMethod(self, name, doc, sig): + self.methodCompartment.append((name, doc, sig)) + + + def beginFunctions(self, names): + h1, h2, h3, bt = self.h1, self.h2, self.h3, self.bt + if names: + self.story.append(Paragraph('Functions', h2)) + self.classCompartment = chr(171) + ' Module-Level Functions ' + chr(187) + self.methodCompartment = [] + + + def beginFunction(self, name, doc, sig): + self.methodCompartment.append((name, doc, sig)) + + + def endFunctions(self, names): + h1, h2, h3, bt, code = self.h1, self.h2, self.h3, self.bt, self.code + styleSheet = getSampleStyleSheet() + bt1 = styleSheet['BodyText'] + story = self.story + if not names: + return + + tsa = tableStyleAttributes = [] + + # Make table with class and method rows + # and add it to the story. + p = Paragraph('%s' % self.classCompartment, bt) + p.style.alignment = TA_CENTER + rows = [(p,)] + lenRows = len(rows) + tsa.append(('BOX', (0,0), (-1,lenRows-1), 0.25, colors.black)) + for name, doc, sig in self.methodCompartment: + nameAndSig = Paragraph('%s%s' % (name, sig), bt1) + rows.append((nameAndSig,)) + # No doc strings, now... + # docStr = Paragraph('%s' % reduceDocStringLength(doc), bt1) + # rows.append((docStr,)) + tsa.append(('BOX', (0,lenRows), (-1,-1), 0.25, colors.black)) + t = Table(rows, (12*cm,)) + tableStyle = TableStyle(tableStyleAttributes) + t.setStyle(tableStyle) + self.story.append(t) + self.story.append(Spacer(1*cm, 1*cm)) + + +#################################################################### +# +# Main +# +#################################################################### + +def printUsage(): + """docpy.py - Automated documentation for Python source code. + +Usage: python docpy.py [options] + + [options] + -h Print this help message. + + -f name Use the document builder indicated by 'name', + e.g. Ascii, Html, Pdf (default), UmlPdf. + + -m module Generate document for module named 'module' + (default is 'docpy'). + 'module' may follow any of these forms: + - docpy.py + - docpy + - c:\\test\\docpy + and can be any of these: + - standard Python modules + - modules in the Python search path + - modules in the current directory + + -p package Generate document for package named 'package'. + 'package' may follow any of these forms: + - reportlab + - reportlab.platypus + - c:\\test\\reportlab + and can be any of these: + - standard Python packages (?) + - packages in the Python search path + - packages in the current directory + + -s Silent mode (default is unset). + +Examples: + + python docpy.py -h + python docpy.py -m docpy.py -f Ascii + python docpy.py -m string -f Html + python docpy.py -m signsandsymbols.py -f Pdf + python docpy.py -p reportlab.platypus -f UmlPdf + python docpy.py -p reportlab.lib -s -f UmlPdf +""" + + +def documentModule0(pathOrName, builder, opts={}): + """Generate documentation for one Python file in some format. + + This handles Python standard modules like string, custom modules + on the Python search path like e.g. docpy as well as modules + specified with their full path like C:/tmp/junk.py. + + The doc file will always be saved in the current directory with + a basename equal to that of the module, e.g. docpy. + """ + + cwd = os.getcwd() + + # Append directory to Python search path if we get one. + dirName = os.path.dirname(pathOrName) + if dirName: + sys.path.append(dirName) + + # Remove .py extension from module name. + if pathOrName[-3:] == '.py': + modname = pathOrName[:-3] + else: + modname = pathOrName + + # Remove directory paths from module name. + if dirName: + modname = os.path.basename(modname) + + # Load the module. + try: + module = __import__(modname) + except: + print 'Failed to import %s.' % modname + os.chdir(cwd) + return + + # Do the real documentation work. + s = ModuleSkeleton0() + s.inspect(module) + builder.write(s) + + # Remove appended directory from Python search path if we got one. + if dirName: + del sys.path[-1] + + os.chdir(cwd) + + +def _packageWalkCallback((builder, opts), dirPath, files): + "A callback function used when waking over a package tree." + + # Skip __init__ files. + files = filter(lambda f:f != '__init__.py', files) + + files = filter(lambda f:f[-3:] == '.py', files) + for f in files: + path = os.path.join(dirPath, f) + if not opts.get('isSilent', 0): + print path + builder.indentLevel = builder.indentLevel + 1 + documentModule0(path, builder) + builder.indentLevel = builder.indentLevel - 1 + + +def documentPackage0(pathOrName, builder, opts={}): + """Generate documentation for one Python package in some format. + + 'pathOrName' can be either a filesystem path leading to a Python + package or package name whose path will be resolved by importing + the top-level module. + + The doc file will always be saved in the current directory with + a basename equal to that of the package, e.g. reportlab.lib. + """ + + # Did we get a package path with OS-dependant seperators...? + if os.sep in pathOrName: + path = pathOrName + name = os.path.splitext(os.path.basename(path))[0] + # ... or rather a package name? + else: + name = pathOrName + package = __import__(name) + # Some special care needed for dotted names. + if '.' in name: + subname = 'package' + name[find(name, '.'):] + package = eval(subname) + path = os.path.dirname(package.__file__) + + cwd = os.getcwd() + builder.beginPackage(name) + os.path.walk(path, _packageWalkCallback, (builder, opts)) + builder.endPackage(name) + os.chdir(cwd) + + +def main(): + "Handle command-line options and trigger corresponding action." + + opts, args = getopt.getopt(sys.argv[1:], 'hsf:m:p:') + + # Make an options dictionary that is easier to use. + optsDict = {} + for k, v in opts: + optsDict[k] = v + hasOpt = optsDict.has_key + + # On -h print usage and exit immediately. + if hasOpt('-h'): + print printUsage.__doc__ + sys.exit(0) + + # On -s set silent mode. + isSilent = hasOpt('-s') + + # On -f set the appropriate DocBuilder to use or a default one. + builderClassName = optsDict.get('-f', 'Pdf') + 'DocBuilder0' + builder = eval(builderClassName + '()') + + # Set default module or package to document. + if not hasOpt('-p') and not hasOpt('-m'): + optsDict['-m'] = 'docpy' + + # Save a few options for further use. + options = {'isSilent':isSilent} + + # Now call the real documentation functions. + if hasOpt('-m'): + nameOrPath = optsDict['-m'] + if not isSilent: + print "Generating documentation for module %s..." % nameOrPath + builder.begin(name=nameOrPath, typ='module') + documentModule0(nameOrPath, builder, options) + elif hasOpt('-p'): + nameOrPath = optsDict['-p'] + if not isSilent: + print "Generating documentation for package %s..." % nameOrPath + builder.begin(name=nameOrPath, typ='package') + documentPackage0(nameOrPath, builder, options) + builder.end() + + if not isSilent: + print "Saved %s." % builder.outPath + + +if __name__ == '__main__': + main() diff --git a/bin/reportlab/tools/docco/examples.py b/bin/reportlab/tools/docco/examples.py new file mode 100644 index 00000000000..a4eeec54d07 --- /dev/null +++ b/bin/reportlab/tools/docco/examples.py @@ -0,0 +1,851 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/examples.py +import string + +testannotations=""" +def annotations(canvas): + from reportlab.lib.units import inch + canvas.drawString(inch, 2.5*inch, + "setAuthor, setTitle, setSubject have no visible effect") + canvas.drawString(inch, inch, "But if you are viewing this document dynamically") + canvas.drawString(inch, 0.5*inch, "please look at File/Document Info") + canvas.setAuthor("the ReportLab Team") + canvas.setTitle("ReportLab PDF Generation User Guide") + canvas.setSubject("How to Generate PDF files using the ReportLab modules") +""" + +# magic function making module + +test1 = """ +def f(a,b): + print "it worked", a, b + return a+b +""" + +test2 = """ +def g(n): + if n==0: return 1 + else: return n*g(n-1) + """ + +testhello = """ +def hello(c): + from reportlab.lib.units import inch + # move the origin up and to the left + c.translate(inch,inch) + # define a large font + c.setFont("Helvetica", 14) + # choose some colors + c.setStrokeColorRGB(0.2,0.5,0.3) + c.setFillColorRGB(1,0,1) + # draw some lines + c.line(0,0,0,1.7*inch) + c.line(0,0,1*inch,0) + # draw a rectangle + c.rect(0.2*inch,0.2*inch,1*inch,1.5*inch, fill=1) + # make text go straight up + c.rotate(90) + # change color + c.setFillColorRGB(0,0,0.77) + # say hello (note after rotate the y coord needs to be negative!) + c.drawString(0.3*inch, -inch, "Hello World") +""" + +testcoords = """ +def coords(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import pink, black, red, blue, green + c = canvas + c.setStrokeColor(pink) + c.grid([inch, 2*inch, 3*inch, 4*inch], [0.5*inch, inch, 1.5*inch, 2*inch, 2.5*inch]) + c.setStrokeColor(black) + c.setFont("Times-Roman", 20) + c.drawString(0,0, "(0,0) the Origin") + c.drawString(2.5*inch, inch, "(2.5,1) in inches") + c.drawString(4*inch, 2.5*inch, "(4, 2.5)") + c.setFillColor(red) + c.rect(0,2*inch,0.2*inch,0.3*inch, fill=1) + c.setFillColor(green) + c.circle(4.5*inch, 0.4*inch, 0.2*inch, fill=1) +""" + +testtranslate = """ +def translate(canvas): + from reportlab.lib.units import cm + canvas.translate(2.3*cm, 0.3*cm) + coords(canvas) + """ + +testscale = """ +def scale(canvas): + canvas.scale(0.75, 0.5) + coords(canvas) +""" + +testscaletranslate = """ +def scaletranslate(canvas): + from reportlab.lib.units import inch + canvas.setFont("Courier-BoldOblique", 12) + # save the state + canvas.saveState() + # scale then translate + canvas.scale(0.3, 0.5) + canvas.translate(2.4*inch, 1.5*inch) + canvas.drawString(0, 2.7*inch, "Scale then translate") + coords(canvas) + # forget the scale and translate... + canvas.restoreState() + # translate then scale + canvas.translate(2.4*inch, 1.5*inch) + canvas.scale(0.3, 0.5) + canvas.drawString(0, 2.7*inch, "Translate then scale") + coords(canvas) +""" + +testmirror = """ +def mirror(canvas): + from reportlab.lib.units import inch + canvas.translate(5.5*inch, 0) + canvas.scale(-1.0, 1.0) + coords(canvas) +""" + +testcolors = """ +def colors(canvas): + from reportlab.lib import colors + from reportlab.lib.units import inch + black = colors.black + y = x = 0; dy=inch*3/4.0; dx=inch*5.5/5; w=h=dy/2; rdx=(dx-w)/2 + rdy=h/5.0; texty=h+2*rdy + canvas.setFont("Helvetica",10) + for [namedcolor, name] in ( + [colors.lavenderblush, "lavenderblush"], + [colors.lawngreen, "lawngreen"], + [colors.lemonchiffon, "lemonchiffon"], + [colors.lightblue, "lightblue"], + [colors.lightcoral, "lightcoral"]): + canvas.setFillColor(namedcolor) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, name) + x = x+dx + y = y + dy; x = 0 + for rgb in [(1,0,0), (0,1,0), (0,0,1), (0.5,0.3,0.1), (0.4,0.5,0.3)]: + r,g,b = rgb + canvas.setFillColorRGB(r,g,b) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, "r%s g%s b%s"%rgb) + x = x+dx + y = y + dy; x = 0 + for cmyk in [(1,0,0,0), (0,1,0,0), (0,0,1,0), (0,0,0,1), (0,0,0,0)]: + c,m,y1,k = cmyk + canvas.setFillColorCMYK(c,m,y1,k) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, "c%s m%s y%s k%s"%cmyk) + x = x+dx + y = y + dy; x = 0 + for gray in (0.0, 0.25, 0.50, 0.75, 1.0): + canvas.setFillGray(gray) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, "gray: %s"%gray) + x = x+dx +""" + +testspumoni = """ +def spumoni(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import pink, green, brown, white + x = 0; dx = 0.4*inch + for i in range(4): + for color in (pink, green, brown): + canvas.setFillColor(color) + canvas.rect(x,0,dx,3*inch,stroke=0,fill=1) + x = x+dx + canvas.setFillColor(white) + canvas.setStrokeColor(white) + canvas.setFont("Helvetica-Bold", 85) + canvas.drawCentredString(2.75*inch, 1.3*inch, "SPUMONI") +""" + +testspumoni2 = """ +def spumoni2(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import pink, green, brown, white, black + # draw the previous drawing + spumoni(canvas) + # now put an ice cream cone on top of it: + # first draw a triangle (ice cream cone) + p = canvas.beginPath() + xcenter = 2.75*inch + radius = 0.45*inch + p.moveTo(xcenter-radius, 1.5*inch) + p.lineTo(xcenter+radius, 1.5*inch) + p.lineTo(xcenter, 0) + canvas.setFillColor(brown) + canvas.setStrokeColor(black) + canvas.drawPath(p, fill=1) + # draw some circles (scoops) + y = 1.5*inch + for color in (pink, green, brown): + canvas.setFillColor(color) + canvas.circle(xcenter, y, radius, fill=1) + y = y+radius +""" + +testbezier = """ +def bezier(canvas): + from reportlab.lib.colors import yellow, green, red, black + from reportlab.lib.units import inch + i = inch + d = i/4 + # define the bezier curve control points + x1,y1, x2,y2, x3,y3, x4,y4 = d,1.5*i, 1.5*i,d, 3*i,d, 5.5*i-d,3*i-d + # draw a figure enclosing the control points + canvas.setFillColor(yellow) + p = canvas.beginPath() + p.moveTo(x1,y1) + for (x,y) in [(x2,y2), (x3,y3), (x4,y4)]: + p.lineTo(x,y) + canvas.drawPath(p, fill=1, stroke=0) + # draw the tangent lines + canvas.setLineWidth(inch*0.1) + canvas.setStrokeColor(green) + canvas.line(x1,y1,x2,y2) + canvas.setStrokeColor(red) + canvas.line(x3,y3,x4,y4) + # finally draw the curve + canvas.setStrokeColor(black) + canvas.bezier(x1,y1, x2,y2, x3,y3, x4,y4) +""" + +testbezier2 = """ +def bezier2(canvas): + from reportlab.lib.colors import yellow, green, red, black + from reportlab.lib.units import inch + # make a sequence of control points + xd,yd = 5.5*inch/2, 3*inch/2 + xc,yc = xd,yd + dxdy = [(0,0.33), (0.33,0.33), (0.75,1), (0.875,0.875), + (0.875,0.875), (1,0.75), (0.33,0.33), (0.33,0)] + pointlist = [] + for xoffset in (1,-1): + yoffset = xoffset + for (dx,dy) in dxdy: + px = xc + xd*xoffset*dx + py = yc + yd*yoffset*dy + pointlist.append((px,py)) + yoffset = -xoffset + for (dy,dx) in dxdy: + px = xc + xd*xoffset*dx + py = yc + yd*yoffset*dy + pointlist.append((px,py)) + # draw tangent lines and curves + canvas.setLineWidth(inch*0.1) + while pointlist: + [(x1,y1),(x2,y2),(x3,y3),(x4,y4)] = pointlist[:4] + del pointlist[:4] + canvas.setLineWidth(inch*0.1) + canvas.setStrokeColor(green) + canvas.line(x1,y1,x2,y2) + canvas.setStrokeColor(red) + canvas.line(x3,y3,x4,y4) + # finally draw the curve + canvas.setStrokeColor(black) + canvas.bezier(x1,y1, x2,y2, x3,y3, x4,y4) +""" + +testpencil = """ +def pencil(canvas, text="No.2"): + from reportlab.lib.colors import yellow, red, black,white + from reportlab.lib.units import inch + u = inch/10.0 + canvas.setStrokeColor(black) + canvas.setLineWidth(4) + # draw erasor + canvas.setFillColor(red) + canvas.circle(30*u, 5*u, 5*u, stroke=1, fill=1) + # draw all else but the tip (mainly rectangles with different fills) + canvas.setFillColor(yellow) + canvas.rect(10*u,0,20*u,10*u, stroke=1, fill=1) + canvas.setFillColor(black) + canvas.rect(23*u,0,8*u,10*u,fill=1) + canvas.roundRect(14*u, 3.5*u, 8*u, 3*u, 1.5*u, stroke=1, fill=1) + canvas.setFillColor(white) + canvas.rect(25*u,u,1.2*u,8*u, fill=1,stroke=0) + canvas.rect(27.5*u,u,1.2*u,8*u, fill=1, stroke=0) + canvas.setFont("Times-Roman", 3*u) + canvas.drawCentredString(18*u, 4*u, text) + # now draw the tip + penciltip(canvas,debug=0) + # draw broken lines across the body. + canvas.setDash([10,5,16,10],0) + canvas.line(11*u,2.5*u,22*u,2.5*u) + canvas.line(22*u,7.5*u,12*u,7.5*u) + """ + +testpenciltip = """ +def penciltip(canvas, debug=1): + from reportlab.lib.colors import tan, black, green + from reportlab.lib.units import inch + u = inch/10.0 + canvas.setLineWidth(4) + if debug: + canvas.scale(2.8,2.8) # make it big + canvas.setLineWidth(1) # small lines + canvas.setStrokeColor(black) + canvas.setFillColor(tan) + p = canvas.beginPath() + p.moveTo(10*u,0) + p.lineTo(0,5*u) + p.lineTo(10*u,10*u) + p.curveTo(11.5*u,10*u, 11.5*u,7.5*u, 10*u,7.5*u) + p.curveTo(12*u,7.5*u, 11*u,2.5*u, 9.7*u,2.5*u) + p.curveTo(10.5*u,2.5*u, 11*u,0, 10*u,0) + canvas.drawPath(p, stroke=1, fill=1) + canvas.setFillColor(black) + p = canvas.beginPath() + p.moveTo(0,5*u) + p.lineTo(4*u,3*u) + p.lineTo(5*u,4.5*u) + p.lineTo(3*u,6.5*u) + canvas.drawPath(p, stroke=1, fill=1) + if debug: + canvas.setStrokeColor(green) # put in a frame of reference + canvas.grid([0,5*u,10*u,15*u], [0,5*u,10*u]) +""" + +testnoteannotation = """ +from reportlab.platypus.flowables import Flowable +class NoteAnnotation(Flowable): + '''put a pencil in the margin.''' + def wrap(self, *args): + return (1,10) # I take up very little space! (?) + def draw(self): + canvas = self.canv + canvas.translate(-10,-10) + canvas.rotate(180) + canvas.scale(0.2,0.2) + pencil(canvas, text="NOTE") +""" + +testhandannotation = """ +from reportlab.platypus.flowables import Flowable +from reportlab.lib.colors import tan, green +class HandAnnotation(Flowable): + '''A hand flowable.''' + def __init__(self, xoffset=0, size=None, fillcolor=tan, strokecolor=green): + from reportlab.lib.units import inch + if size is None: size=4*inch + self.fillcolor, self.strokecolor = fillcolor, strokecolor + self.xoffset = xoffset + self.size = size + # normal size is 4 inches + self.scale = size/(4.0*inch) + def wrap(self, *args): + return (self.xoffset, self.size) + def draw(self): + canvas = self.canv + canvas.setLineWidth(6) + canvas.setFillColor(self.fillcolor) + canvas.setStrokeColor(self.strokecolor) + canvas.translate(self.xoffset+self.size,0) + canvas.rotate(90) + canvas.scale(self.scale, self.scale) + hand(canvas, debug=0, fill=1) +""" + +lyrics = '''\ +well she hit Net Solutions +and she registered her own .com site now +and filled it up with yahoo profile pics +she snarfed in one night now +and she made 50 million when Hugh Hefner +bought up the rights now +and she'll have fun fun fun +til her Daddy takes the keyboard away''' + +lyrics = string.split(lyrics, "\n") +testtextsize = """ +def textsize(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import magenta, red + canvas.setFont("Times-Roman", 20) + canvas.setFillColor(red) + canvas.drawCentredString(2.75*inch, 2.5*inch, "Font size examples") + canvas.setFillColor(magenta) + size = 7 + y = 2.3*inch + x = 1.3*inch + for line in lyrics: + canvas.setFont("Helvetica", size) + canvas.drawRightString(x,y,"%s points: " % size) + canvas.drawString(x,y, line) + y = y-size*1.2 + size = size+1.5 +""" + +teststar = """ +def star(canvas, title="Title Here", aka="Comment here.", + xcenter=None, ycenter=None, nvertices=5): + from math import pi + from reportlab.lib.units import inch + radius=inch/3.0 + if xcenter is None: xcenter=2.75*inch + if ycenter is None: ycenter=1.5*inch + canvas.drawCentredString(xcenter, ycenter+1.3*radius, title) + canvas.drawCentredString(xcenter, ycenter-1.4*radius, aka) + p = canvas.beginPath() + p.moveTo(xcenter,ycenter+radius) + from math import pi, cos, sin + angle = (2*pi)*2/5.0 + startangle = pi/2.0 + for vertex in range(nvertices-1): + nextangle = angle*(vertex+1)+startangle + x = xcenter + radius*cos(nextangle) + y = ycenter + radius*sin(nextangle) + p.lineTo(x,y) + if nvertices==5: + p.close() + canvas.drawPath(p) +""" + +testjoins = """ +def joins(canvas): + from reportlab.lib.units import inch + # make lines big + canvas.setLineWidth(5) + star(canvas, "Default: mitered join", "0: pointed", xcenter = 1*inch) + canvas.setLineJoin(1) + star(canvas, "Round join", "1: rounded") + canvas.setLineJoin(2) + star(canvas, "Bevelled join", "2: square", xcenter=4.5*inch) +""" + +testcaps = """ +def caps(canvas): + from reportlab.lib.units import inch + # make lines big + canvas.setLineWidth(5) + star(canvas, "Default", "no projection",xcenter = 1*inch, + nvertices=4) + canvas.setLineCap(1) + star(canvas, "Round cap", "1: ends in half circle", nvertices=4) + canvas.setLineCap(2) + star(canvas, "Square cap", "2: projects out half a width", xcenter=4.5*inch, + nvertices=4) +""" + +testdashes = """ +def dashes(canvas): + from reportlab.lib.units import inch + # make lines big + canvas.setDash(6,3) + star(canvas, "Simple dashes", "6 points on, 3 off", xcenter = 1*inch) + canvas.setDash(1,2) + star(canvas, "Dots", "One on, two off") + canvas.setDash([1,1,3,3,1,4,4,1], 0) + star(canvas, "Complex Pattern", "[1,1,3,3,1,4,4,1]", xcenter=4.5*inch) +""" + +testcustomfont1 = """ +def customfont1(canvas): + # we know some glyphs are missing, suppress warnings + import reportlab.rl_config + reportlab.rl_config.warnOnMissingFontGlyphs = 0 + + import rl_doc_utils + from reportlab.pdfbase import pdfmetrics + afmFile, pfbFile = rl_doc_utils.getJustFontPaths() + justFace = pdfmetrics.EmbeddedType1Face(afmFile, pfbFile) + faceName = 'LettErrorRobot-Chrome' # pulled from AFM file + pdfmetrics.registerTypeFace(justFace) + justFont = pdfmetrics.Font('LettErrorRobot-Chrome', + faceName, + 'WinAnsiEncoding') + pdfmetrics.registerFont(justFont) + + canvas.setFont('LettErrorRobot-Chrome', 32) + canvas.drawString(10, 150, 'This should be in') + canvas.drawString(10, 100, 'LettErrorRobot-Chrome') +""" + +testttffont1 = """ +def ttffont1(canvas): + # we know some glyphs are missing, suppress warnings + import reportlab.rl_config + reportlab.rl_config.warnOnMissingFontGlyphs = 0 + from reportlab.pdfbase import pdfmetrics + from reportlab.pdfbase.ttfonts import TTFont + pdfmetrics.registerFont(TTFont('Rina', 'rina.ttf')) + from reportlab.pdfgen.canvas import Canvas + + canvas.setFont('Rina', 32) + canvas.drawString(10, 150, "Some UTF-8 text encoded") + canvas.drawString(10, 100, "in the Rina TT Font!") +""" + +testcursormoves1 = """ +def cursormoves1(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(inch, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 14) + for line in lyrics: + textobject.textLine(line) + textobject.setFillGray(0.4) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testcursormoves2 = """ +def cursormoves2(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(2, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 14) + for line in lyrics: + textobject.textOut(line) + textobject.moveCursor(14,14) # POSITIVE Y moves down!!! + textobject.setFillColorRGB(0.4,0,1) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testcharspace = """ +def charspace(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 10) + charspace = 0 + for line in lyrics: + textobject.setCharSpace(charspace) + textobject.textLine("%s: %s" %(charspace,line)) + charspace = charspace+0.5 + textobject.setFillGray(0.4) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testwordspace = """ +def wordspace(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 12) + wordspace = 0 + for line in lyrics: + textobject.setWordSpace(wordspace) + textobject.textLine("%s: %s" %(wordspace,line)) + wordspace = wordspace+2.5 + textobject.setFillColorCMYK(0.4,0,0.4,0.2) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" +testhorizontalscale = """ +def horizontalscale(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 12) + horizontalscale = 80 # 100 is default + for line in lyrics: + textobject.setHorizScale(horizontalscale) + textobject.textLine("%s: %s" %(horizontalscale,line)) + horizontalscale = horizontalscale+10 + textobject.setFillColorCMYK(0.0,0.4,0.4,0.2) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" +testleading = """ +def leading(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 14) + leading = 8 + for line in lyrics: + textobject.setLeading(leading) + textobject.textLine("%s: %s" %(leading,line)) + leading = leading+2.5 + textobject.setFillColorCMYK(0.8,0,0,0.3) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testhand = """ +def hand(canvas, debug=1, fill=0): + (startx, starty) = (0,0) + curves = [ + ( 0, 2), ( 0, 4), ( 0, 8), # back of hand + ( 5, 8), ( 7,10), ( 7,14), + (10,14), (10,13), ( 7.5, 8), # thumb + (13, 8), (14, 8), (17, 8), + (19, 8), (19, 6), (17, 6), + (15, 6), (13, 6), (11, 6), # index, pointing + (12, 6), (13, 6), (14, 6), + (16, 6), (16, 4), (14, 4), + (13, 4), (12, 4), (11, 4), # middle + (11.5, 4), (12, 4), (13, 4), + (15, 4), (15, 2), (13, 2), + (12.5, 2), (11.5, 2), (11, 2), # ring + (11.5, 2), (12, 2), (12.5, 2), + (14, 2), (14, 0), (12.5, 0), + (10, 0), (8, 0), (6, 0), # pinky, then close + ] + from reportlab.lib.units import inch + if debug: canvas.setLineWidth(6) + u = inch*0.2 + p = canvas.beginPath() + p.moveTo(startx, starty) + ccopy = list(curves) + while ccopy: + [(x1,y1), (x2,y2), (x3,y3)] = ccopy[:3] + del ccopy[:3] + p.curveTo(x1*u,y1*u,x2*u,y2*u,x3*u,y3*u) + p.close() + canvas.drawPath(p, fill=fill) + if debug: + from reportlab.lib.colors import red, green + (lastx, lasty) = (startx, starty) + ccopy = list(curves) + while ccopy: + [(x1,y1), (x2,y2), (x3,y3)] = ccopy[:3] + del ccopy[:3] + canvas.setStrokeColor(red) + canvas.line(lastx*u,lasty*u, x1*u,y1*u) + canvas.setStrokeColor(green) + canvas.line(x2*u,y2*u, x3*u,y3*u) + (lastx,lasty) = (x3,y3) +""" + +testhand2 = """ +def hand2(canvas): + canvas.translate(20,10) + canvas.setLineWidth(3) + canvas.setFillColorRGB(0.1, 0.3, 0.9) + canvas.setStrokeGray(0.5) + hand(canvas, debug=0, fill=1) +""" + +testfonts = """ +def fonts(canvas): + from reportlab.lib.units import inch + text = "Now is the time for all good men to..." + x = 1.8*inch + y = 2.7*inch + for font in canvas.getAvailableFonts(): + canvas.setFont(font, 10) + canvas.drawString(x,y,text) + canvas.setFont("Helvetica", 10) + canvas.drawRightString(x-10,y, font+":") + y = y-13 +""" + +testarcs = """ +def arcs(canvas): + from reportlab.lib.units import inch + canvas.setLineWidth(4) + canvas.setStrokeColorRGB(0.8, 1, 0.6) + # draw rectangles enclosing the arcs + canvas.rect(inch, inch, 1.5*inch, inch) + canvas.rect(3*inch, inch, inch, 1.5*inch) + canvas.setStrokeColorRGB(0, 0.2, 0.4) + canvas.setFillColorRGB(1, 0.6, 0.8) + p = canvas.beginPath() + p.moveTo(0.2*inch, 0.2*inch) + p.arcTo(inch, inch, 2.5*inch,2*inch, startAng=-30, extent=135) + p.arc(3*inch, inch, 4*inch, 2.5*inch, startAng=-45, extent=270) + canvas.drawPath(p, fill=1, stroke=1) +""" +testvariousshapes = """ +def variousshapes(canvas): + from reportlab.lib.units import inch + inch = int(inch) + canvas.setStrokeGray(0.5) + canvas.grid(range(0,11*inch/2,inch/2), range(0,7*inch/2,inch/2)) + canvas.setLineWidth(4) + canvas.setStrokeColorRGB(0, 0.2, 0.7) + canvas.setFillColorRGB(1, 0.6, 0.8) + p = canvas.beginPath() + p.rect(0.5*inch, 0.5*inch, 0.5*inch, 2*inch) + p.circle(2.75*inch, 1.5*inch, 0.3*inch) + p.ellipse(3.5*inch, 0.5*inch, 1.2*inch, 2*inch) + canvas.drawPath(p, fill=1, stroke=1) +""" + +testclosingfigures = """ +def closingfigures(canvas): + from reportlab.lib.units import inch + h = inch/3.0; k = inch/2.0 + canvas.setStrokeColorRGB(0.2,0.3,0.5) + canvas.setFillColorRGB(0.8,0.6,0.2) + canvas.setLineWidth(4) + p = canvas.beginPath() + for i in (1,2,3,4): + for j in (1,2): + xc,yc = inch*i, inch*j + p.moveTo(xc,yc) + p.arcTo(xc-h, yc-k, xc+h, yc+k, startAng=0, extent=60*i) + # close only the first one, not the second one + if j==1: + p.close() + canvas.drawPath(p, fill=1, stroke=1) +""" + +testforms = """ +def forms(canvas): + #first create a form... + canvas.beginForm("SpumoniForm") + #re-use some drawing functions from earlier + spumoni(canvas) + canvas.endForm() + + #then draw it + canvas.doForm("SpumoniForm") +""" + +def doctemplateillustration(canvas): + from reportlab.lib.units import inch + canvas.setFont("Helvetica", 10) + canvas.drawString(inch/4.0, 2.75*inch, "DocTemplate") + W = 4/3.0*inch + H = 2*inch + Wd = x = inch/4.0 + Hd =y = inch/2.0 + for name in ("two column", "chapter page", "title page"): + canvas.setFillColorRGB(0.5,1.0,1.0) + canvas.rect(x,y,W,H, fill=1) + canvas.setFillColorRGB(0,0,0) + canvas.drawString(x+inch/8, y+H-Wd, "PageTemplate") + canvas.drawCentredString(x+W/2.0, y-Wd, name) + x = x+W+Wd + canvas.saveState() + d = inch/16 + dW = (W-3*d)/2.0 + hD = H -2*d-Wd + canvas.translate(Wd+d, Hd+d) + for name in ("left Frame", "right Frame"): + canvas.setFillColorRGB(1.0,0.5,1.0) + canvas.rect(0,0, dW,hD, fill=1) + canvas.setFillGray(0.7) + dd= d/2.0 + ddH = (hD-6*dd)/5.0 + ddW = dW-2*dd + yy = dd + xx = dd + for i in range(5): + canvas.rect(xx,yy,ddW,ddH, fill=1, stroke=0) + yy = yy+ddH+dd + canvas.setFillColorRGB(0,0,0) + canvas.saveState() + canvas.rotate(90) + canvas.drawString(d,-dW/2, name) + canvas.restoreState() + canvas.translate(dW+d,0) + canvas.restoreState() + canvas.setFillColorRGB(1.0, 0.5, 1.0) + mx = Wd+W+Wd+d + my = Hd+d + mW = W-2*d + mH = H-d-Hd + canvas.rect(mx, my, mW, mH, fill=1) + canvas.rect(Wd+2*(W+Wd)+d, Hd+3*d, W-2*d, H/2.0, fill=1) + canvas.setFillGray(0.7) + canvas.rect(Wd+2*(W+Wd)+d+dd, Hd+5*d, W-2*d-2*dd, H/2.0-2*d-dd, fill=1) + xx = mx+dd + yy = my+mH/5.0 + ddH = (mH-6*dd-mH/5.0)/3.0 + ddW = mW - 2*dd + for i in range(3): + canvas.setFillGray(0.7) + canvas.rect(xx,yy,ddW,ddH, fill=1, stroke=1) + canvas.setFillGray(0) + canvas.drawString(xx+dd/2.0,yy+dd/2.0, "flowable %s" %(157-i)) + yy = yy+ddH+dd + canvas.drawCentredString(3*Wd+2*W+W/2, Hd+H/2.0, "First Flowable") + canvas.setFont("Times-BoldItalic", 8) + canvas.setFillGray(0) + canvas.drawCentredString(mx+mW/2.0, my+mH+3*dd, "Chapter 6: Lubricants") + canvas.setFont("Times-BoldItalic", 10) + canvas.drawCentredString(3*Wd+2*W+W/2, Hd+H-H/4, "College Life") + +# D = dir() +g = globals() +Dprime = {} +from types import StringType +from string import strip +for (a,b) in g.items(): + if a[:4]=="test" and type(b) is StringType: + #print 'for', a + #print b + b = strip(b) + exec(b+'\n') + +platypussetup = """ +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.rl_config import defaultPageSize +from reportlab.lib.units import inch +PAGE_HEIGHT=defaultPageSize[1]; PAGE_WIDTH=defaultPageSize[0] +styles = getSampleStyleSheet() +""" +platypusfirstpage = """ +Title = "Hello world" +pageinfo = "platypus example" +def myFirstPage(canvas, doc): + canvas.saveState() + canvas.setFont('Times-Bold',16) + canvas.drawCentredString(PAGE_WIDTH/2.0, PAGE_HEIGHT-108, Title) + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo) + canvas.restoreState() +""" +platypusnextpage = """ +def myLaterPages(canvas, doc): + canvas.saveState() + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo)) + canvas.restoreState() +""" +platypusgo = """ +def go(): + doc = SimpleDocTemplate("phello.pdf") + Story = [Spacer(1,2*inch)] + style = styles["Normal"] + for i in range(100): + bogustext = ("This is Paragraph number %s. " % i) *20 + p = Paragraph(bogustext, style) + Story.append(p) + Story.append(Spacer(1,0.2*inch)) + doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages) +""" + +if __name__=="__main__": + # then do the platypus hello world + for b in platypussetup, platypusfirstpage, platypusnextpage, platypusgo: + b = strip(b) + exec(b+'\n') + go() diff --git a/bin/reportlab/tools/docco/graphdocpy.py b/bin/reportlab/tools/docco/graphdocpy.py new file mode 100644 index 00000000000..29aecf16c6c --- /dev/null +++ b/bin/reportlab/tools/docco/graphdocpy.py @@ -0,0 +1,980 @@ +#!/usr/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/graphdocpy.py + +"""Generate documentation for reportlab.graphics classes. + +Type the following for usage info: + + python graphdocpy.py -h +""" + + +__version__ = '0.8' + + +import sys +sys.path.insert(0, '.') +import os, re, types, string, getopt, pickle, copy, time, pprint, traceback +from string import find, join, split, replace, expandtabs, rstrip +import reportlab +from reportlab import rl_config + +from docpy import PackageSkeleton0, ModuleSkeleton0 +from docpy import DocBuilder0, PdfDocBuilder0, HtmlDocBuilder0 +from docpy import htmlescape, htmlrepr, defaultformat, \ + getdoc, reduceDocStringLength +from docpy import makeHtmlSection, makeHtmlSubSection, \ + makeHtmlInlineImage + +from reportlab.lib.units import inch, cm +from reportlab.lib.pagesizes import A4 +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_LEFT +from reportlab.lib.utils import getStringIO +#from StringIO import StringIO +#getStringIO=StringIO +from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle +from reportlab.pdfgen import canvas +from reportlab.platypus.flowables import Flowable, Spacer +from reportlab.platypus.paragraph import Paragraph +from reportlab.platypus.tableofcontents import TableOfContents +from reportlab.platypus.flowables \ + import Flowable, Preformatted,Spacer, Image, KeepTogether, PageBreak +from reportlab.platypus.xpreformatted import XPreformatted +from reportlab.platypus.frames import Frame +from reportlab.platypus.doctemplate \ + import PageTemplate, BaseDocTemplate +from reportlab.platypus.tables import TableStyle, Table +from reportlab.graphics.shapes import NotImplementedError +import inspect + +# Needed to draw Widget/Drawing demos. + +from reportlab.graphics.widgetbase import Widget +from reportlab.graphics.shapes import Drawing +from reportlab.graphics import shapes +from reportlab.graphics import renderPDF + +VERBOSE = rl_config.verbose +VERIFY = 1 + +_abstractclasserr_re = re.compile(r'^\s*abstract\s*class\s*(\w+)\s*instantiated',re.I) + +#################################################################### +# +# Stuff needed for building PDF docs. +# +#################################################################### + +def mainPageFrame(canvas, doc): + "The page frame used for all PDF documents." + + canvas.saveState() + + pageNumber = canvas.getPageNumber() + canvas.line(2*cm, A4[1]-2*cm, A4[0]-2*cm, A4[1]-2*cm) + canvas.line(2*cm, 2*cm, A4[0]-2*cm, 2*cm) + if pageNumber > 1: + canvas.setFont('Times-Roman', 12) + canvas.drawString(4 * inch, cm, "%d" % pageNumber) + if hasattr(canvas, 'headerLine'): # hackish + headerline = string.join(canvas.headerLine, ' \xc2\x8d ') + canvas.drawString(2*cm, A4[1]-1.75*cm, headerline) + + canvas.setFont('Times-Roman', 8) + msg = "Generated with docpy. See http://www.reportlab.com!" + canvas.drawString(2*cm, 1.65*cm, msg) + + canvas.restoreState() + + +class MyTemplate(BaseDocTemplate): + "The document template used for all PDF documents." + + _invalidInitArgs = ('pageTemplates',) + + def __init__(self, filename, **kw): + frame1 = Frame(2.5*cm, 2.5*cm, 15*cm, 25*cm, id='F1') + self.allowSplitting = 0 + apply(BaseDocTemplate.__init__, (self, filename), kw) + self.addPageTemplates(PageTemplate('normal', [frame1], mainPageFrame)) + + def afterFlowable(self, flowable): + "Takes care of header line, TOC and outline entries." + + if flowable.__class__.__name__ == 'Paragraph': + f = flowable + + # Build a list of heading parts. + # So far, this is the *last* item on the *previous* page... + if f.style.name[:8] == 'Heading0': + self.canv.headerLine = [f.text] # hackish + elif f.style.name[:8] == 'Heading1': + if len(self.canv.headerLine) == 2: + del self.canv.headerLine[-1] + elif len(self.canv.headerLine) == 3: + del self.canv.headerLine[-1] + del self.canv.headerLine[-1] + self.canv.headerLine.append(f.text) + elif f.style.name[:8] == 'Heading2': + if len(self.canv.headerLine) == 3: + del self.canv.headerLine[-1] + self.canv.headerLine.append(f.text) + + if f.style.name[:7] == 'Heading': + # Register TOC entries. + headLevel = int(f.style.name[7:]) + self.notify('TOCEntry', (headLevel, flowable.getPlainText(), self.page)) + + # Add PDF outline entries. + c = self.canv + title = f.text + key = str(hash(f)) + lev = int(f.style.name[7:]) + try: + if lev == 0: + isClosed = 0 + else: + isClosed = 1 + c.bookmarkPage(key) + c.addOutlineEntry(title, key, level=lev, closed=isClosed) + c.showOutline() + except: + if VERBOSE: + # AR hacking in exception handlers + print 'caught exception in MyTemplate.afterFlowable with heading text %s' % f.text + traceback.print_exc() + else: + pass + + +#################################################################### +# +# Utility functions +# +#################################################################### +def indentLevel(line, spacesPerTab=4): + """Counts the indent levels on the front. + + It is assumed that one tab equals 4 spaces. + """ + + x = 0 + nextTab = 4 + for ch in line: + if ch == ' ': + x = x + 1 + elif ch == '\t': + x = nextTab + nextTab = x + spacesPerTab + else: + return x + + +assert indentLevel('hello') == 0, 'error in indentLevel' +assert indentLevel(' hello') == 1, 'error in indentLevel' +assert indentLevel(' hello') == 2, 'error in indentLevel' +assert indentLevel(' hello') == 3, 'error in indentLevel' +assert indentLevel('\thello') == 4, 'error in indentLevel' +assert indentLevel(' \thello') == 4, 'error in indentLevel' +assert indentLevel('\t hello') == 5, 'error in indentLevel' + +#################################################################### +# +# Special-purpose document builders +# +#################################################################### + +class GraphPdfDocBuilder0(PdfDocBuilder0): + """A PDF document builder displaying widgets and drawings. + + This generates a PDF file where only methods named 'demo' are + listed for any class C. If C happens to be a subclass of Widget + and has a 'demo' method, this method is assumed to generate and + return a sample widget instance, that is then appended graphi- + cally to the Platypus story. + + Something similar happens for functions. If their names start + with 'sample' they are supposed to generate and return a sample + drawing. This is then taken and appended graphically to the + Platypus story, as well. + """ + + fileSuffix = '.pdf' + + def begin(self, name='', typ=''): + styleSheet = getSampleStyleSheet() + self.code = styleSheet['Code'] + self.bt = styleSheet['BodyText'] + self.story = [] + + # Cover page + t = time.gmtime(time.time()) + timeString = time.strftime("%Y-%m-%d %H:%M", t) + self.story.append(Paragraph('Documentation for %s "%s"' % (typ, name), self.bt)) + self.story.append(Paragraph('Generated by: graphdocpy.py version %s' % __version__, self.bt)) + self.story.append(Paragraph('Date generated: %s' % timeString, self.bt)) + self.story.append(Paragraph('Format: PDF', self.bt)) + self.story.append(PageBreak()) + + # Table of contents + toc = TableOfContents() + self.story.append(toc) + self.story.append(PageBreak()) + + + def end(self, fileName=None): + if fileName: # overrides output path + self.outPath = fileName + elif self.packageName: + self.outPath = self.packageName + self.fileSuffix + elif self.skeleton: + self.outPath = self.skeleton.getModuleName() + self.fileSuffix + else: + self.outPath = '' + + if self.outPath: + doc = MyTemplate(self.outPath) + doc.multiBuild(self.story) + + + def beginModule(self, name, doc, imported): + story = self.story + bt = self.bt + + # Defer displaying the module header info to later... + self.shouldDisplayModule = (name, doc, imported) + self.hasDisplayedModule = 0 + + + def endModule(self, name, doc, imported): + if self.hasDisplayedModule: + DocBuilder0.endModule(self, name, doc, imported) + + + def beginClasses(self, names): + # Defer displaying the module header info to later... + if self.shouldDisplayModule: + self.shouldDisplayClasses = names + + + # Skip all methods. + def beginMethod(self, name, doc, sig): + pass + + + def endMethod(self, name, doc, sig): + pass + + + def beginClass(self, name, doc, bases): + "Append a graphic demo of a Widget or Drawing at the end of a class." + + if VERBOSE: + print 'GraphPdfDocBuilder.beginClass(%s...)' % name + + aClass = eval('self.skeleton.moduleSpace.' + name) + if issubclass(aClass, Widget): + if self.shouldDisplayModule: + modName, modDoc, imported = self.shouldDisplayModule + self.story.append(Paragraph(modName, self.makeHeadingStyle(self.indentLevel-2, 'module'))) + self.story.append(XPreformatted(modDoc, self.bt)) + self.shouldDisplayModule = 0 + self.hasDisplayedModule = 1 + if self.shouldDisplayClasses: + self.story.append(Paragraph('Classes', self.makeHeadingStyle(self.indentLevel-1))) + self.shouldDisplayClasses = 0 + PdfDocBuilder0.beginClass(self, name, doc, bases) + self.beginAttributes(aClass) + + elif issubclass(aClass, Drawing): + if self.shouldDisplayModule: + modName, modDoc, imported = self.shouldDisplayModule + self.story.append(Paragraph(modName, self.makeHeadingStyle(self.indentLevel-2, 'module'))) + self.story.append(XPreformatted(modDoc, self.bt)) + self.shouldDisplayModule = 0 + self.hasDisplayedModule = 1 + if self.shouldDisplayClasses: + self.story.append(Paragraph('Classes', self.makeHeadingStyle(self.indentLevel-1))) + self.shouldDisplayClasses = 0 + PdfDocBuilder0.beginClass(self, name, doc, bases) + + + def beginAttributes(self, aClass): + "Append a list of annotated attributes of a class." + + self.story.append(Paragraph( + 'Public Attributes', + self.makeHeadingStyle(self.indentLevel+1))) + + map = aClass._attrMap + if map: + map = map.items() + map.sort() + else: + map = [] + for name, typ in map: + if typ != None: + if hasattr(typ, 'desc'): + desc = typ.desc + else: + desc = '%s' % typ.__class__.__name__ + else: + desc = 'None' + self.story.append(Paragraph( + "%s %s" % (name, desc), self.bt)) + self.story.append(Paragraph("", self.bt)) + + + def endClass(self, name, doc, bases): + "Append a graphic demo of a Widget or Drawing at the end of a class." + + PdfDocBuilder0.endClass(self, name, doc, bases) + + aClass = eval('self.skeleton.moduleSpace.' + name) + if hasattr(aClass, '_nodoc'): + pass + elif issubclass(aClass, Widget): + try: + widget = aClass() + except AssertionError, err: + if _abstractclasserr_re.match(str(err)): return + raise + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showWidgetDemoCode(widget) + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showWidgetDemo(widget) + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showWidgetProperties(widget) + self.story.append(PageBreak()) + elif issubclass(aClass, Drawing): + drawing = aClass() + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showDrawingCode(drawing) + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showDrawingDemo(drawing) + self.story.append(Spacer(0*cm, 0.5*cm)) + + + def beginFunctions(self, names): + srch = string.join(names, ' ') + if string.find(string.join(names, ' '), ' sample') > -1: + PdfDocBuilder0.beginFunctions(self, names) + + + # Skip non-sample functions. + def beginFunction(self, name, doc, sig): + "Skip function for 'uninteresting' names." + + if name[:6] == 'sample': + PdfDocBuilder0.beginFunction(self, name, doc, sig) + + + def endFunction(self, name, doc, sig): + "Append a drawing to the story for special function names." + + if name[:6] != 'sample': + return + + if VERBOSE: + print 'GraphPdfDocBuilder.endFunction(%s...)' % name + PdfDocBuilder0.endFunction(self, name, doc, sig) + aFunc = eval('self.skeleton.moduleSpace.' + name) + drawing = aFunc() + + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showFunctionDemoCode(aFunc) + self.story.append(Spacer(0*cm, 0.5*cm)) + self._showDrawingDemo(drawing) + + self.story.append(PageBreak()) + + + def _showFunctionDemoCode(self, function): + """Show a demo code of the function generating the drawing.""" + # Heading + self.story.append(Paragraph("Example", self.bt)) + self.story.append(Paragraph("", self.bt)) + + # Sample code + codeSample = inspect.getsource(function) + self.story.append(Preformatted(codeSample, self.code)) + + + def _showDrawingCode(self, drawing): + """Show code of the drawing class.""" + # Heading + #className = drawing.__class__.__name__ + self.story.append(Paragraph("Example", self.bt)) + + # Sample code + codeSample = inspect.getsource(drawing.__class__.__init__) + self.story.append(Preformatted(codeSample, self.code)) + + + def _showDrawingDemo(self, drawing): + """Show a graphical demo of the drawing.""" + + # Add the given drawing to the story. + # Ignored if no GD rendering available + # or the demo method does not return a drawing. + try: + flo = renderPDF.GraphicsFlowable(drawing) + self.story.append(Spacer(6,6)) + self.story.append(flo) + self.story.append(Spacer(6,6)) + except: + if VERBOSE: + print 'caught exception in _showDrawingDemo' + traceback.print_exc() + else: + pass + + + def _showWidgetDemo(self, widget): + """Show a graphical demo of the widget.""" + + # Get a demo drawing from the widget and add it to the story. + # Ignored if no GD rendering available + # or the demo method does not return a drawing. + try: + if VERIFY: + widget.verify() + drawing = widget.demo() + flo = renderPDF.GraphicsFlowable(drawing) + self.story.append(Spacer(6,6)) + self.story.append(flo) + self.story.append(Spacer(6,6)) + except: + if VERBOSE: + print 'caught exception in _showWidgetDemo' + traceback.print_exc() + else: + pass + + + def _showWidgetDemoCode(self, widget): + """Show a demo code of the widget.""" + # Heading + #className = widget.__class__.__name__ + self.story.append(Paragraph("Example", self.bt)) + + # Sample code + codeSample = inspect.getsource(widget.__class__.demo) + self.story.append(Preformatted(codeSample, self.code)) + + + def _showWidgetProperties(self, widget): + """Dump all properties of a widget.""" + + props = widget.getProperties() + keys = props.keys() + keys.sort() + lines = [] + for key in keys: + value = props[key] + + f = getStringIO() + pprint.pprint(value, f) + value = f.getvalue()[:-1] + valueLines = string.split(value, '\n') + for i in range(1, len(valueLines)): + valueLines[i] = ' '*(len(key)+3) + valueLines[i] + value = string.join(valueLines, '\n') + + lines.append('%s = %s' % (key, value)) + + text = join(lines, '\n') + self.story.append(Paragraph("Properties of Example Widget", self.bt)) + self.story.append(Paragraph("", self.bt)) + self.story.append(Preformatted(text, self.code)) + + +class GraphHtmlDocBuilder0(HtmlDocBuilder0): + "A class to write the skeleton of a Python source." + + fileSuffix = '.html' + + def beginModule(self, name, doc, imported): + # Defer displaying the module header info to later... + self.shouldDisplayModule = (name, doc, imported) + self.hasDisplayedModule = 0 + + + def endModule(self, name, doc, imported): + if self.hasDisplayedModule: + HtmlDocBuilder0.endModule(self, name, doc, imported) + + + def beginClasses(self, names): + # Defer displaying the module header info to later... + if self.shouldDisplayModule: + self.shouldDisplayClasses = names + + + # Skip all methods. + def beginMethod(self, name, doc, sig): + pass + + + def endMethod(self, name, doc, sig): + pass + + + def beginClass(self, name, doc, bases): + "Append a graphic demo of a widget at the end of a class." + + aClass = eval('self.skeleton.moduleSpace.' + name) + if issubclass(aClass, Widget): + if self.shouldDisplayModule: + modName, modDoc, imported = self.shouldDisplayModule + self.outLines.append('

              %s

              ' % modName) + self.outLines.append('
              %s
              ' % modDoc) + self.shouldDisplayModule = 0 + self.hasDisplayedModule = 1 + if self.shouldDisplayClasses: + self.outLines.append('

              Classes

              ') + self.shouldDisplayClasses = 0 + + HtmlDocBuilder0.beginClass(self, name, doc, bases) + + + def endClass(self, name, doc, bases): + "Append a graphic demo of a widget at the end of a class." + + HtmlDocBuilder0.endClass(self, name, doc, bases) + + aClass = eval('self.skeleton.moduleSpace.' + name) + if issubclass(aClass, Widget): + widget = aClass() + self._showWidgetDemoCode(widget) + self._showWidgetDemo(widget) + self._showWidgetProperties(widget) + + + def beginFunctions(self, names): + if string.find(string.join(names, ' '), ' sample') > -1: + HtmlDocBuilder0.beginFunctions(self, names) + + + # Skip non-sample functions. + def beginFunction(self, name, doc, sig): + "Skip function for 'uninteresting' names." + + if name[:6] == 'sample': + HtmlDocBuilder0.beginFunction(self, name, doc, sig) + + + def endFunction(self, name, doc, sig): + "Append a drawing to the story for special function names." + + if name[:6] != 'sample': + return + + HtmlDocBuilder0.endFunction(self, name, doc, sig) + aFunc = eval('self.skeleton.moduleSpace.' + name) + drawing = aFunc() + + self._showFunctionDemoCode(aFunc) + self._showDrawingDemo(drawing, aFunc.__name__) + + + def _showFunctionDemoCode(self, function): + """Show a demo code of the function generating the drawing.""" + # Heading + self.outLines.append('

              Example

              ') + + # Sample code + codeSample = inspect.getsource(function) + self.outLines.append('
              %s
              ' % codeSample) + + + def _showDrawingDemo(self, drawing, name): + """Show a graphical demo of the drawing.""" + + # Add the given drawing to the story. + # Ignored if no GD rendering available + # or the demo method does not return a drawing. + try: + from reportlab.graphics import renderPM + modName = self.skeleton.getModuleName() + path = '%s-%s.jpg' % (modName, name) + renderPM.drawToFile(drawing, path, fmt='JPG') + self.outLines.append('

              Demo

              ') + self.outLines.append(makeHtmlInlineImage(path)) + except: + if VERBOSE: + print 'caught exception in GraphHTMLDocBuilder._showDrawingDemo' + traceback.print_exc() + else: + pass + + + def _showWidgetDemo(self, widget): + """Show a graphical demo of the widget.""" + + # Get a demo drawing from the widget and add it to the story. + # Ignored if no GD rendering available + # or the demo method does not return a drawing. + try: + from reportlab.graphics import renderPM + drawing = widget.demo() + if VERIFY: + widget.verify() + modName = self.skeleton.getModuleName() + path = '%s-%s.jpg' % (modName, widget.__class__.__name__) + renderPM.drawToFile(drawing, path, fmt='JPG') + self.outLines.append('

              Demo

              ') + self.outLines.append(makeHtmlInlineImage(path)) + except: + if VERBOSE: + + print 'caught exception in GraphHTMLDocBuilder._showWidgetDemo' + traceback.print_exc() + else: + pass + + + def _showWidgetDemoCode(self, widget): + """Show a demo code of the widget.""" + # Heading + #className = widget.__class__.__name__ + self.outLines.append('

              Example Code

              ') + + # Sample code + codeSample = inspect.getsource(widget.__class__.demo) + self.outLines.append('
              %s
              ' % codeSample) + self.outLines.append('') + + + def _showWidgetProperties(self, widget): + """Dump all properties of a widget.""" + + props = widget.getProperties() + keys = props.keys() + keys.sort() + lines = [] + for key in keys: + value = props[key] + + # Method 3 + f = getStringIO() + pprint.pprint(value, f) + value = f.getvalue()[:-1] + valueLines = string.split(value, '\n') + for i in range(1, len(valueLines)): + valueLines[i] = ' '*(len(key)+3) + valueLines[i] + value = string.join(valueLines, '\n') + + lines.append('%s = %s' % (key, value)) + text = join(lines, '\n') + self.outLines.append('

              Properties of Example Widget

              ') + self.outLines.append('
              %s
              ' % text) + self.outLines.append('') + + +# Highly experimental! +class PlatypusDocBuilder0(DocBuilder0): + "Document the skeleton of a Python module as a Platypus story." + + fileSuffix = '.pps' # A pickled Platypus story. + + def begin(self, name='', typ=''): + styleSheet = getSampleStyleSheet() + self.code = styleSheet['Code'] + self.bt = styleSheet['BodyText'] + self.story = [] + + + def end(self): + if self.packageName: + self.outPath = self.packageName + self.fileSuffix + elif self.skeleton: + self.outPath = self.skeleton.getModuleName() + self.fileSuffix + else: + self.outPath = '' + + if self.outPath: + f = open(self.outPath, 'w') + pickle.dump(self.story, f) + + + def beginPackage(self, name): + DocBuilder0.beginPackage(self, name) + self.story.append(Paragraph(name, self.bt)) + + + def beginModule(self, name, doc, imported): + story = self.story + bt = self.bt + + story.append(Paragraph(name, bt)) + story.append(XPreformatted(doc, bt)) + + + def beginClasses(self, names): + self.story.append(Paragraph('Classes', self.bt)) + + + def beginClass(self, name, doc, bases): + bt = self.bt + story = self.story + if bases: + bases = map(lambda b:b.__name__, bases) # hack + story.append(Paragraph('%s(%s)' % (name, join(bases, ', ')), bt)) + else: + story.append(Paragraph(name, bt)) + + story.append(XPreformatted(doc, bt)) + + + def beginMethod(self, name, doc, sig): + bt = self.bt + story = self.story + story.append(Paragraph(name+sig, bt)) + story.append(XPreformatted(doc, bt)) + + + def beginFunctions(self, names): + if names: + self.story.append(Paragraph('Functions', self.bt)) + + + def beginFunction(self, name, doc, sig): + bt = self.bt + story = self.story + story.append(Paragraph(name+sig, bt)) + story.append(XPreformatted(doc, bt)) + + +#################################################################### +# +# Main +# +#################################################################### + +def printUsage(): + """graphdocpy.py - Automated documentation for the RL Graphics library. + +Usage: python graphdocpy.py [options] + + [options] + -h Print this help message. + + -f name Use the document builder indicated by 'name', + e.g. Html, Pdf. + + -m module Generate document for module named 'module'. + 'module' may follow any of these forms: + - docpy.py + - docpy + - c:\\test\\docpy + and can be any of these: + - standard Python modules + - modules in the Python search path + - modules in the current directory + + -p package Generate document for package named 'package' + (default is 'reportlab.graphics'). + 'package' may follow any of these forms: + - reportlab + - reportlab.graphics.charts + - c:\\test\\reportlab + and can be any of these: + - standard Python packages (?) + - packages in the Python search path + - packages in the current directory + + -s Silent mode (default is unset). + +Examples: + + python graphdocpy.py reportlab.graphics + python graphdocpy.py -m signsandsymbols.py -f Pdf + python graphdocpy.py -m flags.py -f Html + python graphdocpy.py -m barchart1.py +""" + + +# The following functions, including main(), are actually +# the same as in docpy.py (except for some defaults). + +def documentModule0(pathOrName, builder, opts={}): + """Generate documentation for one Python file in some format. + + This handles Python standard modules like string, custom modules + on the Python search path like e.g. docpy as well as modules + specified with their full path like C:/tmp/junk.py. + + The doc file will always be saved in the current directory with + a basename equal to that of the module, e.g. docpy. + """ + cwd = os.getcwd() + + # Append directory to Python search path if we get one. + dirName = os.path.dirname(pathOrName) + if dirName: + sys.path.append(dirName) + + # Remove .py extension from module name. + if pathOrName[-3:] == '.py': + modname = pathOrName[:-3] + else: + modname = pathOrName + + # Remove directory paths from module name. + if dirName: + modname = os.path.basename(modname) + + # Load the module. + try: + module = __import__(modname) + except: + print 'Failed to import %s.' % modname + os.chdir(cwd) + return + + # Do the real documentation work. + s = ModuleSkeleton0() + s.inspect(module) + builder.write(s) + + # Remove appended directory from Python search path if we got one. + if dirName: + del sys.path[-1] + + os.chdir(cwd) + + +def _packageWalkCallback((builder, opts), dirPath, files): + "A callback function used when waking over a package tree." + #must CD into a directory to document the module correctly + cwd = os.getcwd() + os.chdir(dirPath) + + + # Skip __init__ files. + files = filter(lambda f:f != '__init__.py', files) + + files = filter(lambda f:f[-3:] == '.py', files) + for f in files: + path = os.path.join(dirPath, f) +## if not opts.get('isSilent', 0): +## print path + builder.indentLevel = builder.indentLevel + 1 + #documentModule0(path, builder) + documentModule0(f, builder) + builder.indentLevel = builder.indentLevel - 1 + #CD back out + os.chdir(cwd) + +def documentPackage0(pathOrName, builder, opts={}): + """Generate documentation for one Python package in some format. + + 'pathOrName' can be either a filesystem path leading to a Python + package or package name whose path will be resolved by importing + the top-level module. + + The doc file will always be saved in the current directory with + a basename equal to that of the package, e.g. reportlab.lib. + """ + + # Did we get a package path with OS-dependant seperators...? + if os.sep in pathOrName: + path = pathOrName + name = os.path.splitext(os.path.basename(path))[0] + # ... or rather a package name? + else: + name = pathOrName + package = __import__(name) + # Some special care needed for dotted names. + if '.' in name: + subname = 'package' + name[find(name, '.'):] + package = eval(subname) + path = os.path.dirname(package.__file__) + + cwd = os.getcwd() + os.chdir(path) + builder.beginPackage(name) + os.path.walk(path, _packageWalkCallback, (builder, opts)) + builder.endPackage(name) + os.chdir(cwd) + + +def makeGraphicsReference(outfilename): + "Make graphics_reference.pdf" + builder = GraphPdfDocBuilder0() + + builder.begin(name='reportlab.graphics', typ='package') + documentPackage0('reportlab.graphics', builder, {'isSilent': 0}) + builder.end(outfilename) + print 'made graphics reference in %s' % outfilename + +def main(): + "Handle command-line options and trigger corresponding action." + + opts, args = getopt.getopt(sys.argv[1:], 'hsf:m:p:') + + # Make an options dictionary that is easier to use. + optsDict = {} + for k, v in opts: + optsDict[k] = v + hasOpt = optsDict.has_key + + # On -h print usage and exit immediately. + if hasOpt('-h'): + print printUsage.__doc__ + sys.exit(0) + + # On -s set silent mode. + isSilent = hasOpt('-s') + + # On -f set the appropriate DocBuilder to use or a default one. + builder = { 'Pdf': GraphPdfDocBuilder0, + 'Html': GraphHtmlDocBuilder0, + }[optsDict.get('-f', 'Pdf')]() + + # Set default module or package to document. + if not hasOpt('-p') and not hasOpt('-m'): + optsDict['-p'] = 'reportlab.graphics' + + # Save a few options for further use. + options = {'isSilent':isSilent} + + # Now call the real documentation functions. + if hasOpt('-m'): + nameOrPath = optsDict['-m'] + if not isSilent: + print "Generating documentation for module %s..." % nameOrPath + builder.begin(name=nameOrPath, typ='module') + documentModule0(nameOrPath, builder, options) + elif hasOpt('-p'): + nameOrPath = optsDict['-p'] + if not isSilent: + print "Generating documentation for package %s..." % nameOrPath + builder.begin(name=nameOrPath, typ='package') + documentPackage0(nameOrPath, builder, options) + builder.end() + + if not isSilent: + print "Saved %s." % builder.outPath + + #if doing the usual, put a copy in docs + if builder.outPath == 'reportlab.graphics.pdf': + import shutil, reportlab + dst = os.path.join(os.path.dirname(reportlab.__file__),'docs','graphics_reference.pdf') + shutil.copyfile('reportlab.graphics.pdf', dst) + if not isSilent: + print 'copied to '+dst + +def makeSuite(): + "standard test harness support - run self as separate process" + from reportlab.test.utils import ScriptThatMakesFileTest + return ScriptThatMakesFileTest('tools/docco', + 'graphdocpy.py', + 'reportlab.graphics.pdf') + +if __name__ == '__main__': + main() diff --git a/bin/reportlab/tools/docco/rl_doc_utils.py b/bin/reportlab/tools/docco/rl_doc_utils.py new file mode 100644 index 00000000000..a105b9d7e88 --- /dev/null +++ b/bin/reportlab/tools/docco/rl_doc_utils.py @@ -0,0 +1,411 @@ +#!/bin/env python +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/rl_doc_utils.py +__version__=''' $Id: rl_doc_utils.py 2830 2006-04-05 15:18:32Z rgbecker $ ''' + + +__doc__ = """ +This module contains utilities for generating guides +""" + +import os, sys, glob +import string + +from rltemplate import RLDocTemplate +from stylesheet import getStyleSheet +styleSheet = getStyleSheet() + +#from reportlab.platypus.doctemplate import SimpleDocTemplate +from reportlab.lib.units import inch +from reportlab.lib.pagesizes import letter, A4, A5, A3 # latter two for testing +from reportlab.rl_config import defaultPageSize +from reportlab.platypus import figures +from reportlab.platypus import Paragraph, Spacer, Preformatted,\ + PageBreak, CondPageBreak, Flowable, Table, TableStyle, \ + NextPageTemplate, KeepTogether, Image, XPreformatted +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib import colors +from reportlab.lib.sequencer import getSequencer + +import examples + +appmode=0 + + +from t_parse import Template +QFcodetemplate = Template("X$X$", "X") +QFreptemplate = Template("X^X^", "X") +codesubst = "%s%s" +QFsubst = "%s%s" + + +def quickfix(text): + """inside text find any subsequence of form $subsequence$. + Format the subsequence as code. If similarly if text contains ^arg^ + format the arg as replaceable. The escape sequence for literal + $ is $\\$ (^ is ^\\^. + """ + from string import join + for (template,subst) in [(QFcodetemplate, codesubst), (QFreptemplate, QFsubst)]: + fragment = text + parts = [] + try: + while fragment: + try: + (matches, index) = template.PARSE(fragment) + except: raise ValueError + else: + [prefix, code] = matches + if code == "\\": + part = fragment[:index] + else: + part = subst % (prefix, code) + parts.append(part) + fragment = fragment[index:] + except ValueError: + parts.append(fragment) + text = join(parts, "") + return text +#print quickfix("$testing$ testing $one$ ^two^ $three(^four^)$") + + + +H1 = styleSheet['Heading1'] +H2 = styleSheet['Heading2'] +H3 = styleSheet['Heading3'] +H4 = styleSheet['Heading4'] +B = styleSheet['BodyText'] +BU = styleSheet['Bullet'] +Comment = styleSheet['Comment'] +Centred = styleSheet['Centred'] +Caption = styleSheet['Caption'] + +#set up numbering +seq = getSequencer() +seq.setFormat('Chapter','1') +seq.setFormat('Section','1') +seq.setFormat('Appendix','A') +seq.setFormat('Figure', '1') +seq.chain('Chapter','Section') +seq.chain('Chapter','Figure') + +lessonnamestyle = H2 +discussiontextstyle = B +exampletextstyle = styleSheet['Code'] +# size for every example +examplefunctionxinches = 5.5 +examplefunctionyinches = 3 +examplefunctiondisplaysizes = (examplefunctionxinches*inch, examplefunctionyinches*inch) + +def getJustFontPaths(): + '''return afm and pfb for Just's files''' + import reportlab + folder = os.path.dirname(reportlab.__file__) + os.sep + 'fonts' + return os.path.join(folder, 'LeERC___.AFM'), os.path.join(folder, 'LeERC___.PFB') + +# for testing +def NOP(*x,**y): + return None + +def CPage(inches): + getStory().append(CondPageBreak(inches*inch)) + +def newPage(): + getStory().append(PageBreak()) + +def nextTemplate(templName): + f = NextPageTemplate(templName) + getStory().append(f) + +def disc(text, klass=Paragraph, style=discussiontextstyle): + text = quickfix(text) + P = klass(text, style) + getStory().append(P) + +def restartList(): + getSequencer().reset('list1') + +def list(text, doBullet=1): + text=quickfix(text) + if doBullet: + text='.'+text + P = Paragraph(text, BU) + getStory().append(P) + +def bullet(text): + text='\xe2\x80\xa2' + quickfix(text) + P = Paragraph(text, BU) + getStory().append(P) + +def eg(text,before=0.1,after=0): + space(before) + disc(text, klass=Preformatted, style=exampletextstyle) + space(after) + +def space(inches=1./6): + if inches: getStory().append(Spacer(0,inches*inch)) + +def EmbeddedCode(code,name='t'): + eg(code) + disc("produces") + exec code+("\ngetStory().append(%s)\n"%name) + +def startKeep(): + return len(getStory()) + +def endKeep(s): + S = getStory() + k = KeepTogether(S[s:]) + S[s:] = [k] + +def title(text): + """Use this for the document title only""" + disc(text,style=styleSheet['Title']) + +#AR 3/7/2000 - defining three new levels of headings; code +#should be swapped over to using them. + +def heading1(text): + """Use this for chapters. Lessons within a big chapter + should now use heading2 instead. Chapters get numbered.""" + getStory().append(PageBreak()) + p = Paragraph('Chapter ' + quickfix(text), H1) + getStory().append(p) + +def Appendix1(text,): + global appmode + getStory().append(PageBreak()) + if not appmode: + seq.setFormat('Chapter','A') + seq.reset('Chapter') + appmode = 1 + p = Paragraph('Appendix ' + quickfix(text), H1) + getStory().append(p) + +def heading2(text): + """Used to be 'lesson'""" + getStory().append(CondPageBreak(inch)) + p = Paragraph('' + quickfix(text), H2) + getStory().append(p) + +def heading3(text): + """Used to be most of the plain old 'head' sections""" + getStory().append(CondPageBreak(inch)) + p = Paragraph(quickfix(text), H3) + getStory().append(p) + +def image(path, width=None, height=None ): + s = startKeep() + space(.2) + import reportlab + rlDocImageDir = os.path.join(os.path.dirname(reportlab.__file__), 'docs','images') + getStory().append(Image(os.path.join(rlDocImageDir,path),width,height)) + space(.2) + endKeep(s) + +def heading4(text): + """Used to be most of the plain old 'head' sections""" + getStory().append(CondPageBreak(inch)) + p = Paragraph(quickfix(text), H4) + getStory().append(p) + +def todo(text): + """Used for notes to ourselves""" + getStory().append(Paragraph(quickfix(text), Comment)) + +def centred(text): + getStory().append(Paragraph(quickfix(text), Centred)) + +def caption(text): + getStory().append(Paragraph(quickfix(text), Caption)) + +class Illustration(figures.Figure): + """The examples are all presented as functions which do + something to a canvas, with a constant height and width + used. This puts them inside a figure box with a caption.""" + + def __init__(self, operation, caption, width=None, height=None): + stdwidth, stdheight = examplefunctiondisplaysizes + if not width: + width = stdwidth + if not height: + height = stdheight + #figures.Figure.__init__(self, stdwidth * 0.75, stdheight * 0.75) + figures.Figure.__init__(self, width, height, + 'Figure : ' + quickfix(caption)) + self.operation = operation + + def drawFigure(self): + #shrink it a little... + #self.canv.scale(0.75, 0.75) + self.operation(self.canv) + + +def illust(operation, caption, width=None, height=None): + i = Illustration(operation, caption, width=width, height=height) + getStory().append(i) + + +class GraphicsDrawing(Illustration): + """Lets you include reportlab/graphics drawings seamlessly, + with the right numbering.""" + def __init__(self, drawing, caption): + figures.Figure.__init__(self, + drawing.width, + drawing.height, + 'Figure : ' + quickfix(caption) + ) + self.drawing = drawing + + def drawFigure(self): + d = self.drawing + d.wrap(d.width, d.height) + d.drawOn(self.canv, 0, 0) + +def draw(drawing, caption): + d = GraphicsDrawing(drawing, caption) + getStory().append(d) + +class ParaBox(figures.Figure): + """Illustrates paragraph examples, with style attributes on the left""" + descrStyle = ParagraphStyle('description', + fontName='Courier', + fontSize=8, + leading=9.6) + + def __init__(self, text, style, caption): + figures.Figure.__init__(self, 0, 0, caption) + self.text = text + self.style = style + self.para = Paragraph(text, style) + + styleText = self.getStyleText(style) + self.pre = Preformatted(styleText, self.descrStyle) + + def wrap(self, availWidth, availHeight): + """Left 30% is for attributes, right 50% for sample, + 10% gutter each side.""" + self.x0 = availWidth * 0.05 #left of box + self.x1 = availWidth * 0.1 #left of descriptive text + self.x2 = availWidth * 0.5 #left of para itself + self.x3 = availWidth * 0.9 #right of para itself + self.x4 = availWidth * 0.95 #right of box + self.width = self.x4 - self.x0 + self.dx = 0.5 * (availWidth - self.width) + + paw, self.pah = self.para.wrap(self.x3 - self.x2, availHeight) + self.pah = self.pah + self.style.spaceBefore + self.style.spaceAfter + prw, self.prh = self.pre.wrap(self.x2 - self.x1, availHeight) + self.figureHeight = max(self.prh, self.pah) * 10.0/9.0 + return figures.Figure.wrap(self, availWidth, availHeight) + + def getStyleText(self, style): + """Converts style to preformatted block of text""" + lines = [] + for (key, value) in style.__dict__.items(): + lines.append('%s = %s' % (key, value)) + lines.sort() + return string.join(lines, '\n') + + def drawFigure(self): + + #now we fill in the bounding box and before/after boxes + self.canv.saveState() + self.canv.setFillGray(0.95) + self.canv.setDash(1,3) + self.canv.rect(self.x2 - self.x0, + self.figureHeight * 0.95 - self.pah, + self.x3-self.x2, self.para.height, + fill=1,stroke=1) + + self.canv.setFillGray(0.90) + self.canv.rect(self.x2 - self.x0, #spaceBefore + self.figureHeight * 0.95 - self.pah + self.para.height, + self.x3-self.x2, self.style.spaceBefore, + fill=1,stroke=1) + + self.canv.rect(self.x2 - self.x0, #spaceBefore + self.figureHeight * 0.95 - self.pah - self.style.spaceAfter, + self.x3-self.x2, self.style.spaceAfter, + fill=1,stroke=1) + + self.canv.restoreState() + #self.canv.setFillColor(colors.yellow) + self.para.drawOn(self.canv, self.x2 - self.x0, + self.figureHeight * 0.95 - self.pah) + self.pre.drawOn(self.canv, self.x1 - self.x0, + self.figureHeight * 0.95 - self.prh) + + + def getStyleText(self, style): + """Converts style to preformatted block of text""" + lines = [] + for (key, value) in style.__dict__.items(): + if key not in ('name','parent'): + lines.append('%s = %s' % (key, value)) + return string.join(lines, '\n') + + +class ParaBox2(figures.Figure): + """Illustrates a paragraph side-by-side with the raw + text, to show how the XML works.""" + def __init__(self, text, caption): + figures.Figure.__init__(self, 0, 0, caption) + descrStyle = ParagraphStyle('description', + fontName='Courier', + fontSize=8, + leading=9.6) + textStyle = B + self.text = text + self.left = Paragraph('', descrStyle) + self.right = Paragraph(text, B) + + + def wrap(self, availWidth, availHeight): + self.width = availWidth * 0.9 + colWidth = 0.4 * self.width + lw, self.lh = self.left.wrap(colWidth, availHeight) + rw, self.rh = self.right.wrap(colWidth, availHeight) + self.figureHeight = max(self.lh, self.rh) * 10.0/9.0 + return figures.Figure.wrap(self, availWidth, availHeight) + + def drawFigure(self): + self.left.drawOn(self.canv, + self.width * 0.05, + self.figureHeight * 0.95 - self.lh + ) + self.right.drawOn(self.canv, + self.width * 0.55, + self.figureHeight * 0.95 - self.rh + ) + +def parabox(text, style, caption): + p = ParaBox(text, style, + 'Figure : ' + quickfix(caption) + ) + getStory().append(p) + +def parabox2(text, caption): + p = ParaBox2(text, + 'Figure : ' + quickfix(caption) + ) + getStory().append(p) + +def pencilnote(): + getStory().append(examples.NoteAnnotation()) + + +from reportlab.lib.colors import tan, green +def handnote(xoffset=0, size=None, fillcolor=tan, strokecolor=green): + getStory().append(examples.HandAnnotation(xoffset,size,fillcolor,strokecolor)) + + +#make a singleton, created when requested rather +#than each time a chapter imports it. +_story = [] +def setStory(story=[]): + global _story + _story = story +def getStory(): + return _story diff --git a/bin/reportlab/tools/docco/rltemplate.py b/bin/reportlab/tools/docco/rltemplate.py new file mode 100644 index 00000000000..466660e639f --- /dev/null +++ b/bin/reportlab/tools/docco/rltemplate.py @@ -0,0 +1,135 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/rltemplate.py +# doc template for RL manuals. Currently YAML is hard-coded +#to use this, which is wrong. + + +from reportlab.platypus import PageTemplate, \ + BaseDocTemplate, Frame, Paragraph +from reportlab.lib.units import inch, cm +from reportlab.rl_config import defaultPageSize + + +class FrontCoverTemplate(PageTemplate): + def __init__(self, id, pageSize=defaultPageSize): + self.pageWidth = pageSize[0] + self.pageHeight = pageSize[1] + frame1 = Frame(inch, + 3*inch, + self.pageWidth - 2*inch, + self.pageHeight - 518, id='cover') + PageTemplate.__init__(self, id, [frame1]) # note lack of onPage + + def afterDrawPage(self, canvas, doc): + canvas.saveState() + canvas.drawImage('../images/replogo.gif',2*inch, 8*inch) + + + canvas.setFont('Times-Roman', 10) + canvas.line(inch, 120, self.pageWidth - inch, 120) + + canvas.drawString(inch, 100, '165 The Broadway') + canvas.drawString(inch, 88, 'Wimbledon') + canvas.drawString(inch, 76, 'London SW19 1NE') + canvas.drawString(inch, 64, 'United Kingdom') + + canvas.restoreState() + + +class OneColumnTemplate(PageTemplate): + def __init__(self, id, pageSize=defaultPageSize): + self.pageWidth = pageSize[0] + self.pageHeight = pageSize[1] + frame1 = Frame(inch, + inch, + self.pageWidth - 2*inch, + self.pageHeight - 2*inch, + id='normal') + PageTemplate.__init__(self, id, [frame1]) # note lack of onPage + + def afterDrawPage(self, canvas, doc): + y = self.pageHeight - 50 + canvas.saveState() + canvas.setFont('Times-Roman', 10) + canvas.drawString(inch, y+8, doc.title) + canvas.drawRightString(self.pageWidth - inch, y+8, doc.chapter) + canvas.line(inch, y, self.pageWidth - inch, y) + canvas.drawCentredString(doc.pagesize[0] / 2, 0.75*inch, 'Page %d' % canvas.getPageNumber()) + canvas.restoreState() + +class TwoColumnTemplate(PageTemplate): + def __init__(self, id, pageSize=defaultPageSize): + self.pageWidth = pageSize[0] + self.pageHeight = pageSize[1] + colWidth = 0.5 * (self.pageWidth - 2.25*inch) + frame1 = Frame(inch, + inch, + colWidth, + self.pageHeight - 2*inch, + id='leftCol') + frame2 = Frame(0.5 * self.pageWidth + 0.125, + inch, + colWidth, + self.pageHeight - 2*inch, + id='rightCol') + PageTemplate.__init__(self, id, [frame1, frame2]) # note lack of onPage + + def afterDrawPage(self, canvas, doc): + y = self.pageHeight - 50 + canvas.saveState() + canvas.setFont('Times-Roman', 10) + canvas.drawString(inch, y+8, doc.title) + canvas.drawRightString(self.pageWidth - inch, y+8, doc.chapter) + canvas.line(inch, y, self.pageWidth - inch, y*inch) + canvas.drawCentredString(doc.pagesize[0] / 2, 0.75*inch, 'Page %d' % canvas.getPageNumber()) + canvas.restoreState() + + +class RLDocTemplate(BaseDocTemplate): + def afterInit(self): + self.addPageTemplates(FrontCoverTemplate('Cover', self.pagesize)) + self.addPageTemplates(OneColumnTemplate('Normal', self.pagesize)) + self.addPageTemplates(TwoColumnTemplate('TwoColumn', self.pagesize)) + + #just playing + self.title = "(Document Title Goes Here)" + self.chapter = "(No chapter yet)" + self.chapterNo = 1 #unique keys + self.sectionNo = 1 # unique keys + +## # AR hack +## self.counter = 1 + def beforeDocument(self): + self.canv.showOutline() + + def afterFlowable(self, flowable): + """Detect Level 1 and 2 headings, build outline, + and track chapter title.""" + if isinstance(flowable, Paragraph): + style = flowable.style.name + +## #AR debug text +## try: +## print '%d: %s...' % (self.counter, flowable.getPlainText()[0:40]) +## except AttributeError: +## print '%d: (something with ABag)' % self.counter +## self.counter = self.counter + 1 + + if style == 'Title': + self.title = flowable.getPlainText() + elif style == 'Heading1': + self.chapter = flowable.getPlainText() + key = 'ch%d' % self.chapterNo + self.canv.bookmarkPage(key) + self.canv.addOutlineEntry(flowable.getPlainText(), + key, 0, 0) + self.chapterNo = self.chapterNo + 1 + self.sectionNo = 1 + elif style == 'Heading2': + self.section = flowable.text + key = 'ch%ds%d' % (self.chapterNo, self.sectionNo) + self.canv.bookmarkPage(key) + self.canv.addOutlineEntry(flowable.getPlainText(), + key, 1, 0) + self.sectionNo = self.sectionNo + 1 \ No newline at end of file diff --git a/bin/reportlab/tools/docco/stylesheet.py b/bin/reportlab/tools/docco/stylesheet.py new file mode 100644 index 00000000000..9e8d2c65840 --- /dev/null +++ b/bin/reportlab/tools/docco/stylesheet.py @@ -0,0 +1,165 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/stylesheet.py +#standard stylesheet for our manuals +from reportlab.lib.styles import StyleSheet1, ParagraphStyle +from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_RIGHT, TA_JUSTIFY +from reportlab.lib import colors + + +def getStyleSheet(): + """Returns a stylesheet object""" + stylesheet = StyleSheet1() + + stylesheet.add(ParagraphStyle(name='Normal', + fontName='Times-Roman', + fontSize=10, + leading=12, + spaceBefore=6) + ) + + stylesheet.add(ParagraphStyle(name='Comment', + fontName='Times-Italic') + ) + + stylesheet.add(ParagraphStyle(name='Indent0', + leftIndent=18,) + ) + + stylesheet.add(ParagraphStyle(name='Indent1', + leftIndent=36, + firstLineIndent=0, + spaceBefore=1, + spaceAfter=7) + ) + + stylesheet.add(ParagraphStyle(name='Indent2', + leftIndent=50, + firstLineIndent=0, + spaceAfter=100) + ) + + stylesheet.add(ParagraphStyle(name='BodyText', + parent=stylesheet['Normal'], + spaceBefore=6) + ) + stylesheet.add(ParagraphStyle(name='Italic', + parent=stylesheet['BodyText'], + fontName = 'Times-Italic') + ) + + stylesheet.add(ParagraphStyle(name='Heading1', + parent=stylesheet['Normal'], + fontName = 'Times-Bold', + alignment=TA_CENTER, + fontSize=18, + leading=22, + spaceAfter=6), + alias='h1') + + stylesheet.add(ParagraphStyle(name='Heading2', + parent=stylesheet['Normal'], + fontName = 'Times-Bold', + fontSize=14, + leading=17, + spaceBefore=12, + spaceAfter=6), + alias='h2') + + stylesheet.add(ParagraphStyle(name='Heading3', + parent=stylesheet['Normal'], + fontName = 'Times-BoldItalic', + fontSize=12, + leading=14, + spaceBefore=12, + spaceAfter=6), + alias='h3') + + stylesheet.add(ParagraphStyle(name='Heading4', + parent=stylesheet['Normal'], + fontName = 'Times-BoldItalic', + spaceBefore=10, + spaceAfter=4), + alias='h4') + + stylesheet.add(ParagraphStyle(name='Title', + parent=stylesheet['Normal'], + fontName = 'Times-Bold', + fontSize=32, + leading=40, + spaceAfter=36, + alignment=TA_CENTER + ), + alias='t') + + stylesheet.add(ParagraphStyle(name='Bullet', + parent=stylesheet['Normal'], + firstLineIndent=0, + leftIndent=54, + bulletIndent=18, + spaceBefore=0, + bulletFontName='Symbol'), + alias='bu') + + stylesheet.add(ParagraphStyle(name='Definition', + parent=stylesheet['Normal'], + firstLineIndent=0, + leftIndent=36, + bulletIndent=0, + spaceBefore=6, + bulletFontName='Times-BoldItalic'), + alias='df') + + stylesheet.add(ParagraphStyle(name='Code', + parent=stylesheet['Normal'], + fontName='Courier', + textColor=colors.navy, + fontSize=8, + leading=8.8, + leftIndent=36, + firstLineIndent=0)) + + stylesheet.add(ParagraphStyle(name='Link', + parent=stylesheet['Code'], + spaceAfter=7, + spaceBefore=0, + leftIndent=55)) + + stylesheet.add(ParagraphStyle(name='FunctionHeader', + parent=stylesheet['Normal'], + fontName='Courier-Bold', + fontSize=8, + leading=8.8)) + + stylesheet.add(ParagraphStyle(name='DocString', + parent=stylesheet['Normal'], + fontName='Courier', + fontSize=8, + leftIndent=18, + leading=8.8)) + + stylesheet.add(ParagraphStyle(name='DocStringIndent', + parent=stylesheet['Normal'], + fontName='Courier', + fontSize=8, + leftIndent=36, + leading=8.8)) + + stylesheet.add(ParagraphStyle(name='URL', + parent=stylesheet['Normal'], + fontName='Courier', + textColor=colors.navy, + alignment=TA_CENTER), + alias='u') + + stylesheet.add(ParagraphStyle(name='Centred', + parent=stylesheet['Normal'], + alignment=TA_CENTER + )) + + stylesheet.add(ParagraphStyle(name='Caption', + parent=stylesheet['Centred'], + fontName='Times-Italic' + )) + + return stylesheet \ No newline at end of file diff --git a/bin/reportlab/tools/docco/t_parse.py b/bin/reportlab/tools/docco/t_parse.py new file mode 100644 index 00000000000..0dc66c3c879 --- /dev/null +++ b/bin/reportlab/tools/docco/t_parse.py @@ -0,0 +1,247 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/t_parse.py +""" +Template parsing module inspired by REXX (with thanks to Donn Cave for discussion). + +Template initialization has the form: + T = Template(template_string, wild_card_marker, single_char_marker, + x = regex_x, y = regex_y, ...) +Parsing has the form + ([match1, match2, ..., matchn], lastindex) = T.PARSE(string) + +Only the first argument is mandatory. + +The resultant object efficiently parses strings that match the template_string, +giving a list of substrings that correspond to each "directive" of the template. + +Template directives: + + Wildcard: + The template may be initialized with a wildcard that matches any string + up to the string matching the next directive (which may not be a wild + card or single character marker) or the next literal sequence of characters + of the template. The character that represents a wildcard is specified + by the wild_card_marker parameter, which has no default. + + For example, using X as the wildcard: + + + >>> T = Template("prefixXinteriorX", "X") + >>> T.PARSE("prefix this is before interior and this is after") + ([' this is before ', ' and this is after'], 47) + >>> T = Template("X", "X") + >>> T.PARSE('go to index') + (['A HREF="index.html"', 'go to index', '/A'], 36) + + Obviously the character used to represent the wildcard must be distinct + from the characters used to represent literals or other directives. + + Fixed length character sequences: + The template may have a marker character which indicates a fixed + length field. All adjacent instances of this marker will be matched + by a substring of the same length in the parsed string. For example: + + >>> T = Template("NNN-NN-NNNN", single_char_marker="N") + >>> T.PARSE("1-2-34-5-12") + (['1-2', '34', '5-12'], 11) + >>> T.PARSE("111-22-3333") + (['111', '22', '3333'], 11) + >>> T.PARSE("1111-22-3333") + ValueError: literal not found at (3, '-') + + A template may have multiple fixed length markers, which allows fixed + length fields to be adjacent, but recognized separately. For example: + + >>> T = Template("MMDDYYX", "X", "MDY") + >>> T.PARSE("112489 Somebody's birthday!") + (['11', '24', '89', " Somebody's birthday!"], 27) + + Regular expression markers: + The template may have markers associated with regular expressions. + the regular expressions may be either string represenations of compiled. + For example: + >>> T = Template("v: s i", v=id, s=str, i=int) + >>> T.PARSE("this_is_an_identifier: 'a string' 12344") + (['this_is_an_identifier', "'a string'", '12344'], 39) + >>> + Here id, str, and int are regular expression conveniences provided by + this module. + + Directive markers may be mixed and matched, except that wildcards cannot precede + wildcards or single character markers. + Example: +>>> T = Template("ssnum: NNN-NN-NNNN, fn=X, ln=X, age=I, quote=Q", "X", "N", I=int, Q=str) +>>> T.PARSE("ssnum: 123-45-6789, fn=Aaron, ln=Watters, age=13, quote='do be do be do'") +(['123', '45', '6789', 'Aaron', 'Watters', '13', "'do be do be do'"], 72) +>>> + +""" + +import re, string +from types import StringType +from string import find + +# +# template parsing +# +# EG: T = Template("(NNN)NNN-NNNN X X", "X", "N") +# ([area, exch, ext, fn, ln], index) = T.PARSE("(908)949-2726 Aaron Watters") +# +class Template: + + def __init__(self, + template, + wild_card_marker=None, + single_char_marker=None, + **marker_to_regex_dict): + self.template = template + self.wild_card = wild_card_marker + self.char = single_char_marker + # determine the set of markers for this template + markers = marker_to_regex_dict.keys() + if wild_card_marker: + markers.append(wild_card_marker) + if single_char_marker: + for ch in single_char_marker: # allow multiple scm's + markers.append(ch) + self.char = single_char_primary = single_char_marker[0] + self.markers = markers + for mark in markers: + if len(mark)>1: + raise ValueError, "Marks must be single characters: "+`mark` + # compile the regular expressions if needed + self.marker_dict = marker_dict = {} + for (mark, rgex) in marker_to_regex_dict.items(): + if type(rgex) == StringType: + rgex = re.compile(rgex) + marker_dict[mark] = rgex + # determine the parse sequence + parse_seq = [] + # dummy last char + lastchar = None + index = 0 + last = len(template) + # count the number of directives encountered + ndirectives = 0 + while index s blah", s=str) + s = "' <-- a string --> ' --> 'blah blah another string blah' blah" + print T1.PARSE(s) + + T2 = Template("s --> NNNiX", "X", "N", s=str, i=int) + print T2.PARSE("'A STRING' --> 15964653alpha beta gamma") + + T3 = Template("XsXi", "X", "N", s=str, i=int) + print T3.PARSE("prefix'string'interior1234junk not parsed") + + T4 = Template("MMDDYYX", "X", "MDY") + print T4.PARSE("122961 Somebody's birthday!") + + +if __name__=="__main__": test() \ No newline at end of file diff --git a/bin/reportlab/tools/docco/yaml.py b/bin/reportlab/tools/docco/yaml.py new file mode 100644 index 00000000000..af31788622e --- /dev/null +++ b/bin/reportlab/tools/docco/yaml.py @@ -0,0 +1,201 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/yaml.py +# parses "Yet Another Markup Language" into a list of tuples. +# Each tuple says what the data is e.g. +# ('Paragraph', 'Heading1', 'Why Reportlab Rules') +# and the pattern depends on type. +""" +Parser for "Aaron's Markup Language" - a markup language +which is easier to type in than XML, yet gives us a +reasonable selection of formats. + +The general rule is that if a line begins with a '.', +it requires special processing. Otherwise lines +are concatenated to paragraphs, and blank lines +separate paragraphs. + +If the line ".foo bar bletch" is encountered, +it immediately ends and writes out any current +paragraph. + +It then looks for a parser method called 'foo'; +if found, it is called with arguments (bar, bletch). + +If this is not found, it assumes that 'foo' is a +paragraph style, and the text for the first line +of the paragraph is 'bar bletch'. It would be +up to the formatter to decide whether on not 'foo' +was a valid paragraph. + +Special commands understood at present are: +.image filename +- adds the image to the document +.beginPre Code +- begins a Preformatted object in style 'Code' +.endPre +- ends a preformatted object. +""" + + +import sys +import string +import imp +import codegrab + +#modes: +PLAIN = 1 +PREFORMATTED = 2 + +BULLETCHAR = '\267' # assumes font Symbol, but works on all platforms + +class Parser: + def __init__(self): + self.reset() + + def reset(self): + self._lineNo = 0 + self._style = 'Normal' # the default + self._results = [] + self._buf = [] + self._mode = PLAIN + + def parseFile(self, filename): + #returns list of objects + data = open(filename, 'r').readlines() + + for line in data: + #strip trailing newlines + self.readLine(line[:-1]) + self.endPara() + return self._results + + def readLine(self, line): + #this is the inner loop + self._lineNo = self._lineNo + 1 + stripped = string.lstrip(line) + if len(stripped) == 0: + if self._mode == PLAIN: + self.endPara() + else: #preformatted, append it + self._buf.append(line) + elif line[0]=='.': + # we have a command of some kind + self.endPara() + words = string.split(stripped[1:]) + cmd, args = words[0], words[1:] + + #is it a parser method? + if hasattr(self.__class__, cmd): + method = eval('self.'+cmd) + #this was very bad; any type error in the method was hidden + #we have to hack the traceback + try: + apply(method, tuple(args)) + except TypeError, err: + sys.stderr.write("Parser method: apply(%s,%s) %s at line %d\n" % (cmd, tuple(args), err, self._lineNo)) + raise + else: + # assume it is a paragraph style - + # becomes the formatter's problem + self.endPara() #end the last one + words = string.split(stripped, ' ', 1) + assert len(words)==2, "Style %s but no data at line %d" % (words[0], self._lineNo) + (styletag, data) = words + self._style = styletag[1:] + self._buf.append(data) + else: + #we have data, add to para + self._buf.append(line) + + def endPara(self): + #ends the current paragraph, or preformatted block + + text = string.join(self._buf, ' ') + if text: + if self._mode == PREFORMATTED: + #item 3 is list of lines + self._results.append(('Preformatted', self._style, + string.join(self._buf,'\n'))) + else: + self._results.append(('Paragraph', self._style, text)) + self._buf = [] + self._style = 'Normal' + + def beginPre(self, stylename): + self._mode = PREFORMATTED + self._style = stylename + + def endPre(self): + self.endPara() + self._mode = PLAIN + + def image(self, filename): + self.endPara() + self._results.append(('Image', filename)) + + def vSpace(self, points): + """Inserts a vertical spacer""" + self._results.append(('VSpace', points)) + + def pageBreak(self): + """Inserts a frame break""" + self._results.append(('PageBreak','blah')) # must be a tuple + + def custom(self, moduleName, funcName): + """Goes and gets the Python object and adds it to the story""" + self.endPara() + self._results.append(('Custom',moduleName, funcName)) + + + + def getModuleDoc(self, modulename, pathname=None): + """Documents the entire module at this point by making + paragraphs and preformatted objects""" + docco = codegrab.getObjectsDefinedIn(modulename, pathname) + if docco.doc <> None: + self._results.append(('Paragraph', 'DocString', docco.doc)) + if len(docco.functions) > 0: + for fn in docco.functions: + if fn.status == 'official': + self._results.append(('Preformatted','FunctionHeader', fn.proto)) + self._results.append(('Preformatted','DocString', fn.doc)) + + if len(docco.classes) > 0: + for cls in docco.classes: + if cls.status == 'official': + self._results.append(('Preformatted','FunctionHeader', 'Class %s:' % cls.name)) + self._results.append(('Preformatted','DocString', cls.doc)) + for mth in cls.methods: + if mth.status == 'official': + self._results.append(('Preformatted','FunctionHeader', mth.proto)) + self._results.append(('Preformatted','DocStringIndent', mth.doc)) + + + def getClassDoc(self, modulename, classname, pathname=None): + """Documents the class and its public methods""" + docco = codegrab.getObjectsDefinedIn(modulename, pathname) + found = 0 + for cls in docco.classes: + if cls.name == classname: + found = 1 + self._results.append(('Preformatted','FunctionHeader', 'Class %s:' % cls.name)) + self._results.append(('Preformatted','DocString', cls.doc)) + for mth in cls.methods: + if mth.status == 'official': + self._results.append(('Preformatted','FunctionHeader', mth.proto)) + self._results.append(('Preformatted','DocStringIndent', mth.doc)) + break + assert found, 'No Classes Defined in ' + modulename + + def nextPageTemplate(self, templateName): + self._results.append(('NextPageTemplate',templateName)) + +if __name__=='__main__': #NORUNTESTS + if len(sys.argv) <> 2: + print 'usage: yaml.py source.txt' + else: + p = Parser() + results = p.parseFile(sys.argv[1]) + import pprint + pprint.pprint(results) \ No newline at end of file diff --git a/bin/reportlab/tools/docco/yaml2pdf.py b/bin/reportlab/tools/docco/yaml2pdf.py new file mode 100644 index 00000000000..454bf53cf71 --- /dev/null +++ b/bin/reportlab/tools/docco/yaml2pdf.py @@ -0,0 +1,104 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/docco/yaml2pdf.py +# yaml2pdf - turns stuff in Yet Another Markup Language +# into PDF documents. Very crude - it assumes a +# doc template and stylesheet (hard coded for now) +# and basically cranks out paragraphs in each style +"""yaml2pdf.py - converts Yet Another Markup Language +to reasonable PDF documents. This is ReportLab's +basic documentation tool. + +Usage: +. "yaml2pdf.py filename.ext" will create "filename.pdf" +""" + +import sys +import os +import imp + +import yaml +from rltemplate import RLDocTemplate +from reportlab.lib.styles import ParagraphStyle +from reportlab.lib.enums import * +from reportlab.lib.pagesizes import A4 +from reportlab.platypus import * +from reportlab.lib import colors +from reportlab.lib.units import inch + + +from stylesheet import getStyleSheet + + +def run(infilename, outfilename): + p = yaml.Parser() + results = p.parseFile(infilename) + + ss = getStyleSheet() + + #now make flowables from the results + story = [] + for thingy in results: + typ = thingy[0] + if typ == 'Paragraph': + (typ2, stylename, text) = thingy + if stylename == 'bu': + bulletText='\267' + else: + bulletText=None + try: + style = ss[stylename] + except KeyError: + print 'Paragraph style "%s" not found in stylesheet, using Normal instead' % stylename + style = ss['Normal'] + story.append(Paragraph(text, style, bulletText=bulletText)) + elif typ == 'Preformatted': + (typ2, stylename, text) = thingy + try: + style = ss[stylename] + except KeyError: + print 'Preformatted style "%s" not found in stylesheet, using Normal instead' % stylename + style = ss['Normal'] + story.append(Preformatted(text, style, bulletText=bulletText)) + elif typ == 'Image': + filename = thingy[1] + img = Image(filename) + story.append(img) + elif typ == 'PageBreak': + story.append(PageBreak()) + elif typ == 'VSpace': + height = thingy[1] + story.append(Spacer(0, height)) + elif typ == 'NextPageTemplate': + story.append(NextPageTemplate(thingy[1])) + elif typ == 'Custom': + # go find it + searchPath = [os.getcwd()+'\\'] + (typ2, moduleName, funcName) = thingy + found = imp.find_module(moduleName, searchPath) + assert found, "Custom object module %s not found" % moduleName + (file, pathname, description) = found + mod = imp.load_module(moduleName, file, pathname, description) + + #now get the function + func = getattr(mod, funcName) + story.append(func()) + + else: + print 'skipping',typ, 'for now' + + + #print it + doc = RLDocTemplate(outfilename, pagesize=A4) + doc.build(story) + +if __name__ == '__main__': #NORUNTESTS + if len(sys.argv) == 2: + infilename = sys.argv[1] + outfilename = os.path.splitext(infilename)[0] + '.pdf' + if os.path.isfile(infilename): + run(infilename, outfilename) + else: + print 'File not found %s' % infilename + else: + print __doc__ \ No newline at end of file diff --git a/bin/reportlab/tools/py2pdf/README b/bin/reportlab/tools/py2pdf/README new file mode 100644 index 00000000000..9a3e9c8fb7e --- /dev/null +++ b/bin/reportlab/tools/py2pdf/README @@ -0,0 +1,8 @@ +py2pdf - converts Python source code to PDF +with a LOT of options. + +See the header of py2pdf.py for full details. +execute demo.py to see some output. + +Contributed by Dinu Gherman and copyrighted by him. +Uses Just van Rossum's PyFontify. diff --git a/bin/reportlab/tools/py2pdf/__init__.py b/bin/reportlab/tools/py2pdf/__init__.py new file mode 100644 index 00000000000..e0226c4a6fd --- /dev/null +++ b/bin/reportlab/tools/py2pdf/__init__.py @@ -0,0 +1,3 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/py2pdf/__init__.py diff --git a/bin/reportlab/tools/py2pdf/demo-config.txt b/bin/reportlab/tools/py2pdf/demo-config.txt new file mode 100644 index 00000000000..4c5ed1a314b --- /dev/null +++ b/bin/reportlab/tools/py2pdf/demo-config.txt @@ -0,0 +1,11 @@ +--bgCol=(1,.9,.9) +--lineNum +#--fontSize=12 +#--paperFormat=B5 +#--fontName=Helvetica +#--landscape +#--restCol=(0,1,0) +#--mode=mono +--kwCol=(1,0,1) +#--kwCol=#dd00ff +#--kwCol=(.5,.5,.5) diff --git a/bin/reportlab/tools/py2pdf/demo.py b/bin/reportlab/tools/py2pdf/demo.py new file mode 100644 index 00000000000..aa69f8cf042 --- /dev/null +++ b/bin/reportlab/tools/py2pdf/demo.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python + +"""demo.py - Demo script for py2pdf 0.5. + +The main idea is: take one Python file and make a whole +bunch of PDFs out of it for test purposes. + +Dinu Gherman +""" + + +import string, re, os, os.path, sys, shutil +from py2pdf import * + + +### Custom layouter class used with test3(). + +class ImgPDFLayouter (PythonPDFLayouter): + "A custom layouter displaying an image on each page." + + def setMainFrame(self, frame=None): + "Make a frame in the right half of the page." + + width, height = self.options.realPaperFormat.size + self.frame = height - 2*cm, 2*cm, 250, width-1*cm + + self.makeForm() + + + def makeForm(self): + "Use the experimental ReportLab form support." + + width, height = self.options.realPaperFormat.size + tm, bm, lm, rm = self.frame + c = self.canvas + + # Define a PDF form containing an image frame + # that will be included on every page, but + # stored only once in the resulting file. + c.beginForm("imageFrame") + c.saveState() + x, y = 219.0, 655.0 # Known size of the picture. + c.scale((lm - 1*cm)/x, height/y) + path = 'vertpython.jpg' + c.drawImage(path, 0, 0) + c.restoreState() + c.endForm() + + + def putPageDecoration(self): + "Draw the left border image and page number." + + width, height = self.options.realPaperFormat.size + tm, bm, lm, rm = self.frame + c = self.canvas + + # Footer. + x, y = lm + 0.5 * (rm - lm), 0.5 * bm + c.setFillColor(Color(0, 0, 0)) + c.setFont('Times-Italic', 12) + label = "Page %d" % self.pageNum + c.drawCentredString(x, y, label) + + # Call the previously stored form. + c.doForm("imageFrame") + + +### Helpers. + +def modifyPath(path, new, ext='.py'): + "Modifying the base name of a file." + + rest, ext = os.path.splitext(path) + path, base = os.path.split(rest) + format = "%s-%s%s" % (base, new, ext) + return os.path.join(path, format) + + +def getAllTestFunctions(): + "Return a list of all test functions available." + + globs = globals().keys() + tests = filter(lambda g: re.match('test[\d]+', g), globs) + tests.sort() + return map(lambda t: globals()[t], tests) + + +### Test functions. +### +### In order to be automatically found and applied to +### a Python file all test functions must follow the +### following naming pattern: 'test[0-9]+' and contain +### a doc string. + +def test0(path): + "Creating a PDF assuming an ASCII file." + + p = PDFPrinter() + p.process(path) + + +def test1(path): + "Creating a PDF using only default options." + + p = PythonPDFPrinter() + p.process(path) + + +def test2(path): + "Creating a PDF with some modified options." + + p = PythonPDFPrinter() + p.options.updateOption('landscape', 1) + p.options.updateOption('fontName', 'Helvetica') + p.options.updateOption('fontSize', '14') + p.options.display() + p.process(path) + + +def test3(path): + "Creating several PDFs as 'magazine listings'." + + p = PythonPDFPrinter() + p.Layouter = EmptyPythonPDFLayouter + p.options.updateOption('paperSize', '(250,400)') + p.options.updateOption('multiPage', 1) + p.options.updateOption('lineNum', 1) + p.process(path) + + +def test4(path): + "Creating a PDF in monochrome mode." + + p = PythonPDFPrinter() + p.options.updateOption('mode', 'mono') + p.process(path) + + +def test5(path): + "Creating a PDF with options from a config file." + + p = PythonPDFPrinter() + i = string.find(path, 'test5') + newPath = modifyPath(path[:i-1], 'config') + '.txt' + + try: + p.options.updateWithContentsOfFile(newPath) + p.options.display() + p.process(path) + except IOError: + print "Skipping test5() due to IOError." + + +def test6(path): + "Creating a PDF with modified layout." + + p = PythonPDFPrinter() + p.Layouter = ImgPDFLayouter + p.options.updateOption('fontName', 'Helvetica') + p.options.updateOption('fontSize', '12') + p.options.display() + p.process(path) + + +### Main. + +def main(inPath, *tests): + "Apply various tests to one Python source file." + + for t in tests: + newPath = modifyPath(inPath, t.__name__) + shutil.copyfile(inPath, newPath) + try: + print t.__doc__ + t(newPath) + finally: + os.remove(newPath) + print + + +if __name__=='__main__': + # Usage: "python demo.py [ ...]" + try: + try: + tests = map(lambda a: globals()[a], sys.argv[2:]) + except IndexError: + tests = getAllTestFunctions() + + fileName = sys.argv[1] + apply(main, [fileName]+tests) + + # Usage: "python demo.py" (implicitly does this: + # "python demo.py demo.py" ) + except IndexError: + print "Performing self-test..." + fileName = sys.argv[0] + tests = getAllTestFunctions() + apply(main, [fileName]+tests) diff --git a/bin/reportlab/tools/py2pdf/idle_print.py b/bin/reportlab/tools/py2pdf/idle_print.py new file mode 100644 index 00000000000..18b3080a259 --- /dev/null +++ b/bin/reportlab/tools/py2pdf/idle_print.py @@ -0,0 +1,56 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/py2pdf/idle_print.py + +# idle_print [py2pdf_options] filename +__version__=''' $Id: idle_print.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +# you should adjust the globals below to configure for your system + +import sys, os, py2pdf, string, time +#whether we remove input/output files; if you get trouble on windows try setting _out to 0 +auto_rm_in = 1 +auto_rm_out = 1 +viewOnly = 0 + +#how to call up your acrobat reader +if sys.platform=='win32': + acrord = 'C:\\Program Files\\Adobe\\Acrobat 4.0\\Reader\\AcroRd32.exe' + def printpdf(pdfname): + args = [acrord, pdfname] + os.spawnv(os.P_WAIT, args[0], args) +else: + acrord = 'acroread' + def printpdf(pdfname): + if viewOnly: + cmd = "%s %s" % (acrord,pdfname) + else: + cmd = "%s -toPostScript < %s | lpr" % (acrord,pdfname) + os.system(cmd) + +args = ['--input=python'] +files = [] +for f in sys.argv[1:]: + if f[:2]=='--': + opt = f[2:] + if opt =='no_auto_rm_in': + auto_rm_in = 0 + elif opt =='auto_rm_in': + auto_rm_in = 1 + elif opt =='no_auto_rm_out': + auto_rm_out = 0 + elif opt =='auto_rm_out': + auto_rm_out = 1 + elif opt =='viewonly': + viewOnly = 1 + elif opt[:9] =='acroread=': + acrord = opt[9:] + else: + args.append(f) + else: files.append(f) + +for f in files: + py2pdf.main(args+[f]) + if auto_rm_in: os.remove(f) + pdfname = os.path.splitext(f)[0]+'.pdf' + printpdf(pdfname) + if auto_rm_out: os.remove(pdfname) \ No newline at end of file diff --git a/bin/reportlab/tools/py2pdf/py2pdf.py b/bin/reportlab/tools/py2pdf/py2pdf.py new file mode 100644 index 00000000000..faaeacbc21f --- /dev/null +++ b/bin/reportlab/tools/py2pdf/py2pdf.py @@ -0,0 +1,1567 @@ +#!/usr/bin/env python + +""" Python Highlighter for PDF Version: 0.6 + + py2pdf.py [options] [ ...] + + options: + -h or --help print help (this message) + - read from stdin (writes to stdout) + --stdout read from file, write to stdout + (restricted to first file only) + --title= specify title + --config=<file> read configuration options from <file> + --input=<type> set input file type + 'python': Python code (default) + 'ascii': arbitrary ASCII files (b/w) + --mode=<mode> set output mode + 'color': output in color (default) + 'mono': output in b/w + --paperFormat= set paper format (ISO A, B, C series, + <format> US legal & letter; default: 'A4') + e.g. 'letter', 'A3', 'A4', 'B5', 'C6', ... + --paperSize= set paper size in points (size being a valid + <size> numeric 2-tuple (x,y) w/o any whitespace) + --landscape set landscape format (default: portrait) + --bgCol=<col> set page background-color in hex code like + '#FFA024' or '0xFFA024' for RGB components + (overwrites mono mode) + --<cat>Col=<col> set color of certain code categories, i.e. + <cat> can be the following: + 'comm': comments + 'ident': identifiers + 'kw': keywords + 'strng': strings + 'param': parameters + 'rest': all the rest + --fontName=<name> set base font name (default: 'Courier') + like 'Helvetica', 'Times-Roman' + --fontSize=<size> set font size (default: 8) + --tabSize=<size> set tab size (default: 4) + --lineNum print line numbers + --multiPage generate one file per page (with filenames + tagged by 1, 2...), disables PDF outline + --noOutline don't generate PDF outline (default: unset) + -v or --verbose set verbose mode + + Takes the input, assuming it is Python source code and formats + it into PDF. + + * Uses Just van Rossum's PyFontify version 0.4 to tag Python + scripts, now included in reportlab.lib. + + * Uses the ReportLab library (version 0.92 or higher) to + generate PDF. You can get it without charge from ReportLab: + http://www.reportlab.com + + * Parts of this code still borrow heavily from Marc-Andre + Lemburg's py2html who has kindly given permission to + include them in py2pdf. Thanks, M.-A.! +""" + +__copyright__ = """ +---------------------------------------------------------------------- +(c) Copyright by Dinu C. Gherman, 2000 (gherman@europemail.com) + + Permission to use, copy, modify, and distribute this + software and its documentation for any purpose and + without fee or royalty is hereby granted, provided + that the above copyright notice appear in all copies + and that both that copyright notice and this permission + notice appear in supporting documentation or portions + thereof, including modifications, that you make. + + THE AUTHOR DINU C. GHERMAN DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE, INCLUDING ALL IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO EVENT + SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, INDIRECT + OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER + RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER + IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE + OR PERFORMANCE OF THIS SOFTWARE! +""" + +__version__ = '0.6' +__author__ = 'Dinu C. Gherman' +__date__ = '2001-08-17' +__url__ = 'http://starship.python.net/crew/gherman/programs/py2pdf' + + +import sys, string, re, os, getopt + +from reportlab.pdfgen import canvas +from reportlab.lib.colors import Color, HexColor +from reportlab.lib import fonts +from reportlab.lib.units import cm, inch + + +### Helpers functions. + +def makeTuple(aString): + """Evaluate a string securely into a tuple. + + Match the string and return an evaluated object thereof if and + only if we think it is a tuple of two or more numeric decimal(!) + values, integers or floats. E-notation, hex or octal is not + supported, though! Shorthand notation (omitting leading or trail- + zeros, before or after the decimal point) like .25 or 25. is + supported. + """ + + c = string.count(aString, ',') + num = '(\d*?\.?\d+?)|(\d+?\.?\d*?)' + tup = string.join([num]*c, ',') + + if re.match('\(' + tup + '\)', aString): + return eval(aString) + else: + details = '%s cannot be parsed into a numeric tuple!' % aString + raise 'ValueError', details + + +def loadFontifier(options=None): + "Load a tagging module and return a corresponding function." + + # We can't use 'if options' because of the modified + # attribute lookup it seems. + + if type(options) != type(None) and options.marcs: + # Use mxTextTool's tagging engine. + + from mxTextTools import tag + from mxTextTools.Examples.Python import python_script + tagFunc = lambda text, tag=tag, pytable=python_script: \ + tag(text,pytable)[1] + + else: + # Load Just's. + + try: + from reportlab.lib import PyFontify + + if PyFontify.__version__ < '0.3': + raise ValueError + + tagFunc = PyFontify.fontify + + except: + print """ + Sorry, but this script needs the PyFontify.py module version 0.3 + or higher; You can download it from Just's homepage at + + URL: http://starship.python.net/crew/just +""" + sys.exit() + + return tagFunc + + +def makeColorFromString(aString): + """Convert a string to a ReportLab RGB color object. + + Supported formats are: '0xFFFFFF', '#FFFFFF' and '(R,G,B)' + where the latter is a tuple of three floats in the range + [0.0, 1.0]. + """ + + s = aString + + if s[0] == '#' or s[:2] in ('0x', '0X'): + if s[:2] in ('0x', '0X'): + return HexColor('#' + s[2:]) + + elif s[0] == '(': + r, g, b = makeTuple(aString) + return Color(r, g, b) + + +### Utility classes. + +# For py2pdf this is some kind of overkill, but it's fun, too. +class PaperFormat: + """Class to capture a paper format/size and its orientation. + + This class represents an abstract paper format, including + the size and orientation of a sheet of paper in some formats + plus a few operations to change its size and orientation. + """ + + _A4W, _A4H = 21*cm, 29.7*cm + A0 = (4*_A4W, 4*_A4H) + + _B4W, _B4H = 25*cm, 35.3*cm + B0 = (4*_B4W, 4*_B4H) + + _C4W, _C4H = 22.9*cm, 32.4*cm + C0 = (4*_C4W, 4*_C4H) + + letter = (8.5*inch, 11*inch) + legal = (8.5*inch, 14*inch) + + + def __init__(self, nameOrSize='A4', landscape=0): + "Initialisation." + + t = type(nameOrSize) + + if t == type(''): + self.setFormatName(nameOrSize, landscape) + elif t == type((1,)): + self.setSize(nameOrSize) + self.setLandscape(landscape) + + + def __repr__(self): + """Return a string representation of ourself. + + The returned string can also be used to recreate + the same PaperFormat object again. + """ + + if self.name != 'custom': + nos = `self.name` + else: + nos = `self.size` + + format = "PaperFormat(nameOrSize=%s, landscape=%d)" + tuple = (nos, self.landscape) + + return format % tuple + + + def setSize(self, size=None): + "Set explicit paper size." + + self.name = 'custom' + x, y = self.size = size + self.landscape = x < y + + + def setLandscape(self, flag): + "Set paper orientation as desired." + + # Swap paper orientation if needed. + + self.landscape = flag + x, y = self.size + + ls = self.landscape + if (ls and x < y) or (not ls and x > y): + self.size = y, x + + + def setFormatName(self, name='A4', landscape=0): + "Set paper size derived from a format name." + + if name[0] in 'ABC': + # Assume ISO-A, -B, -C series. + # (Hmm, are B and C really ISO standards + # or just DIN? Well...) + c, f = name[0], int(name[1:]) + self.size = getattr(self, c + '0') + self.name = c + '0' + self.makeHalfSize(f) + + elif name == 'letter': + self.size = self.letter + self.name = 'letter' + + elif name == 'legal': + self.size = self.legal + self.name = 'legal' + + self.setLandscape(landscape) + + + def makeHalfSize(self, times=1): + "Reduce paper size (surface) by 50% multiple times." + + # Orientation remains unchanged. + + # Iterates only for times >= 1. + for i in xrange(times): + s = self.size + self.size = s[1] / 2.0, s[0] + + if self.name[0] in 'ABC': + self.name = self.name[0] + `int(self.name[1:]) + 1` + else: + self.name = 'custom' + + + def makeDoubleSize(self, times=1): + "Increase paper size (surface) by 50% multiple times." + + # Orientation remains unchanged. + + # Iterates only for times >= 1. + for i in xrange(times): + s = self.size + self.size = s[1], s[0] * 2.0 + + if self.name[0] in 'ABC': + self.name = self.name[0] + `int(self.name[1:]) - 1` + else: + self.name = 'custom' + + +class Options: + """Container class for options from command line and config files. + + This class is a container for options as they are specified + when a program is called from a command line, but also from + the same kind of options that are saved in configuration + files. Both the short UNIX style (e.g. '-v') as well as the + extended GNU style (e.g. '--help') are supported. + + An 'option' is a <name>/<value> pair with both parts being + strings at the beginning, but where <value> might be conver- + ted later into any other Python object. + + Option values can be accessed by using their name as an attri- + bute of an Options object, returning None if no such option + name exists (that makes None values impossible, but so what?). + """ + + # Hmm, could also use UserDict... maybe later. + + def __init__(self): + "Initialize with some default options name/values." + + # Hmm, could also pass an initial optional dict... + + # Create a dictionary containing the options + # and populate it with defaults. + self.pool = {} + self.setDefaults() + + + def __getattr__(self, name): + "Turn attribute access into dictionary lookup." + + return self.pool.get(name) + + + def setDefaults(self): + "Set default options." + + ### Maybe get these from a site config file... + self.pool.update({'fontName' : 'Courier', + 'fontSize' : 8, + 'bgCol' : Color(1, 1, 1), + 'mode' : 'color', + 'lineNum' : 0, + 'tabSize' : 4, + 'paperFormat': 'A4', + 'landscape' : 0, + 'title' : None, + 'multiPage' : 0}) + + # Default colors (for color mode), mostly taken from py2html. + self.pool.update({'commCol' : HexColor('#1111CC'), + 'kwCol' : HexColor('#3333CC'), + 'identCol' : HexColor('#CC0000'), + 'paramCol' : HexColor('#000066'), + 'strngCol' : HexColor('#119911'), + 'restCol' : HexColor('#000000')}) + + # Add a default 'real' paper format object. + pf = PaperFormat(self.paperFormat, self.landscape) + self.pool.update({'realPaperFormat' : pf}) + self.pool.update({'files' : []}) + + + def display(self): + "Display all current option names and values." + + self.saveToFile(sys.stdout) + + + def saveToFile(self, path): + "Save options as a log file." + + if type(path) == type(''): + f = open(path, 'w') + else: + # Assume a file-like object. + f = path + + items = self.pool.items() + items.sort() + + for n, v in items: + f.write("%-15s : %s\n" % (n, `v`)) + + + def updateWithContentsOfFile(self, path): + """Update options as specified in a config file. + + The file is expected to contain one option name/value pair + (seperated by an equal sign) per line, but no other whitespace. + Option values may contain equal signs, but no whitespace. + Option names must be valid Python strings, preceeded by one + or two dashes. + """ + + config = open(path).read() + config = string.split(config, '\n') + + for cfg in config: + if cfg == None: + break + + if cfg == '' or cfg[0] == '#': + continue + + # GNU long options + if '=' in cfg and cfg[:2] == '--': + p = string.find(cfg, '=') + opt, arg = cfg[2:p], cfg[p+1:] + self.updateOption(opt, arg) + + # Maybe treat single-letter options as well? + # elif ':' in cfg and cfg[0] == '-' and cfg[1] != '-': + # pass + + else: + self.updateOption(cfg[2:], None) + + + def updateWithContentsOfArgv(self, argv): + "Update options as specified in a (command line) argument vector." + + # Specify accepted short option names (UNIX style). + shortOpts = 'hv' + + # Specify accepted long option names (GNU style). + lo = 'tabSize= paperFormat= paperSize= landscape stdout title= fontName= fontSize=' + lo = lo + ' bgCol= verbose lineNum marcs help multiPage noOutline config= input= mode=' + lo = lo + ' commCol= identCol= kwCol= strngCol= paramCol= restCol=' + longOpts = string.split(lo, ' ') + + try: + optList, args = getopt.getopt(argv, shortOpts, longOpts) + except getopt.error, msg: + sys.stderr.write("%s\nuse -h or --help for help\n" % str(msg)) + sys.exit(2) + + self.updateOption('files', args) # Hmm, really needed? + + for o, v in optList: + # Remove leading dashes (max. two). + if o[0] == '-': + o = o[1:] + if o[0] == '-': + o = o[1:] + + self.updateOption(o, v) + + + def updateOption(self, name, value): + "Update an option from a string value." + + # Special treatment for coloring options... + if name[-3:] == 'Col': + if name[:-3] in string.split('bg comm ident kw strng param rest', ' '): + self.pool[name] = makeColorFromString(value) + + elif name == 'paperSize': + tup = makeTuple(value) + self.pool['paperSize'] = tup + if not self.realPaperFormat: + pf = PaperFormat(self.paperFormat, self.landscape) + self.pool['realPaperFormat'] = pf + self.pool['realPaperFormat'].setSize(tup) + + elif name == 'paperFormat': + self.pool['paperFormat'] = value + if not self.realPaperFormat: + pf = PaperFormat(self.paperFormat, self.landscape) + self.pool['realPaperFormat'] = pf + self.pool['realPaperFormat'].setFormatName(self.paperFormat, self.landscape) + + elif name == 'landscape': + self.pool['landscape'] = 1 + if self.realPaperFormat: + self.pool['realPaperFormat'].setLandscape(1) + + elif name == 'fontSize': + self.pool['fontSize'] = int(value) + + elif name == 'tabSize': + self.pool['tabSize'] = int(value) + + elif name == 'mode': + self.pool['mode'] = value + if value == 'mono': + cats = 'comm ident kw strng param rest' + for cat in string.split(cats, ' '): + self.pool[cat + 'Col'] = Color(0, 0, 0) + + # Parse configuration file... + elif name == 'config': + self.updateWithContentsOfFile(value) + + elif name == 'stdout': + self.pool['stdout'] = 1 + + elif name == 'files': + self.pool['files'] = value + + else: + # Set the value found or 1 for options without values. + self.pool[name] = value or 1 + + + def update(self, **options): + "Update options." + + # Not much tested and/or used, yet!! + + for n, v in options.items(): + self.pool[n] = v + + +### Layouting classes. + +class PDFLayouter: + """A class to layout a simple PDF document. + + This is intended to help generate PDF documents where all pages + follow the same kind of 'template' which is supposed to be the + same adornments (header, footer, etc.) on each page plus a main + 'frame' on each page. These frames are 'connected' such that one + can add individual text lines one by one with automatic line + wrapping, page breaks and text flow between frames. + """ + + def __init__(self, options): + "Initialisation." + + self.options = options + self.canvas = None + self.multiLineStringStarted = 0 + self.lineNum = 0 + + # Set a default color and font. + o = self.options + self.currColor = o.restCol + self.currFont = (o.fontName, o.fontSize) + + + ### Helper methods. + + def setMainFrame(self, frame=None): + "Define the main drawing frame of interest for each page." + + if frame: + self.frame = frame + else: + # Maybe a long-term candidate for additional options... + width, height = self.options.realPaperFormat.size + self.frame = height - 3*cm, 3*cm, 2*cm, width - 2*cm + + # self.frame is a 4-tuple: + # (topMargin, bottomMargin, leftMargin, rightMargin) + + + def setPDFMetaInfo(self): + "Set PDF meta information." + + o = self.options + c = self.canvas + c.setAuthor('py2pdf %s' % __version__) + c.setSubject('') + + # Set filename. + filename = '' + + # For stdin use title option or empty... + if self.srcPath == sys.stdin: + if o.title: + filename = o.title + # otherwise take the input file's name. + else: + path = os.path.basename(self.srcPath) + filename = o.title or path + + c.setTitle(filename) + + + def setFillColorAndFont(self, color, font): + "Set new color/font (maintaining the current 'state')." + + self.currFont = font + self.currColor = color + + fontName, fontSize = font + self.text.setFont(fontName, fontSize) + self.text.setFillColor(color) + + + ### API + + def begin(self, srcPath, numLines): + "Things to do before doing anything else." + + self.lineNum = 0 + self.pageNum = 0 + self.numLines = numLines + self.srcPath = srcPath + + # Set output filename (stdout if desired). + o = self.options + if o.stdout: + self.pdfPath = sys.stdout + else: + if srcPath != sys.stdin: + self.pdfPath = os.path.splitext(srcPath)[0] + '.pdf' + else: + self.pdfPath = sys.stdout + + + def beginDocument(self): + """Things to do when a new document should be started. + + The initial page counter is 0, meaning that beginPage() + will be called by beginLine()... + """ + + # Set initial page number and store file name. + self.pageNum = 0 + o = self.options + + if not o.multiPage: + # Create canvas. + size = o.realPaperFormat.size + self.canvas = canvas.Canvas(self.pdfPath, size, verbosity=0) + c = self.canvas + c.setPageCompression(1) + c.setFont(o.fontName, o.fontSize) + + # Create document meta information. + self.setPDFMetaInfo() + + # Set drawing frame. + self.setMainFrame() + + # Determine the left text margin by adding the with + # of the line number to the left margin of the main frame. + format = "%%%dd " % len(`self.numLines`) + fn, fs = self.currFont + text = format % self.lineNum + tm, bm, lm, rm = self.frame + self.txm = lm + if o.lineNum: + self.txm = self.txm + c.stringWidth(text, fn, fs) + + + def beginPage(self): + "Things to do when a new page has to be added." + + o = self.options + self.pageNum = self.pageNum + 1 + + if not o.multiPage: + tm, bm, lm, rm = self.frame + self.text = self.canvas.beginText(lm, tm - o.fontSize) + self.setFillColorAndFont(self.currColor, self.currFont) + else: + # Fail if stdout desired (with multiPage). + if o.stdout: + raise "IOError", "Can't create multiple pages on stdout!" + + # Create canvas with a modified path name. + base, ext = os.path.splitext(self.pdfPath) + newPath = "%s-%d%s" % (base, self.pageNum, ext) + size = o.realPaperFormat.size + self.canvas = canvas.Canvas(newPath, size, verbosity=0) + c = self.canvas + c.setPageCompression(1) + c.setFont(o.fontName, o.fontSize) + + # Create document meta information. + self.setPDFMetaInfo() + + # Set drawing frame. + self.setMainFrame() + + tm, bm, lm, rm = self.frame + self.text = self.canvas.beginText(lm, tm - o.fontSize) + self.setFillColorAndFont(self.currColor, self.currFont) + + self.putPageDecoration() + + + def beginLine(self, wrapped=0): + "Things to do when a new line has to be added." + + # If there is no page yet, create the first one. + if self.pageNum == 0: + self.beginPage() + + # If bottom of current page reached, do a page break. + # (This works only with one text object being used + # for the entire page. Otherwise we need to maintain + # the vertical position of the current line ourself.) + y = self.text.getY() + tm, bm, lm, rm = self.frame + if y < bm: + self.endPage() + self.beginPage() + + # Print line number label, if needed. + o = self.options + if o.lineNum: + #self.putLineNumLabel() + font = ('Courier', o.fontSize) + + if not wrapped: + # Print a label containing the line number. + self.setFillColorAndFont(o.restCol, font) + format = "%%%dd " % len(`self.numLines`) + self.text.textOut(format % self.lineNum) + else: + # Print an empty label (using bgCol). Hackish! + currCol = self.currColor + currFont = self.currFont + self.setFillColorAndFont(o.bgCol, font) + self.text.textOut(' '*(len(`self.numLines`) + 1)) + self.setFillColorAndFont(currCol, currFont) + + + def endLine(self, wrapped=0): + "Things to do after a line is basically done." + + # End the current line by adding an 'end of line'. + # (Actually done by the text object...) + self.text.textLine('') + + if not wrapped: + self.lineNum = self.lineNum + 1 + + + def endPage(self): + "Things to do after a page is basically done." + + c = self.canvas + + # Draw the current text object (later we might want + # to do that after each line...). + c.drawText(self.text) + c.showPage() + + if self.options.multiPage: + c.save() + + + def endDocument(self): + "Things to do after the document is basically done." + + c = self.canvas + + # Display rest of last page and save it. + c.drawText(self.text) + c.showPage() + c.save() + + + def end(self): + "Things to do after everything has been done." + + pass + + + ### The real meat: methods writing something to a canvas. + + def putLineNumLabel(self, text, wrapped=0): + "Add a long text that can't be split into chunks." + + o = self.options + font = ('Courier', o.fontSize) + + if not wrapped: + # Print a label containing the line number. + self.setFillColorAndFont(o.restCol, font) + format = "%%%dd " % len(`self.numLines`) + self.text.textOut(format % self.lineNum) + else: + # Print an empty label (using bgCol). Hackish! + currCol = self.currColor + currFont = self.currFont + self.setFillColorAndFont(o.bgCol, font) + self.text.textOut(' '*(len(`self.numLines`) + 1)) + self.setFillColorAndFont(currCol, currFont) + + + # Tried this recursively before, in order to determine + # an appropriate string limit rapidly, but this wasted + # much space and showed very poor results... + # This linear method is very slow, but such lines should + # be very rare, too! + + def putSplitLongText(self, text): + "Add a long text that can't be split into chunks." + + # Now, the splitting will be with 'no mercy', + # at the right margin of the main drawing area. + + M = len(text) + t = self.text + x = t.getX() + o = self.options + tm, bm, lm, rm = self.frame + + width = self.canvas.stringWidth + fn, fs = self.currFont + tw = width(text, fn, fs) + + tx = self.text + if tw > rm - lm - x: + i = 1 + T = '' + while text: + T = text[:i] + tx = self.text # Can change after a page break. + tw = width(T, fn, fs) + + if x + tw > rm: + tx.textOut(T[:-1]) + self.endLine(wrapped=1) + self.beginLine(wrapped=1) + x = tx.getX() + text = text[i-1:] + M = len(text) + i = 0 + + i = i + 1 + + if i > M: + break + + tx.textOut(T) + + else: + t.textOut(text) + + + def putLongText(self, text): + "Add a long text by gracefully splitting it into chunks." + + # Splitting is currently done only at blanks, but other + # characters such as '.' or braces are also good + # possibilities... later... + + o = self.options + tm, bm, lm, rm = self.frame + + width = self.canvas.stringWidth + fn, fs = self.currFont + tw = width(text, fn, fs) + + arr = string.split(text, ' ') + + for i in range(len(arr)): + a = arr[i] + t = self.text # Can change during the loop... + tw = width(a, fn, fs) + x = t.getX() + + # If current item does not fit on current line, have it + # split and then put before/after a line (maybe also + # page) break. + if x + tw > rm: + self.putSplitLongText(a) + t = self.text # Can change after a page break... + + # If it fits, just add it to the current text object. + else: + t.textOut(a) + + # Add the character we used to split th original text. + if i < len(arr) - 1: + t.textOut(' ') + + + def putText(self, text): + "Add some text to the current line." + + t = self.text + x = t.getX() + o = self.options + fn, fs = o.fontName, o.fontSize + tw = self.canvas.stringWidth(text, fn, fs) + rm = self.frame[3] + + if x + tw < rm: + t.textOut(text) + else: + self.putLongText(text) + + + # Not yet tested. + def putLine(self, text): + "Add a line to the current text." + + self.putText(text) + self.endLine() + + + def putPageDecoration(self): + "Draw some decoration on each page." + + # Use some abbreviations. + o = self.options + c = self.canvas + tm, bm, lm, rm = self.frame + + # Restore default font. + c.setFont(o.fontName, o.fontSize) + + c.setLineWidth(0.5) # in pt. + + # Background color. + c.setFillColor(o.bgCol) + pf = o.realPaperFormat.size + c.rect(0, 0, pf[0], pf[1], stroke=0, fill=1) + + # Header. + c.setFillColorRGB(0, 0, 0) + c.line(lm, tm + .5*cm, rm, tm + .5*cm) + c.setFont('Times-Italic', 12) + + if self.pdfPath == sys.stdout: + filename = o.title or ' ' + else: + path = os.path.basename(self.srcPath) + filename = o.title or path + + c.drawString(lm, tm + 0.75*cm + 2, filename) + + # Footer. + c.line(lm, bm - .5*cm, rm, bm - .5*cm) + c.drawCentredString(0.5 * pf[0], 0.5*bm, "Page %d" % self.pageNum) + + # Box around main frame. + # c.rect(lm, bm, rm - lm, tm - bm) + + +class PythonPDFLayouter (PDFLayouter): + """A class to layout a simple multi-page PDF document. + """ + + ### API for adding specific Python entities. + + def addKw(self, t): + "Add a keyword." + + o = self.options + + # Make base font bold. + fam, b, i = fonts.ps2tt(o.fontName) + ps = fonts.tt2ps(fam, 1, i) + font = (ps, o.fontSize) + + self.setFillColorAndFont(o.kwCol, font) + + # Do bookmarking... + if not o.noOutline and not o.multiPage: + if t in ('class', 'def'): + tm, bm, lm, rm = self.frame + pos = self.text.getX() + + if pos == self.txm: + self.startPositions = [] + + self.startPos = pos + + if not hasattr(self, 'startPositions'): + self.startPositions = [] + + if pos not in self.startPositions: + self.startPositions.append(pos) + + # Memorize certain keywords. + self.itemFound = t + + else: + self.itemFound = None + self.startPos = None + + self.putText(t) + + + def addIdent(self, t): + "Add an identifier." + + o = self.options + + # Make base font bold. + fam, b, i = fonts.ps2tt(o.fontName) + ps = fonts.tt2ps(fam, 1, i) + font = (ps, o.fontSize) + + self.setFillColorAndFont(o.identCol, font) + self.putText(t) + + # Bookmark certain identifiers (class and function names). + if not o.noOutline and not o.multiPage: + item = self.itemFound + if item: + # Add line height to current vert. position. + pos = self.text.getY() + o.fontSize + + nameTag = "p%sy%s" % (self.pageNum, pos) + c = self.canvas + i = self.startPositions.index(self.startPos) + c.bookmarkHorizontalAbsolute(nameTag, pos) + c.addOutlineEntry('%s %s' % (item, t), nameTag, i) + + + def addParam(self, t): + "Add a parameter." + + o = self.options + font = (o.fontName, o.fontSize) + self.setFillColorAndFont(o.paramCol, font) + self.text.putText(t) + + + def addSimpleString(self, t): + "Add a simple string." + + o = self.options + font = (o.fontName, o.fontSize) + self.setFillColorAndFont(o.strngCol, font) + self.putText(t) + + + def addTripleStringBegin(self): + "Memorize begin of a multi-line string." + + # Memorise that we started a multi-line string. + self.multiLineStringStarted = 1 + + + def addTripleStringEnd(self, t): + "Add a multi-line string." + + self.putText(t) + + # Forget about the multi-line string again. + self.multiLineStringStarted = 0 + + + def addComm(self, t): + "Add a comment." + + o = self.options + + # Make base font slanted. + fam, b, i = fonts.ps2tt(o.fontName) + ps = fonts.tt2ps(fam, b, 1) + font = (ps, o.fontSize) + + self.setFillColorAndFont(o.commCol, font) + self.putText(t) + + + def addRest(self, line, eol): + "Add a regular thing." + + o = self.options + + # Nothing else to be done, print line as-is... + if line: + font = (o.fontName, o.fontSize) + self.setFillColorAndFont(o.restCol, font) + + # ... except if the multi-line-string flag is set, then we + # decide to change the current color to that of strings and + # just go on. + if self.multiLineStringStarted: + self.setFillColorAndFont(o.strngCol, font) + + self.putText(line) + + # Print an empty line. + else: + if eol != -1: + self.putText('') + + ### End of API. + + +class EmptyPythonPDFLayouter (PythonPDFLayouter): + """A PDF layout with no decoration and no margins. + + The main frame extends fully to all paper edges. This is + useful for creating PDFs when writing one page per file, + in order to provide pre-rendered, embellished Python + source code to magazine publishers, who can include the + individual files and only need to add their own captures. + """ + + def setMainFrame(self, frame=None): + "Make a frame extending to all paper edges." + + width, height = self.options.realPaperFormat.size + self.frame = height, 0, 0, width + + + def putPageDecoration(self): + "Draw no decoration at all." + + pass + + +### Pretty-printing classes. + +class PDFPrinter: + """Generic PDF Printer class. + + Does not do much, but write a PDF file created from + any ASCII input file. + """ + + outFileExt = '.pdf' + + + def __init__(self, options=None): + "Initialisation." + + self.data = None # Contains the input file. + self.inPath = None # Path of input file. + self.Layouter = PDFLayouter + + if type(options) != type(None): + self.options = options + else: + self.options = Options() + + + ### I/O. + + def readFile(self, path): + "Read the content of a file." + + if path == sys.stdin: + f = path + else: + f = open(path) + + self.inPath = path + + data = f.read() + o = self.options + self.data = re.sub('\t', ' '*o.tabSize, data) + f.close() + + + def formatLine(self, line, eol=0): + "Format one line of Python source code." + + font = ('Courier', 8) + self.layouter.setFillColorAndFont(Color(0, 0, 0), font) + self.layouter.putText(line) + + + def writeData(self, srcCodeLines, inPath, outPath=None): + "Convert Python source code lines into a PDF document." + + # Create a layouter object. + self.layouter = self.Layouter(self.options) + l = self.layouter + + # Loop over all tagged source lines, dissect them into + # Python entities ourself and let the layouter do the + # rendering. + splitCodeLines = string.split(srcCodeLines, '\n') + + ### Must also handle the case of outPath being sys.stdout!! + l.begin(inPath, len(splitCodeLines)) + l.beginDocument() + + for line in splitCodeLines: + l.beginLine() + self.formatLine(line) + l.endLine() + + l.endDocument() + l.end() + + + def writeFile(self, data, inPath=None, outPath=None): + "Write some data into a file." + + if inPath == sys.stdin: + self.outPath = sys.stdout + else: + if not outPath: + path = os.path.splitext(self.inPath)[0] + self.outPath = path + self.outFileExt + + self.writeData(data, inPath, outPath or self.outPath) + + + def process(self, inPath, outPath=None): + "The real 'action point' for working with Pretty-Printers." + + self.readFile(inPath) + self.writeFile(self.data, inPath, outPath) + + +class PythonPDFPrinter (PDFPrinter): + """A class to nicely format tagged Python source code. + + """ + + comm = 'COMMENT' + kw = 'KEYWORD' + strng = 'STRING' + ident = 'IDENT' + param = 'PARAMETER' + + outFileExt = '.pdf' + + + def __init__(self, options=None): + "Initialisation, calling self._didInit() at the end." + + if type(options) != type(None): + self.options = options + else: + self.options = Options() + + self._didInit() + + + def _didInit(self): + "Post-Initialising" + + # Define regular expression patterns. + s = self + comp = re.compile + + s.commPat = comp('(.*?)<' + s.comm + '>(.*?)</' + s.comm + '>(.*)') + s.kwPat = comp('(.*?)<' + s.kw + '>(.*?)</' + s.kw + '>(.*)') + s.identPat = comp('(.*?)<' + s.ident + '>(.*?)</' + s.ident + '>(.*)') + s.paramPat = comp('(.*?)<' + s.param + '>(.*?)</' + s.param + '>(.*)') + s.stPat = comp('(.*?)<' + s.strng + '>(.*?)</' + s.strng + '>(.*)') + s.strng1Pat = comp('(.*?)<' + s.strng + '>(.*)') + s.strng2Pat = comp('(.*?)</' + s.strng + '>(.*)') + s.allPat = comp('(.*)') + + cMatch = s.commPat.match + kMatch = s.kwPat.match + iMatch = s.identPat.match + pMatch = s.paramPat.match + sMatch = s.stPat.match + s1Match = s.strng1Pat.match + s2Match = s.strng2Pat.match + aMatch = s.allPat.match + + self.matchList = ((cMatch, 'Comm'), + (kMatch, 'Kw'), + (iMatch, 'Ident'), + (pMatch, 'Param'), + (sMatch, 'SimpleString'), + (s1Match, 'TripleStringBegin'), + (s2Match, 'TripleStringEnd'), + (aMatch, 'Rest')) + + self.Layouter = PythonPDFLayouter + + # Load fontifier. + self.tagFunc = loadFontifier(self.options) + + + ### + + def formatLine(self, line, eol=0): + "Format one line of Python source code." + + # Values for eol: -1:no-eol, 0:dunno-yet, 1:do-eol. + + for match, meth in self.matchList: + res = match(line) + + if res: + groups = res.groups() + method = getattr(self, '_format%s' % meth) + method(groups, eol) + break + + + def _formatIdent(self, groups, eol): + "Format a Python identifier." + + before, id, after = groups + self.formatLine(before, -1) + self.layouter.addIdent(id) + self.formatLine(after, eol) + + + def _formatParam(self, groups, eol): + "Format a Python parameter." + + before, param, after = groups + self.formatLine(before, -1) + self.layouter.addParam(before) + self.formatLine(after, eol) + + + def _formatSimpleString(self, groups, eol): + "Format a Python one-line string." + + before, s, after = groups + self.formatLine(before, -1) + self.layouter.addSimpleString(s) + self.formatLine(after, eol) + + + def _formatTripleStringBegin(self, groups, eol): + "Format a Python multi-line line string (1)." + + before, after = groups + self.formatLine(before, -1) + self.layouter.addTripleStringBegin() + self.formatLine(after, 1) + + + def _formatTripleStringEnd(self, groups, eol): + "Format a Python multi-line line string (2)." + + before, after = groups + self.layouter.addTripleStringEnd(before) + self.formatLine(after, 1) + + + def _formatKw(self, groups, eol): + "Format a Python keyword." + + before, kw, after = groups + self.formatLine(before, -1) + self.layouter.addKw(kw) + self.formatLine(after, eol) + + + def _formatComm(self, groups, eol): + "Format a Python comment." + + before, comment, after = groups + self.formatLine(before, -1) + self.layouter.addComm(comment) + self.formatLine(after, 1) + + + def _formatRest(self, groups, eol): + "Format a piece of a Python line w/o anything special." + + line = groups[0] + self.layouter.addRest(line, eol) + + + ### + + def writeData(self, srcCodeLines, inPath, outPath=None): + "Convert Python source code lines into a PDF document." + + # Create a layouter object. + self.layouter = self.Layouter(self.options) + l = self.layouter + + # Loop over all tagged source lines, dissect them into + # Python entities ourself and let the layouter do the + # rendering. + splitCodeLines = string.split(srcCodeLines, '\n') + l.begin(inPath, len(splitCodeLines)) + l.beginDocument() + + for line in splitCodeLines: + l.beginLine() + self.formatLine(line) + l.endLine() + + l.endDocument() + l.end() + + + def process(self, inPath, outPath=None): + "The real 'action point' for working with Pretty-Printers." + + self.readFile(inPath) + self.taggedData = self._fontify(self.data) + self.writeFile(self.taggedData, inPath, outPath) + + + ### Fontifying. + + def _fontify(self, pytext): + "" + + formats = { + 'rest' : ('', ''), + 'comment' : ('<%s>' % self.comm, '</%s>' % self.comm), + 'keyword' : ('<%s>' % self.kw, '</%s>' % self.kw), + 'parameter' : ('<%s>' % self.param, '</%s>' % self.param), + 'identifier' : ('<%s>' % self.ident, '</%s>' % self.ident), + 'string' : ('<%s>' % self.strng, '</%s>' % self.strng) } + + # Parse. + taglist = self.tagFunc(pytext) + + # Prepend special 'rest' tag. + taglist[:0] = [('rest', 0, len(pytext), None)] + + # Prepare splitting. + splits = [] + self._addSplits(splits, pytext, formats, taglist) + + # Do splitting & inserting. + splits.sort() + l = [] + li = 0 + + for ri, dummy, insert in splits: + if ri > li: + l.append(pytext[li:ri]) + + l.append(insert) + li = ri + + if li < len(pytext): + l.append(pytext[li:]) + + return string.join(l, '') + + + def _addSplits(self, splits, text, formats, taglist): + "" + + # Helper for fontify(). + for id, left, right, sublist in taglist: + + try: + pre, post = formats[id] + except KeyError: + # msg = 'Warning: no format ' + # msg = msg + 'for %s specified\n'%repr(id) + # sys.stderr.write(msg) + pre, post = '', '' + + if type(pre) != type(''): + pre = pre(text[left:right]) + + if type(post) != type(''): + post = post(text[left:right]) + + # len(splits) is a dummy used to make sorting stable. + splits.append((left, len(splits), pre)) + + if sublist: + self._addSplits(splits, text, formats, sublist) + + splits.append((right, len(splits), post)) + + +### Main + +def main(cmdline): + "Process command line as if it were sys.argv" + + # Create default options and initialize with argv + # from the command line. + options = Options() + options.updateWithContentsOfArgv(cmdline[1:]) + + # Print help message if desired, then exit. + if options.h or options.help: + print __doc__ + sys.exit() + + # Apply modest consistency checks and exit if needed. + cmdStr = string.join(cmdline, ' ') + find = string.find + if find(cmdStr, 'paperSize') >= 0 and find(cmdStr, 'paperFormat') >= 0: + details = "You can specify either paperSize or paperFormat, " + details = detail + "but not both!" + raise 'ValueError', details + + # Create PDF converter and pass options to it. + if options.input: + input = string.lower(options.input) + + if input == 'python': + P = PythonPDFPrinter + elif input == 'ascii': + P = PDFPrinter + else: + details = "Input file type must be 'python' or 'ascii'." + raise 'ValueError', details + + else: + P = PythonPDFPrinter + + p = P(options) + + # Display options if needed. + if options.v or options.verbose: + pass # p.options.display() + + # Start working. + verbose = options.v or options.verbose + + if options.stdout: + if len(options.files) > 1 and verbose: + print "Warning: will only convert first file on command line." + f = options.files[0] + p.process(f, sys.stdout) + else: + if verbose: + print 'py2pdf: working on:' + + for f in options.files: + try: + if verbose: + print ' %s' % f + if f != '-': + p.process(f) + else: + p.process(sys.stdin, sys.stdout) + except IOError: + if verbose: + print '(IOError!)', + + if verbose: + print + print 'Done.' + + +def process(*args, **kwargs): + "Provides a way of using py2pdf from within other Python scripts." + + noValOpts = 'h v verbose landscape stdout lineNum marcs help multiPage noOutline' + noValOpts = string.split(noValOpts, ' ') + + s = 'py2pdf.py' + for k, v in kwargs.items(): + if len(k) == 1: + s = s + ' -%s' % k + if k not in noValOpts: + s = s + ' %s' % v + elif len(k) > 1: + s = s + ' --%s' % k + if k not in noValOpts: + s = s + '=%s' % v + s = s + ' ' + string.join(args, ' ') + s = string.split(s, ' ') + + main(s) + + +### + +if __name__=='__main__': #NORUNTESTS + main(sys.argv) \ No newline at end of file diff --git a/bin/reportlab/tools/py2pdf/vertpython.jpg b/bin/reportlab/tools/py2pdf/vertpython.jpg new file mode 100644 index 00000000000..8655230bf43 Binary files /dev/null and b/bin/reportlab/tools/py2pdf/vertpython.jpg differ diff --git a/bin/reportlab/tools/pythonpoint/README b/bin/reportlab/tools/pythonpoint/README new file mode 100644 index 00000000000..f6d8808bdab --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/README @@ -0,0 +1,29 @@ +PythonPoint is a utility for generating PDF slides from +a simple XML format - "PythonPoint Markup Language". + +This is early days. It lets you produce quite sophisticated +output, but the DTD will undoubtedly evolve and change. +However, I want people to be able to use it this summer so am +releasing now. + +It is part of the ReportLab distribution; if you have managed +to run the ReportLab tests, and have installed the Python +Imaging Library, usage should be straightforward. PIL is not +required to make slides, but the demo will have a few blanks if +you omit it. + +To use, cd to the pythonpoint directory and execute: + pythonpoint.py pythonpoint.xml +This will create pythonpoint.pdf, which is your manual. + +You can also try 'monterey.xml', which is a talk I gave +at the O'Reilly Open Source conference in Monterey +in summer 1999. + +We have issues to resolve over module load paths; +the easiest solution for now is to put the PythonPoint +directory on your path; work in a new directory; +and explicitly include the paths to any custom shapes, +and style sheets you use. + +- Andy Robinson, 6 April 2000 \ No newline at end of file diff --git a/bin/reportlab/tools/pythonpoint/__init__.py b/bin/reportlab/tools/pythonpoint/__init__.py new file mode 100644 index 00000000000..c4b994f848b --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/__init__.py @@ -0,0 +1,3 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/pythonpoint/__init__.py diff --git a/bin/reportlab/tools/pythonpoint/customshapes.py b/bin/reportlab/tools/pythonpoint/customshapes.py new file mode 100644 index 00000000000..779a236eec6 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/customshapes.py @@ -0,0 +1,298 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/pythonpoint/customshapes.py +__version__=''' $Id: customshapes.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' + +# xml parser stuff for PythonPoint +# PythonPoint Markup Language! + +__doc__=""" +This demonstrates a custom shape for use with the <customshape> tag. +The shape must fulfil a very simple interface, which may change in +future. + +The XML tag currently has this form: + <customshape + module="customshapes.py" + class = "MyShape" + initargs="(100,200,3)" + /> + +PythonPoint will look in the given module for the given class, +evaluate the arguments string and pass it to the constructor. +Then, it will call + + object.drawOn(canvas) + +Thus your object must be fully defined by the constructor. +For this one, we pass three argumenyts: x, y and scale. +This does a five-tile jigsaw over which words can be overlaid; +based on work done for a customer's presentation. +""" + + +import reportlab.pdfgen.canvas +from reportlab.lib import colors +from reportlab.lib.corp import RL_CorpLogo +from reportlab.graphics.shapes import Drawing + +## custom shape for use with PythonPoint. + +class Jigsaw: + """This draws a jigsaw patterm. By default it is centred on 0,0 + and has dimensions of 200 x 140; use the x/y/scale attributes + to move it around.""" + #Using my usual bulldozer coding style - I am sure a mathematician could + #derive an elegant way to draw this, but I just took a ruler, guessed at + #the control points, and reflected a few lists at the interactive prompt. + + def __init__(self, x, y, scale=1): + self.width = 200 + self.height = 140 + self.x = x + self.y = y + self.scale = scale + + + def drawOn(self, canvas): + canvas.saveState() + + canvas.setFont('Helvetica-Bold',24) + canvas.drawString(600, 100, 'A Custom Shape') + + canvas.translate(self.x, self.y) + canvas.scale(self.scale, self.scale) + self.drawBounds(canvas) + + self.drawCentre(canvas) + self.drawTopLeft(canvas) + self.drawBottomLeft(canvas) + self.drawBottomRight(canvas) + self.drawTopRight(canvas) + + canvas.restoreState() + + + def curveThrough(self, path, pointlist): + """Helper to curve through set of control points.""" + assert len(pointlist) % 3 == 1, "No. of points must be 3n+1 for integer n" + (x,y) = pointlist[0] + path.moveTo(x, y) + idx = 1 + while idx < len(pointlist)-2: + p1, p2, p3 = pointlist[idx:idx+3] + path.curveTo(p1[0], p1[1], p2[0], p2[1], p3[0], p3[1]) + idx = idx + 3 + + + def drawShape(self, canvas, controls, color): + """Utlity to draw a closed shape through a list of control points; + extends the previous proc""" + canvas.setFillColor(color) + p = canvas.beginPath() + self.curveThrough(p, controls) + p.close() + canvas.drawPath(p, stroke=1, fill=1) + + + def drawBounds(self, canvas): + """Guidelines to help me draw - not needed in production""" + canvas.setStrokeColor(colors.red) + canvas.rect(-100,-70,200,140) + canvas.line(-100,0,100,0) + canvas.line(0,70,0,-70) + canvas.setStrokeColor(colors.black) + + + def drawCentre(self, canvas): + controls = [ (0,50), #top + + #top right edge - duplicated for that corner piece + (5,50),(10,45),(10,40), + (10,35),(15,30),(20,30), + (25,30),(30,25),(30,20), + (30,15),(35,10),(40,10), + (45,10),(50,5),(50,0), + + #bottom right edge + (50, -5), (45,-10), (40,-10), + (35,-10), (30,-15), (30, -20), + (30,-25), (25,-30), (20,-30), + (15,-30), (10,-35), (10,-40), + (10,-45),(5,-50),(0,-50), + + #bottom left + (-5,-50),(-10,-45),(-10,-40), + (-10,-35),(-15,-30),(-20,-30), + (-25,-30),(-30,-25),(-30,-20), + (-30,-15),(-35,-10),(-40,-10), + (-45,-10),(-50,-5),(-50,0), + + #top left + (-50,5),(-45,10),(-40,10), + (-35,10),(-30,15),(-30,20), + (-30,25),(-25,30),(-20,30), + (-15,30),(-10,35),(-10,40), + (-10,45),(-5,50),(0,50) + + ] + + self.drawShape(canvas, controls, colors.yellow) + + + def drawTopLeft(self, canvas): + controls = [(-100,70), + (-100,69),(-100,1),(-100,0), + (-99,0),(-91,0),(-90,0), + + #jigsaw interlock - 4 sections + (-90,5),(-92,5),(-92,10), + (-92,15), (-85,15), (-80,15), + (-75,15),(-68,15),(-68,10), + (-68,5),(-70,5),(-70,0), + (-69,0),(-51,0),(-50,0), + + #five distinct curves + (-50,5),(-45,10),(-40,10), + (-35,10),(-30,15),(-30,20), + (-30,25),(-25,30),(-20,30), + (-15,30),(-10,35),(-10,40), + (-10,45),(-5,50),(0,50), + + (0,51),(0,69),(0,70), + (-1,70),(-99,70),(-100,70) + ] + self.drawShape(canvas, controls, colors.teal) + + + def drawBottomLeft(self, canvas): + + controls = [(-100,-70), + (-99,-70),(-1,-70),(0,-70), + (0,-69),(0,-51),(0,-50), + + #wavyline + (-5,-50),(-10,-45),(-10,-40), + (-10,-35),(-15,-30),(-20,-30), + (-25,-30),(-30,-25),(-30,-20), + (-30,-15),(-35,-10),(-40,-10), + (-45,-10),(-50,-5),(-50,0), + + #jigsaw interlock - 4 sections + + (-51, 0), (-69, 0), (-70, 0), + (-70, 5), (-68, 5), (-68, 10), + (-68, 15), (-75, 15), (-80, 15), + (-85, 15), (-92, 15), (-92, 10), + (-92, 5), (-90, 5), (-90, 0), + + (-91,0),(-99,0),(-100,0) + + ] + self.drawShape(canvas, controls, colors.green) + + + def drawBottomRight(self, canvas): + + controls = [ (100,-70), + (100,-69),(100,-1),(100,0), + (99,0),(91,0),(90,0), + + #jigsaw interlock - 4 sections + (90, -5), (92, -5), (92, -10), + (92, -15), (85, -15), (80, -15), + (75, -15), (68, -15), (68, -10), + (68, -5), (70, -5), (70, 0), + (69, 0), (51, 0), (50, 0), + + #wavyline + (50, -5), (45,-10), (40,-10), + (35,-10), (30,-15), (30, -20), + (30,-25), (25,-30), (20,-30), + (15,-30), (10,-35), (10,-40), + (10,-45),(5,-50),(0,-50), + + (0,-51), (0,-69), (0,-70), + (1,-70),(99,-70),(100,-70) + + ] + self.drawShape(canvas, controls, colors.navy) + + + def drawBottomLeft(self, canvas): + + controls = [(-100,-70), + (-99,-70),(-1,-70),(0,-70), + (0,-69),(0,-51),(0,-50), + + #wavyline + (-5,-50),(-10,-45),(-10,-40), + (-10,-35),(-15,-30),(-20,-30), + (-25,-30),(-30,-25),(-30,-20), + (-30,-15),(-35,-10),(-40,-10), + (-45,-10),(-50,-5),(-50,0), + + #jigsaw interlock - 4 sections + + (-51, 0), (-69, 0), (-70, 0), + (-70, 5), (-68, 5), (-68, 10), + (-68, 15), (-75, 15), (-80, 15), + (-85, 15), (-92, 15), (-92, 10), + (-92, 5), (-90, 5), (-90, 0), + + (-91,0),(-99,0),(-100,0) + + ] + self.drawShape(canvas, controls, colors.green) + + + def drawTopRight(self, canvas): + controls = [(100, 70), + (99, 70), (1, 70), (0, 70), + (0, 69), (0, 51), (0, 50), + (5, 50), (10, 45), (10, 40), + (10, 35), (15, 30), (20, 30), + (25, 30), (30, 25), (30, 20), + (30, 15), (35, 10), (40, 10), + (45, 10), (50, 5), (50, 0), + (51, 0), (69, 0), (70, 0), + (70, -5), (68, -5), (68, -10), + (68, -15), (75, -15), (80, -15), + (85, -15), (92, -15), (92, -10), + (92, -5), (90, -5), (90, 0), + (91, 0), (99, 0), (100, 0) + ] + + self.drawShape(canvas, controls, colors.magenta) + + +class Logo: + """This draws a ReportLab Logo.""" + + def __init__(self, x, y, width, height): + logo = RL_CorpLogo() + logo.x = x + logo.y = y + logo.width = width + logo.height = height + self.logo = logo + + def drawOn(self, canvas): + logo = self.logo + x, y = logo.x, logo.y + w, h = logo.width, logo.height + D = Drawing(w, h) + D.add(logo) + D.drawOn(canvas, 0, 0) + + +def run(): + c = reportlab.pdfgen.canvas.Canvas('customshape.pdf') + + J = Jigsaw(300, 540, 2) + J.drawOn(c) + c.save() + + +if __name__ == '__main__': + run() \ No newline at end of file diff --git a/bin/reportlab/tools/pythonpoint/demos/LeERC___.AFM b/bin/reportlab/tools/pythonpoint/demos/LeERC___.AFM new file mode 100644 index 00000000000..fe491e473a2 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/LeERC___.AFM @@ -0,0 +1,93 @@ +StartFontMetrics 2.0 +Comment Generated by RoboFog 08-06-2001 4:26:40 PM +FontName LettErrorRobot-Chrome +FullName LettErrorRobot-Chrome +FamilyName LettErrorRobot +Weight Medium +Notice (C) 1998-2001 LettError, Just van Rossum, Erik van Blokland, http://www.letterror.com/ +ItalicAngle 0 +IsFixedPitch false +UnderlinePosition -133 +UnderlineThickness 20 +Version 001.000 +EncodingScheme AdobeStandardEncoding +FontBBox -53 -252 1047 752 +CapHeight 647 +XHeight 548 +Descender -252 +Ascender 747 +StartCharMetrics 68 +C 32 ; WX 300 ; N space ; B 0 0 0 0 ; +C 33 ; WX 300 ; N exclam ; B 48 -52 253 652 ; +C 38 ; WX 700 ; N ampersand ; B 48 -47 653 647 ; +C 42 ; WX 700 ; N asterisk ; B 48 148 653 752 ; +C 48 ; WX 700 ; N zero ; B 53 -47 648 548 ; +C 49 ; WX 500 ; N one ; B 48 -47 453 553 ; +C 50 ; WX 600 ; N two ; B 48 -47 553 548 ; +C 51 ; WX 600 ; N three ; B 48 -147 553 548 ; +C 52 ; WX 700 ; N four ; B 48 -152 653 553 ; +C 53 ; WX 600 ; N five ; B 48 -147 553 548 ; +C 54 ; WX 600 ; N six ; B 53 -47 553 647 ; +C 55 ; WX 600 ; N seven ; B 48 -152 548 548 ; +C 56 ; WX 600 ; N eight ; B 48 -47 553 647 ; +C 57 ; WX 600 ; N nine ; B 48 -147 548 548 ; +C 63 ; WX 500 ; N question ; B 48 -52 448 647 ; +C 64 ; WX 800 ; N at ; B 53 -47 748 647 ; +C 65 ; WX 700 ; N A ; B 53 -52 648 652 ; +C 66 ; WX 600 ; N B ; B 53 -47 553 647 ; +C 67 ; WX 600 ; N C ; B 53 -47 553 647 ; +C 68 ; WX 700 ; N D ; B 53 -47 648 647 ; +C 69 ; WX 600 ; N E ; B 53 -47 553 647 ; +C 70 ; WX 600 ; N F ; B 53 -52 553 647 ; +C 71 ; WX 700 ; N G ; B 53 -47 653 647 ; +C 72 ; WX 700 ; N H ; B 53 -52 648 652 ; +C 73 ; WX 300 ; N I ; B 53 -52 248 652 ; +C 74 ; WX 300 ; N J ; B -53 -252 248 652 ; +C 75 ; WX 700 ; N K ; B 53 -52 653 652 ; +C 76 ; WX 600 ; N L ; B 53 -47 553 652 ; +C 77 ; WX 900 ; N M ; B 53 -52 848 652 ; +C 78 ; WX 700 ; N N ; B 53 -52 648 652 ; +C 79 ; WX 700 ; N O ; B 53 -47 648 647 ; +C 80 ; WX 600 ; N P ; B 53 -52 548 647 ; +C 81 ; WX 700 ; N Q ; B 53 -252 653 647 ; +C 82 ; WX 600 ; N R ; B 53 -52 653 647 ; +C 83 ; WX 600 ; N S ; B 48 -47 553 647 ; +C 84 ; WX 700 ; N T ; B 48 -52 653 647 ; +C 85 ; WX 700 ; N U ; B 53 -47 648 652 ; +C 86 ; WX 700 ; N V ; B 53 -52 648 652 ; +C 87 ; WX 1100 ; N W ; B 53 -52 1047 652 ; +C 88 ; WX 700 ; N X ; B 48 -52 653 652 ; +C 89 ; WX 700 ; N Y ; B 53 -52 648 652 ; +C 90 ; WX 700 ; N Z ; B 48 -47 653 647 ; +C 97 ; WX 600 ; N a ; B 48 -47 548 548 ; +C 98 ; WX 600 ; N b ; B 53 -47 548 752 ; +C 99 ; WX 600 ; N c ; B 53 -47 553 548 ; +C 100 ; WX 600 ; N d ; B 53 -47 548 752 ; +C 101 ; WX 600 ; N e ; B 53 -47 553 548 ; +C 102 ; WX 400 ; N f ; B -53 -52 453 747 ; +C 103 ; WX 600 ; N g ; B 48 -247 548 548 ; +C 104 ; WX 600 ; N h ; B 53 -52 548 752 ; +C 105 ; WX 300 ; N i ; B 48 -52 253 752 ; +C 106 ; WX 300 ; N j ; B -53 -252 253 752 ; +C 107 ; WX 600 ; N k ; B 53 -52 553 752 ; +C 108 ; WX 300 ; N l ; B 53 -52 248 752 ; +C 109 ; WX 900 ; N m ; B 53 -52 848 548 ; +C 110 ; WX 600 ; N n ; B 53 -52 548 548 ; +C 111 ; WX 600 ; N o ; B 53 -47 548 548 ; +C 112 ; WX 600 ; N p ; B 53 -252 548 548 ; +C 113 ; WX 600 ; N q ; B 53 -252 548 548 ; +C 114 ; WX 400 ; N r ; B 53 -52 453 553 ; +C 115 ; WX 600 ; N s ; B 48 -47 553 548 ; +C 116 ; WX 400 ; N t ; B -53 -47 453 652 ; +C 117 ; WX 600 ; N u ; B 53 -47 548 553 ; +C 118 ; WX 600 ; N v ; B 53 -52 548 553 ; +C 119 ; WX 900 ; N w ; B 53 -52 848 553 ; +C 120 ; WX 700 ; N x ; B 48 -52 653 553 ; +C 121 ; WX 600 ; N y ; B 48 -252 548 553 ; +C 122 ; WX 500 ; N z ; B -53 -47 553 548 ; +EndCharMetrics +StartKernData +StartKernPairs 0 +EndKernPairs +EndKernData +EndFontMetrics diff --git a/bin/reportlab/tools/pythonpoint/demos/LeERC___.PFB b/bin/reportlab/tools/pythonpoint/demos/LeERC___.PFB new file mode 100644 index 00000000000..c1cf7ec189a Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/LeERC___.PFB differ diff --git a/bin/reportlab/tools/pythonpoint/demos/examples.py b/bin/reportlab/tools/pythonpoint/demos/examples.py new file mode 100644 index 00000000000..14aee2336fb --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/examples.py @@ -0,0 +1,841 @@ +import string + +testannotations=""" +def annotations(canvas): + from reportlab.lib.units import inch + canvas.drawString(inch, 2.5*inch, + "setAuthor, setTitle, setSubject have no visible effect") + canvas.drawString(inch, inch, "But if you are viewing this document dynamically") + canvas.drawString(inch, 0.5*inch, "please look at File/Document Info") + canvas.setAuthor("the ReportLab Team") + canvas.setTitle("ReportLab PDF Generation User Guide") + canvas.setSubject("How to Generate PDF files using the ReportLab modules") +""" + +# magic function making module + +test1 = """ +def f(a,b): + print "it worked", a, b + return a+b +""" + +test2 = """ +def g(n): + if n==0: return 1 + else: return n*g(n-1) + """ + +testhello = """ +def hello(c): + from reportlab.lib.units import inch + # move the origin up and to the left + c.translate(inch,inch) + # define a large font + c.setFont("Helvetica", 14) + # choose some colors + c.setStrokeColorRGB(0.2,0.5,0.3) + c.setFillColorRGB(1,0,1) + # draw some lines + c.line(0,0,0,1.7*inch) + c.line(0,0,1*inch,0) + # draw a rectangle + c.rect(0.2*inch,0.2*inch,1*inch,1.5*inch, fill=1) + # make text go straight up + c.rotate(90) + # change color + c.setFillColorRGB(0,0,0.77) + # say hello (note after rotate the y coord needs to be negative!) + c.drawString(0.3*inch, -inch, "Hello World") +""" + +testcoords = """ +def coords(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import pink, black, red, blue, green + c = canvas + c.setStrokeColor(pink) + c.grid([inch, 2*inch, 3*inch, 4*inch], [0.5*inch, inch, 1.5*inch, 2*inch, 2.5*inch]) + c.setStrokeColor(black) + c.setFont("Times-Roman", 20) + c.drawString(0,0, "(0,0) the Origin") + c.drawString(2.5*inch, inch, "(2.5,1) in inches") + c.drawString(4*inch, 2.5*inch, "(4, 2.5)") + c.setFillColor(red) + c.rect(0,2*inch,0.2*inch,0.3*inch, fill=1) + c.setFillColor(green) + c.circle(4.5*inch, 0.4*inch, 0.2*inch, fill=1) +""" + +testtranslate = """ +def translate(canvas): + from reportlab.lib.units import cm + canvas.translate(2.3*cm, 0.3*cm) + coords(canvas) + """ + +testscale = """ +def scale(canvas): + canvas.scale(0.75, 0.5) + coords(canvas) +""" + +testscaletranslate = """ +def scaletranslate(canvas): + from reportlab.lib.units import inch + canvas.setFont("Courier-BoldOblique", 12) + # save the state + canvas.saveState() + # scale then translate + canvas.scale(0.3, 0.5) + canvas.translate(2.4*inch, 1.5*inch) + canvas.drawString(0, 2.7*inch, "Scale then translate") + coords(canvas) + # forget the scale and translate... + canvas.restoreState() + # translate then scale + canvas.translate(2.4*inch, 1.5*inch) + canvas.scale(0.3, 0.5) + canvas.drawString(0, 2.7*inch, "Translate then scale") + coords(canvas) +""" + +testmirror = """ +def mirror(canvas): + from reportlab.lib.units import inch + canvas.translate(5.5*inch, 0) + canvas.scale(-1.0, 1.0) + coords(canvas) +""" + +testcolors = """ +def colors(canvas): + from reportlab.lib import colors + from reportlab.lib.units import inch + black = colors.black + y = x = 0; dy=inch*3/4.0; dx=inch*5.5/5; w=h=dy/2; rdx=(dx-w)/2 + rdy=h/5.0; texty=h+2*rdy + canvas.setFont("Helvetica",10) + for [namedcolor, name] in ( + [colors.lavenderblush, "lavenderblush"], + [colors.lawngreen, "lawngreen"], + [colors.lemonchiffon, "lemonchiffon"], + [colors.lightblue, "lightblue"], + [colors.lightcoral, "lightcoral"]): + canvas.setFillColor(namedcolor) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, name) + x = x+dx + y = y + dy; x = 0 + for rgb in [(1,0,0), (0,1,0), (0,0,1), (0.5,0.3,0.1), (0.4,0.5,0.3)]: + r,g,b = rgb + canvas.setFillColorRGB(r,g,b) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, "r%s g%s b%s"%rgb) + x = x+dx + y = y + dy; x = 0 + for cmyk in [(1,0,0,0), (0,1,0,0), (0,0,1,0), (0,0,0,1), (0,0,0,0)]: + c,m,y1,k = cmyk + canvas.setFillColorCMYK(c,m,y1,k) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, "c%s m%s y%s k%s"%cmyk) + x = x+dx + y = y + dy; x = 0 + for gray in (0.0, 0.25, 0.50, 0.75, 1.0): + canvas.setFillGray(gray) + canvas.rect(x+rdx, y+rdy, w, h, fill=1) + canvas.setFillColor(black) + canvas.drawCentredString(x+dx/2, y+texty, "gray: %s"%gray) + x = x+dx +""" + +testspumoni = """ +def spumoni(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import pink, green, brown, white + x = 0; dx = 0.4*inch + for i in range(4): + for color in (pink, green, brown): + canvas.setFillColor(color) + canvas.rect(x,0,dx,3*inch,stroke=0,fill=1) + x = x+dx + canvas.setFillColor(white) + canvas.setStrokeColor(white) + canvas.setFont("Helvetica-Bold", 85) + canvas.drawCentredString(2.75*inch, 1.3*inch, "SPUMONI") +""" + +testspumoni2 = """ +def spumoni2(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import pink, green, brown, white, black + # draw the previous drawing + spumoni(canvas) + # now put an ice cream cone on top of it: + # first draw a triangle (ice cream cone) + p = canvas.beginPath() + xcenter = 2.75*inch + radius = 0.45*inch + p.moveTo(xcenter-radius, 1.5*inch) + p.lineTo(xcenter+radius, 1.5*inch) + p.lineTo(xcenter, 0) + canvas.setFillColor(brown) + canvas.setStrokeColor(black) + canvas.drawPath(p, fill=1) + # draw some circles (scoops) + y = 1.5*inch + for color in (pink, green, brown): + canvas.setFillColor(color) + canvas.circle(xcenter, y, radius, fill=1) + y = y+radius +""" + +testbezier = """ +def bezier(canvas): + from reportlab.lib.colors import yellow, green, red, black + from reportlab.lib.units import inch + i = inch + d = i/4 + # define the bezier curve control points + x1,y1, x2,y2, x3,y3, x4,y4 = d,1.5*i, 1.5*i,d, 3*i,d, 5.5*i-d,3*i-d + # draw a figure enclosing the control points + canvas.setFillColor(yellow) + p = canvas.beginPath() + p.moveTo(x1,y1) + for (x,y) in [(x2,y2), (x3,y3), (x4,y4)]: + p.lineTo(x,y) + canvas.drawPath(p, fill=1, stroke=0) + # draw the tangent lines + canvas.setLineWidth(inch*0.1) + canvas.setStrokeColor(green) + canvas.line(x1,y1,x2,y2) + canvas.setStrokeColor(red) + canvas.line(x3,y3,x4,y4) + # finally draw the curve + canvas.setStrokeColor(black) + canvas.bezier(x1,y1, x2,y2, x3,y3, x4,y4) +""" + +testbezier2 = """ +def bezier2(canvas): + from reportlab.lib.colors import yellow, green, red, black + from reportlab.lib.units import inch + # make a sequence of control points + xd,yd = 5.5*inch/2, 3*inch/2 + xc,yc = xd,yd + dxdy = [(0,0.33), (0.33,0.33), (0.75,1), (0.875,0.875), + (0.875,0.875), (1,0.75), (0.33,0.33), (0.33,0)] + pointlist = [] + for xoffset in (1,-1): + yoffset = xoffset + for (dx,dy) in dxdy: + px = xc + xd*xoffset*dx + py = yc + yd*yoffset*dy + pointlist.append((px,py)) + yoffset = -xoffset + for (dy,dx) in dxdy: + px = xc + xd*xoffset*dx + py = yc + yd*yoffset*dy + pointlist.append((px,py)) + # draw tangent lines and curves + canvas.setLineWidth(inch*0.1) + while pointlist: + [(x1,y1),(x2,y2),(x3,y3),(x4,y4)] = pointlist[:4] + del pointlist[:4] + canvas.setLineWidth(inch*0.1) + canvas.setStrokeColor(green) + canvas.line(x1,y1,x2,y2) + canvas.setStrokeColor(red) + canvas.line(x3,y3,x4,y4) + # finally draw the curve + canvas.setStrokeColor(black) + canvas.bezier(x1,y1, x2,y2, x3,y3, x4,y4) +""" + +testpencil = """ +def pencil(canvas, text="No.2"): + from reportlab.lib.colors import yellow, red, black,white + from reportlab.lib.units import inch + u = inch/10.0 + canvas.setStrokeColor(black) + canvas.setLineWidth(4) + # draw erasor + canvas.setFillColor(red) + canvas.circle(30*u, 5*u, 5*u, stroke=1, fill=1) + # draw all else but the tip (mainly rectangles with different fills) + canvas.setFillColor(yellow) + canvas.rect(10*u,0,20*u,10*u, stroke=1, fill=1) + canvas.setFillColor(black) + canvas.rect(23*u,0,8*u,10*u,fill=1) + canvas.roundRect(14*u, 3.5*u, 8*u, 3*u, 1.5*u, stroke=1, fill=1) + canvas.setFillColor(white) + canvas.rect(25*u,u,1.2*u,8*u, fill=1,stroke=0) + canvas.rect(27.5*u,u,1.2*u,8*u, fill=1, stroke=0) + canvas.setFont("Times-Roman", 3*u) + canvas.drawCentredString(18*u, 4*u, text) + # now draw the tip + penciltip(canvas,debug=0) + # draw broken lines across the body. + canvas.setDash([10,5,16,10],0) + canvas.line(11*u,2.5*u,22*u,2.5*u) + canvas.line(22*u,7.5*u,12*u,7.5*u) + """ + +testpenciltip = """ +def penciltip(canvas, debug=1): + from reportlab.lib.colors import tan, black, green + from reportlab.lib.units import inch + u = inch/10.0 + canvas.setLineWidth(4) + if debug: + canvas.scale(2.8,2.8) # make it big + canvas.setLineWidth(1) # small lines + canvas.setStrokeColor(black) + canvas.setFillColor(tan) + p = canvas.beginPath() + p.moveTo(10*u,0) + p.lineTo(0,5*u) + p.lineTo(10*u,10*u) + p.curveTo(11.5*u,10*u, 11.5*u,7.5*u, 10*u,7.5*u) + p.curveTo(12*u,7.5*u, 11*u,2.5*u, 9.7*u,2.5*u) + p.curveTo(10.5*u,2.5*u, 11*u,0, 10*u,0) + canvas.drawPath(p, stroke=1, fill=1) + canvas.setFillColor(black) + p = canvas.beginPath() + p.moveTo(0,5*u) + p.lineTo(4*u,3*u) + p.lineTo(5*u,4.5*u) + p.lineTo(3*u,6.5*u) + canvas.drawPath(p, stroke=1, fill=1) + if debug: + canvas.setStrokeColor(green) # put in a frame of reference + canvas.grid([0,5*u,10*u,15*u], [0,5*u,10*u]) +""" + +testnoteannotation = """ +from reportlab.platypus.flowables import Flowable +class NoteAnnotation(Flowable): + '''put a pencil in the margin.''' + def wrap(self, *args): + return (1,10) # I take up very little space! (?) + def draw(self): + canvas = self.canv + canvas.translate(-10,-10) + canvas.rotate(180) + canvas.scale(0.2,0.2) + pencil(canvas, text="NOTE") +""" + +testhandannotation = """ +from reportlab.platypus.flowables import Flowable +from reportlab.lib.colors import tan, green +class HandAnnotation(Flowable): + '''A hand flowable.''' + def __init__(self, xoffset=0, size=None, fillcolor=tan, strokecolor=green): + from reportlab.lib.units import inch + if size is None: size=4*inch + self.fillcolor, self.strokecolor = fillcolor, strokecolor + self.xoffset = xoffset + self.size = size + # normal size is 4 inches + self.scale = size/(4.0*inch) + def wrap(self, *args): + return (self.xoffset, self.size) + def draw(self): + canvas = self.canv + canvas.setLineWidth(6) + canvas.setFillColor(self.fillcolor) + canvas.setStrokeColor(self.strokecolor) + canvas.translate(self.xoffset+self.size,0) + canvas.rotate(90) + canvas.scale(self.scale, self.scale) + hand(canvas, debug=0, fill=1) +""" + +lyrics = '''\ +well she hit Net Solutions +and she registered her own .com site now +and filled it up with yahoo profile pics +she snarfed in one night now +and she made 50 million when Hugh Hefner +bought up the rights now +and she'll have fun fun fun +til her Daddy takes the keyboard away''' + +lyrics = string.split(lyrics, "\n") +testtextsize = """ +def textsize(canvas): + from reportlab.lib.units import inch + from reportlab.lib.colors import magenta, red + canvas.setFont("Times-Roman", 20) + canvas.setFillColor(red) + canvas.drawCentredString(2.75*inch, 2.5*inch, "Font size examples") + canvas.setFillColor(magenta) + size = 7 + y = 2.3*inch + x = 1.3*inch + for line in lyrics: + canvas.setFont("Helvetica", size) + canvas.drawRightString(x,y,"%s points: " % size) + canvas.drawString(x,y, line) + y = y-size*1.2 + size = size+1.5 +""" + +teststar = """ +def star(canvas, title="Title Here", aka="Comment here.", + xcenter=None, ycenter=None, nvertices=5): + from math import pi + from reportlab.lib.units import inch + radius=inch/3.0 + if xcenter is None: xcenter=2.75*inch + if ycenter is None: ycenter=1.5*inch + canvas.drawCentredString(xcenter, ycenter+1.3*radius, title) + canvas.drawCentredString(xcenter, ycenter-1.4*radius, aka) + p = canvas.beginPath() + p.moveTo(xcenter,ycenter+radius) + from math import pi, cos, sin + angle = (2*pi)*2/5.0 + startangle = pi/2.0 + for vertex in range(nvertices-1): + nextangle = angle*(vertex+1)+startangle + x = xcenter + radius*cos(nextangle) + y = ycenter + radius*sin(nextangle) + p.lineTo(x,y) + if nvertices==5: + p.close() + canvas.drawPath(p) +""" + +testjoins = """ +def joins(canvas): + from reportlab.lib.units import inch + # make lines big + canvas.setLineWidth(5) + star(canvas, "Default: mitered join", "0: pointed", xcenter = 1*inch) + canvas.setLineJoin(1) + star(canvas, "Round join", "1: rounded") + canvas.setLineJoin(2) + star(canvas, "Bevelled join", "2: square", xcenter=4.5*inch) +""" + +testcaps = """ +def caps(canvas): + from reportlab.lib.units import inch + # make lines big + canvas.setLineWidth(5) + star(canvas, "Default", "no projection",xcenter = 1*inch, + nvertices=4) + canvas.setLineCap(1) + star(canvas, "Round cap", "1: ends in half circle", nvertices=4) + canvas.setLineCap(2) + star(canvas, "Square cap", "2: projects out half a width", xcenter=4.5*inch, + nvertices=4) +""" + +testdashes = """ +def dashes(canvas): + from reportlab.lib.units import inch + # make lines big + canvas.setDash(6,3) + star(canvas, "Simple dashes", "6 points on, 3 off", xcenter = 1*inch) + canvas.setDash(1,2) + star(canvas, "Dots", "One on, two off") + canvas.setDash([1,1,3,3,1,4,4,1], 0) + star(canvas, "Complex Pattern", "[1,1,3,3,1,4,4,1]", xcenter=4.5*inch) +""" + +testcursormoves1 = """ +def cursormoves1(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(inch, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 14) + for line in lyrics: + textobject.textLine(line) + textobject.setFillGray(0.4) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testcursormoves2 = """ +def cursormoves2(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(2, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 14) + for line in lyrics: + textobject.textOut(line) + textobject.moveCursor(14,14) # POSITIVE Y moves down!!! + textobject.setFillColorRGB(0.4,0,1) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testcharspace = """ +def charspace(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 10) + charspace = 0 + for line in lyrics: + textobject.setCharSpace(charspace) + textobject.textLine("%s: %s" %(charspace,line)) + charspace = charspace+0.5 + textobject.setFillGray(0.4) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testwordspace = """ +def wordspace(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 12) + wordspace = 0 + for line in lyrics: + textobject.setWordSpace(wordspace) + textobject.textLine("%s: %s" %(wordspace,line)) + wordspace = wordspace+2.5 + textobject.setFillColorCMYK(0.4,0,0.4,0.2) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" +testhorizontalscale = """ +def horizontalscale(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 12) + horizontalscale = 80 # 100 is default + for line in lyrics: + textobject.setHorizScale(horizontalscale) + textobject.textLine("%s: %s" %(horizontalscale,line)) + horizontalscale = horizontalscale+10 + textobject.setFillColorCMYK(0.0,0.4,0.4,0.2) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" +testleading = """ +def leading(canvas): + from reportlab.lib.units import inch + textobject = canvas.beginText() + textobject.setTextOrigin(3, 2.5*inch) + textobject.setFont("Helvetica-Oblique", 14) + leading = 8 + for line in lyrics: + textobject.setLeading(leading) + textobject.textLine("%s: %s" %(leading,line)) + leading = leading+2.5 + textobject.setFillColorCMYK(0.8,0,0,0.3) + textobject.textLines(''' + With many apologies to the Beach Boys + and anyone else who finds this objectionable + ''') + canvas.drawText(textobject) +""" + +testhand = """ +def hand(canvas, debug=1, fill=0): + (startx, starty) = (0,0) + curves = [ + ( 0, 2), ( 0, 4), ( 0, 8), # back of hand + ( 5, 8), ( 7,10), ( 7,14), + (10,14), (10,13), ( 7.5, 8), # thumb + (13, 8), (14, 8), (17, 8), + (19, 8), (19, 6), (17, 6), + (15, 6), (13, 6), (11, 6), # index, pointing + (12, 6), (13, 6), (14, 6), + (16, 6), (16, 4), (14, 4), + (13, 4), (12, 4), (11, 4), # middle + (11.5, 4), (12, 4), (13, 4), + (15, 4), (15, 2), (13, 2), + (12.5, 2), (11.5, 2), (11, 2), # ring + (11.5, 2), (12, 2), (12.5, 2), + (14, 2), (14, 0), (12.5, 0), + (10, 0), (8, 0), (6, 0), # pinky, then close + ] + from reportlab.lib.units import inch + if debug: canvas.setLineWidth(6) + u = inch*0.2 + p = canvas.beginPath() + p.moveTo(startx, starty) + ccopy = list(curves) + while ccopy: + [(x1,y1), (x2,y2), (x3,y3)] = ccopy[:3] + del ccopy[:3] + p.curveTo(x1*u,y1*u,x2*u,y2*u,x3*u,y3*u) + p.close() + canvas.drawPath(p, fill=fill) + if debug: + from reportlab.lib.colors import red, green + (lastx, lasty) = (startx, starty) + ccopy = list(curves) + while ccopy: + [(x1,y1), (x2,y2), (x3,y3)] = ccopy[:3] + del ccopy[:3] + canvas.setStrokeColor(red) + canvas.line(lastx*u,lasty*u, x1*u,y1*u) + canvas.setStrokeColor(green) + canvas.line(x2*u,y2*u, x3*u,y3*u) + (lastx,lasty) = (x3,y3) +""" + +testhand2 = """ +def hand2(canvas): + canvas.translate(20,10) + canvas.setLineWidth(3) + canvas.setFillColorRGB(0.1, 0.3, 0.9) + canvas.setStrokeGray(0.5) + hand(canvas, debug=0, fill=1) +""" + +testfonts = """ +def fonts(canvas): + from reportlab.lib.units import inch + text = "Now is the time for all good men to..." + x = 1.8*inch + y = 2.7*inch + for font in canvas.getAvailableFonts(): + canvas.setFont(font, 10) + canvas.drawString(x,y,text) + canvas.setFont("Helvetica", 10) + canvas.drawRightString(x-10,y, font+":") + y = y-13 +""" + +testarcs = """ +def arcs(canvas): + from reportlab.lib.units import inch + canvas.setLineWidth(4) + canvas.setStrokeColorRGB(0.8, 1, 0.6) + # draw rectangles enclosing the arcs + canvas.rect(inch, inch, 1.5*inch, inch) + canvas.rect(3*inch, inch, inch, 1.5*inch) + canvas.setStrokeColorRGB(0, 0.2, 0.4) + canvas.setFillColorRGB(1, 0.6, 0.8) + p = canvas.beginPath() + p.moveTo(0.2*inch, 0.2*inch) + p.arcTo(inch, inch, 2.5*inch,2*inch, startAng=-30, extent=135) + p.arc(3*inch, inch, 4*inch, 2.5*inch, startAng=-45, extent=270) + canvas.drawPath(p, fill=1, stroke=1) +""" +testvariousshapes = """ +def variousshapes(canvas): + from reportlab.lib.units import inch + inch = int(inch) + canvas.setStrokeGray(0.5) + canvas.grid(range(0,11*inch/2,inch/2), range(0,7*inch/2,inch/2)) + canvas.setLineWidth(4) + canvas.setStrokeColorRGB(0, 0.2, 0.7) + canvas.setFillColorRGB(1, 0.6, 0.8) + p = canvas.beginPath() + p.rect(0.5*inch, 0.5*inch, 0.5*inch, 2*inch) + p.circle(2.75*inch, 1.5*inch, 0.3*inch) + p.ellipse(3.5*inch, 0.5*inch, 1.2*inch, 2*inch) + canvas.drawPath(p, fill=1, stroke=1) +""" + +testclosingfigures = """ +def closingfigures(canvas): + from reportlab.lib.units import inch + h = inch/3.0; k = inch/2.0 + canvas.setStrokeColorRGB(0.2,0.3,0.5) + canvas.setFillColorRGB(0.8,0.6,0.2) + canvas.setLineWidth(4) + p = canvas.beginPath() + for i in (1,2,3,4): + for j in (1,2): + xc,yc = inch*i, inch*j + p.moveTo(xc,yc) + p.arcTo(xc-h, yc-k, xc+h, yc+k, startAng=0, extent=60*i) + # close only the first one, not the second one + if j==1: + p.close() + canvas.drawPath(p, fill=1, stroke=1) +""" + +testforms = """ +def forms(canvas): + #first create a form... + canvas.beginForm("SpumoniForm") + #re-use some drawing functions from earlier + spumoni(canvas) + canvas.endForm() + + #then draw it + canvas.doForm("SpumoniForm") +""" + +def doctemplateillustration(canvas): + from reportlab.lib.units import inch + canvas.setFont("Helvetica", 10) + canvas.drawString(inch/4.0, 2.75*inch, "DocTemplate") + W = 4/3.0*inch + H = 2*inch + Wd = x = inch/4.0 + Hd =y = inch/2.0 + for name in ("two column", "chapter page", "title page"): + canvas.setFillColorRGB(0.5,1.0,1.0) + canvas.rect(x,y,W,H, fill=1) + canvas.setFillColorRGB(0,0,0) + canvas.drawString(x+inch/8, y+H-Wd, "PageTemplate") + canvas.drawCentredString(x+W/2.0, y-Wd, name) + x = x+W+Wd + canvas.saveState() + d = inch/16 + dW = (W-3*d)/2.0 + hD = H -2*d-Wd + canvas.translate(Wd+d, Hd+d) + for name in ("left Frame", "right Frame"): + canvas.setFillColorRGB(1.0,0.5,1.0) + canvas.rect(0,0, dW,hD, fill=1) + canvas.setFillGray(0.7) + dd= d/2.0 + ddH = (hD-6*dd)/5.0 + ddW = dW-2*dd + yy = dd + xx = dd + for i in range(5): + canvas.rect(xx,yy,ddW,ddH, fill=1, stroke=0) + yy = yy+ddH+dd + canvas.setFillColorRGB(0,0,0) + canvas.saveState() + canvas.rotate(90) + canvas.drawString(d,-dW/2, name) + canvas.restoreState() + canvas.translate(dW+d,0) + canvas.restoreState() + canvas.setFillColorRGB(1.0, 0.5, 1.0) + mx = Wd+W+Wd+d + my = Hd+d + mW = W-2*d + mH = H-d-Hd + canvas.rect(mx, my, mW, mH, fill=1) + canvas.rect(Wd+2*(W+Wd)+d, Hd+3*d, W-2*d, H/2.0, fill=1) + canvas.setFillGray(0.7) + canvas.rect(Wd+2*(W+Wd)+d+dd, Hd+5*d, W-2*d-2*dd, H/2.0-2*d-dd, fill=1) + xx = mx+dd + yy = my+mH/5.0 + ddH = (mH-6*dd-mH/5.0)/3.0 + ddW = mW - 2*dd + for i in range(3): + canvas.setFillGray(0.7) + canvas.rect(xx,yy,ddW,ddH, fill=1, stroke=1) + canvas.setFillGray(0) + canvas.drawString(xx+dd/2.0,yy+dd/2.0, "flowable %s" %(157-i)) + yy = yy+ddH+dd + canvas.drawCentredString(3*Wd+2*W+W/2, Hd+H/2.0, "First Flowable") + canvas.setFont("Times-BoldItalic", 8) + canvas.setFillGray(0) + canvas.drawCentredString(mx+mW/2.0, my+mH+3*dd, "Chapter 6: Lubricants") + canvas.setFont("Times-BoldItalic", 10) + canvas.drawCentredString(3*Wd+2*W+W/2, Hd+H-H/4, "College Life") + +class PlatIllust: + #wrap the above for PP# + def __init__(self, x, y, scale=1): + self.x = x + self.y = y + self.scale = scale + def drawOn(self, canvas): + canvas.saveState() + canvas.translate(self.x, self.y) + canvas.scale(self.scale, self.scale) + doctemplateillustration(canvas) + canvas.restoreState() + +class PingoIllust: + #wrap the above for PP# + def __init__(self, x, y, scale=1): +## print 'Pingo illustration %f, %f, %f' % (x,y,scale) + self.x = x + self.y = y + self.scale = scale + def drawOn(self, canvas): + canvas.rect(self.x, self.y, 100,100, stroke=1, fill=1) +## from pingo import testdrawings +## from pingo import pingopdf +## drawing = testdrawings.getDrawing3() +## canvas.saveState() +## canvas.scale(self.scale, self.scale) +## pingopdf.draw(drawing, canvas, self.x, self.y) +## canvas.restoreState() + +# D = dir() +g = globals() +Dprime = {} +from types import StringType +from string import strip +for (a,b) in g.items(): + if a[:4]=="test" and type(b) is StringType: + #print 'for', a + #print b + b = strip(b) + exec(b+'\n') + +platypussetup = """ +from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer +from reportlab.lib.styles import getSampleStyleSheet +from reportlab.lib.pagesizes import DEFAULT_PAGE_SIZE +from reportlab.lib.units import inch +PAGE_HEIGHT=DEFAULT_PAGE_SIZE[1]; PAGE_WIDTH=DEFAULT_PAGE_SIZE[0] +styles = getSampleStyleSheet() +""" +platypusfirstpage = """ +Title = "Hello world" +pageinfo = "platypus example" +def myFirstPage(canvas, doc): + canvas.saveState() + canvas.setFont('Times-Bold',16) + canvas.drawCentredString(PAGE_WIDTH/2.0, PAGE_HEIGHT-108, Title) + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "First Page / %s" % pageinfo) + canvas.restoreState() +""" +platypusnextpage = """ +def myLaterPages(canvas, doc): + canvas.saveState() + canvas.setFont('Times-Roman',9) + canvas.drawString(inch, 0.75 * inch, "Page %d %s" % (doc.page, pageinfo)) + canvas.restoreState() +""" +platypusgo = """ +def go(): + doc = SimpleDocTemplate("phello.pdf") + Story = [Spacer(1,2*inch)] + style = styles["Normal"] + for i in range(100): + bogustext = ("This is Paragraph number %s. " % i) *20 + p = Paragraph(bogustext, style) + Story.append(p) + Story.append(Spacer(1,0.2*inch)) + doc.build(Story, onFirstPage=myFirstPage, onLaterPages=myLaterPages) +""" + +if __name__=="__main__": + # then do the platypus hello world + for b in platypussetup, platypusfirstpage, platypusnextpage, platypusgo: + b = strip(b) + exec(b+'\n') + go() diff --git a/bin/reportlab/tools/pythonpoint/demos/figures.xml b/bin/reportlab/tools/pythonpoint/demos/figures.xml new file mode 100644 index 00000000000..b3314ed8298 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/figures.xml @@ -0,0 +1,71 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- edited with XMLSPY v2004 rel. 3 U (http://www.xmlspy.com) by Andy Robinson (ReportLab Europe Ltd.) --> +<!DOCTYPE presentation SYSTEM "../pythonpoint.dtd"> +<presentation filename="figures.xml" pageDuration="10"> + <stylesheet module="standard" function="getParagraphStyles"/> + <title>New Feature Tests + + Andy Robinson + + + Reportlab Sample Applications + +
              + + + +
              + diff --git a/bin/reportlab/tools/pythonpoint/demos/htu.xml b/bin/reportlab/tools/pythonpoint/demos/htu.xml new file mode 100644 index 00000000000..56cb92457dc --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/htu.xml @@ -0,0 +1,96 @@ + + + + +
              + + + © 2002, H. Turgut UYAR + + + + + + New features in PythonPoint + H. Turgut Uyar + uyar@cs.itu.edu.tr + + + + + TrueType Support + + + PythonPoint can read UTF-8 encoded input files and produce + correct output (provided your font has all necessary + characters). That enables you, for example, to have native (in my + case, Turkish) characters in your document: + + +  0 ^  1 _ + + + + + + Effects + + + Paragraphs, images, tables and geometric shapes can now have + effectname, effectdirection, effectdimension, effectmotion and + effectduration attributes: + A paragraph + + + Col1,Col2,Col3 + Row1Col1,Row1Col2,Row1Col3 + Row2Col1,Row2Col2,Row2Col3 +
              + + + String + +
              + + + Printing + + + Be careful when using effects: A new slide is created for + each effect, so DON'T print the resulting PDF file. + new command-line option: --printout + produces printable PDF + + + + + New Paragraph Styles + + + Bullet2 - Second level bullets + Here's an example + Or an example with a longer text to see + how it wraps at the end of each line + Author and EMail + See the cover page for examples + They have to be in the style file, so either use + the htu style file or edit your style file + + + + + ToDo + + + Unicode chars in the outline + + +
              +
              diff --git a/bin/reportlab/tools/pythonpoint/demos/leftlogo.a85 b/bin/reportlab/tools/pythonpoint/demos/leftlogo.a85 new file mode 100644 index 00000000000..7d55afcf348 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/leftlogo.a85 @@ -0,0 +1,53 @@ +BI +/W 103 /H 68 /BPC 8 /CS /RGB /F [/A85 /Fl] +ID +Gb"/*cYjIe']/l*^;)UQj:Ec+nd!,-,*G/5;0"E.+@qg.\92h2HbiE)jC5*M +$a,@:RoXB@WPjMu=[F;88l[?@OO=6qV,HNNiL#(cac+!&rF)2^DHJ?_:WQkn +1[fb`hOBr--B>FoSDXMt35Gnj6n\3XT8sf/Y%-NPLqg'pmQ+*EiV6D:,$akf +!i2MA/A-"n[R.R)S!ePi$u('8mrCG8[3:WLlB[amBYAS4hR2*f7tR;&\%]Ld +ZX5^f6Qj@\h7^FuQ(">[ml32FWhJG;Z)t2oADV4[H6GbW.mH`MDlesQZO5+Q5)]OWf2G9[ +U9W99*2'#PD+cIXnp"J"(BrY^=(rg;ITf("c#0"S<;fjK,APV+p9p+05LO&X +jQNf`8c26f]J54N'k#I9=Bt4]e&6gN'#S[nK3*VhQ!`PpXcJ0i:J_2rC@=>X\5dUbEcCZ?\6O)+F/35ZJD6R$H^-Z!R +5;8=l//9SUkC;LhQ*?di<'6e&/BNIqm(%(SDYXJ +p%%Z+J.IshkX$tLPOD'"jSV#/6iY?QI>8P-%o0k3`e#X33RJS`f'D!D2rTJ- +(4Znb03&koo>X[bKs.qpBOkU,KCCO3S$2.Gr@e8E=$On&hUiAB2[.;G@FQf7_K1&D6RJ.Ih@-56Ss&B1q:\)MgJ8P4Sa-(Fm +J"d8Z]Mf`Q$G^\tK@pQWQs3iRB[KP=+H&@+iH@XGFQ'#n!DJ$)Mc?"U#od!oYl3En7nT"7UC,,Q&Hr +9.SPlHak6s)J0;k;hQC:k82'=COeskp&dB-#@RrY1B=H^'L=,;9uJssq%XGf +h/fXmq>1G\?k6ZH*GSk&M)?&TJ?rYW.9f!gdM2&:Kr51A3^MX?GV'T@$h)Da +-RIIcpl8fhiQr=`\k[7;3`X"d!or(jb1YM>"Ppi`,67@]M"d ++mXd;WRE"IKX@n&dNNV/55(]`==+_r?K5bp?$^4Pn/[2ncL(/l#CfSYAg:iN +@3j5R$qtm;AIF7k%^ak'%4Z1354d"1VT\g]cm[^hI.Yf>DcLS\rq+As2F?LI +'gq+/_o9&kK,4@]qbdfA8FauP;!N'$5J@1P/\Th=9i^\b$/-6-kL#$E&39t% +/O[1Fm`/F?!q;%bB')DM`KY&D!,6)lG5TQ88ejHiTWKt;3_8:@M!NnP'D2s$ +2Q25"dX)7pQ%VUq+r1%.iMQiLR#&.@\:Pe96FEbkgLu`Fb;!#d,DPC-H/d?u +^']=P86?6qr/@_/T-T0?,+>brI`?=13["Y(3DYSMX7UAUk0'!l!aPN4$e\f&(i2TTrbVuOYhc05Rko:?UP09gLc(-/ni'Q6T.B&8]k +#a_e\0FTMV`IT;W7M>_'NT_f=~> +EI diff --git a/bin/reportlab/tools/pythonpoint/demos/leftlogo.gif b/bin/reportlab/tools/pythonpoint/demos/leftlogo.gif new file mode 100644 index 00000000000..2d9c990ba7a Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/leftlogo.gif differ diff --git a/bin/reportlab/tools/pythonpoint/demos/lj8100.jpg b/bin/reportlab/tools/pythonpoint/demos/lj8100.jpg new file mode 100644 index 00000000000..be3c6183d3b Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/lj8100.jpg differ diff --git a/bin/reportlab/tools/pythonpoint/demos/monterey.xml b/bin/reportlab/tools/pythonpoint/demos/monterey.xml new file mode 100644 index 00000000000..41bd979fa67 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/monterey.xml @@ -0,0 +1,306 @@ + + + + + + +
              + + + + + + + + + Printing with Python + + + + Andy Robinson, Robinson Analytics Ltd. + + + O'Reilly Python Conference, Monterey, 24th August 1999 + + + + + + + Background to the project: + + + London-based consultant and corporate developer + + + want to do neat Python stuff in the daytime + + + working for many years on financial modelling + + + this is one of 6 modules in that system + + + quickest to deliver, offers very wide benefits + + + 25% of architecture done, but already very useful + + + Release early, release often! + + + + + + + + Goal: + + + A Reporting Package on the Next Curve... + + + Report on objects, not databases + + + Scalable to million page runs + + + Light enough to embed in any application + + + Allow reuse of graphical objects across reports + + + Open and extensible on several levels + + + Publication quality + + + Support all the world's languages - one day + + + + + + + Portable Document Format + + + The New PostScript + + + Free readers on all platforms + + + Better than paper - view it, email it, print it + + + 'Final Form' for documents + + + High end solution - no limits to quality + + + ...but you can't learn it in Notepad! + + + + + + + + + PDFgen and PIDDLE + + + + + + + Layer One - PDFgen + + + makes PDF documents from pure Python + + + wraps up PDF document structure + + + exposes nice effects - page transitions, outline trees (RSN!) + + + low level graphics promitives (postscript imaging model) + + + Fine control of text placement + + + Supports Asian text + + + Supports coordinate transformations and clipping + + + ...a foundation for other apps to build on + + + + + + + PDFgen Image Support + + + Python Imaging Library and zlib do all the work - many formats. + Images cached (like .pyc files) - very fast builds possible. + + + + + + + + Layer Two: PIDDLE + + + Plug In Drawing, Does Little Else + + + Easy Graphics Library + + + Abstract Canvas Interface + + + Pluggable Back Ends + + + Same code can do viewing and printing + + + Standard set of test patterns + + + Uses Python Imaging Library + + + Back ends includeTkinter, wxPython, Mac, Pythonwin, PDF, PostScript, + OpenGL, Adobe Illustrator and PIL. Really easy to add a new one! + + + + + + + Layer Three: PLATYPUS + + + "Page Layout And Typography Using Scripts" + + + Trying to work out the API now. Key Concepts: + + + Drawable objects - can 'wrap to fit' + + + Frames on page + + + Frame consumes from a list of drawables until full + + + Document Models e.g. SimpleFlowDocument + + + XSL Flow Object model may be a good target + + + + + + + Drawable Objects + + + Next layer of PIDDLE extensibility. + Each draws in its own coodinate system + + + paragraph, image, table + + + chart libraries + + + diagrams + + + Open Source - let people contribute new ones. + Anything you could have in a view can be a new + drawable type. + + + + + + + Style Sheet Driven + + + Styles use instance inheritance + + + Paragraph Styles - Style Sheet Compulsory! + + + Text Styles within a paragraph + + + Table and Table Cell Styles + + + + + + + Vision + + + XML to PDF in one step + + + Publish to web and print from same source + + + Financial and Scientific reporting tool + + + Embedded reporting engine + + + Volume reporting tool for business + + + + + + + PythonPoint + + + How I made this presentation... + + + +
              +
              diff --git a/bin/reportlab/tools/pythonpoint/demos/outline.gif b/bin/reportlab/tools/pythonpoint/demos/outline.gif new file mode 100644 index 00000000000..4a294b59277 Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/outline.gif differ diff --git a/bin/reportlab/tools/pythonpoint/demos/pplogo.gif b/bin/reportlab/tools/pythonpoint/demos/pplogo.gif new file mode 100644 index 00000000000..d0344497ca5 Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/pplogo.gif differ diff --git a/bin/reportlab/tools/pythonpoint/demos/python.gif b/bin/reportlab/tools/pythonpoint/demos/python.gif new file mode 100644 index 00000000000..c68e1e4ec37 Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/python.gif differ diff --git a/bin/reportlab/tools/pythonpoint/demos/pythonpoint.xml b/bin/reportlab/tools/pythonpoint/demos/pythonpoint.xml new file mode 100644 index 00000000000..15d1ad709b5 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/pythonpoint.xml @@ -0,0 +1,1051 @@ + + + + + PythonPoint Demonstration + Andy Robinson + Reportlab Sample Applications +
              + + + +
              +
              diff --git a/bin/reportlab/tools/pythonpoint/demos/slidebox.py b/bin/reportlab/tools/pythonpoint/demos/slidebox.py new file mode 100644 index 00000000000..f0d9cf308e9 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/demos/slidebox.py @@ -0,0 +1,29 @@ +#Autogenerated by ReportLab guiedit do not edit +from reportlab.graphics.shapes import _DrawingEditorMixin +from reportlab.graphics.charts.slidebox import SlideBox +from rlextra.graphics.guiedit.datacharts import ODBCDataSource, CSVDataSource, DataAssociation, DataAwareDrawing + +class SlideBoxDrawing(_DrawingEditorMixin,DataAwareDrawing): + def __init__(self,width=400,height=200,*args,**kw): + apply(DataAwareDrawing.__init__,(self,width,height)+args,kw) + self._add(self,SlideBox(),name='SlideBox',validate=None,desc='The main chart') + self.height = 40 + self.width = 168 + #self.dataSource = ODBCDataSource() + self.dataSource = CSVDataSource() + self.dataSource.filename = 'slidebox.csv' + self.dataSource.integerColumns = ['chartId','value','numberOfBoxes'] + self.dataSource.sql = 'SELECT chartId,numberOfBoxes,label,value FROM generic_slidebox' + self.dataSource.associations.size = 4 + self.dataSource.associations.element00 = DataAssociation(column=0, target='chartId', assocType='scalar') + self.dataSource.associations.element01 = DataAssociation(column=1, target='SlideBox.numberOfBoxes', assocType='scalar') + self.dataSource.associations.element02 = DataAssociation(column=2, target='SlideBox.sourceLabelText', assocType='scalar') + self.dataSource.associations.element03 = DataAssociation(column=3, target='SlideBox.trianglePosition', assocType='scalar') + self.verbose = 1 + self.formats = ['eps', 'pdf'] + self.outDir = './output/' + self.fileNamePattern = 'slidebox%03d' + + +if __name__=="__main__": #NORUNTESTS + SlideBoxDrawing().go() diff --git a/bin/reportlab/tools/pythonpoint/demos/spectrum.png b/bin/reportlab/tools/pythonpoint/demos/spectrum.png new file mode 100644 index 00000000000..06747655056 Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/spectrum.png differ diff --git a/bin/reportlab/tools/pythonpoint/demos/vertpython.gif b/bin/reportlab/tools/pythonpoint/demos/vertpython.gif new file mode 100644 index 00000000000..5e5c60c3308 Binary files /dev/null and b/bin/reportlab/tools/pythonpoint/demos/vertpython.gif differ diff --git a/bin/reportlab/tools/pythonpoint/pythonpoint.dtd b/bin/reportlab/tools/pythonpoint/pythonpoint.dtd new file mode 100644 index 00000000000..ec876f68387 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/pythonpoint.dtd @@ -0,0 +1,275 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/bin/reportlab/tools/pythonpoint/pythonpoint.py b/bin/reportlab/tools/pythonpoint/pythonpoint.py new file mode 100644 index 00000000000..c5e823de04d --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/pythonpoint.py @@ -0,0 +1,1124 @@ +#!/usr/bin/env python + +""" +This is PythonPoint! + +The idea is a simple markup languages for describing presentation +slides, and other documents which run page by page. I expect most +of it will be reusable in other page layout stuff. + +Look at the sample near the top, which shows how the presentation +should be coded up. + +The parser, which is in a separate module to allow for multiple +parsers, turns the XML sample into an object tree. There is a +simple class hierarchy of items, the inner levels of which create +flowable objects to go in the frames. These know how to draw +themselves. + +The currently available 'Presentation Objects' are: + + The main hierarchy... + PPPresentation + PPSection + PPSlide + PPFrame + + PPAuthor, PPTitle and PPSubject are optional + + Things to flow within frames... + PPPara - flowing text + PPPreformatted - text with line breaks and tabs, for code.. + PPImage + PPTable - bulk formatted tabular data + PPSpacer + + Things to draw directly on the page... + PPRect + PPRoundRect + PPDrawingElement - user base class for graphics + PPLine + PPEllipse + +Features added by H. Turgut Uyar +- TrueType support (actually, just an import in the style file); + this also enables the use of Unicode symbols +- para, image, table, line, rectangle, roundrect, ellipse, polygon + and string elements can now have effect attributes + (careful: new slide for each effect!) +- added printout mode (no new slides for effects, see item above) +- added a second-level bullet: Bullet2 +- small bugfixes in handleHiddenSlides: + corrected the outlineEntry of included hidden slide + and made sure to include the last slide even if hidden + +Recently added features are: + +- file globbing +- package structure +- named colors throughout (using names from reportlab/lib/colors.py) +- handout mode with arbitrary number of columns per page +- stripped off pages hidden in the outline tree (hackish) +- new tag for speaker notes (paragraphs only) +- new tag for syntax-colorized Python code +- reformatted pythonpoint.xml and monterey.xml demos +- written/extended DTD +- arbitrary font support +- print proper speaker notes (TODO) +- fix bug with partially hidden graphics (TODO) +- save in combined presentation/handout mode (TODO) +- add pyRXP support (TODO) +""" + +import os, sys, imp, string, pprint, getopt, glob + +from reportlab import rl_config +from reportlab.lib import styles +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.utils import getStringIO +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfgen import canvas +from reportlab.platypus.doctemplate import SimpleDocTemplate +from reportlab.platypus.flowables import Flowable +from reportlab.platypus.xpreformatted import PythonPreformatted +from reportlab.platypus import Preformatted, Paragraph, Frame, \ + Image, Table, TableStyle, Spacer + + +USAGE_MESSAGE = """\ +PythonPoint - a tool for making presentations in PDF. + +Usage: + pythonpoint.py [options] file1.xml [file2.xml [...]] + + where options can be any of these: + + -h / --help prints this message + -n / --notes leave room for comments + -v / --verbose verbose mode + -s / --silent silent mode (NO output) + --handout produce handout document + --printout produce printout document + --cols specify number of columns + on handout pages (default: 2) + +To create the PythonPoint user guide, do: + pythonpoint.py pythonpoint.xml +""" + + +##################################################################### +# This should probably go into reportlab/lib/fonts.py... +##################################################################### + +class FontNameNotFoundError(Exception): + pass + + +class FontFilesNotFoundError(Exception): + pass + + +##def findFontName(path): +## "Extract a Type-1 font name from an AFM file." +## +## f = open(path) +## +## found = 0 +## while not found: +## line = f.readline()[:-1] +## if not found and line[:16] == 'StartCharMetrics': +## raise FontNameNotFoundError, path +## if line[:8] == 'FontName': +## fontName = line[9:] +## found = 1 +## +## return fontName +## +## +##def locateFilesForFontWithName(name): +## "Search known paths for AFM/PFB files describing T1 font with given name." +## +## join = os.path.join +## splitext = os.path.splitext +## +## afmFile = None +## pfbFile = None +## +## found = 0 +## while not found: +## for p in rl_config.T1SearchPath: +## afmFiles = glob.glob(join(p, '*.[aA][fF][mM]')) +## for f in afmFiles: +## T1name = findFontName(f) +## if T1name == name: +## afmFile = f +## found = 1 +## break +## if afmFile: +## break +## break +## +## if afmFile: +## pfbFile = glob.glob(join(splitext(afmFile)[0] + '.[pP][fF][bB]'))[0] +## +## return afmFile, pfbFile +## +## +##def registerFont(name): +## "Register Type-1 font for future use." +## +## rl_config.warnOnMissingFontGlyphs = 0 +## rl_config.T1SearchPath.append(r'C:\Programme\Python21\reportlab\test') +## +## afmFile, pfbFile = locateFilesForFontWithName(name) +## if not afmFile and not pfbFile: +## raise FontFilesNotFoundError +## +## T1face = pdfmetrics.EmbeddedType1Face(afmFile, pfbFile) +## T1faceName = name +## pdfmetrics.registerTypeFace(T1face) +## T1font = pdfmetrics.Font(name, T1faceName, 'WinAnsiEncoding') +## pdfmetrics.registerFont(T1font) + + +def registerFont0(sourceFile, name, path): + "Register Type-1 font for future use, simple version." + + rl_config.warnOnMissingFontGlyphs = 0 + + p = os.path.join(os.path.dirname(sourceFile), path) + afmFiles = glob.glob(p + '.[aA][fF][mM]') + pfbFiles = glob.glob(p + '.[pP][fF][bB]') + assert len(afmFiles) == len(pfbFiles) == 1, FontFilesNotFoundError + + T1face = pdfmetrics.EmbeddedType1Face(afmFiles[0], pfbFiles[0]) + T1faceName = name + pdfmetrics.registerTypeFace(T1face) + T1font = pdfmetrics.Font(name, T1faceName, 'WinAnsiEncoding') + pdfmetrics.registerFont(T1font) + +##################################################################### + + +def checkColor(col): + "Converts a color name to an RGB tuple, if possible." + + if type(col) == type('') and col in dir(colors): + col = getattr(colors, col) + col = (col.red, col.green, col.blue) + + return col + + +def handleHiddenSlides(slides): + """Filters slides from a list of slides. + + In a sequence of hidden slides all but the last one are + removed. Also, the slide before the sequence of hidden + ones is removed. + + This assumes to leave only those slides in the handout + that also appear in the outline, hoping to reduce se- + quences where each new slide only adds one new line + to a list of items... + """ + + itd = indicesToDelete = map(lambda s:s.outlineEntry == None, slides) + + for i in range(len(itd)-1): + if itd[i] == 1: + if itd[i+1] == 0: + itd[i] = 0 + if i > 0 and itd[i-1] == 0: + itd[i-1] = 1 + + itd[len(itd)-1] = 0 + + for i in range(len(itd)): + if slides[i].outlineEntry: + curOutlineEntry = slides[i].outlineEntry + if itd[i] == 1: + slides[i].delete = 1 + else: + slides[i].outlineEntry = curOutlineEntry + slides[i].delete = 0 + + slides = filter(lambda s:s.delete == 0, slides) + + return slides + + +def makeSlideTable(slides, pageSize, docWidth, numCols): + """Returns a table containing a collection of SlideWrapper flowables. + """ + + slides = handleHiddenSlides(slides) + + # Set table style. + tabStyle = TableStyle( + [('GRID', (0,0), (-1,-1), 0.25, colors.black), + ('ALIGN', (0,0), (-1,-1), 'CENTRE') + ]) + + # Build table content. + width = docWidth/numCols + height = width * pageSize[1]/pageSize[0] + matrix = [] + row = [] + for slide in slides: + sw = SlideWrapper(width, height, slide, pageSize) + if (len(row)) < numCols: + row.append(sw) + else: + matrix.append(row) + row = [] + row.append(sw) + if len(row) > 0: + for i in range(numCols-len(row)): + row.append('') + matrix.append(row) + + # Make Table flowable. + t = Table(matrix, + [width + 5]*len(matrix[0]), + [height + 5]*len(matrix)) + t.setStyle(tabStyle) + + return t + + +class SlideWrapper(Flowable): + """A Flowable wrapping a PPSlide object. + """ + + def __init__(self, width, height, slide, pageSize): + Flowable.__init__(self) + self.width = width + self.height = height + self.slide = slide + self.pageSize = pageSize + + + def __repr__(self): + return "SlideWrapper(w=%s, h=%s)" % (self.width, self.height) + + + def draw(self): + "Draw the slide in our relative coordinate system." + + slide = self.slide + pageSize = self.pageSize + canv = self.canv + + canv.saveState() + canv.scale(self.width/pageSize[0], self.height/pageSize[1]) + slide.effectName = None + slide.drawOn(self.canv) + canv.restoreState() + + +class PPPresentation: + def __init__(self): + self.sourceFilename = None + self.filename = None + self.outDir = None + self.description = None + self.title = None + self.author = None + self.subject = None + self.notes = 0 # different printing mode + self.handout = 0 # prints many slides per page + self.printout = 0 # remove hidden slides + self.cols = 0 # columns per handout page + self.slides = [] + self.effectName = None + self.showOutline = 1 #should it be displayed when opening? + self.compression = rl_config.pageCompression + self.pageDuration = None + #assume landscape + self.pageWidth = rl_config.defaultPageSize[1] + self.pageHeight = rl_config.defaultPageSize[0] + self.verbose = rl_config.verbose + + + def saveAsPresentation(self): + """Write the PDF document, one slide per page.""" + if self.verbose: + print 'saving presentation...' + pageSize = (self.pageWidth, self.pageHeight) + if self.sourceFilename: + filename = os.path.splitext(self.sourceFilename)[0] + '.pdf' + if self.outDir: filename = os.path.join(self.outDir,os.path.basename(filename)) + if self.verbose: + print filename + #canv = canvas.Canvas(filename, pagesize = pageSize) + outfile = getStringIO() + if self.notes: + #translate the page from landscape to portrait + pageSize= pageSize[1], pageSize[0] + canv = canvas.Canvas(outfile, pagesize = pageSize) + canv.setPageCompression(self.compression) + canv.setPageDuration(self.pageDuration) + if self.title: + canv.setTitle(self.title) + if self.author: + canv.setAuthor(self.author) + if self.subject: + canv.setSubject(self.subject) + + slideNo = 0 + for slide in self.slides: + #need diagnostic output if something wrong with XML + slideNo = slideNo + 1 + if self.verbose: + print 'doing slide %d, id = %s' % (slideNo, slide.id) + if self.notes: + #frame and shift the slide + #canv.scale(0.67, 0.67) + scale_amt = (min(pageSize)/float(max(pageSize)))*.95 + #canv.translate(self.pageWidth / 6.0, self.pageHeight / 3.0) + #canv.translate(self.pageWidth / 2.0, .025*self.pageHeight) + canv.translate(.025*self.pageHeight, (self.pageWidth/2.0) + 5) + #canv.rotate(90) + canv.scale(scale_amt, scale_amt) + canv.rect(0,0,self.pageWidth, self.pageHeight) + slide.drawOn(canv) + canv.showPage() + + #ensure outline visible by default + if self.showOutline: + canv.showOutline() + + canv.save() + return self.savetofile(outfile, filename) + + + def saveAsHandout(self): + """Write the PDF document, multiple slides per page.""" + + styleSheet = getSampleStyleSheet() + h1 = styleSheet['Heading1'] + bt = styleSheet['BodyText'] + + if self.sourceFilename : + filename = os.path.splitext(self.sourceFilename)[0] + '.pdf' + + outfile = getStringIO() + doc = SimpleDocTemplate(outfile, pagesize=rl_config.defaultPageSize, showBoundary=0) + doc.leftMargin = 1*cm + doc.rightMargin = 1*cm + doc.topMargin = 2*cm + doc.bottomMargin = 2*cm + multiPageWidth = rl_config.defaultPageSize[0] - doc.leftMargin - doc.rightMargin - 50 + + story = [] + orgFullPageSize = (self.pageWidth, self.pageHeight) + t = makeSlideTable(self.slides, orgFullPageSize, multiPageWidth, self.cols) + story.append(t) + +## #ensure outline visible by default +## if self.showOutline: +## doc.canv.showOutline() + + doc.build(story) + return self.savetofile(outfile, filename) + + def savetofile(self, pseudofile, filename): + """Save the pseudo file to disk and return its content as a + string of text.""" + pseudofile.flush() + content = pseudofile.getvalue() + pseudofile.close() + if filename : + outf = open(filename, "wb") + outf.write(content) + outf.close() + return content + + + + def save(self): + "Save the PDF document." + + if self.handout: + return self.saveAsHandout() + else: + return self.saveAsPresentation() + + +#class PPSection: +# """A section can hold graphics which will be drawn on all +# pages within it, before frames and other content are done. +# In other words, a background template.""" +# def __init__(self, name): +# self.name = name +# self.graphics = [] +# +# def drawOn(self, canv): +# for graphic in self.graphics: +### graphic.drawOn(canv) +# +# name = str(hash(graphic)) +# internalname = canv._doc.hasForm(name) +# +# canv.saveState() +# if not internalname: +# canv.beginForm(name) +# graphic.drawOn(canv) +# canv.endForm() +# canv.doForm(name) +# else: +# canv.doForm(name) +# canv.restoreState() + + +definedForms = {} + +class PPSection: + """A section can hold graphics which will be drawn on all + pages within it, before frames and other content are done. + In other words, a background template.""" + + def __init__(self, name): + self.name = name + self.graphics = [] + + def drawOn(self, canv): + for graphic in self.graphics: + graphic.drawOn(canv) + continue + name = str(hash(graphic)) + #internalname = canv._doc.hasForm(name) + if definedForms.has_key(name): + internalname = 1 + else: + internalname = None + definedForms[name] = 1 + if not internalname: + canv.beginForm(name) + canv.saveState() + graphic.drawOn(canv) + canv.restoreState() + canv.endForm() + canv.doForm(name) + else: + canv.doForm(name) + + +class PPNotes: + def __init__(self): + self.content = [] + + def drawOn(self, canv): + print self.content + + +class PPSlide: + def __init__(self): + self.id = None + self.title = None + self.outlineEntry = None + self.outlineLevel = 0 # can be higher for sub-headings + self.effectName = None + self.effectDirection = 0 + self.effectDimension = 'H' + self.effectMotion = 'I' + self.effectDuration = 1 + self.frames = [] + self.notes = [] + self.graphics = [] + self.section = None + + def drawOn(self, canv): + if self.effectName: + canv.setPageTransition( + effectname=self.effectName, + direction = self.effectDirection, + dimension = self.effectDimension, + motion = self.effectMotion, + duration = self.effectDuration + ) + + if self.outlineEntry: + #gets an outline automatically + self.showOutline = 1 + #put an outline entry in the left pane + tag = self.title + canv.bookmarkPage(tag) + canv.addOutlineEntry(tag, tag, self.outlineLevel) + + if self.section: + self.section.drawOn(canv) + + for graphic in self.graphics: + graphic.drawOn(canv) + + for frame in self.frames: + frame.drawOn(canv) + +## # Need to draw the notes *somewhere*... +## for note in self.notes: +## print note + + +class PPFrame: + def __init__(self, x, y, width, height): + self.x = x + self.y = y + self.width = width + self.height = height + self.content = [] + self.showBoundary = 0 + + def drawOn(self, canv): + #make a frame + frame = Frame( self.x, + self.y, + self.width, + self.height + ) + frame.showBoundary = self.showBoundary + + #build a story for the frame + story = [] + for thingy in self.content: + #ask it for any flowables + story.append(thingy.getFlowable()) + #draw it + frame.addFromList(story,canv) + + +class PPPara: + """This is a placeholder for a paragraph.""" + def __init__(self): + self.rawtext = '' + self.style = None + + def escapeAgain(self, text): + """The XML has been parsed once, so '>' became '>' + in rawtext. We need to escape this to get back to + something the Platypus parser can accept""" + pass + + def getFlowable(self): +## print 'rawText for para:' +## print repr(self.rawtext) + p = Paragraph( + self.rawtext, + getStyles()[self.style], + self.bulletText + ) + return p + + +class PPPreformattedText: + """Use this for source code, or stuff you do not want to wrap""" + def __init__(self): + self.rawtext = '' + self.style = None + + def getFlowable(self): + return Preformatted(self.rawtext, getStyles()[self.style]) + + +class PPPythonCode: + """Use this for colored Python source code""" + def __init__(self): + self.rawtext = '' + self.style = None + + def getFlowable(self): + return PythonPreformatted(self.rawtext, getStyles()[self.style]) + + +class PPImage: + """Flowing image within the text""" + def __init__(self): + self.filename = None + self.width = None + self.height = None + + def getFlowable(self): + return Image(self.filename, self.width, self.height) + + +class PPTable: + """Designed for bulk loading of data for use in presentations.""" + def __init__(self): + self.rawBlocks = [] #parser stuffs things in here... + self.fieldDelim = ',' #tag args can override + self.rowDelim = '\n' #tag args can override + self.data = None + self.style = None #tag args must specify + self.widths = None #tag args can override + self.heights = None #tag args can override + + def getFlowable(self): + self.parseData() + t = Table( + self.data, + self.widths, + self.heights) + if self.style: + t.setStyle(getStyles()[self.style]) + + return t + + def parseData(self): + """Try to make sense of the table data!""" + rawdata = string.strip(string.join(self.rawBlocks, '')) + lines = string.split(rawdata, self.rowDelim) + #clean up... + lines = map(string.strip, lines) + self.data = [] + for line in lines: + cells = string.split(line, self.fieldDelim) + self.data.append(cells) + + #get the width list if not given + if not self.widths: + self.widths = [None] * len(self.data[0]) + if not self.heights: + self.heights = [None] * len(self.data) + +## import pprint +## print 'table data:' +## print 'style=',self.style +## print 'widths=',self.widths +## print 'heights=',self.heights +## print 'fieldDelim=',repr(self.fieldDelim) +## print 'rowDelim=',repr(self.rowDelim) +## pprint.pprint(self.data) + + +class PPSpacer: + def __init__(self): + self.height = 24 #points + + def getFlowable(self): + return Spacer(72, self.height) + + + ############################################################# + # + # The following are things you can draw on a page directly. + # + ############################################################## + +##class PPDrawingElement: +## """Base class for something which you draw directly on the page.""" +## def drawOn(self, canv): +## raise "NotImplementedError", "Abstract base class!" + + +class PPFixedImage: + """You place this on the page, rather than flowing it""" + def __init__(self): + self.filename = None + self.x = 0 + self.y = 0 + self.width = None + self.height = None + + def drawOn(self, canv): + if self.filename: + x, y = self.x, self.y + w, h = self.width, self.height + canv.drawImage(self.filename, x, y, w, h) + + +class PPRectangle: + def __init__(self, x, y, width, height): + self.x = x + self.y = y + self.width = width + self.height = height + self.fillColor = None + self.strokeColor = (1,1,1) + self.lineWidth=0 + + def drawOn(self, canv): + canv.saveState() + canv.setLineWidth(self.lineWidth) + if self.fillColor: + r,g,b = checkColor(self.fillColor) + canv.setFillColorRGB(r,g,b) + if self.strokeColor: + r,g,b = checkColor(self.strokeColor) + canv.setStrokeColorRGB(r,g,b) + canv.rect(self.x, self.y, self.width, self.height, + stroke=(self.strokeColor<>None), + fill = (self.fillColor<>None) + ) + canv.restoreState() + + +class PPRoundRect: + def __init__(self, x, y, width, height, radius): + self.x = x + self.y = y + self.width = width + self.height = height + self.radius = radius + self.fillColor = None + self.strokeColor = (1,1,1) + self.lineWidth=0 + + def drawOn(self, canv): + canv.saveState() + canv.setLineWidth(self.lineWidth) + if self.fillColor: + r,g,b = checkColor(self.fillColor) + canv.setFillColorRGB(r,g,b) + if self.strokeColor: + r,g,b = checkColor(self.strokeColor) + canv.setStrokeColorRGB(r,g,b) + canv.roundRect(self.x, self.y, self.width, self.height, + self.radius, + stroke=(self.strokeColor<>None), + fill = (self.fillColor<>None) + ) + canv.restoreState() + + +class PPLine: + def __init__(self, x1, y1, x2, y2): + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + self.fillColor = None + self.strokeColor = (1,1,1) + self.lineWidth=0 + + def drawOn(self, canv): + canv.saveState() + canv.setLineWidth(self.lineWidth) + if self.strokeColor: + r,g,b = checkColor(self.strokeColor) + canv.setStrokeColorRGB(r,g,b) + canv.line(self.x1, self.y1, self.x2, self.y2) + canv.restoreState() + + +class PPEllipse: + def __init__(self, x1, y1, x2, y2): + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + self.fillColor = None + self.strokeColor = (1,1,1) + self.lineWidth=0 + + def drawOn(self, canv): + canv.saveState() + canv.setLineWidth(self.lineWidth) + if self.strokeColor: + r,g,b = checkColor(self.strokeColor) + canv.setStrokeColorRGB(r,g,b) + if self.fillColor: + r,g,b = checkColor(self.fillColor) + canv.setFillColorRGB(r,g,b) + canv.ellipse(self.x1, self.y1, self.x2, self.y2, + stroke=(self.strokeColor<>None), + fill = (self.fillColor<>None) + ) + canv.restoreState() + + +class PPPolygon: + def __init__(self, pointlist): + self.points = pointlist + self.fillColor = None + self.strokeColor = (1,1,1) + self.lineWidth=0 + + def drawOn(self, canv): + canv.saveState() + canv.setLineWidth(self.lineWidth) + if self.strokeColor: + r,g,b = checkColor(self.strokeColor) + canv.setStrokeColorRGB(r,g,b) + if self.fillColor: + r,g,b = checkColor(self.fillColor) + canv.setFillColorRGB(r,g,b) + + path = canv.beginPath() + (x,y) = self.points[0] + path.moveTo(x,y) + for (x,y) in self.points[1:]: + path.lineTo(x,y) + path.close() + canv.drawPath(path, + stroke=(self.strokeColor<>None), + fill=(self.fillColor<>None)) + canv.restoreState() + + +class PPString: + def __init__(self, x, y): + self.text = '' + self.x = x + self.y = y + self.align = TA_LEFT + self.font = 'Times-Roman' + self.size = 12 + self.color = (0,0,0) + self.hasInfo = 0 # these can have data substituted into them + + def normalizeText(self): + """It contains literal XML text typed over several lines. + We want to throw away + tabs, newlines and so on, and only accept embedded string + like '\n'""" + lines = string.split(self.text, '\n') + newtext = [] + for line in lines: + newtext.append(string.strip(line)) + #accept all the '\n' as newlines + + self.text = newtext + + def drawOn(self, canv): + # for a string in a section, this will be drawn several times; + # so any substitution into the text should be in a temporary + # variable + if self.hasInfo: + # provide a dictionary of stuff which might go into + # the string, so they can number pages, do headers + # etc. + info = {} + info['title'] = canv._doc.info.title + info['author'] = canv._doc.info.author + info['subject'] = canv._doc.info.subject + info['page'] = canv.getPageNumber() + drawText = self.text % info + else: + drawText = self.text + + if self.color is None: + return + lines = string.split(string.strip(drawText), '\\n') + canv.saveState() + + canv.setFont(self.font, self.size) + + r,g,b = checkColor(self.color) + canv.setFillColorRGB(r,g,b) + cur_y = self.y + for line in lines: + if self.align == TA_LEFT: + canv.drawString(self.x, cur_y, line) + elif self.align == TA_CENTER: + canv.drawCentredString(self.x, cur_y, line) + elif self.align == TA_RIGHT: + canv.drawRightString(self.x, cur_y, line) + cur_y = cur_y - 1.2*self.size + + canv.restoreState() + +class PPDrawing: + def __init__(self): + self.drawing = None + def getFlowable(self): + return self.drawing + +class PPFigure: + def __init__(self): + self.figure = None + def getFlowable(self): + return self.figure + +def getSampleStyleSheet(): + from reportlab.tools.pythonpoint.styles.standard import getParagraphStyles + return getParagraphStyles() + + +#make a singleton and a function to access it +_styles = None +def getStyles(): + global _styles + if not _styles: + _styles = getSampleStyleSheet() + return _styles + + +def setStyles(newStyleSheet): + global _styles + _styles = newStyleSheet + +_pyRXP_Parser = None +def validate(rawdata): + global _pyRXP_Parser + if not _pyRXP_Parser: + try: + import pyRXP + except ImportError: + return + from reportlab.lib.utils import open_and_read, _RL_DIR, rl_isfile + dtd = 'pythonpoint.dtd' + if not rl_isfile(dtd): + dtd = os.path.join(_RL_DIR,'tools','pythonpoint','pythonpoint.dtd') + if not rl_isfile(dtd): return + def eocb(URI,dtdText=open_and_read(dtd),dtd=dtd): + if os.path.basename(URI)=='pythonpoint.dtd': return dtd,dtdText + return URI + _pyRXP_Parser = pyRXP.Parser(eoCB=eocb) + return _pyRXP_Parser.parse(rawdata) + +def process(datafile, notes=0, handout=0, printout=0, cols=0, verbose=0, outDir=None, datafilename=None, fx=1): + "Process one PythonPoint source file." + if not hasattr(datafile, "read"): + if not datafilename: datafilename = datafile + datafile = open(datafile) + else: + if not datafilename: datafilename = "PseudoFile" + rawdata = datafile.read() + + #if pyRXP present, use it to check and get line numbers for errors... + validate(rawdata) + return _process(rawdata, datafilename, notes, handout, printout, cols, verbose, outDir, fx) + +def _process(rawdata, datafilename, notes=0, handout=0, printout=0, cols=0, verbose=0, outDir=None, fx=1): + #print 'inner process fx=%d' % fx + from reportlab.tools.pythonpoint.stdparser import PPMLParser + parser = PPMLParser() + parser.fx = fx + parser.sourceFilename = datafilename + parser.feed(rawdata) + pres = parser.getPresentation() + pres.sourceFilename = datafilename + pres.outDir = outDir + pres.notes = notes + pres.handout = handout + pres.printout = printout + pres.cols = cols + pres.verbose = verbose + + if printout: + pres.slides = handleHiddenSlides(pres.slides) + + #this does all the work + pdfcontent = pres.save() + + if verbose: + print 'saved presentation %s.pdf' % os.path.splitext(datafilename)[0] + parser.close() + + return pdfcontent +##class P: +## def feed(self, text): +## parser = stdparser.PPMLParser() +## d = pyRXP.parse(text) +## +## +##def process2(datafilename, notes=0, handout=0, cols=0): +## "Process one PythonPoint source file." +## +## import pyRXP, pprint +## +## rawdata = open(datafilename).read() +## d = pyRXP.parse(rawdata) +## pprint.pprint(d) + + +def handleOptions(): + # set defaults + from reportlab import rl_config + options = {'cols':2, + 'handout':0, + 'printout':0, + 'help':0, + 'notes':0, + 'fx':1, + 'verbose':rl_config.verbose, + 'silent':0, + 'outDir': None} + + args = sys.argv[1:] + args = filter(lambda x: x and x[0]=='-',args) + filter(lambda x: not x or x[0]!='-',args) + try: + shortOpts = 'hnvsx' + longOpts = string.split('cols= outdir= handout help notes printout verbose silent nofx') + optList, args = getopt.getopt(args, shortOpts, longOpts) + except getopt.error, msg: + options['help'] = 1 + + if not args and os.path.isfile('pythonpoint.xml'): + args = ['pythonpoint.xml'] + + # Remove leading dashes (max. two). + for i in range(len(optList)): + o, v = optList[i] + while o[0] == '-': + o = o[1:] + optList[i] = (o, v) + + if o == 'cols': options['cols'] = int(v) + elif o=='outdir': options['outDir'] = v + + if filter(lambda ov: ov[0] == 'handout', optList): + options['handout'] = 1 + + if filter(lambda ov: ov[0] == 'printout', optList): + options['printout'] = 1 + + if optList == [] and args == [] or \ + filter(lambda ov: ov[0] in ('h', 'help'), optList): + options['help'] = 1 + + if filter(lambda ov: ov[0] in ('n', 'notes'), optList): + options['notes'] = 1 + + if filter(lambda ov: ov[0] in ('x', 'nofx'), optList): + options['fx'] = 0 + + if filter(lambda ov: ov[0] in ('v', 'verbose'), optList): + options['verbose'] = 1 + + #takes priority over verbose. Used by our test suite etc. + #to ensure no output at all + if filter(lambda ov: ov[0] in ('s', 'silent'), optList): + options['silent'] = 1 + options['verbose'] = 0 + + + return options, args + +def main(): + options, args = handleOptions() + + if options['help']: + print USAGE_MESSAGE + sys.exit(0) + + if options['verbose'] and options['notes']: + print 'speaker notes mode' + + if options['verbose'] and options['handout']: + print 'handout mode' + + if options['verbose'] and options['printout']: + print 'printout mode' + + if not options['fx']: + print 'suppressing special effects' + for fileGlobs in args: + files = glob.glob(fileGlobs) + if not files: + print fileGlobs, "not found" + return + for datafile in files: + if os.path.isfile(datafile): + file = os.path.join(os.getcwd(), datafile) + notes, handout, printout, cols, verbose, fx = options['notes'], options['handout'], options['printout'], options['cols'], options['verbose'], options['fx'] + process(file, notes, handout, printout, cols, verbose, options['outDir'], fx=fx) + else: + print 'Data file not found:', datafile + +if __name__ == '__main__': + main() diff --git a/bin/reportlab/tools/pythonpoint/stdparser.py b/bin/reportlab/tools/pythonpoint/stdparser.py new file mode 100644 index 00000000000..aaca2df2000 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/stdparser.py @@ -0,0 +1,813 @@ +""" +Parser for PythonPoint using the xmllib.py in the standard Python +distribution. Slow, but always present. We intend to add new parsers +as Python 2.x and the XML package spread in popularity and stabilise. + +The parser has a getPresentation method; it is called from +pythonpoint.py. +""" + +import string, imp, sys, os, copy +from reportlab.lib.utils import SeqTypes +from reportlab.lib import xmllib +from reportlab.lib import colors +from reportlab.lib.enums import TA_LEFT, TA_RIGHT, TA_CENTER, TA_JUSTIFY +from reportlab.lib.utils import recursiveImport +from reportlab.tools.pythonpoint import pythonpoint +from reportlab.platypus import figures + + +def getModule(modulename,fromPath='reportlab.tools.pythonpoint.styles'): + """Get a module containing style declarations. + + Search order is: + reportlab/tools/pythonpoint/ + reportlab/tools/pythonpoint/styles/ + ./ + """ + + try: + exec 'from reportlab.tools.pythonpoint import '+modulename + return eval(modulename) + except ImportError: + try: + exec 'from reportlab.tools.pythonpoint.styles import '+modulename + return eval(modulename) + except ImportError: + exec 'import '+modulename + return eval(modulename) + + +class PPMLParser(xmllib.XMLParser): + attributes = { + #this defines the available attributes for all objects, + #and their default values. Although these don't have to + #be strings, the ones parsed from the XML do, so + #everything is a quoted string and the parser has to + #convert these to numbers where appropriate. + 'stylesheet': { + 'path':'None', + 'module':'None', + 'function':'getParagraphStyles' + }, + 'frame': { + 'x':'0', + 'y':'0', + 'width':'0', + 'height':'0', + 'border':'false', + 'leftmargin':'0', #this is ignored + 'topmargin':'0', #this is ignored + 'rightmargin':'0', #this is ignored + 'bottommargin':'0', #this is ignored + }, + 'slide': { + 'id':'None', + 'title':'None', + 'effectname':'None', # Split, Blinds, Box, Wipe, Dissolve, Glitter + 'effectdirection':'0', # 0,90,180,270 + 'effectdimension':'H', # H or V - horizontal or vertical + 'effectmotion':'I', # Inwards or Outwards + 'effectduration':'1', #seconds, + 'outlineentry':'None', + 'outlinelevel':'0' # 1 is a child, 2 is a grandchild etc. + }, + 'para': { + 'style':'Normal', + 'bullettext':'', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'image': { + 'filename':'', + 'width':'None', + 'height':'None', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'table': { + 'widths':'None', + 'heights':'None', + 'fieldDelim':',', + 'rowDelim':'\n', + 'style':'None', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'rectangle': { + 'x':'0', + 'y':'0', + 'width':'100', + 'height':'100', + 'fill':'None', + 'stroke':'(0,0,0)', + 'linewidth':'0', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'roundrect': { + 'x':'0', + 'y':'0', + 'width':'100', + 'height':'100', + 'radius':'6', + 'fill':'None', + 'stroke':'(0,0,0)', + 'linewidth':'0', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'line': { + 'x1':'0', + 'y1':'0', + 'x2':'100', + 'y2':'100', + 'stroke':'(0,0,0)', + 'width':'0', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'ellipse': { + 'x1':'0', + 'y1':'0', + 'x2':'100', + 'y2':'100', + 'stroke':'(0,0,0)', + 'fill':'None', + 'linewidth':'0', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'polygon': { + 'points':'(0,0),(50,0),(25,25)', + 'stroke':'(0,0,0)', + 'linewidth':'0', + 'stroke':'(0,0,0)', + 'fill':'None', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'string':{ + 'x':'0', + 'y':'0', + 'color':'(0,0,0)', + 'font':'Times-Roman', + 'size':'12', + 'align':'left', + 'effectname':'None', + 'effectdirection':'0', + 'effectdimension':'H', + 'effectmotion':'I', + 'effectduration':'1' + }, + 'customshape':{ + 'path':'None', + 'module':'None', + 'class':'None', + 'initargs':'None' + } + } + + def __init__(self): + self.presentations = [] + #yes, I know a generic stack would be easier... + #still, testing if we are 'in' something gives + #a degree of validation. + self._curPres = None + self._curSection = None + self._curSlide = None + self._curFrame = None + self._curPara = None #the only places we are interested in + self._curPrefmt = None + self._curPyCode = None + self._curString = None + self._curTable = None + self._curTitle = None + self._curAuthor = None + self._curSubject = None + self.fx = 1 + xmllib.XMLParser.__init__(self) + + def _arg(self,tag,args,name): + "What's this for???" + if args.has_key(name): + v = args[name] + else: + if self.attributes.has_key(tag): + v = self.attributes[tag][name] + else: + v = None + return v + + def ceval(self,tag,args,name): + if args.has_key(name): + v = args[name] + else: + if self.attributes.has_key(tag): + v = self.attributes[tag][name] + else: + return None + + # handle named colors (names from reportlab.lib.colors) + if name in ('color', 'stroke', 'fill'): + v = str(pythonpoint.checkColor(v)) + + return eval(v) + + def getPresentation(self): + return self._curPres + + + def handle_data(self, data): + #the only data should be paragraph text, preformatted para + #text, 'string text' for a fixed string on the page, + #or table data + + if self._curPara: + self._curPara.rawtext = self._curPara.rawtext + data + elif self._curPrefmt: + self._curPrefmt.rawtext = self._curPrefmt.rawtext + data + elif self._curPyCode: + self._curPyCode.rawtext = self._curPyCode.rawtext + data + elif self._curString: + self._curString.text = self._curString.text + data + elif self._curTable: + self._curTable.rawBlocks.append(data) + elif self._curTitle <> None: # need to allow empty strings, + # hence explicitly testing for None + self._curTitle = self._curTitle + data + elif self._curAuthor <> None: + self._curAuthor = self._curAuthor + data + elif self._curSubject <> None: + self._curSubject = self._curSubject + data + + def handle_cdata(self, data): + #just append to current paragraph text, so we can quote XML + if self._curPara: + self._curPara.rawtext = self._curPara.rawtext + data + elif self._curPrefmt: + self._curPrefmt.rawtext = self._curPrefmt.rawtext + data + elif self._curPyCode: + self._curPyCode.rawtext = self._curPyCode.rawtext + data + elif self._curString: + self._curString.text = self._curString.text + data + elif self._curTable: + self._curTable.rawBlocks.append(data) + elif self._curAuthor <> None: + self._curAuthor = self._curAuthor + data + elif self._curSubject <> None: + self._curSubject = self._curSubject + data + + def start_presentation(self, args): + self._curPres = pythonpoint.PPPresentation() + self._curPres.filename = self._arg('presentation',args,'filename') + self._curPres.effectName = self._arg('presentation',args,'effect') + self._curPres.pageDuration = self._arg('presentation',args,'pageDuration') + + h = self._arg('presentation',args,'pageHeight') + if h: + self._curPres.pageHeight = h + w = self._arg('presentation',args,'pageWidth') + if w: + self._curPres.pageWidth = w + #print 'page size =', self._curPres.pageSize + + def end_presentation(self): + pass +## print 'Fully parsed presentation',self._curPres.filename + + def start_title(self, args): + self._curTitle = '' + + + def end_title(self): + self._curPres.title = self._curTitle + self._curTitle = None + + def start_author(self, args): + self._curAuthor = '' + + def end_author(self): + self._curPres.author = self._curAuthor + self._curAuthor = None + + def start_subject(self, args): + self._curSubject = '' + + def end_subject(self): + self._curPres.subject = self._curSubject + self._curSubject = None + + def start_stylesheet(self, args): + #makes it the current style sheet. + path = self._arg('stylesheet',args,'path') + if path=='None': path = [] + if type(path) not in SeqTypes: path = [path] + path.append('styles') + path.append(os.getcwd()) + modulename = self._arg('stylesheet', args, 'module') + funcname = self._arg('stylesheet', args, 'function') + try: + found = imp.find_module(modulename, path) + (file, pathname, description) = found + mod = imp.load_module(modulename, file, pathname, description) + except ImportError: + #last gasp + mod = getModule(modulename) + + #now get the function + func = getattr(mod, funcname) + pythonpoint.setStyles(func()) +## print 'set global stylesheet to %s.%s()' % (modulename, funcname) + + def end_stylesheet(self): + pass + + def start_section(self, args): + name = self._arg('section',args,'name') + self._curSection = pythonpoint.PPSection(name) + + def end_section(self): + self._curSection = None + + + def start_slide(self, args): + s = pythonpoint.PPSlide() + s.id = self._arg('slide',args,'id') + s.title = self._arg('slide',args,'title') + a = self._arg('slide',args,'effectname') + if a <> 'None': + s.effectName = a + s.effectDirection = self.ceval('slide',args,'effectdirection') + s.effectDimension = self._arg('slide',args,'effectdimension') + s.effectDuration = self.ceval('slide',args,'effectduration') + s.effectMotion = self._arg('slide',args,'effectmotion') + + #HACK - may not belong here in the long run... + #by default, use the slide title for the outline entry, + #unless it is specified as an arg. + a = self._arg('slide',args,'outlineentry') + if a == "Hide": + s.outlineEntry = None + elif a <> 'None': + s.outlineEntry = a + else: + s.outlineEntry = s.title + + s.outlineLevel = self.ceval('slide',args,'outlinelevel') + + #let it know its section, which may be none + s.section = self._curSection + self._curSlide = s + + def end_slide(self): + self._curPres.slides.append(self._curSlide) + self._curSlide = None + + def start_frame(self, args): + self._curFrame = pythonpoint.PPFrame( + self.ceval('frame',args,'x'), + self.ceval('frame',args,'y'), + self.ceval('frame',args,'width'), + self.ceval('frame',args,'height') + ) + if self._arg('frame',args,'border')=='true': + self._curFrame.showBoundary = 1 + + def end_frame(self): + self._curSlide.frames.append(self._curFrame) + self._curFrame = None + + def start_notes(self, args): + name = self._arg('notes',args,'name') + self._curNotes = pythonpoint.PPNotes() + + def end_notes(self): + self._curSlide.notes.append(self._curNotes) + self._curNotes = None + + def start_registerFont(self, args): + name = self._arg('font',args,'name') + path = self._arg('font',args,'path') + pythonpoint.registerFont0(self.sourceFilename, name, path) + + + def end_registerFont(self): + pass + + + def pack_slide(self, element, args): + if self.fx: + effectName = self._arg(element,args,'effectname') + if effectName <> 'None': + curSlide = copy.deepcopy(self._curSlide) + if self._curFrame: + curFrame = copy.deepcopy(self._curFrame) + curSlide.frames.append(curFrame) + self._curPres.slides.append(curSlide) + self._curSlide.effectName = effectName + self._curSlide.effectDirection = self.ceval(element,args,'effectdirection') + self._curSlide.effectDimension = self._arg(element,args,'effectdimension') + self._curSlide.effectDuration = self.ceval(element,args,'effectduration') + self._curSlide.effectMotion = self._arg(element,args,'effectmotion') + self._curSlide.outlineEntry = None + + def start_para(self, args): + self.pack_slide('para', args) + self._curPara = pythonpoint.PPPara() + self._curPara.style = self._arg('para',args,'style') + + # hack - bullet character if bullet style + bt = self._arg('para',args,'bullettext') + if bt == '': + if self._curPara.style == 'Bullet': + bt = '\xc2\xb7' # Symbol Font bullet character, reasonable default + elif self._curPara.style == 'Bullet2': + bt = '\xc2\xb7' # second-level bullet + else: + bt = None + + self._curPara.bulletText = bt + + def end_para(self): + if self._curFrame: + self._curFrame.content.append(self._curPara) + self._curPara = None + elif self._curNotes: + self._curNotes.content.append(self._curPara) + self._curPara = None + + + def start_prefmt(self, args): + self._curPrefmt = pythonpoint.PPPreformattedText() + self._curPrefmt.style = self._arg('prefmt',args,'style') + + + def end_prefmt(self): + self._curFrame.content.append(self._curPrefmt) + self._curPrefmt = None + + + def start_pycode(self, args): + self._curPyCode = pythonpoint.PPPythonCode() + self._curPyCode.style = self._arg('pycode',args,'style') + + + def end_pycode(self): + self._curFrame.content.append(self._curPyCode) + self._curPyCode = None + + + def start_image(self, args): + self.pack_slide('image',args) + sourceFilename = self.sourceFilename # XXX + filename = self._arg('image',args,'filename') + filename = os.path.join(os.path.dirname(sourceFilename), filename) + self._curImage = pythonpoint.PPImage() + self._curImage.filename = filename + self._curImage.width = self.ceval('image',args,'width') + self._curImage.height = self.ceval('image',args,'height') + + + def end_image(self): + self._curFrame.content.append(self._curImage) + self._curImage = None + + + def start_table(self, args): + self.pack_slide('table',args) + self._curTable = pythonpoint.PPTable() + self._curTable.widths = self.ceval('table',args,'widths') + self._curTable.heights = self.ceval('table',args,'heights') + #these may contain escapes like tabs - handle with + #a bit more care. + if args.has_key('fieldDelim'): + self._curTable.fieldDelim = eval('"' + args['fieldDelim'] + '"') + if args.has_key('rowDelim'): + self._curTable.rowDelim = eval('"' + args['rowDelim'] + '"') + if args.has_key('style'): + self._curTable.style = args['style'] + + + def end_table(self): + self._curFrame.content.append(self._curTable) + self._curTable = None + + + def start_spacer(self, args): + """No contents so deal with it here.""" + sp = pythonpoint.PPSpacer() + sp.height = eval(args['height']) + self._curFrame.content.append(sp) + + + def end_spacer(self): + pass + + + ## the graphics objects - go into either the current section + ## or the current slide. + def start_fixedimage(self, args): + sourceFilename = self.sourceFilename + filename = self._arg('image',args,'filename') + filename = os.path.join(os.path.dirname(sourceFilename), filename) + img = pythonpoint.PPFixedImage() + img.filename = filename + img.x = self.ceval('fixedimage',args,'x') + img.y = self.ceval('fixedimage',args,'y') + img.width = self.ceval('fixedimage',args,'width') + img.height = self.ceval('fixedimage',args,'height') + self._curFixedImage = img + + + def end_fixedimage(self): + if self._curSlide: + self._curSlide.graphics.append(self._curFixedImage) + elif self._curSection: + self._curSection.graphics.append(self._curFixedImage) + self._curFixedImage = None + + + def start_rectangle(self, args): + self.pack_slide('rectangle', args) + rect = pythonpoint.PPRectangle( + self.ceval('rectangle',args,'x'), + self.ceval('rectangle',args,'y'), + self.ceval('rectangle',args,'width'), + self.ceval('rectangle',args,'height') + ) + rect.fillColor = self.ceval('rectangle',args,'fill') + rect.strokeColor = self.ceval('rectangle',args,'stroke') + self._curRectangle = rect + + + def end_rectangle(self): + if self._curSlide: + self._curSlide.graphics.append(self._curRectangle) + elif self._curSection: + self._curSection.graphics.append(self._curRectangle) + self._curRectangle = None + + + def start_roundrect(self, args): + self.pack_slide('roundrect', args) + rrect = pythonpoint.PPRoundRect( + self.ceval('roundrect',args,'x'), + self.ceval('roundrect',args,'y'), + self.ceval('roundrect',args,'width'), + self.ceval('roundrect',args,'height'), + self.ceval('roundrect',args,'radius') + ) + rrect.fillColor = self.ceval('roundrect',args,'fill') + rrect.strokeColor = self.ceval('roundrect',args,'stroke') + self._curRoundRect = rrect + + + def end_roundrect(self): + if self._curSlide: + self._curSlide.graphics.append(self._curRoundRect) + elif self._curSection: + self._curSection.graphics.append(self._curRoundRect) + self._curRoundRect = None + + + def start_line(self, args): + self.pack_slide('line', args) + self._curLine = pythonpoint.PPLine( + self.ceval('line',args,'x1'), + self.ceval('line',args,'y1'), + self.ceval('line',args,'x2'), + self.ceval('line',args,'y2') + ) + self._curLine.strokeColor = self.ceval('line',args,'stroke') + + + def end_line(self): + if self._curSlide: + self._curSlide.graphics.append(self._curLine) + elif self._curSection: + self._curSection.graphics.append(self._curLine) + self._curLine = None + + + def start_ellipse(self, args): + self.pack_slide('ellipse', args) + self._curEllipse = pythonpoint.PPEllipse( + self.ceval('ellipse',args,'x1'), + self.ceval('ellipse',args,'y1'), + self.ceval('ellipse',args,'x2'), + self.ceval('ellipse',args,'y2') + ) + self._curEllipse.strokeColor = self.ceval('ellipse',args,'stroke') + self._curEllipse.fillColor = self.ceval('ellipse',args,'fill') + + + def end_ellipse(self): + if self._curSlide: + self._curSlide.graphics.append(self._curEllipse) + elif self._curSection: + self._curSection.graphics.append(self._curEllipse) + self._curEllipse = None + + + def start_polygon(self, args): + self.pack_slide('polygon', args) + self._curPolygon = pythonpoint.PPPolygon(self.ceval('polygon',args,'points')) + self._curPolygon.strokeColor = self.ceval('polygon',args,'stroke') + self._curPolygon.fillColor = self.ceval('polygon',args,'fill') + + + def end_polygon(self): + if self._curSlide: + self._curSlide.graphics.append(self._curPolygon) + elif self._curSection: + self._curSection.graphics.append(self._curPolygon) + self._curEllipse = None + + + def start_string(self, args): + self.pack_slide('string', args) + self._curString = pythonpoint.PPString( + self.ceval('string',args,'x'), + self.ceval('string',args,'y') + ) + self._curString.color = self.ceval('string',args,'color') + self._curString.font = self._arg('string',args,'font') + self._curString.size = self.ceval('string',args,'size') + if args['align'] == 'left': + self._curString.align = TA_LEFT + elif args['align'] == 'center': + self._curString.align = TA_CENTER + elif args['align'] == 'right': + self._curString.align = TA_RIGHT + elif args['align'] == 'justify': + self._curString.align = TA_JUSTIFY + #text comes later within the tag + + + def end_string(self): + #controller should have set the text + if self._curSlide: + self._curSlide.graphics.append(self._curString) + elif self._curSection: + self._curSection.graphics.append(self._curString) + self._curString = None + + + def start_infostring(self, args): + # like a string, but lets them embed page no, author etc. + self.start_string(args) + self._curString.hasInfo = 1 + + + def end_infostring(self): + self.end_string() + + + def start_customshape(self, args): + #loads one + path = self._arg('customshape',args,'path') + if path=='None': + path = [] + else: + path=[path] + + # add package root folder and input file's folder to path + path.append(os.path.dirname(self.sourceFilename)) + path.append(os.path.dirname(pythonpoint.__file__)) + + modulename = self._arg('customshape',args,'module') + funcname = self._arg('customshape',args,'class') + try: + found = imp.find_module(modulename, path) + (file, pathname, description) = found + mod = imp.load_module(modulename, file, pathname, description) + except ImportError: + mod = getModule(modulename) + + #now get the function + + func = getattr(mod, funcname) + initargs = self.ceval('customshape',args,'initargs') + self._curCustomShape = apply(func, initargs) + + def end_customshape(self): + if self._curSlide: + self._curSlide.graphics.append(self._curCustomShape) + elif self._curSection: + self._curSection.graphics.append(self._curCustomShape) + self._curCustomShape = None + + def start_drawing(self, args): + #loads one + moduleName = args["module"] + funcName = args["constructor"] + showBoundary = int(args.get("showBoundary", "0")) + hAlign = args.get("hAlign", "CENTER") + + + # the path for the imports should include: + # 1. document directory + # 2. python path if baseDir not given, or + # 3. baseDir if given + try: + dirName = sdict["baseDir"] + except: + dirName = None + importPath = [os.getcwd()] + if dirName is None: + importPath.extend(sys.path) + else: + importPath.insert(0, dirName) + + modul = recursiveImport(moduleName, baseDir=importPath) + func = getattr(modul, funcName) + drawing = func() + + drawing.hAlign = hAlign + if showBoundary: + drawing._showBoundary = 1 + + self._curDrawing = pythonpoint.PPDrawing() + self._curDrawing.drawing = drawing + + def end_drawing(self): + self._curFrame.content.append(self._curDrawing) + self._curDrawing = None + + def start_pageCatcherFigure(self, args): + filename = args["filename"] + pageNo = int(args["pageNo"]) + width = float(args.get("width", "595")) + height = float(args.get("height", "842")) + + + fig = figures.PageCatcherFigureNonA4(filename, pageNo, args.get("caption", ""), width, height) + sf = args.get('scaleFactor', None) + if sf: sf = float(sf) + border = not (args.get('border', None) in ['0','no']) + + fig.scaleFactor = sf + fig.border = border + + #self.ceval('pageCatcherFigure',args,'scaleFactor'), + #initargs = self.ceval('customshape',args,'initargs') + self._curFigure = pythonpoint.PPFigure() + self._curFigure.figure = fig + + def end_pageCatcherFigure(self): + self._curFrame.content.append(self._curFigure) + self._curFigure = None + + ## intra-paragraph XML should be allowed through into PLATYPUS + def unknown_starttag(self, tag, attrs): + if self._curPara: + echo = '<%s' % tag + for (key, value) in attrs.items(): + echo = echo + ' %s="%s"' % (key, value) + echo = echo + '>' + self._curPara.rawtext = self._curPara.rawtext + echo + else: + print 'Unknown start tag %s' % tag + + + def unknown_endtag(self, tag): + if self._curPara: + self._curPara.rawtext = self._curPara.rawtext + ''% tag + else: + print 'Unknown end tag %s' % tag + + def handle_charref(self, name): + try: + if name[0]=='x': + n = int(name[1:],16) + else: + n = int(name) + except ValueError: + self.unknown_charref(name) + return + self.handle_data(unichr(n).encode('utf8')) diff --git a/bin/reportlab/tools/pythonpoint/styles/__init__.py b/bin/reportlab/tools/pythonpoint/styles/__init__.py new file mode 100644 index 00000000000..e67468fd9ee --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/styles/__init__.py @@ -0,0 +1,3 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/pythonpoint/styles/__init__.py diff --git a/bin/reportlab/tools/pythonpoint/styles/horrible.py b/bin/reportlab/tools/pythonpoint/styles/horrible.py new file mode 100644 index 00000000000..fa6d501b043 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/styles/horrible.py @@ -0,0 +1,101 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/pythonpoint/styles/horrible.py +__version__=''' $Id: horrible.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +# style_modern.py +__doc__="""This is an example style sheet. You can create your own, and +have them loaded by the presentation. A style sheet is just a +dictionary, where they keys are style names and the values are +ParagraphStyle objects. + +You must provide a function called "getParagraphStyles()" to +return it. In future, we can put things like LineStyles, +TableCellStyles etc. in the same modules. + +You might wish to have two parallel style sheets, one for colour +and one for black and white, so you can switch your presentations +easily. + +A style sheet MUST define a style called 'Normal'. +""" + +from reportlab.lib import styles, enums +def getParagraphStyles(): + """Returns a dictionary of styles based on Helvetica""" + stylesheet = {} + + para = styles.ParagraphStyle('Normal', None) #the ancestor of all + para.fontName = 'Courier' + para.fontSize = 24 + para.leading = 28 + stylesheet['Normal'] = para + + para = ParagraphStyle('BodyText', stylesheet['Normal']) + para.spaceBefore = 12 + stylesheet['BodyText'] = para + + para = ParagraphStyle('BigCentered', stylesheet['Normal']) + para.spaceBefore = 12 + para.alignment = enums.TA_CENTER + stylesheet['BigCentered'] = para + + para = ParagraphStyle('Italic', stylesheet['BodyText']) + para.fontName = 'Courier-Oblique' + stylesheet['Italic'] = para + + para = ParagraphStyle('Title', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 48 + para.Leading = 58 + para.spaceAfter = 36 + para.alignment = enums.TA_CENTER + stylesheet['Title'] = para + + para = ParagraphStyle('Heading1', stylesheet['Normal']) + para.fontName = 'Courier-Bold' + para.fontSize = 36 + para.leading = 44 + para.spaceAfter = 36 + para.alignment = enums.TA_CENTER + stylesheet['Heading1'] = para + + para = ParagraphStyle('Heading2', stylesheet['Normal']) + para.fontName = 'Courier-Bold' + para.fontSize = 28 + para.leading = 34 + para.spaceBefore = 24 + para.spaceAfter = 12 + stylesheet['Heading2'] = para + + para = ParagraphStyle('Heading3', stylesheet['Normal']) + para.fontName = 'Courier-BoldOblique' + para.spaceBefore = 24 + para.spaceAfter = 12 + stylesheet['Heading3'] = para + + para = ParagraphStyle('Bullet', stylesheet['Normal']) + para.firstLineIndent = -18 + para.leftIndent = 72 + para.spaceBefore = 6 + #para.bulletFontName = 'Symbol' + para.bulletFontSize = 24 + para.bulletIndent = 36 + stylesheet['Bullet'] = para + + para = ParagraphStyle('Definition', stylesheet['Normal']) + #use this for definition lists + para.firstLineIndent = 0 + para.leftIndent = 72 + para.bulletIndent = 0 + para.spaceBefore = 12 + para.bulletFontName = 'Couruer-BoldOblique' + stylesheet['Definition'] = para + + para = ParagraphStyle('Code', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + stylesheet['Code'] = para + + return stylesheet \ No newline at end of file diff --git a/bin/reportlab/tools/pythonpoint/styles/htu.py b/bin/reportlab/tools/pythonpoint/styles/htu.py new file mode 100644 index 00000000000..bc79a7ba68e --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/styles/htu.py @@ -0,0 +1,158 @@ +from reportlab.lib import styles +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY +from reportlab.platypus import Preformatted, Paragraph, Frame, \ + Image, Table, TableStyle, Spacer +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont + + +def getParagraphStyles(): + """Returns a dictionary of styles to get you started. + + We will provide a way to specify a module of these. Note that + this just includes TableStyles as well as ParagraphStyles for any + tables you wish to use. + """ + + pdfmetrics.registerFont(TTFont('Verdana','verdana.ttf')) + pdfmetrics.registerFont(TTFont('Verdana-Bold','verdanab.ttf')) + pdfmetrics.registerFont(TTFont('Verdana-Italic','verdanai.ttf')) + pdfmetrics.registerFont(TTFont('Verdana-BoldItalic','verdanaz.ttf')) + pdfmetrics.registerFont(TTFont('Arial Narrow','arialn.ttf')) + pdfmetrics.registerFont(TTFont('Arial Narrow-Bold','arialnb.ttf')) + pdfmetrics.registerFont(TTFont('Arial Narrow-Italic','arialni.ttf')) + pdfmetrics.registerFont(TTFont('Arial Narrow-BoldItalic','arialnbi.ttf')) + + stylesheet = {} + ParagraphStyle = styles.ParagraphStyle + + para = ParagraphStyle('Normal', None) #the ancestor of all + para.fontName = 'Verdana' + para.fontSize = 28 + para.leading = 32 + para.spaceAfter = 6 + stylesheet['Normal'] = para + + #This one is spaced out a bit... + para = ParagraphStyle('BodyText', stylesheet['Normal']) + para.spaceBefore = 12 + stylesheet['BodyText'] = para + + #Indented, for lists + para = ParagraphStyle('Indent', stylesheet['Normal']) + para.leftIndent = 60 + para.firstLineIndent = 0 + stylesheet['Indent'] = para + + para = ParagraphStyle('Centered', stylesheet['Normal']) + para.alignment = TA_CENTER + stylesheet['Centered'] = para + + para = ParagraphStyle('BigCentered', stylesheet['Normal']) + para.fontSize = 32 + para.alignment = TA_CENTER + para.spaceBefore = 12 + para.spaceAfter = 12 + stylesheet['BigCentered'] = para + + para = ParagraphStyle('Italic', stylesheet['BodyText']) + para.fontName = 'Verdana-Italic' + stylesheet['Italic'] = para + + para = ParagraphStyle('Title', stylesheet['Normal']) + para.fontName = 'Arial Narrow-Bold' + para.fontSize = 48 + para.leading = 58 + para.alignment = TA_CENTER + stylesheet['Title'] = para + + para = ParagraphStyle('Heading1', stylesheet['Normal']) + para.fontName = 'Arial Narrow-Bold' + para.fontSize = 40 + para.leading = 44 + para.alignment = TA_CENTER + stylesheet['Heading1'] = para + + para = ParagraphStyle('Heading2', stylesheet['Normal']) + para.fontName = 'Verdana' + para.fontSize = 32 + para.leading = 36 + para.spaceBefore = 32 + para.spaceAfter = 12 + stylesheet['Heading2'] = para + + para = ParagraphStyle('Heading3', stylesheet['Normal']) + para.fontName = 'Verdana' + para.spaceBefore = 20 + para.spaceAfter = 6 + stylesheet['Heading3'] = para + + para = ParagraphStyle('Heading4', stylesheet['Normal']) + para.fontName = 'Verdana-BoldItalic' + para.spaceBefore = 6 + stylesheet['Heading4'] = para + + para = ParagraphStyle('Bullet', stylesheet['Normal']) + para.firstLineIndent = 0 + para.leftIndent = 56 + para.spaceBefore = 6 + para.bulletFontName = 'Symbol' + para.bulletFontSize = 24 + para.bulletIndent = 20 + stylesheet['Bullet'] = para + + para = ParagraphStyle('Bullet2', stylesheet['Normal']) + para.firstLineIndent = 0 + para.leftIndent = 80 + para.spaceBefore = 6 + para.fontSize = 24 + para.bulletFontName = 'Symbol' + para.bulletFontSize = 20 + para.bulletIndent = 60 + stylesheet['Bullet2'] = para + + para = ParagraphStyle('Definition', stylesheet['Normal']) + #use this for definition lists + para.firstLineIndent = 0 + para.leftIndent = 60 + para.bulletIndent = 0 + para.bulletFontName = 'Verdana-BoldItalic' + para.bulletFontSize = 24 + stylesheet['Definition'] = para + + para = ParagraphStyle('Code', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + stylesheet['Code'] = para + + para = ParagraphStyle('PythonCode', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + stylesheet['Code'] = para + + para = ParagraphStyle('Small', stylesheet['Normal']) + para.fontSize = 12 + para.leading = 14 + stylesheet['Small'] = para + + #now for a table + ts = TableStyle([ + ('FONT', (0,0), (-1,-1), 'Arial Narrow', 22), + ('LINEABOVE', (0,1), (-1,1), 2, colors.green), + ('LINEABOVE', (0,2), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), + ('LINEBEFORE', (0,1), (-1,-1), 2, colors.black), + ('LINEAFTER', (0,1), (-1,-1), 2, colors.black), + ('ALIGN', (4,1), (-1,-1), 'RIGHT'), #all numeric cells right aligned + ('TEXTCOLOR', (0,2), (0,-1), colors.black), + ('BACKGROUND', (0,1), (-1,1), colors.Color(0,0.7,0.7)) + ]) + stylesheet['table1'] = ts + + return stylesheet diff --git a/bin/reportlab/tools/pythonpoint/styles/modern.py b/bin/reportlab/tools/pythonpoint/styles/modern.py new file mode 100644 index 00000000000..97583b57857 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/styles/modern.py @@ -0,0 +1,120 @@ +#Copyright ReportLab Europe Ltd. 2000-2004 +#see license.txt for license details +#history http://www.reportlab.co.uk/cgi-bin/viewcvs.cgi/public/reportlab/trunk/reportlab/tools/pythonpoint/styles/modern.py +__version__=''' $Id: modern.py 2385 2004-06-17 15:26:05Z rgbecker $ ''' +# style_modern.py +__doc__="""This is an example style sheet. You can create your own, and +have them loaded by the presentation. A style sheet is just a +dictionary, where they keys are style names and the values are +ParagraphStyle objects. + +You must provide a function called "getParagraphStyles()" to +return it. In future, we can put things like LineStyles, +TableCellStyles etc. in the same modules. + +You might wish to have two parallel style sheets, one for colour +and one for black and white, so you can switch your presentations +easily. + +A style sheet MUST define a style called 'Normal'. +""" + +from reportlab.lib import styles +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY + +def getParagraphStyles(): + """Returns a dictionary of styles based on Helvetica""" + stylesheet = {} + ParagraphStyle = styles.ParagraphStyle + + para = ParagraphStyle('Normal', None) #the ancestor of all + para.fontName = 'Helvetica' + para.fontSize = 24 + para.leading = 28 + stylesheet['Normal'] = para + + para = ParagraphStyle('BodyText', stylesheet['Normal']) + para.spaceBefore = 12 + stylesheet['BodyText'] = para + + para = ParagraphStyle('Indent', stylesheet['Normal']) + para.leftIndent = 36 + para.firstLineIndent = 0 + stylesheet['Indent'] = para + + para = ParagraphStyle('Centered', stylesheet['Normal']) + para.alignment = TA_CENTER + stylesheet['Centered'] = para + + para = ParagraphStyle('BigCentered', stylesheet['Normal']) + para.spaceBefore = 12 + para.alignment = TA_CENTER + stylesheet['BigCentered'] = para + + para = ParagraphStyle('Italic', stylesheet['BodyText']) + para.fontName = 'Helvetica-Oblique' + stylesheet['Italic'] = para + + para = ParagraphStyle('Title', stylesheet['Normal']) + para.fontName = 'Helvetica' + para.fontSize = 48 + para.Leading = 58 + para.spaceAfter = 36 + para.alignment = TA_CENTER + stylesheet['Title'] = para + + para = ParagraphStyle('Heading1', stylesheet['Normal']) + para.fontName = 'Helvetica-Bold' + para.fontSize = 36 + para.leading = 44 + para.spaceAfter = 36 + para.alignment = TA_CENTER + stylesheet['Heading1'] = para + + para = ParagraphStyle('Heading2', stylesheet['Normal']) + para.fontName = 'Helvetica-Bold' + para.fontSize = 28 + para.leading = 34 + para.spaceBefore = 24 + para.spaceAfter = 12 + stylesheet['Heading2'] = para + + para = ParagraphStyle('Heading3', stylesheet['Normal']) + para.fontName = 'Helvetica-BoldOblique' + para.spaceBefore = 24 + para.spaceAfter = 12 + stylesheet['Heading3'] = para + + para = ParagraphStyle('Bullet', stylesheet['Normal']) + para.firstLineIndent = -18 + para.leftIndent = 72 + para.spaceBefore = 6 + para.bulletFontName = 'Symbol' + para.bulletFontSize = 24 + para.bulletIndent = 36 + stylesheet['Bullet'] = para + + para = ParagraphStyle('Bullet2', stylesheet['Bullet']) + para.firstLineIndent = 0 + para.bulletIndent = 72 + para.leftIndent = 108 + stylesheet['Bullet2'] = para + + + para = ParagraphStyle('Definition', stylesheet['Normal']) + #use this for definition lists + para.firstLineIndent = 0 + para.leftIndent = 72 + para.bulletIndent = 0 + para.spaceBefore = 12 + para.bulletFontName = 'Helvetica-BoldOblique' + stylesheet['Definition'] = para + + para = ParagraphStyle('Code', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + stylesheet['Code'] = para + + return stylesheet \ No newline at end of file diff --git a/bin/reportlab/tools/pythonpoint/styles/projection.py b/bin/reportlab/tools/pythonpoint/styles/projection.py new file mode 100644 index 00000000000..a3c818556d0 --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/styles/projection.py @@ -0,0 +1,106 @@ +"""This is an example style sheet. You can create your own, and +have them loaded by the presentation. A style sheet is just a +dictionary, where they keys are style names and the values are +ParagraphStyle objects. + +You must provide a function called "getParagraphStyles()" to +return it. In future, we can put things like LineStyles, +TableCellStyles etc. in the same modules. + +You might wish to have two parallel style sheets, one for colour +and one for black and white, so you can switch your presentations +easily. + +A style sheet MUST define a style called 'Normal'. +""" + +from reportlab.lib import styles +from reportlab.lib.colors import * +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY + + +def getParagraphStyles(): + """Returns a dictionary of styles based on Helvetica""" + + stylesheet = {} + ParagraphStyle = styles.ParagraphStyle + + para = ParagraphStyle('Normal', None) #the ancestor of all + para.fontName = 'Helvetica-Bold' + para.fontSize = 24 + para.leading = 28 + para.textColor = white + stylesheet['Normal'] = para + + para = ParagraphStyle('BodyText', stylesheet['Normal']) + para.spaceBefore = 12 + stylesheet['BodyText'] = para + + para = ParagraphStyle('BigCentered', stylesheet['Normal']) + para.spaceBefore = 12 + para.alignment = TA_CENTER + stylesheet['BigCentered'] = para + + para = ParagraphStyle('Italic', stylesheet['BodyText']) + para.fontName = 'Helvetica-Oblique' + para.textColor = white + stylesheet['Italic'] = para + + para = ParagraphStyle('Title', stylesheet['Normal']) + para.fontName = 'Helvetica' + para.fontSize = 48 + para.Leading = 58 + para.spaceAfter = 36 + para.alignment = TA_CENTER + stylesheet['Title'] = para + + para = ParagraphStyle('Heading1', stylesheet['Normal']) + para.fontName = 'Helvetica-Bold' + para.fontSize = 48# 36 + para.leading = 44 + para.spaceAfter = 36 + para.textColor = green + para.alignment = TA_LEFT + stylesheet['Heading1'] = para + + para = ParagraphStyle('Heading2', stylesheet['Normal']) + para.fontName = 'Helvetica-Bold' + para.fontSize = 28 + para.leading = 34 + para.spaceBefore = 24 + para.spaceAfter = 12 + stylesheet['Heading2'] = para + + para = ParagraphStyle('Heading3', stylesheet['Normal']) + para.fontName = 'Helvetica-BoldOblique' + para.spaceBefore = 24 + para.spaceAfter = 12 + stylesheet['Heading3'] = para + + para = ParagraphStyle('Bullet', stylesheet['Normal']) + para.firstLineIndent = -18 + para.leftIndent = 72 + para.spaceBefore = 6 + para.bulletFontName = 'Symbol' + para.bulletFontSize = 24 + para.bulletIndent = 36 + stylesheet['Bullet'] = para + + para = ParagraphStyle('Definition', stylesheet['Normal']) + #use this for definition lists + para.firstLineIndent = 0 + para.leftIndent = 72 + para.bulletIndent = 0 + para.spaceBefore = 12 + para.bulletFontName = 'Helvetica-BoldOblique' + stylesheet['Definition'] = para + + para = ParagraphStyle('Code', stylesheet['Normal']) + para.fontName = 'Courier-Bold' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + para.textColor = chartreuse + stylesheet['Code'] = para + + return stylesheet \ No newline at end of file diff --git a/bin/reportlab/tools/pythonpoint/styles/standard.py b/bin/reportlab/tools/pythonpoint/styles/standard.py new file mode 100644 index 00000000000..c525964585d --- /dev/null +++ b/bin/reportlab/tools/pythonpoint/styles/standard.py @@ -0,0 +1,132 @@ +from reportlab.lib import styles +from reportlab.lib import colors +from reportlab.lib.units import cm +from reportlab.lib.enums import TA_LEFT, TA_CENTER, TA_RIGHT, TA_JUSTIFY +from reportlab.platypus import Preformatted, Paragraph, Frame, \ + Image, Table, TableStyle, Spacer + + +def getParagraphStyles(): + """Returns a dictionary of styles to get you started. + + We will provide a way to specify a module of these. Note that + this just includes TableStyles as well as ParagraphStyles for any + tables you wish to use. + """ + + stylesheet = {} + ParagraphStyle = styles.ParagraphStyle + + para = ParagraphStyle('Normal', None) #the ancestor of all + para.fontName = 'Times-Roman' + para.fontSize = 24 + para.leading = 28 + stylesheet['Normal'] = para + + #This one is spaced out a bit... + para = ParagraphStyle('BodyText', stylesheet['Normal']) + para.spaceBefore = 12 + stylesheet['BodyText'] = para + + #Indented, for lists + para = ParagraphStyle('Indent', stylesheet['Normal']) + para.leftIndent = 36 + para.firstLineIndent = 0 + stylesheet['Indent'] = para + + para = ParagraphStyle('Centered', stylesheet['Normal']) + para.alignment = TA_CENTER + stylesheet['Centered'] = para + + para = ParagraphStyle('BigCentered', stylesheet['Normal']) + para.spaceBefore = 12 + para.alignment = TA_CENTER + stylesheet['BigCentered'] = para + + para = ParagraphStyle('Italic', stylesheet['BodyText']) + para.fontName = 'Times-Italic' + stylesheet['Italic'] = para + + para = ParagraphStyle('Title', stylesheet['Normal']) + para.fontName = 'Times-Roman' + para.fontSize = 48 + para.leading = 58 + para.alignment = TA_CENTER + stylesheet['Title'] = para + + para = ParagraphStyle('Heading1', stylesheet['Normal']) + para.fontName = 'Times-Bold' + para.fontSize = 36 + para.leading = 44 + para.alignment = TA_CENTER + stylesheet['Heading1'] = para + + para = ParagraphStyle('Heading2', stylesheet['Normal']) + para.fontName = 'Times-Bold' + para.fontSize = 28 + para.leading = 34 + para.spaceBefore = 24 + stylesheet['Heading2'] = para + + para = ParagraphStyle('Heading3', stylesheet['Normal']) + para.fontName = 'Times-BoldItalic' + para.spaceBefore = 24 + stylesheet['Heading3'] = para + + para = ParagraphStyle('Heading4', stylesheet['Normal']) + para.fontName = 'Times-BoldItalic' + para.spaceBefore = 6 + stylesheet['Heading4'] = para + + para = ParagraphStyle('Bullet', stylesheet['Normal']) + para.firstLineIndent = 0 + para.leftIndent = 56 + para.spaceBefore = 6 + para.bulletFontName = 'Symbol' + para.bulletFontSize = 24 + para.bulletIndent = 20 + stylesheet['Bullet'] = para + + para = ParagraphStyle('Definition', stylesheet['Normal']) + #use this for definition lists + para.firstLineIndent = 0 + para.leftIndent = 72 + para.bulletIndent = 0 + para.spaceBefore = 12 + para.bulletFontName = 'Helvetica-BoldOblique' + para.bulletFontSize = 24 + stylesheet['Definition'] = para + + para = ParagraphStyle('Code', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + stylesheet['Code'] = para + + para = ParagraphStyle('PythonCode', stylesheet['Normal']) + para.fontName = 'Courier' + para.fontSize = 16 + para.leading = 18 + para.leftIndent = 36 + stylesheet['PythonCode'] = para + + para = ParagraphStyle('Small', stylesheet['Normal']) + para.fontSize = 12 + para.leading = 14 + stylesheet['Small'] = para + + #now for a table + ts = TableStyle([ + ('FONT', (0,0), (-1,-1), 'Times-Roman', 24), + ('LINEABOVE', (0,0), (-1,0), 2, colors.green), + ('LINEABOVE', (0,1), (-1,-1), 0.25, colors.black), + ('LINEBELOW', (0,-1), (-1,-1), 2, colors.green), + ('LINEBEFORE', (-1,0), (-1,-1), 2, colors.black), + ('ALIGN', (1,1), (-1,-1), 'RIGHT'), #all numeric cells right aligned + ('TEXTCOLOR', (0,1), (0,-1), colors.red), + ('BACKGROUND', (0,0), (-1,0), colors.Color(0,0.7,0.7)) + ]) + stylesheet['table1'] = ts + + return stylesheet diff --git a/bin/reportlab/tools/utils/add_bleed.py b/bin/reportlab/tools/utils/add_bleed.py new file mode 100644 index 00000000000..ab4a7824628 --- /dev/null +++ b/bin/reportlab/tools/utils/add_bleed.py @@ -0,0 +1,28 @@ +#How to add bleed to a page in this case 6mm to a landscape A4 +from reportlab.lib import units, pagesizes +from reportlab.pdfgen.canvas import Canvas +import sys, os, glob, time +bleedX = 6*units.mm +bleedY = 6*units.mm +pageWidth, pageHeight = pagesizes.landscape(pagesizes.A4) +def process_pdf(c,infn,prefix='PageForms'): + from rlextra.pageCatcher import pageCatcher + names, data = pageCatcher.storeFormsInMemory(open(infn,'rb').read(),prefix=prefix,all=1) + names = pageCatcher.restoreFormsInMemory(data,c) + del data + for i in xrange(len(names)): + thisname = names[i] + c.saveState() + c.translate(bleedX,bleedY) + c.doForm(thisname) + c.restoreState() + c.showPage() + +def main(): + for infn in sys.argv[1:]: + outfn = 'bleeding_'+os.path.basename(infn) + c = Canvas(outfn,pagesize=(pageWidth+2*bleedX,pageHeight+2*bleedY)) + process_pdf(c,infn) + c.save() +if __name__=='__main__': + main() diff --git a/bin/reportlab/tools/utils/dumpttf.py b/bin/reportlab/tools/utils/dumpttf.py new file mode 100644 index 00000000000..8cb4610c5af --- /dev/null +++ b/bin/reportlab/tools/utils/dumpttf.py @@ -0,0 +1,60 @@ +__all__=('dumpttf',) +def dumpttf(fn,fontName=None, verbose=0): + '''dump out known glyphs from a ttf file''' + import os + if not os.path.isfile(fn): + raise IOError('No such file "%s"' % fn) + from reportlab.pdfbase.pdfmetrics import registerFont, stringWidth + from reportlab.pdfbase.ttfonts import TTFont + from reportlab.pdfgen.canvas import Canvas + if fontName is None: + fontName = os.path.splitext(os.path.basename(fn))[0] + dmpfn = '%s-ttf-dump.pdf' % fontName + ttf = TTFont(fontName, fn) + K = ttf.face.charToGlyph.keys() + registerFont(ttf) + c = Canvas(dmpfn) + W,H = c._pagesize + titleFontSize = 30 # title font size + titleFontName = 'Helvetica' + labelFontName = 'Courier' + fontSize = 10 + border = 36 + dx0 = stringWidth('12345: ', fontName, fontSize) + dx = dx0+20 + dy = 20 + K.sort() + y = 0 + page = 0 + for i, k in enumerate(K): + if yW-border: + x = border + y -= dy + c.showPage() + c.save() + if verbose: + print 'Font %s("%s") has %d glyphs\ndumped to "%s"' % (fontName,fn,len(K),dmpfn) + +if __name__=='__main__': + import sys, glob + if '--verbose' in sys.argv: + sys.argv.remove('--verbose') + verbose = 1 + else: + verbose = 0 + for a in sys.argv[1:]: + for fn in glob.glob(a): + dumpttf(fn, verbose=verbose)