ArcGIS JS 基础教程(12):视图截图 takeScreenshot

ArcGIS JS 基础教程(12):视图截图 takeScreenshot

零、写在前面

📌 本系列教程完整目录ArcGIS JS 系列基础教程(100个项目常用热门功能)

💡 在线示例 :完整可运行的 HTML 示例,无需任何环境配置,可直接在浏览器中打开体验

🗂️ 专栏导航 :收藏 + 关注,专栏文章第一时间送达

❤️ 一键三连:点赞(给教程充电)+ 评论(提问必回)+ 收藏(下次再看)


一、功能介绍

在 WebGIS 开发中,经常需要将当前地图视图保存为图片------用于生成报告、分享成果、制作演示稿等。ArcGIS Maps SDK for JavaScript 提供了 SceneView.takeScreenshot() 方法,可以一键捕获当前三维场景的高清截图

takeScreenshot() 的核心特性:

  • 异步截图:返回 Promise,在场景渲染完成后自动捕获当前帧
  • 格式可选:支持 PNG(无损)和 JPEG / JPG(有损压缩)
  • 自定义尺寸:可以指定输出图片的宽高(像素),实现 4K/8K 高清截图
  • 质量控制:JPEG 格式支持 0~1 质量参数,平衡清晰度和文件大小
  • 直接下载 :返回对象的 dataUrl 属性可直接用于 <a> 下载或 <img> 预览

二、功能实现

核心 API: SceneView.takeScreenshot(options?),返回 Promise<{ dataUrl: string }>(5.0 中返回带 dataUrl 属性的对象)。

2.1 基础截图

javascript 复制代码
// 最简单的用法:捕获当前视图,默认 PNG 格式
view.takeScreenshot().then(shot => {
    // shot.dataUrl 是 base64 编码的图片数据
    console.log("截图完成!", shot.dataUrl.substring(0, 100) + "...");
});

2.2 指定输出格式

javascript 复制代码
// PNG 截图(默认,无损)
const pngShot = await view.takeScreenshot({ format: "png" });

// JPEG 截图(有损压缩,文件更小)
const jpgShot = await view.takeScreenshot({
    format: "jpg",
    quality: 0.8            // 0~1,默认 0.92。值越小文件越小但质量越低
});
console.log(jpgShot.dataUrl);  // 直接取 dataUrl 属性

2.3 自定义图片尺寸

默认情况下截图尺寸等于当前视口尺寸。指定 width / height 可以生成更高分辨率的图片:

javascript 复制代码
// 生成 4K(3840x2160)高清截图
const shot = await view.takeScreenshot({
    width: 3840,
    height: 2160,
    format: "png"
});
// shot.dataUrl 中包含 4K 分辨率的 base64 图片数据

⚠️ 注意: 超大尺寸截图会消耗更多内存和时间,并且可能受限于浏览器和 GPU 的性能上限。

2.4 下载截图为图片文件

直接使用 dataUrl 触发浏览器下载:

javascript 复制代码
async function downloadScreenshot() {
    const shot = await view.takeScreenshot({
        format: "png",
        width: 1920,
        height: 1080
    });

    // 通过 dataUrl 创建下载链接
    const link = document.createElement("a");
    link.href = shot.dataUrl;
    link.download = "screenshot_" + Date.now() + ".png";
    document.body.appendChild(link);
    link.click();
    document.body.removeChild(link);
}

2.5 在新窗口预览截图

javascript 复制代码
async function previewScreenshot() {
    const shot = await view.takeScreenshot({ format: "png" });

    // 在新窗口打开截图
    const win = window.open("", "_blank");
    win.document.write(`<img src="${shot.dataUrl}" style="max-width:100%;" />`);
    win.document.title = "场景截图预览";
}

2.6 格式与质量对比

格式 有无损 质量参数 典型文件大小(1920×1080) 适用场景
"png" ✅ 无损 不支持 ~3-8 MB 需要清晰细节的报告、印刷输出
"jpg" ❌ 有损 0.5~1.0 ~150-500 KB (q=0.8) 网页展示、邮件附件、快速分享
"jpeg" ❌ 有损 0.5~1.0 同 jpg 同 jpg(别名)

