Skip to content

Fix to "exported SVG files blurred in viewers" #17062

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

Merged
merged 7 commits into from
May 20, 2020

Conversation

cover-me
Copy link
Contributor

@cover-me cover-me commented Apr 7, 2020

PR Summary

As mentioned in #12065 and #10112, SVG viewers (Chrome, Inkscape...) perform interpolation (blurring) by default. However, for files generated by imshow(interpolation='none'), there shouldn't be any interpolation on the viewer's side. image-rendering:pixelated disable the blurred-interpolation in SVG viewers.

'preserveAspectRatio':'none' allow pixels to be rectangles in SVG viewers. Otherwise, pixels may be rendered as squares only, leading to problems if the image is not 1:1 scaled.

SVG is important because the text of PNG in Jupyter notebooks inline mode looks blurred.

PR Checklist

Just a change of two lines.

Po added 2 commits April 7, 2020 15:42
Change `image-rendering` from `optimizeSpeed` to `pixelated`. `optimizeSpeed` is used by Inkscape only
@tacaswell
Copy link
Member

tacaswell commented Apr 7, 2020

SVG is important because the text of PNG in Jupyter notebooks inline mode looks blurred.

I agree svg is important in and of its self, however you can fix the blurry text in the notebook by using ipympl (which also gets you live figures!) or by increasing the dpi that inline uses.

@tacaswell tacaswell added this to the v3.3.0 milestone Apr 7, 2020
@tacaswell
Copy link
Member

I am concerned that we are applying this change both un-conditionally and to only one of the two branches that write out image elements to the svg. I can see the argument that in all cases, even if we are already re-sampling the image, that we don't want the viewer further re-sampling (which I assume it would do in RGB(A) space which in the case of false-color images leads to "wrong" colors showing up) but I am less sure about the aspect ratio.

This will definitely require some tests.

@jklymak
Copy link
Member

jklymak commented Apr 7, 2020

Can you post before/after examples where this improves things?

@cover-me
Copy link
Contributor Author

cover-me commented Apr 8, 2020

Can you post before/after examples where this improves things?

# generate an SVG image with 4 pixels
import matplotlib.pylab as plt
a = [[0,0],[0,1]]
# interpolation='none' is the only way to avoid resampling images in saved SVG files.
# interpolation=None will produce a new image with much more pixels in SVG files.
plt.imshow(a,interpolation='none')
plt.savefig('a.svg')

Before

Screenshot 2020-04-07 at 20 06 33

Below is the <image> part in a text editor. The image has only 4 pixels. It's scaled up to fit the axes. However, SVG viewers will do interpolations such as bilinear for the scaled-up images (Ref).

<image height="2" id="imageb494a1e85e" transform="matrix(109 0 0 109 112.68 34)" width="2" xlink:href="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAABHNCSVQICAgIfAhkiAAAABpJREFUCJljdGEM+c/AwMDAxMDAwLDz2QUGAB7eBAuDpB2sAAAAAElFTkSuQmCC"/>

After
Screenshot 2020-04-07 at 20 08 57

Below is the <image> part in a text editor, two new attributes are added. style="image-rendering:pixelated" works. preserveAspectRatio="none" looks not necessary here because height and width attributes are the same for this image. The ratio and real figure size are controlled by the transform matrix. In order versions of Matplotlib, however, the 'width' and 'height' are set to real values (usually not 1:1), so preserveAspectRatio="none" would be a good solution at that time. Somehow the base64 data is a bit different (maybe due to other updates to the Matplotlib master branch?)

<image height="2" id="image2ef7ee41b4" preserveAspectRatio="none" style="image-rendering:pixelated" transform="matrix(109 0 0 109 112.68 34)" width="2" xlink:href="data:image/png;base64,
iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAGklEQVR4nGN0YQz5z8DAwMDEwMDAsPPZBQYAHt4ECzzPS7MAAAAASUVORK5CYII="/>

@cover-me
Copy link
Contributor Author

cover-me commented Apr 8, 2020

