kkfileview预览pdf格式文件,实现多关键词高亮和定位

        最近在做文件搜索功能(搜索用的ElasticSearch),需要在前端搜索得到文档后,在浏览器预览文档,并将搜索关键词高亮和定位。文档预览选用的开源项目kkfileview,可以很好地预览文档,但是并没有预览文档关键词高亮的功能。经kkfileview技术交流群大佬的指导,得知要修改pdfjs,具体步骤请往下看。【注,这里只实现了kkfileview用pdf格式预览时的高亮和定位功能】

关键词:ElasticSearch    kkFileView    Pdf.js    多关键词高亮        关键词定位

参考博客:https://blog.csdn.net/a973685825/article/details/81285819

kkfileview的GitHub地址:https://github.com/kekingcn/kkFileView

pdf.js的GitHub地址:https://github.com/mozilla/pdf.js

目录

一 ES获取高亮关键词

二  kkfileview传递关键词

三  pdf.js获取关键词

四  pdf.js高亮关键词

五  打包pdf.js

运行效果

结语


一 ES获取高亮关键词

        其他的先不管,首先我们需要获取高亮关键词。对于我们搜索,我是采用的_analyze这个API去获取查询语句的分词结果,作为我们的高亮关键词。关于elasticsearch网上资料很多,我这里就不赘述了。将获取到的关键词组成字符串,用空格进行分隔(当然,你也可以用其他的分隔符,之后将字符串转为数组的时候注意分隔符就可以了),命名为keyString,在调用kkfileview的时候把关键词参数传入进去(关于kkfileview如何调用,请看其官方文档)。


# keyString为由关键词组成的字符串,用空格分隔。示例: "知识 图谱 综述"
# 相比较于之前,最后面添加了keyString
'http://127.0.0.1:8012/onlinePreview?url='+encodeURIComponent(Base64.encode(url)) + keyString;

二  kkfileview传递关键词

        kkfileview这部分主要是承上启下的作用,先从ES那边获取到keyString,然后把它传递给pdf.js。先来看看怎么获取,这个项目文件不少,但是放心,我们只需要修改server/src/main/resources/web/pdf.ftl和server/src/main/java/cn/keking/web/controller/OnlinePreviewController.java这两个文件。

        kkfileview通过OnlinePreviewController.java文件中的onlinePreview函数获取到url中的参数,所以我们要修改这个函数来获取keyString。需要修改两处:1 函数参数添加keyString;2 给model添加keyword属性,详情请看下面代码块中的注释。

    @RequestMapping(value = "/onlinePreview")
    // 传给这个函数的参数添加了keyString
    public String onlinePreview(String url, String keyword, Model model, HttpServletRequest req) {
        String fileUrl;
        try {
            fileUrl = new String(Base64.decodeBase64(url), StandardCharsets.UTF_8);
        } catch (Exception ex) {
            String errorMsg = String.format(BASE64_DECODE_ERROR_MSG, "url");
            return otherFilePreview.notSupportedFile(model, errorMsg);
        }
        FileAttribute fileAttribute = fileHandlerService.getFileAttribute(fileUrl, req);
        model.addAttribute("file", fileAttribute);
        model.addAttribute("keyword", keyword);  // 添加了这一行
        FilePreview filePreview = previewFactory.get(fileAttribute);
        logger.info("预览文件url:{},previewType:{}", fileUrl, fileAttribute.getType());
        return filePreview.filePreviewHandle(fileUrl, model, fileAttribute);
    }

        修改完上面的部分,kkfileview就已经获取到了高亮关键词,现在需要把它传递给pdf.js,需要修改pdf.ftl文件中的script部分,其实就是先获取关键词 ,再通过url传给pdf.js。注意这里的pdfExt,这是我新建的一个文件夹,在第五部分会讲到。

    var url = '${finalUrl}';
    var baseUrl = '${baseUrl}'.endsWith('/') ? '${baseUrl}' : '${baseUrl}' + '/';
    if (!url.startsWith(baseUrl)) {
        url = baseUrl + 'getCorsFile?urlPath=' + encodeURIComponent(url);
    }
    // 首先从我们的model中获取keyword
    var keyword = '${keyword}';

    // 然后在最后添加了keyword参数
    // 在kkfileview的源码中,参数里还有disabledownload,被我删掉了
    // 注意这里的pdfExt,这是我新建的一个文件夹,之后会提到
    document.getElementsByTagName('iframe')[0].src = "${baseUrl}pdfExt/web/viewer.html?file=" + encodeURIComponent(url)+ "&keyword="+ keyword;

    document.getElementsByTagName('iframe')[0].height = document.documentElement.clientHeight - 10;

      

