diff --git a/CHANGELOG b/CHANGELOG index b28b148d..993ebe19 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,25 +2,28 @@ ### DAL Improvements -- MongoDB support in DAL (experimental) +- Support for DAL(lazy_tables=True) and db.define_table(on_define=lambda table:), thanks Jonathan +- MongoDB support in DAL (experimental), thanks Mark Breedveld - geodal and spatialite, thanks Denes and Fran (experimental) - db.mytable._before_insert, _after_insert, _before_update, _after_update, _before_delete. _after_delete (list of callbacks) - db(...).update_naive(...) same as update but ignores table._before_update and table._after_update - DAL BIGINT support and DAL(...,bigint_id=True) - IS_IN_DB(..., distinct=True) -- new syntax: db.mytable.insert(myuploadfield=open(....)) +- new syntax: db.mytable.insert(myuploadfield=open(....)), thank you Iceberg - db(...).select(db.mytable.myfield.count(distinct=True)) - db(db.a)._update(name=db(db.b.a==db.a.id).nested_select(db.b.id)) - db.mytable.myfield.filter_in, filter_out -- db.mytable._enable_record_versioning(db) adds verining to this table +- db.mytable._enable_record_versioning(db) adds versioning to this table - teradata adapter, thanks Andrew Willimott - experimental Sybase Adapter - added db.table.field.avg() -- Suport for Google App Engine projections +- Support for Google App Engine projections, thanks Christian +- Field(... 'upload', default=path) now accepts a path to a local file as default value, if user does not upload a file. Relative path looks inside current application folder, thanks Marin +- executesql(...,fields=,columns=) allows parsing of results in Rows, thanks Anthony ### Auth improvements -- auth.enable_record_versioning(db) adds full versining to all tables +- auth.enable_record_versioning(db) adds full versioning to all tables - @auth.requires_login(otherwise=URL(...)) - auth supports salt and compatible with third party data, thanks Dave Stoll - CRYPT now defaults to pbkdf2(1000,20,sha1) @@ -33,14 +36,14 @@ - FORM.confirm('Are you sure?',{'Back':URL(...)}) - SQLFORM.smartdictform(dict) - form.add_button(value,link) -- SQLFORM.grid(groupby=...') +- SQLFORM.grid(groupby='...') - fixed security issue with SQLFORM.grid and SQLFORM.smartgrid - more export options in SQLFORM.grid and SQLFORM.smartgrid (html, xml, csv, ...) ### Admin improvements - new admin pages: manage_students, bulk_regsiter, and progress reports -- increased secure admin against CSRF +- increased security in admin against CSRF - experimental Git integration - experimental OpenShift deployment - multi-language pluralization engine @@ -49,17 +52,32 @@ - Romanian translation for welcome, thanks ionel - support for mercurial 2.6, thanks Vlad -### Scheduler Inprovements +### Scheduler Improvements (thanks to niphlod, ykessler, dhx, toomim) - web2py.py -K myapp -X starts the myapp scheduler alongside the webserver +- tasks are marked EXPIRED (if stop_time passed) +- functions with no result don't end up in scheduler_run - more options: web2py.py -E -b -L -- scheduler can now handle 10k tasks with 20 concurrent workers and no known issues (thanks to niphlod, ykessler, dhx, toomim) -- tasks can be found in the environment (no need to define the tasks parameter) -- max_empty_runs kills the workers automatically if no new tasks are found in queue (nice for "spikes" of processing power), discard_results to completely discard the results (if you don't need the output of the task), utc_time enables datetime calculations with UTC time, task_name is no longer required (filled automatically with function_name if found empty), uuid makes easy to coordinate scheduler_task maintenance (filled automatically if not provided), stop_time has no default (previously was today+1), retry_failed to requeue automatically failed tasks, sync_output refreshes automatically the output (nice to report percentages) -- tasks can be DISABLED (put to sleep and do nothing if not sending the heartbeat every 30 seconds), TERMINATE (complete the current task and then die), KILL (kill ASAP), EXPIRED (if stop_time passed) +- scheduler can now handle 10k tasks with 20 concurrent workers and with no issues +- new params: + tasks can be found in the environment (no need to define the tasks parameter) + max_empty_runs kills the workers automatically if no new tasks are found in queue (nice for "spikes" of processing power) + discard_results to completely discard the results (if you don't need the output of the task) + utc_time enables datetime calculations with UTC time +- scheduler_task changes: + task_name is no longer required (filled automatically with function_name if found empty) + uuid makes easy to coordinate scheduler_task maintenance (filled automatically if not provided) + stop_time has no default (previously was today+1) + retry_failed to requeue automatically failed tasks + sync_output refreshes automatically the output (nice to report percentages) +- workers can be: + DISABLED (put to sleep and do nothing if not sending the heartbeat every 30 seconds) + TERMINATE (complete the current task and then die) + KILL (kill ASAP) ### Other Improvements +- DIV(..).elements(...replace=...), thanks Anthony - new layout based on Twitter Bootstrap - New generic views: generic.ics (Mac Mail Calendar) and generic.map (Google Maps) - request.args(0,default=0, cast=int, otherwise=URL(...)), thanks Anthony @@ -67,18 +85,19 @@ - routes in can redirect outside with routes_in=[('/path','303->http://..')] - better memcache support - improved spreadsheet, thanks Alan -- new interantionalization engine, thanks Vladyslav +- new internationalization engine, thanks Vladyslav - pluralization engine, thanks Vladyslav -- new makrmin with supports for nested lists, , , autolinks, thanks Vladyslav +- new markmin with support for nested lists, , , autolinks, thanks Vladyslav - new syntax: {{=BR()*5}} - gluon.cache.lazy_cache decorator allows caching functions in modules -- .coffee and .less support in response.fields, thanks Sam Sheftel +- .coffee and .less support in response.files, thanks Sam Sheftel - ldap certificate support - pg8000 postgresql driver support (experimental) - @cache('%(name)s%(args)s%(vars)s',5) and cache.autokey - added tox.ini, thanks Marc - web2py.py --run_system_tests, thanks Marc Abramowitz - html.py (and web2py helpers) can be used without web2py dependencies +- new fpdf, thanks Mariano ## 1.99.5-1.99.7 diff --git a/Makefile b/Makefile index 472d89b5..864d37ec 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,7 @@ +all: + echo "The Makefile is used to build the distribution." + echo "In order to run web2py you do not need to make anything." + echo "just run web2py.py" clean: rm -f httpserver.log rm -f parameters*.py @@ -13,10 +17,6 @@ clean: find ./applications/examples/ -name '.*' -exec rm -f {} \; find ./applications/welcome/ -name '.*' -exec rm -f {} \; find ./ -name '*.pyc' -exec rm -f {} \; -all: - echo "The Makefile is used to build the distribution." - echo "In order to run web2py you do not need to make anything." - echo "just run web2py.py" epydoc: ### build epydoc rm -f -r applications/examples/static/epydoc/ diff --git a/VERSION b/VERSION index 88809e6c..012b7f9c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -Version 2.00.0 (2012-08-22 19:47:08) dev +Version 2.00.0 (2012-08-25 16:19:46) dev diff --git a/__init__.py b/__init__.py index 12a6f48e..584ba879 100644 --- a/__init__.py +++ b/__init__.py @@ -6,3 +6,5 @@ + + diff --git a/anyserver.py b/anyserver.py index 3d6c929c..53985442 100644 --- a/anyserver.py +++ b/anyserver.py @@ -309,3 +309,4 @@ if __name__=='__main__': + diff --git a/appengine_config.py b/appengine_config.py index 33916e4d..4bb11668 100644 --- a/appengine_config.py +++ b/appengine_config.py @@ -6,3 +6,4 @@ def webapp_add_wsgi_middleware(app): + diff --git a/applications/admin/controllers/default.py b/applications/admin/controllers/default.py index cec8bef9..4f685ba0 100644 --- a/applications/admin/controllers/default.py +++ b/applications/admin/controllers/default.py @@ -20,7 +20,7 @@ try: have_git = True except ImportError: have_git = False - GIT_MISSING = 'requires python-git module, but not installed' + GIT_MISSING = 'requires python-git module, but not installed or incompatible version' from gluon.languages import (regex_language, read_possible_languages, read_possible_plurals, lang_sampling, @@ -206,7 +206,8 @@ def site(): elif form_create.accepted: # create a new application appname = cleanpath(form_create.vars.name) - if app_create(appname, request): + created, error = app_create(appname, request,info=True) + if created: if MULTI_USER_MODE: db.app.insert(name=appname,owner=auth.user.id) log_progress(appname) @@ -214,14 +215,15 @@ def site(): redirect(URL('design',args=appname)) else: session.flash = \ - T('unable to create application "%s" (it may exist already)', - form_create.vars.name) + DIV(T('unable to create application "%s"' % appname), + PRE(error)) redirect(URL(r=request)) elif form_update.accepted: if (form_update.vars.url or '').endswith('.git'): if not have_git: session.flash = GIT_MISSING + redirect(URL(r=request)) target = os.path.join(apath(r=request),form_update.vars.name) try: new_repo = Repo.clone_from(form_update.vars.url,target) @@ -336,7 +338,7 @@ def pack_plugin(): redirect(URL('plugin',args=request.args)) def upgrade_web2py(): - dialog = FORM.confim(T('Upgrade'), + dialog = FORM.confirm(T('Upgrade'), {T('Cancel'):URL('site')}) if dialog.accepted: (success, error) = upgrade(request) @@ -350,7 +352,7 @@ def upgrade_web2py(): def uninstall(): app = get_app() - dialog = FORM.confim(T('Uninstall'), + dialog = FORM.confirm(T('Uninstall'), {T('Cancel'):URL('site')}) if dialog.accepted: @@ -433,7 +435,7 @@ def delete(): if isinstance(sender, list): # ## fix a problem with Vista sender = sender[0] - dialog = FORM.confim(T('Delete'), + dialog = FORM.confirm(T('Delete'), {T('Cancel'):URL(sender, anchor=request.vars.id)}) if dialog.accepted: @@ -1003,7 +1005,7 @@ def delete_plugin(): plugin = request.args(1) plugin_name='plugin_'+plugin - dialog = FORM.confim( + dialog = FORM.confirm( T('Delete'), {T('Cancel'):URL('design', args=app)}) @@ -1652,7 +1654,7 @@ def git_pull(): if not have_git: session.flash = GIT_MISSING redirect(URL('site')) - dialog = FORM.confim(T('Pull'), + dialog = FORM.confirm(T('Pull'), {T('Cancel'):URL('site')}) if dialog.accepted: try: diff --git a/applications/admin/static/css/styles.css b/applications/admin/static/css/styles.css index 8375f145..c9e4c8d3 100644 --- a/applications/admin/static/css/styles.css +++ b/applications/admin/static/css/styles.css @@ -274,13 +274,13 @@ ul.button li a { display: block; z-index: 2; } -a.button { +a.button, a.btn { display: inline-block; vertical-align: middle; } /* 1.1. Normal status */ -a.button, +a.button, a.btn, ul.button li a { background-position: 100% 0; background-repeat: no-repeat; @@ -313,14 +313,14 @@ ul.button li.select a span { /* 2. EDITABLE STYLES */ /* 2.1. Image background used */ -a.button, +a.button, a.btn, .button a, .button span { background-image: url(../images/button.png); } /* 2.2. Normal status (Example for padding 10px) */ -a.button, +a.button, a.btn, .button a { padding: 0 10px 0 0; /* Padding-right: 10px */ margin: 0 1px 0 10px; /* Margin-left: 10px */ @@ -342,7 +342,7 @@ a.button span { _float: left; /* Only IE6 */ _position: relative; /* Only IE6 */ } -a.button { +a.button, a.btn { display: -moz-inline-box; /* FF<3 */ display: inline-block; /* FF<3 */ -moz-box-orient: vertical; /* FF<3 */ diff --git a/applications/admin/static/css/web2py.css b/applications/admin/static/css/web2py.css index 13f24237..54b1cbce 100644 --- a/applications/admin/static/css/web2py.css +++ b/applications/admin/static/css/web2py.css @@ -80,11 +80,11 @@ fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4p td.w2p_fw {padding-bottom:1px} td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top} -td.w2p_fl {text-align:right} +td.w2p_fl {text-align:left} td.w2p_fl, td.w2p_fw {padding-right:7px} td.w2p_fl,td.w2p_fc {padding-top:4px} div.w2p_export_menu {margin:5px 0} -div.w2p_export_menu a {margin-left:5px; padding:2px 5px; background-color:#f1f1f1; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px;} +div.w2p_export_menu a, div.w2p_wiki_tags a, div.w2p_cloud a {margin-left:5px; padding:2px 5px; background-color:#f1f1f1; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px;} /* tr#submit_record__row {border-top:1px solid #E5E5E5} */ #submit_record__row td {padding-top:.5em} @@ -294,8 +294,14 @@ div.error { margin-bottom:18px; } -.web2py_breadcrumbs ul li { +li.w2p_grid_breadcrumb_elem { display:inline-block; } .ie9 #query_panel {padding-bottom:2px} + +#wiki_page_body { + width: 600px; + height: auto; + min-height: 400px; +} \ No newline at end of file diff --git a/applications/admin/views/default/site.html b/applications/admin/views/default/site.html index 879dccc2..e983d00c 100644 --- a/applications/admin/views/default/site.html +++ b/applications/admin/views/default/site.html @@ -78,6 +78,16 @@

{{pass}} + + {{if MULTI_USER_MODE and is_manager():}} +
+

{{=T("Multi User Mode")}}

+

+ {{=button(URL('bulk_register'),T('Bulk Register'))}} + {{=button(URL('manage_students'),T('Manage Students'))}} +

+
+ {{pass}}

{{=T("New application wizard")}}

