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

desktop: Fixes #7521 Svg images are correctly sized when exported as png #7546

Merged
merged 7 commits into from
Feb 5, 2023

Conversation

adarsh-sgh
Copy link
Contributor

@adarsh-sgh adarsh-sgh commented Dec 28, 2022

Mermaid images when exported as png were having very low resolution.

Before

using rendered image's dimensions as exported image dimension
image

After

using SVG's viewBox attribute as exported image's dimensions
image

Fixes #7521

@github-actions
Copy link
Contributor

github-actions bot commented Dec 28, 2022

CLA Assistant Lite bot All contributors have signed the CLA ✍️ ✅

@adarsh-sgh
Copy link
Contributor Author

I have read the CLA Document and I hereby sign the CLA

@adarsh-sgh adarsh-sgh marked this pull request as draft December 28, 2022 09:48
@adarsh-sgh
Copy link
Contributor Author

adarsh-sgh commented Dec 28, 2022

converted to draft since added jest test fails when run together using yarn test, they are passing when only that test file is run individually

TypeError: (0 , contextMenuUtils_1.svgDimensions) is not a function or its return value is not iterable

edit: was some hoisting err perhaps, solved by removing and reinstalling node_modules

Copy link
Collaborator

@personalizedrefrigerator personalizedrefrigerator left a comment

Choose a reason for hiding this comment

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

Thank you for working on this! I've left a few comments.


