欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

基于pdf.js實現(xiàn)文本劃詞高亮效果

 更新時間:2024年05月29日 08:39:59   作者:留簡328  
最近有一個需求,需要對于pdf文本進行操作,對接ai大模型對pdf文檔進行高效解讀,其中一個功能就是對于pdf的文本進行劃詞高亮,用戶可進行閱讀標記,本文給大家介紹了如何基于pdf.js實現(xiàn)文本劃詞高亮效果,需要的朋友可以參考下

前言

本司最近有一個需求,需要對于pdf文本進行操作,對接ai大模型對pdf文檔進行高效解讀,其中一個功能就是對于pdf的文本進行劃詞高亮,用戶可進行閱讀標記。

這是最簡易版的demo:

話不多說 開始吧

劃詞高亮

基于項目需求需要實現(xiàn)一個劃詞的功能,最開始的方法是將DOM元素進行切割,這樣需要破壞頁面 DOM 結(jié)構(gòu),但是由于pdf的特殊性,如果破壞了 DOM 結(jié)構(gòu)這樣對展示以及再對文本進行某些操作就會存在問題。在經(jīng)過多次嘗試最后基于 canvas 實現(xiàn)了這一功能。

主要思路:如何在不破壞原本的頁面DOM結(jié)構(gòu)的前提下實現(xiàn)高亮的效果呢,那就不能再在原來的文本元素上進行操作了,最好就是新生成一個元素作為一個畫布,將我們想要的效果畫下來蓋在原本的文本容器元素上。最后在輾轉(zhuǎn)反側(cè)的時候想到了這樣一個解決方案:生成一個 canvas 元素,讓 canvas 元素與需要劃詞高亮功能的文本容器元素等寬高,并且重疊在文本容器上,劃詞的時候獲取劃詞區(qū)域的文本節(jié)點相對于文本容器的位置信息,然后通過這些位置信息進行高亮背景的渲染。最終在不斷調(diào)試以及細節(jié)完善之后實現(xiàn)了該功能。

實現(xiàn)細節(jié)

  • 讓 canvas 與文本容器元素重疊

讓 canvas 與文本容器元素重疊最好的實現(xiàn)方式就是將 canvas 做為文本容器的直接子節(jié)點,然后設(shè)置文本容易為相對定位,將 canvas 設(shè)置為絕對定位,然后將 top、left、right、bottom 都設(shè)置為 0,這樣就可以時刻保證 canvas 元素與文本容器元素始終等寬高,且 canvas 重疊在文本容器上。不過這種實現(xiàn)方式也有一個問題,我把 canvas 的層級提高了,蓋住了文本容器中的其他文本節(jié)點,這樣就沒辦法進行劃詞了,所以這時候需要給 canvas 再添加一個 css 屬性:pointerEvents: 'none',這樣就可以讓 canvas 不響應(yīng)鼠標事件,從而讓底部文本節(jié)點可以正常劃詞了。

  private createContainer() {
    const el = document.createElement('div');
    el.style.position = 'absolute';
    el.style.top = '0';
    el.style.left = '0';
    el.style.right = '0';
    el.style.bottom = '0';
    el.style.pointerEvents = 'none';
    return el;
  }
  • 獲取劃詞區(qū)域文本節(jié)點的位置信息

獲取劃詞區(qū)域信息需要使用 document.getSelection().getRangeAt(0) 來獲得當(dāng)前劃詞區(qū)域的 range 對象,在這個對象上可以獲取到劃詞區(qū)域的起始和終止文本節(jié)點以及偏移量信息。

雖然拿到了節(jié)點信息,但是怎么獲得具體的位置信息呢?這時候就需要借助 Range 對象的強大功能了。

// 創(chuàng)建一個 range 對象
const range = document.createRange()
// 設(shè)置需要獲取位置信息的文本節(jié)點以及偏移量
range.setStart(startContainer, startOffset)
range.setEnd(startContainer, startContainer.textContent.length)
// 通過 getBoundingClientRect 獲取位置信息
const rect = range.getBoundingClientRect()

通過創(chuàng)建 range 對象可以獲得任何一個文本節(jié)點中的任何一段文本相對與整個頁面的位置信息,然后再通過減去文本容器元素相對于整個頁面的位置信息,就可以得到劃詞區(qū)域文本相對與文本容器的位置信息了。

  • 獲取頭尾中間的文本節(jié)點

雖然通過 document.getSelection().getRangeAt(0) 獲得了劃詞頭尾節(jié)點的信息,但是頭尾中間如果有其他的文本節(jié)點也需要進行背景高亮,那么中間的文本節(jié)點該怎么獲得呢?這里我想到的辦法是從頭節(jié)點開始進行深度優(yōu)先遍歷,遍歷到尾節(jié)點為止,然后收集遍歷過程中的所有文本節(jié)點,這樣就得到了整個劃詞區(qū)域內(nèi)的所有文本節(jié)點,然后通過上面第 2 點的辦法也可以得到所有文本節(jié)點的位置信息。

