├── requirements-tests.txt ├── setup.cfg ├── .gitignore ├── tox.ini ├── tests ├── test_encoding.py ├── test_forge_fdf.py └── test_fdf_identifier.py ├── .github └── dependabot.yml ├── .travis.yml ├── setup.py ├── LICENSE ├── README.md └── fdfgen └── __init__.py /requirements-tests.txt: -------------------------------------------------------------------------------- 1 | tox==4.32.0 2 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .cache 3 | *.egg-info 4 | .tox 5 | dist 6 | build 7 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py34,py35,py36,py37 3 | [testenv] 4 | deps=pytest 5 | commands=pytest {posargs} 6 | -------------------------------------------------------------------------------- /tests/test_encoding.py: -------------------------------------------------------------------------------- 1 | import fdfgen 2 | 3 | 4 | def test_string_with_unbalanced_paren(): 5 | s = 'a) 1st item' 6 | e = b'\xfe\xff\x00a\x00\\)\x00 \x001\x00s\x00t\x00 \x00i\x00t\x00e\x00m' 7 | assert fdfgen.smart_encode_str(s) == e 8 | -------------------------------------------------------------------------------- /tests/test_forge_fdf.py: -------------------------------------------------------------------------------- 1 | import fdfgen 2 | 3 | 4 | def test_key_encoding(): 5 | expected_field_name = b'T(\xfe\xff\x00f\x00o\x00o)' # T(foo) where foo is UTF-16 coded 6 | result = fdfgen.forge_fdf(fdf_data_strings=[('foo', 'bar')]) 7 | assert expected_field_name in result 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: pip 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "10:00" 8 | open-pull-requests-limit: 10 9 | - package-ecosystem: github-actions 10 | directory: "/" 11 | schedule: 12 | interval: daily 13 | time: "10:00" 14 | -------------------------------------------------------------------------------- /tests/test_fdf_identifier.py: -------------------------------------------------------------------------------- 1 | import fdfgen 2 | 3 | 4 | def test_identifier_with_slash(): 5 | expected_identifier = b'/Off' 6 | result = fdfgen.FDFIdentifier('/Off').value 7 | assert result == expected_identifier 8 | 9 | 10 | def test_identifier_without_slash(): 11 | expected_identifier = b'/Off' 12 | result = fdfgen.FDFIdentifier('Off').value 13 | assert result == expected_identifier 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | language: python 3 | python: 4 | - '2.7' 5 | - '3.4' 6 | - '3.5' 7 | - '3.6' 8 | - '3.7' 9 | install: pip install tox-travis 10 | script: tox 11 | deploy: 12 | provider: pypi 13 | distributions: "sdist bdist_wheel" 14 | user: ctlpypi 15 | password: 16 | secure: U5KwqQpfUsyHRmwnOV0Pv/qRSteavHJ9tHiYzCnGAhHBPKLeAC5EUa7WfkXSkuQ4eRvohvxb5ybytMP+R5If4ZL3y7Z3MNosLumj0I2IRtl7+xKmouCEgilda1vVZIIWOQB/SMRmGxKadhNfv9W/9GkSdhsHllqkSWOS+U/Pv9g= 17 | on: 18 | tags: true 19 | repo: ccnmtl/fdfgen 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name="fdfgen", 5 | version="0.16.1", 6 | author="Anders Pearson", 7 | author_email="ctl-dev@columbia.edu", 8 | url="http://github.com/ccnmtl/fdfgen/", 9 | description="library for creating FDF files", 10 | long_description="Python port of the PHP forge_fdf library for creating FDF files", 11 | scripts = [], 12 | license = "BSD-3-Clause", 13 | platforms = ["any"], 14 | zip_safe=False, 15 | packages=find_packages() 16 | ) 17 | 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2017 Anders Pearson. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | 1. Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the 12 | distribution. 13 | 3. Neither the name of the copyright holder nor the names of its 14 | contributors may be used to endorse or promote products derived 15 | from this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # fdfgen 2 | 3 | [![Build Status](https://travis-ci.org/ccnmtl/fdfgen.svg?branch=master)](https://travis-ci.org/ccnmtl/fdfgen) 4 | 5 | Python port of the PHP [forge_fdf](http://www.pdfhacks.com/forge_fdf/) library by Sid Steward 6 | 7 | PDF forms work with FDF data. I ported a PHP FDF library to Python a while back when I had to do this and released it as fdfgen. I use that to generate an fdf file with the data for the form, then use [`pdftk`](http://www.pdflabs.com/tools/pdftk-server/) to push the fdf into a PDF form and generate the output. 8 | 9 | ## QUICK INSTALL 10 | 11 | pip install fdfgen 12 | 13 | ## HOW IT WORKS 14 | 15 | 1. You (or a designer) design the `form.pdf` in Acrobat. 16 | 2. Mark the form fields and take note of the field names. This can be done either through Acrobat or by installing pdftk and entering the command line 17 | 18 | pdftk [pdf name] dump_data_fields 19 | 20 | 3. Let's say your form has fields "name" and "telephone". 21 | 22 | Use fdfgen to create a FDF file: 23 | 24 | #!/usr/bin/env python 25 | from fdfgen import forge_fdf 26 | 27 | fields = [('name', 'John Smith'), ('telephone', '555-1234')] 28 | fdf = forge_fdf("",fields,[],[],[]) 29 | 30 | with open("data.fdf", "wb") as fdf_file: 31 | fdf_file.write(fdf) 32 | 33 | 4. Then you run pdftk to merge and flatten: 34 | 35 | pdftk form.pdf fill_form data.fdf output output.pdf flatten 36 | 37 | and a filled out, flattened (meaning that there are no longer editable form fields) pdf will be in `output.pdf`. 38 | 39 | ## CHANGELOG 40 | 41 | * 0.16.1 -- 2017-11-21 -- Fix `TypeError` in python 3.6 by Tom Grundy (@caver456) 42 | * 0.16.0 -- 2017-02-22 -- Allow for different values for each checkbox by 43 | * 0.15.0 -- 2016-09-23 -- Encode field names as UTF-16 fix by Andreas Pelme 44 | * 0.14.0 -- 2016-08-09 -- Adobe FDF Compatibility added by Cooper Stimson (@6C1) 45 | * 0.13.0 -- 2016-04-22 -- python 3 bugfix from Julien Enselme 46 | * 0.12.1 -- 2015-11-01 -- handle alternative checkbox values fix from Bil Bas 47 | * 0.12.0 -- 2015-07-29 -- python 3 bugfixes 48 | * 0.11.0 -- 2013-12-07 -- python 3 port from Evan Fredericksen 49 | * 0.10.2 -- 2013-06-16 -- minor code refactor and added command line options from Robert Stewart 50 | * 0.10.1 -- 2013-04-22 -- unbalanced paren bugfix from Brandon Rhodes 51 | * 0.10.0 -- 2012-06-14 -- support checkbox fields and parenthesis in strings from Guangcong Luo 52 | * 0.9.2 -- 2011-01-12 -- merged unicode fix from Sébastien Fievet 53 | 54 | ## RUNNING TESTS: 55 | 56 | * Create a virtual environment 57 | * tox is required to run the tests. You can install the correct version with 58 | `pip install -r requirements-tests.txt` 59 | * Run `tox` to run tests for all Python versions. 60 | * To run a specific test and specific Python versions, you may use `tox -e py27 61 | -- tests/test_encoding.py` 62 | -------------------------------------------------------------------------------- /fdfgen/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Port of the PHP forge_fdf library by Sid Steward 4 | (http://www.pdfhacks.com/forge_fdf/) 5 | 6 | Anders Pearson at Columbia Center For New Media Teaching 7 | and Learning 8 | """ 9 | 10 | __author__ = "Anders Pearson " 11 | __credits__ = ("Sébastien Fievet ", 12 | "Brandon Rhodes ", 13 | "Robert Stewart ", 14 | "Evan Fredericksen ") 15 | 16 | import codecs 17 | import sys 18 | 19 | if sys.version_info[0] < 3: 20 | bytes = str 21 | 22 | 23 | def smart_encode_str(s): 24 | """Create a UTF-16 encoded PDF string literal for `s`.""" 25 | try: 26 | utf16 = s.encode('utf_16_be') 27 | except AttributeError: # ints and floats 28 | utf16 = str(s).encode('utf_16_be') 29 | safe = utf16.replace(b'\x00)', b'\x00\\)').replace(b'\x00(', b'\x00\\(') 30 | return b''.join((codecs.BOM_UTF16_BE, safe)) 31 | 32 | 33 | def handle_hidden(key, fields_hidden): 34 | if key in fields_hidden: 35 | return b"/SetF 2" 36 | else: 37 | return b"/ClrF 2" 38 | 39 | 40 | def handle_readonly(key, fields_readonly): 41 | if key in fields_readonly: 42 | return b"/SetFf 1" 43 | else: 44 | return b"/ClrFf 1" 45 | 46 | 47 | class FDFIdentifier(object): 48 | """A PDF value, such as /Yes or /Off that should be passed through with the / and without parenthesis (which would indicate it was a value, not an identifier) 49 | This allows for different checkbox checked/unchecked names per checkbox! 50 | """ 51 | def __init__(self, value): 52 | # make sure value is str right away, to avoid TypeError in startswith 53 | if isinstance(value, bytes): 54 | value = value.decode('utf-8') 55 | 56 | if value.startswith('/'): 57 | value = value[1:] 58 | 59 | value = u'/%s' % value 60 | value = value.encode('utf-8') 61 | 62 | self._value = value 63 | 64 | @property 65 | def value(self): 66 | return self._value 67 | 68 | 69 | def handle_data_strings(fdf_data_strings, fields_hidden, fields_readonly, 70 | checkbox_checked_name): 71 | if isinstance(fdf_data_strings, dict): 72 | fdf_data_strings = fdf_data_strings.items() 73 | 74 | for (key, value) in fdf_data_strings: 75 | if value is True: 76 | value = FDFIdentifier(checkbox_checked_name).value 77 | elif value is False: 78 | value = FDFIdentifier('Off').value 79 | elif isinstance(value, FDFIdentifier): 80 | value = value.value 81 | else: 82 | value = b''.join([b'(', smart_encode_str(value), b')']) 83 | 84 | yield b''.join([ 85 | b'<<', 86 | b'/T(', 87 | smart_encode_str(key), 88 | b')', 89 | b'/V', 90 | value, 91 | handle_hidden(key, fields_hidden), 92 | b'', 93 | handle_readonly(key, fields_readonly), 94 | b'>>', 95 | ]) 96 | 97 | 98 | def handle_data_names(fdf_data_names, fields_hidden, fields_readonly): 99 | if isinstance(fdf_data_names, dict): 100 | fdf_data_names = fdf_data_names.items() 101 | 102 | for (key, value) in fdf_data_names: 103 | yield b''.join([b'<<\x0a/V /', value.encode("utf-8"), b'\x0a/T (', 104 | smart_encode_str(key), b')\x0a', 105 | handle_hidden(key, fields_hidden), b'\x0a', 106 | handle_readonly(key, fields_readonly), b'\x0a>>\x0a']) 107 | 108 | 109 | def forge_fdf(pdf_form_url=None, fdf_data_strings=[], fdf_data_names=[], 110 | fields_hidden=[], fields_readonly=[], 111 | checkbox_checked_name=b"Yes"): 112 | """Generates fdf string from fields specified 113 | 114 | * pdf_form_url (default: None): just the url for the form. 115 | * fdf_data_strings (default: []): array of (string, value) tuples for the 116 | form fields (or dicts). Value is passed as a UTF-16 encoded string, 117 | unless True/False, in which case it is assumed to be a checkbox 118 | (and passes names, '/Yes' (by default) or '/Off'). 119 | * fdf_data_names (default: []): array of (string, value) tuples for the 120 | form fields (or dicts). Value is passed to FDF as a name, '/value' 121 | * fields_hidden (default: []): list of field names that should be set 122 | hidden. 123 | * fields_readonly (default: []): list of field names that should be set 124 | readonly. 125 | * checkbox_checked_value (default: "Yes"): By default means a checked 126 | checkboxes gets passed the value "/Yes". You may find that the default 127 | does not work with your PDF, in which case you might want to try "On". 128 | 129 | The result is a string suitable for writing to a .fdf file. 130 | 131 | """ 132 | fdf = [b'%FDF-1.2\x0a%\xe2\xe3\xcf\xd3\x0d\x0a'] 133 | fdf.append(b'1 0 obj\x0a<>\x0a') 144 | fdf.append(b'>>\x0aendobj\x0a') 145 | fdf.append(b'trailer\x0a\x0a<<\x0a/Root 1 0 R\x0a>>\x0a') 146 | fdf.append(b'%%EOF\x0a\x0a') 147 | return b''.join(fdf) 148 | 149 | 150 | if __name__ == "__main__": 151 | # a simple example of using fdfgen 152 | # this will create an FDF file suitable to fill in 153 | # the vacation request forms we use at work. 154 | 155 | from datetime import datetime 156 | fields = [('Name', 'Anders Pearson'), 157 | ('Date', datetime.now().strftime("%x")), 158 | ('Request_1', 'Next Monday through Friday'), 159 | ('Request_2', ''), 160 | ('Request_3', ''), 161 | ('Total_days', 5), 162 | ('emergency_phone', '857-6309')] 163 | fdf = forge_fdf(fdf_data_strings=fields) 164 | fdf_file = open("vacation.fdf", "wb") 165 | fdf_file.write(fdf) 166 | fdf_file.close() 167 | 168 | # Parse command-line arguments 169 | import argparse 170 | parser = argparse.ArgumentParser() 171 | parser.add_argument( 172 | "--output", "-o", 173 | help="FDF File to output to", 174 | default='vacation.fdf', 175 | type=argparse.FileType('wb')) 176 | parser.add_argument( 177 | "--fields", "-f", 178 | help="Fields used in form; syntax is fieldname=fieldvalue", 179 | default=fields, 180 | nargs='*') 181 | args = parser.parse_args() 182 | if args.fields is not fields: 183 | for e, x in enumerate(args. fields): 184 | args.fields[e] = x.split('=') 185 | fdf = forge_fdf(fdf_data_strings=args.fields) 186 | args.output.write(fdf) 187 | args.output.close() 188 | --------------------------------------------------------------------------------