feat: add --structure-text, like pdfinfo -struct-text (but better)

This commit is contained in:
David Huggins-Daines
2023-08-10 13:39:13 -04:00
parent 3c83366d9d
commit 183d5a840b
2 changed files with 152 additions and 4 deletions
+38 -4
View File
@@ -2,8 +2,9 @@
import argparse
import json
import sys
from collections import defaultdict, deque
from itertools import chain
from typing import List
from typing import Any, DefaultDict, Dict, List
from .pdf import PDF
@@ -22,13 +23,20 @@ def parse_args(args_raw: List[str]) -> argparse.Namespace:
parser.add_argument(
"infile", nargs="?", type=argparse.FileType("rb"), default=sys.stdin.buffer
)
parser.add_argument(
group = parser.add_mutually_exclusive_group()
group.add_argument(
"--structure",
help="Write the structure tree as JSON. "
"All arguments except --pages, --laparams, and --indent will be ignored",
"All other arguments except --pages, --laparams, and --indent will be ignored",
action="store_true",
)
group.add_argument(
"--structure-text",
help="Write the structure tree as JSON including text contents. "
"All other arguments except --pages, --laparams, and --indent will be ignored",
action="store_true",
)
parser.add_argument("--format", choices=["csv", "json"], default="csv")
parser.add_argument("--types", nargs="+")
@@ -61,12 +69,38 @@ def parse_args(args_raw: List[str]) -> argparse.Namespace:
return args
def add_text_to_mcids(pdf: PDF, data: List[Dict[str, Any]]) -> None:
page_contents: DefaultDict[int, Any] = defaultdict(lambda: defaultdict(str))
for page in pdf.pages:
text_contents = page_contents[page.page_number]
for c in page.chars:
mcid = c.get("mcid")
if mcid is None:
continue
text_contents[mcid] += c["text"]
d = deque(data)
while d:
el = d.popleft()
if "children" in el:
d.extend(el["children"])
pageno = el.get("page_number")
if pageno is None:
continue
text_contents = page_contents[pageno]
if "mcids" in el:
el["text"] = [text_contents[mcid] for mcid in el["mcids"]]
def main(args_raw: List[str] = sys.argv[1:]) -> None:
args = parse_args(args_raw)
with PDF.open(args.infile, pages=args.pages, laparams=args.laparams) as pdf:
if args.structure:
json.dump(pdf.structure_tree, sys.stdout, indent=args.indent)
elif args.structure_text:
tree = pdf.structure_tree
add_text_to_mcids(pdf, tree)
json.dump(tree, sys.stdout, indent=args.indent, ensure_ascii=False)
elif args.format == "csv":
pdf.to_csv(
sys.stdout,
+114
View File
@@ -16,6 +16,114 @@ logging.disable(logging.ERROR)
HERE = os.path.abspath(os.path.dirname(__file__))
SCOTUS_TEXT = [
{
"type": "Div",
"children": [
{
"type": "P",
"page_number": 1,
"attributes": {
"LineHeight": 25.75,
"TextIndent": 21.625,
"O": "Layout",
},
"mcids": [1],
"text": [
"IN THE SUPREME COURT OF THE UNITED STATES - - - - - - - - - - - - "
"- - - - - x MICHAEL A. KNOWLES, : WARDEN, :"
],
},
{
"type": "P",
"page_number": 1,
"attributes": {
"LineHeight": 25.75,
"StartIndent": 86.375,
"O": "Layout",
},
"mcids": [2],
"text": [" Petitioner :"],
},
{
"type": "P",
"page_number": 1,
"attributes": {
"LineHeight": 25.75,
"TextIndent": 50.375,
"O": "Layout",
},
"mcids": [3, 4],
"text": [
" v. ",
": No. 07-1315 ALEXANDRE MIRZAYANCE. : - - - - - - - - - - - - - -"
" - - - x",
],
},
{
"type": "P",
"page_number": 1,
"attributes": {
"O": "Layout",
"SpaceAfter": 24.5,
"LineHeight": 25.75,
"StartIndent": 165.625,
"EndIndent": 57.625,
},
"mcids": [5],
"text": [" Washington, D.C. Tuesday, January 13, 2009"],
},
{
"type": "P",
"page_number": 1,
"attributes": {
"LineHeight": 25.75,
"TextIndent": 100.75,
"O": "Layout",
},
"mcids": [6],
"text": [
" The above-entitled matter came on for oral argument before the "
"Supreme Court of the United States at 1:01 p.m. APPEARANCES: "
"STEVEN E. MERCER, ESQ., Deputy Attorney General, Los"
],
},
{
"type": "P",
"page_number": 1,
"attributes": {
"O": "Layout",
"SpaceAfter": 179.125,
"LineHeight": 25.75,
"TextIndent": 21.625,
"EndIndent": 50.375,
"TextAlign": "None",
},
"mcids": [7],
"text": [
" Angeles, Cal.; on behalf of the Petitioner. CHARLES M. SEVILLA, "
"ESQ., San Diego, Cal.; on behalf of the Respondent. "
],
},
{
"type": "P",
"page_number": 1,
"attributes": {"O": "Layout", "TextAlign": "Center", "SpaceAfter": 8.5},
"mcids": [8],
"text": ["1\n"],
},
{
"type": "P",
"page_number": 1,
"attributes": {"O": "Layout", "TextAlign": "Center"},
"mcids": [9],
"text": ["Alderson Reporting Company "],
},
],
}
]
def run(cmd):
return Popen(cmd, stdout=PIPE).communicate()[0]
@@ -89,6 +197,12 @@ class Test(unittest.TestCase):
# lol no structure
assert c == []
def test_cli_structure_text(self):
path = os.path.join(HERE, "pdfs/scotus-transcript-p1.pdf")
res = run([sys.executable, "-m", "pdfplumber.cli", path, "--structure-text"])
c = json.loads(res)
assert c == SCOTUS_TEXT
def test_cli_json(self):
res = run(
[