JPEG画像をSVGファイルへ変換するWebAPP

Screen shot

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 初版

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です