From cd56dd2edf00198fb8e5c88551ce6d9901e665b0 Mon Sep 17 00:00:00 2001 From: hius07 <62179190+hius07@users.noreply.github.com> Date: Fri, 2 Dec 2022 20:22:27 +0200 Subject: [PATCH] ReaderHighlight: pdf multi-page highlights (#9850) --- .../apps/reader/modules/readerbookmark.lua | 26 +- .../apps/reader/modules/readerhighlight.lua | 228 +++++++++++++----- .../apps/reader/modules/readerthumbnail.lua | 15 +- frontend/apps/reader/modules/readerview.lua | 77 +++--- frontend/document/koptinterface.lua | 9 +- frontend/document/pdfdocument.lua | 4 + 6 files changed, 249 insertions(+), 110 deletions(-) diff --git a/frontend/apps/reader/modules/readerbookmark.lua b/frontend/apps/reader/modules/readerbookmark.lua index 2ce85931c..b6dd2e4b0 100644 --- a/frontend/apps/reader/modules/readerbookmark.lua +++ b/frontend/apps/reader/modules/readerbookmark.lua @@ -206,11 +206,15 @@ function ReaderBookmark:isBookmarkInPositionOrder(a, b) if a.page == b.page then -- both bookmarks in the same page if a.highlighted and b.highlighted then -- both are highlights, compare positions local is_reflow = self.ui.document.configurable.text_wrap -- save reflow mode - -- reflow mode doesn't set page in positions - a.pos0.page = a.page - a.pos1.page = a.page - b.pos0.page = b.page - b.pos1.page = b.page + -- reflow mode didn't set page in positions (in older bookmarks) + if not a.pos0.page then + a.pos0.page = a.page + a.pos1.page = a.page + end + if not b.pos0.page then + b.pos0.page = b.page + b.pos1.page = b.page + end self.ui.document.configurable.text_wrap = 0 -- native positions -- sort start and end positions of each highlight local compare_pos, a_start, a_end, b_start, b_end, result @@ -1022,10 +1026,8 @@ function ReaderBookmark:renameBookmark(item, from_highlight, is_new_note, new_te local bookmark if from_highlight then -- Called by ReaderHighlight:editHighlight, we need to find the bookmark - local pboxes = item.pboxes for __, bm in ipairs(self.bookmarks) do if item.datetime == bm.datetime and item.page == bm.page then - bm.pboxes = pboxes if bm.text == nil or bm.text == "" then bm.text = self:getBookmarkAutoText(bm) end @@ -1093,13 +1095,7 @@ function ReaderBookmark:renameBookmark(item, from_highlight, is_new_note, new_te if bookmark.datetime == bm.datetime and bookmark.page == bm.page then bm.text = value self.ui:handleEvent(Event:new("BookmarkEdited", bm)) - -- A bookmark isn't necessarily a highlight (it doesn't have pboxes) - if bookmark.pboxes then - local setting = G_reader_settings:readSetting("save_document") - if setting ~= "disable" then - self.ui.document:updateHighlightContents(bookmark.page, bookmark, bookmark.text) - end - end + self.ui.highlight:writePdfAnnotation("content", bookmark.page, bookmark, bookmark.text) break end end @@ -1438,7 +1434,7 @@ end function ReaderBookmark:getBookmarkNote(item) for _, bm in ipairs(self.bookmarks) do - if item.datetime == bm.datetime and item.page == bm.page then + if item.datetime == bm.datetime then return not self:isBookmarkAutoText(bm) and bm.text end end diff --git a/frontend/apps/reader/modules/readerhighlight.lua b/frontend/apps/reader/modules/readerhighlight.lua index 2214551b6..8f907f2bf 100644 --- a/frontend/apps/reader/modules/readerhighlight.lua +++ b/frontend/apps/reader/modules/readerhighlight.lua @@ -648,17 +648,22 @@ end function ReaderHighlight:onTapPageSavedHighlight(ges) local pages = self.view:getCurrentPageList() local pos = self.view:screenToPageTransform(ges.pos) - for key, page in pairs(pages) do - local items = self.view.highlight.saved[page] + for _, page in ipairs(pages) do + local items = self.view:getPageSavedHighlights(page) if items then - for i = 1, #items do - local pos0, pos1 = items[i].pos0, items[i].pos1 - local boxes = self.ui.document:getPageBoxesFromPositions(page, pos0, pos1) + for i, item in ipairs(items) do + local boxes = self.ui.document:getPageBoxesFromPositions(page, item.pos0, item.pos1) if boxes then - for index, box in pairs(boxes) do + for _, box in ipairs(boxes) do if inside_box(pos, box) then logger.dbg("Tap on highlight") - return self:onShowHighlightNoteOrDialog(page, i) + local hl_page, hl_i + if item.parent then -- multi-page highlight + hl_page, hl_i = unpack(item.parent) + else + hl_page, hl_i = page, i + end + return self:onShowHighlightNoteOrDialog(hl_page, hl_i) end end end @@ -816,10 +821,7 @@ function ReaderHighlight:onShowHighlightNoteOrDialog(page, index) return true end local item = self.view.highlight.saved[page][index] - local bookmark_note = self.ui.bookmark:getBookmarkNote({ - page = self.ui.document.info.has_pages and item.pos0.page or item.pos0, - datetime = item.datetime, - }) + local bookmark_note = self.ui.bookmark:getBookmarkNote({datetime = item.datetime}) if bookmark_note then local textviewer textviewer = TextViewer:new{ @@ -1512,20 +1514,13 @@ function ReaderHighlight:onHoldRelease() if self.select_mode then -- extended highlighting, ending fragment if self.selected_text then - if self.ui.paging and self.hold_pos.page ~= self.highlight_page then - self.ui.paging:_gotoPage(self.highlight_page) - UIManager:show(Notification:new{ - text = _("Fragments must be within one page"), - }) + self.select_mode = false + self:extendSelection() + if default_highlight_action == "select" then + self:saveHighlight(true) + self:clear() else - self.select_mode = false - self:extendSelection() - if default_highlight_action == "select" then - self:saveHighlight(true) - self:clear() - else - self:onShowHighlightMenu() - end + self:onShowHighlightMenu() end end return true @@ -1731,7 +1726,7 @@ function ReaderHighlight:getHighlightBookmarkItem() end if self.selected_text and self.selected_text.pos0 and self.selected_text.pos1 then return { - page = self.ui.paging and self.hold_pos.page or self.selected_text.pos0, + page = self.ui.paging and self.selected_text.pos0.page or self.selected_text.pos0, pos0 = self.selected_text.pos0, pos1 = self.selected_text.pos1, notes = cleanupSelectedText(self.selected_text.text), @@ -1743,14 +1738,14 @@ end function ReaderHighlight:saveHighlight(extend_to_sentence) self.ui:handleEvent(Event:new("AddHighlight")) logger.dbg("save highlight") - if self.hold_pos and self.selected_text and self.selected_text.pos0 and self.selected_text.pos1 then + if self.selected_text and self.selected_text.pos0 and self.selected_text.pos1 then if extend_to_sentence and self.ui.rolling then local extended_text = self.ui.document:extendXPointersToSentenceSegment(self.selected_text.pos0, self.selected_text.pos1) if extended_text then self.selected_text = extended_text end end - local page = self.hold_pos.page + local page = self.ui.paging and self.selected_text.pos0.page or self.ui.document:getPageFromXPointer(self.selected_text.pos0) if not self.view.highlight.saved[page] then self.view.highlight.saved[page] = {} end @@ -1763,6 +1758,7 @@ function ReaderHighlight:saveHighlight(extend_to_sentence) pos0 = self.selected_text.pos0, pos1 = self.selected_text.pos1, pboxes = self.selected_text.pboxes, + ext = self.selected_text.ext, drawer = self.view.highlight.saved_drawer, chapter = chapter_name, } @@ -1773,18 +1769,52 @@ function ReaderHighlight:saveHighlight(extend_to_sentence) bookmark_item.chapter = chapter_name self.ui.bookmark:addBookmark(bookmark_item) end - if self.selected_text.pboxes then - self:exportToDocument(page, hl_item) - end + self:writePdfAnnotation("save", page, hl_item) return page, #self.view.highlight.saved[page] end end -function ReaderHighlight:exportToDocument(page, item) - local setting = G_reader_settings:readSetting("save_document") - if setting == "disable" then return end - logger.dbg("export highlight to document", item) - local can_write = self.ui.document:saveHighlight(page, item) +function ReaderHighlight:writePdfAnnotation(action, page, item, content) + if self.ui.rolling or G_reader_settings:readSetting("save_document") == "disable" then + return + end + logger.dbg("write to pdf document", action, item) + local function doAction(_action, _page, _item, _content) + if _action == "save" then + return self.ui.document:saveHighlight(_page, _item) + elseif _action == "delete" then + return self.ui.document:deleteHighlight(_page, _item) + elseif _action == "content" then + return self.ui.document:updateHighlightContents(_page, _item, _content) + end + end + local can_write + if item.pos0.page == item.pos1.page then -- single-page highlight + local item_ + if item.pboxes then + item_ = item + else -- called from bookmarks to write bookmark note to annotation + for _, hl in ipairs(self.view.highlight.saved[page]) do + if hl.datetime == item.datetime then + item_ = {pboxes = hl.pboxes} + break + end + end + end + can_write = doAction(action, page, item_, content) + else -- multi-page highlight + local is_reflow = self.ui.document.configurable.text_wrap + for hl_page = item.pos0.page, item.pos1.page do + self.ui.document.configurable.text_wrap = 0 + local hl_part = self:getSavedExtendedHighlightPage(item, hl_page) + self.ui.document.configurable.text_wrap = is_reflow + can_write = doAction(action, hl_page, hl_part, content) + if can_write == false then break end + if action == "save" then -- update pboxes from quadpoints + item.ext[hl_page].pboxes = hl_part.pboxes + end + end + end if can_write == false and not self.warned_once then self.warned_once = true UIManager:show(InfoMessage:new{ @@ -1851,11 +1881,7 @@ function ReaderHighlight:deleteHighlight(page, i, bookmark_item) datetime = removed.datetime, }) end - local setting = G_reader_settings:readSetting("save_document") - if setting ~= "disable" then - logger.dbg("delete highlight from document", removed) - self.ui.document:deleteHighlight(page, removed) - end + self:writePdfAnnotation("delete", page, removed) UIManager:setDirty(self.dialog, "ui") end @@ -1864,13 +1890,11 @@ function ReaderHighlight:editHighlight(page, i, is_new_note, text) self.ui.bookmark:renameBookmark({ page = self.ui.document.info.has_pages and page or item.pos0, datetime = item.datetime, - pboxes = item.pboxes }, true, is_new_note, text) end function ReaderHighlight:editHighlightStyle(page, i) local item = self.view.highlight.saved[page][i] - local save_document = self.ui.paging and G_reader_settings:readSetting("save_document") ~= "disable" local radio_buttons = {} for _, v in ipairs(highlight_style) do table.insert(radio_buttons, { @@ -1889,12 +1913,12 @@ function ReaderHighlight:editHighlightStyle(page, i) default_provider = self.view.highlight.saved_drawer or G_reader_settings:readSetting("highlight_drawing_style", "lighten"), callback = function(radio) - if save_document then - self.ui.document:deleteHighlight(page, item) - end + self:writePdfAnnotation("delete", page, item) item.drawer = radio.provider - if save_document then - self.ui.document:saveHighlight(page, item) + self:writePdfAnnotation("save", page, item) + local bm_note = self.ui.bookmark:getBookmarkNote(item) + if bm_note then + self:writePdfAnnotation("content", page, item, bm_note) end UIManager:setDirty(self.dialog, "ui") self.ui:handleEvent(Event:new("BookmarkUpdated", @@ -1917,15 +1941,10 @@ function ReaderHighlight:extendSelection() local item1 = self.view.highlight.saved[self.highlight_page][self.highlight_idx] local item2_pos0, item2_pos1 = self.selected_text.pos0, self.selected_text.pos1 -- getting starting and ending positions, text and pboxes of extended highlight - local new_pos0, new_pos1, new_text, new_pboxes - if self.ui.document.info.has_pages then + local new_pos0, new_pos1, new_text, new_pboxes, ext + if self.ui.paging then + local cur_page = self.hold_pos.page local is_reflow = self.ui.document.configurable.text_wrap - local new_page = self.hold_pos.page - -- reflow mode doesn't set page in positions - item1.pos0.page = new_page - item1.pos1.page = new_page - item2_pos0.page = new_page - item2_pos1.page = new_page -- pos0 and pos1 are not in order within highlights, hence sorting all local function comparePositions (pos1, pos2) return self.ui.document:comparePositions(pos1, pos2) == 1 @@ -1935,17 +1954,35 @@ function ReaderHighlight:extendSelection() table.sort(positions, comparePositions) new_pos0 = positions[1] new_pos1 = positions[4] - local text_boxes = self.ui.document:getTextFromPositions(new_pos0, new_pos1) - new_text = text_boxes.text - new_pboxes = text_boxes.pboxes + local temp_pos0, temp_pos1 + if new_pos0.page == new_pos1.page then -- single-page highlight + local text_boxes = self.ui.document:getTextFromPositions(new_pos0, new_pos1) + new_text = text_boxes.text + new_pboxes = text_boxes.pboxes + temp_pos0, temp_pos1 = new_pos0, new_pos1 + else -- multi-page highlight + new_text = "" + ext = {} + for page = new_pos0.page, new_pos1.page do + local item = self:getExtendedHighlightPage(new_pos0, new_pos1, page) + new_text = new_text .. item.text + ext[page] = { -- for every page of multi-page highlight + pos0 = item.pos0, + pos1 = item.pos1, + pboxes = item.pboxes, + } + if page == cur_page then + temp_pos0, temp_pos1 = item.pos0, item.pos1 + end + end + end self.ui.document.configurable.text_wrap = is_reflow -- restore reflow -- draw - self.view.highlight.temp[new_page] = self.ui.document:getPageBoxesFromPositions(new_page, new_pos0, new_pos1) + self.view.highlight.temp[cur_page] = self.ui.document:getPageBoxesFromPositions(cur_page, temp_pos0, temp_pos1) else -- pos0 and pos1 are in order within highlights new_pos0 = self.ui.document:compareXPointers(item1.pos0, item2_pos0) == 1 and item1.pos0 or item2_pos0 new_pos1 = self.ui.document:compareXPointers(item1.pos1, item2_pos1) == 1 and item2_pos1 or item1.pos1 - self.hold_pos.page = self.ui.document:getPageFromXPointer(new_pos0) -- true to draw new_text = self.ui.document:getTextFromXPointers(new_pos0, new_pos1, true) end @@ -1955,10 +1992,81 @@ function ReaderHighlight:extendSelection() pos0 = new_pos0, pos1 = new_pos1, pboxes = new_pboxes, + ext = ext, } UIManager:setDirty(self.dialog, "ui") end +-- Calculates positions, text, pboxes of one page of selected multi-page highlight +-- (For pdf documents only, reflow mode must be off) +function ReaderHighlight:getExtendedHighlightPage(pos0, pos1, cur_page) + local item = {} + for page = pos0.page, pos1.page do + if page == cur_page then + local page_boxes = self.ui.document:getTextBoxes(page) + if page == pos0.page then + -- first page (from the start of highlight to the end of the page) + item.pos0 = pos0 + item.pos1 = { + x = page_boxes[#page_boxes][#page_boxes[#page_boxes]].x1, + y = page_boxes[#page_boxes][#page_boxes[#page_boxes]].y1, + } + elseif page ~= pos1.page then + -- middle pages (full pages) + item.pos0 = { + x = page_boxes[1][1].x0, + y = page_boxes[1][1].y0, + } + item.pos1 = { + x = page_boxes[#page_boxes][#page_boxes[#page_boxes]].x1, + y = page_boxes[#page_boxes][#page_boxes[#page_boxes]].y1, + } + else + -- last page (from the start of the page to the end of highlight) + item.pos0 = { + x = page_boxes[1][1].x0, + y = page_boxes[1][1].y0, + } + item.pos1 = pos1 + end + item.pos0.page = page + item.pos1.page = page + local text_boxes = self.ui.document:getTextFromPositions(item.pos0, item.pos1) + item.text = text_boxes.text + item.pboxes = text_boxes.pboxes + end + end + return item +end + +-- Returns one page of saved multi-page highlight +-- (For pdf documents only) +function ReaderHighlight:getSavedExtendedHighlightPage(hl_or_bm, page, index) + local highlight + if hl_or_bm.ext then + highlight = hl_or_bm + else -- called from bookmark, need to find the corresponding highlight + for _, hl in ipairs(self.view.highlight.saved[hl_or_bm.page]) do + if hl.datetime == hl_or_bm.datetime then + highlight = hl + break + end + end + end + local item = {} + item.datetime = highlight.datetime + item.drawer = highlight.drawer + item.pos0 = highlight.ext[page].pos0 + item.pos0.zoom = highlight.pos0.zoom + item.pos0.rotation = highlight.pos0.rotation + item.pos1 = highlight.ext[page].pos1 + item.pos1.zoom = highlight.pos0.zoom + item.pos1.rotation = highlight.pos0.rotation + item.pboxes = highlight.ext[page].pboxes + item.parent = {highlight.pos0.page, index} + return item +end + function ReaderHighlight:onReadSettings(config) self.view.highlight.saved_drawer = config:readSetting("highlight_drawer") or G_reader_settings:readSetting("highlight_drawing_style") or self.view.highlight.saved_drawer diff --git a/frontend/apps/reader/modules/readerthumbnail.lua b/frontend/apps/reader/modules/readerthumbnail.lua index a4140f947..39c8e48e1 100644 --- a/frontend/apps/reader/modules/readerthumbnail.lua +++ b/frontend/apps/reader/modules/readerthumbnail.lua @@ -199,12 +199,15 @@ function ReaderThumbnail:resetCachedPagesForBookmarks(...) end else if bm.page and type(bm.page) == "number" then - local p = bm.page - if not start_page or p < start_page then - start_page = p - end - if not end_page or p > end_page then - end_page = p + local bm_page0 = (bm.pos0 and bm.pos0.page) or bm.page + local bm_page1 = (bm.pos1 and bm.pos1.page) or bm.page + for p = bm_page0, bm_page1 do + if not start_page or p < start_page then + start_page = p + end + if not end_page or p > end_page then + end_page = p + end end end end diff --git a/frontend/apps/reader/modules/readerview.lua b/frontend/apps/reader/modules/readerview.lua index 3eb220d9a..274d3138f 100644 --- a/frontend/apps/reader/modules/readerview.lua +++ b/frontend/apps/reader/modules/readerview.lua @@ -506,32 +506,53 @@ function ReaderView:drawSavedHighlight(bb, x, y) end end +-- Returns the list of highlights in page. +-- The list includes full single-page highlights and parts of multi-page highlights. +function ReaderView:getPageSavedHighlights(page) + local highlights = {} + local is_reflow = self.document.configurable.text_wrap + self.document.configurable.text_wrap = 0 + for page_num, page_highlights in pairs(self.highlight.saved) do + for i, highlight in ipairs(page_highlights) do + -- old single-page reflow highlights do not have page in position + local pos0_page = highlight.pos0.page or page_num + local pos1_page = highlight.pos1.page or page_num + if pos0_page <= page and page <= pos1_page then + if pos0_page == pos1_page then -- single-page highlight + table.insert(highlights, highlight) + else -- multi-page highlight + local item = self.ui.highlight:getSavedExtendedHighlightPage(highlight, page, i) + table.insert(highlights, item) + end + end + end + end + self.document.configurable.text_wrap = is_reflow + return highlights +end + function ReaderView:drawPageSavedHighlight(bb, x, y) local pages = self:getCurrentPageList() - for _, page in pairs(pages) do - local items = self.highlight.saved[page] - if items then - for i = 1, #items do - local item = items[i] - local pos0, pos1 = item.pos0, item.pos1 - local boxes = self.document:getPageBoxesFromPositions(page, pos0, pos1) - if boxes then - local drawer = item.drawer or self.highlight.saved_drawer - local draw_note_mark = self.highlight.note_mark and - self.ui.bookmark:getBookmarkNote({ page = page, datetime = item.datetime, }) - for _, box in pairs(boxes) do - local rect = self:pageToScreenTransform(page, box) - if rect then - self:drawHighlightRect(bb, x, y, rect, drawer, draw_note_mark) - if draw_note_mark and self.highlight.note_mark == "sidemark" then - draw_note_mark = false -- side mark in the first line only - end + for _, page in ipairs(pages) do + local items = self:getPageSavedHighlights(page) + for _, item in ipairs(items) do + local boxes = self.document:getPageBoxesFromPositions(page, item.pos0, item.pos1) + if boxes then + local drawer = item.drawer or self.highlight.saved_drawer + local draw_note_mark = self.highlight.note_mark and + self.ui.bookmark:getBookmarkNote({datetime = item.datetime}) + for _, box in ipairs(boxes) do + local rect = self:pageToScreenTransform(page, box) + if rect then + self:drawHighlightRect(bb, x, y, rect, drawer, draw_note_mark) + if draw_note_mark and self.highlight.note_mark == "sidemark" then + draw_note_mark = false -- side mark in the first line only end - end -- end for each box - end -- end if boxes - end -- end for each highlight + end + end + end end - end -- end for each page + end end function ReaderView:drawXPointerSavedHighlight(bb, x, y) @@ -567,11 +588,13 @@ function ReaderView:drawXPointerSavedHighlight(bb, x, y) if boxes then local drawer = item.drawer or self.highlight.saved_drawer local draw_note_mark = self.highlight.note_mark and - self.ui.bookmark:getBookmarkNote({ page = item.pos0, datetime = item.datetime, }) - for _, box in pairs(boxes) do - self:drawHighlightRect(bb, x, y, box, drawer, draw_note_mark) - if draw_note_mark and self.highlight.note_mark == "sidemark" then - draw_note_mark = false -- side mark in the first line only + self.ui.bookmark:getBookmarkNote({datetime = item.datetime}) + for _, box in ipairs(boxes) do + if box.h ~= 0 then + self:drawHighlightRect(bb, x, y, box, drawer, draw_note_mark) + if draw_note_mark and self.highlight.note_mark == "sidemark" then + draw_note_mark = false -- side mark in the first line only + end end end -- end for each box end -- end if boxes diff --git a/frontend/document/koptinterface.lua b/frontend/document/koptinterface.lua index 9de1ea38f..b8be38da5 100644 --- a/frontend/document/koptinterface.lua +++ b/frontend/document/koptinterface.lua @@ -1184,7 +1184,7 @@ Transform position in native page to reflowed page. ]]-- function KoptInterface:nativeToReflowPosTransform(doc, pageno, pos) local kc = self:getCachedContext(doc, pageno) - local rpos = {} + local rpos = {page = pageno} rpos.x, rpos.y = kc:nativeToReflowPosTransform(pos.x, pos.y) return rpos end @@ -1194,7 +1194,7 @@ Transform position in reflowed page to native page. ]]-- function KoptInterface:reflowToNativePosTransform(doc, pageno, abs_pos, rel_pos) local kc = self:getCachedContext(doc, pageno) - local npos = {} + local npos = {page = pageno} npos.x, npos.y = kc:reflowToNativePosTransform(abs_pos.x, abs_pos.y, rel_pos.x, rel_pos.y) return npos end @@ -1294,6 +1294,11 @@ Returns 1 if positions are ordered (if ppos2 is after ppos1), -1 if not, 0 if sa Positions of the word boxes containing ppos1 and ppos2 are compared. --]] function KoptInterface:comparePositions(doc, ppos1, ppos2) + if ppos1.page < ppos2.page then + return 1 + elseif ppos1.page > ppos2.page then + return -1 + end local box1 = self:getWordFromPosition(doc, ppos1).pbox local box2 = self:getWordFromPosition(doc, ppos2).pbox if box1.y == box2.y then diff --git a/frontend/document/pdfdocument.lua b/frontend/document/pdfdocument.lua index fe18acc76..b0bf0942e 100644 --- a/frontend/document/pdfdocument.lua +++ b/frontend/document/pdfdocument.lua @@ -121,6 +121,10 @@ function PdfDocument:getTextFromPositions(spos0, spos1) return self.koptinterface:getTextFromPositions(self, spos0, spos1) end +function PdfDocument:getTextBoxes(pageno) + return self.koptinterface:getTextBoxes(self, pageno) +end + function PdfDocument:getPageBoxesFromPositions(pageno, ppos0, ppos1) return self.koptinterface:getPageBoxesFromPositions(self, pageno, ppos0, ppos1) end