155 lines
4.9 KiB
Python
155 lines
4.9 KiB
Python
"""
|
||
Utils dealing with URLs.
|
||
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
from pathlib import Path
|
||
from urllib.parse import urljoin, urlparse
|
||
from urllib.request import Request, urlopen
|
||
|
||
from . import VERSION
|
||
|
||
HTTP_HEADERS = {'User-Agent': f'CairoSVG {VERSION}'}
|
||
|
||
URL = re.compile(r'url\((.+)\)')
|
||
|
||
|
||
def normalize_url(url):
|
||
"""Normalize ``url`` for underlying NT/Unix operating systems.
|
||
|
||
The input ``url`` may look like the following:
|
||
|
||
- C:\\Directory\\zzz.svg
|
||
- file://C:\\Directory\\zzz.svg
|
||
- zzz.svg
|
||
|
||
The output ``url`` on NT systems would look like below:
|
||
|
||
- file:///C:/Directory/zzz.svg
|
||
|
||
"""
|
||
if url and os.name == 'nt' and not url.startswith('data:'):
|
||
# Match input ``url`` like the following:
|
||
# - C:\\Directory\\zzz.svg
|
||
# - Blah.svg
|
||
if not url.startswith('file:') and os.path.isabs(url):
|
||
url = os.path.abspath(url)
|
||
if '#' in url:
|
||
url, part = url.rsplit('#', 1)
|
||
else:
|
||
part = None
|
||
url = Path(url).resolve().as_uri()
|
||
if part is not None:
|
||
url = url + '#' + part
|
||
|
||
# Match input ``url`` like the following:
|
||
# - file://C:\\Directory\\zzz.svg
|
||
elif re.match(
|
||
'^file://[a-z]:', url,
|
||
re.IGNORECASE | re.MULTILINE | re.DOTALL):
|
||
url = url.replace('//', '///')
|
||
url = url.replace('\\', '/')
|
||
|
||
return url
|
||
|
||
|
||
def nt_compatible_path(path):
|
||
"""Provide compatible NT file paths for ``os.path`` functions
|
||
|
||
``os.path`` expects NT paths with no ``/`` at the beginning. For
|
||
example, ``/C:/Directory/zzz.svg`` would fail ``os.path.isfile()``,
|
||
``os.path.isdir()`` etc. where the expected input for `os.path`
|
||
functions is ``/C:/Directory/zzz.svg``.
|
||
|
||
Currently ``nt_compatible_path`` performs some basic checks and
|
||
eliminates the unwanted ``/`` at the beginning.
|
||
|
||
"""
|
||
if os.name == 'nt' and re.match(
|
||
'^/[a-z]:/', path, re.IGNORECASE | re.MULTILINE | re.DOTALL):
|
||
return re.sub('^/', '', path, re.IGNORECASE | re.MULTILINE | re.DOTALL)
|
||
else:
|
||
return path
|
||
|
||
|
||
def fetch(url, resource_type):
|
||
"""Fetch the content of ``url``.
|
||
|
||
``resource_type`` is the mimetype of the resource (currently one of
|
||
image/*, image/svg+xml, text/css).
|
||
|
||
"""
|
||
return urlopen(Request(url, headers=HTTP_HEADERS)).read()
|
||
|
||
|
||
def safe_fetch(url, resource_type):
|
||
"""Fetch the content of ``url`` only if it’s a data-URL.
|
||
|
||
Otherwise, return an empty SVG.
|
||
|
||
"""
|
||
if url and url.startswith('data:'):
|
||
return fetch(url, resource_type)
|
||
return b'<svg width="1" height="1"></svg>'
|
||
|
||
|
||
def parse_url(url, base=None):
|
||
"""Parse an URL.
|
||
|
||
The URL can be surrounded by a ``url()`` string. If ``base`` is not `None`,
|
||
the "folder" part of it is prepended to the URL.
|
||
|
||
"""
|
||
if url:
|
||
match = URL.search(url)
|
||
if match:
|
||
url = match.group(1)
|
||
if base:
|
||
parsed_base = urlparse(base)
|
||
parsed_url = urlparse(url)
|
||
if parsed_base.scheme in ('', 'file'):
|
||
if parsed_url.scheme in ('', 'file'):
|
||
parsed_base_path = nt_compatible_path(parsed_base.path)
|
||
parsed_url_path = nt_compatible_path(parsed_url.path)
|
||
# We are sure that `url` and `base` are both file-like URLs
|
||
if os.path.isfile(parsed_base_path):
|
||
if parsed_url_path:
|
||
# Take the "folder" part of `base`, as
|
||
# `os.path.join` doesn't strip the file name
|
||
url = os.path.join(
|
||
os.path.dirname(parsed_base_path),
|
||
parsed_url_path)
|
||
else:
|
||
url = parsed_base_path
|
||
elif os.path.isdir(parsed_base_path):
|
||
if parsed_url_path:
|
||
url = os.path.join(
|
||
parsed_base_path, parsed_url_path)
|
||
else:
|
||
url = ''
|
||
else:
|
||
url = ''
|
||
if parsed_url.fragment:
|
||
url = f'{url}#{parsed_url.fragment}'
|
||
elif parsed_url.scheme in ('', parsed_base.scheme):
|
||
# `urljoin` automatically uses the "folder" part of `base`
|
||
url = urljoin(base, url)
|
||
url = normalize_url(url.strip('\'"'))
|
||
return urlparse(url or '')
|
||
|
||
|
||
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:
|
||
url = f'file://{os.path.abspath(url.geturl())}'
|
||
url = normalize_url(url)
|
||
|
||
return url_fetcher(url, resource_type)
|