三  pdf.js获取关键词

        pdf.js是可以获取文件地址参数的,我们首先找到这部分代码在哪里写的,然后添加获取keyword参数的代码。pdf.js是在web/viewer.js这部分代码获取文件地址的。

  (function rewriteUrlClosure() {
    // Run this code outside DOMContentLoaded to make sure that the URL
    // is rewritten as soon as possible.
    const queryString = document.location.search.slice(1);
    const m = /(^|&)file=([^&]*)/.exec(queryString);
    defaultUrl = m ? decodeURIComponent(m[2]) : "";

    // Example: chrome-extension://.../http://example.com/file.pdf
    const humanReadableUrl = "/" + defaultUrl + location.hash;
    history.replaceState(history.state, "", humanReadableUrl);
    if (top === window) {
      // eslint-disable-next-line no-undef
      chrome.runtime.sendMessage("showPageAction");
    }
  })();

        那我们只需要仿照着这个获取keyword参数就可以了,我把获取keyword参数的代码放在了这个文件的getViewerConfiguration函数中。

function getViewerConfiguration() {
  
  // 添加了这部分代码
  const queryString = document.location.search.slice(1);
  const m = /(^|&)keyword=([^&]*)/.exec(queryString);
  const keyword = m ? decodeURIComponent(m[2]) : "";
  console.log("keyword", keyword);

  let errorWrapper = null;
  if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")){
    errorWrapper = {
      container: document.getElementById("errorWrapper"),
      errorMessage: document.getElementById("errorMessage"),
      closeButton: document.getElementById("errorClose"),
      errorMoreInfo: document.getElementById("errorMoreInfo"),
      moreInfoButton: document.getElementById("errorShowMore"),
      lessInfoButton: document.getElementById("errorShowLess"),
    };
  }

  // 内容太多,这里省略了
}

        好了,现在我们的pdf.js终于拿到了关键词,下面说一下怎么对关键词进行高亮

四  pdf.js高亮关键词

        现在我们看一下pdf.js文件的内容,在看这部分内容之前,强烈建议先看一遍文章最前参考博客的内容。 如同参考博客讲的,我们高亮关键词的方法就是调用pdf.js自带的搜索功能,把关键词传进去,那我们看一下这部分怎么做。

        (1)我们需要把关键词传入web/viewer.html文件中id="findInput" 的输入框里,也就是下面这里。

          <div id="findbarInputContainer">
            <input id="findInput" class="toolbarField" title="Find" placeholder="Find in document…" tabindex="91" data-l10n-id="find_input">
            <div class="splitToolbarButton">
              <button id="findPrevious" class="toolbarButton findPrevious" title="Find the previous occurrence of the phrase" tabindex="92" data-l10n-id="find_previous">
                <span data-l10n-id="find_previous_label">Previous</span>
              </button>
              <div class="splitToolbarButtonSeparator"></div>
              <button id="findNext" class="toolbarButton findNext" title="Find the next occurrence of the phrase" tabindex="93" data-l10n-id="find_next">
                <span data-l10n-id="find_next_label">Next</span>
              </button>
            </div>
          </div>

        找到了id,修改值就很简单了,我选择还是在getViewerConfiguration函数中修改,具体如下

function getViewerConfiguration() {

  //这里是刚才添加的代码
  const queryString = document.location.search.slice(1);
  const m = /(^|&)keyword=([^&]*)/.exec(queryString);
  const keyword = m ? decodeURIComponent(m[2]) : "";
  console.log("keyword", keyword);

  let errorWrapper = null;
  if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) {
    errorWrapper = {
      container: document.getElementById("errorWrapper"),
      errorMessage: document.getElementById("errorMessage"),
      closeButton: document.getElementById("errorClose"),
      errorMoreInfo: document.getElementById("errorMoreInfo"),
      moreInfoButton: document.getElementById("errorShowMore"),
      lessInfoButton: document.getElementById("errorShowLess"),
    };
  }

  // 添加了这部分代码,把keyword的值传给input
  document.getElementById("findInput").value = keyword;
  // 要把findbar给禁用掉,具体原因见参考博客
  document.getElementById("findbar").style.display = "none";

  return {
      //内容太多这里省略了
  }
}

        (2)然后我们要处理一下传递给findinput的值,要修改web/app.js中的这个函数

  async _initializeViewerComponents() {
    // 省略
    
    //修改这个判断里的内容
    if (!this.supportsIntegratedFind) {
      this.findBar = new PDFFindBar(appConfig.findBar, eventBus, this.l10n); // 实例化PDFFindBar

      // 获取value值
      const highLightStr = appConfig.findBar.findField.value;
      // 把字符串转换成数组(如果你在调用kkfileview时传入的keystring分隔符不是用的空格,这里记得也修改一下
      const highLightWords = highLightStr.split(" ");
      // 把数组传递给我们新加的这个函数(下一步会讲)
      wordHighLight(highLightWords);
    }

    // 省略
  }

        (3)然后我们去web/app.js文件中添加刚刚提到的函数wordHighLight,在这里调用了搜索执行函数

