Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How do you rasterise these SVG without using browser? #145

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions kerykeion/charts/inline_css.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import re

import logging

from typing import Dict

# Suppress cssutils warnings


def resolve_nested_variables(value: str, css_variables: Dict[str, str]) -> str:
"""
Recursively replaces var(--something) with its actual value from CSS variables.
"""
while "var(--" in value: # Keep resolving until no nested variables remain
match = re.search(r"var\((--[^)]+)\)", value)
if not match:
break
var_name = match.group(1) # Extract --variable-name
replacement = css_variables.get(var_name, match.group(0)) # Replace if exists
value = value.replace(match.group(0), replacement)
return value

def extract_css_variables(svg_content: str) -> Dict[str, str]:
"""
Extracts all CSS variables from <style> blocks in the SVG.
"""
import cssutils
cssutils.log.setLevel(logging.CRITICAL)
soup: BeautifulSoup = BeautifulSoup(svg_content, "xml")
css_variables: Dict[str, str] = {}

for style_tag in soup.find_all("style"):
css = cssutils.parseString(style_tag.text)
for rule in css:
if rule.type == rule.STYLE_RULE:
for i in range(rule.style.length):
property_name: str = rule.style.item(i)
if property_name.startswith("--"): # Only capture CSS variables
css_variables[property_name] = resolve_nested_variables(
rule.style.getPropertyValue(property_name), css_variables
) # Resolve if nested variables exist

return css_variables

def replace_css_variables(svg_content: str) -> str:
"""
Converts CSS variables to inline styles and direct attributes in an SVG file.
"""
from bs4 import BeautifulSoup, Tag
soup: BeautifulSoup = BeautifulSoup(svg_content, "xml")
css_variables: Dict[str, str] = extract_css_variables(svg_content)

if not css_variables:
return str(soup) # No variables found, return unchanged SVG

# Process elements with style attributes
for element in soup.find_all(True): # Iterate over all SVG elements
if isinstance(element, Tag):
# Replace CSS variables in style=""
if element.has_attr("style"):
new_style: str = element["style"]
for var, value in css_variables.items():
var_pattern: str = f"var\\({re.escape(var)}\\)"
new_style = re.sub(var_pattern, value, new_style) # Replace CSS variable
element["style"] = new_style # Apply updated styles

# Replace CSS variables in direct attributes like fill, stroke, etc.
for attr in ["fill", "stroke", "stop-color"]: # Add more attributes if needed
if element.has_attr(attr):
attr_value = element[attr]
element[attr] = resolve_nested_variables(attr_value, css_variables)

# Remove all <style> tags since they are no longer needed
for style_tag in soup.find_all("style"):
style_tag.decompose()

return str(soup)
21 changes: 18 additions & 3 deletions kerykeion/charts/kerykeion_chart_svg.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
from string import Template
from typing import Union, List, Literal
from datetime import datetime
from .inline_css import replace_css_variables

class KerykeionChartSVG:
"""
Expand Down Expand Up @@ -634,7 +635,7 @@ def _create_template_dictionary(self) -> ChartTemplateDictionary:

return template_dict

def makeTemplate(self, minify: bool = False) -> str:
def makeTemplate(self, minify: bool = False, inline_css: bool = False) -> str:
"""Creates the template for the SVG file"""
td = self._create_template_dictionary()

Expand All @@ -657,20 +658,34 @@ def makeTemplate(self, minify: bool = False) -> str:
else:
template = template.replace('"', "'")

if inline_css:
# Use serializing styles inline for embedding in PDF
template = replace_css_variables(template)

return template

def makeSVG(self, minify: bool = False):
def makeSVG(self, minify: bool = False, inline_css: bool = False):
"""Prints out the SVG file in the specified folder"""

if not hasattr(self, "template"):
self.template = self.makeTemplate(minify)
self.template = self.makeTemplate(minify, inline_css)

chartname = self.output_directory / f"{self.user.name} - {self.chart_type} Chart.svg"

with open(chartname, "w", encoding="utf-8", errors="ignore") as output_file:
output_file.write(self.template)

print(f"SVG Generated Correctly in: {chartname}")


def makeSvgString(self, minify: bool = False, inline_css: bool = False):
"""Prints out the SVG file in the specified folder"""

if not hasattr(self, "template"):
self.template = self.makeTemplate(minify, inline_css)
return self.template


def makeWheelOnlyTemplate(self, minify: bool = False):
"""Creates the template for the SVG file with only the wheel"""

Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ simple-ascii-tables = "^1.0.0"
pytz = "^2024.2"
typing-extensions = "^4.12.2"

[tool.poetry.group.optionalgroup.dependencies]
beautifulsoup4 = "^4.13.3"
cssutils = "^2.11.1"
lxml = "^5.3.1"


[tool.poetry.scripts]
create-docs = "scripts.docs:main"

Expand All @@ -63,6 +69,8 @@ types-requests = "^2.28.11.7"
types-pytz = "^2022.7.0.0"
poethepoet = "^0.19.0"



# MyPy Static Analysis
[tool.mypy]
ignore_missing_imports = true
Expand Down