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

HEX/RGB colors are getting corrupted #402

Open
jdefockert opened this issue Oct 8, 2024 · 10 comments
Open

HEX/RGB colors are getting corrupted #402

jdefockert opened this issue Oct 8, 2024 · 10 comments

Comments

@jdefockert
Copy link

When using SVGlib to convert plotly SVG charts created using Kaleido.

Hex colors containing the value "01" are converted to "FF" causing the color to change from what it is originally in the SVG.

Example:
F57D01 (Orange) changes into F57DFF (Pink)

010063 (Dark Blue) changes into FF0063 (Pinkish red)

Using: Python 3.11.9, SVGlib 1.5.1, Reportlab 4.2.5, Plotly 5.24.1, Kaleido 0.1.0.post1

Thank you in advance for looking into it.

Copy link

github-actions bot commented Oct 8, 2024

Thank you for raising your first issue! Your help to improve svglib is much appreciated!

@claudep
Copy link
Collaborator

claudep commented Oct 8, 2024

Thanks for the report. Having a smallest possible sample to reproduce would be very appreciated!

@jdefockert
Copy link
Author

jdefockert commented Oct 8, 2024

Please find a sample that reproduces the error:

import plotly.graph_objects as go
import io
from svglib.svglib import svg2rlg
from reportlab.lib.pagesizes import letter
from reportlab.lib.units import inch
from reportlab.platypus import BaseDocTemplate, PageTemplate, Frame, Spacer
from plotly.io import to_image, write_image

# Create a function to generate a chart using Plotly and export to SVG format
def generate_plotly_chart_as_svg(title, color):
    fig = go.Figure()
    fig.add_trace(go.Scatter(x=[1, 2, 3, 4], y=[10, 11, 12, 13], mode='lines+markers', marker_color=color, name=title))
    fig.update_layout(title=title)

    # Export as an in-memory SVG
    buffer = io.BytesIO()
    svg_data = to_image(fig, format="svg")
    write_image(fig, title.replace(' ','_') + '.svg')
    buffer.write(svg_data)
    buffer.seek(0)
    return buffer

# Function to convert SVG into ReportLab Drawable and scale it to the specified dimensions
def svg_to_flowable(svg_buffer, target_width, target_height):
    drawing = svg2rlg(svg_buffer)
    
    # Scale the drawing to fit the desired width and height
    scale_x = target_width / drawing.width
    scale_y = target_height / drawing.height
    scale = min(scale_x, scale_y)  # Use the smaller scale to preserve aspect ratio
    
    # Apply scaling
    drawing.width = drawing.width * scale
    drawing.height = drawing.height * scale
    drawing.scale(scale, scale)
    
    return drawing

# Create 3 Plotly charts and export as SVGs
chart1_svg = generate_plotly_chart_as_svg("Dark Blue Chart", "#010063")
chart2_svg = generate_plotly_chart_as_svg("Orange Chart", "#F57D01")

# Create the document with letter-size pages
doc = BaseDocTemplate("color_corruption_example.pdf", pagesize=letter)
doc.title('Color corruption example')
frame = Frame(0.5 * inch, 0.5 * inch, 7.5 * inch, 10 * inch)  # Margins set at 0.5 inch
doc.addPageTemplates([PageTemplate(id="chart_template", frames=[frame])])

# Create a list to hold all content elements
elements = []

elements.append(svg_to_flowable(chart1_svg, 7.5 * inch, 3 * inch))  # Full-width chart

elements.append(Spacer(1, 0.5 * inch))

elements.append(svg_to_flowable(chart2_svg, 7.5 * inch, 3 * inch))  # Full-width chart

# Build the PDF document
doc.build(elements)

print("PDF created successfully!")

@claudep
Copy link
Collaborator

claudep commented Oct 9, 2024

Thanks for the procedure, I also thought about a SVG sample, if possible.

@jdefockert
Copy link
Author

I updated the example so that it saves the generated charts as SVG as well.

@jdefockert
Copy link
Author

Uploaded them here for your convenience as well.
Orange_Chart
Dark_Blue_Chart

@claudep
Copy link
Collaborator

claudep commented Oct 14, 2024

Many thanks for the sample files.

I was able to identify this issue as a bug in reportlab color converter (when any color in the rgb() expression has the value 1). This patch in reportlab should fix the issue:

diff --git a/src/reportlab/lib/colors.py b/src/reportlab/lib/colors.py
index 2c409bc8..f2c9ba80 100644
--- a/src/reportlab/lib/colors.py
+++ b/src/reportlab/lib/colors.py
@@ -16,6 +16,8 @@ rhese can be constructed from several popular formats.  We also include
 These tests are here because doctest cannot find them otherwise.
 >>> toColor('rgb(128,0,0)')==toColor('rgb(50%,0%,0%)')
 True
+>>> toColor('rgb(255,1,0)')==Color(1,.003922,0)
+True
 >>> toColor('rgb(50%,0%,0%)')!=Color(0.5,0,0,1)
 True
 >>> toColor('hsl(0,100%,50%)')==toColor('rgb(255,0,0)')
@@ -787,7 +789,7 @@ class cssParse:
         v = v.strip()
         try:
             c=float(v)
-            if 0<c<=1: c *= 255
+            if 0<c<1: c *= 255
             return int(min(255,max(0,c)))/255.
         except:
             raise ValueError('bad argument value %r in css color %r' % (v,self.s))

I don't know if the support of a rgb() syntax with values between 0 and 1 was something deliberate or not. I don't find any reference of such a syntax in the CSS specs.

@replabrobin, would you be able to upstream that fix in reportlab, or would you like a post on the mailing list?

@replabrobin
Copy link
Contributor

I think the intention was to support the case where people used only 0-1 values for all rgba values. The assumption is then that these represent fractions of the whole range. Clearly not well thought out; blame me.

Is there a soltution that's better. Probably we should check that all values are in the range [0, 1] before assuming the 255 factor.

@claudep
Copy link
Collaborator

claudep commented Oct 15, 2024

I guess that rgb(1, 1, 1) might be ambiguous in any case. Do you think it's worth supporting the rgb(0.3, 0.2, 0.7) use case at all, as it doesn't seem to conform to the specs?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants
@claudep @replabrobin @jdefockert and others