update eaf package
This commit is contained in:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user