describe('contextMenuUtils',()=>{
test('should return the correct dimensions for svg with space seperated viewBox values',()=>{
const svg = '<svg viewBox="0 0 45.9375 60" "></svg> ';
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
const svg = '<svg viewBox="0 0 45.9375 60" "></svg> ';
const svg = '<svg viewBox="0 0 45.9375 60"></svg> ';

Consider removing the extra ".

expect(width).toBe(undefined);
expect(height).toBe(undefined);
});

Copy link
Collaborator

Choose a reason for hiding this comment

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

Please also consider the following cases:

  • viewBox has the form viewBox='1,2,3,4' or viewBox = "1,2,3,4" instead of viewBox="1,2,3,4"
  • viewBox has the form
viewBox='
	1,
	2,
	3 ,
	4 ,
'

@@ -106,8 +106,12 @@ export function menuItems(dispatch: Function): ContextMenuItems {
if (!options.filename) {
throw new Error('Filename is needed to save as png');
}
const [width,height] = svgDimensions(options.textToCopy);
if (!Number.isInteger(width) || !Number.isInteger(height)) {
bridge().showErrorMessageBox(_('failed to get width and height of image'));
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think, with this change, <svg xmlns="http:https://www.w3.org/2000/svg"><rect x='5' y='5' width='50' height='50' fill='red' stroke='black' style='stroke-width: 5;'/></svg> would show an error message when it wouldn't before, because of the missing viewBox attribute.

@adarsh-sgh adarsh-sgh marked this pull request as ready for review December 28, 2022 11:25
@laurent22
Copy link
Owner

The viewport is the dimension in user space - doesn't it mean that if the window is small, the exported PNG will be small too? And we wouldn't want that, instead it should be dimensions that make sense (although I don't know what that is), independent of the viewport.

Why is the PNG small to begin with? Isn't it one of those things where we can just double the exported dimensions and it will work?

@adarsh-sgh
Copy link
Contributor Author

adarsh-sgh commented Dec 28, 2022

if the window is small, the exported PNG will be small too

yeah that would be a problem in this approach 🤔
viewBox is not affected by resizing window

Why is the PNG small to begin with

cause we are making an <img> element for the svg and using it's resolution to set exported image's resolution
img is a replaced element and it's default size is 300 * 150 a/c to google search.

we can just double the exported dimensions and it will work?

considering below two images
image
image

both have same height of 150px (default height of replaced elements)
so if we double them these will still have same height which I guess is not desirable in this case.

@laurent22
Copy link
Owner

So what would be the solution then?

@personalizedrefrigerator
Copy link
Collaborator

It might make sense to have a minimum dimension when exporting SVGs. We could then increase the image's width and height if necessary.
For example, if the minimum dimension is 400, img.height = 20, and img.width = 100, we could multiply width and height by 20, making $\min(\texttt{img.width}, \texttt{img.height}) = \min(20\times 100, 20\times 20) = \min(1000, 400) = 400$.

@adarsh-sgh
Copy link
Contributor Author

The viewport is the dimension in user space

it's viewBox actually 🥲.

@adarsh-sgh
Copy link
Contributor Author

adarsh-sgh commented Dec 28, 2022

yeah that would be a problem in this approach 🤔

my bad 😅 ,
the viewbox's values are not varying when I resize the window of joplin if that's what we are concerned with.

it might make sense to have a minimum dimension when exporting SVGs. We could then increase the image's width and height if necessary.
For example, if the minimum dimension is 400, img.height = 20, and img.width = 100, we could multiply width and height by 20, making min(img.width,img.height)=min(20×100,20×20)=min(1000,400)=400.

yeah that would be nice

@adarsh-sgh
Copy link
Contributor Author

so what is the final implementation ?
IMO combining viewbox information and minimum dimension approach explained by Henry would be sufficient for this ?
should I implement and push that ?

@laurent22
Copy link
Owner

It might make sense to have a minimum dimension when exporting SVGs. We could then increase the image's width and height if necessary. For example, if the minimum dimension is 400, img.height = 20, and img.width = 100, we could multiply width and height by 20, making min(img.width,img.height)=min(20×100,20×20)=min(1000,400)=400.

I don't think a hard coded minimum dimension would work, because 400 is way too big for certain tiny diagrams, or way too small for massive ones. Basically there can be all sizes of diagrams so the dimensions we choose need to be based on the diagram.

@adarsh-sgh, did you check the Mermaid API - do they provide some info about the diagram such as recommended width or similar?

@adarsh-sgh
Copy link
Contributor Author

did you check the Mermaid API

could not find anything like that there, but found about their live editor which also has PNG download functionality.
they are using dimensions of rendered svg (on the preview side) as default resolution for downloaded image, and allowing user to override height or width by entering a value for it.

https://github.com/mermaid-js/mermaid-live-editor/blob/7c64a6549779435986739d48d5dbf3710725c281/src/lib/components/Actions.svelte#L35

@laurent22
Copy link
Owner

they are using dimensions of rendered svg (on the preview side) as default resolution for downloaded image, and allowing user to override height or width by entering a value for it.

Sounds reasonable. How about using this technique but double the dimensions (just to be sure it's high res enough for printing)?

@adarsh-sgh
Copy link
Contributor Author

Sounds reasonable. How about using this technique but double the dimensions (just to be sure it's high res enough for printing)?

Dimensions of rendered Svg will depend on window size, That's something we want to avoid I guess ?

if the window is small, the exported PNG will be small too? And we wouldn't want that

@adarsh-sgh
Copy link
Contributor Author

How about using this technique

Yes, allowing user's to override auto detected dimensions would be a very helpful feature IMO not sure about other point.

@laurent22
Copy link
Owner

Dimensions of rendered Svg will depend on window size, That's something we want to avoid I guess ?

But what's the alternative, do you have any suggestion?

We don't want to ask the user for dimensions, just double it and that should work in most cases.

@adarsh-sgh
Copy link
Contributor Author

But what's the alternative, do you have any suggestion?

using viewBox attribute's height and width values is a good plan IMO.

  • it's independent of window size in which diagram is rendered.
  • it's values are proportional to size of mermaid diagram.
small and big diagrams example

these are PNGs exported with viewBox dimensions

small-fixed
mermaid with a single block (39 * 55)


mermaid-1672247198591
bigger mermaid diagram (389*303)

@laurent22
Copy link
Owner

But since those are user space dimensions, isn't it the same as using the window dimensions? Also what is setting the viewbox? Is that the mermaid lib? And how do they determine the dimensions then?

@adarsh-sgh
Copy link
Contributor Author

adarsh-sgh commented Jan 2, 2023

what is setting the viewbox? Is that the mermaid lib

Yes.

how do they determine the dimensions then?

They $draw^1$ the svg and use it's getBBox method to get the smallest rectangle in which object fits.

setupGraphViewbox fxn of mermaid

  1. d3 lib is used to draw it

But since those are user space dimensions, isn't it the same as using the window dimensions?

sorry I could not get which window we are talking about.

@laurent22
Copy link
Owner

But the part I don't get here is that it's vector graphics, so the vector dimensions are irrelevant and can't be of any help to export to png. And the coordinates are floating values, so for example you could fit an infinity of vector graphics in a 1.0 x 1.0 square. However if you export that to a 1x1 pixels image, that's not going to make any sense.

So how is it done normally, are there ways to get reasonable dimensions from some API or calculations? Does the DOM SVG API provides this maybe?

@adarsh-sgh
Copy link
Contributor Author

adarsh-sgh commented Jan 4, 2023

are there ways to get reasonable dimensions from some API or calculations? Does the DOM SVG API provides this maybe?

Could not find anything useful in DOM svg APIs.
getBBox was already discussed above but it has same pros and cons as viewBox.

When we render a mermaid diagram using mermaid js in browser, then the size in which browser renders it, can we say that that size is a reasonable dimension ?

it's vector graphics, so the vector dimensions are irrelevant and can't be of any help to export to png. And the coordinates are floating values, so for example you could fit an infinity of vector graphics in a 1.0 x 1.0 square.

yes that is a problem, by using viewBox as exported dimensions we are making this assumption that viewBox size is also the recommended size of svg, which seems true in case of mermaid exported svgs since they render with same size as defined in viewBox not scaled up or down.

@laurent22
Copy link
Owner

When we render a mermaid diagram using mermaid js in browser, then the size in which browser renders it, can we say that that size is a reasonable dimension ?

Yes that's what I meant by window dimensions.

So it seems we always go back to this, so let's do this. You don't need the XML regex parsing as that's too unreliable, I think you can get the width and height directly from the DOM. Then double those dimensions to make sure it's always big enough even on hdpi screens.

In fact, doesn't it mean that we keep the current code as it is but add * 2 for the dimensions?

@adarsh-sgh
Copy link
Contributor Author

You don't need the XML regex parsing as that's too unreliable

Yes already removed that and was using dom parser to get those values, didn't pushed since I was not sure if we will choose this way or not.

double those dimensions to make sure it's always big enough even on hdpi screens

Okay, on it.

doesn't it mean that we keep the current code as it is but add * 2 for the dimensions?

No, That will be different, all images will have dimension 600 * 300. 2nd part of comment linked below explains why #7546 (comment)

@laurent22
Copy link
Owner

No, That will be different, all images will have dimension 600 * 300. 2nd part of comment linked below explains why #7546 (comment)

I don't know about the technical details, but the dimensions of all the exported diagrams should not be 600x300 - at a minimum the aspect ratio should match the diagram aspect ratio, which I assume we can get accurately. And the size should be based on the note viewer size.

@adarsh-sgh
Copy link
Contributor Author

dimensions of all the exported diagrams should not be 600x300 - at a minimum the aspect ratio should match the diagram aspect ratio, which I assume we can get accurately. And the size should be based on the note viewer size.

Sure, That's how it will be. 🙌🏼

we will

  1. Parse the svg using DOMParser to get id of svg
  2. getBoundingClientRect by selecting the svg from dom (like mermaid live editor does)

@laurent22
Copy link
Owner

  1. Parse the svg using DOMParser to get id of svg

But why do you need to parse the SVG? In svgUriToPng there's full access to the DOM so you shouldn't need to do any XML regex parsing.

@adarsh-sgh
Copy link
Contributor Author

adarsh-sgh commented Jan 6, 2023

why do you need to parse the SVG
there's full access to the DOM

we have access to the document but we need to select the svg element to get it's rendered size, I was thinking of getting the ID of svg using the svg string we are given, then select svg element from dom using querySelector

you shouldn't need to do any XML regex parsing.

instead of regex, DOMParser.parseFromString() can be used, it wouldn't have those drawbacks I think.

edit: I have pushed the proposed changes for discussion. will add tests if this approach is fine.

const id = parser.parseFromString(svg, 'text/html').querySelector('svg').id;
({ width , height } = document.querySelector<HTMLIFrameElement>('.noteTextViewer').contentWindow.document.querySelector(`#${id}`).getBoundingClientRect());
} catch {
// do nothing
Copy link
Owner

Choose a reason for hiding this comment

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

We don't hide errors, so at least logger.warn(...). Please search how loggers are created

@@ -106,8 +106,10 @@ export function menuItems(dispatch: Function): ContextMenuItems {
if (!options.filename) {
throw new Error('Filename is needed to save as png');
}
// double dimensions to make sure it's always big enough even on hdpi screens
const [width,height] = svgDimensions(document,options.textToCopy).map((x: number) => x * 2);
Copy link
Owner

Choose a reason for hiding this comment

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

The dimensions can be undefined, and you'll end up with NaN as width and height.

Also when it's undefined, what dimensions do we end up using?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

when it's undefined, what dimensions do we end up using?

The dimensions that used to be before this fix.
anything better we can choose as default ? 🤔

Copy link
Owner

Choose a reason for hiding this comment

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

How about the NaN dimensions, what do you plan to do about it?

The dimensions that used to be before this fix.

Which are?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

How about the NaN dimensions, what do you plan to do about it?

assigned dimensions as undefined if it's NaN

Which are?

almost of zero use, but since we have no better fallback we have to use them if we can't find any sensible default value.
Dimensions were being calculated as follows,:
we are making an element for the svg and using it's resolution to set exported image's resolution
img is a replaced element and it's default size is 300 * 150 a/c to google search.
different sized mermaids i exported to check size were having height 150px each. and different heights all less than 300.

Copy link
Owner

Choose a reason for hiding this comment

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

Ok thanks for clarifying. The default dimensions indeed aren't useful so let's make them 1024. Is it possible at least to respect the diagram aspect ratio? Otherwise make it 1024x1024

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let's make them 1024

when we have the aspect ratio, should the bigger/smaller dimension be 1024 or make width = 1024 ?

Copy link
Owner

Choose a reason for hiding this comment

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

The max dimension would be 1024. For example 600x1024, or 1024x800

@laurent22
Copy link
Owner

Please run yarn run linter ./ since I've updated the linter rules

@adarsh-sgh
Copy link
Contributor Author

I am facing a problem while trying to write jest tests , jsDom does not render elements so getBoundingClientRect() always returns all dimensions as 0.
I was trying to test svgDimensions function. any suggestions how to do this ?

@tessus tessus added the desktop All desktop platforms label Jan 22, 2023
@laurent22 laurent22 merged commit 8aad67c into laurent22:dev Feb 5, 2023
@laurent22
Copy link
Owner

Ok let's drop the testing. Thanks for implementing this

@github-actions github-actions bot locked and limited conversation to collaborators Feb 5, 2023
@adarsh-sgh adarsh-sgh deleted the svg-png-fix branch February 8, 2023 06:24
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
desktop All desktop platforms
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Exported Mermaid graphs as PNG are incredibly small
4 participants