CairoSVG icon indicating copy to clipboard operation
CairoSVG copied to clipboard

Error converting local file on Windows

Open scholer opened this issue 6 years ago • 5 comments

Environment:

  • OS: Windows
  • Python: 3.4

It appears that since version 2.0, I'm unable to convert local files using cairosvg CLI command on Windows:

$ cairosvg -o test.png input.svg

Traceback (most recent call last):
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 1345, in open_local_file
    stats = os.stat(localfile)
FileNotFoundError: [WinError 3] The system cannot find the path specified: ''

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\runpy.py", line 170, in _run_module_as_main
    "__main__", mod_spec)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "C:\Users\rasse\Anaconda3\envs\gelutils-py34\Scripts\cairosvg.exe\__main__.py", line 9, in <module>
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\site-packages\cairosvg\__init__.py", line 96, in main
    SURFACES[output_format.upper()].convert(**kwargs)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\site-packages\cairosvg\surface.py", line 135, in convert
    tree = Tree(**kwargs)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\site-packages\cairosvg\parser.py", line 382, in __init__
    parse_url(self.url), 'image/svg+xml')
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\site-packages\cairosvg\parser.py", line 258, in fetch_url
    return read_url(url, self.url_fetcher, resource_type)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\site-packages\cairosvg\url.py", line 96, in read_url
    return url_fetcher(url, resource_type)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\site-packages\cairosvg\url.py", line 42, in fetch
    return urlopen(Request(url, headers=HTTP_HEADERS)).read()
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 161, in urlopen
    return opener.open(url, data, timeout)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 466, in open
    response = self._open(req, data)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 484, in _open
    '_open', req)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 443, in _call_chain
    result = func(*args)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 1319, in file_open
    return self.open_local_file(req)
  File "c:\users\rasse\anaconda3\envs\gelutils-py34\lib\urllib\request.py", line 1363, in open_local_file
    raise URLError(exp)
urllib.error.URLError: <urlopen error [WinError 3] The system cannot find the path specified: ''>

The error happens with CairoSVG 2.0.0 and 2.1.3 (and, I assume, versions in between).

It did not happen with CairoSVG 1.0.22.

Adding file:/// in front of the filename does work:

cairosvg -o test.png file:///D:/temp/input.svg
cairosvg -o test.png file:///D:\temp\input.svg
# This does not work:
cairosvg -o test.png "file://D:\temp\input.svg"

Notice the three slashes after file:.

The reason this works is because urllib interprets the string between file:// (the scheme) and the third slash as the request host.

On Windows, this means that when cairosvg.url.read_url()/fetch() passes the url argument to urllib.request.Request the filename is interpreted as the host, leaving the selector part of the request empty.

When the file is opened by urllib.request.FileHandler.open_local_file() it uses the empty req.selector as the local filename. When it then tries to os.stat(localfile) with an empty filename, it raises an exception.

On Mac/Linux, absolute filepaths starts with /, which effectively adds the third slash in file:///. So this, I expect, is only an issue on Windows, where absolute filepaths starts with the drive letter (and we also use backslash as path separator). Relative paths are converted to absolute paths in cairosvg.url.read_url().

scholer avatar May 23 '18 10:05 scholer

FWIW, version 1.0.22 had an explicit check to see if the url looked like a local windows filepath, and dealing accordingly in that was case.

https://github.com/Kozea/CairoSVG/blob/1.0.22/cairosvg/parser.py#L299

scholer avatar May 23 '18 11:05 scholer

The following also works:

cairosvg -o test.png "file://localhost/D:/temp/input.svg"
cairosvg -o test.png "file://localhost/D:\temp\input.svg"
cairosvg -o test.png "file://localhost//D:/temp/input.svg"
cairosvg -o test.png "file://localhost//D:\temp\input.svg"

Maybe a fix could be to change https://github.com/Kozea/CairoSVG/blob/2.1.3/cairosvg/url.py#L95, adding the host information explicitly to satisfy the host part when Request parses the url?

<<<         url = 'file://{}'.format(os.path.abspath(url.geturl()))

>>>         url = 'file://localhost/{}'.format(os.path.abspath(url.geturl()))

Would this work on Mac and Linux? Let me know if you would like a PR with this change.

scholer avatar May 23 '18 11:05 scholer

I've run into this issue as well and ended up patching the function read_url referenced by @scholer with:

def read_url(url, url_fetcher, resource_type):
    """Get bytes in a parsed ``url`` using ``url_fetcher``.

    If ``url_fetcher`` is None a default (no limitations) URLFetcher is used.
    """
    if url.scheme:
        url_ = url.geturl()
    else:
        # urllib fails to properly open local files on Windows if backslashes
        # are used in the file URL like:  file://C:\directory\file.svg
        if sys.platform == 'win32':
            path = os.path.abspath(url.geturl()).replace('\\', '/')
            # The root slash must be prepended as well.
            url_ = 'file:///{}'.format(path)
        else:
            path = os.path.abspath(url.geturl())
            url_ = 'file://{}'.format(path)

    return url_fetcher(url_, resource_type)

This works, however, I'm not sure if just swapping the slashes is the safest approach to converting to a valid path format for urllib.

chancyk avatar Jun 09 '18 22:06 chancyk

The above replacement didn't work for me. This still appears to be a problem. Any idea how I can rollback to a previous version without this error?

fionafibration avatar Oct 07 '18 02:10 fionafibration

Additionally, I'm calling CairoSVG as a function in python code.

fionafibration avatar Oct 07 '18 02:10 fionafibration