update eaf package

This commit is contained in:
2021-01-30 14:52:51 +01:00
parent 84eb4929ee
commit 5207af83cb
4981 changed files with 11795 additions and 761570 deletions

View File

@@ -20,10 +20,11 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from PyQt5 import QtCore
from PyQt5.QtCore import Qt, QRect, QEvent
from PyQt5.QtCore import Qt, QRect, QEvent, QTimer, QFileSystemWatcher
from PyQt5.QtGui import QColor, QPixmap, QImage, QFont, QCursor
from PyQt5.QtGui import QPainter
from PyQt5.QtWidgets import QWidget
from PyQt5.QtWidgets import QToolTip
from core.buffer import Buffer
from core.utils import touch, interactive
import fitz
@@ -32,6 +33,7 @@ import random
import math
import os
import hashlib
import json
class AppBuffer(Buffer):
def __init__(self, buffer_id, url, config_dir, arguments, emacs_var_dict, module_path):
@@ -53,18 +55,18 @@ class AppBuffer(Buffer):
def get_table_file(self):
return self.buffer_widget.table_file_path
def handle_input_message(self, result_type, result_content):
if result_type == "jump_page":
def handle_input_response(self, callback_tag, result_content):
if callback_tag == "jump_page":
self.buffer_widget.jump_to_page(int(result_content))
elif result_type == "jump_percent":
elif callback_tag == "jump_percent":
self.buffer_widget.jump_to_percent(int(result_content))
elif result_type == "jump_link":
elif callback_tag == "jump_link":
self.buffer_widget.jump_to_link(str(result_content))
elif result_type == "search_text":
elif callback_tag == "search_text":
self.buffer_widget.search_text(str(result_content))
def cancel_input_message(self, result_type):
if result_type == "jump_link":
def cancel_input_response(self, callback_tag):
if callback_tag == "jump_link":
self.buffer_widget.cleanup_links()
def scroll_other_buffer(self, scroll_direction, scroll_type):
@@ -80,17 +82,24 @@ class AppBuffer(Buffer):
self.scroll_down()
def save_session_data(self):
return "{0}:{1}:{2}:{3}".format(self.buffer_widget.scroll_offset,
return "{0}:{1}:{2}:{3}:{4}".format(self.buffer_widget.scroll_offset,
self.buffer_widget.scale,
self.buffer_widget.read_mode,
self.buffer_widget.inverted_mode)
self.buffer_widget.inverted_mode,
self.buffer_widget.rotation)
def restore_session_data(self, session_data):
(scroll_offset, scale, read_mode, inverted_mode) = session_data.split(":")
(scroll_offset, scale, read_mode, inverted_mode, rotation) = ("", "", "", "", "0")
if session_data.count(":") == 3:
(scroll_offset, scale, read_mode, inverted_mode) = session_data.split(":")
else:
(scroll_offset, scale, read_mode, inverted_mode, rotation) = session_data.split(":")
self.buffer_widget.scroll_offset = float(scroll_offset)
self.buffer_widget.scale = float(scale)
self.buffer_widget.read_mode = read_mode
self.buffer_widget.inverted_mode = inverted_mode == "True"
self.buffer_widget.rotation = int(rotation)
if self.emacs_var_dict["eaf-pdf-dark-mode"] == "ignore":
self.buffer_widget.inverted_mode = inverted_mode == "True"
self.buffer_widget.update()
def jump_to_page(self):
@@ -103,6 +112,10 @@ class AppBuffer(Buffer):
def jump_to_percent(self):
self.send_input_message("Jump to Percent: ", "jump_percent")
def jump_to_percent_with_num(self, percent):
self.buffer_widget.jump_to_percent(float(percent))
return ""
def jump_to_link(self):
self.buffer_widget.add_mark_jump_link_tips()
self.send_input_message("Jump to Link: ", "jump_link")
@@ -130,15 +143,21 @@ class AppBuffer(Buffer):
def copy_select(self):
if self.buffer_widget.is_select_mode:
content = self.buffer_widget.parse_select_char_list()
self.eval_in_emacs.emit('''(kill-new "{}")'''.format(content))
self.eval_in_emacs.emit('kill-new', [content])
self.message_to_emacs.emit(content)
self.buffer_widget.cleanup_select()
else:
self.message_to_emacs.emit("Cannot copy, you should double click your mouse and hover through the text on the PDF. Don't click and drag!")
def page_total_number(self):
return str(self.buffer_widget.page_total_number)
def current_page(self):
return str(self.buffer_widget.get_start_page_index() + 1)
def current_percent(self):
return str(self.buffer_widget.current_percent())
def add_annot_highlight(self):
if self.buffer_widget.is_select_mode:
self.buffer_widget.annot_select_char_area("highlight")
@@ -162,12 +181,16 @@ class AppBuffer(Buffer):
self.buffer_widget.get_focus_text.emit(self.buffer_id, "")
elif self.buffer_widget.is_hover_annot:
self.buffer_widget.annot_handler("edit")
else:
self.buffer_widget.enable_free_text_annot_mode()
def set_focus_text(self, new_text):
if self.buffer_widget.is_select_mode:
self.buffer_widget.annot_select_char_area("text", new_text)
elif self.buffer_widget.is_hover_annot:
self.buffer_widget.update_annot_text(new_text)
else:
self.buffer_widget.annot_free_text_annot(new_text)
def get_toc(self):
result = ""
@@ -176,6 +199,30 @@ class AppBuffer(Buffer):
result += "{0}{1} {2}\n".format("".join(" " * (line[0] - 1)), line[1], line[2])
return result
def get_annots(self, page_index):
'''
Return a list of annotations on page_index of types.
'''
# Notes: annots need the pymupdf above 1.16.4 version.
annots = self.buffer_widget.get_annots(int(page_index))
result = {}
for annot in annots:
id = annot.info["id"]
rect = annot.rect
type = annot.type
if len(type) != 2:
continue
result[id] = {"page": page_index, "type_int": type[0], "type_name": type[1], "rect": "%s:%s:%s:%s" %(rect.x0, rect.y0, rect.x1, rect.y1)}
return json.dumps(result)
def jump_to_rect(self, page_index, rect):
arr = rect.split(":")
if len(arr) != 4:
return ""
rect = fitz.Rect(float(arr[0]), float(arr[1]), float(arr[2]), float(arr[3]))
self.buffer_widget.jump_to_rect(int(page_index), rect)
return ""
class PdfViewerWidget(QWidget):
translate_double_click_word = QtCore.pyqtSignal(str)
get_focus_text = QtCore.pyqtSignal(str, str)
@@ -198,19 +245,32 @@ class PdfViewerWidget(QWidget):
self.first_pixmap = self.document.getPagePixmap(0)
self.page_width = self.first_pixmap.width
self.page_height = self.first_pixmap.height
self.original_page_width = self.page_width
self.original_page_height = self.page_height
self.page_total_number = self.document.pageCount
# Init scale and scale mode.
self.scale = 1.0
self.read_mode = "fit_to_width"
self.rotation = 0
# Simple string comparation.
if (self.emacs_var_dict["eaf-pdf-default-zoom"] != "1.0"):
self.read_mode = "fit_to_customize"
self.scale = float(self.emacs_var_dict["eaf-pdf-default-zoom"])
self.horizontal_offset = 0
# Inverted mode.
self.inverted_mode = False
if (self.emacs_var_dict["eaf-pdf-dark-mode"] == "true" or \
(self.emacs_var_dict["eaf-pdf-dark-mode"] == "" and self.emacs_var_dict["eaf-emacs-theme-mode"] == "dark")):
((self.emacs_var_dict["eaf-pdf-dark-mode"] == "follow" or self.emacs_var_dict["eaf-pdf-dark-mode"] == "ignore") and \
self.emacs_var_dict["eaf-emacs-theme-mode"] == "dark")):
self.inverted_mode = True
# Inverted mode exclude image.
self.inverted_mode_exclude_image = self.emacs_var_dict["eaf-pdf-dark-exclude-image"] == "true"
# mark link
self.is_mark_link = False
self.mark_link_annot_cache_dict = {}
@@ -237,21 +297,34 @@ class PdfViewerWidget(QWidget):
# annot
self.is_hover_annot = False
self.free_text_annot_timer = QTimer()
self.free_text_annot_timer.setInterval(300)
self.free_text_annot_timer.setSingleShot(True)
self.free_text_annot_timer.timeout.connect(self.handle_free_text_annot_mode)
self.is_free_text_annot_mode = False
self.free_text_annot_pos = (None, None)
self.edited_page_annot = (None, None)
# Init scroll attributes.
self.scroll_step = 20
self.scroll_offset = 0
self.mouse_scroll_offset = 20
self.scroll_ratio = 0.05
self.scroll_wheel_lasttime = time.time()
if self.emacs_var_dict["eaf-pdf-scroll-ratio"] != "0.05":
self.scroll_ratio = float(self.emacs_var_dict["eaf-pdf-scroll-ratio"])
# Default presentation mode
self.presentation_mode = False
# Padding between pages.
self.page_padding = 10
# Init font.
self.page_annotate_height = 22
self.page_annotate_padding_right = 10
self.page_annotate_padding_bottom = 10
self.page_annotate_light_color = QColor("#333333")
self.page_annotate_dark_color = QColor("#999999")
self.page_annotate_light_color = QColor(self.emacs_var_dict["eaf-emacs-theme-foreground-color"])
self.page_annotate_dark_color = QColor(1-QColor(self.emacs_var_dict["eaf-emacs-theme-foreground-color"]).redF(),\
1-QColor(self.emacs_var_dict["eaf-emacs-theme-foreground-color"]).greenF(),\
1-QColor(self.emacs_var_dict["eaf-emacs-theme-foreground-color"]).blueF())
self.font = QFont()
self.font.setPointSize(12)
@@ -267,15 +340,82 @@ class PdfViewerWidget(QWidget):
self.remember_offset = None
self.last_hover_annot_id = None
# To avoid 'PDF only' method errors
self.inpdf = True
if os.path.splitext(self.url)[-1] != ".pdf":
self.inpdf = False
self.refresh_file()
def handle_color(self,color,inverted=False):
r = float(color.redF())
g = float(color.greenF())
b = float(color.blueF())
if inverted:
r = 1.0-r
g = 1.0-g
b = 1.0-b
return (r,g,b)
def repeat_to_length(self, string_to_expand, length):
return (string_to_expand * (int(length/len(string_to_expand))+1))[:length]
@interactive()
def handle_file_changed(self, path):
'''
Use the QFileSystemWatcher watch file changed. If the watch file have been remove or rename,
this watch will auto remove.
'''
if path == self.url:
try:
# Some program will generate `middle` file, but file already changed, fitz try to
# open the `middle` file caused error.
time.sleep(0.1)
self.document = fitz.open(path)
except:
return
self.buffer.message_to_emacs.emit("Detected that %s has been changed. Refreshing buffer..." %path)
self.page_cache_pixmap_dict.clear()
self.update()
# if the file have been renew save, file_changed_watcher will remove the path form monitor list.
if len(self.file_changed_wacher.files()) == 0 :
self.file_changed_wacher.addPath(self.url)
def refresh_file(self):
'''
Refresh content with PDF file changed.
'''
self.file_changed_wacher = QFileSystemWatcher()
if self.file_changed_wacher.addPath(self.url):
self.file_changed_wacher.fileChanged.connect(self.handle_file_changed)
@interactive
def toggle_presentation_mode(self):
'''
Toggle presentation mode.
'''
self.presentation_mode = not self.presentation_mode
if self.presentation_mode:
# Make current page fill the view.
self.zoom_reset("fit_to_height")
self.jump_to_page(self.get_start_page_index() + 1)
self.buffer.message_to_emacs.emit("Presentation Mode.")
else:
self.buffer.message_to_emacs.emit("Continuous Mode.")
@property
def scroll_step(self):
return self.rect().height() if self.presentation_mode else self.rect().size().height() * self.scroll_ratio
@interactive
def save_current_pos(self):
self.remember_offset = self.scroll_offset
self.buffer.message_to_emacs.emit("Saved current position.")
@interactive()
@interactive
def jump_to_saved_pos(self):
if self.remember_offset is None:
self.buffer.message_to_emacs.emit("Cannot jump from this position.")
@@ -286,7 +426,7 @@ class PdfViewerWidget(QWidget):
self.remember_offset = current_scroll_offset
self.buffer.message_to_emacs.emit("Jumped to saved position.")
def get_page_pixmap(self, index, scale):
def get_page_pixmap(self, index, scale, rotation=0):
# Just return cache pixmap when found match index and scale in cache dict.
if self.page_cache_scale == scale:
if index in self.page_cache_pixmap_dict.keys():
@@ -295,12 +435,22 @@ class PdfViewerWidget(QWidget):
else:
self.page_cache_pixmap_dict.clear()
self.page_cache_scale = scale
self.page_cache_trans = fitz.Matrix(scale, scale)
page = self.document[index]
if self.inpdf:
page.setRotation(rotation)
if self.is_mark_link:
page = self.add_mark_link(index)
if rotation % 180 != 0:
self.page_width = self.original_page_height
self.page_height = self.original_page_width
else:
self.page_width = self.original_page_width
self.page_height = self.original_page_height
scale = scale * self.page_width / page.rect.width
# follow page search text
if self.is_mark_search:
page = self.add_mark_search_text(page, index)
@@ -310,23 +460,86 @@ class PdfViewerWidget(QWidget):
self.char_dict[index] = self.get_page_char_rect_list(index)
self.select_area_annot_cache_dict[index] = None
trans = self.page_cache_trans if self.page_cache_trans is not None else fitz.Matrix(scale, scale)
pixmap = page.getPixmap(matrix=trans, alpha=False)
if self.emacs_var_dict["eaf-pdf-dark-mode"] == "follow" and self.inpdf:
col = self.handle_color(QColor(self.emacs_var_dict["eaf-emacs-theme-background-color"]), self.inverted_mode)
page.drawRect(page.CropBox, color=col, fill=col, overlay=False)
pixmap = page.getPixmap(matrix=fitz.Matrix(scale, scale), alpha=False)
if self.inverted_mode:
pixmap.invertIRect(pixmap.irect)
# exclude images
imagelist = page.getImageList()
for image in imagelist:
if self.inverted_mode_exclude_image:
# Exclude images
imagelist = None
try:
# image[7] is the name of the picture
imagerect = page.getImageBbox(image[7])
if imagerect.isInfinite or imagerect.isEmpty:
continue
pixmap.invertIRect(imagerect * self.scale)
imagelist = page.getImageList(full=True)
except Exception:
pass
# PyMupdf 1.14 not include argument 'full'.
imagelist = page.getImageList()
imagebboxlist = []
for image in imagelist:
try:
imagerect = page.getImageBbox(image)
if imagerect.isInfinite or imagerect.isEmpty:
continue
else:
imagebboxlist.append(imagerect)
except Exception:
pass
newly_added_overlapbboxlist = imagebboxlist
# Nth time of loop represents N+1 rectanges' intesects' overlaps
time = 0
while len(newly_added_overlapbboxlist) > 1:
temp_overlapbboxlist = []
time += 1
# calculate overlap
for i in range(len(newly_added_overlapbboxlist)):
for j in range(i+1,len(newly_added_overlapbboxlist)):
x0a = newly_added_overlapbboxlist[i].x0
y0a = newly_added_overlapbboxlist[i].y0
x1a = newly_added_overlapbboxlist[i].x1
y1a = newly_added_overlapbboxlist[i].y1
x0b = newly_added_overlapbboxlist[j].x0
y0b = newly_added_overlapbboxlist[j].y0
x1b = newly_added_overlapbboxlist[j].x1
y1b = newly_added_overlapbboxlist[j].y1
x0c = max(x0a,x0b)
y0c = max(y0a,y0b)
x1c = min(x1a,x1b)
y1c = min(y1a,y1b)
if x0c < x1c and y0c < y1c:
temp_overlapbboxlist.append(fitz.Rect(x0c,y0c,x1c,y1c))
# remove duplicate overlaps for one time
for item in set(temp_overlapbboxlist):
if temp_overlapbboxlist.count(item) % 2 == 0:
while item in temp_overlapbboxlist:
temp_overlapbboxlist.remove(item)
else:
while temp_overlapbboxlist.count(item) > 1:
temp_overlapbboxlist.remove(item)
newly_added_overlapbboxlist = temp_overlapbboxlist
imagebboxlist.extend(newly_added_overlapbboxlist)
if time%2 == 1 and time//2 > 0:
imagebboxlist.extend(newly_added_overlapbboxlist)
if len(imagebboxlist) != len(set(imagebboxlist)):
# remove duplicate to make it run faster
for item in set(imagebboxlist):
if imagebboxlist.count(item) % 2 == 0:
while item in imagebboxlist:
imagebboxlist.remove(item)
else:
while imagebboxlist.count(item) > 1:
imagebboxlist.remove(item)
for bbox in imagebboxlist:
if self.inpdf:
pixmap.invertIRect(bbox * page.rotationMatrix * scale)
else:
pixmap.invertIRect(bbox * scale)
img = QImage(pixmap.samples, pixmap.width, pixmap.height, pixmap.stride, QImage.Format_RGB888)
qpixmap = QPixmap.fromImage(img)
@@ -376,16 +589,17 @@ class PdfViewerWidget(QWidget):
painter.translate(0, translate_y)
# Render pages in visible area.
render_x = 0
render_y = 0
for index in list(range(start_page_index, last_page_index)):
if index < self.page_total_number:
# Get page image.
qpixmap = self.get_page_pixmap(index, self.scale)
qpixmap = self.get_page_pixmap(index, self.scale, self.rotation)
# Init render rect.
render_width = self.page_width * self.scale
render_height = self.page_height * self.scale
render_width = qpixmap.width()
render_height = qpixmap.height()
render_x = (self.rect().width() - render_width) / 2
render_y = (index - start_page_index) * self.scale * self.page_height
# Add padding between pages.
if (index - start_page_index) > 0:
@@ -394,8 +608,11 @@ class PdfViewerWidget(QWidget):
# Draw page image.
if self.read_mode == "fit_to_customize" and render_width >= self.rect().width():
render_x = max(min(render_x + self.horizontal_offset, 0), self.rect().width() - render_width) # limit the visiable area size
painter.drawPixmap(QRect(render_x, render_y, render_width, render_height), qpixmap)
render_y += render_height
# Clean unused pixmap cache that avoid use too much memory.
self.clean_unused_page_cache_pixmap()
@@ -409,12 +626,15 @@ class PdfViewerWidget(QWidget):
else:
painter.setPen(self.page_annotate_light_color)
# Draw progress.
progress_percent = int((start_page_index + 1) * 100 / self.page_total_number)
current_page = start_page_index + 1
painter.drawText(QRect(self.rect().x(),
self.rect().y() + self.rect().height() - self.page_annotate_height - self.page_annotate_padding_bottom,
self.rect().y(),
self.rect().width() - self.page_annotate_padding_right,
self.page_annotate_height),
Qt.AlignRight,
"{0}% ({1}/{2})".format(int((start_page_index + 1) * 100 / self.page_total_number), start_page_index + 1, self.page_total_number))
self.rect().height() - self.page_annotate_padding_bottom),
Qt.AlignRight | Qt.AlignBottom,
"{0}% ({1}/{2})".format(progress_percent, current_page, self.page_total_number))
def build_context_wrap(f):
def wrapper(*args):
@@ -443,9 +663,21 @@ class PdfViewerWidget(QWidget):
def wheelEvent(self, event):
if not event.accept():
if event.angleDelta().y():
self.update_vertical_offset(max(min(self.scroll_offset - self.scale * event.angleDelta().y() / 120 * self.mouse_scroll_offset, self.max_scroll_offset()), 0))
numSteps = event.angleDelta().y()
if self.presentation_mode:
# page scrolling
curtime = time.time()
if curtime - self.scroll_wheel_lasttime > 0.1:
numSteps = 1 if numSteps > 0 else -1
self.scroll_wheel_lasttime = curtime
else:
numSteps = 0
else:
# fixed pixel scrolling
numSteps = numSteps / 120
self.update_vertical_offset(max(min(self.scroll_offset - numSteps * self.scroll_step, self.max_scroll_offset()), 0))
if event.angleDelta().x():
new_pos = (self.horizontal_offset + self.scale * event.angleDelta().x() / 120 * self.mouse_scroll_offset)
new_pos = (self.horizontal_offset + event.angleDelta().x() / 120 * self.scroll_step)
max_pos = (self.page_width * self.scale - self.rect().width())
self.update_horizontal_offset(max(min(new_pos , max_pos), -max_pos))
@@ -464,7 +696,7 @@ class PdfViewerWidget(QWidget):
last_page_index = min(self.page_total_number, self.get_last_page_index() + 1)
for index in list(range(start_page_index, last_page_index)):
self.get_page_pixmap(index, self.scale)
self.get_page_pixmap(index, self.scale, self.rotation)
def scale_to(self, new_scale):
self.scroll_offset = new_scale * 1.0 / self.scale * self.scroll_offset
@@ -485,7 +717,7 @@ class PdfViewerWidget(QWidget):
def max_scroll_offset(self):
return self.scale * self.page_height * self.page_total_number - self.rect().height()
@interactive()
@interactive
def toggle_read_mode(self):
if self.read_mode == "fit_to_customize":
self.read_mode = "fit_to_width"
@@ -497,41 +729,41 @@ class PdfViewerWidget(QWidget):
self.update_scale()
self.update()
@interactive()
@interactive
def scroll_up(self):
self.update_vertical_offset(min(self.scroll_offset + self.scale * self.scroll_step, self.max_scroll_offset()))
self.update_vertical_offset(min(self.scroll_offset + self.scroll_step, self.max_scroll_offset()))
@interactive()
@interactive
def scroll_down(self):
self.update_vertical_offset(max(self.scroll_offset - self.scale * self.scroll_step, 0))
self.update_vertical_offset(max(self.scroll_offset - self.scroll_step, 0))
@interactive()
@interactive
def scroll_right(self):
self.update_horizontal_offset(max(self.horizontal_offset - self.scale * 30, (self.rect().width() - self.page_width * self.scale) / 2))
self.update_horizontal_offset(max(self.horizontal_offset - self.scroll_step, (self.rect().width() - self.page_width * self.scale) / 2))
@interactive()
@interactive
def scroll_left(self):
self.update_horizontal_offset(min(self.horizontal_offset + (self.scale * 30), (self.page_width * self.scale - self.rect().width()) / 2))
self.update_horizontal_offset(min(self.horizontal_offset + self.scroll_step, (self.page_width * self.scale - self.rect().width()) / 2))
@interactive()
@interactive
def scroll_up_page(self):
# Adjust scroll step to make users continue reading fluently.
self.update_vertical_offset(min(self.scroll_offset + self.rect().height() - self.scroll_step, self.max_scroll_offset()))
@interactive()
@interactive
def scroll_down_page(self):
# Adjust scroll step to make users continue reading fluently.
self.update_vertical_offset(max(self.scroll_offset - self.rect().height() + self.scroll_step, 0))
@interactive()
@interactive
def scroll_to_begin(self):
self.update_vertical_offset(0)
@interactive()
@interactive
def scroll_to_end(self):
self.update_vertical_offset(self.max_scroll_offset())
@interactive()
@interactive
def zoom_in(self):
if self.is_mark_search:
self.cleanup_search()
@@ -539,7 +771,7 @@ class PdfViewerWidget(QWidget):
self.scale_to(min(10, self.scale + 0.2))
self.update()
@interactive()
@interactive
def zoom_out(self):
if self.is_mark_search:
self.cleanup_search()
@@ -547,26 +779,32 @@ class PdfViewerWidget(QWidget):
self.scale_to(max(1, self.scale - 0.2))
self.update()
@interactive()
def zoom_reset(self):
@interactive
def zoom_reset(self, read_mode="fit_to_width"):
if self.is_mark_search:
self.cleanup_search()
self.read_mode = "fit_to_width"
self.read_mode = read_mode
self.update_scale()
self.update()
@interactive()
@interactive
def toggle_inverted_mode(self):
# Need clear page cache first, otherwise current page will not inverted until next page.
self.page_cache_pixmap_dict.clear()
# Toggle inverted status.
self.inverted_mode = not self.inverted_mode
if self.inverted_mode and self.inverted_mode_exclude_image:
self.inverted_mode_exclude_image = False
elif self.inverted_mode:
self.inverted_mode = False
else:
self.inverted_mode_exclude_image = True
self.inverted_mode = True
# Re-render page.
self.update()
@interactive()
@interactive
def toggle_mark_link(self): # mark_link will add underline mark on link, using prompt link position.
if self.is_mark_link:
self.cleanup_mark_link()
@@ -576,6 +814,32 @@ class PdfViewerWidget(QWidget):
self.page_cache_pixmap_dict.clear()
self.update()
@interactive
def rotate_clockwise(self):
if self.inpdf:
self.rotation = (self.rotation + 90) % 360
# Need clear page cache first, otherwise current page will not inverted until next page.
self.page_cache_pixmap_dict.clear()
self.update_scale()
self.update()
else:
self.buffer.message_to_emacs.emit("Only support PDF!")
@interactive
def rotate_counterclockwise(self):
if self.inpdf:
self.rotation = (self.rotation - 90) % 360
# Need clear page cache first, otherwise current page will not inverted until next page.
self.page_cache_pixmap_dict.clear()
self.update_scale()
self.update()
else:
self.buffer.message_to_emacs.emit("Only support PDF!")
def add_mark_link(self, index):
annot_list = []
page = self.document[index]
@@ -824,6 +1088,17 @@ class PdfViewerWidget(QWidget):
self.document.saveIncr()
self.select_area_annot_quad_cache_dict.clear()
def annot_free_text_annot(self, text=None):
(point, page_index) = self.free_text_annot_pos
if point == None or page_index == None:
return
page = self.document[page_index]
new_annot = page.addTextAnnot(point, text, icon="Note")
new_annot.parent = page
self.save_annot()
def cleanup_select(self):
self.is_select_mode = False
self.delete_all_mark_select_area()
@@ -857,7 +1132,14 @@ class PdfViewerWidget(QWidget):
# if only one char selected.
line_rect_list.append(bbox_list[0])
line_rect_list = list(map(lambda x: fitz.Rect(x), line_rect_list))
def check_rect(rect):
tl_x, tl_y, br_x, br_y = rect
if tl_x <= br_x and tl_y <= br_y:
return fitz.Rect(rect)
# discard the illegal rect. return a micro rect
return fitz.Rect(tl_x, tl_y, tl_x+1, tl_y+1)
line_rect_list = list(map(check_rect, line_rect_list))
page = self.document[page_index]
old_annot = self.select_area_annot_cache_dict[page_index]
@@ -887,31 +1169,60 @@ class PdfViewerWidget(QWidget):
self.start_char_page_index = None
self.start_char_rect_index = None
def hover_annot(self):
ex, ey, page_index = self.get_cursor_absolute_position()
def get_annots(self, page_index, types=None):
'''
Return a list of annotations on page_index of types.
'''
# Notes: annots need the pymupdf above 1.16.4 version.
page = self.document[page_index]
annot = page.firstAnnot
if not annot:
return None, None
return page.annots(types)
annots = []
while annot:
annots.append(annot)
annot = annot.next
def hover_annot(self):
try:
ex, ey, page_index = self.get_cursor_absolute_position()
page = self.document[page_index]
annot = page.firstAnnot
if not annot:
return None, None
for annot in annots:
if fitz.Point(ex, ey) in annot.rect:
self.is_hover_annot = True
annot.setOpacity(0.5)
self.buffer.message_to_emacs.emit("[d]Delete Annot [e]Edit Annot")
annots = []
while annot:
annots.append(annot)
annot = annot.next
is_hover_annot = False
current_annot = None
for annot in annots:
if fitz.Point(ex, ey) in annot.rect:
# self.buffer.message_to_emacs.emit(annot.info["content"])
is_hover_annot = True
current_annot = annot
opacity = 0.5
self.buffer.message_to_emacs.emit("[d]Delete Annot [e]Edit Annot")
else:
opacity = 1.0
if opacity != annot.opacity:
annot.setOpacity(opacity)
annot.update()
# update only if changed
if is_hover_annot != self.is_hover_annot:
self.is_hover_annot = is_hover_annot
self.page_cache_pixmap_dict.clear()
self.update()
if current_annot and current_annot.info["content"]:
if current_annot.info["id"] != self.last_hover_annot_id or not QToolTip.isVisible():
QToolTip.showText(QCursor.pos(), current_annot.info["content"], None, QRect(), 10 * 1000)
self.last_hover_annot_id = current_annot.info["id"]
else:
annot.setOpacity(1) # restore annot
self.is_hover_annot = False
annot.update()
if QToolTip.isVisible():
QToolTip.hideText()
self.page_cache_pixmap_dict.clear()
self.update()
return page, annot
return page, current_annot
except Exception as e:
print("Hove Annot: ", e)
return None, None
def save_annot(self):
self.document.saveIncr()
@@ -925,17 +1236,16 @@ class PdfViewerWidget(QWidget):
page.deleteAnnot(annot)
self.save_annot()
if action == "edit":
if annot.type[0] == 0:
self.get_focus_text.emit(self.buffer_id, annot.info["content"])
else:
self.buffer.message_to_emacs.emit("Cannot edit. Only support text annot type.")
self.edited_page_annot = (page, annot)
self.get_focus_text.emit(self.buffer_id, annot.info["content"].replace("\r", "\n"))
def update_annot_text(self, annot_text):
page, annot = self.hover_annot()
page, annot = self.edited_page_annot
if annot.parent:
annot.setInfo(content=annot_text)
annot.update()
self.save_annot()
self.edited_page_annot = (None, None)
def jump_to_page(self, page_num):
self.update_vertical_offset(min(max(self.scale * (int(page_num) - 1) * self.page_height, 0), self.max_scroll_offset()))
@@ -943,6 +1253,13 @@ class PdfViewerWidget(QWidget):
def jump_to_percent(self, percent):
self.update_vertical_offset(min(max(self.scale * (self.page_total_number * self.page_height * percent / 100.0), 0), self.max_scroll_offset()))
def jump_to_rect(self, page_index, rect):
quad = rect.quad
self.update_vertical_offset((page_index * self.page_height + quad.ul.y) * self.scale)
def current_percent(self):
return 100.0 * self.scroll_offset / (self.max_scroll_offset() + self.rect().height())
def update_vertical_offset(self, new_offset):
if self.scroll_offset != new_offset:
self.scroll_offset = new_offset
@@ -955,7 +1272,7 @@ class PdfViewerWidget(QWidget):
def get_cursor_absolute_position(self):
start_page_index = self.get_start_page_index()
last_page_index = self.get_last_page_index()
last_page_index = min(self.page_total_number - 1, self.get_last_page_index())
pos = self.mapFromGlobal(QCursor.pos()) # map global coordinate to widget coordinate.
ex, ey = pos.x(), pos.y()
@@ -977,6 +1294,17 @@ class PdfViewerWidget(QWidget):
page_index = index + 1
y = (ey + page_offset) * 1.0 / self.scale
temp = x
if self.rotation == 90:
x = y
y = self.page_width - temp
elif self.rotation == 180:
x = self.page_width - x
y = self.page_height - y
elif self.rotation == 270:
x = self.page_height - y
y = temp
return x, y, page_index
return None, None, None
@@ -1026,18 +1354,25 @@ class PdfViewerWidget(QWidget):
if self.is_select_mode:
self.cleanup_select()
if event.button() == Qt.LeftButton:
# In order to catch mouse move event when drap mouse.
self.setMouseTracking(False)
elif event.button() == Qt.RightButton:
self.handle_click_link()
if self.is_free_text_annot_mode:
if event.button() != Qt.LeftButton:
self.disable_free_text_annot_mode()
else:
if event.button() == Qt.LeftButton:
# In order to catch mouse move event when drap mouse.
self.setMouseTracking(False)
elif event.button() == Qt.RightButton:
self.handle_click_link()
elif event.type() == QEvent.MouseButtonRelease:
# Capture move event, event without holding down the mouse.
self.setMouseTracking(True)
self.releaseMouse()
if not self.free_text_annot_timer.isActive():
self.free_text_annot_timer.start()
elif event.type() == QEvent.MouseButtonDblClick:
self.disable_free_text_annot_mode()
if self.is_mark_search:
self.cleanup_search()
if event.button() == Qt.RightButton:
@@ -1045,6 +1380,21 @@ class PdfViewerWidget(QWidget):
return False
def enable_free_text_annot_mode(self):
self.is_free_text_annot_mode = True
self.free_text_annot_pos = (None, None)
def disable_free_text_annot_mode(self):
self.is_free_text_annot_mode = False
def handle_free_text_annot_mode(self):
if self.is_free_text_annot_mode:
self.disable_free_text_annot_mode()
ex, ey, page_index = self.get_cursor_absolute_position()
self.free_text_annot_pos = (fitz.Point(ex, ey), page_index)
self.get_focus_text.emit(self.buffer_id, "")
def handle_select_mode(self):
self.is_select_mode = True
rect_index, page_index = self.get_char_rect_index()