使用技巧
设置字体大小
可以为某处的字体设置大小
// 个人信息居中大号显示
doc.setFontSize(30)
const title = '个人信息'
// 随后再设置回默认字体大小
doc.setFontSize(20)
设置字体颜色
doc.setTextColor(255, 0, 0)
设置字体样式
doc.setFontStyle('bold')
获取页面宽度和高度
const pageWidth = doc.internal.pageSize.width
const pageHeight = doc.internal.pageSize.height
获取文本宽度
const textWidth = doc.getStringUnitWidth('个人信息')
// 或者
const titleWidth = doc.getTextWidth(title)
两者的区别是,getStringUnitWidth
是以字体大小为单位,getTextWidth
是以像素为单位。 假如字体大小为20,那么getStringUnitWidth
返回的是20,getTextWidth
返回的是文本的像素宽度。 例如我要将文本居中显示,那么我就需要用getTextWidth
获取文本的像素宽度,然后用pageWidth - textWidth
得到文本左边的空白宽度,然后除以2,就是文本左边的空白宽度
添加图片
doc对象的addImage方法可以添加图片
addImage方法的参数
该方法的参数如下:
doc.addImage(imgData, format, x, y, width, height, alias, compression, rotation)
其中, imgData是图片的base64编码,format是图片的格式,x和y是图片的左上角坐标,width和height是图片的宽和高,alias是图片的别名,compression是图片的压缩方式,rotation是图片的旋转角度。 后三个参数是可选的,可以不传。
例如:要在坐标(10, 10)处添加一张宽为100,高为100的JPG图片,可以这样写:
doc.addImage(imgData, 'JPEG', 10, 10, 100, 100)
URL地址
在实际使用中,除了使用base64编码的字符串,还可以使用图片的URL地址,例如:
doc.addImage('https://www.baidu.com/img/bd_logo1.png', 'PNG', 10, 10, 100, 100)
但是这样会有跨域问题,所以需要在服务器端设置允许跨域访问,或者使用代理。
Image对象
还有一种方法是,创建一个img标签,然后将图片的URL地址赋值给img标签的src属性,然后再将该图片传给doc.addImage方法,例如:
const img = new Image()
img.src = 'xxx'
img.onload = function () {
doc.addImage(img, 'PNG', 10, 10, 100, 100)
}
这个src属性可以是base64编码的字符串,也可以是图片的URL地址。
设置文本对齐方式
// 个人信息居中大号显示
doc.setFontSize(30)
const title = '个人信息'
const titleWidth = doc.getTextWidth(title)
// 页面宽度
const pageWidth = doc.internal.pageSize.width
const titleX = (pageWidth - titleWidth) / 2
doc.text('个人信息', titleX, 20)
// 保存文件
doc.save(this.studentInfo.name + ' - 学生信息.pdf')
上述doc.text('个人信息', titleX, 20)中的第二个参数是文本的左边距,第三个参数是文本的上边距。 实际效果展示:
表格
doc.rect(x, y, w, h, style)
方法可以绘制矩形,其中x和y是矩形左上角的坐标,w和h是矩形的宽和高,style是矩形的样式,可以是S、D、B、FD、DF、FD或者空字符串。 例如:要在坐标(10, 10)处绘制一个宽为100,高为20的矩形,可以这样写:
// 表格
doc.rect(10, 10, 100, 20, 'S')
要在绘出的矩形中填充文字,可以使用doc.text(text, x, y, options)
方法,其中text是要填充的文字,x和y是文字的左上角坐标,options是可选参数,可以设置文字的对齐方式、字体大小、字体样式等。 例如:要在坐标(10, 10)处绘制一个宽为100,高为20的矩形,并在矩形中填充文字“个人信息”,可以这样写:
// 表格
doc.rect(10, 10, 100, 20, 'S')
doc.text('个人信息', 15, 20)
效果如下: 但是一般来说,我们会将文字居中显示,这时需要计算出文字在这个矩形方框的中间位置,然后再填充文字。 设置一个方法,用来计算文字在矩形方框中间的位置:
// 获取PDF文本居中的x坐标
getCenterX(doc, text, x, tableWidth)
{
// x是表格的起始X轴坐标值,tableWidth是表格的宽度
const textWidth = doc.getStringUnitWidth(text) * doc.internal.getFontSize() / doc.internal.scaleFactor
return x + (tableWidth - textWidth) / 2
/*
* 计算文本在表格中水平居中时的X轴坐标值。
* 具体实现是先使用doc.getStringUnitWidth(text)计算文本的宽度,
* 再乘以当前字体大小和文本缩放比例的乘积得到真实的文本宽度,
* 然后用表格宽度减去文本宽度,再除以2,即可得到文本居中时的X轴坐标值。
* 最后将这个值加上表格的起始X轴坐标值x,即可得到文本在表格中的水平居中位置。
* */
}
此时调用该方法,就可以得到文本在矩形方框中间的位置:
// 表格
doc.rect(10, 10, 100, 20, 'S')
const title = '个人信息'
const titleX = this.getCenterX(doc, title, 10, 100)
doc.text(title, titleX, 10)
效果如下: 随后再根据需要调整高度即可。
react-pdf 预览
在react中实现边下载边预览功能, 需要安装另一个库
npm install pdfjs-dist
项目的目录结构如下:
├─PDFPreview
├─ style
│ └─ index.less
├─ index.tsx
├─ usePDF.ts
├─ Page.tsx
├─ Preview.tsx
如图所示:
usePDF.ts
import * as pdf from 'pdfjs-dist'
// @ts-ignore
import pdfWorker from 'pdfjs-dist/build/pdf.worker.js?url'
import { useEffect, useRef, useState } from 'react'
pdf.GlobalWorkerOptions.workerSrc = pdfWorker
export const usePDFData = (options: { src: string, scale?: number }) => {
const previewUrls = useRef<string[]>([])
const urls = useRef<string[]>([])
const [loading, setLoading] = useState(false) // 初始状态设置为true
const [currentPage, setCurrentPage] = useState(0)
const [numPages, setNumPages] = useState(0)
const [loadedPages, setLoadedPages] = useState(0) // 新增已下载的页数状态
useEffect(() => {
urls.current = []
setCurrentPage(0)
setNumPages(0)
setLoadedPages(0) // 每次重新加载时,将已下载的页数重置为0
// setLoading(true) // 将loading状态设为true,显示loading提示
;(async () => {
const pdfDocument = await pdf.getDocument(options.src).promise
const totalNumPages = pdfDocument.numPages
setNumPages(totalNumPages)
for (let i = 0; i < totalNumPages; i++) {
const page = await pdfDocument.getPage(i + 1)
const viewport = page.getViewport({ scale: options.scale || 2 })
const canvas = document.createElement('canvas')
canvas.width = viewport.width
canvas.height = viewport.height
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
const renderTask = page.render({
canvasContext: ctx,
viewport
})
await renderTask.promise
urls.current[i] = canvas.toDataURL('image/jpeg', 1)
previewUrls.current[i] = canvas.toDataURL('image/jpeg', 0.5)
setLoadedPages(i + 1) // 更新已下载的页数
}
setLoading(false) // 设置loading状态为false,隐藏loading提示
})()
}, [options.src])
return {
loading,
currentPage,
numPages,
urls: urls.current.slice(0, loadedPages), // 仅渲染已下载的页面
previewUrls: previewUrls.current.slice(0, loadedPages), // 仅渲染已下载的页面
setCurrentPage
}
}
Page.tsx
import { styled } from '@mui/styles'
import { useLayoutEffect, useRef } from 'react'
const Image = styled('img')({
marginBottom: 20,
width: '100%'
})
export const Page = (props: { src: string, io: any, index: number }) => {
const { io, src, index } = props
const ref = useRef<HTMLImageElement | null>(null)
useLayoutEffect(() => {
if (!ref.current) {
return
}
io.observe(ref.current)
ref.current?.setAttribute('index', String(index))
return () => io.unobserve(ref.current)
})
return (
<Image className="page" src={src} ref={ref} />
)
}
Preview.tsx
import React, { useEffect, useRef, useState } from 'react'
import { styled } from '@mui/styles'
import { usePDFData } from './usePDF'
import { Page } from './Page'
import { message } from 'antd'
const Box = styled('div')({
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
})
const Sidebar = styled('div')({
position: 'fixed',
height: '100vh',
boxSizing: 'border-box',
padding: '40px 0 20px',
background: 'rgb(34, 38, 45)',
overflowY: 'auto',
left: 0,
top: 0,
width: 300,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
textAlign: 'center'
})
const Preview = styled('div')({
width: '90vw',
paddingLeft: 300
})
const Image = styled('img')({
marginTop: 20,
width: 250,
border: '6px solid transparent',
cursor: 'pointer',
'&.active': {
borderColor: 'rgb(121, 162, 246)'
}
})
const PageNumber = styled('span')({
background: 'transparent',
fontSize: 14,
marginTop: 4,
color: '#fff'
})
export const PDFRender: React.FC<{ src: string }> = (props) => {
const { loading, urls, previewUrls, currentPage, setCurrentPage , numPages } = usePDFData({
src: props.src
})
const [api, contextHolder] = message.useMessage()
const io = useRef(new IntersectionObserver((entries) => {
entries.forEach(item => {
item.intersectionRatio >= 0.5 && setCurrentPage(Number(item.target.getAttribute('index')))
})
}, {
threshold: [0.5]
}))
useEffect(() => {
api.loading({
content: 'PDF loading...',
duration: 0,
key: 'pdfLoad'
}).then()
if (numPages) {
api.destroy('pdfLoad')
api.success({
content: 'Success!',
duration: 1
}).then()
}
}, [numPages])
const goPage = (i: number) => {
setCurrentPage(i) // 使用setCurrentPage来更新当前页码
document.querySelectorAll('.page')[i]!.scrollIntoView({ behavior: 'smooth' })
}
if (loading) {
return <div>loading...</div>
}
return (
<Box>
{contextHolder}
<Sidebar>
{previewUrls.map((item, i) => (
<React.Fragment key={item}>
<Image
src={item}
className={currentPage === i ? 'active' : ''}
onClick={() => goPage(i)}
/>
<PageNumber>{i + 1}</PageNumber>
</React.Fragment>
))}
</Sidebar>
<Preview>
{urls.slice(0, numPages).map((item, i) => ( // 仅渲染下载的页数范围内的页面
<Page index={i} io={io.current} src={item} key={item}/>
))}
</Preview>
</Box>
)
}
index.tsx
在此文件中使用PDFRender组件
import React, { useEffect } from 'react'
import { message } from 'antd'
import { PDFRender } from './Preview'
import './style/index.less'
const PDFPreview = () => {
// 可以通过任意方式获得pdf文件的地址
const filePath = JSON.parse(localStorage.getItem('pdfUrl') as string)
return (
// 样式在style/index.less中
<div className="pdf-container">
<PDFRender src={filePath as string}/>
</div>
)
}
export default PDFPreview
style/index.less
.pdf-container {
width: 98vw;
position: absolute;
padding: 30px;
top: 0;
left: 0;
bottom: 0;
right: 0;
z-index: 999;
background-color: #fff;
display: flex;
justify-content: center;
}