function wordHighLight(hightLightWords) {
  // 从参考博客那里复制来的

  const evt = {
    // source: PDFFindBar, // PDFFindBar的实例,不确定是干嘛用的?
    type: "", // 这里默认应该是空的
    // 这里能默认跳转到query的位置,刚好能满足要求
    query: hightLightWords, // 高亮的关键词
    phraseSearch: false, // 支持整段文字匹配,如果时多个词的匹配只能是false
    caseSensitive: false, // 默认为false,搜索时忽略大小写
    highlightAll: true, // 设为true即关键词全部高亮
    // findPrevious: true,
  };
  PDFViewerApplication.findController.executeCommand("find" + evt.type, {
    // 搜索执行函数
    query: evt.query,
    phraseSearch: evt.phraseSearch,
    caseSensitive: evt.caseSensitive,
    highlightAll: evt.highlightAll,
    findPrevious: evt.findPrevious,
  });
}

         (4)现在我们要去修改执行搜索的代码,我们是要对多关键词进行高亮(毕竟搜索语句的关键词经常不止一个),但是pdf.js自带的搜索功能只能对单个关键词进行高亮,所以我们需要去稍微修改一下web/pdf_find_controller.js文件

        wordHighLight ==> executeCommand ==>_nextMatch ==> _calculateMatch  ==> _calculateWordMatch(==>为调用的意思),我们先去修改_calculateMatch,详细修改的内容见下面的代码和注释。

