Export Image
Runnable Leaflet example: Export Image. Open the live demo, read the complete source, and copy the file into your project. No build step or API key.
single HTML file · CDN dependencies onlyinterface may contain Chinese labels
Source
<!--
示例:地图导出图片
要点:按当前视野拼接 OSM 瓦片到离屏 canvas、叠加 canvas 渲染器的矢量层、toDataURL 下载
依赖:Leaflet 1.9.4 · jsDelivr CDN
-->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta name="robots" content="noindex,follow">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>地图导出图片</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.css">
<style>
html,body{height:100%;margin:0}
body{display:flex;flex-direction:column;overflow:hidden;
font:13px/1.6 -apple-system,"Segoe UI","PingFang SC",sans-serif}
.bar{flex:0 0 auto;padding:8px 12px;background:#f6f7f9;border-bottom:1px solid #e3e6ea;
display:flex;gap:10px;align-items:center;font-size:12px;color:#58606e}
.bar button{font:inherit;font-size:12px;padding:4px 10px;border-radius:4px;cursor:pointer;
border:1px solid #ccd1d9;background:#fff;color:#16181d}
.bar button:hover{background:#eef0f3}
.bar b{color:#16181d;font-weight:600}
#map{flex:1 1 auto;min-height:0;position:relative}
.shot{position:absolute;left:12px;bottom:12px;z-index:600;background:#fff;padding:6px;
border-radius:6px;box-shadow:0 4px 14px rgba(0,0,0,.2);font-size:11px;color:#58606e}
.shot img{display:none;width:168px;height:112px;object-fit:cover;border-radius:3px;
background:#eef1f5;margin-bottom:4px}
.shot .ph{width:168px;height:112px;border-radius:3px;background:#eef1f5;
display:flex;align-items:center;justify-content:center;color:#9aa3b0;
font-size:11px;margin-bottom:4px}
</style>
</head>
<body>
<div class="bar">
<button id="shot">导出当前视野</button>
<span id="status">等待导出</span>
<span>画布 <b id="size">—</b></span>
</div>
<div id="map">
<div class="shot" id="shotbox">
<img id="preview" alt="导出预览">
<div class="ph" id="ph">点击上方按钮生成预览</div>
预览 · 实际文件为完整分辨率
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
(function () {
'use strict';
var map = L.map('map', {
center: [39.907359, 116.391263],
zoom: 14,
zoomSnap: 1
});
var OSM = 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}';
L.tileLayer(OSM, {
maxZoom: 19,
/* 打开跨域,图片才能画进 canvas 而不污染 */
crossOrigin: 'anonymous',
attribution: 'Tiles © Esri'
}).addTo(map);
/* padding 设 0,画布尺寸恰好等于地图视野,导出时无需换算偏移 */
var renderer = L.canvas({ padding: 0 }).addTo(map);
L.circle([39.907359, 116.391263], {
radius: 900,
renderer: renderer,
color: '#1f5fd0',
weight: 2,
fillColor: '#1f5fd0',
fillOpacity: 0.12
}).addTo(map);
L.polygon([
[39.915, 116.380], [39.921, 116.402],
[39.912, 116.412], [39.903, 116.398], [39.905, 116.382]
], {
renderer: renderer,
color: '#c2410c',
weight: 3,
dashArray: '7 5',
fillColor: '#f97316',
fillOpacity: 0.10
}).addTo(map);
var status = document.getElementById('status');
var preview = document.getElementById('preview');
var sizeEl = document.getElementById('size');
function loadTile(x, y, z, tileSize, origin, ctx) {
return new Promise(function (resolve) {
var img = new Image();
img.crossOrigin = 'anonymous';
img.onload = function () {
ctx.drawImage(img, x * tileSize - origin.x, y * tileSize - origin.y, tileSize, tileSize);
resolve();
};
img.onerror = function () { resolve(); };
img.src = OSM.replace('{z}', z).replace('{x}', x).replace('{y}', y);
});
}
map.on('moveend zoomend', function () {
var s = map.getSize();
sizeEl.textContent = s.x + ' × ' + s.y;
});
setTimeout(function () {
var s = map.getSize();
sizeEl.textContent = s.x + ' × ' + s.y;
}, 0);
document.getElementById('shot').addEventListener('click', function () {
var btn = this;
btn.disabled = true;
status.textContent = '拼接瓦片中…';
var size = map.getSize();
var z = map.getZoom();
var tileSize = 256;
var pb = map.getPixelBounds();
var out = document.createElement('canvas');
out.width = size.x;
out.height = size.y;
var ctx = out.getContext('2d');
ctx.fillStyle = '#e6ebf1';
ctx.fillRect(0, 0, size.x, size.y);
var min = pb.min.divideBy(tileSize).floor();
var max = pb.max.divideBy(tileSize).floor();
var jobs = [];
for (var x = min.x; x <= max.x; x++) {
for (var y = min.y; y <= max.y; y++) {
jobs.push(loadTile(x, y, z, tileSize, pb.min, ctx));
}
}
function annotate(c, size, z) {
c.fillStyle = 'rgba(255, 255, 255, 0.9)';
c.fillRect(0, size.y - 34, 236, 34);
c.fillStyle = '#16181d';
c.font = '600 13px sans-serif';
c.fillText('北京核心区 · z' + z, 12, size.y - 19);
c.fillStyle = '#58606e';
c.font = '11px sans-serif';
c.fillText('Tiles © Esri', 12, size.y - 6);
}
function savePng(c, okText) {
var url = c.toDataURL('image/png');
preview.src = url;
preview.style.display = 'block';
document.getElementById('ph').style.display = 'none';
var a = document.createElement('a');
a.href = url;
a.download = 'map-' + z + '-' + Date.now() + '.png';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
status.textContent = okText;
}
Promise.all(jobs).then(function () {
/* 矢量层是 canvas 渲染器画在同一视野坐标上的,直接按 0,0 叠上去 */
var layerCanvas = map.getPane('overlayPane').querySelector('canvas');
if (layerCanvas) ctx.drawImage(layerCanvas, 0, 0);
/* 图面注记手动绘制:HTML 标记不在 canvas 里 */
annotate(ctx, size, z);
try {
savePng(out, '已导出 ' + size.x + ' × ' + size.y + ' 像素');
} catch (err) {
/* 瓦片服务没开跨域时,退化为只导出矢量图层 */
var plain = document.createElement('canvas');
plain.width = size.x;
plain.height = size.y;
var pc = plain.getContext('2d');
pc.fillStyle = '#f4f6f8';
pc.fillRect(0, 0, size.x, size.y);
if (layerCanvas) pc.drawImage(layerCanvas, 0, 0);
annotate(pc, size, z);
try {
savePng(plain, '瓦片跨域受限,已导出矢量图层');
} catch (err2) {
status.textContent = '导出失败:画布无法读取像素';
}
}
btn.disabled = false;
});
});
})();
</script>
</body>
</html>
Language: the code and this page are in English; the full tutorial and API notes for this example are in Chinese. The demo itself is the authoritative version — it runs exactly as shown.