Update docx, xlsx, pdf, pptx skills with latest improvements (#330)

docx: Add commenting and track-changes support. Reorganize OOXML
tooling into a shared office/ module.

pptx: Streamline SKILL.md, add slide-editing and pptxgenjs guides,
bundle html2pptx as a tgz. Reorganize OOXML tooling into a shared
office/ module.

xlsx: Move recalc script into scripts/ and expand it. Add shared
office/ module for OOXML pack/unpack/validate.

pdf: Improve form-filling workflow with new form-structure extraction
script and updated field-info extraction.
This commit is contained in:
Keith Lazuka
2026-02-03 21:09:36 -05:00
committed by GitHub
parent 69c0b1a067
commit 4e6907a33c
202 changed files with 28554 additions and 9472 deletions
+1 -31
View File
@@ -4,11 +4,8 @@ import sys
from pypdf import PdfReader
# Extracts data for the fillable form fields in a PDF and outputs JSON that
# Claude uses to fill the fields. See forms.md.
# This matches the format used by PdfReader `get_fields` and `update_page_form_field_values` methods.
def get_full_annotation_field_id(annotation):
components = []
while annotation:
@@ -25,12 +22,9 @@ def make_field_dict(field, field_id):
if ft == "/Tx":
field_dict["type"] = "text"
elif ft == "/Btn":
field_dict["type"] = "checkbox" # radio groups handled separately
field_dict["type"] = "checkbox"
states = field.get("/_States_", [])
if len(states) == 2:
# "/Off" seems to always be the unchecked value, as suggested by
# https://opensource.adobe.com/dc-acrobat-sdk-docs/standards/pdfstandards/pdf/PDF32000_2008.pdf#page=448
# It can be either first or second in the "/_States_" list.
if "/Off" in states:
field_dict["checked_value"] = states[0] if states[0] != "/Off" else states[1]
field_dict["unchecked_value"] = "/Off"
@@ -50,15 +44,6 @@ def make_field_dict(field, field_id):
return field_dict
# Returns a list of fillable PDF fields:
# [
# {
# "field_id": "name",
# "page": 1,
# "type": ("text", "checkbox", "radio_group", or "choice")
# // Per-type additional fields described in forms.md
# },
# ]
def get_field_info(reader: PdfReader):
fields = reader.get_fields()
@@ -66,19 +51,13 @@ def get_field_info(reader: PdfReader):
possible_radio_names = set()
for field_id, field in fields.items():
# Skip if this is a container field with children, except that it might be
# a parent group for radio button options.
if field.get("/Kids"):
if field.get("/FT") == "/Btn":
possible_radio_names.add(field_id)
continue
field_info_by_id[field_id] = make_field_dict(field, field_id)
# Bounding rects are stored in annotations in page objects.
# Radio button options have a separate annotation for each choice;
# all choices have the same field name.
# See https://westhealth.github.io/exploring-fillable-forms-with-pdfrw.html
radio_fields_by_id = {}
for page_index, page in enumerate(reader.pages):
@@ -90,8 +69,6 @@ def get_field_info(reader: PdfReader):
field_info_by_id[field_id]["rect"] = ann.get('/Rect')
elif field_id in possible_radio_names:
try:
# ann['/AP']['/N'] should have two items. One of them is '/Off',
# the other is the active value.
on_values = [v for v in ann["/AP"]["/N"] if v != "/Off"]
except KeyError:
continue
@@ -104,17 +81,11 @@ def get_field_info(reader: PdfReader):
"page": page_index + 1,
"radio_options": [],
}
# Note: at least on macOS 15.7, Preview.app doesn't show selected
# radio buttons correctly. (It does if you remove the leading slash
# from the value, but that causes them not to appear correctly in
# Chrome/Firefox/Acrobat/etc).
radio_fields_by_id[field_id]["radio_options"].append({
"value": on_values[0],
"rect": rect,
})
# Some PDFs have form field definitions without corresponding annotations,
# so we can't tell where they are. Ignore these fields for now.
fields_with_location = []
for field_info in field_info_by_id.values():
if "page" in field_info:
@@ -122,7 +93,6 @@ def get_field_info(reader: PdfReader):
else:
print(f"Unable to determine location for field id: {field_info.get('field_id')}, ignoring")
# Sort by page number, then Y position (flipped in PDF coordinate system), then X.
def sort_key(f):
if "radio_options" in f:
rect = f["radio_options"][0]["rect"] or [0, 0, 0, 0]