CairoMakie: Restrict PDF version

I need to restrict the version of output PDFs to 1.5. I didn’t figure out how to do it. I could only find a Discourse topic, where it was pointed out that cairo provides a function to restrict the version.

I am not familiar with Makie’s codebase and I needed the feature quickly, so I hacked it in the following way. I ]deved Makie, and changed surface_from_output_type to:

function surface_from_output_type(type::RenderType, io, w, h)
    # ...
    elseif type === PDF
        surface = Cairo.CairoPDFSurface(io, w, h)
        ccall((:cairo_pdf_surface_restrict_to_version, Cairo.libcairo), Nothing,
          (Ptr{UInt8},Int32), surface.ptr, 1) # 1 means 1.5
        return surface
     # ...
end

It worked, but it is obviously not the ideal way. I have two questions:

  1. Have I missed an easier way to set the PDF version?
  2. If not, can we turn this into a PR (not sure if it belongs to Makie or Cairo.jl) to provide a more convenient way?
2 Likes

Looking to do the same thing, looks like it might be easier to make changes in the save function, between the screen = getscreen and backend_show call

using CairoMakie
using CairoMakie.Makie: FigureLike, current_backend, update_state_before_display!, getscreen, get_scene, filetype, isvisible, backend_show
using CairoMakie.Makie.FileIO

CairoMakie.activate!()

function save_pdf(
    filename::String, fig::FigureLike; args...
)
    save_pdf(FileIO.query(filename), fig; args...)
end

function save_pdf(
    file::FileIO.Formatted, fig::FigureLike;
    size=Base.size(get_scene(fig)),
    resolution=nothing,
    backend=current_backend(),
    update=true,
    pdf_vers=1,
    screen_config...
)
    scene = get_scene(fig)
    if resolution !== nothing
        @warn "The keyword argument `resolution` for `save()` has been deprecated. Use `size` instead, which better reflects that this is a unitless size and not a pixel resolution."
        size = resolution
    end
    if size != Base.size(scene)
        resize!(scene, size)
    end
    filename = FileIO.filename(file)
    # Delete previous file if it exists and query only the file string for type.
    # We overwrite existing files anyway, so this doesn't change the behavior.
    # But otherwise we could get a filetype :UNKNOWN from a corrupt existing file
    # (from an error during save, e.g.), therefore we don't want to rely on the
    # type readout from an existing file.
    isfile(filename) && rm(filename)
    # query the filetype only from the file extension
    F = filetype(file)
    mime = MIME("application/pdf")

    try
        return open(filename, "w") do io
            # If the scene already got displayed, we get the current screen its displayed on
            # Else, we create a new scene and update the state of the fig
            update && update_state_before_display!(fig)
            visible = isvisible(getscreen(scene)) # if already has a screen, don't hide it!
            config = Dict{Symbol,Any}(screen_config)
            get!(config, :visible, visible)
            screen = getscreen(backend, scene, config, io, mime)
            ccall((:cairo_pdf_surface_restrict_to_version, CairoMakie.Cairo.libcairo), Nothing,
                (Ptr{UInt8}, Int32), screen.surface.ptr, pdf_vers)
            backend_show(screen, io, mime, scene)
        end
    catch e
        # So, if open(io-> error(...), "w"), the file will get created, but not removed...
        isfile(filename) && rm(filename; force=true)
        rethrow(e)
    end
end

# ---

begin
    fig = Figure()
    ax = Axis(fig[1, 1])
    x = 0:0.05:pi
    lines!(x, sin.(x))
    fig
end

save_pdf("test2.pdf", fig)

gives

pdfinfo test2.pdf                                                                                                                              09:04:35 PM
Producer:        cairo 1.18.0 (https://cairographics.org)
CreationDate:    Wed May  8 21:09:23 2024 EDT
Custom Metadata: no
Metadata Stream: no
Tagged:          no
UserProperties:  no
Suspects:        no
Form:            none
JavaScript:      no
Pages:           1
Encrypted:       no
Page size:       450 x 338 pts
Page rot:        0
File size:       4071 bytes
Optimized:       no
PDF version:     1.5
2 Likes

I took some time to look around the codebase and proposed a PR: CairoMakie: Allow restricting PDF version by barucden · Pull Request #3845 · MakieOrg/Makie.jl · GitHub

I am sure the maintainers will have some suggestions but it already allows to do this:

fig = Figure()
ax = Axis(fig[1, 1])
x = range(0, 10, length=100)
y = sin.(x)
save("version_1-4.pdf", fig, pdf_version="1.4")
save("version_1-7.pdf", fig, pdf_version="1.7")

shell> file version_1-4.pdf
version_1-4.pdf: PDF document, version 1.4, 1 page(s)

shell> file version_1-7.pdf
version_1-7.pdf: PDF document, version 1.7
3 Likes

Good news for anyone interested: the feature has been merged (Restrict pdf version by SimonDanisch · Pull Request #3885 · MakieOrg/Makie.jl · GitHub). It should be a part of the next release, v0.21.3.

@barucden just out of curiosity, did you need to restrict to 1.5 for latex purposes? If so,

\pdfminorversion=7 % needs to be before the documentclass (i.e. pdf can not be started)

in the preamble (or pre-preamble?) should allow you to use later pdf versions.

For example

\pdfminorversion=7 
\documentclass[aspectratio=1610]{beamer}

It was indeed for a figure in a paper written in latex.

I did not know about \pdfminorversion. Anyway, I am not sure if the paper is even allowed to be of a version higher than 1.4. I might have tried it. Thanks for letting me know.