#!/usr/bin/env python import logging import os import unittest import pytest import pdfplumber logging.disable(logging.ERROR) HERE = os.path.abspath(os.path.dirname(__file__)) class Test(unittest.TestCase): @classmethod def setup_class(self): path = os.path.join(HERE, "pdfs/nics-background-checks-2015-11.pdf") self.pdf = pdfplumber.open(path) # via http://www.pdfill.com/example/pdf_drawing_new.pdf path_2 = os.path.join(HERE, "pdfs/pdffill-demo.pdf") self.pdf_2 = pdfplumber.open(path_2) @classmethod def teardown_class(self): self.pdf.close() self.pdf_2.close() def test_metadata(self): metadata = self.pdf.metadata assert isinstance(metadata["Producer"], str) def test_pagecount(self): assert len(self.pdf.pages) == 1 def test_page_number(self): assert self.pdf.pages[0].page_number == 1 assert str(self.pdf.pages[0]) == "" def test_objects(self): assert len(self.pdf.chars) assert len(self.pdf.rects) assert len(self.pdf.lines) assert len(self.pdf.rect_edges) assert len(self.pdf_2.curve_edges) # Ensure that caching is working: assert id(self.pdf._rect_edges) == id(self.pdf.rect_edges) assert id(self.pdf_2._curve_edges) == id(self.pdf_2.curve_edges) assert id(self.pdf.pages[0]._layout) == id(self.pdf.pages[0].layout) def test_annots(self): pdf = self.pdf_2 assert len(pdf.annots) assert len(pdf.hyperlinks) == 17 uri = "http://www.pdfill.com/pdf_drawing.html" assert pdf.hyperlinks[0]["uri"] == uri path = os.path.join(HERE, "pdfs/annotations.pdf") with pdfplumber.open(path) as pdf: assert len(pdf.annots) def test_annots_cropped(self): pdf = self.pdf_2 page = pdf.pages[0] assert len(page.annots) == 13 assert len(page.hyperlinks) == 1 cropped = page.crop(page.bbox) assert len(cropped.annots) == 13 assert len(cropped.hyperlinks) == 1 h0_bbox = pdfplumber.utils.obj_to_bbox(page.hyperlinks[0]) cropped = page.crop(h0_bbox) assert len(cropped.annots) == len(cropped.hyperlinks) == 1 def test_annots_rotated(self): def get_annot(filename, n=0): path = os.path.join(HERE, "pdfs", filename) with pdfplumber.open(path) as pdf: return pdf.pages[0].annots[n] a = get_annot("annotations.pdf", 3) b = get_annot("annotations-rotated-180.pdf", 3) c = get_annot("annotations-rotated-90.pdf", 3) d = get_annot("annotations-rotated-270.pdf", 3) assert ( int(a["width"]) == int(b["width"]) == int(c["height"]) == int(d["height"]) ) assert ( int(a["height"]) == int(b["height"]) == int(c["width"]) == int(d["width"]) ) assert int(a["x0"]) == int(c["top"]) == int(d["y0"]) assert int(a["x1"]) == int(c["bottom"]) == int(d["y1"]) assert int(a["top"]) == int(b["y0"]) == int(d["x0"]) assert int(a["bottom"]) == int(b["y1"]) == int(d["x1"]) def test_crop_and_filter(self): def test(obj): return obj["object_type"] == "char" bbox = (0, 0, 200, 200) original = self.pdf.pages[0] cropped = original.crop(bbox) assert id(cropped.chars) == id(cropped._objects["char"]) assert cropped.width == 200 assert len(cropped.rects) > 0 assert len(cropped.chars) < len(original.chars) within_bbox = original.within_bbox(bbox) assert len(within_bbox.chars) < len(cropped.chars) assert len(within_bbox.chars) > 0 filtered = cropped.filter(test) assert id(filtered.chars) == id(filtered._objects["char"]) assert len(filtered.rects) == 0 def test_outside_bbox(self): original = self.pdf.pages[0] outside_bbox = original.outside_bbox(original.find_tables()[0].bbox) assert outside_bbox.extract_text() == "Page 1 of 205" assert outside_bbox.bbox == original.bbox def test_relative_crop(self): page = self.pdf.pages[0] cropped = page.crop((10, 10, 40, 40)) recropped = cropped.crop((10, 15, 20, 25), relative=True) target_bbox = (20, 25, 30, 35) assert recropped.bbox == target_bbox recropped_wi = cropped.within_bbox((10, 15, 20, 25), relative=True) assert recropped_wi.bbox == target_bbox # via issue #245, should not throw error when using `relative=True` bottom = page.crop((0, 0.8 * float(page.height), page.width, page.height)) bottom.crop((0, 0, 0.5 * float(bottom.width), bottom.height), relative=True) bottom.crop( (0.5 * float(bottom.width), 0, bottom.width, bottom.height), relative=True ) # An extra test for issue #914, in which relative crops were # using the the wrong bboxes for cropping, leading to empty object-lists crop_right = page.crop((page.width / 2, 0, page.width, page.height)) crop_right_again_rel = crop_right.crop( (0, 0, crop_right.width / 2, page.height), relative=True ) assert len(crop_right_again_rel.chars) def test_invalid_crops(self): page = self.pdf.pages[0] with pytest.raises(ValueError): page.crop((0, 0, 0, 0)) with pytest.raises(ValueError): page.crop((0, 0, 10000, 10)) with pytest.raises(ValueError): page.crop((-10, 0, 10, 10)) with pytest.raises(ValueError): page.crop((100, 0, 0, 100)) with pytest.raises(ValueError): page.crop((0, 100, 100, 0)) # via issue #245 bottom = page.crop((0, 0.8 * float(page.height), page.width, page.height)) with pytest.raises(ValueError): bottom.crop((0, 0, 0.5 * float(bottom.width), bottom.height)) with pytest.raises(ValueError): bottom.crop((0.5 * float(bottom.width), 0, bottom.width, bottom.height)) # via issue #421, testing strict=True/False with pytest.raises(ValueError): page.crop((0, 0, page.width + 10, page.height + 10)) page.crop((0, 0, page.width + 10, page.height + 10), strict=False) def test_rotation(self): assert self.pdf.pages[0].width == 1008 assert self.pdf.pages[0].height == 612 path = os.path.join(HERE, "pdfs/nics-background-checks-2015-11-rotated.pdf") with pdfplumber.open(path) as rotated: assert rotated.pages[0].width == 612 assert rotated.pages[0].height == 1008 assert rotated.pages[0].cropbox != self.pdf.pages[0].cropbox assert rotated.pages[0].bbox != self.pdf.pages[0].bbox def test_password(self): path = os.path.join(HERE, "pdfs/password-example.pdf") with pdfplumber.open(path, password="test") as pdf: assert len(pdf.chars) > 0 def test_unicode_normalization(self): path = os.path.join(HERE, "pdfs/issue-905.pdf") with pdfplumber.open(path) as pdf: page = pdf.pages[0] print(page.extract_text()) assert ord(page.chars[0]["text"]) == 894 with pdfplumber.open(path, unicode_norm="NFC") as pdf: page = pdf.pages[0] assert ord(page.chars[0]["text"]) == 59 assert page.extract_text() == ";;" def test_colors(self): rect = self.pdf.pages[0].rects[0] assert rect["non_stroking_color"] == (0.8, 1, 1) def test_text_colors(self): char = self.pdf.pages[0].chars[3358] assert char["non_stroking_color"] == (1, 0, 0) def test_load_with_custom_laparams(self): # See https://github.com/jsvine/pdfplumber/issues/168 path = os.path.join(HERE, "pdfs/cupertino_usd_4-6-16.pdf") laparams = dict(line_margin=0.2) with pdfplumber.open(path, laparams=laparams) as pdf: assert round(pdf.pages[0].chars[0]["top"], 3) == 66.384 def test_loading_pathobj(self): from pathlib import Path path = os.path.join(HERE, "pdfs/nics-background-checks-2015-11.pdf") path_obj = Path(path) with pdfplumber.open(path_obj) as pdf: assert len(pdf.metadata) def test_loading_fileobj(self): path = os.path.join(HERE, "pdfs/nics-background-checks-2015-11.pdf") with open(path, "rb") as f: with pdfplumber.open(f) as pdf: assert len(pdf.metadata) assert not f.closed def test_bad_fileobj(self): path = os.path.join(HERE, "pdfs/empty.pdf") with pytest.raises(pdfplumber.utils.exceptions.PdfminerException): pdfplumber.open(path) f = open(path) with pytest.raises(pdfplumber.utils.exceptions.PdfminerException): pdfplumber.open(f) # File objects passed to pdfplumber should not be auto-closed assert not f.closed f.close() def test_uncommon_boxes(self): path = os.path.join(HERE, "pdfs/page-boxes-example.pdf") with pdfplumber.open(path) as pdf: page = pdf.pages[0] assert page.artbox == (42.51969, 70.86613999999997, 552.75591, 827.71653) assert page.bleedbox == (0, 0.0, 623.62205, 870.23622) assert page.trimbox == (28.34646, 56.69290999999998, 566.92913, 841.88976)