Skip to content

Instantly share code, notes, and snippets.

@oxplot
Last active February 3, 2025 21:38
Show Gist options
  • Save oxplot/4b5ded9875cbba3e514f to your computer and use it in GitHub Desktop.
Save oxplot/4b5ded9875cbba3e514f to your computer and use it in GitHub Desktop.

Revisions

  1. oxplot revised this gist Nov 24, 2022. 2 changed files with 2 additions and 3 deletions.
    2 changes: 2 additions & 0 deletions svglinkify.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    Latest Inkscape version has this functionality built-in now - except for internal links. For that, see
    https://github.com/oxplot/svglinkify
    3 changes: 0 additions & 3 deletions svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1,3 +0,0 @@
    #
    # Latest Inkscape version has this functionality built-in now.
    #
  2. oxplot revised this gist Nov 23, 2022. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1 +1,3 @@
    # Latest Inkscape version has this functionality built-in now.
    #
    # Latest Inkscape version has this functionality built-in now.
    #
  3. oxplot revised this gist Nov 23, 2022. 1 changed file with 1 addition and 5 deletions.
    6 changes: 1 addition & 5 deletions svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1,5 +1 @@
    #
    # New version with much better usability: https://github.com/oxplot/svglinkify/releases
    #
    # Old version: https://github.com/oxplot/gists/blob/master/svglinkify.py
    #
    # Latest Inkscape version has this functionality built-in now.
  4. oxplot revised this gist Oct 31, 2018. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,5 @@
    #
    # New version: https://github.com/oxplot/svglinkify/releases
    # New version with much better usability: https://github.com/oxplot/svglinkify/releases
    #
    # Old version: https://github.com/oxplot/gists/blob/master/svglinkify.py
    #
  5. oxplot revised this gist Oct 31, 2018. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    #
    # Moved to https://github.com/oxplot/gists/blob/master/svglinkify.py
    # New version: https://github.com/oxplot/svglinkify/releases
    #
    # Old version: https://github.com/oxplot/gists/blob/master/svglinkify.py
    #
  6. oxplot revised this gist Feb 12, 2016. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1 +1,3 @@
    # Moved to https://github.com/oxplot/gists/blob/master/svglinkify.py
    #
    # Moved to https://github.com/oxplot/gists/blob/master/svglinkify.py
    #
  7. oxplot revised this gist Feb 12, 2016. 1 changed file with 1 addition and 238 deletions.
    239 changes: 1 addition & 238 deletions svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -1,238 +1 @@
    #!/usr/bin/env python
    # svglinkify.py - Add hyperlinks to PDFs generated by Inkscape
    # Copyright (C) 2015 Mansour Behabadi <[email protected]>
    #
    # This script comes with no warranty whatsoever. Use at your own risk.
    # If you decide to distribute verbatim or modified versions of this
    # code, you must retain this copyright notice.
    #
    # Usage: svglinkify.py <svg-file> <inkscape-gen-pdf> <linkified-pdf>
    # Requires:
    # qpdf
    # inkscape
    # python 2/3
    #
    # WARNING Since this script is one heck of a hack, you should follow the
    # instructions below to the letter, or you will fail miserably.
    #
    # 1. Start by making an SVG that looks nice and everything and add a
    # piece of text somewhere.
    #
    # 2. Select the rectangle tool and draw a box on top of the text.
    # This box will be the clickable area of our link. Set its fill color
    # to #ff00ff (magenta) and remove any strokes.
    #
    # 3. Right click the box and select "Create Link". In the "Object
    # attributes" window that opens up, type the destination link in
    # "Href".
    #
    # 4. Send the box to the back (using End key on the keyboard) so you can
    # see your text. DO NOT move your box at any time after you've
    # created the link. More details below.
    #
    # 5. Export your SVG as PDF and run svglinkify.py:
    #
    # $ svglinkify.py my_doc.svg my_doc.pdf my_doc_with_links.pdf
    #
    # So you pass your SVG file as the first arg, the exported PDF as the
    # 2nd arg and the name of final PDF as 3rd arg.
    #
    # 6. If you did everything right, open my_doc_with_links.pdf and you
    # should be able to click your text and open the link in browser. You
    # also notice that the magenta box is gone. That's it. Now read the
    # sections below if you hate being frustrated when things break.
    #
    # HOW IT WORKS
    #
    # The script looks for magenta boxes (surprise!) that have a link. It
    # then extracts their x,y position and hyperlinks. It does the same
    # search for magenta boxes in the generated PDF and tries to match them
    # up by their relative locations. Therefore it's crucial to get the
    # locations right. Since SVG is pretty damn flexible, locations aren't
    # always simple x,y attributes. When you create a link for an object,
    # you wrap it in a group. Groups don't have x,y, instead they are
    # transformed using 2D matrices which means, maths calculations must be
    # done in order to find out where the enclosed box really is. This
    # script is too dumb to do that. That's why you should not move a box
    # after you create a link for it.
    #
    # You could either delete it and draw a new one, or if you like it
    # dangerous, you can enter the group (ie double cliking the box) and
    # then move the box. This way, you're not moving the group so no
    # transformations will be applied. You're bound to make a mistake sooner
    # or later this way, so don't do it.
    #
    # If you can't get this to work after at least several attempts, email
    # me your SVG and the PDF inkscape generated for you and I should be
    # able to help.

    from __future__ import unicode_literals
    from __future__ import print_function

    from itertools import count
    from subprocess import call, PIPE, Popen
    import os
    import re
    import sys
    import tempfile

    # Magic to support python both 2 and 3

    try:
    range = xrange
    except:
    pass

    try:
    import HTMLParser as html_parser
    except:
    import html.parser as html_parser
    _html_parser = html_parser.HTMLParser()
    try:
    html_unescape = _html_parser.unescape
    except:
    import html
    html_unescape = html.unescape

    # Command line parsing

    if len(sys.argv) < 4:
    print('Usage: %s <svg-file> <inkscape-gen-pdf> <linkified-pdf>'
    % sys.argv[0], file=sys.stderr)
    exit(1)

    svg_path = sys.argv[1]
    pdf_in_path = sys.argv[2]
    pdf_out_path = sys.argv[3]

    # Load the link rects from SVG file

    SVG_X_PAT = re.compile(r'\bx="([^"]+)"')
    SVG_Y_PAT = re.compile(r'\by="([^"]+)"')

    with open(svg_path, 'r') as svg_file:
    svg_rects = [(
    html_unescape(i[0]),
    float(SVG_X_PAT.search(i[1]).group(1)),
    float(SVG_Y_PAT.search(i[1]).group(1))
    ) for i in re.findall(r'''
    <a[^>]*?\bxlink:href="([^"]+)"[^>]*>\s*<rect
    ([^>]*?\bstyle="[^"]*?\bfill:[#]ff00ff\b[^>]*)
    ''', svg_file.read(), re.X)]

    # QDFy the input PDF & load the resulting PDF to memory

    fd, qdf_tmppath = tempfile.mkstemp()
    os.close(fd)
    try:
    if call(['qpdf', '--qdf', pdf_in_path, qdf_tmppath]) != 0:
    print('error: qpdf failed', file=sys.stderr)
    exit(1)
    with open(qdf_tmppath, 'rb') as ps_file:
    pdf_data = ps_file.read()
    finally:
    try:
    os.unlink(qdf_tmppath)
    except:
    pass

    # Load the rects and last object ID from PDF file

    PDF_RECT_PAT = re.compile(br'''
    \b1\s+0\s+1\s+rg(?:\s+/a0\s+gs)?
    ((?:\s+[\d.-]+\s+[\d.-]+\s+[\d.-]+\s+[\d.-]+\s+re\s+f)+)\b
    ''', re.X)
    m = PDF_RECT_PAT.search(pdf_data)
    pdf_rects = re.split(br'\s+', m.group(1).strip()) if m else []
    pdf_rects = [
    list(map(float, pdf_rects[i:i + 4]))
    for i in range(0, len(pdf_rects), 6)
    ]
    last_obj = re.search(br'\bxref\s+(\d+)\s+(\d+)\b', pdf_data)
    if not last_obj:
    print('error: could not find last obj id', file=sys.stderr)
    exit(1)
    last_obj = tuple(map(int, last_obj.groups()))

    # Some sanity check to ensure our matches are good

    if len(svg_rects) != len(pdf_rects):
    print('''
    error: found diff # of rects in svg & ps
    This can be due to number of reasons:
    - you've moved the box after creating a link for it - bad move!
    fix: delete it and draw a new box and DON'T MOVE it this time
    - you've grouped the boxes and done some fancy things
    fix: see above
    - you forgot to remove the strokes from the boxes
    - you have removed a box but Inkscape is still keeping it in the file
    fix: do a document cleanup or close/re-open your file
    '''.strip(), file=sys.stderr)
    exit(1)

    # Match up the rects based on their relative X,Y position
    # FIXME there is a possibility that due to rounding errors, links get
    # matched up incorrectly. Always check the final PDF before sharing.

    svg_rects.sort(key=(lambda x: int(x[2] * 100)), reverse=True)
    svg_rects.sort(key=lambda x: int(x[1] * 100))
    pdf_rects.sort(key=lambda x: (int(x[0] * 100), int(x[1] * 100)))

    # Generate the PDF hyperlink objects

    pdf_link_tpl = '''
    %%QDF: ignore_newline
    %d %d obj
    <<
    /A << /S /URI /URI (%s) >>
    /Border [ 0 0 0 ]
    /Rect [ %f %f %f %f ]
    /Subtype /Link
    /Type /Annot
    >>
    endobj
    '''.strip()

    pdf_links = '\n'.join(pdf_link_tpl % (
    c, last_obj[0], s[0], p[0], p[1], p[0] + p[2], p[1] + p[3]
    ) for p, s, c in zip(pdf_rects, svg_rects, count(last_obj[1])))

    # Remove the visual rects from PDF, write out the new hyperlink objs

    pdf_data = PDF_RECT_PAT.sub(b'', pdf_data)
    pdf_data = re.sub(
    (r'\bxref\s+%d\s+%d\b' % last_obj).encode('ascii'),
    (pdf_links + '\nxref\n%d %d' % (
    last_obj[0], last_obj[1] + len(svg_rects)
    )).encode('ascii'),
    pdf_data
    )
    pdf_data = re.sub(
    br'([%][%]\s+Page\s+1\s+[%][%][^\n]+\s+\d+\s+\d+\s+obj\s+<<)',
    (r'\1/Annots [%s] ' % ' '.join(
    '%d %d R' % (i + last_obj[1], last_obj[0])
    for i in range(len(svg_rects))
    )).encode('ascii'), pdf_data)

    # Optimize and save the new file

    fd, out_tmppath = tempfile.mkstemp()
    os.close(fd)
    try:
    with open(out_tmppath, 'wb') as out_tmpfile:
    fix_qdf_proc = Popen(['fix-qdf'], stdin=PIPE, stdout=out_tmpfile)
    fix_qdf_proc.communicate(pdf_data)
    if fix_qdf_proc.wait() != 0:
    print('error: failed writing the mod pdf', file=sys.stderr)
    exit(1)
    if call([
    'qpdf', '--object-streams=generate', '--stream-data=compress',
    out_tmppath, pdf_out_path
    ]) != 0:
    print('error: failed writing the mod pdf', file=sys.stderr)
    exit(1)
    finally:
    try:
    os.unlink(out_tmppath)
    except:
    pass
    # Moved to https://github.com/oxplot/gists/blob/master/svglinkify.py
  8. oxplot revised this gist Mar 6, 2015. 1 changed file with 30 additions and 8 deletions.
    38 changes: 30 additions & 8 deletions svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -10,7 +10,7 @@
    # Requires:
    # qpdf
    # inkscape
    # python 3.4 (it's trivial to make it work with earlier versions)
    # python 2/3
    #
    # WARNING Since this script is one heck of a hack, you should follow the
    # instructions below to the letter, or you will fail miserably.
    @@ -66,14 +66,36 @@
    # me your SVG and the PDF inkscape generated for you and I should be
    # able to help.

    from __future__ import unicode_literals
    from __future__ import print_function

    from itertools import count
    from subprocess import call, PIPE, Popen
    import html
    import os
    import re
    import sys
    import tempfile

    # Magic to support python both 2 and 3

    try:
    range = xrange
    except:
    pass

    try:
    import HTMLParser as html_parser
    except:
    import html.parser as html_parser
    _html_parser = html_parser.HTMLParser()
    try:
    html_unescape = _html_parser.unescape
    except:
    import html
    html_unescape = html.unescape

    # Command line parsing

    if len(sys.argv) < 4:
    print('Usage: %s <svg-file> <inkscape-gen-pdf> <linkified-pdf>'
    % sys.argv[0], file=sys.stderr)
    @@ -90,7 +112,7 @@

    with open(svg_path, 'r') as svg_file:
    svg_rects = [(
    html.unescape(i[0]),
    html_unescape(i[0]),
    float(SVG_X_PAT.search(i[1]).group(1)),
    float(SVG_Y_PAT.search(i[1]).group(1))
    ) for i in re.findall(r'''
    @@ -116,17 +138,17 @@

    # Load the rects and last object ID from PDF file

    PDF_RECT_PAT = re.compile(rb'''
    PDF_RECT_PAT = re.compile(br'''
    \b1\s+0\s+1\s+rg(?:\s+/a0\s+gs)?
    ((?:\s+[\d.-]+\s+[\d.-]+\s+[\d.-]+\s+[\d.-]+\s+re\s+f)+)\b
    ''', re.X)
    m = PDF_RECT_PAT.search(pdf_data)
    pdf_rects = re.split(rb'\s+', m.group(1).strip()) if m else []
    pdf_rects = re.split(br'\s+', m.group(1).strip()) if m else []
    pdf_rects = [
    list(map(float, pdf_rects[i:i + 4]))
    for i in range(0, len(pdf_rects), 6)
    ]
    last_obj = re.search(rb'\bxref\s+(\d+)\s+(\d+)\b', pdf_data)
    last_obj = re.search(br'\bxref\s+(\d+)\s+(\d+)\b', pdf_data)
    if not last_obj:
    print('error: could not find last obj id', file=sys.stderr)
    exit(1)
    @@ -186,7 +208,7 @@
    pdf_data
    )
    pdf_data = re.sub(
    rb'([%][%]\s+Page\s+1\s+[%][%][^\n]+\s+\d+\s+\d+\s+obj\s+<<)',
    br'([%][%]\s+Page\s+1\s+[%][%][^\n]+\s+\d+\s+\d+\s+obj\s+<<)',
    (r'\1/Annots [%s] ' % ' '.join(
    '%d %d R' % (i + last_obj[1], last_obj[0])
    for i in range(len(svg_rects))
    @@ -213,4 +235,4 @@
    try:
    os.unlink(out_tmppath)
    except:
    pass
    pass
  9. oxplot revised this gist Mar 6, 2015. 1 changed file with 0 additions and 0 deletions.
    Empty file modified svglinkify.py
    100644 → 100755
    Empty file.
  10. oxplot revised this gist Mar 6, 2015. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -143,8 +143,9 @@
    - you've grouped the boxes and done some fancy things
    fix: see above
    - you forgot to remove the strokes from the boxes
    - you have removed a box but Inkscape is still keeping it in the file
    fix: do a document cleanup or close/re-open your file
    '''.strip(), file=sys.stderr)
    print('%r\n%r' % (svg_rects, pdf_rects), file=sys.stderr)
    exit(1)

    # Match up the rects based on their relative X,Y position
  11. oxplot created this gist Mar 6, 2015.
    215 changes: 215 additions & 0 deletions svglinkify.py
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,215 @@
    #!/usr/bin/env python
    # svglinkify.py - Add hyperlinks to PDFs generated by Inkscape
    # Copyright (C) 2015 Mansour Behabadi <[email protected]>
    #
    # This script comes with no warranty whatsoever. Use at your own risk.
    # If you decide to distribute verbatim or modified versions of this
    # code, you must retain this copyright notice.
    #
    # Usage: svglinkify.py <svg-file> <inkscape-gen-pdf> <linkified-pdf>
    # Requires:
    # qpdf
    # inkscape
    # python 3.4 (it's trivial to make it work with earlier versions)
    #
    # WARNING Since this script is one heck of a hack, you should follow the
    # instructions below to the letter, or you will fail miserably.
    #
    # 1. Start by making an SVG that looks nice and everything and add a
    # piece of text somewhere.
    #
    # 2. Select the rectangle tool and draw a box on top of the text.
    # This box will be the clickable area of our link. Set its fill color
    # to #ff00ff (magenta) and remove any strokes.
    #
    # 3. Right click the box and select "Create Link". In the "Object
    # attributes" window that opens up, type the destination link in
    # "Href".
    #
    # 4. Send the box to the back (using End key on the keyboard) so you can
    # see your text. DO NOT move your box at any time after you've
    # created the link. More details below.
    #
    # 5. Export your SVG as PDF and run svglinkify.py:
    #
    # $ svglinkify.py my_doc.svg my_doc.pdf my_doc_with_links.pdf
    #
    # So you pass your SVG file as the first arg, the exported PDF as the
    # 2nd arg and the name of final PDF as 3rd arg.
    #
    # 6. If you did everything right, open my_doc_with_links.pdf and you
    # should be able to click your text and open the link in browser. You
    # also notice that the magenta box is gone. That's it. Now read the
    # sections below if you hate being frustrated when things break.
    #
    # HOW IT WORKS
    #
    # The script looks for magenta boxes (surprise!) that have a link. It
    # then extracts their x,y position and hyperlinks. It does the same
    # search for magenta boxes in the generated PDF and tries to match them
    # up by their relative locations. Therefore it's crucial to get the
    # locations right. Since SVG is pretty damn flexible, locations aren't
    # always simple x,y attributes. When you create a link for an object,
    # you wrap it in a group. Groups don't have x,y, instead they are
    # transformed using 2D matrices which means, maths calculations must be
    # done in order to find out where the enclosed box really is. This
    # script is too dumb to do that. That's why you should not move a box
    # after you create a link for it.
    #
    # You could either delete it and draw a new one, or if you like it
    # dangerous, you can enter the group (ie double cliking the box) and
    # then move the box. This way, you're not moving the group so no
    # transformations will be applied. You're bound to make a mistake sooner
    # or later this way, so don't do it.
    #
    # If you can't get this to work after at least several attempts, email
    # me your SVG and the PDF inkscape generated for you and I should be
    # able to help.

    from itertools import count
    from subprocess import call, PIPE, Popen
    import html
    import os
    import re
    import sys
    import tempfile

    if len(sys.argv) < 4:
    print('Usage: %s <svg-file> <inkscape-gen-pdf> <linkified-pdf>'
    % sys.argv[0], file=sys.stderr)
    exit(1)

    svg_path = sys.argv[1]
    pdf_in_path = sys.argv[2]
    pdf_out_path = sys.argv[3]

    # Load the link rects from SVG file

    SVG_X_PAT = re.compile(r'\bx="([^"]+)"')
    SVG_Y_PAT = re.compile(r'\by="([^"]+)"')

    with open(svg_path, 'r') as svg_file:
    svg_rects = [(
    html.unescape(i[0]),
    float(SVG_X_PAT.search(i[1]).group(1)),
    float(SVG_Y_PAT.search(i[1]).group(1))
    ) for i in re.findall(r'''
    <a[^>]*?\bxlink:href="([^"]+)"[^>]*>\s*<rect
    ([^>]*?\bstyle="[^"]*?\bfill:[#]ff00ff\b[^>]*)
    ''', svg_file.read(), re.X)]

    # QDFy the input PDF & load the resulting PDF to memory

    fd, qdf_tmppath = tempfile.mkstemp()
    os.close(fd)
    try:
    if call(['qpdf', '--qdf', pdf_in_path, qdf_tmppath]) != 0:
    print('error: qpdf failed', file=sys.stderr)
    exit(1)
    with open(qdf_tmppath, 'rb') as ps_file:
    pdf_data = ps_file.read()
    finally:
    try:
    os.unlink(qdf_tmppath)
    except:
    pass

    # Load the rects and last object ID from PDF file

    PDF_RECT_PAT = re.compile(rb'''
    \b1\s+0\s+1\s+rg(?:\s+/a0\s+gs)?
    ((?:\s+[\d.-]+\s+[\d.-]+\s+[\d.-]+\s+[\d.-]+\s+re\s+f)+)\b
    ''', re.X)
    m = PDF_RECT_PAT.search(pdf_data)
    pdf_rects = re.split(rb'\s+', m.group(1).strip()) if m else []
    pdf_rects = [
    list(map(float, pdf_rects[i:i + 4]))
    for i in range(0, len(pdf_rects), 6)
    ]
    last_obj = re.search(rb'\bxref\s+(\d+)\s+(\d+)\b', pdf_data)
    if not last_obj:
    print('error: could not find last obj id', file=sys.stderr)
    exit(1)
    last_obj = tuple(map(int, last_obj.groups()))

    # Some sanity check to ensure our matches are good

    if len(svg_rects) != len(pdf_rects):
    print('''
    error: found diff # of rects in svg & ps
    This can be due to number of reasons:
    - you've moved the box after creating a link for it - bad move!
    fix: delete it and draw a new box and DON'T MOVE it this time
    - you've grouped the boxes and done some fancy things
    fix: see above
    - you forgot to remove the strokes from the boxes
    '''.strip(), file=sys.stderr)
    print('%r\n%r' % (svg_rects, pdf_rects), file=sys.stderr)
    exit(1)

    # Match up the rects based on their relative X,Y position
    # FIXME there is a possibility that due to rounding errors, links get
    # matched up incorrectly. Always check the final PDF before sharing.

    svg_rects.sort(key=(lambda x: int(x[2] * 100)), reverse=True)
    svg_rects.sort(key=lambda x: int(x[1] * 100))
    pdf_rects.sort(key=lambda x: (int(x[0] * 100), int(x[1] * 100)))

    # Generate the PDF hyperlink objects

    pdf_link_tpl = '''
    %%QDF: ignore_newline
    %d %d obj
    <<
    /A << /S /URI /URI (%s) >>
    /Border [ 0 0 0 ]
    /Rect [ %f %f %f %f ]
    /Subtype /Link
    /Type /Annot
    >>
    endobj
    '''.strip()

    pdf_links = '\n'.join(pdf_link_tpl % (
    c, last_obj[0], s[0], p[0], p[1], p[0] + p[2], p[1] + p[3]
    ) for p, s, c in zip(pdf_rects, svg_rects, count(last_obj[1])))

    # Remove the visual rects from PDF, write out the new hyperlink objs

    pdf_data = PDF_RECT_PAT.sub(b'', pdf_data)
    pdf_data = re.sub(
    (r'\bxref\s+%d\s+%d\b' % last_obj).encode('ascii'),
    (pdf_links + '\nxref\n%d %d' % (
    last_obj[0], last_obj[1] + len(svg_rects)
    )).encode('ascii'),
    pdf_data
    )
    pdf_data = re.sub(
    rb'([%][%]\s+Page\s+1\s+[%][%][^\n]+\s+\d+\s+\d+\s+obj\s+<<)',
    (r'\1/Annots [%s] ' % ' '.join(
    '%d %d R' % (i + last_obj[1], last_obj[0])
    for i in range(len(svg_rects))
    )).encode('ascii'), pdf_data)

    # Optimize and save the new file

    fd, out_tmppath = tempfile.mkstemp()
    os.close(fd)
    try:
    with open(out_tmppath, 'wb') as out_tmpfile:
    fix_qdf_proc = Popen(['fix-qdf'], stdin=PIPE, stdout=out_tmpfile)
    fix_qdf_proc.communicate(pdf_data)
    if fix_qdf_proc.wait() != 0:
    print('error: failed writing the mod pdf', file=sys.stderr)
    exit(1)
    if call([
    'qpdf', '--object-streams=generate', '--stream-data=compress',
    out_tmppath, pdf_out_path
    ]) != 0:
    print('error: failed writing the mod pdf', file=sys.stderr)
    exit(1)
    finally:
    try:
    os.unlink(out_tmppath)
    except:
    pass