Web端截屏方案调研

46 阅读5分钟

话不多说,直接上方案~

方案对比

方案star实现成本原理特点live demo其他
HTML Canvas原生绘制-通过HTMLCanvasElementdrawImage方法,将页面元素(如<video><img>或DOM节点)绘制到Canvas,再通过toDataURLtoBlob生成图片如果需要区域截屏、对截图进行标注,需要额外开发**如果需要支持对页面任意结构截图,最好在html2canvas/dom-to-image基础上二次封装
第三方库(html2canvas31.3k需二次封装通过解析DOM结构,模拟浏览器渲染过程,将CSS样式和DOM节点转换为Canvas绘制指令,最终生成图片支持大部分CSS(flex、grid、伪类),但对SVG支持有限如果需要区域截屏、对截图进行标注,一些方案需要额外开发
第三方库(dom-to-image10.6k需二次封装核心原理是利用 SVG 的 标签嵌入 HTML,再通过 Canvas 渲染为图像支持更多现代CSS(如filter、clip-path)如果需要区域截屏、对截图进行标注,需要额外开发
js-screen-shot889> 引用- 获取当前可视区域的内容,将其存储起来- 为整个cnavas画布绘制蒙层- 在获取到的内容中进行拖拽,绘制镂空选区- 选择截图工具栏的工具,选择画笔大小等信息- 在选区内拖拽绘制对应的图形- 将选区内的内容转换为图片www.kaisir.cn/js-screen-s…文章分享:www.kaisir.cn/js-screen-s…
第三方库(region-screenshot-js52基于 dom-to-imagejquerygetDisplayMedia 实现实现选区框绘制、拖动缩放、标注工具集成,最终生成带标注的截图weijun-lab.github.io/region-scre…
视频捕获-通过 drawImage(video, 0, 0) 将当前视频帧绘制到 canvas调用 canvas.toDataURL('image/png') 生成图片数据截取视频某一帧

技术方案

基于HTML Canvas的原生绘制

原理:通过HTMLCanvasElementdrawImage方法,将页面元素(如<video><img>或DOM节点)绘制到Canvas,再通过toDataURLtoBlob生成图片。

适用场景

  • 截取特定DOM节点(如图表、卡片)

  • 截取视频帧或实时画面

实现步骤(React+TS示例)

// 截取指定Ref的DOM节点
const captureElement = async (ref: React.RefObject<HTMLElement>) => {
  const element = ref.current;
  if (!element) return null;
  // 获取元素尺寸
  const { width, height } = element.getBoundingClientRect();

  // 创建Canvas并绘制
  const canvas = document.createElement('canvas');
  canvas.width = width;
  canvas.height = height;
  const ctx = canvas.getContext('2d');
  // 绘制DOM节点(需处理跨域)
  const blob = await new Promise<Blob | null>(resolve =>
    html2canvas(element).then(canvas => canvas.toBlob(resolve))
  );

  return blob;
};

优缺点

优点缺点
无需第三方依赖复杂CSS(如阴影、伪类)丢失
支持实时视频帧截取跨域资源(图片、字体)无法绘制

第三方截图库(html2canvas/dom-to-image)

原理:通过解析DOM结构,模拟浏览器渲染过程,将CSS样式和DOM节点转换为Canvas绘制指令,最终生成图片。

实现示例(React+html2canvas)

import html2canvas from 'html2canvas';
const ScreenshotComponent = () => {
  const targetRef = useRef<HTMLDivElement>(null);
  const handleCapture = async () => {
    if (!targetRef.current) return;

    // 生成截图
    const canvas = await html2canvas(targetRef.current, {
      useCORS: true, // 解决跨域图片问题
      logging: false, // 关闭日志
      backgroundColor: null // 保留透明背景
    });

    // 生成下载链接
    const url = canvas.toDataURL('image/png');
    const a = document.createElement('a');
    a.href = url;
    a.download = 'screenshot.png';
    a.click();
  };
  return (
    <div>
      <div ref={targetRef} style={{ padding: '20px', border: '1px solid #ddd' }}>
        <h3>待截图内容</h3>
        <p>示例文本与图片:<img src="/assets/example.png" alt="示例" /></p>
      </div>
      <Button onClick={handleCapture}>截图</Button>
    </div>
  );
};

兼容性

  • 支持现代浏览器(Chrome 60+、Firefox 53+、Edge 79+)

  • 不支持IE(需polyfill)

  • 部分CSS特性(如box-shadow: insetbackdrop-filter)可能无法正确渲染

选区截屏插件(region-screenshot-js)

原理:基于Canvas和鼠标事件监听,实现选区框绘制、拖动缩放、标注工具集成,最终生成带标注的截图。

核心功能

  • 自由选区(矩形/圆形/多边形)

  • 标注工具(画笔、文字、表情、马赛克)

  • 截图参数配置(初始选区、输出格式)

实现示例(React+TS)

import RegionScreenshot from 'region-screenshot-js';
const RegionScreenshotTool = () => {
  const [screenshotUrl, setScreenshotUrl] = useState('');
  const handleCapture = () => {
    const screenshot = new RegionScreenshot({
      initialRegion: { top: 100, left: 100, width: 800, height: 600 }, // 初始选区
      customDrawing: [
        {
          className: 'emoji',
          optionsHtml: 
            <img class="active" src="/assets/emoji-1.png"/>             <img src="/assets/emoji-2.png"/>          
,
          onDrawingOpen: (canvas, options, save) => {
            canvas.onclick = (e) => {
              const img = options.querySelector('img.active') as HTMLImageElement;
              canvas.getContext('2d')?.drawImage(img, e.offsetX - 10, e.offsetY - 10);
              save();
            };
          }
        }
      ]
    });
    screenshot.on('screenshotGenerated', (url) => {
      setScreenshotUrl(url);
    });
  };
  return (
    <div>
      <Button onClick={handleCapture}>选区截图</Button>
      {screenshotUrl && <img src={screenshotUrl} alt="选区截图" />}
    </div>
  );
};

优势

  • 提供完整的交互组件(选区框、工具面板)

  • 支持自定义标注工具扩展

  • 输出包含标注的最终图片

视频捕获

关键技术点:

  • Canvas 绘图
  • 使用隐藏的 <canvas> 元素
  • 通过 drawImage(video, 0, 0) 将当前视频帧绘制到 canvas
  • 调用 canvas.toDataURL('image/png') 生成图片数据
const ctx = canvas.getContext('2d');
ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);
  • 视频时间控制

  • 通过 videoRef.current.currentTime 获取当前播放时间

  • 监听 onTimeUpdate 事件实时更新时间状态

实现示例:

import React, { useState, useRef, useEffect } from 'react';
import { Button, message, Upload, Modal, Image } from 'antd';
import { UploadOutlined, CameraOutlined } from '@ant-design/icons';
import { 
  containerStyle, 
  videoContainerStyle, 
  videoStyle, 
  controlsStyle, 
  previewStyle 
} from './styles';
import { VideoFrameCaptureProps, VideoState } from './types';

const VideoFrameCapture = ({ 
  onCapture,
  previewWidth = 300,
  previewHeight = 180 
}: VideoFrameCaptureProps) => {
  const [videoState, setVideoState] = useState<VideoState>({
    videoUrl: '',
    isLoaded: false,
    currentTime: 0,
  });
  const [capturePreview, setCapturePreview] = useState<string>('');
  const [isPreviewVisible, setIsPreviewVisible] = useState(false);
  const videoRef = useRef<HTMLVideoElement>(null);
  const canvasRef = useRef<HTMLCanvasElement>(null);

  // 处理视频文件上传
  const handleUpload = (file: File) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      if (e.target?.result) {
        setVideoState(prev => ({
          ...prev,
          videoUrl: e.target.result as string,
          isLoaded: false,
        }));
      }
    };
    reader.readAsDataURL(file);
    return false; // 阻止默认上传行为
  };

  // 视频加载完成事件
  const handleVideoLoaded = () => {
    setVideoState(prev => ({ ...prev, isLoaded: true }));
  };

  // 截取当前帧
  const captureFrame = () => {
    if (!videoRef.current || !canvasRef.current) return;

    const video = videoRef.current;
    const canvas = canvasRef.current;
    const ctx = canvas.getContext('2d');

    // 设置Canvas尺寸与视频一致
    canvas.width = video.videoWidth;
    canvas.height = video.videoHeight;

    // 绘制当前帧到Canvas
    ctx?.drawImage(video, 0, 0, canvas.width, canvas.height);

    // 生成图片Data URL
    const dataUrl = canvas.toDataURL('image/png');
    setCapturePreview(dataUrl);
    setIsPreviewVisible(true);
    onCapture?.(dataUrl);
    message.success('截图成功!');
  };

  // 视频时间更新监听
  const handleTimeUpdate = () => {
    if (videoRef.current) {
      setVideoState(prev => ({
        ...prev,
        currentTime: videoRef.current.currentTime,
      }));
    }
  };

  return (
    <div style={containerStyle}>
      <h2>视频帧截取</h2>

      {/* 视频上传 */}
      <Upload
        accept="video/*"
        beforeUpload={handleUpload}
        showUploadList={false}
      >
        <Button icon={<UploadOutlined />}>上传视频文件</Button>
      </Upload>

      {/* 视频播放器 */}
      {videoState.videoUrl && (
        <div style={videoContainerStyle}>
          <video
            ref={videoRef}
            style={videoStyle}
            src={videoState.videoUrl}
            controls
            onLoadedData={handleVideoLoaded}
            onTimeUpdate={handleTimeUpdate}
            aria-label="视频播放器"
          />
          
          {/* 隐藏的Canvas用于截图 */}
          <canvas ref={canvasRef} style={{ display: 'none' }} />
        </div>
      )}

      {/* 操作按钮 */}
      <div style={controlsStyle}>
        <Button
          type="primary"
          icon={<CameraOutlined />}
          onClick={captureFrame}
          disabled={!videoState.isLoaded}
          aria-label="截取当前帧"
        >
          截取当前帧
        </Button>
      </div>

      {/* 截图预览模态框 */}
      <Modal
        title="截图预览"
        visible={isPreviewVisible}
        onOk={() => setIsPreviewVisible(false)}
        onCancel={() => setIsPreviewVisible(false)}
        width={previewWidth + 48} // 加上模态框内边距
      >
        <div style={previewStyle}>
          <Image
            src={capturePreview}
            width={previewWidth}
            height={previewHeight}
            alt="截取帧"
          />
        </div>
      </Modal>
    </div>
  );
};

export default VideoFrameCapture;