extract_form_field_info.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. import json
  2. import sys
  3. from pypdf import PdfReader
  4. def get_full_annotation_field_id(annotation):
  5. components = []
  6. while annotation:
  7. field_name = annotation.get('/T')
  8. if field_name:
  9. components.append(field_name)
  10. annotation = annotation.get('/Parent')
  11. return ".".join(reversed(components)) if components else None
  12. def make_field_dict(field, field_id):
  13. field_dict = {"field_id": field_id}
  14. ft = field.get('/FT')
  15. if ft == "/Tx":
  16. field_dict["type"] = "text"
  17. elif ft == "/Btn":
  18. field_dict["type"] = "checkbox"
  19. states = field.get("/_States_", [])
  20. if len(states) == 2:
  21. if "/Off" in states:
  22. field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1]
  23. field_dict["unchecked_value"] = "/Off"
  24. else:
  25. print(f"Unexpected state values for checkbox `${field_id}`. Its checked and unchecked values may not be correct; if you're trying to check it, visually verify the results.")
  26. field_dict["checked_value"] = states[0]
  27. field_dict["unchecked_value"] = states[1]
  28. elif ft == "/Ch":
  29. field_dict["type"] = "choice"
  30. states = field.get("/_States_", [])
  31. field_dict["choice_options"] = [{
  32. "value": state[0],
  33. "text": state[1],
  34. } for state in states]
  35. else:
  36. field_dict["type"] = f"unknown ({ft})"
  37. return field_dict
  38. def get_field_info(reader: PdfReader):
  39. fields = reader.get_fields()
  40. field_info_by_id = {}
  41. possible_radio_names = set()
  42. for field_id, field in fields.items():
  43. if field.get("/Kids"):
  44. if field.get("/FT") == "/Btn":
  45. possible_radio_names.add(field_id)
  46. continue
  47. field_info_by_id[field_id] = make_field_dict(field, field_id)
  48. radio_fields_by_id = {}
  49. for page_index, page in enumerate(reader.pages):
  50. annotations = page.get('/Annots', [])
  51. for ann in annotations:
  52. field_id = get_full_annotation_field_id(ann)
  53. if field_id in field_info_by_id:
  54. field_info_by_id[field_id]["page"] = page_index + 1
  55. field_info_by_id[field_id]["rect"] = ann.get('/Rect')
  56. elif field_id in possible_radio_names:
  57. try:
  58. on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"]
  59. except KeyError:
  60. continue
  61. if len(on_values) == 1:
  62. rect = ann.get("/Rect")
  63. if field_id not in radio_fields_by_id:
  64. radio_fields_by_id[field_id] = {
  65. "field_id": field_id,
  66. "type": "radio_group",
  67. "page": page_index + 1,
  68. "radio_options": [],
  69. }
  70. radio_fields_by_id[field_id]["radio_options"].append({
  71. "value": on_values[0],
  72. "rect": rect,
  73. })
  74. fields_with_location = []
  75. for field_info in field_info_by_id.values():
  76. if "page" in field_info:
  77. fields_with_location.append(field_info)
  78. else:
  79. print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring")
  80. def sort_key(f):
  81. if "radio_options" in f:
  82. rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0]
  83. else:
  84. rect = f.get("rect") or [0, 0, 0, 0]
  85. adjusted_position = [-rect[1], rect[0]]
  86. return [f.get("page"), adjusted_position]
  87. sorted_fields = fields_with_location + list(radio_fields_by_id.values())
  88. sorted_fields.sort(key=sort_key)
  89. return sorted_fields
  90. def write_field_info(pdf_path: str, json_output_path: str):
  91. reader = PdfReader(pdf_path)
  92. field_info = get_field_info(reader)
  93. with open(json_output_path, "w") as f:
  94. json.dump(field_info, f, indent=2)
  95. print(f"Wrote {len(field_info)} fields to {json_output_path}")
  96. if __name__ == "__main__":
  97. if len(sys.argv) != 3:
  98. print("Usage: extract_form_field_info.py [input pdf] [output json]")
  99. sys.exit(1)
  100. write_field_info(sys.argv[1], sys.argv[2])