使用技巧

设置字体大小

可以为某处的字体设置大小

// 个人信息居中大号显示
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;
}
Last Updated:
Contributors: 黄定鑫