solr版本:8.4.1
springboot版本:2.3.0.RELEASE
pdf.js:2.2.228
服务器环境:centos
使用场景
公司提出需要一个文库用来存放文档,并且要求可以全文检索,还要和自己的系统权限集成,部分文件还要求保密,打水印、不让复制、不让下载等等各种个性化的、奇葩的需求,然后发现现成的工具要么功能不满足,要么要花钱,没有一个可以满足老板的要求。
具体实现步骤
首先,我们考虑一下要用到的技术:全文检索、文件操作。
-
全文检索有很多种可以选,那么我们考虑的时候就希望选一种最简单的,非常幸运的是solr和tika有集成插件。tika可以帮我们提取pdf的内容,当然不嫌麻烦的话可以自己去提取pdf的内容,自己提取的话虽然有点麻烦但是数据会更符合自己的使用场景需求。
-
文件操作方面主要是给文件加水印和前端渲染,浏览器虽然可以直接渲染PDF但是不能实现搜索文本标亮,和定位。那么我选了pdf.js,主要也是因为可以偷懒。官方给的例子稍微改一下完全可以满足使用。
其次,考虑一下流程,在什么时候给文件加水印呢?我当时的流程如下:
从上图看得出,这里面我们自己编写的代码其实不多,实现效果如下:
操作步骤:
1.配置solr插件:抄袭借鉴solr-8.4.1/example/example-DIH/solr/tika文件价内的配置
重点关注一下:solrconfig.xml里面的
<requestHandler name="/dataimport" class="solr.DataImportHandler">
<lst name="defaults">
<str name="config">tika-data-config.xml</str>
</lst>
</requestHandler>
看得出来是让我们把里面的内容好好抄袭借鉴一下。
tika-data-config.xml
<dataConfig>
<dataSource type="BinFileDataSource"/>
<document>
<entity name="file" processor="FileListEntityProcessor" dataSource="null"
baseDir="${solr.install.dir}/example/exampledocs" fileName=".*pdf"
rootEntity="false">
<field column="file" name="id"/>
<entity name="pdf" processor="TikaEntityProcessor"
url="${file.fileAbsolutePath}" format="text">
<field column="Author" name="author" meta="true"/>
<!-- in the original PDF, the Author meta-field name is upper-cased,
but in Solr schema it is lower-cased
-->
<field column="title" name="title" meta="true"/>
<field column="dc:format" name="format" meta="true"/>
<field column="text" name="text"/>
<field column="fileAbsolutePath" name="filePath" />
<field column="fileSize" name="size" />
<field column="fileLastModified" name="lastModified" />
</entity>
</entity>
</document>
</dataConfig>
另外需要注意的是字段,managed-schema文件里面默认有几个字段,我配置了一个动态字段来获取pdf的内容,至于原因么,主要还是因为这个可以偷懒
2.上传文件代码编写
核心代码例子如下,重点是 attr_这里面会拿到所有需要的字段,path是上传后文件的路径,参数bytes是上传后的文件流转换的,另外我这里配置了solr自动id,不然还需要加一个literal.id
public void indexFilesByTika(byte[] bytes,String path) throws IOException, SolrServerException {
ContentStreamUpdateRequest up = new ContentStreamUpdateRequest("/update/extract");
String contentType = "application/pdf";
ContentStreamBase cs = new ContentStreamBase.ByteArrayStream(bytes,document.getFileName());
cs.setContentType(contentType);
up.addContentStream(cs);
up.setParam("literal.path", path);
up.setParam("uprefix", "attr_");
up.setParam("fmap.content", "text");
up.setAction(AbstractUpdateRequest.ACTION.COMMIT, true, true);
solrClient.request(up);
}
3.查询数据
这时候需要注意的是水印,这个也很简单用itextpdf就可以了
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itextpdf</artifactId>
<version>5.5.13</version>
</dependency>
<dependency>
<groupId>com.itextpdf</groupId>
<artifactId>itext-asian</artifactId>
<version>5.2.0</version>
</dependency>
/**
*
* @param text 水印文字
* @param url 文件URL
* @param os 输出流
* @param widthExtra 水印的宽
* @param heightExtra 水印的高
* @throws DocumentException
* @throws IOException
*/
public static void waterMark(String text, URL url, OutputStream os,int widthExtra,int heightExtra) throws DocumentException, IOException {
PdfReader reader = new PdfReader(url);
// 加完水印的文件
PdfStamper stamper = new PdfStamper(reader, os);
float width = reader.getPageSize(1).getWidth() / widthExtra;
float height = reader.getPageSize(1).getHeight() / heightExtra;
int total = reader.getNumberOfPages() + 1;
PdfContentByte content;
BaseFont font = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
for (int i = 1; i < total; i++) {
content = stamper.getOverContent(i);
content.saveState();
PdfGState gs = new PdfGState();
gs.setFillOpacity(0.2f);// 设置透明度为0.2
content.setGState(gs);
for (int j = 0; j < height; j++) {
for (int j2 = 0; j2 < width; j2++) {
content.beginText();
content.setColorFill(BaseColor.BLUE);
content.setFontAndSize(font, 24);
content.showTextAligned(Element.ALIGN_RIGHT, text, (float)widthExtra * j2, (float)heightExtra * j, 35);
content.endText();
}
}
content.restoreState();
}
stamper.close();
}
4.前端页面接收与渲染
修改pdf.js官方的例子,viewer.html,这里主要是改了渲染的方式,官方例子的渲染方式是通过文件渲染,修改后可以通过接口返回的二进制流渲染
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="google" content="notranslate">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>PDF.js viewer</title>
<link rel="stylesheet" href="viewer.css">
<!-- This snippet is used in production (included from viewer.html) -->
<link rel="resource" type="application/l10n" href="./locale/locale.properties">
<script src="../build/pdf.js"></script>
<script src="jquery.min.js"></script>
<script>
function convertDataURIToBinary(dataURI) { //编码转换
var raw = atob(dataURI);
var rawLength = raw.length;
var array = new Uint8Array(new ArrayBuffer(rawLength));
for (i = 0; i < rawLength; i++) {
array[i] = raw.charCodeAt(i);
}
return array;
}
var DEFAULT_URL;
var param = {};
param.id = getQueryString("id");
if (param.id != null) {
var PDFData = "";
var BASE64_MARKER = ';base64,';
$.ajax({
url: '接口' + param.id,
type: "get",
async: false,
headers: {
'token': '123456'
},
contentType: "application/pdf;charset=utf-8",
beforeSend: function (request) {},
success: function (data) {
if (data.status == 1) {
DEFAULT_URL = convertDataURIToBinary(data.data);
}
}
});
}
</script>
<script src="viewer.js"></script>
</head>
至于怎么实现禁止复制黏贴和调用自身的find方法这个,请自己研究吧,最简单的方法就是通过禁用鼠标右键和键盘上一些组合键来实现禁止复制黏贴,找到viewer.js里面的搜索框方法和viewer.html的搜索框代码,在页面渲染结束后赋值再调用搜索方法。有点麻烦但是并不是很难。