SVG เป็น Canvas: โหลดภาพก่อนส่งออก PNG
โหลด SVG ทั้งไฟล์ด้วย Image.decode วาดตามขนาดบิตแมปที่เลือก ทดสอบ CORS และอ่าน PNG แล้วเก็บต้นฉบับแก้ไขได้แยกจากพิกเซลส่งออก
ในหน้านี้
หากต้องการวาด SVG ทั้งไฟล์บน canvas ให้โหลดเป็นภาพ รอ image.decode() แล้วส่งภาพเข้า drawImage() จากนั้นตรวจส่งออก PNG แยก ภาพอาจเห็นบน canvas แม้เบราว์เซอร์บล็อกการอ่านหรือบันทึกพิกเซล
วิธีนี้ระบายเอกสาร SVG เป็นภาพ ไม่ให้เส้น SVG แก้ไขได้กับ canvas หากต้องการสร้างบางเส้นขอบใหม่ ใช้คู่มือ Path2D SVG เก็บ SVG ต้นฉบับไม่ว่าจะเลือกทางแสดงใด
แยกถอดรหัส ระบาย และส่งออก
HTMLImageElement เป็นอินพุตที่ canvas drawImage() รับ ความกว้างสูงปลายทางควบคุมขนาดระบาย Image.decode() คืน promise ที่สำเร็จเมื่อภาพถอดรหัสแล้ว คำขอล้มเหลวหรือปัญหาถอดรหัสอาจทำให้ถูกปฏิเสธ
ถอดรหัสสำเร็จเป็นเพียงการตรวจแรก ภาพข้ามต้นทางที่โหลดโดยไม่มีสิทธิ์ CORS ยังวาดได้ แต่ทำให้ canvas ปนเปื้อน การอ่านพิกเซลและส่งออกจะล้มเหลว คู่มือภาพเปิด CORS ของ MDN อธิบายความต่าง

