JuliaGraphics / Cairo.jl

Bindings to the Cairo graphics library.
Other
87 stars 57 forks source link

Distinguishing between CairoSVGSurface and CairoPDFSurface #316

Open MaximeBouton opened 4 years ago

MaximeBouton commented 4 years ago

When trying to write a cairo surface to a file, it is not possible to distinguish whether the surface object is a pdf or svg surface.

MWE:

using Cairo 

function Base.write(filename::String, surface::Cairo.CairoSurfaceIOStream)
    finish(surface)
    seek(surface.stream, 0)
    open(filename, "w") do io
        write(io, read(surface.stream, String))
    end
    return
end

c = CairoSVGSurface(IOBuffer(), 1000, 600);
write("output.svg", c) # this works fine 
write("output.pdf", c) # this would also have worked but the output file would not really be a pdf

Is there a convenient way to enforce that the file extension matches the type of Cairo stream? Right now both CairoSVGSurface and CairoPDFSurface outputs a CairoSurfaceIOStream which makes it not possible to dispatch between the two.

lobingera commented 4 years ago

What are you trying to do? The logic of libcairo is, that you decide the type of surface before starting to draw and to render. If you are looking for a lazy/late decision, you might look at recordingSurfaces. btw: the non-stream (with filename) surface constructors still exist (although libcairo documentation discourages the use).

MaximeBouton commented 4 years ago

We have a visualization package that relies on Cairo: https://github.com/sisl/AutoViz.jl The main usage is through a render function, that accepts a CairoSurface as keyword argument (default to CairoSVGSurface).

Now we would like users to be able to save the rendered scenes in different format using write. If they want a PDF they would have to pass in a CairoPDFSurface to the function which is fine, but we were wondering if there was a way to warn user when they try to do something like this:

c = render([roadway, veh1, veh2], camera=CarFollowCamera(...))  # default to SVGSurface 
write("scene.pdf", c) 

This code will run fine but the user might think they have a pdf and instead they have an svg.

The correct way would have been to do:

c = render([roadway, veh1, veh2], camera=CarFollowCamera(...), surface=CairoPDFSurface(...))
write("scene.pdf", c) 
lobingera commented 4 years ago

I see. a) One could get the 'type' of a surface by looking via the surface type ptr into the c-struct of a cairo surface i.e.

struct _cairo_surface {
    const cairo_surface_backend_t *backend;
    cairo_device_t *device;

    /* We allow surfaces to override the backend->type by shoving something
     * else into surface->type. This is for "wrapper" surfaces that want to
     * hide their internal type from the user-level API. */
    cairo_surface_type_t type;

    cairo_content_t content;

    cairo_reference_count_t ref_count;
    cairo_status_t status;

b) workaround could be to look at the actual stream, as both PDF and SVG have headers (files need to start with a sequence of know bytes)

c) take the information along your 'c' output (type) of the render function. d) ... as someone not believing in file extensions -> you cannot avoid users to do wrong.

MaximeBouton commented 4 years ago

Thanks for the tips, I will give a shot to a)!

MaximeBouton commented 4 years ago

this worked:

typ = ccall((:cairo_surface_get_type, Cairo.libcairo), Cint, (Ptr{Nothing},), c.ptr)
if typ == Cairo.CAIRO_SURFACE_TYPE_PDF 
    # do stuff 
end 

Thanks!