favicon.py 3.0 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
  1. import base64
  2. import io
  3. import urllib.parse
  4. from pathlib import Path
  5. from typing import TYPE_CHECKING, Optional, Tuple, Union
  6. from fastapi.responses import FileResponse, Response, StreamingResponse
  7. from . import __version__, globals
  8. from .helpers import is_file
  9. if TYPE_CHECKING:
  10. from .page import page
  11. def create_favicon_route(path: str, favicon: Optional[Union[str, Path]]) -> None:
  12. if is_file(favicon):
  13. globals.app.add_route(f'{path}/favicon.ico', lambda _: FileResponse(favicon))
  14. def get_favicon_url(page: 'page', prefix: str) -> str:
  15. favicon = page.favicon or globals.favicon
  16. if not favicon:
  17. return f'{prefix}/_nicegui/{__version__}/static/favicon.ico'
  18. favicon = str(favicon).strip()
  19. if is_remote_url(favicon):
  20. return favicon
  21. elif is_data_url(favicon):
  22. return favicon
  23. elif is_svg(favicon):
  24. return svg_to_data_url(favicon)
  25. elif is_char(favicon):
  26. return svg_to_data_url(char_to_svg(favicon))
  27. elif page.path == '/':
  28. return f'{prefix}/favicon.ico'
  29. else:
  30. return f'{prefix}{page.path}/favicon.ico'
  31. def get_favicon_response() -> Response:
  32. if not globals.favicon:
  33. raise ValueError(f'invalid favicon: {globals.favicon}')
  34. favicon = str(globals.favicon).strip()
  35. if is_svg(favicon):
  36. return Response(favicon, media_type='image/svg+xml')
  37. elif is_data_url(favicon):
  38. media_type, bytes = data_url_to_bytes(favicon)
  39. return StreamingResponse(io.BytesIO(bytes), media_type=media_type)
  40. elif is_char(favicon):
  41. return Response(char_to_svg(favicon), media_type='image/svg+xml')
  42. else:
  43. raise ValueError(f'invalid favicon: {favicon}')
  44. def is_remote_url(favicon: str) -> bool:
  45. return favicon.startswith('http://') or favicon.startswith('https://')
  46. def is_char(favicon: str) -> bool:
  47. return len(favicon) == 1
  48. def is_svg(favicon: str) -> bool:
  49. return favicon.strip().startswith('<svg')
  50. def is_data_url(favicon: str) -> bool:
  51. return favicon.startswith('data:')
  52. def char_to_svg(char: str) -> str:
  53. return f'''
  54. <svg viewBox="0 0 128 128" width="128" height="128" xmlns="http://www.w3.org/2000/svg" >
  55. <style>
  56. @supports (-moz-appearance:none) {{
  57. text {{
  58. font-size: 100px;
  59. transform: translateY(0.1em);
  60. }}
  61. }}
  62. text {{
  63. font-family: Arial, sans-serif;
  64. }}
  65. </style>
  66. <text y=".9em" font-size="128" font-family="Georgia, sans-serif">{char}</text>
  67. </svg>
  68. '''
  69. def svg_to_data_url(svg: str) -> str:
  70. svg_urlencoded = urllib.parse.quote(svg)
  71. return f'data:image/svg+xml,{svg_urlencoded}'
  72. def data_url_to_bytes(data_url: str) -> Tuple[str, bytes]:
  73. media_type, base64_image = data_url.split(',', 1)
  74. media_type = media_type.split(':')[1].split(';')[0]
  75. return media_type, base64.b64decode(base64_image)