// 獲取 start 到 end 深度優(yōu)先遍歷之間的所有 Text Node 節(jié)點
export function getTextNodesByDfs(start: Text, end: Text) {
  if (start === end) return [];
  const iterator = nodeDfsGenerator(start, false);
  const textNodes = [];
  iterator.next();
  let value = iterator.next().value;
  while (value) {
    if (value === end) {
      textNodes.push(value);
      return textNodes;
    } else if (isTextNode(value)) {
      textNodes.push(value);
    }
    value = iterator.next().value;
  }
  if (!value) {
    return [];
  }
  return textNodes;
}
  • 處理跨行文本節(jié)點的位置信息

其實之前第 2 點獲取劃詞區(qū)域文本節(jié)點的位置信息的方案還有缺陷,對于跨行的文本節(jié)點如果仍然采用一個 range 去獲取位置信息,那么得到的就是下面這種情況:

沒錯,位置信息是錯誤的,因為很明顯 range 只能是一個矩形,并沒有辦法表示跨行選中時的不規(guī)則圖形的位置信息。

既然一個 range 不行,那么多個呢?所以我的解決思路就是將一個跨行的 range 拆分成多個不跨行的 range。

怎么拆呢?我使用的辦法是通過判斷起始和結(jié)束節(jié)點是否為同一個節(jié)點,如果起始和結(jié)束節(jié)點不是同一個節(jié)點,說明文本范圍跨越了多個節(jié)點。

createRects(range: IRange) {
    const rects: DOMRect[] = [];
    const { start, end } = range;
    const startNode = this.getNodeByPath(start.path, range);
    const endNode = this.getNodeByPath(end.path, range);
    if (startNode === endNode) {
      rects.push(...getTextNodeRects(startNode, start.offset, end.offset));
    } else {
      const textNodes = getTextNodesByDfs(startNode, endNode);
      rects.push(...getTextNodeRects(startNode, start.offset));
      textNodes.forEach((i, index) => {
        if (index === 0 || index === textNodes?.length - 1) {
          return;
        }
        const nodeRects = getTextNodeRects(i);
        if (nodeRects.length === 1 && (nodeRects[0].width === 0 || nodeRects[0].height === 0)) {
          // 過濾空 Text
          return;
        } else {
          rects.push(...nodeRects);
        }
      });
      rects.push(...getTextNodeRects(endNode, 0, end.offset));
    }
    return rects;
  }

// 獲取文本節(jié)點 DOMRect 對象,支持跨行場景
export function getTextNodeRects(node: Text, startOffset?: number, endOffset?: number): DOMRect[] {
  const iframe = document.getElementById('iframe') as HTMLElement | any;
  const iframeDocument = iframe?.contentDocument || iframe?.contentWindow?.document;
  if (!node) {
    return [];
  }
  if (startOffset === undefined) {
    startOffset = 0;
  }
  if (endOffset === undefined) {
    endOffset = node.textContent!.length as number;
  }
  let TextNode = isTextNode(node) ? node : node.firstChild;
  const range = document.createRange();
  range.setStart(TextNode, startOffset);
  range.setEnd(TextNode, endOffset);
  return Array.from(range.getClientRects());
}
  • 高亮

高亮區(qū)域采用 konva 庫進行渲染,會在劃詞區(qū)域渲染一個 Rect 和底部渲染一個 Line,可通過修改傳入 range 對象上的 config 屬性進行自定義。