diff --git a/applications/examples/private/content/en/default/documentation/more.markmin b/applications/examples/private/content/en/default/documentation/more.markmin index 854e076d..fe803573 100644 --- a/applications/examples/private/content/en/default/documentation/more.markmin +++ b/applications/examples/private/content/en/default/documentation/more.markmin @@ -7,6 +7,7 @@ - [[User Voice http://web2py.uservoice.com/ popup]] #### Learning and Demos +- [[Killer Web Development Tutorial http://killer-web-development.com/]] - [[Admin Demo http://www.web2py.com/demo_admin popup]] (web-based IDE) - [[Welcome App Demo http://www.web2py.com/welcome]] (scaffolding application) - [[Videos http://www.web2py.com/examples/default/videos/]] diff --git a/applications/examples/static/css/web2py.css b/applications/examples/static/css/web2py.css index 13f24237..54b1cbce 100644 --- a/applications/examples/static/css/web2py.css +++ b/applications/examples/static/css/web2py.css @@ -80,11 +80,11 @@ fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4p td.w2p_fw {padding-bottom:1px} td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top} -td.w2p_fl {text-align:right} +td.w2p_fl {text-align:left} td.w2p_fl, td.w2p_fw {padding-right:7px} td.w2p_fl,td.w2p_fc {padding-top:4px} div.w2p_export_menu {margin:5px 0} -div.w2p_export_menu a {margin-left:5px; padding:2px 5px; background-color:#f1f1f1; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px;} +div.w2p_export_menu a, div.w2p_wiki_tags a, div.w2p_cloud a {margin-left:5px; padding:2px 5px; background-color:#f1f1f1; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px;} /* tr#submit_record__row {border-top:1px solid #E5E5E5} */ #submit_record__row td {padding-top:.5em} @@ -294,8 +294,14 @@ div.error { margin-bottom:18px; } -.web2py_breadcrumbs ul li { +li.w2p_grid_breadcrumb_elem { display:inline-block; } .ie9 #query_panel {padding-bottom:2px} + +#wiki_page_body { + width: 600px; + height: auto; + min-height: 400px; +} \ No newline at end of file diff --git a/applications/welcome/static/css/web2py.css b/applications/welcome/static/css/web2py.css index 2ac5c115..54b1cbce 100644 --- a/applications/welcome/static/css/web2py.css +++ b/applications/welcome/static/css/web2py.css @@ -80,11 +80,11 @@ fieldset legend {text-transform:uppercase; font-weight:bold; padding:4px 16px 4p td.w2p_fw {padding-bottom:1px} td.w2p_fl,td.w2p_fw,td.w2p_fc {vertical-align:top} -td.w2p_fl {text-align:right} +td.w2p_fl {text-align:left} td.w2p_fl, td.w2p_fw {padding-right:7px} td.w2p_fl,td.w2p_fc {padding-top:4px} div.w2p_export_menu {margin:5px 0} -div.w2p_export_menu a {margin-left:5px; padding:2px 5px; background-color:#f1f1f1; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px;} +div.w2p_export_menu a, div.w2p_wiki_tags a, div.w2p_cloud a {margin-left:5px; padding:2px 5px; background-color:#f1f1f1; border-radius:5px; -moz-border-radius:5px; -webkit-border-radius:5px;} /* tr#submit_record__row {border-top:1px solid #E5E5E5} */ #submit_record__row td {padding-top:.5em} @@ -299,3 +299,9 @@ li.w2p_grid_breadcrumb_elem { } .ie9 #query_panel {padding-bottom:2px} + +#wiki_page_body { + width: 600px; + height: auto; + min-height: 400px; +} \ No newline at end of file diff --git a/applications/welcome/static/css/bootswatch.css b/applications/welcome/static/css/web2py_bootstrap.css similarity index 100% rename from applications/welcome/static/css/bootswatch.css rename to applications/welcome/static/css/web2py_bootstrap.css diff --git a/applications/welcome/static/css/bootswatch_nojs.css b/applications/welcome/static/css/web2py_bootstrap_nojs.css similarity index 100% rename from applications/welcome/static/css/bootswatch_nojs.css rename to applications/welcome/static/css/web2py_bootstrap_nojs.css diff --git a/applications/welcome/views/default/index.html b/applications/welcome/views/default/index.html index 50e64114..6e9ab1ff 100644 --- a/applications/welcome/views/default/index.html +++ b/applications/welcome/views/default/index.html @@ -1,4 +1,4 @@ -{{left_sidebar_enabled,right_sidebar_enabled=False,True}} +{{left_sidebar_enabled,right_sidebar_enabled=False,('message' in globals())}} {{extend 'layout.html'}} {{if 'message' in globals():}} @@ -17,6 +17,8 @@ _href=URL('admin','default','peek',args=(request.application,'views',request.controller,'index.html')))))}}
  • {{=T('You can modify this application and adapt it to your needs')}}
  • +{{elif 'content' in globals():}} +{{=content}} {{else:}} {{=BEAUTIFY(response._vars)}} {{pass}} @@ -31,4 +33,3 @@
  • {{=T('Documentation')}}
  • {{end}} - diff --git a/applications/welcome/views/layout.html b/applications/welcome/views/layout.html index 7d5349e1..addb3682 100644 --- a/applications/welcome/views/layout.html +++ b/applications/welcome/views/layout.html @@ -43,12 +43,8 @@ response.files.append(URL('static','css/bootstrap.min.css')) response.files.append(URL('static','css/bootstrap-responsive.min.css')) response.files.append(URL('static','css/web2py.css')) - response.files.append(URL('static','css/bootswatch.css')) + response.files.append(URL('static','css/web2py_bootstrap.css')) }} - - {{include 'web2py_ajax.html'}} @@ -64,7 +60,7 @@ uncomment to load jquery-ui //--> - + {{block head}}{{end}} @@ -160,7 +156,6 @@ if(jQuery(this).find('ul').length) jQuery(this).children('a').contents().before(''); }); - if(jQuery(document).width()>=980) { jQuery('ul.nav li.dropdown').hover(function() { jQuery(this).find('.dropdown-menu').stop(true, true).delay(200).fadeIn(); diff --git a/cgihandler.py b/cgihandler.py index 16620054..c4c14993 100755 --- a/cgihandler.py +++ b/cgihandler.py @@ -65,3 +65,4 @@ wsgiref.handlers.CGIHandler().run(gluon.main.wsgibase) + diff --git a/fcgihandler.py b/fcgihandler.py index 196e538f..f45067a6 100755 --- a/fcgihandler.py +++ b/fcgihandler.py @@ -55,3 +55,4 @@ fcgi.WSGIServer(application, bindAddress='/tmp/fcgi.sock').run() + diff --git a/gaehandler.py b/gaehandler.py index 0d09987b..7557a982 100755 --- a/gaehandler.py +++ b/gaehandler.py @@ -103,3 +103,4 @@ if __name__ == '__main__': + diff --git a/gluon/__init__.py b/gluon/__init__.py index 2f48d365..fd88cd43 100644 --- a/gluon/__init__.py +++ b/gluon/__init__.py @@ -52,3 +52,4 @@ if 0: + diff --git a/gluon/admin.py b/gluon/admin.py index ce7bdaab..bb3bcf92 100644 --- a/gluon/admin.py +++ b/gluon/admin.py @@ -18,6 +18,7 @@ from fileutils import up, fix_newlines, abspath, recursive_unlink from fileutils import read_file, write_file, parse_version from restricted import RestrictedError from settings import global_settings +from http import HTTP if not global_settings.web2py_runtime_gae: import site @@ -126,7 +127,7 @@ def app_cleanup(app, request): r = False # Remove cache files - path = apath('%s/sessions/' % app, request) + path = apath('%s/cache/' % app, request) if os.path.exists(path): for f in os.listdir(path): try: @@ -157,7 +158,7 @@ def app_compile(app, request): remove_compiled_application(folder) return tb -def app_create(app, request,force=False,key=None): +def app_create(app, request,force=False,key=None,info=False): """ Create a copy of welcome.w2p (scaffolding) app @@ -169,11 +170,19 @@ def app_create(app, request,force=False,key=None): the global request object """ - try: - path = apath(app, request) - os.mkdir(path) - except: - if not force: + path = apath(app, request) + if not os.path.exists(path): + try: + os.mkdir(path) + except: + if info: + return False, traceback.format_exc(sys.exc_info) + else: + return False + elif not force: + if info: + return False, "Application exists" + else: return False try: w2p_unpack('welcome.w2p', path) @@ -189,10 +198,16 @@ def app_create(app, request,force=False,key=None): data = data.replace('', 'sha512:'+(key or web2py_uuid())) write_file(db, data) - return True + if info: + return True, None + else: + return True except: rmtree(path) - return False + if info: + return False, traceback.format_exc(sys.exc_info) + else: + return False def app_install(app, fobj, request, filename, overwrite=None): @@ -465,3 +480,4 @@ def create_missing_app_folders(request): + diff --git a/gluon/cache.py b/gluon/cache.py index cbe2b215..bc9cc0f9 100644 --- a/gluon/cache.py +++ b/gluon/cache.py @@ -498,3 +498,4 @@ def lazy_cache(key=None,time_expire=None,cache_model='ram'): return decorator + diff --git a/gluon/cfs.py b/gluon/cfs.py index d643882b..b3669f4b 100644 --- a/gluon/cfs.py +++ b/gluon/cfs.py @@ -56,3 +56,4 @@ def getcfs(key, filename, filter=None): + diff --git a/gluon/compileapp.py b/gluon/compileapp.py index 378e8441..a6bb23f8 100644 --- a/gluon/compileapp.py +++ b/gluon/compileapp.py @@ -691,3 +691,4 @@ if __name__ == '__main__': + diff --git a/gluon/contenttype.py b/gluon/contenttype.py index 6e4c78b6..cb11c687 100644 --- a/gluon/contenttype.py +++ b/gluon/contenttype.py @@ -723,3 +723,4 @@ def contenttype(filename, default='text/plain'): + diff --git a/gluon/contrib/AuthorizeNet.py b/gluon/contrib/AuthorizeNet.py index aa6bf785..8b24ef0a 100755 --- a/gluon/contrib/AuthorizeNet.py +++ b/gluon/contrib/AuthorizeNet.py @@ -263,3 +263,4 @@ if __name__=='__main__': + diff --git a/gluon/contrib/DowCommerce.py b/gluon/contrib/DowCommerce.py index 090fd077..d71c3911 100644 --- a/gluon/contrib/DowCommerce.py +++ b/gluon/contrib/DowCommerce.py @@ -243,3 +243,4 @@ if __name__=='__main__': + diff --git a/gluon/contrib/__init__.py b/gluon/contrib/__init__.py index 9e5f5f8a..12a6f48e 100644 --- a/gluon/contrib/__init__.py +++ b/gluon/contrib/__init__.py @@ -5,3 +5,4 @@ + diff --git a/gluon/contrib/aes.py b/gluon/contrib/aes.py index bb745310..cfc212b5 100644 --- a/gluon/contrib/aes.py +++ b/gluon/contrib/aes.py @@ -501,3 +501,4 @@ aes_Rcon = array('B', '61c29f254a943366cc831d3a74e8cb'.decode('hex') ) + diff --git a/gluon/contrib/autolinks.py b/gluon/contrib/autolinks.py index cb05079a..be72db03 100644 --- a/gluon/contrib/autolinks.py +++ b/gluon/contrib/autolinks.py @@ -135,7 +135,7 @@ def oembed(url): for k,v in EMBED_MAPS: if k.match(url): oembed = v+'?format=json&url='+cgi.escape(url) - try: + try: data = urllib.urlopen(oembed).read() print data return loads(data) # json! @@ -152,7 +152,7 @@ def expand_one(url,cdict): r = cdict[url] else: r = oembed(url) - if isinstance(cdict,dict): + if isinstance(cdict,dict): cdict[url] = r # if oembed service if 'html' in r: @@ -204,3 +204,4 @@ if __name__=="__main__": else: print test() + diff --git a/gluon/contrib/comet_messaging.py b/gluon/contrib/comet_messaging.py index d41afb51..c3f2ed35 100644 --- a/gluon/contrib/comet_messaging.py +++ b/gluon/contrib/comet_messaging.py @@ -194,3 +194,4 @@ if __name__ == "__main__": + diff --git a/gluon/contrib/feedparser.py b/gluon/contrib/feedparser.py index 371323e9..1afb2c11 100755 --- a/gluon/contrib/feedparser.py +++ b/gluon/contrib/feedparser.py @@ -3910,3 +3910,4 @@ def parse(url_file_stream_or_string, etag=None, modified=None, agent=None, refer + diff --git a/gluon/contrib/fpdf/__init__.py b/gluon/contrib/fpdf/__init__.py new file mode 100644 index 00000000..e0ffc017 --- /dev/null +++ b/gluon/contrib/fpdf/__init__.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"FPDF for python" + +__license__ = "LGPL 3.0" +__version__ = "1.7" + +from fpdf import * +try: + from html import HTMLMixin +except ImportError: + import warnings + warnings.warn("web2py gluon package not installed, required for html2pdf") + +from template import Template + + diff --git a/gluon/contrib/fpdf/fonts.py b/gluon/contrib/fpdf/fonts.py new file mode 100644 index 00000000..f584a1e8 --- /dev/null +++ b/gluon/contrib/fpdf/fonts.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python +# -*- coding: latin-1 -*- + +# Fonts: + +fpdf_charwidths = {} + +fpdf_charwidths['courier']={} + +for i in xrange(0,256): + fpdf_charwidths['courier'][chr(i)]=600 + fpdf_charwidths['courierB']=fpdf_charwidths['courier'] + fpdf_charwidths['courierI']=fpdf_charwidths['courier'] + fpdf_charwidths['courierBI']=fpdf_charwidths['courier'] + +fpdf_charwidths['helvetica']={ + '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, + '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':278,'"':355,'#':556,'$':556,'%':889,'&':667,'\'':191,'(':333,')':333,'*':389,'+':584, + ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':278,';':278,'<':584,'=':584,'>':584,'?':556,'@':1015,'A':667, + 'B':667,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':500,'K':667,'L':556,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, + 'X':667,'Y':667,'Z':611,'[':278,'\\':278,']':278,'^':469,'_':556,'`':333,'a':556,'b':556,'c':500,'d':556,'e':556,'f':278,'g':556,'h':556,'i':222,'j':222,'k':500,'l':222,'m':833, + 'n':556,'o':556,'p':556,'q':556,'r':333,'s':500,'t':278,'u':556,'v':500,'w':722,'x':500,'y':500,'z':500,'{':334,'|':260,'}':334,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':222,'\x83':556, + '\x84':333,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':222,'\x92':222,'\x93':333,'\x94':333,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, + '\x9a':500,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':260,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, + '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':556,'\xb6':537,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':667,'\xc1':667,'\xc2':667,'\xc3':667,'\xc4':667,'\xc5':667, + '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':500,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':556,'\xf1':556, + '\xf2':556,'\xf3':556,'\xf4':556,'\xf5':556,'\xf6':556,'\xf7':584,'\xf8':611,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':500,'\xfe':556,'\xff':500} + +fpdf_charwidths['helveticaB']={ + '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, + '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':333,'"':474,'#':556,'$':556,'%':889,'&':722,'\'':238,'(':333,')':333,'*':389,'+':584, + ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':333,';':333,'<':584,'=':584,'>':584,'?':611,'@':975,'A':722, + 'B':722,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':556,'K':722,'L':611,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, + 'X':667,'Y':667,'Z':611,'[':333,'\\':278,']':333,'^':584,'_':556,'`':333,'a':556,'b':611,'c':556,'d':611,'e':556,'f':333,'g':611,'h':611,'i':278,'j':278,'k':556,'l':278,'m':889, + 'n':611,'o':611,'p':611,'q':611,'r':389,'s':556,'t':333,'u':611,'v':556,'w':778,'x':556,'y':556,'z':500,'{':389,'|':280,'}':389,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':278,'\x83':556, + '\x84':500,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':278,'\x92':278,'\x93':500,'\x94':500,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, + '\x9a':556,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':280,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, + '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':611,'\xb6':556,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, + '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':556,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':611,'\xf1':611, + '\xf2':611,'\xf3':611,'\xf4':611,'\xf5':611,'\xf6':611,'\xf7':584,'\xf8':611,'\xf9':611,'\xfa':611,'\xfb':611,'\xfc':611,'\xfd':556,'\xfe':611,'\xff':556 +} + +fpdf_charwidths['helveticaBI']={ + '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, + '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':333,'"':474,'#':556,'$':556,'%':889,'&':722,'\'':238,'(':333,')':333,'*':389,'+':584, + ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':333,';':333,'<':584,'=':584,'>':584,'?':611,'@':975,'A':722, + 'B':722,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':556,'K':722,'L':611,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, + 'X':667,'Y':667,'Z':611,'[':333,'\\':278,']':333,'^':584,'_':556,'`':333,'a':556,'b':611,'c':556,'d':611,'e':556,'f':333,'g':611,'h':611,'i':278,'j':278,'k':556,'l':278,'m':889, + 'n':611,'o':611,'p':611,'q':611,'r':389,'s':556,'t':333,'u':611,'v':556,'w':778,'x':556,'y':556,'z':500,'{':389,'|':280,'}':389,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':278,'\x83':556, + '\x84':500,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':278,'\x92':278,'\x93':500,'\x94':500,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, + '\x9a':556,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':280,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, + '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':611,'\xb6':556,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, + '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':556,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':611,'\xf1':611, + '\xf2':611,'\xf3':611,'\xf4':611,'\xf5':611,'\xf6':611,'\xf7':584,'\xf8':611,'\xf9':611,'\xfa':611,'\xfb':611,'\xfc':611,'\xfd':556,'\xfe':611,'\xff':556} + +fpdf_charwidths['helveticaI']={ + '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, + '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':278,'"':355,'#':556,'$':556,'%':889,'&':667,'\'':191,'(':333,')':333,'*':389,'+':584, + ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':278,';':278,'<':584,'=':584,'>':584,'?':556,'@':1015,'A':667, + 'B':667,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':500,'K':667,'L':556,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, + 'X':667,'Y':667,'Z':611,'[':278,'\\':278,']':278,'^':469,'_':556,'`':333,'a':556,'b':556,'c':500,'d':556,'e':556,'f':278,'g':556,'h':556,'i':222,'j':222,'k':500,'l':222,'m':833, + 'n':556,'o':556,'p':556,'q':556,'r':333,'s':500,'t':278,'u':556,'v':500,'w':722,'x':500,'y':500,'z':500,'{':334,'|':260,'}':334,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':222,'\x83':556, + '\x84':333,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':222,'\x92':222,'\x93':333,'\x94':333,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, + '\x9a':500,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':260,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, + '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':556,'\xb6':537,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':667,'\xc1':667,'\xc2':667,'\xc3':667,'\xc4':667,'\xc5':667, + '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':500,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':556,'\xf1':556, + '\xf2':556,'\xf3':556,'\xf4':556,'\xf5':556,'\xf6':556,'\xf7':584,'\xf8':611,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':500,'\xfe':556,'\xff':500} + +fpdf_charwidths['symbol']={ + '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, + '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':713,'#':500,'$':549,'%':833,'&':778,'\'':439,'(':333,')':333,'*':500,'+':549, + ',':250,'-':549,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':278,';':278,'<':549,'=':549,'>':549,'?':444,'@':549,'A':722, + 'B':667,'C':722,'D':612,'E':611,'F':763,'G':603,'H':722,'I':333,'J':631,'K':722,'L':686,'M':889,'N':722,'O':722,'P':768,'Q':741,'R':556,'S':592,'T':611,'U':690,'V':439,'W':768, + 'X':645,'Y':795,'Z':611,'[':333,'\\':863,']':333,'^':658,'_':500,'`':500,'a':631,'b':549,'c':549,'d':494,'e':439,'f':521,'g':411,'h':603,'i':329,'j':603,'k':549,'l':549,'m':576, + 'n':521,'o':549,'p':549,'q':521,'r':549,'s':603,'t':439,'u':576,'v':713,'w':686,'x':493,'y':686,'z':494,'{':480,'|':200,'}':480,'~':549,'\x7f':0,'\x80':0,'\x81':0,'\x82':0,'\x83':0, + '\x84':0,'\x85':0,'\x86':0,'\x87':0,'\x88':0,'\x89':0,'\x8a':0,'\x8b':0,'\x8c':0,'\x8d':0,'\x8e':0,'\x8f':0,'\x90':0,'\x91':0,'\x92':0,'\x93':0,'\x94':0,'\x95':0,'\x96':0,'\x97':0,'\x98':0,'\x99':0, + '\x9a':0,'\x9b':0,'\x9c':0,'\x9d':0,'\x9e':0,'\x9f':0,'\xa0':750,'\xa1':620,'\xa2':247,'\xa3':549,'\xa4':167,'\xa5':713,'\xa6':500,'\xa7':753,'\xa8':753,'\xa9':753,'\xaa':753,'\xab':1042,'\xac':987,'\xad':603,'\xae':987,'\xaf':603, + '\xb0':400,'\xb1':549,'\xb2':411,'\xb3':549,'\xb4':549,'\xb5':713,'\xb6':494,'\xb7':460,'\xb8':549,'\xb9':549,'\xba':549,'\xbb':549,'\xbc':1000,'\xbd':603,'\xbe':1000,'\xbf':658,'\xc0':823,'\xc1':686,'\xc2':795,'\xc3':987,'\xc4':768,'\xc5':768, + '\xc6':823,'\xc7':768,'\xc8':768,'\xc9':713,'\xca':713,'\xcb':713,'\xcc':713,'\xcd':713,'\xce':713,'\xcf':713,'\xd0':768,'\xd1':713,'\xd2':790,'\xd3':790,'\xd4':890,'\xd5':823,'\xd6':549,'\xd7':250,'\xd8':713,'\xd9':603,'\xda':603,'\xdb':1042, + '\xdc':987,'\xdd':603,'\xde':987,'\xdf':603,'\xe0':494,'\xe1':329,'\xe2':790,'\xe3':790,'\xe4':786,'\xe5':713,'\xe6':384,'\xe7':384,'\xe8':384,'\xe9':384,'\xea':384,'\xeb':384,'\xec':494,'\xed':494,'\xee':494,'\xef':494,'\xf0':0,'\xf1':329, + '\xf2':274,'\xf3':686,'\xf4':686,'\xf5':686,'\xf6':384,'\xf7':384,'\xf8':384,'\xf9':384,'\xfa':384,'\xfb':384,'\xfc':494,'\xfd':494,'\xfe':494,'\xff':0} + +fpdf_charwidths['times']={ + '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, + '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':408,'#':500,'$':500,'%':833,'&':778,'\'':180,'(':333,')':333,'*':500,'+':564, + ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':278,';':278,'<':564,'=':564,'>':564,'?':444,'@':921,'A':722, + 'B':667,'C':667,'D':722,'E':611,'F':556,'G':722,'H':722,'I':333,'J':389,'K':722,'L':611,'M':889,'N':722,'O':722,'P':556,'Q':722,'R':667,'S':556,'T':611,'U':722,'V':722,'W':944, + 'X':722,'Y':722,'Z':611,'[':333,'\\':278,']':333,'^':469,'_':500,'`':333,'a':444,'b':500,'c':444,'d':500,'e':444,'f':333,'g':500,'h':500,'i':278,'j':278,'k':500,'l':278,'m':778, + 'n':500,'o':500,'p':500,'q':500,'r':333,'s':389,'t':278,'u':500,'v':500,'w':722,'x':500,'y':500,'z':444,'{':480,'|':200,'}':480,'~':541,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, + '\x84':444,'\x85':1000,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':556,'\x8b':333,'\x8c':889,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':444,'\x94':444,'\x95':350,'\x96':500,'\x97':1000,'\x98':333,'\x99':980, + '\x9a':389,'\x9b':333,'\x9c':722,'\x9d':350,'\x9e':444,'\x9f':722,'\xa0':250,'\xa1':333,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':200,'\xa7':500,'\xa8':333,'\xa9':760,'\xaa':276,'\xab':500,'\xac':564,'\xad':333,'\xae':760,'\xaf':333, + '\xb0':400,'\xb1':564,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':500,'\xb6':453,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':310,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':444,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, + '\xc6':889,'\xc7':667,'\xc8':611,'\xc9':611,'\xca':611,'\xcb':611,'\xcc':333,'\xcd':333,'\xce':333,'\xcf':333,'\xd0':722,'\xd1':722,'\xd2':722,'\xd3':722,'\xd4':722,'\xd5':722,'\xd6':722,'\xd7':564,'\xd8':722,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':722,'\xde':556,'\xdf':500,'\xe0':444,'\xe1':444,'\xe2':444,'\xe3':444,'\xe4':444,'\xe5':444,'\xe6':667,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':500, + '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':564,'\xf8':500,'\xf9':500,'\xfa':500,'\xfb':500,'\xfc':500,'\xfd':500,'\xfe':500,'\xff':500} + +fpdf_charwidths['timesB']={ + '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, + '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':555,'#':500,'$':500,'%':1000,'&':833,'\'':278,'(':333,')':333,'*':500,'+':570, + ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':333,';':333,'<':570,'=':570,'>':570,'?':500,'@':930,'A':722, + 'B':667,'C':722,'D':722,'E':667,'F':611,'G':778,'H':778,'I':389,'J':500,'K':778,'L':667,'M':944,'N':722,'O':778,'P':611,'Q':778,'R':722,'S':556,'T':667,'U':722,'V':722,'W':1000, + 'X':722,'Y':722,'Z':667,'[':333,'\\':278,']':333,'^':581,'_':500,'`':333,'a':500,'b':556,'c':444,'d':556,'e':444,'f':333,'g':500,'h':556,'i':278,'j':333,'k':556,'l':278,'m':833, + 'n':556,'o':500,'p':556,'q':556,'r':444,'s':389,'t':333,'u':556,'v':500,'w':722,'x':500,'y':500,'z':444,'{':394,'|':220,'}':394,'~':520,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, + '\x84':500,'\x85':1000,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':556,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':667,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':500,'\x94':500,'\x95':350,'\x96':500,'\x97':1000,'\x98':333,'\x99':1000, + '\x9a':389,'\x9b':333,'\x9c':722,'\x9d':350,'\x9e':444,'\x9f':722,'\xa0':250,'\xa1':333,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':220,'\xa7':500,'\xa8':333,'\xa9':747,'\xaa':300,'\xab':500,'\xac':570,'\xad':333,'\xae':747,'\xaf':333, + '\xb0':400,'\xb1':570,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':556,'\xb6':540,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':330,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':500,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, + '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':389,'\xcd':389,'\xce':389,'\xcf':389,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':570,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':722,'\xde':611,'\xdf':556,'\xe0':500,'\xe1':500,'\xe2':500,'\xe3':500,'\xe4':500,'\xe5':500,'\xe6':722,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':556, + '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':570,'\xf8':500,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':500,'\xfe':556,'\xff':500} + +fpdf_charwidths['timesBI']={ + '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, + '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':389,'"':555,'#':500,'$':500,'%':833,'&':778,'\'':278,'(':333,')':333,'*':500,'+':570, + ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':333,';':333,'<':570,'=':570,'>':570,'?':500,'@':832,'A':667, + 'B':667,'C':667,'D':722,'E':667,'F':667,'G':722,'H':778,'I':389,'J':500,'K':667,'L':611,'M':889,'N':722,'O':722,'P':611,'Q':722,'R':667,'S':556,'T':611,'U':722,'V':667,'W':889, + 'X':667,'Y':611,'Z':611,'[':333,'\\':278,']':333,'^':570,'_':500,'`':333,'a':500,'b':500,'c':444,'d':500,'e':444,'f':333,'g':500,'h':556,'i':278,'j':278,'k':500,'l':278,'m':778, + 'n':556,'o':500,'p':500,'q':500,'r':389,'s':389,'t':278,'u':556,'v':444,'w':667,'x':500,'y':444,'z':389,'{':348,'|':220,'}':348,'~':570,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, + '\x84':500,'\x85':1000,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':556,'\x8b':333,'\x8c':944,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':500,'\x94':500,'\x95':350,'\x96':500,'\x97':1000,'\x98':333,'\x99':1000, + '\x9a':389,'\x9b':333,'\x9c':722,'\x9d':350,'\x9e':389,'\x9f':611,'\xa0':250,'\xa1':389,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':220,'\xa7':500,'\xa8':333,'\xa9':747,'\xaa':266,'\xab':500,'\xac':606,'\xad':333,'\xae':747,'\xaf':333, + '\xb0':400,'\xb1':570,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':576,'\xb6':500,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':300,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':500,'\xc0':667,'\xc1':667,'\xc2':667,'\xc3':667,'\xc4':667,'\xc5':667, + '\xc6':944,'\xc7':667,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':389,'\xcd':389,'\xce':389,'\xcf':389,'\xd0':722,'\xd1':722,'\xd2':722,'\xd3':722,'\xd4':722,'\xd5':722,'\xd6':722,'\xd7':570,'\xd8':722,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':611,'\xde':611,'\xdf':500,'\xe0':500,'\xe1':500,'\xe2':500,'\xe3':500,'\xe4':500,'\xe5':500,'\xe6':722,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':556, + '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':570,'\xf8':500,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':444,'\xfe':500,'\xff':444} + +fpdf_charwidths['timesI']={ + '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, + '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':420,'#':500,'$':500,'%':833,'&':778,'\'':214,'(':333,')':333,'*':500,'+':675, + ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':333,';':333,'<':675,'=':675,'>':675,'?':500,'@':920,'A':611, + 'B':611,'C':667,'D':722,'E':611,'F':611,'G':722,'H':722,'I':333,'J':444,'K':667,'L':556,'M':833,'N':667,'O':722,'P':611,'Q':722,'R':611,'S':500,'T':556,'U':722,'V':611,'W':833, + 'X':611,'Y':556,'Z':556,'[':389,'\\':278,']':389,'^':422,'_':500,'`':333,'a':500,'b':500,'c':444,'d':500,'e':444,'f':278,'g':500,'h':500,'i':278,'j':278,'k':444,'l':278,'m':722, + 'n':500,'o':500,'p':500,'q':500,'r':389,'s':389,'t':278,'u':500,'v':444,'w':667,'x':444,'y':444,'z':389,'{':400,'|':275,'}':400,'~':541,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, + '\x84':556,'\x85':889,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':500,'\x8b':333,'\x8c':944,'\x8d':350,'\x8e':556,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':556,'\x94':556,'\x95':350,'\x96':500,'\x97':889,'\x98':333,'\x99':980, + '\x9a':389,'\x9b':333,'\x9c':667,'\x9d':350,'\x9e':389,'\x9f':556,'\xa0':250,'\xa1':389,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':275,'\xa7':500,'\xa8':333,'\xa9':760,'\xaa':276,'\xab':500,'\xac':675,'\xad':333,'\xae':760,'\xaf':333, + '\xb0':400,'\xb1':675,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':500,'\xb6':523,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':310,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':500,'\xc0':611,'\xc1':611,'\xc2':611,'\xc3':611,'\xc4':611,'\xc5':611, + '\xc6':889,'\xc7':667,'\xc8':611,'\xc9':611,'\xca':611,'\xcb':611,'\xcc':333,'\xcd':333,'\xce':333,'\xcf':333,'\xd0':722,'\xd1':667,'\xd2':722,'\xd3':722,'\xd4':722,'\xd5':722,'\xd6':722,'\xd7':675,'\xd8':722,'\xd9':722,'\xda':722,'\xdb':722, + '\xdc':722,'\xdd':556,'\xde':611,'\xdf':500,'\xe0':500,'\xe1':500,'\xe2':500,'\xe3':500,'\xe4':500,'\xe5':500,'\xe6':667,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':500, + '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':675,'\xf8':500,'\xf9':500,'\xfa':500,'\xfb':500,'\xfc':500,'\xfd':444,'\xfe':500,'\xff':444} + +fpdf_charwidths['zapfdingbats']={ + '\x00':0,'\x01':0,'\x02':0,'\x03':0,'\x04':0,'\x05':0,'\x06':0,'\x07':0,'\x08':0,'\t':0,'\n':0,'\x0b':0,'\x0c':0,'\r':0,'\x0e':0,'\x0f':0,'\x10':0,'\x11':0,'\x12':0,'\x13':0,'\x14':0,'\x15':0, + '\x16':0,'\x17':0,'\x18':0,'\x19':0,'\x1a':0,'\x1b':0,'\x1c':0,'\x1d':0,'\x1e':0,'\x1f':0,' ':278,'!':974,'"':961,'#':974,'$':980,'%':719,'&':789,'\'':790,'(':791,')':690,'*':960,'+':939, + ',':549,'-':855,'.':911,'/':933,'0':911,'1':945,'2':974,'3':755,'4':846,'5':762,'6':761,'7':571,'8':677,'9':763,':':760,';':759,'<':754,'=':494,'>':552,'?':537,'@':577,'A':692, + 'B':786,'C':788,'D':788,'E':790,'F':793,'G':794,'H':816,'I':823,'J':789,'K':841,'L':823,'M':833,'N':816,'O':831,'P':923,'Q':744,'R':723,'S':749,'T':790,'U':792,'V':695,'W':776, + 'X':768,'Y':792,'Z':759,'[':707,'\\':708,']':682,'^':701,'_':826,'`':815,'a':789,'b':789,'c':707,'d':687,'e':696,'f':689,'g':786,'h':787,'i':713,'j':791,'k':785,'l':791,'m':873, + 'n':761,'o':762,'p':762,'q':759,'r':759,'s':892,'t':892,'u':788,'v':784,'w':438,'x':138,'y':277,'z':415,'{':392,'|':392,'}':668,'~':668,'\x7f':0,'\x80':390,'\x81':390,'\x82':317,'\x83':317, + '\x84':276,'\x85':276,'\x86':509,'\x87':509,'\x88':410,'\x89':410,'\x8a':234,'\x8b':234,'\x8c':334,'\x8d':334,'\x8e':0,'\x8f':0,'\x90':0,'\x91':0,'\x92':0,'\x93':0,'\x94':0,'\x95':0,'\x96':0,'\x97':0,'\x98':0,'\x99':0, + '\x9a':0,'\x9b':0,'\x9c':0,'\x9d':0,'\x9e':0,'\x9f':0,'\xa0':0,'\xa1':732,'\xa2':544,'\xa3':544,'\xa4':910,'\xa5':667,'\xa6':760,'\xa7':760,'\xa8':776,'\xa9':595,'\xaa':694,'\xab':626,'\xac':788,'\xad':788,'\xae':788,'\xaf':788, + '\xb0':788,'\xb1':788,'\xb2':788,'\xb3':788,'\xb4':788,'\xb5':788,'\xb6':788,'\xb7':788,'\xb8':788,'\xb9':788,'\xba':788,'\xbb':788,'\xbc':788,'\xbd':788,'\xbe':788,'\xbf':788,'\xc0':788,'\xc1':788,'\xc2':788,'\xc3':788,'\xc4':788,'\xc5':788, + '\xc6':788,'\xc7':788,'\xc8':788,'\xc9':788,'\xca':788,'\xcb':788,'\xcc':788,'\xcd':788,'\xce':788,'\xcf':788,'\xd0':788,'\xd1':788,'\xd2':788,'\xd3':788,'\xd4':894,'\xd5':838,'\xd6':1016,'\xd7':458,'\xd8':748,'\xd9':924,'\xda':748,'\xdb':918, + '\xdc':927,'\xdd':928,'\xde':928,'\xdf':834,'\xe0':873,'\xe1':828,'\xe2':924,'\xe3':924,'\xe4':917,'\xe5':930,'\xe6':931,'\xe7':463,'\xe8':883,'\xe9':836,'\xea':836,'\xeb':867,'\xec':867,'\xed':696,'\xee':696,'\xef':874,'\xf0':0,'\xf1':874, + '\xf2':760,'\xf3':946,'\xf4':771,'\xf5':865,'\xf6':771,'\xf7':888,'\xf8':967,'\xf9':888,'\xfa':831,'\xfb':873,'\xfc':927,'\xfd':970,'\xfe':918,'\xff':0} + + diff --git a/gluon/contrib/pyfpdf/fpdf.py b/gluon/contrib/fpdf/fpdf.py similarity index 52% rename from gluon/contrib/pyfpdf/fpdf.py rename to gluon/contrib/fpdf/fpdf.py index fec42f03..4f9a3eb5 100644 --- a/gluon/contrib/pyfpdf/fpdf.py +++ b/gluon/contrib/fpdf/fpdf.py @@ -1,20 +1,27 @@ #!/usr/bin/env python # -*- coding: latin-1 -*- -# ****************************************************************************** -# * Software: FPDF for python * -# * Version: 1.54c * -# * Date: 2010-09-10 * -# * License: LGPL v3.0 * -# * * -# * Original Author (PHP): Olivier PLATHEY 2004-12-31 * -# * Ported to Python 2.4 by Max (maxpat78@yahoo.it) on 2006-05 * -# * Maintainer: Mariano Reingart (reingart@gmail.com) et al since 2008 (est.) * -# * NOTE: 'I' and 'D' destinations are disabled, and simply print to STDOUT * -# *****************************************************************************/ +# **************************************************************************** +# * Software: FPDF for python * +# * Version: 1.7.1 * +# * Date: 2010-09-10 * +# * Last update: 2012-08-16 * +# * License: LGPL v3.0 * +# * * +# * Original Author (PHP): Olivier PLATHEY 2004-12-31 * +# * Ported to Python 2.4 by Max (maxpat78@yahoo.it) on 2006-05 * +# * Maintainer: Mariano Reingart (reingart@gmail.com) et al since 2008 est. * +# * NOTE: 'I' and 'D' destinations are disabled, and simply print to STDOUT * +# **************************************************************************** from datetime import datetime import math -import os, sys, zlib, struct +import errno +import os, sys, zlib, struct, re, tempfile, struct + +try: + import cPickle as pickle +except ImportError: + import pickle try: # Check if PIL is available, necessary for JPEG support. @@ -22,115 +29,62 @@ try: except ImportError: Image = None -def substr(s, start, length=-1): - if length < 0: - length=len(s)-start - return s[start:start+length] -def sprintf(fmt, *args): return fmt % args +from ttfonts import TTFontFile +from fonts import fpdf_charwidths +from php import substr, sprintf, print_r, UTF8ToUTF16BE, UTF8StringToArray + # Global variables -FPDF_VERSION='1.54b' -FPDF_FONT_DIR=os.path.join(os.path.dirname(__file__),'font') -fpdf_charwidths = {} +FPDF_VERSION = '1.7.1' +FPDF_FONT_DIR = os.path.join(os.path.dirname(__file__),'font') +SYSTEM_TTFONTS = None -class FPDF: -#Private properties -#~ page; #current page number -#~ n; #current object number -#~ offsets; #array of object offsets -#~ buffer; #buffer holding in-memory PDF -#~ pages; #array containing pages -#~ state; #current document state -#~ compress; #compression flag -#~ def_orientation; #default orientation -#~ cur_orientation; #current orientation -#~ orientation_changes; #array indicating orientation changes -#~ k; #scale factor (number of points in user unit) -#~ fw_pt,fh_pt; #dimensions of page format in points -#~ fw,fh; #dimensions of page format in user unit -#~ w_pt,h_pt; #current dimensions of page in points -#~ w,h; #current dimensions of page in user unit -#~ l_margin; #left margin -#~ t_margin; #top margin -#~ r_margin; #right margin -#~ b_margin; #page break margin -#~ c_margin; #cell margin -#~ x,y; #current position in user unit for cell positioning -#~ lasth; #height of last cell printed -#~ line_width; #line width in user unit -#~ core_fonts; #array of standard font names -#~ fonts; #array of used fonts -#~ font_files; #array of font files -#~ diffs; #array of encoding differences -#~ images; #array of used images -#~ page_links; #array of links in pages -#~ links; #array of internal links -#~ font_family; #current font family -#~ font_style; #current font style -#~ underline; #underlining flag -#~ current_font; #current font info -#~ font_size_pt; #current font size in points -#~ font_size; #current font size in user unit -#~ draw_color; #commands for drawing color -#~ fill_color; #commands for filling color -#~ text_color; #commands for text color -#~ color_flag; #indicates whether fill and text colors are different -#~ ws; #word spacing -#~ auto_page_break; #automatic page breaking -#~ page_break_trigger; #threshold used to trigger page breaks -#~ in_footer; #flag set when processing footer -#~ zoom_mode; #zoom display mode -#~ layout_mode; #layout display mode -#~ title; #title -#~ subject; #subject -#~ author; #author -#~ keywords; #keywords -#~ creator; #creator -#~ alias_nb_pages; #alias for total number of pages -#~ pdf_version; #PDF version number +PY3K = sys.version_info >= (3, 0) + +def set_global(var, val): + globals()[var] = val + + +class FPDF(object): + "PDF Generation class" -# ****************************************************************************** -# * * -# * Public methods * -# * * -# *******************************************************************************/ def __init__(self, orientation='P',unit='mm',format='A4'): - #Some checks + # Some checks self._dochecks() - #Initialization of properties - self.offsets={} - self.page=0 - self.n=2 - self.buffer='' - self.pages={} - self.orientation_changes={} - self.state=0 - self.fonts={} - self.font_files={} - self.diffs={} - self.images={} - self.page_links={} - self.links={} - self.in_footer=0 + # Initialization of properties + self.offsets={} # array of object offsets + self.page=0 # current page number + self.n=2 # current object number + self.buffer='' # buffer holding in-memory PDF + self.pages={} # array containing pages + self.orientation_changes={} # array indicating orientation changes + self.state=0 # current document state + self.fonts={} # array of used fonts + self.font_files={} # array of font files + self.diffs={} # array of encoding differences + self.images={} # array of used images + self.page_links={} # array of links in pages + self.links={} # array of internal links + self.in_footer=0 # flag set when processing footer self.lastw=0 - self.lasth=0 - self.font_family='' - self.font_style='' - self.font_size_pt=12 - self.underline=0 + self.lasth=0 # height of last cell printed + self.font_family='' # current font family + self.font_style='' # current font style + self.font_size_pt=12 # current font size in points + self.underline=0 # underlining flag self.draw_color='0 G' self.fill_color='0 g' self.text_color='0 g' - self.color_flag=0 - self.ws=0 + self.color_flag=0 # indicates whether fill and text colors are different + self.ws=0 # word spacing self.angle=0 - #Standard fonts + # Standard fonts self.core_fonts={'courier':'Courier','courierB':'Courier-Bold','courierI':'Courier-Oblique','courierBI':'Courier-BoldOblique', 'helvetica':'Helvetica','helveticaB':'Helvetica-Bold','helveticaI':'Helvetica-Oblique','helveticaBI':'Helvetica-BoldOblique', 'times':'Times-Roman','timesB':'Times-Bold','timesI':'Times-Italic','timesBI':'Times-BoldItalic', 'symbol':'Symbol','zapfdingbats':'ZapfDingbats'} - #Scale factor + # Scale factor if(unit=='pt'): self.k=1 elif(unit=='mm'): @@ -141,7 +95,7 @@ class FPDF: self.k=72 else: self.error('Incorrect unit: '+unit) - #Page format + # Page format if(isinstance(format,basestring)): format=format.lower() if(format=='a3'): @@ -163,7 +117,7 @@ class FPDF: self.fh_pt=format[1]*self.k self.fw=self.fw_pt/self.k self.fh=self.fh_pt/self.k - #Page orientation + # Page orientation orientation=orientation.lower() if(orientation=='p' or orientation=='portrait'): self.def_orientation='P' @@ -178,20 +132,20 @@ class FPDF: self.cur_orientation=self.def_orientation self.w=self.w_pt/self.k self.h=self.h_pt/self.k - #Page margins (1 cm) + # Page margins (1 cm) margin=28.35/self.k self.set_margins(margin,margin) - #Interior cell margin (1 mm) + # Interior cell margin (1 mm) self.c_margin=margin/10.0 - #line width (0.2 mm) + # line width (0.2 mm) self.line_width=.567/self.k - #Automatic page break + # Automatic page break self.set_auto_page_break(1,2*margin) - #Full width display mode + # Full width display mode self.set_display_mode('fullwidth') - #Enable compression + # Enable compression self.set_compression(1) - #Set default PDF version number + # Set default PDF version number self.pdf_version='1.3' def set_margins(self, left,top,right=-1): @@ -389,8 +343,20 @@ class FPDF: cw=self.current_font['cw'] w=0 l=len(s) - for i in xrange(0, l): - w += cw.get(s[i],0) + if self.unifontsubset: + for char in s: + char = ord(char) + if len(cw) > char: + w += cw[char] # ord(cw[2*char])<<8 + ord(cw[2*char+1]) + #elif (char>0 and char<128 and isset($cw[chr($char)])) { $w += $cw[chr($char)]; } + elif (self.current_font['desc']['MissingWidth']) : + w += self.current_font['desc']['MissingWidth'] + #elif (isset($this->CurrentFont['MissingWidth'])) { $w += $this->CurrentFont['MissingWidth']; } + else: + w += 500 + else: + for i in xrange(0, l): + w += cw.get(s[i],0) return w*self.font_size/1000.0 def set_line_width(self, width): @@ -413,42 +379,119 @@ class FPDF: op='S' self._out(sprintf('%.2f %.2f %.2f %.2f re %s',x*self.k,(self.h-y)*self.k,w*self.k,-h*self.k,op)) - def add_font(self, family,style='',fname=''): + def add_font(self, family, style='', fname='', uni=False): "Add a TrueType or Type1 font" - family=family.lower() - if(fname==''): - fname=family.replace(' ','')+style.lower()+'.font' - fname=os.path.join(FPDF_FONT_DIR,fname) - if(family=='arial'): - family='helvetica' - style=style.upper() - if(style=='IB'): - style='BI' - fontkey=family+style + family = family.lower() + if (fname == ''): + fname = family.replace(' ','') + style.lower() + '.pkl' + if (family == 'arial'): + family = 'helvetica' + style = style.upper() + if (style == 'IB'): + style = 'BI' + fontkey = family+style if fontkey in self.fonts: - self.error('Font already added: '+family+' '+style) - execfile(fname, globals(), globals()) - if 'name' not in globals(): - self.error('Could not include font definition file') - i=len(self.fonts)+1 - self.fonts[fontkey]={'i':i,'type':type,'name':name,'desc':desc,'up':up,'ut':ut,'cw':cw,'enc':enc,'file':filename} - if(diff): - #Search existing encodings - d=0 - nb=len(self.diffs) - for i in xrange(1,nb+1): - if(self.diffs[i]==diff): - d=i - break - if(d==0): - d=nb+1 - self.diffs[d]=diff - self.fonts[fontkey]['diff']=d - if(filename): - if(type=='TrueType'): - self.font_files[filename]={'length1':originalsize} + # Font already added! + return + if (uni): + global SYSTEM_TTFONTS + if os.path.exists(fname): + ttffilename = fname + elif (FPDF_FONT_DIR and + os.path.exists(os.path.join(FPDF_FONT_DIR, fname))): + ttffilename = os.path.join(FPDF_FONT_DIR, fname) + elif (SYSTEM_TTFONTS and + os.path.exists(os.path.join(SYSTEM_TTFONTS, fname))): + ttffilename = os.path.join(SYSTEM_TTFONTS, fname) else: - self.font_files[filename]={'length1':size1,'length2':size2} + raise RuntimeError("TTF Font file not found: %s" % fname) + unifilename = os.path.splitext(ttffilename)[0] + '.pkl' + name = '' + if os.path.exists(unifilename): + fh = open(unifilename) + try: + font_dict = pickle.load(fh) + finally: + fh.close() + else: + ttf = TTFontFile() + ttf.getMetrics(ttffilename) + desc = { + 'Ascent': int(round(ttf.ascent, 0)), + 'Descent': int(round(ttf.descent, 0)), + 'CapHeight': int(round(ttf.capHeight, 0)), + 'Flags': ttf.flags, + 'FontBBox': "[%s %s %s %s]" % ( + int(round(ttf.bbox[0], 0)), + int(round(ttf.bbox[1], 0)), + int(round(ttf.bbox[2], 0)), + int(round(ttf.bbox[3], 0))), + 'ItalicAngle': int(ttf.italicAngle), + 'StemV': int(round(ttf.stemV, 0)), + 'MissingWidth': int(round(ttf.defaultWidth, 0)), + } + # Generate metrics .pkl file + font_dict = { + 'name': re.sub('[ ()]', '', ttf.fullName), + 'type': 'TTF', + 'desc': desc, + 'up': round(ttf.underlinePosition), + 'ut': round(ttf.underlineThickness), + 'ttffile': ttffilename, + 'fontkey': fontkey, + 'originalsize': os.stat(ttffilename).st_size, + 'cw': ttf.charWidths, + } + try: + fh = open(unifilename, "w") + pickle.dump(font_dict, fh) + fh.close() + except IOError as e: + if not e.errno == errno.EACCES: + raise # Not a permission error. + del ttf + if hasattr(self,'str_alias_nb_pages'): + sbarr = range(0,57) # include numbers in the subset! + else: + sbarr = range(0,32) + self.fonts[fontkey] = { + 'i': len(self.fonts)+1, 'type': font_dict['type'], + 'name': font_dict['name'], 'desc': font_dict['desc'], + 'up': font_dict['up'], 'ut': font_dict['ut'], + 'cw': font_dict['cw'], + 'ttffile': font_dict['ttffile'], 'fontkey': fontkey, + 'subset': sbarr, 'unifilename': unifilename, + } + self.font_files[fontkey] = {'length1': font_dict['originalsize'], + 'type': "TTF", 'ttffile': ttffilename} + self.font_files[fname] = {'type': "TTF"} + else: + fontfile = open(fname) + try: + font_dict = pickle.load(fontfile) + finally: + fontfile.close() + self.fonts[fontkey] = {'i': len(self.fonts)+1} + self.fonts[fontkey].update(font_dict) + if (diff): + #Search existing encodings + d = 0 + nb = len(self.diffs) + for i in xrange(1, nb+1): + if(self.diffs[i] == diff): + d = i + break + if (d == 0): + d = nb + 1 + self.diffs[d] = diff + self.fonts[fontkey]['diff'] = d + filename = font_dict.get('filename') + if (filename): + if (type == 'TrueType'): + self.font_files[filename]={'length1': originalsize} + else: + self.font_files[filename]={'length1': size1, + 'length2': size2} def set_font(self, family,style='',size=0): "Select a font; size given in points" @@ -495,6 +538,7 @@ class FPDF: self.font_size_pt=size self.font_size=size/self.k self.current_font=self.fonts[fontkey] + self.unifontsubset = (self.fonts[fontkey]['type'] == 'TTF') if(self.page>0): self._out(sprintf('BT /F%d %.2f Tf ET',self.current_font['i'],self.font_size_pt)) @@ -527,9 +571,16 @@ class FPDF: self.page_links[self.page] = [] self.page_links[self.page] += [(x*self.k,self.h_pt-y*self.k,w*self.k,h*self.k,link),] - def text(self, x,y,txt): + def text(self, x, y, txt=''): "Output a string" - s=sprintf('BT %.2f %.2f Td (%s) Tj ET',x*self.k,(self.h-y)*self.k,self._escape(txt)) + txt = self.normalize_text(txt) + if (self.unifontsubset): + txt2 = self._escape(UTF8ToUTF16BE(txt, False)) + for uni in UTF8StringToArray(txt): + self.current_font['subset'].append(uni) + else: + txt2 = self._escape(txt) + s=sprintf('BT %.2f %.2f Td (%s) Tj ET',x*self.k,(self.h-y)*self.k, txt2) if(self.underline and txt!=''): s+=' '+self._dounderline(x,y,txt) if(self.color_flag): @@ -559,6 +610,7 @@ class FPDF: def cell(self, w,h=0,txt='',border=0,ln=0,align='',fill=0,link=''): "Output a cell" + txt = self.normalize_text(txt) k=self.k if(self.y+h>self.page_break_trigger and not self.in_footer and self.accept_page_break()): #Automatic page break @@ -604,8 +656,33 @@ class FPDF: dx=self.c_margin if(self.color_flag): s+='q '+self.text_color+' ' - txt2=txt.replace('\\','\\\\').replace(')','\\)').replace('(','\\(') - s+=sprintf('BT %.2f %.2f Td (%s) Tj ET',(self.x+dx)*k,(self.h-(self.y+.5*h+.3*self.font_size))*k,txt2) + + # If multibyte, Tw has no effect - do word spacing using an adjustment before each space + if (self.ws and self.unifontsubset): + for uni in UTF8StringToArray(txt): + self.current_font['subset'].append(uni) + space = self._escape(UTF8ToUTF16BE(' ', False)) + s += sprintf('BT 0 Tw %.2F %.2F Td [',(self.x + dx) * k,(self.h - (self.y + 0.5*h+ 0.3 * self.font_size)) * k) + t = txt.split(' ') + numt = len(t) + for i in range(numt): + tx = t[i] + tx = '(' + self._escape(UTF8ToUTF16BE(tx, False)) + ')' + s += sprintf('%s ', tx); + if ((i+1)0): self.ws=0 - self._out('0 Tw') + if not split_only: + self._out('0 Tw') if not split_only: self.cell(w,h,substr(s,j,i-j),b,2,align,fill) else: @@ -681,7 +760,10 @@ class FPDF: sep=i ls=l ns+=1 - l+=cw.get(c,0) + if self.unifontsubset: + l += self.get_string_width(c) / self.font_size*1000.0 + else: + l += cw.get(c,0) if(l>wmax): #Automatic line break if(sep==-1): @@ -689,7 +771,8 @@ class FPDF: i+=1 if(self.ws>0): self.ws=0 - self._out('0 Tw') + if not split_only: + self._out('0 Tw') if not split_only: self.cell(w,h,substr(s,j,i-j),b,2,align,fill) else: @@ -700,7 +783,8 @@ class FPDF: self.ws=(wmax-ls)/1000.0*self.font_size/(ns-1) else: self.ws=0 - self._out(sprintf('%.3f Tw',self.ws*self.k)) + if not split_only: + self._out(sprintf('%.3f Tw',self.ws*self.k)) if not split_only: self.cell(w,h,substr(s,j,sep-j),b,2,align,fill) else: @@ -718,18 +802,20 @@ class FPDF: #Last chunk if(self.ws>0): self.ws=0 - self._out('0 Tw') + if not split_only: + self._out('0 Tw') if(border and 'B' in border): b+='B' if not split_only: self.cell(w,h,substr(s,j,i-j),b,2,align,fill) + self.x=self.l_margin else: ret.append(substr(s,j,i-j)) - self.x=self.l_margin return ret - def write(self, h,txt,link=''): + def write(self, h, txt='', link=''): "Output text in flowing mode" + txt = self.normalize_text(txt) cw=self.current_font['cw'] w=self.w-self.r_margin-self.x wmax=(w-2*self.c_margin)*1000.0/self.font_size @@ -758,7 +844,10 @@ class FPDF: continue if(c==' '): sep=i - l+=cw.get(c,0) + if self.unifontsubset: + l += self.get_string_width(c) / self.font_size*1000.0 + else: + l += cw.get(c,0) if(l>wmax): #Automatic line break if(sep==-1): @@ -791,7 +880,7 @@ class FPDF: if(i!=j): self.cell(l/1000.0*self.font_size,h,substr(s,j),0,0,'',0,link) - def image(self, name,x,y,w=0,h=0,type='',link=''): + def image(self, name, x=None, y=None, w=0,h=0,type='',link=''): "Put an image on the page" if not name in self.images: #First use of image, get info @@ -810,7 +899,7 @@ class FPDF: mtd='_parse'+type if not hasattr(self,mtd): self.error('Unsupported image type: '+type) - info=self.mtd(name) + info=getattr(self, mtd)(name) info['i']=len(self.images)+1 self.images[name]=info else: @@ -820,10 +909,21 @@ class FPDF: #Put image at 72 dpi w=info['w']/self.k h=info['h']/self.k - if(w==0): + elif(w==0): w=h*info['w']/info['h'] - if(h==0): + elif(h==0): h=w*info['h']/info['w'] + # Flowing mode + if y is None: + if (self.y + h > self.page_break_trigger and not self.in_footer and self.accept_page_break()): + #Automatic page break + x = self.x + self.add_page(self.cur_orientation) + self.x = x + y = self.y + self.y += h + if x is None: + x = self.x self._out(sprintf('q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q',w*self.k,h*self.k,x*self.k,(self.h-(y+h))*self.k,info['i'])) if(link): self.link(x,y,w,h,link) @@ -869,12 +969,6 @@ class FPDF: #Finish document if necessary if(self.state<3): self.close() - #Normalize parameters - # if(type(dest)==type(bool())): - # if dest: - # dest='D' - # else: - # dest='F' dest=dest.upper() if(dest==''): if(name==''): @@ -888,10 +982,14 @@ class FPDF: print self.buffer elif dest=='F': #Save to local file - f=file(name,'wb') + f=open(name,'wb') if(not f): self.error('Unable to create output file: '+name) - f.write(self.buffer) + if PY3K: + # TODO: proper unicode support + f.write(self.buffer.encode("latin1")) + else: + f.write(self.buffer) f.close() elif dest=='S': #Return as a string @@ -900,11 +998,17 @@ class FPDF: self.error('Incorrect output destination: '+dest) return '' -# ****************************************************************************** -# * * -# * Protected methods * -# * * -# *******************************************************************************/ + def normalize_text(self, txt): + "Check that text input is in the correct format/encoding" + # - for TTF unicode fonts: unicode object (utf8 encoding) + # - for built-in fonts: string instances (latin 1 encoding) + if self.unifontsubset and isinstance(txt, str): + txt = txt.decode('utf8') + elif not self.unifontsubset and isinstance(txt, unicode) and not PY3K: + txt = txt.encode('latin1') + return txt + + def _dochecks(self): #Check for locale-related bug # if(1.1==1): @@ -920,7 +1024,12 @@ class FPDF: def _putpages(self): nb=self.page if hasattr(self,'str_alias_nb_pages'): - #Replace number of pages + # Replace number of pages in fonts using subsets (unicode) + alias = UTF8ToUTF16BE(self.str_alias_nb_pages, False); + r = UTF8ToUTF16BE(str(nb), False) + for n in xrange(1, nb+1): + self.pages[n] = self.pages[n].replace(alias, r) + # Now repeat for no pages in non-subset fonts for n in xrange(1,nb+1): self.pages[n]=self.pages[n].replace(self.str_alias_nb_pages,str(nb)) if(self.def_orientation=='P'): @@ -957,6 +1066,8 @@ class FPDF: h=h_pt annots+=sprintf('/Dest [%d 0 R /XYZ 0 %.2f null]>>',1+2*l[0],h-l[1]*self.k) self._out(annots+']') + if(self.pdf_version>'1.3'): + self._out('/Group <>') self._out('/Contents '+str(self.n+1)+' 0 R>>') self._out('endobj') #Page content @@ -989,33 +1100,34 @@ class FPDF: self._out('<>') self._out('endobj') for name,info in self.font_files.iteritems(): - #Font file embedding - self._newobj() - self.font_files[name]['n']=self.n - font='' - f=file(self._getfontpath()+name,'rb',1) - if(not f): - self.error('Font file not found') - font=f.read() - f.close() - compressed=(substr(name,-2)=='.z') - if(not compressed and 'length2' in info): - header=(ord(font[0])==128) - if(header): - #Strip first binary header - font=substr(font,6) - if(header and ord(font[info['length1']])==128): - #Strip second binary header - font=substr(font,0,info['length1'])+substr(font,info['length1']+6) - self._out('<>') - self._putstream(font) - self._out('endobj') + if 'type' in info and info['type'] != 'TTF': + #Font file embedding + self._newobj() + self.font_files[name]['n']=self.n + font='' + f=open(self._getfontpath()+name,'rb',1) + if(not f): + self.error('Font file not found') + font=f.read() + f.close() + compressed=(substr(name,-2)=='.z') + if(not compressed and 'length2' in info): + header=(ord(font[0])==128) + if(header): + #Strip first binary header + font=substr(font,6) + if(header and ord(font[info['length1']])==128): + #Strip second binary header + font=substr(font,0,info['length1'])+substr(font,info['length1']+6) + self._out('<>') + self._putstream(font) + self._out('endobj') for k,font in self.fonts.iteritems(): #Font objects self.fonts[k]['n']=self.n+1 @@ -1059,8 +1171,8 @@ class FPDF: #Descriptor self._newobj() s='<>') self._out('endobj') + elif (type == 'TTF'): + self.fonts[k]['n'] = self.n + 1 + ttf = TTFontFile() + fontname = 'MPDFAA' + '+' + font['name'] + subset = font['subset'] + del subset[0] + ttfontstream = ttf.makeSubset(font['ttffile'], subset) + ttfontsize = len(ttfontstream) + fontstream = zlib.compress(ttfontstream) + codeToGlyph = ttf.codeToGlyph + ##del codeToGlyph[0] + # Type0 Font + # A composite font - a font composed of other fonts, organized hierarchically + self._newobj() + self._out('<>') + self._out('endobj') + + # CIDFontType2 + # A CIDFont whose glyph descriptions are based on TrueType font technology + self._newobj() + self._out('<>') + self._out('endobj') + + # ToUnicode + self._newobj() + toUni = "/CIDInit /ProcSet findresource begin\n" \ + "12 dict begin\n" \ + "begincmap\n" \ + "/CIDSystemInfo\n" \ + "<> def\n" \ + "/CMapName /Adobe-Identity-UCS def\n" \ + "/CMapType 2 def\n" \ + "1 begincodespacerange\n" \ + "<0000> \n" \ + "endcodespacerange\n" \ + "1 beginbfrange\n" \ + "<0000> <0000>\n" \ + "endbfrange\n" \ + "endcmap\n" \ + "CMapName currentdict /CMap defineresource pop\n" \ + "end\n" \ + "end" + self._out('<>') + self._putstream(toUni) + self._out('endobj') + + # CIDSystemInfo dictionary + self._newobj() + self._out('<>') + self._out('endobj') + + # Font descriptor + self._newobj() + self._out('<>') + self._out('endobj') + + # Embed CIDToGIDMap + # A specification of the mapping from CIDs to glyph indices + cidtogidmap = ''; + cidtogidmap = ["\x00"] * 256*256*2 + for cc, glyph in codeToGlyph.items(): + cidtogidmap[cc*2] = chr(glyph >> 8) + cidtogidmap[cc*2 + 1] = chr(glyph & 0xFF) + cidtogidmap = zlib.compress(''.join(cidtogidmap)); + self._newobj() + self._out('<>') + self._putstream(cidtogidmap) + self._out('endobj') + + #Font file + self._newobj() + self._out('<>') + self._putstream(fontstream) + self._out('endobj') + del ttf else: #Allow for additional types mtd='_put'+type.lower() @@ -1076,13 +1298,121 @@ class FPDF: self.error('Unsupported font type: '+type) self.mtd(font) + def _putTTfontwidths(self, font, maxUni): + cw127fname = os.path.splitext(font['unifilename'])[0] + '.cw127.pkl' + if (os.path.exists(cw127fname)): + fh = open(cw127fname); + try: + font_dict = pickle.load(fh) + finally: + fh.close() + rangeid = font_dict['rangeid'] + range_ = font_dict['range'] + prevcid = font_dict['prevcid'] + prevwidth = font_dict['prevwidth'] + interval = font_dict['interval'] + range_interval = font_dict['range_interval'] + startcid = 128 + else: + rangeid = 0 + range_ = {} + range_interval = {} + prevcid = -2 + prevwidth = -1 + interval = False + startcid = 1 + cwlen = maxUni + 1 + + # for each character + for cid in range(startcid, cwlen): + if (cid==128 and not os.path.exists(cw127fname)): + try: + fh = open(cw127fname, "wb") + font_dict = {} + font_dict['rangeid'] = rangeid + font_dict['prevcid'] = prevcid + font_dict['prevwidth'] = prevwidth + font_dict['interval'] = interval + font_dict['range_interval'] = range_interval + font_dict['range'] = range_ + pickle.dump(font_dict, fh) + fh.close() + except IOError as e: + if not e.errno == errno.EACCES: + raise # Not a permission error. + if (font['cw'][cid] == 0): + continue + width = font['cw'][cid] + if (width == 65535): width = 0 + if (cid > 255 and (cid not in font['subset']) or not cid): # + continue + if ('dw' not in font or (font['dw'] and width != font['dw'])): + if (cid == (prevcid + 1)): + if (width == prevwidth): + if (width == range_[rangeid][0]): + range_.setdefault(rangeid, []).append(width) + else: + range_[rangeid].pop() + # new range + rangeid = prevcid + range_[rangeid] = [prevwidth, width] + interval = True + range_interval[rangeid] = True + else: + if (interval): + # new range + rangeid = cid + range_[rangeid] = [width] + else: + range_[rangeid].append(width) + interval = False + else: + rangeid = cid + range_[rangeid] = [width] + interval = False + prevcid = cid + prevwidth = width + prevk = -1 + nextk = -1 + prevint = False + for k, ws in sorted(range_.items()): + cws = len(ws) + if (k == nextk and not prevint and (not k in range_interval or cws < 3)): + if (k in range_interval): + del range_interval[k] + range_[prevk] = range_[prevk] + range_[k] + del range_[k] + else: + prevk = k + nextk = k + cws + if (k in range_interval): + prevint = (cws > 3) + del range_interval[k] + nextk -= 1 + else: + prevint = False + w = [] + for k, ws in sorted(range_.items()): + if (len(set(ws)) == 1): + w.append(' %s %s %s' % (k, k + len(ws) - 1, ws[0])) + else: + w.append(' %s [ %s ]\n' % (k, ' '.join([str(int(h)) for h in ws]))) ## + self._out('/W [%s]' % ''.join(w)) + def _putimages(self): filter='' if self.compress: filter='/Filter /FlateDecode ' for filename,info in self.images.iteritems(): + self._putimage(info) + del info['data'] + if 'smask' in info: + del info['smask'] + + def _putimage(self, info): + if 'data' in info: self._newobj() - self.images[filename]['n']=self.n + info['n']=self.n self._out('<>') + if('trns' in info and isinstance(info['trns'], list)): trns='' for i in xrange(0,len(info['trns'])): trns+=str(info['trns'][i])+' '+str(info['trns'][i])+' ' self._out('/Mask ['+trns+']') + if('smask' in info): + self._out('/SMask ' + str(self.n+1) + ' 0 R'); self._out('/Length '+str(len(info['data']))+'>>') self._putstream(info['data']) - self.images[filename]['data'] = None self._out('endobj') + # Soft mask + if('smask' in info): + dp = '/Predictor 15 /Colors 1 /BitsPerComponent 8 /Columns ' + str(info['w']) + smask = {'w': info['w'], 'h': info['h'], 'cs': 'DeviceGray', 'bpc': 8, 'f': info['f'], 'dp': dp, 'data': info['smask']} + self._putimage(smask) #Palette if(info['cs']=='Indexed'): self._newobj() + filter = self.compress and '/Filter /FlateDecode ' or '' if self.compress: pal=zlib.compress(info['pal']) else: @@ -1288,6 +1625,25 @@ class FPDF: f.close() return {'w':a[0],'h':a[1],'cs':colspace,'bpc':bpc,'f':'DCTDecode','data':data} + def _parsegif(self, filename): + # Extract info from a GIF file (via PNG conversion) + if Image is None: + self.error('PIL is required for GIF support') + try: + im = Image.open(filename) + except Exception, e: + self.error('Missing or incorrect image file: %s. error: %s' % (filename, str(e))) + else: + # Use temporary file + f = tempfile.NamedTemporaryFile(delete=False, suffix=".png") + tmp = f.name + transparency = im.info['transparency'] + f.close() + im.save(tmp, transparency=transparency) + info = self._parsepng(tmp) + os.unlink(tmp) + return info + def _parsepng(self, name): #Extract info from a PNG file if name.startswith("http://") or name.startswith("https://"): @@ -1310,14 +1666,14 @@ class FPDF: if(bpc>8): self.error('16-bit depth not supported: '+name) ct=ord(f.read(1)) - if(ct==0): + if(ct==0 or ct==4): colspace='DeviceGray' - elif(ct==2): + elif(ct==2 or ct==6): colspace='DeviceRGB' elif(ct==3): colspace='Indexed' else: - self.error('Alpha channel not supported: '+name) + self.error('Unknown color type: '+name) if(ord(f.read(1))!=0): self.error('Unknown compression method: '+name) if(ord(f.read(1))!=0): @@ -1325,12 +1681,12 @@ class FPDF: if(ord(f.read(1))!=0): self.error('Interlacing not supported: '+name) f.read(4) - parms='/DecodeParms <>' + dp+='1' + dp+=' /BitsPerComponent '+str(bpc)+' /Columns '+str(w)+'' #Scan chunks looking for palette, transparency and image data pal='' trns='' @@ -1366,7 +1722,39 @@ class FPDF: if(colspace=='Indexed' and not pal): self.error('Missing palette in '+name) f.close() - return {'w':w,'h':h,'cs':colspace,'bpc':bpc,'f':'FlateDecode','parms':parms,'pal':pal,'trns':trns,'data':data} + info = {'w':w,'h':h,'cs':colspace,'bpc':bpc,'f':'FlateDecode','dp':dp,'pal':pal,'trns':trns,} + if(ct>=4): + # Extract alpha channel + data = zlib.decompress(data) + color = ''; + alpha = ''; + if(ct==4): + # Gray image + length = 2*w + for i in range(h): + pos = (1+length)*i + color += data[pos] + alpha += data[pos] + line = substr(data, pos+1, length) + color += re.sub('(.).',lambda m: m.group(1),line, flags=re.DOTALL) + alpha += re.sub('.(.)',lambda m: m.group(1),line, flags=re.DOTALL) + else: + # RGB image + length = 4*w + for i in range(h): + pos = (1+length)*i + color += data[pos] + alpha += data[pos] + line = substr(data, pos+1, length) + color += re.sub('(.{3}).',lambda m: m.group(1),line, flags=re.DOTALL) + alpha += re.sub('.{3}(.)',lambda m: m.group(1),line, flags=re.DOTALL) + del data + data = zlib.compress(color) + info['smask'] = zlib.compress(alpha) + if (self.pdf_version < '1.4'): + self.pdf_version = '1.4' + info['data'] = data + return info def _freadint(self, f): #Read a 4-byte integer from file @@ -1401,19 +1789,9 @@ class FPDF: wide = w # wide/narrow codes for the digits - bar_char={} - bar_char['0'] = 'nnwwn' - bar_char['1'] = 'wnnnw' - bar_char['2'] = 'nwnnw' - bar_char['3'] = 'wwnnn' - bar_char['4'] = 'nnwnw' - bar_char['5'] = 'wnwnn' - bar_char['6'] = 'nwwnn' - bar_char['7'] = 'nnnww' - bar_char['8'] = 'wnnwn' - bar_char['9'] = 'nwnwn' - bar_char['A'] = 'nn' - bar_char['Z'] = 'wn' + bar_char={'0': 'nnwwn', '1': 'wnnnw', '2': 'nwnnw', '3': 'wwnnn', + '4': 'nnwnw', '5': 'wnwnn', '6': 'nwwnn', '7': 'nnnww', + '8': 'wnnwn', '9': 'nwnwn', 'A': 'nn', 'Z': 'wn'} self.set_fill_color(0) code = txt @@ -1426,15 +1804,15 @@ class FPDF: for i in xrange(0, len(code), 2): # choose next pair of digits - char_bar = code[i]; - char_space = code[i+1]; + char_bar = code[i] + char_space = code[i+1] # check whether it is a valid digit if not char_bar in bar_char.keys(): - raise RuntimeError ('Caractér "%s" inválido para el código de barras I25: ' % char_bar) + raise RuntimeError ('Char "%s" invalid for I25: ' % char_bar) if not char_space in bar_char.keys(): - raise RuntimeError ('Caractér "%s" inválido para el código de barras I25: ' % char_space) + raise RuntimeError ('Char "%s" invalid for I25: ' % char_space) - # create a wide/narrow-sequence (first digit=bars, second digit=spaces) + # create a wide/narrow-seq (first digit=bars, second digit=spaces) seq = '' for s in xrange(0, len(bar_char[char_bar])): seq += bar_char[char_bar][s] + bar_char[char_space][s] @@ -1446,7 +1824,7 @@ class FPDF: else: line_width = wide - # draw every second value, because the second digit of the pair is represented by the spaces + # draw every second value, the other is represented by space if bar % 2 == 0: self.rect(x, y, line_width, h, 'F') @@ -1456,64 +1834,34 @@ class FPDF: def code39(self, txt, x, y, w=1.5, h=5.0): "Barcode 3of9" wide = w - narrow = w /3.0 + narrow = w / 3.0 gap = narrow - bar_char={} - bar_char['0'] = 'nnnwwnwnn' - bar_char['1'] = 'wnnwnnnnw' - bar_char['2'] = 'nnwwnnnnw' - bar_char['3'] = 'wnwwnnnnn' - bar_char['4'] = 'nnnwwnnnw' - bar_char['5'] = 'wnnwwnnnn' - bar_char['6'] = 'nnwwwnnnn' - bar_char['7'] = 'nnnwnnwnw' - bar_char['8'] = 'wnnwnnwnn' - bar_char['9'] = 'nnwwnnwnn' - bar_char['A'] = 'wnnnnwnnw' - bar_char['B'] = 'nnwnnwnnw' - bar_char['C'] = 'wnwnnwnnn' - bar_char['D'] = 'nnnnwwnnw' - bar_char['E'] = 'wnnnwwnnn' - bar_char['F'] = 'nnwnwwnnn' - bar_char['G'] = 'nnnnnwwnw' - bar_char['H'] = 'wnnnnwwnn' - bar_char['I'] = 'nnwnnwwnn' - bar_char['J'] = 'nnnnwwwnn' - bar_char['K'] = 'wnnnnnnww' - bar_char['L'] = 'nnwnnnnww' - bar_char['M'] = 'wnwnnnnwn' - bar_char['N'] = 'nnnnwnnww' - bar_char['O'] = 'wnnnwnnwn' - bar_char['P'] = 'nnwnwnnwn' - bar_char['Q'] = 'nnnnnnwww' - bar_char['R'] = 'wnnnnnwwn' - bar_char['S'] = 'nnwnnnwwn' - bar_char['T'] = 'nnnnwnwwn' - bar_char['U'] = 'wwnnnnnnw' - bar_char['V'] = 'nwwnnnnnw' - bar_char['W'] = 'wwwnnnnnn' - bar_char['X'] = 'nwnnwnnnw' - bar_char['Y'] = 'wwnnwnnnn' - bar_char['Z'] = 'nwwnwnnnn' - bar_char['-'] = 'nwnnnnwnw' - bar_char['.'] = 'wwnnnnwnn' - bar_char[' '] = 'nwwnnnwnn' - bar_char['*'] = 'nwnnwnwnn' - bar_char['$'] = 'nwnwnwnnn' - bar_char['/'] = 'nwnwnnnwn' - bar_char['+'] = 'nwnnnwnwn' - bar_char['%'] = 'nnnwnwnwn' + bar_char={'0': 'nnnwwnwnn', '1': 'wnnwnnnnw', '2': 'nnwwnnnnw', + '3': 'wnwwnnnnn', '4': 'nnnwwnnnw', '5': 'wnnwwnnnn', + '6': 'nnwwwnnnn', '7': 'nnnwnnwnw', '8': 'wnnwnnwnn', + '9': 'nnwwnnwnn', 'A': 'wnnnnwnnw', 'B': 'nnwnnwnnw', + 'C': 'wnwnnwnnn', 'D': 'nnnnwwnnw', 'E': 'wnnnwwnnn', + 'F': 'nnwnwwnnn', 'G': 'nnnnnwwnw', 'H': 'wnnnnwwnn', + 'I': 'nnwnnwwnn', 'J': 'nnnnwwwnn', 'K': 'wnnnnnnww', + 'L': 'nnwnnnnww', 'M': 'wnwnnnnwn', 'N': 'nnnnwnnww', + 'O': 'wnnnwnnwn', 'P': 'nnwnwnnwn', 'Q': 'nnnnnnwww', + 'R': 'wnnnnnwwn', 'S': 'nnwnnnwwn', 'T': 'nnnnwnwwn', + 'U': 'wwnnnnnnw', 'V': 'nwwnnnnnw', 'W': 'wwwnnnnnn', + 'X': 'nwnnwnnnw', 'Y': 'wwnnwnnnn', 'Z': 'nwwnwnnnn', + '-': 'nwnnnnwnw', '.': 'wwnnnnwnn', ' ': 'nwwnnnwnn', + '*': 'nwnnwnwnn', '$': 'nwnwnwnnn', '/': 'nwnwnnnwn', + '+': 'nwnnnwnwn', '%': 'nnnwnwnwn'} self.set_fill_color(0) code = txt code = code.upper() for i in xrange (0, len(code), 2): - char_bar = code[i]; + char_bar = code[i] if not char_bar in bar_char.keys(): - raise RuntimeError ('Caracter "%s" inválido para el código de barras' % char_bar) + raise RuntimeError ('Char "%s" invalid for Code39' % char_bar) seq= '' for s in xrange(0, len(bar_char[char_bar])): @@ -1526,162 +1874,9 @@ class FPDF: line_width = wide if bar % 2 == 0: - self.rect(x,y,line_width,h,'F') + self.rect(x, y, line_width, h, 'F') x += line_width x += gap -#End of class - -# Fonts: - -fpdf_charwidths['courier']={} - -for i in xrange(0,256): - fpdf_charwidths['courier'][chr(i)]=600 - fpdf_charwidths['courierB']=fpdf_charwidths['courier'] - fpdf_charwidths['courierI']=fpdf_charwidths['courier'] - fpdf_charwidths['courierBI']=fpdf_charwidths['courier'] - -fpdf_charwidths['helvetica']={ - '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, - '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':278,'"':355,'#':556,'$':556,'%':889,'&':667,'\'':191,'(':333,')':333,'*':389,'+':584, - ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':278,';':278,'<':584,'=':584,'>':584,'?':556,'@':1015,'A':667, - 'B':667,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':500,'K':667,'L':556,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, - 'X':667,'Y':667,'Z':611,'[':278,'\\':278,']':278,'^':469,'_':556,'`':333,'a':556,'b':556,'c':500,'d':556,'e':556,'f':278,'g':556,'h':556,'i':222,'j':222,'k':500,'l':222,'m':833, - 'n':556,'o':556,'p':556,'q':556,'r':333,'s':500,'t':278,'u':556,'v':500,'w':722,'x':500,'y':500,'z':500,'{':334,'|':260,'}':334,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':222,'\x83':556, - '\x84':333,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':222,'\x92':222,'\x93':333,'\x94':333,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, - '\x9a':500,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':260,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, - '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':556,'\xb6':537,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':667,'\xc1':667,'\xc2':667,'\xc3':667,'\xc4':667,'\xc5':667, - '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':500,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':556,'\xf1':556, - '\xf2':556,'\xf3':556,'\xf4':556,'\xf5':556,'\xf6':556,'\xf7':584,'\xf8':611,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':500,'\xfe':556,'\xff':500} - -fpdf_charwidths['helveticaB']={ - '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, - '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':333,'"':474,'#':556,'$':556,'%':889,'&':722,'\'':238,'(':333,')':333,'*':389,'+':584, - ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':333,';':333,'<':584,'=':584,'>':584,'?':611,'@':975,'A':722, - 'B':722,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':556,'K':722,'L':611,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, - 'X':667,'Y':667,'Z':611,'[':333,'\\':278,']':333,'^':584,'_':556,'`':333,'a':556,'b':611,'c':556,'d':611,'e':556,'f':333,'g':611,'h':611,'i':278,'j':278,'k':556,'l':278,'m':889, - 'n':611,'o':611,'p':611,'q':611,'r':389,'s':556,'t':333,'u':611,'v':556,'w':778,'x':556,'y':556,'z':500,'{':389,'|':280,'}':389,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':278,'\x83':556, - '\x84':500,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':278,'\x92':278,'\x93':500,'\x94':500,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, - '\x9a':556,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':280,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, - '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':611,'\xb6':556,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, - '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':556,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':611,'\xf1':611, - '\xf2':611,'\xf3':611,'\xf4':611,'\xf5':611,'\xf6':611,'\xf7':584,'\xf8':611,'\xf9':611,'\xfa':611,'\xfb':611,'\xfc':611,'\xfd':556,'\xfe':611,'\xff':556 -} - -fpdf_charwidths['helveticaBI']={ - '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, - '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':333,'"':474,'#':556,'$':556,'%':889,'&':722,'\'':238,'(':333,')':333,'*':389,'+':584, - ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':333,';':333,'<':584,'=':584,'>':584,'?':611,'@':975,'A':722, - 'B':722,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':556,'K':722,'L':611,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, - 'X':667,'Y':667,'Z':611,'[':333,'\\':278,']':333,'^':584,'_':556,'`':333,'a':556,'b':611,'c':556,'d':611,'e':556,'f':333,'g':611,'h':611,'i':278,'j':278,'k':556,'l':278,'m':889, - 'n':611,'o':611,'p':611,'q':611,'r':389,'s':556,'t':333,'u':611,'v':556,'w':778,'x':556,'y':556,'z':500,'{':389,'|':280,'}':389,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':278,'\x83':556, - '\x84':500,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':278,'\x92':278,'\x93':500,'\x94':500,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, - '\x9a':556,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':280,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, - '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':611,'\xb6':556,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, - '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':556,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':611,'\xf1':611, - '\xf2':611,'\xf3':611,'\xf4':611,'\xf5':611,'\xf6':611,'\xf7':584,'\xf8':611,'\xf9':611,'\xfa':611,'\xfb':611,'\xfc':611,'\xfd':556,'\xfe':611,'\xff':556} - -fpdf_charwidths['helveticaI']={ - '\x00':278,'\x01':278,'\x02':278,'\x03':278,'\x04':278,'\x05':278,'\x06':278,'\x07':278,'\x08':278,'\t':278,'\n':278,'\x0b':278,'\x0c':278,'\r':278,'\x0e':278,'\x0f':278,'\x10':278,'\x11':278,'\x12':278,'\x13':278,'\x14':278,'\x15':278, - '\x16':278,'\x17':278,'\x18':278,'\x19':278,'\x1a':278,'\x1b':278,'\x1c':278,'\x1d':278,'\x1e':278,'\x1f':278,' ':278,'!':278,'"':355,'#':556,'$':556,'%':889,'&':667,'\'':191,'(':333,')':333,'*':389,'+':584, - ',':278,'-':333,'.':278,'/':278,'0':556,'1':556,'2':556,'3':556,'4':556,'5':556,'6':556,'7':556,'8':556,'9':556,':':278,';':278,'<':584,'=':584,'>':584,'?':556,'@':1015,'A':667, - 'B':667,'C':722,'D':722,'E':667,'F':611,'G':778,'H':722,'I':278,'J':500,'K':667,'L':556,'M':833,'N':722,'O':778,'P':667,'Q':778,'R':722,'S':667,'T':611,'U':722,'V':667,'W':944, - 'X':667,'Y':667,'Z':611,'[':278,'\\':278,']':278,'^':469,'_':556,'`':333,'a':556,'b':556,'c':500,'d':556,'e':556,'f':278,'g':556,'h':556,'i':222,'j':222,'k':500,'l':222,'m':833, - 'n':556,'o':556,'p':556,'q':556,'r':333,'s':500,'t':278,'u':556,'v':500,'w':722,'x':500,'y':500,'z':500,'{':334,'|':260,'}':334,'~':584,'\x7f':350,'\x80':556,'\x81':350,'\x82':222,'\x83':556, - '\x84':333,'\x85':1000,'\x86':556,'\x87':556,'\x88':333,'\x89':1000,'\x8a':667,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':222,'\x92':222,'\x93':333,'\x94':333,'\x95':350,'\x96':556,'\x97':1000,'\x98':333,'\x99':1000, - '\x9a':500,'\x9b':333,'\x9c':944,'\x9d':350,'\x9e':500,'\x9f':667,'\xa0':278,'\xa1':333,'\xa2':556,'\xa3':556,'\xa4':556,'\xa5':556,'\xa6':260,'\xa7':556,'\xa8':333,'\xa9':737,'\xaa':370,'\xab':556,'\xac':584,'\xad':333,'\xae':737,'\xaf':333, - '\xb0':400,'\xb1':584,'\xb2':333,'\xb3':333,'\xb4':333,'\xb5':556,'\xb6':537,'\xb7':278,'\xb8':333,'\xb9':333,'\xba':365,'\xbb':556,'\xbc':834,'\xbd':834,'\xbe':834,'\xbf':611,'\xc0':667,'\xc1':667,'\xc2':667,'\xc3':667,'\xc4':667,'\xc5':667, - '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':278,'\xcd':278,'\xce':278,'\xcf':278,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':584,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':667,'\xde':667,'\xdf':611,'\xe0':556,'\xe1':556,'\xe2':556,'\xe3':556,'\xe4':556,'\xe5':556,'\xe6':889,'\xe7':500,'\xe8':556,'\xe9':556,'\xea':556,'\xeb':556,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':556,'\xf1':556, - '\xf2':556,'\xf3':556,'\xf4':556,'\xf5':556,'\xf6':556,'\xf7':584,'\xf8':611,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':500,'\xfe':556,'\xff':500} - -fpdf_charwidths['symbol']={ - '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, - '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':713,'#':500,'$':549,'%':833,'&':778,'\'':439,'(':333,')':333,'*':500,'+':549, - ',':250,'-':549,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':278,';':278,'<':549,'=':549,'>':549,'?':444,'@':549,'A':722, - 'B':667,'C':722,'D':612,'E':611,'F':763,'G':603,'H':722,'I':333,'J':631,'K':722,'L':686,'M':889,'N':722,'O':722,'P':768,'Q':741,'R':556,'S':592,'T':611,'U':690,'V':439,'W':768, - 'X':645,'Y':795,'Z':611,'[':333,'\\':863,']':333,'^':658,'_':500,'`':500,'a':631,'b':549,'c':549,'d':494,'e':439,'f':521,'g':411,'h':603,'i':329,'j':603,'k':549,'l':549,'m':576, - 'n':521,'o':549,'p':549,'q':521,'r':549,'s':603,'t':439,'u':576,'v':713,'w':686,'x':493,'y':686,'z':494,'{':480,'|':200,'}':480,'~':549,'\x7f':0,'\x80':0,'\x81':0,'\x82':0,'\x83':0, - '\x84':0,'\x85':0,'\x86':0,'\x87':0,'\x88':0,'\x89':0,'\x8a':0,'\x8b':0,'\x8c':0,'\x8d':0,'\x8e':0,'\x8f':0,'\x90':0,'\x91':0,'\x92':0,'\x93':0,'\x94':0,'\x95':0,'\x96':0,'\x97':0,'\x98':0,'\x99':0, - '\x9a':0,'\x9b':0,'\x9c':0,'\x9d':0,'\x9e':0,'\x9f':0,'\xa0':750,'\xa1':620,'\xa2':247,'\xa3':549,'\xa4':167,'\xa5':713,'\xa6':500,'\xa7':753,'\xa8':753,'\xa9':753,'\xaa':753,'\xab':1042,'\xac':987,'\xad':603,'\xae':987,'\xaf':603, - '\xb0':400,'\xb1':549,'\xb2':411,'\xb3':549,'\xb4':549,'\xb5':713,'\xb6':494,'\xb7':460,'\xb8':549,'\xb9':549,'\xba':549,'\xbb':549,'\xbc':1000,'\xbd':603,'\xbe':1000,'\xbf':658,'\xc0':823,'\xc1':686,'\xc2':795,'\xc3':987,'\xc4':768,'\xc5':768, - '\xc6':823,'\xc7':768,'\xc8':768,'\xc9':713,'\xca':713,'\xcb':713,'\xcc':713,'\xcd':713,'\xce':713,'\xcf':713,'\xd0':768,'\xd1':713,'\xd2':790,'\xd3':790,'\xd4':890,'\xd5':823,'\xd6':549,'\xd7':250,'\xd8':713,'\xd9':603,'\xda':603,'\xdb':1042, - '\xdc':987,'\xdd':603,'\xde':987,'\xdf':603,'\xe0':494,'\xe1':329,'\xe2':790,'\xe3':790,'\xe4':786,'\xe5':713,'\xe6':384,'\xe7':384,'\xe8':384,'\xe9':384,'\xea':384,'\xeb':384,'\xec':494,'\xed':494,'\xee':494,'\xef':494,'\xf0':0,'\xf1':329, - '\xf2':274,'\xf3':686,'\xf4':686,'\xf5':686,'\xf6':384,'\xf7':384,'\xf8':384,'\xf9':384,'\xfa':384,'\xfb':384,'\xfc':494,'\xfd':494,'\xfe':494,'\xff':0} - -fpdf_charwidths['times']={ - '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, - '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':408,'#':500,'$':500,'%':833,'&':778,'\'':180,'(':333,')':333,'*':500,'+':564, - ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':278,';':278,'<':564,'=':564,'>':564,'?':444,'@':921,'A':722, - 'B':667,'C':667,'D':722,'E':611,'F':556,'G':722,'H':722,'I':333,'J':389,'K':722,'L':611,'M':889,'N':722,'O':722,'P':556,'Q':722,'R':667,'S':556,'T':611,'U':722,'V':722,'W':944, - 'X':722,'Y':722,'Z':611,'[':333,'\\':278,']':333,'^':469,'_':500,'`':333,'a':444,'b':500,'c':444,'d':500,'e':444,'f':333,'g':500,'h':500,'i':278,'j':278,'k':500,'l':278,'m':778, - 'n':500,'o':500,'p':500,'q':500,'r':333,'s':389,'t':278,'u':500,'v':500,'w':722,'x':500,'y':500,'z':444,'{':480,'|':200,'}':480,'~':541,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, - '\x84':444,'\x85':1000,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':556,'\x8b':333,'\x8c':889,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':444,'\x94':444,'\x95':350,'\x96':500,'\x97':1000,'\x98':333,'\x99':980, - '\x9a':389,'\x9b':333,'\x9c':722,'\x9d':350,'\x9e':444,'\x9f':722,'\xa0':250,'\xa1':333,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':200,'\xa7':500,'\xa8':333,'\xa9':760,'\xaa':276,'\xab':500,'\xac':564,'\xad':333,'\xae':760,'\xaf':333, - '\xb0':400,'\xb1':564,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':500,'\xb6':453,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':310,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':444,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, - '\xc6':889,'\xc7':667,'\xc8':611,'\xc9':611,'\xca':611,'\xcb':611,'\xcc':333,'\xcd':333,'\xce':333,'\xcf':333,'\xd0':722,'\xd1':722,'\xd2':722,'\xd3':722,'\xd4':722,'\xd5':722,'\xd6':722,'\xd7':564,'\xd8':722,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':722,'\xde':556,'\xdf':500,'\xe0':444,'\xe1':444,'\xe2':444,'\xe3':444,'\xe4':444,'\xe5':444,'\xe6':667,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':500, - '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':564,'\xf8':500,'\xf9':500,'\xfa':500,'\xfb':500,'\xfc':500,'\xfd':500,'\xfe':500,'\xff':500} - -fpdf_charwidths['timesB']={ - '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, - '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':555,'#':500,'$':500,'%':1000,'&':833,'\'':278,'(':333,')':333,'*':500,'+':570, - ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':333,';':333,'<':570,'=':570,'>':570,'?':500,'@':930,'A':722, - 'B':667,'C':722,'D':722,'E':667,'F':611,'G':778,'H':778,'I':389,'J':500,'K':778,'L':667,'M':944,'N':722,'O':778,'P':611,'Q':778,'R':722,'S':556,'T':667,'U':722,'V':722,'W':1000, - 'X':722,'Y':722,'Z':667,'[':333,'\\':278,']':333,'^':581,'_':500,'`':333,'a':500,'b':556,'c':444,'d':556,'e':444,'f':333,'g':500,'h':556,'i':278,'j':333,'k':556,'l':278,'m':833, - 'n':556,'o':500,'p':556,'q':556,'r':444,'s':389,'t':333,'u':556,'v':500,'w':722,'x':500,'y':500,'z':444,'{':394,'|':220,'}':394,'~':520,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, - '\x84':500,'\x85':1000,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':556,'\x8b':333,'\x8c':1000,'\x8d':350,'\x8e':667,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':500,'\x94':500,'\x95':350,'\x96':500,'\x97':1000,'\x98':333,'\x99':1000, - '\x9a':389,'\x9b':333,'\x9c':722,'\x9d':350,'\x9e':444,'\x9f':722,'\xa0':250,'\xa1':333,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':220,'\xa7':500,'\xa8':333,'\xa9':747,'\xaa':300,'\xab':500,'\xac':570,'\xad':333,'\xae':747,'\xaf':333, - '\xb0':400,'\xb1':570,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':556,'\xb6':540,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':330,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':500,'\xc0':722,'\xc1':722,'\xc2':722,'\xc3':722,'\xc4':722,'\xc5':722, - '\xc6':1000,'\xc7':722,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':389,'\xcd':389,'\xce':389,'\xcf':389,'\xd0':722,'\xd1':722,'\xd2':778,'\xd3':778,'\xd4':778,'\xd5':778,'\xd6':778,'\xd7':570,'\xd8':778,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':722,'\xde':611,'\xdf':556,'\xe0':500,'\xe1':500,'\xe2':500,'\xe3':500,'\xe4':500,'\xe5':500,'\xe6':722,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':556, - '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':570,'\xf8':500,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':500,'\xfe':556,'\xff':500} - -fpdf_charwidths['timesBI']={ - '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, - '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':389,'"':555,'#':500,'$':500,'%':833,'&':778,'\'':278,'(':333,')':333,'*':500,'+':570, - ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':333,';':333,'<':570,'=':570,'>':570,'?':500,'@':832,'A':667, - 'B':667,'C':667,'D':722,'E':667,'F':667,'G':722,'H':778,'I':389,'J':500,'K':667,'L':611,'M':889,'N':722,'O':722,'P':611,'Q':722,'R':667,'S':556,'T':611,'U':722,'V':667,'W':889, - 'X':667,'Y':611,'Z':611,'[':333,'\\':278,']':333,'^':570,'_':500,'`':333,'a':500,'b':500,'c':444,'d':500,'e':444,'f':333,'g':500,'h':556,'i':278,'j':278,'k':500,'l':278,'m':778, - 'n':556,'o':500,'p':500,'q':500,'r':389,'s':389,'t':278,'u':556,'v':444,'w':667,'x':500,'y':444,'z':389,'{':348,'|':220,'}':348,'~':570,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, - '\x84':500,'\x85':1000,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':556,'\x8b':333,'\x8c':944,'\x8d':350,'\x8e':611,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':500,'\x94':500,'\x95':350,'\x96':500,'\x97':1000,'\x98':333,'\x99':1000, - '\x9a':389,'\x9b':333,'\x9c':722,'\x9d':350,'\x9e':389,'\x9f':611,'\xa0':250,'\xa1':389,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':220,'\xa7':500,'\xa8':333,'\xa9':747,'\xaa':266,'\xab':500,'\xac':606,'\xad':333,'\xae':747,'\xaf':333, - '\xb0':400,'\xb1':570,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':576,'\xb6':500,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':300,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':500,'\xc0':667,'\xc1':667,'\xc2':667,'\xc3':667,'\xc4':667,'\xc5':667, - '\xc6':944,'\xc7':667,'\xc8':667,'\xc9':667,'\xca':667,'\xcb':667,'\xcc':389,'\xcd':389,'\xce':389,'\xcf':389,'\xd0':722,'\xd1':722,'\xd2':722,'\xd3':722,'\xd4':722,'\xd5':722,'\xd6':722,'\xd7':570,'\xd8':722,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':611,'\xde':611,'\xdf':500,'\xe0':500,'\xe1':500,'\xe2':500,'\xe3':500,'\xe4':500,'\xe5':500,'\xe6':722,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':556, - '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':570,'\xf8':500,'\xf9':556,'\xfa':556,'\xfb':556,'\xfc':556,'\xfd':444,'\xfe':500,'\xff':444} - -fpdf_charwidths['timesI']={ - '\x00':250,'\x01':250,'\x02':250,'\x03':250,'\x04':250,'\x05':250,'\x06':250,'\x07':250,'\x08':250,'\t':250,'\n':250,'\x0b':250,'\x0c':250,'\r':250,'\x0e':250,'\x0f':250,'\x10':250,'\x11':250,'\x12':250,'\x13':250,'\x14':250,'\x15':250, - '\x16':250,'\x17':250,'\x18':250,'\x19':250,'\x1a':250,'\x1b':250,'\x1c':250,'\x1d':250,'\x1e':250,'\x1f':250,' ':250,'!':333,'"':420,'#':500,'$':500,'%':833,'&':778,'\'':214,'(':333,')':333,'*':500,'+':675, - ',':250,'-':333,'.':250,'/':278,'0':500,'1':500,'2':500,'3':500,'4':500,'5':500,'6':500,'7':500,'8':500,'9':500,':':333,';':333,'<':675,'=':675,'>':675,'?':500,'@':920,'A':611, - 'B':611,'C':667,'D':722,'E':611,'F':611,'G':722,'H':722,'I':333,'J':444,'K':667,'L':556,'M':833,'N':667,'O':722,'P':611,'Q':722,'R':611,'S':500,'T':556,'U':722,'V':611,'W':833, - 'X':611,'Y':556,'Z':556,'[':389,'\\':278,']':389,'^':422,'_':500,'`':333,'a':500,'b':500,'c':444,'d':500,'e':444,'f':278,'g':500,'h':500,'i':278,'j':278,'k':444,'l':278,'m':722, - 'n':500,'o':500,'p':500,'q':500,'r':389,'s':389,'t':278,'u':500,'v':444,'w':667,'x':444,'y':444,'z':389,'{':400,'|':275,'}':400,'~':541,'\x7f':350,'\x80':500,'\x81':350,'\x82':333,'\x83':500, - '\x84':556,'\x85':889,'\x86':500,'\x87':500,'\x88':333,'\x89':1000,'\x8a':500,'\x8b':333,'\x8c':944,'\x8d':350,'\x8e':556,'\x8f':350,'\x90':350,'\x91':333,'\x92':333,'\x93':556,'\x94':556,'\x95':350,'\x96':500,'\x97':889,'\x98':333,'\x99':980, - '\x9a':389,'\x9b':333,'\x9c':667,'\x9d':350,'\x9e':389,'\x9f':556,'\xa0':250,'\xa1':389,'\xa2':500,'\xa3':500,'\xa4':500,'\xa5':500,'\xa6':275,'\xa7':500,'\xa8':333,'\xa9':760,'\xaa':276,'\xab':500,'\xac':675,'\xad':333,'\xae':760,'\xaf':333, - '\xb0':400,'\xb1':675,'\xb2':300,'\xb3':300,'\xb4':333,'\xb5':500,'\xb6':523,'\xb7':250,'\xb8':333,'\xb9':300,'\xba':310,'\xbb':500,'\xbc':750,'\xbd':750,'\xbe':750,'\xbf':500,'\xc0':611,'\xc1':611,'\xc2':611,'\xc3':611,'\xc4':611,'\xc5':611, - '\xc6':889,'\xc7':667,'\xc8':611,'\xc9':611,'\xca':611,'\xcb':611,'\xcc':333,'\xcd':333,'\xce':333,'\xcf':333,'\xd0':722,'\xd1':667,'\xd2':722,'\xd3':722,'\xd4':722,'\xd5':722,'\xd6':722,'\xd7':675,'\xd8':722,'\xd9':722,'\xda':722,'\xdb':722, - '\xdc':722,'\xdd':556,'\xde':611,'\xdf':500,'\xe0':500,'\xe1':500,'\xe2':500,'\xe3':500,'\xe4':500,'\xe5':500,'\xe6':667,'\xe7':444,'\xe8':444,'\xe9':444,'\xea':444,'\xeb':444,'\xec':278,'\xed':278,'\xee':278,'\xef':278,'\xf0':500,'\xf1':500, - '\xf2':500,'\xf3':500,'\xf4':500,'\xf5':500,'\xf6':500,'\xf7':675,'\xf8':500,'\xf9':500,'\xfa':500,'\xfb':500,'\xfc':500,'\xfd':444,'\xfe':500,'\xff':444} - -fpdf_charwidths['zapfdingbats']={ - '\x00':0,'\x01':0,'\x02':0,'\x03':0,'\x04':0,'\x05':0,'\x06':0,'\x07':0,'\x08':0,'\t':0,'\n':0,'\x0b':0,'\x0c':0,'\r':0,'\x0e':0,'\x0f':0,'\x10':0,'\x11':0,'\x12':0,'\x13':0,'\x14':0,'\x15':0, - '\x16':0,'\x17':0,'\x18':0,'\x19':0,'\x1a':0,'\x1b':0,'\x1c':0,'\x1d':0,'\x1e':0,'\x1f':0,' ':278,'!':974,'"':961,'#':974,'$':980,'%':719,'&':789,'\'':790,'(':791,')':690,'*':960,'+':939, - ',':549,'-':855,'.':911,'/':933,'0':911,'1':945,'2':974,'3':755,'4':846,'5':762,'6':761,'7':571,'8':677,'9':763,':':760,';':759,'<':754,'=':494,'>':552,'?':537,'@':577,'A':692, - 'B':786,'C':788,'D':788,'E':790,'F':793,'G':794,'H':816,'I':823,'J':789,'K':841,'L':823,'M':833,'N':816,'O':831,'P':923,'Q':744,'R':723,'S':749,'T':790,'U':792,'V':695,'W':776, - 'X':768,'Y':792,'Z':759,'[':707,'\\':708,']':682,'^':701,'_':826,'`':815,'a':789,'b':789,'c':707,'d':687,'e':696,'f':689,'g':786,'h':787,'i':713,'j':791,'k':785,'l':791,'m':873, - 'n':761,'o':762,'p':762,'q':759,'r':759,'s':892,'t':892,'u':788,'v':784,'w':438,'x':138,'y':277,'z':415,'{':392,'|':392,'}':668,'~':668,'\x7f':0,'\x80':390,'\x81':390,'\x82':317,'\x83':317, - '\x84':276,'\x85':276,'\x86':509,'\x87':509,'\x88':410,'\x89':410,'\x8a':234,'\x8b':234,'\x8c':334,'\x8d':334,'\x8e':0,'\x8f':0,'\x90':0,'\x91':0,'\x92':0,'\x93':0,'\x94':0,'\x95':0,'\x96':0,'\x97':0,'\x98':0,'\x99':0, - '\x9a':0,'\x9b':0,'\x9c':0,'\x9d':0,'\x9e':0,'\x9f':0,'\xa0':0,'\xa1':732,'\xa2':544,'\xa3':544,'\xa4':910,'\xa5':667,'\xa6':760,'\xa7':760,'\xa8':776,'\xa9':595,'\xaa':694,'\xab':626,'\xac':788,'\xad':788,'\xae':788,'\xaf':788, - '\xb0':788,'\xb1':788,'\xb2':788,'\xb3':788,'\xb4':788,'\xb5':788,'\xb6':788,'\xb7':788,'\xb8':788,'\xb9':788,'\xba':788,'\xbb':788,'\xbc':788,'\xbd':788,'\xbe':788,'\xbf':788,'\xc0':788,'\xc1':788,'\xc2':788,'\xc3':788,'\xc4':788,'\xc5':788, - '\xc6':788,'\xc7':788,'\xc8':788,'\xc9':788,'\xca':788,'\xcb':788,'\xcc':788,'\xcd':788,'\xce':788,'\xcf':788,'\xd0':788,'\xd1':788,'\xd2':788,'\xd3':788,'\xd4':894,'\xd5':838,'\xd6':1016,'\xd7':458,'\xd8':748,'\xd9':924,'\xda':748,'\xdb':918, - '\xdc':927,'\xdd':928,'\xde':928,'\xdf':834,'\xe0':873,'\xe1':828,'\xe2':924,'\xe3':924,'\xe4':917,'\xe5':930,'\xe6':931,'\xe7':463,'\xe8':883,'\xe9':836,'\xea':836,'\xeb':867,'\xec':867,'\xed':696,'\xee':696,'\xef':874,'\xf0':0,'\xf1':874, - '\xf2':760,'\xf3':946,'\xf4':771,'\xf5':865,'\xf6':771,'\xf7':888,'\xf8':967,'\xf9':888,'\xfa':831,'\xfb':873,'\xfc':927,'\xfd':970,'\xfe':918,'\xff':0} - diff --git a/gluon/contrib/pyfpdf/html.py b/gluon/contrib/fpdf/html.py similarity index 82% rename from gluon/contrib/pyfpdf/html.py rename to gluon/contrib/fpdf/html.py index 71e5ef57..95b7e8b3 100644 --- a/gluon/contrib/pyfpdf/html.py +++ b/gluon/contrib/fpdf/html.py @@ -26,22 +26,23 @@ def hex2dec(color = "#000000"): class HTML2FPDF(HTMLParser): "Render basic HTML to FPDF" - def __init__(self, pdf, image_map, **kwargs): + def __init__(self, pdf): HTMLParser.__init__(self) - self.image_map = image_map self.style = {} self.pre = False self.href = '' self.align = '' self.page_links = {} self.font_list = ("times","courier", "helvetica") + self.font = None + self.font_stack = [] self.pdf = pdf self.r = self.g = self.b = 0 self.indent = 0 self.bullet = [] - self.font_face="times" # initialize font - self.color=0 # initialize font color - self.set_font(kwargs.get("font","times"), kwargs.get("fontsize",12)) + self.set_font("times", 12) + self.font_face = "times" # initialize font + self.color = 0 #initialize font color self.table = None # table attributes self.table_col_width = None # column (header) widths self.table_col_index = None # current column index @@ -66,7 +67,10 @@ class HTML2FPDF(HTMLParser): def handle_data(self, txt): if self.td is not None: # drawing a table? if 'width' not in self.td and 'colspan' not in self.td: - l = [self.table_col_width[self.table_col_index]] + try: + l = [self.table_col_width[self.table_col_index]] + except IndexError: + raise RuntimeError("Table column/cell width not specified, unable to continue") elif 'colspan' in self.td: i = self.table_col_index colspan = int(self.td['colspan']) @@ -83,7 +87,7 @@ class HTML2FPDF(HTMLParser): else: self.set_style('B',True) border = border or 'B' - align = self.td.get('align', 'C')[0].upper() + align = 'C' bgcolor = hex2dec(self.td.get('bgcolor', self.tr.get('bgcolor', ''))) # parsing table header/footer (drawn later): if self.thead is not None: @@ -173,7 +177,7 @@ class HTML2FPDF(HTMLParser): if tag=='p': self.pdf.ln(5) if attrs: - self.align=attrs['align'].lower() + if attrs: self.align = attrs.get('align') if tag in ('h1', 'h2', 'h3', 'h4', 'h5', 'h6'): k = (2, 1.5, 1.17, 1, 0.83, 0.67)[int(tag[1])] self.pdf.ln(5*k) @@ -208,6 +212,8 @@ class HTML2FPDF(HTMLParser): self.pdf.write(self.h,'%s%s ' % (' '*5*self.indent, bullet)) self.set_text_color() if tag=='font': + # save previous font state: + self.font_stack.append((self.font_face, self.font_size, self.color)) if 'color' in attrs: self.color = hex2dec(attrs['color']) self.set_text_color(*color) @@ -234,6 +240,7 @@ class HTML2FPDF(HTMLParser): self.tfooter = [] self.thead = None self.tfoot = None + self.table_h = 0 self.pdf.ln() if tag=='tr': self.tr = dict([(k.lower(), v) for k,v in attrs.items()]) @@ -244,7 +251,7 @@ class HTML2FPDF(HTMLParser): if tag=='th': self.td = dict([(k.lower(), v) for k,v in attrs.items()]) self.th = True - if self.td['width']: + if 'width' in self.td: self.table_col_width.append(self.td['width']) if tag=='thead': self.thead = {} @@ -258,8 +265,7 @@ class HTML2FPDF(HTMLParser): h = px2mm(attrs.get('height',0)) if self.align and self.align[0].upper() == 'C': x = (self.pdf.w-x)/2.0 - w/2.0 - self.pdf.image(self.image_map(attrs['src']), - x, y, w, h, link=self.href) + self.pdf.image(attrs['src'], x, y, w, h, link=self.href) self.pdf.set_x(x+w) self.pdf.set_y(y+h) if tag=='b' or tag=='i' or tag=='u': @@ -324,12 +330,13 @@ class HTML2FPDF(HTMLParser): self.td = None self.th = False if tag=='font': - if self.color: + # recover last font state + face, size, color = self.font_stack.pop() + if face: self.pdf.set_text_color(0,0,0) self.color = None - if self.font_face: - self.set_font('Times',12) - + self.set_font(face, size) + self.font = None if tag=='center': self.align = None @@ -381,79 +388,10 @@ class HTML2FPDF(HTMLParser): self.pdf.line(self.pdf.get_x(),self.pdf.get_y(),self.pdf.get_x()+187,self.pdf.get_y()) self.pdf.ln(3) -class HTMLMixin(): - def write_html(self, text, image_map=lambda x:x, **kwargs): +class HTMLMixin(object): + def write_html(self, text): "Parse HTML and convert it to PDF" - h2p = HTML2FPDF(self,image_map=image_map,**kwargs) + h2p = HTML2FPDF(self) h2p.feed(text) -if __name__=='__main__': - html=""" -

    html2fpdf

    -

    Basic usage

    -

    You can now easily print text mixing different -styles : bold, italic, underlined, or -all at once!
    You can also insert links -on text, such as www.fpdf.org, -or on an image: click on the logo.
    -

    - -
    -

    Sample List

    -
    • option 1
    • -
      1. option 2
      -
    • option 3
    - - - - - - - -
    Header 1header 2
    cell 1cell 2
    cell 2cell 3
    - - - - - - - - - - - - - -""" + """ - - - -""" * 200 + """ - -
    Header 1header 2
    footer 1footer 2
    cell 1cell 2
    cell 1cell 2
    cell spanned
    cell 3cell 4
    cell 5cell 6
    -""" - - class MyFPDF(FPDF, HTMLMixin): - def header(self): - self.image('tutorial/logo_pb.png',10,8,33) - self.set_font('Arial','B',15) - self.cell(80) - self.cell(30,10,'Title',1,0,'C') - self.ln(20) - - def footer(self): - self.set_y(-15) - self.set_font('Arial','I',8) - txt = 'Page %s of %s' % (self.page_no(), self.alias_nb_pages()) - self.cell(0,10,txt,0,0,'C') - - pdf=MyFPDF() - #First page - pdf.add_page() - pdf.write_html(html) - pdf.output('html.pdf','F') - - import os - os.system("evince html.pdf") - diff --git a/gluon/contrib/fpdf/php.py b/gluon/contrib/fpdf/php.py new file mode 100644 index 00000000..569bf9e0 --- /dev/null +++ b/gluon/contrib/fpdf/php.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python +# -*- coding: latin-1 -*- + +# fpdf php helpers: + +def substr(s, start, length=-1): + if length < 0: + length=len(s)-start + return s[start:start+length] + +def sprintf(fmt, *args): return fmt % args + +def print_r(array): + if not isinstance(array, dict): + array = dict([(k, k) for k in array]) + for k, v in array.items(): + print "[%s] => %s" % (k, v), + +def UTF8ToUTF16BE(instr, setbom=True): + "Converts UTF-8 strings to UTF16-BE." + outstr = "" + if (setbom): + outstr += "\xFE\xFF"; + if not isinstance(instr, unicode): + instr = instr.decode('UTF-8') + outstr += instr.encode('UTF-16BE') + return outstr + +def UTF8StringToArray(instr): + "Converts UTF-8 strings to codepoints array" + return [ord(c) for c in instr] + +# ttfints php helpers: + +def die(msg): + raise RuntimeError(msg) + +def str_repeat(s, count): + return s * count + +def str_pad(s, pad_length=0, pad_char= " ", pad_type= +1 ): + if pad_type<0: # pad left + return s.rjust(pad_length, pad_char) + elif pad_type>0: # pad right + return s.ljust(pad_length, pad_char) + else: # pad both + return s.center(pad_length, pad_char) + +strlen = count = lambda s: len(s) diff --git a/gluon/contrib/pyfpdf/template.py b/gluon/contrib/fpdf/template.py old mode 100755 new mode 100644 similarity index 80% rename from gluon/contrib/pyfpdf/template.py rename to gluon/contrib/fpdf/template.py index 8b158b36..651a1b83 --- a/gluon/contrib/pyfpdf/template.py +++ b/gluon/contrib/fpdf/template.py @@ -16,7 +16,8 @@ class Template: def __init__(self, infile=None, elements=None, format='A4', orientation='portrait', title='', author='', subject='', creator='', keywords=''): if elements: - self.elements = dict([(v['name'].lower(),v) for v in elements]) + self.elements = elements + self.keys = [v['name'].lower() for v in self.elements] self.handlers = {'T': self.text, 'L': self.line, 'I': self.image, 'B': self.rect, 'BC': self.barcode, } self.pg_no = 0 @@ -32,34 +33,33 @@ class Template: "Parse template format csv file and create elements dict" keys = ('name','type','x1','y1','x2','y2','font','size', 'bold','italic','underline','foreground','background', - 'align','text','priority') - self.elements = {} - f = open(infile, 'rb') - try: - for row in csv.reader(f, delimiter=delimiter): - kargs = {} - for i,v in enumerate(row): - if not v.startswith("'") and decimal_sep!=".": - v = v.replace(decimal_sep,".") - else: - v = v - if v=='': - v = None - else: - v = eval(v.strip()) - kargs[keys[i]] = v - self.elements[kargs['name'].lower()] = kargs - finally: - f.close() + 'align','text','priority', 'multiline') + self.elements = [] + for row in csv.reader(open(infile, 'rb'), delimiter=delimiter): + kargs = {} + for i,v in enumerate(row): + if not v.startswith("'") and decimal_sep!=".": + v = v.replace(decimal_sep,".") + else: + v = v + if v=='': + v = None + else: + v = eval(v.strip()) + kargs[keys[i]] = v + self.elements.append(kargs) + self.keys = [v['name'].lower() for v in self.elements] def add_page(self): self.pg_no += 1 self.texts[self.pg_no] = {} def __setitem__(self, name, value): - if name.lower() in self.elements: + if self.has_key(name): if isinstance(value,unicode): value = value.encode("latin1","ignore") + elif value is None: + value = "" else: value = str(value) self.texts[self.pg_no][name.lower()] = value @@ -67,14 +67,27 @@ class Template: # setitem shortcut (may be further extended) set = __setitem__ + def has_key(self, name): + return name.lower() in self.keys + def __getitem__(self, name): - if name.lower() in self.elements: - return self.texts[self.pg_no].get(name.lower(), self.elements[name.lower()]['text']) + if self.has_key(name): + key = name.lower() + if key in self.texts: + # text for this page: + return self.texts[self.pg_no][key] + else: + # find first element for default text: + elements = [element for element in self.elements + if element['name'].lower() == key] + if elements: + return elements[0]['text'] def split_multicell(self, text, element_name): "Divide (\n) a string using a given element width" pdf = self.pdf - element = self.elements[element_name.lower()] + element = [element for element in self.elements + if element['name'].lower() == element_name.lower()][0] style = "" if element['bold']: style += "B" if element['italic']: style += "I" @@ -96,7 +109,7 @@ class Template: pdf.set_font('Arial','B',16) pdf.set_auto_page_break(False,margin=0) - for element in sorted(self.elements.values(),key=lambda x: x['priority']): + for element in sorted(self.elements,key=lambda x: x['priority']): #print "dib",element['type'], element['name'], element['x1'], element['y1'], element['x2'], element['y2'] element = element.copy() element['text'] = self.texts[pg].get(element['name'].lower(), element['text']) @@ -110,7 +123,7 @@ class Template: def text(self, pdf, x1=0, y1=0, x2=0, y2=0, text='', font="arial", size=10, bold=False, italic=False, underline=False, align="", - foreground=0, backgroud=65535, + foreground=0, backgroud=65535, multiline=None, *args, **kwargs): if text: if pdf.text_color!=rgb(foreground): @@ -134,7 +147,19 @@ class Template: ##m_k = 72 / 2.54 ##h = (size/m_k) pdf.set_xy(x1,y1) - pdf.cell(w=x2-x1,h=y2-y1,txt=text,border=0,ln=0,align=align) + if multiline is None: + # multiline==None: write without wrapping/trimming (default) + pdf.cell(w=x2-x1,h=y2-y1,txt=text,border=0,ln=0,align=align) + elif multiline: + # multiline==True: automatic word - warp + pdf.multi_cell(w=x2-x1,h=y2-y1,txt=text,border=0,align=align) + else: + # multiline==False: trim to fit exactly the space defined + text = pdf.multi_cell(w=x2-x1, h=y2-y1, + txt=text, align=align, split_only=True)[0] + print "trimming: *%s*" % text + pdf.cell(w=x2-x1,h=y2-y1,txt=text,border=0,ln=0,align=align) + #pdf.Text(x=x1,y=y1,txt=text) def line(self, pdf, x1=0, y1=0, x2=0, y2=0, size=0, foreground=0, *args, **kwargs): @@ -275,4 +300,3 @@ if __name__ == "__main__": else: os.system("./invoice.pdf") - diff --git a/gluon/contrib/fpdf/ttfonts.py b/gluon/contrib/fpdf/ttfonts.py new file mode 100644 index 00000000..ddab0798 --- /dev/null +++ b/gluon/contrib/fpdf/ttfonts.py @@ -0,0 +1,1033 @@ +#****************************************************************************** +# TTFontFile class +# +# This class is based on The ReportLab Open Source PDF library +# written in Python - http://www.reportlab.com/software/opensource/ +# together with ideas from the OpenOffice source code and others. +# +# Version: 1.04 +# Date: 2011-09-18 +# Author: Ian Back +# License: LGPL +# Copyright (c) Ian Back, 2010 +# Ported to Python 2.7 by Mariano Reingart (reingart@gmail.com) on 2012 +# This header must be retained in any redistribution or +# modification of the file. +# +#****************************************************************************** + +from struct import pack, unpack, unpack_from +import re +import warnings +from php import die, substr, str_repeat, str_pad, strlen, count + + +# Define the value used in the "head" table of a created TTF file +# 0x74727565 "true" for Mac +# 0x00010000 for Windows +# Either seems to work for a font embedded in a PDF file +# when read by Adobe Reader on a Windows PC(!) +_TTF_MAC_HEADER = False + + +# TrueType Font Glyph operators +GF_WORDS = (1 << 0) +GF_SCALE = (1 << 3) +GF_MORE = (1 << 5) +GF_XYSCALE = (1 << 6) +GF_TWOBYTWO = (1 << 7) + + +def sub32(x, y): + xlo = x[1] + xhi = x[0] + ylo = y[1] + yhi = y[0] + if (ylo > xlo): + xlo += 1 << 16 + yhi += 1 + reslo = xlo-ylo + if (yhi > xhi): + xhi += 1 << 16 + reshi = xhi-yhi + reshi = reshi & 0xFFFF + return (reshi, reslo) + +def calcChecksum(data): + if (strlen(data) % 4): + data += str_repeat("\0", (4-(len(data) % 4))) + hi=0x0000 + lo=0x0000 + for i in range(0, len(data), 4): + hi += (ord(data[i])<<8) + ord(data[i+1]) + lo += (ord(data[i+2])<<8) + ord(data[i+3]) + hi += lo >> 16 + lo = lo & 0xFFFF + hi = hi & 0xFFFF + return (hi, lo) + + +class TTFontFile: + + def __init__(self): + self.maxStrLenRead = 200000 # Maximum size of glyf table to read in as string (otherwise reads each glyph from file) + + def getMetrics(self, file): + self.filename = file + self.fh = open(file,'rb') + self._pos = 0 + self.charWidths = [] + self.glyphPos = {} + self.charToGlyph = {} + self.tables = {} + self.otables = {} + self.ascent = 0 + self.descent = 0 + self.TTCFonts = {} + self.version = version = self.read_ulong() + if (version==0x4F54544F): + die("Postscript outlines are not supported") + if (version==0x74746366): + die("ERROR - TrueType Fonts Collections not supported") + if (version not in (0x00010000,0x74727565)): + die("Not a TrueType font: version=" + version) + self.readTableDirectory() + self.extractInfo() + self.fh.close() + + def readTableDirectory(self, ): + self.numTables = self.read_ushort() + self.searchRange = self.read_ushort() + self.entrySelector = self.read_ushort() + self.rangeShift = self.read_ushort() + self.tables = {} + for i in range(self.numTables): + record = {} + record['tag'] = self.read_tag() + record['checksum'] = (self.read_ushort(),self.read_ushort()) + record['offset'] = self.read_ulong() + record['length'] = self.read_ulong() + self.tables[record['tag']] = record + + def get_table_pos(self, tag): + offset = self.tables[tag]['offset'] + length = self.tables[tag]['length'] + return (offset, length) + + def seek(self, pos): + self._pos = pos + self.fh.seek(self._pos) + + def skip(self, delta): + self._pos = self._pos + delta + self.fh.seek(self._pos) + + def seek_table(self, tag, offset_in_table = 0): + tpos = self.get_table_pos(tag) + self._pos = tpos[0] + offset_in_table + self.fh.seek(self._pos) + return self._pos + + def read_tag(self): + self._pos += 4 + return self.fh.read(4) + + def read_short(self): + self._pos += 2 + s = self.fh.read(2) + a = (ord(s[0])<<8) + ord(s[1]) + if (a & (1 << 15) ): + a = (a - (1 << 16)) + return a + + def unpack_short(self, s): + a = (ord(s[0])<<8) + ord(s[1]) + if (a & (1 << 15) ): + a = (a - (1 << 16)) + return a + + def read_ushort(self): + self._pos += 2 + s = self.fh.read(2) + return (ord(s[0])<<8) + ord(s[1]) + + def read_ulong(self): + self._pos += 4 + s = self.fh.read(4) + # if large uInt32 as an integer, PHP converts it to -ve + return (ord(s[0])*16777216) + (ord(s[1])<<16) + (ord(s[2])<<8) + ord(s[3]) # 16777216 = 1<<24 + + def get_ushort(self, pos): + self.fh.seek(pos) + s = self.fh.read(2) + return (ord(s[0])<<8) + ord(s[1]) + + def get_ulong(self, pos): + self.fh.seek(pos) + s = self.fh.read(4) + # iF large uInt32 as an integer, PHP converts it to -ve + return (ord(s[0])*16777216) + (ord(s[1])<<16) + (ord(s[2])<<8) + ord(s[3]) # 16777216 = 1<<24 + + def pack_short(self, val): + if (val<0): + val = abs(val) + val = ~val + val += 1 + return pack(">H",val) + + def splice(self, stream, offset, value): + return substr(stream,0,offset) + value + substr(stream,offset+strlen(value)) + + def _set_ushort(self, stream, offset, value): + up = pack(">H", value) + return self.splice(stream, offset, up) + + def _set_short(self, stream, offset, val): + if (val<0): + val = abs(val) + val = ~val + val += 1 + up = pack(">H",val) + return self.splice(stream, offset, up) + + def get_chunk(self, pos, length): + self.fh.seek(pos) + if (length <1): return '' + return (self.fh.read(length)) + + def get_table(self, tag): + (pos, length) = self.get_table_pos(tag) + if (length == 0): + die('Truetype font (' + self.filename + '): error reading table: ' + tag) + self.fh.seek(pos) + return (self.fh.read(length)) + + def add(self, tag, data): + if (tag == 'head') : + data = self.splice(data, 8, "\0\0\0\0") + self.otables[tag] = data + +############################################/ +############################################/ + +############################################/ + + def extractInfo(self): + #################/ + # name - Naming table + #################/ + self.sFamilyClass = 0 + self.sFamilySubClass = 0 + + name_offset = self.seek_table("name") + format = self.read_ushort() + if (format != 0): + die("Unknown name table format " + format) + numRecords = self.read_ushort() + string_data_offset = name_offset + self.read_ushort() + names = {1:'',2:'',3:'',4:'',6:''} + K = names.keys() + nameCount = len(names) + for i in range(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 = '' + if (platformId == 3 and encodingId == 1 and languageId == 0x409): # Microsoft, Unicode, US English, PS Name + opos = self._pos + self.seek(string_data_offset + offset) + if (length % 2 != 0): + die("PostScript name is UTF-16BE string of odd length") + length /= 2 + N = '' + while (length > 0): + char = self.read_ushort() + N += (chr(char)) + length -= 1 + self._pos = opos + self.seek(opos) + + elif (platformId == 1 and encodingId == 0 and languageId == 0): # Macintosh, Roman, English, PS Name + opos = self._pos + N = self.get_chunk(string_data_offset + offset, length) + self._pos = opos + self.seek(opos) + + if (N and names[nameId]==''): + names[nameId] = N + nameCount -= 1 + if (nameCount==0): break + + + if (names[6]): + psName = names[6] + elif (names[4]): + psName = re.sub(' ','-',names[4]) + elif (names[1]): + psName = re.sub(' ','-',names[1]) + else: + psName = '' + if (not psName): + die("Could not find PostScript font name") + self.name = psName + if (names[1]): + self.familyName = names[1] + else: + self.familyName = psName + if (names[2]): + self.styleName = names[2] + else: + self.styleName = 'Regular' + if (names[4]): + self.fullName = names[4] + else: + self.fullName = psName + if (names[3]): + self.uniqueFontID = names[3] + else: + self.uniqueFontID = psName + if (names[6]): + self.fullName = names[6] + + #################/ + # head - Font header table + #################/ + self.seek_table("head") + self.skip(18) + self.unitsPerEm = unitsPerEm = self.read_ushort() + scale = 1000 / float(unitsPerEm) + self.skip(16) + xMin = self.read_short() + yMin = self.read_short() + xMax = self.read_short() + yMax = self.read_short() + self.bbox = [(xMin*scale), (yMin*scale), (xMax*scale), (yMax*scale)] + self.skip(3*2) + indexToLocFormat = self.read_ushort() + glyphDataFormat = self.read_ushort() + if (glyphDataFormat != 0): + die('Unknown glyph data format ' + glyphDataFormat) + + #################/ + # hhea metrics table + #################/ + # ttf2t1 seems to use this value rather than the one in OS/2 - so put in for compatibility + if ("hhea" in self.tables): + self.seek_table("hhea") + self.skip(4) + hheaAscender = self.read_short() + hheaDescender = self.read_short() + self.ascent = (hheaAscender *scale) + self.descent = (hheaDescender *scale) + + + #################/ + # OS/2 - OS/2 and Windows metrics table + #################/ + if ("OS/2" in self.tables): + 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): + die('ERROR - Font file ' + self.filename + ' cannot be embedded due to copyright restrictions.') + self.restrictedUse = True + + self.skip(20) + sF = self.read_short() + self.sFamilyClass = (sF >> 8) + self.sFamilySubClass = (sF & 0xFF) + self._pos += 10 #PANOSE = 10 byte length + panose = self.fh.read(10) + self.skip(26) + sTypoAscender = self.read_short() + sTypoDescender = self.read_short() + if (not self.ascent): + self.ascent = (sTypoAscender*scale) + if (not self.descent): + self.descent = (sTypoDescender*scale) + if (version > 1): + self.skip(16) + sCapHeight = self.read_short() + self.capHeight = (sCapHeight*scale) + else: + self.capHeight = self.ascent + + else: + usWeightClass = 500 + if (not self.ascent): self.ascent = (yMax*scale) + if (not self.descent): self.descent = (yMin*scale) + self.capHeight = self.ascent + + self.stemV = 50 + int(pow((usWeightClass / 65.0),2)) + + #################/ + # post - PostScript table + #################/ + self.seek_table("post") + self.skip(4) + self.italicAngle = self.read_short() + self.read_ushort() / 65536.0 + self.underlinePosition = self.read_short() * scale + self.underlineThickness = self.read_short() * scale + isFixedPitch = self.read_ulong() + + self.flags = 4 + + if (self.italicAngle!= 0): + self.flags = self.flags | 64 + if (usWeightClass >= 600): + self.flags = self.flags | 262144 + if (isFixedPitch): + self.flags = self.flags | 1 + + #################/ + # hhea - Horizontal header table + #################/ + self.seek_table("hhea") + self.skip(32) + metricDataFormat = self.read_ushort() + if (metricDataFormat != 0): + die('Unknown horizontal metric data format '.metricDataFormat) + numberOfHMetrics = self.read_ushort() + if (numberOfHMetrics == 0): + die('Number of horizontal metrics is 0') + + #################/ + # maxp - Maximum profile table + #################/ + self.seek_table("maxp") + self.skip(4) + numGlyphs = self.read_ushort() + + #################/ + # cmap - Character to glyph index mapping table + #################/ + cmap_offset = self.seek_table("cmap") + self.skip(2) + cmapTableCount = self.read_ushort() + unicode_cmap_offset = 0 + for i in range(cmapTableCount): + platformID = self.read_ushort() + encodingID = self.read_ushort() + offset = self.read_ulong() + save_pos = self._pos + if ((platformID == 3 and encodingID == 1) or platformID == 0): # Microsoft, Unicode + format = self.get_ushort(cmap_offset + offset) + if (format == 4): + if (not unicode_cmap_offset): + unicode_cmap_offset = cmap_offset + offset + break + self.seek(save_pos ) + + if (not unicode_cmap_offset): + die('Font (' + self.filename + ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)') + + glyphToChar = {} + charToGlyph = {} + self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph ) + + #################/ + # hmtx - Horizontal metrics table + #################/ + self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale) + + +############################################/ +############################################/ + + def makeSubset(self, file, subset): + self.filename = file + self.fh = open(file ,'rb') + self._pos = 0 + self.charWidths = [] + self.glyphPos = {} + self.charToGlyph = {} + self.tables = {} + self.otables = {} + self.ascent = 0 + self.descent = 0 + self.skip(4) + self.maxUni = 0 + self.readTableDirectory() + + #################/ + # head - Font header table + #################/ + self.seek_table("head") + self.skip(50) + indexToLocFormat = self.read_ushort() + glyphDataFormat = self.read_ushort() + + #################/ + # hhea - Horizontal header table + #################/ + self.seek_table("hhea") + self.skip(32) + metricDataFormat = self.read_ushort() + orignHmetrics = numberOfHMetrics = self.read_ushort() + + #################/ + # maxp - Maximum profile table + #################/ + self.seek_table("maxp") + self.skip(4) + numGlyphs = self.read_ushort() + + #################/ + # cmap - Character to glyph index mapping table + #################/ + cmap_offset = self.seek_table("cmap") + self.skip(2) + cmapTableCount = self.read_ushort() + unicode_cmap_offset = 0 + for i in range(cmapTableCount): + platformID = self.read_ushort() + encodingID = self.read_ushort() + offset = self.read_ulong() + save_pos = self._pos + if ((platformID == 3 and encodingID == 1) or platformID == 0): # Microsoft, Unicode + format = self.get_ushort(cmap_offset + offset) + if (format == 4): + unicode_cmap_offset = cmap_offset + offset + break + + self.seek(save_pos ) + + if (not unicode_cmap_offset): + die('Font (' + self.filename + ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)') + + glyphToChar = {} + charToGlyph = {} + self.getCMAP4(unicode_cmap_offset, glyphToChar, charToGlyph ) + + self.charToGlyph = charToGlyph + + #################/ + # hmtx - Horizontal metrics table + #################/ + scale = 1 # not used + self.getHMTX(numberOfHMetrics, numGlyphs, glyphToChar, scale) + + #################/ + # loca - Index to location + #################/ + self.getLOCA(indexToLocFormat, numGlyphs) + + subsetglyphs = [(0, 0)] # special "sorted dict"! + subsetCharToGlyph = {} + for code in subset: + if (code in self.charToGlyph): + if (self.charToGlyph[code], code) not in subsetglyphs: + subsetglyphs.append((self.charToGlyph[code], code)) # Old Glyph ID => Unicode + subsetCharToGlyph[code] = self.charToGlyph[code] # Unicode to old GlyphID + self.maxUni = max(self.maxUni, code) + (start,dummy) = self.get_table_pos('glyf') + + subsetglyphs.sort() + glyphSet = {} + n = 0 + fsLastCharIndex = 0 # maximum Unicode index (character code) in this font, according to the cmap subtable for platform ID 3 and platform- specific encoding ID 0 or 1. + for originalGlyphIdx, uni in subsetglyphs: + fsLastCharIndex = max(fsLastCharIndex , uni) + glyphSet[originalGlyphIdx] = n # old glyphID to new glyphID + n += 1 + + codeToGlyph = {} + for uni, originalGlyphIdx in sorted(subsetCharToGlyph.items()): + codeToGlyph[uni] = glyphSet[originalGlyphIdx] + + self.codeToGlyph = codeToGlyph + + for originalGlyphIdx, uni in subsetglyphs: + nonlocals = {'start': start, 'glyphSet': glyphSet, + 'subsetglyphs': subsetglyphs} + self.getGlyphs(originalGlyphIdx, nonlocals) + + numGlyphs = numberOfHMetrics = len(subsetglyphs) + + #tables copied from the original + tags = ['name'] + for tag in tags: + self.add(tag, self.get_table(tag)) + tags = ['cvt ', 'fpgm', 'prep', 'gasp'] + for tag in tags: + if (tag in self.tables): + self.add(tag, self.get_table(tag)) + + # post - PostScript + opost = self.get_table('post') + post = "\x00\x03\x00\x00" + substr(opost,4,12) + "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00" + self.add('post', post) + + # Sort CID2GID map into segments of contiguous codes + if 0 in codeToGlyph: + del codeToGlyph[0] + #unset(codeToGlyph[65535]) + rangeid = 0 + range_ = {} + prevcid = -2 + prevglidx = -1 + # for each character + for cid, glidx in sorted(codeToGlyph.items()): + if (cid == (prevcid + 1) and glidx == (prevglidx + 1)): + range_[rangeid].append(glidx) + else: + # new range + rangeid = cid + range_[rangeid] = [] + range_[rangeid].append(glidx) + prevcid = cid + prevglidx = glidx + + # cmap - Character to glyph mapping - Format 4 (MS / ) + segCount = len(range_) + 1 # + 1 Last segment has missing character 0xFFFF + searchRange = 1 + entrySelector = 0 + while (searchRange * 2 <= segCount ): + searchRange = searchRange * 2 + entrySelector = entrySelector + 1 + + searchRange = searchRange * 2 + rangeShift = segCount * 2 - searchRange + length = 16 + (8*segCount ) + (numGlyphs+1) + cmap = [0, 1, # Index : version, number of encoding subtables + 3, 1, # Encoding Subtable : platform (MS=3), encoding (Unicode) + 0, 12, # Encoding Subtable : offset (hi,lo) + 4, length, 0, # Format 4 Mapping subtable: format, length, language + segCount*2, + searchRange, + entrySelector, + rangeShift] + + range_ = sorted(range_.items()) + + # endCode(s) + for start, subrange in range_: + endCode = start + (len(subrange)-1) + cmap.append(endCode) # endCode(s) + + cmap.append(0xFFFF) # endCode of last Segment + cmap.append(0) # reservedPad + + # startCode(s) + for start, subrange in range_: + cmap.append(start) # startCode(s) + + cmap.append(0xFFFF) # startCode of last Segment + # idDelta(s) + for start, subrange in range_: + idDelta = -(start-subrange[0]) + n += count(subrange) + cmap.append(idDelta) # idDelta(s) + + cmap.append(1) # idDelta of last Segment + # idRangeOffset(s) + for subrange in range_: + cmap.append(0) # idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 + + cmap.append(0) # idRangeOffset of last Segment + for subrange, glidx in range_: + cmap.extend(glidx) + + cmap.append(0) # Mapping for last character + cmapstr = '' + for cm in cmap: + if cm >= 0: + cmapstr += pack(">H", cm) + else: + try: + cmapstr += pack(">h", cm) + except: + warnings.warn("cmap value too big/small: %s" % cm) + cmapstr += pack(">H", -cm) + self.add('cmap', cmapstr) + + # glyf - Glyph data + (glyfOffset,glyfLength) = self.get_table_pos('glyf') + if (glyfLength < self.maxStrLenRead): + glyphData = self.get_table('glyf') + + offsets = [] + glyf = '' + pos = 0 + + hmtxstr = '' + xMinT = 0 + yMinT = 0 + xMaxT = 0 + yMaxT = 0 + advanceWidthMax = 0 + minLeftSideBearing = 0 + minRightSideBearing = 0 + xMaxExtent = 0 + maxPoints = 0 # points in non-compound glyph + maxContours = 0 # contours in non-compound glyph + maxComponentPoints = 0 # points in compound glyph + maxComponentContours = 0 # contours in compound glyph + maxComponentElements = 0 # number of glyphs referenced at top level + maxComponentDepth = 0 # levels of recursion, set to 0 if font has only simple glyphs + self.glyphdata = {} + + for originalGlyphIdx, uni in subsetglyphs: + # hmtx - Horizontal Metrics + hm = self.getHMetric(orignHmetrics, originalGlyphIdx) + hmtxstr += hm + + offsets.append(pos) + try: + glyphPos = self.glyphPos[originalGlyphIdx] + glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos + except IndexError: + warnings.warn("missing glyph %s" % (originalGlyphIdx)) + glyphLen = 0 + + if (glyfLength < self.maxStrLenRead): + data = substr(glyphData,glyphPos,glyphLen) + else: + if (glyphLen > 0): + data = self.get_chunk(glyfOffset+glyphPos,glyphLen) + else: + data = '' + + if (glyphLen > 0): + up = unpack(">H", substr(data,0,2))[0] + if (glyphLen > 2 and (up & (1 << 15)) ): # If number of contours <= -1 i.e. composiste glyph + pos_in_glyph = 10 + flags = GF_MORE + nComponentElements = 0 + while (flags & GF_MORE): + nComponentElements += 1 # number of glyphs referenced at top level + up = unpack(">H", substr(data,pos_in_glyph,2)) + flags = up[0] + up = unpack(">H", substr(data,pos_in_glyph+2,2)) + glyphIdx = up[0] + self.glyphdata.setdefault(originalGlyphIdx, {}).setdefault('compGlyphs', []).append(glyphIdx) + try: + data = self._set_ushort(data, pos_in_glyph + 2, glyphSet[glyphIdx]) + except KeyError: + data = 0 + warnings.warn("missing glyph data %s" % glyphIdx) + pos_in_glyph += 4 + if (flags & GF_WORDS): + pos_in_glyph += 4 + else: + pos_in_glyph += 2 + if (flags & GF_SCALE): + pos_in_glyph += 2 + elif (flags & GF_XYSCALE): + pos_in_glyph += 4 + elif (flags & GF_TWOBYTWO): + pos_in_glyph += 8 + + maxComponentElements = max(maxComponentElements, nComponentElements) + + glyf += data + pos += glyphLen + if (pos % 4 != 0): + padding = 4 - (pos % 4) + glyf += str_repeat("\0",padding) + pos += padding + + offsets.append(pos) + self.add('glyf', glyf) + + # hmtx - Horizontal Metrics + self.add('hmtx', hmtxstr) + + # loca - Index to location + locastr = '' + if (((pos + 1) >> 1) > 0xFFFF): + indexToLocFormat = 1 # long format + for offset in offsets: + locastr += pack(">L",offset) + else: + indexToLocFormat = 0 # short format + for offset in offsets: + locastr += pack(">H",(offset/2)) + + self.add('loca', locastr) + + # head - Font header + head = self.get_table('head') + head = self._set_ushort(head, 50, indexToLocFormat) + self.add('head', head) + + # hhea - Horizontal Header + hhea = self.get_table('hhea') + hhea = self._set_ushort(hhea, 34, numberOfHMetrics) + self.add('hhea', hhea) + + # maxp - Maximum Profile + maxp = self.get_table('maxp') + maxp = self._set_ushort(maxp, 4, numGlyphs) + self.add('maxp', maxp) + + # OS/2 - OS/2 + os2 = self.get_table('OS/2') + self.add('OS/2', os2 ) + + self.fh.close() + + # Put the TTF file together + stm = self.endTTFile('') + return stm + + + ######################################### + # Recursively get composite glyph data + def getGlyphData(self, originalGlyphIdx, nonlocals): + # &maxdepth, &depth, &points, &contours + nonlocals['depth'] += 1 + nonlocals['maxdepth'] = max(nonlocals['maxdepth'], nonlocals['depth']) + if (len(self.glyphdata[originalGlyphIdx]['compGlyphs'])): + for glyphIdx in self.glyphdata[originalGlyphIdx]['compGlyphs']: + self.getGlyphData(glyphIdx, nonlocals) + + elif ((self.glyphdata[originalGlyphIdx]['nContours'] > 0) and nonlocals['depth'] > 0): # simple + contours += self.glyphdata[originalGlyphIdx]['nContours'] + points += self.glyphdata[originalGlyphIdx]['nPoints'] + + nonlocals['depth'] -= 1 + + + ######################################### + # Recursively get composite glyphs + def getGlyphs(self, originalGlyphIdx, nonlocals): + # &start, &glyphSet, &subsetglyphs) + + try: + glyphPos = self.glyphPos[originalGlyphIdx] + glyphLen = self.glyphPos[originalGlyphIdx + 1] - glyphPos + except IndexError: + warnings.warn("missing glyph %s" % (originalGlyphIdx)) + return + + if (not glyphLen): + return + + self.seek(nonlocals['start'] + glyphPos) + numberOfContours = self.read_short() + if (numberOfContours < 0): + self.skip(8) + flags = GF_MORE + while (flags & GF_MORE): + flags = self.read_ushort() + glyphIdx = self.read_ushort() + if (glyphIdx not in nonlocals['glyphSet']): + nonlocals['glyphSet'][glyphIdx] = len(nonlocals['subsetglyphs']) # old glyphID to new glyphID + nonlocals['subsetglyphs'].append((glyphIdx, 1)) + + savepos = self.fh.tell() + self.getGlyphs(glyphIdx, nonlocals) + self.seek(savepos) + if (flags & GF_WORDS): + self.skip(4) + else: + self.skip(2) + if (flags & GF_SCALE): + self.skip(2) + elif (flags & GF_XYSCALE): + self.skip(4) + elif (flags & GF_TWOBYTWO): + self.skip(8) + + ######################################### + + def getHMTX(self, numberOfHMetrics, numGlyphs, glyphToChar, scale): + start = self.seek_table("hmtx") + aw = 0 + self.charWidths = [0] * 256*256*2 + nCharWidths = 0 + if ((numberOfHMetrics*4) < self.maxStrLenRead): + data = self.get_chunk(start,(numberOfHMetrics*4)) + arr = unpack(">" + "H" * (len(data)/2), data) + else: + self.seek(start) + for glyph in range(numberOfHMetrics): + if ((numberOfHMetrics*4) < self.maxStrLenRead): + aw = arr[(glyph*2)] # PHP starts arrays from index 0!? +1 + else: + aw = self.read_ushort() + lsb = self.read_ushort() + + if (glyph in glyphToChar or glyph == 0): + if (aw >= (1 << 15) ): + aw = 0 # 1.03 Some (arabic) fonts have -ve values for width + # although should be unsigned value - comes out as e.g. 65108 (intended -50) + if (glyph == 0): + self.defaultWidth = scale*aw + continue + + for char in glyphToChar[glyph]: + if (char != 0 and char != 65535): + w = int(round(scale*aw)) + if (w == 0): w = 65535 + if (char < 196608): + self.charWidths[char] = w + nCharWidths += 1 + + + data = self.get_chunk((start+numberOfHMetrics*4),(numGlyphs*2)) + arr = unpack(">" + "H" * (len(data)/2), data) + diff = numGlyphs-numberOfHMetrics + for pos in range(diff): + glyph = pos + numberOfHMetrics + if (glyph in glyphToChar): + for char in glyphToChar[glyph]: + if (char != 0 and char != 65535): + w = int(round(scale*aw)) + if (w == 0): w = 65535 + if (char < 196608): + self.charWidths[char] = w + nCharWidths += 1 + + + # NB 65535 is a set width of 0 + # First bytes define number of chars in font + self.charWidths[0] = nCharWidths + + + def getHMetric(self, numberOfHMetrics, gid): + start = self.seek_table("hmtx") + if (gid < numberOfHMetrics): + self.seek(start+(gid*4)) + hm = self.fh.read(4) + else: + self.seek(start+((numberOfHMetrics-1)*4)) + hm = self.fh.read(2) + self.seek(start+(numberOfHMetrics*2)+(gid*2)) + hm += self.fh.read(2) + return hm + + + def getLOCA(self, indexToLocFormat, numGlyphs): + start = self.seek_table('loca') + self.glyphPos = [] + if (indexToLocFormat == 0): + data = self.get_chunk(start,(numGlyphs*2)+2) + arr = unpack(">" + "H" * (len(data)/2), data) + for n in range(numGlyphs): + self.glyphPos.append((arr[n] * 2)) # n+1 !? + elif (indexToLocFormat == 1): + data = self.get_chunk(start,(numGlyphs*4)+4) + arr = unpack(">" + "L" * (len(data)/4), data) + for n in range(numGlyphs): + self.glyphPos.append((arr[n])) # n+1 !? + else: + die('Unknown location table format ' + indexToLocFormat) + + # CMAP Format 4 + def getCMAP4(self, unicode_cmap_offset, glyphToChar, charToGlyph ): + self.maxUniChar = 0 + 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 = [] + for i in range(segCount): + endCount.append(self.read_ushort()) + self.skip(2) + startCount = [] + for i in range(segCount): + startCount.append(self.read_ushort()) + idDelta = [] + for i in range(segCount): + idDelta.append(self.read_short()) # ???? was unsigned short + idRangeOffset_start = self._pos + idRangeOffset = [] + for i in range(segCount): + idRangeOffset.append(self.read_ushort()) + + for n in range(segCount): + endpoint = (endCount[n] + 1) + for unichar in range(startCount[n], endpoint, 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): + glyph = 0 + else: + glyph = self.get_ushort(offset) + if (glyph != 0): + glyph = (glyph + idDelta[n]) & 0xFFFF + + charToGlyph[unichar] = glyph + if (unichar < 196608): + self.maxUniChar = max(unichar,self.maxUniChar) + glyphToChar.setdefault(glyph, []).append(unichar) + + + # Put the TTF file together + def endTTFile(self, stm): + stm = '' + numTables = count(self.otables) + searchRange = 1 + entrySelector = 0 + while (searchRange * 2 <= numTables): + searchRange = searchRange * 2 + entrySelector = entrySelector + 1 + + searchRange = searchRange * 16 + rangeShift = numTables * 16 - searchRange + + # Header + if (_TTF_MAC_HEADER): + stm += (pack(">LHHHH", 0x74727565, numTables, searchRange, entrySelector, rangeShift)) # Mac + else: + stm += (pack(">LHHHH", 0x00010000 , numTables, searchRange, entrySelector, rangeShift)) # Windows + + + # Table directory + tables = self.otables + + offset = 12 + numTables * 16 + sorted_tables = sorted(tables.items()) + for tag, data in sorted_tables: + if (tag == 'head'): + head_start = offset + stm += tag + checksum = calcChecksum(data) + stm += pack(">HH", checksum[0],checksum[1]) + stm += pack(">LL", offset, strlen(data)) + paddedLength = (strlen(data)+3)&~3 + offset = offset + paddedLength + + # Table data + for tag, data in sorted_tables: + data += "\0\0\0" + stm += substr(data,0,(strlen(data)&~3)) + + checksum = calcChecksum(stm) + checksum = sub32((0xB1B0,0xAFBA), checksum) + chk = pack(">HH", checksum[0],checksum[1]) + stm = self.splice(stm,(head_start + 8),chk) + return stm + +if __name__ == '__main__': + ttf = TTFontFile() + ttffile = 'DejaVuSansCondensed.ttf'; + ttf.getMetrics(ttffile) + # test basic metrics: + assert round(ttf.descent, 0) == -236 + assert round(ttf.capHeight, 0) == 928 + assert ttf.flags == 4 + assert [round(i, 0) for i in ttf.bbox] == [-918, -415, 1513, 1167] + assert ttf.italicAngle == 0 + assert ttf.stemV == 87 + assert round(ttf.defaultWidth, 0) == 540 + assert round(ttf.underlinePosition, 0) == -63 + assert round(ttf.underlineThickness, 0) == 44 + # test char widths 8(against binary file generated by tfpdf.php): + assert ''.join(ttf.charWidths) == open("dejavusanscondensed.cw.dat").read() + diff --git a/gluon/contrib/gae_memcache.py b/gluon/contrib/gae_memcache.py index e03e74fe..99c4f3cf 100644 --- a/gluon/contrib/gae_memcache.py +++ b/gluon/contrib/gae_memcache.py @@ -55,3 +55,4 @@ class MemcacheClient(Client): + diff --git a/gluon/contrib/gae_retry.py b/gluon/contrib/gae_retry.py index a3f671f2..134112a4 100644 --- a/gluon/contrib/gae_retry.py +++ b/gluon/contrib/gae_retry.py @@ -90,3 +90,4 @@ def autoretry_datastore_timeouts(attempts=5.0, interval=0.1, exponent=2.0): + diff --git a/gluon/contrib/generics.py b/gluon/contrib/generics.py index a5f89dd7..0c9c2a23 100644 --- a/gluon/contrib/generics.py +++ b/gluon/contrib/generics.py @@ -7,7 +7,7 @@ import gluon.serializers from gluon import current, HTTP from gluon.html import markmin_serializer, TAG, HTML, BODY, UL, XML, H1 from gluon.contenttype import contenttype -from gluon.contrib.pyfpdf import FPDF, HTMLMixin +from gluon.contrib.fpdf import FPDF, HTMLMixin from gluon.sanitizer import sanitize from gluon.contrib.markmin.markmin2latex import markmin2latex from gluon.contrib.markmin.markmin2pdf import markmin2pdf @@ -67,3 +67,4 @@ def pdf_from_html(html): + diff --git a/gluon/contrib/google_wallet.py b/gluon/contrib/google_wallet.py index 84d39171..9ea64ea6 100644 --- a/gluon/contrib/google_wallet.py +++ b/gluon/contrib/google_wallet.py @@ -19,3 +19,4 @@ def button(merchant_id="123456789012345", + diff --git a/gluon/contrib/gql.py b/gluon/contrib/gql.py index 94465a7d..92b637e9 100644 --- a/gluon/contrib/gql.py +++ b/gluon/contrib/gql.py @@ -9,3 +9,4 @@ from gluon.dal import DAL, Field, Table, Query, Set, Expression, Row, Rows, driv + diff --git a/gluon/contrib/imageutils.py b/gluon/contrib/imageutils.py index 1c5041ff..e8094458 100644 --- a/gluon/contrib/imageutils.py +++ b/gluon/contrib/imageutils.py @@ -58,3 +58,4 @@ def THUMB(image, nx=120, ny=120, gae=False, name='thumb'): else: return image + diff --git a/gluon/contrib/login_methods/__init__.py b/gluon/contrib/login_methods/__init__.py index 139597f9..b28b04f6 100644 --- a/gluon/contrib/login_methods/__init__.py +++ b/gluon/contrib/login_methods/__init__.py @@ -1,2 +1,3 @@ + diff --git a/gluon/contrib/login_methods/basic_auth.py b/gluon/contrib/login_methods/basic_auth.py index 78b461cb..fee1c35b 100644 --- a/gluon/contrib/login_methods/basic_auth.py +++ b/gluon/contrib/login_methods/basic_auth.py @@ -23,3 +23,4 @@ def basic_auth(server="http://127.0.0.1"): return False return basic_login_aux + diff --git a/gluon/contrib/login_methods/browserid_account.py b/gluon/contrib/login_methods/browserid_account.py index 8d74af30..83375c8e 100644 --- a/gluon/contrib/login_methods/browserid_account.py +++ b/gluon/contrib/login_methods/browserid_account.py @@ -6,18 +6,18 @@ developed by Madhukar R Pai (Copyright 2012) Email License : LGPL - + thanks and credits to the web2py community - + This custom authenticator allows web2py to authenticate using browserid (https://browserid.org/) BrowserID is a project by Mozilla Labs (http://mozillalabs.com/) to Know how browserid works please visit http://identity.mozilla.com/post/7616727542/introducing-browserid-a-better-way-to-sign-in - + bottom line BrowserID provides a free, secure, de-centralized, easy to use(for users and developers) login solution. You can use any email id as your login id. Browserid just verifys the email id and lets you login with that id. - + credits for the doPost jquery function - itsadok (http://stackoverflow.com/users/7581/itsadok) - + """ import time from gluon import * @@ -32,7 +32,7 @@ class BrowserID(object): audience = "http://127.0.0.1:8000" assertion_post_url = "http://127.0.0.1:8000/%s/default/user/login" % request.application) """ - + def __init__(self, request, audience = "", @@ -45,7 +45,7 @@ class BrowserID(object): crypto_js = "https://crypto-js.googlecode.com/files/2.2.0-crypto-md5.js", on_login_failure = None, ): - + self.request = request self.audience = audience self.assertion_post_url = assertion_post_url @@ -64,13 +64,13 @@ class BrowserID(object): def get_user(self): request = self.request - if request.vars.assertion: + if request.vars.assertion: audience = self.audience issuer = self.issuer assertion = XML(request.vars.assertion,sanitize=True) verify_data = {'assertion':assertion,'audience':audience} auth_info_json = fetch(self.verify_url,data=verify_data) - j = json.loads(auth_info_json) + j = json.loads(auth_info_json) epoch_time = int(time.time()*1000) # we need 13 digit epoch time if j["status"] == "okay" and j["audience"] == audience and j['issuer'] == issuer and j['expires'] >= epoch_time: return dict(email = j['email']) @@ -88,3 +88,4 @@ class BrowserID(object): A(IMG(_src=self.browserid_button,_alt=self.prompt),_href="#",_onclick=onclick,_class="browserid",_title="Login With BrowserID"), SCRIPT(self.asertion_js)) return form + diff --git a/gluon/contrib/login_methods/cas_auth.py b/gluon/contrib/login_methods/cas_auth.py index d53f3608..0c922a56 100644 --- a/gluon/contrib/login_methods/cas_auth.py +++ b/gluon/contrib/login_methods/cas_auth.py @@ -136,3 +136,4 @@ class CasAuth( object ): import urllib redirect("%s?service=%s" % (self.cas_logout_url,self.cas_my_url)) + diff --git a/gluon/contrib/login_methods/dropbox_account.py b/gluon/contrib/login_methods/dropbox_account.py index 54b71c46..bfd94723 100644 --- a/gluon/contrib/login_methods/dropbox_account.py +++ b/gluon/contrib/login_methods/dropbox_account.py @@ -12,7 +12,7 @@ import os import re import urllib -from dropbox import client, rest, session +from dropbox import client, rest, session from gluon import * from gluon.tools import fetch from gluon.storage import Storage @@ -50,15 +50,15 @@ class DropboxAccount(object): self.sess = session.DropboxSession( self.key,self.secret,self.access_type) - + def get_user(self): request = self.request token = current.session.dropbox_token try: - access_token = self.sess.obtain_access_token(token) + access_token = self.sess.obtain_access_token(token) except: access_token = None - if access_token: + if access_token: user = Storage() self.client = client.DropboxClient(self.sess) data = self.client.account_info() @@ -68,7 +68,7 @@ class DropboxAccount(object): last_name = display_name[-1], registration_id = data.get('uid',None)) if not user['registration_id'] and self.on_login_failure: - redirect(self.on_login_failure) + redirect(self.on_login_failure) return user return None @@ -107,3 +107,4 @@ def use_dropbox(auth,filename='private/dropbox.key',**kwargs): auth.settings.login_form = DropboxAccount( request,key=key,secret=secret,access_type=access_type, login_url = login_url,**kwargs) + diff --git a/gluon/contrib/login_methods/email_auth.py b/gluon/contrib/login_methods/email_auth.py index 5cba3108..a53011a2 100644 --- a/gluon/contrib/login_methods/email_auth.py +++ b/gluon/contrib/login_methods/email_auth.py @@ -43,3 +43,4 @@ def email_auth(server="smtp.gmail.com:587", pass return False return email_auth_aux + diff --git a/gluon/contrib/login_methods/extended_login_form.py b/gluon/contrib/login_methods/extended_login_form.py index 26e105b1..059d4816 100644 --- a/gluon/contrib/login_methods/extended_login_form.py +++ b/gluon/contrib/login_methods/extended_login_form.py @@ -86,7 +86,7 @@ class ExtendedLoginForm(object): Otherwise it will render the normal login form combined with alt_login_form.login_form. """ - + request = current.request args = request.args @@ -102,3 +102,4 @@ class ExtendedLoginForm(object): form.components.append(self.alt_login_form.login_form()) return form + diff --git a/gluon/contrib/login_methods/gae_google_account.py b/gluon/contrib/login_methods/gae_google_account.py index 2d7c2375..9559b42a 100644 --- a/gluon/contrib/login_methods/gae_google_account.py +++ b/gluon/contrib/login_methods/gae_google_account.py @@ -36,3 +36,4 @@ class GaeGoogleAccount(object): return dict(nickname=user.nickname(), email=user.email(), user_id=user.user_id(), source="google account") + diff --git a/gluon/contrib/login_methods/ldap_auth.py b/gluon/contrib/login_methods/ldap_auth.py index 1df9b3b0..2eb76bcf 100644 --- a/gluon/contrib/login_methods/ldap_auth.py +++ b/gluon/contrib/login_methods/ldap_auth.py @@ -634,3 +634,4 @@ def ldap_auth(server='ldap', port=None, if filterstr[0] == '(' and filterstr[-1] == ')': # rfc4515 syntax filterstr = filterstr[1:-1] # parens added again where used return ldap_auth_aux + diff --git a/gluon/contrib/login_methods/linkedin_account.py b/gluon/contrib/login_methods/linkedin_account.py index b97cab97..f16deadd 100644 --- a/gluon/contrib/login_methods/linkedin_account.py +++ b/gluon/contrib/login_methods/linkedin_account.py @@ -49,3 +49,4 @@ class LinkedInAccount(object): username = profile.id) + diff --git a/gluon/contrib/login_methods/loginza.py b/gluon/contrib/login_methods/loginza.py index 2a665e42..a54e3ca9 100644 --- a/gluon/contrib/login_methods/loginza.py +++ b/gluon/contrib/login_methods/loginza.py @@ -110,3 +110,4 @@ class Loginza(object): SCRIPT(_src="https://s3-eu-west-1.amazonaws.com/s1.loginza.ru/js/widget.js", _type="text/javascript")) return form + diff --git a/gluon/contrib/login_methods/motp_auth.py b/gluon/contrib/login_methods/motp_auth.py index a776b3e4..8d2bfa70 100644 --- a/gluon/contrib/login_methods/motp_auth.py +++ b/gluon/contrib/login_methods/motp_auth.py @@ -6,24 +6,24 @@ from gluon.dal import DAL def motp_auth(db=DAL('sqlite://storage.sqlite'), time_offset=60): - + """ - motp allows you to login with a one time password(OTP) generated on a motp client, + motp allows you to login with a one time password(OTP) generated on a motp client, motp clients are available for practically all platforms. to know more about OTP visit http://en.wikipedia.org/wiki/One-time_password to know more visit http://motp.sourceforge.net - - + + Written by Madhukar R Pai (madspai@gmail.com) License : MIT or GPL v2 - + thanks and credits to the web2py community - + to use motp_auth: motp_auth.py has to be located in gluon/contrib/login_methods/ folder first auth_user has to have 2 extra fields - motp_secret and motp_pin for that define auth like shown below: - + ## after auth = Auth(db) db.define_table( auth.settings.table_user_name, @@ -42,7 +42,7 @@ def motp_auth(db=DAL('sqlite://storage.sqlite'), writable=False, readable=False, default=''), Field('registration_id', length=512, # required writable=False, readable=False, default='')) - + ##validators custom_auth_table = db[auth.settings.table_user_name] # get the custom_auth_table custom_auth_table.first_name.requires = \ @@ -53,22 +53,22 @@ def motp_auth(db=DAL('sqlite://storage.sqlite'), custom_auth_table.email.requires = [ IS_EMAIL(error_message=auth.messages.invalid_email), IS_NOT_IN_DB(db, custom_auth_table.email)] - + auth.settings.table_user = custom_auth_table # tell auth to use custom_auth_table ## before auth.define_tables() - - ##after that: - + + ##after that: + from gluon.contrib.login_methods.motp_auth import motp_auth auth.settings.login_methods.append(motp_auth(db=db)) - + ##Instructions for using MOTP - after configuring motp for web2py, Install a MOTP client on your phone (android,IOS, java, windows phone, etc) - - initialize the motp client (to reset a motp secret type in #**#), + - initialize the motp client (to reset a motp secret type in #**#), During user creation enter the secret generated during initialization into the motp_secret field in auth_user and similarly enter a pre-decided pin into the motp_pin - done.. to login, just generate a fresh OTP by typing in the pin and use the OTP as password - + ###To Dos### - both motp_secret and pin are stored in plain text! need to have some way of encrypting - web2py stores the password in db on successful login (should not happen) @@ -85,7 +85,7 @@ def motp_auth(db=DAL('sqlite://storage.sqlite'), hash = md5(to_hash).hexdigest()[:6] if otp == hash: return True return False - + def motp_auth_aux(email, password, db=db, @@ -102,3 +102,4 @@ def motp_auth(db=DAL('sqlite://storage.sqlite'), else: return False return False return motp_auth_aux + diff --git a/gluon/contrib/login_methods/oauth10a_account.py b/gluon/contrib/login_methods/oauth10a_account.py index fe388dde..534a7582 100644 --- a/gluon/contrib/login_methods/oauth10a_account.py +++ b/gluon/contrib/login_methods/oauth10a_account.py @@ -188,3 +188,4 @@ class OAuthAccount(object): + diff --git a/gluon/contrib/login_methods/oauth20_account.py b/gluon/contrib/login_methods/oauth20_account.py index 354fa9ba..8d740499 100644 --- a/gluon/contrib/login_methods/oauth20_account.py +++ b/gluon/contrib/login_methods/oauth20_account.py @@ -66,7 +66,7 @@ class OAuthAccount(object): if not self.accessToken(): return None - + if not self.graph: self.graph = GraphAPI((self.accessToken())) @@ -117,7 +117,7 @@ server for requests. It can be used for the optional"scope" parameters for Face """ Build the url opener for managing HTTP Basic Athentication """ - # Create an OpenerDirector with support + # Create an OpenerDirector with support # for Basic HTTP Authentication... auth_handler = urllib2.HTTPBasicAuthHandler() @@ -172,7 +172,7 @@ server for requests. It can be used for the optional"scope" parameters for Face if current.session.token.has_key('expires_in'): exps = 'expires_in' elif current.session.token.has_key('expires'): - exps = 'expires' + exps = 'expires' else: exps = None current.session.token['expires'] = exps and \ @@ -185,7 +185,7 @@ server for requests. It can be used for the optional"scope" parameters for Face current.session.token = None return None - def __init__(self, g=None, + def __init__(self, g=None, client_id=None, client_secret=None, auth_url=None, token_url=None, **args): """ @@ -215,7 +215,7 @@ server for requests. It can be used for the optional"scope" parameters for Face def get_user(self): """ Override this method by sublcassing the class. - + """ if not current.session.token: return None return dict(first_name = 'Pinco', @@ -274,3 +274,4 @@ server for requests. It can be used for the optional"scope" parameters for Face return current.session.code return None + diff --git a/gluon/contrib/login_methods/openid_auth.py b/gluon/contrib/login_methods/openid_auth.py index d754e5e4..a2c22633 100644 --- a/gluon/contrib/login_methods/openid_auth.py +++ b/gluon/contrib/login_methods/openid_auth.py @@ -630,3 +630,4 @@ class Web2pyStore(OpenIDStore): return self.cleanupNonces(), self.cleanupAssociations() + diff --git a/gluon/contrib/login_methods/pam_auth.py b/gluon/contrib/login_methods/pam_auth.py index a8a59523..31c343e0 100644 --- a/gluon/contrib/login_methods/pam_auth.py +++ b/gluon/contrib/login_methods/pam_auth.py @@ -20,3 +20,4 @@ def pam_auth(): return pam_auth_aux + diff --git a/gluon/contrib/login_methods/rpx_account.py b/gluon/contrib/login_methods/rpx_account.py index 544f4ba3..34688a69 100644 --- a/gluon/contrib/login_methods/rpx_account.py +++ b/gluon/contrib/login_methods/rpx_account.py @@ -53,12 +53,12 @@ class RPXAccount(object): self.prompt = prompt self.on_login_failure = on_login_failure self.mappings = Storage() - + dn = {'givenName':'','familyName':''} self.mappings.Facebook = lambda profile, dn=dn:\ dict(registration_id = profile.get("identifier",""), username = profile.get("preferredUsername",""), - email = profile.get("email",""), + email = profile.get("email",""), first_name = profile.get("name",dn).get("givenName",""), last_name = profile.get("name",dn).get("familyName","")) self.mappings.Google = lambda profile, dn=dn:\ @@ -120,8 +120,9 @@ def use_janrain(auth,filename='private/janrain.key',**kwargs): request = current.request domain,key = open(path,'r').read().strip().split(':') host = current.request.env.http_host - url = '%s' % URL(r=request, c='default', f='user', args=['login'], scheme=True) + url = URL('default', 'user', args='login', scheme=True) auth.settings.actions_disabled = \ ['register','change_password','request_reset_password'] auth.settings.login_form = RPXAccount( request, api_key=key,domain=domain, url = url,**kwargs) + diff --git a/gluon/contrib/login_methods/x509_auth.py b/gluon/contrib/login_methods/x509_auth.py index 09c3e922..36db643d 100644 --- a/gluon/contrib/login_methods/x509_auth.py +++ b/gluon/contrib/login_methods/x509_auth.py @@ -53,7 +53,7 @@ class X509Auth(object): # cn = self.subject.cn self.subject = Storage(filter(None, map(lambda x: - (x,map(lambda y: + (x,map(lambda y: y.get_data().as_text(), subject.get_entries_by_nid(subject.nid[x]))), subject.nid.keys()))) @@ -63,7 +63,7 @@ class X509Auth(object): def login_form(self, **args): raise HTTP(403,'Login not allowed. No valid x509 crentials') - + def login_url(self, next="/"): raise HTTP(403,'Login not allowed. No valid x509 crentials') @@ -102,3 +102,4 @@ class X509Auth(object): return profile + diff --git a/gluon/contrib/markmin/markmin2html.py b/gluon/contrib/markmin/markmin2html.py index a84c6d3a..8e6431bc 100755 --- a/gluon/contrib/markmin/markmin2html.py +++ b/gluon/contrib/markmin/markmin2html.py @@ -523,7 +523,7 @@ regex_list=re.compile('^(?:(#{1,6}|\.+ |\++ |\++\. |\-+ |\-+\. )\s*)?(.*)$') regex_bq_headline=re.compile('^(?:(\.+|\++|\-+)(\.)?\s+)?(-{3}-*)$') regex_tq=re.compile('^(-{3}-*)(?::(?P[a-zA-Z][_a-zA-Z\-\d]*)(?:\[(?P

    [a-zA-Z][_a-zA-Z\-\d]*)\])?)?$') regex_proto = re.compile(r'(?/=])(?P

    \w+):(?P\w+://[\w\d\-+=?%&/:.]+)', re.M) -regex_auto = re.compile(r'(?/=])(?P\w+://[\w\d\-+_=?%&/:.]+)',re.M) +regex_auto = re.compile(r'(?/=])(?P\w+://[^\s\'\"\]\}\)]+)',re.M) regex_link=re.compile(r'('+LINK+r')|\[\[(?P.+?)\]\]') regex_link_level2=re.compile(r'^(?P\S.*?)?(?:\s+\[(?P.+?)\])?(?:\s+(?P\S+))?(?:\s+(?P

    popup))?\s*$') regex_media_level2=re.compile(r'^(?P\S.*?)?(?:\s+\[(?P.+?)\])?(?:\s+(?P\S+))?\s+(?P

    img|IMG|left|right|center|video|audio)(?:\s+(?P\d+px))?\s*$') @@ -598,7 +598,7 @@ def render(text, - class_prefix is a prefix for ALL classes in markmin text. E.g. if class_prefix='my_' then for ``test``:cls class will be changed to "my_cls" (default value is '') - id_prefix is prefix for ALL ids in markmin text (default value is 'markmin_'). E.g.: - -- [[id]] will be converted to + -- [[id]] will be converted to

    -- [[link #id]] will be converted to link -- ``test``:cls[id] will be converted to test @@ -729,19 +729,19 @@ def render(text, '

    [[probe]]

    ' >>> render(r"\\\\[[probe]]") - '

    \\\\

    ' + '

    \\\\

    ' >>> render(r"\\\\\\[[probe]]") '

    \\\\[[probe]]

    ' >>> render(r"\\\\\\\\[[probe]]") - '

    \\\\\\\\

    ' + '

    \\\\\\\\

    ' >>> render(r"\\\\\\\\\[[probe]]") '

    \\\\\\\\[[probe]]

    ' >>> render(r"\\\\\\\\\\\[[probe]]") - '

    \\\\\\\\\\\\

    ' + '

    \\\\\\\\\\\\

    ' >>> render("``[[ [\\[[probe\]\\]] URL\\[x\\]]]``:red[dummy_params]") 'URL[x]' @@ -789,7 +789,7 @@ def render(text, '

    test 1

    ' >>> render('[[id1 [span **messag** in ''markmin''] ]] ... [[**link** to id [link\\\'s title] #mark1]]') - '

    span messag in markmin ... link to id

    ' + '

    span messag in markmin
    ... link to id

    ' """ if autolinks=="default": autolinks = autolinks_simple @@ -801,7 +801,7 @@ def render(text, # this is experimental @{function/args} # turns into a digitally signed URL def u1(match,URL=URL): - a,c,f,args = match.group('a','c','f','args') + a,c,f,args = match.group('a','c','f','args') return URL(a=a or None,c=c or None,f = f or None, args=args.split('/'), scheme=True, host=True) text = regex_URL.sub(u1,text) @@ -1200,7 +1200,7 @@ def render(text, protolinks, class_prefix, id_prefix) if t else k return '%(t)s' \ % dict(k=k, title=title, target=target, t=t) - return '%s' % (escape(id_prefix+t), + return '
    %s
    ' % (escape(id_prefix+t), render(a, {},{},'br', URL, environment, latex, autolinks, protolinks, class_prefix, id_prefix)) diff --git a/gluon/contrib/memdb.py b/gluon/contrib/memdb.py index ee25566d..ae2189aa 100644 --- a/gluon/contrib/memdb.py +++ b/gluon/contrib/memdb.py @@ -911,3 +911,4 @@ if __name__ == '__main__': + diff --git a/gluon/contrib/pam.py b/gluon/contrib/pam.py index 57b9e5be..4622b7cc 100644 --- a/gluon/contrib/pam.py +++ b/gluon/contrib/pam.py @@ -127,3 +127,4 @@ if __name__ == "__main__": + diff --git a/gluon/contrib/pbkdf2.py b/gluon/contrib/pbkdf2.py index c751b4be..a58a9dd1 100644 --- a/gluon/contrib/pbkdf2.py +++ b/gluon/contrib/pbkdf2.py @@ -129,3 +129,4 @@ def test(): if __name__ == '__main__': test() + diff --git a/gluon/contrib/populate.py b/gluon/contrib/populate.py index efd90670..38760f00 100644 --- a/gluon/contrib/populate.py +++ b/gluon/contrib/populate.py @@ -171,3 +171,4 @@ if __name__ == '__main__': + diff --git a/gluon/contrib/pyfpdf.py b/gluon/contrib/pyfpdf.py new file mode 100644 index 00000000..1cc8c8f0 --- /dev/null +++ b/gluon/contrib/pyfpdf.py @@ -0,0 +1,12 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +"FPDF for python (a.k.a. pyfpdf)" +# Read more about this http://code.google.com/p/pyfpdf +# Please note that new package name is fpdf (to avoid some naming conflicts) +# import fpdf into pyfpdf for backward compatibility (prior web2py 2.0): +from fpdf import * + +# import warnings +# warnings.warn("pyfpdf package name is deprecated, please use fpdf instead") + diff --git a/gluon/contrib/pyfpdf/README b/gluon/contrib/pyfpdf/README deleted file mode 100644 index 88916d70..00000000 --- a/gluon/contrib/pyfpdf/README +++ /dev/null @@ -1 +0,0 @@ -Read more about this http://code.google.com/p/pyfpdf diff --git a/gluon/contrib/pyfpdf/__init__.py b/gluon/contrib/pyfpdf/__init__.py deleted file mode 100644 index 7c4a2ca3..00000000 --- a/gluon/contrib/pyfpdf/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -from fpdf import FPDF -from html import HTMLMixin -from template import Template - - diff --git a/gluon/contrib/pyfpdf/designer.py b/gluon/contrib/pyfpdf/designer.py deleted file mode 100644 index 3807bd6d..00000000 --- a/gluon/contrib/pyfpdf/designer.py +++ /dev/null @@ -1,736 +0,0 @@ -#!/usr/bin/python -# -*- coding: latin-1 -*- -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by the -# Free Software Foundation; either version 3, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTIBILITY -# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License -# for more details. - -"Visual Template designer for PyFPDF (using wxPython OGL library)" - -__author__ = "Mariano Reingart " -__copyright__ = "Copyright (C) 2011 Mariano Reingart" -__license__ = "GPL 3.0" -__version__ = "1.01a" - -# Based on: -# * pySjetch.py wxPython sample application -# * OGL.py and other wxPython demo modules - - -import os, sys -import wx -import wx.lib.ogl as ogl -from wx.lib.wordwrap import wordwrap - -DEBUG = True - - -class CustomDialog(wx.Dialog): - "A dinamyc dialog to ask user about arbitrary fields" - - def __init__( - self, parent, ID, title, size=wx.DefaultSize, pos=wx.DefaultPosition, - style=wx.DEFAULT_DIALOG_STYLE, fields=None, data=None, - ): - - wx.Dialog.__init__ (self, parent, ID, title, pos, size, style) - - sizer = wx.BoxSizer(wx.VERTICAL) - - self.textctrls = {} - for field in fields: - box = wx.BoxSizer(wx.HORIZONTAL) - label = wx.StaticText(self, -1, field) - label.SetHelpText("This is the help text for the label") - box.Add(label, 1, wx.ALIGN_CENTRE|wx.ALL, 5) - text = wx.TextCtrl(self, -1, "", size=(80,-1)) - text.SetHelpText("Here's some help text for field #1") - if field in data: - text.SetValue(repr(data[field])) - box.Add(text, 1, wx.ALIGN_CENTRE|wx.ALL, 1) - sizer.Add(box, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.ALL, 1) - self.textctrls[field] = text - - line = wx.StaticLine(self, -1, size=(20,-1), style=wx.LI_HORIZONTAL) - sizer.Add(line, 0, wx.GROW|wx.ALIGN_CENTER_VERTICAL|wx.RIGHT|wx.TOP, 5) - - btnsizer = wx.StdDialogButtonSizer() - - btn = wx.Button(self, wx.ID_OK) - btn.SetHelpText("The OK button completes the dialog") - btn.SetDefault() - btnsizer.AddButton(btn) - - btn = wx.Button(self, wx.ID_CANCEL) - btn.SetHelpText("The Cancel button cancels the dialog. (Cool, huh?)") - btnsizer.AddButton(btn) - btnsizer.Realize() - - sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 5) - - self.SetSizer(sizer) - sizer.Fit(self) - - @classmethod - def do_input(Class, parent, title, fields, data): - dlg = Class(parent, -1, title, size=(350, 200), - style=wx.DEFAULT_DIALOG_STYLE, # & ~wx.CLOSE_BOX, - fields=fields, data=data - ) - dlg.CenterOnScreen() - while 1: - val = dlg.ShowModal() - if val == wx.ID_OK: - values = {} - for field in fields: - try: - values[field] = eval(dlg.textctrls[field].GetValue()) - except Exception, e: - msg = wx.MessageDialog(parent, unicode(e), - "Error in field %s" % field, - wx.OK | wx.ICON_INFORMATION - ) - msg.ShowModal() - msg.Destroy() - break - else: - return dict([(field, values[field]) for field in fields]) - else: - return None - - -class MyEvtHandler(ogl.ShapeEvtHandler): - "Custom Event Handler for Shapes" - def __init__(self, callback): - ogl.ShapeEvtHandler.__init__(self) - self.callback = callback - - def OnLeftClick(self, x, y, keys=0, attachment=0): - shape = self.GetShape() - canvas = shape.GetCanvas() - dc = wx.ClientDC(canvas) - canvas.PrepareDC(dc) - - if shape.Selected() and keys & ogl.KEY_SHIFT: - shape.Select(False, dc) - #canvas.Redraw(dc) - canvas.Refresh(False) - else: - redraw = False - shapeList = canvas.GetDiagram().GetShapeList() - toUnselect = [] - - for s in shapeList: - if s.Selected() and not keys & ogl.KEY_SHIFT: - # If we unselect it now then some of the objects in - # shapeList will become invalid (the control points are - # shapes too!) and bad things will happen... - toUnselect.append(s) - - shape.Select(True, dc) - - if toUnselect: - for s in toUnselect: - s.Select(False, dc) - ##canvas.Redraw(dc) - canvas.Refresh(False) - - self.callback() - - def OnEndDragLeft(self, x, y, keys=0, attachment=0): - shape = self.GetShape() - ogl.ShapeEvtHandler.OnEndDragLeft(self, x, y, keys, attachment) - - if not shape.Selected(): - self.OnLeftClick(x, y, keys, attachment) - - self.callback() - - def OnSizingEndDragLeft(self, pt, x, y, keys, attch): - ogl.ShapeEvtHandler.OnSizingEndDragLeft(self, pt, x, y, keys, attch) - self.callback() - - def OnMovePost(self, dc, x, y, oldX, oldY, display): - shape = self.GetShape() - ogl.ShapeEvtHandler.OnMovePost(self, dc, x, y, oldX, oldY, display) - self.callback() - if "wxMac" in wx.PlatformInfo: - shape.GetCanvas().Refresh(False) - - def OnLeftDoubleClick(self, x, y, keys = 0, attachment = 0): - self.callback("LeftDoubleClick") - - def OnRightClick(self, *dontcare): - self.callback("RightClick") - - -class Element(object): - "Visual class that represent a placeholder in the template" - - fields = ['name', 'type', - 'x1', 'y1', 'x2', 'y2', - 'font', 'size', - 'bold', 'italic', 'underline', - 'foreground', 'background', - 'align', 'text', 'priority',] - - def __init__(self, canvas=None, frame=None, zoom=5.0, static=False, **kwargs): - self.kwargs = kwargs - self.zoom = zoom - self.frame = frame - self.canvas = canvas - self.static = static - - name = kwargs['name'] - kwargs['type'] - type = kwargs['type'] - - x, y, w, h = self.set_coordinates(kwargs['x1'], kwargs['y1'], kwargs['x2'], kwargs['y2']) - - text = kwargs['text'] - - shape = self.shape = ogl.RectangleShape(w, h) - - if not static: - shape.SetDraggable(True, True) - - shape.SetX(x) - shape.SetY(y) - #if pen: shape.SetPen(pen) - #if brush: shape.SetBrush(brush) - shape.SetBrush(wx.TRANSPARENT_BRUSH) - - if type not in ('L', 'B', 'BC'): - if not static: - pen = wx.LIGHT_GREY_PEN - else: - pen = wx.RED_PEN - shape.SetPen(pen) - - self.text = kwargs['text'] - - evthandler = MyEvtHandler(self.evt_callback) - evthandler.SetShape(shape) - evthandler.SetPreviousHandler(shape.GetEventHandler()) - shape.SetEventHandler(evthandler) - shape.SetCentreResize(False) - shape.SetMaintainAspectRatio(False) - - canvas.AddShape( shape ) - - @classmethod - def new(Class, parent): - data = dict(name='some_name', type='T', - x1=5.0, y1=5.0, x2=100.0, y2=10.0, - font="Arial", size=12, - bold=False, italic=False, underline=False, - foreground= 0x000000, background=0xFFFFFF, - align="L", text="", priority=0) - data = CustomDialog.do_input(parent, 'New element', Class.fields, data) - if data: - return Class(canvas=parent.canvas, frame=parent, **data) - - def edit(self): - "Edit current element (show a dialog box with all fields)" - data = self.kwargs.copy() - x1, y1, x2, y2 = self.get_coordinates() - data.update(dict(name=self.name, - text=self.text, - x1=x1, y1=y1, x2=x2, y2=y2, - )) - data = CustomDialog.do_input(self.frame, 'Edit element', self.fields, data) - if data: - self.kwargs.update(data) - self.name = data['name'] - self.text = data['text'] - x,y, w, h = self.set_coordinates(data['x1'], data['y1'], data['x2'], data['y2']) - self.shape.SetX(x) - self.shape.SetY(y) - self.shape.SetWidth(w) - self.shape.SetHeight(h) - self.canvas.Refresh(False) - self.canvas.GetDiagram().ShowAll(1) - - def edit_text(self): - "Allow text edition (i.e. for doubleclick)" - dlg = wx.TextEntryDialog( - self.frame, 'Text for %s' % self.name, - 'Edit Text', '') - if self.text: - dlg.SetValue(self.text) - if dlg.ShowModal() == wx.ID_OK: - self.text = dlg.GetValue().encode("latin1") - dlg.Destroy() - - def copy(self): - "Return an identical duplicate" - kwargs = self.as_dict() - element = Element(canvas=self.canvas, frame=self.frame, zoom=self.zoom, static=self.static, **kwargs) - return element - - def remove(self): - "Erases visual shape from OGL canvas (element must be deleted manually)" - self.canvas.RemoveShape(self.shape) - - def move(self, dx, dy): - "Change pdf coordinates (converting to wx internal values)" - x1, y1, x2, y2 = self.get_coordinates() - x1 += dx - x2 += dx - y1 += dy - y2 += dy - x, y, w, h = self.set_coordinates(x1, y1, x2, y2) - self.shape.SetX(x) - self.shape.SetY(y) - - def evt_callback(self, evt_type=None): - "Event dispatcher" - if evt_type=="LeftDoubleClick": - self.edit_text() - if evt_type=='RightClick': - self.edit() - - # update the status bar - x1, y1, x2, y2 = self.get_coordinates() - self.frame.SetStatusText("%s (%0.2f, %0.2f) - (%0.2f, %0.2f)" % - (self.name, x1, y1, x2, y2)) - - def get_coordinates(self): - "Convert from wx to pdf coordinates" - x, y = self.shape.GetX(), self.shape.GetY() - w, h = self.shape.GetBoundingBoxMax() - w -= 1 - h -= 1 - x1 = x/self.zoom - w/self.zoom/2.0 - x2 = x/self.zoom + w/self.zoom/2.0 - y1 = y/self.zoom - h/self.zoom/2.0 - y2 = y/self.zoom + h/self.zoom/2.0 - return x1, y1, x2, y2 - - def set_coordinates(self, x1, y1, x2, y2): - "Convert from pdf to wx coordinates" - x1 = x1 * self.zoom - x2 = x2 * self.zoom - y1 = y1 * self.zoom - y2 = y2 * self.zoom - - # shapes seems to be centred, pdf coord not - w = max(x1, x2) - min(x1, x2) + 1 - h = max(y1, y2) - min(y1, y2) + 1 - x = (min(x1, x2) + w/2.0) - y = (min(y1, y2) + h/2.0) - return x, y, w, h - - def text(self, txt=None): - if txt is not None: - if not isinstance(txt,str): - txt = str(txt) - self.kwargs['text'] = txt - self.shape.ClearText() - for line in txt.split('\n'): - self.shape.AddText(unicode(line, "latin1")) - self.canvas.Refresh(False) - return self.kwargs['text'] - text = property(text, text) - - def set_x(self, x): - self.shape.SetX(x) - self.canvas.Refresh(False) - self.evt_callback() - def set_y(self, y): - self.shape.SetY(y) - self.canvas.Refresh(False) - self.evt_callback() - def get_x(self): - return self.shape.GetX() - def get_y(self): - return self.shape.GetY() - - x = property(get_x, set_x) - y = property(get_y, set_y) - - def selected(self, sel=None): - if sel is not None: - print "Setting Select(%s)" % sel - self.shape.Select(sel) - return self.shape.Selected() - selected = property(selected, selected) - - def name(self, name=None): - if name is not None: - self.kwargs['name'] = name - return self.kwargs['name'] - name = property(name, name) - - def __contains__(self, k): - "Implement in keyword for searchs" - return k in self.name.lower() or self.text and k in self.text.lower() - - def as_dict(self): - "Return a dictionary representation, used by pyfpdf" - d = self.kwargs - x1, y1, x2, y2 = self.get_coordinates() - d.update({ - 'x1': x1, 'y1': y1, 'x2': x2, 'y2': y2, - 'text': self.text}) - return d - - -class AppFrame(wx.Frame): - "OGL Designer main window" - title = "PyFPDF Template Designer (wx OGL)" - - def __init__(self): - wx.Frame.__init__( self, - None, -1, self.title, - size=(640,480), - style=wx.DEFAULT_FRAME_STYLE ) - sys.excepthook = self.except_hook - self.filename = "" - # Create a toolbar: - tsize = (16,16) - self.toolbar = self.CreateToolBar(wx.TB_HORIZONTAL | wx.NO_BORDER | wx.TB_FLAT) - - artBmp = wx.ArtProvider.GetBitmap - self.toolbar.AddSimpleTool( - wx.ID_NEW, artBmp(wx.ART_NEW, wx.ART_TOOLBAR, tsize), "New") - self.toolbar.AddSimpleTool( - wx.ID_OPEN, artBmp(wx.ART_FILE_OPEN, wx.ART_TOOLBAR, tsize), "Open") - self.toolbar.AddSimpleTool( - wx.ID_SAVE, artBmp(wx.ART_FILE_SAVE, wx.ART_TOOLBAR, tsize), "Save") - self.toolbar.AddSimpleTool( - wx.ID_SAVEAS, artBmp(wx.ART_FILE_SAVE_AS, wx.ART_TOOLBAR, tsize), - "Save As...") - #------- - self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool( - wx.ID_UNDO, artBmp(wx.ART_UNDO, wx.ART_TOOLBAR, tsize), "Undo") - self.toolbar.AddSimpleTool( - wx.ID_REDO, artBmp(wx.ART_REDO, wx.ART_TOOLBAR, tsize), "Redo") - self.toolbar.AddSeparator() - #------- - self.toolbar.AddSimpleTool( - wx.ID_CUT, artBmp(wx.ART_CUT, wx.ART_TOOLBAR, tsize), "Remove") - self.toolbar.AddSimpleTool( - wx.ID_COPY, artBmp(wx.ART_COPY, wx.ART_TOOLBAR, tsize), "Duplicate") - self.toolbar.AddSimpleTool( - wx.ID_PASTE, artBmp(wx.ART_PASTE, wx.ART_TOOLBAR, tsize), "Insert") - self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool( - wx.ID_FIND, artBmp(wx.ART_FIND, wx.ART_TOOLBAR, tsize), "Find") - self.toolbar.AddSeparator() - self.toolbar.AddSimpleTool( - wx.ID_PRINT, artBmp(wx.ART_PRINT, wx.ART_TOOLBAR, tsize), "Print") - self.toolbar.AddSimpleTool( - wx.ID_ABOUT, artBmp(wx.ART_HELP, wx.ART_TOOLBAR, tsize), "About") - - self.toolbar.Realize() - - self.toolbar.EnableTool(wx.ID_SAVEAS, False) - self.toolbar.EnableTool(wx.ID_UNDO, False) - self.toolbar.EnableTool(wx.ID_REDO, False) - - menu_handlers = [ - (wx.ID_NEW, self.do_new), - (wx.ID_OPEN, self.do_open), - (wx.ID_SAVE, self.do_save), - (wx.ID_PRINT, self.do_print), - (wx.ID_FIND, self.do_find), - (wx.ID_CUT, self.do_cut), - (wx.ID_COPY, self.do_copy), - (wx.ID_PASTE, self.do_paste), - (wx.ID_ABOUT, self.do_about), - ] - for menu_id, handler in menu_handlers: - self.Bind(wx.EVT_MENU, handler, id = menu_id) - - sizer = wx.BoxSizer(wx.VERTICAL) - # put stuff into sizer - - self.CreateStatusBar() - - canvas = self.canvas = ogl.ShapeCanvas( self ) - maxWidth = 1500 - maxHeight = 2000 - canvas.SetScrollbars(20, 20, maxWidth/20, maxHeight/20) - sizer.Add( canvas, 1, wx.GROW ) - - canvas.SetBackgroundColour("WHITE") # - - diagram = self.diagram = ogl.Diagram() - canvas.SetDiagram( diagram ) - diagram.SetCanvas( canvas ) - diagram.SetSnapToGrid( False ) - - # apply sizer - self.SetSizer(sizer) - self.SetAutoLayout(1) - self.Show(1) - - self.Bind(wx.EVT_CHAR_HOOK, self.on_key_event) - self.elements = [] - - def on_key_event(self, event): - """ Respond to a keypress event. - - We make the arrow keys move the selected object(s) by one pixel in - the given direction. - """ - step = 1 - if event.ControlDown(): - step = 20 - - if event.GetKeyCode() == wx.WXK_UP: - self.move_elements(0, -step) - elif event.GetKeyCode() == wx.WXK_DOWN: - self.move_elements(0, step) - elif event.GetKeyCode() == wx.WXK_LEFT: - self.move_elements(-step, 0) - elif event.GetKeyCode() == wx.WXK_RIGHT: - self.move_elements(step, 0) - elif event.GetKeyCode() == wx.WXK_DELETE: - self.do_cut() - else: - event.Skip() - - def do_new(self, evt=None): - for element in self.elements: - element.remove() - self.elements = [] - # draw paper size guides - for k, (w, h) in [('legal', (216, 356)), ('A4', (210, 297)), ('letter', (216, 279))]: - self.create_elements( - k, 'R', 0, 0, w, h, - size=70, foreground=0x808080, priority=-100, - canvas=self.canvas, frame=self, static=True) - self.diagram.ShowAll( 1 ) - - def do_open(self, evt): - dlg = wx.FileDialog( - self, message="Choose a file", - defaultDir=os.getcwd(), - defaultFile="invoice.csv", - wildcard="CSV Files (*.csv)|*.csv", - style=wx.OPEN - ) - - if dlg.ShowModal() == wx.ID_OK: - # This returns a Python list of files that were selected. - self.filename = dlg.GetPaths()[0] - - dlg.Destroy() - self.SetTitle(self.filename + " - " + self.title) - - self.do_new() - tmp = [] - f = open(self.filename) - try: - filedata = f.readlines() - finally: - f.close() - for lno, linea in enumerate(filedata): - if DEBUG: print "processing line", lno, linea - args = [] - for i,v in enumerate(linea.split(";")): - if not v.startswith("'"): - v = v.replace(",",".") - else: - v = v#.decode('latin1') - if v.strip()=='': - v = None - else: - v = eval(v.strip()) - args.append(v) - tmp.append(args) - - # sort by z-order (priority) - for args in sorted(tmp, key=lambda t: t[-1]): - if DEBUG: print args - self.create_elements(*args) - self.diagram.ShowAll( 1 ) # - - return True - - def do_save(self, evt, filename=None): - try: - from time import gmtime, strftime - ts = strftime("%Y%m%d%H%M%S", gmtime()) - os.rename(self.filename, self.filename + ts + ".bak") - except Exception, e: - if DEBUG: print e - pass - - def csv_repr(v, decimal_sep="."): - if isinstance(v, float): - return ("%0.2f" % v).replace(".", decimal_sep) - else: - return repr(v) - - f = open(self.filename, "w") - try: - for element in sorted(self.elements, key=lambda e:e.name): - if element.static: - continue - d = element.as_dict() - l = [d['name'], d['type'], - d['x1'], d['y1'], d['x2'], d['y2'], - d['font'], d['size'], - d['bold'], d['italic'], d['underline'], - d['foreground'], d['background'], - d['align'], d['text'], d['priority'], - ] - f.write(";".join([csv_repr(v) for v in l])) - f.write("\n") - finally: - f.close() - - def do_print(self, evt): - # genero el renderizador con propiedades del PDF - from template import Template - t = Template(elements=[e.as_dict() for e in self.elements if not e.static]) - t.add_page() - if not t['logo'] or not os.path.exists(t['logo']): - # put a default logo so it doesn't trow an exception - logo = os.path.join(os.path.dirname(__file__), 'tutorial','logo.png') - t.set('logo', logo) - try: - t.render(self.filename +".pdf") - except: - if DEBUG and False: - import pdb; - pdb.pm() - else: - raise - if sys.platform=="linux2": - os.system("evince ""%s""" % self.filename +".pdf") - else: - os.startfile(self.filename +".pdf") - - def do_find(self, evt): - # busco nombre o texto - dlg = wx.TextEntryDialog( - self, 'Enter text to search for', - 'Find Text', '') - if dlg.ShowModal() == wx.ID_OK: - txt = dlg.GetValue().encode("latin1").lower() - for element in self.elements: - if txt in element: - element.selected = True - print "Found:", element.name - self.canvas.Refresh(False) - dlg.Destroy() - - def do_cut(self, evt=None): - "Delete selected elements" - new_elements = [] - for element in self.elements: - if element.selected: - print "Erasing:", element.name - element.selected = False - self.canvas.Refresh(False) - element.remove() - else: - new_elements.append(element) - self.elements = new_elements - self.canvas.Refresh(False) - self.diagram.ShowAll( 1 ) - - def do_copy(self, evt): - "Duplicate selected elements" - fields = ['qty', 'dx', 'dy'] - data = {'qty': 1, 'dx': 0.0, 'dy': 5.0} - data = CustomDialog.do_input(self, 'Copy elements', fields, data) - if data: - new_elements = [] - for i in range(1, data['qty']+1): - for element in self.elements: - if element.selected: - print "Copying:", element.name - new_element = element.copy() - name = new_element.name - if len(name)>2 and name[-2:].isdigit(): - new_element.name = name[:-2] + "%02d" % (int(name[-2:])+i) - else: - new_element.name = new_element.name + "_copy" - new_element.selected = False - new_element.move(data['dx']*i, data['dy']*i) - new_elements.append(new_element) - self.elements.extend(new_elements) - self.canvas.Refresh(False) - self.diagram.ShowAll( 1 ) - - def do_paste(self, evt): - "Insert new elements" - element = Element.new(self) - if element: - self.canvas.Refresh(False) - self.elements.append(element) - self.diagram.ShowAll( 1 ) - - def create_elements(self, name, type, x1, y1, x2, y2, - font="Arial", size=12, - bold=False, italic=False, underline=False, - foreground= 0x000000, background=0xFFFFFF, - align="L", text="", priority=0, canvas=None, frame=None, static=False, - **kwargs): - element = Element(name=name, type=type, x1=x1, y1=y1, x2=x2, y2=y2, - font=font, size=size, - bold=bold, italic=italic, underline=underline, - foreground= foreground, background=background, - align=align, text=text, priority=priority, - canvas=canvas or self.canvas, frame=frame or self, - static=static) - self.elements.append(element) - - def move_elements(self, x, y): - for element in self.elements: - if element.selected: - print "moving", element.name, x, y - element.x = element.x + x - element.y = element.y + y - - def do_about(self, evt): - info = wx.AboutDialogInfo() - info.Name = self.title - info.Version = __version__ - info.Copyright = __copyright__ - info.Description = ( - "Visual Template designer for PyFPDF (using wxPython OGL library)\n" - "Input files are CSV format describing the layout, separated by ;\n" - "Use toolbar buttons to open, save, print (preview) your template, " - "and there are buttons to find, add, remove or duplicate elements.\n" - "Over an element, a double left click opens edit text dialog, " - "and a right click opens edit properties dialog. \n" - "Multiple element can be selected with shift left click. \n" - "Use arrow keys or drag-and-drop to move elements.\n" - "For further information see project webpage:" - ) - info.WebSite = ("http://code.google.com/p/pyfpdf/wiki/Templates", - "pyfpdf Google Code Project") - info.Developers = [ __author__, ] - - info.License = wordwrap(__license__, 500, wx.ClientDC(self)) - - # Then we call wx.AboutBox giving it that info object - wx.AboutBox(info) - - def except_hook(self, type, value, trace): - import traceback - exc = traceback.format_exception(type, value, trace) - for e in exc: wx.LogError(e) - wx.LogError('Unhandled Error: %s: %s'%(str(type), str(value))) - - -app = wx.PySimpleApp() -ogl.OGLInitialize() -frame = AppFrame() -app.MainLoop() -app.Destroy() - - - diff --git a/gluon/contrib/qdb.py b/gluon/contrib/qdb.py index bb6ecc0e..a23038d2 100644 --- a/gluon/contrib/qdb.py +++ b/gluon/contrib/qdb.py @@ -964,3 +964,4 @@ if __name__ == '__main__': + diff --git a/gluon/contrib/redis_cache.py b/gluon/contrib/redis_cache.py index 3a1a47fa..2f2366d9 100644 --- a/gluon/contrib/redis_cache.py +++ b/gluon/contrib/redis_cache.py @@ -167,3 +167,4 @@ class RedisClient(object): + diff --git a/gluon/contrib/rss2.py b/gluon/contrib/rss2.py index 8f1a601e..47fde9d2 100644 --- a/gluon/contrib/rss2.py +++ b/gluon/contrib/rss2.py @@ -592,3 +592,4 @@ if __name__ == '__main__': + diff --git a/gluon/contrib/shell.py b/gluon/contrib/shell.py index d40400b3..227a1aec 100755 --- a/gluon/contrib/shell.py +++ b/gluon/contrib/shell.py @@ -270,3 +270,4 @@ if __name__=='__main__': + diff --git a/gluon/contrib/simplejsonrpc.py b/gluon/contrib/simplejsonrpc.py index 865bb1c4..5a29b5b8 100644 --- a/gluon/contrib/simplejsonrpc.py +++ b/gluon/contrib/simplejsonrpc.py @@ -151,3 +151,4 @@ if __name__ == "__main__": + diff --git a/gluon/contrib/sms_utils.py b/gluon/contrib/sms_utils.py index 29a50226..5777661e 100644 --- a/gluon/contrib/sms_utils.py +++ b/gluon/contrib/sms_utils.py @@ -116,3 +116,4 @@ def sms_email(number,provider): + diff --git a/gluon/contrib/spreadsheet.py b/gluon/contrib/spreadsheet.py index b343eb31..b13dced0 100644 --- a/gluon/contrib/spreadsheet.py +++ b/gluon/contrib/spreadsheet.py @@ -879,3 +879,4 @@ if __name__ == '__main__': print s['c'].computed_value + diff --git a/gluon/contrib/stripe.py b/gluon/contrib/stripe.py index fc1a12b9..dbde9117 100644 --- a/gluon/contrib/stripe.py +++ b/gluon/contrib/stripe.py @@ -66,3 +66,4 @@ if __name__=='__main__': + diff --git a/gluon/contrib/taskbar_widget.py b/gluon/contrib/taskbar_widget.py index 8009c93f..8f9cba9e 100644 --- a/gluon/contrib/taskbar_widget.py +++ b/gluon/contrib/taskbar_widget.py @@ -247,3 +247,4 @@ class TaskBarIcon: + diff --git a/gluon/contrib/timecollect.py b/gluon/contrib/timecollect.py index 229ac2e5..601b7fb2 100644 --- a/gluon/contrib/timecollect.py +++ b/gluon/contrib/timecollect.py @@ -95,3 +95,4 @@ if __name__=='__main__': print(t.getReportText(orderByCost=False)) + diff --git a/gluon/contrib/user_agent_parser.py b/gluon/contrib/user_agent_parser.py index c326e92c..e84f4892 100644 --- a/gluon/contrib/user_agent_parser.py +++ b/gluon/contrib/user_agent_parser.py @@ -520,3 +520,4 @@ class mobilize(object): + diff --git a/gluon/custom_import.py b/gluon/custom_import.py index f31d3630..afcaff33 100644 --- a/gluon/custom_import.py +++ b/gluon/custom_import.py @@ -329,3 +329,4 @@ class _Web2pyDateTrackerImporter(_Web2pyImporter, _DateTrackerImporter): + diff --git a/gluon/dal.py b/gluon/dal.py index 8d313931..97088915 100644 --- a/gluon/dal.py +++ b/gluon/dal.py @@ -174,7 +174,7 @@ import platform CALLABLETYPES = (types.LambdaType, types.FunctionType, types.BuiltinFunctionType, types.MethodType, types.BuiltinMethodType) -TABLE_ARGS = ('migrate','primarykey','fake_migrate','format','singular','plural','trigger_name','sequence_name','common_filter','polymodel','table_class') +TABLE_ARGS = ('migrate','primarykey','fake_migrate','format','singular','plural','trigger_name','sequence_name','common_filter','polymodel','table_class','on_define') ################################################################################### # following checks allow the use of dal without web2py, as a standalone module @@ -1317,7 +1317,7 @@ class BaseAdapter(ConnectionPool): tablename,fieldname = item.split('.') new_fields.append(self.db[tablename][fieldname]) else: - new_fields.append(Expression(self.db,item)) + new_fields.append(Expression(self.db,lambda:item)) else: new_fields.append(item) # ## if no fields specified take them all from the requested tables @@ -2735,6 +2735,14 @@ class OracleAdapter(BaseAdapter): self.execute('SELECT %s.currval FROM dual;' % sequence_name) return int(self.cursor.fetchone()[0]) + def parse_value(self, value, field_type, blob_decode=True): + if blob_decode and isinstance(value, cx_Oracle.LOB): + try: + value = value.read() + except cx_Oracle.ProgrammingError: + # After a subsequent fetch the LOB value is not valid anymore + pass + return BaseAdapter.parse_value(self, value, field_type, blob_decode) class MSSQLAdapter(BaseAdapter): @@ -6281,11 +6289,9 @@ class Row(dict): this is only used to store a Row """ - def __getattr__(self, key): - return self[key] - - def __setattr__(self, key, value): - self[key] = value + __setattr__ = dict.__setitem__ + __getattr__ = dict.__getitem__ + __delattr__ = dict.__delitem__ def __getitem__(self, key): key=str(key) @@ -6299,8 +6305,10 @@ class Row(dict): key = m.group(2) return dict.__getitem__(self, key) - def __call__(self,key): - return self.__getitem__(key) + __call__ = __getitem__ + + #def __call__(self,key): + # return self.__getitem__(key) def __setitem__(self, key, value): dict.__setitem__(self, str(key), value) @@ -6924,7 +6932,7 @@ def index(): tablename, *fields, **args - ): + ): if not isinstance(tablename,str): raise SyntaxError, "missing table name" elif tablename.startswith('_') or hasattr(self,tablename) or \ @@ -6938,7 +6946,7 @@ def index(): invalid_args = [key for key in args if not key in TABLE_ARGS] if invalid_args: raise SyntaxError, 'invalid table "%s" attributes: %s' \ - % (tablename,invalid_args) + % (tablename,invalid_args) if self._lazy_tables and not tablename in self._LAZY_TABLES: self._LAZY_TABLES[tablename] = (tablename,fields,args) table = None @@ -6977,6 +6985,8 @@ def index(): sql_locker.release() else: table._dbt = None + on_define = args.get('on_define',None) + if on_define: on_define(table) return table def __iter__(self): @@ -7025,9 +7035,10 @@ def index(): thread.instances.remove(self._adapter) self._adapter.close() - def executesql(self, query, placeholders=None, as_dict=False): + def executesql(self, query, placeholders=None, as_dict=False, + fields=None, colnames=None): """ - placeholders is optional and will always be None when using DAL. + placeholders is optional and will always be None. If using raw SQL with placeholders, placeholders may be a sequence of values to be substituted in or, (if supported by the DB driver), a dictionary with keys @@ -7044,7 +7055,21 @@ def index(): [{field1: value1, field2: value2}, {field1: value1b, field2: value2b}] - --bmeredyk + Added 2012-08-24 "fields" optional argument. If not None, the + results cursor returned by the DB driver will be converted to a + DAL Rows object using the db._adapter.parse() method. This requires + specifying the "fields" argument as a list of DAL Field objects + that match the fields returned from the DB. The Field objects should + be part of one or more Table objects defined on the DAL object. + The "fields" list can include one or more DAL Table objects in addition + to or instead of including Field objects, or it can be just a single + table (not in a list). In that case, the Field objects will be + extracted from the table(s). + + The field names will be extracted from the Field objects, or optionally, + a list of field names can be provided (in tablename.fieldname format) + via the "colnames" argument. Note, the fields and colnames must be in + the same order as the fields in the results cursor returned from the DB. """ if placeholders: self._adapter.execute(query, placeholders) @@ -7064,11 +7089,22 @@ def index(): # convert the list for each row into a dictionary so it's # easier to work with. row['field_name'] rather than row[0] return [dict(zip(fields,row)) for row in data] - # see if any results returned from database - try: - return self._adapter.cursor.fetchall() - except: - return None + data = self._adapter.cursor.fetchall() + if fields: + if not isinstance(fields, list): + fields = [fields] + extracted_fields = [] + for field in fields: + if isinstance(field, Table): + extracted_fields.extend([f for f in field]) + else: + extracted_fields.append(field) + if not colnames: + colnames = ['%s.%s' % (f.tablename, f.name) + for f in extracted_fields] + data = self._adapter.parse( + data, fields=extracted_fields, colnames=colnames) + return data def _update_referenced_by(self, other): for tablename in self.tables: @@ -7462,15 +7498,15 @@ class Table(dict): 'value must be a dictionary: %s' % value dict.__setitem__(self, str(key), value) - def __getattr__(self, key): - return self[key] + __getattr__ = __getitem__ def __delitem__(self, key): if isinstance(key, dict): query = self._build_query(key) if not self._db(query).delete(): raise SyntaxError, 'No such record: %s' % key - elif not str(key).isdigit() or not self._db(self._id == key).delete(): + elif not str(key).isdigit() or \ + not self._db(self._id == key).delete(): raise SyntaxError, 'No such record: %s' % key def __setattr__(self, key, value): @@ -7499,33 +7535,38 @@ class Table(dict): return self._db._adapter.drop(self,mode) def _listify(self,fields,update=False): - new_fields = [] - new_fields_names = [] + new_fields = {} # format: new_fields[name] = (field,value) + # store all fields passed as input in new_fields for name in fields: if not name in self.fields: if name != 'id': - raise SyntaxError, 'Field %s does not belong to the table' % name + raise SyntaxError, \ + 'Field %s does not belong to the table' % name else: field = self[name] value = fields[name] - if field.filter_in: value = field.filter_in(value) - new_fields.append((field,value)) - new_fields_names.append(name) - for ofield in self: - if not ofield.name in new_fields_names: - if not update and not ofield.default is None: - new_fields.append((ofield,ofield.default)) - elif update and not ofield.update is None: - new_fields.append((ofield,ofield.update)) + if field.filter_in: + value = field.filter_in(value) + new_fields[name] = (field,value) + # check all fields that should be in the self table for ofield in self: + name = ofield.name + # if field is supposed to be computed, compute it! if ofield.compute: try: - new_fields.append((ofield,ofield.compute(Row(fields)))) + new_fields[name] = (ofield,ofield.compute(Row(fields))) except KeyError: pass - if not update and ofield.required and not ofield.name in new_fields_names: - raise SyntaxError,'Table: missing required field: %s' % ofield.name - return new_fields + # if field is required, check its default value + elif not name in new_fields: + if not update and not ofield.default is None: + new_fields[name] = (ofield,ofield.default) + elif update and not ofield.update is None: + new_fields[name] = (ofield,ofield.update) + # error if field if required, record to be create and field missing + if not update and ofield.required and not name in new_fields: + raise SyntaxError, 'Table: missing required field: %s' % name + return new_fields.values() def _attempt_upload(self, fields): for field in self: @@ -8149,6 +8190,9 @@ class Field(Expression): self.label = label if label!=None else fieldname.replace('_',' ').title() self.requires = requires if requires!=None else [] + def set_attributes(self,*args,**attributes): + self.__dict__.update(*args,**attributes) + def clone(self,point_self_references_to=False,**args): field = copy.copy(self) if point_self_references_to and \ @@ -9109,3 +9153,4 @@ if __name__ == '__main__': + diff --git a/gluon/debug.py b/gluon/debug.py index 6cb29b7f..b638cc12 100644 --- a/gluon/debug.py +++ b/gluon/debug.py @@ -192,3 +192,4 @@ gluon.main.global_settings.debugging = True + diff --git a/gluon/decoder.py b/gluon/decoder.py index 713a2c75..4eecf8ab 100644 --- a/gluon/decoder.py +++ b/gluon/decoder.py @@ -78,3 +78,4 @@ def decoder(buffer): + diff --git a/gluon/fileutils.py b/gluon/fileutils.py index f66c5fb7..fd23c604 100644 --- a/gluon/fileutils.py +++ b/gluon/fileutils.py @@ -397,3 +397,4 @@ def abspath(*relpath, **base): return os.path.join(global_settings.applications_parent, path) + diff --git a/gluon/globals.py b/gluon/globals.py index 23723731..7bf41549 100644 --- a/gluon/globals.py +++ b/gluon/globals.py @@ -101,7 +101,7 @@ class Request(Storage): self.is_https = False self.is_local = False self.global_settings = settings.global_settings - + def compute_uuid(self): self.uuid = '%s/%s.%s.%s' % ( self.application, @@ -691,3 +691,4 @@ class Session(Storage): + diff --git a/gluon/highlight.py b/gluon/highlight.py index 93c810c4..59332571 100644 --- a/gluon/highlight.py +++ b/gluon/highlight.py @@ -348,3 +348,4 @@ if __name__ == '__main__': + diff --git a/gluon/html.py b/gluon/html.py index 7aa16ec6..b758c597 100644 --- a/gluon/html.py +++ b/gluon/html.py @@ -484,6 +484,19 @@ class XmlComponent(object): components += [other] return CAT(*components) + def add_class(self, name): + """ add a class to _class attribute """ + classes = set(self['_class'].split())|set(name.split()) + self['_class'] = ' '.join(classes) if classes else None + return self + + def remove_class(self, name): + """ remove a class from _class attribute """ + classes = set(self['_class'].split())-set(name.split()) + self['_class'] = ' '.join(classes) if classes else None + return self + + class XML(XmlComponent): """ use it to wrap a string that contains XML/HTML so that it will not be @@ -949,6 +962,48 @@ class DIV(XmlComponent): >>> for c in a.elements('input, select, textarea'): c['_disabled'] = 'disabled' >>> a.xml() '
    ' + + Elements that are matched can also be replaced or removed by specifying + a "replace" argument (note, a list of the original matching elements + is still returned as usual). + + >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) + >>> b = a.elements('span.abc', replace=P('x', _class='xyz')) + >>> print a +

    x

    x

    x

    + + "replace" can be a callable, which will be passed the original element and + should return a new element to replace it. + + >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) + >>> b = a.elements('span.abc', replace=lambda el: P(el[0], _class='xyz')) + >>> print a +

    x

    y

    z

    + + If replace=None, matching elements will be removed completely. + + >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) + >>> b = a.elements('span', find='y', replace=None) + >>> print a +
    x
    z
    + + If a "find_text" argument is specified, elements will be searched for text + components that match find_text, and any matching text components will be + replaced (find_text is ignored if "replace" is not also specified). + Like the "find" argument, "find_text" can be a string or a compiled regex. + + >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='abc'), SPAN('z', _class='abc')))) + >>> b = a.elements(find_text=re.compile('x|y|z'), replace='hello') + >>> print a +
    hello
    hellohello
    + + If other attributes are specified along with find_text, then only components + that match the specified attributes will be searched for find_text. + + >>> a = DIV(DIV(SPAN('x', _class='abc'), DIV(SPAN('y', _class='efg'), SPAN('z', _class='abc')))) + >>> b = a.elements('span.efg', find_text=re.compile('x|y|z'), replace='hello') + >>> print a +
    x
    helloz
    """ if len(args)==1: args = [a.strip() for a in args[0].split(',')] @@ -977,49 +1032,60 @@ class DIV(XmlComponent): return self.elements(*args,**kargs) # make a copy of the components matches = [] - first_only = False - if kargs.has_key("first_only"): - first_only = kargs["first_only"] - del kargs["first_only"] # check if the component has an attribute with the same # value as provided check = True - tag = getattr(self,'tag').replace("/","") + tag = getattr(self,'tag').replace('/', '') if args and tag not in args: check = False for (key, value) in kargs.items(): - if isinstance(value,(str,int)): - if self[key] != str(value): + if key not in ['first_only', 'replace', 'find_text']: + if isinstance(value, (str, int)): + if self[key] != str(value): + check = False + elif key in self.attributes: + if not value.search(str(self[key])): + check = False + else: check = False - elif key in self.attributes: - if not value.search(str(self[key])): - check = False - else: - check = False if 'find' in kargs: find = kargs['find'] + is_regex = not isinstance(find, (str, int)) for c in self.components: - if isinstance(find,(str,int)): - if isinstance(c,str) and str(find) in c: - check = True - else: - if isinstance(c,str) and find.search(c): - check = True + if (isinstance(c, str) and ((is_regex and find.search(c)) or + (str(find) in c))): + check = True # if found, return the component if check: matches.append(self) - if first_only: - return matches - # loop the copy - for c in self.components: - if isinstance(c, XmlComponent): - kargs['first_only'] = first_only - child_matches = c.elements( *args, **kargs ) - if first_only and len(child_matches) != 0: - return child_matches - matches.extend( child_matches ) - return matches + first_only = kargs.get('first_only', False) + replace = kargs.get('replace', False) + find_text = replace is not False and kargs.get('find_text', False) + is_regex = not isinstance(find_text, (str, int, bool)) + find_components = not (check and first_only) + def replace_component(i): + if replace is None: + del self[i] + elif callable(replace): + self[i] = replace(self[i]) + else: + self[i] = replace + # loop the components + if find_text or find_components: + for i, c in enumerate(self.components): + if check and find_text and isinstance(c, str) and \ + ((is_regex and find_text.search(c)) or (str(find_text) in c)): + replace_component(i) + if find_components and isinstance(c, XmlComponent): + child_matches = c.elements(*args, **kargs) + if len(child_matches): + if not find_text and replace is not False and child_matches[0] is c: + replace_component(i) + if first_only: + return child_matches + matches.extend(child_matches) + return matches def element(self, *args, **kargs): """ @@ -2437,3 +2503,4 @@ if __name__ == '__main__': + diff --git a/gluon/http.py b/gluon/http.py index bf8f55ef..c4389b9e 100644 --- a/gluon/http.py +++ b/gluon/http.py @@ -142,3 +142,4 @@ def redirect(location, how=303, client_side=False): + diff --git a/gluon/import_all.py b/gluon/import_all.py index 01c4e8c6..46f231b8 100644 --- a/gluon/import_all.py +++ b/gluon/import_all.py @@ -113,3 +113,4 @@ for module in base_modules + contributed_modules: + diff --git a/gluon/languages.py b/gluon/languages.py index 24f41a49..2c8231d5 100644 --- a/gluon/languages.py +++ b/gluon/languages.py @@ -939,3 +939,4 @@ if __name__ == '__main__': doctest.testmod() + diff --git a/gluon/main.py b/gluon/main.py index 5aa0f15a..29a17992 100644 --- a/gluon/main.py +++ b/gluon/main.py @@ -854,3 +854,4 @@ class HttpServer(object): + diff --git a/gluon/messageboxhandler.py b/gluon/messageboxhandler.py index 17184a57..d22ce719 100644 --- a/gluon/messageboxhandler.py +++ b/gluon/messageboxhandler.py @@ -14,3 +14,4 @@ class MessageBoxHandler(logging.Handler): msg = self.format(record) tkMessageBox.showinfo('info1', msg) + diff --git a/gluon/myregex.py b/gluon/myregex.py index 04b48b73..9808a8cb 100644 --- a/gluon/myregex.py +++ b/gluon/myregex.py @@ -32,3 +32,4 @@ regex_extend = re.compile(\ + diff --git a/gluon/newcron.py b/gluon/newcron.py index ff917551..a238b8a8 100644 --- a/gluon/newcron.py +++ b/gluon/newcron.py @@ -343,3 +343,4 @@ def crondance(applications_parent, ctype='soft', startup=False, apps=None): + diff --git a/gluon/portalocker.py b/gluon/portalocker.py index 36c94547..f516e73e 100644 --- a/gluon/portalocker.py +++ b/gluon/portalocker.py @@ -151,3 +151,4 @@ if __name__=='__main__': + diff --git a/gluon/reserved_sql_keywords.py b/gluon/reserved_sql_keywords.py index 713ac48d..698768c8 100644 --- a/gluon/reserved_sql_keywords.py +++ b/gluon/reserved_sql_keywords.py @@ -1717,3 +1717,4 @@ ADAPTERS['all'] = reduce(lambda a,b:a.union(b),(x for x in ADAPTERS.values())) + diff --git a/gluon/restricted.py b/gluon/restricted.py index 770fe48e..74f3b34c 100644 --- a/gluon/restricted.py +++ b/gluon/restricted.py @@ -308,3 +308,4 @@ def snapshot(info=None, context=5, code=None, environment=None): + diff --git a/gluon/rewrite.py b/gluon/rewrite.py index 14bb1dc6..d7faf450 100644 --- a/gluon/rewrite.py +++ b/gluon/rewrite.py @@ -476,7 +476,7 @@ def load_routers(all_apps): def regex_uri(e, regexes, tag, default=None): "filter incoming URI against a list of regexes" path = e['PATH_INFO'] - host = e.get('http_host', e.get('SERVER_NAME','localhost')).lower() + host = e.get('http_host', e.get('SERVER_NAME','localhost')).lower() i = host.find(':') if i > 0: host = host[:i] @@ -1311,3 +1311,4 @@ def get_effective_router(appname): + diff --git a/gluon/rocket.py b/gluon/rocket.py index a091d8d2..12fd8dc6 100644 --- a/gluon/rocket.py +++ b/gluon/rocket.py @@ -2085,3 +2085,4 @@ if __name__=='__main__': + diff --git a/gluon/sanitizer.py b/gluon/sanitizer.py index 5e88f6e4..fa5ffc4e 100644 --- a/gluon/sanitizer.py +++ b/gluon/sanitizer.py @@ -228,3 +228,4 @@ def sanitize(text, permitted_tags=[ + diff --git a/gluon/scheduler.py b/gluon/scheduler.py index 1875fe7e..9e585d39 100644 --- a/gluon/scheduler.py +++ b/gluon/scheduler.py @@ -869,3 +869,4 @@ def main(): if __name__=='__main__': main() + diff --git a/gluon/serializers.py b/gluon/serializers.py index 8af80157..8934c9b2 100644 --- a/gluon/serializers.py +++ b/gluon/serializers.py @@ -117,3 +117,4 @@ def rss(feed): + diff --git a/gluon/settings.py b/gluon/settings.py index 19617882..3715edc6 100644 --- a/gluon/settings.py +++ b/gluon/settings.py @@ -34,3 +34,4 @@ global_settings.is_jython = 'java' in sys.platform.lower() or \ str(sys.copyright).find('Jython') > 0 + diff --git a/gluon/shell.py b/gluon/shell.py index 50f6d9f3..011ecb6d 100644 --- a/gluon/shell.py +++ b/gluon/shell.py @@ -431,3 +431,4 @@ if __name__ == '__main__': + diff --git a/gluon/sql.py b/gluon/sql.py index c9304adb..1a43f4cc 100644 --- a/gluon/sql.py +++ b/gluon/sql.py @@ -9,3 +9,4 @@ from dal import DAL, Field, Table, Query, Set, Expression, Row, Rows, drivers, B + diff --git a/gluon/sqlhtml.py b/gluon/sqlhtml.py index a8a709c3..b3df0aee 100644 --- a/gluon/sqlhtml.py +++ b/gluon/sqlhtml.py @@ -238,8 +238,10 @@ class ListWidget(StringWidget): if field.type=='list:integer': _class = 'integer' else: _class = 'string' requires = field.requires if isinstance(field.requires, (IS_NOT_EMPTY, IS_LIST_OF)) else None - items=[LI(INPUT(_id=_id, _class=_class, _name=_name, value=v, hideerror=True, requires=requires)) \ - for v in value or ['']] + attributes['_style'] = 'list-style:none' + items=[LI(INPUT(_id=_id, _class=_class, _name=_name, + value=v, hideerror=True, requires=requires), + **attributes) for v in value or ['']] script=SCRIPT(""" // from http://refactormycode.com/codes/694-expanding-input-list-using-jquery (function(){ @@ -270,7 +272,8 @@ function rel(ul) { })(); jQuery(document).ready(function(){jQuery('#%s_grow_input').grow_input();}); """ % _id) - attributes['_id']=_id+'_grow_input' + attributes['_id'] = _id+'_grow_input' + attributes['_style'] = 'list-style:none' return TAG[''](UL(*items,**attributes),script) @@ -2067,7 +2070,7 @@ class SQLFORM(FORM): selectable(records) redirect(referrer) else: - htmltable = DIV(T('No records found')) + htmltable = DIV(current.T('No records found')) if csv and nrows: export_links =[] @@ -2666,3 +2669,4 @@ class ExporterXML(ExportClass): + diff --git a/gluon/storage.py b/gluon/storage.py index b1d36a8c..50c2b9f8 100644 --- a/gluon/storage.py +++ b/gluon/storage.py @@ -35,19 +35,23 @@ class Storage(dict): 2 >>> del o.a - >>> print o.a + >>> print o.a None """ - def __getattr__(self, key): - return dict.get(self, key, None) - def __setattr__(self, key, value): - self[key] = value - def __delattr__(self, key): - del self[key] - def __getitem__(self, key): - return dict.get(self, key, None) + __setattr__ = dict.__setitem__ + __delattr__ = dict.__delitem__ + __getitem__ = dict.get + __getattr__ = dict.get + # def __getattr__(self, key): + # return dict.get(self, key, None) + # def __setattr__(self, key, value): + # self[key] = value + # def __getitem__(self, key): + # return dict.get(self, key, None) + # def __delattr__(self, key): + # del self[key] def __repr__(self): - return '' + dict.__repr__(self) + return '' % dict.__repr__(self) def __getstate__(self): return dict(self) def __setstate__(self,values): @@ -100,7 +104,7 @@ class Storage(dict): def getlast(self,key,default=None): """ - Returns the last or only single value when + Returns the last or only single value when given a request.vars-style key. If the value is a list, the last item will be returned; @@ -155,9 +159,9 @@ def save_storage(storage, filename): class Settings(Storage): def __setattr__(self, key, value): - if key != 'lock_keys' and 'lock_keys' in self and not key in self: + if key != 'lock_keys' and self['lock_keys'] and key not in self: raise SyntaxError, 'setting key \'%s\' does not exist' % key - if key != 'lock_values' and 'lock_values' in self: + if key != 'lock_values' and self['lock_values']: raise SyntaxError, 'setting value cannot be changed: %s' % key self[key] = value @@ -170,6 +174,64 @@ class Messages(Settings): return str(self.T(value)) return value +class FastStorage(dict): + """ + Eventually this should replace class Storage but causes memory leak + because of http://bugs.python.org/issue1469629 + + >>> s = FastStorage() + >>> s.a = 1 + >>> s.a + 1 + >>> s['a'] + 1 + >>> s.b + >>> s['b'] + >>> s['b']=2 + >>> s['b'] + 2 + >>> s.b + 2 + >>> isinstance(s,dict) + True + >>> dict(s) + {'a': 1, 'b': 2} + >>> dict(FastStorage(s)) + {'a': 1, 'b': 2} + >>> import pickle + >>> s = pickle.loads(pickle.dumps(s)) + >>> dict(s) + {'a': 1, 'b': 2} + >>> del s.b + >>> del s.a + >>> s.a + >>> s.b + >>> s['a'] + >>> s['b'] + """ + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.__dict__ = self + def __getattr__(self,key): + return getattr(self,key) if key in self else None + def __getitem__(self,key): + return dict.get(self,key,None) + def copy(self): + self.__dict__ = {} + s = FastStorage(self) + self.__dict__ = self + return s + def __repr__(self): + return '' % dict.__repr__(self) + def __getstate__(self): + return dict(self) + def __setstate__(self, sdict): + dict.__init__(self, sdict) + self.__dict__=self + def update(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + self.__dict__=self + class List(list): """ Like a regular python list but a[i] if i is out of bounds return None @@ -211,3 +273,4 @@ if __name__ == '__main__': + diff --git a/gluon/streamer.py b/gluon/streamer.py index 2a25aa52..a46ae7cf 100644 --- a/gluon/streamer.py +++ b/gluon/streamer.py @@ -112,3 +112,4 @@ def stream_file_or_304_or_206( + diff --git a/gluon/template.py b/gluon/template.py index 0127f1e2..9f147895 100644 --- a/gluon/template.py +++ b/gluon/template.py @@ -841,6 +841,32 @@ def get_parsed(text): """ return str(TemplateParser(text)) +class DummyResponse(): + def __init__(self): + self.body = cStringIO.StringIO() + def write(self, data, escape=True): + if not escape: + self.body.write(str(data)) + elif hasattr(data,'xml') and callable(data.xml): + self.body.write(data.xml()) + else: + # make it a string + if not isinstance(data, (str, unicode)): + data = str(data) + elif isinstance(data, unicode): + data = data.encode('utf8', 'xmlcharrefreplace') + data = cgi.escape(data, True).replace("'","'") + self.body.write(data) + +class NOESCAPE(): + """ + A little helper to avoid escaping. + """ + def __init__(self, text): + self.text = text + def xml(self): + return self.text + # And this is a generic render function. # Here for integration with gluon. def render(content = "hello world", @@ -877,42 +903,31 @@ def render(content = "hello world", >>> render(content='{{for i in range(3):\\n=i\\npass}}') '012' """ - # Here to avoid circular Imports + # here to avoid circular Imports try: from globals import Response - except: + except ImportError: # Working standalone. Build a mock Response object. - class Response(): - def __init__(self): - self.body = cStringIO.StringIO() - def write(self, data, escape=True): - if not escape: - self.body.write(str(data)) - elif hasattr(data,'xml') and callable(data.xml): - self.body.write(data.xml()) - else: - # make it a string - if not isinstance(data, (str, unicode)): - data = str(data) - elif isinstance(data, unicode): - data = data.encode('utf8', 'xmlcharrefreplace') - data = cgi.escape(data, True).replace("'","'") - self.body.write(data) + Response = DummyResponse - # A little helper to avoid escaping. - class NOESCAPE(): - def __init__(self, text): - self.text = text - def xml(self): - return self.text # Add it to the context so we can use it. - context['NOESCAPE'] = NOESCAPE + if not 'NOESCAPE' in context: + context['NOESCAPE'] = XML + + # save current response class + if context and 'response' in context: + old_response_body = context['response'].body + context['response'].body = cStringIO.StringIO() + else: + old_response_body = None + context['response'] = Response() # If we don't have anything to render, why bother? if not content and not stream and not filename: raise SyntaxError, "Must specify a stream or filename or content" - # Here for legacy purposes, probably can be reduced to something more simple. + # Here for legacy purposes, probably can be reduced to + # something more simple. close_stream = False if not stream: if filename: @@ -921,9 +936,6 @@ def render(content = "hello world", elif content: stream = cStringIO.StringIO(content) - # Get a response class. - context['response'] = Response() - # Execute the template. code = str(TemplateParser(stream.read(), context=context, path=path, lexers=lexers, delimiters=delimiters)) try: @@ -936,7 +948,10 @@ def render(content = "hello world", stream.close() # Returned the rendered content. - return context['response'].body.getvalue() + text = context['response'].body.getvalue() + if old_response_body is not None: + context['response'].body = old_response_body + return text if __name__ == '__main__': @@ -948,3 +963,4 @@ if __name__ == '__main__': + diff --git a/gluon/tests/test_contribs.py b/gluon/tests/test_contribs.py new file mode 100644 index 00000000..6af457ad --- /dev/null +++ b/gluon/tests/test_contribs.py @@ -0,0 +1,41 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" Unit tests for contribs """ + +import sys +import os +import unittest +if os.path.isdir('gluon'): + sys.path.append(os.path.realpath('gluon')) +else: + sys.path.append(os.path.realpath('../')) + +from utils import md5_hash +import contrib.fpdf as fpdf +import contrib.pyfpdf as pyfpdf + + +class TestContribs(unittest.TestCase): + """ Tests the contrib package """ + + def test_fpdf(self): + """ Basic PDF test and sanity checks """ + + self.assertEqual(fpdf.FPDF_VERSION, pyfpdf.FPDF_VERSION, 'version mistmatch') + self.assertEqual(fpdf.FPDF, pyfpdf.FPDF, 'class mistmatch') + + pdf = fpdf.FPDF() + pdf.add_page() + pdf.compress = False + pdf.set_font('Arial', '',14) + pdf.ln(10) + pdf.write(5, 'hello world') + pdf_out = pdf.output('', 'S') + + self.assertTrue(fpdf.FPDF_VERSION in pdf_out, 'version string') + self.assertTrue('hello world' in pdf_out, 'sample message') + + +if __name__ == '__main__': + unittest.main() diff --git a/gluon/tools.py b/gluon/tools.py index e67d5e4c..ce086c9d 100644 --- a/gluon/tools.py +++ b/gluon/tools.py @@ -997,7 +997,7 @@ class Auth(object): settings.table_event_name = 'auth_event' settings.table_cas_name = 'auth_cas' - # ## if none, they will be created + # ## if none, they will be created, unless DAL(lazy_tables=True)!!! settings.table_user = None settings.table_group = None @@ -1084,8 +1084,8 @@ class Auth(object): messages.is_empty = "Cannot be empty" messages.mismatched_password = "Password fields don't match" messages.verify_email = \ - 'Click on the link http://' + current.request.env.http_host + \ - URL('default','user',args=['verify_email']) + \ + 'Click on the link ' + \ + URL('default','user',args='verify_email',scheme=True) + \ '/%(key)s to verify your email' messages.verify_email_subject = 'Email verification' messages.username_sent = 'Your username was emailed to you' @@ -1096,8 +1096,8 @@ class Auth(object): messages.retrieve_password = 'Your password is: %(password)s' messages.retrieve_password_subject = 'Password retrieve' messages.reset_password = \ - 'Click on the link http://' + current.request.env.http_host + \ - URL('default','user',args=['reset_password']) + \ + 'Click on the link ' + \ + URL('default','user',args='reset_password',scheme=True) + \ '/%(key)s to reset your password' messages.reset_password_subject = 'Password reset' messages.invalid_reset_password = 'Invalid reset password' @@ -1167,6 +1167,19 @@ class Auth(object): user_id = property(_get_user_id, doc="user.id or None") + def table_user(self): + return self.db[self.settings.table_user_name] + def table_group(self): + return self.db[self.settings.table_group_name] + def table_membership(self): + return self.db[self.settings.table_membership_name] + def table_permission(self): + return self.db[self.settings.table_permission_name] + def table_event(self): + return self.db[self.settings.table_event_name] + def table_cas(self): + return self.db[self.settings.table_cas_name] + def _HTTP(self, *a, **b): """ only used in lambda: self._HTTP(404) @@ -1261,7 +1274,7 @@ class Auth(object): if not 'register' in self.settings.actions_disabled: bar.insert(-1, s2) bar.insert(-1, register) - if 'username' in self.settings.table_user.fields() and \ + if self.use_username and \ not 'retrieve_username' in self.settings.actions_disabled: bar.insert(-1, s2) bar.insert(-1, retrieve_username) @@ -1373,6 +1386,7 @@ class Auth(object): db = self.db settings = self.settings + self.use_username = username if not self.signature: self.define_signature() if signature==True: @@ -1384,23 +1398,41 @@ class Auth(object): else: signature_list = signature lazy_tables, db._lazy_tables = db._lazy_tables, False + is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty) + is_crypted = CRYPT(key=settings.hmac_key, + min_length=settings.password_min_length) + is_unique_email = [ + IS_EMAIL(error_message=self.messages.invalid_email), + IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name)] + if not settings.email_case_sensitive: + is_unique_email.insert(1,IS_LOWER()) if not settings.table_user_name in db.tables: passfield = settings.password_field extra_fields = settings.extra_fields.get( settings.table_user_name,[])+signature_list - if username or settings.cas_provider: + if username or settings.cas_provider: + is_unique_username = \ + [IS_MATCH('[\w\.\-]+'), + IS_NOT_IN_DB(db,'%s.username' % settings.table_user_name)] + if not settings.username_case_sensitive: + is_unique_username.insert(1,IS_LOWER()) table = db.define_table( settings.table_user_name, Field('first_name', length=128, default='', - label=self.messages.label_first_name), + label=self.messages.label_first_name, + requires = is_not_empty), Field('last_name', length=128, default='', - label=self.messages.label_last_name), + label=self.messages.label_last_name, + requires = is_not_empty), Field('email', length=512, default='', - label=self.messages.label_email), + label=self.messages.label_email, + requires = is_unique_email), Field('username', length=128, default='', - label=self.messages.label_username), + label=self.messages.label_username, + requires=is_unique_username), Field(passfield, 'password', length=512, - readable=False, label=self.messages.label_password), + readable=False, label=self.messages.label_password, + requires = [is_crypted]), Field('registration_key', length=512, writable=False, readable=False, default='', label=self.messages.label_registration_key), @@ -1416,22 +1448,21 @@ class Auth(object): migrate), fake_migrate=fake_migrate, format='%(username)s')) - table.username.requires = \ - [IS_MATCH('[\w\.\-]+'), - IS_NOT_IN_DB(db, table.username)] - if not settings.username_case_sensitive: - table.username.requires.insert(1,IS_LOWER()) else: table = db.define_table( settings.table_user_name, Field('first_name', length=128, default='', - label=self.messages.label_first_name), + label=self.messages.label_first_name, + requires = is_not_empty), Field('last_name', length=128, default='', - label=self.messages.label_last_name), + label=self.messages.label_last_name, + requires = is_not_empty), Field('email', length=512, default='', - label=self.messages.label_email), + label=self.messages.label_email, + requires = is_unique_email), Field(passfield, 'password', length=512, - readable=False, label=self.messages.label_password), + readable=False, label=self.messages.label_password, + requires = [is_crypted]), Field('registration_key', length=512, writable=False, readable=False, default='', label=self.messages.label_registration_key), @@ -1447,27 +1478,16 @@ class Auth(object): migrate), fake_migrate=fake_migrate, format='%(first_name)s %(last_name)s (%(id)s)')) - table.first_name.requires = \ - IS_NOT_EMPTY(error_message=self.messages.is_empty) - table.last_name.requires = \ - IS_NOT_EMPTY(error_message=self.messages.is_empty) - table[passfield].requires = [ - CRYPT(key=settings.hmac_key, - min_length=settings.password_min_length)] - table.email.requires = \ - [IS_EMAIL(error_message=self.messages.invalid_email), - IS_NOT_IN_DB(db, table.email)] - if not settings.email_case_sensitive: - table.email.requires.insert(1,IS_LOWER()) - table.registration_key.default = '' - settings.table_user = db[settings.table_user_name] + reference_table_user = 'reference %s' % settings.table_user_name if not settings.table_group_name in db.tables: extra_fields = settings.extra_fields.get( settings.table_group_name,[])+signature_list table = db.define_table( settings.table_group_name, Field('role', length=512, default='', - label=self.messages.label_role), + label=self.messages.label_role, + requires = IS_NOT_IN_DB( + db, '%s.role'% settings.table_group_name)), Field('description', 'text', label=self.messages.label_description), *extra_fields, @@ -1476,55 +1496,41 @@ class Auth(object): settings.table_group_name, migrate), fake_migrate=fake_migrate, format = '%(role)s (%(id)s)')) - table.role.requires = IS_NOT_IN_DB(db, '%s.role' - % settings.table_group_name) - settings.table_group = db[settings.table_group_name] + reference_table_group = 'reference %s' % settings.table_group_name if not settings.table_membership_name in db.tables: extra_fields = settings.extra_fields.get( settings.table_membership_name,[])+signature_list table = db.define_table( settings.table_membership_name, - Field('user_id', settings.table_user, + Field('user_id', reference_table_user, label=self.messages.label_user_id), - Field('group_id', settings.table_group, + Field('group_id', reference_table_group, label=self.messages.label_group_id), *extra_fields, **dict( migrate=self.__get_migrate( settings.table_membership_name, migrate), fake_migrate=fake_migrate)) - table.user_id.requires = IS_IN_DB(db, '%s.id' % - settings.table_user_name, - settings.table_user._format) - table.group_id.requires = IS_IN_DB(db, '%s.id' % - settings.table_group_name, - '%(role)s (%(id)s)') - settings.table_membership = db[settings.table_membership_name] if not settings.table_permission_name in db.tables: extra_fields = settings.extra_fields.get( settings.table_permission_name,[])+signature_list table = db.define_table( settings.table_permission_name, - Field('group_id', settings.table_group, - label=self.messages.label_group_id), + Field('group_id', reference_table_group, + label=self.messages.label_group_id), Field('name', default='default', length=512, - label=self.messages.label_name), + label=self.messages.label_name, + requires=is_not_empty), Field('table_name', length=512, - label=self.messages.label_table_name), + label=self.messages.label_table_name), Field('record_id', 'integer',default=0, - label=self.messages.label_record_id), + label=self.messages.label_record_id, + requires = IS_INT_IN_RANGE(0, 10 ** 9)), *extra_fields, **dict( migrate=self.__get_migrate( settings.table_permission_name, migrate), fake_migrate=fake_migrate)) - table.group_id.requires = IS_IN_DB(db, '%s.id' % - settings.table_group_name, - '%(role)s (%(id)s)') - table.name.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) - #table.table_name.requires = IS_EMPTY_OR(IS_IN_SET(self.db.tables)) - table.record_id.requires = IS_INT_IN_RANGE(0, 10 ** 9) - settings.table_permission = db[settings.table_permission_name] if not settings.table_event_name in db.tables: table = db.define_table( settings.table_event_name, @@ -1534,29 +1540,25 @@ class Auth(object): Field('client_ip', default=current.request.client, label=self.messages.label_client_ip), - Field('user_id', settings.table_user, default=None, - label=self.messages.label_user_id), + Field('user_id', reference_table_user, default=None, + label=self.messages.label_user_id), Field('origin', default='auth', length=512, - label=self.messages.label_origin), + label=self.messages.label_origin, + requires=is_not_empty), Field('description', 'text', default='', - label=self.messages.label_description), + label=self.messages.label_description, + requires=is_not_empty), *settings.extra_fields.get(settings.table_event_name,[]), **dict( migrate=self.__get_migrate( settings.table_event_name, migrate), fake_migrate=fake_migrate)) - table.user_id.requires = IS_IN_DB(db, '%s.id' % - settings.table_user_name, - settings.table_user._format) - table.origin.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) - table.description.requires = IS_NOT_EMPTY(error_message=self.messages.is_empty) - settings.table_event = db[settings.table_event_name] now = current.request.now if settings.cas_domains: if not settings.table_cas_name in db.tables: table = db.define_table( settings.table_cas_name, - Field('user_id', settings.table_user, default=None, + Field('user_id', reference_table_user, default=None, label=self.messages.label_user_id), Field('created_on','datetime',default=now), Field('service',requires=IS_URL()), @@ -1567,20 +1569,26 @@ class Auth(object): migrate=self.__get_migrate( settings.table_cas_name, migrate), fake_migrate=fake_migrate)) - table.user_id.requires = IS_IN_DB(db, '%s.id' % \ - settings.table_user_name, - settings.table_user._format) - settings.table_cas = db[settings.table_cas_name] - db._lazy_tables = lazy_tables - if settings.cas_provider: + if not db._lazy_tables: + settings.table_user = db[settings.table_user_name] + settings.table_group = db[settings.table_group_name] + settings.table_membership = db[settings.table_membership_name] + settings.table_permission = db[settings.table_permission_name] + settings.table_event = db[settings.table_event_name] + if settings.cas_domains: + settings.table_cas = db[settings.table_cas_name] + + if settings.cas_provider: ### THIS IS NOT LAZY settings.actions_disabled = \ - ['profile','register','change_password','request_reset_password'] + ['profile','register','change_password', + 'request_reset_password'] from gluon.contrib.login_methods.cas_auth import CasAuth maps = settings.cas_maps if not maps: + table_user = self.table_user() maps = dict((name,lambda v,n=name:v.get(n,None)) for name in \ - settings.table_user.fields if name!='id' \ - and settings.table_user[name].readable) + table_user.fields if name!='id' \ + and table_user[name].readable) maps['registration_id'] = \ lambda v,p=settings.cas_provider:'%s/%s' % (p,v['user']) actions = [settings.cas_actions['login'], @@ -1606,8 +1614,9 @@ class Auth(object): else: user_id = None # user unknown vars = vars or {} - self.settings.table_event.insert(description=description % vars, - origin=origin, user_id=user_id) + self.table_event().insert( + description=description % vars, + origin=origin, user_id=user_id) def get_or_create_user(self, keys, update_fields=['email']): """ @@ -1615,7 +1624,7 @@ class Auth(object): If the user exists already then password is updated. If the user doesn't yet exist, then they are created. """ - table_user = self.settings.table_user + table_user = self.table_user() user = None checks = [] # make a guess about who this user is @@ -1673,7 +1682,7 @@ class Auth(object): request = current.request session = current.session - table_user = self.settings.table_user + table_user = self.table_user() if self.settings.login_userfield: userfield = self.settings.login_userfield elif 'username' in table_user.fields: @@ -1711,7 +1720,7 @@ class Auth(object): request = current.request response = current.response session = current.session - db, table = self.db, self.settings.table_cas + db, table = self.db, self.table_cas() session._cas_service = request.vars.service or session._cas_service if not request.env.http_host in self.settings.cas_domains or \ not session._cas_service: @@ -1747,7 +1756,7 @@ class Auth(object): def cas_validate(self, version=2, proxy=False): request = current.request - db, table = self.db, self.settings.table_cas + db, table = self.db, self.table_cas() current.response.headers['Content-Type']='text' ticket = request.vars.ticket renew = True if request.vars.has_key('renew') else False @@ -1763,7 +1772,7 @@ class Auth(object): # If ticket is a service Ticket and RENEW flag respected if ticket[0:3] == 'ST-' and \ not ((row.renew and renew) ^ renew): - user = self.settings.table_user(row.user_id) + user = self.table_user()(row.user_id) row.delete_record() success = True def build_response(body): @@ -1779,7 +1788,7 @@ class Auth(object): TAG['cas:authenticationSuccess']( TAG['cas:user'](username), *[TAG['cas:'+field.name](user[field.name]) \ - for field in self.settings.table_user \ + for field in self.table_user() \ if field.readable])) else: if version == 1: @@ -1808,7 +1817,7 @@ class Auth(object): """ - table_user = self.settings.table_user + table_user = self.table_user() if self.settings.login_userfield: username = self.settings.login_userfield elif 'username' in table_user.fields: @@ -2043,7 +2052,7 @@ class Auth(object): """ - table_user = self.settings.table_user + table_user = self.table_user() request = current.request response = current.response session = current.session @@ -2058,7 +2067,7 @@ class Auth(object): if log is DEFAULT: log = self.messages.register_log - table_user = self.settings.table_user + table_user = self.table_user() if 'username' in table_user.fields: username = 'username' else: @@ -2182,7 +2191,7 @@ class Auth(object): """ key = getarg(-1) - table_user = self.settings.table_user + table_user = self.table_user() user = self.db(table_user.registration_key == key).select().first() if not user: redirect(self.settings.login_url) @@ -2221,7 +2230,7 @@ class Auth(object): """ - table_user = self.settings.table_user + table_user = self.table_user() if not 'username' in table_user.fields: raise HTTP(404) request = current.request @@ -2306,7 +2315,7 @@ class Auth(object): """ - table_user = self.settings.table_user + table_user = self.table_user() request = current.request response = current.response session = current.session @@ -2383,7 +2392,7 @@ class Auth(object): """ - table_user = self.settings.table_user + table_user = self.table_user() request = current.request # response = current.response session = current.session @@ -2403,7 +2412,7 @@ class Auth(object): form = SQLFORM.factory( Field('new_password', 'password', label=self.messages.new_password, - requires=self.settings.table_user[passfield].requires), + requires=self.table_user()[passfield].requires), Field('new_password2', 'password', label=self.messages.verify_password, requires=[IS_EXPR('value==%s' % repr(request.vars.new_password), @@ -2435,7 +2444,7 @@ class Auth(object): [, onvalidation=DEFAULT [, onaccept=DEFAULT [, log=DEFAULT]]]]) """ - table_user = self.settings.table_user + table_user = self.table_user() request = current.request response = current.response session = current.session @@ -2467,7 +2476,7 @@ class Auth(object): separator=self.settings.label_separator ) if captcha: - addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') + addrow(form, captcha.label, captcha, captcha.comment, self.settings.formstyle,'captcha__row') if form.accepts(request, session, formname='reset_password', dbio=False, onvalidation=onvalidation, @@ -2533,7 +2542,7 @@ class Auth(object): if not self.is_logged_in(): redirect(self.settings.login_url) db = self.db - table_user = self.settings.table_user + table_user = self.table_user() usern = self.settings.table_user_name s = db(table_user.id == self.user.id) @@ -2599,11 +2608,11 @@ class Auth(object): """ - table_user = self.settings.table_user + table_user = self.table_user() if not self.is_logged_in(): redirect(self.settings.login_url) passfield = self.settings.password_field - self.settings.table_user[passfield].writable = False + table_user[passfield].writable = False request = current.request session = current.session if next is DEFAULT: @@ -2654,6 +2663,7 @@ class Auth(object): request = current.request session = current.session auth = session.auth + table_user = self.table_user() if not self.is_logged_in(): raise HTTP(401, "Not Authorized") current_id = auth.user.id @@ -2665,12 +2675,12 @@ class Auth(object): self.settings.table_user_name, user_id): raise HTTP(403, "Forbidden") - user = self.settings.table_user(user_id) + user = table_user(user_id) if not user: raise HTTP(401, "Not Authorized") auth.impersonator = cPickle.dumps(session) auth.user.update( - self.settings.table_user._filter_fields(user, True)) + table_user._filter_fields(user, True)) self.user = auth.user if self.settings.login_onaccept: form = Storage(dict(vars=self.user)) @@ -2691,10 +2701,11 @@ class Auth(object): user_groups = self.user_groups = {} if current.session.auth: current.session.auth.user_groups = self.user_groups - memberships = self.db(self.settings.table_membership.user_id - == self.user.id).select() + table_group = self.table_group() + table_membership = self.table_membership() + memberships = self.db(table_membership.user_id==self.user.id).select() for membership in memberships: - group = self.settings.table_group(membership.group_id) + group = table_group(membership.group_id) if group: user_groups[membership.group_id] = group.role @@ -2705,12 +2716,12 @@ class Auth(object): if not self.is_logged_in(): redirect(self.settings.login_url) - memberships = self.db(self.settings.table_membership.user_id - == self.user.id).select() + table_membership = self.table_membership() + memberships = self.db(table_membership.user_id==self.user.id).select() table = TABLE() for membership in memberships: - groups = self.db(self.settings.table_group.id - == membership.group_id).select() + table_group = self.db[self.settings.table_group_name] + groups = self.db(table_group.id==membership.group_id).select() if groups: group = groups[0] table.append(TR(H3(group.role, '(%s)' % group.id))) @@ -2814,7 +2825,7 @@ class Auth(object): creates a group associated to a role """ - group_id = self.settings.table_group.insert( + group_id = self.table_group().insert( role=role, description=description) self.log_event(self.messages.add_group_log, dict(group_id=group_id, role=role)) @@ -2824,10 +2835,9 @@ class Auth(object): """ deletes a group """ - - self.db(self.settings.table_group.id == group_id).delete() - self.db(self.settings.table_membership.group_id == group_id).delete() - self.db(self.settings.table_permission.group_id == group_id).delete() + self.db(self.table_group().id == group_id).delete() + self.db(self.table_membership().group_id == group_id).delete() + self.db(self.table_permission().group_id == group_id).delete() self.update_groups() self.log_event(self.messages.del_group_log,dict(group_id=group_id)) @@ -2835,7 +2845,7 @@ class Auth(object): """ returns the group_id of the group specified by the role """ - rows = self.db(self.settings.table_group.role == role).select() + rows = self.db(self.table_group().role == role).select() if not rows: return None return rows[0].id @@ -2849,7 +2859,7 @@ class Auth(object): def user_group_role(self, user_id=None): if user_id: - user = self.settings.table_user[user_id] + user = self.table_user()[user_id] else: user = self.user return self.settings.create_user_groups % user @@ -2867,7 +2877,7 @@ class Auth(object): group_id = self.id_group(group_id) # interpret group_id as a role if not user_id and self.user: user_id = self.user.id - membership = self.settings.table_membership + membership = self.table_membership() if self.db((membership.user_id == user_id) & (membership.group_id == group_id)).select(): r = True @@ -2890,7 +2900,7 @@ class Auth(object): group_id = self.id_group(group_id) # interpret group_id as a role if not user_id and self.user: user_id = self.user.id - membership = self.settings.table_membership + membership = self.table_membership() record = membership(user_id = user_id,group_id = group_id) if record: return record.id @@ -2910,7 +2920,7 @@ class Auth(object): group_id = group_id or self.id_group(role) if not user_id and self.user: user_id = self.user.id - membership = self.settings.table_membership + membership = self.table_membership() self.log_event(self.messages.del_membership_log, dict(user_id=user_id,group_id=group_id)) ret = self.db(membership.user_id @@ -2941,7 +2951,7 @@ class Auth(object): if not user_id and not group_id and self.user: user_id = self.user.id if user_id: - membership = self.settings.table_membership + membership = self.table_membership() rows = self.db(membership.user_id == user_id).select(membership.group_id) groups = set([row.group_id for row in rows]) @@ -2949,7 +2959,7 @@ class Auth(object): return False else: groups = set([group_id]) - permission = self.settings.table_permission + permission = self.table_permission() rows = self.db(permission.name == name)(permission.table_name == str(table_name))(permission.record_id == record_id).select(permission.group_id) @@ -2982,7 +2992,7 @@ class Auth(object): gives group_id 'name' access to 'table_name' and 'record_id' """ - permission = self.settings.table_permission + permission = self.table_permission() if group_id == 0: group_id = self.user_group() record = self.db(permission.group_id==group_id)(permission.name==name)\ @@ -3011,7 +3021,7 @@ class Auth(object): revokes group_id 'name' access to 'table_name' and 'record_id' """ - permission = self.settings.table_permission + permission = self.table_permission() self.log_event(self.messages.del_permission_log, dict(group_id=group_id, name=name, table_name=table_name, record_id=record_id)) @@ -3039,8 +3049,8 @@ class Auth(object): self.has_permission(name, table, 0, user_id): return table.id > 0 db = self.db - membership = self.settings.table_membership - permission = self.settings.table_permission + membership = self.table_membership() + permission = self.table_permission() query = table.id.belongs( db(membership.user_id == user_id)\ (membership.group_id == permission.group_id)\ @@ -3123,7 +3133,7 @@ class Auth(object): archive_table_name, Field(current_record,table), *[field.clone(unique=False) for field in table]) - archive_table = table._db[archive_table_name] + archive_table = table._db[archive_table_name] new_record = {current_record:form.vars.id} for fieldname in archive_table.fields: if not fieldname in ['id',current_record]: @@ -4456,8 +4466,12 @@ class Wiki(object): rows_page = 25 regex_redirect = re.compile('redirect\s+(\w+\://\S+)\s*') def markmin_render(self,page): - return MARKMIN(page.body,url=True,environment=self.env, + html = MARKMIN(page.body,url=True,environment=self.env, autolinks=lambda link: expand_one(link,{})).xml() + html += DIV(_class='w2p_wiki_tags', + *[A(t.strip(),_href=URL(args='_search',vars=dict(q=t))) + for t in page.tags or [] if t.strip()]).xml() + return html @staticmethod def component(text): """ @@ -4470,7 +4484,7 @@ class Wiki(object): def __init__(self,auth,env=None,render='markmin', manage_permissions=False,force_prefix=''): self.env = env or {} - self.env['component'] = Wiki.component + self.env['component'] = Wiki.component if render == 'markmin': render=self.markmin_render self.auth = auth if self.auth.user: @@ -4522,7 +4536,7 @@ class Wiki(object): not 'wiki_editor' in auth.user_groups.values(): group = db.auth_group(role='wiki_editor') gid = group.id if group else db.auth_group.insert(role='wiki_editor') - auth.add_membership(gid) + auth.add_membership(gid) # WIKI ACCESS POLICY def not_authorized(self,page=None): raise HTTP(401) @@ -4580,7 +4594,6 @@ class Wiki(object): elif zero=='_cloud': return self.cloud() - def first_paragraph(self,page): if not self.can_read(page): mm = page.body.replace('\r','') @@ -4591,10 +4604,10 @@ class Wiki(object): def fix_hostname(self,body): return body.replace('://HOSTNAME','://%s' % self.host) - + def read(self,slug): - if slug in '_cloud': - return self.cloud() + if slug in '_cloud': return self.cloud() + elif slug in '_search': return self.search() page = self.auth.db.wiki_page(slug=slug) if not page: redirect(URL(args=('_create',slug))) @@ -4649,7 +4662,8 @@ class Wiki(object): vars = current.request.post_vars if vars.body: vars.body=vars.body.replace('://%s' % self.host,'://HOSTNAME') - form = SQLFORM(db.wiki_page,page,deletable=True,showid=False).process() + form = SQLFORM(db.wiki_page,page,deletable=True, + formstyle='table2cols',showid=False).process() if form.deleted: current.session.flash = 'page deleted' redirect(URL()) @@ -4660,7 +4674,7 @@ class Wiki(object): def editmedia(self,slug): auth = self.auth - db = auth.db + db = auth.db page = db.wiki_page(slug=slug) if not (page and self.can_edit(page)): return self.not_authorized(page) self.auth.db.wiki_media.id.represent = lambda id,row:\ @@ -4678,10 +4692,11 @@ class Wiki(object): def create(self): if not self.can_edit(): return self.not_authorized() db = self.auth.db - form = SQLFORM.factory( - Field('slug',default=current.request.args(1), - label = 'New Page', - requires=(IS_SLUG(),IS_NOT_IN_DB(db,db.wiki_page.slug)))) + form = FORM(INPUT(_name='slug',value=current.request.args(1), + requires=(IS_SLUG(), + IS_NOT_IN_DB(db,db.wiki_page.slug))), + INPUT(_type='submit', + _value=current.T('Create Page from Slug'))) if form.process().accepted: redirect(URL(args=('_edit',form.vars.slug))) return dict(content=form) @@ -4724,9 +4739,9 @@ class Wiki(object): for match in regex.finditer(self.fix_hostname(menu_page.body)): base = match.group('base').replace(' ','') title = match.group('title') - link = match.group('link') + link = match.group('link') if link.startswith('@'): - items = link[1:].split('/') + items = link[2:].split('/') if len(items)>3: link = URL(a=items[0] or None,c=items[1] or None,f=items[2] or None, args=items[3:]) parent = tree.get(base[1:],tree['']) @@ -4760,7 +4775,7 @@ class Wiki(object): submenu.append((current.T('Create New Page'),None, URL(controller,function,args=('_create')))) - # if self.can_manage(): + if self.can_manage(): submenu.append((current.T('Manage Pages'),None, URL(controller,function,args=('_pages')))) submenu.append((current.T('Edit Menu'),None, @@ -4775,12 +4790,13 @@ class Wiki(object): request = current.request content = CAT() if tags is None and query is None: - form = SQLFORM.factory(Field('tags',requires=IS_NOT_EMPTY(), - default=request.vars.tags, - label=current.T('Search'))) + form = FORM(INPUT(_name='q',requires=IS_NOT_EMPTY(), + value=request.vars.q), + INPUT(_type="submit",_value=current.T('Search')), + _method='GET') content.append(DIV(form,_class='w2p_wiki_form')) - if request.vars: - tags = [v.strip() for v in request.vars.tags.split(',')] + if request.vars.q: + tags = [v.strip() for v in request.vars.q.split(',')] tags = [v for v in tags if v] if tags or not query is None: db = self.auth.db @@ -4793,21 +4809,24 @@ class Wiki(object): if query is None: query = (db.wiki_page.id==db.wiki_tag.wiki_page)&\ (db.wiki_tag.name.belongs(tags)) + query = query|db.wiki_page.title.startswith(request.vars.q) pages = db(query).select( *fields,**dict(orderby=orderby or ~count, groupby=db.wiki_page.id, limitby=limitby)) if request.extension in ('html','load'): if not pages: - content.append(DIV(T("No results",_class='w2p_wiki_form'))) + content.append(DIV(current.T("No results"), + _class='w2p_wiki_form')) def link(t): - return A(t,_href=URL(args='_search',vars=dict(tags=t))) + return A(t,_href=URL(args='_search',vars=dict(q=t))) items = [DIV(H3(A(p.title,_href=URL(args=p.slug))), MARKMIN(self.first_paragraph(p)) \ if preview else '', - SPAN(*[link(t.strip()) for t in \ - p.tags or [] if t.strip()]), - _class='w2p_wiki_tags') + DIV(_class='w2p_wiki_tags', + *[link(t.strip()) for t in \ + p.tags or [] if t.strip()]), + _class='w2p_wiki_search_item') for p in pages] content.append(DIV(_class='w2p_wiki_pages',*items)) else: @@ -4829,11 +4848,11 @@ class Wiki(object): a,b = ids[0](count), ids[-1](count) def scale(c): return '%.2f' % (3.0*(c-b)/max(a-b,1)+1) - items = [A(item.wiki_tag.name,_class='w2p_cloud_tag', - _style='padding-right:0.2em;font-size:%sem' \ - % scale(item(count)), + items = [A(item.wiki_tag.name, + _style='padding:0.2em;font-size:%sem' \ + % scale(item(count)), _href=URL(args='_search', - vars=dict(tags=item.wiki_tag.name))) + vars=dict(q=item.wiki_tag.name))) for item in ids] return dict(content=DIV(_class='w2p_cloud',*items)) @@ -4844,3 +4863,4 @@ if __name__ == '__main__': + diff --git a/gluon/utf8.py b/gluon/utf8.py index 794489af..1af3fe15 100644 --- a/gluon/utf8.py +++ b/gluon/utf8.py @@ -654,3 +654,4 @@ if __name__ == '__main__': doctests() + diff --git a/gluon/utils.py b/gluon/utils.py index 963d89a0..92e58010 100644 --- a/gluon/utils.py +++ b/gluon/utils.py @@ -186,3 +186,4 @@ def is_valid_ip_address(address): + diff --git a/gluon/validators.py b/gluon/validators.py index 27fae504..eebf54ef 100644 --- a/gluon/validators.py +++ b/gluon/validators.py @@ -3153,3 +3153,4 @@ if __name__ == '__main__': + diff --git a/gluon/widget.py b/gluon/widget.py index a0bb9dfd..13114466 100644 --- a/gluon/widget.py +++ b/gluon/widget.py @@ -754,9 +754,11 @@ def console(): default='', help=msg) - msg = 'run scheduled tasks for the specified apps' - msg += '-K app1,app2,app3' - msg += 'requires a scheduler defined in the models' + msg = 'run scheduled tasks for the specified apps: expects a list of ' + msg += 'app names as -K app1,app2,app3 ' + msg += 'or a list of app:groups as -K app1:group1:group2,app2:group1 ' + msg += 'to override specific group_names. (only strings, no spaces ' + msg += 'allowed. Requires a scheduler defined in the models' parser.add_option('-K', '--scheduler', dest='scheduler', @@ -917,6 +919,17 @@ def console(): options.interfaces = [ tuple(interface) for interface in options.interfaces] + # accepts --scheduler in the form + # "app:group1,group2,app2:group1" + scheduler = [] + options.scheduler_groups = None + if isinstance(options.scheduler, str): + if ':' in options.scheduler: + for opt in options.scheduler.split(','): + scheduler.append(opt.split(':')) + options.scheduler = ','.join([app[0] for app in scheduler]) + options.scheduler_groups = scheduler + if options.numthreads is not None and options.minthreads is None: options.minthreads = options.numthreads # legacy @@ -940,20 +953,28 @@ def check_existent_app(options,appname): return True def start_schedulers(options): - apps = [app.strip() for app in options.scheduler.split(',')] try: from multiprocessing import Process except: sys.stderr.write('Sorry, -K only supported for python 2.6-2.7\n') return processes = [] - code = "from gluon import current;current._scheduler.loop()" + apps = [(app.strip(), None) for app in options.scheduler.split(',')] + if options.scheduler_groups: + apps = options.scheduler_groups for app in apps: - if not check_existent_app(options, app): - print "Application '%s' doesn't exist, skipping" % (app) + if len(app) == 1 or app[1] == None: + code = "from gluon import current;current._scheduler.loop()" + else: + code = "from gluon import current;current._scheduler.group_names = ['%s'];" + code += "current._scheduler.loop()" + code = code % ("','".join(app[1:])) + app_ = app[0] + if not check_existent_app(options, app_): + print "Application '%s' doesn't exist, skipping" % (app_) continue - print 'starting scheduler for "%s"...' % app - args = (app,True,True,None,False,code) + print 'starting scheduler for "%s"...' % app_ + args = (app_,True,True,None,False,code) logging.getLogger().setLevel(options.debuglevel) p = Process(target=run, args=args) processes.append(p) @@ -1184,3 +1205,4 @@ end tell + diff --git a/gluon/winservice.py b/gluon/winservice.py index aa762a15..48b58e91 100644 --- a/gluon/winservice.py +++ b/gluon/winservice.py @@ -172,3 +172,4 @@ if __name__ == '__main__': + diff --git a/gluon/xmlrpc.py b/gluon/xmlrpc.py index 24fd8f9c..6bf931cb 100644 --- a/gluon/xmlrpc.py +++ b/gluon/xmlrpc.py @@ -25,3 +25,4 @@ def handler(request, response, methods): + diff --git a/isapiwsgihandler.py b/isapiwsgihandler.py index 9a5c891a..28973d2d 100644 --- a/isapiwsgihandler.py +++ b/isapiwsgihandler.py @@ -37,3 +37,4 @@ if __name__=='__main__': + diff --git a/modpythonhandler.py b/modpythonhandler.py index add46cd3..4ed135f1 100755 --- a/modpythonhandler.py +++ b/modpythonhandler.py @@ -226,3 +226,4 @@ def handler(req): + diff --git a/options_std.py b/options_std.py index feb0a48f..85cd071b 100644 --- a/options_std.py +++ b/options_std.py @@ -31,3 +31,4 @@ shutdown_timeout = 5 folder = os.getcwd() extcron = None nocron = None + diff --git a/router.example.py b/router.example.py index a98b5436..fef9737c 100644 --- a/router.example.py +++ b/router.example.py @@ -213,3 +213,4 @@ if __name__ == '__main__': + diff --git a/routes.example.py b/routes.example.py index c48bd282..ea7a8142 100644 --- a/routes.example.py +++ b/routes.example.py @@ -174,3 +174,4 @@ if __name__ == '__main__': + diff --git a/scgihandler.py b/scgihandler.py index dbadb1d4..e0dd6593 100755 --- a/scgihandler.py +++ b/scgihandler.py @@ -75,3 +75,4 @@ SCGIServer(application, port=4000).enable_sighandler().run() + diff --git a/scripts/setup-web2py-nginx-uwsgi-on-centos.sh b/scripts/setup-web2py-nginx-uwsgi-on-centos.sh index d6d613c1..0892026d 100644 --- a/scripts/setup-web2py-nginx-uwsgi-on-centos.sh +++ b/scripts/setup-web2py-nginx-uwsgi-on-centos.sh @@ -315,3 +315,4 @@ reboot as superuser " + diff --git a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh index 1d50f6ad..f62a529b 100644 --- a/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh +++ b/scripts/setup-web2py-nginx-uwsgi-ubuntu.sh @@ -1,79 +1,78 @@ -#!/bin/bash - -echo 'setup-web2py-nginx-uwsgi-ubuntu-precise.sh' -echo 'Requires Ubuntu 12.04 and installs Nginx + uWSGI + Web2py' - -# Get Web2py Admin Password -echo -e "Web2py Admin Password: \c " -read PW - -# Upgrade and install needed software -apt-get update -apt-get -y upgrade -apt-get -y dist-upgrade -apt-get autoremove -apt-get autoclean -apt-get -y install nginx-full -apt-get -y install uwsgi uwsgi-plugin-python - -# Create configuration file /etc/nginx/sites-available/web2py -echo 'server { - listen 80; - server_name $hostname; - location ~* /(\w+)/static/ { - root /home/www-data/web2py/applications/; - } - location / { - uwsgi_pass 127.0.0.1:9001; - include uwsgi_params; - uwsgi_param UWSGI_SCHEME $scheme; - uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; - } -} - -server { - listen 443; - server_name $hostname; - ssl on; - ssl_certificate /etc/nginx/ssl/web2py.crt; - ssl_certificate_key /etc/nginx/ssl/web2py.key; - location / { - uwsgi_pass 127.0.0.1:9001; - include uwsgi_params; - uwsgi_param UWSGI_SCHEME $scheme; - uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; - } - -}' >/etc/nginx/sites-available/web2py - -ln -s /etc/nginx/sites-available/web2py /etc/nginx/sites-enabled/web2py -rm /etc/nginx/sites-enabled/default -mkdir /etc/nginx/ssl -cd /etc/nginx/ssl -openssl genrsa -out web2py.key 1024 -openssl req -batch -new -key web2py.key -out web2py.csr -openssl x509 -req -days 1780 -in web2py.csr -signkey web2py.key -out web2py.crt - -# Create configuration file /etc/uwsgi/apps-available/web2py.xml -echo ' - python - 127.0.0.1:9001 - /home/www-data/web2py/ - - - -' >/etc/uwsgi/apps-available/web2py.xml -ln -s /etc/uwsgi/apps-available/web2py.xml /etc/uwsgi/apps-enabled/web2py.xml - -# Install Web2py -apt-get -y install unzip -mkdir /home/www-data -cd /home/www-data -wget http://web2py.com/examples/static/web2py_src.zip -unzip web2py_src.zip -rm web2py_src.zip -chown -R www-data:www-data web2py -cd /home/www-data/web2py -sudo -u www-data python -c "from gluon.main import save_password; save_password('$PW',443)" -/etc/init.d/uwsgi restart -/etc/init.d/nginx restart +echo 'setup-web2py-nginx-uwsgi-ubuntu-precise.sh' +echo 'Requires Ubuntu 12.04 and installs Nginx + uWSGI + Web2py' + +# Get Web2py Admin Password +echo -e "Web2py Admin Password: \c " +read PW + +# Upgrade and install needed software +apt-get update +apt-get -y upgrade +apt-get -y dist-upgrade +apt-get autoremove +apt-get autoclean +apt-get -y install nginx-full +apt-get -y install uwsgi uwsgi-plugin-python + +# Create configuration file /etc/nginx/sites-available/web2py +echo 'server { + listen 80; + server_name $hostname; + location ~* /(\w+)/static/ { + root /home/www-data/web2py/applications/; + } + location / { + uwsgi_pass 127.0.0.1:9001; + include uwsgi_params; + uwsgi_param UWSGI_SCHEME $scheme; + uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; + } +} + +server { + listen 443; + server_name $hostname; + ssl on; + ssl_certificate /etc/nginx/ssl/web2py.crt; + ssl_certificate_key /etc/nginx/ssl/web2py.key; + location / { + uwsgi_pass 127.0.0.1:9001; + include uwsgi_params; + uwsgi_param UWSGI_SCHEME $scheme; + uwsgi_param SERVER_SOFTWARE nginx/$nginx_version; + } + +}' >/etc/nginx/sites-available/web2py + +ln -s /etc/nginx/sites-available/web2py /etc/nginx/sites-enabled/web2py +rm /etc/nginx/sites-enabled/default +mkdir /etc/nginx/ssl +cd /etc/nginx/ssl +openssl genrsa -out web2py.key 1024 +openssl req -batch -new -key web2py.key -out web2py.csr +openssl x509 -req -days 1780 -in web2py.csr -signkey web2py.key -out web2py.crt + +# Create configuration file /etc/uwsgi/apps-available/web2py.xml +echo ' + python + 127.0.0.1:9001 + /home/www-data/web2py/ + + + +' >/etc/uwsgi/apps-available/web2py.xml +ln -s /etc/uwsgi/apps-available/web2py.xml /etc/uwsgi/apps-enabled/web2py.xml + +# Install Web2py +apt-get -y install unzip +mkdir /home/www-data +cd /home/www-data +wget http://web2py.com/examples/static/web2py_src.zip +unzip web2py_src.zip +rm web2py_src.zip +chown -R www-data:www-data web2py +cd /home/www-data/web2py +sudo -u www-data python -c "from gluon.main import save_password; save_password('$PW',443)" +/etc/init.d/uwsgi restart +/etc/init.d/nginx restart + diff --git a/setup.py b/setup.py index 25c4f173..3dbc628e 100644 --- a/setup.py +++ b/setup.py @@ -81,3 +81,4 @@ if __name__ == '__main__': + diff --git a/setup_app.py b/setup_app.py index 67e0da5b..cb57c8fc 100755 --- a/setup_app.py +++ b/setup_app.py @@ -56,3 +56,4 @@ setup(app=['web2py.py'], + diff --git a/setup_exe.py b/setup_exe.py index dda70ca0..9b6e88e0 100755 --- a/setup_exe.py +++ b/setup_exe.py @@ -186,3 +186,4 @@ print "Enjoy web2py " +web2py_version_line + diff --git a/setup_exe_2.6.py b/setup_exe_2.6.py index fcb49525..cec07199 100644 --- a/setup_exe_2.6.py +++ b/setup_exe_2.6.py @@ -168,3 +168,4 @@ print "Enjoy web2py " +web2py_version_line + diff --git a/web2py.py b/web2py.py index 3446da0a..fbd402e9 100755 --- a/web2py.py +++ b/web2py.py @@ -29,3 +29,4 @@ if __name__ == '__main__': + diff --git a/wsgihandler.py b/wsgihandler.py index 590f2b46..85368eb3 100644 --- a/wsgihandler.py +++ b/wsgihandler.py @@ -46,3 +46,4 @@ if SOFTCRON: +