update app

This commit is contained in:
Sean McElwain 2026-05-07 13:03:18 -05:00
parent b49e4b732a
commit 664e8b44fb
2 changed files with 148 additions and 0 deletions

112
app/bluebeam_bci.py Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env python3
from pathlib import Path
import argparse
import pandas as pd
BASE_DIR = Path(__file__).resolve().parents[1]
DATA_DIR = BASE_DIR / "data"
OUTPUT_DIR = BASE_DIR / "output"
def clean_value(v):
if pd.isna(v):
return ""
return str(v).strip()
def bci_escape(v):
v = clean_value(v)
v = v.replace("\\", "\\\\")
v = v.replace("'", "\\'")
v = v.replace("\n", "\\n")
return v
def build_column_dict(row, exclude_cols):
data = {}
for col in row.index:
if col in exclude_cols:
continue
val = clean_value(row[col])
if val != "":
data[col] = val
return data
def dict_to_bci_payload(d):
parts = []
for k, v in d.items():
parts.append(f"'{bci_escape(k)}':'{bci_escape(v)}'")
return "{" + ",".join(parts) + "}"
def main():
parser = argparse.ArgumentParser(description="Generate Bluebeam BCI script from Markups CSV + update CSV.")
parser.add_argument("--bb-csv", default=str(DATA_DIR / "bluebeam_markups.csv"), help="Bluebeam Markups List CSV export")
parser.add_argument("--updates-csv", default=str(DATA_DIR / "bluebeam_updates.csv"), help="Your update CSV")
parser.add_argument("--out", default=str(OUTPUT_DIR / "update_columns.bci"), help="Output BCI file")
parser.add_argument("--pdf-path", default=r"C:\PATH\TO\TARGET.pdf", help="Windows path to target PDF for the BCI Open() command")
parser.add_argument("--match-column", default="Comment", help="Column in Bluebeam CSV to match against match_value")
parser.add_argument("--page-column", default="Page Index", help="Bluebeam page index column")
parser.add_argument("--id-column", default="ID", help="Bluebeam markup ID column")
parser.add_argument("--contains", action="store_true", help="Use contains match instead of exact match")
args = parser.parse_args()
bb_csv = Path(args.bb_csv)
updates_csv = Path(args.updates_csv)
out_path = Path(args.out)
OUTPUT_DIR.mkdir(exist_ok=True)
bb = pd.read_csv(bb_csv, dtype=str).fillna("")
upd = pd.read_csv(updates_csv, dtype=str).fillna("")
required_bb = {args.page_column, args.id_column, args.match_column}
missing_bb = required_bb - set(bb.columns)
if missing_bb:
raise ValueError(f"Bluebeam CSV missing required columns: {sorted(missing_bb)}")
required_upd = {"match_value"}
missing_upd = required_upd - set(upd.columns)
if missing_upd:
raise ValueError(f"Updates CSV missing required columns: {sorted(missing_upd)}")
lines = []
lines.append(f"Open('{args.pdf_path}', '')")
total_matches = 0
for _, urow in upd.iterrows():
match_value = clean_value(urow["match_value"])
if not match_value:
continue
if args.contains:
mask = bb[args.match_column].astype(str).str.contains(match_value, na=False, regex=False)
else:
mask = bb[args.match_column].astype(str).str.strip() == match_value
matches = bb[mask]
col_data = build_column_dict(urow, exclude_cols={"match_value"})
if not col_data:
continue
payload = dict_to_bci_payload(col_data)
for _, brow in matches.iterrows():
page_index = clean_value(brow[args.page_column])
markup_id = clean_value(brow[args.id_column])
if page_index == "" or markup_id == "":
continue
lines.append(f'ColumnDataSet({page_index},"{markup_id}","{payload}")')
total_matches += 1
lines.append("Save()")
lines.append("Close()")
out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"WROTE: {out_path}")
print(f"MATCHED MARKUPS: {total_matches}")
if __name__ == "__main__":
main()

36
app/update_existing.py Normal file
View File

@ -0,0 +1,36 @@
#!/usr/bin/env python3
from pathlib import Path
import fitz
BASE_DIR = Path(__file__).resolve().parents[1]
INPUT = BASE_DIR / "input" / "okular_test.pdf"
OUTPUT = BASE_DIR / "output" / "okular_test_updated.pdf"
OLD_TEXT = "TITLE TEXT HERE"
NEW_TEXT = "UPDATED TITLE TEXT\nLINE 2 UPDATED\nLINE 3 UPDATED"
doc = fitz.open(INPUT)
changed = 0
for page in doc:
for annot in page.annots() or []:
info = annot.info or {}
content = info.get("content", "")
if OLD_TEXT in content:
info["content"] = NEW_TEXT
annot.set_info(info)
# FreeText annotations need appearance regenerated
if annot.type[1] == "FreeText":
annot.update()
changed += 1
doc.save(OUTPUT, garbage=4, deflate=True)
doc.close()
print(f"Updated {changed} annotation(s)")
print(f"Wrote {OUTPUT}")