2018-02-07 16:27:15 +01:00
|
|
|
#! /usr/bin/python3
|
2018-02-15 21:43:25 +01:00
|
|
|
# -*- coding: UTF-8 -*-
|
|
|
|
|
2018-02-07 16:27:15 +01:00
|
|
|
"""Help decrypting the field names in pdf files.
|
|
|
|
|
2018-02-15 21:43:25 +01:00
|
|
|
FormFields model all information existing about fields in pdf forms.
|
|
|
|
|
|
|
|
Include a method to go through the list of pdf form fields and ask the
|
2018-02-07 16:27:15 +01:00
|
|
|
user for suitable descriptions.
|
2018-02-08 11:08:38 +01:00
|
|
|
|
|
|
|
The information stored for the fields are:
|
2018-02-15 21:43:25 +01:00
|
|
|
information from the pdf file
|
|
|
|
Name
|
|
|
|
Description
|
|
|
|
commands (as in config files)
|
2018-02-07 16:27:15 +01:00
|
|
|
"""
|
|
|
|
|
2018-02-15 20:41:35 +01:00
|
|
|
# todo: disable focus on pdf - apparently not simple :(
|
2018-02-15 01:02:05 +01:00
|
|
|
|
2018-02-07 16:27:15 +01:00
|
|
|
import subprocess as cmd
|
2018-02-08 11:08:38 +01:00
|
|
|
import os.path
|
|
|
|
import fdfgen
|
2018-03-21 18:32:05 +01:00
|
|
|
from constants import NON_FORMFIELD
|
2018-02-08 11:08:38 +01:00
|
|
|
|
|
|
|
|
|
|
|
class FormField():
|
|
|
|
"""
|
|
|
|
Store all information for one field.
|
|
|
|
|
2018-02-15 01:02:05 +01:00
|
|
|
They can be accessed via [...]
|
|
|
|
(via __getitem__).
|
|
|
|
|
2018-02-08 11:08:38 +01:00
|
|
|
Those are:
|
2018-02-15 01:02:05 +01:00
|
|
|
information from dump_data_fields ("Field...")
|
|
|
|
informative name (["Name"])
|
|
|
|
long description (["Description"])
|
2018-02-08 11:08:38 +01:00
|
|
|
|
|
|
|
"""
|
|
|
|
|
2018-02-15 01:02:05 +01:00
|
|
|
def convertToBool(self, possBool):
|
|
|
|
"""Convert value to True or or False if this field is a Checkbox.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
possBool if this is no Checkbox or it is unclear of True or False is
|
|
|
|
meant. Otherwise True or False
|
|
|
|
|
|
|
|
"""
|
|
|
|
try:
|
|
|
|
if self["FieldType"] == "Button":
|
|
|
|
if possBool.lower() in ["yes", "on", "x", "true"]:
|
|
|
|
return True
|
|
|
|
elif possBool.lower() in ["no", "_", "off", "false"]:
|
|
|
|
return False
|
|
|
|
else:
|
|
|
|
return possBool
|
|
|
|
else:
|
|
|
|
return possBool
|
|
|
|
except AttributeError:
|
|
|
|
# lower probably did not work:
|
|
|
|
return possBool
|
|
|
|
except KeyError:
|
|
|
|
# "FieldType" does not exist
|
|
|
|
# maybe something more intelligent necessary
|
|
|
|
return possBool
|
|
|
|
|
2018-02-15 20:41:35 +01:00
|
|
|
@staticmethod
|
|
|
|
def convertUmlauts(value):
|
|
|
|
"""Convert all html-Syntax-Umlauts to the correct symbols.
|
|
|
|
|
|
|
|
Necessary to make fdfgen generate files that are read correctly
|
|
|
|
by pdftk.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
value, &#number replaced by the correct unicode
|
|
|
|
|
|
|
|
"""
|
|
|
|
for entity, uni in [("Ä", "Ä"),
|
|
|
|
("Ö", "Ö"),
|
|
|
|
("Ü", "Ü"),
|
|
|
|
("ß", "ß"),
|
|
|
|
("ä", "ä"),
|
|
|
|
("ö", "ö"),
|
|
|
|
("ü", "ü")]:
|
|
|
|
value = value.replace(entity, uni)
|
|
|
|
return value
|
|
|
|
|
2018-02-08 11:08:38 +01:00
|
|
|
def __init__(self):
|
|
|
|
"""
|
|
|
|
Initialise a FormField.
|
|
|
|
|
|
|
|
With an empty dictionary for pdfinfo
|
|
|
|
and None for the other values.
|
2018-02-15 20:41:35 +01:00
|
|
|
|
2018-02-08 11:08:38 +01:00
|
|
|
"""
|
2018-02-15 01:02:05 +01:00
|
|
|
self.__pdfinfo = {}
|
|
|
|
|
2018-03-15 14:35:01 +01:00
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
"""Get the name of this field.
|
|
|
|
|
|
|
|
If "Name" exists, take this.
|
|
|
|
Otherwise if "FieldName" exists, take this.
|
|
|
|
Otherwise if "FieldNameAlt" exists, take this.
|
|
|
|
Otherwise take "[Error: Unnamed]".
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
A name of the FormField usable in error messages.
|
|
|
|
|
|
|
|
"""
|
|
|
|
for key in ["Name", "FieldName", "FieldNameAlt"]:
|
|
|
|
try:
|
|
|
|
return self[key]
|
|
|
|
except KeyError:
|
|
|
|
pass # try next one
|
|
|
|
return "[Error: Unnamed]"
|
|
|
|
|
2018-02-15 01:02:05 +01:00
|
|
|
def __getitem__(self, key):
|
|
|
|
"""Get an information about this field.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
self[key]
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
KeyError: as usual if key is not a valid key
|
|
|
|
|
|
|
|
"""
|
|
|
|
if key in self:
|
|
|
|
assert self.__pdfinfo[key] is not None
|
|
|
|
return self.__pdfinfo[key]
|
|
|
|
|
|
|
|
def __setitem__(self, key, value):
|
|
|
|
"""Set an information about this field.
|
|
|
|
|
|
|
|
Do: self[key] = value.
|
|
|
|
|
|
|
|
If value is None, the key is removed.
|
|
|
|
|
|
|
|
"""
|
|
|
|
if value is None:
|
|
|
|
try:
|
|
|
|
del self.__pdfinfo[key]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
# todo: add warning in some log file
|
|
|
|
else:
|
|
|
|
self.__pdfinfo[key] = value
|
|
|
|
|
|
|
|
def __delitem__(self, key):
|
|
|
|
"""Delete an information from this field.
|
|
|
|
|
|
|
|
Do: del self.__pdfinfo[key]
|
|
|
|
|
|
|
|
Raises:
|
|
|
|
KeyError: as usual for dicts
|
|
|
|
"""
|
|
|
|
del self.__pdfinfo[key]
|
|
|
|
|
|
|
|
def __contains__(self, item):
|
|
|
|
"""Return if item is a valid info."""
|
|
|
|
return item in self.__pdfinfo
|
|
|
|
|
|
|
|
def expanditer(self):
|
|
|
|
"""Return an iterator that iterates over all information.
|
2018-02-08 11:08:38 +01:00
|
|
|
|
2018-02-15 01:02:05 +01:00
|
|
|
Yielding those having lists once for each element.
|
|
|
|
|
|
|
|
For each information the name and the value in a tuple
|
|
|
|
is returned.
|
2018-02-08 11:08:38 +01:00
|
|
|
"""
|
2018-02-15 01:02:05 +01:00
|
|
|
for info in self.__pdfinfo:
|
|
|
|
# strings behave like lists, hence I cannot identify a
|
|
|
|
# difference
|
|
|
|
if str(self[info]) == self[info]:
|
|
|
|
yield (info, self[info])
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
for oneinfo in self[info]:
|
|
|
|
yield (info, oneinfo)
|
|
|
|
except TypeError:
|
|
|
|
# e.g. bool is not iterable
|
|
|
|
yield (info, self[info])
|
|
|
|
|
2018-02-21 22:33:25 +01:00
|
|
|
def iterkeys(self):
|
|
|
|
"""Return what you would expect of an iteration over a dict.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
an iterator that iterates over all information keys.
|
|
|
|
|
|
|
|
"""
|
|
|
|
return iter(self.__pdfinfo)
|
|
|
|
|
2018-02-15 01:02:05 +01:00
|
|
|
def __iter__(self):
|
|
|
|
"""Return all info:value pairs.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
iter(self.__pdfinfo.items()).
|
|
|
|
|
|
|
|
"""
|
|
|
|
return iter(self.__pdfinfo.items())
|
|
|
|
|
|
|
|
def __len__(self):
|
|
|
|
"""Return number of information(s)."""
|
|
|
|
return len(self.__pdfinfo)
|
|
|
|
|
|
|
|
def essentiallyempty(self):
|
|
|
|
"""Return if any information from the user is stored.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
True if any info starts not with "Field"
|
|
|
|
|
|
|
|
"""
|
|
|
|
return all([info.startswith("Field") for info in self.__pdfinfo])
|
|
|
|
|
|
|
|
def askUser(self, origpdf, update=False):
|
|
|
|
"""Ask the user for information about this form field.
|
2018-02-08 11:08:38 +01:00
|
|
|
|
|
|
|
Create a pdf where only this field is filled with some data
|
|
|
|
such that the user can easily identify which field we are
|
|
|
|
talking about.
|
|
|
|
Doing this a file origpdf-tmpfilled.pdf and field-tmp.fdf are created.
|
|
|
|
It is not checked if this is a problem.
|
2018-02-15 01:02:05 +01:00
|
|
|
|
|
|
|
Attributes:
|
|
|
|
origpdf (str): path to the pdf file that all is about
|
|
|
|
update (bool): if fields that have an information should be queried
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
if the user wants to abort this and considers her/himself done
|
2018-02-08 11:08:38 +01:00
|
|
|
"""
|
2018-02-15 01:02:05 +01:00
|
|
|
if update or self.essentiallyempty():
|
|
|
|
# no data there or should be updated
|
|
|
|
# all information from dump_data_fields start with Field
|
|
|
|
try:
|
|
|
|
fdfinfo = [(self["FieldName"], "On")]
|
|
|
|
except KeyError:
|
|
|
|
raise ValueError("This field does not have a FieldName. This" +
|
|
|
|
" should not be possible in a pdf.")
|
|
|
|
else:
|
|
|
|
fdf = fdfgen.forge_fdf("", fdfinfo, [], [], [])
|
|
|
|
with open("field-tmp.fdf", "wb") as fdf_file: # includes close
|
|
|
|
fdf_file.write(fdf)
|
|
|
|
tmppdf = os.path.basename(origpdf) + "-tmpfilled.pdf"
|
|
|
|
pdfcreation = cmd.Popen(["pdftk", origpdf,
|
|
|
|
"fill_form", "field-tmp.fdf", "output",
|
|
|
|
tmppdf], stderr=cmd.PIPE)
|
|
|
|
# those pdfs are pseudo-encrypted. That makes pdftk issue
|
|
|
|
# a warning that one should respect copyright.
|
|
|
|
pdfcreation.wait(timeout=5) # wait a maximum of 5 seconds
|
|
|
|
# for pdftk to finish
|
|
|
|
# then evince can work
|
|
|
|
cmd.Popen(["evince", tmppdf], stderr=cmd.PIPE) # do not need
|
|
|
|
# the errors to show
|
|
|
|
print("The current field is called " + self["FieldName"])
|
|
|
|
|
2018-02-15 21:43:25 +01:00
|
|
|
# def buttonmessage():
|
|
|
|
# """Show a special message for buttons.
|
|
|
|
#
|
|
|
|
# Explains Button options.
|
|
|
|
#
|
|
|
|
# """
|
|
|
|
# if self["FieldType"] == "Button":
|
|
|
|
# print("It is a butten.",
|
|
|
|
# "'X', 'On', 'Yes' all say 'cross it'",
|
|
|
|
# " while '_', 'Off', 'No' all say",
|
|
|
|
# "'do not make a cross here'")
|
2018-02-15 01:02:05 +01:00
|
|
|
|
|
|
|
for message, info, specialmessage, converting in [
|
|
|
|
("Descriptive name", "Name", lambda: None,
|
|
|
|
lambda x: x),
|
|
|
|
("Long description", "Description", lambda: None,
|
|
|
|
lambda x: x),
|
2018-02-15 21:43:25 +01:00
|
|
|
# ("Standard value", "Stdvalue", buttonmessage,
|
|
|
|
# self.convertToBool),
|
|
|
|
# ("Special handling", "Special", lambda: None,
|
|
|
|
# lambda x: x)
|
|
|
|
]:
|
2018-02-15 01:02:05 +01:00
|
|
|
if info in self:
|
|
|
|
print("Value now:", self[info])
|
|
|
|
print("Enter nothing and this value is used.")
|
|
|
|
specialmessage()
|
|
|
|
try:
|
|
|
|
newvalue = converting(input(message + ":"))
|
|
|
|
except EOFError:
|
|
|
|
# user hit Ctrl+D
|
|
|
|
return False
|
|
|
|
try:
|
|
|
|
if newvalue.strip() != "":
|
|
|
|
self[info] = newvalue
|
|
|
|
# otherwise take old value
|
|
|
|
except AttributeError:
|
|
|
|
# strip is not a method (not a string)
|
|
|
|
if newvalue is not None:
|
|
|
|
self[info] = newvalue
|
|
|
|
# otherwise take old value
|
|
|
|
|
|
|
|
return True
|
2018-02-08 11:08:38 +01:00
|
|
|
|
|
|
|
def __str__(self):
|
2018-02-15 01:02:05 +01:00
|
|
|
"""Return a several line long string with all info of this field.
|
|
|
|
|
|
|
|
Information that are missing are not displayed.
|
|
|
|
|
2018-02-08 11:08:38 +01:00
|
|
|
"""
|
2018-02-15 01:02:05 +01:00
|
|
|
return "\n".join([str(info) + ": " + str(value)
|
|
|
|
for info, value in self.expanditer()])
|
|
|
|
|
|
|
|
def merge(self, otherField):
|
|
|
|
"""Merge two fields.
|
|
|
|
|
|
|
|
Add the information from otherField that are not in self to self.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
self after adding stuff
|
|
|
|
Raises:
|
|
|
|
ValueError: if otherField and self disagree on a value.
|
|
|
|
! self might already have changed !
|
2018-02-08 11:08:38 +01:00
|
|
|
|
|
|
|
"""
|
2018-02-15 01:02:05 +01:00
|
|
|
for info, value in otherField:
|
|
|
|
# otherField.__iter__() is called
|
|
|
|
try:
|
|
|
|
if self[info] != value:
|
|
|
|
raise ValueError("The two fields disagree on " + info
|
|
|
|
+ ": '" + str(self[info]) + "' != '"
|
|
|
|
+ str(value) + "'")
|
|
|
|
except KeyError:
|
|
|
|
pass # ok, info does not exist in self
|
|
|
|
# else: do nothing, info exists already
|
|
|
|
|
|
|
|
for info, value in otherField:
|
|
|
|
# info does not exist in self or is equal
|
|
|
|
self[info] = value
|
2018-02-16 13:13:38 +01:00
|
|
|
|
|
|
|
@staticmethod
|
2018-03-21 17:48:35 +01:00
|
|
|
def findByFieldName(fieldList, fieldName,
|
2018-03-21 18:32:05 +01:00
|
|
|
include=(lambda x: (NON_FORMFIELD
|
2018-03-21 17:48:35 +01:00
|
|
|
not in x))):
|
2018-02-16 13:13:38 +01:00
|
|
|
"""Return a FormField out of a list given the searched fieldName.
|
|
|
|
|
|
|
|
If there are several fields with this fieldName,
|
|
|
|
the first one is returned. This should not be the case though.
|
|
|
|
|
2018-03-21 17:48:35 +01:00
|
|
|
Attributes:
|
|
|
|
fieldList ([FormField]): list of formfields to search in
|
|
|
|
fieldName (str): the fieldName looked for
|
|
|
|
include (fct: FormField -> Bool):
|
|
|
|
fct. that returns True if the argument should be looked for
|
|
|
|
Default: only use FormFields that do not have the info
|
|
|
|
NON_FORMFIELD.
|
|
|
|
|
2018-02-16 13:13:38 +01:00
|
|
|
Raises:
|
|
|
|
KeyError: if no such FieldName exist.
|
|
|
|
|
|
|
|
"""
|
|
|
|
for f in fieldList:
|
|
|
|
try:
|
2018-03-21 17:48:35 +01:00
|
|
|
if include(f) and f["FieldName"] == fieldName:
|
2018-02-16 13:13:38 +01:00
|
|
|
return f
|
|
|
|
except KeyError:
|
|
|
|
pass # should not happen but who knows?
|
|
|
|
raise KeyError("No field with FieldName '" + fieldName + "' exist.")
|