2.7 完整参数说明

javascript 复制代码
view.takeScreenshot({
    format: "png",           // "png" | "jpg" | "jpeg",默认 "png"
    quality: 0.92,           // JPEG 质量 0~1,PNG 忽略此参数,默认 0.92
    width: 1920,             // 输出宽度(像素),默认 = 当前视口宽度
    height: 1080             // 输出高度(像素),默认 = 当前视口高度
});

三、功能应用

应用场景 格式 尺寸 说明
生成项目汇报截图 PNG 1920×1080 无损细节,适合插入 PPT/文档
网页快速分享 JPEG, q=0.7 视口尺寸 文件小,适合聊天/邮件
4K 高清出品图 PNG 3840×2160 宣传物料、大屏展示
自动定期截图(监控) JPEG, q=0.5 1280×720 节省存储空间,时序对比
截图预览弹窗 PNG 视口尺寸 截图后在新窗口查看
带水印下载 PNG 1920×1080 截图后通过 Canvas 叠加水印再下载

四、核心代码

📦 完整代码 已保存至 sample/lesson12_take_screenshot.html,可直接在浏览器打开。

html 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>第12课:视图截图 takeScreenshot</title>
    <link rel="stylesheet" href="https://js.arcgis.com/5.0/esri/themes/light/main.css">
    <script type="module" src="https://js.arcgis.com/5.0/"></script>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { font-family: "Microsoft YaHei", sans-serif; }
        #mapContainer { width: 100vw; height: 100vh; }
        .page-title {
            position: absolute;
            top: 20px; left: 50%;
            transform: translateX(-50%);
            background: rgba(255,255,255,0.95);
            padding: 10px 24px; border-radius: 6px;
            font-size: 18px; font-weight: bold;
            z-index: 100;
            box-shadow: 0 2px 8px rgba(0,0,0,0.15);
        }
        .control-panel {
            position: absolute;
            top: 80px; right: 20px;
            background: rgba(255,255,255,0.95);
            padding: 16px; border-radius: 8px;
            box-shadow: 0 2px 12px rgba(0,0,0,0.15);
            z-index: 100; min-width: 300px;
        }
        .control-panel h3 { margin: 0 0 10px 0; font-size: 14px; color: #333; }
        .section { margin-bottom: 14px; padding-bottom: 10px; border-bottom: 1px solid #eee; }
        .section:last-child { border-bottom: none; margin-bottom: 0; }
        .btn-row { display: flex; gap: 8px; flex-wrap: wrap; }
        .btn-row button {
            flex: 1; min-width: 80px; padding: 8px 0;
            border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 13px;
            transition: all 0.2s;
        }
        .btn-row button:hover { border-color: #1890ff; color: #1890ff; }
        .btn-row button.primary { background: #1890ff; color: white; border-color: #1890ff; }
        .btn-row button.primary:hover { background: #40a9ff; }
        .form-group { margin-bottom: 10px; }
        .form-group label { display: block; font-size: 12px; color: #666; margin-bottom: 4px; }
        .form-group select, .form-group input { width: 100%; padding: 5px 8px; border: 1px solid #d9d9d9; border-radius: 4px; font-size: 12px; }
        .preview-modal {
            display: none;
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: rgba(0,0,0,0.8); z-index: 999;
            justify-content: center; align-items: center;
        }
        .preview-modal.show { display: flex; }
        .preview-modal .modal-content {
            max-width: 90vw; max-height: 90vh;
            background: white; border-radius: 8px; padding: 12px;
            box-shadow: 0 8px 32px rgba(0,0,0,0.3);
            position: relative;
        }
        .preview-modal img { max-width: 100%; max-height: 80vh; display: block; }
        .preview-modal .close-btn {
            position: absolute; top: -12px; right: -12px;
            width: 32px; height: 32px; border-radius: 50%;
            background: #ff4d4f; color: white; border: none;
            font-size: 18px; cursor: pointer; line-height: 32px; text-align: center;
        }
        .preview-modal .modal-actions { margin-top: 10px; display: flex; gap: 8px; justify-content: center; }
        .preview-modal .modal-actions button {
            padding: 6px 16px; border: 1px solid #d9d9d9; border-radius: 4px;
            background: white; cursor: pointer; font-size: 13px;
        }
        .preview-modal .modal-actions button:hover { border-color: #1890ff; color: #1890ff; }
        .status-text {
            position: absolute; bottom: 20px; left: 50%;
            transform: translateX(-50%);
            background: rgba(0,0,0,0.7); color: white;
            padding: 8px 20px; border-radius: 20px;
            font-size: 13px; z-index: 100; pointer-events: none;
            white-space: nowrap;
        }
    </style>
</head>
<body>
<h1 class="page-title">第12课:视图截图 takeScreenshot</h1>

<div class="control-panel">
    <div class="section">
        <h3>📸 快速操作</h3>
        <div class="btn-row">
            <button id="btnScreenshot" class="primary">📸 截图</button>
            <button id="btnDownload">💾 下载 PNG</button>
            <button id="btnDownloadJpg">📦 下载 JPG</button>
        </div>
        <div class="btn-row" style="margin-top:6px;">
            <button id="btnPreview">🔍 预览截图</button>
            <button id="btn4K">🖥️ 4K 高清截图</button>
        </div>
    </div>

    <div class="section">
        <h3>⚙️ 截图参数</h3>
        <div class="form-group">
            <label>格式(format):</label>
            <select id="selFormat">
                <option value="png">PNG(无损)</option>
                <option value="jpg">JPEG(有损压缩)</option>
            </select>
        </div>
        <div class="form-group">
            <label>质量(quality,仅 JPEG):</label>
            <select id="selQuality">
                <option value="0.5">低 0.5(文件最小)</option>
                <option value="0.7">中 0.7</option>
                <option value="0.85" selected>高 0.85(推荐)</option>
                <option value="1.0">最高 1.0</option>
            </select>
        </div>
        <div class="form-group">
            <label>自定义宽度(像素,留空=视口宽):</label>
            <input type="number" id="inputWidth" placeholder="如 1920" min="100" max="7680">
        </div>
        <div class="form-group">
            <label>自定义高度(像素,留空=视口高):</label>
            <input type="number" id="inputHeight" placeholder="如 1080" min="100" max="4320">
        </div>
    </div>
</div>

<div class="status-text" id="statusText">📸 takeScreenshot | 点击按钮截图</div>

<div class="preview-modal" id="previewModal">
    <div class="modal-content">
        <button class="close-btn" id="btnClosePreview">✕</button>
        <img id="previewImage" src="" alt="截图预览" />
        <div class="modal-actions">
            <button id="btnModalDownload">💾 下载此图</button>
        </div>
    </div>
</div>

<div id="mapContainer"></div>

<script type="module">
    const Map = await $arcgis.import("@arcgis/core/Map.js");
    const SceneView = await $arcgis.import("@arcgis/core/views/SceneView.js");
    const GraphicsLayer = await $arcgis.import("@arcgis/core/layers/GraphicsLayer.js");
    const Graphic = await $arcgis.import("@arcgis/core/Graphic.js");
    const Mesh = await $arcgis.import("@arcgis/core/geometry/Mesh.js");
    const Point = await $arcgis.import("@arcgis/core/geometry/Point.js");
    const getTianditu = await $arcgis.import("https://openlayers.vip/examples/resources/tianditu.js");

    // 天地图底图 + 高程
    const vecLayers = getTianditu.default({ type: "vec_w" });
    const map = new Map({
        basemap: { baseLayers: [vecLayers.base, vecLayers.anno] },
        ground: {
            surface: {
                elevationLayers: [{
                    url: "https://www.geosceneonline.cn/image/rest/services/OpenData/ChinaTerrain3D/ImageServer/"
                }]
            }
        }
    });

    const view = new SceneView({
        container: "mapContainer",
        map: map,
        camera: {
            position: { longitude: 116.397, latitude: 39.917, z: 3000 },
            heading: 30,
            tilt: 50
        }
    });
    window.view = view;

    let lastCanvas = null;   // 保存最近一次截图结果

    // ===== 添加 3D 场景元素(让截图更丰富) =====
    function addSceneObjects() {
        const layer = new GraphicsLayer({
            title: "场景元素",
            castShadows: true,
            receiveShadows: true
        });

        // 中心金色大楼
        layer.add(new Graphic({
            geometry: Mesh.createBox(
                new Point({ longitude: 116.397, latitude: 39.917, z: 0 }),
                { size: { width: 300, height: 500, depth: 300 } }
            ),
            symbol: {
                type: "mesh-3d",
                symbolLayers: [{ type: "fill", material: { color: [220, 180, 80, 0.9] } }]
            }
        }));

        // 周围小建筑群
        [[0.004, 0.003], [-0.004, 0.002], [0.003, -0.003], [-0.003, -0.002]].forEach(([dx, dy]) => {
            layer.add(new Graphic({
                geometry: Mesh.createBox(
                    new Point({ longitude: 116.397 + dx, latitude: 39.917 + dy, z: 0 }),
                    { size: { width: 120, height: 200 + Math.random() * 150, depth: 120 } }
                ),
                symbol: {
                    type: "mesh-3d",
                    symbolLayers: [{ type: "fill", material: { color: [150 + Math.random() * 80, 160, 200, 0.9] } }]
                }
            }));
        });

        map.add(layer);
    }

    // ===== 读取截图参数 =====
    function getScreenshotOptions() {
        const format = document.getElementById("selFormat").value;
        const quality = parseFloat(document.getElementById("selQuality").value);
        const width = document.getElementById("inputWidth").value
            ? parseInt(document.getElementById("inputWidth").value) : undefined;
        const height = document.getElementById("inputHeight").value
            ? parseInt(document.getElementById("inputHeight").value) : undefined;

        const opts = { format };
        if (format === "jpg" || format === "jpeg") opts.quality = quality;
        if (width) opts.width = width;
        if (height) opts.height = height;
        return opts;
    }

    function getMimeType(format) {
        return format === "jpg" || format === "jpeg" ? "image/jpeg" : "image/png";
    }

    function getFileExt(format) {
        return format === "jpg" || format === "jpeg" ? "jpg" : "png";
    }

    // ===== 下载截图(5.0 返回 { dataUrl } 对象) =====
    function downloadScreenshot(screenshot, format) {
        const ext = getFileExt(format);
        const dataUrl = screenshot.dataUrl;
        const link = document.createElement("a");
        link.href = dataUrl;
        link.download = `screenshot_${Date.now()}.${ext}`;
        document.body.appendChild(link);
        link.click();
        document.body.removeChild(link);
    }

    function setStatus(msg, duration) {
        document.getElementById("statusText").textContent = msg;
        if (duration) setTimeout(() => {
            document.getElementById("statusText").textContent = "📸 takeScreenshot | 点击按钮截图";
        }, duration);
    }

    // ===== 初始化 =====
    view.when(() => {
        console.log("场景加载完成");
        addSceneObjects();

        // 截图
        document.getElementById("btnScreenshot").addEventListener("click", async () => {
            setStatus("⏳ 正在截图...");
            const opts = getScreenshotOptions();
            lastCanvas = await view.takeScreenshot(opts);
            const dataUrl = lastCanvas.dataUrl;
            setStatus(
                `✅ 截图完成!格式:${opts.format.toUpperCase()}` +
                `,DataURL 长度:${Math.round(dataUrl.length / 1024)}KB`,
                5000
            );
            console.log("截图完成");
        });

        // 下载 PNG
        document.getElementById("btnDownload").addEventListener("click", async () => {
            setStatus("⏳ 正在生成 PNG 截图...");
            const shot = await view.takeScreenshot({ format: "png" });
            lastCanvas = shot;
            downloadScreenshot(shot, "png");
            setStatus("✅ PNG 下载已触发!", 3000);
        });

        // 下载 JPG
        document.getElementById("btnDownloadJpg").addEventListener("click", async () => {
            setStatus("⏳ 正在生成 JPG 截图...");
            const shot = await view.takeScreenshot({
                format: "jpg",
                quality: parseFloat(document.getElementById("selQuality").value)
            });
            lastCanvas = shot;
            downloadScreenshot(shot, "jpg");
            setStatus("✅ JPG 下载已触发!质量:" + document.getElementById("selQuality").value, 3000);
        });

        // 预览截图
        document.getElementById("btnPreview").addEventListener("click", async () => {
            setStatus("⏳ 正在生成预览...");
            const shot = lastCanvas || await view.takeScreenshot(getScreenshotOptions());
            lastCanvas = shot;
            document.getElementById("previewImage").src = shot.dataUrl;
            document.getElementById("previewModal").classList.add("show");
            setStatus("🔍 截图预览已打开");
        });

        // 关闭预览
        document.getElementById("btnClosePreview").addEventListener("click", () => {
            document.getElementById("previewModal").classList.remove("show");
        });
        document.getElementById("previewModal").addEventListener("click", (e) => {
            if (e.target === e.currentTarget) {
                document.getElementById("previewModal").classList.remove("show");
            }
        });

        // 预览中的下载按钮
        document.getElementById("btnModalDownload").addEventListener("click", () => {
            if (lastCanvas) {
                const opts = getScreenshotOptions();
                downloadScreenshot(lastCanvas, opts.format);
                setStatus("✅ 截图已下载!", 3000);
            }
        });

        // 4K 高清截图
        document.getElementById("btn4K").addEventListener("click", async () => {
            setStatus("⏳ 正在生成 4K 高清截图...");
            const shot = await view.takeScreenshot({
                format: "png",
                width: 3840,
                height: 2160
            });
            lastCanvas = shot;
            downloadScreenshot(shot, "png");
            setStatus("✅ 4K 截图下载已触发!尺寸:3840×2160px", 5000);
        });
    });
</script>
</body>
</html>

五、在线示例

🔗 在线体验地址(GitHub资源,等待时间较长,或者架梯子)https://southjor.github.io/arcgis-examples/lessons/lesson12.html

操作说明

  1. 调整视角(拖拽/缩放/旋转),点击「📸 截图」捕获当前视图
  2. 点击「💾 下载 PNG」一键截图并下载为 PNG 文件
  3. 点击「📦 下载 JPG」截图并下载为 JPG,注意对比文件大小
  4. 修改格式(PNG/JPEG)和质量(0.5~1.0),再截图观察差异
  5. 设置自定义宽度/高度(如 3840×2160),生成超高清截图
  6. 点击「🖥️ 4K 高清截图」一键生成 3840×2160 截图并下载
  7. 点击「🔍 预览截图」在弹窗中查看截图效果,弹窗中也支持下载

六、关键API说明

API 类型 默认值 说明
view.takeScreenshot(options?) 方法 --- 异步截取当前视图,返回 Promise<{ dataUrl: string }>
options.format "png" | "jpg" | "jpeg" "png" 输出图片格式。PNG 无损,JPEG 有损
options.quality number (0~1) 0.92 JPEG 质量参数。仅对 JPEG 格式生效,PNG 忽略
options.width number 视口宽度 输出图片宽度(像素)
options.height number 视口高度 输出图片高度(像素)
shot.dataUrl string --- 返回对象的 base64 Data URL,可直接用于 <img src><a href>

常见坑点

问题 原因 解决方案
截图内容与屏幕不一致 UI 控件(标题栏、按钮)也被截入 截图是纯 WebGL 渲染内容,UI HTML 不会出现在截图中
PNG 文件很大(>5MB) PNG 无损压缩,场景越复杂文件越大 换用 JPEG 格式 + quality: 0.7~0.85
quality 参数设置后无效 quality 仅对 JPEG 有效,PNG 忽略 确认 format: "jpg"
超宽尺寸截图失败 GPU 内存不足或浏览器限制 降低 width/height,或分块截图后拼接
toBlob is not a function 5.0 返回的是 { dataUrl } 对象而非 Canvas 直接使用 shot.dataUrl 下载/预览

七、系列导航

⬅️ 上一篇ArcGIS JS 基础教程(11):飞行定位 goTo

➡️ 下一篇ArcGIS JS 基础教程(13):屏幕坐标与地理坐标互转


💡 小贴士takeScreenshot() 返回的是 HTMLCanvasElement------这意味着你可以在截图后使用 Canvas API 进行二次加工!比如叠加水印文字、绘制标注箭头、合成图例,甚至在截图上画热力圈。只要你能用 Canvas 画出来,就能在截图基础上实现。