diegomura / react-pdf

📄 Create PDF files using React
https://react-pdf.org
MIT License
14.88k stars 1.18k forks source link

Expose API to render graphics #410

Closed markusjohnsson closed 5 years ago

markusjohnsson commented 5 years ago

Description: It would be useful to be able to render vector graphics using react-pdf.

One way could be to expose a moveTo-lineTo-type API (like PDFKit):

  <Document>
    <Page size="A4" style={styles.page}>
      <Canvas>
        {painter => painter
            .moveTo(100, 150)
            .lineTo(100, 250)
            .lineTo(200, 250)
            .fill("#FF3300")}
      </Canvas>
    </Page>
  </Document>

Another, to provide graphical primitives:

  <Document>
    <Page size="A4" style={styles.page}>
      <Path points={[[100, 150], [100, 250], [200, 250]]} fill="#FF3300"/>
    </Page>
  </Document>
diegomura commented 5 years ago

Like both ideas!

There's already an issue for supporting svg primitives, but couldn't work on that so far.

The Canvas primitive would be much easy to implement meanwhile, since we can just expose some pdfkit methods. What is unclear yet is how the coordinate system inside the canvas would work. Currently coordinates are handled internally, so giving the ability to the user to set up fixed things on page can create some conflicts.

One alternative would be making the canvas work always as an absolute element in the page, but that would make almos imposible to layout a canvas with other elements in the page.

Another would be considering (0, 0) the top-left corner of the canvas, whenever this ends up in the page, and calculate dynamically how much space this "box" will occupy in the page by inspecting the calls applied to painter. However, this raises questions about how the canvas should wrap between pages.

Thoughts?

markusjohnsson commented 5 years ago

Yes, I also thought Canvas would be the simpler way to go. I've implemented a proof-of-concept that just exposes this.root.instance with the API above. I can probably do a PR when we are happy with the API and implementation.

Regarding coordinate system; before invoking the render callback I do this:

    const { left, top } = this.getAbsoluteLayout();
    this.root.instance.translate(left, top);

That makes the output render according to the calculated position in the document, but it does not alter scaling. Perhaps this plus just passing the calculated width and height to the render callback would be enough? That way you can either draw using that size, or do .scale(1/width, 1/height) to get into 0-1 normalized space.

Not sure how to handle page wrapping though..

Using Canvas one could build the other primitives if people prefer that:

const Path = ({points: [head, ...tail]}) => (
    <Canvas>
        {(painter) => painter
                    .path(`M ${head[0]},${head[1]} L ${tail.map(p => `${p[0]},${p[1]}`).join(" ")}`)
                    .stroke()}
    </Canvas>);

<Path points={[[100, 150], [100, 250], [200, 250]]} />

As I said, right now my implementation exposes the PDFKit instance directly. Do you think it should be wrapped or limited in some way?

diegomura commented 5 years ago

Awesome research!

In your solution I guess Canvas extends from Base as the other elements do right? It's cool to call this.root.instance.translate with left and top. But does the canvas has a width and height? You can always define some fixed ones with the style prop, but nodes should also be dynamic. I think this is important if there's another element rendering below the canvas. However, these variables are dynamic and uncertain. That why I thought we might have to inspect the calls to painter, but maybe for a first version it's not needed.

I love how the canvas can then enable the creation of other primitives! Fascinating.

I do think we should only export a limited interface of the pdfkit instance. I'm a bit afraid about people messing with it and breaking some other part of the rendering process. Also, by doing that we can eventually add more logic inside each painter call, totally invisible for the user.

Would love to see a PR of this! I would be happy to help you in anything needed

markusjohnsson commented 5 years ago

Thanks.

I have not used react-pdf enough to know, when are width and height dynamic and not set with style? Is it when an element is within of a View with styled width and height? In that case, does getAbsoluteLayout return the correct thing?

I think it is OK for Canvas to have size (0,0) unless something else is given, or calculated from a parent. When you deal with drawing graphics in your document you probably know the size you need. A zero-sized canvas also allows to do some stylish graphics behind text and other Views 😄.

Trying to inspect draw calls to rescale probably makes it messier to use imo. Consider these examples:

<Canvas>{p => p.circle({ radius: 100 }).fill('red')}</Canvas>
<Canvas>{p => p.circle({ radius: 10 }).fill('green')}</Canvas>
<Canvas>{p => p.circle({ radius: 1 }).fill('blue')}</Canvas>

If we inspect draw calls and rescale the canvas on behalf of the user, these circles will be the same size, even though we have specified different radii. I would have to use style (or some size prop) to get different sizes. (I'm not saying no though, just raising one issue with rescaling the coordinate system based on content).

Wrapping pdfkit to limit functionality makes sense, although re-creating a fluent API is somewhat tedious. I shall give it a shot (edit: wasn't that hard)

Another issue is DOM rendering. Although I have not used it, I see in the Readme that it is supported to render a Document using ReactDOM as well as with react-pdf:

ReactDOM.render(<Canvas>{p => p.circle({ radius: 5 }).fill('green')}</Canvas>)

This would need a separare implementation of the drawing API using html canvas. Are other elements rendering properly using ReactDOM? How would I override the html implementation of an element?

You can see my branch over at https://github.com/markusjohnsson/react-pdf/tree/canvas_node

diegomura commented 5 years ago

Great work @markusjohnsson ! Love how this is looking so far. I made some comments. Hope you don't mind 😄 I really appreciate your effort! Looking forward to see a PR of this!

diegomura commented 5 years ago

Hi @markusjohnsson Sorry for the silence.

I understand that you had issues using render as the prop for the Canvas element. However, I do feel we should be consistent on this. We own the page wrap algorithm so we can do changes on it (or in the other nodes) to be able to support that. Let me know if you need help on it

markusjohnsson commented 5 years ago

Not sure what you are suggesting.. The problem I encountered was that if Canvas had a render prop, then it got called as part of the dynamic nodes feature. Are you suggesting that those two be integrated and the painter object be passed to all dynamic nodes render calls, or just the ones in a Canvas?

That being said, I haven't really thought about the integration between those two features and I appreciate any suggestions you have on the topic.

EDIT: right now I'm on a tight deadline and will probably ship a custom version of react-pdf with canvas support for a project in a couple of days. I will come back and work on the pull request to make it suitable for inclusion after this deadline and then update my project with any changes necessary.

markusjohnsson commented 5 years ago

Concerning render size and page wrapping, I do think Canvas should behave like Image in that it is not page wrappable. I think that providing the size as style is the best way to know how large the user want the element to be. Trying to measure content based on draw calls might be hard to do, as bezier curves and splines can overshoot and are not contained within the convex hull of the coordinates that define them.