Skip to content

Commit 2c1095c

Browse files
committed
1 parent 09a82f8 commit 2c1095c

File tree

2 files changed

+289
-0
lines changed

2 files changed

+289
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
- [Image resize and quality comparison](https://tools.simonwillison.net/image-resize-quality) converts an image to JPEGs using a number of different quality settings so you can select the smallest file size that is still usefully legible ([about](https://simonwillison.net/2024/Jul/26/image-resize-and-quality-comparison/))
1212
- [YouTube Thumbnails](https://tools.simonwillison.net/youtube-thumbnails) - paste in the URL to a YouTube video, get back all of the URLs to thumbnail images of different sizes for that video
1313
- [Gemini API Image Bounding Box Visualizer](https://tools.simonwillison.net/gemini-bbox) - run prompts against Google Gemini models that return bounding box co-ordinates and visualize them against the original image, see [this post](https://simonwillison.net/2024/Aug/26/gemini-bounding-box-visualization/) for details
14+
- [SVG to Image Converter](https://tools.simonwillison.net/svg-render) - turn an SVG file into a rendered JPEG or PNG
1415

1516
On [Observable](https://observablehq.com/):
1617

svg-render.html

+288
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>SVG to Image Converter</title>
7+
<style>
8+
body {
9+
font-family: Arial, sans-serif;
10+
max-width: 800px;
11+
margin: 0 auto;
12+
padding: 20px;
13+
font-size: 16px;
14+
}
15+
textarea, input, button, select {
16+
font-size: 16px;
17+
}
18+
#dropZone {
19+
width: 100%;
20+
height: 150px;
21+
border: 2px dashed #ccc;
22+
border-radius: 5px;
23+
padding: 10px;
24+
margin-bottom: 10px;
25+
position: relative;
26+
}
27+
#dropZone.dragover {
28+
border-color: #000;
29+
background-color: #f0f0f0;
30+
}
31+
#svgInput {
32+
width: 100%;
33+
height: 100%;
34+
border: none;
35+
resize: none;
36+
}
37+
#imageContainer {
38+
margin-top: 20px;
39+
text-align: center;
40+
}
41+
#convertedImage {
42+
max-width: 90%;
43+
cursor: pointer;
44+
transition: max-width 0.3s ease;
45+
}
46+
#convertedImage.full-size {
47+
max-width: unset;
48+
}
49+
#base64Output {
50+
word-wrap: break-word;
51+
}
52+
.option-group {
53+
margin-bottom: 10px;
54+
}
55+
#fileInput {
56+
margin-bottom: 10px;
57+
}
58+
#downloadLink, #loadExampleLink {
59+
display: inline-block;
60+
margin-top: 10px;
61+
margin-right: 10px;
62+
}
63+
#widthInput {
64+
width: 60px;
65+
}
66+
#bgColor {
67+
vertical-align: middle;
68+
}
69+
#fileSize {
70+
margin-left: 10px;
71+
}
72+
</style>
73+
</head>
74+
<body>
75+
<h1>SVG to Image Converter</h1>
76+
<input type="file" id="fileInput" accept=".svg">
77+
<a href="https://gist.githubusercontent.com/simonw/aedecb93564af13ac1596810d40cac3c/raw/83e7f3be5b65bba61124684700fa7925d37c36c3/tiger.svg" id="loadExampleLink">Load example image</a>
78+
<div id="dropZone">
79+
<textarea id="svgInput" placeholder="Paste your SVG code here or drag & drop an SVG file"></textarea>
80+
</div>
81+
<div class="option-group">
82+
<label>
83+
<input type="radio" name="format" value="image/jpeg" checked> JPEG
84+
</label>
85+
<label>
86+
<input type="radio" name="format" value="image/png"> PNG
87+
</label>
88+
</div>
89+
<div class="option-group">
90+
<label for="bgColor">Background Color:</label>
91+
<input type="color" id="bgColor" value="#000000">
92+
<label>
93+
<input type="checkbox" id="transparentBg" checked> Transparent
94+
</label>
95+
</div>
96+
<div class="option-group">
97+
<label for="widthInput">Output Width:</label>
98+
<input type="number" id="widthInput" value="800" min="1">
99+
</div>
100+
<button onclick="convertSvgToImage()">Convert SVG</button>
101+
<div id="imageContainer"></div>
102+
<a id="downloadLink" style="display: none;">Download Image</a>
103+
<span id="fileSize"></span>
104+
<h2>Base64 Image Tag:</h2>
105+
<pre id="base64Output"></pre>
106+
<button onclick="copyBase64Tag()">Copy Image Tag</button>
107+
108+
<script>
109+
const dropZone = document.getElementById('dropZone');
110+
const svgInput = document.getElementById('svgInput');
111+
const fileInput = document.getElementById('fileInput');
112+
const widthInput = document.getElementById('widthInput');
113+
const loadExampleLink = document.getElementById('loadExampleLink');
114+
const bgColor = document.getElementById('bgColor');
115+
const transparentBg = document.getElementById('transparentBg');
116+
const fileSizeSpan = document.getElementById('fileSize');
117+
118+
// Load example image functionality
119+
loadExampleLink.addEventListener('click', (e) => {
120+
e.preventDefault();
121+
const exampleSvgUrl = loadExampleLink.href;
122+
fetch(exampleSvgUrl)
123+
.then(response => response.text())
124+
.then(data => {
125+
svgInput.value = data;
126+
})
127+
.catch(error => {
128+
console.error('Error loading example SVG:', error);
129+
alert('Failed to load example SVG. Please try again later.');
130+
});
131+
});
132+
133+
// Color and transparency handling
134+
bgColor.addEventListener('input', () => {
135+
transparentBg.checked = false;
136+
});
137+
138+
transparentBg.addEventListener('change', () => {
139+
if (transparentBg.checked) {
140+
bgColor.value = "#000000";
141+
}
142+
});
143+
144+
// Drag and drop functionality
145+
dropZone.addEventListener('dragover', (e) => {
146+
e.preventDefault();
147+
dropZone.classList.add('dragover');
148+
});
149+
150+
dropZone.addEventListener('dragleave', () => {
151+
dropZone.classList.remove('dragover');
152+
});
153+
154+
dropZone.addEventListener('drop', (e) => {
155+
e.preventDefault();
156+
dropZone.classList.remove('dragover');
157+
const file = e.dataTransfer.files[0];
158+
if (file && file.type === 'image/svg+xml') {
159+
readFile(file);
160+
}
161+
});
162+
163+
// File input functionality
164+
fileInput.addEventListener('change', (e) => {
165+
const file = e.target.files[0];
166+
if (file && file.type === 'image/svg+xml') {
167+
readFile(file);
168+
}
169+
});
170+
171+
function readFile(file) {
172+
const reader = new FileReader();
173+
reader.onload = (e) => {
174+
svgInput.value = e.target.result;
175+
};
176+
reader.readAsText(file);
177+
}
178+
179+
function convertSvgToImage() {
180+
let svgInput = document.getElementById('svgInput').value;
181+
const imageContainer = document.getElementById('imageContainer');
182+
const base64Output = document.getElementById('base64Output');
183+
const downloadLink = document.getElementById('downloadLink');
184+
const format = document.querySelector('input[name="format"]:checked').value;
185+
const newWidth = parseInt(widthInput.value) || 800;
186+
187+
// Clear previous content
188+
imageContainer.innerHTML = '';
189+
base64Output.textContent = '';
190+
downloadLink.style.display = 'none';
191+
fileSizeSpan.textContent = '';
192+
193+
// Find the <?xml tag and ignore everything before it
194+
const xmlIndex = svgInput.indexOf('<?xml');
195+
if (xmlIndex !== -1) {
196+
svgInput = svgInput.substring(xmlIndex);
197+
}
198+
199+
// Create a temporary SVG element
200+
const svgElement = new DOMParser().parseFromString(svgInput, 'image/svg+xml').documentElement;
201+
202+
if (!svgElement || svgElement.nodeName !== 'svg') {
203+
alert('Invalid SVG input');
204+
return;
205+
}
206+
207+
// Get SVG viewBox
208+
let viewBox = svgElement.getAttribute('viewBox');
209+
let width, height;
210+
if (viewBox) {
211+
[, , width, height] = viewBox.split(' ').map(Number);
212+
} else {
213+
width = parseInt(svgElement.getAttribute('width')) || 300;
214+
height = parseInt(svgElement.getAttribute('height')) || 150;
215+
}
216+
217+
// Calculate new dimensions
218+
const aspectRatio = width / height;
219+
const newHeight = Math.round(newWidth / aspectRatio);
220+
221+
// Create off-screen canvas
222+
const canvas = document.createElement('canvas');
223+
canvas.width = newWidth;
224+
canvas.height = newHeight;
225+
226+
// Draw SVG on canvas
227+
const ctx = canvas.getContext('2d');
228+
229+
// Set background color if not transparent
230+
if (!transparentBg.checked) {
231+
ctx.fillStyle = bgColor.value;
232+
ctx.fillRect(0, 0, newWidth, newHeight);
233+
}
234+
235+
const svgBlob = new Blob([svgInput], {type: 'image/svg+xml;charset=utf-8'});
236+
const URL = window.URL || window.webkitURL || window;
237+
const svgUrl = URL.createObjectURL(svgBlob);
238+
239+
const img = new Image();
240+
img.onload = function() {
241+
ctx.drawImage(img, 0, 0, newWidth, newHeight);
242+
URL.revokeObjectURL(svgUrl);
243+
244+
// Convert to selected format
245+
const imageDataUrl = canvas.toDataURL(format);
246+
247+
// Calculate file size
248+
const fileSizeInBytes = Math.round((imageDataUrl.length * 3) / 4);
249+
const fileSizeInKB = (fileSizeInBytes / 1024).toFixed(2);
250+
251+
// Display converted image
252+
const convertedImg = document.createElement('img');
253+
convertedImg.src = imageDataUrl;
254+
convertedImg.id = 'convertedImage';
255+
convertedImg.onclick = toggleImageSize;
256+
imageContainer.appendChild(convertedImg);
257+
258+
// Set up download link and display file size
259+
downloadLink.href = imageDataUrl;
260+
downloadLink.download = `converted_image.${format === 'image/jpeg' ? 'jpg' : 'png'}`;
261+
downloadLink.style.display = 'inline-block';
262+
fileSizeSpan.textContent = `(${fileSizeInKB} KB)`;
263+
264+
// Display base64 image tag
265+
const imgTag = `<img src="${imageDataUrl}" alt="Converted ${format === 'image/jpeg' ? 'JPEG' : 'PNG'}" width="${newWidth}" height="${newHeight}">`;
266+
base64Output.textContent = imgTag;
267+
};
268+
img.src = svgUrl;
269+
}
270+
271+
function toggleImageSize() {
272+
const img = document.getElementById('convertedImage');
273+
img.classList.toggle('full-size');
274+
}
275+
276+
function copyBase64Tag() {
277+
const base64Output = document.getElementById('base64Output');
278+
const range = document.createRange();
279+
range.selectNode(base64Output);
280+
window.getSelection().removeAllRanges();
281+
window.getSelection().addRange(range);
282+
document.execCommand('copy');
283+
window.getSelection().removeAllRanges();
284+
alert('Image tag copied to clipboard!');
285+
}
286+
</script>
287+
</body>
288+
</html>

0 commit comments

Comments
 (0)