IMG2SVG - JPG file convert to SVG
JPEGファイルを白黒2値画像へ加工しスケーラブルなSVGファイルへ変換する。
Sample Site
さくらインターネット・レンタルサーバスタンダードプランへ実装した作例。
(動作テスト用のためjpgファイルサイズを10Mbyteに制限)
https://yanmos.jpn.org/img2svg/img2svg
Local環境ではuvicornで実行し確認、レンタルサーバではcgi経由でa2wsgiを利用した。
Structure
flowchart LR
subgraph fastAPI
cgi[main.py]
end
subgraph uikit
html[main.html]
end
infile((.jpg file))
outfile((.svg file))
html-->cgi-->outfile
infile-->html
Library, Framework
使用したライブラリ、フレームワークは下記の通り。
UIkit・・・Page appearance
fastAPI・・・Python web framework
potrace・・・tracing a bitmap
pillow・・・Python image library
Directory Tree
デプロイ時のディレクトリ構成。
img2svg/
├── static/
│ ├── css/
│ │ └── uikit.min.css
│ └── js/
│ ├── uikit-icons.min.js
│ └── uikit.min.js
├── templates/
│ └── main.html
├── work/
└── main.py
Source code
ソースコードを下記に示す。実際の運用版とは細部が異なる。
main.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="{{ url_for('static', path='/css/uikit.min.css') }}" rel="stylesheet">
<script type=text/javascript src="{{ url_for('static', path='/js/uikit.min.js') }}"></script>
<script type=text/javascript src="{{ url_for('static', path='/js/uikit-icons.min.js') }}"></script>
<link rel="shortcut icon" href="#">
<title>Convert JPEG to SVG</title>
</head>
<body class="text-center">
<header>
<nav class="uk-container uk-navbar">
<div class="uk-navbar-left">
<h1 class="uk-heading">JPEG to SVG</h1>
</div>
</nav>
</header>
<main>
<div class="uk-container uk-width-expand">
<div class="uk-container">
<form id="work" class="uk-form">
<p class="uk-margin-top">Drag and drop the .jpg file to the area below or click the icon to select it</p>
<div class="js-upload-jpg uk-placeholder uk-text-center uk-background-muted uk-padding-small">
<div class="uk-text-right">
<div uk-form-custom>
<input id="input-jpg" type="file" />
<span class="upload-icon"><span uk-icon='icon: cloud-upload; ratio: 1.2'></span></span>
</div>
</div>
</div>
<progress id="js-progressbar-jpg" class="uk-progress" value="0" max="100" hidden></progress>
<div id="imgtile" class="uk-child-width-1-3@s uk-grid-collapse uk-text-center" hidden="hidden" uk-grid>
<div class="uk-tile uk-tile-default">
<p class="uk-h4">Source</p>
<div id="srcimg" class="uk-margin"></div>
</div>
<div class="uk-tile uk-tile-default">
<p class="uk-h4">Black and White</p>
<div id="bnwimg" class="uk-margin"></div>
</div>
<div class="uk-tile uk-tile-default">
<p class="uk-h4">SVG</p>
<div id="svgimg" class="uk-margin"></div>
<div id="download"></div>
</div>
</div>
</form>
</div>
</div>
</main>
<footer>
<div class="uk-container uk-text-right">
yam.ktm@gmail.com
</div>
</footer>
<script>
var bar_jpg = document.getElementById('js-progressbar-jpg');
UIkit.upload('.js-upload-jpg', {
url: '/img2svg/img2svg/upload',
method: 'post',
multiple: false,
allowedTypes: ['image/jpeg'],
name: 'file',
error: function () {
console.log('Error', arguments);
},
beforeSend: function (e) {
return true;
},
loadStart: function (e) {
bar_jpg.removeAttribute('hidden');
bar_jpg.max = e.total;
bar_jpg.value = e.loaded;
},
progress: function (e) {
bar_jpg.max = e.total;
bar_jpg.value = e.loaded;
},
loadEnd: function (e) {
bar_jpg.max = e.total;
bar_jpg.value = e.loaded;
display_img(e.target.response)
},
completeAll: function (e) {
setTimeout(function () {
bar_jpg.setAttribute('hidden', 'hidden');
}, 1000);
}
});
function display_img(c) {
document.getElementById("imgtile").removeAttribute('hidden');
var file = JSON.parse(c)["file"]
var srcurl = file["srcurl"];
var bnwurl = file["bnwurl"];
var svgurl = file["svgurl"];
var downloadurl = file["downloadurl"];
srchtml = `<img class="uk-box-shadow-small" src="${srcurl}">`;
bnwhtml = `<img class="uk-box-shadow-small" src="${bnwurl}">`;
svghtml = `<img class="uk-box-shadow-small" src="${svgurl}">`;
downloadhtml = `<a id="download" class="uk-button uk-button-default" href="${downloadurl}">Download .svg file</a>`;
document.getElementById("srcimg").innerHTML = srchtml;
document.getElementById("bnwimg").innerHTML = bnwhtml;
document.getElementById("svgimg").innerHTML = svghtml;
document.getElementById("download").innerHTML = downloadhtml;
}
// Disable drag & drop
window.addEventListener('dragover', function (e) {
e.preventDefault();
}, false);
window.addEventListener('drop', function (e) {
e.preventDefault();
e.stopPropagation();
}, false);
</script>
</body>
</html>
main.py
# import uvicorn
import os
import socket
import platform
import subprocess
import tempfile
import urllib
from fastapi import FastAPI, Request, Response
from fastapi.responses import HTMLResponse, FileResponse, JSONResponse
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
from io import BytesIO
from PIL import Image
PORT = 8089
WORK_DIR = 'work'
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
PLATFORM = platform.system()
if PLATFORM == 'Windows': # windows
POTRACE_CMD = 'C:\\Program Files\\potrace.exe'
DIR_SEP = '\\'
elif PLATFORM == 'Darwin': # mac
POTRACE_CMD = 'potrace'
DIR_SEP = '/'
elif PLATFORM == 'FreeBSD': # freebsd
POTRACE_CMD = '/usr/bin/potrace'
DIR_SEP = '/'
elif PLATFORM == 'Linux': # Linux
POTRACE_CMD = 'potrace'
DIR_SEP = '/'
else:
raise('Error: Unknown platform.')
app = FastAPI()
app.mount("/static", StaticFiles(directory="static"), name="static")
templates = Jinja2Templates(directory="templates")
@app.get("/img2svg", response_class=HTMLResponse)
async def img2svg(request: Request):
global PORT
PORT = request.scope["server"][1] # get server port
return templates.TemplateResponse("main.html", {"request": request})
@app.post("/img2svg/upload", response_class=JSONResponse)
async def img2svg_upload(request: Request, response: Response):
form = await request.form()
uploadf = form['file']
infor = dict()
workdirpath = tempfile.mkdtemp(dir=os.path.join(BASE_DIR, WORK_DIR))
tmpdir = workdirpath.split(DIR_SEP)[-1]
name, _ = os.path.splitext(uploadf.filename)
infor['srcfile'] = urllib.parse.quote(uploadf.filename)
infor['bnwfile'] = urllib.parse.quote(name + '.bmp')
infor['svgfile'] = urllib.parse.quote(name + '.svg')
fin = BytesIO(uploadf.file.read())
with open(os.path.join(workdirpath, infor['srcfile']), "wb") as f:
f.write(fin.getvalue())
img = Image.open(fin).convert('L') # grayscale conversion
threshold = 128
bw = img.point(lambda x: 255 if x > threshold else 0, '1') # black and white conversion
bnwfilepath = os.path.join(workdirpath, infor['bnwfile'])
bw.save(bnwfilepath)
svgfilepath = os.path.join(workdirpath, infor['svgfile'])
cmd = [POTRACE_CMD, bnwfilepath, '-s', '-o', svgfilepath]
with open("img2svg_error.log", "w") as err_file:
subprocess.run(cmd, stderr=err_file)
infor['srcurl'] = '/img2svg/img2svg/img/' + tmpdir + '/' + infor['srcfile']
infor['bnwurl'] = '/img2svg/img2svg/img/' + tmpdir + '/' + infor['bnwfile']
infor['svgurl'] = '/img2svg/img2svg/img/' + tmpdir + '/' + infor['svgfile']
infor['downloadurl'] = '/img2svg/img2svg/download/' + tmpdir + '/' + infor['svgfile']
return JSONResponse(content={'file': infor})
@app.get("/img2svg/img/{dname}/{fname}", response_class=FileResponse)
async def download_img_file(dname, fname, request: Request):
fpath = os.path.join(BASE_DIR, WORK_DIR, dname, urllib.parse.quote(fname))
return FileResponse(fpath)
@app.get("/img2svg/download/{workdir}/{filename}")
async def download_file_result(workdir, filename):
fpath = os.path.join(BASE_DIR, WORK_DIR, workdir, urllib.parse.quote(filename))
content_disposition = (
f"attachment; filename*=UTF-8''{urllib.parse.quote(filename)}; "
f'filename="{filename.encode("utf-8").decode("latin1")}"'
)
headers = {"Content-Disposition": content_disposition}
return FileResponse(fpath, headers=headers, media_type="image/svg+xml")
if __name__ == "__main__":
# uvicorn.run("main:app", host="0.0.0.0", port=PORT, reload=False)
pass
2025/08/11 初版