renderRange(domRects: DOMRect[], id: string, config: IRangeConfig) {
    const { group, rectGroup, lineGroup, shapeGroup } = this.createGroup(id, config);

    const { top, left } = this.getRootPosition();
    const positions: IRectPosition[] = [];

    domRects.forEach((i, index) => {
      const x = i.left - left;
      const y = i.top - top;
      const position = {
        x,
        y,
        width: i.width,
        height: i.height,
      };
      positions.push(position);

      const shapeConstructors = this.config.shapeConstructors;
      if (shapeGroup && shapeConstructors) {
        shapeConstructors.forEach((fn) => {
          shapeGroup.add(fn(position, id, domRects, index));
        });
      }
      rectGroup.add(this.createRect(position, config.rect));
      lineGroup.add(this.createLine(position, config.line));
    });
    this.groups.push({ id, group, positions });
    this.layer.add(group);
  }

  private createGroup(id: string, config: IRangeConfig) {
    const group = new Konva.Group({ id, x: 0, y: 0 });
    const rectGroup = new Konva.Group({
      id: RECT_PREFIX + id,
      x: 0,
      y: 0,
      visible: config.rect?.visible || true,
    });
    const lineGroup = new Konva.Group({
      id: LINE_PREFIX + id,
      x: 0,
      y: 0,
      visible: config.line?.visible || true,
    });
    const shapeConstructors = this.config.shapeConstructors;
    let shapeGroup: Konva.Group | null = null;
    if (shapeConstructors && shapeConstructors.length > 0) {
      shapeGroup = new Konva.Group({
        id: SHAPE_PREFIX + id,
        x: 0,
        y: 0,
      });
      group.add(shapeGroup);
    }
    group.add(rectGroup);
    group.add(lineGroup);
    return { group, rectGroup, lineGroup, shapeGroup };
  }

  private createRect(position: IRectPosition, config: IRangeConfig['rect']) {
    return new Konva.Rect({
      ...position,
      fill: config.fill,
      ...config.konvaConfig,
    });
  }

  private createLine(position: IRectPosition, config: IRangeConfig['line']) {
    const { x, y, width, height } = position;
    return new Konva.Line({
      points: [x, y + height, x + width, y + height],
      stroke: config.stroke,
      strokeWidth: config.strokeWidth,
      ...config.konvaConfig,
    });
  }
  • 劃詞信息持久化與返顯

雖然實現(xiàn)了高亮的功能,但是想要實現(xiàn)劃詞信息持久化與返顯功能,那么肯定還涉及到將劃詞信息保存到后端,但是這一切的開頭都是從系統(tǒng)提供的一個 range 對象開始的,但是 range 對象上的 startContainer 和 endContainer 是保存著 DOM 節(jié)點的引用,這肯定沒辦法序列化存儲到后端的,所以需要一種方式能讓我們準確的找到想要的文本節(jié)點。

    function createRange(selection: Selection): IRange | null {
      if (!isValidSelection(selection)) return null;
      const { startContainer: start, startOffset, endContainer: end, endOffset } = selection.getRangeAt(0);
      const sPath = getPath(start);
      const ePath = start === end ? sPath : getPath(end);
      const page = Number(
        findParentWithAriaLabel(selection.anchorNode, 'aria-label')?.getAttribute('data-page-number')
      );
      if (!sPath || !ePath) return null;

      const text = getStartAndEndRangeText(start, startOffset, end, endOffset);

      return {
        id: uuid(8),
        page,
        text: selection.toString(),
        start: {
          path: sPath,
          offset: startOffset,
          text: text.start,
        },
        end: {
          path: ePath,
          offset: endOffset,
          text: text.end,
        },
        config: {
          color: newColor,
          rect: {
            fill: newColor,
            visible: true,
          },
          line: {
            stroke: newColor,
            visible: false,
            strokeWidth: 0,
          },
        },
      };
    }
    
    function getPath(textNode: Node) {
      const path = [0];
      let parentNode = textNode.parentNode;
      let cur = textNode;
      let shouldBreak = false;
      while (parentNode && !shouldBreak) {
        if (cur === parentNode.firstChild) {
          if (parentNode?.classList.contains('markedContent')) {
            shouldBreak = true;
            path.push(parentNode?.getAttribute('id'));
            break;
          }
          if (parentNode.hasAttribute('data-main-rotation')) {
            shouldBreak = true;
            break;
          } else {
            cur = parentNode;
            parentNode = cur.parentNode;
            path.unshift(0);
          }
        } else {
          cur = cur?.previousSibling;
          path[0]++;
        }
      }
      return parentNode ? path : null;
    }

    function getStartAndEndRangeText(start: Text, startOffset: number, end: Text, endOffset: number) {
      let startText = '';
      let endText = '';
      if (start === end) {
        startText = start.textContent ? start.textContent.slice(startOffset, endOffset) : '';
        endText = startText;
      } else {
        startText = start.textContent ? start.textContent.slice(startOffset) : '';
        endText = end.textContent ? end.textContent.slice(0, endOffset) : '';
      }
    
      return {
        start: startText,
        end: endText,
      };
    }

這里我采用的是類似 XPath 的方式進行儲存,對于頭尾節(jié)點,我們保存一個路徑數(shù)組,里面儲存的是從文本容器通過 childNodes 屬性遍歷下去找到該節(jié)點的信息,這樣對于任何的頁面結(jié)構(gòu)都可以使用了。

  • 移除高亮效果