สร้าง SVG ต้นฉบับครบในตัว
บันทึกเป็น master.svg มีหนึ่งเส้นและหนึ่งวงกลม ไม่มีภาพฝัง ฟอนต์ หรือการพึ่งภายนอก รูปข้าวหลามตัดด้านในและพื้นที่รอบโปร่งใสในต้นฉบับ
<svg xmlns="http://www.w3.org/2000/svg" width="240" height="160" viewBox="0 0 240 160">
<path d="M96 24 L152 80 L96 136 L40 80 Z M96 58 L118 80 L96 102 L74 80 Z" fill="#176b5b" fill-rule="evenodd"/>
<circle cx="200" cy="80" r="24" fill="#d4a53a"/>
</svg>เราใช้ทั้งไฟล์เป็นภาพ รวมสองรูปและ viewBox ต่างจากดึงเฉพาะค่า d ของเส้น วิธีนี้ให้ตัวเรนเดอร์ภาพ SVG เบราว์เซอร์ระบายเอกสาร
ตัวอย่างตั้งใจครบในตัว SVG ที่ใช้เป็นภาพมีข้อจำกัด รวมทรัพยากรภายนอกและสคริปต์ ดูคู่มือ SVG เป็นภาพของ MDN อย่าถือว่าถอดรหัสไฟล์เล็กนี้สำเร็จพิสูจน์ว่า SVG ใด ๆ จะโหลดฟอนต์ สไตล์ หรือภาพเชื่อมภายนอกได้ สำหรับทางส่งงาน เทียบ SVG inline กับองค์ประกอบภาพ
รันสองต้นทางในเครื่องที่ตอบ CORS ต่างกัน
ต้นทางรวมพอร์ตด้วย เซิร์ฟเวอร์เฉพาะในเครื่องต่อไปให้ไบต์ SVG เดียวกันผ่านสองต้นทาง ต้นทางที่สอง /cors/master.svg มี Access-Control-Allow-Origin: * ส่วน /plain/master.svg ไม่มี จึงแยกสิทธิ์คำตอบออกจากตัวภาพ
บันทึกเป็น serve-proof.py ข้าง SVG กับ HTML ด้านล่าง รัน python3 serve-proof.py แล้วเปิด http://127.0.0.1:8770/starter.html ทั้งสองพอร์ตต้องว่าง หากเปลี่ยนให้อัปเดตเซิร์ฟเวอร์และ HTML พร้อมกัน กด Enter ในเทอร์มินัลเพื่อหยุดทั้งสองเซิร์ฟเวอร์
from http.server import ThreadingHTTPServer, SimpleHTTPRequestHandler
from pathlib import Path
from threading import Thread
root = Path(__file__).parent
class Handler(SimpleHTTPRequestHandler):
def __init__(self, *args, **kwargs):
super().__init__(*args, directory=str(root), **kwargs)
def do_GET(self):
if self.path in ['/cors/master.svg', '/plain/master.svg']:
payload = (root / 'master.svg').read_bytes()
self.send_response(200)
self.send_header('Content-Type', 'image/svg+xml')
self.send_header('Cache-Control', 'no-store')
if self.path.startswith('/cors/'):
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
self.wfile.write(payload)
else:
super().do_GET()
servers = [ThreadingHTTPServer(('127.0.0.1', port), Handler) for port in (8770, 8783)]
for server in servers:
Thread(target=server.serve_forever, daemon=True).start()
try:
input('Original SVG proof on 127.0.0.1:8770 and :8783. Enter to stop.\n')
finally:
for server in servers:
server.shutdown()
server.server_close()เฮดเดอร์เปิดกว้างนี้เป็นของการทดสอบในเครื่องที่ให้ภาพของเราเอง สำหรับโฮสต์ภาพใช้งานจริง ให้ตั้งต้นทางที่อนุญาตอย่างเหมาะสมบนเซิร์ฟเวอร์ที่คุณควบคุม การตั้งแอตทริบิวต์ภาพไม่ได้ให้สิทธิ์เนื้อหาอีกเซิร์ฟเวอร์
ถอดรหัสภาพก่อนวาด
บันทึกหน้าครบนี้เป็น starter.html แต่ละกรณีมี canvas ของตน จึงไม่ให้กรณีปนเปื้อนส่งผลต่อกรณีสำเร็จ ตั้ง crossOrigin ก่อน src สำหรับคำขอที่ต้องใช้ CORS ตามเอกสาร crossOrigin ของ MDN
บิตแมป canvas ระบุ 480 × 320 ส่วนขนาดแสดง CSS คือ 240 × 160 ภาพต้นฉบับมีขนาดธรรมชาติ 240 × 160 และ drawImage() ปรับลงขนาดบิตแมปที่เลือก นี่เป็นขนาดที่เลือก ไม่ใช่การวัดอัตราส่วนพิกเซลอุปกรณ์
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Original SVG to canvas export</title>
<style>
body { margin: 24px; font: 16px system-ui; background: #f6f2e8; }
main { display: grid; grid-template-columns: repeat(2, 280px); gap: 20px; }
figure { margin: 0; }
canvas { display: block; width: 240px; height: 160px; background: white; }
figcaption { margin: 6px 0; }
pre { white-space: pre-wrap; font-size: 12px; }
</style>
<h1>Decode, paint, then check PNG access</h1>
<main id="cases"></main>
<pre id="report"></pre>
<script>
function pngBlob(canvas) {
return new Promise((resolve, reject) => {
try { canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error('Empty PNG')), 'image/png'); }
catch (error) { reject(error); }
});
}
function dataURL(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsDataURL(blob);
});
}
async function render(test) {
const figure = document.createElement('figure');
const canvas = document.createElement('canvas');
canvas.width = 480;
canvas.height = 320;
const caption = document.createElement('figcaption');
figure.append(canvas, caption);
document.getElementById('cases').append(figure);
const result = { id: test.id, url: new URL(test.src, location.href).href,
crossOrigin: test.crossOrigin ?? null, canvasWidth: canvas.width, canvasHeight: canvas.height };
const image = new Image();
if (test.crossOrigin) image.crossOrigin = test.crossOrigin;
image.src = result.url;
try { await image.decode(); }
catch (error) {
result.stage = 'decode';
result.error = error.name;
caption.textContent = test.id + ': decode ' + error.name;
return result;
}
result.naturalWidth = image.naturalWidth;
result.naturalHeight = image.naturalHeight;
const context = canvas.getContext('2d');
context.drawImage(image, 0, 0, canvas.width, canvas.height);
result.stage = 'painted';
try {
result.opening = [...context.getImageData(192, 160, 1, 1).data];
result.fill = [...context.getImageData(192, 80, 1, 1).data];
result.corner = [...context.getImageData(0, 0, 1, 1).data];
} catch (error) { result.readbackError = error.name; }
try {
const blob = await pngBlob(canvas);
result.pngType = blob.type;
result.pngBytes = blob.size;
result.pngDataURL = await dataURL(blob);
result.export = 'ok';
const download = document.createElement('a');
download.href = result.pngDataURL;
download.download = test.id + '.png';
download.textContent = 'Save PNG';
figure.append(download);
} catch (error) { result.export = 'failed'; result.exportError = error.name; }
caption.textContent = test.id + ': ' + result.stage + ', PNG ' + result.export;
return result;
}
async function run() {
const tests = [
{ id: 'same-origin', src: 'master.svg' },
{ id: 'cors-allowed', src: 'http://127.0.0.1:8783/cors/master.svg', crossOrigin: 'anonymous' },
{ id: 'no-cors-image', src: 'http://127.0.0.1:8783/plain/master.svg' },
{ id: 'cors-denied', src: 'http://127.0.0.1:8783/plain/master.svg', crossOrigin: 'anonymous' },
{ id: 'missing-file', src: 'missing.svg' },
];
const results = [];
for (const test of tests) results.push(await render(test));
document.getElementById('report').textContent = JSON.stringify({ pageOrigin: location.origin, results }, (key, value) => key === 'pngDataURL' ? undefined : value, 2);
}
run();
</script>
</html>ฟังก์ชันส่งออกจัดการทั้ง exception ที่โยนและ blob ว่าง Canvas.toBlob() รองรับ PNG และอาจโยน SecurityError เมื่อ canvas ไม่สะอาดด้านต้นทาง สองกรณีสำเร็จเพิ่มลิงก์ Save PNG รายงานที่เห็นละข้อมูล base64
อ่านขั้นที่ล้มเหลวก่อนเปลี่ยน SVG
เรารันตัวอย่างนี้ตรง ๆ ในสภาพแวดล้อม Chrome หนึ่งชุด แล้วเปิด PNG ที่บันทึกทั้งคู่ใหม่ ข้อสังเกตเกี่ยวกับไฟล์ต้นฉบับครบในตัวและสองต้นทางในเครื่องนี้
| กรณี | คำขอภาพ | ผลวาด/อ่าน/ส่งออกที่เห็น |
|---|---|---|
| ต้นทางเดียว | SVG ในเครื่อง ไม่มีแอตทริบิวต์ CORS | ถอดรหัส 240 × 160 ระบาย อ่านและ PNG สำเร็จ |
| ข้ามต้นทาง อนุญาต | anonymous บวกเฮดเดอร์ตอบ | ถอดรหัส ระบาย อ่านและ PNG สำเร็จ |
| ข้ามต้นทาง ไม่ขอ CORS | ไม่มีแอตทริบิวต์และเฮดเดอร์ตอบ | ถอดรหัสและระบาย แต่อ่านกับส่งออกโยน SecurityError |
| ข้ามต้นทาง ไม่อนุญาต | anonymous ไม่มีเฮดเดอร์ตอบ | ถอดรหัสถูกปฏิเสธด้วย EncodingError ไม่ได้ลองวาด |
| ไฟล์หาย | 404 ต้นทางเดียว | ถอดรหัสถูกปฏิเสธด้วย EncodingError ไม่ได้ลองวาด |
กรณีไม่มี CORS เห็นภาพจริง การเห็นไม่ได้ยืนยันสิทธิ์อ่านหรือส่งออก กรณี CORS ไม่อนุญาตและไฟล์หายล้มก่อน การเขียนรูปทรงใหม่จึงไม่แก้ความล้มเหลวของคำขอเหล่านั้น
PNG ส่งออกทั้งคู่เปิดใหม่ที่ 480 × 320 ในการรันนี้ไบต์ตรงกัน แต่ละไฟล์ 10,509 ไบต์ ที่พิกัดบิตแมป (192,160) ช่องคือ RGBA (0,0,0,0) มุมก็มีอัลฟาศูนย์ ตัวอย่างสีเขียวน้ำทะเลที่ (192,80) คือ (23,107,91,255) ตัวอย่างเหล่านี้ตรวจช่องกับสีเติมของไฟล์นี้ ไม่ใช่ทุกพิกเซลขอบหรือทุกคุณสมบัติ SVG
สี่เหลี่ยมขาวบนหน้าเบราว์เซอร์คือพื้นหลัง CSS หลัง canvas ไม่ใช่สีขาวเพิ่มใน PNG หากต้องการพื้นทึบ ให้ระบายพื้นนั้นชัดเจนก่อนวาดภาพ แล้วตรวจไฟล์ส่งออกอีกครั้ง
เก็บต้นฉบับแก้ไขได้แยกจาก PNG
PNG ที่บันทึกมีพิกเซล ไม่ได้บรรจุเส้น วงกลม หรือขอบในที่แก้ไขได้เดิม เก็บ master.svg เป็นต้นทางเวกเตอร์ และเลือกขนาดบิตแมป canvas สำหรับปลายทางที่ต้องใช้ PNG จริง การขยาย PNG เดิมภายหลังไม่คืนรูปทรงต้นฉบับ
ชื่อ SVG อย่างเดียวก็ไม่พิสูจน์ว่าภายในทั้งหมดเป็นเวกเตอร์ ใช้คู่มือตรวจแรสเตอร์ฝังตัว เมื่อไฟล์มีภาพเชื่อมหรือฝัง
หากต้นฉบับมีเพียงแรสเตอร์และต้องการเส้นขอบแก้ไขได้ ขั้นตอนภาพเป็นเวกเตอร์ของ PerfectVector เตรียมต้นฉบับ SVG ได้ ตรวจเส้น สี และช่องก่อนใช้ในรูทีนโหลดนี้ การแปลงเวกเตอร์ไม่แก้เฮดเดอร์ CORS หรือให้สิทธิ์อ่าน canvas
คำถามที่พบบ่อย
ส่งมาร์กอัป SVG ตรงให้ drawImage() ได้หรือไม่? ไม่ได้ ตัวอย่างนี้โหลดไฟล์ SVG เป็น HTMLImageElement และส่งภาพถอดรหัสให้ drawImage() สตริงมาร์กอัปไม่ใช่วัตถุภาพนั้น แยกการโหลดเอกสารออกจากสร้างข้อมูลเส้น Path2D
ทำไมเห็นภาพแต่ส่งออก PNG ไม่ได้? ภาพข้ามต้นทางที่โหลดโดยไม่มีสิทธิ์ CORS อาจทำ canvas ปนเปื้อนหลังวาด ในกรณีไม่มี CORS ของเรา ระบายสำเร็จแต่อ่านพิกเซลและส่งออกโยน SecurityError ตรวจโหมดคำขอกับคำตอบเซิร์ฟเวอร์แทนคิดว่ารูปทรง SVG เสีย
crossOrigin anonymous แก้ทุก URL ภาพหรือไม่? ไม่ ตั้งก่อน src และเซิร์ฟเวอร์ภาพต้องตอบ CORS ที่เข้ากัน ภาพเดียวกันของเราถอดรหัสล้มเมื่อขอ anonymous จากปลายทางไม่มีเฮดเดอร์สิทธิ์
ส่งออก canvas เป็น PNG เก็บเส้น SVG แก้ไขได้หรือไม่? ไม่ PNG เป็นผลแรสเตอร์ เก็บ SVG ต้นฉบับแยกหากต้องแก้เส้น รูป หรือช่อง และตรวจ PNG ที่ขนาดส่งงานเป้าหมาย
แหล่งข้อมูล
- MDN: CanvasRenderingContext2D.drawImage() — วัตถุภาพที่รับ ขนาดปลายทาง และขนาดธรรมชาติ
- MDN: HTMLImageElement.decode() — Promise ภาพพร้อมและการปฏิเสธคำขอ/ถอดรหัส
- MDN: ภาพเปิด CORS — ข้อจำกัดต้นทางสะอาดในการอ่านและส่งออกภาพวาด
- MDN: HTMLImageElement.crossOrigin — โหมดคำขอ CORS สำหรับโหลดภาพ
- MDN: HTMLCanvasElement.toBlob() — สร้าง PNG blob ผลว่าง และ SecurityError
- MDN: SVG เป็นภาพ — บริบทภาพ SVG และข้อจำกัดทรัพยากรภายนอก
ทำงานจากแรสเตอร์เดิมที่ต้องการภาพแก้ไขได้หรือไม่? สร้างต้นฉบับ SVG ด้วย PerfectVector ตรวจเส้นขอบและช่อง แล้วเก็บต้นฉบับขณะตรวจ canvas เรนเดอร์กับ PNG ส่งออกแยก
เพิ่มเติมจากบล็อก

ไอคอน SVG ใน MapLibre: ตรวจพิกเซลและขนาดสัญลักษณ์
ถอดรหัส SVG สำหรับเลเยอร์สัญลักษณ์ MapLibre ควบคุมความหนาแน่นภาพกับขนาดไอคอน และเก็บต้นฉบับแก้ไขได้ ตรวจรูปกับช่องที่แสดง

ภาพ SVG ใน p5.js: โหลด ปรับขนาด และเก็บต้นฉบับ
โหลด SVG ใน p5.js 2.3.4 ด้วย async setup ตรวจช่องโปร่งใสที่สามขนาด Canvas และเก็บเส้นที่แก้ไขได้แยกจากภาพที่แสดง