_calculateMatch(pageIndex) {
    let pageContent = this._pageContents[pageIndex];
    const pageDiffs = this._pageDiffs[pageIndex];
    
    // 一会我们要去修改这里的_query函数
    // 注意,之前的query是字符串,现在是数组
    const query = this._query;
    const { caseSensitive, entireWord, phraseSearch } = this._state;

    if (query.length === 0) {
      // Do nothing: the matches should be wiped out already.
      return;
    }

    if (!caseSensitive) {
      pageContent = pageContent.toLowerCase();
      // 修改了这里,添加了循环,因为现在的query已经不是一个字符串了,而是一个数组
      for (let i = 0; i < query.length; i++) {
        query[i] = query[i].toLowerCase();
      }
    }

    if (phraseSearch) {
      this._calculatePhraseMatch(
        query,
        pageIndex,
        pageContent,
        pageDiffs,
        entireWord
      );
    } else {
      this._calculateWordMatch(
        query,
        pageIndex,
        pageContent,
        pageDiffs,
        entireWord
      );
    }

    // When `highlightAll` is set, ensure that the matches on previously
    // rendered (and still active) pages are correctly highlighted.
    if (this._state.highlightAll) {
      this._updatePage(pageIndex);
    }
    if (this._resumePageIdx === pageIndex) {
      this._resumePageIdx = null;
      this._nextPageMatch();
    }

    // Update the match count.
    const pageMatchesCount = this._pageMatches[pageIndex].length;
    if (pageMatchesCount > 0) {
      this._matchesCountTotal += pageMatchesCount;
      this._updateUIResultsCount();
    }
  }

        然后我们要去修改_query的内容

  get _query() {
    const query = this._state.query;
    // 之前的query语句是字符串,现在改成了数组,所以需要循环处理
    if (typeof query === "object" && query.length !== 0) {
      for (let i = 0; i < query.length; i++) {
        if (query[i] !== this._rawQuery[i]) {
          this._rawQuery[i] = query[i];
          [this._normalizedQuery[i]] = normalize(query[i]);
        }
      }
    } else {
      // 这里是原先的版本,其实这个分支现在用不到,因为肯定是obj类型
      if (query !== this._rawQuery) {
        this._rawQuery = this._state.query;
        [this._normalizedQuery] = normalize(this._state.query);
      }
    }
    return this._normalizedQuery;
  }

         同样,由于我们把this._rawQuery和 this._normalizedQuery 都变成了数组类型来使用,所以这两个变量用之前需要先定义一下,不然会报错。可以在executeCommand这个函数里定义。

  executeCommand(cmd, state) {
    if (!state) {
      return;
    }
    const pdfDocument = this._pdfDocument;

    if (this._state === null || this._shouldDirtyMatch(cmd, state)) {
      this._dirtyMatch = true;
    }
    this._state = state;
    if (cmd !== "findhighlightallchange") {
      this._updateUIState(FindState.PENDING);
    }
    
    // 添加了下面这两行
    this._rawQuery = new Array(this._state.query.length);
    this._normalizedQuery = new Array(this._state.query.length);

    this._firstPageCapability.promise.then(
    // 内容太多了,这里省略了,没有粘贴上来
    );
  }

        然后我们去修改_calculateWordMatch函数,主要就是添加个循环

  _calculateWordMatch(query, pageIndex, pageContent, pageDiffs, entireWord) {
    const matchesWithLength = [];

    // Divide the query into pieces and search for text in each piece.

    // 改成了循环
    for (let x = 0; x < query.length; x++) {
      const queryArray = query[x].match(/\S+/g);
      for (let i = 0, len = queryArray.length; i < len; i++) {
        const subquery = queryArray[i];
        const subqueryLen = subquery.length;

        let matchIdx = -subqueryLen;
        while (true) {
          matchIdx = pageContent.indexOf(subquery, matchIdx + subqueryLen);
          if (matchIdx === -1) {
            break;
          }
          if (
            entireWord &&
            !this._isEntireWord(pageContent, matchIdx, subqueryLen)
          ) {
            continue;
          }
          const originalMatchIdx = getOriginalIndex(matchIdx, pageDiffs),
            matchEnd = matchIdx + subqueryLen - 1,
            originalQueryLen =
              getOriginalIndex(matchEnd, pageDiffs) - originalMatchIdx + 1;

          // Other searches do not, so we store the length.
          matchesWithLength.push({
            match: originalMatchIdx,
            matchLength: originalQueryLen,
            skipped: false,
          });
        }
      }
    }

    // Prepare arrays for storing the matches.
    this._pageMatchesLength[pageIndex] = [];
    this._pageMatches[pageIndex] = [];

    // Sort `matchesWithLength`, remove intersecting terms and put the result
    // into the two arrays.
    this._prepareMatches(
      matchesWithLength,
      this._pageMatches[pageIndex],
      this._pageMatchesLength[pageIndex]
    );
  }

        到这里,我们的代码就修改完了。 

五  打包pdf.js

         现在我们要把修改后的pdf.js打包放到kkfileview里,让kkfileview调用我们修改后的而不是默认的。

        (1)打包pdf.js

        打包pdf.js的方法在GitHub的readme里面有写,直接在terminal里运行gulp generic就可以了。

        在terminal里运行gulp generic

        打包成功 

         (2)kkfileview新建文件夹

        kkfileview默认使用的打包后的pdf.js放在了server/src/main/resources/static/pdfjs这个文件夹里。我在server/src/main/resources/static下新建了一个名为pdfExt的文件夹,准备把修改后的pdf.js文件打包放在这里。

        (3)复制文件

        然后就很简单了,把pdf.js项目build/generic目录下的内容复制到kkfileview新建的pdfExt文件夹里,就可以了。

运行效果

        我的查询语句是“知识图谱”,_analyze返回的结果(也就是对查询语句进行分词等预处理之后的结果)是“知识”和“图谱”。这两个词也就是我的高亮关键词,打开预览文件可以看到,所有和“知识”和“图谱”相关的词都被高亮标注了。并且可以在打开文件时自动定位到首个关键词出现的位置。

结语

        到这里就全部修改完毕了,可以运行一下试试看效果。第一次写博客,不完善之处请各位大佬们包容,希望能够帮助到大家。什么?你说我怎么这里只讲了怎么实现高亮,没有讲怎么实现定位啊?运行后可以发现,pdf.js自带的搜索功能,已经帮我们把关键词定位实现了,只要你能把高亮关键词按照上面的方法成功传进去,关键词定位的问题也就自然而然地解决了

        最后,特此感谢kk开源技术交流2群里的高雄大佬,给我提供修改的思路并且帮忙找到了参考博客。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值