上面已經(jīng)儲存下來了劃詞信息,可以看到每條數(shù)據(jù)我們都給到了一個唯一的id值,這樣當(dāng)用戶點擊到想要清除高亮效果的文本元素的時候,即可根據(jù)這個id移除掉其持久化信息的保存和繪制的canvas圖像

  removeHighlight() {
    let _this = this;
    _this.rootDocument.addEventListener('click', function (e: MouseEvent) {
      clearCanvas();
      if (_this.eraser.classList.contains('eraser-click')) {
        const targetEvent = e.target as HTMLElement;
        if (!targetEvent?.className) {
          _this.btn.style.display = 'none';
        }

        // 通過傳入點擊位置獲取 range id
        const id = _this.stage.getGroupIdByPointer(e.clientX, e.clientY);
        if (id) {
          let temList = _this.highList.filter((item) => item.id !== id);
          _this.stage.deleteRange(id);
          _this.setHighlight(temList);
          _this.highList = temList;
        }
      }
    });
  }
  
  deleteRange(id: string) {
    const index = this.groups.findIndex((i) => i.id === id);
    if (index === -1) return false;
    this.groups.splice(index, 1);
    const group = this.layer.find('#' + id)[0];
    if (group) group.destroy();
  }

以上就是基于pdf.js實現(xiàn)文本劃詞高亮效果的詳細內(nèi)容,更多關(guān)于pdf.js文本劃詞高亮的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • js 創(chuàng)建快捷方式的代碼(fso)

    js 創(chuàng)建快捷方式的代碼(fso)

    js 創(chuàng)建快捷方式的代碼,這個是在本地運行的需要確認的,需要的朋友可以參考下。
    2010-11-11
  • 使用Webpack壓縮與轉(zhuǎn)譯JavaScript代碼的操作方法

    使用Webpack壓縮與轉(zhuǎn)譯JavaScript代碼的操作方法

    在Web開發(fā)中,代碼的性能和加載時間是用戶體驗的重要組成部分,為此,將JavaScript代碼壓縮和優(yōu)化是發(fā)布前一個必不可少的步驟,所以本文給大家介紹了如何使用Webpack壓縮與轉(zhuǎn)譯JavaScript代碼,需要的朋友可以參考下
    2024-05-05
  • 用VsCode編輯TypeScript的實現(xiàn)方法

    用VsCode編輯TypeScript的實現(xiàn)方法

    這篇文章主要介紹了用VsCode編輯TypeScript的實現(xiàn)方法,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-05-05
  • JS表格組件BootstrapTable行內(nèi)編輯解決方案x-editable

    JS表格組件BootstrapTable行內(nèi)編輯解決方案x-editable

    這篇文章主要介紹了JS組件系列BootstrapTable行內(nèi)編輯解決方案:x-editable,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2016-09-09
  • 用JavaScript做一個簡易計算器的三種方法舉例

    用JavaScript做一個簡易計算器的三種方法舉例

    這篇文章主要給大家介紹了關(guān)于用JavaScript做一個簡易計算器的三種方法,JS中實現(xiàn)一個簡單的計算器并不困難,我們只需利用基本的數(shù)學(xué)運算符和JavaScript的語法即可,需要的朋友可以參考下
    2023-10-10
  • 第三篇Bootstrap網(wǎng)格基礎(chǔ)

    第三篇Bootstrap網(wǎng)格基礎(chǔ)

    Bootstrap 提供了一套響應(yīng)式、移動設(shè)備優(yōu)先的流式網(wǎng)格系統(tǒng),網(wǎng)格系統(tǒng)類似一個表格。接下來通過本文給大家介紹Bootstrap網(wǎng)格基礎(chǔ),非常不錯,具有參考借鑒價值,感興趣的朋友一起學(xué)習(xí)吧
    2016-06-06
  • 10個經(jīng)典的網(wǎng)頁鼠標特效代碼

    10個經(jīng)典的網(wǎng)頁鼠標特效代碼

    小編為廣大讀者們整理了10個經(jīng)典的網(wǎng)頁鼠標特效代碼,并對代碼進行了編譯和解釋,需要的朋友收藏下吧。
    2018-01-01
  • 微信小程序?qū)崿F(xiàn)搜索指定景點周邊美食、酒店

    微信小程序?qū)崿F(xiàn)搜索指定景點周邊美食、酒店

    這篇文章主要為大家詳細介紹了微信小程序?qū)崿F(xiàn)搜索指定景點周邊美食、酒店的功能,具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-05-05
  • js實現(xiàn)自動鎖屏功能

    js實現(xiàn)自動鎖屏功能

    有這么一個需求,開發(fā)了一套系統(tǒng),當(dāng)用戶離開桌面或者一段時間不操作的話,需要把該系統(tǒng)所有打開頁面鎖定起來,本文就詳細的介紹一下,感興趣的可以了解一下
    2021-06-06
  • 微信小程序用戶后臺定位及錄音授權(quán)及請求示例

    微信小程序用戶后臺定位及錄音授權(quán)及請求示例

    這篇文章主要為大家介紹了微信小程序用戶后臺定位及錄音授權(quán)及請求示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪
    2022-04-04

最新評論