-
Notifications
You must be signed in to change notification settings - Fork 340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adjustments for upstream upgrades #253
Merged
Merged
Changes from 5 commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
118fef8
fixed intersection and union of fitz.Rect issue due to upgrading of p…
dothinking 08e617a
go back #194
dothinking fd52953
optimize class structure
dothinking 888f72f
downgrade PyMuPDF due to PyMuPDF/issues/3058
dothinking cadacf5
no need github page publish process for doc
dothinking 293fb7c
accommodate different behavior of Pixmap.colorspace; update test cases
dothinking 45587dd
change back to original codes and set remark
dothinking cbcbefe
check PyMuPDF version: 1.19.0<=v<=1.23.8 or v>=1.23.16
dothinking File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,3 @@ | ||
# -*- coding: utf-8 -*- | ||
|
||
'''Object with a bounding box, e.g. Block, Line, Span. | ||
|
||
Based on ``PyMuPDF``, the coordinates (e.g. bbox of ``page.get_text('rawdict')``) are generally | ||
|
@@ -49,20 +47,28 @@ def pure_rotation_matrix(cls): | |
|
||
|
||
def __init__(self, raw:dict=None, parent=None): | ||
''' Initialize Element and convert to the real (rotation considered) page coordinate system.''' | ||
''' Initialize Element and convert to the real (rotation considered) page CS.''' | ||
self.bbox = fitz.Rect() # type: fitz.Rect | ||
self._parent = parent # type: Element | ||
|
||
# NOTE: Any coordinates provided in raw is in original page CS (without considering page rotation). | ||
# NOTE: Any coordinates provided in raw is in original page CS | ||
# (without considering page rotation). | ||
if 'bbox' in (raw or {}): | ||
rect = fitz.Rect(raw['bbox']) * Element.ROTATION_MATRIX | ||
self.update_bbox(rect) | ||
|
||
|
||
def __bool__(self): | ||
'''Real object when bbox is defined.''' | ||
# NOTE inconsistent results of fitz.Rect for different version of pymupdf, e.g., | ||
# a = fitz.Rect(3,3,2,2) | ||
# bool(a) a.get_area() a.is_empty | ||
# pymupdf 1.23.5 True 1.0 True | ||
# pymupdf 1.23.8 True 0.0 True | ||
# bool(fitz.Rect())==False | ||
# NOTE: do not use `return not self.bbox.is_empty` here | ||
return bool(self.bbox) | ||
|
||
|
||
def __repr__(self): return f'{self.__class__.__name__}({tuple(self.bbox)})' | ||
|
||
|
@@ -98,18 +104,19 @@ def get_expand_bbox(self, dt:float): | |
|
||
Returns: | ||
fitz.Rect: Expanded bbox. | ||
|
||
.. note:: | ||
This method creates a new bbox, rather than changing the bbox of itself. | ||
""" | ||
""" | ||
return self.bbox + (-dt, -dt, dt, dt) | ||
|
||
|
||
def update_bbox(self, rect): | ||
'''Update current bbox to specified ``rect``. | ||
|
||
Args: | ||
rect (fitz.Rect or list): bbox-like ``(x0, y0, x1, y1)`` in real page CS (with rotation considered). | ||
rect (fitz.Rect or list): bbox-like ``(x0, y0, x1, y1)``, | ||
in real page CS (with rotation considered). | ||
''' | ||
self.bbox = fitz.Rect([round(x,1) for x in rect]) | ||
return self | ||
|
@@ -123,45 +130,44 @@ def union_bbox(self, e): | |
|
||
Returns: | ||
Element: self | ||
""" | ||
""" | ||
return self.update_bbox(self.bbox | e.bbox) | ||
|
||
|
||
# -------------------------------------------- | ||
# location relationship to other Element instance | ||
# -------------------------------------------- | ||
# -------------------------------------------- | ||
def contains(self, e:'Element', threshold:float=1.0): | ||
"""Whether given element is contained in this instance, with margin considered. | ||
|
||
Args: | ||
e (Element): Target element | ||
threshold (float, optional): Intersection rate. Defaults to 1.0. The larger, the stricter. | ||
threshold (float, optional): Intersection rate. | ||
Defaults to 1.0. The larger, the stricter. | ||
|
||
Returns: | ||
bool: [description] | ||
""" | ||
# NOTE the case bool(e)=True but e.bbox.get_area()=0 | ||
S = e.bbox.get_area() | ||
if not S: return False | ||
if not S: return False | ||
|
||
# it's not practical to set a general threshold to consider the margin, so two steps: | ||
# - set a coarse but acceptable area threshold, | ||
# - check the length in main direction strictly | ||
|
||
# A contains B => A & B = B | ||
intersection = self.bbox & e.bbox | ||
factor = round(intersection.get_area()/e.bbox.get_area(), 2) | ||
factor = round(intersection.get_area()/S, 2) | ||
if factor<threshold: return False | ||
|
||
# check length | ||
if self.bbox.width >= self.bbox.height: | ||
return self.bbox.width+constants.MINOR_DIST >= e.bbox.width | ||
else: | ||
return self.bbox.height+constants.MINOR_DIST >= e.bbox.height | ||
|
||
return self.bbox.height+constants.MINOR_DIST >= e.bbox.height | ||
|
||
|
||
def get_main_bbox(self, e, threshold:float=0.95): | ||
"""If the intersection with ``e`` exceeds the threshold, return the union of these two elements; else return None. | ||
"""If the intersection with ``e`` exceeds the threshold, return the union of | ||
these two elements; else return None. | ||
|
||
Args: | ||
e (Element): Target element. | ||
|
@@ -172,43 +178,45 @@ def get_main_bbox(self, e, threshold:float=0.95): | |
""" | ||
bbox_1 = self.bbox | ||
bbox_2 = e.bbox if hasattr(e, 'bbox') else fitz.Rect(e) | ||
|
||
# areas | ||
b = bbox_1 & bbox_2 | ||
if not b: return None # no intersection | ||
|
||
a1, a2, a = bbox_1.get_area(), bbox_2.get_area(), b.get_area() | ||
if b.is_empty: return None # no intersection | ||
|
||
# Note: if bbox_1 and bbox_2 intersects with only an edge, b is not empty but b.get_area()=0 | ||
# so give a small value when they're intersected but the area is zero | ||
a1, a2, a = bbox_1.get_area(), bbox_2.get_area(), b.get_area() | ||
factor = a/min(a1,a2) if a else 1e-6 | ||
return bbox_1 | bbox_2 if factor >= threshold else None | ||
|
||
|
||
def vertically_align_with(self, e, factor:float=0.0, text_direction:bool=True): | ||
'''Check whether two Element instances have enough intersection in vertical direction, i.e. perpendicular to reading direction. | ||
|
||
'''Check whether two Element instances have enough intersection in vertical direction, | ||
i.e. perpendicular to reading direction. | ||
|
||
Args: | ||
e (Element): Object to check with | ||
factor (float, optional): Threshold of overlap ratio, the larger it is, the higher probability the two bbox-es are aligned. | ||
text_direction (bool, optional): Consider text direction or not. True by default, from left to right if False. | ||
factor (float, optional): Threshold of overlap ratio, the larger it is, the higher | ||
probability the two bbox-es are aligned. | ||
text_direction (bool, optional): Consider text direction or not. | ||
True by default,from left to right if False. | ||
|
||
Returns: | ||
bool: [description] | ||
|
||
Examples:: | ||
|
||
+--------------+ | ||
| | | ||
+--------------+ | ||
+--------------+ | ||
L1 | ||
+-------------------+ | ||
| | | ||
+-------------------+ | ||
L2 | ||
|
||
An enough intersection is defined based on the minimum width of two boxes:: | ||
|
||
L1+L2-L>factor*min(L1,L2) | ||
''' | ||
if not e or not bool(self): return False | ||
|
@@ -225,29 +233,32 @@ def vertically_align_with(self, e, factor:float=0.0, text_direction:bool=True): | |
|
||
|
||
def horizontally_align_with(self, e, factor:float=0.0, text_direction:bool=True): | ||
'''Check whether two Element instances have enough intersection in horizontal direction, i.e. along the reading direction. | ||
|
||
'''Check whether two Element instances have enough intersection in horizontal direction, | ||
i.e. along the reading direction. | ||
|
||
Args: | ||
e (Element): Element to check with | ||
factor (float, optional): threshold of overlap ratio, the larger it is, the higher probability the two bbox-es are aligned. | ||
text_direction (bool, optional): consider text direction or not. True by default, from left to right if False. | ||
factor (float, optional): threshold of overlap ratio, the larger it is, the higher | ||
probability the two bbox-es are aligned. | ||
text_direction (bool, optional): consider text direction or not. | ||
True by default, from left to right if False. | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See previous comment. |
||
Examples:: | ||
|
||
+--------------+ | ||
| | L1 +--------------------+ | ||
+--------------+ | | L2 | ||
+--------------------+ | ||
|
||
An enough intersection is defined based on the minimum width of two boxes:: | ||
|
||
L1+L2-L>factor*min(L1,L2) | ||
''' | ||
if not e or not bool(self): return False | ||
|
||
# text direction | ||
idx = 0 if text_direction and self.is_vertical_text else 1 | ||
|
||
L1 = self.bbox[idx+2]-self.bbox[idx] | ||
L2 = e.bbox[idx+2]-e.bbox[idx] | ||
L = max(self.bbox[idx+2], e.bbox[idx+2]) - min(self.bbox[idx], e.bbox[idx]) | ||
|
@@ -257,21 +268,19 @@ def horizontally_align_with(self, e, factor:float=0.0, text_direction:bool=True) | |
|
||
|
||
def in_same_row(self, e): | ||
"""Check whether in same row/line with specified Element instance. With text direction considered. | ||
|
||
"""Check whether in same row/line with specified Element instance. | ||
With text direction considered. | ||
|
||
Taking horizontal text as an example: | ||
|
||
* yes: the bottom edge of each box is lower than the centerline of the other one; | ||
* otherwise, not in same row. | ||
|
||
Args: | ||
e (Element): Target object. | ||
|
||
Returns: | ||
bool: [description] | ||
|
||
.. note:: | ||
The difference to method ``horizontally_align_with``: they may not in same line, though | ||
The difference to method ``horizontally_align_with``: they may not in same line, though | ||
aligned horizontally. | ||
""" | ||
if not e or self.is_horizontal_text != e.is_horizontal_text: | ||
|
@@ -291,9 +300,15 @@ def in_same_row(self, e): | |
# ------------------------------------------------ | ||
def store(self): | ||
'''Store properties in raw dict.''' | ||
return { 'bbox': tuple([x for x in self.bbox]) } | ||
return { 'bbox': tuple(x for x in self.bbox) } | ||
|
||
|
||
|
||
def plot(self, page, stroke:tuple=(0,0,0), width:float=0.5, fill:tuple=None, dashes:str=None): | ||
'''Plot bbox in PDF page for debug purpose.''' | ||
page.draw_rect(self.bbox, color=stroke, fill=fill, width=width, dashes=dashes, overlay=False, fill_opacity=0.5) | ||
page.draw_rect(self.bbox, | ||
color=stroke, | ||
fill=fill, | ||
width=width, | ||
dashes=dashes, | ||
overlay=False, | ||
fill_opacity=0.5) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't really understand what this means "True by default, from left to right if False".
So if the
text_direction
isTrue
then we do consider text direction - okay, but if it isFalse
then we don't consider text directions, however when I read this it seems likeFalse
means we consider a left to right text direction as it says "from left to right if False". I'm confused!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't read any contradictions between your thoughts -> don't consider text directions -> ignore real text directions -> use default text directions -> the most common case, horizontal, i.e., from left to right.
Appreciated if you help a precise wording.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per my understanding, don't consider text directions, means to use default text direction, which is from left to right.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suppose the bit I don't understand then is if
True
then what is the text direction? Basically:False
= from left to rightTrue
= ?Also if we are
False
then we do consider the text direction don't we (left to right)? Which is whytext_direction (bool, optional): Consider text direction or not.
confuses me!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I probably understand your confusion. When
False
, the wordsfrom left to right
does not mean to text direction, but the default direction for "horizontal". I should just keepTrue by default
and delete the rest.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay - yes please just delete to avoid the confusion.