I am concerned that we are applying this change both un-conditionally and to only one of the two branches that write out image elements to the svg. I can see the argument that in all cases, even if we are already re-sampling the image, that we don't want the viewer further re-sampling (which I assume it would do in RGB(A) space which in the case of false-color images leads to "wrong" colors showing up) but I am less sure about the aspect ratio.

This will definitely require some tests.

Yes, there may be many different cases...

The preserveAspectRatio="none": I did some tests... It's not necessary anymore. In older versions of Matplotlib, images are described in physical width and physical height. That requires preserveAspectRatio="none" otherwise the image would show in a ratio based on the pixels. Now Matplotlib describes images in SVG files by pixels and uses transform matrice to scale images. Problem aready solved.

@jklymak
Copy link
Member

jklymak commented Apr 15, 2020

# generate an SVG image with 4 pixels
import matplotlib.pylab as plt
a = [[0,0],[0,1]]
# interpolation='none' is the only way to avoid resampling images in saved SVG files.
# interpolation=None will produce a new image with much more pixels in SVG files.
plt.imshow(a,interpolation='none')

OK, but if I understand correctly, this change turns off antialiasing for svg viewers for full-resolution images as well, not just those with interpolation='none'? I think if something like this is to go in, it should not tell the viewer what to do in all cases, but just for this particular use of imshow.

@cover-me
Copy link
Contributor Author

# generate an SVG image with 4 pixels
import matplotlib.pylab as plt
a = [[0,0],[0,1]]
# interpolation='none' is the only way to avoid resampling images in saved SVG files.
# interpolation=None will produce a new image with much more pixels in SVG files.
plt.imshow(a,interpolation='none')

OK, but if I understand correctly, this change turns off antialiasing for svg viewers for full-resolution images as well, not just those with interpolation='none'? I think if something like this is to go in, it should not tell the viewer what to do in all cases, but just for this particular use of imshow.

I believe it affects interpolation='none' only, where the original image is used for output and a transform matrix is required for scaling up/positioning. For the other cases, the image is resized and interpolated before output, they go into the transform is None brach.

@efiring
Copy link
Member

efiring commented May 8, 2020

Reading the discussion and looking at the diff, my understanding is that this comes down to a single line, since the aspect ratio line is not needed now. The basic idea sounds reasonable, and my guess is that this should go in.

I think it needs a test, though, verifying that it works as intended for images with interpolation='none' and has no other effect. I don't know what form that test should take.

@tacaswell tacaswell modified the milestones: v3.3.0, v3.4.0 May 8, 2020
@QuLogic
Copy link
Member

QuLogic commented May 8, 2020

Looking at Firefox 75, it doesn't look like 'pixelated' is a valid setting, at least looking at the style editor when viewing an SVG directly. Using 'crisp-edges', 'optimizespeed', or '-moz-crisp-edges' works. I expect you would have to set this multiple times to hit both old and new names.

@anntzer anntzer removed their assignment May 11, 2020
@anntzer
Copy link
Contributor

anntzer commented May 11, 2020

The idea looks fine, but I can't review this carefully (in particular browser compat) for now, so unassigning myself.

@cover-me
Copy link
Contributor Author

A full table of browser compatibility can be found here. It looks like Firefox and IE are different from the rest browsers.

I did some tests with Firefox 76, Chrome 81, and Inkscape 0.92 on Win7:

  • optimizespeed or crisp-edges works for Firefox, doesn't work for Chrome or Inkscape. optimizespeed is deprecated according to the link above.
  • pixelated works for Chrome and Inkscape, doesn't work for Firefox.

Setting the style to image-rendering:crisp-edges;image-rendering:pixelated works for Chrome, Firefox and Inkscape. I have updated the code.

Copy link
Member

@QuLogic QuLogic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix flake8.

@jklymak
Copy link
Member

jklymak commented May 20, 2020

Any reason this shouldn't go in for 3.3? I'll re-milestone, but feel free to undo...

@jklymak jklymak merged commit 0edefcd into matplotlib:master May 20, 2020
@jklymak
Copy link
Member

jklymak commented May 20, 2020

Thanks a lot for this fix @cover-me !

@jklymak jklymak modified the milestones: v3.4.0, v3.3.0 May 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants